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