1 /* <lambda>null2 * Copyright (C) 2020 The Android Open Source Project 3 * 4 * Licensed under the Apache License, Version 2.0 (the "License"); 5 * you may not use this file except in compliance with the License. 6 * You may obtain a copy of the License at 7 * 8 * http://www.apache.org/licenses/LICENSE-2.0 9 * 10 * Unless required by applicable law or agreed to in writing, software 11 * distributed under the License is distributed on an "AS IS" BASIS, 12 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 * See the License for the specific language governing permissions and 14 * limitations under the License. 15 */ 16 17 package com.android.systemui.media.controls.domain.pipeline 18 19 import android.annotation.MainThread 20 import android.annotation.SuppressLint 21 import android.app.Notification 22 import android.app.Notification.EXTRA_SUBSTITUTE_APP_NAME 23 import android.app.PendingIntent 24 import android.app.StatusBarManager 25 import android.app.UriGrantsManager 26 import android.content.BroadcastReceiver 27 import android.content.ContentProvider 28 import android.content.ContentResolver 29 import android.content.Context 30 import android.content.Intent 31 import android.content.IntentFilter 32 import android.content.pm.ApplicationInfo 33 import android.content.pm.PackageManager 34 import android.graphics.Bitmap 35 import android.graphics.ImageDecoder 36 import android.graphics.drawable.Icon 37 import android.media.MediaDescription 38 import android.media.MediaMetadata 39 import android.media.session.MediaController 40 import android.media.session.MediaSession 41 import android.media.session.PlaybackState 42 import android.net.Uri 43 import android.os.Process 44 import android.os.UserHandle 45 import android.service.notification.StatusBarNotification 46 import android.support.v4.media.MediaMetadataCompat 47 import android.text.TextUtils 48 import android.util.Log 49 import android.util.Pair as APair 50 import androidx.media.utils.MediaConstants 51 import com.android.app.tracing.coroutines.launchTraced as launch 52 import com.android.app.tracing.traceSection 53 import com.android.internal.logging.InstanceId 54 import com.android.keyguard.KeyguardUpdateMonitor 55 import com.android.systemui.Dumpable 56 import com.android.systemui.Flags 57 import com.android.systemui.broadcast.BroadcastDispatcher 58 import com.android.systemui.dagger.SysUISingleton 59 import com.android.systemui.dagger.qualifiers.Application 60 import com.android.systemui.dagger.qualifiers.Background 61 import com.android.systemui.dagger.qualifiers.Main 62 import com.android.systemui.dump.DumpManager 63 import com.android.systemui.media.NotificationMediaManager.isPlayingState 64 import com.android.systemui.media.controls.domain.pipeline.MediaDataManager.Companion.isMediaNotification 65 import com.android.systemui.media.controls.domain.resume.MediaResumeListener 66 import com.android.systemui.media.controls.domain.resume.ResumeMediaBrowser 67 import com.android.systemui.media.controls.shared.MediaLogger 68 import com.android.systemui.media.controls.shared.model.MediaAction 69 import com.android.systemui.media.controls.shared.model.MediaButton 70 import com.android.systemui.media.controls.shared.model.MediaData 71 import com.android.systemui.media.controls.shared.model.MediaDeviceData 72 import com.android.systemui.media.controls.shared.model.MediaNotificationAction 73 import com.android.systemui.media.controls.ui.view.MediaViewHolder 74 import com.android.systemui.media.controls.util.MediaControllerFactory 75 import com.android.systemui.media.controls.util.MediaDataUtils 76 import com.android.systemui.media.controls.util.MediaFlags 77 import com.android.systemui.media.controls.util.MediaUiEventLogger 78 import com.android.systemui.res.R 79 import com.android.systemui.statusbar.notification.row.HybridGroupManager 80 import com.android.systemui.util.Assert 81 import com.android.systemui.util.Utils 82 import com.android.systemui.util.concurrency.DelayableExecutor 83 import com.android.systemui.util.concurrency.ThreadFactory 84 import com.android.systemui.util.time.SystemClock 85 import java.io.IOException 86 import java.io.PrintWriter 87 import java.util.Collections 88 import java.util.concurrent.Executor 89 import javax.inject.Inject 90 import kotlinx.coroutines.CoroutineDispatcher 91 import kotlinx.coroutines.CoroutineScope 92 import kotlinx.coroutines.withContext 93 94 // URI fields to try loading album art from 95 private val ART_URIS = 96 arrayOf( 97 MediaMetadata.METADATA_KEY_ALBUM_ART_URI, 98 MediaMetadata.METADATA_KEY_ART_URI, 99 MediaMetadata.METADATA_KEY_DISPLAY_ICON_URI, 100 ) 101 102 private const val TAG = "MediaDataManager" 103 private const val DEBUG = true 104 105 private val LOADING = 106 MediaData( 107 userId = -1, 108 initialized = false, 109 app = null, 110 appIcon = null, 111 artist = null, 112 song = null, 113 artwork = null, 114 actions = emptyList(), 115 actionsToShowInCompact = emptyList(), 116 packageName = "INVALID", 117 token = null, 118 clickIntent = null, 119 device = null, 120 active = true, 121 resumeAction = null, 122 instanceId = InstanceId.fakeInstanceId(-1), 123 appUid = Process.INVALID_UID, 124 ) 125 126 /** A class that facilitates management and loading of Media Data, ready for binding. */ 127 @SysUISingleton 128 class LegacyMediaDataManagerImpl( 129 private val context: Context, 130 @Background private val backgroundExecutor: Executor, 131 @Background private val backgroundDispatcher: CoroutineDispatcher, 132 @Main private val foregroundExecutor: DelayableExecutor, 133 @Main private val mainDispatcher: CoroutineDispatcher, 134 @Application private val applicationScope: CoroutineScope, 135 private val mediaControllerFactory: MediaControllerFactory, 136 private val broadcastDispatcher: BroadcastDispatcher, 137 dumpManager: DumpManager, 138 mediaTimeoutListener: MediaTimeoutListener, 139 mediaResumeListener: MediaResumeListener, 140 mediaSessionBasedFilter: MediaSessionBasedFilter, 141 private val mediaDeviceManager: MediaDeviceManager, 142 mediaDataCombineLatest: MediaDataCombineLatest, 143 private val mediaDataFilter: LegacyMediaDataFilterImpl, 144 private var useMediaResumption: Boolean, 145 private val useQsMediaPlayer: Boolean, 146 private val systemClock: SystemClock, 147 private val mediaFlags: MediaFlags, 148 private val logger: MediaUiEventLogger, 149 private val keyguardUpdateMonitor: KeyguardUpdateMonitor, 150 private val mediaDataLoader: dagger.Lazy<MediaDataLoader>, 151 private val mediaLogger: MediaLogger, 152 ) : Dumpable, MediaDataManager { 153 154 companion object { 155 // Maximum number of actions allowed in compact view 156 @JvmField val MAX_COMPACT_ACTIONS = 3 157 158 // Maximum number of actions allowed in expanded view 159 @JvmField val MAX_NOTIFICATION_ACTIONS = MediaViewHolder.genericButtonIds.size 160 } 161 162 private val themeText = 163 com.android.settingslib.Utils.getColorAttr( 164 context, 165 com.android.internal.R.attr.textColorPrimary, 166 ) 167 .defaultColor 168 169 // Internal listeners are part of the internal pipeline. External listeners (those registered 170 // with [MediaDeviceManager.addListener]) receive events after they have propagated through 171 // the internal pipeline. 172 // Another way to think of the distinction between internal and external listeners is the 173 // following. Internal listeners are listeners that MediaDataManager depends on, and external 174 // listeners are listeners that depend on MediaDataManager. 175 // TODO(b/159539991#comment5): Move internal listeners to separate package. 176 private val internalListeners: MutableSet<MediaDataManager.Listener> = mutableSetOf() 177 private val mediaEntries: MutableMap<String, MediaData> = 178 if (Flags.mediaLoadMetadataViaMediaDataLoader()) { 179 Collections.synchronizedMap(LinkedHashMap()) 180 } else { 181 LinkedHashMap() 182 } 183 184 private val artworkWidth = 185 context.resources.getDimensionPixelSize( 186 com.android.internal.R.dimen.config_mediaMetadataBitmapMaxSize 187 ) 188 private val artworkHeight = 189 context.resources.getDimensionPixelSize(R.dimen.qs_media_session_height_expanded) 190 191 @SuppressLint("WrongConstant") // sysui allowed to call STATUS_BAR_SERVICE 192 private val statusBarManager = 193 context.getSystemService(Context.STATUS_BAR_SERVICE) as StatusBarManager 194 195 /** Check whether this notification is an RCN */ 196 private fun isRemoteCastNotification(sbn: StatusBarNotification): Boolean { 197 return sbn.notification.extras.containsKey(Notification.EXTRA_MEDIA_REMOTE_DEVICE) 198 } 199 200 @Inject 201 constructor( 202 context: Context, 203 threadFactory: ThreadFactory, 204 @Background backgroundDispatcher: CoroutineDispatcher, 205 @Main foregroundExecutor: DelayableExecutor, 206 @Main mainDispatcher: CoroutineDispatcher, 207 @Application applicationScope: CoroutineScope, 208 mediaControllerFactory: MediaControllerFactory, 209 dumpManager: DumpManager, 210 broadcastDispatcher: BroadcastDispatcher, 211 mediaTimeoutListener: MediaTimeoutListener, 212 mediaResumeListener: MediaResumeListener, 213 mediaSessionBasedFilter: MediaSessionBasedFilter, 214 mediaDeviceManager: MediaDeviceManager, 215 mediaDataCombineLatest: MediaDataCombineLatest, 216 mediaDataFilter: LegacyMediaDataFilterImpl, 217 clock: SystemClock, 218 mediaFlags: MediaFlags, 219 logger: MediaUiEventLogger, 220 keyguardUpdateMonitor: KeyguardUpdateMonitor, 221 mediaDataLoader: dagger.Lazy<MediaDataLoader>, 222 mediaLogger: MediaLogger, 223 ) : this( 224 context, 225 // Loading bitmap for UMO background can take longer time, so it cannot run on the default 226 // background thread. Use a custom thread for media. 227 threadFactory.buildExecutorOnNewThread(TAG), 228 backgroundDispatcher, 229 foregroundExecutor, 230 mainDispatcher, 231 applicationScope, 232 mediaControllerFactory, 233 broadcastDispatcher, 234 dumpManager, 235 mediaTimeoutListener, 236 mediaResumeListener, 237 mediaSessionBasedFilter, 238 mediaDeviceManager, 239 mediaDataCombineLatest, 240 mediaDataFilter, 241 Utils.useMediaResumption(context), 242 Utils.useQsMediaPlayer(context), 243 clock, 244 mediaFlags, 245 logger, 246 keyguardUpdateMonitor, 247 mediaDataLoader, 248 mediaLogger, 249 ) 250 251 private val appChangeReceiver = 252 object : BroadcastReceiver() { 253 override fun onReceive(context: Context, intent: Intent) { 254 when (intent.action) { 255 Intent.ACTION_PACKAGES_SUSPENDED -> { 256 val packages = intent.getStringArrayExtra(Intent.EXTRA_CHANGED_PACKAGE_LIST) 257 packages?.forEach { removeAllForPackage(it) } 258 } 259 Intent.ACTION_PACKAGE_REMOVED, 260 Intent.ACTION_PACKAGE_RESTARTED -> { 261 intent.data?.encodedSchemeSpecificPart?.let { removeAllForPackage(it) } 262 } 263 } 264 } 265 } 266 267 init { 268 dumpManager.registerNormalDumpable(TAG, this) 269 270 // Initialize the internal processing pipeline. The listeners at the front of the pipeline 271 // are set as internal listeners so that they receive events. From there, events are 272 // propagated through the pipeline. The end of the pipeline is currently mediaDataFilter, 273 // so it is responsible for dispatching events to external listeners. To achieve this, 274 // external listeners that are registered with [MediaDataManager.addListener] are actually 275 // registered as listeners to mediaDataFilter. 276 addInternalListener(mediaTimeoutListener) 277 addInternalListener(mediaResumeListener) 278 addInternalListener(mediaSessionBasedFilter) 279 mediaSessionBasedFilter.addListener(mediaDeviceManager) 280 mediaSessionBasedFilter.addListener(mediaDataCombineLatest) 281 mediaDeviceManager.addListener(mediaDataCombineLatest) 282 mediaDataCombineLatest.addListener(mediaDataFilter) 283 284 // Set up links back into the pipeline for listeners that need to send events upstream. 285 mediaTimeoutListener.timeoutCallback = { key: String, timedOut: Boolean -> 286 setInactive(key, timedOut) 287 } 288 mediaTimeoutListener.stateCallback = { key: String, state: PlaybackState -> 289 updateState(key, state) 290 } 291 mediaTimeoutListener.sessionCallback = { key: String -> onSessionDestroyed(key) } 292 mediaResumeListener.setManager(this) 293 mediaDataFilter.mediaDataManager = this 294 295 val suspendFilter = IntentFilter(Intent.ACTION_PACKAGES_SUSPENDED) 296 broadcastDispatcher.registerReceiver(appChangeReceiver, suspendFilter, null, UserHandle.ALL) 297 298 val uninstallFilter = 299 IntentFilter().apply { 300 addAction(Intent.ACTION_PACKAGE_REMOVED) 301 addAction(Intent.ACTION_PACKAGE_RESTARTED) 302 addDataScheme("package") 303 } 304 // BroadcastDispatcher does not allow filters with data schemes 305 context.registerReceiver(appChangeReceiver, uninstallFilter) 306 } 307 308 override fun destroy() { 309 context.unregisterReceiver(appChangeReceiver) 310 } 311 312 override fun onNotificationAdded(key: String, sbn: StatusBarNotification) { 313 if (useQsMediaPlayer && isMediaNotification(sbn)) { 314 var isNewlyActiveEntry = false 315 var isConvertingToActive = false 316 Assert.isMainThread() 317 val oldKey = findExistingEntry(key, sbn.packageName) 318 if (oldKey == null) { 319 val instanceId = logger.getNewInstanceId() 320 val temp = 321 LOADING.copy( 322 packageName = sbn.packageName, 323 instanceId = instanceId, 324 createdTimestampMillis = systemClock.currentTimeMillis(), 325 ) 326 mediaEntries.put(key, temp) 327 isNewlyActiveEntry = true 328 } else if (oldKey != key) { 329 // Resume -> active conversion; move to new key 330 val oldData = mediaEntries.remove(oldKey)!! 331 isNewlyActiveEntry = true 332 isConvertingToActive = true 333 mediaEntries.put(key, oldData) 334 } 335 loadMediaData(key, sbn, oldKey, isNewlyActiveEntry, isConvertingToActive) 336 } else { 337 onNotificationRemoved(key) 338 } 339 } 340 341 private fun removeAllForPackage(packageName: String) { 342 Assert.isMainThread() 343 val toRemove = mediaEntries.filter { it.value.packageName == packageName } 344 toRemove.forEach { removeEntry(it.key) } 345 } 346 347 override fun setResumeAction(key: String, action: Runnable?) { 348 mediaEntries.get(key)?.let { 349 it.resumeAction = action 350 it.hasCheckedForResume = true 351 } 352 } 353 354 override fun addResumptionControls( 355 userId: Int, 356 desc: MediaDescription, 357 action: Runnable, 358 token: MediaSession.Token, 359 appName: String, 360 appIntent: PendingIntent, 361 packageName: String, 362 ) { 363 // Resume controls don't have a notification key, so store by package name instead 364 if (!mediaEntries.containsKey(packageName)) { 365 val instanceId = logger.getNewInstanceId() 366 val appUid = 367 try { 368 context.packageManager.getApplicationInfo(packageName, 0)?.uid!! 369 } catch (e: PackageManager.NameNotFoundException) { 370 Log.w(TAG, "Could not get app UID for $packageName", e) 371 Process.INVALID_UID 372 } 373 374 val resumeData = 375 LOADING.copy( 376 packageName = packageName, 377 resumeAction = action, 378 hasCheckedForResume = true, 379 instanceId = instanceId, 380 appUid = appUid, 381 createdTimestampMillis = systemClock.currentTimeMillis(), 382 ) 383 mediaEntries.put(packageName, resumeData) 384 logSingleVsMultipleMediaAdded(appUid, packageName, instanceId) 385 logger.logResumeMediaAdded(appUid, packageName, instanceId) 386 } 387 388 if (Flags.mediaLoadMetadataViaMediaDataLoader()) { 389 applicationScope.launch { 390 loadMediaDataForResumption( 391 userId, 392 desc, 393 action, 394 token, 395 appName, 396 appIntent, 397 packageName, 398 ) 399 } 400 } else { 401 backgroundExecutor.execute { 402 loadMediaDataInBgForResumption( 403 userId, 404 desc, 405 action, 406 token, 407 appName, 408 appIntent, 409 packageName, 410 ) 411 } 412 } 413 } 414 415 /** 416 * Check if there is an existing entry that matches the key or package name. Returns the key 417 * that matches, or null if not found. 418 */ 419 private fun findExistingEntry(key: String, packageName: String): String? { 420 if (mediaEntries.containsKey(key)) { 421 return key 422 } 423 // Check if we already had a resume player 424 if (mediaEntries.containsKey(packageName)) { 425 return packageName 426 } 427 return null 428 } 429 430 private fun loadMediaData( 431 key: String, 432 sbn: StatusBarNotification, 433 oldKey: String?, 434 isNewlyActiveEntry: Boolean = false, 435 isConvertingToActive: Boolean = false, 436 ) { 437 if (Flags.mediaLoadMetadataViaMediaDataLoader()) { 438 applicationScope.launch { 439 loadMediaDataWithLoader(key, sbn, oldKey, isNewlyActiveEntry, isConvertingToActive) 440 } 441 } else { 442 backgroundExecutor.execute { loadMediaDataInBg(key, sbn, oldKey, isNewlyActiveEntry) } 443 } 444 } 445 446 private suspend fun loadMediaDataWithLoader( 447 key: String, 448 sbn: StatusBarNotification, 449 oldKey: String?, 450 isNewlyActiveEntry: Boolean = false, 451 isConvertingToActive: Boolean = false, 452 ) = 453 withContext(backgroundDispatcher) { 454 val lastActive = systemClock.elapsedRealtime() 455 val result = mediaDataLoader.get().loadMediaData(key, sbn, isConvertingToActive) 456 if (result == null) { 457 Log.d(TAG, "No result from loadMediaData") 458 return@withContext 459 } 460 461 val currentEntry = mediaEntries[key] 462 val instanceId = currentEntry?.instanceId ?: logger.getNewInstanceId() 463 val createdTimestampMillis = currentEntry?.createdTimestampMillis ?: 0L 464 val resumeAction: Runnable? = currentEntry?.resumeAction 465 val hasCheckedForResume = currentEntry?.hasCheckedForResume == true 466 val active = currentEntry?.active ?: true 467 val mediaController = mediaControllerFactory.create(result.token!!) 468 469 val mediaData = 470 MediaData( 471 userId = sbn.normalizedUserId, 472 initialized = true, 473 app = result.appName, 474 appIcon = result.appIcon, 475 artist = result.artist, 476 song = result.song, 477 artwork = result.artworkIcon, 478 actions = result.actionIcons, 479 actionsToShowInCompact = result.actionsToShowInCompact, 480 semanticActions = result.semanticActions, 481 packageName = sbn.packageName, 482 token = result.token, 483 clickIntent = result.clickIntent, 484 device = result.device, 485 active = active, 486 resumeAction = resumeAction, 487 playbackLocation = result.playbackLocation, 488 notificationKey = key, 489 hasCheckedForResume = hasCheckedForResume, 490 isPlaying = result.isPlaying, 491 isClearable = !sbn.isOngoing, 492 lastActive = lastActive, 493 createdTimestampMillis = createdTimestampMillis, 494 instanceId = instanceId, 495 appUid = result.appUid, 496 isExplicit = result.isExplicit, 497 ) 498 499 if (isSameMediaData(context, mediaController, mediaData, currentEntry)) { 500 mediaLogger.logDuplicateMediaNotification(key) 501 return@withContext 502 } 503 504 // We need to log the correct media added. 505 if (isNewlyActiveEntry) { 506 logSingleVsMultipleMediaAdded(result.appUid, sbn.packageName, instanceId) 507 logger.logActiveMediaAdded( 508 result.appUid, 509 sbn.packageName, 510 instanceId, 511 result.playbackLocation, 512 ) 513 } else if (result.playbackLocation != currentEntry?.playbackLocation) { 514 logger.logPlaybackLocationChange( 515 result.appUid, 516 sbn.packageName, 517 instanceId, 518 result.playbackLocation, 519 ) 520 } 521 522 withContext(mainDispatcher) { onMediaDataLoaded(key, oldKey, mediaData) } 523 } 524 525 /** Add a listener for changes in this class */ 526 override fun addListener(listener: MediaDataManager.Listener) { 527 // mediaDataFilter is the current end of the internal pipeline. Register external 528 // listeners as listeners to it. 529 mediaDataFilter.addListener(listener) 530 } 531 532 /** Remove a listener for changes in this class */ 533 override fun removeListener(listener: MediaDataManager.Listener) { 534 // Since mediaDataFilter is the current end of the internal pipelie, external listeners 535 // have been registered to it. So, they need to be removed from it too. 536 mediaDataFilter.removeListener(listener) 537 } 538 539 /** Add a listener for internal events. */ 540 private fun addInternalListener(listener: MediaDataManager.Listener) = 541 internalListeners.add(listener) 542 543 /** 544 * Notify internal listeners of media loaded event. 545 * 546 * External listeners registered with [addListener] will be notified after the event propagates 547 * through the internal listener pipeline. 548 */ 549 private fun notifyMediaDataLoaded(key: String, oldKey: String?, info: MediaData) { 550 internalListeners.forEach { it.onMediaDataLoaded(key, oldKey, info) } 551 } 552 553 /** 554 * Notify internal listeners of media removed event. 555 * 556 * External listeners registered with [addListener] will be notified after the event propagates 557 * through the internal listener pipeline. 558 */ 559 private fun notifyMediaDataRemoved(key: String, userInitiated: Boolean = false) { 560 internalListeners.forEach { it.onMediaDataRemoved(key, userInitiated) } 561 } 562 563 /** 564 * Called whenever the player has been paused or stopped for a while, or swiped from QQS. This 565 * will make the player not active anymore, hiding it from QQS and Keyguard. 566 * 567 * @see MediaData.active 568 */ 569 override fun setInactive(key: String, timedOut: Boolean, forceUpdate: Boolean) { 570 mediaEntries[key]?.let { 571 if (timedOut && !forceUpdate) { 572 // Only log this event when media expires on its own 573 logger.logMediaTimeout(it.appUid, it.packageName, it.instanceId) 574 } 575 if (it.active == !timedOut && !forceUpdate) { 576 if (it.resumption) { 577 if (DEBUG) Log.d(TAG, "timing out resume player $key") 578 dismissMediaData(key, delay = 0L, userInitiated = false) 579 } 580 return 581 } 582 // Update last active if media was still active. 583 if (it.active) { 584 it.lastActive = systemClock.elapsedRealtime() 585 } 586 it.active = !timedOut 587 if (DEBUG) Log.d(TAG, "Updating $key timedOut: $timedOut") 588 onMediaDataLoaded(key, key, it) 589 } 590 } 591 592 /** Called when the player's [PlaybackState] has been updated with new actions and/or state */ 593 private fun updateState(key: String, state: PlaybackState) { 594 mediaEntries.get(key)?.let { 595 backgroundExecutor.execute { 596 val token = it.token 597 if (token == null) { 598 if (DEBUG) Log.d(TAG, "State updated, but token was null") 599 return@execute 600 } 601 val actions = 602 createActionsFromState( 603 it.packageName, 604 mediaControllerFactory.create(it.token), 605 UserHandle(it.userId), 606 ) 607 608 // Control buttons 609 // If flag is enabled and controller has a PlaybackState, 610 // create actions from session info 611 // otherwise, no need to update semantic actions. 612 val data = 613 if (actions != null) { 614 it.copy(semanticActions = actions, isPlaying = isPlayingState(state.state)) 615 } else { 616 it.copy(isPlaying = isPlayingState(state.state)) 617 } 618 if (DEBUG) Log.d(TAG, "State updated outside of notification") 619 foregroundExecutor.execute { onMediaDataLoaded(key, key, data) } 620 } 621 } 622 } 623 624 private fun removeEntry(key: String, logEvent: Boolean = true, userInitiated: Boolean = false) { 625 mediaEntries.remove(key)?.let { 626 if (logEvent) { 627 logger.logMediaRemoved(it.appUid, it.packageName, it.instanceId) 628 } 629 } 630 notifyMediaDataRemoved(key, userInitiated) 631 } 632 633 /** Dismiss a media entry. Returns false if the key was not found. */ 634 override fun dismissMediaData(key: String, delay: Long, userInitiated: Boolean): Boolean { 635 val existed = mediaEntries[key] != null 636 backgroundExecutor.execute { 637 mediaEntries[key]?.let { mediaData -> 638 if (mediaData.isLocalSession()) { 639 mediaData.token?.let { 640 val mediaController = mediaControllerFactory.create(it) 641 mediaController.transportControls.stop() 642 } 643 } 644 } 645 } 646 foregroundExecutor.executeDelayed( 647 { removeEntry(key = key, userInitiated = userInitiated) }, 648 delay, 649 ) 650 return existed 651 } 652 653 /** 654 * Called whenever the recommendation has been expired or removed by the user. This will remove 655 * the recommendation card entirely from the carousel. 656 */ 657 override fun dismissSmartspaceRecommendation(key: String, delay: Long) { 658 // TODO(b/382680767): remove 659 } 660 661 private suspend fun loadMediaDataForResumption( 662 userId: Int, 663 desc: MediaDescription, 664 resumeAction: Runnable, 665 token: MediaSession.Token, 666 appName: String, 667 appIntent: PendingIntent, 668 packageName: String, 669 ) = 670 withContext(backgroundDispatcher) { 671 val lastActive = systemClock.elapsedRealtime() 672 val currentEntry = mediaEntries[packageName] 673 val createdTimestampMillis = currentEntry?.createdTimestampMillis ?: 0L 674 val result = 675 mediaDataLoader 676 .get() 677 .loadMediaDataForResumption( 678 userId, 679 desc, 680 resumeAction, 681 currentEntry, 682 token, 683 appName, 684 appIntent, 685 packageName, 686 ) 687 if (result == null || desc.title.isNullOrBlank()) { 688 Log.d(TAG, "No MediaData result for resumption") 689 mediaEntries.remove(packageName) 690 return@withContext 691 } 692 693 val instanceId = currentEntry?.instanceId ?: logger.getNewInstanceId() 694 withContext(mainDispatcher) { 695 onMediaDataLoaded( 696 packageName, 697 null, 698 MediaData( 699 userId = userId, 700 initialized = true, 701 app = result.appName, 702 appIcon = null, 703 artist = result.artist, 704 song = result.song, 705 artwork = result.artworkIcon, 706 actions = result.actionIcons, 707 actionsToShowInCompact = result.actionsToShowInCompact, 708 semanticActions = result.semanticActions, 709 packageName = packageName, 710 token = result.token, 711 clickIntent = result.clickIntent, 712 device = result.device, 713 active = false, 714 resumeAction = resumeAction, 715 resumption = true, 716 notificationKey = packageName, 717 hasCheckedForResume = true, 718 lastActive = lastActive, 719 createdTimestampMillis = createdTimestampMillis, 720 instanceId = instanceId, 721 appUid = result.appUid, 722 isExplicit = result.isExplicit, 723 resumeProgress = result.resumeProgress, 724 ), 725 ) 726 } 727 } 728 729 @Deprecated("Cleanup when media_load_metadata_via_media_data_loader is cleaned up") 730 private fun loadMediaDataInBgForResumption( 731 userId: Int, 732 desc: MediaDescription, 733 resumeAction: Runnable, 734 token: MediaSession.Token, 735 appName: String, 736 appIntent: PendingIntent, 737 packageName: String, 738 ) { 739 if (desc.title.isNullOrBlank()) { 740 Log.e(TAG, "Description incomplete") 741 // Delete the placeholder entry 742 mediaEntries.remove(packageName) 743 return 744 } 745 746 if (DEBUG) { 747 Log.d(TAG, "adding track for $userId from browser: $desc") 748 } 749 750 val currentEntry = mediaEntries.get(packageName) 751 val appUid = currentEntry?.appUid ?: Process.INVALID_UID 752 753 // Album art 754 var artworkBitmap = desc.iconBitmap 755 if (artworkBitmap == null && desc.iconUri != null) { 756 artworkBitmap = loadBitmapFromUriForUser(desc.iconUri!!, userId, appUid, packageName) 757 } 758 val artworkIcon = 759 if (artworkBitmap != null) { 760 Icon.createWithBitmap(artworkBitmap) 761 } else { 762 null 763 } 764 765 val instanceId = currentEntry?.instanceId ?: logger.getNewInstanceId() 766 val isExplicit = 767 desc.extras?.getLong(MediaConstants.METADATA_KEY_IS_EXPLICIT) == 768 MediaConstants.METADATA_VALUE_ATTRIBUTE_PRESENT 769 770 val progress = MediaDataUtils.getDescriptionProgress(desc.extras) 771 val mediaAction = getResumeMediaAction(resumeAction) 772 val lastActive = systemClock.elapsedRealtime() 773 val createdTimestampMillis = currentEntry?.createdTimestampMillis ?: 0L 774 foregroundExecutor.execute { 775 onMediaDataLoaded( 776 packageName, 777 null, 778 MediaData( 779 userId, 780 true, 781 appName, 782 null, 783 desc.subtitle, 784 desc.title, 785 artworkIcon, 786 listOf(), 787 listOf(0), 788 MediaButton(playOrPause = mediaAction), 789 packageName, 790 token, 791 appIntent, 792 device = null, 793 active = false, 794 resumeAction = resumeAction, 795 resumption = true, 796 notificationKey = packageName, 797 hasCheckedForResume = true, 798 lastActive = lastActive, 799 createdTimestampMillis = createdTimestampMillis, 800 instanceId = instanceId, 801 appUid = appUid, 802 isExplicit = isExplicit, 803 resumeProgress = progress, 804 ), 805 ) 806 } 807 } 808 809 @Deprecated("Cleanup when media_load_metadata_via_media_data_loader is cleaned up") 810 fun loadMediaDataInBg( 811 key: String, 812 sbn: StatusBarNotification, 813 oldKey: String?, 814 isNewlyActiveEntry: Boolean = false, 815 ) { 816 val token = 817 sbn.notification.extras.getParcelable( 818 Notification.EXTRA_MEDIA_SESSION, 819 MediaSession.Token::class.java, 820 ) 821 if (token == null) { 822 return 823 } 824 val mediaController = mediaControllerFactory.create(token) 825 val metadata = mediaController.metadata 826 val notif: Notification = sbn.notification 827 828 val appInfo = 829 notif.extras.getParcelable( 830 Notification.EXTRA_BUILDER_APPLICATION_INFO, 831 ApplicationInfo::class.java, 832 ) ?: getAppInfoFromPackage(sbn.packageName) 833 834 // App name 835 val appName = getAppName(sbn, appInfo) 836 837 // Song name 838 var song: CharSequence? = metadata?.getString(MediaMetadata.METADATA_KEY_DISPLAY_TITLE) 839 if (song.isNullOrBlank()) { 840 song = metadata?.getString(MediaMetadata.METADATA_KEY_TITLE) 841 } 842 if (song.isNullOrBlank()) { 843 song = HybridGroupManager.resolveTitle(notif) 844 } 845 if (song.isNullOrBlank()) { 846 // For apps that don't include a title, log and add a placeholder 847 song = context.getString(R.string.controls_media_empty_title, appName) 848 try { 849 statusBarManager.logBlankMediaTitle(sbn.packageName, sbn.user.identifier) 850 } catch (e: RuntimeException) { 851 Log.e(TAG, "Error reporting blank media title for package ${sbn.packageName}") 852 } 853 } 854 855 // Album art 856 var artworkBitmap = metadata?.let { loadBitmapFromUri(it) } 857 if (artworkBitmap == null) { 858 artworkBitmap = metadata?.getBitmap(MediaMetadata.METADATA_KEY_ART) 859 } 860 if (artworkBitmap == null) { 861 artworkBitmap = metadata?.getBitmap(MediaMetadata.METADATA_KEY_ALBUM_ART) 862 } 863 val artWorkIcon = 864 if (artworkBitmap == null) { 865 notif.getLargeIcon() 866 } else { 867 Icon.createWithBitmap(artworkBitmap) 868 } 869 870 // App Icon 871 val smallIcon = sbn.notification.smallIcon 872 873 // Explicit Indicator 874 var isExplicit = false 875 val mediaMetadataCompat = MediaMetadataCompat.fromMediaMetadata(metadata) 876 isExplicit = 877 mediaMetadataCompat?.getLong(MediaConstants.METADATA_KEY_IS_EXPLICIT) == 878 MediaConstants.METADATA_VALUE_ATTRIBUTE_PRESENT 879 880 // Artist name 881 var artist: CharSequence? = metadata?.getString(MediaMetadata.METADATA_KEY_ARTIST) 882 if (artist.isNullOrBlank()) { 883 artist = HybridGroupManager.resolveText(notif) 884 } 885 886 // Device name (used for remote cast notifications) 887 var device: MediaDeviceData? = null 888 if (isRemoteCastNotification(sbn)) { 889 val extras = sbn.notification.extras 890 val deviceName = extras.getCharSequence(Notification.EXTRA_MEDIA_REMOTE_DEVICE, null) 891 val deviceIcon = extras.getInt(Notification.EXTRA_MEDIA_REMOTE_ICON, -1) 892 val deviceIntent = 893 extras.getParcelable( 894 Notification.EXTRA_MEDIA_REMOTE_INTENT, 895 PendingIntent::class.java, 896 ) 897 Log.d(TAG, "$key is RCN for $deviceName") 898 899 if (deviceName != null && deviceIcon > -1) { 900 // Name and icon must be present, but intent may be null 901 val enabled = deviceIntent != null && deviceIntent.isActivity 902 val deviceDrawable = 903 Icon.createWithResource(sbn.packageName, deviceIcon) 904 .loadDrawable(sbn.getPackageContext(context)) 905 device = 906 MediaDeviceData( 907 enabled, 908 deviceDrawable, 909 deviceName, 910 deviceIntent, 911 showBroadcastButton = false, 912 ) 913 } 914 } 915 916 // Control buttons 917 // If controller has a PlaybackState, create actions from session info 918 // Otherwise, use the notification actions 919 var actionIcons: List<MediaNotificationAction> = emptyList() 920 var actionsToShowCollapsed: List<Int> = emptyList() 921 val semanticActions = createActionsFromState(sbn.packageName, mediaController, sbn.user) 922 if (semanticActions == null) { 923 val actions = createActionsFromNotification(context, sbn) 924 actionIcons = actions.first 925 actionsToShowCollapsed = actions.second 926 } 927 928 val playbackLocation = 929 if (isRemoteCastNotification(sbn)) MediaData.PLAYBACK_CAST_REMOTE 930 else if ( 931 mediaController.playbackInfo?.playbackType == 932 MediaController.PlaybackInfo.PLAYBACK_TYPE_LOCAL 933 ) 934 MediaData.PLAYBACK_LOCAL 935 else MediaData.PLAYBACK_CAST_LOCAL 936 val isPlaying = mediaController.playbackState?.let { isPlayingState(it.state) } ?: null 937 938 val currentEntry = mediaEntries.get(key) 939 val instanceId = currentEntry?.instanceId ?: logger.getNewInstanceId() 940 val appUid = appInfo?.uid ?: Process.INVALID_UID 941 942 val lastActive = systemClock.elapsedRealtime() 943 val createdTimestampMillis = currentEntry?.createdTimestampMillis ?: 0L 944 val resumeAction: Runnable? = mediaEntries[key]?.resumeAction 945 val hasCheckedForResume = mediaEntries[key]?.hasCheckedForResume == true 946 val active = mediaEntries[key]?.active ?: true 947 var mediaData = 948 MediaData( 949 sbn.normalizedUserId, 950 true, 951 appName, 952 smallIcon, 953 artist, 954 song, 955 artWorkIcon, 956 actionIcons, 957 actionsToShowCollapsed, 958 semanticActions, 959 sbn.packageName, 960 token, 961 notif.contentIntent, 962 device, 963 active, 964 resumeAction = resumeAction, 965 playbackLocation = playbackLocation, 966 notificationKey = key, 967 hasCheckedForResume = hasCheckedForResume, 968 isPlaying = isPlaying, 969 isClearable = !sbn.isOngoing, 970 lastActive = lastActive, 971 createdTimestampMillis = createdTimestampMillis, 972 instanceId = instanceId, 973 appUid = appUid, 974 isExplicit = isExplicit, 975 ) 976 977 if (isSameMediaData(context, mediaController, mediaData, currentEntry)) { 978 mediaLogger.logDuplicateMediaNotification(key) 979 return 980 } 981 982 if (isNewlyActiveEntry) { 983 logSingleVsMultipleMediaAdded(appUid, sbn.packageName, instanceId) 984 logger.logActiveMediaAdded(appUid, sbn.packageName, instanceId, playbackLocation) 985 } else if (playbackLocation != currentEntry?.playbackLocation) { 986 logger.logPlaybackLocationChange(appUid, sbn.packageName, instanceId, playbackLocation) 987 } 988 989 foregroundExecutor.execute { 990 val oldResumeAction: Runnable? = mediaEntries[key]?.resumeAction 991 val oldHasCheckedForResume = mediaEntries[key]?.hasCheckedForResume == true 992 val oldActive = mediaEntries[key]?.active ?: true 993 mediaData = 994 mediaData.copy( 995 resumeAction = oldResumeAction, 996 hasCheckedForResume = oldHasCheckedForResume, 997 active = oldActive, 998 ) 999 onMediaDataLoaded(key, oldKey, mediaData) 1000 } 1001 } 1002 1003 private fun logSingleVsMultipleMediaAdded( 1004 appUid: Int, 1005 packageName: String, 1006 instanceId: InstanceId, 1007 ) { 1008 if (mediaEntries.size == 1) { 1009 logger.logSingleMediaPlayerInCarousel(appUid, packageName, instanceId) 1010 } else if (mediaEntries.size == 2) { 1011 // Since this method is only called when there is a new media session added. 1012 // logging needed once there is more than one media session in carousel. 1013 logger.logMultipleMediaPlayersInCarousel(appUid, packageName, instanceId) 1014 } 1015 } 1016 1017 @Deprecated("Cleanup when media_load_metadata_via_media_data_loader is cleaned up") 1018 private fun getAppInfoFromPackage(packageName: String): ApplicationInfo? { 1019 try { 1020 return context.packageManager.getApplicationInfo(packageName, 0) 1021 } catch (e: PackageManager.NameNotFoundException) { 1022 Log.w(TAG, "Could not get app info for $packageName", e) 1023 } 1024 return null 1025 } 1026 1027 @Deprecated("Cleanup when media_load_metadata_via_media_data_loader is cleaned up") 1028 private fun getAppName(sbn: StatusBarNotification, appInfo: ApplicationInfo?): String { 1029 val name = sbn.notification.extras.getString(EXTRA_SUBSTITUTE_APP_NAME) 1030 if (name != null) { 1031 return name 1032 } 1033 1034 return if (appInfo != null) { 1035 context.packageManager.getApplicationLabel(appInfo).toString() 1036 } else { 1037 sbn.packageName 1038 } 1039 } 1040 1041 private fun createActionsFromState( 1042 packageName: String, 1043 controller: MediaController, 1044 user: UserHandle, 1045 ): MediaButton? { 1046 if (!mediaFlags.areMediaSessionActionsEnabled(packageName, user)) { 1047 return null 1048 } 1049 return createActionsFromState(context, packageName, controller) 1050 } 1051 1052 /** Load a bitmap from the various Art metadata URIs */ 1053 @Deprecated("Cleanup when media_load_metadata_via_media_data_loader is cleaned up") 1054 private fun loadBitmapFromUri(metadata: MediaMetadata): Bitmap? { 1055 for (uri in ART_URIS) { 1056 val uriString = metadata.getString(uri) 1057 if (!TextUtils.isEmpty(uriString)) { 1058 val albumArt = loadBitmapFromUri(Uri.parse(uriString)) 1059 if (albumArt != null) { 1060 if (DEBUG) Log.d(TAG, "loaded art from $uri") 1061 return albumArt 1062 } 1063 } 1064 } 1065 return null 1066 } 1067 1068 /** Returns a bitmap if the user can access the given URI, else null */ 1069 private fun loadBitmapFromUriForUser( 1070 uri: Uri, 1071 userId: Int, 1072 appUid: Int, 1073 packageName: String, 1074 ): Bitmap? { 1075 try { 1076 val ugm = UriGrantsManager.getService() 1077 ugm.checkGrantUriPermission_ignoreNonSystem( 1078 appUid, 1079 packageName, 1080 ContentProvider.getUriWithoutUserId(uri), 1081 Intent.FLAG_GRANT_READ_URI_PERMISSION, 1082 ContentProvider.getUserIdFromUri(uri, userId), 1083 ) 1084 return loadBitmapFromUri(uri) 1085 } catch (e: SecurityException) { 1086 Log.e(TAG, "Failed to get URI permission: $e") 1087 } 1088 return null 1089 } 1090 1091 /** 1092 * Load a bitmap from a URI 1093 * 1094 * @param uri the uri to load 1095 * @return bitmap, or null if couldn't be loaded 1096 */ 1097 private fun loadBitmapFromUri(uri: Uri): Bitmap? { 1098 // ImageDecoder requires a scheme of the following types 1099 if (uri.scheme == null) { 1100 return null 1101 } 1102 1103 if ( 1104 !uri.scheme.equals(ContentResolver.SCHEME_CONTENT) && 1105 !uri.scheme.equals(ContentResolver.SCHEME_ANDROID_RESOURCE) && 1106 !uri.scheme.equals(ContentResolver.SCHEME_FILE) 1107 ) { 1108 return null 1109 } 1110 1111 val source = ImageDecoder.createSource(context.contentResolver, uri) 1112 return try { 1113 ImageDecoder.decodeBitmap(source) { decoder, info, _ -> 1114 val width = info.size.width 1115 val height = info.size.height 1116 val scale = 1117 MediaDataUtils.getScaleFactor( 1118 APair(width, height), 1119 APair(artworkWidth, artworkHeight), 1120 ) 1121 1122 // Downscale if needed 1123 if (scale != 0f && scale < 1) { 1124 decoder.setTargetSize((scale * width).toInt(), (scale * height).toInt()) 1125 } 1126 decoder.allocator = ImageDecoder.ALLOCATOR_SOFTWARE 1127 } 1128 } catch (e: IOException) { 1129 Log.e(TAG, "Unable to load bitmap", e) 1130 null 1131 } catch (e: RuntimeException) { 1132 Log.e(TAG, "Unable to load bitmap", e) 1133 null 1134 } 1135 } 1136 1137 private fun getResumeMediaAction(action: Runnable): MediaAction { 1138 val iconId = 1139 if (Flags.mediaControlsUiUpdate()) { 1140 R.drawable.ic_media_play_button 1141 } else { 1142 R.drawable.ic_media_play 1143 } 1144 return MediaAction( 1145 Icon.createWithResource(context, iconId).setTint(themeText).loadDrawable(context), 1146 action, 1147 context.getString(R.string.controls_media_button_play), 1148 if (Flags.mediaControlsUiUpdate()) { 1149 context.getDrawable(R.drawable.ic_media_play_button_container) 1150 } else { 1151 context.getDrawable(R.drawable.ic_media_play_container) 1152 }, 1153 ) 1154 } 1155 1156 @MainThread 1157 fun onMediaDataLoaded(key: String, oldKey: String?, data: MediaData) = 1158 traceSection("MediaDataManager#onMediaDataLoaded") { 1159 Assert.isMainThread() 1160 if (mediaEntries.containsKey(key)) { 1161 // Otherwise this was removed already 1162 mediaEntries.put(key, data) 1163 notifyMediaDataLoaded(key, oldKey, data) 1164 } 1165 } 1166 1167 override fun onNotificationRemoved(key: String) { 1168 Assert.isMainThread() 1169 val removed = mediaEntries.remove(key) ?: return 1170 if (keyguardUpdateMonitor.isUserInLockdown(removed.userId)) { 1171 logger.logMediaRemoved(removed.appUid, removed.packageName, removed.instanceId) 1172 } else if (isAbleToResume(removed)) { 1173 convertToResumePlayer(key, removed) 1174 } else if (mediaFlags.isRetainingPlayersEnabled()) { 1175 handlePossibleRemoval(key, removed, notificationRemoved = true) 1176 } else { 1177 notifyMediaDataRemoved(key) 1178 logger.logMediaRemoved(removed.appUid, removed.packageName, removed.instanceId) 1179 } 1180 } 1181 1182 private fun onSessionDestroyed(key: String) { 1183 if (DEBUG) Log.d(TAG, "session destroyed for $key") 1184 val entry = mediaEntries.remove(key) ?: return 1185 // Clear token since the session is no longer valid 1186 val updated = entry.copy(token = null) 1187 handlePossibleRemoval(key, updated) 1188 } 1189 1190 private fun isAbleToResume(data: MediaData): Boolean { 1191 val isEligibleForResume = data.isLocalSession() 1192 return useMediaResumption && data.resumeAction != null && isEligibleForResume 1193 } 1194 1195 /** 1196 * Convert to resume state if the player is no longer valid and active, then notify listeners 1197 * that the data was updated. Does not convert to resume state if the player is still valid, or 1198 * if it was removed before becoming inactive. (Assumes that [removed] was removed from 1199 * [mediaEntries] before this function was called) 1200 */ 1201 private fun handlePossibleRemoval( 1202 key: String, 1203 removed: MediaData, 1204 notificationRemoved: Boolean = false, 1205 ) { 1206 val hasSession = removed.token != null 1207 if (hasSession && removed.semanticActions != null) { 1208 // The app was using session actions, and the session is still valid: keep player 1209 if (DEBUG) Log.d(TAG, "Notification removed but using session actions $key") 1210 mediaEntries.put(key, removed) 1211 notifyMediaDataLoaded(key, key, removed) 1212 } else if (!notificationRemoved && removed.semanticActions == null) { 1213 // The app was using notification actions, and notif wasn't removed yet: keep player 1214 if (DEBUG) Log.d(TAG, "Session destroyed but using notification actions $key") 1215 mediaEntries.put(key, removed) 1216 notifyMediaDataLoaded(key, key, removed) 1217 } else if (removed.active && !isAbleToResume(removed)) { 1218 // This player was still active - it didn't last long enough to time out, 1219 // and its app doesn't normally support resume: remove 1220 if (DEBUG) Log.d(TAG, "Removing still-active player $key") 1221 notifyMediaDataRemoved(key) 1222 logger.logMediaRemoved(removed.appUid, removed.packageName, removed.instanceId) 1223 } else if (mediaFlags.isRetainingPlayersEnabled() || isAbleToResume(removed)) { 1224 // Convert to resume 1225 if (DEBUG) { 1226 Log.d( 1227 TAG, 1228 "Notification ($notificationRemoved) and/or session " + 1229 "($hasSession) gone for inactive player $key", 1230 ) 1231 } 1232 convertToResumePlayer(key, removed) 1233 } else { 1234 // Retaining players flag is off and app doesn't support resume: remove player. 1235 if (DEBUG) Log.d(TAG, "Removing player $key") 1236 notifyMediaDataRemoved(key) 1237 logger.logMediaRemoved(removed.appUid, removed.packageName, removed.instanceId) 1238 } 1239 } 1240 1241 /** Set the given [MediaData] as a resume state player and notify listeners */ 1242 private fun convertToResumePlayer(key: String, data: MediaData) { 1243 if (DEBUG) Log.d(TAG, "Converting $key to resume") 1244 // Resumption controls must have a title. 1245 if (data.song.isNullOrBlank()) { 1246 Log.e(TAG, "Description incomplete") 1247 notifyMediaDataRemoved(key) 1248 logger.logMediaRemoved(data.appUid, data.packageName, data.instanceId) 1249 return 1250 } 1251 // Move to resume key (aka package name) if that key doesn't already exist. 1252 val resumeAction = data.resumeAction?.let { getResumeMediaAction(it) } 1253 val actions = resumeAction?.let { listOf(resumeAction) } ?: emptyList() 1254 val launcherIntent = 1255 context.packageManager.getLaunchIntentForPackage(data.packageName)?.let { 1256 PendingIntent.getActivity(context, 0, it, PendingIntent.FLAG_IMMUTABLE) 1257 } 1258 val lastActive = 1259 if (data.active) { 1260 systemClock.elapsedRealtime() 1261 } else { 1262 data.lastActive 1263 } 1264 val updated = 1265 data.copy( 1266 token = null, 1267 actions = listOf(), 1268 semanticActions = MediaButton(playOrPause = resumeAction), 1269 actionsToShowInCompact = listOf(0), 1270 active = false, 1271 resumption = true, 1272 isPlaying = false, 1273 isClearable = true, 1274 clickIntent = launcherIntent, 1275 lastActive = lastActive, 1276 ) 1277 val pkg = data.packageName 1278 val migrate = mediaEntries.put(pkg, updated) == null 1279 // Notify listeners of "new" controls when migrating or removed and update when not 1280 Log.d(TAG, "migrating? $migrate from $key -> $pkg") 1281 if (migrate) { 1282 notifyMediaDataLoaded(key = pkg, oldKey = key, info = updated) 1283 } else { 1284 // Since packageName is used for the key of the resumption controls, it is 1285 // possible that another notification has already been reused for the resumption 1286 // controls of this package. In this case, rather than renaming this player as 1287 // packageName, just remove it and then send a update to the existing resumption 1288 // controls. 1289 notifyMediaDataRemoved(key) 1290 notifyMediaDataLoaded(key = pkg, oldKey = pkg, info = updated) 1291 } 1292 logger.logActiveConvertedToResume(updated.appUid, pkg, updated.instanceId) 1293 1294 // Limit total number of resume controls 1295 val resumeEntries = mediaEntries.filter { (key, data) -> data.resumption } 1296 val numResume = resumeEntries.size 1297 if (numResume > ResumeMediaBrowser.MAX_RESUMPTION_CONTROLS) { 1298 resumeEntries 1299 .toList() 1300 .sortedBy { (key, data) -> data.lastActive } 1301 .subList(0, numResume - ResumeMediaBrowser.MAX_RESUMPTION_CONTROLS) 1302 .forEach { (key, data) -> 1303 Log.d(TAG, "Removing excess control $key") 1304 mediaEntries.remove(key) 1305 notifyMediaDataRemoved(key) 1306 logger.logMediaRemoved(data.appUid, data.packageName, data.instanceId) 1307 } 1308 } 1309 } 1310 1311 override fun setMediaResumptionEnabled(isEnabled: Boolean) { 1312 if (useMediaResumption == isEnabled) { 1313 return 1314 } 1315 1316 useMediaResumption = isEnabled 1317 1318 if (!useMediaResumption) { 1319 // Remove any existing resume controls 1320 val filtered = mediaEntries.filter { !it.value.active } 1321 filtered.forEach { 1322 mediaEntries.remove(it.key) 1323 notifyMediaDataRemoved(it.key) 1324 logger.logMediaRemoved(it.value.appUid, it.value.packageName, it.value.instanceId) 1325 } 1326 } 1327 } 1328 1329 /** Invoked when the user has dismissed the media carousel */ 1330 override fun onSwipeToDismiss() = mediaDataFilter.onSwipeToDismiss() 1331 1332 /** Are there any media notifications active, including the recommendations? */ 1333 override fun hasActiveMediaOrRecommendation() = mediaDataFilter.hasActiveMedia() 1334 1335 /** 1336 * Are there any media entries we should display, including the recommendations? 1337 * - If resumption is enabled, this will include inactive players 1338 * - If resumption is disabled, we only want to show active players 1339 */ 1340 override fun hasAnyMediaOrRecommendation() = mediaDataFilter.hasAnyMedia() 1341 1342 /** Are there any resume media notifications active, excluding the recommendations? */ 1343 override fun hasActiveMedia() = mediaDataFilter.hasActiveMedia() 1344 1345 /** 1346 * Are there any resume media notifications active, excluding the recommendations? 1347 * - If resumption is enabled, this will include inactive players 1348 * - If resumption is disabled, we only want to show active players 1349 */ 1350 override fun hasAnyMedia() = mediaDataFilter.hasAnyMedia() 1351 1352 override fun isRecommendationActive() = false 1353 1354 override fun dump(pw: PrintWriter, args: Array<out String>) { 1355 pw.apply { 1356 println("internalListeners: $internalListeners") 1357 println("externalListeners: ${mediaDataFilter.listeners}") 1358 println("mediaEntries: $mediaEntries") 1359 println("useMediaResumption: $useMediaResumption") 1360 } 1361 mediaDeviceManager.dump(pw) 1362 } 1363 } 1364