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