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