1 /* <lambda>null2 * Copyright (C) 2022 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.annotation.WorkerThread 20 import android.app.PendingIntent 21 import android.content.Context 22 import android.content.Intent 23 import android.content.res.ColorStateList 24 import android.content.res.Configuration 25 import android.database.ContentObserver 26 import android.os.UserHandle 27 import android.provider.Settings 28 import android.provider.Settings.ACTION_MEDIA_CONTROLS_SETTINGS 29 import android.util.Log 30 import android.util.MathUtils 31 import android.view.LayoutInflater 32 import android.view.View 33 import android.view.ViewGroup 34 import android.view.animation.PathInterpolator 35 import android.widget.LinearLayout 36 import androidx.annotation.VisibleForTesting 37 import androidx.lifecycle.Lifecycle 38 import androidx.lifecycle.repeatOnLifecycle 39 import androidx.recyclerview.widget.DiffUtil 40 import com.android.app.tracing.coroutines.launchTraced as launch 41 import com.android.app.tracing.traceSection 42 import com.android.internal.logging.InstanceId 43 import com.android.keyguard.KeyguardUpdateMonitor 44 import com.android.keyguard.KeyguardUpdateMonitorCallback 45 import com.android.systemui.Dumpable 46 import com.android.systemui.Flags.mediaControlsUmoInflationInBackground 47 import com.android.systemui.dagger.SysUISingleton 48 import com.android.systemui.dagger.qualifiers.Application 49 import com.android.systemui.dagger.qualifiers.Background 50 import com.android.systemui.dagger.qualifiers.Main 51 import com.android.systemui.deviceentry.domain.interactor.DeviceEntryInteractor 52 import com.android.systemui.dump.DumpManager 53 import com.android.systemui.keyguard.domain.interactor.KeyguardTransitionInteractor 54 import com.android.systemui.keyguard.shared.model.Edge 55 import com.android.systemui.keyguard.shared.model.KeyguardState.GONE 56 import com.android.systemui.keyguard.shared.model.KeyguardState.LOCKSCREEN 57 import com.android.systemui.keyguard.shared.model.TransitionState 58 import com.android.systemui.lifecycle.repeatWhenAttached 59 import com.android.systemui.media.controls.domain.pipeline.MediaDataManager 60 import com.android.systemui.media.controls.shared.model.MediaData 61 import com.android.systemui.media.controls.shared.model.SmartspaceMediaData 62 import com.android.systemui.media.controls.ui.binder.MediaControlViewBinder 63 import com.android.systemui.media.controls.ui.binder.MediaRecommendationsViewBinder 64 import com.android.systemui.media.controls.ui.controller.MediaControlPanel.SMARTSPACE_CARD_DISMISS_EVENT 65 import com.android.systemui.media.controls.ui.util.MediaViewModelCallback 66 import com.android.systemui.media.controls.ui.util.MediaViewModelListUpdateCallback 67 import com.android.systemui.media.controls.ui.view.MediaCarouselScrollHandler 68 import com.android.systemui.media.controls.ui.view.MediaHostState 69 import com.android.systemui.media.controls.ui.view.MediaScrollView 70 import com.android.systemui.media.controls.ui.view.MediaViewHolder 71 import com.android.systemui.media.controls.ui.view.RecommendationViewHolder 72 import com.android.systemui.media.controls.ui.viewmodel.MediaCarouselViewModel 73 import com.android.systemui.media.controls.ui.viewmodel.MediaCommonViewModel 74 import com.android.systemui.media.controls.util.MediaFlags 75 import com.android.systemui.media.controls.util.MediaUiEventLogger 76 import com.android.systemui.media.controls.util.SmallHash 77 import com.android.systemui.plugins.ActivityStarter 78 import com.android.systemui.plugins.FalsingManager 79 import com.android.systemui.qs.PageIndicator 80 import com.android.systemui.res.R 81 import com.android.systemui.scene.shared.flag.SceneContainerFlag 82 import com.android.systemui.scene.shared.model.Scenes 83 import com.android.systemui.shared.system.SysUiStatsLog 84 import com.android.systemui.shared.system.SysUiStatsLog.SMARTSPACE_CARD_REPORTED 85 import com.android.systemui.shared.system.SysUiStatsLog.SMART_SPACE_CARD_REPORTED__CARD_TYPE__UNKNOWN_CARD 86 import com.android.systemui.shared.system.SysUiStatsLog.SMART_SPACE_CARD_REPORTED__DISPLAY_SURFACE__DREAM_OVERLAY as SSPACE_CARD_REPORTED__DREAM_OVERLAY 87 import com.android.systemui.shared.system.SysUiStatsLog.SMART_SPACE_CARD_REPORTED__DISPLAY_SURFACE__LOCKSCREEN as SSPACE_CARD_REPORTED__LOCKSCREEN 88 import com.android.systemui.shared.system.SysUiStatsLog.SMART_SPACE_CARD_REPORTED__DISPLAY_SURFACE__SHADE 89 import com.android.systemui.statusbar.notification.collection.provider.OnReorderingAllowedListener 90 import com.android.systemui.statusbar.notification.collection.provider.VisualStabilityProvider 91 import com.android.systemui.statusbar.policy.ConfigurationController 92 import com.android.systemui.util.Utils 93 import com.android.systemui.util.animation.UniqueObjectHostView 94 import com.android.systemui.util.animation.requiresRemeasuring 95 import com.android.systemui.util.concurrency.DelayableExecutor 96 import com.android.systemui.util.settings.GlobalSettings 97 import com.android.systemui.util.settings.SecureSettings 98 import com.android.systemui.util.settings.SettingsProxyExt.observerFlow 99 import com.android.systemui.util.time.SystemClock 100 import java.io.PrintWriter 101 import java.util.Locale 102 import java.util.TreeMap 103 import java.util.concurrent.Executor 104 import javax.inject.Inject 105 import javax.inject.Provider 106 import kotlinx.coroutines.CoroutineDispatcher 107 import kotlinx.coroutines.CoroutineScope 108 import kotlinx.coroutines.ExperimentalCoroutinesApi 109 import kotlinx.coroutines.Job 110 import kotlinx.coroutines.flow.SharingStarted 111 import kotlinx.coroutines.flow.collectLatest 112 import kotlinx.coroutines.flow.distinctUntilChanged 113 import kotlinx.coroutines.flow.filter 114 import kotlinx.coroutines.flow.flowOn 115 import kotlinx.coroutines.flow.map 116 import kotlinx.coroutines.flow.merge 117 import kotlinx.coroutines.flow.onStart 118 import kotlinx.coroutines.flow.stateIn 119 import kotlinx.coroutines.withContext 120 121 private const val TAG = "MediaCarouselController" 122 private val settingsIntent = Intent().setAction(ACTION_MEDIA_CONTROLS_SETTINGS) 123 private val DEBUG = Log.isLoggable(TAG, Log.DEBUG) 124 125 /** 126 * Class that is responsible for keeping the view carousel up to date. This also handles changes in 127 * state and applies them to the media carousel like the expansion. 128 */ 129 @OptIn(ExperimentalCoroutinesApi::class) 130 @SysUISingleton 131 class MediaCarouselController 132 @Inject 133 constructor( 134 @Application applicationScope: CoroutineScope, 135 private val context: Context, 136 private val mediaControlPanelFactory: Provider<MediaControlPanel>, 137 private val visualStabilityProvider: VisualStabilityProvider, 138 private val mediaHostStatesManager: MediaHostStatesManager, 139 private val activityStarter: ActivityStarter, 140 private val systemClock: SystemClock, 141 @Main private val mainDispatcher: CoroutineDispatcher, 142 @Main private val uiExecutor: DelayableExecutor, 143 @Background private val bgExecutor: Executor, 144 @Background private val backgroundDispatcher: CoroutineDispatcher, 145 private val mediaManager: MediaDataManager, 146 configurationController: ConfigurationController, 147 private val falsingManager: FalsingManager, 148 dumpManager: DumpManager, 149 private val logger: MediaUiEventLogger, 150 private val debugLogger: MediaCarouselControllerLogger, 151 private val mediaFlags: MediaFlags, 152 private val keyguardUpdateMonitor: KeyguardUpdateMonitor, 153 private val keyguardTransitionInteractor: KeyguardTransitionInteractor, 154 private val globalSettings: GlobalSettings, 155 private val secureSettings: SecureSettings, 156 private val mediaCarouselViewModel: MediaCarouselViewModel, 157 private val mediaViewControllerFactory: Provider<MediaViewController>, 158 private val deviceEntryInteractor: DeviceEntryInteractor, 159 ) : Dumpable { 160 /** The current width of the carousel */ 161 var currentCarouselWidth: Int = 0 162 private set 163 164 /** The current height of the carousel */ 165 private var currentCarouselHeight: Int = 0 166 167 /** Are we currently showing only active players */ 168 private var currentlyShowingOnlyActive: Boolean = false 169 170 /** Is the player currently visible (at the end of the transformation */ 171 private var playersVisible: Boolean = false 172 173 /** Are we currently disabling pagination only allowing one media session to show */ 174 private var currentlyDisablePagination: Boolean = false 175 176 /** 177 * The desired location where we'll be at the end of the transformation. Usually this matches 178 * the end location, except when we're still waiting on a state update call. 179 */ 180 @MediaLocation private var desiredLocation: Int = MediaHierarchyManager.LOCATION_UNKNOWN 181 182 /** 183 * The ending location of the view where it ends when all animations and transitions have 184 * finished 185 */ 186 @MediaLocation 187 @VisibleForTesting 188 var currentEndLocation: Int = MediaHierarchyManager.LOCATION_UNKNOWN 189 190 /** 191 * The ending location of the view where it ends when all animations and transitions have 192 * finished 193 */ 194 @MediaLocation private var currentStartLocation: Int = MediaHierarchyManager.LOCATION_UNKNOWN 195 196 /** The progress of the transition or 1.0 if there is no transition happening */ 197 private var currentTransitionProgress: Float = 1.0f 198 199 /** The measured width of the carousel */ 200 private var carouselMeasureWidth: Int = 0 201 202 /** The measured height of the carousel */ 203 private var carouselMeasureHeight: Int = 0 204 private var desiredHostState: MediaHostState? = null 205 @VisibleForTesting var mediaCarousel: MediaScrollView 206 val mediaCarouselScrollHandler: MediaCarouselScrollHandler 207 val mediaFrame: ViewGroup 208 209 @VisibleForTesting 210 lateinit var settingsButton: View 211 private set 212 213 private val mediaContent: ViewGroup 214 @VisibleForTesting var pageIndicator: PageIndicator 215 private var needsReordering: Boolean = false 216 private var isUserInitiatedRemovalQueued: Boolean = false 217 private var keysNeedRemoval = mutableSetOf<String>() 218 var shouldScrollToKey: Boolean = false 219 private var isRtl: Boolean = false 220 set(value) { 221 if (value != field) { 222 field = value 223 mediaFrame.layoutDirection = 224 if (value) View.LAYOUT_DIRECTION_RTL else View.LAYOUT_DIRECTION_LTR 225 mediaCarouselScrollHandler.scrollToStart() 226 } 227 } 228 229 private var carouselLocale: Locale? = null 230 231 private val animationScaleObserver: ContentObserver = 232 object : ContentObserver(uiExecutor, 0) { 233 override fun onChange(selfChange: Boolean) { 234 if (!SceneContainerFlag.isEnabled) { 235 MediaPlayerData.players().forEach { it.updateAnimatorDurationScale() } 236 } else { 237 controllerById.values.forEach { it.updateAnimatorDurationScale() } 238 } 239 } 240 } 241 242 private var allowMediaPlayerOnLockScreen = false 243 244 /** Whether the media card currently has the "expanded" layout */ 245 @VisibleForTesting 246 var currentlyExpanded = true 247 set(value) { 248 if (field != value) { 249 field = value 250 updateSeekbarListening(mediaCarouselScrollHandler.visibleToUser) 251 } 252 } 253 254 companion object { 255 val TRANSFORM_BEZIER = PathInterpolator(0.68F, 0F, 0F, 1F) 256 257 fun calculateAlpha( 258 squishinessFraction: Float, 259 startPosition: Float, 260 endPosition: Float, 261 ): Float { 262 val transformFraction = 263 MathUtils.constrain( 264 (squishinessFraction - startPosition) / (endPosition - startPosition), 265 0F, 266 1F, 267 ) 268 return TRANSFORM_BEZIER.getInterpolation(transformFraction) 269 } 270 } 271 272 private val configListener = 273 object : ConfigurationController.ConfigurationListener { 274 275 override fun onDensityOrFontScaleChanged() { 276 // System font changes should only happen when UMO is offscreen or a flicker may 277 // occur 278 updatePlayers(recreateMedia = true) 279 inflateSettingsButton() 280 } 281 282 override fun onThemeChanged() { 283 updatePlayers(recreateMedia = false) 284 inflateSettingsButton() 285 } 286 287 override fun onConfigChanged(newConfig: Configuration?) { 288 if (newConfig == null) return 289 isRtl = newConfig.layoutDirection == View.LAYOUT_DIRECTION_RTL 290 } 291 292 override fun onUiModeChanged() { 293 updatePlayers(recreateMedia = false) 294 inflateSettingsButton() 295 } 296 297 override fun onLocaleListChanged() { 298 // Update players only if system primary language changes. 299 if (carouselLocale != context.resources.configuration.locales.get(0)) { 300 carouselLocale = context.resources.configuration.locales.get(0) 301 updatePlayers(recreateMedia = true) 302 inflateSettingsButton() 303 } 304 } 305 } 306 307 private val keyguardUpdateMonitorCallback = 308 object : KeyguardUpdateMonitorCallback() { 309 override fun onStrongAuthStateChanged(userId: Int) { 310 if (keyguardUpdateMonitor.isUserInLockdown(userId)) { 311 debugLogger.logCarouselHidden() 312 hideMediaCarousel() 313 } else if (keyguardUpdateMonitor.isUserUnlocked(userId)) { 314 debugLogger.logCarouselVisible() 315 showMediaCarousel() 316 } 317 } 318 } 319 320 /** 321 * Update MediaCarouselScrollHandler.visibleToUser to reflect media card container visibility. 322 * It will be called when the container is out of view. 323 */ 324 lateinit var updateUserVisibility: () -> Unit 325 var updateHostVisibility: () -> Unit = {} 326 set(value) { 327 field = value 328 mediaCarouselViewModel.updateHostVisibility = value 329 } 330 331 private val isReorderingAllowed: Boolean 332 get() = visualStabilityProvider.isReorderingAllowed 333 334 /** Size provided by the scene framework container */ 335 private var widthInSceneContainerPx = 0 336 private var heightInSceneContainerPx = 0 337 338 private val controllerById = mutableMapOf<String, MediaViewController>() 339 private val commonViewModels = mutableListOf<MediaCommonViewModel>() 340 341 private val isOnGone = 342 keyguardTransitionInteractor 343 .isFinishedIn(Scenes.Gone, GONE) 344 .stateIn(applicationScope, SharingStarted.Eagerly, true) 345 346 init { 347 dumpManager.registerDumpable(TAG, this) 348 mediaFrame = inflateMediaCarousel() 349 mediaCarousel = mediaFrame.requireViewById(R.id.media_carousel_scroller) 350 pageIndicator = mediaFrame.requireViewById(R.id.media_page_indicator) 351 mediaCarouselScrollHandler = 352 MediaCarouselScrollHandler( 353 mediaCarousel, 354 pageIndicator, 355 uiExecutor, 356 this::onSwipeToDismiss, 357 this::updatePageIndicatorLocation, 358 this::updateSeekbarListening, 359 this::closeGuts, 360 falsingManager, 361 this::logSmartspaceImpression, 362 logger, 363 ) 364 carouselLocale = context.resources.configuration.locales.get(0) 365 isRtl = context.resources.configuration.layoutDirection == View.LAYOUT_DIRECTION_RTL 366 inflateSettingsButton() 367 mediaContent = mediaCarousel.requireViewById(R.id.media_carousel) 368 configurationController.addCallback(configListener) 369 if (!SceneContainerFlag.isEnabled) { 370 setUpListeners() 371 } else { 372 val visualStabilityCallback = OnReorderingAllowedListener { 373 mediaCarouselViewModel.onReorderingAllowed() 374 375 // Update user visibility so that no extra impression will be logged when 376 // activeMediaIndex resets to 0 377 if (this::updateUserVisibility.isInitialized) { 378 updateUserVisibility() 379 } 380 381 // Let's reset our scroll position 382 mediaCarouselScrollHandler.scrollToStart() 383 } 384 visualStabilityProvider.addPersistentReorderingAllowedListener(visualStabilityCallback) 385 } 386 mediaFrame.addOnLayoutChangeListener { _, _, _, _, _, _, _, _, _ -> 387 // The pageIndicator is not laid out yet when we get the current state update, 388 // Lets make sure we have the right dimensions 389 updatePageIndicatorLocation() 390 } 391 mediaHostStatesManager.addCallback( 392 object : MediaHostStatesManager.Callback { 393 override fun onHostStateChanged( 394 @MediaLocation location: Int, 395 mediaHostState: MediaHostState, 396 ) { 397 updateUserVisibility() 398 if (location == desiredLocation) { 399 onDesiredLocationChanged(desiredLocation, mediaHostState, animate = false) 400 } 401 } 402 } 403 ) 404 keyguardUpdateMonitor.registerCallback(keyguardUpdateMonitorCallback) 405 mediaCarousel.repeatWhenAttached { 406 repeatOnLifecycle(Lifecycle.State.STARTED) { 407 listenForAnyStateToGoneKeyguardTransition(this) 408 listenForAnyStateToLockscreenTransition(this) 409 410 if (!SceneContainerFlag.isEnabled) return@repeatOnLifecycle 411 listenForMediaItemsChanges(this) 412 } 413 } 414 listenForLockscreenSettingChanges(applicationScope) 415 416 // Notifies all active players about animation scale changes. 417 bgExecutor.execute { 418 globalSettings.registerContentObserverSync( 419 Settings.Global.getUriFor(Settings.Global.ANIMATOR_DURATION_SCALE), 420 animationScaleObserver, 421 ) 422 } 423 } 424 425 private fun setUpListeners() { 426 val visualStabilityCallback = OnReorderingAllowedListener { 427 if (needsReordering) { 428 needsReordering = false 429 reorderAllPlayers(previousVisiblePlayerKey = null) 430 } 431 432 keysNeedRemoval.forEach { 433 removePlayer(it, userInitiated = isUserInitiatedRemovalQueued) 434 } 435 if (keysNeedRemoval.size > 0) { 436 // Carousel visibility may need to be updated after late removals 437 updateHostVisibility() 438 } 439 keysNeedRemoval.clear() 440 isUserInitiatedRemovalQueued = false 441 442 // Update user visibility so that no extra impression will be logged when 443 // activeMediaIndex resets to 0 444 if (this::updateUserVisibility.isInitialized) { 445 updateUserVisibility() 446 } 447 448 // Let's reset our scroll position 449 mediaCarouselScrollHandler.scrollToStart() 450 } 451 visualStabilityProvider.addPersistentReorderingAllowedListener(visualStabilityCallback) 452 mediaManager.addListener( 453 object : MediaDataManager.Listener { 454 override fun onMediaDataLoaded( 455 key: String, 456 oldKey: String?, 457 data: MediaData, 458 immediately: Boolean, 459 receivedSmartspaceCardLatency: Int, 460 isSsReactivated: Boolean, 461 ) { 462 debugLogger.logMediaLoaded(key, data.active) 463 val onUiExecutionEnd = 464 if (mediaControlsUmoInflationInBackground()) { 465 Runnable { 466 if (immediately) { 467 updateHostVisibility() 468 } 469 } 470 } else { 471 null 472 } 473 if (addOrUpdatePlayer(key, oldKey, data, isSsReactivated, onUiExecutionEnd)) { 474 // Log card received if a new resumable media card is added 475 MediaPlayerData.getMediaPlayer(key)?.let { 476 logSmartspaceCardReported( 477 759, // SMARTSPACE_CARD_RECEIVED 478 it.mSmartspaceId, 479 it.mUid, 480 surfaces = 481 intArrayOf( 482 SMART_SPACE_CARD_REPORTED__DISPLAY_SURFACE__SHADE, 483 SSPACE_CARD_REPORTED__LOCKSCREEN, 484 SSPACE_CARD_REPORTED__DREAM_OVERLAY, 485 ), 486 rank = MediaPlayerData.getMediaPlayerIndex(key), 487 ) 488 } 489 if ( 490 mediaCarouselScrollHandler.visibleToUser && 491 mediaCarouselScrollHandler.visibleMediaIndex == 492 MediaPlayerData.getMediaPlayerIndex(key) 493 ) { 494 logSmartspaceImpression(mediaCarouselScrollHandler.qsExpanded) 495 } 496 } else if (receivedSmartspaceCardLatency != 0) { 497 // Log resume card received if resumable media card is reactivated and 498 // resume card is ranked first 499 MediaPlayerData.players().forEachIndexed { index, it -> 500 if (it.recommendationViewHolder == null) { 501 it.mSmartspaceId = 502 SmallHash.hash( 503 it.mUid + systemClock.currentTimeMillis().toInt() 504 ) 505 it.mIsImpressed = false 506 507 logSmartspaceCardReported( 508 759, // SMARTSPACE_CARD_RECEIVED 509 it.mSmartspaceId, 510 it.mUid, 511 surfaces = 512 intArrayOf( 513 SMART_SPACE_CARD_REPORTED__DISPLAY_SURFACE__SHADE, 514 SSPACE_CARD_REPORTED__LOCKSCREEN, 515 SSPACE_CARD_REPORTED__DREAM_OVERLAY, 516 ), 517 rank = index, 518 receivedLatencyMillis = receivedSmartspaceCardLatency, 519 ) 520 } 521 } 522 // If media container area already visible to the user, log impression for 523 // reactivated card. 524 if ( 525 mediaCarouselScrollHandler.visibleToUser && 526 !mediaCarouselScrollHandler.qsExpanded 527 ) { 528 logSmartspaceImpression(mediaCarouselScrollHandler.qsExpanded) 529 } 530 } 531 532 val canRemove = data.isPlaying?.let { !it } ?: data.isClearable && !data.active 533 if (canRemove && !Utils.useMediaResumption(context)) { 534 // This media control is both paused and timed out, and the resumption 535 // setting is off - let's remove it 536 if (isReorderingAllowed) { 537 onMediaDataRemoved(key, userInitiated = MediaPlayerData.isSwipedAway) 538 } else { 539 isUserInitiatedRemovalQueued = MediaPlayerData.isSwipedAway 540 keysNeedRemoval.add(key) 541 } 542 } else { 543 keysNeedRemoval.remove(key) 544 } 545 MediaPlayerData.isSwipedAway = false 546 } 547 548 override fun onSmartspaceMediaDataLoaded( 549 key: String, 550 data: SmartspaceMediaData, 551 shouldPrioritize: Boolean, 552 ) { 553 debugLogger.logRecommendationLoaded(key, data.isActive) 554 // Log the case where the hidden media carousel with the existed inactive resume 555 // media is shown by the Smartspace signal. 556 if (data.isActive) { 557 val hasActivatedExistedResumeMedia = 558 !mediaManager.hasActiveMedia() && 559 mediaManager.hasAnyMedia() && 560 shouldPrioritize 561 if (hasActivatedExistedResumeMedia) { 562 // Log resume card received if resumable media card is reactivated and 563 // recommendation card is valid and ranked first 564 MediaPlayerData.players().forEachIndexed { index, it -> 565 if (it.recommendationViewHolder == null) { 566 it.mSmartspaceId = 567 SmallHash.hash( 568 it.mUid + systemClock.currentTimeMillis().toInt() 569 ) 570 it.mIsImpressed = false 571 572 logSmartspaceCardReported( 573 759, // SMARTSPACE_CARD_RECEIVED 574 it.mSmartspaceId, 575 it.mUid, 576 surfaces = 577 intArrayOf( 578 SMART_SPACE_CARD_REPORTED__DISPLAY_SURFACE__SHADE, 579 SSPACE_CARD_REPORTED__LOCKSCREEN, 580 SSPACE_CARD_REPORTED__DREAM_OVERLAY, 581 ), 582 rank = index, 583 receivedLatencyMillis = 584 (systemClock.currentTimeMillis() - 585 data.headphoneConnectionTimeMillis) 586 .toInt(), 587 ) 588 } 589 } 590 } 591 addSmartspaceMediaRecommendations(key, data, shouldPrioritize) 592 MediaPlayerData.getMediaPlayer(key)?.let { 593 logSmartspaceCardReported( 594 759, // SMARTSPACE_CARD_RECEIVED 595 it.mSmartspaceId, 596 it.mUid, 597 surfaces = 598 intArrayOf( 599 SMART_SPACE_CARD_REPORTED__DISPLAY_SURFACE__SHADE, 600 SSPACE_CARD_REPORTED__LOCKSCREEN, 601 SSPACE_CARD_REPORTED__DREAM_OVERLAY, 602 ), 603 rank = MediaPlayerData.getMediaPlayerIndex(key), 604 receivedLatencyMillis = 605 (systemClock.currentTimeMillis() - 606 data.headphoneConnectionTimeMillis) 607 .toInt(), 608 ) 609 } 610 if ( 611 mediaCarouselScrollHandler.visibleToUser && 612 mediaCarouselScrollHandler.visibleMediaIndex == 613 MediaPlayerData.getMediaPlayerIndex(key) 614 ) { 615 logSmartspaceImpression(mediaCarouselScrollHandler.qsExpanded) 616 } 617 } else { 618 if (!mediaFlags.isPersistentSsCardEnabled()) { 619 // Handle update to inactive as a removal 620 onSmartspaceMediaDataRemoved(data.targetId, immediately = true) 621 } else { 622 addSmartspaceMediaRecommendations(key, data, shouldPrioritize) 623 } 624 } 625 MediaPlayerData.isSwipedAway = false 626 } 627 628 override fun onMediaDataRemoved(key: String, userInitiated: Boolean) { 629 debugLogger.logMediaRemoved(key, userInitiated) 630 removePlayer(key, userInitiated = userInitiated) 631 } 632 633 override fun onSmartspaceMediaDataRemoved(key: String, immediately: Boolean) { 634 debugLogger.logRecommendationRemoved(key, immediately) 635 if (immediately || isReorderingAllowed) { 636 removePlayer(key) 637 if (!immediately) { 638 // Although it wasn't requested, we were able to process the removal 639 // immediately since reordering is allowed. So, notify hosts to update 640 updateHostVisibility() 641 } 642 } else { 643 keysNeedRemoval.add(key) 644 } 645 } 646 } 647 ) 648 } 649 650 private fun inflateSettingsButton() { 651 val settings = 652 LayoutInflater.from(context) 653 .inflate(R.layout.media_carousel_settings_button, mediaFrame, false) as View 654 if (this::settingsButton.isInitialized) { 655 mediaFrame.removeView(settingsButton) 656 } 657 settingsButton = settings 658 mediaFrame.addView(settingsButton) 659 mediaCarouselScrollHandler.onSettingsButtonUpdated(settings) 660 settingsButton.setOnClickListener { 661 logger.logCarouselSettings() 662 activityStarter.startActivity(settingsIntent, /* dismissShade= */ true) 663 } 664 } 665 666 private fun inflateMediaCarousel(): ViewGroup { 667 val mediaCarousel = 668 LayoutInflater.from(context) 669 .inflate(R.layout.media_carousel, UniqueObjectHostView(context), false) as ViewGroup 670 // Because this is inflated when not attached to the true view hierarchy, it resolves some 671 // potential issues to force that the layout direction is defined by the locale 672 // (rather than inherited from the parent, which would resolve to LTR when unattached). 673 mediaCarousel.layoutDirection = View.LAYOUT_DIRECTION_LOCALE 674 return mediaCarousel 675 } 676 677 private fun hideMediaCarousel() { 678 mediaCarousel.visibility = View.GONE 679 } 680 681 private fun showMediaCarousel() { 682 mediaCarousel.visibility = View.VISIBLE 683 } 684 685 @VisibleForTesting 686 internal fun listenForAnyStateToGoneKeyguardTransition(scope: CoroutineScope): Job { 687 return scope.launch { 688 keyguardTransitionInteractor 689 .isFinishedIn(scene = Scenes.Gone, stateWithoutSceneContainer = GONE) 690 .filter { it } 691 .collect { 692 showMediaCarousel() 693 updateHostVisibility() 694 } 695 } 696 } 697 698 @VisibleForTesting 699 internal fun listenForAnyStateToLockscreenTransition(scope: CoroutineScope): Job { 700 return scope.launch { 701 keyguardTransitionInteractor 702 .transition(Edge.create(to = LOCKSCREEN)) 703 .filter { it.transitionState == TransitionState.FINISHED } 704 .collect { 705 if (!allowMediaPlayerOnLockScreen) { 706 updateHostVisibility() 707 } 708 } 709 } 710 } 711 712 @VisibleForTesting 713 internal fun listenForLockscreenSettingChanges(scope: CoroutineScope): Job { 714 return scope.launch { 715 secureSettings 716 .observerFlow(UserHandle.USER_ALL, Settings.Secure.MEDIA_CONTROLS_LOCK_SCREEN) 717 // query to get initial value 718 .onStart { emit(Unit) } 719 .map { getMediaLockScreenSetting() } 720 .distinctUntilChanged() 721 .flowOn(backgroundDispatcher) 722 .collectLatest { 723 allowMediaPlayerOnLockScreen = it 724 updateHostVisibility() 725 } 726 } 727 } 728 729 private fun listenForMediaItemsChanges(scope: CoroutineScope): Job { 730 return scope.launch { 731 mediaCarouselViewModel.mediaItems.collectLatest { 732 val diffUtilCallback = MediaViewModelCallback(commonViewModels, it) 733 val listUpdateCallback = 734 MediaViewModelListUpdateCallback( 735 old = commonViewModels, 736 new = it, 737 onAdded = this@MediaCarouselController::onAdded, 738 onUpdated = this@MediaCarouselController::onUpdated, 739 onRemoved = this@MediaCarouselController::onRemoved, 740 onMoved = this@MediaCarouselController::onMoved, 741 ) 742 DiffUtil.calculateDiff(diffUtilCallback).dispatchUpdatesTo(listUpdateCallback) 743 setNewViewModelsList(it) 744 745 // Update host visibility when media changes. 746 merge( 747 mediaCarouselViewModel.hasAnyMediaOrRecommendations, 748 mediaCarouselViewModel.hasActiveMediaOrRecommendations, 749 ) 750 .collect { updateHostVisibility() } 751 } 752 } 753 } 754 755 private fun onAdded( 756 commonViewModel: MediaCommonViewModel, 757 position: Int, 758 configChanged: Boolean = false, 759 ) { 760 val viewController = mediaViewControllerFactory.get() 761 viewController.sizeChangedListener = this::updateCarouselDimensions 762 val lp = 763 LinearLayout.LayoutParams( 764 ViewGroup.LayoutParams.MATCH_PARENT, 765 ViewGroup.LayoutParams.WRAP_CONTENT, 766 ) 767 when (commonViewModel) { 768 is MediaCommonViewModel.MediaControl -> { 769 val viewHolder = MediaViewHolder.create(LayoutInflater.from(context), mediaContent) 770 viewController.widthInSceneContainerPx = widthInSceneContainerPx 771 viewController.heightInSceneContainerPx = heightInSceneContainerPx 772 viewController.attachPlayer(viewHolder) 773 viewController.mediaViewHolder?.player?.layoutParams = lp 774 if (configChanged) { 775 commonViewModel.controlViewModel.onMediaConfigChanged() 776 } 777 MediaControlViewBinder.bind( 778 viewHolder, 779 commonViewModel.controlViewModel, 780 viewController, 781 falsingManager, 782 backgroundDispatcher, 783 mainDispatcher, 784 ) 785 mediaContent.addView(viewHolder.player, position) 786 controllerById[commonViewModel.instanceId.toString()] = viewController 787 } 788 is MediaCommonViewModel.MediaRecommendations -> { 789 val viewHolder = 790 RecommendationViewHolder.create(LayoutInflater.from(context), mediaContent) 791 viewController.attachRecommendations(viewHolder) 792 viewController.recommendationViewHolder?.recommendations?.layoutParams = lp 793 MediaRecommendationsViewBinder.bind( 794 viewHolder, 795 commonViewModel.recsViewModel, 796 viewController, 797 falsingManager, 798 backgroundDispatcher, 799 mainDispatcher, 800 ) 801 mediaContent.addView(viewHolder.recommendations, position) 802 controllerById[commonViewModel.key] = viewController 803 } 804 } 805 onAddOrUpdateVisibleToUserCard(position, isMediaCardUpdate = false) 806 viewController.setListening(mediaCarouselScrollHandler.visibleToUser && currentlyExpanded) 807 updateViewControllerToState(viewController, noAnimation = true) 808 updatePageIndicator() 809 if ( 810 commonViewModel is MediaCommonViewModel.MediaControl && commonViewModel.isMediaFromRec 811 ) { 812 mediaCarouselScrollHandler.scrollToPlayer( 813 mediaCarouselScrollHandler.visibleMediaIndex, 814 destIndex = 0, 815 ) 816 } 817 mediaCarouselScrollHandler.onPlayersChanged() 818 mediaFrame.requiresRemeasuring = true 819 commonViewModel.onAdded(commonViewModel) 820 } 821 822 private fun onUpdated(commonViewModel: MediaCommonViewModel, position: Int) { 823 commonViewModel.onUpdated(commonViewModel) 824 updatePageIndicator() 825 mediaCarouselScrollHandler.onPlayersChanged() 826 onAddOrUpdateVisibleToUserCard( 827 position, 828 commonViewModel is MediaCommonViewModel.MediaControl, 829 ) 830 } 831 832 private fun onRemoved(commonViewModel: MediaCommonViewModel) { 833 val id = 834 when (commonViewModel) { 835 is MediaCommonViewModel.MediaControl -> commonViewModel.instanceId.toString() 836 is MediaCommonViewModel.MediaRecommendations -> commonViewModel.key 837 } 838 controllerById.remove(id)?.let { 839 when (commonViewModel) { 840 is MediaCommonViewModel.MediaControl -> { 841 mediaCarouselScrollHandler.onPrePlayerRemoved(it.mediaViewHolder!!.player) 842 mediaContent.removeView(it.mediaViewHolder!!.player) 843 } 844 is MediaCommonViewModel.MediaRecommendations -> { 845 mediaContent.removeView(it.recommendationViewHolder!!.recommendations) 846 } 847 } 848 it.onDestroy() 849 mediaCarouselScrollHandler.onPlayersChanged() 850 updatePageIndicator() 851 commonViewModel.onRemoved(true) 852 } 853 } 854 855 private fun onMoved(commonViewModel: MediaCommonViewModel, from: Int, to: Int) { 856 val id = 857 when (commonViewModel) { 858 is MediaCommonViewModel.MediaControl -> commonViewModel.instanceId.toString() 859 is MediaCommonViewModel.MediaRecommendations -> commonViewModel.key 860 } 861 controllerById[id]?.let { 862 mediaContent.removeViewAt(from) 863 when (commonViewModel) { 864 is MediaCommonViewModel.MediaControl -> { 865 mediaContent.addView(it.mediaViewHolder!!.player, to) 866 } 867 is MediaCommonViewModel.MediaRecommendations -> { 868 mediaContent.addView(it.recommendationViewHolder!!.recommendations, to) 869 } 870 } 871 } 872 updatePageIndicator() 873 mediaCarouselScrollHandler.onPlayersChanged() 874 } 875 876 private fun onAddOrUpdateVisibleToUserCard(position: Int, isMediaCardUpdate: Boolean) { 877 if ( 878 mediaCarouselScrollHandler.visibleToUser && 879 mediaCarouselScrollHandler.visibleMediaIndex == position 880 ) { 881 mediaCarouselViewModel.onCardVisibleToUser( 882 mediaCarouselScrollHandler.qsExpanded, 883 mediaCarouselScrollHandler.visibleMediaIndex, 884 currentEndLocation, 885 isMediaCardUpdate, 886 ) 887 } 888 } 889 890 private fun setNewViewModelsList(viewModels: List<MediaCommonViewModel>) { 891 commonViewModels.clear() 892 commonViewModels.addAll(viewModels) 893 894 // Ensure we only show the needed UMOs in media carousel. 895 val viewIds = 896 viewModels 897 .map { mediaCommonViewModel -> 898 when (mediaCommonViewModel) { 899 is MediaCommonViewModel.MediaControl -> 900 mediaCommonViewModel.instanceId.toString() 901 is MediaCommonViewModel.MediaRecommendations -> mediaCommonViewModel.key 902 } 903 } 904 .toHashSet() 905 controllerById 906 .filter { !viewIds.contains(it.key) } 907 .forEach { 908 mediaCarouselScrollHandler.onPrePlayerRemoved(it.value.mediaViewHolder?.player) 909 mediaContent.removeView(it.value.mediaViewHolder?.player) 910 mediaContent.removeView(it.value.recommendationViewHolder?.recommendations) 911 it.value.onDestroy() 912 mediaCarouselScrollHandler.onPlayersChanged() 913 updatePageIndicator() 914 } 915 } 916 917 private suspend fun getMediaLockScreenSetting(): Boolean { 918 return withContext(backgroundDispatcher) { 919 secureSettings.getBoolForUser( 920 Settings.Secure.MEDIA_CONTROLS_LOCK_SCREEN, 921 true, 922 UserHandle.USER_CURRENT, 923 ) 924 } 925 } 926 927 fun setSceneContainerSize(width: Int, height: Int) { 928 if (width == widthInSceneContainerPx && height == heightInSceneContainerPx) { 929 return 930 } 931 if (width <= 0 || height <= 0) { 932 // reject as invalid 933 return 934 } 935 widthInSceneContainerPx = width 936 heightInSceneContainerPx = height 937 mediaCarouselScrollHandler.playerWidthPlusPadding = 938 width + context.resources.getDimensionPixelSize(R.dimen.qs_media_padding) 939 updatePlayers(recreateMedia = true) 940 } 941 942 /** Return true if the carousel should be hidden because lockscreen is currently visible */ 943 fun isLockedAndHidden(): Boolean { 944 val isOnLockscreen = 945 if (SceneContainerFlag.isEnabled) { 946 !deviceEntryInteractor.isDeviceEntered.value 947 } else { 948 !isOnGone.value 949 } 950 return !allowMediaPlayerOnLockScreen && isOnLockscreen 951 } 952 953 private fun reorderAllPlayers( 954 previousVisiblePlayerKey: MediaPlayerData.MediaSortKey?, 955 key: String? = null, 956 ) { 957 mediaContent.removeAllViews() 958 for (mediaPlayer in MediaPlayerData.players()) { 959 mediaPlayer.mediaViewHolder?.let { mediaContent.addView(it.player) } 960 ?: mediaPlayer.recommendationViewHolder?.let { 961 mediaContent.addView(it.recommendations) 962 } 963 } 964 mediaCarouselScrollHandler.onPlayersChanged() 965 MediaPlayerData.updateVisibleMediaPlayers() 966 // Automatically scroll to the active player if needed 967 if (shouldScrollToKey) { 968 shouldScrollToKey = false 969 val mediaIndex = key?.let { MediaPlayerData.getMediaPlayerIndex(it) } ?: -1 970 if (mediaIndex != -1) { 971 previousVisiblePlayerKey?.let { 972 val previousVisibleIndex = 973 MediaPlayerData.playerKeys().indexOfFirst { key -> it == key } 974 mediaCarouselScrollHandler.scrollToPlayer(previousVisibleIndex, mediaIndex) 975 } ?: mediaCarouselScrollHandler.scrollToPlayer(destIndex = mediaIndex) 976 } 977 } else if (isRtl && mediaContent.childCount > 0) { 978 // In RTL, Scroll to the first player as it is the rightmost player in media carousel. 979 mediaCarouselScrollHandler.scrollToPlayer(destIndex = 0) 980 } 981 // Check postcondition: mediaContent should have the same number of children as there 982 // are 983 // elements in mediaPlayers. 984 if (MediaPlayerData.players().size != mediaContent.childCount) { 985 Log.e( 986 TAG, 987 "Size of players list and number of views in carousel are out of sync. " + 988 "Players size is ${MediaPlayerData.players().size}. " + 989 "View count is ${mediaContent.childCount}.", 990 ) 991 } 992 } 993 994 // Returns true if new player is added 995 private fun addOrUpdatePlayer( 996 key: String, 997 oldKey: String?, 998 data: MediaData, 999 isSsReactivated: Boolean, 1000 onUiExecutionEnd: Runnable? = null, 1001 ): Boolean = 1002 traceSection("MediaCarouselController#addOrUpdatePlayer") { 1003 MediaPlayerData.moveIfExists(oldKey, key) 1004 val existingPlayer = MediaPlayerData.getMediaPlayer(key) 1005 val curVisibleMediaKey = 1006 MediaPlayerData.visiblePlayerKeys() 1007 .elementAtOrNull(mediaCarouselScrollHandler.visibleMediaIndex) 1008 if (mediaControlsUmoInflationInBackground()) { 1009 if (existingPlayer == null) { 1010 bgExecutor.execute { 1011 val mediaViewHolder = createMediaViewHolderInBg() 1012 // Add the new player in the main thread. 1013 uiExecutor.execute { 1014 setupNewPlayer( 1015 key, 1016 data, 1017 isSsReactivated, 1018 curVisibleMediaKey, 1019 mediaViewHolder, 1020 ) 1021 updatePageIndicator() 1022 mediaCarouselScrollHandler.onPlayersChanged() 1023 mediaFrame.requiresRemeasuring = true 1024 onUiExecutionEnd?.run() 1025 } 1026 } 1027 } else { 1028 updatePlayer(key, data, isSsReactivated, curVisibleMediaKey, existingPlayer) 1029 updatePageIndicator() 1030 mediaCarouselScrollHandler.onPlayersChanged() 1031 mediaFrame.requiresRemeasuring = true 1032 onUiExecutionEnd?.run() 1033 } 1034 } else { 1035 if (existingPlayer == null) { 1036 val mediaViewHolder = 1037 MediaViewHolder.create(LayoutInflater.from(context), mediaContent) 1038 setupNewPlayer(key, data, isSsReactivated, curVisibleMediaKey, mediaViewHolder) 1039 } else { 1040 updatePlayer(key, data, isSsReactivated, curVisibleMediaKey, existingPlayer) 1041 } 1042 updatePageIndicator() 1043 mediaCarouselScrollHandler.onPlayersChanged() 1044 mediaFrame.requiresRemeasuring = true 1045 onUiExecutionEnd?.run() 1046 } 1047 return existingPlayer == null 1048 } 1049 1050 private fun updatePlayer( 1051 key: String, 1052 data: MediaData, 1053 isSsReactivated: Boolean, 1054 curVisibleMediaKey: MediaPlayerData.MediaSortKey?, 1055 existingPlayer: MediaControlPanel, 1056 ) { 1057 existingPlayer.bindPlayer(data, key) 1058 MediaPlayerData.addMediaPlayer( 1059 key, 1060 data, 1061 existingPlayer, 1062 systemClock, 1063 isSsReactivated, 1064 debugLogger, 1065 ) 1066 val packageName = MediaPlayerData.smartspaceMediaData?.packageName ?: String() 1067 // In case of recommendations hits. 1068 // Check the playing status of media player and the package name. 1069 // To make sure we scroll to the right app's media player. 1070 if ( 1071 isReorderingAllowed || 1072 shouldScrollToKey && data.isPlaying == true && packageName == data.packageName 1073 ) { 1074 reorderAllPlayers(curVisibleMediaKey, key) 1075 } else { 1076 needsReordering = true 1077 } 1078 } 1079 1080 private fun setupNewPlayer( 1081 key: String, 1082 data: MediaData, 1083 isSsReactivated: Boolean, 1084 curVisibleMediaKey: MediaPlayerData.MediaSortKey?, 1085 mediaViewHolder: MediaViewHolder, 1086 ) { 1087 val newPlayer = mediaControlPanelFactory.get() 1088 newPlayer.attachPlayer(mediaViewHolder) 1089 newPlayer.mediaViewController.sizeChangedListener = 1090 this@MediaCarouselController::updateCarouselDimensions 1091 val lp = 1092 LinearLayout.LayoutParams( 1093 ViewGroup.LayoutParams.MATCH_PARENT, 1094 ViewGroup.LayoutParams.WRAP_CONTENT, 1095 ) 1096 newPlayer.mediaViewHolder?.player?.setLayoutParams(lp) 1097 newPlayer.bindPlayer(data, key) 1098 newPlayer.setListening(mediaCarouselScrollHandler.visibleToUser && currentlyExpanded) 1099 MediaPlayerData.addMediaPlayer( 1100 key, 1101 data, 1102 newPlayer, 1103 systemClock, 1104 isSsReactivated, 1105 debugLogger, 1106 ) 1107 updateViewControllerToState(newPlayer.mediaViewController, noAnimation = true) 1108 // Media data added from a recommendation card should starts playing. 1109 if ((shouldScrollToKey && data.isPlaying == true) || (!shouldScrollToKey && data.active)) { 1110 reorderAllPlayers(curVisibleMediaKey, key) 1111 } else { 1112 needsReordering = true 1113 } 1114 } 1115 1116 @WorkerThread 1117 private fun createMediaViewHolderInBg(): MediaViewHolder { 1118 return MediaViewHolder.create(LayoutInflater.from(context), mediaContent) 1119 } 1120 1121 private fun addSmartspaceMediaRecommendations( 1122 key: String, 1123 data: SmartspaceMediaData, 1124 shouldPrioritize: Boolean, 1125 ) = 1126 traceSection("MediaCarouselController#addSmartspaceMediaRecommendations") { 1127 if (DEBUG) Log.d(TAG, "Updating smartspace target in carousel") 1128 MediaPlayerData.getMediaPlayer(key)?.let { 1129 if (mediaFlags.isPersistentSsCardEnabled()) { 1130 // The card exists, but could have changed active state, so update for sorting 1131 MediaPlayerData.addMediaRecommendation( 1132 key, 1133 data, 1134 it, 1135 shouldPrioritize, 1136 systemClock, 1137 debugLogger, 1138 update = true, 1139 ) 1140 } 1141 Log.w(TAG, "Skip adding smartspace target in carousel") 1142 return 1143 } 1144 1145 val existingSmartspaceMediaKey = MediaPlayerData.smartspaceMediaKey() 1146 existingSmartspaceMediaKey?.let { 1147 val removedPlayer = 1148 removePlayer(existingSmartspaceMediaKey, dismissMediaData = false) 1149 removedPlayer?.run { 1150 debugLogger.logPotentialMemoryLeak(existingSmartspaceMediaKey) 1151 onDestroy() 1152 } 1153 } 1154 1155 val newRecs = mediaControlPanelFactory.get() 1156 newRecs.attachRecommendation( 1157 RecommendationViewHolder.create(LayoutInflater.from(context), mediaContent) 1158 ) 1159 newRecs.mediaViewController.sizeChangedListener = this::updateCarouselDimensions 1160 val lp = 1161 LinearLayout.LayoutParams( 1162 ViewGroup.LayoutParams.MATCH_PARENT, 1163 ViewGroup.LayoutParams.WRAP_CONTENT, 1164 ) 1165 newRecs.recommendationViewHolder?.recommendations?.setLayoutParams(lp) 1166 newRecs.bindRecommendation(data) 1167 val curVisibleMediaKey = 1168 MediaPlayerData.visiblePlayerKeys() 1169 .elementAtOrNull(mediaCarouselScrollHandler.visibleMediaIndex) 1170 MediaPlayerData.addMediaRecommendation( 1171 key, 1172 data, 1173 newRecs, 1174 shouldPrioritize, 1175 systemClock, 1176 debugLogger, 1177 ) 1178 updateViewControllerToState(newRecs.mediaViewController, noAnimation = true) 1179 reorderAllPlayers(curVisibleMediaKey) 1180 updatePageIndicator() 1181 mediaFrame.requiresRemeasuring = true 1182 // Check postcondition: mediaContent should have the same number of children as there 1183 // are 1184 // elements in mediaPlayers. 1185 if (MediaPlayerData.players().size != mediaContent.childCount) { 1186 Log.e( 1187 TAG, 1188 "Size of players list and number of views in carousel are out of sync. " + 1189 "Players size is ${MediaPlayerData.players().size}. " + 1190 "View count is ${mediaContent.childCount}.", 1191 ) 1192 } 1193 } 1194 1195 fun removePlayer( 1196 key: String, 1197 dismissMediaData: Boolean = true, 1198 dismissRecommendation: Boolean = true, 1199 userInitiated: Boolean = false, 1200 ): MediaControlPanel? { 1201 if (key == MediaPlayerData.smartspaceMediaKey()) { 1202 MediaPlayerData.smartspaceMediaData?.let { 1203 logger.logRecommendationRemoved(it.packageName, it.instanceId) 1204 } 1205 } 1206 val removed = 1207 MediaPlayerData.removeMediaPlayer(key, dismissMediaData || dismissRecommendation) 1208 return removed?.apply { 1209 mediaCarouselScrollHandler.onPrePlayerRemoved(removed.mediaViewHolder?.player) 1210 mediaContent.removeView(removed.mediaViewHolder?.player) 1211 mediaContent.removeView(removed.recommendationViewHolder?.recommendations) 1212 removed.onDestroy() 1213 mediaCarouselScrollHandler.onPlayersChanged() 1214 updatePageIndicator() 1215 1216 if (dismissMediaData) { 1217 // Inform the media manager of a potentially late dismissal 1218 mediaManager.dismissMediaData(key, delay = 0L, userInitiated = userInitiated) 1219 } 1220 if (dismissRecommendation) { 1221 // Inform the media manager of a potentially late dismissal 1222 mediaManager.dismissSmartspaceRecommendation(key, delay = 0L) 1223 } 1224 } 1225 } 1226 1227 private fun updatePlayers(recreateMedia: Boolean) { 1228 if (SceneContainerFlag.isEnabled) { 1229 updateMediaPlayers(recreateMedia) 1230 return 1231 } 1232 pageIndicator.tintList = 1233 ColorStateList.valueOf(context.getColor(R.color.media_paging_indicator)) 1234 val previousVisibleKey = 1235 MediaPlayerData.visiblePlayerKeys() 1236 .elementAtOrNull(mediaCarouselScrollHandler.visibleMediaIndex) 1237 val onUiExecutionEnd = Runnable { 1238 if (recreateMedia) { 1239 reorderAllPlayers(previousVisibleKey) 1240 } 1241 } 1242 1243 val mediaDataList = MediaPlayerData.mediaData() 1244 // Do not loop through the original list of media data because the re-addition of media data 1245 // is being executed in background thread. 1246 mediaDataList.forEach { (key, data, isSsMediaRec) -> 1247 if (isSsMediaRec) { 1248 val smartspaceMediaData = MediaPlayerData.smartspaceMediaData 1249 removePlayer(key, dismissMediaData = false, dismissRecommendation = false) 1250 smartspaceMediaData?.let { 1251 addSmartspaceMediaRecommendations( 1252 it.targetId, 1253 it, 1254 MediaPlayerData.shouldPrioritizeSs, 1255 ) 1256 } 1257 onUiExecutionEnd.run() 1258 } else { 1259 val isSsReactivated = MediaPlayerData.isSsReactivated(key) 1260 if (recreateMedia) { 1261 removePlayer(key, dismissMediaData = false, dismissRecommendation = false) 1262 } 1263 addOrUpdatePlayer( 1264 key = key, 1265 oldKey = null, 1266 data = data, 1267 isSsReactivated = isSsReactivated, 1268 onUiExecutionEnd = onUiExecutionEnd, 1269 ) 1270 } 1271 } 1272 } 1273 1274 private fun updateMediaPlayers(recreateMedia: Boolean) { 1275 pageIndicator.tintList = 1276 ColorStateList.valueOf(context.getColor(R.color.media_paging_indicator)) 1277 if (recreateMedia) { 1278 mediaContent.removeAllViews() 1279 commonViewModels.forEachIndexed { index, viewModel -> 1280 when (viewModel) { 1281 is MediaCommonViewModel.MediaControl -> 1282 controllerById[viewModel.instanceId.toString()]?.onDestroy() 1283 is MediaCommonViewModel.MediaRecommendations -> 1284 controllerById[viewModel.key]?.onDestroy() 1285 } 1286 onAdded(viewModel, index, configChanged = true) 1287 } 1288 } 1289 } 1290 1291 private fun updatePageIndicator() { 1292 val numPages = mediaContent.getChildCount() 1293 pageIndicator.setNumPages(numPages) 1294 if (numPages == 1) { 1295 pageIndicator.setLocation(0f) 1296 } 1297 updatePageIndicatorAlpha() 1298 } 1299 1300 /** 1301 * Set a new interpolated state for all players. This is a state that is usually controlled by a 1302 * finger movement where the user drags from one state to the next. 1303 * 1304 * @param startLocation the start location of our state or -1 if this is directly set 1305 * @param endLocation the ending location of our state. 1306 * @param progress the progress of the transition between startLocation and endlocation. If 1307 * 1308 * ``` 1309 * this is not a guided transformation, this will be 1.0f 1310 * @param immediately 1311 * ``` 1312 * 1313 * should this state be applied immediately, canceling all animations? 1314 */ 1315 fun setCurrentState( 1316 @MediaLocation startLocation: Int, 1317 @MediaLocation endLocation: Int, 1318 progress: Float, 1319 immediately: Boolean, 1320 ) { 1321 if ( 1322 startLocation != currentStartLocation || 1323 endLocation != currentEndLocation || 1324 progress != currentTransitionProgress || 1325 immediately 1326 ) { 1327 currentStartLocation = startLocation 1328 currentEndLocation = endLocation 1329 currentTransitionProgress = progress 1330 if (!SceneContainerFlag.isEnabled) { 1331 for (mediaPlayer in MediaPlayerData.players()) { 1332 updateViewControllerToState(mediaPlayer.mediaViewController, immediately) 1333 } 1334 } else { 1335 controllerById.values.forEach { updateViewControllerToState(it, immediately) } 1336 } 1337 maybeResetSettingsCog() 1338 updatePageIndicatorAlpha() 1339 } 1340 } 1341 1342 @VisibleForTesting 1343 fun updatePageIndicatorAlpha() { 1344 val hostStates = mediaHostStatesManager.mediaHostStates 1345 val endIsVisible = hostStates[currentEndLocation]?.visible ?: false 1346 val startIsVisible = hostStates[currentStartLocation]?.visible ?: false 1347 val startAlpha = if (startIsVisible) 1.0f else 0.0f 1348 // when squishing in split shade, only use endState, which keeps changing 1349 // to provide squishFraction 1350 val squishFraction = hostStates[currentEndLocation]?.squishFraction ?: 1.0F 1351 val endAlpha = 1352 (if (endIsVisible) 1.0f else 0.0f) * 1353 calculateAlpha( 1354 squishFraction, 1355 (pageIndicator.translationY + pageIndicator.height) / 1356 mediaCarousel.measuredHeight, 1357 1F, 1358 ) 1359 var alpha = 1.0f 1360 if (!endIsVisible || !startIsVisible) { 1361 var progress = currentTransitionProgress 1362 if (!endIsVisible) { 1363 progress = 1.0f - progress 1364 } 1365 // Let's fade in quickly at the end where the view is visible 1366 progress = 1367 MathUtils.constrain(MathUtils.map(0.95f, 1.0f, 0.0f, 1.0f, progress), 0.0f, 1.0f) 1368 alpha = MathUtils.lerp(startAlpha, endAlpha, progress) 1369 } 1370 pageIndicator.alpha = alpha 1371 } 1372 1373 private fun updatePageIndicatorLocation() { 1374 // Update the location of the page indicator, carousel clipping 1375 val translationX = 1376 if (isRtl) { 1377 (pageIndicator.width - currentCarouselWidth) / 2.0f 1378 } else { 1379 (currentCarouselWidth - pageIndicator.width) / 2.0f 1380 } 1381 pageIndicator.translationX = translationX + mediaCarouselScrollHandler.contentTranslation 1382 val layoutParams = pageIndicator.layoutParams as ViewGroup.MarginLayoutParams 1383 pageIndicator.translationY = 1384 (mediaCarousel.measuredHeight - pageIndicator.height - layoutParams.bottomMargin) 1385 .toFloat() 1386 } 1387 1388 /** Update listening to seekbar. */ 1389 private fun updateSeekbarListening(visibleToUser: Boolean) { 1390 if (!SceneContainerFlag.isEnabled) { 1391 for (player in MediaPlayerData.players()) { 1392 player.setListening(visibleToUser && currentlyExpanded) 1393 } 1394 } else { 1395 controllerById.values.forEach { it.setListening(visibleToUser && currentlyExpanded) } 1396 } 1397 } 1398 1399 /** Update the dimension of this carousel. */ 1400 private fun updateCarouselDimensions() { 1401 var width = 0 1402 var height = 0 1403 if (!SceneContainerFlag.isEnabled) { 1404 for (mediaPlayer in MediaPlayerData.players()) { 1405 val controller = mediaPlayer.mediaViewController 1406 // When transitioning the view to gone, the view gets smaller, but the translation 1407 // Doesn't, let's add the translation 1408 width = Math.max(width, controller.currentWidth + controller.translationX.toInt()) 1409 height = 1410 Math.max(height, controller.currentHeight + controller.translationY.toInt()) 1411 } 1412 } else { 1413 controllerById.values.forEach { 1414 // When transitioning the view to gone, the view gets smaller, but the translation 1415 // Doesn't, let's add the translation 1416 width = Math.max(width, it.currentWidth + it.translationX.toInt()) 1417 height = Math.max(height, it.currentHeight + it.translationY.toInt()) 1418 } 1419 } 1420 if (width != currentCarouselWidth || height != currentCarouselHeight) { 1421 currentCarouselWidth = width 1422 currentCarouselHeight = height 1423 mediaCarouselScrollHandler.setCarouselBounds( 1424 currentCarouselWidth, 1425 currentCarouselHeight, 1426 ) 1427 updatePageIndicatorLocation() 1428 updatePageIndicatorAlpha() 1429 } 1430 } 1431 1432 private fun maybeResetSettingsCog() { 1433 val hostStates = mediaHostStatesManager.mediaHostStates 1434 val endShowsActive = hostStates[currentEndLocation]?.showsOnlyActiveMedia ?: true 1435 val startShowsActive = 1436 hostStates[currentStartLocation]?.showsOnlyActiveMedia ?: endShowsActive 1437 val startDisablePagination = hostStates[currentStartLocation]?.disablePagination ?: false 1438 val endDisablePagination = hostStates[currentEndLocation]?.disablePagination ?: false 1439 1440 if ( 1441 currentlyShowingOnlyActive != endShowsActive || 1442 currentlyDisablePagination != endDisablePagination || 1443 ((currentTransitionProgress != 1.0f && currentTransitionProgress != 0.0f) && 1444 (startShowsActive != endShowsActive || 1445 startDisablePagination != endDisablePagination)) 1446 ) { 1447 // Whenever we're transitioning from between differing states or the endstate differs 1448 // we reset the translation 1449 currentlyShowingOnlyActive = endShowsActive 1450 currentlyDisablePagination = endDisablePagination 1451 mediaCarouselScrollHandler.resetTranslation(animate = true) 1452 } 1453 } 1454 1455 private fun updateViewControllerToState( 1456 viewController: MediaViewController, 1457 noAnimation: Boolean, 1458 ) { 1459 viewController.setCurrentState( 1460 startLocation = currentStartLocation, 1461 endLocation = currentEndLocation, 1462 transitionProgress = currentTransitionProgress, 1463 applyImmediately = noAnimation, 1464 ) 1465 } 1466 1467 /** 1468 * The desired location of this view has changed. We should remeasure the view to match the new 1469 * bounds and kick off bounds animations if necessary. If an animation is happening, an 1470 * animation is kicked of externally, which sets a new current state until we reach the 1471 * targetState. 1472 * 1473 * @param desiredLocation the location we're going to 1474 * @param desiredHostState the target state we're transitioning to 1475 * @param animate should this be animated 1476 */ 1477 fun onDesiredLocationChanged( 1478 desiredLocation: Int, 1479 desiredHostState: MediaHostState?, 1480 animate: Boolean, 1481 duration: Long = 200, 1482 startDelay: Long = 0, 1483 ) = 1484 traceSection("MediaCarouselController#onDesiredLocationChanged") { 1485 desiredHostState?.let { 1486 if (this.desiredLocation != desiredLocation) { 1487 // Only log an event when location changes 1488 bgExecutor.execute { logger.logCarouselPosition(desiredLocation) } 1489 } 1490 1491 // This is a hosting view, let's remeasure our players 1492 this.desiredLocation = desiredLocation 1493 this.desiredHostState = it 1494 currentlyExpanded = it.expansion > 0 1495 1496 val shouldCloseGuts = 1497 !currentlyExpanded && 1498 !mediaManager.hasActiveMediaOrRecommendation() && 1499 desiredHostState.showsOnlyActiveMedia 1500 1501 if (!SceneContainerFlag.isEnabled) { 1502 for (mediaPlayer in MediaPlayerData.players()) { 1503 if (animate) { 1504 mediaPlayer.mediaViewController.animatePendingStateChange( 1505 duration = duration, 1506 delay = startDelay, 1507 ) 1508 } 1509 if (shouldCloseGuts && mediaPlayer.mediaViewController.isGutsVisible) { 1510 mediaPlayer.closeGuts(!animate) 1511 } 1512 1513 mediaPlayer.mediaViewController.onLocationPreChange(desiredLocation) 1514 } 1515 } else { 1516 controllerById.values.forEach { controller -> 1517 if (animate) { 1518 controller.animatePendingStateChange(duration, startDelay) 1519 } 1520 if (shouldCloseGuts && controller.isGutsVisible) { 1521 controller.closeGuts(!animate) 1522 } 1523 1524 controller.onLocationPreChange(desiredLocation) 1525 } 1526 } 1527 mediaCarouselScrollHandler.showsSettingsButton = !it.showsOnlyActiveMedia 1528 mediaCarouselScrollHandler.falsingProtectionNeeded = it.falsingProtectionNeeded 1529 val nowVisible = it.visible 1530 if (nowVisible != playersVisible) { 1531 playersVisible = nowVisible 1532 if (nowVisible) { 1533 mediaCarouselScrollHandler.resetTranslation() 1534 } 1535 } 1536 updateCarouselSize() 1537 } 1538 } 1539 1540 fun closeGuts(immediate: Boolean = true) { 1541 if (!SceneContainerFlag.isEnabled) { 1542 MediaPlayerData.players().forEach { it.closeGuts(immediate) } 1543 } else { 1544 controllerById.values.forEach { it.closeGuts(immediate) } 1545 } 1546 } 1547 1548 /** Update the size of the carousel, remeasuring it if necessary. */ 1549 private fun updateCarouselSize() { 1550 val width = desiredHostState?.measurementInput?.width ?: 0 1551 val height = desiredHostState?.measurementInput?.height ?: 0 1552 if ( 1553 width != carouselMeasureWidth && width != 0 || 1554 height != carouselMeasureHeight && height != 0 1555 ) { 1556 carouselMeasureWidth = width 1557 carouselMeasureHeight = height 1558 val playerWidthPlusPadding = 1559 carouselMeasureWidth + 1560 context.resources.getDimensionPixelSize(R.dimen.qs_media_padding) 1561 // Let's remeasure the carousel 1562 val widthSpec = desiredHostState?.measurementInput?.widthMeasureSpec ?: 0 1563 val heightSpec = desiredHostState?.measurementInput?.heightMeasureSpec ?: 0 1564 mediaCarousel.measure(widthSpec, heightSpec) 1565 mediaCarousel.layout(0, 0, width, mediaCarousel.measuredHeight) 1566 // Update the padding after layout; view widths are used in RTL to calculate scrollX 1567 mediaCarouselScrollHandler.playerWidthPlusPadding = playerWidthPlusPadding 1568 } 1569 } 1570 1571 /** Log the user impression for media card at visibleMediaIndex. */ 1572 fun logSmartspaceImpression(qsExpanded: Boolean) { 1573 if (SceneContainerFlag.isEnabled) { 1574 mediaCarouselViewModel.onCardVisibleToUser( 1575 qsExpanded, 1576 mediaCarouselScrollHandler.visibleMediaIndex, 1577 currentEndLocation, 1578 ) 1579 return 1580 } 1581 val visibleMediaIndex = mediaCarouselScrollHandler.visibleMediaIndex 1582 if (MediaPlayerData.players().size > visibleMediaIndex) { 1583 val mediaControlPanel = MediaPlayerData.getMediaControlPanel(visibleMediaIndex) 1584 val hasActiveMediaOrRecommendationCard = 1585 MediaPlayerData.hasActiveMediaOrRecommendationCard() 1586 if (!hasActiveMediaOrRecommendationCard && !qsExpanded) { 1587 // Skip logging if on LS or QQS, and there is no active media card 1588 return 1589 } 1590 mediaControlPanel?.let { 1591 logSmartspaceCardReported( 1592 800, // SMARTSPACE_CARD_SEEN 1593 it.mSmartspaceId, 1594 it.mUid, 1595 intArrayOf(it.surfaceForSmartspaceLogging), 1596 ) 1597 it.mIsImpressed = true 1598 } 1599 } 1600 } 1601 1602 /** 1603 * Log Smartspace events 1604 * 1605 * @param eventId UI event id (e.g. 800 for SMARTSPACE_CARD_SEEN) 1606 * @param instanceId id to uniquely identify a card, e.g. each headphone generates a new 1607 * instanceId 1608 * @param uid uid for the application that media comes from 1609 * @param surfaces list of display surfaces the media card is on (e.g. lockscreen, shade) when 1610 * the event happened 1611 * @param interactedSubcardRank the rank for interacted media item for recommendation card, -1 1612 * for tapping on card but not on any media item, 0 for first media item, 1 for second, etc. 1613 * @param interactedSubcardCardinality how many media items were shown to the user when there is 1614 * user interaction 1615 * @param rank the rank for media card in the media carousel, starting from 0 1616 * @param receivedLatencyMillis latency in milliseconds for card received events. E.g. latency 1617 * between headphone connection to sysUI displays media recommendation card 1618 * @param isSwipeToDismiss whether is to log swipe-to-dismiss event 1619 */ 1620 @JvmOverloads 1621 fun logSmartspaceCardReported( 1622 eventId: Int, 1623 instanceId: Int, 1624 uid: Int, 1625 surfaces: IntArray, 1626 interactedSubcardRank: Int = 0, 1627 interactedSubcardCardinality: Int = 0, 1628 rank: Int = mediaCarouselScrollHandler.visibleMediaIndex, 1629 receivedLatencyMillis: Int = 0, 1630 isSwipeToDismiss: Boolean = false, 1631 ) { 1632 if (MediaPlayerData.players().size <= rank) { 1633 return 1634 } 1635 1636 val mediaControlKey = MediaPlayerData.visiblePlayerKeys().elementAt(rank) 1637 // Only log media resume card when Smartspace data is available 1638 if ( 1639 !mediaControlKey.isSsMediaRec && 1640 !mediaManager.isRecommendationActive() && 1641 MediaPlayerData.smartspaceMediaData == null 1642 ) { 1643 return 1644 } 1645 1646 val cardinality = mediaContent.getChildCount() 1647 surfaces.forEach { surface -> 1648 SysUiStatsLog.write( 1649 SMARTSPACE_CARD_REPORTED, 1650 eventId, 1651 instanceId, 1652 // Deprecated, replaced with AiAi feature type so we don't need to create logging 1653 // card type for each new feature. 1654 SMART_SPACE_CARD_REPORTED__CARD_TYPE__UNKNOWN_CARD, 1655 surface, 1656 // Use -1 as rank value to indicate user swipe to dismiss the card 1657 if (isSwipeToDismiss) -1 else rank, 1658 cardinality, 1659 if (mediaControlKey.isSsMediaRec) { 1660 15 // MEDIA_RECOMMENDATION 1661 } else if (mediaControlKey.isSsReactivated) { 1662 43 // MEDIA_RESUME_SS_ACTIVATED 1663 } else { 1664 31 1665 }, // MEDIA_RESUME 1666 uid, 1667 interactedSubcardRank, 1668 interactedSubcardCardinality, 1669 receivedLatencyMillis, 1670 null, // Media cards cannot have subcards. 1671 null, // Media cards don't have dimensions today. 1672 ) 1673 1674 if (DEBUG) { 1675 Log.d( 1676 TAG, 1677 "Log Smartspace card event id: $eventId instance id: $instanceId" + 1678 " surface: $surface rank: $rank cardinality: $cardinality " + 1679 "isRecommendationCard: ${mediaControlKey.isSsMediaRec} " + 1680 "isSsReactivated: ${mediaControlKey.isSsReactivated}" + 1681 "uid: $uid " + 1682 "interactedSubcardRank: $interactedSubcardRank " + 1683 "interactedSubcardCardinality: $interactedSubcardCardinality " + 1684 "received_latency_millis: $receivedLatencyMillis", 1685 ) 1686 } 1687 } 1688 } 1689 1690 @VisibleForTesting 1691 fun onSwipeToDismiss() { 1692 if (SceneContainerFlag.isEnabled) { 1693 mediaCarouselViewModel.onSwipeToDismiss(currentEndLocation) 1694 return 1695 } 1696 MediaPlayerData.players().forEachIndexed { index, it -> 1697 if (it.mIsImpressed) { 1698 logSmartspaceCardReported( 1699 SMARTSPACE_CARD_DISMISS_EVENT, 1700 it.mSmartspaceId, 1701 it.mUid, 1702 intArrayOf(it.surfaceForSmartspaceLogging), 1703 rank = index, 1704 isSwipeToDismiss = true, 1705 ) 1706 // Reset card impressed state when swipe to dismissed 1707 it.mIsImpressed = false 1708 } 1709 } 1710 MediaPlayerData.isSwipedAway = true 1711 logger.logSwipeDismiss() 1712 mediaManager.onSwipeToDismiss() 1713 } 1714 1715 fun getCurrentVisibleMediaContentIntent(): PendingIntent? { 1716 return MediaPlayerData.playerKeys() 1717 .elementAtOrNull(mediaCarouselScrollHandler.visibleMediaIndex) 1718 ?.data 1719 ?.clickIntent 1720 } 1721 1722 override fun dump(pw: PrintWriter, args: Array<out String>) { 1723 pw.apply { 1724 println("keysNeedRemoval: $keysNeedRemoval") 1725 println("dataKeys: ${MediaPlayerData.dataKeys()}") 1726 println("orderedPlayerSortKeys: ${MediaPlayerData.playerKeys()}") 1727 println("visiblePlayerSortKeys: ${MediaPlayerData.visiblePlayerKeys()}") 1728 println("commonViewModels: $commonViewModels") 1729 println("smartspaceMediaData: ${MediaPlayerData.smartspaceMediaData}") 1730 println("shouldPrioritizeSs: ${MediaPlayerData.shouldPrioritizeSs}") 1731 println("current size: $currentCarouselWidth x $currentCarouselHeight") 1732 println("location: $desiredLocation") 1733 println( 1734 "state: ${desiredHostState?.expansion}, " + 1735 "only active ${desiredHostState?.showsOnlyActiveMedia}" 1736 ) 1737 println("isSwipedAway: ${MediaPlayerData.isSwipedAway}") 1738 println("allowMediaPlayerOnLockScreen: $allowMediaPlayerOnLockScreen") 1739 } 1740 } 1741 } 1742 1743 @VisibleForTesting 1744 internal object MediaPlayerData { 1745 private val EMPTY = 1746 MediaData( 1747 userId = -1, 1748 initialized = false, 1749 app = null, 1750 appIcon = null, 1751 artist = null, 1752 song = null, 1753 artwork = null, 1754 actions = emptyList(), 1755 actionsToShowInCompact = emptyList(), 1756 packageName = "INVALID", 1757 token = null, 1758 clickIntent = null, 1759 device = null, 1760 active = true, 1761 resumeAction = null, 1762 instanceId = InstanceId.fakeInstanceId(-1), 1763 appUid = -1, 1764 ) 1765 1766 // Whether should prioritize Smartspace card. 1767 internal var shouldPrioritizeSs: Boolean = false 1768 private set 1769 1770 internal var smartspaceMediaData: SmartspaceMediaData? = null 1771 private set 1772 1773 data class MediaSortKey( 1774 val isSsMediaRec: Boolean, // Whether the item represents a Smartspace media recommendation. 1775 val data: MediaData, 1776 val key: String, 1777 val updateTime: Long = 0, 1778 val isSsReactivated: Boolean = false, 1779 ) 1780 1781 private val comparator = <lambda>null1782 compareByDescending<MediaSortKey> { 1783 it.data.isPlaying == true && it.data.playbackLocation == MediaData.PLAYBACK_LOCAL 1784 } <lambda>null1785 .thenByDescending { 1786 it.data.isPlaying == true && 1787 it.data.playbackLocation == MediaData.PLAYBACK_CAST_LOCAL 1788 } <lambda>null1789 .thenByDescending { it.data.active } <lambda>null1790 .thenByDescending { shouldPrioritizeSs == it.isSsMediaRec } <lambda>null1791 .thenByDescending { !it.data.resumption } <lambda>null1792 .thenByDescending { it.data.playbackLocation != MediaData.PLAYBACK_CAST_REMOTE } <lambda>null1793 .thenByDescending { it.data.lastActive } <lambda>null1794 .thenByDescending { it.updateTime } <lambda>null1795 .thenByDescending { it.data.notificationKey } 1796 1797 private val mediaPlayers = TreeMap<MediaSortKey, MediaControlPanel>(comparator) 1798 private val mediaData: MutableMap<String, MediaSortKey> = mutableMapOf() 1799 1800 // A map that tracks order of visible media players before they get reordered. 1801 private val visibleMediaPlayers = LinkedHashMap<String, MediaSortKey>() 1802 1803 // Whether the user swiped away the carousel since its last update 1804 internal var isSwipedAway: Boolean = false 1805 addMediaPlayernull1806 fun addMediaPlayer( 1807 key: String, 1808 data: MediaData, 1809 player: MediaControlPanel, 1810 clock: SystemClock, 1811 isSsReactivated: Boolean, 1812 debugLogger: MediaCarouselControllerLogger? = null, 1813 ) { 1814 val removedPlayer = removeMediaPlayer(key) 1815 if (removedPlayer != null && removedPlayer != player) { 1816 debugLogger?.logPotentialMemoryLeak(key) 1817 removedPlayer.onDestroy() 1818 } 1819 val sortKey = 1820 MediaSortKey( 1821 isSsMediaRec = false, 1822 data, 1823 key, 1824 clock.currentTimeMillis(), 1825 isSsReactivated = isSsReactivated, 1826 ) 1827 mediaData.put(key, sortKey) 1828 mediaPlayers.put(sortKey, player) 1829 visibleMediaPlayers.put(key, sortKey) 1830 } 1831 addMediaRecommendationnull1832 fun addMediaRecommendation( 1833 key: String, 1834 data: SmartspaceMediaData, 1835 player: MediaControlPanel, 1836 shouldPrioritize: Boolean, 1837 clock: SystemClock, 1838 debugLogger: MediaCarouselControllerLogger? = null, 1839 update: Boolean = false, 1840 ) { 1841 shouldPrioritizeSs = shouldPrioritize 1842 val removedPlayer = removeMediaPlayer(key) 1843 if (!update && removedPlayer != null && removedPlayer != player) { 1844 debugLogger?.logPotentialMemoryLeak(key) 1845 removedPlayer.onDestroy() 1846 } 1847 val sortKey = 1848 MediaSortKey( 1849 isSsMediaRec = true, 1850 EMPTY.copy(active = data.isActive, isPlaying = false), 1851 key, 1852 clock.currentTimeMillis(), 1853 isSsReactivated = true, 1854 ) 1855 mediaData.put(key, sortKey) 1856 mediaPlayers.put(sortKey, player) 1857 visibleMediaPlayers.put(key, sortKey) 1858 smartspaceMediaData = data 1859 } 1860 moveIfExistsnull1861 fun moveIfExists( 1862 oldKey: String?, 1863 newKey: String, 1864 debugLogger: MediaCarouselControllerLogger? = null, 1865 ) { 1866 if (oldKey == null || oldKey == newKey) { 1867 return 1868 } 1869 1870 mediaData.remove(oldKey)?.let { 1871 // MediaPlayer should not be visible 1872 // no need to set isDismissed flag. 1873 val removedPlayer = removeMediaPlayer(newKey) 1874 removedPlayer?.run { 1875 debugLogger?.logPotentialMemoryLeak(newKey) 1876 onDestroy() 1877 } 1878 mediaData.put(newKey, it) 1879 } 1880 } 1881 getMediaControlPanelnull1882 fun getMediaControlPanel(visibleIndex: Int): MediaControlPanel? { 1883 return mediaPlayers.get(visiblePlayerKeys().elementAt(visibleIndex)) 1884 } 1885 getMediaPlayernull1886 fun getMediaPlayer(key: String): MediaControlPanel? { 1887 return mediaData.get(key)?.let { mediaPlayers.get(it) } 1888 } 1889 getMediaPlayerIndexnull1890 fun getMediaPlayerIndex(key: String): Int { 1891 val sortKey = mediaData.get(key) 1892 mediaPlayers.entries.forEachIndexed { index, e -> 1893 if (e.key == sortKey) { 1894 return index 1895 } 1896 } 1897 return -1 1898 } 1899 1900 /** 1901 * Removes media player given the key. 1902 * 1903 * @param isDismissed determines whether the media player is removed from the carousel. 1904 */ removeMediaPlayernull1905 fun removeMediaPlayer(key: String, isDismissed: Boolean = false) = 1906 mediaData.remove(key)?.let { 1907 if (it.isSsMediaRec) { 1908 smartspaceMediaData = null 1909 } 1910 if (isDismissed) { 1911 visibleMediaPlayers.remove(key) 1912 } 1913 mediaPlayers.remove(it) 1914 } 1915 mediaDatanull1916 fun mediaData() = 1917 mediaData.entries.map { e -> Triple(e.key, e.value.data, e.value.isSsMediaRec) } 1918 dataKeysnull1919 fun dataKeys() = mediaData.keys 1920 1921 fun players() = mediaPlayers.values 1922 1923 fun playerKeys() = mediaPlayers.keys 1924 1925 fun visiblePlayerKeys() = visibleMediaPlayers.values 1926 1927 /** Returns the index of the first non-timeout media. */ 1928 fun firstActiveMediaIndex(): Int { 1929 mediaPlayers.entries.forEachIndexed { index, e -> 1930 if (!e.key.isSsMediaRec && e.key.data.active) { 1931 return index 1932 } 1933 } 1934 return -1 1935 } 1936 1937 /** Returns the existing Smartspace target id. */ smartspaceMediaKeynull1938 fun smartspaceMediaKey(): String? { 1939 mediaData.entries.forEach { e -> 1940 if (e.value.isSsMediaRec) { 1941 return e.key 1942 } 1943 } 1944 return null 1945 } 1946 1947 @VisibleForTesting clearnull1948 fun clear() { 1949 mediaData.clear() 1950 mediaPlayers.clear() 1951 visibleMediaPlayers.clear() 1952 } 1953 1954 /* Returns true if there is active media player card or recommendation card */ hasActiveMediaOrRecommendationCardnull1955 fun hasActiveMediaOrRecommendationCard(): Boolean { 1956 if (smartspaceMediaData != null && smartspaceMediaData?.isActive!!) { 1957 return true 1958 } 1959 if (firstActiveMediaIndex() != -1) { 1960 return true 1961 } 1962 return false 1963 } 1964 isSsReactivatednull1965 fun isSsReactivated(key: String): Boolean = mediaData.get(key)?.isSsReactivated ?: false 1966 1967 /** 1968 * This method is called when media players are reordered. To make sure we have the new version 1969 * of the order of media players visible to user. 1970 */ 1971 fun updateVisibleMediaPlayers() { 1972 visibleMediaPlayers.clear() 1973 playerKeys().forEach { visibleMediaPlayers.put(it.key, it) } 1974 } 1975 } 1976