summaryrefslogtreecommitdiffstats
diff options
context:
space:
mode:
-rw-r--r--api/current.txt1
-rw-r--r--media/java/android/media/RemoteControlClient.java2
-rw-r--r--media/java/android/media/RemoteController.java70
-rw-r--r--packages/Keyguard/res/layout/keyguard_transport_control_view.xml163
-rw-r--r--packages/Keyguard/res/values/strings.xml7
-rw-r--r--packages/Keyguard/src/com/android/keyguard/KeyguardHostView.java9
-rw-r--r--packages/Keyguard/src/com/android/keyguard/KeyguardTransportControlView.java514
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() {