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