| /* |
| * Copyright (C) 2020 The Android Open Source Project |
| * |
| * Licensed under the Apache License, Version 2.0 (the "License"); |
| * you may not use this file except in compliance with the License. |
| * You may obtain a copy of the License at |
| * |
| * http://www.apache.org/licenses/LICENSE-2.0 |
| * |
| * Unless required by applicable law or agreed to in writing, software |
| * distributed under the License is distributed on an "AS IS" BASIS, |
| * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. |
| * See the License for the specific language governing permissions and |
| * limitations under the License. |
| */ |
| |
| package com.android.systemui.media |
| |
| import android.content.ComponentName |
| import android.content.Context |
| import android.media.session.MediaController |
| import android.media.session.MediaController.PlaybackInfo |
| import android.media.session.MediaSession |
| import android.media.session.MediaSessionManager |
| import android.util.Log |
| import com.android.systemui.dagger.qualifiers.Background |
| import com.android.systemui.dagger.qualifiers.Main |
| import com.android.systemui.statusbar.phone.NotificationListenerWithPlugins |
| import java.util.concurrent.Executor |
| import javax.inject.Inject |
| |
| private const val TAG = "MediaSessionBasedFilter" |
| |
| /** |
| * Filters media loaded events for local media sessions while an app is casting. |
| * |
| * When an app is casting there can be one remote media sessions and potentially more local media |
| * sessions. In this situation, there should only be a media object for the remote session. To |
| * achieve this, update events for the local session need to be filtered. |
| */ |
| class MediaSessionBasedFilter @Inject constructor( |
| context: Context, |
| private val sessionManager: MediaSessionManager, |
| @Main private val foregroundExecutor: Executor, |
| @Background private val backgroundExecutor: Executor |
| ) : MediaDataManager.Listener { |
| |
| private val listeners: MutableSet<MediaDataManager.Listener> = mutableSetOf() |
| |
| // Keep track of MediaControllers for a given package to check if an app is casting and it |
| // filter loaded events for local sessions. |
| private val packageControllers: LinkedHashMap<String, MutableList<MediaController>> = |
| LinkedHashMap() |
| |
| // Keep track of the key used for the session tokens. This information is used to know when to |
| // dispatch a removed event so that a media object for a local session will be removed. |
| private val keyedTokens: MutableMap<String, MutableSet<MediaSession.Token>> = mutableMapOf() |
| |
| // Keep track of which media session tokens have associated notifications. |
| private val tokensWithNotifications: MutableSet<MediaSession.Token> = mutableSetOf() |
| |
| private val sessionListener = object : MediaSessionManager.OnActiveSessionsChangedListener { |
| override fun onActiveSessionsChanged(controllers: List<MediaController>) { |
| handleControllersChanged(controllers) |
| } |
| } |
| |
| init { |
| backgroundExecutor.execute { |
| val name = ComponentName(context, NotificationListenerWithPlugins::class.java) |
| sessionManager.addOnActiveSessionsChangedListener(sessionListener, name) |
| handleControllersChanged(sessionManager.getActiveSessions(name)) |
| } |
| } |
| |
| /** |
| * Add a listener for filtered [MediaData] changes |
| */ |
| fun addListener(listener: MediaDataManager.Listener) = listeners.add(listener) |
| |
| /** |
| * Remove a listener that was registered with addListener |
| */ |
| fun removeListener(listener: MediaDataManager.Listener) = listeners.remove(listener) |
| |
| /** |
| * May filter loaded events by not passing them along to listeners. |
| * |
| * If an app has only one session with playback type PLAYBACK_TYPE_REMOTE, then assuming that |
| * the app is casting. Sometimes apps will send redundant updates to a local session with |
| * playback type PLAYBACK_TYPE_LOCAL. These updates should be filtered to improve the usability |
| * of the media controls. |
| */ |
| override fun onMediaDataLoaded(key: String, oldKey: String?, info: MediaData) { |
| backgroundExecutor.execute { |
| info.token?.let { |
| tokensWithNotifications.add(it) |
| } |
| val isMigration = oldKey != null && key != oldKey |
| if (isMigration) { |
| keyedTokens.remove(oldKey)?.let { removed -> keyedTokens.put(key, removed) } |
| } |
| if (info.token != null) { |
| keyedTokens.get(key)?.let { |
| tokens -> |
| tokens.add(info.token) |
| } ?: run { |
| val tokens = mutableSetOf(info.token) |
| keyedTokens.put(key, tokens) |
| } |
| } |
| // Determine if an app is casting by checking if it has a session with playback type |
| // PLAYBACK_TYPE_REMOTE. |
| val remoteControllers = packageControllers.get(info.packageName)?.filter { |
| it.playbackInfo?.playbackType == PlaybackInfo.PLAYBACK_TYPE_REMOTE |
| } |
| // Limiting search to only apps with a single remote session. |
| val remote = if (remoteControllers?.size == 1) remoteControllers.firstOrNull() else null |
| if (isMigration || remote == null || remote.sessionToken == info.token || |
| !tokensWithNotifications.contains(remote.sessionToken)) { |
| // Not filtering in this case. Passing the event along to listeners. |
| dispatchMediaDataLoaded(key, oldKey, info) |
| } else { |
| // Filtering this event because the app is casting and the loaded events is for a |
| // local session. |
| Log.d(TAG, "filtering key=$key local=${info.token} remote=${remote?.sessionToken}") |
| // If the local session uses a different notification key, then lets go a step |
| // farther and dismiss the media data so that media controls for the local session |
| // don't hang around while casting. |
| if (!keyedTokens.get(key)!!.contains(remote.sessionToken)) { |
| dispatchMediaDataRemoved(key) |
| } |
| } |
| } |
| } |
| |
| override fun onMediaDataRemoved(key: String) { |
| // Queue on background thread to ensure ordering of loaded and removed events is maintained. |
| backgroundExecutor.execute { |
| keyedTokens.remove(key) |
| dispatchMediaDataRemoved(key) |
| } |
| } |
| |
| private fun dispatchMediaDataLoaded(key: String, oldKey: String?, info: MediaData) { |
| foregroundExecutor.execute { |
| listeners.toSet().forEach { it.onMediaDataLoaded(key, oldKey, info) } |
| } |
| } |
| |
| private fun dispatchMediaDataRemoved(key: String) { |
| foregroundExecutor.execute { |
| listeners.toSet().forEach { it.onMediaDataRemoved(key) } |
| } |
| } |
| |
| private fun handleControllersChanged(controllers: List<MediaController>) { |
| packageControllers.clear() |
| controllers.forEach { |
| controller -> |
| packageControllers.get(controller.packageName)?.let { |
| tokens -> |
| tokens.add(controller) |
| } ?: run { |
| val tokens = mutableListOf(controller) |
| packageControllers.put(controller.packageName, tokens) |
| } |
| } |
| tokensWithNotifications.retainAll(controllers.map { it.sessionToken }) |
| } |
| } |