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

Commit b6e19667 authored by Ian Baker's avatar Ian Baker
Browse files

Add locking around MediaSession.Callback.mSession

Use the same lock to guard the handler.removeCallbacksAndMessages call
too.

Bug: 430063286
Test: MediaControllerTest
Flag: EXEMPT bugfix
Change-Id: I23c8a268a242856df7325a7c8d14b63f5fba2656
parent e26dbb2a
Loading
Loading
Loading
Loading
+126 −87
Original line number Diff line number Diff line
@@ -240,15 +240,15 @@ public final class MediaSession {
        synchronized (mLock) {
            if (mCallback != null) {
                // We're updating the callback, clear the session from the old one.
                mCallback.mCallback.mSession = null;
                mCallback.removeCallbacksAndMessages(null);
                mCallback.mCallback.clearSession();

            }
            if (callback == null) {
                mCallback = null;
                return;
            }
            Looper looper = handler != null ? handler.getLooper() : Looper.myLooper();
            callback.mSession = this;
            callback.setSession(this);
            CallbackMessageHandler msgHandler = new CallbackMessageHandler(looper, callback);
            mCallback = msgHandler;
        }
@@ -916,6 +916,7 @@ public final class MediaSession {
     */
    public abstract static class Callback {

        private final Object mSessionLock = new Object();
        private MediaSession mSession;
        private CallbackMessageHandler mHandler;
        private boolean mMediaPlayPauseKeyPending;
@@ -923,6 +924,20 @@ public final class MediaSession {
        public Callback() {
        }

        private void setSession(MediaSession session) {
            synchronized (mSessionLock) {
                mSession = session;
            }
        }

        private void clearSession() {
            synchronized (mSessionLock) {
                mSession = null;
                mMediaPlayPauseKeyPending = false;
                mHandler.removeCallbacksAndMessages(null);
            }
        }

        /**
         * Called when a controller has sent a command to this session.
         * The owner of the session may handle custom commands but is not
@@ -950,25 +965,39 @@ public final class MediaSession {
         * @return True if the event was handled, false otherwise.
         */
        public boolean onMediaButtonEvent(@NonNull Intent mediaButtonIntent) {
            if (mSession != null && mHandler != null
                    && Intent.ACTION_MEDIA_BUTTON.equals(mediaButtonIntent.getAction())) {
                KeyEvent ke = mediaButtonIntent.getParcelableExtra(Intent.EXTRA_KEY_EVENT, android.view.KeyEvent.class);
                if (ke != null && ke.getAction() == KeyEvent.ACTION_DOWN) {
            if (mHandler == null
                    || !Intent.ACTION_MEDIA_BUTTON.equals(mediaButtonIntent.getAction())) {
                return false;
            }
            KeyEvent ke = mediaButtonIntent.getParcelableExtra(
                    Intent.EXTRA_KEY_EVENT, android.view.KeyEvent.class);
            if (ke == null || ke.getAction() != KeyEvent.ACTION_DOWN) {
                return false;
            }
            // Store some results into locals so we can invoke any app-implemented methods (like
            // onSkipToNext()) outside the synchronized block and avoid the risk of a deadlock.
            long validActions;
            boolean eventHandled/* = false*/;
            Runnable doubleTapHandlingAction = null;
            synchronized (mSessionLock) {
                if (mSession == null) {
                    return false;
                }
                PlaybackState state = mSession.mPlaybackState;
                    long validActions = state == null ? 0 : state.getActions();
                validActions = state == null ? 0 : state.getActions();
                switch (ke.getKeyCode()) {
                        case KeyEvent.KEYCODE_MEDIA_PLAY_PAUSE:
                        case KeyEvent.KEYCODE_HEADSETHOOK:
                    case KeyEvent.KEYCODE_MEDIA_PLAY_PAUSE, KeyEvent.KEYCODE_HEADSETHOOK -> {
                        if (ke.getRepeatCount() > 0) {
                            // Consider long-press as a single tap.
                                handleMediaPlayPauseKeySingleTapIfPending();
                            doubleTapHandlingAction =
                                    this::handleMediaPlayPauseKeySingleTapIfPending;
                        } else if (mMediaPlayPauseKeyPending) {
                            // Consider double tap as the next.
                            mHandler.removeMessages(CallbackMessageHandler
                                    .MSG_PLAY_PAUSE_KEY_DOUBLE_TAP_TIMEOUT);
                            mMediaPlayPauseKeyPending = false;
                            if ((validActions & PlaybackState.ACTION_SKIP_TO_NEXT) != 0) {
                                    onSkipToNext();
                                doubleTapHandlingAction = this::onSkipToNext;
                            }
                        } else {
                            mMediaPlayPauseKeyPending = true;
@@ -980,70 +1009,80 @@ public final class MediaSession {
                                            .getDoubleTapTimeoutMillis()
                                            : ViewConfiguration.getDoubleTapTimeout());
                        }
                            return true;
                        default:
                        eventHandled = true;
                    }
                    default -> {
                        // If another key is pressed within double tap timeout, consider the
                        // pending play/pause as a single tap to handle media keys in order.
                            handleMediaPlayPauseKeySingleTapIfPending();
                            break;
                        doubleTapHandlingAction = this::handleMediaPlayPauseKeySingleTapIfPending;
                        eventHandled = false;
                    }
                }
            }
            if (doubleTapHandlingAction != null) {
                doubleTapHandlingAction.run();
            }
            if (eventHandled) {
                return true;
            }

            switch (ke.getKeyCode()) {
                        case KeyEvent.KEYCODE_MEDIA_PLAY:
                case KeyEvent.KEYCODE_MEDIA_PLAY -> {
                    if ((validActions & PlaybackState.ACTION_PLAY) != 0) {
                        onPlay();
                        return true;
                    }
                            break;
                        case KeyEvent.KEYCODE_MEDIA_PAUSE:
                }
                case KeyEvent.KEYCODE_MEDIA_PAUSE -> {
                    if ((validActions & PlaybackState.ACTION_PAUSE) != 0) {
                        onPause();
                        return true;
                    }
                            break;
                        case KeyEvent.KEYCODE_MEDIA_NEXT:
                }
                case KeyEvent.KEYCODE_MEDIA_NEXT -> {
                    if ((validActions & PlaybackState.ACTION_SKIP_TO_NEXT) != 0) {
                        onSkipToNext();
                        return true;
                    }
                            break;
                        case KeyEvent.KEYCODE_MEDIA_PREVIOUS:
                }
                case KeyEvent.KEYCODE_MEDIA_PREVIOUS -> {
                    if ((validActions & PlaybackState.ACTION_SKIP_TO_PREVIOUS) != 0) {
                        onSkipToPrevious();
                        return true;
                    }
                            break;
                        case KeyEvent.KEYCODE_MEDIA_STOP:
                }
                case KeyEvent.KEYCODE_MEDIA_STOP -> {
                    if ((validActions & PlaybackState.ACTION_STOP) != 0) {
                        onStop();
                        return true;
                    }
                            break;
                        case KeyEvent.KEYCODE_MEDIA_FAST_FORWARD:
                }
                case KeyEvent.KEYCODE_MEDIA_FAST_FORWARD -> {
                    if ((validActions & PlaybackState.ACTION_FAST_FORWARD) != 0) {
                        onFastForward();
                        return true;
                    }
                            break;
                        case KeyEvent.KEYCODE_MEDIA_REWIND:
                }
                case KeyEvent.KEYCODE_MEDIA_REWIND -> {
                    if ((validActions & PlaybackState.ACTION_REWIND) != 0) {
                        onRewind();
                        return true;
                    }
                            break;
                    }
                }
            }
            return false;
        }

        private void handleMediaPlayPauseKeySingleTapIfPending() {
            if (!mMediaPlayPauseKeyPending) {
            PlaybackState state;
            synchronized (mSessionLock) {
                if (!mMediaPlayPauseKeyPending || mSession == null) {
                    return;
                }
                mMediaPlayPauseKeyPending = false;
            mHandler.removeMessages(CallbackMessageHandler.MSG_PLAY_PAUSE_KEY_DOUBLE_TAP_TIMEOUT);
            PlaybackState state = mSession.mPlaybackState;
                mHandler.removeMessages(
                        CallbackMessageHandler.MSG_PLAY_PAUSE_KEY_DOUBLE_TAP_TIMEOUT);
                state = mSession.mPlaybackState;
            }
            long validActions = state == null ? 0 : state.getActions();
            boolean isPlaying = state != null
                    && state.getState() == PlaybackState.STATE_PLAYING;