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