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