Use compat framework to gate session actions

Adds CompatChange to check if an app posting media controls targets T,
and if so, that app's controls will use PlaybackState actions.
Otherwise, the notification actions will be used.

The MEDIA_SESSION_ACTIONS flag is set to false by default, and can be
used to override this and use session actions for all apps.

Fixes: 220034304
Test: atest com.android.systemui.media
Test: manual - verify in developer settings, override behavior works

Change-Id: Ie032fe9ec8ce7d46b1f9dc8f590ed6fbfa7ee9aa
This commit is contained in:
Beth Thibodeau
2022-03-10 11:50:00 -05:00
parent 8909727b97
commit 85dddfc3c6
10 changed files with 68 additions and 34 deletions

View File

@@ -24,6 +24,9 @@ import android.annotation.RequiresPermission;
import android.annotation.SystemApi;
import android.annotation.SystemService;
import android.annotation.TestApi;
import android.app.compat.CompatChanges;
import android.compat.annotation.ChangeId;
import android.compat.annotation.EnabledSince;
import android.compat.annotation.UnsupportedAppUsage;
import android.content.ComponentName;
import android.content.Context;
@@ -39,6 +42,7 @@ import android.os.Bundle;
import android.os.IBinder;
import android.os.RemoteException;
import android.os.ServiceManager;
import android.os.UserHandle;
import android.util.Pair;
import android.util.Slog;
import android.view.View;
@@ -520,6 +524,27 @@ public class StatusBarManager {
private final Map<NearbyMediaDevicesProvider, NearbyMediaDevicesProviderWrapper>
nearbyMediaDevicesProviderMap = new HashMap<>();
/**
* Media controls based on {@link android.app.Notification.MediaStyle} notifications will have
* actions based on the media session's {@link android.media.session.PlaybackState}, rather than
* the notification's actions.
*
* These actions will be:
* - Play/Pause (depending on whether the current state is a playing state)
* - Previous (if declared), or a custom action if the slot is not reserved with
* {@code SESSION_EXTRAS_KEY_SLOT_RESERVATION_SKIP_TO_PREV}
* - Next (if declared), or a custom action if the slot is not reserved with
* {@code SESSION_EXTRAS_KEY_SLOT_RESERVATION_SKIP_TO_NEXT}
* - Custom action
* - Custom action
*
* @see androidx.media.utils.MediaConstants#SESSION_EXTRAS_KEY_SLOT_RESERVATION_SKIP_TO_PREV
* @see androidx.media.utils.MediaConstants#SESSION_EXTRAS_KEY_SLOT_RESERVATION_SKIP_TO_NEXT
*/
@ChangeId
@EnabledSince(targetSdkVersion = Build.VERSION_CODES.TIRAMISU)
private static final long MEDIA_CONTROL_SESSION_ACTIONS = 203800354L;
@UnsupportedAppUsage
private Context mContext;
private IStatusBarService mService;
@@ -1127,6 +1152,20 @@ public class StatusBarManager {
}
}
/**
* Checks whether the given package should use session-based actions for its media controls.
*
* @param packageName App posting media controls
* @param userId Current user ID
* @return true if the app supports session actions
*
* @hide
*/
public static boolean useMediaSessionActionsForApp(String packageName, int userId) {
UserHandle handle = UserHandle.getUserHandleForUid(userId);
return CompatChanges.isChangeEnabled(MEDIA_CONTROL_SESSION_ACTIONS, packageName, handle);
}
/** @hide */
public static String windowStateToString(int state) {
if (state == WINDOW_STATE_HIDING) return "WINDOW_STATE_HIDING";

View File

@@ -76,5 +76,8 @@
<permission name="android.permission.FORCE_STOP_PACKAGES" />
<permission name="android.permission.ACCESS_FPS_COUNTER" />
<permission name="android.permission.CHANGE_CONFIGURATION" />
<permission name="android.permission.LOG_COMPAT_CHANGE" />
<permission name="android.permission.READ_COMPAT_CHANGE_CONFIG" />
<permission name="android.permission.READ_DEVICE_CONFIG" />
</privapp-permissions>
</permissions>

View File

@@ -314,6 +314,11 @@
<!-- To change system captions state -->
<uses-permission android:name="android.permission.SET_SYSTEM_AUDIO_CAPTION" />
<!-- Compat framework -->
<uses-permission android:name="android.permission.LOG_COMPAT_CHANGE" />
<uses-permission android:name="android.permission.READ_COMPAT_CHANGE_CONFIG" />
<uses-permission android:name="android.permission.READ_DEVICE_CONFIG" />
<protected-broadcast android:name="com.android.settingslib.action.REGISTER_SLICE_RECEIVER" />
<protected-broadcast android:name="com.android.settingslib.action.UNREGISTER_SLICE_RECEIVER" />
<protected-broadcast android:name="com.android.settings.flashlight.action.FLASHLIGHT_CHANGED" />

View File

@@ -145,7 +145,7 @@ public class Flags {
/***************************************/
// 900 - media
public static final BooleanFlag MEDIA_TAP_TO_TRANSFER = new BooleanFlag(900, true);
public static final BooleanFlag MEDIA_SESSION_ACTIONS = new BooleanFlag(901, true);
public static final BooleanFlag MEDIA_SESSION_ACTIONS = new BooleanFlag(901, false);
public static final BooleanFlag MEDIA_SESSION_LAYOUT = new BooleanFlag(902, true);
public static final BooleanFlag MEDIA_NEARBY_DEVICES = new BooleanFlag(903, true);
public static final BooleanFlag MEDIA_MUTE_AWAIT = new BooleanFlag(904, true);

View File

@@ -137,7 +137,6 @@ public class MediaControlPanel {
private MediaCarouselController mMediaCarouselController;
private final MediaOutputDialogFactory mMediaOutputDialogFactory;
private final FalsingManager mFalsingManager;
private final MediaFlags mMediaFlags;
// Used for swipe-to-dismiss logging.
protected boolean mIsImpressed = false;
@@ -156,7 +155,7 @@ public class MediaControlPanel {
Lazy<MediaDataManager> lazyMediaDataManager,
MediaOutputDialogFactory mediaOutputDialogFactory,
MediaCarouselController mediaCarouselController,
FalsingManager falsingManager, MediaFlags mediaFlags, SystemClock systemClock) {
FalsingManager falsingManager, SystemClock systemClock) {
mContext = context;
mBackgroundExecutor = backgroundExecutor;
mActivityStarter = activityStarter;
@@ -167,7 +166,6 @@ public class MediaControlPanel {
mMediaOutputDialogFactory = mediaOutputDialogFactory;
mMediaCarouselController = mediaCarouselController;
mFalsingManager = falsingManager;
mMediaFlags = mediaFlags;
mSystemClock = systemClock;
loadDimens();
@@ -506,9 +504,8 @@ public class MediaControlPanel {
List<MediaAction> actionIcons = data.getActions();
List<Integer> actionsWhenCollapsed = data.getActionsToShowInCompact();
// If the session actions flag is enabled, but we're still using the regular layout, use
// the session actions anyways
if (mMediaFlags.areMediaSessionActionsEnabled() && data.getSemanticActions() != null) {
// If we got session actions, use those instead
if (data.getSemanticActions() != null) {
MediaButton semanticActions = data.getSemanticActions();
actionIcons = new ArrayList<MediaAction>();

View File

@@ -610,7 +610,8 @@ class MediaDataManager(
var actionIcons: List<MediaAction> = emptyList()
var actionsToShowCollapsed: List<Int> = emptyList()
var semanticActions: MediaButton? = null
if (mediaFlags.areMediaSessionActionsEnabled() && mediaController.playbackState != null) {
if (mediaFlags.areMediaSessionActionsEnabled(sbn.packageName, sbn.user.identifier) &&
mediaController.playbackState != null) {
semanticActions = createActionsFromState(sbn.packageName, mediaController)
} else {
val actions = createActionsFromNotification(sbn)
@@ -726,7 +727,7 @@ class MediaDataManager(
}
}
// Finally, assign the remaining button slots: C A play/pause B D
// Finally, assign the remaining button slots: play/pause A B C D
// A = previous, else custom action (if not reserved)
// B = next, else custom action (if not reserved)
// C and D are always custom actions

View File

@@ -16,6 +16,7 @@
package com.android.systemui.media
import android.app.StatusBarManager
import com.android.systemui.dagger.SysUISingleton
import com.android.systemui.flags.FeatureFlags
import com.android.systemui.flags.Flags
@@ -26,16 +27,17 @@ class MediaFlags @Inject constructor(private val featureFlags: FeatureFlags) {
/**
* Check whether media control actions should be based on PlaybackState instead of notification
*/
fun areMediaSessionActionsEnabled(): Boolean {
return featureFlags.isEnabled(Flags.MEDIA_SESSION_ACTIONS)
fun areMediaSessionActionsEnabled(packageName: String, userId: Int): Boolean {
val enabled = StatusBarManager.useMediaSessionActionsForApp(packageName, userId)
// Allow global override with flag
return enabled || featureFlags.isEnabled(Flags.MEDIA_SESSION_ACTIONS)
}
/**
* Check whether media controls should use the new session-based layout
*/
fun useMediaSessionLayout(): Boolean {
return featureFlags.isEnabled(Flags.MEDIA_SESSION_ACTIONS) &&
featureFlags.isEnabled(Flags.MEDIA_SESSION_LAYOUT)
return featureFlags.isEnabled(Flags.MEDIA_SESSION_LAYOUT)
}
/**

View File

@@ -37,7 +37,6 @@ import org.junit.runner.RunWith
import org.mockito.Mock
import org.mockito.MockitoAnnotations
import javax.inject.Provider
import org.mockito.Mockito.`when` as whenever
private val DATA = MediaData(
userId = -1,
@@ -83,7 +82,6 @@ class MediaCarouselControllerTest : SysuiTestCase() {
@Before
fun setup() {
MockitoAnnotations.initMocks(this)
whenever(mediaFlags.areMediaSessionActionsEnabled()).thenReturn(true)
mediaCarouselController = MediaCarouselController(
context,
mediaControlPanelFactory,

View File

@@ -101,7 +101,6 @@ public class MediaControlPanelTest : SysuiTestCase() {
@Mock private lateinit var mediaOutputDialogFactory: MediaOutputDialogFactory
@Mock private lateinit var mediaCarouselController: MediaCarouselController
@Mock private lateinit var falsingManager: FalsingManager
@Mock private lateinit var mediaFlags: MediaFlags
private lateinit var appIcon: ImageView
private lateinit var albumView: ImageView
private lateinit var titleText: TextView
@@ -147,7 +146,7 @@ public class MediaControlPanelTest : SysuiTestCase() {
player = MediaControlPanel(context, bgExecutor, activityStarter, broadcastSender,
mediaViewController, seekBarViewModel, Lazy { mediaDataManager },
mediaOutputDialogFactory, mediaCarouselController, falsingManager, mediaFlags, clock)
mediaOutputDialogFactory, mediaCarouselController, falsingManager, clock)
whenever(seekBarViewModel.progress).thenReturn(seekBarData)
// Set up mock views for the players
@@ -215,9 +214,6 @@ public class MediaControlPanelTest : SysuiTestCase() {
device = device,
active = true,
resumeAction = null)
whenever(mediaFlags.areMediaSessionActionsEnabled()).thenReturn(false)
whenever(mediaFlags.useMediaSessionLayout()).thenReturn(false)
}
/**
@@ -295,9 +291,6 @@ public class MediaControlPanelTest : SysuiTestCase() {
@Test
fun bindSemanticActionsOldLayout() {
whenever(mediaFlags.areMediaSessionActionsEnabled()).thenReturn(true)
whenever(mediaFlags.useMediaSessionLayout()).thenReturn(false)
val icon = Icon.createWithResource(context, android.R.drawable.ic_media_play)
val semanticActions = MediaButton(
playOrPause = MediaAction(icon, Runnable {}, "play"),
@@ -332,9 +325,6 @@ public class MediaControlPanelTest : SysuiTestCase() {
@Test
fun bindSemanticActionsNewLayout() {
whenever(mediaFlags.areMediaSessionActionsEnabled()).thenReturn(true)
whenever(mediaFlags.useMediaSessionLayout()).thenReturn(true)
val icon = Icon.createWithResource(context, android.R.drawable.ic_media_play)
val semanticActions = MediaButton(
playOrPause = MediaAction(icon, Runnable {}, "play"),
@@ -381,9 +371,6 @@ public class MediaControlPanelTest : SysuiTestCase() {
@Test
fun bindNotificationActionsNewLayout() {
whenever(mediaFlags.areMediaSessionActionsEnabled()).thenReturn(true)
whenever(mediaFlags.useMediaSessionLayout()).thenReturn(true)
val icon = Icon.createWithResource(context, android.R.drawable.ic_media_play)
val actions = listOf(
MediaAction(icon, Runnable {}, "previous"),

View File

@@ -26,6 +26,7 @@ import com.android.systemui.plugins.ActivityStarter
import com.android.systemui.statusbar.SbnBuilder
import com.android.systemui.tuner.TunerService
import com.android.systemui.util.concurrency.FakeExecutor
import com.android.systemui.util.mockito.any
import com.android.systemui.util.mockito.capture
import com.android.systemui.util.mockito.eq
import com.android.systemui.util.time.FakeSystemClock
@@ -37,6 +38,7 @@ import org.junit.Test
import org.junit.runner.RunWith
import org.mockito.ArgumentCaptor
import org.mockito.ArgumentMatchers.anyBoolean
import org.mockito.ArgumentMatchers.anyInt
import org.mockito.Captor
import org.mockito.Mock
import org.mockito.Mockito
@@ -167,7 +169,7 @@ class MediaDataManagerTest : SysuiTestCase() {
whenever(mediaSmartspaceTarget.featureType).thenReturn(SmartspaceTarget.FEATURE_MEDIA)
whenever(mediaSmartspaceTarget.iconGrid).thenReturn(listOf(mediaRecommendationItem))
whenever(mediaSmartspaceTarget.creationTimeMillis).thenReturn(1234L)
whenever(mediaFlags.areMediaSessionActionsEnabled()).thenReturn(false)
whenever(mediaFlags.areMediaSessionActionsEnabled(any(), anyInt())).thenReturn(false)
}
@After
@@ -594,7 +596,7 @@ class MediaDataManagerTest : SysuiTestCase() {
@Test
fun testPlaybackActions_noState_usesNotification() {
val desc = "Notification Action"
whenever(mediaFlags.areMediaSessionActionsEnabled()).thenReturn(true)
whenever(mediaFlags.areMediaSessionActionsEnabled(any(), anyInt())).thenReturn(true)
whenever(controller.playbackState).thenReturn(null)
val notifWithAction = SbnBuilder().run {
@@ -621,7 +623,7 @@ class MediaDataManagerTest : SysuiTestCase() {
@Test
fun testPlaybackActions_hasPrevNext() {
val customDesc = arrayOf("custom 1", "custom 2", "custom 3", "custom 4")
whenever(mediaFlags.areMediaSessionActionsEnabled()).thenReturn(true)
whenever(mediaFlags.areMediaSessionActionsEnabled(any(), anyInt())).thenReturn(true)
val stateActions = PlaybackState.ACTION_PLAY or
PlaybackState.ACTION_SKIP_TO_PREVIOUS or
PlaybackState.ACTION_SKIP_TO_NEXT
@@ -669,7 +671,7 @@ class MediaDataManagerTest : SysuiTestCase() {
@Test
fun testPlaybackActions_noPrevNext_usesCustom() {
val customDesc = arrayOf("custom 1", "custom 2", "custom 3", "custom 4", "custom 5")
whenever(mediaFlags.areMediaSessionActionsEnabled()).thenReturn(true)
whenever(mediaFlags.areMediaSessionActionsEnabled(any(), anyInt())).thenReturn(true)
val stateActions = PlaybackState.ACTION_PLAY
val stateBuilder = PlaybackState.Builder()
.setActions(stateActions)
@@ -707,7 +709,7 @@ class MediaDataManagerTest : SysuiTestCase() {
@Test
fun testPlaybackActions_reservedSpace() {
val customDesc = arrayOf("custom 1", "custom 2", "custom 3", "custom 4")
whenever(mediaFlags.areMediaSessionActionsEnabled()).thenReturn(true)
whenever(mediaFlags.areMediaSessionActionsEnabled(any(), anyInt())).thenReturn(true)
val stateActions = PlaybackState.ACTION_PLAY
val stateBuilder = PlaybackState.Builder()
.setActions(stateActions)