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 package android.view.surfacecontrol.cts;
17 
18 import static android.content.res.Configuration.ORIENTATION_LANDSCAPE;
19 import static android.content.res.Configuration.ORIENTATION_PORTRAIT;
20 import static android.server.wm.BuildUtils.HW_TIMEOUT_MULTIPLIER;
21 import static android.server.wm.CtsWindowInfoUtils.waitForWindowOnTop;
22 import static android.view.cts.surfacevalidator.BitmapPixelChecker.validateScreenshot;
23 
24 import static com.google.common.truth.Truth.assertThat;
25 
26 import static org.junit.Assert.assertTrue;
27 import static org.junit.Assert.fail;
28 import static org.junit.Assume.assumeFalse;
29 
30 import android.app.Activity;
31 import android.content.Context;
32 import android.content.pm.ActivityInfo;
33 import android.content.pm.PackageManager;
34 import android.graphics.Canvas;
35 import android.graphics.Color;
36 import android.graphics.Insets;
37 import android.graphics.Rect;
38 import android.platform.test.annotations.RequiresFlagsEnabled;
39 import android.platform.test.flag.junit.CheckFlagsRule;
40 import android.platform.test.flag.junit.DeviceFlagsValueProvider;
41 import android.server.wm.IgnoreOrientationRequestSession;
42 import android.server.wm.WindowManagerStateHelper;
43 import android.util.Log;
44 import android.view.AttachedSurfaceControl;
45 import android.view.Gravity;
46 import android.view.Surface;
47 import android.view.SurfaceControl;
48 import android.view.SurfaceControlViewHost;
49 import android.view.SurfaceHolder;
50 import android.view.SurfaceView;
51 import android.view.View;
52 import android.view.ViewTreeObserver;
53 import android.view.cts.surfacevalidator.BitmapPixelChecker;
54 import android.widget.FrameLayout;
55 
56 import androidx.annotation.NonNull;
57 import androidx.test.core.app.ActivityScenario;
58 import androidx.test.filters.SmallTest;
59 import androidx.test.platform.app.InstrumentationRegistry;
60 import androidx.test.rule.ActivityTestRule;
61 
62 import com.android.compatibility.common.util.SystemUtil;
63 import com.android.window.flags.Flags;
64 
65 import org.junit.After;
66 import org.junit.Assert;
67 import org.junit.Assume;
68 import org.junit.Before;
69 import org.junit.Rule;
70 import org.junit.Test;
71 import org.junit.rules.TestName;
72 
73 import java.util.concurrent.CountDownLatch;
74 import java.util.concurrent.TimeUnit;
75 import java.util.function.IntConsumer;
76 
77 @SmallTest
78 public class AttachedSurfaceControlTest {
79     private static final String TAG = "AttachedSurfaceControlTest";
80     private static final String FIXED_TO_USER_ROTATION_COMMAND =
81             "cmd window fixed-to-user-rotation";
82     private IgnoreOrientationRequestSession mOrientationSession;
83     private WindowManagerStateHelper mWmState;
84 
85     private static final long WAIT_TIMEOUT_S = 5L * HW_TIMEOUT_MULTIPLIER;
86 
87     @Rule
88     public TestName mName = new TestName();
89 
90     @Rule
91     public final CheckFlagsRule mCheckFlagsRule = DeviceFlagsValueProvider.createCheckFlagsRule();
92 
93     @Rule
94     public ActivityTestRule<HandleConfigurationActivity> mActivityRule = new ActivityTestRule<>(
95             HandleConfigurationActivity.class);
96 
97     private HandleConfigurationActivity mActivity;
98 
99 
100     private static class TransformHintListener implements
101             AttachedSurfaceControl.OnBufferTransformHintChangedListener {
102         Activity activity;
103         int expectedOrientation;
104         CountDownLatch latch = new CountDownLatch(1);
105         IntConsumer hintConsumer;
106 
TransformHintListener(Activity activity, int expectedOrientation, IntConsumer hintConsumer)107         TransformHintListener(Activity activity, int expectedOrientation,
108                 IntConsumer hintConsumer) {
109             this.activity = activity;
110             this.expectedOrientation = expectedOrientation;
111             this.hintConsumer = hintConsumer;
112         }
113 
114         @Override
onBufferTransformHintChanged(int hint)115         public void onBufferTransformHintChanged(int hint) {
116             int orientation = activity.getResources().getConfiguration().orientation;
117             Log.d(TAG, "onBufferTransformHintChanged: orientation actual=" + orientation
118                     + " expected=" + expectedOrientation + " transformHint=" + hint);
119             Assert.assertEquals("Failed to switch orientation hint=" + hint, orientation,
120                     expectedOrientation);
121 
122             // Check the callback value matches the call to get the transform hint.
123             int actualTransformHint =
124                     activity.getWindow().getRootSurfaceControl().getBufferTransformHint();
125             Assert.assertEquals(
126                     "Callback " + hint + " doesn't match transform hint=" + actualTransformHint,
127                     hint,
128                     actualTransformHint);
129             hintConsumer.accept(hint);
130             latch.countDown();
131             activity.getWindow().getRootSurfaceControl()
132                     .removeOnBufferTransformHintChangedListener(this);
133         }
134     }
135 
136     @Before
setup()137     public void setup() throws InterruptedException {
138         mOrientationSession = new IgnoreOrientationRequestSession(false /* enable */);
139         mWmState = new WindowManagerStateHelper();
140         mActivity = mActivityRule.getActivity();
141         waitForWindowOnTop(mActivity.getWindow());
142     }
143 
supportRotationCheck()144     private void supportRotationCheck() {
145         PackageManager pm =
146                 InstrumentationRegistry.getInstrumentation().getContext().getPackageManager();
147         boolean supportsRotation = pm.hasSystemFeature(PackageManager.FEATURE_SCREEN_PORTRAIT)
148                 && pm.hasSystemFeature(PackageManager.FEATURE_SCREEN_LANDSCAPE);
149         final boolean isFixedToUserRotation =
150                 "enabled".equals(SystemUtil.runShellCommand(FIXED_TO_USER_ROTATION_COMMAND).trim());
151         Assume.assumeTrue(supportsRotation && !isFixedToUserRotation);
152     }
153 
154     @After
teardown()155     public void teardown() {
156         if (mOrientationSession != null) {
157             mOrientationSession.close();
158         }
159     }
160 
161     @Test
testOnBufferTransformHintChangedListener()162     public void testOnBufferTransformHintChangedListener() throws InterruptedException {
163         supportRotationCheck();
164 
165         final int[] transformHintResult = new int[2];
166         final CountDownLatch[] firstCallback = new CountDownLatch[1];
167         final CountDownLatch[] secondCallback = new CountDownLatch[1];
168         mWmState.computeState();
169         assumeFalse("Skipping test: display area is ignoring orientation request",
170                 mWmState.isTaskDisplayAreaIgnoringOrientationRequest(
171                         mActivity.getComponentName()));
172         int requestedOrientation = getRequestedOrientation(mActivity);
173         TransformHintListener listener = new TransformHintListener(mActivity,
174                 requestedOrientation, hint -> transformHintResult[0] = hint);
175         firstCallback[0] = listener.latch;
176         mActivity.getWindow().getRootSurfaceControl()
177                 .addOnBufferTransformHintChangedListener(listener);
178         setRequestedOrientation(mActivity, requestedOrientation);
179         // Check we get a callback since the orientation has changed and we expect transform
180         // hint to change.
181         Assert.assertTrue(firstCallback[0].await(10, TimeUnit.SECONDS));
182 
183         requestedOrientation = getRequestedOrientation(mActivity);
184         TransformHintListener secondListener = new TransformHintListener(mActivity,
185                 requestedOrientation, hint -> transformHintResult[1] = hint);
186         secondCallback[0] = secondListener.latch;
187         mActivity.getWindow().getRootSurfaceControl()
188                 .addOnBufferTransformHintChangedListener(secondListener);
189         setRequestedOrientation(mActivity, requestedOrientation);
190         // Check we get a callback since the orientation has changed and we expect transform
191         // hint to change.
192         Assert.assertTrue(secondCallback[0].await(10, TimeUnit.SECONDS));
193 
194         // If the app orientation was changed, we should get a different transform hint
195         Assert.assertNotEquals(transformHintResult[0], transformHintResult[1]);
196     }
197 
getRequestedOrientation(Activity activity)198     private int getRequestedOrientation(Activity activity) {
199         int currentOrientation = activity.getResources().getConfiguration().orientation;
200         return currentOrientation == ORIENTATION_LANDSCAPE ? ORIENTATION_PORTRAIT
201                 : ORIENTATION_LANDSCAPE;
202     }
203 
setRequestedOrientation(Activity activity, int requestedOrientation)204     private void setRequestedOrientation(Activity activity,
205             /* @Configuration.Orientation */ int requestedOrientation) {
206         /* @ActivityInfo.ScreenOrientation */
207         Log.d(TAG, "setRequestedOrientation: requestedOrientation=" + requestedOrientation);
208         int screenOrientation =
209                 requestedOrientation == ORIENTATION_LANDSCAPE
210                         ? ActivityInfo.SCREEN_ORIENTATION_LANDSCAPE
211                         : ActivityInfo.SCREEN_ORIENTATION_PORTRAIT;
212         activity.setRequestedOrientation(screenOrientation);
213     }
214 
215     @Test
testOnBufferTransformHintChangesFromLandToSea()216     public void testOnBufferTransformHintChangesFromLandToSea() throws InterruptedException {
217         supportRotationCheck();
218 
219         final int[] transformHintResult = new int[2];
220         final CountDownLatch[] firstCallback = new CountDownLatch[1];
221         final CountDownLatch[] secondCallback = new CountDownLatch[1];
222         mWmState.computeState();
223         assumeFalse("Skipping test: display area is ignoring orientation request",
224                 mWmState.isTaskDisplayAreaIgnoringOrientationRequest(
225                         mActivity.getComponentName()));
226         if (mActivity.getResources().getConfiguration().orientation
227                 != ORIENTATION_LANDSCAPE) {
228             Log.d(TAG, "Request landscape orientation");
229             TransformHintListener listener = new TransformHintListener(mActivity,
230                     ORIENTATION_LANDSCAPE, hint -> {
231                 transformHintResult[0] = hint;
232                 Log.d(TAG, "firstListener fired with hint =" + hint);
233             });
234             firstCallback[0] = listener.latch;
235             mActivity.getWindow().getRootSurfaceControl()
236                     .addOnBufferTransformHintChangedListener(listener);
237             setRequestedOrientation(mActivity, ORIENTATION_LANDSCAPE);
238             Assert.assertTrue(firstCallback[0].await(10, TimeUnit.SECONDS));
239         } else {
240             transformHintResult[0] =
241                     mActivity.getWindow().getRootSurfaceControl().getBufferTransformHint();
242             Log.d(TAG, "Skipped request landscape orientation: hint=" + transformHintResult[0]);
243         }
244 
245         TransformHintListener secondListener = new TransformHintListener(mActivity,
246                 ORIENTATION_LANDSCAPE, hint -> {
247             transformHintResult[1] = hint;
248             Log.d(TAG, "secondListener fired with hint =" + hint);
249         });
250         secondCallback[0] = secondListener.latch;
251         mActivity.getWindow().getRootSurfaceControl()
252                 .addOnBufferTransformHintChangedListener(secondListener);
253         Log.d(TAG, "Requesting reverse landscape");
254         mActivity.setRequestedOrientation(ActivityInfo.SCREEN_ORIENTATION_REVERSE_LANDSCAPE);
255 
256         Assert.assertTrue(secondCallback[0].await(10, TimeUnit.SECONDS));
257         Assert.assertNotEquals(transformHintResult[0], transformHintResult[1]);
258     }
259 
260     private static class GreenAnchorViewWithInsets extends View {
261         SurfaceControl mSurfaceControl;
262         final Surface mSurface;
263 
264         private final Rect mChildBoundingInsets;
265 
266         private final CountDownLatch mDrawCompleteLatch = new CountDownLatch(1);
267 
268         private boolean mChildScAttached;
269 
GreenAnchorViewWithInsets(Context c, Rect insets)270         GreenAnchorViewWithInsets(Context c, Rect insets) {
271             super(c, null, 0, 0);
272             mSurfaceControl = new SurfaceControl.Builder()
273                     .setName("SurfaceAnchorView")
274                     .setBufferSize(100, 100)
275                     .build();
276             mSurface = new Surface(mSurfaceControl);
277             Canvas canvas = mSurface.lockHardwareCanvas();
278             canvas.drawColor(Color.GREEN);
279             mSurface.unlockCanvasAndPost(canvas);
280             mChildBoundingInsets = insets;
281 
282             getViewTreeObserver().addOnPreDrawListener(new ViewTreeObserver.OnPreDrawListener() {
283                 @Override
284                 public boolean onPreDraw() {
285                     attachChildSc();
286                     getViewTreeObserver().removeOnPreDrawListener(this);
287                     return true;
288                 }
289             });
290         }
291 
292         @Override
onAttachedToWindow()293         protected void onAttachedToWindow() {
294             super.onAttachedToWindow();
295             attachChildSc();
296         }
297 
attachChildSc()298         private void attachChildSc() {
299             if (mChildScAttached) {
300                 return;
301             }
302             // This should be called even if buildReparentTransaction fails the first time since
303             // the second call will come from preDrawListener which is called after bounding insets
304             // are updated in VRI.
305             getRootSurfaceControl().setChildBoundingInsets(mChildBoundingInsets);
306 
307             SurfaceControl.Transaction t =
308                     getRootSurfaceControl().buildReparentTransaction(mSurfaceControl);
309 
310             if (t == null) {
311                 // TODO (b/286406553) SurfaceControl was not yet setup. Wait until the draw request
312                 // to attach since the SurfaceControl will be created by that point. This can be
313                 // cleaned up when the bug is fixed.
314                 return;
315             }
316 
317             t.setLayer(mSurfaceControl, 1).setVisibility(mSurfaceControl, true);
318             t.addTransactionCommittedListener(Runnable::run, mDrawCompleteLatch::countDown);
319             getRootSurfaceControl().applyTransactionOnDraw(t);
320             mChildScAttached = true;
321         }
322 
323         @Override
onDetachedFromWindow()324         protected void onDetachedFromWindow() {
325             new SurfaceControl.Transaction().reparent(mSurfaceControl, null).apply();
326             mSurfaceControl.release();
327             mSurface.release();
328             mChildScAttached = false;
329 
330             super.onDetachedFromWindow();
331         }
332 
waitForDrawn()333         public void waitForDrawn() {
334             try {
335                 assertTrue("Failed to wait for frame to draw",
336                         mDrawCompleteLatch.await(WAIT_TIMEOUT_S, TimeUnit.SECONDS));
337             } catch (InterruptedException e) {
338                 fail();
339             }
340         }
341     }
342 
343     @Test
testCropWithChildBoundingInsets()344     public void testCropWithChildBoundingInsets() throws Throwable {
345         try (ActivityScenario<TestActivity> scenario =
346                      ActivityScenario.launch(TestActivity.class)) {
347             CountDownLatch countDownLatch = new CountDownLatch(1);
348             final GreenAnchorViewWithInsets[] view = new GreenAnchorViewWithInsets[1];
349             final Activity[] activity = new Activity[1];
350             scenario.onActivity(a -> {
351                 activity[0] = a;
352                 FrameLayout parentLayout = a.getParentLayout();
353                 GreenAnchorViewWithInsets anchorView = new GreenAnchorViewWithInsets(a,
354                         new Rect(0, 10, 0, 0));
355                 parentLayout.addView(anchorView,
356                         new FrameLayout.LayoutParams(100, 100, Gravity.LEFT | Gravity.TOP));
357 
358                 view[0] = anchorView;
359                 countDownLatch.countDown();
360             });
361             assertTrue("Failed to wait for activity to start",
362                     countDownLatch.await(WAIT_TIMEOUT_S, TimeUnit.SECONDS));
363 
364             view[0].waitForDrawn();
365             // Do not include system insets because the child SC is not laid out in the system
366             // insets
367             validateScreenshot(mName, activity[0],
368                     new BitmapPixelChecker(Color.GREEN, new Rect(0, 10, 100, 100)),
369                     9000 /* expectedMatchingPixels */, Insets.NONE);
370         }
371     }
372 
373     private static class ScvhSurfaceView extends SurfaceView implements SurfaceHolder.Callback {
374         CountDownLatch mReadyLatch = new CountDownLatch(1);
375         SurfaceControlViewHost mScvh;
376         final View mView;
377 
ScvhSurfaceView(Context context, View view)378         ScvhSurfaceView(Context context, View view) {
379             super(context);
380             getHolder().addCallback(this);
381             mView = view;
382         }
383 
384         @Override
surfaceCreated(@onNull SurfaceHolder surfaceHolder)385         public void surfaceCreated(@NonNull SurfaceHolder surfaceHolder) {
386             mView.getViewTreeObserver().addOnWindowAttachListener(
387                     new ViewTreeObserver.OnWindowAttachListener() {
388                         @Override
389                         public void onWindowAttached() {
390                             mReadyLatch.countDown();
391                         }
392 
393                         @Override
394                         public void onWindowDetached() {
395                         }
396                     });
397             mScvh = new SurfaceControlViewHost(getContext(), getDisplay(), getHostToken());
398             mScvh.setView(mView, getWidth(), getHeight());
399             setChildSurfacePackage(mScvh.getSurfacePackage());
400         }
401 
402         @Override
surfaceChanged(@onNull SurfaceHolder surfaceHolder, int i, int i1, int i2)403         public void surfaceChanged(@NonNull SurfaceHolder surfaceHolder, int i, int i1, int i2) {
404 
405         }
406 
407         @Override
surfaceDestroyed(@onNull SurfaceHolder surfaceHolder)408         public void surfaceDestroyed(@NonNull SurfaceHolder surfaceHolder) {
409 
410         }
411 
waitForReady()412         public void waitForReady() throws InterruptedException {
413             assertTrue("Failed to wait for ScvhSurfaceView to get added",
414                     mReadyLatch.await(WAIT_TIMEOUT_S, TimeUnit.SECONDS));
415         }
416     }
417 
418     @Test
419     @RequiresFlagsEnabled(Flags.FLAG_SURFACE_CONTROL_INPUT_RECEIVER)
testGetHostToken()420     public void testGetHostToken() throws Throwable {
421         try (ActivityScenario<TestActivity> scenario =
422                      ActivityScenario.launch(TestActivity.class)) {
423             CountDownLatch countDownLatch = new CountDownLatch(1);
424             final ScvhSurfaceView[] scvhSurfaceView = new ScvhSurfaceView[1];
425             final View[] view = new View[1];
426             final Activity[] activity = new Activity[1];
427             scenario.onActivity(a -> {
428                 activity[0] = a;
429                 view[0] = new View(a);
430                 FrameLayout parentLayout = a.getParentLayout();
431                 scvhSurfaceView[0] = new ScvhSurfaceView(a, view[0]);
432                 parentLayout.addView(scvhSurfaceView[0]);
433 
434                 countDownLatch.countDown();
435             });
436             assertTrue("Failed to wait for activity to start",
437                     countDownLatch.await(WAIT_TIMEOUT_S, TimeUnit.SECONDS));
438 
439             final AttachedSurfaceControl attachedSurfaceControl =
440                     scvhSurfaceView[0].getRootSurfaceControl();
441             assertThat(attachedSurfaceControl.getInputTransferToken())
442                     .isNotEqualTo(null);
443         }
444     }
445 
446     /**
447      * Ensure the synced transaction is applied even if the view isn't visible and won't draw a
448      * frame.
449      */
450     @Test
testSyncTransactionViewNotVisible()451     public void testSyncTransactionViewNotVisible() throws Throwable {
452         try (ActivityScenario<TestActivity> scenario =
453                      ActivityScenario.launch(TestActivity.class)) {
454             CountDownLatch countDownLatch = new CountDownLatch(1);
455             final ScvhSurfaceView[] scvhSurfaceView = new ScvhSurfaceView[1];
456             final View[] view = new View[1];
457             final Activity[] activity = new Activity[1];
458             scenario.onActivity(a -> {
459                 activity[0] = a;
460                 view[0] = new View(a);
461                 FrameLayout parentLayout = a.getParentLayout();
462                 scvhSurfaceView[0] = new ScvhSurfaceView(a, view[0]);
463                 parentLayout.addView(scvhSurfaceView[0],
464                         new FrameLayout.LayoutParams(100, 100, Gravity.LEFT | Gravity.TOP));
465 
466                 countDownLatch.countDown();
467             });
468             assertTrue("Failed to wait for activity to start",
469                     countDownLatch.await(WAIT_TIMEOUT_S, TimeUnit.SECONDS));
470 
471             scvhSurfaceView[0].waitForReady();
472 
473             CountDownLatch committedLatch = new CountDownLatch(1);
474             activity[0].runOnUiThread(() -> {
475                 view[0].setVisibility(View.INVISIBLE);
476                 SurfaceControl.Transaction transaction = new SurfaceControl.Transaction();
477                 transaction.addTransactionCommittedListener(Runnable::run,
478                         committedLatch::countDown);
479                 view[0].getRootSurfaceControl().applyTransactionOnDraw(transaction);
480             });
481 
482             assertTrue("Failed to receive transaction committed callback for scvh with no view",
483                     committedLatch.await(WAIT_TIMEOUT_S, TimeUnit.SECONDS));
484         }
485     }
486 
487     /**
488      * Ensure the synced transaction is applied even if there was nothing new to draw
489      */
490     @Test
testSyncTransactionNothingToDraw()491     public void testSyncTransactionNothingToDraw() throws Throwable {
492         try (ActivityScenario<TestActivity> scenario =
493                      ActivityScenario.launch(TestActivity.class)) {
494             CountDownLatch countDownLatch = new CountDownLatch(1);
495             final View[] view = new View[1];
496             final Activity[] activity = new Activity[1];
497             scenario.onActivity(a -> {
498                 activity[0] = a;
499                 view[0] = new View(a);
500                 FrameLayout parentLayout = a.getParentLayout();
501                 parentLayout.addView(view[0],
502                         new FrameLayout.LayoutParams(100, 100, Gravity.LEFT | Gravity.TOP));
503 
504                 countDownLatch.countDown();
505             });
506             assertTrue("Failed to wait for activity to start",
507                     countDownLatch.await(WAIT_TIMEOUT_S, TimeUnit.SECONDS));
508 
509             CountDownLatch committedLatch = new CountDownLatch(1);
510             activity[0].runOnUiThread(() -> {
511                 SurfaceControl.Transaction transaction = new SurfaceControl.Transaction();
512                 transaction.addTransactionCommittedListener(Runnable::run,
513                         committedLatch::countDown);
514                 view[0].getRootSurfaceControl().applyTransactionOnDraw(transaction);
515                 view[0].requestLayout();
516             });
517 
518             assertTrue("Failed to receive transaction committed callback for scvh with no view",
519                     committedLatch.await(WAIT_TIMEOUT_S, TimeUnit.SECONDS));
520         }
521     }
522 }
523