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