Donate to e Foundation | Murena handsets with /e/OS | Own a part of Murena! Learn more

Commit e60a53a8 authored by Bishoy Gendy's avatar Bishoy Gendy
Browse files

Implement Paused state logic for FGS

- This also handling the missing permssion exception in b/333370745
which caused the initial CL to be reverted.

Bug: 295518668
Bug: 333370745
Test: atest cts/tests/app/src/android/app/cts/ActivityManagerFgsDelegateTest.java
Test: atest PlatformScenarioTests:android.platform.test.scenario.sysui.media.QSMediaControllerPlayAndPause#testPlayAndPause
Test: Using MediaRouter SampleApp
Change-Id: I19b832d53550935c56e52e5e35c10d8c10c57a4c
parent 84f78524
Loading
Loading
Loading
Loading
+3 −0
Original line number Diff line number Diff line
@@ -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();
}
+5 −0
Original line number Diff line number Diff line
@@ -156,6 +156,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) {
+97 −2
Original line number Diff line number Diff line
@@ -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);
            }

            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 {
+6 −0
Original line number Diff line number Diff line
@@ -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;
+108 −66
Original line number Diff line number Diff line
@@ -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,69 +654,113 @@ 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);
        }
    }

    private void reportMediaInteractionEvent(MediaSessionRecordImpl record, boolean userEngaged) {
@@ -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);
            }
        }

Loading