[DO NOT MERGE] Support toggling resumption in Settings

Moved setting so we can use Tunable:
adb shell settings put secure qs_media_resumption 1

If setting is changed, removes all existing resume players

Bug: 154039093
Test: manual
Test: atest SettingsProviderTest
Test: atest com.android.systemui.media

Change-Id: Iad056fbad4520cfe762d9e9f5ed62d38ea1117b1
This commit is contained in:
Beth Thibodeau
2020-06-09 23:36:17 -04:00
parent a7143d54c7
commit c1bc307bce
10 changed files with 105 additions and 65 deletions

View File

@@ -1897,6 +1897,15 @@ public final class Settings {
public static final String ACTION_DEVICE_CONTROLS_SETTINGS = public static final String ACTION_DEVICE_CONTROLS_SETTINGS =
"android.settings.ACTION_DEVICE_CONTROLS_SETTINGS"; "android.settings.ACTION_DEVICE_CONTROLS_SETTINGS";
/**
* Activity Action: Show media control settings
*
* @hide
*/
@SdkConstant(SdkConstantType.ACTIVITY_INTENT_ACTION)
public static final String ACTION_MEDIA_CONTROLS_SETTINGS =
"android.settings.ACTION_MEDIA_CONTROLS_SETTINGS";
/** /**
* Activity Action: Show a dialog with disabled by policy message. * Activity Action: Show a dialog with disabled by policy message.
* <p> If an user action is disabled by policy, this dialog can be triggered to let * <p> If an user action is disabled by policy, this dialog can be triggered to let
@@ -8910,6 +8919,15 @@ public final class Settings {
*/ */
public static final String PEOPLE_STRIP = "people_strip"; public static final String PEOPLE_STRIP = "people_strip";
/**
* Whether or not to enable media resumption
* When enabled, media controls in quick settings will populate on boot and persist if
* resumable via a MediaBrowserService.
* @see Settings.Global#SHOW_MEDIA_ON_QUICK_SETTINGS
* @hide
*/
public static final String MEDIA_CONTROLS_RESUME = "qs_media_resumption";
/** /**
* Controls if window magnification is enabled. * Controls if window magnification is enabled.
* @hide * @hide

View File

@@ -2678,4 +2678,9 @@ enum PageId {
// CATEGORY: SETTINGS // CATEGORY: SETTINGS
// OS: R // OS: R
DEVICE_CONTROLS_SETTINGS = 1844; DEVICE_CONTROLS_SETTINGS = 1844;
// OPEN: Settings > Sound > Media
// CATEGORY: SETTINGS
// OS: R
MEDIA_CONTROLS_SETTINGS = 1845;
} }

View File

@@ -164,7 +164,8 @@ public class SecureSettings {
Settings.Secure.AWARE_TAP_PAUSE_GESTURE_COUNT, Settings.Secure.AWARE_TAP_PAUSE_GESTURE_COUNT,
Settings.Secure.AWARE_TAP_PAUSE_TOUCH_COUNT, Settings.Secure.AWARE_TAP_PAUSE_TOUCH_COUNT,
Settings.Secure.PEOPLE_STRIP, Settings.Secure.PEOPLE_STRIP,
Settings.Secure.MEDIA_CONTROLS_RESUME,
Settings.Secure.ACCESSIBILITY_MAGNIFICATION_MODE, Settings.Secure.ACCESSIBILITY_MAGNIFICATION_MODE,
Settings.Secure.ACCESSIBILITY_BUTTON_TARGETS, Settings.Secure.ACCESSIBILITY_BUTTON_TARGETS
}; };
} }

View File

@@ -243,6 +243,7 @@ public class SecureSettingsValidators {
VALIDATORS.put(Secure.DISPLAY_DENSITY_FORCED, NON_NEGATIVE_INTEGER_VALIDATOR); VALIDATORS.put(Secure.DISPLAY_DENSITY_FORCED, NON_NEGATIVE_INTEGER_VALIDATOR);
VALIDATORS.put(Secure.TAP_GESTURE, BOOLEAN_VALIDATOR); VALIDATORS.put(Secure.TAP_GESTURE, BOOLEAN_VALIDATOR);
VALIDATORS.put(Secure.PEOPLE_STRIP, BOOLEAN_VALIDATOR); VALIDATORS.put(Secure.PEOPLE_STRIP, BOOLEAN_VALIDATOR);
VALIDATORS.put(Secure.MEDIA_CONTROLS_RESUME, BOOLEAN_VALIDATOR);
VALIDATORS.put(Secure.ACCESSIBILITY_MAGNIFICATION_MODE, VALIDATORS.put(Secure.ACCESSIBILITY_MAGNIFICATION_MODE,
new InclusiveIntegerRangeValidator( new InclusiveIntegerRangeValidator(
Secure.ACCESSIBILITY_MAGNIFICATION_MODE_FULLSCREEN, Secure.ACCESSIBILITY_MAGNIFICATION_MODE_FULLSCREEN,

View File

@@ -2784,10 +2784,16 @@
recommended controls [CHAR_LIMIT=60] --> recommended controls [CHAR_LIMIT=60] -->
<string name="controls_seeding_in_progress">Loading recommendations</string> <string name="controls_seeding_in_progress">Loading recommendations</string>
<!-- Close the controls associated with a specific media session [CHAR_LIMIT=NONE] --> <!-- Title for media controls [CHAR_LIMIT=50] -->
<string name="controls_media_close_session">Close this media session</string> <string name="controls_media_title">Media</string>
<!-- Explanation for closing controls associated with a specific media session [CHAR_LIMIT=NONE] -->
<string name="controls_media_close_session">Hide the current session.</string>
<!-- Label for a button that will hide media controls [CHAR_LIMIT=30] -->
<string name="controls_media_dismiss_button">Hide</string>
<!-- Label for button to resume media playback [CHAR_LIMIT=NONE] --> <!-- Label for button to resume media playback [CHAR_LIMIT=NONE] -->
<string name="controls_media_resume">Resume</string> <string name="controls_media_resume">Resume</string>
<!-- Label for button to go to media control settings screen [CHAR_LIMIT=30] -->
<string name="controls_media_settings_button">Settings</string>
<!-- Error message indicating that a control timed out while waiting for an update [CHAR_LIMIT=30] --> <!-- Error message indicating that a control timed out while waiting for an update [CHAR_LIMIT=30] -->
<string name="controls_error_timeout">Inactive, check app</string> <string name="controls_error_timeout">Inactive, check app</string>

View File

@@ -44,7 +44,6 @@ import com.android.systemui.broadcast.BroadcastDispatcher
import com.android.systemui.dagger.qualifiers.Background import com.android.systemui.dagger.qualifiers.Background
import com.android.systemui.dagger.qualifiers.Main import com.android.systemui.dagger.qualifiers.Main
import com.android.systemui.dump.DumpManager import com.android.systemui.dump.DumpManager
import com.android.systemui.statusbar.NotificationMediaManager
import com.android.systemui.statusbar.notification.MediaNotificationProcessor import com.android.systemui.statusbar.notification.MediaNotificationProcessor
import com.android.systemui.statusbar.notification.row.HybridGroupManager import com.android.systemui.statusbar.notification.row.HybridGroupManager
import com.android.systemui.util.Assert import com.android.systemui.util.Assert
@@ -96,7 +95,7 @@ class MediaDataManager(
dumpManager: DumpManager, dumpManager: DumpManager,
mediaTimeoutListener: MediaTimeoutListener, mediaTimeoutListener: MediaTimeoutListener,
mediaResumeListener: MediaResumeListener, mediaResumeListener: MediaResumeListener,
private val useMediaResumption: Boolean, private var useMediaResumption: Boolean,
private val useQsMediaPlayer: Boolean private val useQsMediaPlayer: Boolean
) : Dumpable { ) : Dumpable {
@@ -149,18 +148,9 @@ class MediaDataManager(
mediaTimeoutListener.timeoutCallback = { token: String, timedOut: Boolean -> mediaTimeoutListener.timeoutCallback = { token: String, timedOut: Boolean ->
setTimedOut(token, timedOut) } setTimedOut(token, timedOut) }
addListener(mediaTimeoutListener) addListener(mediaTimeoutListener)
if (useMediaResumption) {
mediaResumeListener.addTrackToResumeCallback = { desc: MediaDescription, mediaResumeListener.setManager(this)
resumeAction: Runnable, token: MediaSession.Token, appName: String,
appIntent: PendingIntent, packageName: String ->
addResumptionControls(desc, resumeAction, token, appName, appIntent, packageName)
}
mediaResumeListener.resumeComponentFoundCallback = { key: String, action: Runnable? ->
mediaEntries.get(key)?.resumeAction = action
mediaEntries.get(key)?.hasCheckedForResume = true
}
addListener(mediaResumeListener) addListener(mediaResumeListener)
}
val userFilter = IntentFilter(Intent.ACTION_USER_SWITCHED) val userFilter = IntentFilter(Intent.ACTION_USER_SWITCHED)
broadcastDispatcher.registerReceiver(userChangeReceiver, userFilter, null, UserHandle.ALL) broadcastDispatcher.registerReceiver(userChangeReceiver, userFilter, null, UserHandle.ALL)
@@ -223,7 +213,14 @@ class MediaDataManager(
} }
} }
private fun addResumptionControls( fun setResumeAction(key: String, action: Runnable?) {
mediaEntries.get(key)?.let {
it.resumeAction = action
it.hasCheckedForResume = true
}
}
fun addResumptionControls(
desc: MediaDescription, desc: MediaDescription,
action: Runnable, action: Runnable,
token: MediaSession.Token, token: MediaSession.Token,
@@ -233,7 +230,8 @@ class MediaDataManager(
) { ) {
// Resume controls don't have a notification key, so store by package name instead // Resume controls don't have a notification key, so store by package name instead
if (!mediaEntries.containsKey(packageName)) { if (!mediaEntries.containsKey(packageName)) {
val resumeData = LOADING.copy(packageName = packageName, resumeAction = action) val resumeData = LOADING.copy(packageName = packageName, resumeAction = action,
hasCheckedForResume = true)
mediaEntries.put(packageName, resumeData) mediaEntries.put(packageName, resumeData)
} }
backgroundExecutor.execute { backgroundExecutor.execute {
@@ -301,6 +299,8 @@ class MediaDataManager(
) { ) {
if (TextUtils.isEmpty(desc.title)) { if (TextUtils.isEmpty(desc.title)) {
Log.e(TAG, "Description incomplete") Log.e(TAG, "Description incomplete")
// Delete the placeholder entry
mediaEntries.remove(packageName)
return return
} }
@@ -323,7 +323,8 @@ class MediaDataManager(
onMediaDataLoaded(packageName, null, MediaData(true, bgColor, appName, onMediaDataLoaded(packageName, null, MediaData(true, bgColor, appName,
null, desc.subtitle, desc.title, artworkIcon, listOf(mediaAction), listOf(0), null, desc.subtitle, desc.title, artworkIcon, listOf(mediaAction), listOf(0),
packageName, token, appIntent, device = null, active = false, packageName, token, appIntent, device = null, active = false,
resumeAction = resumeAction)) resumeAction = resumeAction, notificationKey = packageName,
hasCheckedForResume = true))
} }
} }
@@ -432,11 +433,12 @@ class MediaDataManager(
} }
val resumeAction: Runnable? = mediaEntries.get(key)?.resumeAction val resumeAction: Runnable? = mediaEntries.get(key)?.resumeAction
val hasCheckedForResume = mediaEntries.get(key)?.hasCheckedForResume == true
foregroundExecutor.execute { foregroundExecutor.execute {
onMediaDataLoaded(key, oldKey, MediaData(true, bgColor, app, smallIconDrawable, artist, onMediaDataLoaded(key, oldKey, MediaData(true, bgColor, app, smallIconDrawable, artist,
song, artWorkIcon, actionIcons, actionsToShowCollapsed, sbn.packageName, token, song, artWorkIcon, actionIcons, actionsToShowCollapsed, sbn.packageName, token,
notif.contentIntent, null, active = true, resumeAction = resumeAction, notif.contentIntent, null, active = true, resumeAction = resumeAction,
notificationKey = key)) notificationKey = key, hasCheckedForResume = hasCheckedForResume))
} }
} }
@@ -540,7 +542,7 @@ class MediaDataManager(
val data = mediaEntries.remove(key)!! val data = mediaEntries.remove(key)!!
val resumeAction = getResumeMediaAction(data.resumeAction!!) val resumeAction = getResumeMediaAction(data.resumeAction!!)
val updated = data.copy(token = null, actions = listOf(resumeAction), val updated = data.copy(token = null, actions = listOf(resumeAction),
actionsToShowInCompact = listOf(0)) actionsToShowInCompact = listOf(0), active = false)
mediaEntries.put(data.packageName, updated) mediaEntries.put(data.packageName, updated)
// Notify listeners of "new" controls // Notify listeners of "new" controls
val listenersCopy = listeners.toSet() val listenersCopy = listeners.toSet()
@@ -563,20 +565,33 @@ class MediaDataManager(
*/ */
fun hasActiveMedia() = mediaEntries.any { it.value.active } fun hasActiveMedia() = mediaEntries.any { it.value.active }
fun isActive(token: MediaSession.Token?): Boolean {
if (token == null) {
return false
}
val controller = mediaControllerFactory.create(token)
val state = controller?.playbackState?.state
return state != null && NotificationMediaManager.isActiveState(state)
}
/** /**
* Are there any media entries, including resume controls? * Are there any media entries we should display?
* If resumption is enabled, this will include inactive players
* If resumption is disabled, we only want to show active players
*/ */
fun hasAnyMedia() = if (useMediaResumption) mediaEntries.isNotEmpty() else hasActiveMedia() fun hasAnyMedia() = if (useMediaResumption) mediaEntries.isNotEmpty() else hasActiveMedia()
fun setMediaResumptionEnabled(isEnabled: Boolean) {
if (useMediaResumption == isEnabled) {
return
}
useMediaResumption = isEnabled
if (!useMediaResumption) {
// Remove any existing resume controls
val listenersCopy = listeners.toSet()
val filtered = mediaEntries.filter { !it.value.active }
filtered.forEach {
mediaEntries.remove(it.key)
listenersCopy.forEach { listener ->
listener.onMediaDataRemoved(it.key)
}
}
}
}
interface Listener { interface Listener {
/** /**

View File

@@ -16,7 +16,6 @@
package com.android.systemui.media package com.android.systemui.media
import android.app.PendingIntent
import android.content.BroadcastReceiver import android.content.BroadcastReceiver
import android.content.ComponentName import android.content.ComponentName
import android.content.Context import android.content.Context
@@ -25,12 +24,13 @@ import android.content.IntentFilter
import android.content.pm.PackageManager import android.content.pm.PackageManager
import android.media.MediaDescription import android.media.MediaDescription
import android.media.session.MediaController import android.media.session.MediaController
import android.media.session.MediaSession
import android.os.UserHandle import android.os.UserHandle
import android.provider.Settings
import android.service.media.MediaBrowserService import android.service.media.MediaBrowserService
import android.util.Log import android.util.Log
import com.android.systemui.broadcast.BroadcastDispatcher import com.android.systemui.broadcast.BroadcastDispatcher
import com.android.systemui.dagger.qualifiers.Background import com.android.systemui.dagger.qualifiers.Background
import com.android.systemui.tuner.TunerService
import com.android.systemui.util.Utils import com.android.systemui.util.Utils
import java.util.concurrent.ConcurrentLinkedQueue import java.util.concurrent.ConcurrentLinkedQueue
import java.util.concurrent.Executor import java.util.concurrent.Executor
@@ -46,21 +46,14 @@ private const val MEDIA_PREFERENCE_KEY = "browser_components_"
class MediaResumeListener @Inject constructor( class MediaResumeListener @Inject constructor(
private val context: Context, private val context: Context,
private val broadcastDispatcher: BroadcastDispatcher, private val broadcastDispatcher: BroadcastDispatcher,
@Background private val backgroundExecutor: Executor @Background private val backgroundExecutor: Executor,
private val tunerService: TunerService
) : MediaDataManager.Listener { ) : MediaDataManager.Listener {
private val useMediaResumption: Boolean = Utils.useMediaResumption(context) private var useMediaResumption: Boolean = Utils.useMediaResumption(context)
private val resumeComponents: ConcurrentLinkedQueue<ComponentName> = ConcurrentLinkedQueue() private val resumeComponents: ConcurrentLinkedQueue<ComponentName> = ConcurrentLinkedQueue()
lateinit var addTrackToResumeCallback: ( private lateinit var mediaDataManager: MediaDataManager
MediaDescription,
Runnable,
MediaSession.Token,
String,
PendingIntent,
String
) -> Unit
lateinit var resumeComponentFoundCallback: (String, Runnable?) -> Unit
private var mediaBrowser: ResumeMediaBrowser? = null private var mediaBrowser: ResumeMediaBrowser? = null
private var currentUserId: Int private var currentUserId: Int
@@ -96,8 +89,8 @@ class MediaResumeListener @Inject constructor(
} }
Log.d(TAG, "Adding resume controls $desc") Log.d(TAG, "Adding resume controls $desc")
addTrackToResumeCallback(desc, resumeAction, token, appName.toString(), appIntent, mediaDataManager.addResumptionControls(desc, resumeAction, token, appName.toString(),
component.packageName) appIntent, component.packageName)
} }
} }
@@ -113,6 +106,18 @@ class MediaResumeListener @Inject constructor(
} }
} }
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() { private fun loadSavedComponents() {
// Make sure list is empty (if we switched users) // Make sure list is empty (if we switched users)
resumeComponents.clear() resumeComponents.clear()
@@ -165,7 +170,7 @@ class MediaResumeListener @Inject constructor(
} }
} else { } else {
// No service found // No service found
resumeComponentFoundCallback(key, null) mediaDataManager.setResumeAction(key, null)
} }
} }
} }
@@ -182,7 +187,7 @@ class MediaResumeListener @Inject constructor(
object : ResumeMediaBrowser.Callback() { object : ResumeMediaBrowser.Callback() {
override fun onConnected() { override fun onConnected() {
Log.d(TAG, "yes we can resume with $componentName") Log.d(TAG, "yes we can resume with $componentName")
resumeComponentFoundCallback(key, getResumeAction(componentName)) mediaDataManager.setResumeAction(key, getResumeAction(componentName))
updateResumptionList(componentName) updateResumptionList(componentName)
mediaBrowser?.disconnect() mediaBrowser?.disconnect()
mediaBrowser = null mediaBrowser = null
@@ -190,7 +195,7 @@ class MediaResumeListener @Inject constructor(
override fun onError() { override fun onError() {
Log.e(TAG, "Cannot resume with $componentName") Log.e(TAG, "Cannot resume with $componentName")
resumeComponentFoundCallback(key, null) mediaDataManager.setResumeAction(key, null)
mediaBrowser?.disconnect() mediaBrowser?.disconnect()
mediaBrowser = null mediaBrowser = null
} }

View File

@@ -30,8 +30,6 @@ import android.service.media.MediaBrowserService;
import android.text.TextUtils; import android.text.TextUtils;
import android.util.Log; import android.util.Log;
import com.android.systemui.util.Utils;
import java.util.List; import java.util.List;
/** /**
@@ -46,7 +44,6 @@ public class ResumeMediaBrowser {
public static final String DELIMITER = ":"; public static final String DELIMITER = ":";
private static final String TAG = "ResumeMediaBrowser"; private static final String TAG = "ResumeMediaBrowser";
private boolean mIsEnabled = false;
private final Context mContext; private final Context mContext;
private final Callback mCallback; private final Callback mCallback;
private MediaBrowser mMediaBrowser; private MediaBrowser mMediaBrowser;
@@ -59,7 +56,6 @@ public class ResumeMediaBrowser {
* @param componentName Component name of the MediaBrowserService this browser will connect to * @param componentName Component name of the MediaBrowserService this browser will connect to
*/ */
public ResumeMediaBrowser(Context context, Callback callback, ComponentName componentName) { public ResumeMediaBrowser(Context context, Callback callback, ComponentName componentName) {
mIsEnabled = Utils.useMediaResumption(context);
mContext = context; mContext = context;
mCallback = callback; mCallback = callback;
mComponentName = componentName; mComponentName = componentName;
@@ -74,9 +70,6 @@ public class ResumeMediaBrowser {
* ResumeMediaBrowser#disconnect will be called automatically with this function. * ResumeMediaBrowser#disconnect will be called automatically with this function.
*/ */
public void findRecentMedia() { public void findRecentMedia() {
if (!mIsEnabled) {
return;
}
Log.d(TAG, "Connecting to " + mComponentName); Log.d(TAG, "Connecting to " + mComponentName);
disconnect(); disconnect();
Bundle rootHints = new Bundle(); Bundle rootHints = new Bundle();
@@ -186,9 +179,6 @@ public class ResumeMediaBrowser {
* ResumeMediaBrowser#disconnect should be called after this to ensure the connection is closed. * ResumeMediaBrowser#disconnect should be called after this to ensure the connection is closed.
*/ */
public void restart() { public void restart() {
if (!mIsEnabled) {
return;
}
disconnect(); disconnect();
Bundle rootHints = new Bundle(); Bundle rootHints = new Bundle();
rootHints.putBoolean(MediaBrowserService.BrowserRoot.EXTRA_RECENT, true); rootHints.putBoolean(MediaBrowserService.BrowserRoot.EXTRA_RECENT, true);
@@ -250,9 +240,6 @@ public class ResumeMediaBrowser {
* ResumeMediaBrowser#disconnect should be called after this to ensure the connection is closed. * ResumeMediaBrowser#disconnect should be called after this to ensure the connection is closed.
*/ */
public void testConnection() { public void testConnection() {
if (!mIsEnabled) {
return;
}
disconnect(); disconnect();
final MediaBrowser.ConnectionCallback connectionCallback = final MediaBrowser.ConnectionCallback connectionCallback =
new MediaBrowser.ConnectionCallback() { new MediaBrowser.ConnectionCallback() {

View File

@@ -62,7 +62,8 @@ public class TunerServiceImpl extends TunerService {
// shouldn't be reset with tuner settings. // shouldn't be reset with tuner settings.
private static final String[] RESET_BLACKLIST = new String[] { private static final String[] RESET_BLACKLIST = new String[] {
QSTileHost.TILES_SETTING, QSTileHost.TILES_SETTING,
Settings.Secure.DOZE_ALWAYS_ON Settings.Secure.DOZE_ALWAYS_ON,
Settings.Secure.MEDIA_CONTROLS_RESUME
}; };
private final Observer mObserver = new Observer(); private final Observer mObserver = new Observer();

View File

@@ -139,7 +139,8 @@ public class Utils {
* Off by default, but can be enabled by setting to 1 * Off by default, but can be enabled by setting to 1
*/ */
public static boolean useMediaResumption(Context context) { public static boolean useMediaResumption(Context context) {
int flag = Settings.System.getInt(context.getContentResolver(), "qs_media_resumption", 0); int flag = Settings.Secure.getInt(context.getContentResolver(),
Settings.Secure.MEDIA_CONTROLS_RESUME, 1);
return useQsMediaPlayer(context) && flag > 0; return useQsMediaPlayer(context) && flag > 0;
} }
} }