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.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.models.player.MediaData 38 import com.android.systemui.media.controls.pipeline.MediaDataManager 39 import com.android.systemui.media.controls.pipeline.RESUME_MEDIA_TIMEOUT 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 $desc") 126 mediaDataManager.addResumptionControls( 127 currentUserId, 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(TAG, "loaded resume components ${resumeComponents.toArray().contentToString()}") 200 201 if (needsUpdate) { 202 // Save any missing times that we had to fill in 203 writeSharedPrefs() 204 } 205 } 206 207 /** Load controls for resuming media, if available */ loadMediaResumptionControlsnull208 private fun loadMediaResumptionControls() { 209 if (!useMediaResumption) { 210 return 211 } 212 213 val now = systemClock.currentTimeMillis() 214 resumeComponents.forEach { 215 if (now.minus(it.second) <= RESUME_MEDIA_TIMEOUT) { 216 val browser = mediaBrowserFactory.create(mediaBrowserCallback, it.first) 217 browser.findRecentMedia() 218 } 219 } 220 } 221 onMediaDataLoadednull222 override fun onMediaDataLoaded( 223 key: String, 224 oldKey: String?, 225 data: MediaData, 226 immediately: Boolean, 227 receivedSmartspaceCardLatency: Int, 228 isSsReactivated: Boolean 229 ) { 230 if (useMediaResumption) { 231 // If this had been started from a resume state, disconnect now that it's live 232 if (!key.equals(oldKey)) { 233 mediaBrowser = null 234 } 235 // If we don't have a resume action, check if we haven't already 236 val isEligibleForResume = 237 data.isLocalSession() || 238 (mediaFlags.isRemoteResumeAllowed() && 239 data.playbackLocation != MediaData.PLAYBACK_CAST_REMOTE) 240 if (data.resumeAction == null && !data.hasCheckedForResume && isEligibleForResume) { 241 // TODO also check for a media button receiver intended for restarting (b/154127084) 242 // Set null action to prevent additional attempts to connect 243 mediaDataManager.setResumeAction(key, null) 244 Log.d(TAG, "Checking for service component for " + data.packageName) 245 val pm = context.packageManager 246 val serviceIntent = Intent(MediaBrowserService.SERVICE_INTERFACE) 247 val resumeInfo = pm.queryIntentServices(serviceIntent, 0) 248 249 val inf = resumeInfo?.filter { it.serviceInfo.packageName == data.packageName } 250 if (inf != null && inf.size > 0) { 251 backgroundExecutor.execute { 252 tryUpdateResumptionList(key, inf!!.get(0).componentInfo.componentName) 253 } 254 } 255 } 256 } 257 } 258 259 /** 260 * Verify that we can connect to the given component with a MediaBrowser, and if so, add that 261 * component to the list of resumption components 262 */ tryUpdateResumptionListnull263 private fun tryUpdateResumptionList(key: String, componentName: ComponentName) { 264 Log.d(TAG, "Testing if we can connect to $componentName") 265 mediaBrowser = 266 mediaBrowserFactory.create( 267 object : ResumeMediaBrowser.Callback() { 268 override fun onConnected() { 269 Log.d(TAG, "Connected to $componentName") 270 } 271 272 override fun onError() { 273 Log.e(TAG, "Cannot resume with $componentName") 274 mediaBrowser = null 275 } 276 277 override fun addTrack( 278 desc: MediaDescription, 279 component: ComponentName, 280 browser: ResumeMediaBrowser 281 ) { 282 // Since this is a test, just save the component for later 283 Log.d(TAG, "Can get resumable media from $componentName") 284 mediaDataManager.setResumeAction(key, getResumeAction(componentName)) 285 updateResumptionList(componentName) 286 mediaBrowser = null 287 } 288 }, 289 componentName 290 ) 291 mediaBrowser?.testConnection() 292 } 293 294 /** 295 * Add the component to the saved list of media browser services, checking for duplicates and 296 * removing older components that exceed the maximum limit 297 * 298 * @param componentName 299 */ updateResumptionListnull300 private fun updateResumptionList(componentName: ComponentName) { 301 // Remove if exists 302 resumeComponents.remove(resumeComponents.find { it.first.equals(componentName) }) 303 // Insert at front of queue 304 val currentTime = systemClock.currentTimeMillis() 305 resumeComponents.add(componentName to currentTime) 306 // Remove old components if over the limit 307 if (resumeComponents.size > ResumeMediaBrowser.MAX_RESUMPTION_CONTROLS) { 308 resumeComponents.remove() 309 } 310 311 writeSharedPrefs() 312 } 313 writeSharedPrefsnull314 private fun writeSharedPrefs() { 315 val sb = StringBuilder() 316 resumeComponents.forEach { 317 sb.append(it.first.flattenToString()) 318 sb.append("/") 319 sb.append(it.second) 320 sb.append(ResumeMediaBrowser.DELIMITER) 321 } 322 val prefs = context.getSharedPreferences(MEDIA_PREFERENCES, Context.MODE_PRIVATE) 323 prefs.edit().putString(MEDIA_PREFERENCE_KEY + currentUserId, sb.toString()).apply() 324 } 325 326 /** Get a runnable which will resume media playback */ getResumeActionnull327 private fun getResumeAction(componentName: ComponentName): Runnable { 328 return Runnable { 329 mediaBrowser = mediaBrowserFactory.create(null, componentName) 330 mediaBrowser?.restart() 331 } 332 } 333 dumpnull334 override fun dump(pw: PrintWriter, args: Array<out String>) { 335 pw.apply { println("resumeComponents: $resumeComponents") } 336 } 337 } 338