1 /* <lambda>null2 * Copyright (C) 2020 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.controls.ui.controller 18 19 import android.animation.Animator 20 import android.animation.AnimatorSet 21 import android.app.PendingIntent 22 import android.app.smartspace.SmartspaceAction 23 import android.content.Context 24 import android.content.Intent 25 import android.content.pm.ApplicationInfo 26 import android.content.pm.PackageManager 27 import android.content.res.Configuration 28 import android.graphics.Bitmap 29 import android.graphics.Canvas 30 import android.graphics.Color 31 import android.graphics.Matrix 32 import android.graphics.drawable.Animatable2 33 import android.graphics.drawable.AnimatedVectorDrawable 34 import android.graphics.drawable.Drawable 35 import android.graphics.drawable.GradientDrawable 36 import android.graphics.drawable.Icon 37 import android.graphics.drawable.RippleDrawable 38 import android.graphics.drawable.TransitionDrawable 39 import android.media.MediaMetadata 40 import android.media.session.MediaSession 41 import android.media.session.PlaybackState 42 import android.os.Bundle 43 import android.platform.test.annotations.DisableFlags 44 import android.platform.test.annotations.EnableFlags 45 import android.platform.test.annotations.RequiresFlagsEnabled 46 import android.platform.test.flag.junit.DeviceFlagsValueProvider 47 import android.provider.Settings 48 import android.provider.Settings.ACTION_MEDIA_CONTROLS_SETTINGS 49 import android.testing.TestableLooper 50 import android.util.TypedValue 51 import android.view.View 52 import android.view.ViewGroup 53 import android.view.animation.Interpolator 54 import android.widget.FrameLayout 55 import android.widget.ImageButton 56 import android.widget.ImageView 57 import android.widget.SeekBar 58 import android.widget.TextView 59 import androidx.constraintlayout.widget.Barrier 60 import androidx.constraintlayout.widget.ConstraintSet 61 import androidx.lifecycle.LiveData 62 import androidx.media.utils.MediaConstants 63 import androidx.test.ext.junit.runners.AndroidJUnit4 64 import androidx.test.filters.SmallTest 65 import com.android.internal.logging.InstanceId 66 import com.android.internal.widget.CachingIconView 67 import com.android.systemui.ActivityIntentHelper 68 import com.android.systemui.Flags 69 import com.android.systemui.SysuiTestCase 70 import com.android.systemui.bluetooth.BroadcastDialogController 71 import com.android.systemui.broadcast.BroadcastSender 72 import com.android.systemui.communal.domain.interactor.CommunalSceneInteractor 73 import com.android.systemui.flags.DisableSceneContainer 74 import com.android.systemui.media.controls.MediaTestUtils 75 import com.android.systemui.media.controls.domain.pipeline.EMPTY_SMARTSPACE_MEDIA_DATA 76 import com.android.systemui.media.controls.domain.pipeline.MediaDataManager 77 import com.android.systemui.media.controls.shared.model.KEY_SMARTSPACE_APP_NAME 78 import com.android.systemui.media.controls.shared.model.MediaAction 79 import com.android.systemui.media.controls.shared.model.MediaButton 80 import com.android.systemui.media.controls.shared.model.MediaData 81 import com.android.systemui.media.controls.shared.model.MediaDeviceData 82 import com.android.systemui.media.controls.shared.model.MediaNotificationAction 83 import com.android.systemui.media.controls.shared.model.SmartspaceMediaData 84 import com.android.systemui.media.controls.ui.binder.SeekBarObserver 85 import com.android.systemui.media.controls.ui.view.GutsViewHolder 86 import com.android.systemui.media.controls.ui.view.MediaViewHolder 87 import com.android.systemui.media.controls.ui.view.RecommendationViewHolder 88 import com.android.systemui.media.controls.ui.viewmodel.SeekBarViewModel 89 import com.android.systemui.media.controls.util.MediaUiEventLogger 90 import com.android.systemui.media.dialog.MediaOutputDialogManager 91 import com.android.systemui.monet.ColorScheme 92 import com.android.systemui.monet.Style 93 import com.android.systemui.plugins.ActivityStarter 94 import com.android.systemui.plugins.FalsingManager 95 import com.android.systemui.res.R 96 import com.android.systemui.statusbar.NotificationLockscreenUserManager 97 import com.android.systemui.statusbar.policy.KeyguardStateController 98 import com.android.systemui.surfaceeffects.loadingeffect.LoadingEffectView 99 import com.android.systemui.surfaceeffects.ripple.MultiRippleView 100 import com.android.systemui.surfaceeffects.turbulencenoise.TurbulenceNoiseAnimationConfig 101 import com.android.systemui.surfaceeffects.turbulencenoise.TurbulenceNoiseView 102 import com.android.systemui.util.animation.TransitionLayout 103 import com.android.systemui.util.concurrency.FakeExecutor 104 import com.android.systemui.util.settings.GlobalSettings 105 import com.android.systemui.util.time.FakeSystemClock 106 import com.google.common.truth.Truth.assertThat 107 import dagger.Lazy 108 import junit.framework.Assert.assertTrue 109 import org.junit.After 110 import org.junit.Before 111 import org.junit.Rule 112 import org.junit.Test 113 import org.junit.runner.RunWith 114 import org.mockito.ArgumentCaptor 115 import org.mockito.ArgumentMatchers.anyInt 116 import org.mockito.ArgumentMatchers.anyLong 117 import org.mockito.Mock 118 import org.mockito.Mockito.anyString 119 import org.mockito.Mockito.mock 120 import org.mockito.Mockito.never 121 import org.mockito.Mockito.reset 122 import org.mockito.Mockito.times 123 import org.mockito.Mockito.verify 124 import org.mockito.Mockito.`when` as whenever 125 import org.mockito.junit.MockitoJUnit 126 import org.mockito.kotlin.any 127 import org.mockito.kotlin.argumentCaptor 128 import org.mockito.kotlin.eq 129 130 private const val KEY = "TEST_KEY" 131 private const val PACKAGE = "PKG" 132 private const val ARTIST = "ARTIST" 133 private const val TITLE = "TITLE" 134 private const val DEVICE_NAME = "DEVICE_NAME" 135 private const val SESSION_KEY = "SESSION_KEY" 136 private const val SESSION_ARTIST = "SESSION_ARTIST" 137 private const val SESSION_TITLE = "SESSION_TITLE" 138 private const val DISABLED_DEVICE_NAME = "DISABLED_DEVICE_NAME" 139 private const val REC_APP_NAME = "REC APP NAME" 140 private const val APP_NAME = "APP_NAME" 141 142 @SmallTest 143 @RunWith(AndroidJUnit4::class) 144 @TestableLooper.RunWithLooper(setAsMainLooper = true) 145 @DisableSceneContainer 146 public class MediaControlPanelTest : SysuiTestCase() { 147 @get:Rule val checkFlagsRule = DeviceFlagsValueProvider.createCheckFlagsRule() 148 149 private lateinit var player: MediaControlPanel 150 151 private lateinit var bgExecutor: FakeExecutor 152 private lateinit var mainExecutor: FakeExecutor 153 @Mock private lateinit var activityStarter: ActivityStarter 154 @Mock private lateinit var broadcastSender: BroadcastSender 155 156 @Mock private lateinit var gutsViewHolder: GutsViewHolder 157 @Mock private lateinit var viewHolder: MediaViewHolder 158 @Mock private lateinit var view: TransitionLayout 159 @Mock private lateinit var seekBarViewModel: SeekBarViewModel 160 @Mock private lateinit var seekBarData: LiveData<SeekBarViewModel.Progress> 161 @Mock private lateinit var mediaViewController: MediaViewController 162 @Mock private lateinit var mediaDataManager: MediaDataManager 163 @Mock private lateinit var expandedSet: ConstraintSet 164 @Mock private lateinit var collapsedSet: ConstraintSet 165 @Mock private lateinit var mediaOutputDialogManager: MediaOutputDialogManager 166 @Mock private lateinit var mediaCarouselController: MediaCarouselController 167 @Mock private lateinit var falsingManager: FalsingManager 168 @Mock private lateinit var transitionParent: ViewGroup 169 @Mock private lateinit var broadcastDialogController: BroadcastDialogController 170 private lateinit var appIcon: ImageView 171 @Mock private lateinit var albumView: ImageView 172 private lateinit var titleText: TextView 173 private lateinit var artistText: TextView 174 private lateinit var explicitIndicator: CachingIconView 175 private lateinit var seamless: ViewGroup 176 private lateinit var seamlessButton: View 177 @Mock private lateinit var seamlessBackground: RippleDrawable 178 private lateinit var seamlessIcon: ImageView 179 private lateinit var seamlessText: TextView 180 private lateinit var seekBar: SeekBar 181 private lateinit var action0: ImageButton 182 private lateinit var action1: ImageButton 183 private lateinit var action2: ImageButton 184 private lateinit var action3: ImageButton 185 private lateinit var action4: ImageButton 186 private lateinit var actionPlayPause: ImageButton 187 private lateinit var actionNext: ImageButton 188 private lateinit var actionPrev: ImageButton 189 private lateinit var scrubbingElapsedTimeView: TextView 190 private lateinit var scrubbingTotalTimeView: TextView 191 private lateinit var actionsTopBarrier: Barrier 192 @Mock private lateinit var gutsText: TextView 193 @Mock private lateinit var mockAnimator: AnimatorSet 194 private lateinit var settings: ImageButton 195 private lateinit var cancel: View 196 private lateinit var cancelText: TextView 197 private lateinit var dismiss: FrameLayout 198 private lateinit var dismissText: TextView 199 private lateinit var multiRippleView: MultiRippleView 200 private lateinit var turbulenceNoiseView: TurbulenceNoiseView 201 private lateinit var loadingEffectView: LoadingEffectView 202 203 private lateinit var session: MediaSession 204 private lateinit var device: MediaDeviceData 205 private val disabledDevice = 206 MediaDeviceData(false, null, DISABLED_DEVICE_NAME, null, showBroadcastButton = false) 207 private lateinit var mediaData: MediaData 208 private val clock = FakeSystemClock() 209 @Mock private lateinit var logger: MediaUiEventLogger 210 @Mock private lateinit var instanceId: InstanceId 211 @Mock private lateinit var packageManager: PackageManager 212 @Mock private lateinit var applicationInfo: ApplicationInfo 213 @Mock private lateinit var keyguardStateController: KeyguardStateController 214 @Mock private lateinit var activityIntentHelper: ActivityIntentHelper 215 @Mock private lateinit var lockscreenUserManager: NotificationLockscreenUserManager 216 217 @Mock private lateinit var communalSceneInteractor: CommunalSceneInteractor 218 219 @Mock private lateinit var recommendationViewHolder: RecommendationViewHolder 220 @Mock private lateinit var smartspaceAction: SmartspaceAction 221 private lateinit var smartspaceData: SmartspaceMediaData 222 @Mock private lateinit var coverContainer1: ViewGroup 223 @Mock private lateinit var coverContainer2: ViewGroup 224 @Mock private lateinit var coverContainer3: ViewGroup 225 @Mock private lateinit var recAppIconItem: CachingIconView 226 @Mock private lateinit var recCardTitle: TextView 227 @Mock private lateinit var coverItem: ImageView 228 @Mock private lateinit var matrix: Matrix 229 private lateinit var recTitle1: TextView 230 private lateinit var recTitle2: TextView 231 private lateinit var recTitle3: TextView 232 private lateinit var recSubtitle1: TextView 233 private lateinit var recSubtitle2: TextView 234 private lateinit var recSubtitle3: TextView 235 @Mock private lateinit var recProgressBar1: SeekBar 236 @Mock private lateinit var recProgressBar2: SeekBar 237 @Mock private lateinit var recProgressBar3: SeekBar 238 @Mock private lateinit var globalSettings: GlobalSettings 239 240 private val intent = 241 Intent().apply { 242 putExtras(Bundle().also { it.putString(KEY_SMARTSPACE_APP_NAME, REC_APP_NAME) }) 243 setFlags(Intent.FLAG_ACTIVITY_NEW_TASK) 244 } 245 private val pendingIntent = 246 PendingIntent.getActivity( 247 mContext, 248 0, 249 intent.setPackage(mContext.packageName), 250 PendingIntent.FLAG_MUTABLE 251 ) 252 253 @JvmField @Rule val mockito = MockitoJUnit.rule() 254 255 @Before 256 fun setUp() { 257 bgExecutor = FakeExecutor(clock) 258 mainExecutor = FakeExecutor(clock) 259 whenever(mediaViewController.expandedLayout).thenReturn(expandedSet) 260 whenever(mediaViewController.collapsedLayout).thenReturn(collapsedSet) 261 262 // Set up package manager mocks 263 val icon = context.getDrawable(R.drawable.ic_android) 264 whenever(packageManager.getApplicationIcon(anyString())).thenReturn(icon) 265 whenever(packageManager.getApplicationIcon(any<ApplicationInfo>())).thenReturn(icon) 266 whenever(packageManager.getApplicationInfo(eq(PACKAGE), anyInt())) 267 .thenReturn(applicationInfo) 268 whenever(packageManager.getApplicationLabel(any())).thenReturn(PACKAGE) 269 context.setMockPackageManager(packageManager) 270 271 player = 272 object : 273 MediaControlPanel( 274 context, 275 bgExecutor, 276 mainExecutor, 277 activityStarter, 278 broadcastSender, 279 mediaViewController, 280 seekBarViewModel, 281 Lazy { mediaDataManager }, 282 mediaOutputDialogManager, 283 mediaCarouselController, 284 falsingManager, 285 clock, 286 logger, 287 keyguardStateController, 288 activityIntentHelper, 289 communalSceneInteractor, 290 lockscreenUserManager, 291 broadcastDialogController, 292 globalSettings, 293 ) { 294 override fun loadAnimator( 295 animId: Int, 296 otionInterpolator: Interpolator, 297 vararg targets: View 298 ): AnimatorSet { 299 return mockAnimator 300 } 301 } 302 303 initGutsViewHolderMocks() 304 initMediaViewHolderMocks() 305 306 initDeviceMediaData(false, DEVICE_NAME) 307 308 // Set up recommendation view 309 initRecommendationViewHolderMocks() 310 311 // Set valid recommendation data 312 val extras = Bundle() 313 extras.putString(KEY_SMARTSPACE_APP_NAME, REC_APP_NAME) 314 val intent = 315 Intent().apply { 316 putExtras(extras) 317 setFlags(Intent.FLAG_ACTIVITY_NEW_TASK) 318 } 319 whenever(smartspaceAction.intent).thenReturn(intent) 320 whenever(smartspaceAction.extras).thenReturn(extras) 321 smartspaceData = 322 EMPTY_SMARTSPACE_MEDIA_DATA.copy( 323 packageName = PACKAGE, 324 instanceId = instanceId, 325 recommendations = listOf(smartspaceAction, smartspaceAction, smartspaceAction), 326 cardAction = smartspaceAction 327 ) 328 } 329 330 private fun initGutsViewHolderMocks() { 331 settings = ImageButton(context) 332 cancel = View(context) 333 cancelText = TextView(context) 334 dismiss = FrameLayout(context) 335 dismissText = TextView(context) 336 whenever(gutsViewHolder.gutsText).thenReturn(gutsText) 337 whenever(gutsViewHolder.settings).thenReturn(settings) 338 whenever(gutsViewHolder.cancel).thenReturn(cancel) 339 whenever(gutsViewHolder.cancelText).thenReturn(cancelText) 340 whenever(gutsViewHolder.dismiss).thenReturn(dismiss) 341 whenever(gutsViewHolder.dismissText).thenReturn(dismissText) 342 } 343 344 private fun initDeviceMediaData(shouldShowBroadcastButton: Boolean, name: String) { 345 device = 346 MediaDeviceData(true, null, name, null, showBroadcastButton = shouldShowBroadcastButton) 347 348 // Create media session 349 val metadataBuilder = 350 MediaMetadata.Builder().apply { 351 putString(MediaMetadata.METADATA_KEY_ARTIST, SESSION_ARTIST) 352 putString(MediaMetadata.METADATA_KEY_TITLE, SESSION_TITLE) 353 } 354 val playbackBuilder = 355 PlaybackState.Builder().apply { 356 setState(PlaybackState.STATE_PAUSED, 6000L, 1f) 357 setActions(PlaybackState.ACTION_PLAY) 358 } 359 session = 360 MediaSession(context, SESSION_KEY).apply { 361 setMetadata(metadataBuilder.build()) 362 setPlaybackState(playbackBuilder.build()) 363 } 364 session.setActive(true) 365 366 mediaData = 367 MediaTestUtils.emptyMediaData.copy( 368 artist = ARTIST, 369 song = TITLE, 370 packageName = PACKAGE, 371 token = session.sessionToken, 372 device = device, 373 instanceId = instanceId 374 ) 375 } 376 377 /** Initialize elements in media view holder */ 378 private fun initMediaViewHolderMocks() { 379 whenever(seekBarViewModel.progress).thenReturn(seekBarData) 380 381 // Set up mock views for the players 382 appIcon = ImageView(context) 383 titleText = TextView(context) 384 artistText = TextView(context) 385 explicitIndicator = CachingIconView(context).also { it.id = R.id.media_explicit_indicator } 386 seamless = FrameLayout(context) 387 seamless.foreground = seamlessBackground 388 seamlessButton = View(context) 389 seamlessIcon = ImageView(context) 390 seamlessText = TextView(context) 391 seekBar = SeekBar(context).also { it.id = R.id.media_progress_bar } 392 393 action0 = ImageButton(context).also { it.setId(R.id.action0) } 394 action1 = ImageButton(context).also { it.setId(R.id.action1) } 395 action2 = ImageButton(context).also { it.setId(R.id.action2) } 396 action3 = ImageButton(context).also { it.setId(R.id.action3) } 397 action4 = ImageButton(context).also { it.setId(R.id.action4) } 398 399 actionPlayPause = ImageButton(context).also { it.setId(R.id.actionPlayPause) } 400 actionPrev = ImageButton(context).also { it.setId(R.id.actionPrev) } 401 actionNext = ImageButton(context).also { it.setId(R.id.actionNext) } 402 scrubbingElapsedTimeView = 403 TextView(context).also { it.setId(R.id.media_scrubbing_elapsed_time) } 404 scrubbingTotalTimeView = 405 TextView(context).also { it.setId(R.id.media_scrubbing_total_time) } 406 407 actionsTopBarrier = 408 Barrier(context).also { 409 it.id = R.id.media_action_barrier_top 410 it.referencedIds = 411 intArrayOf( 412 actionPrev.id, 413 seekBar.id, 414 actionNext.id, 415 action0.id, 416 action1.id, 417 action2.id, 418 action3.id, 419 action4.id 420 ) 421 } 422 423 multiRippleView = MultiRippleView(context, null) 424 turbulenceNoiseView = TurbulenceNoiseView(context, null) 425 loadingEffectView = LoadingEffectView(context, null) 426 427 whenever(viewHolder.player).thenReturn(view) 428 whenever(viewHolder.appIcon).thenReturn(appIcon) 429 whenever(viewHolder.albumView).thenReturn(albumView) 430 whenever(albumView.foreground).thenReturn(mock(Drawable::class.java)) 431 whenever(viewHolder.titleText).thenReturn(titleText) 432 whenever(viewHolder.artistText).thenReturn(artistText) 433 whenever(viewHolder.explicitIndicator).thenReturn(explicitIndicator) 434 whenever(seamlessBackground.getDrawable(0)).thenReturn(mock(GradientDrawable::class.java)) 435 whenever(viewHolder.seamless).thenReturn(seamless) 436 whenever(viewHolder.seamlessButton).thenReturn(seamlessButton) 437 whenever(viewHolder.seamlessIcon).thenReturn(seamlessIcon) 438 whenever(viewHolder.seamlessText).thenReturn(seamlessText) 439 whenever(viewHolder.seekBar).thenReturn(seekBar) 440 whenever(viewHolder.scrubbingElapsedTimeView).thenReturn(scrubbingElapsedTimeView) 441 whenever(viewHolder.scrubbingTotalTimeView).thenReturn(scrubbingTotalTimeView) 442 443 whenever(viewHolder.gutsViewHolder).thenReturn(gutsViewHolder) 444 445 // Transition View 446 whenever(view.parent).thenReturn(transitionParent) 447 whenever(view.rootView).thenReturn(transitionParent) 448 449 // Action buttons 450 whenever(viewHolder.actionPlayPause).thenReturn(actionPlayPause) 451 whenever(viewHolder.getAction(R.id.actionPlayPause)).thenReturn(actionPlayPause) 452 whenever(viewHolder.actionNext).thenReturn(actionNext) 453 whenever(viewHolder.getAction(R.id.actionNext)).thenReturn(actionNext) 454 whenever(viewHolder.actionPrev).thenReturn(actionPrev) 455 whenever(viewHolder.getAction(R.id.actionPrev)).thenReturn(actionPrev) 456 whenever(viewHolder.action0).thenReturn(action0) 457 whenever(viewHolder.getAction(R.id.action0)).thenReturn(action0) 458 whenever(viewHolder.action1).thenReturn(action1) 459 whenever(viewHolder.getAction(R.id.action1)).thenReturn(action1) 460 whenever(viewHolder.action2).thenReturn(action2) 461 whenever(viewHolder.getAction(R.id.action2)).thenReturn(action2) 462 whenever(viewHolder.action3).thenReturn(action3) 463 whenever(viewHolder.getAction(R.id.action3)).thenReturn(action3) 464 whenever(viewHolder.action4).thenReturn(action4) 465 whenever(viewHolder.getAction(R.id.action4)).thenReturn(action4) 466 467 whenever(viewHolder.actionsTopBarrier).thenReturn(actionsTopBarrier) 468 469 whenever(viewHolder.multiRippleView).thenReturn(multiRippleView) 470 whenever(viewHolder.turbulenceNoiseView).thenReturn(turbulenceNoiseView) 471 whenever(viewHolder.loadingEffectView).thenReturn(loadingEffectView) 472 } 473 474 /** Initialize elements for the recommendation view holder */ 475 private fun initRecommendationViewHolderMocks() { 476 recTitle1 = TextView(context) 477 recTitle2 = TextView(context) 478 recTitle3 = TextView(context) 479 recSubtitle1 = TextView(context) 480 recSubtitle2 = TextView(context) 481 recSubtitle3 = TextView(context) 482 483 whenever(recommendationViewHolder.recommendations).thenReturn(view) 484 whenever(recommendationViewHolder.mediaAppIcons) 485 .thenReturn(listOf(recAppIconItem, recAppIconItem, recAppIconItem)) 486 whenever(recommendationViewHolder.cardTitle).thenReturn(recCardTitle) 487 whenever(recommendationViewHolder.mediaCoverItems) 488 .thenReturn(listOf(coverItem, coverItem, coverItem)) 489 whenever(recommendationViewHolder.mediaCoverContainers) 490 .thenReturn(listOf(coverContainer1, coverContainer2, coverContainer3)) 491 whenever(recommendationViewHolder.mediaTitles) 492 .thenReturn(listOf(recTitle1, recTitle2, recTitle3)) 493 whenever(recommendationViewHolder.mediaSubtitles) 494 .thenReturn(listOf(recSubtitle1, recSubtitle2, recSubtitle3)) 495 whenever(recommendationViewHolder.mediaProgressBars) 496 .thenReturn(listOf(recProgressBar1, recProgressBar2, recProgressBar3)) 497 whenever(coverItem.imageMatrix).thenReturn(matrix) 498 499 // set ids for recommendation containers 500 whenever(coverContainer1.id).thenReturn(1) 501 whenever(coverContainer2.id).thenReturn(2) 502 whenever(coverContainer3.id).thenReturn(3) 503 504 whenever(recommendationViewHolder.gutsViewHolder).thenReturn(gutsViewHolder) 505 506 val actionIcon = Icon.createWithResource(context, R.drawable.ic_android) 507 whenever(smartspaceAction.icon).thenReturn(actionIcon) 508 509 // Needed for card and item action click 510 val mockContext = mock(Context::class.java) 511 whenever(view.context).thenReturn(mockContext) 512 whenever(coverContainer1.context).thenReturn(mockContext) 513 whenever(coverContainer2.context).thenReturn(mockContext) 514 whenever(coverContainer3.context).thenReturn(mockContext) 515 } 516 517 @After 518 fun tearDown() { 519 session.release() 520 player.onDestroy() 521 } 522 523 @Test 524 fun bindWhenUnattached() { 525 val state = mediaData.copy(token = null) 526 player.bindPlayer(state, PACKAGE) 527 assertThat(player.isPlaying()).isFalse() 528 } 529 530 @Test 531 fun bindSemanticActions() { 532 val icon = context.getDrawable(android.R.drawable.ic_media_play) 533 val bg = context.getDrawable(R.drawable.qs_media_round_button_background) 534 val semanticActions = 535 MediaButton( 536 playOrPause = MediaAction(icon, Runnable {}, "play", bg), 537 nextOrCustom = MediaAction(icon, Runnable {}, "next", bg), 538 custom0 = MediaAction(icon, null, "custom 0", bg), 539 custom1 = MediaAction(icon, null, "custom 1", bg) 540 ) 541 val state = mediaData.copy(semanticActions = semanticActions) 542 player.attachPlayer(viewHolder) 543 player.bindPlayer(state, PACKAGE) 544 545 assertThat(actionPrev.isEnabled()).isFalse() 546 assertThat(actionPrev.drawable).isNull() 547 verify(collapsedSet).setVisibility(R.id.actionPrev, ConstraintSet.GONE) 548 549 assertThat(actionPlayPause.isEnabled()).isTrue() 550 assertThat(actionPlayPause.contentDescription).isEqualTo("play") 551 verify(collapsedSet).setVisibility(R.id.actionPlayPause, ConstraintSet.VISIBLE) 552 553 assertThat(actionNext.isEnabled()).isTrue() 554 assertThat(actionNext.isFocusable()).isTrue() 555 assertThat(actionNext.isClickable()).isTrue() 556 assertThat(actionNext.contentDescription).isEqualTo("next") 557 verify(collapsedSet).setVisibility(R.id.actionNext, ConstraintSet.VISIBLE) 558 559 // Called twice since these IDs are used as generic buttons 560 assertThat(action0.contentDescription).isEqualTo("custom 0") 561 assertThat(action0.isEnabled()).isFalse() 562 verify(collapsedSet, times(2)).setVisibility(R.id.action0, ConstraintSet.GONE) 563 564 assertThat(action1.contentDescription).isEqualTo("custom 1") 565 assertThat(action1.isEnabled()).isFalse() 566 verify(collapsedSet, times(2)).setVisibility(R.id.action1, ConstraintSet.GONE) 567 568 // Verify generic buttons are hidden 569 verify(collapsedSet).setVisibility(R.id.action2, ConstraintSet.GONE) 570 verify(expandedSet).setVisibility(R.id.action2, ConstraintSet.GONE) 571 572 verify(collapsedSet).setVisibility(R.id.action3, ConstraintSet.GONE) 573 verify(expandedSet).setVisibility(R.id.action3, ConstraintSet.GONE) 574 575 verify(collapsedSet).setVisibility(R.id.action4, ConstraintSet.GONE) 576 verify(expandedSet).setVisibility(R.id.action4, ConstraintSet.GONE) 577 } 578 579 @Test 580 fun bindSemanticActions_reservedPrev() { 581 val icon = context.getDrawable(android.R.drawable.ic_media_play) 582 val bg = context.getDrawable(R.drawable.qs_media_round_button_background) 583 584 // Setup button state: no prev or next button and their slots reserved 585 val semanticActions = 586 MediaButton( 587 playOrPause = MediaAction(icon, Runnable {}, "play", bg), 588 nextOrCustom = null, 589 prevOrCustom = null, 590 custom0 = MediaAction(icon, null, "custom 0", bg), 591 custom1 = MediaAction(icon, null, "custom 1", bg), 592 false, 593 true 594 ) 595 val state = mediaData.copy(semanticActions = semanticActions) 596 597 player.attachPlayer(viewHolder) 598 player.bindPlayer(state, PACKAGE) 599 600 assertThat(actionPrev.isEnabled()).isFalse() 601 assertThat(actionPrev.drawable).isNull() 602 assertThat(actionPrev.isFocusable()).isFalse() 603 assertThat(actionPrev.isClickable()).isFalse() 604 verify(expandedSet).setVisibility(R.id.actionPrev, ConstraintSet.INVISIBLE) 605 606 assertThat(actionNext.isEnabled()).isFalse() 607 assertThat(actionNext.drawable).isNull() 608 verify(expandedSet).setVisibility(R.id.actionNext, ConstraintSet.GONE) 609 } 610 611 @Test 612 fun bindSemanticActions_reservedNext() { 613 val icon = context.getDrawable(android.R.drawable.ic_media_play) 614 val bg = context.getDrawable(R.drawable.qs_media_round_button_background) 615 616 // Setup button state: no prev or next button and their slots reserved 617 val semanticActions = 618 MediaButton( 619 playOrPause = MediaAction(icon, Runnable {}, "play", bg), 620 nextOrCustom = null, 621 prevOrCustom = null, 622 custom0 = MediaAction(icon, null, "custom 0", bg), 623 custom1 = MediaAction(icon, null, "custom 1", bg), 624 true, 625 false 626 ) 627 val state = mediaData.copy(semanticActions = semanticActions) 628 629 player.attachPlayer(viewHolder) 630 player.bindPlayer(state, PACKAGE) 631 632 assertThat(actionPrev.isEnabled()).isFalse() 633 assertThat(actionPrev.drawable).isNull() 634 verify(expandedSet).setVisibility(R.id.actionPrev, ConstraintSet.GONE) 635 636 assertThat(actionNext.isEnabled()).isFalse() 637 assertThat(actionNext.drawable).isNull() 638 assertThat(actionNext.isFocusable()).isFalse() 639 assertThat(actionNext.isClickable()).isFalse() 640 verify(expandedSet).setVisibility(R.id.actionNext, ConstraintSet.INVISIBLE) 641 } 642 643 @Test 644 fun bindAlbumView_testHardwareAfterAttach() { 645 player.attachPlayer(viewHolder) 646 647 verify(albumView).setLayerType(View.LAYER_TYPE_HARDWARE, null) 648 } 649 650 @Test 651 fun bindAlbumView_artUsesResource() { 652 val albumArt = Icon.createWithResource(context, R.drawable.ic_android) 653 val state = mediaData.copy(artwork = albumArt) 654 655 player.attachPlayer(viewHolder) 656 player.bindPlayer(state, PACKAGE) 657 bgExecutor.runAllReady() 658 mainExecutor.runAllReady() 659 660 verify(albumView).setImageDrawable(any<Drawable>()) 661 } 662 663 @Test 664 fun bindAlbumView_setAfterExecutors() { 665 val albumArt = getColorIcon(Color.RED) 666 val state = mediaData.copy(artwork = albumArt) 667 668 player.attachPlayer(viewHolder) 669 player.bindPlayer(state, PACKAGE) 670 bgExecutor.runAllReady() 671 mainExecutor.runAllReady() 672 673 verify(albumView).setImageDrawable(any<Drawable>()) 674 } 675 676 @Test 677 fun bindAlbumView_bitmapInLaterStates_setAfterExecutors() { 678 val redArt = getColorIcon(Color.RED) 679 val greenArt = getColorIcon(Color.GREEN) 680 681 val state0 = mediaData.copy(artwork = null) 682 val state1 = mediaData.copy(artwork = redArt) 683 val state2 = mediaData.copy(artwork = redArt) 684 val state3 = mediaData.copy(artwork = greenArt) 685 player.attachPlayer(viewHolder) 686 687 // First binding sets (empty) drawable 688 player.bindPlayer(state0, PACKAGE) 689 bgExecutor.runAllReady() 690 mainExecutor.runAllReady() 691 verify(albumView).setImageDrawable(any<Drawable>()) 692 693 // Run Metadata update so that later states don't update 694 val captor = argumentCaptor<Animator.AnimatorListener>() 695 verify(mockAnimator, times(2)).addListener(captor.capture()) 696 captor.lastValue.onAnimationEnd(mockAnimator) 697 assertThat(titleText.getText()).isEqualTo(TITLE) 698 assertThat(artistText.getText()).isEqualTo(ARTIST) 699 700 // Second binding sets transition drawable 701 player.bindPlayer(state1, PACKAGE) 702 bgExecutor.runAllReady() 703 mainExecutor.runAllReady() 704 val drawableCaptor = argumentCaptor<Drawable>() 705 verify(albumView, times(2)).setImageDrawable(drawableCaptor.capture()) 706 assertTrue(drawableCaptor.allValues[1] is TransitionDrawable) 707 708 // Third binding doesn't run transition or update background 709 player.bindPlayer(state2, PACKAGE) 710 bgExecutor.runAllReady() 711 mainExecutor.runAllReady() 712 verify(albumView, times(2)).setImageDrawable(any<Drawable>()) 713 714 // Fourth binding to new image runs transition due to color scheme change 715 player.bindPlayer(state3, PACKAGE) 716 bgExecutor.runAllReady() 717 mainExecutor.runAllReady() 718 verify(albumView, times(3)).setImageDrawable(any<Drawable>()) 719 } 720 721 @Test 722 fun addTwoPlayerGradients_differentStates() { 723 // Setup redArtwork and its color scheme. 724 val redArt = getColorIcon(Color.RED) 725 val redWallpaperColor = player.getWallpaperColor(redArt) 726 val redColorScheme = ColorScheme(redWallpaperColor, true, Style.CONTENT) 727 728 // Setup greenArt and its color scheme. 729 val greenArt = getColorIcon(Color.GREEN) 730 val greenWallpaperColor = player.getWallpaperColor(greenArt) 731 val greenColorScheme = ColorScheme(greenWallpaperColor, true, Style.CONTENT) 732 733 // Add gradient to both icons. 734 val redArtwork = player.addGradientToPlayerAlbum(redArt, redColorScheme, 10, 10) 735 val greenArtwork = player.addGradientToPlayerAlbum(greenArt, greenColorScheme, 10, 10) 736 737 // They should have different constant states as they have different gradient color. 738 assertThat(redArtwork.getDrawable(1).constantState) 739 .isNotEqualTo(greenArtwork.getDrawable(1).constantState) 740 } 741 742 @Test 743 fun getWallpaperColor_recycledBitmap_notCrashing() { 744 // Setup redArt icon. 745 val redBmp = Bitmap.createBitmap(10, 10, Bitmap.Config.ARGB_8888) 746 val redArt = Icon.createWithBitmap(redBmp) 747 748 // Recycle bitmap of redArt icon. 749 redArt.bitmap.recycle() 750 751 // get wallpaperColor without illegal exception. 752 player.getWallpaperColor(redArt) 753 } 754 755 @Test 756 fun bind_seekBarDisabled_hasActions_seekBarVisibilityIsSetToInvisible() { 757 useRealConstraintSets() 758 759 val icon = context.getDrawable(android.R.drawable.ic_media_play) 760 val semanticActions = 761 MediaButton( 762 playOrPause = MediaAction(icon, Runnable {}, "play", null), 763 nextOrCustom = MediaAction(icon, Runnable {}, "next", null) 764 ) 765 val state = mediaData.copy(semanticActions = semanticActions) 766 767 player.attachPlayer(viewHolder) 768 getEnabledChangeListener().onEnabledChanged(enabled = false) 769 770 player.bindPlayer(state, PACKAGE) 771 772 assertThat(expandedSet.getVisibility(seekBar.id)).isEqualTo(ConstraintSet.INVISIBLE) 773 } 774 775 @Test 776 fun bind_seekBarDisabled_noActions_seekBarVisibilityIsSetToInvisible() { 777 useRealConstraintSets() 778 779 val state = mediaData.copy(semanticActions = MediaButton()) 780 player.attachPlayer(viewHolder) 781 getEnabledChangeListener().onEnabledChanged(enabled = false) 782 783 player.bindPlayer(state, PACKAGE) 784 785 assertThat(expandedSet.getVisibility(seekBar.id)).isEqualTo(ConstraintSet.INVISIBLE) 786 } 787 788 @Test 789 fun bind_seekBarEnabled_seekBarVisible() { 790 useRealConstraintSets() 791 792 val state = mediaData.copy(semanticActions = MediaButton()) 793 player.attachPlayer(viewHolder) 794 getEnabledChangeListener().onEnabledChanged(enabled = true) 795 796 player.bindPlayer(state, PACKAGE) 797 798 assertThat(expandedSet.getVisibility(seekBar.id)).isEqualTo(ConstraintSet.VISIBLE) 799 } 800 801 @Test 802 fun seekBarChangesToEnabledAfterBind_seekBarChangesToVisible() { 803 useRealConstraintSets() 804 805 val state = mediaData.copy(semanticActions = MediaButton()) 806 player.attachPlayer(viewHolder) 807 player.bindPlayer(state, PACKAGE) 808 809 getEnabledChangeListener().onEnabledChanged(enabled = true) 810 811 assertThat(expandedSet.getVisibility(seekBar.id)).isEqualTo(ConstraintSet.VISIBLE) 812 } 813 814 @Test 815 fun seekBarChangesToDisabledAfterBind_noActions_seekBarChangesToInvisible() { 816 useRealConstraintSets() 817 818 val state = mediaData.copy(semanticActions = MediaButton()) 819 820 player.attachPlayer(viewHolder) 821 getEnabledChangeListener().onEnabledChanged(enabled = true) 822 player.bindPlayer(state, PACKAGE) 823 824 getEnabledChangeListener().onEnabledChanged(enabled = false) 825 826 assertThat(expandedSet.getVisibility(seekBar.id)).isEqualTo(ConstraintSet.INVISIBLE) 827 } 828 829 @Test 830 fun seekBarChangesToDisabledAfterBind_hasActions_seekBarChangesToInvisible() { 831 useRealConstraintSets() 832 833 val icon = context.getDrawable(android.R.drawable.ic_media_play) 834 val semanticActions = 835 MediaButton(nextOrCustom = MediaAction(icon, Runnable {}, "next", null)) 836 val state = mediaData.copy(semanticActions = semanticActions) 837 838 player.attachPlayer(viewHolder) 839 getEnabledChangeListener().onEnabledChanged(enabled = true) 840 player.bindPlayer(state, PACKAGE) 841 842 getEnabledChangeListener().onEnabledChanged(enabled = false) 843 844 assertThat(expandedSet.getVisibility(seekBar.id)).isEqualTo(ConstraintSet.INVISIBLE) 845 } 846 847 @Test 848 fun bind_notScrubbing_scrubbingViewsGone() { 849 val icon = context.getDrawable(android.R.drawable.ic_media_play) 850 val semanticActions = 851 MediaButton( 852 prevOrCustom = MediaAction(icon, {}, "prev", null), 853 nextOrCustom = MediaAction(icon, {}, "next", null) 854 ) 855 val state = mediaData.copy(semanticActions = semanticActions) 856 857 player.attachPlayer(viewHolder) 858 player.bindPlayer(state, PACKAGE) 859 860 verify(expandedSet).setVisibility(R.id.media_scrubbing_elapsed_time, ConstraintSet.GONE) 861 verify(expandedSet).setVisibility(R.id.media_scrubbing_total_time, ConstraintSet.GONE) 862 } 863 864 @Test 865 fun setIsScrubbing_noSemanticActions_viewsNotChanged() { 866 val state = mediaData.copy(semanticActions = null) 867 player.attachPlayer(viewHolder) 868 player.bindPlayer(state, PACKAGE) 869 reset(expandedSet) 870 871 val listener = getScrubbingChangeListener() 872 873 listener.onScrubbingChanged(true) 874 mainExecutor.runAllReady() 875 876 verify(expandedSet, never()).setVisibility(eq(R.id.actionPrev), anyInt()) 877 verify(expandedSet, never()).setVisibility(eq(R.id.actionNext), anyInt()) 878 verify(expandedSet, never()).setVisibility(eq(R.id.media_scrubbing_elapsed_time), anyInt()) 879 verify(expandedSet, never()).setVisibility(eq(R.id.media_scrubbing_total_time), anyInt()) 880 } 881 882 @Test 883 fun setIsScrubbing_noPrevButton_scrubbingTimesNotShown() { 884 val icon = context.getDrawable(android.R.drawable.ic_media_play) 885 val semanticActions = 886 MediaButton(prevOrCustom = null, nextOrCustom = MediaAction(icon, {}, "next", null)) 887 val state = mediaData.copy(semanticActions = semanticActions) 888 player.attachPlayer(viewHolder) 889 player.bindPlayer(state, PACKAGE) 890 reset(expandedSet) 891 892 getScrubbingChangeListener().onScrubbingChanged(true) 893 mainExecutor.runAllReady() 894 895 verify(expandedSet).setVisibility(R.id.actionNext, View.VISIBLE) 896 verify(expandedSet).setVisibility(R.id.media_scrubbing_elapsed_time, View.GONE) 897 verify(expandedSet).setVisibility(R.id.media_scrubbing_total_time, View.GONE) 898 } 899 900 @Test 901 fun setIsScrubbing_noNextButton_scrubbingTimesNotShown() { 902 val icon = context.getDrawable(android.R.drawable.ic_media_play) 903 val semanticActions = 904 MediaButton(prevOrCustom = MediaAction(icon, {}, "prev", null), nextOrCustom = null) 905 val state = mediaData.copy(semanticActions = semanticActions) 906 player.attachPlayer(viewHolder) 907 player.bindPlayer(state, PACKAGE) 908 reset(expandedSet) 909 910 getScrubbingChangeListener().onScrubbingChanged(true) 911 mainExecutor.runAllReady() 912 913 verify(expandedSet).setVisibility(R.id.actionPrev, View.VISIBLE) 914 verify(expandedSet).setVisibility(R.id.media_scrubbing_elapsed_time, View.GONE) 915 verify(expandedSet).setVisibility(R.id.media_scrubbing_total_time, View.GONE) 916 } 917 918 @Test 919 fun setIsScrubbing_true_scrubbingViewsShownAndPrevNextHiddenOnlyInExpanded() { 920 val icon = context.getDrawable(android.R.drawable.ic_media_play) 921 val semanticActions = 922 MediaButton( 923 prevOrCustom = MediaAction(icon, {}, "prev", null), 924 nextOrCustom = MediaAction(icon, {}, "next", null) 925 ) 926 val state = mediaData.copy(semanticActions = semanticActions) 927 player.attachPlayer(viewHolder) 928 player.bindPlayer(state, PACKAGE) 929 reset(expandedSet) 930 931 getScrubbingChangeListener().onScrubbingChanged(true) 932 mainExecutor.runAllReady() 933 934 // Only in expanded, we should show the scrubbing times and hide prev+next 935 verify(expandedSet).setVisibility(R.id.media_scrubbing_elapsed_time, ConstraintSet.VISIBLE) 936 verify(expandedSet).setVisibility(R.id.media_scrubbing_total_time, ConstraintSet.VISIBLE) 937 verify(expandedSet).setVisibility(R.id.actionPrev, ConstraintSet.GONE) 938 verify(expandedSet).setVisibility(R.id.actionNext, ConstraintSet.GONE) 939 } 940 941 @Test 942 fun setIsScrubbing_trueThenFalse_scrubbingTimeGoneAtEnd() { 943 val icon = context.getDrawable(android.R.drawable.ic_media_play) 944 val semanticActions = 945 MediaButton( 946 prevOrCustom = MediaAction(icon, {}, "prev", null), 947 nextOrCustom = MediaAction(icon, {}, "next", null) 948 ) 949 val state = mediaData.copy(semanticActions = semanticActions) 950 951 player.attachPlayer(viewHolder) 952 player.bindPlayer(state, PACKAGE) 953 954 getScrubbingChangeListener().onScrubbingChanged(true) 955 mainExecutor.runAllReady() 956 reset(expandedSet) 957 958 getScrubbingChangeListener().onScrubbingChanged(false) 959 mainExecutor.runAllReady() 960 961 // Only in expanded, we should hide the scrubbing times and show prev+next 962 verify(expandedSet).setVisibility(R.id.media_scrubbing_elapsed_time, ConstraintSet.GONE) 963 verify(expandedSet).setVisibility(R.id.media_scrubbing_total_time, ConstraintSet.GONE) 964 verify(expandedSet).setVisibility(R.id.actionPrev, ConstraintSet.VISIBLE) 965 verify(expandedSet).setVisibility(R.id.actionNext, ConstraintSet.VISIBLE) 966 } 967 968 @Test 969 fun bind_resumeState_withProgress() { 970 val progress = 0.5 971 val state = mediaData.copy(resumption = true, resumeProgress = progress) 972 973 player.attachPlayer(viewHolder) 974 player.bindPlayer(state, PACKAGE) 975 976 verify(seekBarViewModel).updateStaticProgress(progress) 977 } 978 979 @Test 980 fun animationSettingChange_updateSeekbar() { 981 // When animations are enabled 982 globalSettings.putFloat(Settings.Global.ANIMATOR_DURATION_SCALE, 1f) 983 val progress = 0.5 984 val state = mediaData.copy(resumption = true, resumeProgress = progress) 985 player.attachPlayer(viewHolder) 986 player.bindPlayer(state, PACKAGE) 987 988 val captor = argumentCaptor<SeekBarObserver>() 989 verify(seekBarData).observeForever(captor.capture()) 990 val seekBarObserver = captor.lastValue 991 992 // Then the seekbar is set to animate 993 assertThat(seekBarObserver.animationEnabled).isTrue() 994 995 // When the setting changes, 996 globalSettings.putFloat(Settings.Global.ANIMATOR_DURATION_SCALE, 0f) 997 player.updateAnimatorDurationScale() 998 999 // Then the seekbar is set to not animate 1000 assertThat(seekBarObserver.animationEnabled).isFalse() 1001 } 1002 1003 @Test 1004 fun bindNotificationActions() { 1005 val icon = context.getDrawable(android.R.drawable.ic_media_play) 1006 val actions = 1007 listOf( 1008 MediaNotificationAction(true, actionIntent = pendingIntent, icon, "previous"), 1009 MediaNotificationAction(true, actionIntent = pendingIntent, icon, "play"), 1010 MediaNotificationAction(true, actionIntent = null, icon, "next"), 1011 MediaNotificationAction(true, actionIntent = null, icon, "custom 0"), 1012 MediaNotificationAction(true, actionIntent = pendingIntent, icon, "custom 1") 1013 ) 1014 val state = 1015 mediaData.copy( 1016 actions = actions, 1017 actionsToShowInCompact = listOf(1, 2), 1018 semanticActions = null 1019 ) 1020 1021 player.attachPlayer(viewHolder) 1022 player.bindPlayer(state, PACKAGE) 1023 1024 // Verify semantic actions are hidden 1025 verify(collapsedSet).setVisibility(R.id.actionPrev, ConstraintSet.GONE) 1026 verify(expandedSet).setVisibility(R.id.actionPrev, ConstraintSet.GONE) 1027 1028 verify(collapsedSet).setVisibility(R.id.actionPlayPause, ConstraintSet.GONE) 1029 verify(expandedSet).setVisibility(R.id.actionPlayPause, ConstraintSet.GONE) 1030 1031 verify(collapsedSet).setVisibility(R.id.actionNext, ConstraintSet.GONE) 1032 verify(expandedSet).setVisibility(R.id.actionNext, ConstraintSet.GONE) 1033 1034 // Generic actions all enabled 1035 assertThat(action0.contentDescription).isEqualTo("previous") 1036 assertThat(action0.isEnabled()).isTrue() 1037 verify(collapsedSet).setVisibility(R.id.action0, ConstraintSet.GONE) 1038 1039 assertThat(action1.contentDescription).isEqualTo("play") 1040 assertThat(action1.isEnabled()).isTrue() 1041 verify(collapsedSet).setVisibility(R.id.action1, ConstraintSet.VISIBLE) 1042 1043 assertThat(action2.contentDescription).isEqualTo("next") 1044 assertThat(action2.isEnabled()).isFalse() 1045 verify(collapsedSet).setVisibility(R.id.action2, ConstraintSet.VISIBLE) 1046 1047 assertThat(action3.contentDescription).isEqualTo("custom 0") 1048 assertThat(action3.isEnabled()).isFalse() 1049 verify(collapsedSet).setVisibility(R.id.action3, ConstraintSet.GONE) 1050 1051 assertThat(action4.contentDescription).isEqualTo("custom 1") 1052 assertThat(action4.isEnabled()).isTrue() 1053 verify(collapsedSet).setVisibility(R.id.action4, ConstraintSet.GONE) 1054 } 1055 1056 @Test 1057 fun bindAnimatedSemanticActions() { 1058 val mockAvd0 = mock(AnimatedVectorDrawable::class.java) 1059 val mockAvd1 = mock(AnimatedVectorDrawable::class.java) 1060 val mockAvd2 = mock(AnimatedVectorDrawable::class.java) 1061 whenever(mockAvd0.mutate()).thenReturn(mockAvd0) 1062 whenever(mockAvd1.mutate()).thenReturn(mockAvd1) 1063 whenever(mockAvd2.mutate()).thenReturn(mockAvd2) 1064 1065 val icon = context.getDrawable(R.drawable.ic_media_play) 1066 val bg = context.getDrawable(R.drawable.ic_media_play_container) 1067 val semanticActions0 = 1068 MediaButton(playOrPause = MediaAction(mockAvd0, Runnable {}, "play", null)) 1069 val semanticActions1 = 1070 MediaButton(playOrPause = MediaAction(mockAvd1, Runnable {}, "pause", null)) 1071 val semanticActions2 = 1072 MediaButton(playOrPause = MediaAction(mockAvd2, Runnable {}, "loading", null)) 1073 val state0 = mediaData.copy(semanticActions = semanticActions0) 1074 val state1 = mediaData.copy(semanticActions = semanticActions1) 1075 val state2 = mediaData.copy(semanticActions = semanticActions2) 1076 1077 player.attachPlayer(viewHolder) 1078 player.bindPlayer(state0, PACKAGE) 1079 1080 // Validate first binding 1081 assertThat(actionPlayPause.isEnabled()).isTrue() 1082 assertThat(actionPlayPause.contentDescription).isEqualTo("play") 1083 assertThat(actionPlayPause.getBackground()).isNull() 1084 verify(collapsedSet).setVisibility(R.id.actionPlayPause, ConstraintSet.VISIBLE) 1085 assertThat(actionPlayPause.hasOnClickListeners()).isTrue() 1086 1087 // Trigger animation & update mock 1088 actionPlayPause.performClick() 1089 verify(mockAvd0, times(1)).start() 1090 whenever(mockAvd0.isRunning()).thenReturn(true) 1091 1092 // Validate states no longer bind 1093 player.bindPlayer(state1, PACKAGE) 1094 player.bindPlayer(state2, PACKAGE) 1095 assertThat(actionPlayPause.contentDescription).isEqualTo("play") 1096 1097 // Complete animation and run callbacks 1098 whenever(mockAvd0.isRunning()).thenReturn(false) 1099 val captor = ArgumentCaptor.forClass(Animatable2.AnimationCallback::class.java) 1100 verify(mockAvd0, times(1)).registerAnimationCallback(captor.capture()) 1101 verify(mockAvd1, never()).registerAnimationCallback(any<Animatable2.AnimationCallback>()) 1102 verify(mockAvd2, never()).registerAnimationCallback(any<Animatable2.AnimationCallback>()) 1103 captor.getValue().onAnimationEnd(mockAvd0) 1104 1105 // Validate correct state was bound 1106 assertThat(actionPlayPause.contentDescription).isEqualTo("loading") 1107 assertThat(actionPlayPause.getBackground()).isNull() 1108 verify(mockAvd0, times(1)).registerAnimationCallback(any<Animatable2.AnimationCallback>()) 1109 verify(mockAvd1, times(1)).registerAnimationCallback(any<Animatable2.AnimationCallback>()) 1110 verify(mockAvd2, times(1)).registerAnimationCallback(any<Animatable2.AnimationCallback>()) 1111 verify(mockAvd0, times(1)).unregisterAnimationCallback(any<Animatable2.AnimationCallback>()) 1112 verify(mockAvd1, times(1)).unregisterAnimationCallback(any<Animatable2.AnimationCallback>()) 1113 verify(mockAvd2, never()).unregisterAnimationCallback(any<Animatable2.AnimationCallback>()) 1114 } 1115 1116 @Test 1117 fun bindText() { 1118 useRealConstraintSets() 1119 player.attachPlayer(viewHolder) 1120 player.bindPlayer(mediaData, PACKAGE) 1121 1122 // Capture animation handler 1123 val captor = argumentCaptor<Animator.AnimatorListener>() 1124 verify(mockAnimator, times(2)).addListener(captor.capture()) 1125 val handler = captor.lastValue 1126 1127 // Validate text views unchanged but animation started 1128 assertThat(titleText.getText()).isEqualTo("") 1129 assertThat(artistText.getText()).isEqualTo("") 1130 verify(mockAnimator, times(1)).start() 1131 1132 // Binding only after animator runs 1133 handler.onAnimationEnd(mockAnimator) 1134 assertThat(titleText.getText()).isEqualTo(TITLE) 1135 assertThat(artistText.getText()).isEqualTo(ARTIST) 1136 assertThat(expandedSet.getVisibility(explicitIndicator.id)).isEqualTo(ConstraintSet.GONE) 1137 assertThat(collapsedSet.getVisibility(explicitIndicator.id)).isEqualTo(ConstraintSet.GONE) 1138 1139 // Rebinding should not trigger animation 1140 player.bindPlayer(mediaData, PACKAGE) 1141 verify(mockAnimator, times(2)).start() 1142 } 1143 1144 @Test 1145 fun bindTextWithExplicitIndicator() { 1146 useRealConstraintSets() 1147 val mediaDataWitExp = mediaData.copy(isExplicit = true) 1148 player.attachPlayer(viewHolder) 1149 player.bindPlayer(mediaDataWitExp, PACKAGE) 1150 1151 // Capture animation handler 1152 val captor = argumentCaptor<Animator.AnimatorListener>() 1153 verify(mockAnimator, times(2)).addListener(captor.capture()) 1154 val handler = captor.lastValue 1155 1156 // Validate text views unchanged but animation started 1157 assertThat(titleText.getText()).isEqualTo("") 1158 assertThat(artistText.getText()).isEqualTo("") 1159 verify(mockAnimator, times(1)).start() 1160 1161 // Binding only after animator runs 1162 handler.onAnimationEnd(mockAnimator) 1163 assertThat(titleText.getText()).isEqualTo(TITLE) 1164 assertThat(artistText.getText()).isEqualTo(ARTIST) 1165 assertThat(expandedSet.getVisibility(explicitIndicator.id)).isEqualTo(ConstraintSet.VISIBLE) 1166 assertThat(collapsedSet.getVisibility(explicitIndicator.id)) 1167 .isEqualTo(ConstraintSet.VISIBLE) 1168 1169 // Rebinding should not trigger animation 1170 player.bindPlayer(mediaData, PACKAGE) 1171 verify(mockAnimator, times(3)).start() 1172 } 1173 1174 @Test 1175 fun bindTextInterrupted() { 1176 val data0 = mediaData.copy(artist = "ARTIST_0") 1177 val data1 = mediaData.copy(artist = "ARTIST_1") 1178 val data2 = mediaData.copy(artist = "ARTIST_2") 1179 1180 player.attachPlayer(viewHolder) 1181 player.bindPlayer(data0, PACKAGE) 1182 1183 // Capture animation handler 1184 val captor = argumentCaptor<Animator.AnimatorListener>() 1185 verify(mockAnimator, times(2)).addListener(captor.capture()) 1186 val handler = captor.lastValue 1187 1188 handler.onAnimationEnd(mockAnimator) 1189 assertThat(artistText.getText()).isEqualTo("ARTIST_0") 1190 1191 // Bind trigges new animation 1192 player.bindPlayer(data1, PACKAGE) 1193 verify(mockAnimator, times(3)).start() 1194 whenever(mockAnimator.isRunning()).thenReturn(true) 1195 1196 // Rebind before animation end binds corrct data 1197 player.bindPlayer(data2, PACKAGE) 1198 handler.onAnimationEnd(mockAnimator) 1199 assertThat(artistText.getText()).isEqualTo("ARTIST_2") 1200 } 1201 1202 @Test 1203 fun bindDevice() { 1204 player.attachPlayer(viewHolder) 1205 player.bindPlayer(mediaData, PACKAGE) 1206 assertThat(seamlessText.getText()).isEqualTo(DEVICE_NAME) 1207 assertThat(seamless.contentDescription).isEqualTo(DEVICE_NAME) 1208 assertThat(seamless.isEnabled()).isTrue() 1209 } 1210 1211 @Test 1212 fun bindDisabledDevice() { 1213 seamless.id = 1 1214 player.attachPlayer(viewHolder) 1215 val state = mediaData.copy(device = disabledDevice) 1216 player.bindPlayer(state, PACKAGE) 1217 assertThat(seamless.isEnabled()).isFalse() 1218 assertThat(seamlessText.getText()).isEqualTo(DISABLED_DEVICE_NAME) 1219 assertThat(seamless.contentDescription).isEqualTo(DISABLED_DEVICE_NAME) 1220 } 1221 1222 @Test 1223 fun bindNullDevice() { 1224 val fallbackString = context.getResources().getString(R.string.media_seamless_other_device) 1225 player.attachPlayer(viewHolder) 1226 val state = mediaData.copy(device = null) 1227 player.bindPlayer(state, PACKAGE) 1228 assertThat(seamless.isEnabled()).isTrue() 1229 assertThat(seamlessText.getText()).isEqualTo(fallbackString) 1230 assertThat(seamless.contentDescription).isEqualTo(fallbackString) 1231 } 1232 1233 @Test 1234 fun bindDeviceWithNullName() { 1235 val fallbackString = context.getResources().getString(R.string.media_seamless_other_device) 1236 player.attachPlayer(viewHolder) 1237 val state = mediaData.copy(device = device.copy(name = null)) 1238 player.bindPlayer(state, PACKAGE) 1239 assertThat(seamless.isEnabled()).isTrue() 1240 assertThat(seamlessText.getText()).isEqualTo(fallbackString) 1241 assertThat(seamless.contentDescription).isEqualTo(fallbackString) 1242 } 1243 1244 @Test 1245 fun bindDeviceResumptionPlayer() { 1246 player.attachPlayer(viewHolder) 1247 val state = mediaData.copy(resumption = true) 1248 player.bindPlayer(state, PACKAGE) 1249 assertThat(seamlessText.getText()).isEqualTo(DEVICE_NAME) 1250 assertThat(seamless.isEnabled()).isFalse() 1251 } 1252 1253 @Test 1254 @RequiresFlagsEnabled(com.android.settingslib.flags.Flags.FLAG_LEGACY_LE_AUDIO_SHARING) 1255 fun bindBroadcastButton() { 1256 initMediaViewHolderMocks() 1257 initDeviceMediaData(true, APP_NAME) 1258 1259 val mockAvd0 = mock(AnimatedVectorDrawable::class.java) 1260 whenever(mockAvd0.mutate()).thenReturn(mockAvd0) 1261 val semanticActions0 = 1262 MediaButton(playOrPause = MediaAction(mockAvd0, Runnable {}, "play", null)) 1263 val state = 1264 mediaData.copy(resumption = true, semanticActions = semanticActions0, isPlaying = false) 1265 player.attachPlayer(viewHolder) 1266 player.bindPlayer(state, PACKAGE) 1267 assertThat(seamlessText.getText()).isEqualTo(APP_NAME) 1268 assertThat(seamless.isEnabled()).isTrue() 1269 1270 seamless.callOnClick() 1271 1272 verify(logger).logOpenBroadcastDialog(anyInt(), eq(PACKAGE), eq(instanceId)) 1273 } 1274 1275 /* ***** Guts tests for the player ***** */ 1276 1277 @Test 1278 fun player_longClick_isFalse() { 1279 whenever(falsingManager.isFalseLongTap(FalsingManager.LOW_PENALTY)).thenReturn(true) 1280 player.attachPlayer(viewHolder) 1281 1282 val captor = ArgumentCaptor.forClass(View.OnLongClickListener::class.java) 1283 verify(viewHolder.player).onLongClickListener = captor.capture() 1284 1285 captor.value.onLongClick(viewHolder.player) 1286 verify(mediaViewController, never()).openGuts() 1287 verify(mediaViewController, never()).closeGuts() 1288 } 1289 1290 @Test 1291 fun player_longClickWhenGutsClosed_gutsOpens() { 1292 player.attachPlayer(viewHolder) 1293 player.bindPlayer(mediaData, KEY) 1294 whenever(mediaViewController.isGutsVisible).thenReturn(false) 1295 1296 val captor = ArgumentCaptor.forClass(View.OnLongClickListener::class.java) 1297 verify(viewHolder.player).setOnLongClickListener(captor.capture()) 1298 1299 captor.value.onLongClick(viewHolder.player) 1300 verify(mediaViewController).openGuts() 1301 verify(logger).logLongPressOpen(anyInt(), eq(PACKAGE), eq(instanceId)) 1302 } 1303 1304 @Test 1305 fun player_longClickWhenGutsOpen_gutsCloses() { 1306 player.attachPlayer(viewHolder) 1307 whenever(mediaViewController.isGutsVisible).thenReturn(true) 1308 1309 val captor = ArgumentCaptor.forClass(View.OnLongClickListener::class.java) 1310 verify(viewHolder.player).setOnLongClickListener(captor.capture()) 1311 1312 captor.value.onLongClick(viewHolder.player) 1313 verify(mediaViewController, never()).openGuts() 1314 verify(mediaViewController).closeGuts(false) 1315 } 1316 1317 @Test 1318 fun player_cancelButtonClick_animation() { 1319 player.attachPlayer(viewHolder) 1320 player.bindPlayer(mediaData, KEY) 1321 1322 cancel.callOnClick() 1323 1324 verify(mediaViewController).closeGuts(false) 1325 } 1326 1327 @Test 1328 fun player_settingsButtonClick() { 1329 player.attachPlayer(viewHolder) 1330 player.bindPlayer(mediaData, KEY) 1331 1332 settings.callOnClick() 1333 verify(logger).logLongPressSettings(anyInt(), eq(PACKAGE), eq(instanceId)) 1334 1335 val captor = ArgumentCaptor.forClass(Intent::class.java) 1336 verify(activityStarter).startActivity(captor.capture(), eq(true)) 1337 1338 assertThat(captor.value.action).isEqualTo(ACTION_MEDIA_CONTROLS_SETTINGS) 1339 } 1340 1341 @Test 1342 fun player_dismissButtonClick() { 1343 val mediaKey = "key for dismissal" 1344 player.attachPlayer(viewHolder) 1345 val state = mediaData.copy(notificationKey = KEY) 1346 player.bindPlayer(state, mediaKey) 1347 1348 assertThat(dismiss.isEnabled).isEqualTo(true) 1349 dismiss.callOnClick() 1350 verify(logger).logLongPressDismiss(anyInt(), eq(PACKAGE), eq(instanceId)) 1351 verify(mediaDataManager).dismissMediaData(eq(mediaKey), anyLong(), eq(true)) 1352 } 1353 1354 @Test 1355 fun player_dismissButtonDisabled() { 1356 val mediaKey = "key for dismissal" 1357 player.attachPlayer(viewHolder) 1358 val state = mediaData.copy(isClearable = false, notificationKey = KEY) 1359 player.bindPlayer(state, mediaKey) 1360 1361 assertThat(dismiss.isEnabled).isEqualTo(false) 1362 } 1363 1364 @Test 1365 fun player_dismissButtonClick_notInManager() { 1366 val mediaKey = "key for dismissal" 1367 whenever(mediaDataManager.dismissMediaData(eq(mediaKey), anyLong(), eq(true))) 1368 .thenReturn(false) 1369 1370 player.attachPlayer(viewHolder) 1371 val state = mediaData.copy(notificationKey = KEY) 1372 player.bindPlayer(state, mediaKey) 1373 1374 assertThat(dismiss.isEnabled).isEqualTo(true) 1375 dismiss.callOnClick() 1376 1377 verify(mediaDataManager).dismissMediaData(eq(mediaKey), anyLong(), eq(true)) 1378 verify(mediaCarouselController).removePlayer(eq(mediaKey), eq(false), eq(false), eq(true)) 1379 } 1380 1381 @Test 1382 fun player_gutsOpen_contentDescriptionIsForGuts() { 1383 whenever(mediaViewController.isGutsVisible).thenReturn(true) 1384 player.attachPlayer(viewHolder) 1385 1386 val gutsTextString = "gutsText" 1387 whenever(gutsText.text).thenReturn(gutsTextString) 1388 player.bindPlayer(mediaData, KEY) 1389 1390 val descriptionCaptor = ArgumentCaptor.forClass(CharSequence::class.java) 1391 verify(viewHolder.player).contentDescription = descriptionCaptor.capture() 1392 val description = descriptionCaptor.value.toString() 1393 1394 assertThat(description).isEqualTo(gutsTextString) 1395 } 1396 1397 @Test 1398 fun player_gutsClosed_contentDescriptionIsForPlayer() { 1399 whenever(mediaViewController.isGutsVisible).thenReturn(false) 1400 player.attachPlayer(viewHolder) 1401 1402 val app = "appName" 1403 player.bindPlayer(mediaData.copy(app = app), KEY) 1404 1405 val descriptionCaptor = ArgumentCaptor.forClass(CharSequence::class.java) 1406 verify(viewHolder.player).contentDescription = descriptionCaptor.capture() 1407 val description = descriptionCaptor.value.toString() 1408 1409 assertThat(description).contains(mediaData.song!!) 1410 assertThat(description).contains(mediaData.artist!!) 1411 assertThat(description).contains(app) 1412 } 1413 1414 @Test 1415 fun player_gutsChangesFromOpenToClosed_contentDescriptionUpdated() { 1416 // Start out open 1417 whenever(mediaViewController.isGutsVisible).thenReturn(true) 1418 whenever(gutsText.text).thenReturn("gutsText") 1419 player.attachPlayer(viewHolder) 1420 val app = "appName" 1421 player.bindPlayer(mediaData.copy(app = app), KEY) 1422 1423 // Update to closed by long pressing 1424 val captor = ArgumentCaptor.forClass(View.OnLongClickListener::class.java) 1425 verify(viewHolder.player).onLongClickListener = captor.capture() 1426 reset(viewHolder.player) 1427 1428 whenever(mediaViewController.isGutsVisible).thenReturn(false) 1429 captor.value.onLongClick(viewHolder.player) 1430 1431 // Then content description is now the player content description 1432 val descriptionCaptor = ArgumentCaptor.forClass(CharSequence::class.java) 1433 verify(viewHolder.player).contentDescription = descriptionCaptor.capture() 1434 val description = descriptionCaptor.value.toString() 1435 1436 assertThat(description).contains(mediaData.song!!) 1437 assertThat(description).contains(mediaData.artist!!) 1438 assertThat(description).contains(app) 1439 } 1440 1441 @Test 1442 fun player_gutsChangesFromClosedToOpen_contentDescriptionUpdated() { 1443 // Start out closed 1444 whenever(mediaViewController.isGutsVisible).thenReturn(false) 1445 val gutsTextString = "gutsText" 1446 whenever(gutsText.text).thenReturn(gutsTextString) 1447 player.attachPlayer(viewHolder) 1448 player.bindPlayer(mediaData.copy(app = "appName"), KEY) 1449 1450 // Update to open by long pressing 1451 val captor = ArgumentCaptor.forClass(View.OnLongClickListener::class.java) 1452 verify(viewHolder.player).onLongClickListener = captor.capture() 1453 reset(viewHolder.player) 1454 1455 whenever(mediaViewController.isGutsVisible).thenReturn(true) 1456 captor.value.onLongClick(viewHolder.player) 1457 1458 // Then content description is now the guts content description 1459 val descriptionCaptor = ArgumentCaptor.forClass(CharSequence::class.java) 1460 verify(viewHolder.player).contentDescription = descriptionCaptor.capture() 1461 val description = descriptionCaptor.value.toString() 1462 1463 assertThat(description).isEqualTo(gutsTextString) 1464 } 1465 1466 /* ***** END guts tests for the player ***** */ 1467 1468 /* ***** Guts tests for the recommendations ***** */ 1469 1470 @Test 1471 fun recommendations_longClick_isFalse() { 1472 whenever(falsingManager.isFalseLongTap(FalsingManager.LOW_PENALTY)).thenReturn(true) 1473 player.attachRecommendation(recommendationViewHolder) 1474 player.bindRecommendation(smartspaceData) 1475 1476 val captor = ArgumentCaptor.forClass(View.OnLongClickListener::class.java) 1477 verify(viewHolder.player).onLongClickListener = captor.capture() 1478 1479 captor.value.onLongClick(viewHolder.player) 1480 verify(mediaViewController, never()).openGuts() 1481 verify(mediaViewController, never()).closeGuts() 1482 } 1483 1484 @Test 1485 fun recommendations_longClickWhenGutsClosed_gutsOpens() { 1486 player.attachRecommendation(recommendationViewHolder) 1487 player.bindRecommendation(smartspaceData) 1488 whenever(mediaViewController.isGutsVisible).thenReturn(false) 1489 1490 val captor = ArgumentCaptor.forClass(View.OnLongClickListener::class.java) 1491 verify(viewHolder.player).onLongClickListener = captor.capture() 1492 1493 captor.value.onLongClick(viewHolder.player) 1494 verify(mediaViewController).openGuts() 1495 verify(logger).logLongPressOpen(anyInt(), eq(PACKAGE), eq(instanceId)) 1496 } 1497 1498 @Test 1499 fun recommendations_longClickWhenGutsOpen_gutsCloses() { 1500 player.attachRecommendation(recommendationViewHolder) 1501 player.bindRecommendation(smartspaceData) 1502 whenever(mediaViewController.isGutsVisible).thenReturn(true) 1503 1504 val captor = ArgumentCaptor.forClass(View.OnLongClickListener::class.java) 1505 verify(viewHolder.player).onLongClickListener = captor.capture() 1506 1507 captor.value.onLongClick(viewHolder.player) 1508 verify(mediaViewController, never()).openGuts() 1509 verify(mediaViewController).closeGuts(false) 1510 } 1511 1512 @Test 1513 fun recommendations_cancelButtonClick_animation() { 1514 player.attachRecommendation(recommendationViewHolder) 1515 player.bindRecommendation(smartspaceData) 1516 1517 cancel.callOnClick() 1518 1519 verify(mediaViewController).closeGuts(false) 1520 } 1521 1522 @Test 1523 fun recommendations_settingsButtonClick() { 1524 player.attachRecommendation(recommendationViewHolder) 1525 player.bindRecommendation(smartspaceData) 1526 1527 settings.callOnClick() 1528 verify(logger).logLongPressSettings(anyInt(), eq(PACKAGE), eq(instanceId)) 1529 1530 val captor = ArgumentCaptor.forClass(Intent::class.java) 1531 verify(activityStarter).startActivity(captor.capture(), eq(true)) 1532 1533 assertThat(captor.value.action).isEqualTo(ACTION_MEDIA_CONTROLS_SETTINGS) 1534 } 1535 1536 @Test 1537 fun recommendations_dismissButtonClick() { 1538 val mediaKey = "key for dismissal" 1539 player.attachRecommendation(recommendationViewHolder) 1540 player.bindRecommendation(smartspaceData.copy(targetId = mediaKey)) 1541 1542 assertThat(dismiss.isEnabled).isEqualTo(true) 1543 dismiss.callOnClick() 1544 verify(logger).logLongPressDismiss(anyInt(), eq(PACKAGE), eq(instanceId)) 1545 verify(mediaDataManager).dismissSmartspaceRecommendation(eq(mediaKey), anyLong()) 1546 } 1547 1548 @Test 1549 fun recommendation_gutsOpen_contentDescriptionIsForGuts() { 1550 whenever(mediaViewController.isGutsVisible).thenReturn(true) 1551 player.attachRecommendation(recommendationViewHolder) 1552 1553 val gutsTextString = "gutsText" 1554 whenever(gutsText.text).thenReturn(gutsTextString) 1555 player.bindRecommendation(smartspaceData) 1556 1557 val descriptionCaptor = ArgumentCaptor.forClass(CharSequence::class.java) 1558 verify(viewHolder.player).contentDescription = descriptionCaptor.capture() 1559 val description = descriptionCaptor.value.toString() 1560 1561 assertThat(description).isEqualTo(gutsTextString) 1562 } 1563 1564 @Test 1565 fun recommendation_gutsClosed_contentDescriptionIsForPlayer() { 1566 whenever(mediaViewController.isGutsVisible).thenReturn(false) 1567 player.attachRecommendation(recommendationViewHolder) 1568 1569 player.bindRecommendation(smartspaceData) 1570 1571 val descriptionCaptor = ArgumentCaptor.forClass(CharSequence::class.java) 1572 verify(viewHolder.player).contentDescription = descriptionCaptor.capture() 1573 val description = descriptionCaptor.value.toString() 1574 1575 assertThat(description) 1576 .isEqualTo(context.getString(R.string.controls_media_smartspace_rec_header)) 1577 } 1578 1579 @Test 1580 fun recommendation_gutsChangesFromOpenToClosed_contentDescriptionUpdated() { 1581 // Start out open 1582 whenever(mediaViewController.isGutsVisible).thenReturn(true) 1583 whenever(gutsText.text).thenReturn("gutsText") 1584 player.attachRecommendation(recommendationViewHolder) 1585 player.bindRecommendation(smartspaceData) 1586 1587 // Update to closed by long pressing 1588 val captor = ArgumentCaptor.forClass(View.OnLongClickListener::class.java) 1589 verify(viewHolder.player).onLongClickListener = captor.capture() 1590 reset(viewHolder.player) 1591 1592 whenever(mediaViewController.isGutsVisible).thenReturn(false) 1593 captor.value.onLongClick(viewHolder.player) 1594 1595 // Then content description is now the player content description 1596 val descriptionCaptor = ArgumentCaptor.forClass(CharSequence::class.java) 1597 verify(viewHolder.player).contentDescription = descriptionCaptor.capture() 1598 val description = descriptionCaptor.value.toString() 1599 1600 assertThat(description) 1601 .isEqualTo(context.getString(R.string.controls_media_smartspace_rec_header)) 1602 } 1603 1604 @Test 1605 fun recommendation_gutsChangesFromClosedToOpen_contentDescriptionUpdated() { 1606 // Start out closed 1607 whenever(mediaViewController.isGutsVisible).thenReturn(false) 1608 val gutsTextString = "gutsText" 1609 whenever(gutsText.text).thenReturn(gutsTextString) 1610 player.attachRecommendation(recommendationViewHolder) 1611 player.bindRecommendation(smartspaceData) 1612 1613 // Update to open by long pressing 1614 val captor = ArgumentCaptor.forClass(View.OnLongClickListener::class.java) 1615 verify(viewHolder.player).onLongClickListener = captor.capture() 1616 reset(viewHolder.player) 1617 1618 whenever(mediaViewController.isGutsVisible).thenReturn(true) 1619 captor.value.onLongClick(viewHolder.player) 1620 1621 // Then content description is now the guts content description 1622 val descriptionCaptor = ArgumentCaptor.forClass(CharSequence::class.java) 1623 verify(viewHolder.player).contentDescription = descriptionCaptor.capture() 1624 val description = descriptionCaptor.value.toString() 1625 1626 assertThat(description).isEqualTo(gutsTextString) 1627 } 1628 1629 /* ***** END guts tests for the recommendations ***** */ 1630 1631 @Test 1632 fun actionPlayPauseClick_isLogged() { 1633 val semanticActions = 1634 MediaButton(playOrPause = MediaAction(null, Runnable {}, "play", null)) 1635 val data = mediaData.copy(semanticActions = semanticActions) 1636 1637 player.attachPlayer(viewHolder) 1638 player.bindPlayer(data, KEY) 1639 1640 viewHolder.actionPlayPause.callOnClick() 1641 verify(logger).logTapAction(eq(R.id.actionPlayPause), anyInt(), eq(PACKAGE), eq(instanceId)) 1642 } 1643 1644 @Test 1645 fun actionPrevClick_isLogged() { 1646 val semanticActions = 1647 MediaButton(prevOrCustom = MediaAction(null, Runnable {}, "previous", null)) 1648 val data = mediaData.copy(semanticActions = semanticActions) 1649 1650 player.attachPlayer(viewHolder) 1651 player.bindPlayer(data, KEY) 1652 1653 viewHolder.actionPrev.callOnClick() 1654 verify(logger).logTapAction(eq(R.id.actionPrev), anyInt(), eq(PACKAGE), eq(instanceId)) 1655 } 1656 1657 @Test 1658 fun actionNextClick_isLogged() { 1659 val semanticActions = 1660 MediaButton(nextOrCustom = MediaAction(null, Runnable {}, "next", null)) 1661 val data = mediaData.copy(semanticActions = semanticActions) 1662 1663 player.attachPlayer(viewHolder) 1664 player.bindPlayer(data, KEY) 1665 1666 viewHolder.actionNext.callOnClick() 1667 verify(logger).logTapAction(eq(R.id.actionNext), anyInt(), eq(PACKAGE), eq(instanceId)) 1668 } 1669 1670 @Test 1671 fun actionCustom0Click_isLogged() { 1672 val semanticActions = 1673 MediaButton(custom0 = MediaAction(null, Runnable {}, "custom 0", null)) 1674 val data = mediaData.copy(semanticActions = semanticActions) 1675 1676 player.attachPlayer(viewHolder) 1677 player.bindPlayer(data, KEY) 1678 1679 viewHolder.action0.callOnClick() 1680 verify(logger).logTapAction(eq(R.id.action0), anyInt(), eq(PACKAGE), eq(instanceId)) 1681 } 1682 1683 @Test 1684 fun actionCustom1Click_isLogged() { 1685 val semanticActions = 1686 MediaButton(custom1 = MediaAction(null, Runnable {}, "custom 1", null)) 1687 val data = mediaData.copy(semanticActions = semanticActions) 1688 1689 player.attachPlayer(viewHolder) 1690 player.bindPlayer(data, KEY) 1691 1692 viewHolder.action1.callOnClick() 1693 verify(logger).logTapAction(eq(R.id.action1), anyInt(), eq(PACKAGE), eq(instanceId)) 1694 } 1695 1696 @Test 1697 fun actionCustom2Click_isLogged() { 1698 val actions = 1699 listOf( 1700 MediaNotificationAction(true, actionIntent = pendingIntent, null, "action 0"), 1701 MediaNotificationAction(true, actionIntent = pendingIntent, null, "action 1"), 1702 MediaNotificationAction(true, actionIntent = pendingIntent, null, "action 2"), 1703 MediaNotificationAction(true, actionIntent = pendingIntent, null, "action 3"), 1704 MediaNotificationAction(true, actionIntent = pendingIntent, null, "action 4") 1705 ) 1706 val data = mediaData.copy(actions = actions) 1707 1708 player.attachPlayer(viewHolder) 1709 player.bindPlayer(data, KEY) 1710 1711 viewHolder.action2.callOnClick() 1712 verify(logger).logTapAction(eq(R.id.action2), anyInt(), eq(PACKAGE), eq(instanceId)) 1713 } 1714 1715 @Test 1716 fun actionCustom3Click_isLogged() { 1717 val actions = 1718 listOf( 1719 MediaNotificationAction(true, actionIntent = pendingIntent, null, "action 0"), 1720 MediaNotificationAction(true, actionIntent = pendingIntent, null, "action 1"), 1721 MediaNotificationAction(true, actionIntent = pendingIntent, null, "action 2"), 1722 MediaNotificationAction(true, actionIntent = pendingIntent, null, "action 3"), 1723 MediaNotificationAction(true, actionIntent = pendingIntent, null, "action 4") 1724 ) 1725 val data = mediaData.copy(actions = actions) 1726 1727 player.attachPlayer(viewHolder) 1728 player.bindPlayer(data, KEY) 1729 1730 viewHolder.action1.callOnClick() 1731 verify(logger).logTapAction(eq(R.id.action1), anyInt(), eq(PACKAGE), eq(instanceId)) 1732 } 1733 1734 @Test 1735 fun actionCustom4Click_isLogged() { 1736 val actions = 1737 listOf( 1738 MediaNotificationAction(true, actionIntent = pendingIntent, null, "action 0"), 1739 MediaNotificationAction(true, actionIntent = pendingIntent, null, "action 1"), 1740 MediaNotificationAction(true, actionIntent = pendingIntent, null, "action 2"), 1741 MediaNotificationAction(true, actionIntent = pendingIntent, null, "action 3"), 1742 MediaNotificationAction(true, actionIntent = pendingIntent, null, "action 4") 1743 ) 1744 val data = mediaData.copy(actions = actions) 1745 1746 player.attachPlayer(viewHolder) 1747 player.bindPlayer(data, KEY) 1748 1749 viewHolder.action1.callOnClick() 1750 verify(logger).logTapAction(eq(R.id.action1), anyInt(), eq(PACKAGE), eq(instanceId)) 1751 } 1752 1753 @Test 1754 fun openOutputSwitcher_isLogged() { 1755 player.attachPlayer(viewHolder) 1756 player.bindPlayer(mediaData, KEY) 1757 1758 seamless.callOnClick() 1759 1760 verify(logger).logOpenOutputSwitcher(anyInt(), eq(PACKAGE), eq(instanceId)) 1761 } 1762 1763 @Test 1764 fun tapContentView_isLogged() { 1765 val pendingIntent = mock(PendingIntent::class.java) 1766 val captor = ArgumentCaptor.forClass(View.OnClickListener::class.java) 1767 val data = mediaData.copy(clickIntent = pendingIntent) 1768 player.attachPlayer(viewHolder) 1769 player.bindPlayer(data, KEY) 1770 verify(viewHolder.player).setOnClickListener(captor.capture()) 1771 1772 captor.value.onClick(viewHolder.player) 1773 1774 verify(logger).logTapContentView(anyInt(), eq(PACKAGE), eq(instanceId)) 1775 } 1776 1777 @Test 1778 fun logSeek() { 1779 player.attachPlayer(viewHolder) 1780 player.bindPlayer(mediaData, KEY) 1781 1782 val captor = argumentCaptor<() -> Unit>() 1783 verify(seekBarViewModel).logSeek = captor.capture() 1784 captor.lastValue.invoke() 1785 1786 verify(logger).logSeek(anyInt(), eq(PACKAGE), eq(instanceId)) 1787 } 1788 1789 @EnableFlags(Flags.FLAG_MEDIA_LOCKSCREEN_LAUNCH_ANIMATION) 1790 @Test 1791 fun tapContentView_showOverLockscreen_openActivity_withOriginAnimation() { 1792 // WHEN we are on lockscreen and this activity can show over lockscreen 1793 whenever(keyguardStateController.isShowing).thenReturn(true) 1794 whenever(activityIntentHelper.wouldPendingShowOverLockscreen(any(), any())).thenReturn(true) 1795 1796 val clickIntent = mock(Intent::class.java) 1797 val pendingIntent = mock(PendingIntent::class.java) 1798 whenever(pendingIntent.intent).thenReturn(clickIntent) 1799 val captor = ArgumentCaptor.forClass(View.OnClickListener::class.java) 1800 val data = mediaData.copy(clickIntent = pendingIntent) 1801 player.attachPlayer(viewHolder) 1802 player.bindPlayer(data, KEY) 1803 verify(viewHolder.player).setOnClickListener(captor.capture()) 1804 1805 // THEN it sends the PendingIntent without dismissing keyguard first, 1806 // and does not use the Intent directly (see b/271845008) 1807 captor.value.onClick(viewHolder.player) 1808 verify(activityStarter) 1809 .startPendingIntentMaybeDismissingKeyguard( 1810 eq(pendingIntent), 1811 eq(true), 1812 eq(null), 1813 any(), 1814 eq(null), 1815 eq(null), 1816 eq(null), 1817 ) 1818 verify(activityStarter, never()).postStartActivityDismissingKeyguard(eq(clickIntent), any()) 1819 } 1820 1821 @DisableFlags(Flags.FLAG_MEDIA_LOCKSCREEN_LAUNCH_ANIMATION) 1822 @Test 1823 fun tapContentView_showOverLockscreen_openActivity_withoutOriginAnimation() { 1824 // WHEN we are on lockscreen and this activity can show over lockscreen 1825 whenever(keyguardStateController.isShowing).thenReturn(true) 1826 whenever(activityIntentHelper.wouldPendingShowOverLockscreen(any(), any())).thenReturn(true) 1827 1828 val clickIntent = mock(Intent::class.java) 1829 val pendingIntent = mock(PendingIntent::class.java) 1830 whenever(pendingIntent.intent).thenReturn(clickIntent) 1831 val captor = ArgumentCaptor.forClass(View.OnClickListener::class.java) 1832 val data = mediaData.copy(clickIntent = pendingIntent) 1833 player.attachPlayer(viewHolder) 1834 player.bindPlayer(data, KEY) 1835 verify(viewHolder.player).setOnClickListener(captor.capture()) 1836 1837 // THEN it sends the PendingIntent without dismissing keyguard first, 1838 // and does not use the Intent directly (see b/271845008) 1839 captor.value.onClick(viewHolder.player) 1840 verify(pendingIntent).send(any<Bundle>()) 1841 verify(pendingIntent, never()).getIntent() 1842 verify(activityStarter, never()).postStartActivityDismissingKeyguard(eq(clickIntent), any()) 1843 } 1844 1845 @Test 1846 fun tapContentView_noShowOverLockscreen_dismissKeyguard() { 1847 // WHEN we are on lockscreen and the activity cannot show over lockscreen 1848 whenever(keyguardStateController.isShowing).thenReturn(true) 1849 whenever(activityIntentHelper.wouldPendingShowOverLockscreen(any(), any())) 1850 .thenReturn(false) 1851 1852 val clickIntent = mock(Intent::class.java) 1853 val pendingIntent = mock(PendingIntent::class.java) 1854 whenever(pendingIntent.intent).thenReturn(clickIntent) 1855 val captor = ArgumentCaptor.forClass(View.OnClickListener::class.java) 1856 val data = mediaData.copy(clickIntent = pendingIntent) 1857 player.attachPlayer(viewHolder) 1858 player.bindPlayer(data, KEY) 1859 verify(viewHolder.player).setOnClickListener(captor.capture()) 1860 1861 // THEN keyguard has to be dismissed 1862 captor.value.onClick(viewHolder.player) 1863 verify(activityStarter).postStartActivityDismissingKeyguard(eq(pendingIntent), any()) 1864 } 1865 1866 @Test 1867 fun recommendation_gutsClosed_longPressOpens() { 1868 player.attachRecommendation(recommendationViewHolder) 1869 player.bindRecommendation(smartspaceData) 1870 whenever(mediaViewController.isGutsVisible).thenReturn(false) 1871 1872 val captor = ArgumentCaptor.forClass(View.OnLongClickListener::class.java) 1873 verify(recommendationViewHolder.recommendations).setOnLongClickListener(captor.capture()) 1874 1875 captor.value.onLongClick(recommendationViewHolder.recommendations) 1876 verify(mediaViewController).openGuts() 1877 verify(logger).logLongPressOpen(anyInt(), eq(PACKAGE), eq(instanceId)) 1878 } 1879 1880 @Test 1881 fun recommendation_settingsButtonClick_isLogged() { 1882 player.attachRecommendation(recommendationViewHolder) 1883 player.bindRecommendation(smartspaceData) 1884 1885 settings.callOnClick() 1886 verify(logger).logLongPressSettings(anyInt(), eq(PACKAGE), eq(instanceId)) 1887 1888 val captor = ArgumentCaptor.forClass(Intent::class.java) 1889 verify(activityStarter).startActivity(captor.capture(), eq(true)) 1890 1891 assertThat(captor.value.action).isEqualTo(ACTION_MEDIA_CONTROLS_SETTINGS) 1892 } 1893 1894 @Test 1895 fun recommendation_dismissButton_isLogged() { 1896 player.attachRecommendation(recommendationViewHolder) 1897 player.bindRecommendation(smartspaceData) 1898 1899 dismiss.callOnClick() 1900 verify(logger).logLongPressDismiss(anyInt(), eq(PACKAGE), eq(instanceId)) 1901 } 1902 1903 @Test 1904 fun recommendation_tapOnCard_isLogged() { 1905 val captor = ArgumentCaptor.forClass(View.OnClickListener::class.java) 1906 player.attachRecommendation(recommendationViewHolder) 1907 player.bindRecommendation(smartspaceData) 1908 1909 verify(recommendationViewHolder.recommendations).setOnClickListener(captor.capture()) 1910 captor.value.onClick(recommendationViewHolder.recommendations) 1911 1912 verify(logger).logRecommendationCardTap(eq(PACKAGE), eq(instanceId)) 1913 } 1914 1915 @Test 1916 fun recommendation_tapOnItem_isLogged() { 1917 val captor = ArgumentCaptor.forClass(View.OnClickListener::class.java) 1918 player.attachRecommendation(recommendationViewHolder) 1919 player.bindRecommendation(smartspaceData) 1920 1921 verify(coverContainer1).setOnClickListener(captor.capture()) 1922 captor.value.onClick(recommendationViewHolder.recommendations) 1923 1924 verify(logger).logRecommendationItemTap(eq(PACKAGE), eq(instanceId), eq(0)) 1925 } 1926 1927 @Test 1928 fun bindRecommendation_listHasTooFewRecs_notDisplayed() { 1929 player.attachRecommendation(recommendationViewHolder) 1930 val icon = 1931 Icon.createWithResource(context, com.android.settingslib.R.drawable.ic_1x_mobiledata) 1932 val data = 1933 smartspaceData.copy( 1934 recommendations = 1935 listOf( 1936 SmartspaceAction.Builder("id1", "title1") 1937 .setSubtitle("subtitle1") 1938 .setIcon(icon) 1939 .setExtras(Bundle.EMPTY) 1940 .build(), 1941 SmartspaceAction.Builder("id2", "title2") 1942 .setSubtitle("subtitle2") 1943 .setIcon(icon) 1944 .setExtras(Bundle.EMPTY) 1945 .build(), 1946 ) 1947 ) 1948 1949 player.bindRecommendation(data) 1950 1951 assertThat(recTitle1.text).isEqualTo("") 1952 verify(mediaViewController, never()).refreshState() 1953 } 1954 1955 @Test 1956 fun bindRecommendation_listHasTooFewRecsWithIcons_notDisplayed() { 1957 player.attachRecommendation(recommendationViewHolder) 1958 val icon = 1959 Icon.createWithResource(context, com.android.settingslib.R.drawable.ic_1x_mobiledata) 1960 val data = 1961 smartspaceData.copy( 1962 recommendations = 1963 listOf( 1964 SmartspaceAction.Builder("id1", "title1") 1965 .setSubtitle("subtitle1") 1966 .setIcon(icon) 1967 .setExtras(Bundle.EMPTY) 1968 .build(), 1969 SmartspaceAction.Builder("id2", "title2") 1970 .setSubtitle("subtitle2") 1971 .setIcon(icon) 1972 .setExtras(Bundle.EMPTY) 1973 .build(), 1974 SmartspaceAction.Builder("id2", "empty icon 1") 1975 .setSubtitle("subtitle2") 1976 .setIcon(null) 1977 .setExtras(Bundle.EMPTY) 1978 .build(), 1979 SmartspaceAction.Builder("id2", "empty icon 2") 1980 .setSubtitle("subtitle2") 1981 .setIcon(null) 1982 .setExtras(Bundle.EMPTY) 1983 .build(), 1984 ) 1985 ) 1986 1987 player.bindRecommendation(data) 1988 1989 assertThat(recTitle1.text).isEqualTo("") 1990 verify(mediaViewController, never()).refreshState() 1991 } 1992 1993 @Test 1994 fun bindRecommendation_hasTitlesAndSubtitles() { 1995 player.attachRecommendation(recommendationViewHolder) 1996 1997 val title1 = "Title1" 1998 val title2 = "Title2" 1999 val title3 = "Title3" 2000 val subtitle1 = "Subtitle1" 2001 val subtitle2 = "Subtitle2" 2002 val subtitle3 = "Subtitle3" 2003 val icon = 2004 Icon.createWithResource(context, com.android.settingslib.R.drawable.ic_1x_mobiledata) 2005 2006 val data = 2007 smartspaceData.copy( 2008 recommendations = 2009 listOf( 2010 SmartspaceAction.Builder("id1", title1) 2011 .setSubtitle(subtitle1) 2012 .setIcon(icon) 2013 .setExtras(Bundle.EMPTY) 2014 .build(), 2015 SmartspaceAction.Builder("id2", title2) 2016 .setSubtitle(subtitle2) 2017 .setIcon(icon) 2018 .setExtras(Bundle.EMPTY) 2019 .build(), 2020 SmartspaceAction.Builder("id3", title3) 2021 .setSubtitle(subtitle3) 2022 .setIcon(icon) 2023 .setExtras(Bundle.EMPTY) 2024 .build() 2025 ) 2026 ) 2027 player.bindRecommendation(data) 2028 2029 assertThat(recTitle1.text).isEqualTo(title1) 2030 assertThat(recTitle2.text).isEqualTo(title2) 2031 assertThat(recTitle3.text).isEqualTo(title3) 2032 assertThat(recSubtitle1.text).isEqualTo(subtitle1) 2033 assertThat(recSubtitle2.text).isEqualTo(subtitle2) 2034 assertThat(recSubtitle3.text).isEqualTo(subtitle3) 2035 } 2036 2037 @Test 2038 fun bindRecommendation_noTitle_subtitleNotShown() { 2039 player.attachRecommendation(recommendationViewHolder) 2040 2041 val data = 2042 smartspaceData.copy( 2043 recommendations = 2044 listOf( 2045 SmartspaceAction.Builder("id1", "") 2046 .setSubtitle("fake subtitle") 2047 .setIcon( 2048 Icon.createWithResource( 2049 context, 2050 com.android.settingslib.R.drawable.ic_1x_mobiledata 2051 ) 2052 ) 2053 .setExtras(Bundle.EMPTY) 2054 .build() 2055 ) 2056 ) 2057 player.bindRecommendation(data) 2058 2059 assertThat(recSubtitle1.text).isEqualTo("") 2060 } 2061 2062 @Test 2063 fun bindRecommendation_someHaveTitles_allTitleViewsShown() { 2064 useRealConstraintSets() 2065 player.attachRecommendation(recommendationViewHolder) 2066 2067 val icon = 2068 Icon.createWithResource(context, com.android.settingslib.R.drawable.ic_1x_mobiledata) 2069 val data = 2070 smartspaceData.copy( 2071 recommendations = 2072 listOf( 2073 SmartspaceAction.Builder("id1", "") 2074 .setSubtitle("fake subtitle") 2075 .setIcon(icon) 2076 .setExtras(Bundle.EMPTY) 2077 .build(), 2078 SmartspaceAction.Builder("id2", "title2") 2079 .setSubtitle("fake subtitle") 2080 .setIcon(icon) 2081 .setExtras(Bundle.EMPTY) 2082 .build(), 2083 SmartspaceAction.Builder("id3", "") 2084 .setSubtitle("fake subtitle") 2085 .setIcon(icon) 2086 .setExtras(Bundle.EMPTY) 2087 .build() 2088 ) 2089 ) 2090 player.bindRecommendation(data) 2091 2092 assertThat(expandedSet.getVisibility(recTitle1.id)).isEqualTo(ConstraintSet.VISIBLE) 2093 assertThat(expandedSet.getVisibility(recTitle2.id)).isEqualTo(ConstraintSet.VISIBLE) 2094 assertThat(expandedSet.getVisibility(recTitle3.id)).isEqualTo(ConstraintSet.VISIBLE) 2095 } 2096 2097 @Test 2098 fun bindRecommendation_someHaveSubtitles_allSubtitleViewsShown() { 2099 useRealConstraintSets() 2100 player.attachRecommendation(recommendationViewHolder) 2101 2102 val icon = 2103 Icon.createWithResource(context, com.android.settingslib.R.drawable.ic_1x_mobiledata) 2104 val data = 2105 smartspaceData.copy( 2106 recommendations = 2107 listOf( 2108 SmartspaceAction.Builder("id1", "") 2109 .setSubtitle("") 2110 .setIcon(icon) 2111 .setExtras(Bundle.EMPTY) 2112 .build(), 2113 SmartspaceAction.Builder("id2", "title2") 2114 .setSubtitle("subtitle2") 2115 .setIcon(icon) 2116 .setExtras(Bundle.EMPTY) 2117 .build(), 2118 SmartspaceAction.Builder("id3", "title3") 2119 .setSubtitle("") 2120 .setIcon(icon) 2121 .setExtras(Bundle.EMPTY) 2122 .build() 2123 ) 2124 ) 2125 player.bindRecommendation(data) 2126 2127 assertThat(expandedSet.getVisibility(recSubtitle1.id)).isEqualTo(ConstraintSet.VISIBLE) 2128 assertThat(expandedSet.getVisibility(recSubtitle2.id)).isEqualTo(ConstraintSet.VISIBLE) 2129 assertThat(expandedSet.getVisibility(recSubtitle3.id)).isEqualTo(ConstraintSet.VISIBLE) 2130 } 2131 2132 @Test 2133 fun bindRecommendation_noneHaveSubtitles_subtitleViewsGone() { 2134 useRealConstraintSets() 2135 player.attachRecommendation(recommendationViewHolder) 2136 val data = 2137 smartspaceData.copy( 2138 recommendations = 2139 listOf( 2140 SmartspaceAction.Builder("id1", "title1") 2141 .setSubtitle("") 2142 .setIcon( 2143 Icon.createWithResource( 2144 context, 2145 com.android.settingslib.R.drawable.ic_1x_mobiledata 2146 ) 2147 ) 2148 .setExtras(Bundle.EMPTY) 2149 .build(), 2150 SmartspaceAction.Builder("id2", "title2") 2151 .setSubtitle("") 2152 .setIcon(Icon.createWithResource(context, R.drawable.ic_alarm)) 2153 .setExtras(Bundle.EMPTY) 2154 .build(), 2155 SmartspaceAction.Builder("id3", "title3") 2156 .setSubtitle("") 2157 .setIcon( 2158 Icon.createWithResource( 2159 context, 2160 com.android.settingslib.R.drawable.ic_3g_mobiledata 2161 ) 2162 ) 2163 .setExtras(Bundle.EMPTY) 2164 .build() 2165 ) 2166 ) 2167 2168 player.bindRecommendation(data) 2169 2170 assertThat(expandedSet.getVisibility(recSubtitle1.id)).isEqualTo(ConstraintSet.GONE) 2171 assertThat(expandedSet.getVisibility(recSubtitle2.id)).isEqualTo(ConstraintSet.GONE) 2172 assertThat(expandedSet.getVisibility(recSubtitle3.id)).isEqualTo(ConstraintSet.GONE) 2173 } 2174 2175 @Test 2176 fun bindRecommendation_noneHaveTitles_titleAndSubtitleViewsGone() { 2177 useRealConstraintSets() 2178 player.attachRecommendation(recommendationViewHolder) 2179 val data = 2180 smartspaceData.copy( 2181 recommendations = 2182 listOf( 2183 SmartspaceAction.Builder("id1", "") 2184 .setSubtitle("subtitle1") 2185 .setIcon( 2186 Icon.createWithResource( 2187 context, 2188 com.android.settingslib.R.drawable.ic_1x_mobiledata 2189 ) 2190 ) 2191 .setExtras(Bundle.EMPTY) 2192 .build(), 2193 SmartspaceAction.Builder("id2", "") 2194 .setSubtitle("subtitle2") 2195 .setIcon(Icon.createWithResource(context, R.drawable.ic_alarm)) 2196 .setExtras(Bundle.EMPTY) 2197 .build(), 2198 SmartspaceAction.Builder("id3", "") 2199 .setSubtitle("subtitle3") 2200 .setIcon( 2201 Icon.createWithResource( 2202 context, 2203 com.android.settingslib.R.drawable.ic_3g_mobiledata 2204 ) 2205 ) 2206 .setExtras(Bundle.EMPTY) 2207 .build() 2208 ) 2209 ) 2210 2211 player.bindRecommendation(data) 2212 2213 assertThat(expandedSet.getVisibility(recTitle1.id)).isEqualTo(ConstraintSet.GONE) 2214 assertThat(expandedSet.getVisibility(recTitle2.id)).isEqualTo(ConstraintSet.GONE) 2215 assertThat(expandedSet.getVisibility(recTitle3.id)).isEqualTo(ConstraintSet.GONE) 2216 assertThat(expandedSet.getVisibility(recSubtitle1.id)).isEqualTo(ConstraintSet.GONE) 2217 assertThat(expandedSet.getVisibility(recSubtitle2.id)).isEqualTo(ConstraintSet.GONE) 2218 assertThat(expandedSet.getVisibility(recSubtitle3.id)).isEqualTo(ConstraintSet.GONE) 2219 assertThat(collapsedSet.getVisibility(recTitle1.id)).isEqualTo(ConstraintSet.GONE) 2220 assertThat(collapsedSet.getVisibility(recTitle2.id)).isEqualTo(ConstraintSet.GONE) 2221 assertThat(collapsedSet.getVisibility(recTitle3.id)).isEqualTo(ConstraintSet.GONE) 2222 assertThat(collapsedSet.getVisibility(recSubtitle1.id)).isEqualTo(ConstraintSet.GONE) 2223 assertThat(collapsedSet.getVisibility(recSubtitle2.id)).isEqualTo(ConstraintSet.GONE) 2224 assertThat(collapsedSet.getVisibility(recSubtitle3.id)).isEqualTo(ConstraintSet.GONE) 2225 } 2226 2227 @Test 2228 fun bindRecommendation_setAfterExecutors() { 2229 val albumArt = getColorIcon(Color.RED) 2230 val data = 2231 smartspaceData.copy( 2232 recommendations = 2233 listOf( 2234 SmartspaceAction.Builder("id1", "title1") 2235 .setSubtitle("subtitle1") 2236 .setIcon(albumArt) 2237 .setExtras(Bundle.EMPTY) 2238 .build(), 2239 SmartspaceAction.Builder("id2", "title2") 2240 .setSubtitle("subtitle1") 2241 .setIcon(albumArt) 2242 .setExtras(Bundle.EMPTY) 2243 .build(), 2244 SmartspaceAction.Builder("id3", "title3") 2245 .setSubtitle("subtitle1") 2246 .setIcon(albumArt) 2247 .setExtras(Bundle.EMPTY) 2248 .build() 2249 ) 2250 ) 2251 2252 player.attachRecommendation(recommendationViewHolder) 2253 player.bindRecommendation(data) 2254 bgExecutor.runAllReady() 2255 mainExecutor.runAllReady() 2256 2257 verify(recCardTitle).setTextColor(any<Int>()) 2258 verify(recAppIconItem, times(3)).setImageDrawable(any<Drawable>()) 2259 verify(coverItem, times(3)).setImageDrawable(any<Drawable>()) 2260 verify(coverItem, times(3)).imageMatrix = any() 2261 } 2262 2263 @Test 2264 fun bindRecommendationWithProgressBars() { 2265 useRealConstraintSets() 2266 val albumArt = getColorIcon(Color.RED) 2267 val bundle = 2268 Bundle().apply { 2269 putInt( 2270 MediaConstants.DESCRIPTION_EXTRAS_KEY_COMPLETION_STATUS, 2271 MediaConstants.DESCRIPTION_EXTRAS_VALUE_COMPLETION_STATUS_PARTIALLY_PLAYED 2272 ) 2273 putDouble(MediaConstants.DESCRIPTION_EXTRAS_KEY_COMPLETION_PERCENTAGE, 0.5) 2274 } 2275 val data = 2276 smartspaceData.copy( 2277 recommendations = 2278 listOf( 2279 SmartspaceAction.Builder("id1", "title1") 2280 .setSubtitle("subtitle1") 2281 .setIcon(albumArt) 2282 .setExtras(bundle) 2283 .build(), 2284 SmartspaceAction.Builder("id2", "title2") 2285 .setSubtitle("subtitle1") 2286 .setIcon(albumArt) 2287 .setExtras(Bundle.EMPTY) 2288 .build(), 2289 SmartspaceAction.Builder("id3", "title3") 2290 .setSubtitle("subtitle1") 2291 .setIcon(albumArt) 2292 .setExtras(Bundle.EMPTY) 2293 .build() 2294 ) 2295 ) 2296 2297 player.attachRecommendation(recommendationViewHolder) 2298 player.bindRecommendation(data) 2299 2300 verify(recProgressBar1).setProgress(50) 2301 verify(recProgressBar1).visibility = View.VISIBLE 2302 verify(recProgressBar2).visibility = View.GONE 2303 verify(recProgressBar3).visibility = View.GONE 2304 assertThat(recSubtitle1.visibility).isEqualTo(View.GONE) 2305 assertThat(recSubtitle2.visibility).isEqualTo(View.VISIBLE) 2306 assertThat(recSubtitle3.visibility).isEqualTo(View.VISIBLE) 2307 } 2308 2309 @Test 2310 fun bindRecommendation_carouselNotFitThreeRecs_OrientationPortrait() { 2311 useRealConstraintSets() 2312 val albumArt = getColorIcon(Color.RED) 2313 val data = 2314 smartspaceData.copy( 2315 recommendations = 2316 listOf( 2317 SmartspaceAction.Builder("id1", "title1") 2318 .setSubtitle("subtitle1") 2319 .setIcon(albumArt) 2320 .setExtras(Bundle.EMPTY) 2321 .build(), 2322 SmartspaceAction.Builder("id2", "title2") 2323 .setSubtitle("subtitle1") 2324 .setIcon(albumArt) 2325 .setExtras(Bundle.EMPTY) 2326 .build(), 2327 SmartspaceAction.Builder("id3", "title3") 2328 .setSubtitle("subtitle1") 2329 .setIcon(albumArt) 2330 .setExtras(Bundle.EMPTY) 2331 .build() 2332 ) 2333 ) 2334 2335 // set the screen width less than the width of media controls. 2336 player.context.resources.configuration.screenWidthDp = 350 2337 player.context.resources.configuration.orientation = Configuration.ORIENTATION_PORTRAIT 2338 player.attachRecommendation(recommendationViewHolder) 2339 player.bindRecommendation(data) 2340 2341 val res = player.context.resources 2342 val displayAvailableWidth = 2343 TypedValue.applyDimension(TypedValue.COMPLEX_UNIT_DIP, 350f, res.displayMetrics).toInt() 2344 val recCoverWidth: Int = 2345 (res.getDimensionPixelSize(R.dimen.qs_media_rec_album_width) + 2346 res.getDimensionPixelSize(R.dimen.qs_media_info_spacing) * 2) 2347 val numOfRecs = displayAvailableWidth / recCoverWidth 2348 2349 assertThat(player.numberOfFittedRecommendations).isEqualTo(numOfRecs) 2350 recommendationViewHolder.mediaCoverContainers.forEachIndexed { index, container -> 2351 if (index < numOfRecs) { 2352 assertThat(expandedSet.getVisibility(container.id)).isEqualTo(ConstraintSet.VISIBLE) 2353 assertThat(collapsedSet.getVisibility(container.id)) 2354 .isEqualTo(ConstraintSet.VISIBLE) 2355 } else { 2356 assertThat(expandedSet.getVisibility(container.id)).isEqualTo(ConstraintSet.GONE) 2357 assertThat(collapsedSet.getVisibility(container.id)).isEqualTo(ConstraintSet.GONE) 2358 } 2359 } 2360 } 2361 2362 @Test 2363 fun bindRecommendation_carouselNotFitThreeRecs_OrientationLandscape() { 2364 useRealConstraintSets() 2365 val albumArt = getColorIcon(Color.RED) 2366 val data = 2367 smartspaceData.copy( 2368 recommendations = 2369 listOf( 2370 SmartspaceAction.Builder("id1", "title1") 2371 .setSubtitle("subtitle1") 2372 .setIcon(albumArt) 2373 .setExtras(Bundle.EMPTY) 2374 .build(), 2375 SmartspaceAction.Builder("id2", "title2") 2376 .setSubtitle("subtitle1") 2377 .setIcon(albumArt) 2378 .setExtras(Bundle.EMPTY) 2379 .build(), 2380 SmartspaceAction.Builder("id3", "title3") 2381 .setSubtitle("subtitle1") 2382 .setIcon(albumArt) 2383 .setExtras(Bundle.EMPTY) 2384 .build() 2385 ) 2386 ) 2387 2388 // set the screen width less than the width of media controls. 2389 // We should have dp width less than 378 to test. In landscape we should have 2x. 2390 player.context.resources.configuration.screenWidthDp = 700 2391 player.context.resources.configuration.orientation = Configuration.ORIENTATION_LANDSCAPE 2392 player.attachRecommendation(recommendationViewHolder) 2393 player.bindRecommendation(data) 2394 2395 val res = player.context.resources 2396 val displayAvailableWidth = 2397 TypedValue.applyDimension(TypedValue.COMPLEX_UNIT_DIP, 350f, res.displayMetrics).toInt() 2398 val recCoverWidth: Int = 2399 (res.getDimensionPixelSize(R.dimen.qs_media_rec_album_width) + 2400 res.getDimensionPixelSize(R.dimen.qs_media_info_spacing) * 2) 2401 val numOfRecs = displayAvailableWidth / recCoverWidth 2402 2403 assertThat(player.numberOfFittedRecommendations).isEqualTo(numOfRecs) 2404 recommendationViewHolder.mediaCoverContainers.forEachIndexed { index, container -> 2405 if (index < numOfRecs) { 2406 assertThat(expandedSet.getVisibility(container.id)).isEqualTo(ConstraintSet.VISIBLE) 2407 assertThat(collapsedSet.getVisibility(container.id)) 2408 .isEqualTo(ConstraintSet.VISIBLE) 2409 } else { 2410 assertThat(expandedSet.getVisibility(container.id)).isEqualTo(ConstraintSet.GONE) 2411 assertThat(collapsedSet.getVisibility(container.id)).isEqualTo(ConstraintSet.GONE) 2412 } 2413 } 2414 } 2415 2416 @Test 2417 fun addTwoRecommendationGradients_differentStates() { 2418 // Setup redArtwork and its color scheme. 2419 val redArt = getColorIcon(Color.RED) 2420 val redWallpaperColor = player.getWallpaperColor(redArt) 2421 val redColorScheme = ColorScheme(redWallpaperColor, true, Style.CONTENT) 2422 2423 // Setup greenArt and its color scheme. 2424 val greenArt = getColorIcon(Color.GREEN) 2425 val greenWallpaperColor = player.getWallpaperColor(greenArt) 2426 val greenColorScheme = ColorScheme(greenWallpaperColor, true, Style.CONTENT) 2427 2428 // Add gradient to both icons. 2429 val redArtwork = player.addGradientToRecommendationAlbum(redArt, redColorScheme, 10, 10) 2430 val greenArtwork = 2431 player.addGradientToRecommendationAlbum(greenArt, greenColorScheme, 10, 10) 2432 2433 // They should have different constant states as they have different gradient color. 2434 assertThat(redArtwork.getDrawable(1).constantState) 2435 .isNotEqualTo(greenArtwork.getDrawable(1).constantState) 2436 } 2437 2438 @Test 2439 fun onButtonClick_playsTouchRipple() { 2440 val semanticActions = 2441 MediaButton( 2442 playOrPause = 2443 MediaAction( 2444 icon = null, 2445 action = {}, 2446 contentDescription = "play", 2447 background = null 2448 ) 2449 ) 2450 val data = mediaData.copy(semanticActions = semanticActions) 2451 player.attachPlayer(viewHolder) 2452 player.bindPlayer(data, KEY) 2453 2454 viewHolder.actionPlayPause.callOnClick() 2455 2456 assertThat(viewHolder.multiRippleView.ripples.size).isEqualTo(1) 2457 } 2458 2459 @Test 2460 fun playTurbulenceNoise_finishesAfterDuration() { 2461 val semanticActions = 2462 MediaButton( 2463 playOrPause = 2464 MediaAction( 2465 icon = null, 2466 action = {}, 2467 contentDescription = "play", 2468 background = null 2469 ) 2470 ) 2471 val data = mediaData.copy(semanticActions = semanticActions) 2472 player.attachPlayer(viewHolder) 2473 player.bindPlayer(data, KEY) 2474 2475 viewHolder.actionPlayPause.callOnClick() 2476 2477 mainExecutor.execute { 2478 assertThat(turbulenceNoiseView.visibility).isEqualTo(View.VISIBLE) 2479 assertThat(loadingEffectView.visibility).isEqualTo(View.INVISIBLE) 2480 2481 clock.advanceTime( 2482 MediaControlPanel.TURBULENCE_NOISE_PLAY_DURATION + 2483 TurbulenceNoiseAnimationConfig.DEFAULT_EASING_DURATION_IN_MILLIS.toLong() 2484 ) 2485 2486 assertThat(turbulenceNoiseView.visibility).isEqualTo(View.INVISIBLE) 2487 assertThat(loadingEffectView.visibility).isEqualTo(View.INVISIBLE) 2488 } 2489 } 2490 2491 @Test 2492 @EnableFlags(Flags.FLAG_SHADERLIB_LOADING_EFFECT_REFACTOR) 2493 fun playTurbulenceNoise_newLoadingEffect_finishesAfterDuration() { 2494 val semanticActions = 2495 MediaButton( 2496 playOrPause = 2497 MediaAction( 2498 icon = null, 2499 action = {}, 2500 contentDescription = "play", 2501 background = null 2502 ) 2503 ) 2504 val data = mediaData.copy(semanticActions = semanticActions) 2505 player.attachPlayer(viewHolder) 2506 player.bindPlayer(data, KEY) 2507 2508 viewHolder.actionPlayPause.callOnClick() 2509 2510 mainExecutor.execute { 2511 assertThat(loadingEffectView.visibility).isEqualTo(View.VISIBLE) 2512 assertThat(turbulenceNoiseView.visibility).isEqualTo(View.INVISIBLE) 2513 2514 clock.advanceTime( 2515 MediaControlPanel.TURBULENCE_NOISE_PLAY_DURATION + 2516 TurbulenceNoiseAnimationConfig.DEFAULT_EASING_DURATION_IN_MILLIS.toLong() 2517 ) 2518 2519 assertThat(loadingEffectView.visibility).isEqualTo(View.INVISIBLE) 2520 assertThat(turbulenceNoiseView.visibility).isEqualTo(View.INVISIBLE) 2521 } 2522 } 2523 2524 @Test 2525 fun playTurbulenceNoise_whenPlaybackStateIsNotPlaying_doesNotPlayTurbulence() { 2526 val semanticActions = 2527 MediaButton( 2528 custom0 = 2529 MediaAction( 2530 icon = null, 2531 action = {}, 2532 contentDescription = "custom0", 2533 background = null 2534 ), 2535 ) 2536 val data = mediaData.copy(semanticActions = semanticActions) 2537 player.attachPlayer(viewHolder) 2538 player.bindPlayer(data, KEY) 2539 2540 viewHolder.action0.callOnClick() 2541 2542 assertThat(turbulenceNoiseView.visibility).isEqualTo(View.INVISIBLE) 2543 assertThat(loadingEffectView.visibility).isEqualTo(View.INVISIBLE) 2544 } 2545 2546 @Test 2547 @EnableFlags(Flags.FLAG_SHADERLIB_LOADING_EFFECT_REFACTOR) 2548 fun playTurbulenceNoise_newLoadingEffect_whenPlaybackStateIsNotPlaying_doesNotPlayTurbulence() { 2549 val semanticActions = 2550 MediaButton( 2551 custom0 = 2552 MediaAction( 2553 icon = null, 2554 action = {}, 2555 contentDescription = "custom0", 2556 background = null 2557 ), 2558 ) 2559 val data = mediaData.copy(semanticActions = semanticActions) 2560 player.attachPlayer(viewHolder) 2561 player.bindPlayer(data, KEY) 2562 2563 viewHolder.action0.callOnClick() 2564 2565 assertThat(loadingEffectView.visibility).isEqualTo(View.INVISIBLE) 2566 assertThat(turbulenceNoiseView.visibility).isEqualTo(View.INVISIBLE) 2567 } 2568 2569 @Test 2570 fun outputSwitcher_hasCustomIntent_openOverLockscreen() { 2571 // When the device for a media player has an intent that opens over lockscreen 2572 val pendingIntent = mock(PendingIntent::class.java) 2573 whenever(pendingIntent.isActivity).thenReturn(true) 2574 whenever(keyguardStateController.isShowing).thenReturn(true) 2575 whenever(activityIntentHelper.wouldPendingShowOverLockscreen(any(), any())).thenReturn(true) 2576 2577 val customDevice = device.copy(intent = pendingIntent) 2578 val dataWithDevice = mediaData.copy(device = customDevice) 2579 player.attachPlayer(viewHolder) 2580 player.bindPlayer(dataWithDevice, KEY) 2581 2582 // When the user taps on the output switcher, 2583 seamless.callOnClick() 2584 2585 // Then we send the pending intent as is, without modifying the original intent 2586 verify(pendingIntent).send(any<Bundle>()) 2587 verify(pendingIntent, never()).getIntent() 2588 } 2589 2590 @Test 2591 fun outputSwitcher_hasCustomIntent_requiresUnlock() { 2592 // When the device for a media player has an intent that cannot open over lockscreen 2593 val pendingIntent = mock(PendingIntent::class.java) 2594 whenever(pendingIntent.isActivity).thenReturn(true) 2595 whenever(keyguardStateController.isShowing).thenReturn(true) 2596 whenever(activityIntentHelper.wouldPendingShowOverLockscreen(any(), any())) 2597 .thenReturn(false) 2598 2599 val customDevice = device.copy(intent = pendingIntent) 2600 val dataWithDevice = mediaData.copy(device = customDevice) 2601 player.attachPlayer(viewHolder) 2602 player.bindPlayer(dataWithDevice, KEY) 2603 2604 // When the user taps on the output switcher, 2605 seamless.callOnClick() 2606 2607 // Then we request keyguard dismissal 2608 verify(activityStarter).postStartActivityDismissingKeyguard(eq(pendingIntent)) 2609 } 2610 2611 private fun getColorIcon(color: Int): Icon { 2612 val bmp = Bitmap.createBitmap(10, 10, Bitmap.Config.ARGB_8888) 2613 val canvas = Canvas(bmp) 2614 canvas.drawColor(color) 2615 return Icon.createWithBitmap(bmp) 2616 } 2617 2618 private fun getScrubbingChangeListener(): SeekBarViewModel.ScrubbingChangeListener { 2619 val captor = argumentCaptor<SeekBarViewModel.ScrubbingChangeListener>() 2620 verify(seekBarViewModel).setScrubbingChangeListener(captor.capture()) 2621 return captor.lastValue 2622 } 2623 2624 private fun getEnabledChangeListener(): SeekBarViewModel.EnabledChangeListener { 2625 val captor = argumentCaptor<SeekBarViewModel.EnabledChangeListener>() 2626 verify(seekBarViewModel).setEnabledChangeListener(captor.capture()) 2627 return captor.lastValue 2628 } 2629 2630 /** 2631 * Update our test to use real ConstraintSets instead of mocks. 2632 * 2633 * Some item visibilities, such as the seekbar visibility, are dependent on other action's 2634 * visibilities. If we use mocks for the ConstraintSets, then action visibility changes are just 2635 * thrown away instead of being saved for reference later. This method sets us up to use 2636 * ConstraintSets so that we do save visibility changes. 2637 * 2638 * TODO(b/229740380): Can/should we use real expanded and collapsed sets for all tests? 2639 */ 2640 private fun useRealConstraintSets() { 2641 expandedSet = ConstraintSet() 2642 collapsedSet = ConstraintSet() 2643 whenever(mediaViewController.expandedLayout).thenReturn(expandedSet) 2644 whenever(mediaViewController.collapsedLayout).thenReturn(collapsedSet) 2645 } 2646 } 2647