1 /*
2  * Copyright (C) 2008 The Android Open Source Project
3  *
4  * Licensed under the Apache License, Version 2.0 (the "License");
5  * you may not use this file except in compliance with the License.
6  * You may obtain a copy of the License at
7  *
8  *      http://www.apache.org/licenses/LICENSE-2.0
9  *
10  * Unless required by applicable law or agreed to in writing, software
11  * distributed under the License is distributed on an "AS IS" BASIS,
12  * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13  * See the License for the specific language governing permissions and
14  * limitations under the License.
15  */
16 
17 package com.android.compatibility.common.util;
18 
19 import android.content.Context;
20 import android.content.res.Resources;
21 import android.graphics.Bitmap;
22 import android.graphics.BitmapFactory;
23 import androidx.annotation.NonNull;
24 import androidx.annotation.Nullable;
25 import android.support.test.rule.ActivityTestRule;
26 import android.text.Editable;
27 import android.text.TextUtils;
28 import android.view.View;
29 import android.view.ViewTreeObserver;
30 
31 import junit.framework.Assert;
32 
33 import org.hamcrest.BaseMatcher;
34 import org.hamcrest.Description;
35 import org.xmlpull.v1.XmlPullParser;
36 import org.xmlpull.v1.XmlPullParserException;
37 
38 import java.io.IOException;
39 import java.util.concurrent.CountDownLatch;
40 import java.util.concurrent.TimeUnit;
41 
42 import static android.view.ViewTreeObserver.*;
43 import static org.mockito.hamcrest.MockitoHamcrest.argThat;
44 
45 /**
46  * The useful methods for widget test.
47  */
48 public class WidgetTestUtils {
49     /**
50      * Assert that two bitmaps have identical content (same dimensions, same configuration,
51      * same pixel content).
52      *
53      * @param b1 the first bitmap which needs to compare.
54      * @param b2 the second bitmap which needs to compare.
55      */
assertEquals(Bitmap b1, Bitmap b2)56     public static void assertEquals(Bitmap b1, Bitmap b2) {
57         if (b1 == b2) {
58             return;
59         }
60 
61         if (b1 == null || b2 == null) {
62             Assert.fail("the bitmaps are not equal");
63         }
64 
65         // b1 and b2 are all not null.
66         if (b1.getWidth() != b2.getWidth() || b1.getHeight() != b2.getHeight()
67             || b1.getConfig() != b2.getConfig()) {
68             Assert.fail("the bitmaps are not equal");
69         }
70 
71         int w = b1.getWidth();
72         int h = b1.getHeight();
73         int s = w * h;
74         int[] pixels1 = new int[s];
75         int[] pixels2 = new int[s];
76 
77         b1.getPixels(pixels1, 0, w, 0, 0, w, h);
78         b2.getPixels(pixels2, 0, w, 0, 0, w, h);
79 
80         for (int i = 0; i < s; i++) {
81             if (pixels1[i] != pixels2[i]) {
82                 Assert.fail("the bitmaps are not equal");
83             }
84         }
85     }
86 
87     /**
88      * Find beginning of the special element.
89      * @param parser XmlPullParser will be parsed.
90      * @param firstElementName the target element name.
91      *
92      * @throws XmlPullParserException if XML Pull Parser related faults occur.
93      * @throws IOException if I/O-related error occur when parsing.
94      */
beginDocument(XmlPullParser parser, String firstElementName)95     public static final void beginDocument(XmlPullParser parser, String firstElementName)
96             throws XmlPullParserException, IOException {
97         Assert.assertNotNull(parser);
98         Assert.assertNotNull(firstElementName);
99 
100         int type;
101         while ((type = parser.next()) != XmlPullParser.START_TAG
102                 && type != XmlPullParser.END_DOCUMENT) {
103             ;
104         }
105 
106         if (!parser.getName().equals(firstElementName)) {
107             throw new XmlPullParserException("Unexpected start tag: found " + parser.getName()
108                     + ", expected " + firstElementName);
109         }
110     }
111 
112     /**
113      * Compare the expected pixels with actual, scaling for the target context density
114      *
115      * @throws AssertionFailedError
116      */
assertScaledPixels(int expected, int actual, Context context)117     public static void assertScaledPixels(int expected, int actual, Context context) {
118         Assert.assertEquals(expected * context.getResources().getDisplayMetrics().density,
119                 actual, 3);
120     }
121 
122     /** Converts dips into pixels using the {@link Context}'s density. */
convertDipToPixels(Context context, int dip)123     public static int convertDipToPixels(Context context, int dip) {
124       float density = context.getResources().getDisplayMetrics().density;
125       return Math.round(density * dip);
126     }
127 
128     /**
129      * Retrieve a bitmap that can be used for comparison on any density
130      * @param resources
131      * @return the {@link Bitmap} or <code>null</code>
132      */
getUnscaledBitmap(Resources resources, int resId)133     public static Bitmap getUnscaledBitmap(Resources resources, int resId) {
134         BitmapFactory.Options options = new BitmapFactory.Options();
135         options.inScaled = false;
136         return BitmapFactory.decodeResource(resources, resId, options);
137     }
138 
139     /**
140      * Retrieve a dithered bitmap that can be used for comparison on any density
141      * @param resources
142      * @param config the preferred config for the returning bitmap
143      * @return the {@link Bitmap} or <code>null</code>
144      */
getUnscaledAndDitheredBitmap(Resources resources, int resId, Bitmap.Config config)145     public static Bitmap getUnscaledAndDitheredBitmap(Resources resources,
146             int resId, Bitmap.Config config) {
147         BitmapFactory.Options options = new BitmapFactory.Options();
148         options.inDither = true;
149         options.inScaled = false;
150         options.inPreferredConfig = config;
151         return BitmapFactory.decodeResource(resources, resId, options);
152     }
153 
154     /**
155      * Argument matcher for equality check of a CharSequence.
156      *
157      * @param expected expected CharSequence
158      *
159      * @return
160      */
sameCharSequence(final CharSequence expected)161     public static CharSequence sameCharSequence(final CharSequence expected) {
162         return argThat(new BaseMatcher<CharSequence>() {
163             @Override
164             public boolean matches(Object o) {
165                 if (o instanceof CharSequence) {
166                     return TextUtils.equals(expected, (CharSequence) o);
167                 }
168                 return false;
169             }
170 
171             @Override
172             public void describeTo(Description description) {
173                 description.appendText("doesn't match " + expected);
174             }
175         });
176     }
177 
178     /**
179      * Argument matcher for equality check of an Editable.
180      *
181      * @param expected expected Editable
182      *
183      * @return
184      */
185     public static Editable sameEditable(final Editable expected) {
186         return argThat(new BaseMatcher<Editable>() {
187             @Override
188             public boolean matches(Object o) {
189                 if (o instanceof Editable) {
190                     return TextUtils.equals(expected, (Editable) o);
191                 }
192                 return false;
193             }
194 
195             @Override
196             public void describeTo(Description description) {
197                 description.appendText("doesn't match " + expected);
198             }
199         });
200     }
201 
202     /**
203      * Runs the specified Runnable on the main thread and ensures that the specified View's tree is
204      * drawn before returning.
205      *
206      * @param activityTestRule the activity test rule used to run the test
207      * @param view the view whose tree should be drawn before returning
208      * @param runner the runnable to run on the main thread, or {@code null} to
209      *               simply force invalidation and a draw pass
210      */
211     public static void runOnMainAndDrawSync(@NonNull final ActivityTestRule activityTestRule,
212             @NonNull final View view, @Nullable final Runnable runner) throws Throwable {
213         final CountDownLatch latch = new CountDownLatch(1);
214 
215         activityTestRule.runOnUiThread(() -> {
216             final OnDrawListener listener = new OnDrawListener() {
217                 @Override
218                 public void onDraw() {
219                     // posting so that the sync happens after the draw that's about to happen
220                     view.post(() -> {
221                         activityTestRule.getActivity().getWindow().getDecorView().
222                                 getViewTreeObserver().removeOnDrawListener(this);
223                         latch.countDown();
224                     });
225                 }
226             };
227 
228             activityTestRule.getActivity().getWindow().getDecorView().
229                     getViewTreeObserver().addOnDrawListener(listener);
230 
231             if (runner != null) {
232                 runner.run();
233             }
234             view.invalidate();
235         });
236 
237         try {
238             Assert.assertTrue("Expected draw pass occurred within 5 seconds",
239                     latch.await(5, TimeUnit.SECONDS));
240         } catch (InterruptedException e) {
241             throw new RuntimeException(e);
242         }
243     }
244 
245     /**
246      * Runs the specified Runnable on the main thread and ensures that the activity's view tree is
247      * laid out before returning.
248      *
249      * @param activityTestRule the activity test rule used to run the test
250      * @param runner the runnable to run on the main thread. {@code null} is
251      * allowed, and simply means that there no runnable is required.
252      * @param forceLayout true if there should be an explicit call to requestLayout(),
253      * false otherwise
254      */
255     public static void runOnMainAndLayoutSync(@NonNull final ActivityTestRule activityTestRule,
256             @Nullable final Runnable runner, boolean forceLayout)
257             throws Throwable {
258         runOnMainAndLayoutSync(activityTestRule,
259                 activityTestRule.getActivity().getWindow().getDecorView(), runner, forceLayout);
260     }
261 
262     /**
263      * Runs the specified Runnable on the main thread and ensures that the specified view is
264      * laid out before returning.
265      *
266      * @param activityTestRule the activity test rule used to run the test
267      * @param view The view
268      * @param runner the runnable to run on the main thread. {@code null} is
269      * allowed, and simply means that there no runnable is required.
270      * @param forceLayout true if there should be an explicit call to requestLayout(),
271      * false otherwise
272      */
273     public static void runOnMainAndLayoutSync(@NonNull final ActivityTestRule activityTestRule,
274             @NonNull final View view, @Nullable final Runnable runner, boolean forceLayout)
275             throws Throwable {
276         final View rootView = view.getRootView();
277 
278         final CountDownLatch latch = new CountDownLatch(1);
279 
280         activityTestRule.runOnUiThread(() -> {
281             final OnGlobalLayoutListener listener = new ViewTreeObserver.OnGlobalLayoutListener() {
282                 @Override
283                 public void onGlobalLayout() {
284                     rootView.getViewTreeObserver().removeOnGlobalLayoutListener(this);
285                     // countdown immediately since the layout we were waiting on has happened
286                     latch.countDown();
287                 }
288             };
289 
290             rootView.getViewTreeObserver().addOnGlobalLayoutListener(listener);
291 
292             if (runner != null) {
293                 runner.run();
294             }
295 
296             if (forceLayout) {
297                 rootView.requestLayout();
298             }
299         });
300 
301         try {
302             Assert.assertTrue("Expected layout pass within 5 seconds",
303                     latch.await(5, TimeUnit.SECONDS));
304         } catch (InterruptedException e) {
305             throw new RuntimeException(e);
306         }
307     }
308 
309 }
310