1 /* 2 * Copyright (C) 2016 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 package android.view.cts.surfacevalidator; 17 18 import static android.server.wm.BuildUtils.HW_TIMEOUT_MULTIPLIER; 19 import static android.server.wm.CtsWindowInfoUtils.getWindowBoundsInWindowSpace; 20 import static android.view.WindowInsets.Type.statusBars; 21 import static android.view.WindowManager.LayoutParams.LAYOUT_IN_DISPLAY_CUTOUT_MODE_ALWAYS; 22 23 import static org.junit.Assert.assertEquals; 24 import static org.junit.Assert.assertNotNull; 25 import static org.junit.Assert.assertTrue; 26 import static org.junit.Assert.fail; 27 28 import android.Manifest; 29 import android.app.Activity; 30 import android.app.KeyguardManager; 31 import android.content.pm.PackageManager; 32 import android.graphics.Bitmap; 33 import android.graphics.Insets; 34 import android.graphics.Point; 35 import android.graphics.Rect; 36 import android.hardware.display.DisplayManager; 37 import android.hardware.display.VirtualDisplay; 38 import android.os.Bundle; 39 import android.os.Environment; 40 import android.os.Handler; 41 import android.os.Looper; 42 import android.provider.Settings; 43 import android.server.wm.settings.SettingsSession; 44 import android.util.DisplayMetrics; 45 import android.util.Log; 46 import android.util.SparseArray; 47 import android.view.PointerIcon; 48 import android.view.SurfaceControl; 49 import android.view.ViewTreeObserver; 50 import android.view.WindowInsets; 51 import android.view.WindowInsetsController; 52 import android.view.WindowManager; 53 import android.view.WindowMetrics; 54 import android.widget.FrameLayout; 55 56 import com.android.compatibility.common.util.SystemUtil; 57 58 import org.junit.rules.TestName; 59 60 import java.io.File; 61 import java.io.FileOutputStream; 62 import java.io.IOException; 63 import java.util.concurrent.CountDownLatch; 64 import java.util.concurrent.TimeUnit; 65 66 public class CapturedActivity extends Activity { 67 public static final String STORAGE_DIR = "CtsSurfaceControl"; 68 69 public static class TestResult { 70 public int passFrames; 71 public int failFrames; 72 public final SparseArray<Bitmap> failures = new SparseArray<>(); 73 } 74 private static class ImmersiveConfirmationSetting extends SettingsSession<String> { ImmersiveConfirmationSetting()75 ImmersiveConfirmationSetting() { 76 super(Settings.Secure.getUriFor( 77 Settings.Secure.IMMERSIVE_MODE_CONFIRMATIONS), 78 Settings.Secure::getString, Settings.Secure::putString); 79 } 80 } 81 82 private ImmersiveConfirmationSetting mSettingsSession; 83 84 private static final String TAG = "CapturedActivity"; 85 private VirtualDisplay mVirtualDisplay; 86 87 private SurfacePixelValidator2 mSurfacePixelValidator; 88 89 private static final long WAIT_TIMEOUT_S = 5L * HW_TIMEOUT_MULTIPLIER; 90 91 private final Handler mHandler = new Handler(Looper.getMainLooper()); 92 private volatile boolean mOnEmbedded; 93 94 private final Point mTestAreaSize = new Point(); 95 96 private FrameLayout mParentLayout; 97 98 @Override onCreate(Bundle savedInstanceState)99 public void onCreate(Bundle savedInstanceState) { 100 super.onCreate(savedInstanceState); 101 final PackageManager packageManager = getPackageManager(); 102 mParentLayout = new FrameLayout(this); 103 FrameLayout.LayoutParams layoutParams = new FrameLayout.LayoutParams( 104 FrameLayout.LayoutParams.MATCH_PARENT, 105 FrameLayout.LayoutParams.MATCH_PARENT); 106 setContentView(mParentLayout, layoutParams); 107 108 // Embedded devices are significantly slower, and are given 109 // longer duration to capture the expected number of frames 110 mOnEmbedded = packageManager.hasSystemFeature(PackageManager.FEATURE_EMBEDDED); 111 112 mSettingsSession = new ImmersiveConfirmationSetting(); 113 mSettingsSession.set("confirmed"); 114 115 WindowInsetsController windowInsetsController = getWindow().getInsetsController(); 116 windowInsetsController.hide( 117 WindowInsets.Type.navigationBars() | WindowInsets.Type.statusBars()); 118 WindowManager.LayoutParams params = getWindow().getAttributes(); 119 params.layoutInDisplayCutoutMode = LAYOUT_IN_DISPLAY_CUTOUT_MODE_ALWAYS; 120 getWindow().setAttributes(params); 121 getWindow().setDecorFitsSystemWindows(false); 122 123 // Set the NULL pointer icon so that it won't obstruct the captured image. 124 getWindow().getDecorView().setPointerIcon( 125 PointerIcon.getSystemIcon(this, PointerIcon.TYPE_NULL)); 126 getWindow().addFlags(WindowManager.LayoutParams.FLAG_KEEP_SCREEN_ON); 127 128 KeyguardManager keyguardManager = getSystemService(KeyguardManager.class); 129 if (keyguardManager != null) { 130 keyguardManager.requestDismissKeyguard(this, null); 131 } 132 } 133 134 @Override onDestroy()135 public void onDestroy() { 136 super.onDestroy(); 137 restoreSettings(); 138 } 139 getCaptureDurationMs()140 public long getCaptureDurationMs() { 141 return mOnEmbedded ? 100000 : 50000; 142 } 143 runTest(ISurfaceValidatorTestCase animationTestCase)144 public TestResult runTest(ISurfaceValidatorTestCase animationTestCase) throws Throwable { 145 TestResult testResult = new TestResult(); 146 Runnable cleanupRunnable = () -> { 147 Log.d(TAG, "Stopping capture and ending test case"); 148 if (mVirtualDisplay != null) { 149 mVirtualDisplay.release(); 150 mVirtualDisplay = null; 151 } 152 153 animationTestCase.end(); 154 FrameLayout contentLayout = findViewById(android.R.id.content); 155 contentLayout.removeAllViews(); 156 if (mSurfacePixelValidator != null) { 157 mSurfacePixelValidator.finish(testResult); 158 mSurfacePixelValidator = null; 159 } 160 }; 161 162 try { 163 final int numFramesRequired = animationTestCase.getNumFramesRequired(); 164 final long maxCapturedDuration = getCaptureDurationMs(); 165 166 CountDownLatch frameDrawnLatch = new CountDownLatch(1); 167 mHandler.post(() -> { 168 Log.d(TAG, "Setting up test case"); 169 170 // See b/216583939. On some devices, hiding system bars is disabled. In those cases, 171 // adjust the area that is rendering the test content to be outside the status bar 172 // margins to ensure capturing and comparing frames skips the status bar area. 173 Insets statusBarInsets = getWindow() 174 .getDecorView() 175 .getRootWindowInsets() 176 .getInsets(statusBars()); 177 FrameLayout.LayoutParams layoutParams = 178 (FrameLayout.LayoutParams) mParentLayout.getLayoutParams(); 179 layoutParams.setMargins(statusBarInsets.left, statusBarInsets.top, 180 statusBarInsets.right, statusBarInsets.bottom); 181 mParentLayout.setLayoutParams(layoutParams); 182 183 animationTestCase.start(getApplicationContext(), mParentLayout); 184 185 Runnable runnable = () -> { 186 SurfaceControl.Transaction t = new SurfaceControl.Transaction(); 187 t.addTransactionCommittedListener(Runnable::run, frameDrawnLatch::countDown); 188 mParentLayout.getRootSurfaceControl().applyTransactionOnDraw(t); 189 }; 190 191 if (mParentLayout.isAttachedToWindow()) { 192 runnable.run(); 193 } else { 194 mParentLayout.getViewTreeObserver().addOnWindowAttachListener( 195 new ViewTreeObserver.OnWindowAttachListener() { 196 @Override 197 public void onWindowAttached() { 198 runnable.run(); 199 } 200 201 @Override 202 public void onWindowDetached() { 203 } 204 }); 205 } 206 }); 207 208 assertTrue("Failed to wait for animation to start", animationTestCase.waitForReady()); 209 assertTrue("Failed to wait for frame draw", 210 frameDrawnLatch.await(WAIT_TIMEOUT_S, TimeUnit.SECONDS)); 211 212 Rect bounds = getWindowBoundsInWindowSpace(mParentLayout::getWindowToken, 213 getDisplayId()); 214 assertNotNull("Failed to wait for test window bounds", bounds); 215 mTestAreaSize.set(bounds.width(), bounds.height()); 216 217 CountDownLatch setupLatch = new CountDownLatch(1); 218 mHandler.post(() -> { 219 Log.d(TAG, "Starting capture"); 220 221 Log.d(TAG, "testAreaWidth: " + mTestAreaSize.x 222 + ", testAreaHeight: " + mTestAreaSize.y); 223 224 Rect boundsToCheck = animationTestCase.getBoundsToCheck(mParentLayout); 225 if (boundsToCheck != null && (boundsToCheck.width() < 40 226 || boundsToCheck.height() < 40)) { 227 fail("capture bounds too small to be a fullscreen activity: " + boundsToCheck); 228 } 229 230 Log.d(TAG, "Size is " + mTestAreaSize + ", bounds are " 231 + (boundsToCheck == null ? "full screen" : boundsToCheck.toShortString())); 232 233 mSurfacePixelValidator = new SurfacePixelValidator2(mTestAreaSize, 234 boundsToCheck, animationTestCase.getChecker(), numFramesRequired); 235 236 WindowMetrics metrics = getWindowManager().getCurrentWindowMetrics(); 237 Log.d(TAG, "Starting capture: metrics=" + metrics); 238 int densityDpi = (int) (metrics.getDensity() * DisplayMetrics.DENSITY_DEFAULT); 239 240 DisplayManager dm = getSystemService(DisplayManager.class); 241 mVirtualDisplay = dm.createVirtualDisplay("CtsCapturedActivity", 242 mTestAreaSize.x, mTestAreaSize.y, densityDpi, 243 mSurfacePixelValidator.getSurface(), 0, null /*Callbacks*/, 244 null /*Handler*/); 245 assertNotNull("Failed to create VirtualDisplay", mVirtualDisplay); 246 SystemUtil.runWithShellPermissionIdentity( 247 () -> assertTrue("Failed to mirror content onto display", 248 getWindowManager().replaceContentOnDisplayWithMirror( 249 mVirtualDisplay.getDisplay().getDisplayId(), getWindow())), 250 Manifest.permission.ACCESS_SURFACE_FLINGER); 251 252 setupLatch.countDown(); 253 }); 254 255 assertTrue("Failed to complete creating and setting up VD", 256 setupLatch.await(WAIT_TIMEOUT_S, TimeUnit.SECONDS)); 257 assertTrue("Failed to wait for required number of frames", 258 mSurfacePixelValidator.waitForAllFrames(maxCapturedDuration)); 259 final CountDownLatch testRunLatch = new CountDownLatch(1); 260 mHandler.post(() -> { 261 cleanupRunnable.run(); 262 testRunLatch.countDown(); 263 }); 264 265 assertTrue("Failed to wait for test to complete", 266 testRunLatch.await(WAIT_TIMEOUT_S, TimeUnit.SECONDS)); 267 268 Log.d(TAG, "Test finished, passFrames " + testResult.passFrames 269 + ", failFrames " + testResult.failFrames); 270 return testResult; 271 } catch (Throwable throwable) { 272 mHandler.post(cleanupRunnable); 273 Log.e(TAG, "Test Failed, passFrames " + testResult.passFrames + ", failFrames " 274 + testResult.failFrames); 275 throw throwable; 276 } 277 } 278 saveFailureCaptures(SparseArray<Bitmap> failFrames, TestName name)279 private void saveFailureCaptures(SparseArray<Bitmap> failFrames, TestName name) { 280 if (failFrames.size() == 0) return; 281 282 String directoryName = Environment.getExternalStorageDirectory() 283 + "/" + STORAGE_DIR 284 + "/" + getClass().getSimpleName() 285 + "/" + name.getMethodName(); 286 File testDirectory = new File(directoryName); 287 if (testDirectory.exists()) { 288 String[] children = testDirectory.list(); 289 if (children == null) { 290 return; 291 } 292 for (String file : children) { 293 new File(testDirectory, file).delete(); 294 } 295 } else { 296 testDirectory.mkdirs(); 297 } 298 299 for (int i = 0; i < failFrames.size(); i++) { 300 int frameNr = failFrames.keyAt(i); 301 Bitmap bitmap = failFrames.valueAt(i); 302 303 String bitmapName = "frame_" + frameNr + ".png"; 304 Log.d(TAG, "Saving file : " + bitmapName + " in directory : " + directoryName); 305 306 File file = new File(directoryName, bitmapName); 307 try (FileOutputStream fileStream = new FileOutputStream(file)) { 308 bitmap.compress(Bitmap.CompressFormat.PNG, 85, fileStream); 309 fileStream.flush(); 310 } catch (IOException e) { 311 e.printStackTrace(); 312 } 313 } 314 } 315 verifyTest(ISurfaceValidatorTestCase testCase, TestName name)316 public void verifyTest(ISurfaceValidatorTestCase testCase, TestName name) throws Throwable { 317 CapturedActivity.TestResult result = runTest(testCase); 318 saveFailureCaptures(result.failures, name); 319 320 float failRatio = 1.0f * result.failFrames / (result.failFrames + result.passFrames); 321 assertTrue("Error: " + failRatio + " fail ratio - extremely high, is activity obstructed?", 322 failRatio < 0.95f); 323 assertEquals("Error: " + result.failFrames 324 + " incorrect frames observed - incorrect positioning", 0, result.failFrames); 325 } 326 verifyTest(ISurfaceValidatorTestCase testCase, TestName name, int maxExpectedFailFrames)327 public void verifyTest(ISurfaceValidatorTestCase testCase, TestName name, 328 int maxExpectedFailFrames) throws Throwable { 329 CapturedActivity.TestResult result = runTest(testCase); 330 saveFailureCaptures(result.failures, name); 331 332 float failRatio = 1.0f * result.failFrames / (result.failFrames + result.passFrames); 333 assertTrue("Error: " + failRatio + " fail ratio - extremely high, is activity obstructed?", 334 failRatio < 0.95f); 335 assertTrue("Error: " + result.failFrames 336 + " incorrect frames observed - incorrect positioning. " 337 + "maxExpectedFailFrames=" 338 + maxExpectedFailFrames + " passFrames=" + result.passFrames, 339 maxExpectedFailFrames >= result.failFrames); 340 } 341 342 restoreSettings()343 public void restoreSettings() { 344 // Adding try/catch due to bug with UiAutomation crashing the test b/272370325 345 try { 346 if (mSettingsSession != null) { 347 mSettingsSession.close(); 348 mSettingsSession = null; 349 } 350 } catch (Exception e) { 351 Log.e(TAG, "Crash occurred when closing settings session. See b/272370325", e); 352 } 353 } 354 } 355