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
This commit is contained in:
Jim Miller
2013-10-24 19:53:09 -07:00
parent 3229346994
commit cc747ad2da

View File

@@ -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();
}
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);
}
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;
}
}
}