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