• Home
  • Line#
  • Scopes#
  • Navigate#
  • Raw
  • Download
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
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         isSsReactivated: Boolean
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     ) {
148         backgroundExecutor.execute {
149             dispatchSmartspaceMediaDataLoaded(key, data)
150         }
151     }
152 
153     override fun onMediaDataRemoved(key: String) {
154         // Queue on background thread to ensure ordering of loaded and removed events is maintained.
155         backgroundExecutor.execute {
156             keyedTokens.remove(key)
157             dispatchMediaDataRemoved(key)
158         }
159     }
160 
161     override fun onSmartspaceMediaDataRemoved(key: String, immediately: Boolean) {
162         backgroundExecutor.execute {
163             dispatchSmartspaceMediaDataRemoved(key, immediately)
164         }
165     }
166 
167     private fun dispatchMediaDataLoaded(
168         key: String,
169         oldKey: String?,
170         info: MediaData,
171         immediately: Boolean
172     ) {
173         foregroundExecutor.execute {
174             listeners.toSet().forEach { it.onMediaDataLoaded(key, oldKey, info, immediately) }
175         }
176     }
177 
178     private fun dispatchMediaDataRemoved(key: String) {
179         foregroundExecutor.execute {
180             listeners.toSet().forEach { it.onMediaDataRemoved(key) }
181         }
182     }
183 
184     private fun dispatchSmartspaceMediaDataLoaded(key: String, info: SmartspaceMediaData) {
185         foregroundExecutor.execute {
186             listeners.toSet().forEach { it.onSmartspaceMediaDataLoaded(key, info) }
187         }
188     }
189 
190     private fun dispatchSmartspaceMediaDataRemoved(key: String, immediately: Boolean) {
191         foregroundExecutor.execute {
192             listeners.toSet().forEach { it.onSmartspaceMediaDataRemoved(key, immediately) }
193         }
194     }
195 
196     private fun handleControllersChanged(controllers: List<MediaController>) {
197         packageControllers.clear()
198         controllers.forEach {
199             controller ->
200             packageControllers.get(controller.packageName)?.let {
201                 tokens ->
202                 tokens.add(controller)
203             } ?: run {
204                 val tokens = mutableListOf(controller)
205                 packageControllers.put(controller.packageName, tokens)
206             }
207         }
208         tokensWithNotifications.retainAll(controllers.map { it.sessionToken })
209     }
210 }
211