1 /*
2  * Copyright (C) 2021 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.media.taptotransfer.receiver
18 
19 import android.app.StatusBarManager
20 import android.content.pm.ApplicationInfo
21 import android.content.pm.PackageManager
22 import android.graphics.drawable.Drawable
23 import android.media.MediaRoute2Info
24 import android.os.Handler
25 import android.os.PowerManager
26 import android.testing.AndroidTestingRunner
27 import android.testing.TestableLooper
28 import android.view.View
29 import android.view.ViewGroup
30 import android.view.WindowManager
31 import android.view.accessibility.AccessibilityManager
32 import android.widget.ImageView
33 import androidx.test.filters.SmallTest
34 import com.android.internal.logging.InstanceId
35 import com.android.internal.logging.testing.UiEventLoggerFake
36 import com.android.systemui.R
37 import com.android.systemui.SysuiTestCase
38 import com.android.systemui.dump.DumpManager
39 import com.android.systemui.media.taptotransfer.MediaTttFlags
40 import com.android.systemui.statusbar.CommandQueue
41 import com.android.systemui.statusbar.policy.ConfigurationController
42 import com.android.systemui.temporarydisplay.TemporaryViewUiEventLogger
43 import com.android.systemui.util.concurrency.FakeExecutor
44 import com.android.systemui.util.mockito.any
45 import com.android.systemui.util.mockito.eq
46 import com.android.systemui.util.time.FakeSystemClock
47 import com.android.systemui.util.view.ViewUtil
48 import com.android.systemui.util.wakelock.WakeLockFake
49 import com.google.common.truth.Truth.assertThat
50 import org.junit.Before
51 import org.junit.Test
52 import org.junit.runner.RunWith
53 import org.mockito.ArgumentCaptor
54 import org.mockito.Mock
55 import org.mockito.Mockito.never
56 import org.mockito.Mockito.reset
57 import org.mockito.Mockito.verify
58 import org.mockito.Mockito.`when` as whenever
59 import org.mockito.MockitoAnnotations
60 
61 @SmallTest
62 @RunWith(AndroidTestingRunner::class)
63 @TestableLooper.RunWithLooper
64 class MediaTttChipControllerReceiverTest : SysuiTestCase() {
65     private lateinit var controllerReceiver: MediaTttChipControllerReceiver
66 
67     @Mock
68     private lateinit var packageManager: PackageManager
69     @Mock
70     private lateinit var applicationInfo: ApplicationInfo
71     @Mock
72     private lateinit var logger: MediaTttReceiverLogger
73     @Mock
74     private lateinit var accessibilityManager: AccessibilityManager
75     @Mock
76     private lateinit var configurationController: ConfigurationController
77     @Mock
78     private lateinit var dumpManager: DumpManager
79     @Mock
80     private lateinit var mediaTttFlags: MediaTttFlags
81     @Mock
82     private lateinit var powerManager: PowerManager
83     @Mock
84     private lateinit var viewUtil: ViewUtil
85     @Mock
86     private lateinit var windowManager: WindowManager
87     @Mock
88     private lateinit var commandQueue: CommandQueue
89     @Mock
90     private lateinit var rippleController: MediaTttReceiverRippleController
91     private lateinit var commandQueueCallback: CommandQueue.Callbacks
92     private lateinit var fakeAppIconDrawable: Drawable
93     private lateinit var uiEventLoggerFake: UiEventLoggerFake
94     private lateinit var receiverUiEventLogger: MediaTttReceiverUiEventLogger
95     private lateinit var temporaryViewUiEventLogger: TemporaryViewUiEventLogger
96     private lateinit var fakeClock: FakeSystemClock
97     private lateinit var fakeExecutor: FakeExecutor
98     private lateinit var fakeWakeLockBuilder: WakeLockFake.Builder
99     private lateinit var fakeWakeLock: WakeLockFake
100 
101     @Before
102     fun setUp() {
103         MockitoAnnotations.initMocks(this)
104         whenever(mediaTttFlags.isMediaTttEnabled()).thenReturn(true)
105         whenever(mediaTttFlags.isMediaTttReceiverSuccessRippleEnabled()).thenReturn(true)
106 
107         fakeAppIconDrawable = context.getDrawable(R.drawable.ic_cake)!!
108         whenever(packageManager.getApplicationIcon(PACKAGE_NAME)).thenReturn(fakeAppIconDrawable)
109         whenever(applicationInfo.loadLabel(packageManager)).thenReturn(APP_NAME)
110         whenever(packageManager.getApplicationInfo(
111             eq(PACKAGE_NAME), any<PackageManager.ApplicationInfoFlags>()
112         )).thenReturn(applicationInfo)
113         context.setMockPackageManager(packageManager)
114 
115         fakeClock = FakeSystemClock()
116         fakeExecutor = FakeExecutor(fakeClock)
117 
118         uiEventLoggerFake = UiEventLoggerFake()
119         receiverUiEventLogger = MediaTttReceiverUiEventLogger(uiEventLoggerFake)
120         temporaryViewUiEventLogger = TemporaryViewUiEventLogger(uiEventLoggerFake)
121 
122         fakeWakeLock = WakeLockFake()
123         fakeWakeLockBuilder = WakeLockFake.Builder(context)
124         fakeWakeLockBuilder.setWakeLock(fakeWakeLock)
125 
126         controllerReceiver = FakeMediaTttChipControllerReceiver(
127             commandQueue,
128             context,
129             logger,
130             windowManager,
131             fakeExecutor,
132             accessibilityManager,
133             configurationController,
134             dumpManager,
135             powerManager,
136             Handler.getMain(),
137             mediaTttFlags,
138             receiverUiEventLogger,
139             viewUtil,
140             fakeWakeLockBuilder,
141             fakeClock,
142             rippleController,
143             temporaryViewUiEventLogger,
144         )
145         controllerReceiver.start()
146 
147         val callbackCaptor = ArgumentCaptor.forClass(CommandQueue.Callbacks::class.java)
148         verify(commandQueue).addCallback(callbackCaptor.capture())
149         commandQueueCallback = callbackCaptor.value!!
150     }
151 
152     @Test
153     fun commandQueueCallback_flagOff_noCallbackAdded() {
154         reset(commandQueue)
155         whenever(mediaTttFlags.isMediaTttEnabled()).thenReturn(false)
156 
157         controllerReceiver = MediaTttChipControllerReceiver(
158             commandQueue,
159             context,
160             logger,
161             windowManager,
162             FakeExecutor(FakeSystemClock()),
163             accessibilityManager,
164             configurationController,
165             dumpManager,
166             powerManager,
167             Handler.getMain(),
168             mediaTttFlags,
169             receiverUiEventLogger,
170             viewUtil,
171             fakeWakeLockBuilder,
172             fakeClock,
173             rippleController,
174             temporaryViewUiEventLogger,
175         )
176         controllerReceiver.start()
177 
178         verify(commandQueue, never()).addCallback(any())
179     }
180 
181     @Test
182     fun commandQueueCallback_closeToSender_triggersChip() {
183         val appName = "FakeAppName"
184         commandQueueCallback.updateMediaTapToTransferReceiverDisplay(
185             StatusBarManager.MEDIA_TRANSFER_RECEIVER_STATE_CLOSE_TO_SENDER,
186             routeInfo,
187             /* appIcon= */ null,
188             appName
189         )
190 
191         assertThat(getChipView().getAppIconView().contentDescription).isEqualTo(appName)
192         assertThat(uiEventLoggerFake.eventId(0)).isEqualTo(
193             MediaTttReceiverUiEvents.MEDIA_TTT_RECEIVER_CLOSE_TO_SENDER.id
194         )
195         assertThat(uiEventLoggerFake.logs[0].instanceId).isNotNull()
196     }
197 
198     @Test
199     fun commandQueueCallback_farFromSender_noChipShown() {
200         commandQueueCallback.updateMediaTapToTransferReceiverDisplay(
201             StatusBarManager.MEDIA_TRANSFER_RECEIVER_STATE_FAR_FROM_SENDER,
202             routeInfo,
203             null,
204             null
205         )
206 
207         verify(windowManager, never()).addView(any(), any())
208         assertThat(uiEventLoggerFake.eventId(0)).isEqualTo(
209             MediaTttReceiverUiEvents.MEDIA_TTT_RECEIVER_FAR_FROM_SENDER.id
210         )
211         assertThat(uiEventLoggerFake.logs[0].instanceId).isNotNull()
212     }
213 
214     @Test
215     fun commandQueueCallback_transferToReceiverSucceeded_noChipShown() {
216         commandQueueCallback.updateMediaTapToTransferReceiverDisplay(
217                 StatusBarManager.MEDIA_TRANSFER_RECEIVER_STATE_TRANSFER_TO_RECEIVER_SUCCEEDED,
218                 routeInfo,
219                 null,
220                 null
221         )
222 
223         verify(windowManager, never()).addView(any(), any())
224         assertThat(uiEventLoggerFake.eventId(0)).isEqualTo(
225                 MediaTttReceiverUiEvents.MEDIA_TTT_RECEIVER_TRANSFER_TO_RECEIVER_SUCCEEDED.id
226         )
227         assertThat(uiEventLoggerFake.logs[0].instanceId).isNotNull()
228     }
229 
230     @Test
231     fun commandQueueCallback_transferToReceiverFailed_noChipShown() {
232         commandQueueCallback.updateMediaTapToTransferReceiverDisplay(
233                 StatusBarManager.MEDIA_TRANSFER_RECEIVER_STATE_TRANSFER_TO_RECEIVER_FAILED,
234                 routeInfo,
235                 null,
236                 null
237         )
238 
239         verify(windowManager, never()).addView(any(), any())
240         assertThat(uiEventLoggerFake.eventId(0)).isEqualTo(
241                 MediaTttReceiverUiEvents.MEDIA_TTT_RECEIVER_TRANSFER_TO_RECEIVER_FAILED.id
242         )
243         assertThat(uiEventLoggerFake.logs[0].instanceId).isNotNull()
244     }
245 
246     @Test
247     fun commandQueueCallback_closeThenFar_chipShownThenHidden() {
248         commandQueueCallback.updateMediaTapToTransferReceiverDisplay(
249             StatusBarManager.MEDIA_TRANSFER_RECEIVER_STATE_CLOSE_TO_SENDER,
250             routeInfo,
251             null,
252             null
253         )
254 
255         commandQueueCallback.updateMediaTapToTransferReceiverDisplay(
256             StatusBarManager.MEDIA_TRANSFER_RECEIVER_STATE_FAR_FROM_SENDER,
257             routeInfo,
258             null,
259             null
260         )
261 
262         val viewCaptor = ArgumentCaptor.forClass(View::class.java)
263         verify(windowManager).addView(viewCaptor.capture(), any())
264         verify(windowManager).removeView(viewCaptor.value)
265     }
266 
267     @Test
268     fun commandQueueCallback_closeThenSucceeded_chipShownThenHidden() {
269         commandQueueCallback.updateMediaTapToTransferReceiverDisplay(
270             StatusBarManager.MEDIA_TRANSFER_RECEIVER_STATE_CLOSE_TO_SENDER,
271             routeInfo,
272             null,
273             null
274         )
275 
276         commandQueueCallback.updateMediaTapToTransferReceiverDisplay(
277             StatusBarManager.MEDIA_TRANSFER_RECEIVER_STATE_TRANSFER_TO_RECEIVER_SUCCEEDED,
278             routeInfo,
279             null,
280             null
281         )
282 
283         val viewCaptor = ArgumentCaptor.forClass(View::class.java)
284         verify(windowManager).addView(viewCaptor.capture(), any())
285         verify(windowManager).removeView(viewCaptor.value)
286     }
287 
288     @Test
289     fun commandQueueCallback_closeThenSucceeded_sameViewInstanceId() {
290         commandQueueCallback.updateMediaTapToTransferReceiverDisplay(
291             StatusBarManager.MEDIA_TRANSFER_RECEIVER_STATE_CLOSE_TO_SENDER,
292             routeInfo,
293             null,
294             null
295         )
296 
297         commandQueueCallback.updateMediaTapToTransferReceiverDisplay(
298             StatusBarManager.MEDIA_TRANSFER_RECEIVER_STATE_TRANSFER_TO_RECEIVER_SUCCEEDED,
299             routeInfo,
300             null,
301             null
302         )
303 
304         assertThat(uiEventLoggerFake[0].instanceId).isEqualTo(uiEventLoggerFake[1].instanceId)
305     }
306 
307     @Test
308     fun commandQueueCallback_closeThenFailed_chipShownThenHidden() {
309         commandQueueCallback.updateMediaTapToTransferReceiverDisplay(
310             StatusBarManager.MEDIA_TRANSFER_RECEIVER_STATE_CLOSE_TO_SENDER,
311             routeInfo,
312             null,
313             null
314         )
315 
316         commandQueueCallback.updateMediaTapToTransferReceiverDisplay(
317             StatusBarManager.MEDIA_TRANSFER_RECEIVER_STATE_TRANSFER_TO_RECEIVER_FAILED,
318             routeInfo,
319             null,
320             null
321         )
322 
323         val viewCaptor = ArgumentCaptor.forClass(View::class.java)
324         verify(windowManager).addView(viewCaptor.capture(), any())
325         verify(windowManager).removeView(viewCaptor.value)
326     }
327 
328     @Test
329     fun commandQueueCallback_closeThenFar_wakeLockAcquiredThenReleased() {
330         commandQueueCallback.updateMediaTapToTransferReceiverDisplay(
331                 StatusBarManager.MEDIA_TRANSFER_RECEIVER_STATE_CLOSE_TO_SENDER,
332                 routeInfo,
333                 null,
334                 null
335         )
336 
337         assertThat(fakeWakeLock.isHeld).isTrue()
338 
339         commandQueueCallback.updateMediaTapToTransferReceiverDisplay(
340                 StatusBarManager.MEDIA_TRANSFER_RECEIVER_STATE_FAR_FROM_SENDER,
341                 routeInfo,
342                 null,
343                 null
344         )
345 
346         assertThat(fakeWakeLock.isHeld).isFalse()
347     }
348 
349     @Test
350     fun commandQueueCallback_closeThenFar_wakeLockNeverAcquired() {
351         commandQueueCallback.updateMediaTapToTransferReceiverDisplay(
352                 StatusBarManager.MEDIA_TRANSFER_RECEIVER_STATE_FAR_FROM_SENDER,
353                 routeInfo,
354                 null,
355                 null
356         )
357 
358         assertThat(fakeWakeLock.isHeld).isFalse()
359     }
360 
361     @Test
362     fun receivesNewStateFromCommandQueue_isLogged() {
363         commandQueueCallback.updateMediaTapToTransferReceiverDisplay(
364             StatusBarManager.MEDIA_TRANSFER_RECEIVER_STATE_CLOSE_TO_SENDER,
365             routeInfo,
366             null,
367             null
368         )
369 
370         verify(logger).logStateChange(any(), any(), any())
371     }
372 
373     @Test
374     fun updateView_noOverrides_usesInfoFromAppIcon() {
375         controllerReceiver.displayView(
376             ChipReceiverInfo(
377                 routeInfo,
378                 appIconDrawableOverride = null,
379                 appNameOverride = null,
380                 id = "id",
381                 instanceId = InstanceId.fakeInstanceId(0),
382             )
383         )
384 
385         val view = getChipView()
386         assertThat(view.getAppIconView().drawable).isEqualTo(fakeAppIconDrawable)
387         assertThat(view.getAppIconView().contentDescription)
388             .isEqualTo(context.getString(
389                 R.string.media_transfer_receiver_content_description_with_app_name,
390                 APP_NAME,
391             ))
392     }
393 
394     @Test
395     fun updateView_appIconOverride_usesOverride() {
396         val drawableOverride = context.getDrawable(R.drawable.ic_celebration)!!
397 
398         controllerReceiver.displayView(
399             ChipReceiverInfo(
400                 routeInfo,
401                 drawableOverride,
402                 appNameOverride = null,
403                 id = "id",
404                 instanceId = InstanceId.fakeInstanceId(0),
405             )
406         )
407 
408         val view = getChipView()
409         assertThat(view.getAppIconView().drawable).isEqualTo(drawableOverride)
410     }
411 
412     @Test
413     fun updateView_appNameOverride_usesOverride() {
414         val appNameOverride = "Sweet New App"
415 
416         controllerReceiver.displayView(
417             ChipReceiverInfo(
418                 routeInfo,
419                 appIconDrawableOverride = null,
420                 appNameOverride,
421                 id = "id",
422                 instanceId = InstanceId.fakeInstanceId(0),
423             )
424         )
425 
426         val view = getChipView()
427         assertThat(view.getAppIconView().contentDescription).isEqualTo(appNameOverride)
428     }
429 
430     @Test
431     fun updateView_isAppIcon_usesAppIconPadding() {
432         controllerReceiver.displayView(getChipReceiverInfo(packageName = PACKAGE_NAME))
433 
434         val chipView = getChipView()
435         assertThat(chipView.getAppIconView().paddingLeft).isEqualTo(0)
436         assertThat(chipView.getAppIconView().paddingRight).isEqualTo(0)
437         assertThat(chipView.getAppIconView().paddingTop).isEqualTo(0)
438         assertThat(chipView.getAppIconView().paddingBottom).isEqualTo(0)
439     }
440 
441     @Test
442     fun updateView_notAppIcon_usesGenericIconPadding() {
443         controllerReceiver.displayView(getChipReceiverInfo(packageName = null))
444 
445         val chipView = getChipView()
446         val expectedPadding =
447             context.resources.getDimensionPixelSize(R.dimen.media_ttt_generic_icon_padding)
448         assertThat(chipView.getAppIconView().paddingLeft).isEqualTo(expectedPadding)
449         assertThat(chipView.getAppIconView().paddingRight).isEqualTo(expectedPadding)
450         assertThat(chipView.getAppIconView().paddingTop).isEqualTo(expectedPadding)
451         assertThat(chipView.getAppIconView().paddingBottom).isEqualTo(expectedPadding)
452     }
453 
454     @Test
455     fun commandQueueCallback_invalidStateParam_noChipShown() {
456         commandQueueCallback.updateMediaTapToTransferReceiverDisplay(
457             StatusBarManager.MEDIA_TRANSFER_SENDER_STATE_TRANSFER_TO_THIS_DEVICE_SUCCEEDED,
458             routeInfo,
459             null,
460             APP_NAME
461         )
462 
463         verify(windowManager, never()).addView(any(), any())
464     }
465 
466     private fun getChipView(): ViewGroup {
467         val viewCaptor = ArgumentCaptor.forClass(View::class.java)
468         verify(windowManager).addView(viewCaptor.capture(), any())
469         return viewCaptor.value as ViewGroup
470     }
471 
472     private fun getChipReceiverInfo(packageName: String?): ChipReceiverInfo {
473         val routeInfo = MediaRoute2Info.Builder("id", "Test route name")
474             .addFeature("feature")
475             .setClientPackageName(packageName)
476             .build()
477         return ChipReceiverInfo(
478             routeInfo,
479             null,
480             null,
481             id = "id",
482             instanceId = InstanceId.fakeInstanceId(0),
483         )
484     }
485 
486     private fun ViewGroup.getAppIconView() = this.requireViewById<ImageView>(R.id.app_icon)
487 }
488 
489 private const val APP_NAME = "Fake app name"
490 private const val PACKAGE_NAME = "com.android.systemui"
491 
492 private val routeInfo = MediaRoute2Info.Builder("id", "Test route name")
493     .addFeature("feature")
494     .setClientPackageName(PACKAGE_NAME)
495     .build()
496