• Home
  • Line#
  • Scopes#
  • Navigate#
  • Raw
  • Download
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.controls.domain.resume
18 
19 import android.annotation.WorkerThread
20 import android.content.BroadcastReceiver
21 import android.content.ComponentName
22 import android.content.Context
23 import android.content.Intent
24 import android.content.IntentFilter
25 import android.content.pm.PackageManager
26 import android.media.MediaDescription
27 import android.os.UserHandle
28 import android.provider.Settings
29 import android.service.media.MediaBrowserService
30 import android.util.Log
31 import com.android.internal.annotations.VisibleForTesting
32 import com.android.systemui.Dumpable
33 import com.android.systemui.broadcast.BroadcastDispatcher
34 import com.android.systemui.dagger.SysUISingleton
35 import com.android.systemui.dagger.qualifiers.Background
36 import com.android.systemui.dagger.qualifiers.Main
37 import com.android.systemui.dump.DumpManager
38 import com.android.systemui.media.controls.domain.pipeline.MediaDataManager
39 import com.android.systemui.media.controls.domain.pipeline.RESUME_MEDIA_TIMEOUT
40 import com.android.systemui.media.controls.shared.model.MediaData
41 import com.android.systemui.settings.UserTracker
42 import com.android.systemui.tuner.TunerService
43 import com.android.systemui.util.Utils
44 import com.android.systemui.util.kotlin.logD
45 import com.android.systemui.util.time.SystemClock
46 import java.io.PrintWriter
47 import java.util.concurrent.ConcurrentLinkedQueue
48 import java.util.concurrent.Executor
49 import javax.inject.Inject
50 
51 private const val TAG = "MediaResumeListener"
52 
53 private const val MEDIA_PREFERENCES = "media_control_prefs"
54 private const val MEDIA_PREFERENCE_KEY = "browser_components_"
55 
56 @SysUISingleton
57 class MediaResumeListener
58 @Inject
59 constructor(
60     private val context: Context,
61     private val broadcastDispatcher: BroadcastDispatcher,
62     private val userTracker: UserTracker,
63     @Main private val mainExecutor: Executor,
64     @Background private val backgroundExecutor: Executor,
65     private val tunerService: TunerService,
66     private val mediaBrowserFactory: ResumeMediaBrowserFactory,
67     dumpManager: DumpManager,
68     private val systemClock: SystemClock,
69 ) : MediaDataManager.Listener, Dumpable {
70 
71     private var useMediaResumption: Boolean = Utils.useMediaResumption(context)
72     private val resumeComponents: ConcurrentLinkedQueue<Pair<ComponentName, Long>> =
73         ConcurrentLinkedQueue()
74 
75     private lateinit var mediaDataManager: MediaDataManager
76 
77     private var mediaBrowser: ResumeMediaBrowser? = null
78         set(value) {
79             // Always disconnect the old browser -- see b/225403871.
80             field?.disconnect()
81             field = value
82         }
83 
84     private var currentUserId: Int = context.userId
85 
86     @VisibleForTesting
87     val userUnlockReceiver =
88         object : BroadcastReceiver() {
89             @WorkerThread
onReceivenull90             override fun onReceive(context: Context, intent: Intent) {
91                 if (Intent.ACTION_USER_UNLOCKED == intent.action) {
92                     val userId = intent.getIntExtra(Intent.EXTRA_USER_HANDLE, -1)
93                     if (userId == currentUserId) {
94                         loadMediaResumptionControls()
95                     }
96                 }
97             }
98         }
99 
100     private val userTrackerCallback =
101         object : UserTracker.Callback {
onUserChangednull102             override fun onUserChanged(newUser: Int, userContext: Context) {
103                 currentUserId = newUser
104                 loadSavedComponents()
105             }
106         }
107 
108     private val mediaBrowserCallback =
109         object : ResumeMediaBrowser.Callback() {
addTracknull110             override fun addTrack(
111                 desc: MediaDescription,
112                 component: ComponentName,
113                 browser: ResumeMediaBrowser,
114             ) {
115                 val token = browser.token
116                 val appIntent = browser.appIntent
117                 val pm = context.getPackageManager()
118                 var appName: CharSequence = component.packageName
119                 val resumeAction = getResumeAction(component)
120                 try {
121                     appName =
122                         pm.getApplicationLabel(pm.getApplicationInfo(component.packageName, 0))
123                 } catch (e: PackageManager.NameNotFoundException) {
124                     Log.e(TAG, "Error getting package information", e)
125                 }
126 
127                 logD(TAG) { "Adding resume controls for ${browser.userId}: $desc" }
128                 mediaDataManager.addResumptionControls(
129                     browser.userId,
130                     desc,
131                     resumeAction,
132                     token,
133                     appName.toString(),
134                     appIntent,
135                     component.packageName,
136                 )
137             }
138         }
139 
140     init {
141         if (useMediaResumption) {
142             dumpManager.registerNormalDumpable(TAG, this)
143             val unlockFilter = IntentFilter()
144             unlockFilter.addAction(Intent.ACTION_USER_UNLOCKED)
145             broadcastDispatcher.registerReceiver(
146                 userUnlockReceiver,
147                 unlockFilter,
148                 backgroundExecutor,
149                 UserHandle.ALL,
150             )
151             userTracker.addCallback(userTrackerCallback, mainExecutor)
152             loadSavedComponents()
153         }
154     }
155 
setManagernull156     fun setManager(manager: MediaDataManager) {
157         mediaDataManager = manager
158 
159         // Add listener for resumption setting changes
160         tunerService.addTunable(
161             object : TunerService.Tunable {
162                 override fun onTuningChanged(key: String?, newValue: String?) {
163                     useMediaResumption = Utils.useMediaResumption(context)
164                     mediaDataManager.setMediaResumptionEnabled(useMediaResumption)
165                 }
166             },
167             Settings.Secure.MEDIA_CONTROLS_RESUME,
168         )
169     }
170 
loadSavedComponentsnull171     private fun loadSavedComponents() {
172         // Make sure list is empty (if we switched users)
173         resumeComponents.clear()
174         val prefs = context.getSharedPreferences(MEDIA_PREFERENCES, Context.MODE_PRIVATE)
175         val listString = prefs.getString(MEDIA_PREFERENCE_KEY + currentUserId, null)
176         val components =
177             listString?.split(ResumeMediaBrowser.DELIMITER.toRegex())?.dropLastWhile {
178                 it.isEmpty()
179             }
180         var needsUpdate = false
181         components?.forEach {
182             val info = it.split("/")
183             val packageName = info[0]
184             val className = info[1]
185             val component = ComponentName(packageName, className)
186 
187             val lastPlayed =
188                 if (info.size == 3) {
189                     try {
190                         info[2].toLong()
191                     } catch (e: NumberFormatException) {
192                         needsUpdate = true
193                         systemClock.currentTimeMillis()
194                     }
195                 } else {
196                     needsUpdate = true
197                     systemClock.currentTimeMillis()
198                 }
199             resumeComponents.add(component to lastPlayed)
200         }
201 
202         logD(TAG) {
203             "loaded resume components for $currentUserId: " +
204                 resumeComponents.toArray().contentToString()
205         }
206 
207         if (needsUpdate) {
208             // Save any missing times that we had to fill in
209             writeSharedPrefs()
210         }
211     }
212 
213     /** Load controls for resuming media, if available */
loadMediaResumptionControlsnull214     private fun loadMediaResumptionControls() {
215         if (!useMediaResumption) {
216             return
217         }
218 
219         val pm = context.packageManager
220         val now = systemClock.currentTimeMillis()
221         resumeComponents.forEach {
222             if (now.minus(it.second) <= RESUME_MEDIA_TIMEOUT) {
223                 // Verify that the service exists for this user
224                 val intent = Intent(MediaBrowserService.SERVICE_INTERFACE)
225                 intent.component = it.first
226                 val inf = pm.resolveServiceAsUser(intent, 0, currentUserId)
227                 if (inf != null) {
228                     val browser =
229                         mediaBrowserFactory.create(mediaBrowserCallback, it.first, currentUserId)
230                     browser.findRecentMedia()
231                 } else {
232                     logD(TAG) { "User $currentUserId does not have component ${it.first}" }
233                 }
234             }
235         }
236     }
237 
onMediaDataLoadednull238     override fun onMediaDataLoaded(
239         key: String,
240         oldKey: String?,
241         data: MediaData,
242         immediately: Boolean,
243         receivedSmartspaceCardLatency: Int,
244         isSsReactivated: Boolean,
245     ) {
246         if (useMediaResumption) {
247             // If this had been started from a resume state, disconnect now that it's live
248             if (!key.equals(oldKey)) {
249                 mediaBrowser = null
250             }
251             // If we don't have a resume action, check if we haven't already
252             val isEligibleForResume = data.isLocalSession()
253             if (data.resumeAction == null && !data.hasCheckedForResume && isEligibleForResume) {
254                 // TODO also check for a media button receiver intended for restarting (b/154127084)
255                 // Set null action to prevent additional attempts to connect
256                 backgroundExecutor.execute {
257                     mediaDataManager.setResumeAction(key, null)
258                     Log.d(TAG, "Checking for service component for " + data.packageName)
259                     val pm = context.packageManager
260                     val serviceIntent = Intent(MediaBrowserService.SERVICE_INTERFACE)
261                     val resumeInfo = pm.queryIntentServicesAsUser(serviceIntent, 0, currentUserId)
262 
263                     val inf = resumeInfo?.filter { it.serviceInfo.packageName == data.packageName }
264                     if (inf != null && inf.size > 0) {
265                         tryUpdateResumptionList(key, inf!!.get(0).componentInfo.componentName)
266                     }
267                 }
268             }
269         }
270     }
271 
272     /**
273      * Verify that we can connect to the given component with a MediaBrowser, and if so, add that
274      * component to the list of resumption components
275      */
tryUpdateResumptionListnull276     private fun tryUpdateResumptionList(key: String, componentName: ComponentName) {
277         Log.d(TAG, "Testing if we can connect to $componentName")
278         mediaBrowser =
279             mediaBrowserFactory.create(
280                 object : ResumeMediaBrowser.Callback() {
281                     override fun onConnected() {
282                         logD(TAG) { "Connected to $componentName" }
283                     }
284 
285                     override fun onError() {
286                         Log.e(TAG, "Cannot resume with $componentName")
287                         mediaBrowser = null
288                     }
289 
290                     override fun addTrack(
291                         desc: MediaDescription,
292                         component: ComponentName,
293                         browser: ResumeMediaBrowser,
294                     ) {
295                         // Since this is a test, just save the component for later
296                         logD(TAG) {
297                             "Can get resumable media for ${browser.userId} from $componentName"
298                         }
299 
300                         mediaDataManager.setResumeAction(key, getResumeAction(componentName))
301                         updateResumptionList(componentName)
302                         mediaBrowser = null
303                     }
304                 },
305                 componentName,
306                 currentUserId,
307             )
308         mediaBrowser?.testConnection()
309     }
310 
311     /**
312      * Add the component to the saved list of media browser services, checking for duplicates and
313      * removing older components that exceed the maximum limit
314      *
315      * @param componentName
316      */
updateResumptionListnull317     private fun updateResumptionList(componentName: ComponentName) {
318         // Remove if exists
319         resumeComponents.remove(resumeComponents.find { it.first.equals(componentName) })
320         // Insert at front of queue
321         val currentTime = systemClock.currentTimeMillis()
322         resumeComponents.add(componentName to currentTime)
323         // Remove old components if over the limit
324         if (resumeComponents.size > ResumeMediaBrowser.MAX_RESUMPTION_CONTROLS) {
325             resumeComponents.remove()
326         }
327 
328         writeSharedPrefs()
329     }
330 
writeSharedPrefsnull331     private fun writeSharedPrefs() {
332         val sb = StringBuilder()
333         resumeComponents.forEach {
334             sb.append(it.first.flattenToString())
335             sb.append("/")
336             sb.append(it.second)
337             sb.append(ResumeMediaBrowser.DELIMITER)
338         }
339         val prefs = context.getSharedPreferences(MEDIA_PREFERENCES, Context.MODE_PRIVATE)
340         prefs.edit().putString(MEDIA_PREFERENCE_KEY + currentUserId, sb.toString()).apply()
341     }
342 
343     /** Get a runnable which will resume media playback */
getResumeActionnull344     private fun getResumeAction(componentName: ComponentName): Runnable {
345         return Runnable {
346             mediaBrowser = mediaBrowserFactory.create(null, componentName, currentUserId)
347             mediaBrowser?.restart()
348         }
349     }
350 
dumpnull351     override fun dump(pw: PrintWriter, args: Array<out String>) {
352         pw.apply { println("resumeComponents: $resumeComponents") }
353     }
354 }
355