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 }