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 org.junit.Assert.assertTrue; 19 import static org.junit.Assert.fail; 20 21 import android.app.Activity; 22 import android.content.ComponentName; 23 import android.content.Context; 24 import android.content.Intent; 25 import android.content.ServiceConnection; 26 import android.content.pm.PackageManager; 27 import android.graphics.Bitmap; 28 import android.graphics.Point; 29 import android.graphics.Rect; 30 import android.hardware.display.DisplayManager; 31 import android.hardware.display.VirtualDisplay; 32 import android.media.projection.MediaProjection; 33 import android.media.projection.MediaProjectionManager; 34 import android.os.Bundle; 35 import android.os.Environment; 36 import android.os.Handler; 37 import android.os.IBinder; 38 import android.os.Looper; 39 import android.provider.Settings; 40 import android.server.wm.settings.SettingsSession; 41 import android.support.test.uiautomator.By; 42 import android.support.test.uiautomator.UiDevice; 43 import android.support.test.uiautomator.UiObject2; 44 import android.support.test.uiautomator.Until; 45 import android.util.DisplayMetrics; 46 import android.util.Log; 47 import android.util.SparseArray; 48 import android.view.Display; 49 import android.view.PointerIcon; 50 import android.view.View; 51 import android.view.WindowManager; 52 import android.widget.FrameLayout; 53 54 import androidx.test.InstrumentationRegistry; 55 56 import org.junit.rules.TestName; 57 58 import java.io.File; 59 import java.io.FileOutputStream; 60 import java.io.IOException; 61 import java.util.concurrent.CountDownLatch; 62 import java.util.concurrent.TimeUnit; 63 import java.util.concurrent.atomic.AtomicBoolean; 64 65 public class CapturedActivity extends Activity { 66 public static class TestResult { 67 public int passFrames; 68 public int failFrames; 69 public final SparseArray<Bitmap> failures = new SparseArray<>(); 70 } 71 72 private static class ImmersiveConfirmationSetting extends SettingsSession<String> { ImmersiveConfirmationSetting()73 ImmersiveConfirmationSetting() { 74 super(Settings.Secure.getUriFor( 75 Settings.Secure.IMMERSIVE_MODE_CONFIRMATIONS), 76 Settings.Secure::getString, Settings.Secure::putString); 77 } 78 } 79 80 private ImmersiveConfirmationSetting mSettingsSession; 81 82 private static final String TAG = "CapturedActivity"; 83 private static final int PERMISSION_CODE = 1; 84 private MediaProjectionManager mProjectionManager; 85 private MediaProjection mMediaProjection; 86 private VirtualDisplay mVirtualDisplay; 87 88 private SurfacePixelValidator2 mSurfacePixelValidator; 89 90 private static final int PERMISSION_DIALOG_WAIT_MS = 1000; 91 private static final int RETRY_COUNT = 2; 92 93 private static final long START_CAPTURE_DELAY_MS = 4000; 94 95 private static final String ACCEPT_RESOURCE_ID = "android:id/button1"; 96 97 private final Handler mHandler = new Handler(Looper.getMainLooper()); 98 private volatile boolean mOnEmbedded; 99 private volatile boolean mOnWatch; 100 private CountDownLatch mCountDownLatch; 101 private boolean mProjectionServiceBound = false; 102 private Point mLogicalDisplaySize = new Point(); 103 private long mMinimumCaptureDurationMs = 0; 104 105 private AtomicBoolean mIsSharingScreenDenied; 106 107 @Override onCreate(Bundle savedInstanceState)108 public void onCreate(Bundle savedInstanceState) { 109 super.onCreate(savedInstanceState); 110 mIsSharingScreenDenied = new AtomicBoolean(false); 111 final PackageManager packageManager = getPackageManager(); 112 mOnWatch = packageManager.hasSystemFeature(PackageManager.FEATURE_WATCH); 113 if (mOnWatch) { 114 // Don't try and set up test/capture infrastructure - they're not supported 115 return; 116 } 117 // Embedded devices are significantly slower, and are given 118 // longer duration to capture the expected number of frames 119 mOnEmbedded = packageManager.hasSystemFeature(PackageManager.FEATURE_EMBEDDED); 120 121 mSettingsSession = new ImmersiveConfirmationSetting(); 122 mSettingsSession.set("confirmed"); 123 124 getWindow().getDecorView().setSystemUiVisibility( 125 View.SYSTEM_UI_FLAG_HIDE_NAVIGATION | View.SYSTEM_UI_FLAG_FULLSCREEN); 126 // Set the NULL pointer icon so that it won't obstruct the captured image. 127 getWindow().getDecorView().setPointerIcon( 128 PointerIcon.getSystemIcon(this, PointerIcon.TYPE_NULL)); 129 getWindow().addFlags(WindowManager.LayoutParams.FLAG_KEEP_SCREEN_ON); 130 131 132 mProjectionManager = 133 (MediaProjectionManager) getSystemService(Context.MEDIA_PROJECTION_SERVICE); 134 135 mCountDownLatch = new CountDownLatch(1); 136 bindMediaProjectionService(); 137 } 138 setLogicalDisplaySize(Point logicalDisplaySize)139 public void setLogicalDisplaySize(Point logicalDisplaySize) { 140 mLogicalDisplaySize.set(logicalDisplaySize.x, logicalDisplaySize.y); 141 } 142 dismissPermissionDialog()143 public void dismissPermissionDialog() { 144 // The permission dialog will be auto-opened by the activity - find it and accept 145 UiDevice uiDevice = UiDevice.getInstance(InstrumentationRegistry.getInstrumentation()); 146 UiObject2 acceptButton = uiDevice.wait(Until.findObject(By.res(ACCEPT_RESOURCE_ID)), 147 PERMISSION_DIALOG_WAIT_MS); 148 if (acceptButton != null) { 149 Log.d(TAG, "found permission dialog after searching all windows, clicked"); 150 acceptButton.click(); 151 } 152 } 153 154 private ServiceConnection mConnection = new ServiceConnection() { 155 156 @Override 157 public void onServiceConnected(ComponentName className, IBinder service) { 158 startActivityForResult(mProjectionManager.createScreenCaptureIntent(), PERMISSION_CODE); 159 mProjectionServiceBound = true; 160 } 161 162 @Override 163 public void onServiceDisconnected(ComponentName arg0) { 164 mProjectionServiceBound = false; 165 } 166 }; 167 bindMediaProjectionService()168 private void bindMediaProjectionService() { 169 Intent intent = new Intent(this, LocalMediaProjectionService.class); 170 startService(intent); 171 bindService(intent, mConnection, Context.BIND_AUTO_CREATE); 172 } 173 174 @Override onDestroy()175 public void onDestroy() { 176 super.onDestroy(); 177 Log.d(TAG, "onDestroy"); 178 if (mMediaProjection != null) { 179 mMediaProjection.stop(); 180 mMediaProjection = null; 181 } 182 if (mProjectionServiceBound) { 183 unbindService(mConnection); 184 mProjectionServiceBound = false; 185 } 186 restoreSettings(); 187 } 188 189 @Override onActivityResult(int requestCode, int resultCode, Intent data)190 public void onActivityResult(int requestCode, int resultCode, Intent data) { 191 if (mOnWatch) return; 192 getWindow().getDecorView().setSystemUiVisibility( 193 View.SYSTEM_UI_FLAG_HIDE_NAVIGATION | View.SYSTEM_UI_FLAG_FULLSCREEN); 194 195 if (requestCode != PERMISSION_CODE) { 196 throw new IllegalStateException("Unknown request code: " + requestCode); 197 } 198 mIsSharingScreenDenied.set(resultCode != RESULT_OK); 199 if (mIsSharingScreenDenied.get()) { 200 return; 201 } 202 Log.d(TAG, "onActivityResult"); 203 mMediaProjection = mProjectionManager.getMediaProjection(resultCode, data); 204 mMediaProjection.registerCallback(new MediaProjectionCallback(), null); 205 mCountDownLatch.countDown(); 206 } 207 getCaptureDurationMs()208 public long getCaptureDurationMs() { 209 return mOnEmbedded ? 100000 : 50000; 210 } 211 setMinimumCaptureDurationMs(long durationMs)212 public void setMinimumCaptureDurationMs(long durationMs) { 213 mMinimumCaptureDurationMs = durationMs; 214 } 215 runTest(ISurfaceValidatorTestCase animationTestCase)216 public TestResult runTest(ISurfaceValidatorTestCase animationTestCase) throws Throwable { 217 TestResult testResult = new TestResult(); 218 if (mOnWatch) { 219 /** 220 * Watch devices not supported, since they may not support: 221 * 1) displaying unmasked windows 222 * 2) RenderScript 223 * 3) Video playback 224 */ 225 Log.d(TAG, "Skipping test on watch."); 226 testResult.passFrames = 1000; 227 testResult.failFrames = 0; 228 return testResult; 229 } 230 231 final long timeOutMs = mOnEmbedded ? 125000 : 62500; 232 final long captureDuration = animationTestCase.hasAnimation() ? 233 getCaptureDurationMs() : mMinimumCaptureDurationMs; 234 final long endCaptureDelayMs = START_CAPTURE_DELAY_MS + captureDuration; 235 final long endDelayMs = endCaptureDelayMs + 1000; 236 237 int count = 0; 238 // Sometimes system decides to rotate the permission activity to another orientation 239 // right after showing it. This results in: uiautomation thinks that accept button appears, 240 // we successfully click it in terms of uiautomation, but nothing happens, 241 // because permission activity is already recreated. 242 // Thus, we try to click that button multiple times. 243 do { 244 if (mIsSharingScreenDenied.get()) { 245 throw new IllegalStateException("User denied screen sharing permission."); 246 } 247 assertTrue("Can't get the permission", count <= RETRY_COUNT); 248 dismissPermissionDialog(); 249 count++; 250 } while (!mCountDownLatch.await(timeOutMs, TimeUnit.MILLISECONDS)); 251 252 mHandler.post(() -> { 253 Log.d(TAG, "Setting up test case"); 254 255 // shouldn't be necessary, since we've already done this in #create, 256 // but ensure status/nav are hidden for test 257 getWindow().getDecorView().setSystemUiVisibility( 258 View.SYSTEM_UI_FLAG_HIDE_NAVIGATION | View.SYSTEM_UI_FLAG_FULLSCREEN); 259 260 animationTestCase.start(getApplicationContext(), 261 (FrameLayout) findViewById(android.R.id.content)); 262 }); 263 264 mHandler.postDelayed(() -> { 265 Log.d(TAG, "Starting capture"); 266 267 Display display = getWindow().getDecorView().getDisplay(); 268 DisplayMetrics metrics = new DisplayMetrics(); 269 display.getMetrics(metrics); 270 271 final DisplayManager displayManager = 272 (DisplayManager) CapturedActivity.this.getSystemService( 273 Context.DISPLAY_SERVICE); 274 final Display defaultDisplay = displayManager.getDisplay(Display.DEFAULT_DISPLAY); 275 final int rotation = defaultDisplay.getRotation(); 276 277 Rect boundsToCheck = 278 animationTestCase.getBoundsToCheck(findViewById(android.R.id.content)); 279 if (boundsToCheck.width() < 40 || boundsToCheck.height() < 40) { 280 fail("capture bounds too small to be a fullscreen activity: " + boundsToCheck); 281 } 282 283 mSurfacePixelValidator = new SurfacePixelValidator2(CapturedActivity.this, 284 mLogicalDisplaySize, boundsToCheck, animationTestCase.getChecker()); 285 Log.d("MediaProjection", "Size is " + mLogicalDisplaySize.toString() 286 + ", bounds are " + boundsToCheck.toShortString()); 287 mVirtualDisplay = mMediaProjection.createVirtualDisplay("CtsCapturedActivity", 288 mLogicalDisplaySize.x, mLogicalDisplaySize.y, 289 metrics.densityDpi, 290 DisplayManager.VIRTUAL_DISPLAY_FLAG_AUTO_MIRROR, 291 mSurfacePixelValidator.getSurface(), 292 null /*Callbacks*/, 293 null /*Handler*/); 294 }, START_CAPTURE_DELAY_MS); 295 296 final int SINGLE_FRAME_TIMEOUT_MS = 1000; 297 mHandler.postDelayed(() -> { 298 Log.d(TAG, "Stopping capture"); 299 mSurfacePixelValidator.waitForFrame(SINGLE_FRAME_TIMEOUT_MS); 300 mVirtualDisplay.release(); 301 mVirtualDisplay = null; 302 }, endCaptureDelayMs); 303 304 final CountDownLatch latch = new CountDownLatch(1); 305 mHandler.postDelayed(() -> { 306 Log.d(TAG, "Ending test case"); 307 animationTestCase.end(); 308 mSurfacePixelValidator.finish(testResult); 309 latch.countDown(); 310 mSurfacePixelValidator = null; 311 }, endDelayMs); 312 313 boolean latchResult = latch.await(timeOutMs, TimeUnit.MILLISECONDS); 314 if (!latchResult) { 315 testResult.passFrames = 0; 316 testResult.failFrames = 1000; 317 } 318 Log.d(TAG, "Test finished, passFrames " + testResult.passFrames 319 + ", failFrames " + testResult.failFrames); 320 return testResult; 321 } 322 saveFailureCaptures(SparseArray<Bitmap> failFrames, TestName name)323 private void saveFailureCaptures(SparseArray<Bitmap> failFrames, TestName name) { 324 if (failFrames.size() == 0) return; 325 326 String directoryName = Environment.getExternalStorageDirectory() 327 + "/" + getClass().getSimpleName() 328 + "/" + name.getMethodName(); 329 File testDirectory = new File(directoryName); 330 if (testDirectory.exists()) { 331 String[] children = testDirectory.list(); 332 if (children == null) { 333 return; 334 } 335 for (String file : children) { 336 new File(testDirectory, file).delete(); 337 } 338 } else { 339 testDirectory.mkdirs(); 340 } 341 342 for (int i = 0; i < failFrames.size(); i++) { 343 int frameNr = failFrames.keyAt(i); 344 Bitmap bitmap = failFrames.valueAt(i); 345 346 String bitmapName = "frame_" + frameNr + ".png"; 347 Log.d(TAG, "Saving file : " + bitmapName + " in directory : " + directoryName); 348 349 File file = new File(directoryName, bitmapName); 350 try (FileOutputStream fileStream = new FileOutputStream(file)) { 351 bitmap.compress(Bitmap.CompressFormat.PNG, 85, fileStream); 352 fileStream.flush(); 353 } catch (IOException e) { 354 e.printStackTrace(); 355 } 356 } 357 } 358 verifyTest(ISurfaceValidatorTestCase testCase, TestName name)359 public void verifyTest(ISurfaceValidatorTestCase testCase, TestName name) throws Throwable { 360 if (mIsSharingScreenDenied.get()) { 361 throw new IllegalStateException("User denied screen sharing permission."); 362 } 363 364 CapturedActivity.TestResult result = runTest(testCase); 365 saveFailureCaptures(result.failures, name); 366 367 float failRatio = 1.0f * result.failFrames / (result.failFrames + result.passFrames); 368 assertTrue("Error: " + failRatio + " fail ratio - extremely high, is activity obstructed?", 369 failRatio < 0.95f); 370 assertTrue("Error: " + result.failFrames 371 + " incorrect frames observed - incorrect positioning", 372 result.failFrames == 0); 373 374 if (testCase.hasAnimation()) { 375 float framesPerSecond = 1.0f * result.passFrames 376 / TimeUnit.MILLISECONDS.toSeconds(getCaptureDurationMs()); 377 assertTrue("Error, only " + result.passFrames 378 + " frames observed, virtual display only capturing at " 379 + framesPerSecond + " frames per second", 380 result.passFrames > 100); 381 } 382 } 383 384 private class MediaProjectionCallback extends MediaProjection.Callback { 385 @Override onStop()386 public void onStop() { 387 Log.d(TAG, "MediaProjectionCallback#onStop"); 388 if (mVirtualDisplay != null) { 389 mVirtualDisplay.release(); 390 mVirtualDisplay = null; 391 } 392 } 393 } 394 restoreSettings()395 public void restoreSettings() { 396 if (mSettingsSession != null) { 397 mSettingsSession.close(); 398 mSettingsSession = null; 399 } 400 } 401 402 } 403