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