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