diff options
7 files changed, 552 insertions, 214 deletions
diff --git a/api/current.txt b/api/current.txt index b8447af..689cdff 100644 --- a/api/current.txt +++ b/api/current.txt @@ -13356,6 +13356,7 @@ package android.media { ctor public RemoteController(android.content.Context, android.os.Looper) throws java.lang.IllegalArgumentException; method public int clearArtworkConfiguration(); method public android.media.RemoteController.MetadataEditor editMetadata(); + method public long getEstimatedMediaPosition(); method public int seekTo(long); method public int sendMediaKeyEvent(android.view.KeyEvent); method public int setArtworkConfiguration(int, int); diff --git a/media/java/android/media/RemoteControlClient.java b/media/java/android/media/RemoteControlClient.java index ab6bd70..497b8b3 100644 --- a/media/java/android/media/RemoteControlClient.java +++ b/media/java/android/media/RemoteControlClient.java @@ -1674,7 +1674,7 @@ public class RemoteControlClient * @return true during any form of playback, false if it's not playing anything while in this * playback state */ - private static boolean playbackPositionShouldMove(int playstate) { + static boolean playbackPositionShouldMove(int playstate) { switch(playstate) { case PLAYSTATE_STOPPED: case PLAYSTATE_PAUSED: diff --git a/media/java/android/media/RemoteController.java b/media/java/android/media/RemoteController.java index 96f6a92..d056269 100644 --- a/media/java/android/media/RemoteController.java +++ b/media/java/android/media/RemoteController.java @@ -17,6 +17,7 @@ package android.media; import android.Manifest; +import android.app.ActivityManager; import android.app.PendingIntent; import android.app.PendingIntent.CanceledException; import android.content.Context; @@ -30,6 +31,8 @@ import android.os.Looper; import android.os.Message; import android.os.RemoteException; import android.os.ServiceManager; +import android.os.SystemClock; +import android.util.DisplayMetrics; import android.util.Log; import android.view.KeyEvent; @@ -59,6 +62,7 @@ public final class RemoteController private final RcDisplay mRcd; private final Context mContext; private final AudioManager mAudioManager; + private final int mMaxBitmapDimension; private MetadataEditor mMetadataEditor; /** @@ -110,6 +114,13 @@ public final class RemoteController mContext = context; mRcd = new RcDisplay(); mAudioManager = (AudioManager) context.getSystemService(Context.AUDIO_SERVICE); + + if (ActivityManager.isLowRamDeviceStatic()) { + mMaxBitmapDimension = MAX_BITMAP_DIMENSION; + } else { + final DisplayMetrics dm = context.getResources().getDisplayMetrics(); + mMaxBitmapDimension = Math.max(dm.widthPixels, dm.heightPixels); + } } @@ -142,7 +153,7 @@ public final class RemoteController * @param state one of the playback states authorized * in {@link RemoteControlClient#setPlaybackState(int)}. * @param stateChangeTimeMs the system time at which the state change was reported, - * expressed in ms. + * expressed in ms. Based on {@link android.os.SystemClock.elapsedRealtime()}. * @param currentPosMs a positive value for the current media playback position expressed * in ms, a negative value if the position is temporarily unknown. * @param speed a value expressed as a ratio of 1x playback: 1.0f is normal playback, @@ -200,6 +211,50 @@ public final class RemoteController } } + /** + * @hide + */ + public String getRemoteControlClientPackageName() { + return mClientPendingIntentCurrent != null ? + mClientPendingIntentCurrent.getCreatorPackage() : null; + } + + /** + * Return the estimated playback position of the current media track or a negative value + * if not available. + * + * <p>The value returned is estimated by the current process and may not be perfect. + * The time returned by this method is calculated from the last state change time based + * on the current play position at that time and the last known playback speed. + * An application may call {@link #setSynchronizationMode(int)} to apply + * a synchronization policy that will periodically re-sync the estimated position + * with the RemoteControlClient.</p> + * + * @return the current estimated playback position in milliseconds or a negative value + * if not available + * + * @see OnClientUpdateListener#onClientPlaybackStateUpdate(int, long, long, float) + */ + public long getEstimatedMediaPosition() { + if (mLastPlaybackInfo != null) { + if (!RemoteControlClient.playbackPositionShouldMove(mLastPlaybackInfo.mState)) { + return mLastPlaybackInfo.mCurrentPosMs; + } + + // Take the current position at the time of state change and estimate. + final long thenPos = mLastPlaybackInfo.mCurrentPosMs; + if (thenPos < 0) { + return -1; + } + + final long now = SystemClock.elapsedRealtime(); + final long then = mLastPlaybackInfo.mStateChangeTimeMs; + final long sinceThen = now - then; + final long scaledSinceThen = (long) (sinceThen * mLastPlaybackInfo.mSpeed); + return thenPos + scaledSinceThen; + } + return -1; + } /** * Send a simulated key event for a media button to be received by the current client. @@ -301,8 +356,8 @@ public final class RemoteController synchronized (mInfoLock) { if (wantBitmap) { if ((width > 0) && (height > 0)) { - if (width > MAX_BITMAP_DIMENSION) { width = MAX_BITMAP_DIMENSION; } - if (height > MAX_BITMAP_DIMENSION) { height = MAX_BITMAP_DIMENSION; } + if (width > mMaxBitmapDimension) { width = mMaxBitmapDimension; } + if (height > mMaxBitmapDimension) { height = mMaxBitmapDimension; } mArtworkWidth = width; mArtworkHeight = height; } else { @@ -415,7 +470,13 @@ public final class RemoteController protected MetadataEditor(Bundle metadata, long editableKeys) { mEditorMetadata = metadata; mEditableKeys = editableKeys; - mEditorArtwork = null; + + mEditorArtwork = (Bitmap) metadata.getParcelable( + String.valueOf(MediaMetadataEditor.BITMAP_KEY_ARTWORK)); + if (mEditorArtwork != null) { + cleanupBitmapFromBundle(MediaMetadataEditor.BITMAP_KEY_ARTWORK); + } + mMetadataChanged = true; mArtworkChanged = true; mApplied = false; @@ -706,6 +767,7 @@ public final class RemoteController // existing metadata, merge existing and new mMetadataEditor.mEditorMetadata.putAll(metadata); } + mMetadataEditor.putBitmap(MediaMetadataEditor.BITMAP_KEY_ARTWORK, (Bitmap)metadata.getParcelable( String.valueOf(MediaMetadataEditor.BITMAP_KEY_ARTWORK))); diff --git a/packages/Keyguard/res/layout/keyguard_transport_control_view.xml b/packages/Keyguard/res/layout/keyguard_transport_control_view.xml index 801999a..81c7425 100644 --- a/packages/Keyguard/res/layout/keyguard_transport_control_view.xml +++ b/packages/Keyguard/res/layout/keyguard_transport_control_view.xml @@ -22,34 +22,133 @@ android:gravity="center_horizontal" android:id="@+id/keyguard_transport_control"> - <!-- Use ImageView for its cropping features; otherwise could be android:background --> - <ImageView - android:id="@+id/albumart" - android:layout_width="match_parent" - android:layout_height="match_parent" - android:layout_gravity="fill" - android:scaleType="centerCrop" - android:adjustViewBounds="false" - android:contentDescription="@string/keygaurd_accessibility_media_controls" /> - - <LinearLayout android:orientation="vertical" android:layout_width="match_parent" android:layout_height="wrap_content" - android:layout_gravity="bottom"> - <TextView - android:id="@+id/title" + android:layout_gravity="top" + android:gravity="center"> + <ImageView + android:id="@+id/badge" + android:layout_width="32dp" + android:layout_height="32dp" + android:scaleType="fitCenter" /> + <FrameLayout + android:id="@+id/info_container" android:layout_width="match_parent" - android:layout_height="wrap_content" - android:layout_marginTop="8dip" - android:layout_marginStart="16dip" - android:layout_marginEnd="16dip" - android:gravity="center_horizontal" - android:singleLine="true" - android:ellipsize="end" - android:textAppearance="?android:attr/textAppearanceMedium" - /> + android:layout_height="wrap_content"> + <LinearLayout + android:id="@+id/metadata_container" + android:orientation="vertical" + android:layout_width="match_parent" + android:layout_height="wrap_content" + android:layout_gravity="center"> + <TextView + android:id="@+id/title" + android:layout_width="match_parent" + android:layout_height="wrap_content" + android:layout_marginStart="16dip" + android:layout_marginEnd="16dip" + android:gravity="center_horizontal" + android:singleLine="true" + android:ellipsize="marquee" + android:textAppearance="?android:attr/textAppearanceLarge" + android:fontFamily="sans-serif-light" /> + <TextView + android:id="@+id/artist_album" + android:layout_width="match_parent" + android:layout_height="wrap_content" + android:layout_marginStart="16dip" + android:layout_marginEnd="16dip" + android:gravity="center_horizontal" + android:singleLine="true" + android:ellipsize="marquee" + android:textAppearance="?android:attr/textAppearanceSmall" + android:textColor="?android:attr/textColorSecondary" /> + </LinearLayout> + <RelativeLayout + android:id="@+id/transient_seek" + android:layout_width="match_parent" + android:layout_height="wrap_content" + android:layout_gravity="center" + android:visibility="invisible"> + <SeekBar + android:id="@+id/transient_seek_bar" + android:layout_width="match_parent" + android:layout_height="wrap_content" /> + <TextView + android:id="@+id/transient_seek_time_elapsed" + android:layout_width="wrap_content" + android:layout_height="wrap_content" + android:layout_alignParentStart="true" + android:layout_below="@id/transient_seek_bar" + android:textAppearance="?android:attr/textAppearanceSmall" + android:textSize="12dp" /> + <TextView + android:id="@+id/transient_seek_time_remaining" + android:layout_width="wrap_content" + android:layout_height="wrap_content" + android:layout_alignParentEnd="true" + android:layout_below="@id/transient_seek_bar" + android:textAppearance="?android:attr/textAppearanceSmall" + android:textSize="12dp" /> + </RelativeLayout> + <LinearLayout + android:id="@+id/transient_rating" + android:layout_width="wrap_content" + android:layout_height="wrap_content" + android:layout_gravity="center" + android:visibility="invisible"> + <RatingBar + android:id="@+id/transient_rating_bar_stars" + android:layout_width="wrap_content" + android:layout_height="wrap_content" /> + <LinearLayout + android:id="@+id/transient_rating_thumbs" + android:layout_width="match_parent" + android:layout_height="wrap_content" + android:orientation="horizontal"> + <FrameLayout + android:layout_width="wrap_content" + android:layout_height="wrap_content" + android:layout_weight="1"> + <ImageButton + android:id="@+id/btn_thumbs_up" + android:layout_width="wrap_content" + android:layout_height="wrap_content" + android:layout_gravity="center" + android:src="@drawable/ic_media_previous" + android:background="?android:attr/selectableItemBackground" + android:minWidth="48dp" + android:minHeight="48dp" + android:contentDescription="@string/keyguard_accessibility_transport_thumbs_up_description"/> + </FrameLayout> + <FrameLayout + android:layout_width="wrap_content" + android:layout_height="wrap_content" + android:layout_weight="1"> + <ImageButton + android:id="@+id/btn_thumbs_down" + android:layout_width="wrap_content" + android:layout_height="wrap_content" + android:layout_gravity="center" + android:src="@drawable/ic_media_next" + android:background="?android:attr/selectableItemBackground" + android:minWidth="48dp" + android:minHeight="48dp" + android:contentDescription="@string/keyguard_accessibility_transport_thumbs_down_description"/> + </FrameLayout> + </LinearLayout> + <ToggleButton + android:id="@+id/transient_rating_heart" + android:layout_width="wrap_content" + android:layout_height="wrap_content" + android:visibility="invisible" + android:minWidth="48dp" + android:minHeight="48dp" + android:contentDescription="@string/keyguard_accessibility_transport_heart_description" /> + </LinearLayout> + </FrameLayout> <LinearLayout android:layout_width="match_parent" android:layout_height="wrap_content" @@ -59,45 +158,45 @@ android:layout_width="wrap_content" android:layout_height="wrap_content" android:layout_weight="1"> - <ImageView + <ImageButton android:id="@+id/btn_prev" android:layout_width="wrap_content" android:layout_height="wrap_content" android:layout_gravity="center" android:src="@drawable/ic_media_previous" - android:clickable="true" android:background="?android:attr/selectableItemBackground" - android:padding="10dip" + android:minWidth="48dp" + android:minHeight="48dp" android:contentDescription="@string/keyguard_accessibility_transport_prev_description"/> </FrameLayout> <FrameLayout android:layout_width="wrap_content" android:layout_height="wrap_content" android:layout_weight="1"> - <ImageView + <ImageButton android:id="@+id/btn_play" android:layout_width="wrap_content" android:layout_height="wrap_content" android:layout_gravity="center" - android:clickable="true" android:src="@drawable/ic_media_play" android:background="?android:attr/selectableItemBackground" - android:padding="10dip" + android:minWidth="48dp" + android:minHeight="48dp" android:contentDescription="@string/keyguard_accessibility_transport_play_description"/> </FrameLayout> <FrameLayout android:layout_width="wrap_content" android:layout_height="wrap_content" android:layout_weight="1"> - <ImageView + <ImageButton android:id="@+id/btn_next" android:layout_width="wrap_content" android:layout_height="wrap_content" android:layout_gravity="center" - android:clickable="true" android:src="@drawable/ic_media_next" android:background="?android:attr/selectableItemBackground" - android:padding="10dip" + android:minWidth="48dp" + android:minHeight="48dp" android:contentDescription="@string/keyguard_accessibility_transport_next_description"/> </FrameLayout> </LinearLayout> diff --git a/packages/Keyguard/res/values/strings.xml b/packages/Keyguard/res/values/strings.xml index 65322e3..abc4483 100644 --- a/packages/Keyguard/res/values/strings.xml +++ b/packages/Keyguard/res/values/strings.xml @@ -152,6 +152,13 @@ <string name="keyguard_accessibility_transport_play_description">Play button</string> <!-- Shown on transport control of lockscreen. Pressing button pauses playback --> <string name="keyguard_accessibility_transport_stop_description">Stop button</string> + <!-- Shown on transport control of lockscreen. Pressing button rates the track as "thumbs up." --> + <string name="keyguard_accessibility_transport_thumbs_up_description">Thumbs up</string> + <!-- Shown on transport control of lockscreen. Pressing button rates the track as "thumbs down." --> + <string name="keyguard_accessibility_transport_thumbs_down_description">Thumbs down</string> + <!-- Shown on transport control of lockscreen. Pressing button toggles the "heart" rating. --> + <string name="keyguard_accessibility_transport_heart_description">Heart</string> + <!-- Accessibility description for when the device prompts the user to dismiss keyguard in order to complete an action. This will be followed by a message about the current diff --git a/packages/Keyguard/src/com/android/keyguard/KeyguardHostView.java b/packages/Keyguard/src/com/android/keyguard/KeyguardHostView.java index bc8c866..63aab4d 100644 --- a/packages/Keyguard/src/com/android/keyguard/KeyguardHostView.java +++ b/packages/Keyguard/src/com/android/keyguard/KeyguardHostView.java @@ -134,6 +134,10 @@ public class KeyguardHostView extends KeyguardViewBase { void userActivity(); } + interface TransportControlCallback { + void userActivity(); + } + /*package*/ interface OnDismissAction { /* returns true if the dismiss should be deferred */ boolean onDismiss(); @@ -1222,6 +1226,11 @@ public class KeyguardHostView extends KeyguardViewBase { LayoutInflater inflater = LayoutInflater.from(mContext); mTransportControl = (KeyguardTransportControlView) inflater.inflate(R.layout.keyguard_transport_control_view, this, false); + mTransportControl.setTransportControlCallback(new TransportControlCallback() { + public void userActivity() { + mViewMediatorCallback.userActivity(); + } + }); } return mTransportControl; } diff --git a/packages/Keyguard/src/com/android/keyguard/KeyguardTransportControlView.java b/packages/Keyguard/src/com/android/keyguard/KeyguardTransportControlView.java index 2a5f979..83d8ab1 100644 --- a/packages/Keyguard/src/com/android/keyguard/KeyguardTransportControlView.java +++ b/packages/Keyguard/src/com/android/keyguard/KeyguardTransportControlView.java @@ -16,191 +16,263 @@ package com.android.keyguard; -import android.app.PendingIntent; -import android.app.PendingIntent.CanceledException; import android.content.Context; -import android.content.Intent; +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.drawable.Drawable; import android.media.AudioManager; -import android.media.IRemoteControlDisplay; +import android.media.MediaMetadataEditor; import android.media.MediaMetadataRetriever; import android.media.RemoteControlClient; -import android.os.Bundle; -import android.os.Handler; -import android.os.Message; +import android.media.RemoteController; 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.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.View.OnClickListener; +import android.view.ViewGroup; import android.widget.FrameLayout; import android.widget.ImageView; +import android.widget.SeekBar; import android.widget.TextView; -import java.lang.ref.WeakReference; +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 implements OnClickListener { +public class KeyguardTransportControlView extends FrameLayout { - 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 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 ImageView mAlbumArt; + private static final boolean ANIMATE_TRANSITIONS = false; + + private ViewGroup mMetadataContainer; + private ViewGroup mInfoContainer; private TextView mTrackTitle; + private TextView mTrackArtistAlbum; + + private View mTransientSeek; + private SeekBar mTransientSeekBar; + private TextView mTransientSeekTimeElapsed; + private TextView mTransientSeekTimeRemaining; + 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 IRemoteControlDisplayWeak mIRCD; + private RemoteController mRemoteController; + + private ImageView mBadge; + + private boolean mSeekEnabled; + private boolean mUserSeeking; + private java.text.DateFormat mFormat; /** * The metadata which should be populated into the view once we've been attached */ - private Bundle mPopulateMetadataWhenAttached = null; + private RemoteController.MetadataEditor 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() { + private RemoteController.OnClientUpdateListener mRCClientUpdateListener = + new RemoteController.OnClientUpdateListener() { @Override - public void handleMessage(Message msg) { - switch (msg.what) { - case MSG_UPDATE_STATE: - if (mClientGeneration == msg.arg1) updatePlayPauseState(msg.arg2); - break; + public void onClientChange(boolean clearing) { + if (clearing) { + clearMetadata(); + } + } - case MSG_SET_METADATA: - if (mClientGeneration == msg.arg1) updateMetadata((Bundle) msg.obj); - break; + @Override + public void onClientPlaybackStateUpdate(int state) { + setSeekBarsEnabled(false); + updatePlayPauseState(state); + } - case MSG_SET_TRANSPORT_CONTROLS: - if (mClientGeneration == msg.arg1) updateTransportControls(msg.arg2); - break; + @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 + ")"); + } - case MSG_SET_ARTWORK: - if (mClientGeneration == msg.arg1) { - mMetadata.bitmap = (Bitmap) msg.obj; - KeyguardUpdateMonitor.getInstance(getContext()).dispatchSetBackground( - mMetadata.bitmap); - } - break; + @Override + public void onClientTransportControlUpdate(int transportControlFlags) { + updateTransportControls(transportControlFlags); + } - case MSG_SET_GENERATION_ID: - if (DEBUG) Log.v(TAG, "New genId = " + msg.arg1 + ", clearing = " + msg.arg2); - mClientGeneration = msg.arg1; - mClientIntent = (PendingIntent) msg.obj; - break; + @Override + public void onClientMetadataUpdate(RemoteController.MetadataEditor metadataEditor) { + updateMetadata(metadataEditor); + } + }; + private final Runnable mUpdateSeekBars = new Runnable() { + public void run() { + if (updateSeekBars()) { + postDelayed(this, 1000); } } }; - /** - * 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); + private final Runnable mResetToMetadata = new Runnable() { + public void run() { + resetToMetadata(); } + }; - public void setPlaybackState(int generationId, int state, long stateChangeTimeMs, - long currentPosMs, float speed) { - Handler handler = mLocalHandler.get(); - if (handler != null) { - handler.obtainMessage(MSG_UPDATE_STATE, generationId, state).sendToTarget(); + 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; } - } - - public void setMetadata(int generationId, Bundle metadata) { - Handler handler = mLocalHandler.get(); - if (handler != null) { - handler.obtainMessage(MSG_SET_METADATA, generationId, 0, metadata).sendToTarget(); + if (keyCode != -1) { + sendMediaButtonClick(keyCode); } } + }; - public void setTransportControlInfo(int generationId, int flags, int posCapabilities) { - Handler handler = mLocalHandler.get(); - if (handler != null) { - handler.obtainMessage(MSG_SET_TRANSPORT_CONTROLS, generationId, flags) - .sendToTarget(); + private final OnLongClickListener mTransportShowSeekBarListener = new OnLongClickListener() { + @Override + public boolean onLongClick(View v) { + if (mSeekEnabled) { + return tryToggleSeekBar(); } + return false; } + }; - public void setArtwork(int generationId, Bitmap bitmap) { - Handler handler = mLocalHandler.get(); - if (handler != null) { - handler.obtainMessage(MSG_SET_ARTWORK, generationId, 0, bitmap).sendToTarget(); + private final SeekBar.OnSeekBarChangeListener mOnSeekBarChangeListener = + new SeekBar.OnSeekBarChangeListener() { + @Override + public void onProgressChanged(SeekBar seekBar, int progress, boolean fromUser) { + if (fromUser) { + scrubTo(progress); + delayResetToMetadata(); } + updateSeekDisplay(); } - 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(); - } + @Override + public void onStartTrackingTouch(SeekBar seekBar) { + mUserSeeking = true; } - 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(); - } + @Override + public void onStopTrackingTouch(SeekBar seekBar) { + mUserSeeking = false; } }; + private static final int TRANSITION_DURATION = 200; + private final TransitionSet mMetadataChangeTransition; + + KeyguardHostView.TransportControlCallback mTransportControlCallback; + 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 - mIRCD = new IRemoteControlDisplayWeak(mHandler); + mRemoteController = new RemoteController(context); + mRemoteController.setOnClientUpdateListener(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) { + mTransientSeek.setVisibility(INVISIBLE); + mMetadataContainer.setVisibility(VISIBLE); + mUserSeeking = false; + cancelResetToMetadata(); + } + if (enabled) { + mUpdateSeekBars.run(); + postDelayed(mUpdateSeekBars, 1000); + } else { + removeCallbacks(mUpdateSeekBars); + } + } + + public void setTransportControlCallback(KeyguardHostView.TransportControlCallback + transportControlCallback) { + mTransportControlCallback = transportControlCallback; } @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); mTrackTitle.setSelected(true); // enable marquee - mAlbumArt = (ImageView) findViewById(R.id.albumart); + mTrackArtistAlbum = (TextView) findViewById(R.id.artist_album); + mTrackArtistAlbum.setSelected(true); + 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); + mTransientSeekTimeRemaining = (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(this); + view.setOnClickListener(mTransportCommandListener); + view.setOnLongClickListener(mTransportShowSeekBarListener); } } @@ -212,32 +284,34 @@ public class KeyguardTransportControlView extends FrameLayout implements OnClick updateMetadata(mPopulateMetadataWhenAttached); mPopulateMetadataWhenAttached = null; } - if (!mAttached) { - if (DEBUG) Log.v(TAG, "Registering TCV " + this); - mAudioManager.registerRemoteControlDisplay(mIRCD); - } - mAttached = true; + if (DEBUG) Log.v(TAG, "Registering TCV " + this); + mAudioManager.registerRemoteController(mRemoteController); } @Override - protected void onSizeChanged (int w, int h, int oldw, int oldh) { - if (mAttached) { - final DisplayMetrics dm = getContext().getResources().getDisplayMetrics(); - int dim = Math.max(dm.widthPixels, dm.heightPixels); - if (DEBUG) Log.v(TAG, "TCV uses bitmap size=" + dim); - mAudioManager.remoteControlDisplayUsesBitmapSize(mIRCD, dim, dim); - } + 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 (mAttached) { - if (DEBUG) Log.v(TAG, "Unregistering TCV " + this); - mAudioManager.unregisterRemoteControlDisplay(mIRCD); - } - mAttached = false; + if (DEBUG) Log.v(TAG, "Unregistering TCV " + this); + mAudioManager.unregisterRemoteController(mRemoteController); + mUserSeeking = false; + } + + void setBadgeIcon(Drawable bmp) { + mBadge.setImageDrawable(bmp); + + final ColorMatrix cm = new ColorMatrix(); + cm.setSaturation(0); + mBadge.setColorFilter(new ColorMatrixColorFilter(cm)); + mBadge.setImageAlpha(0xef); } class Metadata { @@ -245,21 +319,39 @@ public class KeyguardTransportControlView extends FrameLayout implements OnClick 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 + "]"; + return "Metadata[artist=" + artist + " trackTitle=" + trackTitle + + " albumTitle=" + albumTitle + " duration=" + duration + "]"; } } - private String getMdString(Bundle data, int id) { - return data.getString(Integer.toString(id)); + void clearMetadata() { + mPopulateMetadataWhenAttached = null; + mMetadata.clear(); + populateMetadata(); } - 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); + 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; @@ -270,12 +362,22 @@ public class KeyguardTransportControlView extends FrameLayout implements OnClick * Populates the given metadata into the view */ private void populateMetadata() { - StringBuilder sb = new StringBuilder(); - int trackTitleLength = 0; + 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); if (!TextUtils.isEmpty(mMetadata.trackTitle)) { - sb.append(mMetadata.trackTitle); - trackTitleLength = mMetadata.trackTitle.length(); + mTrackTitle.setText(mMetadata.trackTitle); } + StringBuilder sb = new StringBuilder(); if (!TextUtils.isEmpty(mMetadata.artist)) { if (sb.length() != 0) { sb.append(" - "); @@ -288,16 +390,27 @@ public class KeyguardTransportControlView extends FrameLayout implements OnClick } 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); + mTrackArtistAlbum.setText(sb.toString()); + + 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( @@ -314,6 +427,66 @@ public class KeyguardTransportControlView extends FrameLayout implements OnClick updatePlayPauseState(mCurrentPlayState); } + void updateSeekDisplay() { + if (mMetadata != null && mRemoteController != null && mFormat != null) { + final long timeElapsed = mRemoteController.getEstimatedMediaPosition(); + final long duration = mMetadata.duration; + final long remaining = duration - timeElapsed; + + mTransientSeekTimeElapsed.setText(mFormat.format(new Date(timeElapsed))); + mTransientSeekTimeRemaining.setText(mFormat.format(new Date(remaining))); + + if (DEBUG) Log.d(TAG, "updateSeekDisplay timeElapsed=" + timeElapsed + + " duration=" + duration + " remaining=" + remaining); + } + } + + boolean tryToggleSeekBar() { + if (ANIMATE_TRANSITIONS) { + TransitionManager.beginDelayedTransition(mInfoContainer); + } + if (mTransientSeek.getVisibility() == VISIBLE) { + mTransientSeek.setVisibility(INVISIBLE); + mMetadataContainer.setVisibility(VISIBLE); + cancelResetToMetadata(); + } else { + mTransientSeek.setVisibility(VISIBLE); + mMetadataContainer.setVisibility(INVISIBLE); + delayResetToMetadata(); + } + 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); @@ -341,6 +514,9 @@ public class KeyguardTransportControlView extends FrameLayout implements OnClick case RemoteControlClient.PLAYSTATE_PLAYING: imageResId = R.drawable.ic_media_pause; imageDescId = R.string.keyguard_transport_pause_description; + if (mSeekEnabled) { + postDelayed(mUpdateSeekBars, 1000); + } break; case RemoteControlClient.PLAYSTATE_BUFFERING: @@ -354,11 +530,30 @@ public class KeyguardTransportControlView extends FrameLayout implements OnClick imageDescId = R.string.keyguard_transport_play_description; break; } + + if (state != RemoteControlClient.PLAYSTATE_PLAYING) { + removeCallbacks(mUpdateSeekBars); + updateSeekBars(); + } mBtnPlay.setImageResource(imageResId); mBtnPlay.setContentDescription(getResources().getString(imageDescId)); mCurrentPlayState = state; } + boolean updateSeekBars() { + final int position = (int) mRemoteController.getEstimatedMediaPosition(); + if (position >= 0) { + if (!mUserSeeking) { + 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; @@ -389,48 +584,13 @@ public class KeyguardTransportControlView extends FrameLayout implements OnClick }; } - 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); - } - } - 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(); - } + // 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)); - 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(); - } + mTransportControlCallback.userActivity(); } public boolean providesClock() { |