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.service.dreams.IDreamManager
9 import android.testing.AndroidTestingRunner
10 import android.testing.TestableLooper
11 import android.testing.ViewUtils
12 import android.view.View
13 import android.view.ViewGroup
14 import android.view.ViewGroup.LayoutParams.MATCH_PARENT
15 import android.view.WindowManager
16 import android.widget.LinearLayout
17 import androidx.test.filters.SmallTest
18 import com.android.internal.policy.DecorView
19 import com.android.systemui.SysuiTestCase
20 import junit.framework.Assert.assertEquals
21 import junit.framework.Assert.assertFalse
22 import junit.framework.Assert.assertNotNull
23 import junit.framework.Assert.assertTrue
24 import org.junit.After
25 import org.junit.Before
26 import org.junit.Rule
27 import org.junit.Test
28 import org.junit.runner.RunWith
29 import org.mockito.Mock
30 import org.mockito.junit.MockitoJUnit
31 
32 @SmallTest
33 @RunWith(AndroidTestingRunner::class)
34 @TestableLooper.RunWithLooper
35 class DialogLaunchAnimatorTest : SysuiTestCase() {
36     private val launchAnimator = LaunchAnimator(TEST_TIMINGS, TEST_INTERPOLATORS)
37     private lateinit var dialogLaunchAnimator: DialogLaunchAnimator
38     private val attachedViews = mutableSetOf<View>()
39 
40     @Mock lateinit var dreamManager: IDreamManager
41     @get:Rule val rule = MockitoJUnit.rule()
42 
43     @Before
44     fun setUp() {
45         dialogLaunchAnimator = DialogLaunchAnimator(
46             dreamManager, launchAnimator, isForTesting = true)
47     }
48 
49     @After
50     fun tearDown() {
51         runOnMainThreadAndWaitForIdleSync {
52             attachedViews.forEach {
53                 ViewUtils.detachView(it)
54             }
55         }
56     }
57 
58     @Test
59     fun testShowDialogFromView() {
60         // Show the dialog. showFromView() must be called on the main thread with a dialog created
61         // on the main thread too.
62         val dialog = createAndShowDialog()
63 
64         assertTrue(dialog.isShowing)
65 
66         // The dialog is now fullscreen.
67         val window = dialog.window
68         val decorView = window.decorView as DecorView
69         assertEquals(MATCH_PARENT, window.attributes.width)
70         assertEquals(MATCH_PARENT, window.attributes.height)
71         assertEquals(MATCH_PARENT, decorView.layoutParams.width)
72         assertEquals(MATCH_PARENT, decorView.layoutParams.height)
73 
74         // The single DecorView child is a transparent fullscreen view that will dismiss the dialog
75         // when clicked.
76         assertEquals(1, decorView.childCount)
77         val transparentBackground = decorView.getChildAt(0) as ViewGroup
78         assertEquals(MATCH_PARENT, transparentBackground.layoutParams.width)
79         assertEquals(MATCH_PARENT, transparentBackground.layoutParams.height)
80 
81         // The single transparent background child is a fake window with the same size and
82         // background as the dialog initially had.
83         assertEquals(1, transparentBackground.childCount)
84         val dialogContentWithBackground = transparentBackground.getChildAt(0) as ViewGroup
85         assertEquals(TestDialog.DIALOG_WIDTH, dialogContentWithBackground.layoutParams.width)
86         assertEquals(TestDialog.DIALOG_HEIGHT, dialogContentWithBackground.layoutParams.height)
87         assertEquals(dialog.windowBackground, dialogContentWithBackground.background)
88 
89         // The dialog content is inside this fake window view.
90         assertNotNull(
91             dialogContentWithBackground.findViewByPredicate { it === dialog.contentView })
92 
93         // Clicking the transparent background should dismiss the dialog.
94         runOnMainThreadAndWaitForIdleSync {
95             // TODO(b/204561691): Remove this call to disableAllCurrentDialogsExitAnimations() and
96             // make sure that the test still pass on git_master/cf_x86_64_phone-userdebug in
97             // Forrest.
98             dialogLaunchAnimator.disableAllCurrentDialogsExitAnimations()
99 
100             transparentBackground.performClick()
101         }
102         assertFalse(dialog.isShowing)
103     }
104 
105     @Test
106     fun testStackedDialogsDismissesAll() {
107         val firstDialog = createAndShowDialog()
108         val secondDialog = createDialogAndShowFromDialog(firstDialog)
109 
110         assertTrue(firstDialog.isShowing)
111         assertTrue(secondDialog.isShowing)
112         runOnMainThreadAndWaitForIdleSync {
113             dialogLaunchAnimator.disableAllCurrentDialogsExitAnimations()
114             dialogLaunchAnimator.dismissStack(secondDialog)
115         }
116 
117         assertFalse(firstDialog.isShowing)
118         assertFalse(secondDialog.isShowing)
119     }
120 
121     private fun createAndShowDialog(): TestDialog {
122         return runOnMainThreadAndWaitForIdleSync {
123             val touchSurfaceRoot = LinearLayout(context)
124             val touchSurface = View(context)
125             touchSurfaceRoot.addView(touchSurface)
126 
127             // We need to attach the root to the window manager otherwise the exit animation will
128             // be skipped
129             ViewUtils.attachView(touchSurfaceRoot)
130             attachedViews.add(touchSurfaceRoot)
131 
132             val dialog = TestDialog(context)
133             dialogLaunchAnimator.showFromView(dialog, touchSurface)
134             dialog
135         }
136     }
137 
138     private fun createDialogAndShowFromDialog(animateFrom: Dialog): TestDialog {
139         return runOnMainThreadAndWaitForIdleSync {
140             val dialog = TestDialog(context)
141             dialogLaunchAnimator.showFromDialog(dialog, animateFrom)
142             dialog
143         }
144     }
145 
146     private fun <T : Any> runOnMainThreadAndWaitForIdleSync(f: () -> T): T {
147         lateinit var result: T
148         context.mainExecutor.execute {
149             result = f()
150         }
151         waitForIdleSync()
152         return result
153     }
154 
155     private class TestDialog(context: Context) : Dialog(context) {
156         companion object {
157             const val DIALOG_WIDTH = 100
158             const val DIALOG_HEIGHT = 200
159         }
160 
161         val contentView = View(context)
162         val windowBackground = ColorDrawable(Color.RED)
163 
164         init {
165             // We need to set the window type for dialogs shown by SysUI, otherwise WM will throw.
166             window.setType(WindowManager.LayoutParams.TYPE_STATUS_BAR_SUB_PANEL)
167         }
168 
169         override fun onCreate(savedInstanceState: Bundle?) {
170             super.onCreate(savedInstanceState)
171             setContentView(contentView)
172 
173             window.setLayout(DIALOG_WIDTH, DIALOG_HEIGHT)
174             window.setBackgroundDrawable(windowBackground)
175         }
176     }
177 }