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 assertNotNull("Failed to wait for test window bounds", bounds); 214 mTestAreaSize.set(bounds.width(), bounds.height()); 215 216 CountDownLatch setupLatch = new CountDownLatch(1); 217 mHandler.post(() -> { 218 Log.d(TAG, "Starting capture"); 219 220 Log.d(TAG, "testAreaWidth: " + mTestAreaSize.x 221 + ", testAreaHeight: " + mTestAreaSize.y); 222 223 Rect boundsToCheck = animationTestCase.getBoundsToCheck(mParentLayout); 224 if (boundsToCheck != null && (boundsToCheck.width() < 40 225 || boundsToCheck.height() < 40)) { 226 fail("capture bounds too small to be a fullscreen activity: " + boundsToCheck); 227 } 228 229 Log.d(TAG, "Size is " + mTestAreaSize + ", bounds are " 230 + (boundsToCheck == null ? "full screen" : boundsToCheck.toShortString())); 231 232 mSurfacePixelValidator = new SurfacePixelValidator2(mTestAreaSize, 233 boundsToCheck, animationTestCase.getChecker(), numFramesRequired); 234 235 WindowMetrics metrics = getWindowManager().getCurrentWindowMetrics(); 236 Log.d(TAG, "Starting capture: metrics=" + metrics); 237 int densityDpi = (int) (metrics.getDensity() * DisplayMetrics.DENSITY_DEFAULT); 238 239 DisplayManager dm = getSystemService(DisplayManager.class); 240 mVirtualDisplay = dm.createVirtualDisplay("CtsCapturedActivity", 241 mTestAreaSize.x, mTestAreaSize.y, densityDpi, 242 mSurfacePixelValidator.getSurface(), 0, null /*Callbacks*/, 243 null /*Handler*/); 244 assertNotNull("Failed to create VirtualDisplay", mVirtualDisplay); 245 SystemUtil.runWithShellPermissionIdentity( 246 () -> assertTrue("Failed to mirror content onto display", 247 getWindowManager().replaceContentOnDisplayWithMirror( 248 mVirtualDisplay.getDisplay().getDisplayId(), getWindow())), 249 Manifest.permission.ACCESS_SURFACE_FLINGER); 250 251 setupLatch.countDown(); 252 }); 253 254 assertTrue("Failed to complete creating and setting up VD", 255 setupLatch.await(WAIT_TIMEOUT_S, TimeUnit.SECONDS)); 256 assertTrue("Failed to wait for required number of frames", 257 mSurfacePixelValidator.waitForAllFrames(maxCapturedDuration)); 258 final CountDownLatch testRunLatch = new CountDownLatch(1); 259 mHandler.post(() -> { 260 cleanupRunnable.run(); 261 testRunLatch.countDown(); 262 }); 263 264 assertTrue("Failed to wait for test to complete", 265 testRunLatch.await(WAIT_TIMEOUT_S, TimeUnit.SECONDS)); 266 267 Log.d(TAG, "Test finished, passFrames " + testResult.passFrames 268 + ", failFrames " + testResult.failFrames); 269 return testResult; 270 } catch (Throwable throwable) { 271 mHandler.post(cleanupRunnable); 272 Log.e(TAG, "Test Failed, passFrames " + testResult.passFrames + ", failFrames " 273 + testResult.failFrames); 274 throw throwable; 275 } 276 } 277 saveFailureCaptures(SparseArray<Bitmap> failFrames, TestName name)278 private void saveFailureCaptures(SparseArray<Bitmap> failFrames, TestName name) { 279 if (failFrames.size() == 0) return; 280 281 String directoryName = Environment.getExternalStorageDirectory() 282 + "/" + STORAGE_DIR 283 + "/" + getClass().getSimpleName() 284 + "/" + name.getMethodName(); 285 File testDirectory = new File(directoryName); 286 if (testDirectory.exists()) { 287 String[] children = testDirectory.list(); 288 if (children == null) { 289 return; 290 } 291 for (String file : children) { 292 new File(testDirectory, file).delete(); 293 } 294 } else { 295 testDirectory.mkdirs(); 296 } 297 298 for (int i = 0; i < failFrames.size(); i++) { 299 int frameNr = failFrames.keyAt(i); 300 Bitmap bitmap = failFrames.valueAt(i); 301 302 String bitmapName = "frame_" + frameNr + ".png"; 303 Log.d(TAG, "Saving file : " + bitmapName + " in directory : " + directoryName); 304 305 File file = new File(directoryName, bitmapName); 306 try (FileOutputStream fileStream = new FileOutputStream(file)) { 307 bitmap.compress(Bitmap.CompressFormat.PNG, 85, fileStream); 308 fileStream.flush(); 309 } catch (IOException e) { 310 e.printStackTrace(); 311 } 312 } 313 } 314 verifyTest(ISurfaceValidatorTestCase testCase, TestName name)315 public void verifyTest(ISurfaceValidatorTestCase testCase, TestName name) throws Throwable { 316 CapturedActivity.TestResult result = runTest(testCase); 317 saveFailureCaptures(result.failures, name); 318 319 float failRatio = 1.0f * result.failFrames / (result.failFrames + result.passFrames); 320 assertTrue("Error: " + failRatio + " fail ratio - extremely high, is activity obstructed?", 321 failRatio < 0.95f); 322 assertEquals("Error: " + result.failFrames 323 + " incorrect frames observed - incorrect positioning", 0, result.failFrames); 324 } 325 restoreSettings()326 public void restoreSettings() { 327 // Adding try/catch due to bug with UiAutomation crashing the test b/272370325 328 try { 329 if (mSettingsSession != null) { 330 mSettingsSession.close(); 331 mSettingsSession = null; 332 } 333 } catch (Exception e) { 334 Log.e(TAG, "Crash occurred when closing settings session. See b/272370325", e); 335 } 336 } 337 } 338