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