diff options
6 files changed, 225 insertions, 68 deletions
diff --git a/media/java/android/media/session/ISessionManager.aidl b/media/java/android/media/session/ISessionManager.aidl index 207ccbee0b50..871e9ab87299 100644 --- a/media/java/android/media/session/ISessionManager.aidl +++ b/media/java/android/media/session/ISessionManager.aidl @@ -80,4 +80,7 @@ interface ISessionManager { boolean hasCustomMediaSessionPolicyProvider(String componentName); int getSessionPolicies(in MediaSession.Token token); void setSessionPolicies(in MediaSession.Token token, int policies); + + // For testing of temporarily engaged sessions. + void expireTempEngagedSessions(); } diff --git a/services/core/java/com/android/server/media/MediaSession2Record.java b/services/core/java/com/android/server/media/MediaSession2Record.java index 0cd7654f70ea..dfb2b0a750e3 100644 --- a/services/core/java/com/android/server/media/MediaSession2Record.java +++ b/services/core/java/com/android/server/media/MediaSession2Record.java @@ -157,6 +157,11 @@ public class MediaSession2Record extends MediaSessionRecordImpl { } @Override + public void expireTempEngaged() { + // NA as MediaSession2 doesn't support UserEngagementStates for FGS. + } + + @Override public boolean sendMediaButton(String packageName, int pid, int uid, boolean asSystemService, KeyEvent ke, int sequenceId, ResultReceiver cb) { // TODO(jaewan): Implement. diff --git a/services/core/java/com/android/server/media/MediaSessionRecord.java b/services/core/java/com/android/server/media/MediaSessionRecord.java index 62a947123ddf..8f164d361a56 100644 --- a/services/core/java/com/android/server/media/MediaSessionRecord.java +++ b/services/core/java/com/android/server/media/MediaSessionRecord.java @@ -24,6 +24,7 @@ import static android.media.session.MediaController.PlaybackInfo.PLAYBACK_TYPE_L import static android.media.session.MediaController.PlaybackInfo.PLAYBACK_TYPE_REMOTE; import android.Manifest; +import android.annotation.IntDef; import android.annotation.NonNull; import android.annotation.Nullable; import android.annotation.RequiresPermission; @@ -85,6 +86,8 @@ import com.android.server.LocalServices; import com.android.server.uri.UriGrantsManagerInternal; import java.io.PrintWriter; +import java.lang.annotation.Retention; +import java.lang.annotation.RetentionPolicy; import java.util.ArrayList; import java.util.Arrays; import java.util.Collection; @@ -225,6 +228,49 @@ public class MediaSessionRecord extends MediaSessionRecordImpl implements IBinde private int mPolicies; + private @UserEngagementState int mUserEngagementState = USER_DISENGAGED; + + @IntDef({USER_PERMANENTLY_ENGAGED, USER_TEMPORARY_ENGAGED, USER_DISENGAGED}) + @Retention(RetentionPolicy.SOURCE) + private @interface UserEngagementState {} + + /** + * Indicates that the session is active and in one of the user engaged states. + * + * @see #updateUserEngagedStateIfNeededLocked(boolean) () + */ + private static final int USER_PERMANENTLY_ENGAGED = 0; + + /** + * Indicates that the session is active and in {@link PlaybackState#STATE_PAUSED} state. + * + * @see #updateUserEngagedStateIfNeededLocked(boolean) () + */ + private static final int USER_TEMPORARY_ENGAGED = 1; + + /** + * Indicates that the session is either not active or in one of the user disengaged states + * + * @see #updateUserEngagedStateIfNeededLocked(boolean) () + */ + private static final int USER_DISENGAGED = 2; + + /** + * Indicates the duration of the temporary engaged states. + * + * <p>Some {@link MediaSession} states like {@link PlaybackState#STATE_PAUSED} are temporarily + * engaged, meaning the corresponding session is only considered in an engaged state for the + * duration of this timeout, and only if coming from an engaged state. + * + * <p>For example, if a session is transitioning from a user-engaged state {@link + * PlaybackState#STATE_PLAYING} to a temporary user-engaged state {@link + * PlaybackState#STATE_PAUSED}, then the session will be considered in a user-engaged state for + * the duration of this timeout, starting at the transition instant. However, a temporary + * user-engaged state is not considered user-engaged when transitioning from a non-user engaged + * state {@link PlaybackState#STATE_STOPPED}. + */ + private static final int TEMP_USER_ENGAGED_TIMEOUT = 600000; + public MediaSessionRecord( int ownerPid, int ownerUid, @@ -548,6 +594,7 @@ public class MediaSessionRecord extends MediaSessionRecordImpl implements IBinde mSessionCb.mCb.asBinder().unlinkToDeath(this, 0); mDestroyed = true; mPlaybackState = null; + updateUserEngagedStateIfNeededLocked(/* isTimeoutExpired= */ true); mHandler.post(MessageHandler.MSG_DESTROYED); } } @@ -559,6 +606,12 @@ public class MediaSessionRecord extends MediaSessionRecordImpl implements IBinde } } + @Override + public void expireTempEngaged() { + mHandler.removeCallbacks(mHandleTempEngagedSessionTimeout); + updateUserEngagedStateIfNeededLocked(/* isTimeoutExpired= */ true); + } + /** * Sends media button. * @@ -1129,6 +1182,11 @@ public class MediaSessionRecord extends MediaSessionRecordImpl implements IBinde } }; + private final Runnable mHandleTempEngagedSessionTimeout = + () -> { + updateUserEngagedStateIfNeededLocked(/* isTimeoutExpired= */ true); + }; + @RequiresPermission(Manifest.permission.INTERACT_ACROSS_USERS) private static boolean componentNameExists( @NonNull ComponentName componentName, @NonNull Context context, int userId) { @@ -1145,6 +1203,40 @@ public class MediaSessionRecord extends MediaSessionRecordImpl implements IBinde return !resolveInfos.isEmpty(); } + private void updateUserEngagedStateIfNeededLocked(boolean isTimeoutExpired) { + int oldUserEngagedState = mUserEngagementState; + int newUserEngagedState; + if (!isActive() || mPlaybackState == null) { + newUserEngagedState = USER_DISENGAGED; + } else if (isActive() && mPlaybackState.isActive()) { + newUserEngagedState = USER_PERMANENTLY_ENGAGED; + } else if (mPlaybackState.getState() == PlaybackState.STATE_PAUSED) { + newUserEngagedState = + oldUserEngagedState == USER_PERMANENTLY_ENGAGED || !isTimeoutExpired + ? USER_TEMPORARY_ENGAGED + : USER_DISENGAGED; + } else { + newUserEngagedState = USER_DISENGAGED; + } + if (oldUserEngagedState == newUserEngagedState) { + return; + } + + if (newUserEngagedState == USER_TEMPORARY_ENGAGED) { + mHandler.postDelayed(mHandleTempEngagedSessionTimeout, TEMP_USER_ENGAGED_TIMEOUT); + } else if (oldUserEngagedState == USER_TEMPORARY_ENGAGED) { + mHandler.removeCallbacks(mHandleTempEngagedSessionTimeout); + } + + boolean wasUserEngaged = oldUserEngagedState != USER_DISENGAGED; + boolean isNowUserEngaged = newUserEngagedState != USER_DISENGAGED; + mUserEngagementState = newUserEngagedState; + if (wasUserEngaged != isNowUserEngaged) { + mService.onSessionUserEngagementStateChange( + /* mediaSessionRecord= */ this, /* isUserEngaged= */ isNowUserEngaged); + } + } + private final class SessionStub extends ISession.Stub { @Override public void destroySession() throws RemoteException { @@ -1182,8 +1274,10 @@ public class MediaSessionRecord extends MediaSessionRecordImpl implements IBinde .logFgsApiEnd(ActivityManager.FOREGROUND_SERVICE_API_TYPE_MEDIA_PLAYBACK, callingUid, callingPid); } - - mIsActive = active; + synchronized (mLock) { + mIsActive = active; + updateUserEngagedStateIfNeededLocked(/* isTimeoutExpired= */ false); + } long token = Binder.clearCallingIdentity(); try { mService.onSessionActiveStateChanged(MediaSessionRecord.this, mPlaybackState); @@ -1341,6 +1435,7 @@ public class MediaSessionRecord extends MediaSessionRecordImpl implements IBinde && TRANSITION_PRIORITY_STATES.contains(newState)); synchronized (mLock) { mPlaybackState = state; + updateUserEngagedStateIfNeededLocked(/* isTimeoutExpired= */ false); } final long token = Binder.clearCallingIdentity(); try { diff --git a/services/core/java/com/android/server/media/MediaSessionRecordImpl.java b/services/core/java/com/android/server/media/MediaSessionRecordImpl.java index 09991995099e..b57b14835987 100644 --- a/services/core/java/com/android/server/media/MediaSessionRecordImpl.java +++ b/services/core/java/com/android/server/media/MediaSessionRecordImpl.java @@ -196,6 +196,12 @@ public abstract class MediaSessionRecordImpl { */ public abstract boolean isClosed(); + /** + * Note: This method is only used for testing purposes If the session is temporary engaged, the + * timeout will expire and it will become disengaged. + */ + public abstract void expireTempEngaged(); + @Override public final boolean equals(Object o) { if (this == o) return true; diff --git a/services/core/java/com/android/server/media/MediaSessionService.java b/services/core/java/com/android/server/media/MediaSessionService.java index 53c32cf31d21..74adf5e0d52c 100644 --- a/services/core/java/com/android/server/media/MediaSessionService.java +++ b/services/core/java/com/android/server/media/MediaSessionService.java @@ -367,11 +367,13 @@ public class MediaSessionService extends SystemService implements Monitor { } boolean isUserEngaged = isUserEngaged(record, playbackState); - Log.d(TAG, "onSessionActiveStateChanged: " - + "record=" + record - + "playbackState=" + playbackState - + "allowRunningInForeground=" + isUserEngaged); - setForegroundServiceAllowance(record, /* allowRunningInForeground= */ isUserEngaged); + Log.d( + TAG, + "onSessionActiveStateChanged:" + + " record=" + + record + + " playbackState=" + + playbackState); reportMediaInteractionEvent(record, isUserEngaged); mHandler.postSessionsChanged(record); } @@ -479,11 +481,13 @@ public class MediaSessionService extends SystemService implements Monitor { } user.mPriorityStack.onPlaybackStateChanged(record, shouldUpdatePriority); boolean isUserEngaged = isUserEngaged(record, playbackState); - Log.d(TAG, "onSessionPlaybackStateChanged: " - + "record=" + record - + "playbackState=" + playbackState - + "allowRunningInForeground=" + isUserEngaged); - setForegroundServiceAllowance(record, /* allowRunningInForeground= */ isUserEngaged); + Log.d( + TAG, + "onSessionPlaybackStateChanged:" + + " record=" + + record + + " playbackState=" + + playbackState); reportMediaInteractionEvent(record, isUserEngaged); } } @@ -650,68 +654,112 @@ public class MediaSessionService extends SystemService implements Monitor { session.close(); Log.d(TAG, "destroySessionLocked: record=" + session); - setForegroundServiceAllowance(session, /* allowRunningInForeground= */ false); + reportMediaInteractionEvent(session, /* userEngaged= */ false); mHandler.postSessionsChanged(session); } - private void setForegroundServiceAllowance( - MediaSessionRecordImpl record, boolean allowRunningInForeground) { - if (!Flags.enableNotifyingActivityManagerWithMediaSessionStatusChange()) { - return; - } - ForegroundServiceDelegationOptions foregroundServiceDelegationOptions = - record.getForegroundServiceDelegationOptions(); - if (foregroundServiceDelegationOptions == null) { - return; - } - if (allowRunningInForeground) { - onUserSessionEngaged(record); + void onSessionUserEngagementStateChange( + MediaSessionRecordImpl mediaSessionRecord, boolean isUserEngaged) { + if (isUserEngaged) { + addUserEngagedSession(mediaSessionRecord); + startFgsIfSessionIsLinkedToNotification(mediaSessionRecord); } else { - onUserDisengaged(record); + removeUserEngagedSession(mediaSessionRecord); + stopFgsIfNoSessionIsLinkedToNotification(mediaSessionRecord); } } - private void onUserSessionEngaged(MediaSessionRecordImpl mediaSessionRecord) { + private void addUserEngagedSession(MediaSessionRecordImpl mediaSessionRecord) { synchronized (mLock) { int uid = mediaSessionRecord.getUid(); mUserEngagedSessionsForFgs.putIfAbsent(uid, new HashSet<>()); mUserEngagedSessionsForFgs.get(uid).add(mediaSessionRecord); + } + } + + private void removeUserEngagedSession(MediaSessionRecordImpl mediaSessionRecord) { + synchronized (mLock) { + int uid = mediaSessionRecord.getUid(); + Set<MediaSessionRecordImpl> mUidUserEngagedSessionsForFgs = + mUserEngagedSessionsForFgs.get(uid); + if (mUidUserEngagedSessionsForFgs == null) { + return; + } + + mUidUserEngagedSessionsForFgs.remove(mediaSessionRecord); + if (mUidUserEngagedSessionsForFgs.isEmpty()) { + mUserEngagedSessionsForFgs.remove(uid); + } + } + } + + private void startFgsIfSessionIsLinkedToNotification( + MediaSessionRecordImpl mediaSessionRecord) { + Log.d(TAG, "startFgsIfSessionIsLinkedToNotification: record=" + mediaSessionRecord); + if (!Flags.enableNotifyingActivityManagerWithMediaSessionStatusChange()) { + return; + } + synchronized (mLock) { + int uid = mediaSessionRecord.getUid(); for (Notification mediaNotification : mMediaNotifications.getOrDefault(uid, Set.of())) { if (mediaSessionRecord.isLinkedToNotification(mediaNotification)) { - mActivityManagerInternal.startForegroundServiceDelegate( - mediaSessionRecord.getForegroundServiceDelegationOptions(), - /* connection= */ null); + startFgsDelegate(mediaSessionRecord.getForegroundServiceDelegationOptions()); return; } } } } - private void onUserDisengaged(MediaSessionRecordImpl mediaSessionRecord) { + private void startFgsDelegate( + ForegroundServiceDelegationOptions foregroundServiceDelegationOptions) { + final long token = Binder.clearCallingIdentity(); + try { + mActivityManagerInternal.startForegroundServiceDelegate( + foregroundServiceDelegationOptions, /* connection= */ null); + } finally { + Binder.restoreCallingIdentity(token); + } + } + + private void stopFgsIfNoSessionIsLinkedToNotification( + MediaSessionRecordImpl mediaSessionRecord) { + Log.d(TAG, "stopFgsIfNoSessionIsLinkedToNotification: record=" + mediaSessionRecord); + if (!Flags.enableNotifyingActivityManagerWithMediaSessionStatusChange()) { + return; + } synchronized (mLock) { int uid = mediaSessionRecord.getUid(); - if (mUserEngagedSessionsForFgs.containsKey(uid)) { - mUserEngagedSessionsForFgs.get(uid).remove(mediaSessionRecord); - if (mUserEngagedSessionsForFgs.get(uid).isEmpty()) { - mUserEngagedSessionsForFgs.remove(uid); - } + ForegroundServiceDelegationOptions foregroundServiceDelegationOptions = + mediaSessionRecord.getForegroundServiceDelegationOptions(); + if (foregroundServiceDelegationOptions == null) { + return; } - boolean shouldStopFgs = true; - for (MediaSessionRecordImpl sessionRecord : + for (MediaSessionRecordImpl record : mUserEngagedSessionsForFgs.getOrDefault(uid, Set.of())) { - for (Notification mediaNotification : mMediaNotifications.getOrDefault(uid, - Set.of())) { - if (sessionRecord.isLinkedToNotification(mediaNotification)) { - shouldStopFgs = false; + for (Notification mediaNotification : + mMediaNotifications.getOrDefault(uid, Set.of())) { + if (record.isLinkedToNotification(mediaNotification)) { + // A user engaged session linked with a media notification is found. + // We shouldn't call stop FGS in this case. + return; } } } - if (shouldStopFgs) { - mActivityManagerInternal.stopForegroundServiceDelegate( - mediaSessionRecord.getForegroundServiceDelegationOptions()); - } + + stopFgsDelegate(foregroundServiceDelegationOptions); + } + } + + private void stopFgsDelegate( + ForegroundServiceDelegationOptions foregroundServiceDelegationOptions) { + final long token = Binder.clearCallingIdentity(); + try { + mActivityManagerInternal.stopForegroundServiceDelegate( + foregroundServiceDelegationOptions); + } finally { + Binder.restoreCallingIdentity(token); } } @@ -2502,7 +2550,6 @@ public class MediaSessionService extends SystemService implements Monitor { } MediaSessionRecord session = null; MediaButtonReceiverHolder mediaButtonReceiverHolder = null; - if (mCustomMediaKeyDispatcher != null) { MediaSession.Token token = mCustomMediaKeyDispatcher.getMediaSession( keyEvent, uid, asSystemService); @@ -2630,6 +2677,18 @@ public class MediaSessionService extends SystemService implements Monitor { && streamType <= AudioManager.STREAM_NOTIFICATION; } + @Override + public void expireTempEngagedSessions() { + synchronized (mLock) { + for (Set<MediaSessionRecordImpl> uidSessions : + mUserEngagedSessionsForFgs.values()) { + for (MediaSessionRecordImpl sessionRecord : uidSessions) { + sessionRecord.expireTempEngaged(); + } + } + } + } + private class MediaKeyListenerResultReceiver extends ResultReceiver implements Runnable { private final String mPackageName; private final int mPid; @@ -3127,7 +3186,6 @@ public class MediaSessionService extends SystemService implements Monitor { super.onNotificationPosted(sbn); Notification postedNotification = sbn.getNotification(); int uid = sbn.getUid(); - if (!postedNotification.isMediaNotification()) { return; } @@ -3138,11 +3196,9 @@ public class MediaSessionService extends SystemService implements Monitor { mUserEngagedSessionsForFgs.getOrDefault(uid, Set.of())) { ForegroundServiceDelegationOptions foregroundServiceDelegationOptions = mediaSessionRecord.getForegroundServiceDelegationOptions(); - if (mediaSessionRecord.isLinkedToNotification(postedNotification) - && foregroundServiceDelegationOptions != null) { - mActivityManagerInternal.startForegroundServiceDelegate( - foregroundServiceDelegationOptions, - /* connection= */ null); + if (foregroundServiceDelegationOptions != null + && mediaSessionRecord.isLinkedToNotification(postedNotification)) { + startFgsDelegate(foregroundServiceDelegationOptions); return; } } @@ -3173,21 +3229,7 @@ public class MediaSessionService extends SystemService implements Monitor { return; } - boolean shouldStopFgs = true; - for (MediaSessionRecordImpl mediaSessionRecord : - mUserEngagedSessionsForFgs.getOrDefault(uid, Set.of())) { - for (Notification mediaNotification : - mMediaNotifications.getOrDefault(uid, Set.of())) { - if (mediaSessionRecord.isLinkedToNotification(mediaNotification)) { - shouldStopFgs = false; - } - } - } - if (shouldStopFgs - && notificationRecord.getForegroundServiceDelegationOptions() != null) { - mActivityManagerInternal.stopForegroundServiceDelegate( - notificationRecord.getForegroundServiceDelegationOptions()); - } + stopFgsIfNoSessionIsLinkedToNotification(notificationRecord); } } diff --git a/services/core/java/com/android/server/media/MediaShellCommand.java b/services/core/java/com/android/server/media/MediaShellCommand.java index a56380827f2c..a20de3198d2c 100644 --- a/services/core/java/com/android/server/media/MediaShellCommand.java +++ b/services/core/java/com/android/server/media/MediaShellCommand.java @@ -92,6 +92,8 @@ public class MediaShellCommand extends ShellCommand { runMonitor(); } else if (cmd.equals("volume")) { runVolume(); + } else if (cmd.equals("expire-temp-engaged-sessions")) { + expireTempEngagedSessions(); } else { showError("Error: unknown command '" + cmd + "'"); return -1; @@ -367,4 +369,8 @@ public class MediaShellCommand extends ShellCommand { private void runVolume() throws Exception { VolumeCtrl.run(this); } + + private void expireTempEngagedSessions() throws Exception { + mSessionService.expireTempEngagedSessions(); + } } |