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