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