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.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.domain.pipeline.MediaDataManager
38 import com.android.systemui.media.controls.domain.pipeline.RESUME_MEDIA_TIMEOUT
39 import com.android.systemui.media.controls.shared.model.MediaData
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 for ${browser.userId}: $desc")
126                 mediaDataManager.addResumptionControls(
127                     browser.userId,
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(
200             TAG,
201             "loaded resume components for $currentUserId: " +
202                 "${resumeComponents.toArray().contentToString()}"
203         )
204 
205         if (needsUpdate) {
206             // Save any missing times that we had to fill in
207             writeSharedPrefs()
208         }
209     }
210 
211     /** Load controls for resuming media, if available */
loadMediaResumptionControlsnull212     private fun loadMediaResumptionControls() {
213         if (!useMediaResumption) {
214             return
215         }
216 
217         val pm = context.packageManager
218         val now = systemClock.currentTimeMillis()
219         resumeComponents.forEach {
220             if (now.minus(it.second) <= RESUME_MEDIA_TIMEOUT) {
221                 // Verify that the service exists for this user
222                 val intent = Intent(MediaBrowserService.SERVICE_INTERFACE)
223                 intent.component = it.first
224                 val inf = pm.resolveServiceAsUser(intent, 0, currentUserId)
225                 if (inf != null) {
226                     val browser =
227                         mediaBrowserFactory.create(mediaBrowserCallback, it.first, currentUserId)
228                     browser.findRecentMedia()
229                 } else {
230                     Log.d(TAG, "User $currentUserId does not have component ${it.first}")
231                 }
232             }
233         }
234     }
235 
onMediaDataLoadednull236     override fun onMediaDataLoaded(
237         key: String,
238         oldKey: String?,
239         data: MediaData,
240         immediately: Boolean,
241         receivedSmartspaceCardLatency: Int,
242         isSsReactivated: Boolean
243     ) {
244         if (useMediaResumption) {
245             // If this had been started from a resume state, disconnect now that it's live
246             if (!key.equals(oldKey)) {
247                 mediaBrowser = null
248             }
249             // If we don't have a resume action, check if we haven't already
250             val isEligibleForResume =
251                 data.isLocalSession() ||
252                     (mediaFlags.isRemoteResumeAllowed() &&
253                         data.playbackLocation != MediaData.PLAYBACK_CAST_REMOTE)
254             if (data.resumeAction == null && !data.hasCheckedForResume && isEligibleForResume) {
255                 // TODO also check for a media button receiver intended for restarting (b/154127084)
256                 // Set null action to prevent additional attempts to connect
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                     backgroundExecutor.execute {
266                         tryUpdateResumptionList(key, inf!!.get(0).componentInfo.componentName)
267                     }
268                 }
269             }
270         }
271     }
272 
273     /**
274      * Verify that we can connect to the given component with a MediaBrowser, and if so, add that
275      * component to the list of resumption components
276      */
tryUpdateResumptionListnull277     private fun tryUpdateResumptionList(key: String, componentName: ComponentName) {
278         Log.d(TAG, "Testing if we can connect to $componentName")
279         mediaBrowser =
280             mediaBrowserFactory.create(
281                 object : ResumeMediaBrowser.Callback() {
282                     override fun onConnected() {
283                         Log.d(TAG, "Connected to $componentName")
284                     }
285 
286                     override fun onError() {
287                         Log.e(TAG, "Cannot resume with $componentName")
288                         mediaBrowser = null
289                     }
290 
291                     override fun addTrack(
292                         desc: MediaDescription,
293                         component: ComponentName,
294                         browser: ResumeMediaBrowser
295                     ) {
296                         // Since this is a test, just save the component for later
297                         Log.d(
298                             TAG,
299                             "Can get resumable media for ${browser.userId} from $componentName"
300                         )
301                         mediaDataManager.setResumeAction(key, getResumeAction(componentName))
302                         updateResumptionList(componentName)
303                         mediaBrowser = null
304                     }
305                 },
306                 componentName,
307                 currentUserId
308             )
309         mediaBrowser?.testConnection()
310     }
311 
312     /**
313      * Add the component to the saved list of media browser services, checking for duplicates and
314      * removing older components that exceed the maximum limit
315      *
316      * @param componentName
317      */
updateResumptionListnull318     private fun updateResumptionList(componentName: ComponentName) {
319         // Remove if exists
320         resumeComponents.remove(resumeComponents.find { it.first.equals(componentName) })
321         // Insert at front of queue
322         val currentTime = systemClock.currentTimeMillis()
323         resumeComponents.add(componentName to currentTime)
324         // Remove old components if over the limit
325         if (resumeComponents.size > ResumeMediaBrowser.MAX_RESUMPTION_CONTROLS) {
326             resumeComponents.remove()
327         }
328 
329         writeSharedPrefs()
330     }
331 
writeSharedPrefsnull332     private fun writeSharedPrefs() {
333         val sb = StringBuilder()
334         resumeComponents.forEach {
335             sb.append(it.first.flattenToString())
336             sb.append("/")
337             sb.append(it.second)
338             sb.append(ResumeMediaBrowser.DELIMITER)
339         }
340         val prefs = context.getSharedPreferences(MEDIA_PREFERENCES, Context.MODE_PRIVATE)
341         prefs.edit().putString(MEDIA_PREFERENCE_KEY + currentUserId, sb.toString()).apply()
342     }
343 
344     /** Get a runnable which will resume media playback */
getResumeActionnull345     private fun getResumeAction(componentName: ComponentName): Runnable {
346         return Runnable {
347             mediaBrowser = mediaBrowserFactory.create(null, componentName, currentUserId)
348             mediaBrowser?.restart()
349         }
350     }
351 
dumpnull352     override fun dump(pw: PrintWriter, args: Array<out String>) {
353         pw.apply { println("resumeComponents: $resumeComponents") }
354     }
355 }
356