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