1 package com.android.systemui.media 2 3 import android.content.Context 4 import android.content.Intent 5 import android.content.res.ColorStateList 6 import android.content.res.Configuration 7 import android.provider.Settings.ACTION_MEDIA_CONTROLS_SETTINGS 8 import android.util.Log 9 import android.util.MathUtils 10 import android.view.LayoutInflater 11 import android.view.View 12 import android.view.ViewGroup 13 import android.widget.LinearLayout 14 import androidx.annotation.VisibleForTesting 15 import com.android.systemui.Dumpable 16 import com.android.systemui.R 17 import com.android.systemui.classifier.FalsingCollector 18 import com.android.systemui.dagger.SysUISingleton 19 import com.android.systemui.dagger.qualifiers.Main 20 import com.android.systemui.dump.DumpManager 21 import com.android.systemui.plugins.ActivityStarter 22 import com.android.systemui.plugins.FalsingManager 23 import com.android.systemui.qs.PageIndicator 24 import com.android.systemui.shared.system.SysUiStatsLog 25 import com.android.systemui.statusbar.notification.collection.legacy.VisualStabilityManager 26 import com.android.systemui.statusbar.policy.ConfigurationController 27 import com.android.systemui.util.Utils 28 import com.android.systemui.util.animation.UniqueObjectHostView 29 import com.android.systemui.util.animation.requiresRemeasuring 30 import com.android.systemui.util.concurrency.DelayableExecutor 31 import com.android.systemui.util.time.SystemClock 32 import java.io.FileDescriptor 33 import java.io.PrintWriter 34 import java.util.TreeMap 35 import javax.inject.Inject 36 import javax.inject.Provider 37 38 private const val TAG = "MediaCarouselController" 39 private val settingsIntent = Intent().setAction(ACTION_MEDIA_CONTROLS_SETTINGS) 40 private val DEBUG = Log.isLoggable(TAG, Log.DEBUG) 41 42 /** 43 * Class that is responsible for keeping the view carousel up to date. 44 * This also handles changes in state and applies them to the media carousel like the expansion. 45 */ 46 @SysUISingleton 47 class MediaCarouselController @Inject constructor( 48 private val context: Context, 49 private val mediaControlPanelFactory: Provider<MediaControlPanel>, 50 private val visualStabilityManager: VisualStabilityManager, 51 private val mediaHostStatesManager: MediaHostStatesManager, 52 private val activityStarter: ActivityStarter, 53 private val systemClock: SystemClock, 54 @Main executor: DelayableExecutor, 55 private val mediaManager: MediaDataManager, 56 configurationController: ConfigurationController, 57 falsingCollector: FalsingCollector, 58 falsingManager: FalsingManager, 59 dumpManager: DumpManager 60 ) : Dumpable { 61 /** 62 * The current width of the carousel 63 */ 64 private var currentCarouselWidth: Int = 0 65 66 /** 67 * The current height of the carousel 68 */ 69 private var currentCarouselHeight: Int = 0 70 71 /** 72 * Are we currently showing only active players 73 */ 74 private var currentlyShowingOnlyActive: Boolean = false 75 76 /** 77 * Is the player currently visible (at the end of the transformation 78 */ 79 private var playersVisible: Boolean = false 80 /** 81 * The desired location where we'll be at the end of the transformation. Usually this matches 82 * the end location, except when we're still waiting on a state update call. 83 */ 84 @MediaLocation 85 private var desiredLocation: Int = -1 86 87 /** 88 * The ending location of the view where it ends when all animations and transitions have 89 * finished 90 */ 91 @MediaLocation 92 private var currentEndLocation: Int = -1 93 94 /** 95 * The ending location of the view where it ends when all animations and transitions have 96 * finished 97 */ 98 @MediaLocation 99 private var currentStartLocation: Int = -1 100 101 /** 102 * The progress of the transition or 1.0 if there is no transition happening 103 */ 104 private var currentTransitionProgress: Float = 1.0f 105 106 /** 107 * The measured width of the carousel 108 */ 109 private var carouselMeasureWidth: Int = 0 110 111 /** 112 * The measured height of the carousel 113 */ 114 private var carouselMeasureHeight: Int = 0 115 private var desiredHostState: MediaHostState? = null 116 private val mediaCarousel: MediaScrollView 117 val mediaCarouselScrollHandler: MediaCarouselScrollHandler 118 val mediaFrame: ViewGroup 119 private lateinit var settingsButton: View 120 private val mediaContent: ViewGroup 121 private val pageIndicator: PageIndicator 122 private val visualStabilityCallback: VisualStabilityManager.Callback 123 private var needsReordering: Boolean = false 124 private var keysNeedRemoval = mutableSetOf<String>() 125 private var bgColor = getBackgroundColor() 126 protected var shouldScrollToActivePlayer: Boolean = false 127 private var isRtl: Boolean = false 128 set(value) { 129 if (value != field) { 130 field = value 131 mediaFrame.layoutDirection = 132 if (value) View.LAYOUT_DIRECTION_RTL else View.LAYOUT_DIRECTION_LTR 133 mediaCarouselScrollHandler.scrollToStart() 134 } 135 } 136 private var currentlyExpanded = true 137 set(value) { 138 if (field != value) { 139 field = value 140 for (player in MediaPlayerData.players()) { 141 player.setListening(field) 142 } 143 } 144 } 145 private val configListener = object : ConfigurationController.ConfigurationListener { 146 override fun onDensityOrFontScaleChanged() { 147 recreatePlayers() 148 inflateSettingsButton() 149 } 150 151 override fun onThemeChanged() { 152 recreatePlayers() 153 inflateSettingsButton() 154 } 155 156 override fun onConfigChanged(newConfig: Configuration?) { 157 if (newConfig == null) return 158 isRtl = newConfig.layoutDirection == View.LAYOUT_DIRECTION_RTL 159 } 160 161 override fun onUiModeChanged() { 162 recreatePlayers() 163 inflateSettingsButton() 164 } 165 } 166 167 /** 168 * Update MediaCarouselScrollHandler.visibleToUser to reflect media card container visibility. 169 * It will be called when the container is out of view. 170 */ 171 lateinit var updateUserVisibility: () -> Unit 172 173 init { 174 dumpManager.registerDumpable(TAG, this) 175 mediaFrame = inflateMediaCarousel() 176 mediaCarousel = mediaFrame.requireViewById(R.id.media_carousel_scroller) 177 pageIndicator = mediaFrame.requireViewById(R.id.media_page_indicator) 178 mediaCarouselScrollHandler = MediaCarouselScrollHandler(mediaCarousel, pageIndicator, 179 executor, this::onSwipeToDismiss, this::updatePageIndicatorLocation, 180 this::closeGuts, falsingCollector, falsingManager, this::logSmartspaceImpression) 181 isRtl = context.resources.configuration.layoutDirection == View.LAYOUT_DIRECTION_RTL 182 inflateSettingsButton() 183 mediaContent = mediaCarousel.requireViewById(R.id.media_carousel) 184 configurationController.addCallback(configListener) 185 // TODO (b/162832756): remove visual stability manager when migrating to new pipeline 186 visualStabilityCallback = VisualStabilityManager.Callback { 187 if (needsReordering) { 188 needsReordering = false 189 reorderAllPlayers(previousVisiblePlayerKey = null) 190 } 191 192 keysNeedRemoval.forEach { removePlayer(it) } 193 keysNeedRemoval.clear() 194 195 // Update user visibility so that no extra impression will be logged when 196 // activeMediaIndex resets to 0 197 if (this::updateUserVisibility.isInitialized) { 198 updateUserVisibility() 199 } 200 201 // Let's reset our scroll position 202 mediaCarouselScrollHandler.scrollToStart() 203 } 204 visualStabilityManager.addReorderingAllowedCallback(visualStabilityCallback, 205 true /* persistent */) 206 mediaManager.addListener(object : MediaDataManager.Listener { 207 override fun onMediaDataLoaded( 208 key: String, 209 oldKey: String?, 210 data: MediaData, 211 immediately: Boolean, 212 receivedSmartspaceCardLatency: Int 213 ) { 214 if (addOrUpdatePlayer(key, oldKey, data)) { 215 // Log card received if a new resumable media card is added 216 MediaPlayerData.getMediaPlayer(key)?.let { 217 /* ktlint-disable max-line-length */ 218 logSmartspaceCardReported(759, // SMARTSPACE_CARD_RECEIVED 219 it.mInstanceId, 220 it.mUid, 221 /* isRecommendationCard */ false, 222 intArrayOf( 223 SysUiStatsLog.SMART_SPACE_CARD_REPORTED__DISPLAY_SURFACE__SHADE, 224 SysUiStatsLog.SMART_SPACE_CARD_REPORTED__DISPLAY_SURFACE__LOCKSCREEN), 225 rank = MediaPlayerData.getMediaPlayerIndex(key)) 226 /* ktlint-disable max-line-length */ 227 } 228 if (mediaCarouselScrollHandler.visibleToUser && 229 mediaCarouselScrollHandler.visibleMediaIndex 230 == MediaPlayerData.getMediaPlayerIndex(key)) { 231 logSmartspaceImpression(mediaCarouselScrollHandler.qsExpanded) 232 } 233 } else if (receivedSmartspaceCardLatency != 0) { 234 // Log resume card received if resumable media card is reactivated and 235 // resume card is ranked first 236 MediaPlayerData.players().forEachIndexed { index, it -> 237 if (it.recommendationViewHolder == null) { 238 it.mInstanceId = SmallHash.hash(it.mUid + 239 systemClock.currentTimeMillis().toInt()) 240 it.mIsImpressed = false 241 /* ktlint-disable max-line-length */ 242 logSmartspaceCardReported(759, // SMARTSPACE_CARD_RECEIVED 243 it.mInstanceId, 244 it.mUid, 245 /* isRecommendationCard */ false, 246 intArrayOf( 247 SysUiStatsLog.SMART_SPACE_CARD_REPORTED__DISPLAY_SURFACE__SHADE, 248 SysUiStatsLog.SMART_SPACE_CARD_REPORTED__DISPLAY_SURFACE__LOCKSCREEN), 249 rank = index, 250 receivedLatencyMillis = receivedSmartspaceCardLatency) 251 /* ktlint-disable max-line-length */ 252 } 253 } 254 // If media container area already visible to the user, log impression for 255 // reactivated card. 256 if (mediaCarouselScrollHandler.visibleToUser && 257 !mediaCarouselScrollHandler.qsExpanded) { 258 logSmartspaceImpression(mediaCarouselScrollHandler.qsExpanded) 259 } 260 } 261 262 val canRemove = data.isPlaying?.let { !it } ?: data.isClearable && !data.active 263 if (canRemove && !Utils.useMediaResumption(context)) { 264 // This view isn't playing, let's remove this! This happens e.g when 265 // dismissing/timing out a view. We still have the data around because 266 // resumption could be on, but we should save the resources and release this. 267 if (visualStabilityManager.isReorderingAllowed) { 268 onMediaDataRemoved(key) 269 } else { 270 keysNeedRemoval.add(key) 271 } 272 } else { 273 keysNeedRemoval.remove(key) 274 } 275 } 276 277 override fun onSmartspaceMediaDataLoaded( 278 key: String, 279 data: SmartspaceMediaData, 280 shouldPrioritize: Boolean, 281 isSsReactivated: Boolean 282 ) { 283 if (DEBUG) Log.d(TAG, "Loading Smartspace media update") 284 if (data.isActive) { 285 if (isSsReactivated && shouldPrioritize) { 286 // Log resume card received if resumable media card is reactivated and 287 // recommendation card is valid and ranked first 288 MediaPlayerData.players().forEachIndexed { index, it -> 289 if (it.recommendationViewHolder == null) { 290 it.mInstanceId = SmallHash.hash(it.mUid + 291 systemClock.currentTimeMillis().toInt()) 292 it.mIsImpressed = false 293 /* ktlint-disable max-line-length */ 294 logSmartspaceCardReported(759, // SMARTSPACE_CARD_RECEIVED 295 it.mInstanceId, 296 it.mUid, 297 /* isRecommendationCard */ false, 298 intArrayOf( 299 SysUiStatsLog.SMART_SPACE_CARD_REPORTED__DISPLAY_SURFACE__SHADE, 300 SysUiStatsLog.SMART_SPACE_CARD_REPORTED__DISPLAY_SURFACE__LOCKSCREEN), 301 rank = index, 302 receivedLatencyMillis = (systemClock.currentTimeMillis() - data.headphoneConnectionTimeMillis).toInt()) 303 /* ktlint-disable max-line-length */ 304 } 305 } 306 } 307 addSmartspaceMediaRecommendations(key, data, shouldPrioritize) 308 MediaPlayerData.getMediaPlayer(key)?.let { 309 /* ktlint-disable max-line-length */ 310 logSmartspaceCardReported(759, // SMARTSPACE_CARD_RECEIVED 311 it.mInstanceId, 312 it.mUid, 313 /* isRecommendationCard */ true, 314 intArrayOf( 315 SysUiStatsLog.SMART_SPACE_CARD_REPORTED__DISPLAY_SURFACE__SHADE, 316 SysUiStatsLog.SMART_SPACE_CARD_REPORTED__DISPLAY_SURFACE__LOCKSCREEN), 317 rank = MediaPlayerData.getMediaPlayerIndex(key), 318 receivedLatencyMillis = (systemClock.currentTimeMillis() - data.headphoneConnectionTimeMillis).toInt()) 319 /* ktlint-disable max-line-length */ 320 } 321 if (mediaCarouselScrollHandler.visibleToUser && 322 mediaCarouselScrollHandler.visibleMediaIndex 323 == MediaPlayerData.getMediaPlayerIndex(key)) { 324 logSmartspaceImpression(mediaCarouselScrollHandler.qsExpanded) 325 } 326 } else { 327 onSmartspaceMediaDataRemoved(data.targetId, immediately = true) 328 } 329 } 330 331 override fun onMediaDataRemoved(key: String) { 332 removePlayer(key) 333 } 334 335 override fun onSmartspaceMediaDataRemoved(key: String, immediately: Boolean) { 336 if (DEBUG) Log.d(TAG, "My Smartspace media removal request is received") 337 if (immediately || visualStabilityManager.isReorderingAllowed) { 338 onMediaDataRemoved(key) 339 } else { 340 keysNeedRemoval.add(key) 341 } 342 } 343 }) 344 mediaFrame.addOnLayoutChangeListener { _, _, _, _, _, _, _, _, _ -> 345 // The pageIndicator is not laid out yet when we get the current state update, 346 // Lets make sure we have the right dimensions 347 updatePageIndicatorLocation() 348 } 349 mediaHostStatesManager.addCallback(object : MediaHostStatesManager.Callback { 350 override fun onHostStateChanged(location: Int, mediaHostState: MediaHostState) { 351 if (location == desiredLocation) { 352 onDesiredLocationChanged(desiredLocation, mediaHostState, animate = false) 353 } 354 } 355 }) 356 } 357 358 private fun inflateSettingsButton() { 359 val settings = LayoutInflater.from(context).inflate(R.layout.media_carousel_settings_button, 360 mediaFrame, false) as View 361 if (this::settingsButton.isInitialized) { 362 mediaFrame.removeView(settingsButton) 363 } 364 settingsButton = settings 365 mediaFrame.addView(settingsButton) 366 mediaCarouselScrollHandler.onSettingsButtonUpdated(settings) 367 settingsButton.setOnClickListener { 368 activityStarter.startActivity(settingsIntent, true /* dismissShade */) 369 } 370 } 371 372 private fun inflateMediaCarousel(): ViewGroup { 373 val mediaCarousel = LayoutInflater.from(context).inflate(R.layout.media_carousel, 374 UniqueObjectHostView(context), false) as ViewGroup 375 // Because this is inflated when not attached to the true view hierarchy, it resolves some 376 // potential issues to force that the layout direction is defined by the locale 377 // (rather than inherited from the parent, which would resolve to LTR when unattached). 378 mediaCarousel.layoutDirection = View.LAYOUT_DIRECTION_LOCALE 379 return mediaCarousel 380 } 381 382 private fun reorderAllPlayers(previousVisiblePlayerKey: MediaPlayerData.MediaSortKey?) { 383 mediaContent.removeAllViews() 384 for (mediaPlayer in MediaPlayerData.players()) { 385 mediaPlayer.playerViewHolder?.let { 386 mediaContent.addView(it.player) 387 } ?: mediaPlayer.recommendationViewHolder?.let { 388 mediaContent.addView(it.recommendations) 389 } 390 } 391 mediaCarouselScrollHandler.onPlayersChanged() 392 393 // Automatically scroll to the active player if needed 394 if (shouldScrollToActivePlayer) { 395 shouldScrollToActivePlayer = false 396 val activeMediaIndex = MediaPlayerData.firstActiveMediaIndex() 397 if (activeMediaIndex != -1) { 398 previousVisiblePlayerKey?.let { 399 val previousVisibleIndex = MediaPlayerData.playerKeys() 400 .indexOfFirst { key -> it == key } 401 mediaCarouselScrollHandler 402 .scrollToPlayer(previousVisibleIndex, activeMediaIndex) 403 } ?: { 404 mediaCarouselScrollHandler.scrollToPlayer(destIndex = activeMediaIndex) 405 } 406 } 407 } 408 } 409 410 // Returns true if new player is added 411 private fun addOrUpdatePlayer(key: String, oldKey: String?, data: MediaData): Boolean { 412 val dataCopy = data.copy(backgroundColor = bgColor) 413 MediaPlayerData.moveIfExists(oldKey, key) 414 val existingPlayer = MediaPlayerData.getMediaPlayer(key) 415 val curVisibleMediaKey = MediaPlayerData.playerKeys() 416 .elementAtOrNull(mediaCarouselScrollHandler.visibleMediaIndex) 417 if (existingPlayer == null) { 418 var newPlayer = mediaControlPanelFactory.get() 419 newPlayer.attachPlayer( 420 PlayerViewHolder.create(LayoutInflater.from(context), mediaContent)) 421 newPlayer.mediaViewController.sizeChangedListener = this::updateCarouselDimensions 422 val lp = LinearLayout.LayoutParams(ViewGroup.LayoutParams.MATCH_PARENT, 423 ViewGroup.LayoutParams.WRAP_CONTENT) 424 newPlayer.playerViewHolder?.player?.setLayoutParams(lp) 425 newPlayer.bindPlayer(dataCopy, key) 426 newPlayer.setListening(currentlyExpanded) 427 MediaPlayerData.addMediaPlayer(key, dataCopy, newPlayer, systemClock) 428 updatePlayerToState(newPlayer, noAnimation = true) 429 reorderAllPlayers(curVisibleMediaKey) 430 } else { 431 existingPlayer.bindPlayer(dataCopy, key) 432 MediaPlayerData.addMediaPlayer(key, dataCopy, existingPlayer, systemClock) 433 if (visualStabilityManager.isReorderingAllowed || shouldScrollToActivePlayer) { 434 reorderAllPlayers(curVisibleMediaKey) 435 } else { 436 needsReordering = true 437 } 438 } 439 updatePageIndicator() 440 mediaCarouselScrollHandler.onPlayersChanged() 441 mediaFrame.requiresRemeasuring = true 442 // Check postcondition: mediaContent should have the same number of children as there are 443 // elements in mediaPlayers. 444 if (MediaPlayerData.players().size != mediaContent.childCount) { 445 Log.wtf(TAG, "Size of players list and number of views in carousel are out of sync") 446 } 447 return existingPlayer == null 448 } 449 450 private fun addSmartspaceMediaRecommendations( 451 key: String, 452 data: SmartspaceMediaData, 453 shouldPrioritize: Boolean 454 ) { 455 if (DEBUG) Log.d(TAG, "Updating smartspace target in carousel") 456 if (MediaPlayerData.getMediaPlayer(key) != null) { 457 Log.w(TAG, "Skip adding smartspace target in carousel") 458 return 459 } 460 461 val existingSmartspaceMediaKey = MediaPlayerData.smartspaceMediaKey() 462 existingSmartspaceMediaKey?.let { 463 MediaPlayerData.removeMediaPlayer(existingSmartspaceMediaKey) 464 } 465 466 var newRecs = mediaControlPanelFactory.get() 467 newRecs.attachRecommendation( 468 RecommendationViewHolder.create(LayoutInflater.from(context), mediaContent)) 469 newRecs.mediaViewController.sizeChangedListener = this::updateCarouselDimensions 470 val lp = LinearLayout.LayoutParams(ViewGroup.LayoutParams.MATCH_PARENT, 471 ViewGroup.LayoutParams.WRAP_CONTENT) 472 newRecs.recommendationViewHolder?.recommendations?.setLayoutParams(lp) 473 newRecs.bindRecommendation(data.copy(backgroundColor = bgColor)) 474 val curVisibleMediaKey = MediaPlayerData.playerKeys() 475 .elementAtOrNull(mediaCarouselScrollHandler.visibleMediaIndex) 476 MediaPlayerData.addMediaRecommendation(key, data, newRecs, shouldPrioritize, systemClock) 477 updatePlayerToState(newRecs, noAnimation = true) 478 reorderAllPlayers(curVisibleMediaKey) 479 updatePageIndicator() 480 mediaFrame.requiresRemeasuring = true 481 // Check postcondition: mediaContent should have the same number of children as there are 482 // elements in mediaPlayers. 483 if (MediaPlayerData.players().size != mediaContent.childCount) { 484 Log.wtf(TAG, "Size of players list and number of views in carousel are out of sync") 485 } 486 } 487 488 fun removePlayer( 489 key: String, 490 dismissMediaData: Boolean = true, 491 dismissRecommendation: Boolean = true 492 ) { 493 val removed = MediaPlayerData.removeMediaPlayer(key) 494 removed?.apply { 495 mediaCarouselScrollHandler.onPrePlayerRemoved(removed) 496 mediaContent.removeView(removed.playerViewHolder?.player) 497 mediaContent.removeView(removed.recommendationViewHolder?.recommendations) 498 removed.onDestroy() 499 mediaCarouselScrollHandler.onPlayersChanged() 500 updatePageIndicator() 501 502 if (dismissMediaData) { 503 // Inform the media manager of a potentially late dismissal 504 mediaManager.dismissMediaData(key, delay = 0L) 505 } 506 if (dismissRecommendation) { 507 // Inform the media manager of a potentially late dismissal 508 mediaManager.dismissSmartspaceRecommendation(key, delay = 0L) 509 } 510 } 511 } 512 513 private fun recreatePlayers() { 514 bgColor = getBackgroundColor() 515 pageIndicator.tintList = ColorStateList.valueOf(getForegroundColor()) 516 517 MediaPlayerData.mediaData().forEach { (key, data, isSsMediaRec) -> 518 if (isSsMediaRec) { 519 val smartspaceMediaData = MediaPlayerData.smartspaceMediaData 520 removePlayer(key, dismissMediaData = false, dismissRecommendation = false) 521 smartspaceMediaData?.let { 522 addSmartspaceMediaRecommendations( 523 it.targetId, it, MediaPlayerData.shouldPrioritizeSs) 524 } 525 } else { 526 removePlayer(key, dismissMediaData = false, dismissRecommendation = false) 527 addOrUpdatePlayer(key = key, oldKey = null, data = data) 528 } 529 } 530 } 531 532 private fun getBackgroundColor(): Int { 533 return context.getColor(android.R.color.system_accent2_50) 534 } 535 536 private fun getForegroundColor(): Int { 537 return context.getColor(android.R.color.system_accent2_900) 538 } 539 540 private fun updatePageIndicator() { 541 val numPages = mediaContent.getChildCount() 542 pageIndicator.setNumPages(numPages) 543 if (numPages == 1) { 544 pageIndicator.setLocation(0f) 545 } 546 updatePageIndicatorAlpha() 547 } 548 549 /** 550 * Set a new interpolated state for all players. This is a state that is usually controlled 551 * by a finger movement where the user drags from one state to the next. 552 * 553 * @param startLocation the start location of our state or -1 if this is directly set 554 * @param endLocation the ending location of our state. 555 * @param progress the progress of the transition between startLocation and endlocation. If 556 * this is not a guided transformation, this will be 1.0f 557 * @param immediately should this state be applied immediately, canceling all animations? 558 */ 559 fun setCurrentState( 560 @MediaLocation startLocation: Int, 561 @MediaLocation endLocation: Int, 562 progress: Float, 563 immediately: Boolean 564 ) { 565 if (startLocation != currentStartLocation || 566 endLocation != currentEndLocation || 567 progress != currentTransitionProgress || 568 immediately 569 ) { 570 currentStartLocation = startLocation 571 currentEndLocation = endLocation 572 currentTransitionProgress = progress 573 for (mediaPlayer in MediaPlayerData.players()) { 574 updatePlayerToState(mediaPlayer, immediately) 575 } 576 maybeResetSettingsCog() 577 updatePageIndicatorAlpha() 578 } 579 } 580 581 private fun updatePageIndicatorAlpha() { 582 val hostStates = mediaHostStatesManager.mediaHostStates 583 val endIsVisible = hostStates[currentEndLocation]?.visible ?: false 584 val startIsVisible = hostStates[currentStartLocation]?.visible ?: false 585 val startAlpha = if (startIsVisible) 1.0f else 0.0f 586 val endAlpha = if (endIsVisible) 1.0f else 0.0f 587 var alpha = 1.0f 588 if (!endIsVisible || !startIsVisible) { 589 var progress = currentTransitionProgress 590 if (!endIsVisible) { 591 progress = 1.0f - progress 592 } 593 // Let's fade in quickly at the end where the view is visible 594 progress = MathUtils.constrain( 595 MathUtils.map(0.95f, 1.0f, 0.0f, 1.0f, progress), 596 0.0f, 597 1.0f) 598 alpha = MathUtils.lerp(startAlpha, endAlpha, progress) 599 } 600 pageIndicator.alpha = alpha 601 } 602 603 private fun updatePageIndicatorLocation() { 604 // Update the location of the page indicator, carousel clipping 605 val translationX = if (isRtl) { 606 (pageIndicator.width - currentCarouselWidth) / 2.0f 607 } else { 608 (currentCarouselWidth - pageIndicator.width) / 2.0f 609 } 610 pageIndicator.translationX = translationX + mediaCarouselScrollHandler.contentTranslation 611 val layoutParams = pageIndicator.layoutParams as ViewGroup.MarginLayoutParams 612 pageIndicator.translationY = (currentCarouselHeight - pageIndicator.height - 613 layoutParams.bottomMargin).toFloat() 614 } 615 616 /** 617 * Update the dimension of this carousel. 618 */ 619 private fun updateCarouselDimensions() { 620 var width = 0 621 var height = 0 622 for (mediaPlayer in MediaPlayerData.players()) { 623 val controller = mediaPlayer.mediaViewController 624 // When transitioning the view to gone, the view gets smaller, but the translation 625 // Doesn't, let's add the translation 626 width = Math.max(width, controller.currentWidth + controller.translationX.toInt()) 627 height = Math.max(height, controller.currentHeight + controller.translationY.toInt()) 628 } 629 if (width != currentCarouselWidth || height != currentCarouselHeight) { 630 currentCarouselWidth = width 631 currentCarouselHeight = height 632 mediaCarouselScrollHandler.setCarouselBounds( 633 currentCarouselWidth, currentCarouselHeight) 634 updatePageIndicatorLocation() 635 } 636 } 637 638 private fun maybeResetSettingsCog() { 639 val hostStates = mediaHostStatesManager.mediaHostStates 640 val endShowsActive = hostStates[currentEndLocation]?.showsOnlyActiveMedia 641 ?: true 642 val startShowsActive = hostStates[currentStartLocation]?.showsOnlyActiveMedia 643 ?: endShowsActive 644 if (currentlyShowingOnlyActive != endShowsActive || 645 ((currentTransitionProgress != 1.0f && currentTransitionProgress != 0.0f) && 646 startShowsActive != endShowsActive)) { 647 // Whenever we're transitioning from between differing states or the endstate differs 648 // we reset the translation 649 currentlyShowingOnlyActive = endShowsActive 650 mediaCarouselScrollHandler.resetTranslation(animate = true) 651 } 652 } 653 654 private fun updatePlayerToState(mediaPlayer: MediaControlPanel, noAnimation: Boolean) { 655 mediaPlayer.mediaViewController.setCurrentState( 656 startLocation = currentStartLocation, 657 endLocation = currentEndLocation, 658 transitionProgress = currentTransitionProgress, 659 applyImmediately = noAnimation) 660 } 661 662 /** 663 * The desired location of this view has changed. We should remeasure the view to match 664 * the new bounds and kick off bounds animations if necessary. 665 * If an animation is happening, an animation is kicked of externally, which sets a new 666 * current state until we reach the targetState. 667 * 668 * @param desiredLocation the location we're going to 669 * @param desiredHostState the target state we're transitioning to 670 * @param animate should this be animated 671 */ 672 fun onDesiredLocationChanged( 673 desiredLocation: Int, 674 desiredHostState: MediaHostState?, 675 animate: Boolean, 676 duration: Long = 200, 677 startDelay: Long = 0 678 ) { 679 desiredHostState?.let { 680 // This is a hosting view, let's remeasure our players 681 this.desiredLocation = desiredLocation 682 this.desiredHostState = it 683 currentlyExpanded = it.expansion > 0 684 685 val shouldCloseGuts = !currentlyExpanded && !mediaManager.hasActiveMedia() && 686 desiredHostState.showsOnlyActiveMedia 687 688 for (mediaPlayer in MediaPlayerData.players()) { 689 if (animate) { 690 mediaPlayer.mediaViewController.animatePendingStateChange( 691 duration = duration, 692 delay = startDelay) 693 } 694 if (shouldCloseGuts && mediaPlayer.mediaViewController.isGutsVisible) { 695 mediaPlayer.closeGuts(!animate) 696 } 697 698 mediaPlayer.mediaViewController.onLocationPreChange(desiredLocation) 699 } 700 mediaCarouselScrollHandler.showsSettingsButton = !it.showsOnlyActiveMedia 701 mediaCarouselScrollHandler.falsingProtectionNeeded = it.falsingProtectionNeeded 702 val nowVisible = it.visible 703 if (nowVisible != playersVisible) { 704 playersVisible = nowVisible 705 if (nowVisible) { 706 mediaCarouselScrollHandler.resetTranslation() 707 } 708 } 709 updateCarouselSize() 710 } 711 } 712 713 fun closeGuts(immediate: Boolean = true) { 714 MediaPlayerData.players().forEach { 715 it.closeGuts(immediate) 716 } 717 } 718 719 /** 720 * Update the size of the carousel, remeasuring it if necessary. 721 */ 722 private fun updateCarouselSize() { 723 val width = desiredHostState?.measurementInput?.width ?: 0 724 val height = desiredHostState?.measurementInput?.height ?: 0 725 if (width != carouselMeasureWidth && width != 0 || 726 height != carouselMeasureHeight && height != 0) { 727 carouselMeasureWidth = width 728 carouselMeasureHeight = height 729 val playerWidthPlusPadding = carouselMeasureWidth + 730 context.resources.getDimensionPixelSize(R.dimen.qs_media_padding) 731 // Let's remeasure the carousel 732 val widthSpec = desiredHostState?.measurementInput?.widthMeasureSpec ?: 0 733 val heightSpec = desiredHostState?.measurementInput?.heightMeasureSpec ?: 0 734 mediaCarousel.measure(widthSpec, heightSpec) 735 mediaCarousel.layout(0, 0, width, mediaCarousel.measuredHeight) 736 // Update the padding after layout; view widths are used in RTL to calculate scrollX 737 mediaCarouselScrollHandler.playerWidthPlusPadding = playerWidthPlusPadding 738 } 739 } 740 741 /** 742 * Log the user impression for media card at visibleMediaIndex. 743 */ 744 fun logSmartspaceImpression(qsExpanded: Boolean) { 745 val visibleMediaIndex = mediaCarouselScrollHandler.visibleMediaIndex 746 if (MediaPlayerData.players().size > visibleMediaIndex) { 747 val mediaControlPanel = MediaPlayerData.players().elementAt(visibleMediaIndex) 748 val hasActiveMediaOrRecommendationCard = 749 MediaPlayerData.hasActiveMediaOrRecommendationCard() 750 val isRecommendationCard = mediaControlPanel.recommendationViewHolder != null 751 if (!hasActiveMediaOrRecommendationCard && !qsExpanded) { 752 // Skip logging if on LS or QQS, and there is no active media card 753 return 754 } 755 logSmartspaceCardReported(800, // SMARTSPACE_CARD_SEEN 756 mediaControlPanel.mInstanceId, 757 mediaControlPanel.mUid, 758 isRecommendationCard, 759 intArrayOf(mediaControlPanel.surfaceForSmartspaceLogging)) 760 mediaControlPanel.mIsImpressed = true 761 } 762 } 763 764 @JvmOverloads 765 /** 766 * Log Smartspace events 767 * 768 * @param eventId UI event id (e.g. 800 for SMARTSPACE_CARD_SEEN) 769 * @param instanceId id to uniquely identify a card, e.g. each headphone generates a new 770 * instanceId 771 * @param uid uid for the application that media comes from 772 * @param isRecommendationCard whether the card is media recommendation 773 * @param surfaces list of display surfaces the media card is on (e.g. lockscreen, shade) when 774 * the event happened 775 * @param interactedSubcardRank the rank for interacted media item for recommendation card, -1 776 * for tapping on card but not on any media item, 0 for first media item, 1 for second, etc. 777 * @param interactedSubcardCardinality how many media items were shown to the user when there 778 * is user interaction 779 * @param rank the rank for media card in the media carousel, starting from 0 780 * @param receivedLatencyMillis latency in milliseconds for card received events. E.g. latency 781 * between headphone connection to sysUI displays media recommendation card 782 * 783 */ 784 fun logSmartspaceCardReported( 785 eventId: Int, 786 instanceId: Int, 787 uid: Int, 788 isRecommendationCard: Boolean, 789 surfaces: IntArray, 790 interactedSubcardRank: Int = 0, 791 interactedSubcardCardinality: Int = 0, 792 rank: Int = mediaCarouselScrollHandler.visibleMediaIndex, 793 receivedLatencyMillis: Int = 0 794 ) { 795 // Only log media resume card when Smartspace data is available 796 if (!isRecommendationCard && 797 !mediaManager.smartspaceMediaData.isActive && 798 MediaPlayerData.smartspaceMediaData == null) { 799 return 800 } 801 802 val cardinality = mediaContent.getChildCount() 803 surfaces.forEach { surface -> 804 /* ktlint-disable max-line-length */ 805 SysUiStatsLog.write(SysUiStatsLog.SMARTSPACE_CARD_REPORTED, 806 eventId, 807 instanceId, 808 // Deprecated, replaced with AiAi feature type so we don't need to create logging 809 // card type for each new feature. 810 SysUiStatsLog.SMART_SPACE_CARD_REPORTED__CARD_TYPE__UNKNOWN_CARD, 811 surface, 812 rank, 813 cardinality, 814 if (isRecommendationCard) 815 15 // MEDIA_RECOMMENDATION 816 else 817 31, // MEDIA_RESUME 818 uid, 819 interactedSubcardRank, 820 interactedSubcardCardinality, 821 receivedLatencyMillis 822 ) 823 /* ktlint-disable max-line-length */ 824 if (DEBUG) { 825 Log.d(TAG, "Log Smartspace card event id: $eventId instance id: $instanceId" + 826 " surface: $surface rank: $rank cardinality: $cardinality " + 827 "isRecommendationCard: $isRecommendationCard uid: $uid " + 828 "interactedSubcardRank: $interactedSubcardRank " + 829 "interactedSubcardCardinality: $interactedSubcardCardinality " + 830 "received_latency_millis: $receivedLatencyMillis") 831 } 832 } 833 } 834 835 private fun onSwipeToDismiss() { 836 MediaPlayerData.players().forEachIndexed { 837 index, it -> 838 if (it.mIsImpressed) { 839 logSmartspaceCardReported(761, // SMARTSPACE_CARD_DISMISS 840 it.mInstanceId, 841 it.mUid, 842 it.recommendationViewHolder != null, 843 intArrayOf(it.surfaceForSmartspaceLogging), 844 // Use -1 as rank value to indicate user swipe to dismiss the card 845 rank = -1) 846 // Reset card impressed state when swipe to dismissed 847 it.mIsImpressed = false 848 } 849 } 850 mediaManager.onSwipeToDismiss() 851 } 852 853 override fun dump(fd: FileDescriptor, pw: PrintWriter, args: Array<out String>) { 854 pw.apply { 855 println("keysNeedRemoval: $keysNeedRemoval") 856 println("playerKeys: ${MediaPlayerData.playerKeys()}") 857 println("smartspaceMediaData: ${MediaPlayerData.smartspaceMediaData}") 858 println("shouldPrioritizeSs: ${MediaPlayerData.shouldPrioritizeSs}") 859 } 860 } 861 } 862 863 @VisibleForTesting 864 internal object MediaPlayerData { 865 private val EMPTY = MediaData(-1, false, 0, null, null, null, null, null, 866 emptyList(), emptyList(), "INVALID", null, null, null, true, null) 867 // Whether should prioritize Smartspace card. 868 internal var shouldPrioritizeSs: Boolean = false 869 private set 870 internal var smartspaceMediaData: SmartspaceMediaData? = null 871 private set 872 873 data class MediaSortKey( 874 // Whether the item represents a Smartspace media recommendation. 875 val isSsMediaRec: Boolean, 876 val data: MediaData, 877 val updateTime: Long = 0 878 ) 879 880 private val comparator = 881 compareByDescending<MediaSortKey> { it.data.isPlaying == true && 882 it.data.playbackLocation == MediaData.PLAYBACK_LOCAL } 883 .thenByDescending { it.data.isPlaying == true && 884 it.data.playbackLocation == MediaData.PLAYBACK_CAST_LOCAL } 885 .thenByDescending { if (shouldPrioritizeSs) it.isSsMediaRec else !it.isSsMediaRec } 886 .thenByDescending { !it.data.resumption } 887 .thenByDescending { it.data.playbackLocation != MediaData.PLAYBACK_CAST_REMOTE } 888 .thenByDescending { it.updateTime } 889 .thenByDescending { it.data.notificationKey } 890 891 private val mediaPlayers = TreeMap<MediaSortKey, MediaControlPanel>(comparator) 892 private val mediaData: MutableMap<String, MediaSortKey> = mutableMapOf() 893 894 fun addMediaPlayer(key: String, data: MediaData, player: MediaControlPanel, clock: SystemClock) { 895 removeMediaPlayer(key) 896 val sortKey = MediaSortKey(isSsMediaRec = false, data, clock.currentTimeMillis()) 897 mediaData.put(key, sortKey) 898 mediaPlayers.put(sortKey, player) 899 } 900 901 fun addMediaRecommendation( 902 key: String, 903 data: SmartspaceMediaData, 904 player: MediaControlPanel, 905 shouldPrioritize: Boolean, 906 clock: SystemClock 907 ) { 908 shouldPrioritizeSs = shouldPrioritize 909 removeMediaPlayer(key) 910 val sortKey = MediaSortKey(/* isSsMediaRec= */ true, 911 EMPTY.copy(isPlaying = false), clock.currentTimeMillis()) 912 mediaData.put(key, sortKey) 913 mediaPlayers.put(sortKey, player) 914 smartspaceMediaData = data 915 } 916 917 fun moveIfExists(oldKey: String?, newKey: String) { 918 if (oldKey == null || oldKey == newKey) { 919 return 920 } 921 922 mediaData.remove(oldKey)?.let { 923 removeMediaPlayer(newKey) 924 mediaData.put(newKey, it) 925 } 926 } 927 928 fun getMediaPlayer(key: String): MediaControlPanel? { 929 return mediaData.get(key)?.let { mediaPlayers.get(it) } 930 } 931 932 fun getMediaPlayerIndex(key: String): Int { 933 val sortKey = mediaData.get(key) 934 mediaPlayers.entries.forEachIndexed { index, e -> 935 if (e.key == sortKey) { 936 return index 937 } 938 } 939 return -1 940 } 941 942 fun removeMediaPlayer(key: String) = mediaData.remove(key)?.let { 943 if (it.isSsMediaRec) { 944 smartspaceMediaData = null 945 } 946 mediaPlayers.remove(it) 947 } 948 949 fun mediaData() = mediaData.entries.map { e -> Triple(e.key, e.value.data, e.value.isSsMediaRec) } 950 951 fun players() = mediaPlayers.values 952 953 fun playerKeys() = mediaPlayers.keys 954 955 /** Returns the index of the first non-timeout media. */ 956 fun firstActiveMediaIndex(): Int { 957 mediaPlayers.entries.forEachIndexed { index, e -> 958 if (!e.key.isSsMediaRec && e.key.data.active) { 959 return index 960 } 961 } 962 return -1 963 } 964 965 /** Returns the existing Smartspace target id. */ 966 fun smartspaceMediaKey(): String? { 967 mediaData.entries.forEach { e -> 968 if (e.value.isSsMediaRec) { 969 return e.key 970 } 971 } 972 return null 973 } 974 975 @VisibleForTesting 976 fun clear() { 977 mediaData.clear() 978 mediaPlayers.clear() 979 } 980 981 /* Returns true if there is active media player card or recommendation card */ 982 fun hasActiveMediaOrRecommendationCard(): Boolean { 983 if (smartspaceMediaData != null && smartspaceMediaData?.isActive!!) { 984 return true 985 } 986 if (firstActiveMediaIndex() != -1) { 987 return true 988 } 989 return false 990 } 991 }