1 /*
2  * Copyright (C) 2021 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 
17 package android.view.cts.surfacevalidator;
18 
19 import static android.view.cts.surfacevalidator.CapturedActivity.STORAGE_DIR;
20 
21 import static androidx.test.platform.app.InstrumentationRegistry.getInstrumentation;
22 
23 import static org.junit.Assert.assertNotNull;
24 import static org.junit.Assert.assertTrue;
25 
26 import android.app.Activity;
27 import android.app.Instrumentation;
28 import android.app.UiAutomation;
29 import android.graphics.Bitmap;
30 import android.graphics.Color;
31 import android.graphics.Rect;
32 import android.os.Bundle;
33 import android.os.Environment;
34 import android.os.Handler;
35 import android.os.Looper;
36 import android.util.Log;
37 import android.view.AttachedSurfaceControl;
38 import android.view.Gravity;
39 import android.view.PointerIcon;
40 import android.view.SurfaceControl;
41 import android.view.SurfaceHolder;
42 import android.view.SurfaceView;
43 import android.view.View;
44 import android.view.WindowInsets;
45 import android.view.WindowInsetsAnimation;
46 import android.view.WindowManager;
47 import android.widget.FrameLayout;
48 
49 import androidx.annotation.NonNull;
50 
51 import org.junit.Assert;
52 import org.junit.rules.TestName;
53 
54 import java.io.File;
55 import java.io.FileOutputStream;
56 import java.io.IOException;
57 import java.util.List;
58 import java.util.concurrent.CountDownLatch;
59 import java.util.concurrent.TimeUnit;
60 
61 public class ASurfaceControlTestActivity extends Activity {
62     private static final String TAG = "ASurfaceControlTestActivity";
63     private static final boolean DEBUG = true;
64 
65     private static final int DEFAULT_LAYOUT_WIDTH = 100;
66     private static final int DEFAULT_LAYOUT_HEIGHT = 100;
67     private static final int OFFSET_X = 100;
68     private static final int OFFSET_Y = 100;
69     public static final long WAIT_TIMEOUT_S = 5;
70 
71     private final Handler mHandler = new Handler(Looper.getMainLooper());
72 
73     private SurfaceView mSurfaceView;
74     private FrameLayout.LayoutParams mLayoutParams;
75     private FrameLayout mParent;
76 
77     private Bitmap mScreenshot;
78 
79     private Instrumentation mInstrumentation;
80 
81     private final InsetsAnimationCallback mInsetsAnimationCallback = new InsetsAnimationCallback();
82     private final CountDownLatch mReadyToStart = new CountDownLatch(1);
83     private CountDownLatch mTransactionCommittedLatch;
84 
85     @Override
onEnterAnimationComplete()86     public void onEnterAnimationComplete() {
87         mReadyToStart.countDown();
88     }
89 
90     @Override
onCreate(Bundle savedInstanceState)91     public void onCreate(Bundle savedInstanceState) {
92         super.onCreate(savedInstanceState);
93 
94         final View decorView = getWindow().getDecorView();
95         decorView.setWindowInsetsAnimationCallback(mInsetsAnimationCallback);
96         decorView.setSystemUiVisibility(
97                 View.SYSTEM_UI_FLAG_HIDE_NAVIGATION | View.SYSTEM_UI_FLAG_FULLSCREEN);
98         // Set the NULL pointer icon so that it won't obstruct the captured image.
99         decorView.setPointerIcon(
100                 PointerIcon.getSystemIcon(this, PointerIcon.TYPE_NULL));
101         getWindow().addFlags(WindowManager.LayoutParams.FLAG_KEEP_SCREEN_ON);
102 
103         mLayoutParams = new FrameLayout.LayoutParams(DEFAULT_LAYOUT_WIDTH, DEFAULT_LAYOUT_HEIGHT,
104                 Gravity.LEFT | Gravity.TOP);
105 
106         mLayoutParams.topMargin = OFFSET_Y;
107         mLayoutParams.leftMargin = OFFSET_X;
108         mSurfaceView = new SurfaceView(this);
109         mSurfaceView.getHolder().setFixedSize(DEFAULT_LAYOUT_WIDTH, DEFAULT_LAYOUT_HEIGHT);
110 
111         mParent = findViewById(android.R.id.content);
112 
113         mInstrumentation = getInstrumentation();
114     }
115 
getSurfaceControl()116     public SurfaceControl getSurfaceControl() {
117         return mSurfaceView.getSurfaceControl();
118     }
119 
verifyTest(SurfaceHolder.Callback surfaceHolderCallback, PixelChecker pixelChecker, TestName name)120     public void verifyTest(SurfaceHolder.Callback surfaceHolderCallback,
121             PixelChecker pixelChecker, TestName name) {
122         verifyTest(surfaceHolderCallback, pixelChecker, name, 0);
123     }
124 
verifyTest(SurfaceHolder.Callback surfaceHolderCallback, PixelChecker pixelChecker, TestName name, int numOfTransactionToListen)125     public void verifyTest(SurfaceHolder.Callback surfaceHolderCallback,
126             PixelChecker pixelChecker, TestName name, int numOfTransactionToListen) {
127         final boolean waitForTransactionLatch = numOfTransactionToListen > 0;
128         final CountDownLatch readyFence = new CountDownLatch(1);
129         if (waitForTransactionLatch) {
130             mTransactionCommittedLatch = new CountDownLatch(numOfTransactionToListen);
131         }
132         SurfaceHolderCallback surfaceHolderCallbackWrapper = new SurfaceHolderCallback(
133                 surfaceHolderCallback,
134                 readyFence, mParent.getRootSurfaceControl());
135         createSurface(surfaceHolderCallbackWrapper);
136         try {
137             if (waitForTransactionLatch) {
138                 assertTrue("timeout",
139                         mTransactionCommittedLatch.await(WAIT_TIMEOUT_S, TimeUnit.SECONDS));
140             }
141             assertTrue("timeout", readyFence.await(WAIT_TIMEOUT_S, TimeUnit.SECONDS));
142         } catch (InterruptedException e) {
143             Assert.fail("interrupted");
144         }
145         verifyScreenshot(pixelChecker, name);
146         mHandler.post(() -> mSurfaceView.getHolder().removeCallback(surfaceHolderCallback));
147     }
148 
awaitReadyState()149     public void awaitReadyState() {
150         try {
151             assertTrue(mReadyToStart.await(5, TimeUnit.SECONDS));
152         } catch (InterruptedException e) {
153             throw new RuntimeException(e);
154         }
155     }
156 
createSurface(SurfaceHolderCallback surfaceHolderCallback)157     public void createSurface(SurfaceHolderCallback surfaceHolderCallback) {
158         awaitReadyState();
159 
160         mHandler.post(() -> {
161             mSurfaceView.getHolder().addCallback(surfaceHolderCallback);
162             mParent.addView(mSurfaceView, mLayoutParams);
163         });
164     }
165 
verifyScreenshot(PixelChecker pixelChecker, TestName name)166     public void verifyScreenshot(PixelChecker pixelChecker, TestName name) {
167         // Wait for the stable insets update. The position of the surface view is in correct before
168         // the update. Sometimes this callback isn't called, so we don't want to fail the test
169         // because it times out.
170         if (!mInsetsAnimationCallback.waitForInsetsAnimation()) {
171             Log.w(TAG, "Insets animation wait timed out.");
172         }
173 
174         final CountDownLatch countDownLatch = new CountDownLatch(1);
175         UiAutomation uiAutomation = mInstrumentation.getUiAutomation();
176         mHandler.post(() -> {
177             mScreenshot = uiAutomation.takeScreenshot(getWindow());
178             mParent.removeAllViews();
179             countDownLatch.countDown();
180         });
181 
182         try {
183             countDownLatch.await(WAIT_TIMEOUT_S, TimeUnit.SECONDS);
184         } catch (Exception e) {
185         }
186 
187         assertNotNull(mScreenshot);
188 
189         Bitmap swBitmap = mScreenshot.copy(Bitmap.Config.ARGB_8888, false);
190         mScreenshot.recycle();
191 
192         int numMatchingPixels = pixelChecker.getNumMatchingPixels(swBitmap);
193 
194         int checkedPixels = 0;
195         for (Rect bounds : pixelChecker.getBoundsToCheck(swBitmap)) {
196             checkedPixels += bounds.width() * bounds.height();
197         }
198 
199         boolean success = pixelChecker.checkPixels(numMatchingPixels, swBitmap.getWidth(),
200                 swBitmap.getHeight());
201         if (!success) {
202             saveFailureCapture(swBitmap, name);
203         }
204         swBitmap.recycle();
205 
206         assertTrue("Actual matched pixels:" + numMatchingPixels
207                 + " Number of pixels checked:" + checkedPixels, success);
208     }
209 
getSurfaceView()210     public SurfaceView getSurfaceView() {
211         return mSurfaceView;
212     }
213 
getParentFrameLayout()214     public FrameLayout getParentFrameLayout() {
215         return mParent;
216     }
217 
transactionCommitted()218     public void transactionCommitted() {
219         mTransactionCommittedLatch.countDown();
220     }
221 
222     public static class RectChecker extends PixelChecker {
223         private final List<Rect> mBoundsToCheck;
224 
RectChecker(List<Rect> boundsToCheck)225         public RectChecker(List<Rect> boundsToCheck) {
226             super();
227             mBoundsToCheck = boundsToCheck;
228         }
229 
RectChecker(Rect boundsToCheck)230         public RectChecker(Rect boundsToCheck) {
231             this(List.of(boundsToCheck));
232         }
233 
RectChecker(Rect boundsToCheck, int expectedColor)234         public RectChecker(Rect boundsToCheck, int expectedColor) {
235             super(expectedColor);
236             mBoundsToCheck = List.of(boundsToCheck);
237         }
238 
239         @Override
checkPixels(int matchingPixelCount, int width, int height)240         public boolean checkPixels(int matchingPixelCount, int width, int height) {
241             int expectedPixelCount = 0;
242             for (Rect bounds : mBoundsToCheck) {
243                 expectedPixelCount += bounds.width() * bounds.height();
244             }
245             return expectedPixelCount - 100 < matchingPixelCount
246                     && matchingPixelCount <= expectedPixelCount;
247         }
248 
249         @Override
getBoundsToCheck(Bitmap bitmap)250         public List<Rect> getBoundsToCheck(Bitmap bitmap) {
251             return mBoundsToCheck;
252         }
253     }
254 
255     public abstract static class PixelChecker {
256         private final PixelColor mPixelColor;
257         private final boolean mLogWhenNoMatch;
258 
PixelChecker()259         public PixelChecker() {
260             this(Color.BLACK, true);
261         }
262 
PixelChecker(int color)263         public PixelChecker(int color) {
264             this(color, true);
265         }
266 
PixelChecker(int color, boolean logWhenNoMatch)267         public PixelChecker(int color, boolean logWhenNoMatch) {
268             mPixelColor = new PixelColor(color);
269             mLogWhenNoMatch = logWhenNoMatch;
270         }
271 
getNumMatchingPixels(Bitmap bitmap)272         int getNumMatchingPixels(Bitmap bitmap) {
273             int numMatchingPixels = 0;
274             int numErrorsLogged = 0;
275             for (Rect boundsToCheck : getBoundsToCheck(bitmap)) {
276                 for (int x = boundsToCheck.left; x < boundsToCheck.right; x++) {
277                     for (int y = boundsToCheck.top; y < boundsToCheck.bottom; y++) {
278                         int color = bitmap.getPixel(x + OFFSET_X, y + OFFSET_Y);
279                         if (getExpectedColor(x, y).matchesColor(color)) {
280                             numMatchingPixels++;
281                         } else if (DEBUG && mLogWhenNoMatch && numErrorsLogged < 100) {
282                             // We don't want to spam the logcat with errors if something is really
283                             // broken. Only log the first 100 errors.
284                             PixelColor expected = getExpectedColor(x, y);
285                             int expectedColor = Color.argb(expected.mAlpha, expected.mRed,
286                                     expected.mGreen, expected.mBlue);
287                             Log.e(TAG, String.format(
288                                     "Failed to match (%d, %d) color=0x%08X expected=0x%08X", x, y,
289                                     color, expectedColor));
290                             numErrorsLogged++;
291                         }
292                     }
293                 }
294             }
295             return numMatchingPixels;
296         }
297 
checkPixels(int matchingPixelCount, int width, int height)298         public abstract boolean checkPixels(int matchingPixelCount, int width, int height);
299 
getBoundsToCheck(Bitmap bitmap)300         public List<Rect> getBoundsToCheck(Bitmap bitmap) {
301             return List.of(new Rect(1, 1, DEFAULT_LAYOUT_WIDTH - 1, DEFAULT_LAYOUT_HEIGHT - 1));
302         }
303 
getExpectedColor(int x, int y)304         public PixelColor getExpectedColor(int x, int y) {
305             return mPixelColor;
306         }
307     }
308 
309     public static class SurfaceHolderCallback implements SurfaceHolder.Callback {
310         private final SurfaceHolder.Callback mTestCallback;
311         private final CountDownLatch mSurfaceCreatedLatch;
312         private final AttachedSurfaceControl mAttachedSurfaceControl;
313 
SurfaceHolderCallback(SurfaceHolder.Callback callback, CountDownLatch readyFence, AttachedSurfaceControl attachedSurfaceControl)314         public SurfaceHolderCallback(SurfaceHolder.Callback callback, CountDownLatch readyFence,
315                 AttachedSurfaceControl attachedSurfaceControl) {
316             mTestCallback = callback;
317             mSurfaceCreatedLatch = readyFence;
318             mAttachedSurfaceControl = attachedSurfaceControl;
319         }
320 
321         @Override
surfaceCreated(@onNull SurfaceHolder holder)322         public void surfaceCreated(@NonNull SurfaceHolder holder) {
323             mTestCallback.surfaceCreated(holder);
324             try (SurfaceControl.Transaction transaction = new SurfaceControl.Transaction()) {
325                 transaction.addTransactionCommittedListener(Runnable::run,
326                         mSurfaceCreatedLatch::countDown);
327                 mAttachedSurfaceControl.applyTransactionOnDraw(transaction);
328             }
329         }
330 
331         @Override
surfaceChanged(@onNull SurfaceHolder holder, int format, int width, int height)332         public void surfaceChanged(@NonNull SurfaceHolder holder, int format, int width,
333                 int height) {
334             mTestCallback.surfaceChanged(holder, format, width, height);
335         }
336 
337         @Override
surfaceDestroyed(@onNull SurfaceHolder holder)338         public void surfaceDestroyed(@NonNull SurfaceHolder holder) {
339             mTestCallback.surfaceDestroyed(holder);
340         }
341     }
342 
saveFailureCapture(Bitmap failFrame, TestName name)343     private void saveFailureCapture(Bitmap failFrame, TestName name) {
344         String directoryName = Environment.getExternalStorageDirectory()
345                 + "/" + STORAGE_DIR
346                 + "/" + getClass().getSimpleName()
347                 + "/" + name.getMethodName();
348         File testDirectory = new File(directoryName);
349         if (testDirectory.exists()) {
350             String[] children = testDirectory.list();
351             for (String file : children) {
352                 new File(testDirectory, file).delete();
353             }
354         } else {
355             testDirectory.mkdirs();
356         }
357 
358         String bitmapName = "frame.png";
359         Log.d(TAG, "Saving file : " + bitmapName + " in directory : " + directoryName);
360 
361         File file = new File(directoryName, bitmapName);
362         try (FileOutputStream fileStream = new FileOutputStream(file)) {
363             failFrame.compress(Bitmap.CompressFormat.PNG, 85, fileStream);
364             fileStream.flush();
365         } catch (IOException e) {
366             e.printStackTrace();
367         }
368     }
369 
370     private static class InsetsAnimationCallback extends WindowInsetsAnimation.Callback {
371         private CountDownLatch mLatch = new CountDownLatch(1);
372 
InsetsAnimationCallback()373         private InsetsAnimationCallback() {
374             super(DISPATCH_MODE_CONTINUE_ON_SUBTREE);
375         }
376 
377         @Override
onProgress( WindowInsets insets, List<WindowInsetsAnimation> runningAnimations)378         public WindowInsets onProgress(
379                 WindowInsets insets, List<WindowInsetsAnimation> runningAnimations) {
380             return insets;
381         }
382 
383         @Override
onEnd(WindowInsetsAnimation animation)384         public void onEnd(WindowInsetsAnimation animation) {
385             mLatch.countDown();
386         }
387 
waitForInsetsAnimation()388         private boolean waitForInsetsAnimation() {
389             try {
390                 return mLatch.await(WAIT_TIMEOUT_S, TimeUnit.SECONDS);
391             } catch (InterruptedException e) {
392                 // Should never happen
393                 throw new RuntimeException(e);
394             }
395         }
396     }
397 }
398