blob: 936db8735ad88cc4a405cce079e230e23ede5cfb [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.media.session.MediaController
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.broadcast.BroadcastDispatcher
import com.android.systemui.dagger.qualifiers.Background
import com.android.systemui.tuner.TunerService
import com.android.systemui.util.Utils
import java.util.concurrent.ConcurrentLinkedQueue
import java.util.concurrent.Executor
import javax.inject.Inject
import javax.inject.Singleton
private const val TAG = "MediaResumeListener"
private const val MEDIA_PREFERENCES = "media_control_prefs"
private const val MEDIA_PREFERENCE_KEY = "browser_components_"
@Singleton
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
) : MediaDataManager.Listener {
private var useMediaResumption: Boolean = Utils.useMediaResumption(context)
private val resumeComponents: ConcurrentLinkedQueue<ComponentName> = ConcurrentLinkedQueue()
private lateinit var mediaDataManager: MediaDataManager
private var mediaBrowser: ResumeMediaBrowser? = null
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 $desc")
mediaDataManager.addResumptionControls(currentUserId, desc, resumeAction, token,
appName.toString(), appIntent, component.packageName)
}
}
init {
if (useMediaResumption) {
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() }
components?.forEach {
val info = it.split("/")
val packageName = info[0]
val className = info[1]
val component = ComponentName(packageName, className)
resumeComponents.add(component)
}
Log.d(TAG, "loaded resume components ${resumeComponents.toArray().contentToString()}")
}
/**
* Load controls for resuming media, if available
*/
private fun loadMediaResumptionControls() {
if (!useMediaResumption) {
return
}
resumeComponents.forEach {
val browser = mediaBrowserFactory.create(mediaBrowserCallback, it)
browser.findRecentMedia()
}
}
override fun onMediaDataLoaded(key: String, oldKey: String?, data: MediaData) {
if (useMediaResumption) {
// If this had been started from a resume state, disconnect now that it's live
mediaBrowser?.disconnect()
// If we don't have a resume action, check if we haven't already
if (data.resumeAction == null && !data.hasCheckedForResume) {
// 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.queryIntentServices(serviceIntent, 0)
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")
mediaBrowser?.disconnect()
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")
mediaDataManager.setResumeAction(key, null)
mediaBrowser?.disconnect()
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 from $componentName")
mediaDataManager.setResumeAction(key, getResumeAction(componentName))
updateResumptionList(componentName)
mediaBrowser?.disconnect()
mediaBrowser = null
}
},
componentName)
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(componentName)
// Insert at front of queue
resumeComponents.add(componentName)
// Remove old components if over the limit
if (resumeComponents.size > ResumeMediaBrowser.MAX_RESUMPTION_CONTROLS) {
resumeComponents.remove()
}
// Save changes
val sb = StringBuilder()
resumeComponents.forEach {
sb.append(it.flattenToString())
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?.disconnect()
mediaBrowser = mediaBrowserFactory.create(
object : ResumeMediaBrowser.Callback() {
override fun onConnected() {
if (mediaBrowser?.token == null) {
Log.e(TAG, "Error after connect")
mediaBrowser?.disconnect()
mediaBrowser = null
return
}
Log.d(TAG, "Connected for restart $componentName")
val controller = MediaController(context, mediaBrowser!!.token)
val controls = controller.transportControls
controls.prepare()
controls.play()
}
override fun onError() {
Log.e(TAG, "Resume failed for $componentName")
mediaBrowser?.disconnect()
mediaBrowser = null
}
},
componentName)
mediaBrowser?.restart()
}
}
}