Merge "Add a scrubber to keyguard; layout tweaks" into klp-dev
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 @@
     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 @@
      * @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.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 @@
     private final RcDisplay mRcd;
     private final Context mContext;
     private final AudioManager mAudioManager;
+    private final int mMaxBitmapDimension;
     private MetadataEditor mMetadataEditor;
 
     /**
@@ -110,6 +114,13 @@
         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 @@
          * @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 @@
         }
     }
 
+    /**
+     * @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 @@
         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 @@
         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 @@
                     // 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 @@
         void userActivity();
     }
 
+    interface TransportControlCallback {
+        void userActivity();
+    }
+
     /*package*/ interface OnDismissAction {
         /* returns true if the dismiss should be deferred */
         boolean onDismiss();
@@ -1222,6 +1226,11 @@
             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;
-
-            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) {
-                    mMetadata.bitmap = (Bitmap) msg.obj;
-                    KeyguardUpdateMonitor.getInstance(getContext()).dispatchSetBackground(
-                            mMetadata.bitmap);
-                }
-                break;
-
-            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;
-
+        public void onClientChange(boolean clearing) {
+            if (clearing) {
+                clearMetadata();
             }
         }
-    };
 
-    /**
-     * 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);
+        @Override
+        public void onClientPlaybackStateUpdate(int state) {
+            setSeekBarsEnabled(false);
+            updatePlayPauseState(state);
         }
 
-        public void setPlaybackState(int generationId, int state, long stateChangeTimeMs,
+        @Override
+        public void onClientPlaybackStateUpdate(int state, long stateChangeTimeMs,
                 long currentPosMs, float speed) {
-            Handler handler = mLocalHandler.get();
-            if (handler != null) {
-                handler.obtainMessage(MSG_UPDATE_STATE, generationId, state).sendToTarget();
-            }
+            setSeekBarsEnabled(mMetadata != null && mMetadata.duration > 0);
+            updatePlayPauseState(state);
+            if (DEBUG) Log.d(TAG, "onClientPlaybackStateUpdate(state=" + state +
+                    ", stateChangeTimeMs=" + stateChangeTimeMs + ", currentPosMs=" + currentPosMs +
+                    ", speed=" + speed + ")");
         }
 
-        public void setMetadata(int generationId, Bundle metadata) {
-            Handler handler = mLocalHandler.get();
-            if (handler != null) {
-                handler.obtainMessage(MSG_SET_METADATA, generationId, 0, metadata).sendToTarget();
-            }
+        @Override
+        public void onClientTransportControlUpdate(int transportControlFlags) {
+            updateTransportControls(transportControlFlags);
         }
 
-        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();
-            }
+        @Override
+        public void onClientMetadataUpdate(RemoteController.MetadataEditor metadataEditor) {
+            updateMetadata(metadataEditor);
         }
+    };
 
-        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();
+    private final Runnable mUpdateSeekBars = new Runnable() {
+        public void run() {
+            if (updateSeekBars()) {
+                postDelayed(this, 1000);
             }
         }
     };
 
+    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);
+            }
+        }
+    };
+
+    private final OnLongClickListener mTransportShowSeekBarListener = new OnLongClickListener() {
+        @Override
+        public boolean onLongClick(View v) {
+            if (mSeekEnabled) {
+                return tryToggleSeekBar();
+            }
+            return false;
+        }
+    };
+
+    private final SeekBar.OnSeekBarChangeListener mOnSeekBarChangeListener =
+            new SeekBar.OnSeekBarChangeListener() {
+        @Override
+        public void onProgressChanged(SeekBar seekBar, int progress, boolean fromUser) {
+            if (fromUser) {
+                scrubTo(progress);
+                delayResetToMetadata();
+            }
+            updateSeekDisplay();
+        }
+
+        @Override
+        public void onStartTrackingTouch(SeekBar seekBar) {
+            mUserSeeking = true;
+        }
+
+        @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 @@
             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 @@
         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 @@
      * 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 (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)) {
+            mTrackTitle.setText(mMetadata.trackTitle);
+        }
+        StringBuilder sb = new StringBuilder();
         if (!TextUtils.isEmpty(mMetadata.artist)) {
             if (sb.length() != 0) {
                 sb.append(" - ");
@@ -288,16 +390,27 @@
             }
             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 @@
         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 @@
             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 @@
                 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 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() {