Created
September 14, 2014 03:37
-
-
Save shaobin0604/6376efbf9096eb708212 to your computer and use it in GitHub Desktop.
TransportControlView
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
/* | |
* Copyright (C) 2011 The Android Open Source Project | |
* | |
* Licensed under the Apache License, Version 2.0 (the "License"); | |
* you may not use this file except in compliance with the License. | |
* You may obtain a copy of the License at | |
* | |
* http://www.apache.org/licenses/LICENSE-2.0 | |
* | |
* Unless required by applicable law or agreed to in writing, software | |
* distributed under the License is distributed on an "AS IS" BASIS, | |
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. | |
* See the License for the specific language governing permissions and | |
* limitations under the License. | |
*/ | |
package com.android.internal.widget; | |
import java.lang.ref.WeakReference; | |
import com.android.internal.widget.LockScreenWidgetCallback; | |
import com.android.internal.widget.LockScreenWidgetInterface; | |
import android.app.PendingIntent; | |
import android.app.PendingIntent.CanceledException; | |
import android.content.ComponentName; | |
import android.content.Context; | |
import android.content.Intent; | |
import android.graphics.Bitmap; | |
import android.media.AudioManager; | |
import android.media.MediaMetadataRetriever; | |
import android.media.RemoteControlClient; | |
import android.media.IRemoteControlDisplay; | |
import android.os.Bundle; | |
import android.os.Handler; | |
import android.os.Message; | |
import android.os.Parcel; | |
import android.os.Parcelable; | |
import android.os.RemoteException; | |
import android.os.SystemClock; | |
import android.text.Spannable; | |
import android.text.TextUtils; | |
import android.text.style.ForegroundColorSpan; | |
import android.util.AttributeSet; | |
import android.util.Log; | |
import android.view.KeyEvent; | |
import android.view.View; | |
import android.view.View.OnClickListener; | |
import android.widget.FrameLayout; | |
import android.widget.ImageView; | |
import android.widget.TextView; | |
import com.android.internal.R; | |
public class TransportControlView extends FrameLayout implements OnClickListener, | |
LockScreenWidgetInterface { | |
private static final int MSG_UPDATE_STATE = 100; | |
private static final int MSG_SET_METADATA = 101; | |
private static final int MSG_SET_TRANSPORT_CONTROLS = 102; | |
private static final int MSG_SET_ARTWORK = 103; | |
private static final int MSG_SET_GENERATION_ID = 104; | |
private static final int MAXDIM = 512; | |
private static final int DISPLAY_TIMEOUT_MS = 5000; // 5s | |
protected static final boolean DEBUG = false; | |
protected static final String TAG = "TransportControlView"; | |
private ImageView mAlbumArt; | |
private TextView mTrackTitle; | |
private ImageView mBtnPrev; | |
private ImageView mBtnPlay; | |
private ImageView mBtnNext; | |
private int mClientGeneration; | |
private Metadata mMetadata = new Metadata(); | |
private boolean mAttached; | |
private PendingIntent mClientIntent; | |
private int mTransportControlFlags; | |
private int mCurrentPlayState; | |
private AudioManager mAudioManager; | |
private LockScreenWidgetCallback mWidgetCallbacks; | |
private IRemoteControlDisplayWeak mIRCD; | |
/** | |
* The metadata which should be populated into the view once we've been attached | |
*/ | |
private Bundle mPopulateMetadataWhenAttached = null; | |
// This handler is required to ensure messages from IRCD are handled in sequence and on | |
// the UI thread. | |
private Handler mHandler = new Handler() { | |
@Override | |
public void handleMessage(Message msg) { | |
switch (msg.what) { | |
case MSG_UPDATE_STATE: | |
if (mClientGeneration == msg.arg1) updatePlayPauseState(msg.arg2); | |
break; | |
case MSG_SET_METADATA: | |
if (mClientGeneration == msg.arg1) updateMetadata((Bundle) msg.obj); | |
break; | |
case MSG_SET_TRANSPORT_CONTROLS: | |
if (mClientGeneration == msg.arg1) updateTransportControls(msg.arg2); | |
break; | |
case MSG_SET_ARTWORK: | |
if (mClientGeneration == msg.arg1) { | |
if (mMetadata.bitmap != null) { | |
mMetadata.bitmap.recycle(); | |
} | |
mMetadata.bitmap = (Bitmap) msg.obj; | |
mAlbumArt.setImageBitmap(mMetadata.bitmap); | |
} | |
break; | |
case MSG_SET_GENERATION_ID: | |
if (msg.arg2 != 0) { | |
// This means nobody is currently registered. Hide the view. | |
if (mWidgetCallbacks != null) { | |
mWidgetCallbacks.requestHide(TransportControlView.this); | |
} | |
} | |
if (DEBUG) Log.v(TAG, "New genId = " + msg.arg1 + ", clearing = " + msg.arg2); | |
mClientGeneration = msg.arg1; | |
mClientIntent = (PendingIntent) msg.obj; | |
break; | |
} | |
} | |
}; | |
/** | |
* This class is required to have weak linkage to the current TransportControlView | |
* because the remote process can hold a strong reference to this binder object and | |
* we can't predict when it will be GC'd in the remote process. Without this code, it | |
* would allow a heavyweight object to be held on this side of the binder when there's | |
* no requirement to run a GC on the other side. | |
*/ | |
private static class IRemoteControlDisplayWeak extends IRemoteControlDisplay.Stub { | |
private WeakReference<Handler> mLocalHandler; | |
IRemoteControlDisplayWeak(Handler handler) { | |
mLocalHandler = new WeakReference<Handler>(handler); | |
} | |
public void setPlaybackState(int generationId, int state, long stateChangeTimeMs) { | |
Handler handler = mLocalHandler.get(); | |
if (handler != null) { | |
handler.obtainMessage(MSG_UPDATE_STATE, generationId, state).sendToTarget(); | |
} | |
} | |
public void setMetadata(int generationId, Bundle metadata) { | |
Handler handler = mLocalHandler.get(); | |
if (handler != null) { | |
handler.obtainMessage(MSG_SET_METADATA, generationId, 0, metadata).sendToTarget(); | |
} | |
} | |
public void setTransportControlFlags(int generationId, int flags) { | |
Handler handler = mLocalHandler.get(); | |
if (handler != null) { | |
handler.obtainMessage(MSG_SET_TRANSPORT_CONTROLS, generationId, flags) | |
.sendToTarget(); | |
} | |
} | |
public void setArtwork(int generationId, Bitmap bitmap) { | |
Handler handler = mLocalHandler.get(); | |
if (handler != null) { | |
handler.obtainMessage(MSG_SET_ARTWORK, generationId, 0, bitmap).sendToTarget(); | |
} | |
} | |
public void setAllMetadata(int generationId, Bundle metadata, Bitmap bitmap) { | |
Handler handler = mLocalHandler.get(); | |
if (handler != null) { | |
handler.obtainMessage(MSG_SET_METADATA, generationId, 0, metadata).sendToTarget(); | |
handler.obtainMessage(MSG_SET_ARTWORK, generationId, 0, bitmap).sendToTarget(); | |
} | |
} | |
public void setCurrentClientId(int clientGeneration, PendingIntent mediaIntent, | |
boolean clearing) throws RemoteException { | |
Handler handler = mLocalHandler.get(); | |
if (handler != null) { | |
handler.obtainMessage(MSG_SET_GENERATION_ID, | |
clientGeneration, (clearing ? 1 : 0), mediaIntent).sendToTarget(); | |
} | |
} | |
}; | |
public TransportControlView(Context context, AttributeSet attrs) { | |
super(context, attrs); | |
Log.v(TAG, "Create TCV " + this); | |
mAudioManager = new AudioManager(mContext); | |
mCurrentPlayState = RemoteControlClient.PLAYSTATE_NONE; // until we get a callback | |
mIRCD = new IRemoteControlDisplayWeak(mHandler); | |
} | |
private void updateTransportControls(int transportControlFlags) { | |
mTransportControlFlags = transportControlFlags; | |
} | |
@Override | |
public void onFinishInflate() { | |
super.onFinishInflate(); | |
mTrackTitle = (TextView) findViewById(R.id.title); | |
mTrackTitle.setSelected(true); // enable marquee | |
mAlbumArt = (ImageView) findViewById(R.id.albumart); | |
mBtnPrev = (ImageView) findViewById(R.id.btn_prev); | |
mBtnPlay = (ImageView) findViewById(R.id.btn_play); | |
mBtnNext = (ImageView) findViewById(R.id.btn_next); | |
final View buttons[] = { mBtnPrev, mBtnPlay, mBtnNext }; | |
for (View view : buttons) { | |
view.setOnClickListener(this); | |
} | |
} | |
@Override | |
public void onAttachedToWindow() { | |
super.onAttachedToWindow(); | |
if (mPopulateMetadataWhenAttached != null) { | |
updateMetadata(mPopulateMetadataWhenAttached); | |
mPopulateMetadataWhenAttached = null; | |
} | |
if (!mAttached) { | |
if (DEBUG) Log.v(TAG, "Registering TCV " + this); | |
mAudioManager.registerRemoteControlDisplay(mIRCD); | |
} | |
mAttached = true; | |
} | |
@Override | |
public void onDetachedFromWindow() { | |
super.onDetachedFromWindow(); | |
if (mAttached) { | |
if (DEBUG) Log.v(TAG, "Unregistering TCV " + this); | |
mAudioManager.unregisterRemoteControlDisplay(mIRCD); | |
} | |
mAttached = false; | |
} | |
@Override | |
protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) { | |
super.onMeasure(widthMeasureSpec, heightMeasureSpec); | |
int dim = Math.min(MAXDIM, Math.max(getWidth(), getHeight())); | |
// Log.v(TAG, "setting max bitmap size: " + dim + "x" + dim); | |
// mAudioManager.remoteControlDisplayUsesBitmapSize(mIRCD, dim, dim); | |
} | |
class Metadata { | |
private String artist; | |
private String trackTitle; | |
private String albumTitle; | |
private Bitmap bitmap; | |
public String toString() { | |
return "Metadata[artist=" + artist + " trackTitle=" + trackTitle + " albumTitle=" + albumTitle + "]"; | |
} | |
} | |
private String getMdString(Bundle data, int id) { | |
return data.getString(Integer.toString(id)); | |
} | |
private void updateMetadata(Bundle data) { | |
if (mAttached) { | |
mMetadata.artist = getMdString(data, MediaMetadataRetriever.METADATA_KEY_ALBUMARTIST); | |
mMetadata.trackTitle = getMdString(data, MediaMetadataRetriever.METADATA_KEY_TITLE); | |
mMetadata.albumTitle = getMdString(data, MediaMetadataRetriever.METADATA_KEY_ALBUM); | |
populateMetadata(); | |
} else { | |
mPopulateMetadataWhenAttached = data; | |
} | |
} | |
/** | |
* Populates the given metadata into the view | |
*/ | |
private void populateMetadata() { | |
StringBuilder sb = new StringBuilder(); | |
int trackTitleLength = 0; | |
if (!TextUtils.isEmpty(mMetadata.trackTitle)) { | |
sb.append(mMetadata.trackTitle); | |
trackTitleLength = mMetadata.trackTitle.length(); | |
} | |
if (!TextUtils.isEmpty(mMetadata.artist)) { | |
if (sb.length() != 0) { | |
sb.append(" - "); | |
} | |
sb.append(mMetadata.artist); | |
} | |
if (!TextUtils.isEmpty(mMetadata.albumTitle)) { | |
if (sb.length() != 0) { | |
sb.append(" - "); | |
} | |
sb.append(mMetadata.albumTitle); | |
} | |
mTrackTitle.setText(sb.toString(), TextView.BufferType.SPANNABLE); | |
Spannable str = (Spannable) mTrackTitle.getText(); | |
if (trackTitleLength != 0) { | |
str.setSpan(new ForegroundColorSpan(0xffffffff), 0, trackTitleLength, | |
Spannable.SPAN_EXCLUSIVE_EXCLUSIVE); | |
trackTitleLength++; | |
} | |
if (sb.length() > trackTitleLength) { | |
str.setSpan(new ForegroundColorSpan(0x7fffffff), trackTitleLength, sb.length(), | |
Spannable.SPAN_EXCLUSIVE_EXCLUSIVE); | |
} | |
mAlbumArt.setImageBitmap(mMetadata.bitmap); | |
final int flags = mTransportControlFlags; | |
setVisibilityBasedOnFlag(mBtnPrev, flags, RemoteControlClient.FLAG_KEY_MEDIA_PREVIOUS); | |
setVisibilityBasedOnFlag(mBtnNext, flags, RemoteControlClient.FLAG_KEY_MEDIA_NEXT); | |
setVisibilityBasedOnFlag(mBtnPrev, flags, | |
RemoteControlClient.FLAG_KEY_MEDIA_PLAY | |
| RemoteControlClient.FLAG_KEY_MEDIA_PAUSE | |
| RemoteControlClient.FLAG_KEY_MEDIA_PLAY_PAUSE | |
| RemoteControlClient.FLAG_KEY_MEDIA_STOP); | |
updatePlayPauseState(mCurrentPlayState); | |
} | |
private static void setVisibilityBasedOnFlag(View view, int flags, int flag) { | |
if ((flags & flag) != 0) { | |
view.setVisibility(View.VISIBLE); | |
} else { | |
view.setVisibility(View.GONE); | |
} | |
} | |
private void updatePlayPauseState(int state) { | |
if (DEBUG) Log.v(TAG, | |
"updatePlayPauseState(), old=" + mCurrentPlayState + ", state=" + state); | |
if (state == mCurrentPlayState) { | |
return; | |
} | |
final int imageResId; | |
final int imageDescId; | |
boolean showIfHidden = false; | |
switch (state) { | |
case RemoteControlClient.PLAYSTATE_ERROR: | |
imageResId = com.android.internal.R.drawable.stat_sys_warning; | |
// TODO use more specific image description string for warning, but here the "play" | |
// message is still valid because this button triggers a play command. | |
imageDescId = com.android.internal.R.string.lockscreen_transport_play_description; | |
break; | |
case RemoteControlClient.PLAYSTATE_PLAYING: | |
imageResId = com.android.internal.R.drawable.ic_media_pause; | |
imageDescId = com.android.internal.R.string.lockscreen_transport_pause_description; | |
showIfHidden = true; | |
break; | |
case RemoteControlClient.PLAYSTATE_BUFFERING: | |
imageResId = com.android.internal.R.drawable.ic_media_stop; | |
imageDescId = com.android.internal.R.string.lockscreen_transport_stop_description; | |
showIfHidden = true; | |
break; | |
case RemoteControlClient.PLAYSTATE_PAUSED: | |
default: | |
imageResId = com.android.internal.R.drawable.ic_media_play; | |
imageDescId = com.android.internal.R.string.lockscreen_transport_play_description; | |
showIfHidden = false; | |
break; | |
} | |
mBtnPlay.setImageResource(imageResId); | |
mBtnPlay.setContentDescription(getResources().getString(imageDescId)); | |
if (showIfHidden && mWidgetCallbacks != null && !mWidgetCallbacks.isVisible(this)) { | |
mWidgetCallbacks.requestShow(this); | |
} | |
mCurrentPlayState = state; | |
} | |
static class SavedState extends BaseSavedState { | |
boolean wasShowing; | |
SavedState(Parcelable superState) { | |
super(superState); | |
} | |
private SavedState(Parcel in) { | |
super(in); | |
this.wasShowing = in.readInt() != 0; | |
} | |
@Override | |
public void writeToParcel(Parcel out, int flags) { | |
super.writeToParcel(out, flags); | |
out.writeInt(this.wasShowing ? 1 : 0); | |
} | |
public static final Parcelable.Creator<SavedState> CREATOR | |
= new Parcelable.Creator<SavedState>() { | |
public SavedState createFromParcel(Parcel in) { | |
return new SavedState(in); | |
} | |
public SavedState[] newArray(int size) { | |
return new SavedState[size]; | |
} | |
}; | |
} | |
@Override | |
public Parcelable onSaveInstanceState() { | |
if (DEBUG) Log.v(TAG, "onSaveInstanceState()"); | |
Parcelable superState = super.onSaveInstanceState(); | |
SavedState ss = new SavedState(superState); | |
ss.wasShowing = mWidgetCallbacks != null && mWidgetCallbacks.isVisible(this); | |
return ss; | |
} | |
@Override | |
public void onRestoreInstanceState(Parcelable state) { | |
if (DEBUG) Log.v(TAG, "onRestoreInstanceState()"); | |
if (!(state instanceof SavedState)) { | |
super.onRestoreInstanceState(state); | |
return; | |
} | |
SavedState ss = (SavedState) state; | |
super.onRestoreInstanceState(ss.getSuperState()); | |
if (ss.wasShowing && mWidgetCallbacks != null) { | |
mWidgetCallbacks.requestShow(this); | |
} | |
} | |
public void onClick(View v) { | |
int keyCode = -1; | |
if (v == mBtnPrev) { | |
keyCode = KeyEvent.KEYCODE_MEDIA_PREVIOUS; | |
} else if (v == mBtnNext) { | |
keyCode = KeyEvent.KEYCODE_MEDIA_NEXT; | |
} else if (v == mBtnPlay) { | |
keyCode = KeyEvent.KEYCODE_MEDIA_PLAY_PAUSE; | |
} | |
if (keyCode != -1) { | |
sendMediaButtonClick(keyCode); | |
if (mWidgetCallbacks != null) { | |
mWidgetCallbacks.userActivity(this); | |
} | |
} | |
} | |
private void sendMediaButtonClick(int keyCode) { | |
if (mClientIntent == null) { | |
// Shouldn't be possible because this view should be hidden in this case. | |
Log.e(TAG, "sendMediaButtonClick(): No client is currently registered"); | |
return; | |
} | |
// use the registered PendingIntent that will be processed by the registered | |
// media button event receiver, which is the component of mClientIntent | |
KeyEvent keyEvent = new KeyEvent(KeyEvent.ACTION_DOWN, keyCode); | |
Intent intent = new Intent(Intent.ACTION_MEDIA_BUTTON); | |
intent.putExtra(Intent.EXTRA_KEY_EVENT, keyEvent); | |
try { | |
mClientIntent.send(getContext(), 0, intent); | |
} catch (CanceledException e) { | |
Log.e(TAG, "Error sending intent for media button down: "+e); | |
e.printStackTrace(); | |
} | |
keyEvent = new KeyEvent(KeyEvent.ACTION_UP, keyCode); | |
intent = new Intent(Intent.ACTION_MEDIA_BUTTON); | |
intent.putExtra(Intent.EXTRA_KEY_EVENT, keyEvent); | |
try { | |
mClientIntent.send(getContext(), 0, intent); | |
} catch (CanceledException e) { | |
Log.e(TAG, "Error sending intent for media button up: "+e); | |
e.printStackTrace(); | |
} | |
} | |
public void setCallback(LockScreenWidgetCallback callback) { | |
mWidgetCallbacks = callback; | |
} | |
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; | |
} | |
} | |
} |
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
/* | |
* Copyright (C) 2011 The Android Open Source Project | |
* | |
* Licensed under the Apache License, Version 2.0 (the "License"); | |
* you may not use this file except in compliance with the License. | |
* You may obtain a copy of the License at | |
* | |
* http://www.apache.org/licenses/LICENSE-2.0 | |
* | |
* Unless required by applicable law or agreed to in writing, software | |
* distributed under the License is distributed on an "AS IS" BASIS, | |
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. | |
* See the License for the specific language governing permissions and | |
* limitations under the License. | |
*/ | |
package com.android.keyguard; | |
import android.content.Context; | |
import android.content.pm.PackageManager; | |
import android.content.res.Configuration; | |
import android.graphics.Bitmap; | |
import android.graphics.ColorMatrix; | |
import android.graphics.ColorMatrixColorFilter; | |
import android.graphics.PorterDuff; | |
import android.graphics.PorterDuffXfermode; | |
import android.graphics.drawable.Drawable; | |
import android.media.AudioManager; | |
import android.media.MediaMetadataEditor; | |
import android.media.MediaMetadataRetriever; | |
import android.media.RemoteControlClient; | |
import android.media.RemoteController; | |
import android.os.Parcel; | |
import android.os.Parcelable; | |
import android.os.SystemClock; | |
import android.text.TextUtils; | |
import android.text.format.DateFormat; | |
import android.transition.ChangeBounds; | |
import android.transition.ChangeText; | |
import android.transition.Fade; | |
import android.transition.TransitionManager; | |
import android.transition.TransitionSet; | |
import android.util.AttributeSet; | |
import android.util.DisplayMetrics; | |
import android.util.Log; | |
import android.view.KeyEvent; | |
import android.view.View; | |
import android.view.ViewGroup; | |
import android.widget.FrameLayout; | |
import android.widget.ImageView; | |
import android.widget.SeekBar; | |
import android.widget.TextView; | |
import java.text.SimpleDateFormat; | |
import java.util.Date; | |
import java.util.TimeZone; | |
/** | |
* This is the widget responsible for showing music controls in keyguard. | |
*/ | |
public class KeyguardTransportControlView extends FrameLayout { | |
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; | |
private TextView mTrackTitle; | |
private TextView mTrackArtistAlbum; | |
private View mTransientSeek; | |
private SeekBar mTransientSeekBar; | |
private TextView mTransientSeekTimeElapsed; | |
private TextView mTransientSeekTimeTotal; | |
private ImageView mBtnPrev; | |
private ImageView mBtnPlay; | |
private ImageView mBtnNext; | |
private Metadata mMetadata = new Metadata(); | |
private int mTransportControlFlags; | |
private int mCurrentPlayState; | |
private AudioManager mAudioManager; | |
private RemoteController mRemoteController; | |
private ImageView mBadge; | |
private boolean mSeekEnabled; | |
private java.text.DateFormat mFormat; | |
private Date mTempDate = new Date(); | |
/** | |
* The metadata which should be populated into the view once we've been attached | |
*/ | |
private RemoteController.MetadataEditor mPopulateMetadataWhenAttached = null; | |
private RemoteController.OnClientUpdateListener mRCClientUpdateListener = | |
new RemoteController.OnClientUpdateListener() { | |
@Override | |
public void onClientChange(boolean clearing) { | |
if (clearing) { | |
clearMetadata(); | |
} | |
} | |
@Override | |
public void onClientPlaybackStateUpdate(int state) { | |
updatePlayPauseState(state); | |
} | |
@Override | |
public void onClientPlaybackStateUpdate(int state, long stateChangeTimeMs, | |
long currentPosMs, float speed) { | |
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 | |
public void onClientTransportControlUpdate(int transportControlFlags) { | |
updateTransportControls(transportControlFlags); | |
} | |
@Override | |
public void onClientMetadataUpdate(RemoteController.MetadataEditor metadataEditor) { | |
updateMetadata(metadataEditor); | |
} | |
}; | |
private class UpdateSeekBarRunnable implements Runnable { | |
public void run() { | |
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(); | |
} | |
}; | |
private final OnClickListener mTransportCommandListener = new OnClickListener() { | |
public void onClick(View v) { | |
int keyCode = -1; | |
if (v == mBtnPrev) { | |
keyCode = KeyEvent.KEYCODE_MEDIA_PREVIOUS; | |
} else if (v == mBtnNext) { | |
keyCode = KeyEvent.KEYCODE_MEDIA_NEXT; | |
} else if (v == mBtnPlay) { | |
keyCode = KeyEvent.KEYCODE_MEDIA_PLAY_PAUSE; | |
} | |
if (keyCode != -1) { | |
sendMediaButtonClick(keyCode); | |
delayResetToMetadata(); // if the scrub bar is showing, keep showing it. | |
} | |
} | |
}; | |
private final OnLongClickListener mTransportShowSeekBarListener = new OnLongClickListener() { | |
@Override | |
public boolean onLongClick(View v) { | |
if (mSeekEnabled) { | |
return tryToggleSeekBar(); | |
} | |
return false; | |
} | |
}; | |
// 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) { | |
mFutureSeekRunnable.setProgress(progress); | |
delayResetToMetadata(); | |
mTempDate.setTime(progress); | |
mTransientSeekTimeElapsed.setText(mFormat.format(mTempDate)); | |
} else { | |
updateSeekDisplay(); | |
} | |
} | |
@Override | |
public void onStartTrackingTouch(SeekBar seekBar) { | |
delayResetToMetadata(); | |
removeCallbacks(mUpdateSeekBars); // don't update during user interaction | |
} | |
@Override | |
public void onStopTrackingTouch(SeekBar seekBar) { | |
} | |
}; | |
private static final int TRANSITION_DURATION = 200; | |
private final TransitionSet mMetadataChangeTransition; | |
KeyguardHostView.TransportControlCallback mTransportControlCallback; | |
private final KeyguardUpdateMonitorCallback mUpdateMonitor | |
= new KeyguardUpdateMonitorCallback() { | |
public void onScreenTurnedOff(int why) { | |
setEnableMarquee(false); | |
} | |
public void onScreenTurnedOn() { | |
setEnableMarquee(true); | |
} | |
}; | |
public KeyguardTransportControlView(Context context, AttributeSet attrs) { | |
super(context, attrs); | |
if (DEBUG) Log.v(TAG, "Create TCV " + this); | |
mAudioManager = new AudioManager(mContext); | |
mCurrentPlayState = RemoteControlClient.PLAYSTATE_NONE; // until we get a callback | |
mRemoteController = new RemoteController(context, mRCClientUpdateListener); | |
final DisplayMetrics dm = context.getResources().getDisplayMetrics(); | |
final int dim = Math.max(dm.widthPixels, dm.heightPixels); | |
mRemoteController.setArtworkConfiguration(true, dim, dim); | |
final ChangeText tc = new ChangeText(); | |
tc.setChangeBehavior(ChangeText.CHANGE_BEHAVIOR_OUT_IN); | |
final TransitionSet inner = new TransitionSet(); | |
inner.addTransition(tc).addTransition(new ChangeBounds()); | |
final TransitionSet tg = new TransitionSet(); | |
tg.addTransition(new Fade(Fade.OUT)).addTransition(inner). | |
addTransition(new Fade(Fade.IN)); | |
tg.setOrdering(TransitionSet.ORDERING_SEQUENTIAL); | |
tg.setDuration(TRANSITION_DURATION); | |
mMetadataChangeTransition = tg; | |
} | |
private void updateTransportControls(int transportControlFlags) { | |
mTransportControlFlags = transportControlFlags; | |
setSeekBarsEnabled( | |
(transportControlFlags & RemoteControlClient.FLAG_KEY_MEDIA_POSITION_UPDATE) != 0); | |
} | |
void setSeekBarsEnabled(boolean enabled) { | |
if (enabled == mSeekEnabled) return; | |
mSeekEnabled = enabled; | |
if (mTransientSeek.getVisibility() == VISIBLE && !enabled) { | |
mTransientSeek.setVisibility(INVISIBLE); | |
mMetadataContainer.setVisibility(VISIBLE); | |
cancelResetToMetadata(); | |
} | |
} | |
public void setTransportControlCallback(KeyguardHostView.TransportControlCallback | |
transportControlCallback) { | |
mTransportControlCallback = transportControlCallback; | |
} | |
private void setEnableMarquee(boolean enabled) { | |
if (DEBUG) Log.v(TAG, (enabled ? "Enable" : "Disable") + " transport text marquee"); | |
if (mTrackTitle != null) mTrackTitle.setSelected(enabled); | |
if (mTrackArtistAlbum != null) mTrackTitle.setSelected(enabled); | |
} | |
@Override | |
public void onFinishInflate() { | |
super.onFinishInflate(); | |
mInfoContainer = (ViewGroup) findViewById(R.id.info_container); | |
mMetadataContainer = (ViewGroup) findViewById(R.id.metadata_container); | |
mBadge = (ImageView) findViewById(R.id.badge); | |
mTrackTitle = (TextView) findViewById(R.id.title); | |
mTrackArtistAlbum = (TextView) findViewById(R.id.artist_album); | |
mTransientSeek = findViewById(R.id.transient_seek); | |
mTransientSeekBar = (SeekBar) findViewById(R.id.transient_seek_bar); | |
mTransientSeekBar.setOnSeekBarChangeListener(mOnSeekBarChangeListener); | |
mTransientSeekTimeElapsed = (TextView) findViewById(R.id.transient_seek_time_elapsed); | |
mTransientSeekTimeTotal = (TextView) findViewById(R.id.transient_seek_time_remaining); | |
mBtnPrev = (ImageView) findViewById(R.id.btn_prev); | |
mBtnPlay = (ImageView) findViewById(R.id.btn_play); | |
mBtnNext = (ImageView) findViewById(R.id.btn_next); | |
final View buttons[] = { mBtnPrev, mBtnPlay, mBtnNext }; | |
for (View view : buttons) { | |
view.setOnClickListener(mTransportCommandListener); | |
view.setOnLongClickListener(mTransportShowSeekBarListener); | |
} | |
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 | |
public void onAttachedToWindow() { | |
super.onAttachedToWindow(); | |
if (DEBUG) Log.v(TAG, "onAttachToWindow()"); | |
if (mPopulateMetadataWhenAttached != null) { | |
updateMetadata(mPopulateMetadataWhenAttached); | |
mPopulateMetadataWhenAttached = null; | |
} | |
if (DEBUG) Log.v(TAG, "Registering TCV " + this); | |
mMetadata.clear(); | |
mAudioManager.registerRemoteController(mRemoteController); | |
KeyguardUpdateMonitor.getInstance(mContext).registerCallback(mUpdateMonitor); | |
} | |
@Override | |
protected void onConfigurationChanged(Configuration newConfig) { | |
super.onConfigurationChanged(newConfig); | |
final DisplayMetrics dm = getContext().getResources().getDisplayMetrics(); | |
final int dim = Math.max(dm.widthPixels, dm.heightPixels); | |
mRemoteController.setArtworkConfiguration(true, dim, dim); | |
} | |
@Override | |
public void onDetachedFromWindow() { | |
if (DEBUG) Log.v(TAG, "onDetachFromWindow()"); | |
super.onDetachedFromWindow(); | |
if (DEBUG) Log.v(TAG, "Unregistering TCV " + this); | |
mAudioManager.unregisterRemoteController(mRemoteController); | |
KeyguardUpdateMonitor.getInstance(mContext).removeCallback(mUpdateMonitor); | |
mMetadata.clear(); | |
removeCallbacks(mUpdateSeekBars); | |
} | |
@Override | |
protected Parcelable onSaveInstanceState() { | |
SavedState ss = new SavedState(super.onSaveInstanceState()); | |
ss.artist = mMetadata.artist; | |
ss.trackTitle = mMetadata.trackTitle; | |
ss.albumTitle = mMetadata.albumTitle; | |
ss.duration = mMetadata.duration; | |
ss.bitmap = mMetadata.bitmap; | |
return ss; | |
} | |
@Override | |
protected void onRestoreInstanceState(Parcelable state) { | |
if (!(state instanceof SavedState)) { | |
super.onRestoreInstanceState(state); | |
return; | |
} | |
SavedState ss = (SavedState) state; | |
super.onRestoreInstanceState(ss.getSuperState()); | |
mMetadata.artist = ss.artist; | |
mMetadata.trackTitle = ss.trackTitle; | |
mMetadata.albumTitle = ss.albumTitle; | |
mMetadata.duration = ss.duration; | |
mMetadata.bitmap = ss.bitmap; | |
populateMetadata(); | |
} | |
void setBadgeIcon(Drawable bmp) { | |
mBadge.setImageDrawable(bmp); | |
final ColorMatrix cm = new ColorMatrix(); | |
cm.setSaturation(0); | |
mBadge.setColorFilter(new ColorMatrixColorFilter(cm)); | |
mBadge.setXfermode(new PorterDuffXfermode(PorterDuff.Mode.SCREEN)); | |
mBadge.setImageAlpha(0xef); | |
} | |
class Metadata { | |
private String artist; | |
private String trackTitle; | |
private String albumTitle; | |
private Bitmap bitmap; | |
private long duration; | |
public void clear() { | |
artist = null; | |
trackTitle = null; | |
albumTitle = null; | |
bitmap = null; | |
duration = -1; | |
} | |
public String toString() { | |
return "Metadata[artist=" + artist + " trackTitle=" + trackTitle + | |
" albumTitle=" + albumTitle + " duration=" + duration + "]"; | |
} | |
} | |
void clearMetadata() { | |
mPopulateMetadataWhenAttached = null; | |
mMetadata.clear(); | |
populateMetadata(); | |
} | |
void updateMetadata(RemoteController.MetadataEditor data) { | |
if (isAttachedToWindow()) { | |
mMetadata.artist = data.getString(MediaMetadataRetriever.METADATA_KEY_ALBUMARTIST, | |
mMetadata.artist); | |
mMetadata.trackTitle = data.getString(MediaMetadataRetriever.METADATA_KEY_TITLE, | |
mMetadata.trackTitle); | |
mMetadata.albumTitle = data.getString(MediaMetadataRetriever.METADATA_KEY_ALBUM, | |
mMetadata.albumTitle); | |
mMetadata.duration = data.getLong(MediaMetadataRetriever.METADATA_KEY_DURATION, -1); | |
mMetadata.bitmap = data.getBitmap(MediaMetadataEditor.BITMAP_KEY_ARTWORK, | |
mMetadata.bitmap); | |
populateMetadata(); | |
} else { | |
mPopulateMetadataWhenAttached = data; | |
} | |
} | |
/** | |
* Populates the given metadata into the view | |
*/ | |
private void populateMetadata() { | |
if (ANIMATE_TRANSITIONS && isLaidOut() && mMetadataContainer.getVisibility() == VISIBLE) { | |
TransitionManager.beginDelayedTransition(mMetadataContainer, mMetadataChangeTransition); | |
} | |
final String remoteClientPackage = mRemoteController.getRemoteControlClientPackageName(); | |
Drawable badgeIcon = null; | |
try { | |
badgeIcon = getContext().getPackageManager().getApplicationIcon(remoteClientPackage); | |
} catch (PackageManager.NameNotFoundException e) { | |
Log.e(TAG, "Couldn't get remote control client package icon", e); | |
} | |
setBadgeIcon(badgeIcon); | |
mTrackTitle.setText(!TextUtils.isEmpty(mMetadata.trackTitle) | |
? mMetadata.trackTitle : null); | |
final StringBuilder sb = new StringBuilder(); | |
if (!TextUtils.isEmpty(mMetadata.artist)) { | |
if (sb.length() != 0) { | |
sb.append(" - "); | |
} | |
sb.append(mMetadata.artist); | |
} | |
if (!TextUtils.isEmpty(mMetadata.albumTitle)) { | |
if (sb.length() != 0) { | |
sb.append(" - "); | |
} | |
sb.append(mMetadata.albumTitle); | |
} | |
final String trackArtistAlbum = sb.toString(); | |
mTrackArtistAlbum.setText(!TextUtils.isEmpty(trackArtistAlbum) ? | |
trackArtistAlbum : null); | |
if (mMetadata.duration >= 0) { | |
setSeekBarsEnabled(true); | |
setSeekBarDuration(mMetadata.duration); | |
final String skeleton; | |
if (mMetadata.duration >= 86400000) { | |
skeleton = "DDD kk mm ss"; | |
} else if (mMetadata.duration >= 3600000) { | |
skeleton = "kk mm ss"; | |
} else { | |
skeleton = "mm ss"; | |
} | |
mFormat = new SimpleDateFormat(DateFormat.getBestDateTimePattern( | |
getContext().getResources().getConfiguration().locale, | |
skeleton)); | |
mFormat.setTimeZone(TimeZone.getTimeZone("GMT+0")); | |
} else { | |
setSeekBarsEnabled(false); | |
} | |
KeyguardUpdateMonitor.getInstance(getContext()).dispatchSetBackground(mMetadata.bitmap); | |
final int flags = mTransportControlFlags; | |
setVisibilityBasedOnFlag(mBtnPrev, flags, RemoteControlClient.FLAG_KEY_MEDIA_PREVIOUS); | |
setVisibilityBasedOnFlag(mBtnNext, flags, RemoteControlClient.FLAG_KEY_MEDIA_NEXT); | |
setVisibilityBasedOnFlag(mBtnPlay, flags, | |
RemoteControlClient.FLAG_KEY_MEDIA_PLAY | |
| RemoteControlClient.FLAG_KEY_MEDIA_PAUSE | |
| RemoteControlClient.FLAG_KEY_MEDIA_PLAY_PAUSE | |
| RemoteControlClient.FLAG_KEY_MEDIA_STOP); | |
updatePlayPauseState(mCurrentPlayState); | |
} | |
void updateSeekDisplay() { | |
if (mMetadata != null && mRemoteController != null && mFormat != null) { | |
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=" + mTempDate + | |
" duration=" + mMetadata.duration); | |
} | |
} | |
boolean tryToggleSeekBar() { | |
if (ANIMATE_TRANSITIONS) { | |
TransitionManager.beginDelayedTransition(mInfoContainer); | |
} | |
if (mTransientSeek.getVisibility() == VISIBLE) { | |
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; | |
} | |
void resetToMetadata() { | |
if (ANIMATE_TRANSITIONS) { | |
TransitionManager.beginDelayedTransition(mInfoContainer); | |
} | |
if (mTransientSeek.getVisibility() == VISIBLE) { | |
mTransientSeek.setVisibility(INVISIBLE); | |
mMetadataContainer.setVisibility(VISIBLE); | |
} | |
// TODO Also hide ratings, if applicable | |
} | |
void delayResetToMetadata() { | |
removeCallbacks(mResetToMetadata); | |
postDelayed(mResetToMetadata, RESET_TO_METADATA_DELAY); | |
} | |
void cancelResetToMetadata() { | |
removeCallbacks(mResetToMetadata); | |
} | |
void setSeekBarDuration(long duration) { | |
mTransientSeekBar.setMax((int) duration); | |
} | |
void scrubTo(int progress) { | |
mRemoteController.seekTo(progress); | |
mTransportControlCallback.userActivity(); | |
} | |
private static void setVisibilityBasedOnFlag(View view, int flags, int flag) { | |
if ((flags & flag) != 0) { | |
view.setVisibility(View.VISIBLE); | |
} else { | |
view.setVisibility(View.INVISIBLE); | |
} | |
} | |
private void updatePlayPauseState(int state) { | |
if (DEBUG) Log.v(TAG, | |
"updatePlayPauseState(), old=" + mCurrentPlayState + ", state=" + state); | |
if (state == mCurrentPlayState) { | |
return; | |
} | |
final int imageResId; | |
final int imageDescId; | |
switch (state) { | |
case RemoteControlClient.PLAYSTATE_ERROR: | |
imageResId = R.drawable.stat_sys_warning; | |
// TODO use more specific image description string for warning, but here the "play" | |
// message is still valid because this button triggers a play command. | |
imageDescId = R.string.keyguard_transport_play_description; | |
break; | |
case RemoteControlClient.PLAYSTATE_PLAYING: | |
imageResId = R.drawable.ic_media_pause; | |
imageDescId = R.string.keyguard_transport_pause_description; | |
break; | |
case RemoteControlClient.PLAYSTATE_BUFFERING: | |
imageResId = R.drawable.ic_media_stop; | |
imageDescId = R.string.keyguard_transport_stop_description; | |
break; | |
case RemoteControlClient.PLAYSTATE_PAUSED: | |
default: | |
imageResId = R.drawable.ic_media_play; | |
imageDescId = R.string.keyguard_transport_play_description; | |
break; | |
} | |
boolean clientSupportsSeek = mMetadata != null && mMetadata.duration > 0; | |
setSeekBarsEnabled(clientSupportsSeek); | |
mBtnPlay.setImageResource(imageResId); | |
mBtnPlay.setContentDescription(getResources().getString(imageDescId)); | |
mCurrentPlayState = state; | |
} | |
boolean updateSeekBars() { | |
final int position = (int) mRemoteController.getEstimatedMediaPosition(); | |
if (DEBUG) Log.v(TAG, "Estimated time:" + position); | |
if (position >= 0) { | |
mTransientSeekBar.setProgress(position); | |
return true; | |
} | |
Log.w(TAG, "Updating seek bars; received invalid estimated media position (" + | |
position + "). Disabling seek."); | |
setSeekBarsEnabled(false); | |
return false; | |
} | |
static class SavedState extends BaseSavedState { | |
boolean clientPresent; | |
String artist; | |
String trackTitle; | |
String albumTitle; | |
long duration; | |
Bitmap bitmap; | |
SavedState(Parcelable superState) { | |
super(superState); | |
} | |
private SavedState(Parcel in) { | |
super(in); | |
clientPresent = in.readInt() != 0; | |
artist = in.readString(); | |
trackTitle = in.readString(); | |
albumTitle = in.readString(); | |
duration = in.readLong(); | |
bitmap = Bitmap.CREATOR.createFromParcel(in); | |
} | |
@Override | |
public void writeToParcel(Parcel out, int flags) { | |
super.writeToParcel(out, flags); | |
out.writeInt(clientPresent ? 1 : 0); | |
out.writeString(artist); | |
out.writeString(trackTitle); | |
out.writeString(albumTitle); | |
out.writeLong(duration); | |
bitmap.writeToParcel(out, flags); | |
} | |
public static final Parcelable.Creator<SavedState> CREATOR | |
= new Parcelable.Creator<SavedState>() { | |
public SavedState createFromParcel(Parcel in) { | |
return new SavedState(in); | |
} | |
public SavedState[] newArray(int size) { | |
return new SavedState[size]; | |
} | |
}; | |
} | |
private void sendMediaButtonClick(int keyCode) { | |
// TODO We should think about sending these up/down events accurately with touch up/down | |
// on the buttons, but in the near term this will interfere with the long press behavior. | |
mRemoteController.sendMediaKeyEvent(new KeyEvent(KeyEvent.ACTION_DOWN, keyCode)); | |
mRemoteController.sendMediaKeyEvent(new KeyEvent(KeyEvent.ACTION_UP, keyCode)); | |
mTransportControlCallback.userActivity(); | |
} | |
public boolean providesClock() { | |
return false; | |
} | |
} |
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment