this.location = location hostView = mediaHierarchyManager.register(this) // Listen by default, as the host might not be attached by our clients, until // they get a visibility change. We still want to stay up to date in that case! setListeningToMediaData(true) hostView.addOnAttachStateChangeListener( object : OnAttachStateChangeListener { overridefunonViewAttachedToWindow(v: View?) { setListeningToMediaData(true) updateViewVisibility() }
// Listen to measurement updates and update our state with it hostView.measurementManager = object : UniqueObjectHostView.MeasurementManager { overridefunonMeasure(input: MeasurementInput): MeasurementOutput { // Modify the measurement to exactly match the dimensions if ( View.MeasureSpec.getMode(input.widthMeasureSpec) == View.MeasureSpec.AT_MOST ) { input.widthMeasureSpec = View.MeasureSpec.makeMeasureSpec( View.MeasureSpec.getSize(input.widthMeasureSpec), View.MeasureSpec.EXACTLY ) } // This will trigger a state change that ensures that we now have a state // available state.measurementInput = input return mediaHostStatesManager.updateCarouselDimensions(location, state) } }
// Whenever the state changes, let our state manager know state.changedListener = { mediaHostStatesManager.updateHostState(location, state) }
// Initialize the internal processing pipeline. The listeners at the front of the pipeline // are set as internal listeners so that they receive events. From there, events are // propagated through the pipeline. The end of the pipeline is currently mediaDataFilter, // so it is responsible for dispatching events to external listeners. To achieve this, // external listeners that are registered with [MediaDataManager.addListener] are actually // registered as listeners to mediaDataFilter. addInternalListener(mediaTimeoutListener) addInternalListener(mediaResumeListener) addInternalListener(mediaSessionBasedFilter) mediaSessionBasedFilter.addListener(mediaDeviceManager) mediaSessionBasedFilter.addListener(mediaDataCombineLatest) mediaDeviceManager.addListener(mediaDataCombineLatest) mediaDataCombineLatest.addListener(mediaDataFilter)
// Set up links back into the pipeline for listeners that need to send events upstream. mediaTimeoutListener.timeoutCallback = { key: String, timedOut: Boolean -> setTimedOut(key, timedOut) } mediaTimeoutListener.stateCallback = { key: String, state: PlaybackState -> updateState(key, state) } mediaTimeoutListener.sessionCallback = { key: String -> onSessionDestroyed(key) } mediaResumeListener.setManager(this) mediaDataFilter.mediaDataManager = this
val suspendFilter = IntentFilter(Intent.ACTION_PACKAGES_SUSPENDED) broadcastDispatcher.registerReceiver(appChangeReceiver, suspendFilter, null, UserHandle.ALL)
val uninstallFilter = IntentFilter().apply { addAction(Intent.ACTION_PACKAGE_REMOVED) addAction(Intent.ACTION_PACKAGE_RESTARTED) addDataScheme("package") } // BroadcastDispatcher does not allow filters with data schemes context.registerReceiver(appChangeReceiver, uninstallFilter)
// Register for Smartspace data updates. smartspaceMediaDataProvider.registerListener(this) smartspaceSession = smartspaceManager.createSmartspaceSession( SmartspaceConfig.Builder(context, SMARTSPACE_UI_SURFACE_LABEL).build() ) smartspaceSession?.let { it.addOnTargetsAvailableListener( // Use a main uiExecutor thread listening to Smartspace updates instead of using // the existing background executor. // SmartspaceSession has scheduled routine updates which can be unpredictable on // test simulators, using the backgroundExecutor makes it's hard to test the threads // numbers. uiExecutor, SmartspaceSession.OnTargetsAvailableListener { targets -> smartspaceMediaDataProvider.onTargetsAvailable(targets) } ) } smartspaceSession?.let { it.requestSmartspaceUpdate() } tunerService.addTunable( object : TunerService.Tunable { overridefunonTuningChanged(key: String?, newValue: String?) { allowMediaRecommendations = allowMediaRecommendations(context) if (!allowMediaRecommendations) { dismissSmartspaceRecommendation( key = smartspaceMediaData.targetId, delay = 0L ) } } }, Settings.Secure.MEDIA_CONTROLS_RECOMMENDATION ) }
funloadMediaDataInBg( key: String, sbn: StatusBarNotification, oldKey: String?, logEvent: Boolean = false ) { val token = sbn.notification.extras.getParcelable(...) if (token == null) { return } val mediaController = mediaControllerFactory.create(token) val metadata = mediaController.metadata val notif: Notification = sbn.notification
val appInfo = ...
// Album art var artworkBitmap = metadata?.let { loadBitmapFromUri(it) } if (artworkBitmap == null) { artworkBitmap = metadata?.getBitmap(MediaMetadata.METADATA_KEY_ART) } if (artworkBitmap == null) { artworkBitmap = metadata?.getBitmap(MediaMetadata.METADATA_KEY_ALBUM_ART) } val artWorkIcon = if (artworkBitmap == null) { notif.getLargeIcon() } else { Icon.createWithBitmap(artworkBitmap) }
// App name val appName = getAppName(sbn, appInfo)
// App Icon val smallIcon = sbn.notification.smallIcon
// Song name var song: CharSequence? = metadata?.getString(MediaMetadata.METADATA_KEY_DISPLAY_TITLE) if (song == null) { song = metadata?.getString(MediaMetadata.METADATA_KEY_TITLE) } if (song == null) { song = HybridGroupManager.resolveTitle(notif) }
// Explicit Indicator var isExplicit = false if (mediaFlags.isExplicitIndicatorEnabled()) { val mediaMetadataCompat = MediaMetadataCompat.fromMediaMetadata(metadata) isExplicit = mediaMetadataCompat?.getLong(MediaConstants.METADATA_KEY_IS_EXPLICIT) == MediaConstants.METADATA_VALUE_ATTRIBUTE_PRESENT }
// Artist name var artist: CharSequence? = metadata?.getString(MediaMetadata.METADATA_KEY_ARTIST) if (artist == null) { artist = HybridGroupManager.resolveText(notif) }
// Device name (used for remote cast notifications) var device: MediaDeviceData? = null if (isRemoteCastNotification(sbn)) { ... }
// Control buttons // If flag is enabled and controller has a PlaybackState, create actions from session info // Otherwise, use the notification actions var actionIcons: List<MediaAction> = emptyList() var actionsToShowCollapsed: List<Int> = emptyList() val semanticActions = createActionsFromState(sbn.packageName, mediaController, sbn.user) if (semanticActions == null) { val actions = createActionsFromNotification(sbn) actionIcons = actions.first actionsToShowCollapsed = actions.second }
val playbackLocation = if (isRemoteCastNotification(sbn)) MediaData.PLAYBACK_CAST_REMOTE elseif ( mediaController.playbackInfo?.playbackType == MediaController.PlaybackInfo.PLAYBACK_TYPE_LOCAL ) MediaData.PLAYBACK_LOCAL else MediaData.PLAYBACK_CAST_LOCAL val isPlaying = mediaController.playbackState?.let { isPlayingState(it.state) } ?: null
val currentEntry = mediaEntries.get(key) val instanceId = currentEntry?.instanceId ?: logger.getNewInstanceId() val appUid = appInfo?.uid ?: Process.INVALID_UID
val lastActive = systemClock.elapsedRealtime() foregroundExecutor.execute { val resumeAction: Runnable? = mediaEntries[key]?.resumeAction val hasCheckedForResume = mediaEntries[key]?.hasCheckedForResume == true val active = mediaEntries[key]?.active ?: true onMediaDataLoaded( key, oldKey, MediaData(...) ) } }
privatefunupdateHostAttachment() = traceSection("MediaHierarchyManager#updateHostAttachment") { var newLocation = resolveLocationForFading() // Don't use the overlay when fading or when we don't have active media var canUseOverlay = !isCurrentlyFading() && hasActiveMediaOrRecommendation if (isCrossFadeAnimatorRunning) { if ( getHost(newLocation)?.visible == true && getHost(newLocation)?.hostView?.isShown == false && newLocation != desiredLocation ) { // We're crossfading but the view is already hidden. Let's move to the overlay // instead. This happens when animating to the full shade using a button click. canUseOverlay = true } } val inOverlay = isTransitionRunning() && rootOverlay != null && canUseOverlay newLocation = if (inOverlay) IN_OVERLAY else newLocation if (currentAttachmentLocation != newLocation) { currentAttachmentLocation = newLocation
// Remove the carousel from the old host (mediaFrame.parent as ViewGroup?)?.removeView(mediaFrame)
// Add it to the new one if (inOverlay) { rootOverlay!!.add(mediaFrame) } else { val targetHost = getHost(newLocation)!!.hostView // This will either do a full layout pass and remeasure, or it will bypass // that and directly set the mediaFrame's bounds within the premeasured host. targetHost.addView(mediaFrame) // 把 mediaFrame 添加到 hostView } if (isCrossFadeAnimatorRunning) { // When cross-fading with an animation, we only notify the media carousel of the // location change, once the view is reattached to the new place and not // immediately // when the desired location changes. This callback will update the measurement // of the carousel, only once we've faded out at the old location and then // reattach // to fade it in at the new location. mediaCarouselController.onDesiredLocationChanged( newLocation, getHost(newLocation), animate = false ) } } }
把 mediaFrame 添加到 hostView
MediaCarouselController
mediaFrame
1 2 3 4 5 6 7 8 9 10 11 12
mediaFrame = inflateMediaCarousel()
privatefuninflateMediaCarousel(): ViewGroup { val mediaCarousel = LayoutInflater.from(context) .inflate(R.layout.media_carousel, UniqueObjectHostView(context), false) as ViewGroup // Because this is inflated when not attached to the true view hierarchy, it resolves some // potential issues to force that the layout direction is defined by the locale // (rather than inherited from the parent, which would resolve to LTR when unattached). mediaCarousel.layoutDirection = View.LAYOUT_DIRECTION_LOCALE return mediaCarousel }
mediaManager.addListener( object : MediaDataManager.Listener { overridefunonMediaDataLoaded( key: String, oldKey: String?, data: MediaData, immediately: Boolean, receivedSmartspaceCardLatency: Int, isSsReactivated: Boolean ) { debugLogger.logMediaLoaded(key, data.active) if (addOrUpdatePlayer(key, oldKey, data, isSsReactivated)) { // Log card received if a new resumable media card is added MediaPlayerData.getMediaPlayer(key)?.let { /* ktlint-disable max-line-length */ logSmartspaceCardReported(...) /* ktlint-disable max-line-length */ } if ( mediaCarouselScrollHandler.visibleToUser && mediaCarouselScrollHandler.visibleMediaIndex == MediaPlayerData.getMediaPlayerIndex(key) ) { logSmartspaceImpression(mediaCarouselScrollHandler.qsExpanded) } } elseif (receivedSmartspaceCardLatency != 0) { // Log resume card received if resumable media card is reactivated and // resume card is ranked first MediaPlayerData.players().forEachIndexed { index, it -> if (it.recommendationViewHolder == null) { it.mSmartspaceId = SmallHash.hash( it.mUid + systemClock.currentTimeMillis().toInt() ) it.mIsImpressed = false /* ktlint-disable max-line-length */ logSmartspaceCardReported(...) /* ktlint-disable max-line-length */ } } // If media container area already visible to the user, log impression for // reactivated card. if ( mediaCarouselScrollHandler.visibleToUser && !mediaCarouselScrollHandler.qsExpanded ) { logSmartspaceImpression(mediaCarouselScrollHandler.qsExpanded) } }
val canRemove = data.isPlaying?.let { !it } ?: data.isClearable && !data.active if (canRemove && !Utils.useMediaResumption(context)) { // This view isn't playing, let's remove this! This happens e.g. when // dismissing/timing out a view. We still have the data around because // resumption could be on, but we should save the resources and release // this. if (isReorderingAllowed) { onMediaDataRemoved(key) } else { keysNeedRemoval.add(key) } } else { keysNeedRemoval.remove(key) } }
......
} ) }
在 onMediaDataLoaded() 回调中调用 addOrUpdatePlayer() 添加 player
privatefunreorderAllPlayers( previousVisiblePlayerKey: MediaPlayerData.MediaSortKey?, key: String? = null ) { mediaContent.removeAllViews() for (mediaPlayer in MediaPlayerData.players()) { mediaPlayer.mediaViewHolder?.let { mediaContent.addView(it.player) } ?: mediaPlayer.recommendationViewHolder?.let { mediaContent.addView(it.recommendations) } } mediaCarouselScrollHandler.onPlayersChanged() MediaPlayerData.updateVisibleMediaPlayers() // Automatically scroll to the active player if needed if (shouldScrollToKey) { shouldScrollToKey = false val mediaIndex = key?.let { MediaPlayerData.getMediaPlayerIndex(it) } ?: -1 if (mediaIndex != -1) { previousVisiblePlayerKey?.let { val previousVisibleIndex = MediaPlayerData.playerKeys().indexOfFirst { key -> it == key } mediaCarouselScrollHandler.scrollToPlayer(previousVisibleIndex, mediaIndex) } ?: mediaCarouselScrollHandler.scrollToPlayer(destIndex = mediaIndex) } } }
调用 mediaContent.addView(it.player)
MediaControlPanel
MediaViewHolder
1 2 3 4 5
/** Holder class for media player view */ classMediaViewHolderconstructor(itemView: View) { val player = itemView as TransitionLayout ......
create()
1 2 3 4 5 6 7 8 9 10 11 12 13
@JvmStatic funcreate(inflater: LayoutInflater, parent: ViewGroup): MediaViewHolder { val mediaView = inflater.inflate(R.layout.media_session_view, parent, false) mediaView.setLayerType(View.LAYER_TYPE_HARDWARE, null) // Because this media view (a TransitionLayout) is used to measure and layout the views // in various states before being attached to its parent, we can't depend on the default // LAYOUT_DIRECTION_INHERIT to correctly resolve the ltr direction. mediaView.layoutDirection = View.LAYOUT_DIRECTION_LOCALE return MediaViewHolder(mediaView).apply { // Media playback is in the direction of tape, not time, so it stays LTR seekBar.layoutDirection = View.LAYOUT_DIRECTION_LTR } }