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