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