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