1 /* 2 * 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 18 19 import android.content.ComponentName 20 import android.content.Context 21 import android.media.session.MediaController 22 import android.media.session.MediaController.PlaybackInfo 23 import android.media.session.MediaSession 24 import android.media.session.MediaSessionManager 25 import android.util.Log 26 import com.android.systemui.dagger.qualifiers.Background 27 import com.android.systemui.dagger.qualifiers.Main 28 import com.android.systemui.statusbar.phone.NotificationListenerWithPlugins 29 import java.util.concurrent.Executor 30 import javax.inject.Inject 31 32 private const val TAG = "MediaSessionBasedFilter" 33 34 /** 35 * Filters media loaded events for local media sessions while an app is casting. 36 * 37 * When an app is casting there can be one remote media sessions and potentially more local media 38 * sessions. In this situation, there should only be a media object for the remote session. To 39 * achieve this, update events for the local session need to be filtered. 40 */ 41 class MediaSessionBasedFilter @Inject constructor( 42 context: Context, 43 private val sessionManager: MediaSessionManager, 44 @Main private val foregroundExecutor: Executor, 45 @Background private val backgroundExecutor: Executor 46 ) : MediaDataManager.Listener { 47 48 private val listeners: MutableSet<MediaDataManager.Listener> = mutableSetOf() 49 50 // Keep track of MediaControllers for a given package to check if an app is casting and it 51 // filter loaded events for local sessions. 52 private val packageControllers: LinkedHashMap<String, MutableList<MediaController>> = 53 LinkedHashMap() 54 55 // Keep track of the key used for the session tokens. This information is used to know when to 56 // dispatch a removed event so that a media object for a local session will be removed. 57 private val keyedTokens: MutableMap<String, MutableSet<MediaSession.Token>> = mutableMapOf() 58 59 // Keep track of which media session tokens have associated notifications. 60 private val tokensWithNotifications: MutableSet<MediaSession.Token> = mutableSetOf() 61 62 private val sessionListener = object : MediaSessionManager.OnActiveSessionsChangedListener { 63 override fun onActiveSessionsChanged(controllers: List<MediaController>) { 64 handleControllersChanged(controllers) 65 } 66 } 67 68 init { 69 backgroundExecutor.execute { 70 val name = ComponentName(context, NotificationListenerWithPlugins::class.java) 71 sessionManager.addOnActiveSessionsChangedListener(sessionListener, name) 72 handleControllersChanged(sessionManager.getActiveSessions(name)) 73 } 74 } 75 76 /** 77 * Add a listener for filtered [MediaData] changes 78 */ 79 fun addListener(listener: MediaDataManager.Listener) = listeners.add(listener) 80 81 /** 82 * Remove a listener that was registered with addListener 83 */ 84 fun removeListener(listener: MediaDataManager.Listener) = listeners.remove(listener) 85 86 /** 87 * May filter loaded events by not passing them along to listeners. 88 * 89 * If an app has only one session with playback type PLAYBACK_TYPE_REMOTE, then assuming that 90 * the app is casting. Sometimes apps will send redundant updates to a local session with 91 * playback type PLAYBACK_TYPE_LOCAL. These updates should be filtered to improve the usability 92 * of the media controls. 93 */ 94 override fun onMediaDataLoaded( 95 key: String, 96 oldKey: String?, 97 data: MediaData, 98 immediately: Boolean, 99 receivedSmartspaceCardLatency: Int 100 ) { 101 backgroundExecutor.execute { 102 data.token?.let { 103 tokensWithNotifications.add(it) 104 } 105 val isMigration = oldKey != null && key != oldKey 106 if (isMigration) { 107 keyedTokens.remove(oldKey)?.let { removed -> keyedTokens.put(key, removed) } 108 } 109 if (data.token != null) { 110 keyedTokens.get(key)?.let { 111 tokens -> 112 tokens.add(data.token) 113 } ?: run { 114 val tokens = mutableSetOf(data.token) 115 keyedTokens.put(key, tokens) 116 } 117 } 118 // Determine if an app is casting by checking if it has a session with playback type 119 // PLAYBACK_TYPE_REMOTE. 120 val remoteControllers = packageControllers.get(data.packageName)?.filter { 121 it.playbackInfo?.playbackType == PlaybackInfo.PLAYBACK_TYPE_REMOTE 122 } 123 // Limiting search to only apps with a single remote session. 124 val remote = if (remoteControllers?.size == 1) remoteControllers.firstOrNull() else null 125 if (isMigration || remote == null || remote.sessionToken == data.token || 126 !tokensWithNotifications.contains(remote.sessionToken)) { 127 // Not filtering in this case. Passing the event along to listeners. 128 dispatchMediaDataLoaded(key, oldKey, data, immediately) 129 } else { 130 // Filtering this event because the app is casting and the loaded events is for a 131 // local session. 132 Log.d(TAG, "filtering key=$key local=${data.token} remote=${remote?.sessionToken}") 133 // If the local session uses a different notification key, then lets go a step 134 // farther and dismiss the media data so that media controls for the local session 135 // don't hang around while casting. 136 if (!keyedTokens.get(key)!!.contains(remote.sessionToken)) { 137 dispatchMediaDataRemoved(key) 138 } 139 } 140 } 141 } 142 143 override fun onSmartspaceMediaDataLoaded( 144 key: String, 145 data: SmartspaceMediaData, 146 shouldPrioritize: Boolean, 147 isSsReactivated: Boolean 148 ) { 149 backgroundExecutor.execute { 150 dispatchSmartspaceMediaDataLoaded(key, data) 151 } 152 } 153 154 override fun onMediaDataRemoved(key: String) { 155 // Queue on background thread to ensure ordering of loaded and removed events is maintained. 156 backgroundExecutor.execute { 157 keyedTokens.remove(key) 158 dispatchMediaDataRemoved(key) 159 } 160 } 161 162 override fun onSmartspaceMediaDataRemoved(key: String, immediately: Boolean) { 163 backgroundExecutor.execute { 164 dispatchSmartspaceMediaDataRemoved(key, immediately) 165 } 166 } 167 168 private fun dispatchMediaDataLoaded( 169 key: String, 170 oldKey: String?, 171 info: MediaData, 172 immediately: Boolean 173 ) { 174 foregroundExecutor.execute { 175 listeners.toSet().forEach { it.onMediaDataLoaded(key, oldKey, info, immediately) } 176 } 177 } 178 179 private fun dispatchMediaDataRemoved(key: String) { 180 foregroundExecutor.execute { 181 listeners.toSet().forEach { it.onMediaDataRemoved(key) } 182 } 183 } 184 185 private fun dispatchSmartspaceMediaDataLoaded(key: String, info: SmartspaceMediaData) { 186 foregroundExecutor.execute { 187 listeners.toSet().forEach { it.onSmartspaceMediaDataLoaded(key, info) } 188 } 189 } 190 191 private fun dispatchSmartspaceMediaDataRemoved(key: String, immediately: Boolean) { 192 foregroundExecutor.execute { 193 listeners.toSet().forEach { it.onSmartspaceMediaDataRemoved(key, immediately) } 194 } 195 } 196 197 private fun handleControllersChanged(controllers: List<MediaController>) { 198 packageControllers.clear() 199 controllers.forEach { 200 controller -> 201 packageControllers.get(controller.packageName)?.let { 202 tokens -> 203 tokens.add(controller) 204 } ?: run { 205 val tokens = mutableListOf(controller) 206 packageControllers.put(controller.packageName, tokens) 207 } 208 } 209 tokensWithNotifications.retainAll(controllers.map { it.sessionToken }) 210 } 211 } 212