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:
@@ -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";
|
||||
|
||||
@@ -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>
|
||||
|
||||
@@ -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" />
|
||||
|
||||
@@ -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);
|
||||
|
||||
@@ -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>();
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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)
|
||||
}
|
||||
|
||||
/**
|
||||
|
||||
@@ -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,
|
||||
|
||||
@@ -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"),
|
||||
|
||||
@@ -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)
|
||||
|
||||
Reference in New Issue
Block a user