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

Commit cc747ad2 authored by Jim Miller's avatar Jim Miller
Browse files

Fix scrubbing behavior on keyguard music transport

This fixes a bug where the music scrub position would snap
back to a previous position. The problem was caused by latency
in the music application responding to scrub position changes.
The latency would mean that we'd get a response periodically
to some historical scrub position change.

Since we can't know when the state will become stable, we
just wait a little bit since the last update change before
continuing to update the scroll position.

In order to keep the music client from falling behind, we
throttle scrub updates.

Fixes bug 11351267

Change-Id: I6204833328751d1da781b4e078a2d557c1f93ff3
parent 32293469
Loading
Loading
Loading
Loading
+85 −72
Original line number Diff line number Diff line
@@ -60,12 +60,12 @@ import java.util.TimeZone;
 */
public class KeyguardTransportControlView extends FrameLayout {

    private static final int DISPLAY_TIMEOUT_MS = 5000; // 5s
    private static final int RESET_TO_METADATA_DELAY = 5000;
    protected static final boolean DEBUG = false;
    protected static final String TAG = "TransportControlView";

    private static final boolean ANIMATE_TRANSITIONS = true;
    protected static final long QUIESCENT_PLAYBACK_FACTOR = 1000;

    private ViewGroup mMetadataContainer;
    private ViewGroup mInfoContainer;
@@ -89,11 +89,9 @@ public class KeyguardTransportControlView extends FrameLayout {
    private ImageView mBadge;

    private boolean mSeekEnabled;
    private boolean mUserSeeking;
    private java.text.DateFormat mFormat;

    private Date mTimeElapsed;
    private Date mTrackDuration;
    private Date mTempDate = new Date();

    /**
     * The metadata which should be populated into the view once we've been attached
@@ -111,18 +109,25 @@ public class KeyguardTransportControlView extends FrameLayout {

        @Override
        public void onClientPlaybackStateUpdate(int state) {
            setSeekBarsEnabled(false);
            updatePlayPauseState(state);
        }

        @Override
        public void onClientPlaybackStateUpdate(int state, long stateChangeTimeMs,
                long currentPosMs, float speed) {
            setSeekBarsEnabled(mMetadata != null && mMetadata.duration > 0);
            updatePlayPauseState(state);
            if (DEBUG) Log.d(TAG, "onClientPlaybackStateUpdate(state=" + state +
                    ", stateChangeTimeMs=" + stateChangeTimeMs + ", currentPosMs=" + currentPosMs +
                    ", speed=" + speed + ")");

            removeCallbacks(mUpdateSeekBars);
            // Since the music client may be responding to historical events that cause the
            // playback state to change dramatically, wait until things become quiescent before
            // resuming automatic scrub position update.
            if (mTransientSeek.getVisibility() == View.VISIBLE
                    && playbackPositionShouldMove(mCurrentPlayState)) {
                postDelayed(mUpdateSeekBars, QUIESCENT_PLAYBACK_FACTOR);
            }
        }

        @Override
@@ -136,15 +141,21 @@ public class KeyguardTransportControlView extends FrameLayout {
        }
    };

    private final Runnable mUpdateSeekBars = new Runnable() {
    private class UpdateSeekBarRunnable implements  Runnable {
        public void run() {
            if (updateSeekBars()) {
            boolean seekAble = updateOnce();
            if (seekAble) {
                removeCallbacks(this);
                postDelayed(this, 1000);
            }
        }
        public boolean updateOnce() {
            return updateSeekBars();
        }
    };

    private final UpdateSeekBarRunnable mUpdateSeekBars = new UpdateSeekBarRunnable();

    private final Runnable mResetToMetadata = new Runnable() {
        public void run() {
            resetToMetadata();
@@ -163,6 +174,7 @@ public class KeyguardTransportControlView extends FrameLayout {
            }
            if (keyCode != -1) {
                sendMediaButtonClick(keyCode);
                delayResetToMetadata(); // if the scrub bar is showing, keep showing it.
            }
        }
    };
@@ -177,25 +189,67 @@ public class KeyguardTransportControlView extends FrameLayout {
        }
    };

    // This class is here to throttle scrub position updates to the music client
    class FutureSeekRunnable implements Runnable {
        private int mProgress;
        private boolean mPending;

        public void run() {
            scrubTo(mProgress);
            mPending = false;
        }

        void setProgress(int progress) {
            mProgress = progress;
            if (!mPending) {
                mPending = true;
                postDelayed(this, 30);
            }
        }
    };

    // This is here because RemoteControlClient's method isn't visible :/
    private final static boolean playbackPositionShouldMove(int playstate) {
        switch(playstate) {
            case RemoteControlClient.PLAYSTATE_STOPPED:
            case RemoteControlClient.PLAYSTATE_PAUSED:
            case RemoteControlClient.PLAYSTATE_BUFFERING:
            case RemoteControlClient.PLAYSTATE_ERROR:
            case RemoteControlClient.PLAYSTATE_SKIPPING_FORWARDS:
            case RemoteControlClient.PLAYSTATE_SKIPPING_BACKWARDS:
                return false;
            case RemoteControlClient.PLAYSTATE_PLAYING:
            case RemoteControlClient.PLAYSTATE_FAST_FORWARDING:
            case RemoteControlClient.PLAYSTATE_REWINDING:
            default:
                return true;
        }
    }

    private final FutureSeekRunnable mFutureSeekRunnable = new FutureSeekRunnable();

    private final SeekBar.OnSeekBarChangeListener mOnSeekBarChangeListener =
            new SeekBar.OnSeekBarChangeListener() {
        @Override
        public void onProgressChanged(SeekBar seekBar, int progress, boolean fromUser) {
            if (fromUser) {
                scrubTo(progress);
                mFutureSeekRunnable.setProgress(progress);
                delayResetToMetadata();
            }
                mTempDate.setTime(progress);
                mTransientSeekTimeElapsed.setText(mFormat.format(mTempDate));
            } else {
                updateSeekDisplay();
            }
        }

        @Override
        public void onStartTrackingTouch(SeekBar seekBar) {
            mUserSeeking = true;
            delayResetToMetadata();
            removeCallbacks(mUpdateSeekBars); // don't update during user interaction
        }

        @Override
        public void onStopTrackingTouch(SeekBar seekBar) {
            mUserSeeking = false;
        }
    };

@@ -247,17 +301,11 @@ public class KeyguardTransportControlView extends FrameLayout {
        if (enabled == mSeekEnabled) return;

        mSeekEnabled = enabled;
        if (mTransientSeek.getVisibility() == VISIBLE) {
        if (mTransientSeek.getVisibility() == VISIBLE && !enabled) {
            mTransientSeek.setVisibility(INVISIBLE);
            mMetadataContainer.setVisibility(VISIBLE);
            mUserSeeking = false;
            cancelResetToMetadata();
        }
        if (enabled) {
            mUpdateSeekBars.run();
        } else {
            removeCallbacks(mUpdateSeekBars);
        }
    }

    public void setTransportControlCallback(KeyguardHostView.TransportControlCallback
@@ -294,6 +342,8 @@ public class KeyguardTransportControlView extends FrameLayout {
        }
        final boolean screenOn = KeyguardUpdateMonitor.getInstance(mContext).isScreenOn();
        setEnableMarquee(screenOn);
        // Allow long-press anywhere else in this view to show the seek bar
        setOnLongClickListener(mTransportShowSeekBarListener);
    }

    @Override
@@ -326,7 +376,6 @@ public class KeyguardTransportControlView extends FrameLayout {
        mAudioManager.unregisterRemoteController(mRemoteController);
        KeyguardUpdateMonitor.getInstance(mContext).removeCallback(mUpdateMonitor);
        mMetadata.clear();
        mUserSeeking = false;
        removeCallbacks(mUpdateSeekBars);
    }

@@ -484,18 +533,12 @@ public class KeyguardTransportControlView extends FrameLayout {

    void updateSeekDisplay() {
        if (mMetadata != null && mRemoteController != null && mFormat != null) {
            if (mTimeElapsed == null) {
                mTimeElapsed = new Date();
            }
            if (mTrackDuration == null) {
                mTrackDuration = new Date();
            }
            mTimeElapsed.setTime(mRemoteController.getEstimatedMediaPosition());
            mTrackDuration.setTime(mMetadata.duration);
            mTransientSeekTimeElapsed.setText(mFormat.format(mTimeElapsed));
            mTransientSeekTimeTotal.setText(mFormat.format(mTrackDuration));
            mTempDate.setTime(mRemoteController.getEstimatedMediaPosition());
            mTransientSeekTimeElapsed.setText(mFormat.format(mTempDate));
            mTempDate.setTime(mMetadata.duration);
            mTransientSeekTimeTotal.setText(mFormat.format(mTempDate));

            if (DEBUG) Log.d(TAG, "updateSeekDisplay timeElapsed=" + mTimeElapsed +
            if (DEBUG) Log.d(TAG, "updateSeekDisplay timeElapsed=" + mTempDate +
                    " duration=" + mMetadata.duration);
        }
    }
@@ -508,10 +551,16 @@ public class KeyguardTransportControlView extends FrameLayout {
            mTransientSeek.setVisibility(INVISIBLE);
            mMetadataContainer.setVisibility(VISIBLE);
            cancelResetToMetadata();
            removeCallbacks(mUpdateSeekBars); // don't update if scrubber isn't visible
        } else {
            mTransientSeek.setVisibility(VISIBLE);
            mMetadataContainer.setVisibility(INVISIBLE);
            delayResetToMetadata();
            if (playbackPositionShouldMove(mCurrentPlayState)) {
                mUpdateSeekBars.run();
            } else {
                mUpdateSeekBars.updateOnce();
            }
        }
        mTransportControlCallback.userActivity();
        return true;
@@ -573,9 +622,6 @@ public class KeyguardTransportControlView extends FrameLayout {
            case RemoteControlClient.PLAYSTATE_PLAYING:
                imageResId = R.drawable.ic_media_pause;
                imageDescId = R.string.keyguard_transport_pause_description;
                if (mSeekEnabled) {
                    mUpdateSeekBars.run();
                }
                break;

            case RemoteControlClient.PLAYSTATE_BUFFERING:
@@ -590,10 +636,9 @@ public class KeyguardTransportControlView extends FrameLayout {
                break;
        }

        if (state != RemoteControlClient.PLAYSTATE_PLAYING) {
            removeCallbacks(mUpdateSeekBars);
            updateSeekBars();
        }
        boolean clientSupportsSeek = mMetadata != null && mMetadata.duration > 0;
        setSeekBarsEnabled(clientSupportsSeek);

        mBtnPlay.setImageResource(imageResId);
        mBtnPlay.setContentDescription(getResources().getString(imageDescId));
        mCurrentPlayState = state;
@@ -601,11 +646,9 @@ public class KeyguardTransportControlView extends FrameLayout {

    boolean updateSeekBars() {
        final int position = (int) mRemoteController.getEstimatedMediaPosition();
        if (DEBUG) Log.v(TAG, "Estimated time:" + position);
        if (position >= 0) {
            if (DEBUG) Log.v(TAG, "Seek to " + position);
            if (!mUserSeeking) {
            mTransientSeekBar.setProgress(position);
            }
            return true;
        }
        Log.w(TAG, "Updating seek bars; received invalid estimated media position (" +
@@ -671,34 +714,4 @@ public class KeyguardTransportControlView extends FrameLayout {
    public boolean providesClock() {
        return false;
    }

    private boolean wasPlayingRecently(int state, long stateChangeTimeMs) {
        switch (state) {
            case RemoteControlClient.PLAYSTATE_PLAYING:
            case RemoteControlClient.PLAYSTATE_FAST_FORWARDING:
            case RemoteControlClient.PLAYSTATE_REWINDING:
            case RemoteControlClient.PLAYSTATE_SKIPPING_FORWARDS:
            case RemoteControlClient.PLAYSTATE_SKIPPING_BACKWARDS:
            case RemoteControlClient.PLAYSTATE_BUFFERING:
                // actively playing or about to play
                return true;
            case RemoteControlClient.PLAYSTATE_NONE:
                return false;
            case RemoteControlClient.PLAYSTATE_STOPPED:
            case RemoteControlClient.PLAYSTATE_PAUSED:
            case RemoteControlClient.PLAYSTATE_ERROR:
                // we have stopped playing, check how long ago
                if (DEBUG) {
                    if ((SystemClock.elapsedRealtime() - stateChangeTimeMs) < DISPLAY_TIMEOUT_MS) {
                        Log.v(TAG, "wasPlayingRecently: time < TIMEOUT was playing recently");
                    } else {
                        Log.v(TAG, "wasPlayingRecently: time > TIMEOUT");
                    }
                }
                return ((SystemClock.elapsedRealtime() - stateChangeTimeMs) < DISPLAY_TIMEOUT_MS);
            default:
                Log.e(TAG, "Unknown playback state " + state + " in wasPlayingRecently()");
                return false;
        }
    }
}