1 package com.android.systemui.animation 2 3 import android.app.Dialog 4 import android.content.Context 5 import android.graphics.Color 6 import android.graphics.drawable.ColorDrawable 7 import android.os.Bundle 8 import android.testing.TestableLooper 9 import android.testing.ViewUtils 10 import android.view.View 11 import android.view.ViewGroup 12 import android.view.ViewGroup.LayoutParams.MATCH_PARENT 13 import android.view.WindowManager 14 import android.widget.FrameLayout 15 import android.widget.LinearLayout 16 import androidx.test.ext.junit.runners.AndroidJUnit4 17 import androidx.test.filters.SmallTest 18 import com.android.internal.jank.Cuj 19 import com.android.internal.policy.DecorView 20 import com.android.systemui.SysuiTestCase 21 import com.android.systemui.jank.interactionJankMonitor 22 import com.android.systemui.testKosmos 23 import com.google.common.truth.Truth.assertThat 24 import junit.framework.Assert.assertEquals 25 import junit.framework.Assert.assertFalse 26 import junit.framework.Assert.assertNotNull 27 import junit.framework.Assert.assertNull 28 import junit.framework.Assert.assertTrue 29 import org.junit.After 30 import org.junit.Assert.assertNotEquals 31 import org.junit.Assert.assertThrows 32 import org.junit.Before 33 import org.junit.Rule 34 import org.junit.Test 35 import org.junit.runner.RunWith 36 import org.mockito.Mockito.any 37 import org.mockito.Mockito.verify 38 import org.mockito.junit.MockitoJUnit 39 import org.mockito.junit.MockitoRule 40 41 @SmallTest 42 @RunWith(AndroidJUnit4::class) 43 @TestableLooper.RunWithLooper 44 class DialogTransitionAnimatorTest : SysuiTestCase() { 45 private val kosmos = testKosmos() 46 private lateinit var mDialogTransitionAnimator: DialogTransitionAnimator 47 private val attachedViews = mutableSetOf<View>() 48 @get:Rule 49 val rule: MockitoRule = MockitoJUnit.rule() 50 51 @Before setUpnull52 fun setUp() { 53 mDialogTransitionAnimator = kosmos.dialogTransitionAnimator 54 } 55 56 @After tearDownnull57 fun tearDown() { 58 runOnMainThreadAndWaitForIdleSync { 59 attachedViews.forEach { 60 ViewUtils.detachView(it) 61 } 62 } 63 } 64 65 @Test testShowDialogFromViewnull66 fun testShowDialogFromView() { 67 // Show the dialog. showFromView() must be called on the main thread with a dialog created 68 // on the main thread too. 69 val dialog = createAndShowDialog() 70 71 assertTrue(dialog.isShowing) 72 73 // The dialog is now fullscreen. 74 val window = checkNotNull(dialog.window) 75 val decorView = window.decorView as DecorView 76 assertEquals(MATCH_PARENT, window.attributes.width) 77 assertEquals(MATCH_PARENT, window.attributes.height) 78 assertEquals(MATCH_PARENT, decorView.layoutParams.width) 79 assertEquals(MATCH_PARENT, decorView.layoutParams.height) 80 81 // The single DecorView child is a transparent fullscreen view that will dismiss the dialog 82 // when clicked. 83 assertEquals(1, decorView.childCount) 84 val transparentBackground = decorView.getChildAt(0) as ViewGroup 85 assertEquals(MATCH_PARENT, transparentBackground.layoutParams.width) 86 assertEquals(MATCH_PARENT, transparentBackground.layoutParams.height) 87 88 // The single transparent background child is a fake window with the same size and 89 // background as the dialog initially had. 90 assertEquals(1, transparentBackground.childCount) 91 val dialogContentWithBackground = transparentBackground.getChildAt(0) as ViewGroup 92 assertEquals(TestDialog.DIALOG_WIDTH, dialogContentWithBackground.layoutParams.width) 93 assertEquals(TestDialog.DIALOG_HEIGHT, dialogContentWithBackground.layoutParams.height) 94 assertEquals(dialog.windowBackground, dialogContentWithBackground.background) 95 96 // The dialog content is inside this fake window view. 97 assertNotNull( 98 dialogContentWithBackground.findViewByPredicate { it === dialog.contentView } 99 ) 100 101 // Clicking the transparent background should dismiss the dialog. 102 runOnMainThreadAndWaitForIdleSync { 103 transparentBackground.performClick() 104 } 105 assertFalse(dialog.isShowing) 106 } 107 108 @Test testStackedDialogsDismissesAllnull109 fun testStackedDialogsDismissesAll() { 110 val firstDialog = createAndShowDialog() 111 val secondDialog = createDialogAndShowFromDialog(firstDialog) 112 113 assertTrue(firstDialog.isShowing) 114 assertTrue(secondDialog.isShowing) 115 runOnMainThreadAndWaitForIdleSync { 116 mDialogTransitionAnimator.dismissStack(secondDialog) 117 } 118 119 assertFalse(firstDialog.isShowing) 120 assertFalse(secondDialog.isShowing) 121 } 122 123 @Test testActivityTransitionControllerFromDialognull124 fun testActivityTransitionControllerFromDialog() { 125 val firstDialog = createAndShowDialog() 126 val secondDialog = createDialogAndShowFromDialog(firstDialog) 127 128 val controller = mDialogTransitionAnimator 129 .createActivityTransitionController(secondDialog.contentView)!! 130 131 // The dialog shouldn't be dismissable during the animation. 132 runOnMainThreadAndWaitForIdleSync { 133 controller.onTransitionAnimationStart(isExpandingFullyAbove = true) 134 secondDialog.dismiss() 135 } 136 assertTrue(secondDialog.isShowing) 137 138 // Both dialogs should be dismissed at the end of the animation. 139 runOnMainThreadAndWaitForIdleSync { 140 controller.onTransitionAnimationEnd(isExpandingFullyAbove = true) 141 } 142 assertFalse(firstDialog.isShowing) 143 assertFalse(secondDialog.isShowing) 144 } 145 146 @Test testActivityLaunchFromHiddenDialognull147 fun testActivityLaunchFromHiddenDialog() { 148 val dialog = createAndShowDialog() 149 runOnMainThreadAndWaitForIdleSync { 150 dialog.hide() 151 } 152 assertNull(mDialogTransitionAnimator.createActivityTransitionController(dialog.contentView)) 153 } 154 155 @Test testActivityLaunchWhenLockedWithoutAlternateAuthnull156 fun testActivityLaunchWhenLockedWithoutAlternateAuth() { 157 val dialogTransitionAnimator = 158 fakeDialogTransitionAnimator( 159 mainExecutor = mContext.mainExecutor, 160 isUnlocked = false, 161 isShowingAlternateAuthOnUnlock = false, 162 interactionJankMonitor = kosmos.interactionJankMonitor) 163 val dialog = createAndShowDialog(dialogTransitionAnimator) 164 assertNull(dialogTransitionAnimator.createActivityTransitionController(dialog.contentView)) 165 } 166 167 @Test testActivityLaunchWhenLockedWithAlternateAuthnull168 fun testActivityLaunchWhenLockedWithAlternateAuth() { 169 val dialogTransitionAnimator = fakeDialogTransitionAnimator( 170 mainExecutor = mContext.mainExecutor, 171 isUnlocked = false, 172 isShowingAlternateAuthOnUnlock = true, 173 interactionJankMonitor = kosmos.interactionJankMonitor 174 ) 175 val dialog = createAndShowDialog(dialogTransitionAnimator) 176 assertNotNull( 177 dialogTransitionAnimator.createActivityTransitionController(dialog.contentView) 178 ) 179 } 180 181 @Test testDialogAnimationIsChangedByAnimatornull182 fun testDialogAnimationIsChangedByAnimator() { 183 // Important: the power menu animation relies on this behavior to know when to animate (see 184 // http://ag/16774605). 185 val dialog = runOnMainThreadAndWaitForIdleSync { TestDialog(context) } 186 val window = checkNotNull(dialog.window) 187 window.setWindowAnimations(0) 188 assertEquals(0, window.attributes.windowAnimations) 189 190 val touchSurface = createTouchSurface() 191 runOnMainThreadAndWaitForIdleSync { 192 mDialogTransitionAnimator.showFromView(dialog, touchSurface) 193 } 194 assertNotEquals(0, window.attributes.windowAnimations) 195 } 196 197 @Test testCujSpecificationLogsInteractionnull198 fun testCujSpecificationLogsInteraction() { 199 val touchSurface = createTouchSurface() 200 runOnMainThreadAndWaitForIdleSync { 201 val dialog = TestDialog(context) 202 mDialogTransitionAnimator.showFromView( 203 dialog, 204 touchSurface, 205 cuj = DialogCuj(Cuj.CUJ_SHADE_DIALOG_OPEN) 206 ) 207 } 208 209 verify(kosmos.interactionJankMonitor).begin(any()) 210 verify(kosmos.interactionJankMonitor).end(Cuj.CUJ_SHADE_DIALOG_OPEN) 211 } 212 213 @Test testShowFromDialogCujSpecificationLogsInteractionnull214 fun testShowFromDialogCujSpecificationLogsInteraction() { 215 val firstDialog = createAndShowDialog() 216 runOnMainThreadAndWaitForIdleSync { 217 val dialog = TestDialog(context) 218 mDialogTransitionAnimator.showFromDialog( 219 dialog, 220 firstDialog, 221 cuj = DialogCuj(Cuj.CUJ_USER_DIALOG_OPEN) 222 ) 223 dialog 224 } 225 verify(kosmos.interactionJankMonitor).begin(any()) 226 verify(kosmos.interactionJankMonitor).end(Cuj.CUJ_USER_DIALOG_OPEN) 227 } 228 229 @Test testAnimationDoesNotChangeLaunchableViewVisibility_viewVisiblenull230 fun testAnimationDoesNotChangeLaunchableViewVisibility_viewVisible() { 231 val touchSurface = createTouchSurface() 232 233 // View is VISIBLE when starting the animation. 234 runOnMainThreadAndWaitForIdleSync { touchSurface.visibility = View.VISIBLE } 235 236 // View is invisible while the dialog is shown. 237 val dialog = showDialogFromView(touchSurface) 238 assertThat(touchSurface.visibility).isEqualTo(View.INVISIBLE) 239 240 // View is visible again when the dialog is dismissed. 241 runOnMainThreadAndWaitForIdleSync { dialog.dismiss() } 242 assertThat(touchSurface.visibility).isEqualTo(View.VISIBLE) 243 } 244 245 @Test testAnimationDoesNotChangeLaunchableViewVisibility_viewInvisiblenull246 fun testAnimationDoesNotChangeLaunchableViewVisibility_viewInvisible() { 247 val touchSurface = createTouchSurface() 248 249 // View is INVISIBLE when starting the animation. 250 runOnMainThreadAndWaitForIdleSync { touchSurface.visibility = View.INVISIBLE } 251 252 // View is INVISIBLE while the dialog is shown. 253 val dialog = showDialogFromView(touchSurface) 254 assertThat(touchSurface.visibility).isEqualTo(View.INVISIBLE) 255 256 // View is invisible like it was before showing the dialog. 257 runOnMainThreadAndWaitForIdleSync { dialog.dismiss() } 258 assertThat(touchSurface.visibility).isEqualTo(View.INVISIBLE) 259 } 260 261 @Test testAnimationDoesNotChangeLaunchableViewVisibility_viewVisibleThenGonenull262 fun testAnimationDoesNotChangeLaunchableViewVisibility_viewVisibleThenGone() { 263 val touchSurface = createTouchSurface() 264 265 // View is VISIBLE when starting the animation. 266 runOnMainThreadAndWaitForIdleSync { touchSurface.visibility = View.VISIBLE } 267 268 // View is INVISIBLE while the dialog is shown. 269 val dialog = showDialogFromView(touchSurface) 270 assertThat(touchSurface.visibility).isEqualTo(View.INVISIBLE) 271 272 // Some external call makes the View GONE. It remains INVISIBLE while the dialog is shown, 273 // as all visibility changes should be blocked. 274 runOnMainThreadAndWaitForIdleSync { touchSurface.visibility = View.GONE } 275 assertThat(touchSurface.visibility).isEqualTo(View.INVISIBLE) 276 277 // View is restored to GONE once the dialog is dismissed. 278 runOnMainThreadAndWaitForIdleSync { dialog.dismiss() } 279 assertThat(touchSurface.visibility).isEqualTo(View.GONE) 280 } 281 282 @Test creatingControllerFromNormalViewThrowsnull283 fun creatingControllerFromNormalViewThrows() { 284 assertThrows(IllegalArgumentException::class.java) { 285 DialogTransitionAnimator.Controller.fromView(FrameLayout(mContext)) 286 } 287 } 288 289 @Test showFromDialogDoesNotCrashWhenShownFromRandomDialognull290 fun showFromDialogDoesNotCrashWhenShownFromRandomDialog() { 291 val dialog = createDialogAndShowFromDialog(animateFrom = TestDialog(context)) 292 dialog.dismiss() 293 } 294 createAndShowDialognull295 private fun createAndShowDialog( 296 animator: DialogTransitionAnimator = mDialogTransitionAnimator, 297 ): TestDialog { 298 val touchSurface = createTouchSurface() 299 return showDialogFromView(touchSurface, animator) 300 } 301 createTouchSurfacenull302 private fun createTouchSurface(): View { 303 return runOnMainThreadAndWaitForIdleSync { 304 val touchSurfaceRoot = LinearLayout(context) 305 val touchSurface = TouchSurfaceView(context) 306 touchSurfaceRoot.addView(touchSurface) 307 308 // We need to attach the root to the window manager otherwise the exit animation will 309 // be skipped. 310 ViewUtils.attachView(touchSurfaceRoot) 311 attachedViews.add(touchSurfaceRoot) 312 313 touchSurface 314 } 315 } 316 showDialogFromViewnull317 private fun showDialogFromView( 318 touchSurface: View, 319 animator: DialogTransitionAnimator = mDialogTransitionAnimator, 320 ): TestDialog { 321 return runOnMainThreadAndWaitForIdleSync { 322 val dialog = TestDialog(context) 323 animator.showFromView(dialog, touchSurface) 324 dialog 325 } 326 } 327 createDialogAndShowFromDialognull328 private fun createDialogAndShowFromDialog(animateFrom: Dialog): TestDialog { 329 return runOnMainThreadAndWaitForIdleSync { 330 val dialog = TestDialog(context) 331 mDialogTransitionAnimator.showFromDialog(dialog, animateFrom) 332 dialog 333 } 334 } 335 runOnMainThreadAndWaitForIdleSyncnull336 private fun <T : Any> runOnMainThreadAndWaitForIdleSync(f: () -> T): T { 337 lateinit var result: T 338 context.mainExecutor.execute { 339 result = f() 340 } 341 waitForIdleSync() 342 return result 343 } 344 345 private class TouchSurfaceView(context: Context) : FrameLayout(context), LaunchableView { 346 private val delegate = 347 LaunchableViewDelegate( 348 this, <lambda>null349 superSetVisibility = { super.setVisibility(it) }, 350 ) 351 setShouldBlockVisibilityChangesnull352 override fun setShouldBlockVisibilityChanges(block: Boolean) { 353 delegate.setShouldBlockVisibilityChanges(block) 354 } 355 setVisibilitynull356 override fun setVisibility(visibility: Int) { 357 delegate.setVisibility(visibility) 358 } 359 } 360 361 private class TestDialog(context: Context) : Dialog(context) { 362 companion object { 363 const val DIALOG_WIDTH = 100 364 const val DIALOG_HEIGHT = 200 365 } 366 367 val contentView = View(context) 368 val windowBackground = ColorDrawable(Color.RED) 369 370 init { 371 // We need to set the window type for dialogs shown by SysUI, otherwise WM will throw. 372 checkNotNull(window).setType(WindowManager.LayoutParams.TYPE_STATUS_BAR_SUB_PANEL) 373 } 374 onCreatenull375 override fun onCreate(savedInstanceState: Bundle?) { 376 super.onCreate(savedInstanceState) 377 setContentView(contentView) 378 379 val window = checkNotNull(window) 380 window.setLayout(DIALOG_WIDTH, DIALOG_HEIGHT) 381 window.setBackgroundDrawable(windowBackground) 382 } 383 } 384 } 385