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