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