blob: bad87de438e5f4c5e6154f606bd673e1ebc21bab [file] [log] [blame]
/*
* Copyright (C) 2020 The Android Open Source Project
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package com.android.systemui.media
import android.content.BroadcastReceiver
import android.content.ComponentName
import android.content.Context
import android.content.Intent
import android.content.IntentFilter
import android.content.pm.PackageManager
import android.media.MediaDescription
import android.os.UserHandle
import android.provider.Settings
import android.service.media.MediaBrowserService
import android.util.Log
import com.android.internal.annotations.VisibleForTesting
import com.android.systemui.Dumpable
import com.android.systemui.broadcast.BroadcastDispatcher
import com.android.systemui.dagger.SysUISingleton
import com.android.systemui.dagger.qualifiers.Background
import com.android.systemui.dump.DumpManager
import com.android.systemui.tuner.TunerService
import com.android.systemui.util.Utils
import com.android.systemui.util.time.SystemClock
import java.io.PrintWriter
import java.util.concurrent.ConcurrentLinkedQueue
import java.util.concurrent.Executor
import javax.inject.Inject
private const val TAG = "MediaResumeListener"
private const val MEDIA_PREFERENCES = "media_control_prefs"
private const val MEDIA_PREFERENCE_KEY = "browser_components_"
@SysUISingleton
class MediaResumeListener @Inject constructor(
private val context: Context,
private val broadcastDispatcher: BroadcastDispatcher,
@Background private val backgroundExecutor: Executor,
private val tunerService: TunerService,
private val mediaBrowserFactory: ResumeMediaBrowserFactory,
dumpManager: DumpManager,
private val systemClock: SystemClock
) : MediaDataManager.Listener, Dumpable {
private var useMediaResumption: Boolean = Utils.useMediaResumption(context)
private val resumeComponents: ConcurrentLinkedQueue<Pair<ComponentName, Long>> =
ConcurrentLinkedQueue()
private lateinit var mediaDataManager: MediaDataManager
private var mediaBrowser: ResumeMediaBrowser? = null
set(value) {
// Always disconnect the old browser -- see b/225403871.
field?.disconnect()
field = value
}
private var currentUserId: Int = context.userId
@VisibleForTesting
val userChangeReceiver = object : BroadcastReceiver() {
override fun onReceive(context: Context, intent: Intent) {
if (Intent.ACTION_USER_UNLOCKED == intent.action) {
loadMediaResumptionControls()
} else if (Intent.ACTION_USER_SWITCHED == intent.action) {
currentUserId = intent.getIntExtra(Intent.EXTRA_USER_HANDLE, -1)
loadSavedComponents()
}
}
}
private val mediaBrowserCallback = object : ResumeMediaBrowser.Callback() {
override fun addTrack(
desc: MediaDescription,
component: ComponentName,
browser: ResumeMediaBrowser
) {
val token = browser.token
val appIntent = browser.appIntent
val pm = context.getPackageManager()
var appName: CharSequence = component.packageName
val resumeAction = getResumeAction(component)
try {
appName = pm.getApplicationLabel(
pm.getApplicationInfo(component.packageName, 0))
} catch (e: PackageManager.NameNotFoundException) {
Log.e(TAG, "Error getting package information", e)
}
Log.d(TAG, "Adding resume controls for ${browser.userId}: $desc")
mediaDataManager.addResumptionControls(
browser.userId,
desc,
resumeAction,
token,
appName.toString(),
appIntent,
component.packageName
)
}
}
init {
if (useMediaResumption) {
dumpManager.registerDumpable(TAG, this)
val unlockFilter = IntentFilter()
unlockFilter.addAction(Intent.ACTION_USER_UNLOCKED)
unlockFilter.addAction(Intent.ACTION_USER_SWITCHED)
broadcastDispatcher.registerReceiver(userChangeReceiver, unlockFilter, null,
UserHandle.ALL)
loadSavedComponents()
}
}
fun setManager(manager: MediaDataManager) {
mediaDataManager = manager
// Add listener for resumption setting changes
tunerService.addTunable(object : TunerService.Tunable {
override fun onTuningChanged(key: String?, newValue: String?) {
useMediaResumption = Utils.useMediaResumption(context)
mediaDataManager.setMediaResumptionEnabled(useMediaResumption)
}
}, Settings.Secure.MEDIA_CONTROLS_RESUME)
}
private fun loadSavedComponents() {
// Make sure list is empty (if we switched users)
resumeComponents.clear()
val prefs = context.getSharedPreferences(MEDIA_PREFERENCES, Context.MODE_PRIVATE)
val listString = prefs.getString(MEDIA_PREFERENCE_KEY + currentUserId, null)
val components = listString?.split(ResumeMediaBrowser.DELIMITER.toRegex())
?.dropLastWhile { it.isEmpty() }
var needsUpdate = false
components?.forEach {
val info = it.split("/")
val packageName = info[0]
val className = info[1]
val component = ComponentName(packageName, className)
val lastPlayed = if (info.size == 3) {
try {
info[2].toLong()
} catch (e: NumberFormatException) {
needsUpdate = true
systemClock.currentTimeMillis()
}
} else {
needsUpdate = true
systemClock.currentTimeMillis()
}
resumeComponents.add(component to lastPlayed)
}
Log.d(
TAG,
"loaded resume components for $currentUserId: " +
"${resumeComponents.toArray().contentToString()}"
)
if (needsUpdate) {
// Save any missing times that we had to fill in
writeSharedPrefs()
}
}
/**
* Load controls for resuming media, if available
*/
private fun loadMediaResumptionControls() {
if (!useMediaResumption) {
return
}
val pm = context.packageManager
val now = systemClock.currentTimeMillis()
resumeComponents.forEach {
if (now.minus(it.second) <= RESUME_MEDIA_TIMEOUT) {
// Verify that the service exists for this user
val intent = Intent(MediaBrowserService.SERVICE_INTERFACE)
intent.component = it.first
val inf = pm.resolveServiceAsUser(intent, 0, currentUserId)
if (inf != null) {
val browser =
mediaBrowserFactory.create(mediaBrowserCallback, it.first, currentUserId)
browser.findRecentMedia()
} else {
Log.d(TAG, "User $currentUserId does not have component ${it.first}")
}
}
}
}
override fun onMediaDataLoaded(
key: String,
oldKey: String?,
data: MediaData,
immediately: Boolean,
receivedSmartspaceCardLatency: Int,
isSsReactivated: Boolean
) {
if (useMediaResumption) {
// If this had been started from a resume state, disconnect now that it's live
if (!key.equals(oldKey)) {
mediaBrowser = null
}
// If we don't have a resume action, check if we haven't already
if (data.resumeAction == null && !data.hasCheckedForResume && data.isLocalSession()) {
// TODO also check for a media button receiver intended for restarting (b/154127084)
Log.d(TAG, "Checking for service component for " + data.packageName)
val pm = context.packageManager
val serviceIntent = Intent(MediaBrowserService.SERVICE_INTERFACE)
val resumeInfo = pm.queryIntentServicesAsUser(serviceIntent, 0, currentUserId)
val inf = resumeInfo?.filter {
it.serviceInfo.packageName == data.packageName
}
if (inf != null && inf.size > 0) {
backgroundExecutor.execute {
tryUpdateResumptionList(key, inf!!.get(0).componentInfo.componentName)
}
} else {
// No service found
mediaDataManager.setResumeAction(key, null)
}
}
}
}
/**
* Verify that we can connect to the given component with a MediaBrowser, and if so, add that
* component to the list of resumption components
*/
private fun tryUpdateResumptionList(key: String, componentName: ComponentName) {
Log.d(TAG, "Testing if we can connect to $componentName")
// Set null action to prevent additional attempts to connect
mediaDataManager.setResumeAction(key, null)
mediaBrowser = mediaBrowserFactory.create(
object : ResumeMediaBrowser.Callback() {
override fun onConnected() {
Log.d(TAG, "Connected to $componentName")
}
override fun onError() {
Log.e(TAG, "Cannot resume with $componentName")
mediaBrowser = null
}
override fun addTrack(
desc: MediaDescription,
component: ComponentName,
browser: ResumeMediaBrowser
) {
// Since this is a test, just save the component for later
Log.d(
TAG,
"Can get resumable media for ${browser.userId} from $componentName"
)
mediaDataManager.setResumeAction(key, getResumeAction(componentName))
updateResumptionList(componentName)
mediaBrowser = null
}
},
componentName,
currentUserId
)
mediaBrowser?.testConnection()
}
/**
* Add the component to the saved list of media browser services, checking for duplicates and
* removing older components that exceed the maximum limit
* @param componentName
*/
private fun updateResumptionList(componentName: ComponentName) {
// Remove if exists
resumeComponents.remove(resumeComponents.find { it.first.equals(componentName) })
// Insert at front of queue
val currentTime = systemClock.currentTimeMillis()
resumeComponents.add(componentName to currentTime)
// Remove old components if over the limit
if (resumeComponents.size > ResumeMediaBrowser.MAX_RESUMPTION_CONTROLS) {
resumeComponents.remove()
}
writeSharedPrefs()
}
private fun writeSharedPrefs() {
val sb = StringBuilder()
resumeComponents.forEach {
sb.append(it.first.flattenToString())
sb.append("/")
sb.append(it.second)
sb.append(ResumeMediaBrowser.DELIMITER)
}
val prefs = context.getSharedPreferences(MEDIA_PREFERENCES, Context.MODE_PRIVATE)
prefs.edit().putString(MEDIA_PREFERENCE_KEY + currentUserId, sb.toString()).apply()
}
/**
* Get a runnable which will resume media playback
*/
private fun getResumeAction(componentName: ComponentName): Runnable {
return Runnable {
mediaBrowser = mediaBrowserFactory.create(null, componentName, currentUserId)
mediaBrowser?.restart()
}
}
override fun dump(pw: PrintWriter, args: Array<out String>) {
pw.apply {
println("resumeComponents: $resumeComponents")
}
}
}