1 /*
2  * Copyright (C) 2022 The Android Open Source Project
3  *
4  * Licensed under the Apache License, Version 2.0 (the "License");
5  * you may not use this file except in compliance with the License.
6  * You may obtain a copy of the License at
7  *
8  *      http://www.apache.org/licenses/LICENSE-2.0
9  *
10  * Unless required by applicable law or agreed to in writing, software
11  * distributed under the License is distributed on an "AS IS" BASIS,
12  * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13  * See the License for the specific language governing permissions and
14  * limitations under the License.
15  */
16 
17 package com.android.systemui.screenrecord
18 
19 import android.content.Intent
20 import android.hardware.display.DisplayManager
21 import android.hardware.display.VirtualDisplay
22 import android.hardware.display.VirtualDisplayConfig
23 import android.os.UserHandle
24 import android.platform.test.annotations.RequiresFlagsEnabled
25 import android.testing.TestableLooper
26 import android.view.View
27 import android.widget.Spinner
28 import android.widget.TextView
29 import androidx.test.ext.junit.runners.AndroidJUnit4
30 import androidx.test.filters.SmallTest
31 import com.android.systemui.Dependency
32 import com.android.systemui.SysuiTestCase
33 import com.android.systemui.animation.DialogTransitionAnimator
34 import com.android.systemui.broadcast.BroadcastDispatcher
35 import com.android.systemui.mediaprojection.MediaProjectionMetricsLogger
36 import com.android.systemui.mediaprojection.appselector.MediaProjectionAppSelectorActivity
37 import com.android.systemui.mediaprojection.permission.ENTIRE_SCREEN
38 import com.android.systemui.mediaprojection.permission.SINGLE_APP
39 import com.android.systemui.model.SysUiState
40 import com.android.systemui.plugins.ActivityStarter
41 import com.android.systemui.res.R
42 import com.android.systemui.settings.UserContextProvider
43 import com.android.systemui.statusbar.phone.SystemUIDialog
44 import com.android.systemui.statusbar.phone.SystemUIDialogManager
45 import com.android.systemui.util.mockito.argumentCaptor
46 import com.android.systemui.util.mockito.mock
47 import com.google.common.truth.Truth.assertThat
48 import com.google.common.truth.Truth.assertWithMessage
49 import junit.framework.Assert.assertEquals
50 import org.junit.After
51 import org.junit.Before
52 import org.junit.Test
53 import org.junit.runner.RunWith
54 import org.mockito.Mock
55 import org.mockito.Mockito.eq
56 import org.mockito.Mockito.never
57 import org.mockito.Mockito.verify
58 import org.mockito.MockitoAnnotations
59 
60 @SmallTest
61 @RunWith(AndroidJUnit4::class)
62 @TestableLooper.RunWithLooper(setAsMainLooper = true)
63 class ScreenRecordPermissionDialogDelegateTest : SysuiTestCase() {
64 
65     @Mock private lateinit var starter: ActivityStarter
66     @Mock private lateinit var controller: RecordingController
67     @Mock private lateinit var userContextProvider: UserContextProvider
68     @Mock private lateinit var onStartRecordingClicked: Runnable
69     @Mock private lateinit var mediaProjectionMetricsLogger: MediaProjectionMetricsLogger
70 
71     private lateinit var dialog: SystemUIDialog
72 
73     @Before
setUpnull74     fun setUp() {
75         MockitoAnnotations.initMocks(this)
76 
77         val systemUIDialogFactory =
78             SystemUIDialog.Factory(
79                 context,
80                 Dependency.get(SystemUIDialogManager::class.java),
81                 Dependency.get(SysUiState::class.java),
82                 Dependency.get(BroadcastDispatcher::class.java),
83                 Dependency.get(DialogTransitionAnimator::class.java),
84             )
85 
86         val delegate =
87             ScreenRecordPermissionDialogDelegate(
88                 UserHandle.of(0),
89                 TEST_HOST_UID,
90                 controller,
91                 starter,
92                 userContextProvider,
93                 onStartRecordingClicked,
94                 mediaProjectionMetricsLogger,
95                 systemUIDialogFactory,
96                 context,
97                 context.getSystemService(DisplayManager::class.java)!!,
98             )
99         dialog = delegate.createDialog()
100     }
101 
102     @After
teardownnull103     fun teardown() {
104         if (::dialog.isInitialized) {
105             dismissDialog()
106         }
107     }
108 
109     @Test
testShowDialog_partialScreenSharingEnabled_optionsSpinnerIsVisiblenull110     fun testShowDialog_partialScreenSharingEnabled_optionsSpinnerIsVisible() {
111         showDialog()
112 
113         val visibility = dialog.requireViewById<Spinner>(R.id.screen_share_mode_options).visibility
114         assertThat(visibility).isEqualTo(View.VISIBLE)
115     }
116 
117     @Test
testShowDialog_singleAppSelected_showTapsIsGonenull118     fun testShowDialog_singleAppSelected_showTapsIsGone() {
119         showDialog()
120         onSpinnerItemSelected(SINGLE_APP)
121 
122         val visibility = dialog.requireViewById<View>(R.id.show_taps).visibility
123         assertThat(visibility).isEqualTo(View.GONE)
124     }
125 
126     @Test
testShowDialog_entireScreenSelected_showTapsIsVisiblenull127     fun testShowDialog_entireScreenSelected_showTapsIsVisible() {
128         showDialog()
129         onSpinnerItemSelected(ENTIRE_SCREEN)
130 
131         val visibility = dialog.requireViewById<View>(R.id.show_taps).visibility
132         assertThat(visibility).isEqualTo(View.VISIBLE)
133     }
134 
135     @Test
startButtonText_entireScreenSelectednull136     fun startButtonText_entireScreenSelected() {
137         showDialog()
138 
139         onSpinnerItemSelected(ENTIRE_SCREEN)
140 
141         assertThat(getStartButton().text)
142             .isEqualTo(
143                 context.getString(R.string.screenrecord_permission_dialog_continue_entire_screen)
144             )
145     }
146 
147     @Test
startButtonText_singleAppSelectednull148     fun startButtonText_singleAppSelected() {
149         showDialog()
150 
151         onSpinnerItemSelected(SINGLE_APP)
152 
153         assertThat(getStartButton().text)
154             .isEqualTo(
155                 context.getString(
156                     R.string.media_projection_entry_generic_permission_dialog_continue_single_app
157                 )
158             )
159     }
160 
161     @Test
startClicked_singleAppSelected_passesHostUidToAppSelectornull162     fun startClicked_singleAppSelected_passesHostUidToAppSelector() {
163         showDialog()
164         onSpinnerItemSelected(SINGLE_APP)
165 
166         clickOnStart()
167 
168         assertExtraPassedToAppSelector(
169             extraKey = MediaProjectionAppSelectorActivity.EXTRA_HOST_APP_UID,
170             value = TEST_HOST_UID,
171         )
172     }
173 
174     @Test
showDialog_dialogIsShowingnull175     fun showDialog_dialogIsShowing() {
176         showDialog()
177 
178         assertThat(dialog.isShowing).isTrue()
179     }
180 
181     @Test
showDialog_singleAppIsDefaultnull182     fun showDialog_singleAppIsDefault() {
183         showDialog()
184 
185         val spinner = dialog.requireViewById<Spinner>(R.id.screen_share_mode_options)
186         val singleApp =
187             context.getString(R.string.screenrecord_permission_dialog_option_text_single_app)
188         assertEquals(spinner.adapter.getItem(0), singleApp)
189     }
190 
191     @Test
showDialog_cancelClicked_dialogIsDismissednull192     fun showDialog_cancelClicked_dialogIsDismissed() {
193         showDialog()
194 
195         clickOnCancel()
196 
197         assertThat(dialog.isShowing).isFalse()
198     }
199 
200     @Test
showDialog_cancelClickedMultipleTimes_projectionRequestCancelledIsLoggedOncenull201     fun showDialog_cancelClickedMultipleTimes_projectionRequestCancelledIsLoggedOnce() {
202         showDialog()
203 
204         clickOnCancel()
205         clickOnCancel()
206 
207         verify(mediaProjectionMetricsLogger).notifyProjectionRequestCancelled(TEST_HOST_UID)
208     }
209 
210     @Test
dismissDialog_dismissCalledMultipleTimes_projectionRequestCancelledIsLoggedOncenull211     fun dismissDialog_dismissCalledMultipleTimes_projectionRequestCancelledIsLoggedOnce() {
212         showDialog()
213 
214         dismissDialog()
215         dismissDialog()
216 
217         verify(mediaProjectionMetricsLogger).notifyProjectionRequestCancelled(TEST_HOST_UID)
218     }
219 
220     @Test
showDialog_singleAppSelected_clickOnStart_projectionRequestCancelledIsNotLoggedOncenull221     fun showDialog_singleAppSelected_clickOnStart_projectionRequestCancelledIsNotLoggedOnce() {
222         showDialog()
223         onSpinnerItemSelected(SINGLE_APP)
224 
225         clickOnStart()
226 
227         verify(mediaProjectionMetricsLogger, never())
228             .notifyProjectionRequestCancelled(TEST_HOST_UID)
229     }
230 
231     @Test
232     @RequiresFlagsEnabled(
233         com.android.media.projection.flags.Flags
234             .FLAG_MEDIA_PROJECTION_CONNECTED_DISPLAY_NO_VIRTUAL_DEVICE
235     )
doNotShowVirtualDisplayInDialognull236     fun doNotShowVirtualDisplayInDialog() {
237         val displayManager = context.getSystemService(DisplayManager::class.java)!!
238         var virtualDisplay: VirtualDisplay? = null
239         try {
240             virtualDisplay =
241                 displayManager.createVirtualDisplay(
242                     VirtualDisplayConfig.Builder("virtual display", 1, 1, 160).build()
243                 )
244             showDialog()
245             val spinner = dialog.requireViewById<Spinner>(R.id.screen_share_mode_options)
246             val adapter = spinner.adapter
247             val virtualDisplayAvailable =
248                 (0 until adapter.count)
249                     .mapNotNull { adapter.getItem(it) as? String }
250                     .any { it.contains("virtual display", ignoreCase = true) }
251             assertWithMessage("A Virtual Display was shown in the list of display to record")
252                 .that(virtualDisplayAvailable)
253                 .isFalse()
254         } finally {
255             virtualDisplay?.release()
256         }
257     }
258 
showDialognull259     private fun showDialog() {
260         dialog.show()
261     }
262 
dismissDialognull263     private fun dismissDialog() {
264         dialog.dismiss()
265     }
266 
clickOnCancelnull267     private fun clickOnCancel() {
268         dialog.requireViewById<View>(android.R.id.button2).performClick()
269     }
270 
getStartButtonnull271     private fun getStartButton() = dialog.requireViewById<TextView>(android.R.id.button1)
272 
273     private fun clickOnStart() {
274         getStartButton().performClick()
275     }
276 
onSpinnerItemSelectednull277     private fun onSpinnerItemSelected(position: Int) {
278         val spinner = dialog.requireViewById<Spinner>(R.id.screen_share_mode_options)
279         checkNotNull(spinner.onItemSelectedListener)
280             .onItemSelected(spinner, mock(), position, /* id= */ 0)
281     }
282 
assertExtraPassedToAppSelectornull283     private fun assertExtraPassedToAppSelector(extraKey: String, value: Int) {
284         val intentCaptor = argumentCaptor<Intent>()
285         verify(starter).startActivity(intentCaptor.capture(), /* dismissShade= */ eq(true))
286 
287         val intent = intentCaptor.value
288         assertThat(intent.extras!!.getInt(extraKey)).isEqualTo(value)
289     }
290 
291     companion object {
292         private const val TEST_HOST_UID = 12345
293     }
294 }
295