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