1 /* 2 * Copyright (C) 2021 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 android.view.cts.surfacevalidator; 18 19 import static androidx.test.platform.app.InstrumentationRegistry.getInstrumentation; 20 21 import static org.junit.Assert.assertNotNull; 22 import static org.junit.Assert.assertTrue; 23 24 import android.app.Activity; 25 import android.app.Instrumentation; 26 import android.app.UiAutomation; 27 import android.graphics.Bitmap; 28 import android.graphics.Color; 29 import android.graphics.Rect; 30 import android.os.Bundle; 31 import android.os.Environment; 32 import android.os.Handler; 33 import android.os.Looper; 34 import android.util.Log; 35 import android.view.AttachedSurfaceControl; 36 import android.view.Gravity; 37 import android.view.PointerIcon; 38 import android.view.SurfaceControl; 39 import android.view.SurfaceHolder; 40 import android.view.SurfaceView; 41 import android.view.View; 42 import android.view.WindowInsets; 43 import android.view.WindowInsetsAnimation; 44 import android.view.WindowManager; 45 import android.widget.FrameLayout; 46 47 import androidx.annotation.NonNull; 48 49 import org.junit.Assert; 50 import org.junit.rules.TestName; 51 52 import java.io.File; 53 import java.io.FileOutputStream; 54 import java.io.IOException; 55 import java.util.List; 56 import java.util.concurrent.CountDownLatch; 57 import java.util.concurrent.TimeUnit; 58 59 public class ASurfaceControlTestActivity extends Activity { 60 private static final String TAG = "ASurfaceControlTestActivity"; 61 private static final boolean DEBUG = true; 62 63 private static final int DEFAULT_LAYOUT_WIDTH = 100; 64 private static final int DEFAULT_LAYOUT_HEIGHT = 100; 65 private static final int OFFSET_X = 100; 66 private static final int OFFSET_Y = 100; 67 public static final long WAIT_TIMEOUT_S = 5; 68 69 private final Handler mHandler = new Handler(Looper.getMainLooper()); 70 71 private SurfaceView mSurfaceView; 72 private FrameLayout.LayoutParams mLayoutParams; 73 private FrameLayout mParent; 74 75 private Bitmap mScreenshot; 76 77 private Instrumentation mInstrumentation; 78 79 private final InsetsAnimationCallback mInsetsAnimationCallback = new InsetsAnimationCallback(); 80 private final CountDownLatch mReadyToStart = new CountDownLatch(1); 81 private CountDownLatch mTransactionCommittedLatch; 82 83 @Override onEnterAnimationComplete()84 public void onEnterAnimationComplete() { 85 mReadyToStart.countDown(); 86 } 87 88 @Override onCreate(Bundle savedInstanceState)89 public void onCreate(Bundle savedInstanceState) { 90 super.onCreate(savedInstanceState); 91 92 final View decorView = getWindow().getDecorView(); 93 decorView.setWindowInsetsAnimationCallback(mInsetsAnimationCallback); 94 decorView.setSystemUiVisibility( 95 View.SYSTEM_UI_FLAG_HIDE_NAVIGATION | View.SYSTEM_UI_FLAG_FULLSCREEN); 96 // Set the NULL pointer icon so that it won't obstruct the captured image. 97 decorView.setPointerIcon( 98 PointerIcon.getSystemIcon(this, PointerIcon.TYPE_NULL)); 99 getWindow().addFlags(WindowManager.LayoutParams.FLAG_KEEP_SCREEN_ON); 100 101 mLayoutParams = new FrameLayout.LayoutParams(DEFAULT_LAYOUT_WIDTH, DEFAULT_LAYOUT_HEIGHT, 102 Gravity.LEFT | Gravity.TOP); 103 104 mLayoutParams.topMargin = OFFSET_Y; 105 mLayoutParams.leftMargin = OFFSET_X; 106 mSurfaceView = new SurfaceView(this); 107 mSurfaceView.getHolder().setFixedSize(DEFAULT_LAYOUT_WIDTH, DEFAULT_LAYOUT_HEIGHT); 108 109 mParent = findViewById(android.R.id.content); 110 111 mInstrumentation = getInstrumentation(); 112 } 113 getSurfaceControl()114 public SurfaceControl getSurfaceControl() { 115 return mSurfaceView.getSurfaceControl(); 116 } 117 verifyTest(SurfaceHolder.Callback surfaceHolderCallback, PixelChecker pixelChecker, TestName name)118 public void verifyTest(SurfaceHolder.Callback surfaceHolderCallback, 119 PixelChecker pixelChecker, TestName name) { 120 verifyTest(surfaceHolderCallback, pixelChecker, name, 0); 121 } 122 verifyTest(SurfaceHolder.Callback surfaceHolderCallback, PixelChecker pixelChecker, TestName name, int numOfTransactionToListen)123 public void verifyTest(SurfaceHolder.Callback surfaceHolderCallback, 124 PixelChecker pixelChecker, TestName name, int numOfTransactionToListen) { 125 final boolean waitForTransactionLatch = numOfTransactionToListen > 0; 126 final CountDownLatch readyFence = new CountDownLatch(1); 127 if (waitForTransactionLatch) { 128 mTransactionCommittedLatch = new CountDownLatch(numOfTransactionToListen); 129 } 130 SurfaceHolderCallback surfaceHolderCallbackWrapper = new SurfaceHolderCallback( 131 surfaceHolderCallback, 132 readyFence, mParent.getRootSurfaceControl()); 133 createSurface(surfaceHolderCallbackWrapper); 134 try { 135 if (waitForTransactionLatch) { 136 assertTrue("timeout", 137 mTransactionCommittedLatch.await(WAIT_TIMEOUT_S, TimeUnit.SECONDS)); 138 } 139 assertTrue("timeout", readyFence.await(WAIT_TIMEOUT_S, TimeUnit.SECONDS)); 140 } catch (InterruptedException e) { 141 Assert.fail("interrupted"); 142 } 143 verifyScreenshot(pixelChecker, name); 144 mHandler.post(() -> mSurfaceView.getHolder().removeCallback(surfaceHolderCallback)); 145 } 146 awaitReadyState()147 public void awaitReadyState() { 148 try { 149 assertTrue(mReadyToStart.await(5, TimeUnit.SECONDS)); 150 } catch (InterruptedException e) { 151 throw new RuntimeException(e); 152 } 153 } 154 createSurface(SurfaceHolderCallback surfaceHolderCallback)155 public void createSurface(SurfaceHolderCallback surfaceHolderCallback) { 156 awaitReadyState(); 157 158 mHandler.post(() -> { 159 mSurfaceView.getHolder().addCallback(surfaceHolderCallback); 160 mParent.addView(mSurfaceView, mLayoutParams); 161 }); 162 } 163 verifyScreenshot(PixelChecker pixelChecker, TestName name)164 public void verifyScreenshot(PixelChecker pixelChecker, TestName name) { 165 // Wait for the stable insets update. The position of the surface view is in correct before 166 // the update. Sometimes this callback isn't called, so we don't want to fail the test 167 // because it times out. 168 if (!mInsetsAnimationCallback.waitForInsetsAnimation()) { 169 Log.w(TAG, "Insets animation wait timed out."); 170 } 171 172 final CountDownLatch countDownLatch = new CountDownLatch(1); 173 UiAutomation uiAutomation = mInstrumentation.getUiAutomation(); 174 mHandler.post(() -> { 175 mScreenshot = uiAutomation.takeScreenshot(getWindow()); 176 mParent.removeAllViews(); 177 countDownLatch.countDown(); 178 }); 179 180 try { 181 countDownLatch.await(WAIT_TIMEOUT_S, TimeUnit.SECONDS); 182 } catch (Exception e) { 183 } 184 185 assertNotNull(mScreenshot); 186 187 Bitmap swBitmap = mScreenshot.copy(Bitmap.Config.ARGB_8888, false); 188 mScreenshot.recycle(); 189 190 int numMatchingPixels = pixelChecker.getNumMatchingPixels(swBitmap); 191 192 int checkedPixels = 0; 193 for (Rect bounds : pixelChecker.getBoundsToCheck(swBitmap)) { 194 checkedPixels += bounds.width() * bounds.height(); 195 } 196 197 boolean success = pixelChecker.checkPixels(numMatchingPixels, swBitmap.getWidth(), 198 swBitmap.getHeight()); 199 if (!success) { 200 saveFailureCapture(swBitmap, name); 201 } 202 swBitmap.recycle(); 203 204 assertTrue("Actual matched pixels:" + numMatchingPixels 205 + " Number of pixels checked:" + checkedPixels, success); 206 } 207 getSurfaceView()208 public SurfaceView getSurfaceView() { 209 return mSurfaceView; 210 } 211 getParentFrameLayout()212 public FrameLayout getParentFrameLayout() { 213 return mParent; 214 } 215 transactionCommitted()216 public void transactionCommitted() { 217 mTransactionCommittedLatch.countDown(); 218 } 219 220 public static class RectChecker extends PixelChecker { 221 private final List<Rect> mBoundsToCheck; 222 RectChecker(List<Rect> boundsToCheck)223 public RectChecker(List<Rect> boundsToCheck) { 224 super(); 225 mBoundsToCheck = boundsToCheck; 226 } 227 RectChecker(Rect boundsToCheck)228 public RectChecker(Rect boundsToCheck) { 229 this(List.of(boundsToCheck)); 230 } 231 RectChecker(Rect boundsToCheck, int expectedColor)232 public RectChecker(Rect boundsToCheck, int expectedColor) { 233 super(expectedColor); 234 mBoundsToCheck = List.of(boundsToCheck); 235 } 236 237 @Override checkPixels(int matchingPixelCount, int width, int height)238 public boolean checkPixels(int matchingPixelCount, int width, int height) { 239 int expectedPixelCount = 0; 240 for (Rect bounds : mBoundsToCheck) { 241 expectedPixelCount += bounds.width() * bounds.height(); 242 } 243 return expectedPixelCount - 100 < matchingPixelCount 244 && matchingPixelCount <= expectedPixelCount; 245 } 246 247 @Override getBoundsToCheck(Bitmap bitmap)248 public List<Rect> getBoundsToCheck(Bitmap bitmap) { 249 return mBoundsToCheck; 250 } 251 } 252 253 public abstract static class PixelChecker { 254 private final PixelColor mPixelColor; 255 private final boolean mLogWhenNoMatch; 256 PixelChecker()257 public PixelChecker() { 258 this(Color.BLACK, true); 259 } 260 PixelChecker(int color)261 public PixelChecker(int color) { 262 this(color, true); 263 } 264 PixelChecker(int color, boolean logWhenNoMatch)265 public PixelChecker(int color, boolean logWhenNoMatch) { 266 mPixelColor = new PixelColor(color); 267 mLogWhenNoMatch = logWhenNoMatch; 268 } 269 getNumMatchingPixels(Bitmap bitmap)270 int getNumMatchingPixels(Bitmap bitmap) { 271 int numMatchingPixels = 0; 272 int numErrorsLogged = 0; 273 for (Rect boundsToCheck : getBoundsToCheck(bitmap)) { 274 for (int x = boundsToCheck.left; x < boundsToCheck.right; x++) { 275 for (int y = boundsToCheck.top; y < boundsToCheck.bottom; y++) { 276 int color = bitmap.getPixel(x + OFFSET_X, y + OFFSET_Y); 277 if (getExpectedColor(x, y).matchesColor(color)) { 278 numMatchingPixels++; 279 } else if (DEBUG && mLogWhenNoMatch && numErrorsLogged < 100) { 280 // We don't want to spam the logcat with errors if something is really 281 // broken. Only log the first 100 errors. 282 PixelColor expected = getExpectedColor(x, y); 283 int expectedColor = Color.argb(expected.mAlpha, expected.mRed, 284 expected.mGreen, expected.mBlue); 285 Log.e(TAG, String.format( 286 "Failed to match (%d, %d) color=0x%08X expected=0x%08X", x, y, 287 color, expectedColor)); 288 numErrorsLogged++; 289 } 290 } 291 } 292 } 293 return numMatchingPixels; 294 } 295 checkPixels(int matchingPixelCount, int width, int height)296 public abstract boolean checkPixels(int matchingPixelCount, int width, int height); 297 getBoundsToCheck(Bitmap bitmap)298 public List<Rect> getBoundsToCheck(Bitmap bitmap) { 299 return List.of(new Rect(1, 1, DEFAULT_LAYOUT_WIDTH - 1, DEFAULT_LAYOUT_HEIGHT - 1)); 300 } 301 getExpectedColor(int x, int y)302 public PixelColor getExpectedColor(int x, int y) { 303 return mPixelColor; 304 } 305 } 306 307 public static class SurfaceHolderCallback implements SurfaceHolder.Callback { 308 private final SurfaceHolder.Callback mTestCallback; 309 private final CountDownLatch mSurfaceCreatedLatch; 310 private final AttachedSurfaceControl mAttachedSurfaceControl; 311 SurfaceHolderCallback(SurfaceHolder.Callback callback, CountDownLatch readyFence, AttachedSurfaceControl attachedSurfaceControl)312 public SurfaceHolderCallback(SurfaceHolder.Callback callback, CountDownLatch readyFence, 313 AttachedSurfaceControl attachedSurfaceControl) { 314 mTestCallback = callback; 315 mSurfaceCreatedLatch = readyFence; 316 mAttachedSurfaceControl = attachedSurfaceControl; 317 } 318 319 @Override surfaceCreated(@onNull SurfaceHolder holder)320 public void surfaceCreated(@NonNull SurfaceHolder holder) { 321 mTestCallback.surfaceCreated(holder); 322 try (SurfaceControl.Transaction transaction = new SurfaceControl.Transaction()) { 323 transaction.addTransactionCommittedListener(Runnable::run, 324 mSurfaceCreatedLatch::countDown); 325 mAttachedSurfaceControl.applyTransactionOnDraw(transaction); 326 } 327 } 328 329 @Override surfaceChanged(@onNull SurfaceHolder holder, int format, int width, int height)330 public void surfaceChanged(@NonNull SurfaceHolder holder, int format, int width, 331 int height) { 332 mTestCallback.surfaceChanged(holder, format, width, height); 333 } 334 335 @Override surfaceDestroyed(@onNull SurfaceHolder holder)336 public void surfaceDestroyed(@NonNull SurfaceHolder holder) { 337 mTestCallback.surfaceDestroyed(holder); 338 } 339 } 340 saveFailureCapture(Bitmap failFrame, TestName name)341 private void saveFailureCapture(Bitmap failFrame, TestName name) { 342 String directoryName = Environment.getExternalStorageDirectory() 343 + "/" + getClass().getSimpleName() 344 + "/" + name.getMethodName(); 345 File testDirectory = new File(directoryName); 346 if (testDirectory.exists()) { 347 String[] children = testDirectory.list(); 348 for (String file : children) { 349 new File(testDirectory, file).delete(); 350 } 351 } else { 352 testDirectory.mkdirs(); 353 } 354 355 String bitmapName = "frame.png"; 356 Log.d(TAG, "Saving file : " + bitmapName + " in directory : " + directoryName); 357 358 File file = new File(directoryName, bitmapName); 359 try (FileOutputStream fileStream = new FileOutputStream(file)) { 360 failFrame.compress(Bitmap.CompressFormat.PNG, 85, fileStream); 361 fileStream.flush(); 362 } catch (IOException e) { 363 e.printStackTrace(); 364 } 365 } 366 367 private static class InsetsAnimationCallback extends WindowInsetsAnimation.Callback { 368 private CountDownLatch mLatch = new CountDownLatch(1); 369 InsetsAnimationCallback()370 private InsetsAnimationCallback() { 371 super(DISPATCH_MODE_CONTINUE_ON_SUBTREE); 372 } 373 374 @Override onProgress( WindowInsets insets, List<WindowInsetsAnimation> runningAnimations)375 public WindowInsets onProgress( 376 WindowInsets insets, List<WindowInsetsAnimation> runningAnimations) { 377 return insets; 378 } 379 380 @Override onEnd(WindowInsetsAnimation animation)381 public void onEnd(WindowInsetsAnimation animation) { 382 mLatch.countDown(); 383 } 384 waitForInsetsAnimation()385 private boolean waitForInsetsAnimation() { 386 try { 387 return mLatch.await(WAIT_TIMEOUT_S, TimeUnit.SECONDS); 388 } catch (InterruptedException e) { 389 // Should never happen 390 throw new RuntimeException(e); 391 } 392 } 393 } 394 } 395