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