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