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.SuppressLint 20 import android.app.ActivityOptions 21 import android.app.BroadcastOptions 22 import android.app.Notification 23 import android.app.Notification.EXTRA_SUBSTITUTE_APP_NAME 24 import android.app.PendingIntent 25 import android.app.StatusBarManager 26 import android.app.UriGrantsManager 27 import android.app.smartspace.SmartspaceAction 28 import android.app.smartspace.SmartspaceConfig 29 import android.app.smartspace.SmartspaceManager 30 import android.app.smartspace.SmartspaceSession 31 import android.app.smartspace.SmartspaceTarget 32 import android.content.BroadcastReceiver 33 import android.content.ContentProvider 34 import android.content.ContentResolver 35 import android.content.Context 36 import android.content.Intent 37 import android.content.IntentFilter 38 import android.content.pm.ApplicationInfo 39 import android.content.pm.PackageManager 40 import android.graphics.Bitmap 41 import android.graphics.ImageDecoder 42 import android.graphics.drawable.Animatable 43 import android.graphics.drawable.Icon 44 import android.media.MediaDescription 45 import android.media.MediaMetadata 46 import android.media.session.MediaController 47 import android.media.session.MediaSession 48 import android.media.session.PlaybackState 49 import android.net.Uri 50 import android.os.Parcelable 51 import android.os.Process 52 import android.os.UserHandle 53 import android.provider.Settings 54 import android.service.notification.StatusBarNotification 55 import android.support.v4.media.MediaMetadataCompat 56 import android.text.TextUtils 57 import android.util.Log 58 import android.util.Pair as APair 59 import androidx.media.utils.MediaConstants 60 import com.android.app.tracing.traceSection 61 import com.android.internal.annotations.Keep 62 import com.android.internal.logging.InstanceId 63 import com.android.keyguard.KeyguardUpdateMonitor 64 import com.android.systemui.Dumpable 65 import com.android.systemui.broadcast.BroadcastDispatcher 66 import com.android.systemui.dagger.SysUISingleton 67 import com.android.systemui.dagger.qualifiers.Background 68 import com.android.systemui.dagger.qualifiers.Main 69 import com.android.systemui.dump.DumpManager 70 import com.android.systemui.media.controls.domain.pipeline.MediaDataManager.Companion.isMediaNotification 71 import com.android.systemui.media.controls.domain.resume.MediaResumeListener 72 import com.android.systemui.media.controls.domain.resume.ResumeMediaBrowser 73 import com.android.systemui.media.controls.shared.model.EXTRA_KEY_TRIGGER_SOURCE 74 import com.android.systemui.media.controls.shared.model.EXTRA_VALUE_TRIGGER_PERIODIC 75 import com.android.systemui.media.controls.shared.model.MediaAction 76 import com.android.systemui.media.controls.shared.model.MediaButton 77 import com.android.systemui.media.controls.shared.model.MediaData 78 import com.android.systemui.media.controls.shared.model.MediaDeviceData 79 import com.android.systemui.media.controls.shared.model.SmartspaceMediaData 80 import com.android.systemui.media.controls.shared.model.SmartspaceMediaDataProvider 81 import com.android.systemui.media.controls.ui.view.MediaViewHolder 82 import com.android.systemui.media.controls.util.MediaControllerFactory 83 import com.android.systemui.media.controls.util.MediaDataUtils 84 import com.android.systemui.media.controls.util.MediaFlags 85 import com.android.systemui.media.controls.util.MediaUiEventLogger 86 import com.android.systemui.plugins.ActivityStarter 87 import com.android.systemui.plugins.BcSmartspaceDataPlugin 88 import com.android.systemui.res.R 89 import com.android.systemui.statusbar.NotificationMediaManager.isConnectingState 90 import com.android.systemui.statusbar.NotificationMediaManager.isPlayingState 91 import com.android.systemui.statusbar.notification.row.HybridGroupManager 92 import com.android.systemui.tuner.TunerService 93 import com.android.systemui.util.Assert 94 import com.android.systemui.util.Utils 95 import com.android.systemui.util.concurrency.DelayableExecutor 96 import com.android.systemui.util.concurrency.ThreadFactory 97 import com.android.systemui.util.time.SystemClock 98 import java.io.IOException 99 import java.io.PrintWriter 100 import java.util.concurrent.Executor 101 import javax.inject.Inject 102 103 // URI fields to try loading album art from 104 private val ART_URIS = 105 arrayOf( 106 MediaMetadata.METADATA_KEY_ALBUM_ART_URI, 107 MediaMetadata.METADATA_KEY_ART_URI, 108 MediaMetadata.METADATA_KEY_DISPLAY_ICON_URI 109 ) 110 111 private const val TAG = "MediaDataManager" 112 private const val DEBUG = true 113 private const val EXTRAS_SMARTSPACE_DISMISS_INTENT_KEY = "dismiss_intent" 114 115 private val LOADING = 116 MediaData( 117 userId = -1, 118 initialized = false, 119 app = null, 120 appIcon = null, 121 artist = null, 122 song = null, 123 artwork = null, 124 actions = emptyList(), 125 actionsToShowInCompact = emptyList(), 126 packageName = "INVALID", 127 token = null, 128 clickIntent = null, 129 device = null, 130 active = true, 131 resumeAction = null, 132 instanceId = InstanceId.fakeInstanceId(-1), 133 appUid = Process.INVALID_UID 134 ) 135 136 internal val EMPTY_SMARTSPACE_MEDIA_DATA = 137 SmartspaceMediaData( 138 targetId = "INVALID", 139 isActive = false, 140 packageName = "INVALID", 141 cardAction = null, 142 recommendations = emptyList(), 143 dismissIntent = null, 144 headphoneConnectionTimeMillis = 0, 145 instanceId = InstanceId.fakeInstanceId(-1), 146 expiryTimeMs = 0, 147 ) 148 149 const val MEDIA_TITLE_ERROR_MESSAGE = "Invalid media data: title is null or blank." 150 151 /** 152 * Allow recommendations from smartspace to show in media controls. Requires 153 * [Utils.useQsMediaPlayer] to be enabled. On by default, but can be disabled by setting to 0 154 */ 155 private fun allowMediaRecommendations(context: Context): Boolean { 156 val flag = 157 Settings.Secure.getInt( 158 context.contentResolver, 159 Settings.Secure.MEDIA_CONTROLS_RECOMMENDATION, 160 1 161 ) 162 return Utils.useQsMediaPlayer(context) && flag > 0 163 } 164 165 /** A class that facilitates management and loading of Media Data, ready for binding. */ 166 @SysUISingleton 167 class LegacyMediaDataManagerImpl( 168 private val context: Context, 169 @Background private val backgroundExecutor: Executor, 170 @Main private val uiExecutor: Executor, 171 @Main private val foregroundExecutor: DelayableExecutor, 172 private val mediaControllerFactory: MediaControllerFactory, 173 private val broadcastDispatcher: BroadcastDispatcher, 174 dumpManager: DumpManager, 175 mediaTimeoutListener: MediaTimeoutListener, 176 mediaResumeListener: MediaResumeListener, 177 mediaSessionBasedFilter: MediaSessionBasedFilter, 178 private val mediaDeviceManager: MediaDeviceManager, 179 mediaDataCombineLatest: MediaDataCombineLatest, 180 private val mediaDataFilter: LegacyMediaDataFilterImpl, 181 private val activityStarter: ActivityStarter, 182 private val smartspaceMediaDataProvider: SmartspaceMediaDataProvider, 183 private var useMediaResumption: Boolean, 184 private val useQsMediaPlayer: Boolean, 185 private val systemClock: SystemClock, 186 private val tunerService: TunerService, 187 private val mediaFlags: MediaFlags, 188 private val logger: MediaUiEventLogger, 189 private val smartspaceManager: SmartspaceManager?, 190 private val keyguardUpdateMonitor: KeyguardUpdateMonitor, 191 ) : Dumpable, BcSmartspaceDataPlugin.SmartspaceTargetListener, MediaDataManager { 192 193 companion object { 194 // UI surface label for subscribing Smartspace updates. 195 @JvmField val SMARTSPACE_UI_SURFACE_LABEL = "media_data_manager" 196 197 // Smartspace package name's extra key. 198 @JvmField val EXTRAS_MEDIA_SOURCE_PACKAGE_NAME = "package_name" 199 200 // Maximum number of actions allowed in compact view 201 @JvmField val MAX_COMPACT_ACTIONS = 3 202 203 // Maximum number of actions allowed in expanded view 204 @JvmField val MAX_NOTIFICATION_ACTIONS = MediaViewHolder.genericButtonIds.size 205 } 206 207 private val themeText = 208 com.android.settingslib.Utils.getColorAttr( 209 context, 210 com.android.internal.R.attr.textColorPrimary 211 ) 212 .defaultColor 213 214 // Internal listeners are part of the internal pipeline. External listeners (those registered 215 // with [MediaDeviceManager.addListener]) receive events after they have propagated through 216 // the internal pipeline. 217 // Another way to think of the distinction between internal and external listeners is the 218 // following. Internal listeners are listeners that MediaDataManager depends on, and external 219 // listeners are listeners that depend on MediaDataManager. 220 // TODO(b/159539991#comment5): Move internal listeners to separate package. 221 private val internalListeners: MutableSet<MediaDataManager.Listener> = mutableSetOf() 222 private val mediaEntries: LinkedHashMap<String, MediaData> = LinkedHashMap() 223 // There should ONLY be at most one Smartspace media recommendation. 224 var smartspaceMediaData: SmartspaceMediaData = EMPTY_SMARTSPACE_MEDIA_DATA 225 @Keep private var smartspaceSession: SmartspaceSession? = null 226 private var allowMediaRecommendations = allowMediaRecommendations(context) 227 228 private val artworkWidth = 229 context.resources.getDimensionPixelSize( 230 com.android.internal.R.dimen.config_mediaMetadataBitmapMaxSize 231 ) 232 private val artworkHeight = 233 context.resources.getDimensionPixelSize(R.dimen.qs_media_session_height_expanded) 234 235 @SuppressLint("WrongConstant") // sysui allowed to call STATUS_BAR_SERVICE 236 private val statusBarManager = 237 context.getSystemService(Context.STATUS_BAR_SERVICE) as StatusBarManager 238 239 /** Check whether this notification is an RCN */ isRemoteCastNotificationnull240 private fun isRemoteCastNotification(sbn: StatusBarNotification): Boolean { 241 return sbn.notification.extras.containsKey(Notification.EXTRA_MEDIA_REMOTE_DEVICE) 242 } 243 244 @Inject 245 constructor( 246 context: Context, 247 threadFactory: ThreadFactory, 248 @Main uiExecutor: Executor, 249 @Main foregroundExecutor: DelayableExecutor, 250 mediaControllerFactory: MediaControllerFactory, 251 dumpManager: DumpManager, 252 broadcastDispatcher: BroadcastDispatcher, 253 mediaTimeoutListener: MediaTimeoutListener, 254 mediaResumeListener: MediaResumeListener, 255 mediaSessionBasedFilter: MediaSessionBasedFilter, 256 mediaDeviceManager: MediaDeviceManager, 257 mediaDataCombineLatest: MediaDataCombineLatest, 258 mediaDataFilter: LegacyMediaDataFilterImpl, 259 activityStarter: ActivityStarter, 260 smartspaceMediaDataProvider: SmartspaceMediaDataProvider, 261 clock: SystemClock, 262 tunerService: TunerService, 263 mediaFlags: MediaFlags, 264 logger: MediaUiEventLogger, 265 smartspaceManager: SmartspaceManager?, 266 keyguardUpdateMonitor: KeyguardUpdateMonitor, 267 ) : this( 268 context, 269 // Loading bitmap for UMO background can take longer time, so it cannot run on the default 270 // background thread. Use a custom thread for media. 271 threadFactory.buildExecutorOnNewThread(TAG), 272 uiExecutor, 273 foregroundExecutor, 274 mediaControllerFactory, 275 broadcastDispatcher, 276 dumpManager, 277 mediaTimeoutListener, 278 mediaResumeListener, 279 mediaSessionBasedFilter, 280 mediaDeviceManager, 281 mediaDataCombineLatest, 282 mediaDataFilter, 283 activityStarter, 284 smartspaceMediaDataProvider, 285 Utils.useMediaResumption(context), 286 Utils.useQsMediaPlayer(context), 287 clock, 288 tunerService, 289 mediaFlags, 290 logger, 291 smartspaceManager, 292 keyguardUpdateMonitor, 293 ) 294 295 private val appChangeReceiver = 296 object : BroadcastReceiver() { onReceivenull297 override fun onReceive(context: Context, intent: Intent) { 298 when (intent.action) { 299 Intent.ACTION_PACKAGES_SUSPENDED -> { 300 val packages = intent.getStringArrayExtra(Intent.EXTRA_CHANGED_PACKAGE_LIST) 301 packages?.forEach { removeAllForPackage(it) } 302 } 303 Intent.ACTION_PACKAGE_REMOVED, 304 Intent.ACTION_PACKAGE_RESTARTED -> { 305 intent.data?.encodedSchemeSpecificPart?.let { removeAllForPackage(it) } 306 } 307 } 308 } 309 } 310 311 init { 312 dumpManager.registerDumpable(TAG, this) 313 314 // Initialize the internal processing pipeline. The listeners at the front of the pipeline 315 // are set as internal listeners so that they receive events. From there, events are 316 // propagated through the pipeline. The end of the pipeline is currently mediaDataFilter, 317 // so it is responsible for dispatching events to external listeners. To achieve this, 318 // external listeners that are registered with [MediaDataManager.addListener] are actually 319 // registered as listeners to mediaDataFilter. 320 addInternalListener(mediaTimeoutListener) 321 addInternalListener(mediaResumeListener) 322 addInternalListener(mediaSessionBasedFilter) 323 mediaSessionBasedFilter.addListener(mediaDeviceManager) 324 mediaSessionBasedFilter.addListener(mediaDataCombineLatest) 325 mediaDeviceManager.addListener(mediaDataCombineLatest) 326 mediaDataCombineLatest.addListener(mediaDataFilter) 327 328 // Set up links back into the pipeline for listeners that need to send events upstream. timedOutnull329 mediaTimeoutListener.timeoutCallback = { key: String, timedOut: Boolean -> 330 setInactive(key, timedOut) 331 } statenull332 mediaTimeoutListener.stateCallback = { key: String, state: PlaybackState -> 333 updateState(key, state) 334 } keynull335 mediaTimeoutListener.sessionCallback = { key: String -> onSessionDestroyed(key) } 336 mediaResumeListener.setManager(this) 337 mediaDataFilter.mediaDataManager = this 338 339 val suspendFilter = IntentFilter(Intent.ACTION_PACKAGES_SUSPENDED) 340 broadcastDispatcher.registerReceiver(appChangeReceiver, suspendFilter, null, UserHandle.ALL) 341 342 val uninstallFilter = <lambda>null343 IntentFilter().apply { 344 addAction(Intent.ACTION_PACKAGE_REMOVED) 345 addAction(Intent.ACTION_PACKAGE_RESTARTED) 346 addDataScheme("package") 347 } 348 // BroadcastDispatcher does not allow filters with data schemes 349 context.registerReceiver(appChangeReceiver, uninstallFilter) 350 351 // Register for Smartspace data updates. 352 smartspaceMediaDataProvider.registerListener(this) 353 smartspaceSession = 354 smartspaceManager?.createSmartspaceSession( 355 SmartspaceConfig.Builder(context, SMARTSPACE_UI_SURFACE_LABEL).build() 356 ) <lambda>null357 smartspaceSession?.let { 358 it.addOnTargetsAvailableListener( 359 // Use a main uiExecutor thread listening to Smartspace updates instead of using 360 // the existing background executor. 361 // SmartspaceSession has scheduled routine updates which can be unpredictable on 362 // test simulators, using the backgroundExecutor makes it's hard to test the threads 363 // numbers. 364 uiExecutor, 365 SmartspaceSession.OnTargetsAvailableListener { targets -> 366 smartspaceMediaDataProvider.onTargetsAvailable(targets) 367 } 368 ) 369 } <lambda>null370 smartspaceSession?.let { it.requestSmartspaceUpdate() } 371 tunerService.addTunable( 372 object : TunerService.Tunable { onTuningChangednull373 override fun onTuningChanged(key: String?, newValue: String?) { 374 allowMediaRecommendations = allowMediaRecommendations(context) 375 if (!allowMediaRecommendations) { 376 dismissSmartspaceRecommendation( 377 key = smartspaceMediaData.targetId, 378 delay = 0L 379 ) 380 } 381 } 382 }, 383 Settings.Secure.MEDIA_CONTROLS_RECOMMENDATION 384 ) 385 } 386 destroynull387 override fun destroy() { 388 smartspaceMediaDataProvider.unregisterListener(this) 389 smartspaceSession?.close() 390 smartspaceSession = null 391 context.unregisterReceiver(appChangeReceiver) 392 } 393 onNotificationAddednull394 override fun onNotificationAdded(key: String, sbn: StatusBarNotification) { 395 if (useQsMediaPlayer && isMediaNotification(sbn)) { 396 var isNewlyActiveEntry = false 397 Assert.isMainThread() 398 val oldKey = findExistingEntry(key, sbn.packageName) 399 if (oldKey == null) { 400 val instanceId = logger.getNewInstanceId() 401 val temp = 402 LOADING.copy( 403 packageName = sbn.packageName, 404 instanceId = instanceId, 405 createdTimestampMillis = systemClock.currentTimeMillis(), 406 ) 407 mediaEntries.put(key, temp) 408 isNewlyActiveEntry = true 409 } else if (oldKey != key) { 410 // Resume -> active conversion; move to new key 411 val oldData = mediaEntries.remove(oldKey)!! 412 isNewlyActiveEntry = true 413 mediaEntries.put(key, oldData) 414 } 415 loadMediaData(key, sbn, oldKey, isNewlyActiveEntry) 416 } else { 417 onNotificationRemoved(key) 418 } 419 } 420 removeAllForPackagenull421 private fun removeAllForPackage(packageName: String) { 422 Assert.isMainThread() 423 val toRemove = mediaEntries.filter { it.value.packageName == packageName } 424 toRemove.forEach { removeEntry(it.key) } 425 } 426 setResumeActionnull427 override fun setResumeAction(key: String, action: Runnable?) { 428 mediaEntries.get(key)?.let { 429 it.resumeAction = action 430 it.hasCheckedForResume = true 431 } 432 } 433 addResumptionControlsnull434 override fun addResumptionControls( 435 userId: Int, 436 desc: MediaDescription, 437 action: Runnable, 438 token: MediaSession.Token, 439 appName: String, 440 appIntent: PendingIntent, 441 packageName: String 442 ) { 443 // Resume controls don't have a notification key, so store by package name instead 444 if (!mediaEntries.containsKey(packageName)) { 445 val instanceId = logger.getNewInstanceId() 446 val appUid = 447 try { 448 context.packageManager.getApplicationInfo(packageName, 0)?.uid!! 449 } catch (e: PackageManager.NameNotFoundException) { 450 Log.w(TAG, "Could not get app UID for $packageName", e) 451 Process.INVALID_UID 452 } 453 454 val resumeData = 455 LOADING.copy( 456 packageName = packageName, 457 resumeAction = action, 458 hasCheckedForResume = true, 459 instanceId = instanceId, 460 appUid = appUid, 461 createdTimestampMillis = systemClock.currentTimeMillis(), 462 ) 463 mediaEntries.put(packageName, resumeData) 464 logSingleVsMultipleMediaAdded(appUid, packageName, instanceId) 465 logger.logResumeMediaAdded(appUid, packageName, instanceId) 466 } 467 backgroundExecutor.execute { 468 loadMediaDataInBgForResumption( 469 userId, 470 desc, 471 action, 472 token, 473 appName, 474 appIntent, 475 packageName 476 ) 477 } 478 } 479 480 /** 481 * Check if there is an existing entry that matches the key or package name. Returns the key 482 * that matches, or null if not found. 483 */ findExistingEntrynull484 private fun findExistingEntry(key: String, packageName: String): String? { 485 if (mediaEntries.containsKey(key)) { 486 return key 487 } 488 // Check if we already had a resume player 489 if (mediaEntries.containsKey(packageName)) { 490 return packageName 491 } 492 return null 493 } 494 loadMediaDatanull495 private fun loadMediaData( 496 key: String, 497 sbn: StatusBarNotification, 498 oldKey: String?, 499 isNewlyActiveEntry: Boolean = false, 500 ) { 501 backgroundExecutor.execute { loadMediaDataInBg(key, sbn, oldKey, isNewlyActiveEntry) } 502 } 503 504 /** Add a listener for changes in this class */ addListenernull505 override fun addListener(listener: MediaDataManager.Listener) { 506 // mediaDataFilter is the current end of the internal pipeline. Register external 507 // listeners as listeners to it. 508 mediaDataFilter.addListener(listener) 509 } 510 511 /** Remove a listener for changes in this class */ removeListenernull512 override fun removeListener(listener: MediaDataManager.Listener) { 513 // Since mediaDataFilter is the current end of the internal pipelie, external listeners 514 // have been registered to it. So, they need to be removed from it too. 515 mediaDataFilter.removeListener(listener) 516 } 517 518 /** Add a listener for internal events. */ addInternalListenernull519 private fun addInternalListener(listener: MediaDataManager.Listener) = 520 internalListeners.add(listener) 521 522 /** 523 * Notify internal listeners of media loaded event. 524 * 525 * External listeners registered with [addListener] will be notified after the event propagates 526 * through the internal listener pipeline. 527 */ 528 private fun notifyMediaDataLoaded(key: String, oldKey: String?, info: MediaData) { 529 internalListeners.forEach { it.onMediaDataLoaded(key, oldKey, info) } 530 } 531 532 /** 533 * Notify internal listeners of Smartspace media loaded event. 534 * 535 * External listeners registered with [addListener] will be notified after the event propagates 536 * through the internal listener pipeline. 537 */ notifySmartspaceMediaDataLoadednull538 private fun notifySmartspaceMediaDataLoaded(key: String, info: SmartspaceMediaData) { 539 internalListeners.forEach { it.onSmartspaceMediaDataLoaded(key, info) } 540 } 541 542 /** 543 * Notify internal listeners of media removed event. 544 * 545 * External listeners registered with [addListener] will be notified after the event propagates 546 * through the internal listener pipeline. 547 */ notifyMediaDataRemovednull548 private fun notifyMediaDataRemoved(key: String, userInitiated: Boolean = false) { 549 internalListeners.forEach { it.onMediaDataRemoved(key, userInitiated) } 550 } 551 552 /** 553 * Notify internal listeners of Smartspace media removed event. 554 * 555 * External listeners registered with [addListener] will be notified after the event propagates 556 * through the internal listener pipeline. 557 * 558 * @param immediately indicates should apply the UI changes immediately, otherwise wait until 559 * the next refresh-round before UI becomes visible. Should only be true if the update is 560 * initiated by user's interaction. 561 */ notifySmartspaceMediaDataRemovednull562 private fun notifySmartspaceMediaDataRemoved(key: String, immediately: Boolean) { 563 internalListeners.forEach { it.onSmartspaceMediaDataRemoved(key, immediately) } 564 } 565 566 /** 567 * Called whenever the player has been paused or stopped for a while, or swiped from QQS. This 568 * will make the player not active anymore, hiding it from QQS and Keyguard. 569 * 570 * @see MediaData.active 571 */ setInactivenull572 override fun setInactive(key: String, timedOut: Boolean, forceUpdate: Boolean) { 573 mediaEntries[key]?.let { 574 if (timedOut && !forceUpdate) { 575 // Only log this event when media expires on its own 576 logger.logMediaTimeout(it.appUid, it.packageName, it.instanceId) 577 } 578 if (it.active == !timedOut && !forceUpdate) { 579 if (it.resumption) { 580 if (DEBUG) Log.d(TAG, "timing out resume player $key") 581 dismissMediaData(key, delay = 0L, userInitiated = false) 582 } 583 return 584 } 585 // Update last active if media was still active. 586 if (it.active) { 587 it.lastActive = systemClock.elapsedRealtime() 588 } 589 it.active = !timedOut 590 if (DEBUG) Log.d(TAG, "Updating $key timedOut: $timedOut") 591 onMediaDataLoaded(key, key, it) 592 } 593 594 if (key == smartspaceMediaData.targetId) { 595 if (DEBUG) Log.d(TAG, "smartspace card expired") 596 dismissSmartspaceRecommendation(key, delay = 0L) 597 } 598 } 599 600 /** Called when the player's [PlaybackState] has been updated with new actions and/or state */ updateStatenull601 private fun updateState(key: String, state: PlaybackState) { 602 mediaEntries.get(key)?.let { 603 val token = it.token 604 if (token == null) { 605 if (DEBUG) Log.d(TAG, "State updated, but token was null") 606 return 607 } 608 val actions = 609 createActionsFromState( 610 it.packageName, 611 mediaControllerFactory.create(it.token), 612 UserHandle(it.userId) 613 ) 614 615 // Control buttons 616 // If flag is enabled and controller has a PlaybackState, 617 // create actions from session info 618 // otherwise, no need to update semantic actions. 619 val data = 620 if (actions != null) { 621 it.copy(semanticActions = actions, isPlaying = isPlayingState(state.state)) 622 } else { 623 it.copy(isPlaying = isPlayingState(state.state)) 624 } 625 if (DEBUG) Log.d(TAG, "State updated outside of notification") 626 onMediaDataLoaded(key, key, data) 627 } 628 } 629 removeEntrynull630 private fun removeEntry(key: String, logEvent: Boolean = true, userInitiated: Boolean = false) { 631 mediaEntries.remove(key)?.let { 632 if (logEvent) { 633 logger.logMediaRemoved(it.appUid, it.packageName, it.instanceId) 634 } 635 } 636 notifyMediaDataRemoved(key, userInitiated) 637 } 638 639 /** Dismiss a media entry. Returns false if the key was not found. */ dismissMediaDatanull640 override fun dismissMediaData(key: String, delay: Long, userInitiated: Boolean): Boolean { 641 val existed = mediaEntries[key] != null 642 backgroundExecutor.execute { 643 mediaEntries[key]?.let { mediaData -> 644 if (mediaData.isLocalSession()) { 645 mediaData.token?.let { 646 val mediaController = mediaControllerFactory.create(it) 647 mediaController.transportControls.stop() 648 } 649 } 650 } 651 } 652 foregroundExecutor.executeDelayed( 653 { removeEntry(key = key, userInitiated = userInitiated) }, 654 delay 655 ) 656 return existed 657 } 658 659 /** 660 * Called whenever the recommendation has been expired or removed by the user. This will remove 661 * the recommendation card entirely from the carousel. 662 */ dismissSmartspaceRecommendationnull663 override fun dismissSmartspaceRecommendation(key: String, delay: Long) { 664 if (smartspaceMediaData.targetId != key || !smartspaceMediaData.isValid()) { 665 // If this doesn't match, or we've already invalidated the data, no action needed 666 return 667 } 668 669 if (DEBUG) Log.d(TAG, "Dismissing Smartspace media target") 670 if (smartspaceMediaData.isActive) { 671 smartspaceMediaData = 672 EMPTY_SMARTSPACE_MEDIA_DATA.copy( 673 targetId = smartspaceMediaData.targetId, 674 instanceId = smartspaceMediaData.instanceId 675 ) 676 } 677 foregroundExecutor.executeDelayed( 678 { notifySmartspaceMediaDataRemoved(smartspaceMediaData.targetId, immediately = true) }, 679 delay 680 ) 681 } 682 683 /** Called when the recommendation card should no longer be visible in QQS or lockscreen */ setRecommendationInactivenull684 override fun setRecommendationInactive(key: String) { 685 if (!mediaFlags.isPersistentSsCardEnabled()) { 686 Log.e(TAG, "Only persistent recommendation can be inactive!") 687 return 688 } 689 if (DEBUG) Log.d(TAG, "Setting smartspace recommendation inactive") 690 691 if (smartspaceMediaData.targetId != key || !smartspaceMediaData.isValid()) { 692 // If this doesn't match, or we've already invalidated the data, no action needed 693 return 694 } 695 696 smartspaceMediaData = smartspaceMediaData.copy(isActive = false) 697 notifySmartspaceMediaDataLoaded(smartspaceMediaData.targetId, smartspaceMediaData) 698 } 699 loadMediaDataInBgForResumptionnull700 private fun loadMediaDataInBgForResumption( 701 userId: Int, 702 desc: MediaDescription, 703 resumeAction: Runnable, 704 token: MediaSession.Token, 705 appName: String, 706 appIntent: PendingIntent, 707 packageName: String 708 ) { 709 if (desc.title.isNullOrBlank()) { 710 Log.e(TAG, "Description incomplete") 711 // Delete the placeholder entry 712 mediaEntries.remove(packageName) 713 return 714 } 715 716 if (DEBUG) { 717 Log.d(TAG, "adding track for $userId from browser: $desc") 718 } 719 720 val currentEntry = mediaEntries.get(packageName) 721 val appUid = currentEntry?.appUid ?: Process.INVALID_UID 722 723 // Album art 724 var artworkBitmap = desc.iconBitmap 725 if (artworkBitmap == null && desc.iconUri != null) { 726 artworkBitmap = loadBitmapFromUriForUser(desc.iconUri!!, userId, appUid, packageName) 727 } 728 val artworkIcon = 729 if (artworkBitmap != null) { 730 Icon.createWithBitmap(artworkBitmap) 731 } else { 732 null 733 } 734 735 val instanceId = currentEntry?.instanceId ?: logger.getNewInstanceId() 736 val isExplicit = 737 desc.extras?.getLong(MediaConstants.METADATA_KEY_IS_EXPLICIT) == 738 MediaConstants.METADATA_VALUE_ATTRIBUTE_PRESENT 739 740 val progress = 741 if (mediaFlags.isResumeProgressEnabled()) { 742 MediaDataUtils.getDescriptionProgress(desc.extras) 743 } else null 744 745 val mediaAction = getResumeMediaAction(resumeAction) 746 val lastActive = systemClock.elapsedRealtime() 747 val createdTimestampMillis = currentEntry?.createdTimestampMillis ?: 0L 748 foregroundExecutor.execute { 749 onMediaDataLoaded( 750 packageName, 751 null, 752 MediaData( 753 userId, 754 true, 755 appName, 756 null, 757 desc.subtitle, 758 desc.title, 759 artworkIcon, 760 listOf(mediaAction), 761 listOf(0), 762 MediaButton(playOrPause = mediaAction), 763 packageName, 764 token, 765 appIntent, 766 device = null, 767 active = false, 768 resumeAction = resumeAction, 769 resumption = true, 770 notificationKey = packageName, 771 hasCheckedForResume = true, 772 lastActive = lastActive, 773 createdTimestampMillis = createdTimestampMillis, 774 instanceId = instanceId, 775 appUid = appUid, 776 isExplicit = isExplicit, 777 resumeProgress = progress, 778 ) 779 ) 780 } 781 } 782 loadMediaDataInBgnull783 fun loadMediaDataInBg( 784 key: String, 785 sbn: StatusBarNotification, 786 oldKey: String?, 787 isNewlyActiveEntry: Boolean = false, 788 ) { 789 val token = 790 sbn.notification.extras.getParcelable( 791 Notification.EXTRA_MEDIA_SESSION, 792 MediaSession.Token::class.java 793 ) 794 if (token == null) { 795 return 796 } 797 val mediaController = mediaControllerFactory.create(token) 798 val metadata = mediaController.metadata 799 val notif: Notification = sbn.notification 800 801 val appInfo = 802 notif.extras.getParcelable( 803 Notification.EXTRA_BUILDER_APPLICATION_INFO, 804 ApplicationInfo::class.java 805 ) 806 ?: getAppInfoFromPackage(sbn.packageName) 807 808 // App name 809 val appName = getAppName(sbn, appInfo) 810 811 // Song name 812 var song: CharSequence? = metadata?.getString(MediaMetadata.METADATA_KEY_DISPLAY_TITLE) 813 if (song.isNullOrBlank()) { 814 song = metadata?.getString(MediaMetadata.METADATA_KEY_TITLE) 815 } 816 if (song.isNullOrBlank()) { 817 song = HybridGroupManager.resolveTitle(notif) 818 } 819 if (song.isNullOrBlank()) { 820 // For apps that don't include a title, log and add a placeholder 821 song = context.getString(R.string.controls_media_empty_title, appName) 822 try { 823 statusBarManager.logBlankMediaTitle(sbn.packageName, sbn.user.identifier) 824 } catch (e: RuntimeException) { 825 Log.e(TAG, "Error reporting blank media title for package ${sbn.packageName}") 826 } 827 } 828 829 // Album art 830 var artworkBitmap = metadata?.let { loadBitmapFromUri(it) } 831 if (artworkBitmap == null) { 832 artworkBitmap = metadata?.getBitmap(MediaMetadata.METADATA_KEY_ART) 833 } 834 if (artworkBitmap == null) { 835 artworkBitmap = metadata?.getBitmap(MediaMetadata.METADATA_KEY_ALBUM_ART) 836 } 837 val artWorkIcon = 838 if (artworkBitmap == null) { 839 notif.getLargeIcon() 840 } else { 841 Icon.createWithBitmap(artworkBitmap) 842 } 843 844 // App Icon 845 val smallIcon = sbn.notification.smallIcon 846 847 // Explicit Indicator 848 var isExplicit = false 849 val mediaMetadataCompat = MediaMetadataCompat.fromMediaMetadata(metadata) 850 isExplicit = 851 mediaMetadataCompat?.getLong(MediaConstants.METADATA_KEY_IS_EXPLICIT) == 852 MediaConstants.METADATA_VALUE_ATTRIBUTE_PRESENT 853 854 // Artist name 855 var artist: CharSequence? = metadata?.getString(MediaMetadata.METADATA_KEY_ARTIST) 856 if (artist.isNullOrBlank()) { 857 artist = HybridGroupManager.resolveText(notif) 858 } 859 860 // Device name (used for remote cast notifications) 861 var device: MediaDeviceData? = null 862 if (isRemoteCastNotification(sbn)) { 863 val extras = sbn.notification.extras 864 val deviceName = extras.getCharSequence(Notification.EXTRA_MEDIA_REMOTE_DEVICE, null) 865 val deviceIcon = extras.getInt(Notification.EXTRA_MEDIA_REMOTE_ICON, -1) 866 val deviceIntent = 867 extras.getParcelable( 868 Notification.EXTRA_MEDIA_REMOTE_INTENT, 869 PendingIntent::class.java 870 ) 871 Log.d(TAG, "$key is RCN for $deviceName") 872 873 if (deviceName != null && deviceIcon > -1) { 874 // Name and icon must be present, but intent may be null 875 val enabled = deviceIntent != null && deviceIntent.isActivity 876 val deviceDrawable = 877 Icon.createWithResource(sbn.packageName, deviceIcon) 878 .loadDrawable(sbn.getPackageContext(context)) 879 device = 880 MediaDeviceData( 881 enabled, 882 deviceDrawable, 883 deviceName, 884 deviceIntent, 885 showBroadcastButton = false 886 ) 887 } 888 } 889 890 // Control buttons 891 // If flag is enabled and controller has a PlaybackState, create actions from session info 892 // Otherwise, use the notification actions 893 var actionIcons: List<MediaAction> = emptyList() 894 var actionsToShowCollapsed: List<Int> = emptyList() 895 val semanticActions = createActionsFromState(sbn.packageName, mediaController, sbn.user) 896 if (semanticActions == null) { 897 val actions = createActionsFromNotification(sbn) 898 actionIcons = actions.first 899 actionsToShowCollapsed = actions.second 900 } 901 902 val playbackLocation = 903 if (isRemoteCastNotification(sbn)) MediaData.PLAYBACK_CAST_REMOTE 904 else if ( 905 mediaController.playbackInfo?.playbackType == 906 MediaController.PlaybackInfo.PLAYBACK_TYPE_LOCAL 907 ) 908 MediaData.PLAYBACK_LOCAL 909 else MediaData.PLAYBACK_CAST_LOCAL 910 val isPlaying = mediaController.playbackState?.let { isPlayingState(it.state) } ?: null 911 912 val currentEntry = mediaEntries.get(key) 913 val instanceId = currentEntry?.instanceId ?: logger.getNewInstanceId() 914 val appUid = appInfo?.uid ?: Process.INVALID_UID 915 916 if (isNewlyActiveEntry) { 917 logSingleVsMultipleMediaAdded(appUid, sbn.packageName, instanceId) 918 logger.logActiveMediaAdded(appUid, sbn.packageName, instanceId, playbackLocation) 919 } else if (playbackLocation != currentEntry?.playbackLocation) { 920 logger.logPlaybackLocationChange(appUid, sbn.packageName, instanceId, playbackLocation) 921 } 922 923 val lastActive = systemClock.elapsedRealtime() 924 val createdTimestampMillis = currentEntry?.createdTimestampMillis ?: 0L 925 foregroundExecutor.execute { 926 val resumeAction: Runnable? = mediaEntries[key]?.resumeAction 927 val hasCheckedForResume = mediaEntries[key]?.hasCheckedForResume == true 928 val active = mediaEntries[key]?.active ?: true 929 onMediaDataLoaded( 930 key, 931 oldKey, 932 MediaData( 933 sbn.normalizedUserId, 934 true, 935 appName, 936 smallIcon, 937 artist, 938 song, 939 artWorkIcon, 940 actionIcons, 941 actionsToShowCollapsed, 942 semanticActions, 943 sbn.packageName, 944 token, 945 notif.contentIntent, 946 device, 947 active, 948 resumeAction = resumeAction, 949 playbackLocation = playbackLocation, 950 notificationKey = key, 951 hasCheckedForResume = hasCheckedForResume, 952 isPlaying = isPlaying, 953 isClearable = !sbn.isOngoing, 954 lastActive = lastActive, 955 createdTimestampMillis = createdTimestampMillis, 956 instanceId = instanceId, 957 appUid = appUid, 958 isExplicit = isExplicit, 959 ) 960 ) 961 } 962 } 963 logSingleVsMultipleMediaAddednull964 private fun logSingleVsMultipleMediaAdded( 965 appUid: Int, 966 packageName: String, 967 instanceId: InstanceId 968 ) { 969 if (mediaEntries.size == 1) { 970 logger.logSingleMediaPlayerInCarousel(appUid, packageName, instanceId) 971 } else if (mediaEntries.size == 2) { 972 // Since this method is only called when there is a new media session added. 973 // logging needed once there is more than one media session in carousel. 974 logger.logMultipleMediaPlayersInCarousel(appUid, packageName, instanceId) 975 } 976 } 977 getAppInfoFromPackagenull978 private fun getAppInfoFromPackage(packageName: String): ApplicationInfo? { 979 try { 980 return context.packageManager.getApplicationInfo(packageName, 0) 981 } catch (e: PackageManager.NameNotFoundException) { 982 Log.w(TAG, "Could not get app info for $packageName", e) 983 } 984 return null 985 } 986 getAppNamenull987 private fun getAppName(sbn: StatusBarNotification, appInfo: ApplicationInfo?): String { 988 val name = sbn.notification.extras.getString(EXTRA_SUBSTITUTE_APP_NAME) 989 if (name != null) { 990 return name 991 } 992 993 return if (appInfo != null) { 994 context.packageManager.getApplicationLabel(appInfo).toString() 995 } else { 996 sbn.packageName 997 } 998 } 999 1000 /** Generate action buttons based on notification actions */ createActionsFromNotificationnull1001 private fun createActionsFromNotification( 1002 sbn: StatusBarNotification 1003 ): Pair<List<MediaAction>, List<Int>> { 1004 val notif = sbn.notification 1005 val actionIcons: MutableList<MediaAction> = ArrayList() 1006 val actions = notif.actions 1007 var actionsToShowCollapsed = 1008 notif.extras.getIntArray(Notification.EXTRA_COMPACT_ACTIONS)?.toMutableList() 1009 ?: mutableListOf() 1010 if (actionsToShowCollapsed.size > MAX_COMPACT_ACTIONS) { 1011 Log.e( 1012 TAG, 1013 "Too many compact actions for ${sbn.key}," + 1014 "limiting to first $MAX_COMPACT_ACTIONS" 1015 ) 1016 actionsToShowCollapsed = actionsToShowCollapsed.subList(0, MAX_COMPACT_ACTIONS) 1017 } 1018 1019 if (actions != null) { 1020 for ((index, action) in actions.withIndex()) { 1021 if (index == MAX_NOTIFICATION_ACTIONS) { 1022 Log.w( 1023 TAG, 1024 "Too many notification actions for ${sbn.key}," + 1025 " limiting to first $MAX_NOTIFICATION_ACTIONS" 1026 ) 1027 break 1028 } 1029 if (action.getIcon() == null) { 1030 if (DEBUG) Log.i(TAG, "No icon for action $index ${action.title}") 1031 actionsToShowCollapsed.remove(index) 1032 continue 1033 } 1034 val runnable = 1035 if (action.actionIntent != null) { 1036 Runnable { 1037 if (action.actionIntent.isActivity) { 1038 activityStarter.startPendingIntentDismissingKeyguard( 1039 action.actionIntent 1040 ) 1041 } else if (action.isAuthenticationRequired()) { 1042 activityStarter.dismissKeyguardThenExecute( 1043 { 1044 var result = sendPendingIntent(action.actionIntent) 1045 result 1046 }, 1047 {}, 1048 true 1049 ) 1050 } else { 1051 sendPendingIntent(action.actionIntent) 1052 } 1053 } 1054 } else { 1055 null 1056 } 1057 val mediaActionIcon = 1058 if (action.getIcon()?.getType() == Icon.TYPE_RESOURCE) { 1059 Icon.createWithResource(sbn.packageName, action.getIcon()!!.getResId()) 1060 } else { 1061 action.getIcon() 1062 } 1063 .setTint(themeText) 1064 .loadDrawable(context) 1065 val mediaAction = MediaAction(mediaActionIcon, runnable, action.title, null) 1066 actionIcons.add(mediaAction) 1067 } 1068 } 1069 return Pair(actionIcons, actionsToShowCollapsed) 1070 } 1071 1072 /** 1073 * Generates action button info for this media session based on the PlaybackState 1074 * 1075 * @param packageName Package name for the media app 1076 * @param controller MediaController for the current session 1077 * @return a Pair consisting of a list of media actions, and a list of ints representing which 1078 * 1079 * ``` 1080 * of those actions should be shown in the compact player 1081 * ``` 1082 */ createActionsFromStatenull1083 private fun createActionsFromState( 1084 packageName: String, 1085 controller: MediaController, 1086 user: UserHandle 1087 ): MediaButton? { 1088 val state = controller.playbackState 1089 if (state == null || !mediaFlags.areMediaSessionActionsEnabled(packageName, user)) { 1090 return null 1091 } 1092 1093 // First, check for standard actions 1094 val playOrPause = 1095 if (isConnectingState(state.state)) { 1096 // Spinner needs to be animating to render anything. Start it here. 1097 val drawable = 1098 context.getDrawable(com.android.internal.R.drawable.progress_small_material) 1099 (drawable as Animatable).start() 1100 MediaAction( 1101 drawable, 1102 null, // no action to perform when clicked 1103 context.getString(R.string.controls_media_button_connecting), 1104 context.getDrawable(R.drawable.ic_media_connecting_container), 1105 // Specify a rebind id to prevent the spinner from restarting on later binds. 1106 com.android.internal.R.drawable.progress_small_material 1107 ) 1108 } else if (isPlayingState(state.state)) { 1109 getStandardAction(controller, state.actions, PlaybackState.ACTION_PAUSE) 1110 } else { 1111 getStandardAction(controller, state.actions, PlaybackState.ACTION_PLAY) 1112 } 1113 val prevButton = 1114 getStandardAction(controller, state.actions, PlaybackState.ACTION_SKIP_TO_PREVIOUS) 1115 val nextButton = 1116 getStandardAction(controller, state.actions, PlaybackState.ACTION_SKIP_TO_NEXT) 1117 1118 // Then, create a way to build any custom actions that will be needed 1119 val customActions = 1120 state.customActions 1121 .asSequence() 1122 .filterNotNull() 1123 .map { getCustomAction(state, packageName, controller, it) } 1124 .iterator() 1125 fun nextCustomAction() = if (customActions.hasNext()) customActions.next() else null 1126 1127 // Finally, assign the remaining button slots: play/pause A B C D 1128 // A = previous, else custom action (if not reserved) 1129 // B = next, else custom action (if not reserved) 1130 // C and D are always custom actions 1131 val reservePrev = 1132 controller.extras?.getBoolean( 1133 MediaConstants.SESSION_EXTRAS_KEY_SLOT_RESERVATION_SKIP_TO_PREV 1134 ) == true 1135 val reserveNext = 1136 controller.extras?.getBoolean( 1137 MediaConstants.SESSION_EXTRAS_KEY_SLOT_RESERVATION_SKIP_TO_NEXT 1138 ) == true 1139 1140 val prevOrCustom = 1141 if (prevButton != null) { 1142 prevButton 1143 } else if (!reservePrev) { 1144 nextCustomAction() 1145 } else { 1146 null 1147 } 1148 1149 val nextOrCustom = 1150 if (nextButton != null) { 1151 nextButton 1152 } else if (!reserveNext) { 1153 nextCustomAction() 1154 } else { 1155 null 1156 } 1157 1158 return MediaButton( 1159 playOrPause, 1160 nextOrCustom, 1161 prevOrCustom, 1162 nextCustomAction(), 1163 nextCustomAction(), 1164 reserveNext, 1165 reservePrev 1166 ) 1167 } 1168 1169 /** 1170 * Create a [MediaAction] for a given action and media session 1171 * 1172 * @param controller MediaController for the session 1173 * @param stateActions The actions included with the session's [PlaybackState] 1174 * @param action A [PlaybackState.Actions] value representing what action to generate. One of: 1175 * ``` 1176 * [PlaybackState.ACTION_PLAY] 1177 * [PlaybackState.ACTION_PAUSE] 1178 * [PlaybackState.ACTION_SKIP_TO_PREVIOUS] 1179 * [PlaybackState.ACTION_SKIP_TO_NEXT] 1180 * @return 1181 * ``` 1182 * 1183 * A [MediaAction] with correct values set, or null if the state doesn't support it 1184 */ getStandardActionnull1185 private fun getStandardAction( 1186 controller: MediaController, 1187 stateActions: Long, 1188 @PlaybackState.Actions action: Long 1189 ): MediaAction? { 1190 if (!includesAction(stateActions, action)) { 1191 return null 1192 } 1193 1194 return when (action) { 1195 PlaybackState.ACTION_PLAY -> { 1196 MediaAction( 1197 context.getDrawable(R.drawable.ic_media_play), 1198 { controller.transportControls.play() }, 1199 context.getString(R.string.controls_media_button_play), 1200 context.getDrawable(R.drawable.ic_media_play_container) 1201 ) 1202 } 1203 PlaybackState.ACTION_PAUSE -> { 1204 MediaAction( 1205 context.getDrawable(R.drawable.ic_media_pause), 1206 { controller.transportControls.pause() }, 1207 context.getString(R.string.controls_media_button_pause), 1208 context.getDrawable(R.drawable.ic_media_pause_container) 1209 ) 1210 } 1211 PlaybackState.ACTION_SKIP_TO_PREVIOUS -> { 1212 MediaAction( 1213 context.getDrawable(R.drawable.ic_media_prev), 1214 { controller.transportControls.skipToPrevious() }, 1215 context.getString(R.string.controls_media_button_prev), 1216 null 1217 ) 1218 } 1219 PlaybackState.ACTION_SKIP_TO_NEXT -> { 1220 MediaAction( 1221 context.getDrawable(R.drawable.ic_media_next), 1222 { controller.transportControls.skipToNext() }, 1223 context.getString(R.string.controls_media_button_next), 1224 null 1225 ) 1226 } 1227 else -> null 1228 } 1229 } 1230 1231 /** Check whether the actions from a [PlaybackState] include a specific action */ includesActionnull1232 private fun includesAction(stateActions: Long, @PlaybackState.Actions action: Long): Boolean { 1233 if ( 1234 (action == PlaybackState.ACTION_PLAY || action == PlaybackState.ACTION_PAUSE) && 1235 (stateActions and PlaybackState.ACTION_PLAY_PAUSE > 0L) 1236 ) { 1237 return true 1238 } 1239 return (stateActions and action != 0L) 1240 } 1241 1242 /** Get a [MediaAction] representing a [PlaybackState.CustomAction] */ getCustomActionnull1243 private fun getCustomAction( 1244 state: PlaybackState, 1245 packageName: String, 1246 controller: MediaController, 1247 customAction: PlaybackState.CustomAction 1248 ): MediaAction { 1249 return MediaAction( 1250 Icon.createWithResource(packageName, customAction.icon).loadDrawable(context), 1251 { controller.transportControls.sendCustomAction(customAction, customAction.extras) }, 1252 customAction.name, 1253 null 1254 ) 1255 } 1256 1257 /** Load a bitmap from the various Art metadata URIs */ loadBitmapFromUrinull1258 private fun loadBitmapFromUri(metadata: MediaMetadata): Bitmap? { 1259 for (uri in ART_URIS) { 1260 val uriString = metadata.getString(uri) 1261 if (!TextUtils.isEmpty(uriString)) { 1262 val albumArt = loadBitmapFromUri(Uri.parse(uriString)) 1263 if (albumArt != null) { 1264 if (DEBUG) Log.d(TAG, "loaded art from $uri") 1265 return albumArt 1266 } 1267 } 1268 } 1269 return null 1270 } 1271 sendPendingIntentnull1272 private fun sendPendingIntent(intent: PendingIntent): Boolean { 1273 return try { 1274 val options = BroadcastOptions.makeBasic() 1275 options.setInteractive(true) 1276 options.setPendingIntentBackgroundActivityStartMode( 1277 ActivityOptions.MODE_BACKGROUND_ACTIVITY_START_ALLOWED 1278 ) 1279 intent.send(options.toBundle()) 1280 true 1281 } catch (e: PendingIntent.CanceledException) { 1282 Log.d(TAG, "Intent canceled", e) 1283 false 1284 } 1285 } 1286 1287 /** Returns a bitmap if the user can access the given URI, else null */ loadBitmapFromUriForUsernull1288 private fun loadBitmapFromUriForUser( 1289 uri: Uri, 1290 userId: Int, 1291 appUid: Int, 1292 packageName: String, 1293 ): Bitmap? { 1294 try { 1295 val ugm = UriGrantsManager.getService() 1296 ugm.checkGrantUriPermission_ignoreNonSystem( 1297 appUid, 1298 packageName, 1299 ContentProvider.getUriWithoutUserId(uri), 1300 Intent.FLAG_GRANT_READ_URI_PERMISSION, 1301 ContentProvider.getUserIdFromUri(uri, userId) 1302 ) 1303 return loadBitmapFromUri(uri) 1304 } catch (e: SecurityException) { 1305 Log.e(TAG, "Failed to get URI permission: $e") 1306 } 1307 return null 1308 } 1309 1310 /** 1311 * Load a bitmap from a URI 1312 * 1313 * @param uri the uri to load 1314 * @return bitmap, or null if couldn't be loaded 1315 */ loadBitmapFromUrinull1316 private fun loadBitmapFromUri(uri: Uri): Bitmap? { 1317 // ImageDecoder requires a scheme of the following types 1318 if (uri.scheme == null) { 1319 return null 1320 } 1321 1322 if ( 1323 !uri.scheme.equals(ContentResolver.SCHEME_CONTENT) && 1324 !uri.scheme.equals(ContentResolver.SCHEME_ANDROID_RESOURCE) && 1325 !uri.scheme.equals(ContentResolver.SCHEME_FILE) 1326 ) { 1327 return null 1328 } 1329 1330 val source = ImageDecoder.createSource(context.contentResolver, uri) 1331 return try { 1332 ImageDecoder.decodeBitmap(source) { decoder, info, _ -> 1333 val width = info.size.width 1334 val height = info.size.height 1335 val scale = 1336 MediaDataUtils.getScaleFactor( 1337 APair(width, height), 1338 APair(artworkWidth, artworkHeight) 1339 ) 1340 1341 // Downscale if needed 1342 if (scale != 0f && scale < 1) { 1343 decoder.setTargetSize((scale * width).toInt(), (scale * height).toInt()) 1344 } 1345 decoder.allocator = ImageDecoder.ALLOCATOR_SOFTWARE 1346 } 1347 } catch (e: IOException) { 1348 Log.e(TAG, "Unable to load bitmap", e) 1349 null 1350 } catch (e: RuntimeException) { 1351 Log.e(TAG, "Unable to load bitmap", e) 1352 null 1353 } 1354 } 1355 getResumeMediaActionnull1356 private fun getResumeMediaAction(action: Runnable): MediaAction { 1357 return MediaAction( 1358 Icon.createWithResource(context, R.drawable.ic_media_play) 1359 .setTint(themeText) 1360 .loadDrawable(context), 1361 action, 1362 context.getString(R.string.controls_media_resume), 1363 context.getDrawable(R.drawable.ic_media_play_container) 1364 ) 1365 } 1366 onMediaDataLoadednull1367 fun onMediaDataLoaded(key: String, oldKey: String?, data: MediaData) = 1368 traceSection("MediaDataManager#onMediaDataLoaded") { 1369 Assert.isMainThread() 1370 if (mediaEntries.containsKey(key)) { 1371 // Otherwise this was removed already 1372 mediaEntries.put(key, data) 1373 notifyMediaDataLoaded(key, oldKey, data) 1374 } 1375 } 1376 onSmartspaceTargetsUpdatednull1377 override fun onSmartspaceTargetsUpdated(targets: List<Parcelable>) { 1378 if (!allowMediaRecommendations) { 1379 if (DEBUG) Log.d(TAG, "Smartspace recommendation is disabled in Settings.") 1380 return 1381 } 1382 1383 val mediaTargets = targets.filterIsInstance<SmartspaceTarget>() 1384 when (mediaTargets.size) { 1385 0 -> { 1386 if (!smartspaceMediaData.isActive) { 1387 return 1388 } 1389 if (DEBUG) { 1390 Log.d(TAG, "Set Smartspace media to be inactive for the data update") 1391 } 1392 if (mediaFlags.isPersistentSsCardEnabled()) { 1393 // Smartspace uses this signal to hide the card (e.g. when it expires or user 1394 // disconnects headphones), so treat as setting inactive when flag is on 1395 smartspaceMediaData = smartspaceMediaData.copy(isActive = false) 1396 notifySmartspaceMediaDataLoaded( 1397 smartspaceMediaData.targetId, 1398 smartspaceMediaData, 1399 ) 1400 } else { 1401 smartspaceMediaData = 1402 EMPTY_SMARTSPACE_MEDIA_DATA.copy( 1403 targetId = smartspaceMediaData.targetId, 1404 instanceId = smartspaceMediaData.instanceId, 1405 ) 1406 notifySmartspaceMediaDataRemoved( 1407 smartspaceMediaData.targetId, 1408 immediately = false, 1409 ) 1410 } 1411 } 1412 1 -> { 1413 val newMediaTarget = mediaTargets.get(0) 1414 if (smartspaceMediaData.targetId == newMediaTarget.smartspaceTargetId) { 1415 // The same Smartspace updates can be received. Skip the duplicate updates. 1416 return 1417 } 1418 if (DEBUG) Log.d(TAG, "Forwarding Smartspace media update.") 1419 smartspaceMediaData = toSmartspaceMediaData(newMediaTarget) 1420 notifySmartspaceMediaDataLoaded(smartspaceMediaData.targetId, smartspaceMediaData) 1421 } 1422 else -> { 1423 // There should NOT be more than 1 Smartspace media update. When it happens, it 1424 // indicates a bad state or an error. Reset the status accordingly. 1425 Log.wtf(TAG, "More than 1 Smartspace Media Update. Resetting the status...") 1426 notifySmartspaceMediaDataRemoved( 1427 smartspaceMediaData.targetId, 1428 immediately = false, 1429 ) 1430 smartspaceMediaData = EMPTY_SMARTSPACE_MEDIA_DATA 1431 } 1432 } 1433 } 1434 onNotificationRemovednull1435 override fun onNotificationRemoved(key: String) { 1436 Assert.isMainThread() 1437 val removed = mediaEntries.remove(key) ?: return 1438 if (keyguardUpdateMonitor.isUserInLockdown(removed.userId)) { 1439 logger.logMediaRemoved(removed.appUid, removed.packageName, removed.instanceId) 1440 } else if (isAbleToResume(removed)) { 1441 convertToResumePlayer(key, removed) 1442 } else if (mediaFlags.isRetainingPlayersEnabled()) { 1443 handlePossibleRemoval(key, removed, notificationRemoved = true) 1444 } else { 1445 notifyMediaDataRemoved(key) 1446 logger.logMediaRemoved(removed.appUid, removed.packageName, removed.instanceId) 1447 } 1448 } 1449 onSessionDestroyednull1450 private fun onSessionDestroyed(key: String) { 1451 if (DEBUG) Log.d(TAG, "session destroyed for $key") 1452 val entry = mediaEntries.remove(key) ?: return 1453 // Clear token since the session is no longer valid 1454 val updated = entry.copy(token = null) 1455 handlePossibleRemoval(key, updated) 1456 } 1457 isAbleToResumenull1458 private fun isAbleToResume(data: MediaData): Boolean { 1459 val isEligibleForResume = 1460 data.isLocalSession() || 1461 (mediaFlags.isRemoteResumeAllowed() && 1462 data.playbackLocation != MediaData.PLAYBACK_CAST_REMOTE) 1463 return useMediaResumption && data.resumeAction != null && isEligibleForResume 1464 } 1465 1466 /** 1467 * Convert to resume state if the player is no longer valid and active, then notify listeners 1468 * that the data was updated. Does not convert to resume state if the player is still valid, or 1469 * if it was removed before becoming inactive. (Assumes that [removed] was removed from 1470 * [mediaEntries] before this function was called) 1471 */ handlePossibleRemovalnull1472 private fun handlePossibleRemoval( 1473 key: String, 1474 removed: MediaData, 1475 notificationRemoved: Boolean = false 1476 ) { 1477 val hasSession = removed.token != null 1478 if (hasSession && removed.semanticActions != null) { 1479 // The app was using session actions, and the session is still valid: keep player 1480 if (DEBUG) Log.d(TAG, "Notification removed but using session actions $key") 1481 mediaEntries.put(key, removed) 1482 notifyMediaDataLoaded(key, key, removed) 1483 } else if (!notificationRemoved && removed.semanticActions == null) { 1484 // The app was using notification actions, and notif wasn't removed yet: keep player 1485 if (DEBUG) Log.d(TAG, "Session destroyed but using notification actions $key") 1486 mediaEntries.put(key, removed) 1487 notifyMediaDataLoaded(key, key, removed) 1488 } else if (removed.active && !isAbleToResume(removed)) { 1489 // This player was still active - it didn't last long enough to time out, 1490 // and its app doesn't normally support resume: remove 1491 if (DEBUG) Log.d(TAG, "Removing still-active player $key") 1492 notifyMediaDataRemoved(key) 1493 logger.logMediaRemoved(removed.appUid, removed.packageName, removed.instanceId) 1494 } else if (mediaFlags.isRetainingPlayersEnabled() || isAbleToResume(removed)) { 1495 // Convert to resume 1496 if (DEBUG) { 1497 Log.d( 1498 TAG, 1499 "Notification ($notificationRemoved) and/or session " + 1500 "($hasSession) gone for inactive player $key" 1501 ) 1502 } 1503 convertToResumePlayer(key, removed) 1504 } else { 1505 // Retaining players flag is off and app doesn't support resume: remove player. 1506 if (DEBUG) Log.d(TAG, "Removing player $key") 1507 notifyMediaDataRemoved(key) 1508 logger.logMediaRemoved(removed.appUid, removed.packageName, removed.instanceId) 1509 } 1510 } 1511 1512 /** Set the given [MediaData] as a resume state player and notify listeners */ convertToResumePlayernull1513 private fun convertToResumePlayer(key: String, data: MediaData) { 1514 if (DEBUG) Log.d(TAG, "Converting $key to resume") 1515 // Resumption controls must have a title. 1516 if (data.song.isNullOrBlank()) { 1517 Log.e(TAG, "Description incomplete") 1518 notifyMediaDataRemoved(key) 1519 logger.logMediaRemoved(data.appUid, data.packageName, data.instanceId) 1520 return 1521 } 1522 // Move to resume key (aka package name) if that key doesn't already exist. 1523 val resumeAction = data.resumeAction?.let { getResumeMediaAction(it) } 1524 val actions = resumeAction?.let { listOf(resumeAction) } ?: emptyList() 1525 val launcherIntent = 1526 context.packageManager.getLaunchIntentForPackage(data.packageName)?.let { 1527 PendingIntent.getActivity(context, 0, it, PendingIntent.FLAG_IMMUTABLE) 1528 } 1529 val lastActive = 1530 if (data.active) { 1531 systemClock.elapsedRealtime() 1532 } else { 1533 data.lastActive 1534 } 1535 val updated = 1536 data.copy( 1537 token = null, 1538 actions = actions, 1539 semanticActions = MediaButton(playOrPause = resumeAction), 1540 actionsToShowInCompact = listOf(0), 1541 active = false, 1542 resumption = true, 1543 isPlaying = false, 1544 isClearable = true, 1545 clickIntent = launcherIntent, 1546 lastActive = lastActive, 1547 ) 1548 val pkg = data.packageName 1549 val migrate = mediaEntries.put(pkg, updated) == null 1550 // Notify listeners of "new" controls when migrating or removed and update when not 1551 Log.d(TAG, "migrating? $migrate from $key -> $pkg") 1552 if (migrate) { 1553 notifyMediaDataLoaded(key = pkg, oldKey = key, info = updated) 1554 } else { 1555 // Since packageName is used for the key of the resumption controls, it is 1556 // possible that another notification has already been reused for the resumption 1557 // controls of this package. In this case, rather than renaming this player as 1558 // packageName, just remove it and then send a update to the existing resumption 1559 // controls. 1560 notifyMediaDataRemoved(key) 1561 notifyMediaDataLoaded(key = pkg, oldKey = pkg, info = updated) 1562 } 1563 logger.logActiveConvertedToResume(updated.appUid, pkg, updated.instanceId) 1564 1565 // Limit total number of resume controls 1566 val resumeEntries = mediaEntries.filter { (key, data) -> data.resumption } 1567 val numResume = resumeEntries.size 1568 if (numResume > ResumeMediaBrowser.MAX_RESUMPTION_CONTROLS) { 1569 resumeEntries 1570 .toList() 1571 .sortedBy { (key, data) -> data.lastActive } 1572 .subList(0, numResume - ResumeMediaBrowser.MAX_RESUMPTION_CONTROLS) 1573 .forEach { (key, data) -> 1574 Log.d(TAG, "Removing excess control $key") 1575 mediaEntries.remove(key) 1576 notifyMediaDataRemoved(key) 1577 logger.logMediaRemoved(data.appUid, data.packageName, data.instanceId) 1578 } 1579 } 1580 } 1581 setMediaResumptionEnablednull1582 override fun setMediaResumptionEnabled(isEnabled: Boolean) { 1583 if (useMediaResumption == isEnabled) { 1584 return 1585 } 1586 1587 useMediaResumption = isEnabled 1588 1589 if (!useMediaResumption) { 1590 // Remove any existing resume controls 1591 val filtered = mediaEntries.filter { !it.value.active } 1592 filtered.forEach { 1593 mediaEntries.remove(it.key) 1594 notifyMediaDataRemoved(it.key) 1595 logger.logMediaRemoved(it.value.appUid, it.value.packageName, it.value.instanceId) 1596 } 1597 } 1598 } 1599 1600 /** Invoked when the user has dismissed the media carousel */ onSwipeToDismissnull1601 override fun onSwipeToDismiss() = mediaDataFilter.onSwipeToDismiss() 1602 1603 /** Are there any media notifications active, including the recommendations? */ 1604 override fun hasActiveMediaOrRecommendation() = mediaDataFilter.hasActiveMediaOrRecommendation() 1605 1606 /** 1607 * Are there any media entries we should display, including the recommendations? 1608 * - If resumption is enabled, this will include inactive players 1609 * - If resumption is disabled, we only want to show active players 1610 */ 1611 override fun hasAnyMediaOrRecommendation() = mediaDataFilter.hasAnyMediaOrRecommendation() 1612 1613 /** Are there any resume media notifications active, excluding the recommendations? */ 1614 override fun hasActiveMedia() = mediaDataFilter.hasActiveMedia() 1615 1616 /** 1617 * Are there any resume media notifications active, excluding the recommendations? 1618 * - If resumption is enabled, this will include inactive players 1619 * - If resumption is disabled, we only want to show active players 1620 */ 1621 override fun hasAnyMedia() = mediaDataFilter.hasAnyMedia() 1622 override fun isRecommendationActive() = smartspaceMediaData.isActive 1623 1624 /** 1625 * Converts the pass-in SmartspaceTarget to SmartspaceMediaData 1626 * 1627 * @return An empty SmartspaceMediaData with the valid target Id is returned if the 1628 * SmartspaceTarget's data is invalid. 1629 */ 1630 private fun toSmartspaceMediaData(target: SmartspaceTarget): SmartspaceMediaData { 1631 val baseAction: SmartspaceAction? = target.baseAction 1632 val dismissIntent = 1633 baseAction?.extras?.getParcelable(EXTRAS_SMARTSPACE_DISMISS_INTENT_KEY) as Intent? 1634 1635 val isActive = 1636 when { 1637 !mediaFlags.isPersistentSsCardEnabled() -> true 1638 baseAction == null -> true 1639 else -> { 1640 val triggerSource = baseAction.extras?.getString(EXTRA_KEY_TRIGGER_SOURCE) 1641 triggerSource != EXTRA_VALUE_TRIGGER_PERIODIC 1642 } 1643 } 1644 1645 packageName(target)?.let { 1646 return SmartspaceMediaData( 1647 targetId = target.smartspaceTargetId, 1648 isActive = isActive, 1649 packageName = it, 1650 cardAction = target.baseAction, 1651 recommendations = target.iconGrid, 1652 dismissIntent = dismissIntent, 1653 headphoneConnectionTimeMillis = target.creationTimeMillis, 1654 instanceId = logger.getNewInstanceId(), 1655 expiryTimeMs = target.expiryTimeMillis, 1656 ) 1657 } 1658 return EMPTY_SMARTSPACE_MEDIA_DATA.copy( 1659 targetId = target.smartspaceTargetId, 1660 isActive = isActive, 1661 dismissIntent = dismissIntent, 1662 headphoneConnectionTimeMillis = target.creationTimeMillis, 1663 instanceId = logger.getNewInstanceId(), 1664 expiryTimeMs = target.expiryTimeMillis, 1665 ) 1666 } 1667 packageNamenull1668 private fun packageName(target: SmartspaceTarget): String? { 1669 val recommendationList = target.iconGrid 1670 if (recommendationList == null || recommendationList.isEmpty()) { 1671 Log.w(TAG, "Empty or null media recommendation list.") 1672 return null 1673 } 1674 for (recommendation in recommendationList) { 1675 val extras = recommendation.extras 1676 extras?.let { 1677 it.getString(EXTRAS_MEDIA_SOURCE_PACKAGE_NAME)?.let { packageName -> 1678 return packageName 1679 } 1680 } 1681 } 1682 Log.w(TAG, "No valid package name is provided.") 1683 return null 1684 } 1685 dumpnull1686 override fun dump(pw: PrintWriter, args: Array<out String>) { 1687 pw.apply { 1688 println("internalListeners: $internalListeners") 1689 println("externalListeners: ${mediaDataFilter.listeners}") 1690 println("mediaEntries: $mediaEntries") 1691 println("useMediaResumption: $useMediaResumption") 1692 println("allowMediaRecommendations: $allowMediaRecommendations") 1693 } 1694 mediaDeviceManager.dump(pw) 1695 } 1696 } 1697