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