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