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