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