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