RemoteController class to expose IRemoteControlDisplay features

Wrap all the features of IRemoteControlDisplay.aidl in a
 new class, RemoteController, that implements the
 IRemoteControlDisplay interface.

The API functions to expose in the SDK are tagged with
 "CANDIDATE FOR API"

Bug 8209392

Change-Id: I597bcd503ac93e73889c9ae8b47b16c4fcb363bc
diff --git a/core/res/AndroidManifest.xml b/core/res/AndroidManifest.xml
index c2a830d..6898ab0 100644
--- a/core/res/AndroidManifest.xml
+++ b/core/res/AndroidManifest.xml
@@ -2107,6 +2107,13 @@
         android:description="@string/permdesc_captureSecureVideoOutput"
         android:protectionLevel="signature|system" />
 
+    <!--@hide Allows an application to know what content is playing and control its playback.
+         <p>Not for use by third-party applications due to privacy of media consumption</p>  -->
+    <permission android:name="android.permission.MEDIA_CONTENT_CONTROL"
+        android:label="@string/permlab_mediaContentControl"
+        android:description="@string/permdesc_mediaContentControl"
+        android:protectionLevel="signature|system" />
+
     <!-- Required to be able to disable the device (very dangerous!).
     <p>Not for use by third-party applications.. -->
     <permission android:name="android.permission.BRICK"
diff --git a/core/res/res/values/strings.xml b/core/res/res/values/strings.xml
index 99b703b..56b8ca4 100644
--- a/core/res/res/values/strings.xml
+++ b/core/res/res/values/strings.xml
@@ -1433,6 +1433,11 @@
     <string name="permdesc_captureSecureVideoOutput">Allows the app to capture and redirect secure video output.</string>
 
     <!-- Title of an application permission, listed so the user can choose whether they want to allow the application to do this. -->
+    <string name="permlab_mediaContentControl">control media playback and metadata access</string>
+    <!-- Description of an application permission, listed so the user can choose whether they want to allow the application to do this. -->
+    <string name="permdesc_mediaContentControl">Allows the app to control media playback and access the media information (title, author...).</string>
+
+    <!-- Title of an application permission, listed so the user can choose whether they want to allow the application to do this. -->
     <string name="permlab_modifyAudioSettings">change your audio settings</string>
     <!-- Description of an application permission, listed so the user can choose whether they want to allow the application to do this. -->
     <string name="permdesc_modifyAudioSettings">Allows the app to modify global audio settings such as volume and which speaker is used for output.</string>
diff --git a/media/java/android/media/AudioManager.java b/media/java/android/media/AudioManager.java
index 4542643..a4009cf 100644
--- a/media/java/android/media/AudioManager.java
+++ b/media/java/android/media/AudioManager.java
@@ -444,6 +444,19 @@
 
     /**
      * @hide
+     * @param KeyEvent
+     */
+    protected void dispatchMediaKeyEvent(KeyEvent keyEvent) {
+        IAudioService service = getService();
+        try {
+            service.dispatchMediaKeyEvent(keyEvent);
+        } catch (RemoteException e) {
+            Log.e(TAG, "dispatchMediaKeyEvent threw exception ", e);
+        }
+    }
+
+    /**
+     * @hide
      */
     public void preDispatchKeyEvent(KeyEvent event, int stream) {
         /*
@@ -2235,6 +2248,49 @@
 
     /**
      * @hide
+     * CANDIDATE FOR PUBLIC API
+     * @param rctlr
+     * @return true if the {@link RemoteController} was successfully registered, false if an
+     *     error occurred, due to an internal system error, or insufficient permissions.
+     */
+    public boolean registerRemoteController(RemoteController rctlr) {
+        if (rctlr == null) {
+            return false;
+        }
+        IAudioService service = getService();
+        try {
+            boolean reg = service.registerRemoteControlDisplay(rctlr.getRcDisplay(),
+                    // passing a negative value for art work width and height
+                    //   as they are still unknown at this stage
+                    /*w*/-1, /*h*/ -1);
+            rctlr.setIsRegistered(reg);
+            return reg;
+        } catch (RemoteException e) {
+            Log.e(TAG, "Dead object in registerRemoteControlDisplay " + e);
+            return false;
+        }
+    }
+
+    /**
+     * @hide
+     * CANDIDATE FOR PUBLIC API
+     * @param rctlr
+     */
+    public void unregisterRemoteController(RemoteController rctlr) {
+        if (rctlr == null) {
+            return;
+        }
+        IAudioService service = getService();
+        try {
+            service.unregisterRemoteControlDisplay(rctlr.getRcDisplay());
+            rctlr.setIsRegistered(false);
+        } catch (RemoteException e) {
+            Log.e(TAG, "Dead object in unregisterRemoteControlDisplay " + e);
+        }
+    }
+
+    /**
+     * @hide
      * Registers a remote control display that will be sent information by remote control clients.
      * Use this method if your IRemoteControlDisplay is not going to display artwork, otherwise
      * use {@link #registerRemoteControlDisplay(IRemoteControlDisplay, int, int)} to pass the
@@ -2263,8 +2319,6 @@
         }
         IAudioService service = getService();
         try {
-            // passing a negative value for art work width and height as they are unknown at
-            // this stage
             service.registerRemoteControlDisplay(rcd, w, h);
         } catch (RemoteException e) {
             Log.e(TAG, "Dead object in registerRemoteControlDisplay " + e);
@@ -2357,13 +2411,15 @@
 
     /**
      * @hide
-     * Notify the user of a RemoteControlClient that it should update its metadata
+     * Notify the user of a RemoteControlClient that it should update its metadata with the
+     * new value for the given key.
      * @param generationId the RemoteControlClient generation counter for which this request is
      *         issued. Requests for an older generation than current one will be ignored.
      * @param key the metadata key for which a new value exists
      * @param value the new metadata value
      */
-    public void updateRemoteControlClientMetadata(int generationId, int key, long value) {
+    public void updateRemoteControlClientMetadata(int generationId, int key,
+            Rating value) {
         IAudioService service = getService();
         try {
             service.updateRemoteControlClientMetadata(generationId, key, value);
diff --git a/media/java/android/media/AudioService.java b/media/java/android/media/AudioService.java
index 07f0858..3425c91 100644
--- a/media/java/android/media/AudioService.java
+++ b/media/java/android/media/AudioService.java
@@ -4143,8 +4143,17 @@
     //==========================================================================================
     // RemoteControlDisplay / RemoteControlClient / Remote info
     //==========================================================================================
-    public void registerRemoteControlDisplay(IRemoteControlDisplay rcd, int w, int h) {
-        mMediaFocusControl.registerRemoteControlDisplay(rcd, w, h);
+    public boolean registerRemoteControlDisplay(IRemoteControlDisplay rcd, int w, int h) {
+        if (PackageManager.PERMISSION_GRANTED == mContext.checkCallingOrSelfPermission(
+                android.Manifest.permission.MEDIA_CONTENT_CONTROL)) {
+            mMediaFocusControl.registerRemoteControlDisplay(rcd, w, h);
+            return true;
+        } else {
+            Log.w(TAG, "Access denied to process: " + Binder.getCallingPid() +
+                    ", must have permission " + android.Manifest.permission.MEDIA_CONTENT_CONTROL +
+                    " to register IRemoteControlDisplay");
+            return false;
+        }
     }
 
     public void unregisterRemoteControlDisplay(IRemoteControlDisplay rcd) {
@@ -4190,7 +4199,7 @@
         mMediaFocusControl.setRemoteControlClientPlaybackPosition(generationId, timeMs);
     }
 
-    public void updateRemoteControlClientMetadata(int generationId, int key, long value) {
+    public void updateRemoteControlClientMetadata(int generationId, int key, Rating value) {
         mMediaFocusControl.updateRemoteControlClientMetadata(generationId, key, value);
     }
 
diff --git a/media/java/android/media/IAudioService.aidl b/media/java/android/media/IAudioService.aidl
index e08ecbf..e3b87dd 100644
--- a/media/java/android/media/IAudioService.aidl
+++ b/media/java/android/media/IAudioService.aidl
@@ -26,6 +26,7 @@
 import android.media.IRemoteControlDisplay;
 import android.media.IRemoteVolumeObserver;
 import android.media.IRingtonePlayer;
+import android.media.Rating;
 import android.net.Uri;
 import android.view.KeyEvent;
 
@@ -140,7 +141,7 @@
      * @param h the maximum height of the expected bitmap. Negative or zero values indicate this
      *   display doesn't need to receive artwork.
      */
-    oneway void   registerRemoteControlDisplay(in IRemoteControlDisplay rcd, int w, int h);
+    boolean registerRemoteControlDisplay(in IRemoteControlDisplay rcd, int w, int h);
     /**
      * Unregister an IRemoteControlDisplay.
      * No effect if the IRemoteControlDisplay hasn't been successfully registered.
@@ -178,13 +179,14 @@
      */
      void setRemoteControlClientPlaybackPosition(int generationId, long timeMs);
      /**
-      * Notify the user of a RemoteControlClient that it should update its metadata
+      * Notify the user of a RemoteControlClient that it should update its metadata with the
+      * new value for the given key.
       * @param generationId the RemoteControlClient generation counter for which this request is
       *         issued. Requests for an older generation than current one will be ignored.
       * @param key the metadata key for which a new value exists
       * @param value the new metadata value
       */
-     void updateRemoteControlClientMetadata(int generationId, int key, long value);
+     void updateRemoteControlClientMetadata(int generationId, int key, in Rating value);
 
     /**
      * Do not use directly, use instead
diff --git a/media/java/android/media/MediaFocusControl.java b/media/java/android/media/MediaFocusControl.java
index 34dc580..143ddf2 100644
--- a/media/java/android/media/MediaFocusControl.java
+++ b/media/java/android/media/MediaFocusControl.java
@@ -138,7 +138,7 @@
     private static final int MSG_PROMOTE_RCC = 6;
     private static final int MSG_RCC_NEW_PLAYBACK_STATE = 7;
     private static final int MSG_RCC_SEEK_REQUEST = 8;
-    private static final int MSG_RCC_UPDATE_METADATA_LONG = 9;
+    private static final int MSG_RCC_UPDATE_METADATA = 9;
 
     // sendMsg() flags
     /** If the msg is already queued, replace it with this one. */
@@ -206,9 +206,9 @@
                             msg.arg1 /* generationId */, ((Long)msg.obj).longValue() /* timeMs */);
                     break;
 
-                case MSG_RCC_UPDATE_METADATA_LONG:
-                    onUpdateRemoteControlClientMetadataLong(msg.arg1 /*genId*/, msg.arg2 /*key*/,
-                            ((Long)msg.obj).longValue() /* value */);
+                case MSG_RCC_UPDATE_METADATA:
+                    onUpdateRemoteControlClientMetadata(msg.arg1 /*genId*/, msg.arg2 /*key*/,
+                            (Rating) msg.obj /* value */);
                     break;
 
                 case MSG_PROMOTE_RCC:
@@ -720,11 +720,7 @@
         }
     }
 
-    private static boolean isValidMediaKeyEvent(KeyEvent keyEvent) {
-        if (keyEvent == null) {
-            return false;
-        }
-        final int keyCode = keyEvent.getKeyCode();
+    protected static boolean isMediaKeyCode(int keyCode) {
         switch (keyCode) {
             case KeyEvent.KEYCODE_MUTE:
             case KeyEvent.KEYCODE_HEADSETHOOK:
@@ -740,11 +736,17 @@
             case KeyEvent.KEYCODE_MEDIA_CLOSE:
             case KeyEvent.KEYCODE_MEDIA_EJECT:
             case KeyEvent.KEYCODE_MEDIA_AUDIO_TRACK:
-                break;
+                return true;
             default:
                 return false;
         }
-        return true;
+    }
+
+    private static boolean isValidMediaKeyEvent(KeyEvent keyEvent) {
+        if (keyEvent == null) {
+            return false;
+        }
+        return MediaFocusControl.isMediaKeyCode(keyEvent.getKeyCode());
     }
 
     /**
@@ -2080,24 +2082,21 @@
         }
     }
 
-    protected void updateRemoteControlClientMetadata(int genId, int key, long value) {
-        sendMsg(mEventHandler, MSG_RCC_UPDATE_METADATA_LONG, SENDMSG_QUEUE,
-                genId /* arg1 */, key /* arg2 */, Long.valueOf(value) /* obj */, 0 /* delay */);
+    protected void updateRemoteControlClientMetadata(int genId, int key, Rating value) {
+        sendMsg(mEventHandler, MSG_RCC_UPDATE_METADATA, SENDMSG_QUEUE,
+                genId /* arg1 */, key /* arg2 */, value /* obj */, 0 /* delay */);
     }
 
-    private void onUpdateRemoteControlClientMetadataLong(int genId, int key, long value) {
-        if(DEBUG_RC) Log.d(TAG, "onUpdateRemoteControlClientMetadataLong(genId=" + genId +
-                ", what=" + key + ",val=" + value + ")");
+    private void onUpdateRemoteControlClientMetadata(int genId, int key, Rating value) {
+        if(DEBUG_RC) Log.d(TAG, "onUpdateRemoteControlClientMetadata(genId=" + genId +
+                ", what=" + key + ",rating=" + value + ")");
         synchronized(mRCStack) {
             synchronized(mCurrentRcLock) {
                 if ((mCurrentRcClient != null) && (mCurrentRcClientGen == genId)) {
                     try {
                         switch (key) {
-                            case RemoteControlClient.MetadataEditor.RATING_KEY_BY_USER:
-                                // TODO handle rating update, placeholder code here that sends
-                                //      an unrated percent-based rating
-                                mCurrentRcClient.updateMetadata(genId, key,
-                                        Rating.newUnratedRating(Rating.RATING_PERCENTAGE));
+                            case MediaMetadataEditor.RATING_KEY_BY_USER:
+                                mCurrentRcClient.updateMetadata(genId, key, value);
                                 break;
                             default:
                                 Log.e(TAG, "unhandled metadata key " + key + " update for RCC "
diff --git a/media/java/android/media/Rating.java b/media/java/android/media/Rating.java
index 48443ff..82c0392 100644
--- a/media/java/android/media/Rating.java
+++ b/media/java/android/media/Rating.java
@@ -75,6 +75,16 @@
         mRatingValue = rating;
     }
 
+
+    /**
+     * @hide
+     */
+    @Override
+    public String toString () {
+        return "Rating:style=" + mRatingStyle + " rating="
+                + (mRatingValue < 0.0f ? "unrated" : String.valueOf(mRatingValue));
+    }
+
     @Override
     public int describeContents() {
         return mRatingStyle;
diff --git a/media/java/android/media/RemoteControlClient.java b/media/java/android/media/RemoteControlClient.java
index f8faf3a..4b5a1ca 100644
--- a/media/java/android/media/RemoteControlClient.java
+++ b/media/java/android/media/RemoteControlClient.java
@@ -715,7 +715,7 @@
      * Implement this interface to receive metadata updates after registering your listener
      * through {@link RemoteControlClient#setMetadataUpdateListener(OnMetadataUpdateListener)}.
      */
-    public interface OnMetadataUpdateListener  {
+    public interface OnMetadataUpdateListener {
         /**
          * Called on the implementer to notify that the metadata field for the given key has
          * been updated to the new value.
diff --git a/media/java/android/media/RemoteController.java b/media/java/android/media/RemoteController.java
new file mode 100644
index 0000000..6266160
--- /dev/null
+++ b/media/java/android/media/RemoteController.java
@@ -0,0 +1,796 @@
+/*
+ * Copyright (C) 2013 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package android.media;
+
+import android.app.PendingIntent;
+import android.app.PendingIntent.CanceledException;
+import android.content.Context;
+import android.content.Intent;
+import android.graphics.Bitmap;
+import android.media.IRemoteControlDisplay;
+import android.media.MediaMetadataEditor;
+import android.os.Bundle;
+import android.os.Handler;
+import android.os.Looper;
+import android.os.Message;
+import android.os.RemoteException;
+import android.os.ServiceManager;
+import android.util.Log;
+import android.view.KeyEvent;
+
+/**
+ * @hide
+ * CANDIDATE FOR PUBLIC API
+ * The RemoteController class is used to control media playback, display and update media metadata
+ * and playback status, published by applications using the {@link RemoteControlClient} class.
+ * <p>
+ * A RemoteController shall be registered through
+ * {@link AudioManager#registerRemoteController(RemoteController)} in order for the system to send
+ * media event updates to the listener set in
+ * {@link #setOnClientUpdateListener(OnClientUpdateListener)}. This listener is a subclass of
+ * the {@link OnClientUpdateListener} abstract class. Override its methods to receive the
+ * information published by the active {@link RemoteControlClient} instances.
+ * By default an {@link OnClientUpdateListener} implementation will not receive bitmaps for album
+ * art. Use {@link #setBitmapConfiguration(boolean, int, int)} to receive images as well.
+ * <p>
+ * A RemoteController can also be used without being registered, when it is only meant to send
+ * media key events (for play or stop events for instance),
+ * with {@link #sendMediaKeyEvent(KeyEvent)}.
+ */
+public class RemoteController
+{
+    private final static int MAX_BITMAP_DIMENSION = 512;
+    private final static int TRANSPORT_UNKNOWN = 0;
+    private RcDisplay mRcd;
+    private final static String TAG = "RemoteController";
+    private final static boolean DEBUG = false;
+    private final static Object mGenLock = new Object();
+    private final static Object mInfoLock = new Object();
+    private Context mContext;
+    private AudioManager mAudioManager;
+    private MetadataEditor mMetadataEditor;
+
+    /**
+     * Synchronized on mGenLock
+     */
+    private int mClientGenerationIdCurrent = 0;
+
+    /**
+     * Synchronized on mInfoLock
+     */
+    private boolean mIsRegistered = false;
+    private PendingIntent mClientPendingIntentCurrent;
+    private OnClientUpdateListener mOnClientUpdateListener;
+    private PlaybackInfo mLastPlaybackInfo;
+    private int mLastTransportControlFlags = TRANSPORT_UNKNOWN;
+
+    /**
+     * @hide
+     * CANDIDATE FOR PUBLIC API
+     * @param ctxt non-null {@link Context}
+     * @throws java.lang.IllegalArgumentException
+     */
+    public RemoteController(Context ctxt) throws IllegalArgumentException {
+        if (ctxt == null) {
+            throw new IllegalArgumentException("Invalid null Context");
+        }
+        Looper looper;
+        if ((looper = Looper.myLooper()) != null) {
+            mEventHandler = new EventHandler(this, looper);
+        } else if ((looper = Looper.getMainLooper()) != null) {
+            mEventHandler = new EventHandler(this, looper);
+        } else {
+            mEventHandler = null;
+            Log.e(TAG, "RemoteController() couldn't find main application thread");
+        }
+        mContext = ctxt;
+        mRcd = new RcDisplay();
+        mAudioManager = (AudioManager) ctxt.getSystemService(Context.AUDIO_SERVICE);
+    }
+
+    /**
+     * @hide
+     * CANDIDATE FOR PUBLIC API
+     * @param looper
+     * @param ctxt non-null {@link Context}
+     * @throws java.lang.IllegalArgumentException
+     */
+    public RemoteController(Looper looper, Context ctxt) throws IllegalArgumentException {
+        if (ctxt == null) {
+            throw new IllegalArgumentException("Invalid null Context");
+        }
+        mEventHandler = new EventHandler(this, looper);
+        mContext = ctxt;
+        mRcd = new RcDisplay();
+        mAudioManager = (AudioManager) ctxt.getSystemService(Context.AUDIO_SERVICE);
+    }
+
+
+    /**
+     * @hide
+     * CANDIDATE FOR PUBLIC API
+     */
+    public static abstract class OnClientUpdateListener {
+        /**
+         * @hide
+         * CANDIDATE FOR PUBLIC API
+         * @param clearing
+         */
+        public void onClientReset(boolean clearing) { }
+        /**
+         * @hide
+         * CANDIDATE FOR PUBLIC API
+         * @param state
+         */
+        public void onClientPlaybackStateUpdate(int state) { }
+        /**
+         * @hide
+         * CANDIDATE FOR PUBLIC API
+         * @param state
+         * @param stateChangeTimeMs
+         * @param currentPosMs
+         * @param speed
+         */
+        public void onClientPlaybackStateUpdate(int state, long stateChangeTimeMs,
+                long currentPosMs, float speed) { }
+        /**
+         * @hide
+         * CANDIDATE FOR PUBLIC API
+         * @param transportControlFlags
+         * @param posCapabilities
+         */
+        public void onClientTransportControlUpdate(int transportControlFlags) { }
+        /**
+         * @hide
+         * CANDIDATE FOR PUBLIC API
+         * @param metadataEditor
+         */
+        public void onClientMetadataUpdate(MetadataEditor metadataEditor) { }
+    };
+
+    /**
+     * @hide
+     * CANDIDATE FOR PUBLIC API
+     * @param l
+     */
+    public void setOnClientUpdateListener(OnClientUpdateListener l) {
+        synchronized(mInfoLock) {
+            mOnClientUpdateListener = l;
+            if (!mIsRegistered) {
+                // since the object is not registered, it hasn't received any information from
+                // RemoteControlClients yet, so we can exit here.
+                return;
+            }
+            if (mLastPlaybackInfo != null) {
+                sendMsg(mEventHandler, MSG_NEW_PLAYBACK_INFO, SENDMSG_REPLACE,
+                        mClientGenerationIdCurrent /*arg1*/, 0,
+                        mLastPlaybackInfo /*obj*/, 0 /*delay*/);
+            }
+            if (mLastTransportControlFlags != TRANSPORT_UNKNOWN) {
+                sendMsg(mEventHandler, MSG_NEW_TRANSPORT_INFO, SENDMSG_REPLACE,
+                        mClientGenerationIdCurrent /*arg1*/, mLastTransportControlFlags /*arg2*/,
+                        null /*obj*/, 0 /*delay*/);
+            }
+            if (mMetadataEditor != null) {
+                sendMsg(mEventHandler, MSG_NEW_METADATA, SENDMSG_QUEUE,
+                        mClientGenerationIdCurrent /*arg1*/, 0 /*arg2*/,
+                        mMetadataEditor /*obj*/, 0 /*delay*/);
+            }
+        }
+    }
+
+
+    /**
+     * @hide
+     * CANDIDATE FOR PUBLIC API
+     * Send a simulated key event for a media button.
+     * May be used without registering the RemoteController
+     * with {@link AudioManager#registerRemoteController(RemoteController)}. To simulate a key
+     * press, you must first send a KeyEvent built with a {@link KeyEvent#ACTION_DOWN} action, then
+     * another event with the {@link KeyEvent#ACTION_UP} action.
+     * <p> When used from a registered RemoteController, the key event will be sent to the
+     * application currently promoted to publish its media metadata and playback state (there may be
+     * none under some circumstances). With an unregistered RemoteController, the key event will be
+     * sent to the current media key event consumer
+     * (see {@link AudioManager#registerMediaButtonEventReceiver(PendingIntent)}).
+     * @param keyEvent a {@link KeyEvent} instance whose key code is one of
+     *     {@link KeyEvent.KEYCODE_MUTE},
+     *     {@link KeyEvent.KEYCODE_HEADSETHOOK},
+     *     {@link KeyEvent.KEYCODE_MEDIA_PLAY},
+     *     {@link KeyEvent.KEYCODE_MEDIA_PAUSE},
+     *     {@link KeyEvent.KEYCODE_MEDIA_PLAY_PAUSE},
+     *     {@link KeyEvent.KEYCODE_MEDIA_STOP},
+     *     {@link KeyEvent.KEYCODE_MEDIA_NEXT},
+     *     {@link KeyEvent.KEYCODE_MEDIA_PREVIOUS},
+     *     {@link KeyEvent.KEYCODE_MEDIA_REWIND},
+     *     {@link KeyEvent.KEYCODE_MEDIA_RECORD},
+     *     {@link KeyEvent.KEYCODE_MEDIA_FAST_FORWARD},
+     *     {@link KeyEvent.KEYCODE_MEDIA_CLOSE},
+     *     {@link KeyEvent.KEYCODE_MEDIA_EJECT},
+     *     or {@link KeyEvent.KEYCODE_MEDIA_AUDIO_TRACK}.
+     */
+    public int sendMediaKeyEvent(KeyEvent keyEvent) {
+        if (!MediaFocusControl.isMediaKeyCode(keyEvent.getKeyCode())) {
+            Log.e(TAG, "Cannot use sendMediaKeyEvent() for a non-media key event");
+            return ERROR_BAD_VALUE;
+        }
+        boolean registered = false;
+        final PendingIntent pi;
+        synchronized(mInfoLock) {
+            registered = mIsRegistered;
+            pi = mClientPendingIntentCurrent;
+        }
+        if (registered) {
+            if (pi != null) {
+                Intent intent = new Intent(Intent.ACTION_MEDIA_BUTTON);
+                intent.putExtra(Intent.EXTRA_KEY_EVENT, keyEvent);
+                try {
+                    pi.send(mContext, 0, intent);
+                } catch (CanceledException e) {
+                    Log.e(TAG, "Error sending intent for media button down: ", e);
+                    return ERROR;
+                }
+            } else {
+                Log.i(TAG, "No-op when sending key click, no receiver right now");
+                return ERROR;
+            }
+        } else {
+            mAudioManager.dispatchMediaKeyEvent(keyEvent);
+        }
+        return SUCCESS;
+    }
+
+
+    // Error codes
+    /**
+     * @hide
+     * CANDIDATE FOR PUBLIC API
+     * Successful operation.
+     */
+    public  static final int SUCCESS            = 0;
+    /**
+     * @hide
+     * CANDIDATE FOR PUBLIC API
+     * Unspecified error.
+     */
+    public  static final int ERROR              = -1;
+    /**
+     * @hide
+     * CANDIDATE FOR PUBLIC API
+     * Operation failed due to bad parameter value.
+     */
+    public  static final int ERROR_BAD_VALUE    = -2;
+
+
+    /**
+     * @hide
+     * CANDIDATE FOR PUBLIC API
+     * @param timeMs
+     * @return {@link #SUCCESS}, {@link #ERROR} or {@link #ERROR_BAD_VALUE}
+     */
+    public int seekTo(long timeMs) {
+        if (timeMs < 0) {
+            return ERROR_BAD_VALUE;
+        }
+        final int genId;
+        synchronized (mGenLock) {
+            genId = mClientGenerationIdCurrent;
+        }
+        mAudioManager.setRemoteControlClientPlaybackPosition(genId, timeMs);
+        return SUCCESS;
+    }
+
+
+    /**
+     * @hide
+     * must be called on a registered RemoteController
+     * @param wantBitmap
+     * @param width
+     * @param height
+     * @return {@link #SUCCESS}, {@link #ERROR} or {@link #ERROR_BAD_VALUE}
+     */
+    public int setBitmapConfiguration(boolean wantBitmap, int width, int height) {
+        synchronized (mInfoLock) {
+            if (!mIsRegistered) {
+                Log.e(TAG, "Cannot specify bitmap configuration on unregistered RemoteController");
+                return ERROR;
+            }
+        }
+        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; }
+                mAudioManager.remoteControlDisplayUsesBitmapSize(mRcd, width, height);
+            } else {
+                Log.e(TAG, "Invalid dimensions");
+                return ERROR_BAD_VALUE;
+            }
+        } else {
+            mAudioManager.remoteControlDisplayUsesBitmapSize(mRcd, -1, -1);
+        }
+        return SUCCESS;
+    }
+
+    /**
+     * @hide
+     * CANDIDATE FOR PUBLIC API
+     * must be called on a registered RemoteController
+     * @param width
+     * @param height
+     * @return {@link #SUCCESS}, {@link #ERROR} or {@link #ERROR_BAD_VALUE}
+     */
+    public int setBitmapConfiguration(int width, int height) {
+        return setBitmapConfiguration(true, width, height);
+    }
+
+    /**
+     * @hide
+     * CANDIDATE FOR PUBLIC API
+     * must be called on a registered RemoteController
+     * @return {@link #SUCCESS}, {@link #ERROR}
+     */
+    public int setBitmapConfigurationNone() {
+        return setBitmapConfiguration(false, -1, -1);
+    }
+
+
+    /**
+     * @hide
+     * CANDIDATE FOR PUBLIC API
+     * Default playback position synchronization mode where the RemoteControlClient is not
+     * asked regularly for its playback position to see if it has drifted from the estimated
+     * position.
+     */
+    public static final int POSITION_SYNCHRONIZATION_NONE = 0;
+
+    /**
+     * @hide
+     * CANDIDATE FOR PUBLIC API
+     * The playback position synchronization mode where the RemoteControlClient instances which
+     * expose their playback position to the framework, will be regularly polled to check
+     * whether any drift has been noticed between their estimated position and the one they report.
+     * Note that this mode should only ever be used when needing to display very accurate playback
+     * position, as regularly polling a RemoteControlClient for its position may have an impact
+     * on battery life (if applicable) when this query will trigger network transactions in the
+     * case of remote playback.
+     */
+    public static final int POSITION_SYNCHRONIZATION_CHECK = 1;
+
+    /**
+     * @hide
+     * CANDIDATE FOR PUBLIC API
+     * Set the playback position synchronization mode.
+     * Must be called on a registered RemoteController.
+     * @param sync {@link #POSITION_SYNCHRONIZATION_NONE} or {@link #POSITION_SYNCHRONIZATION_CHECK}
+     * @return {@link #SUCCESS}, {@link #ERROR} or {@link #ERROR_BAD_VALUE}
+     */
+    public int setSynchronizationMode(int sync) {
+        if ((sync != POSITION_SYNCHRONIZATION_NONE) || (sync != POSITION_SYNCHRONIZATION_CHECK)) {
+            Log.e(TAG, "Unknown synchronization mode");
+            return ERROR_BAD_VALUE;
+        }
+        if (!mIsRegistered) {
+            Log.e(TAG, "Cannot set synchronization mode on an unregistered RemoteController");
+            return ERROR;
+        }
+        mAudioManager.remoteControlDisplayWantsPlaybackPositionSync(mRcd,
+                POSITION_SYNCHRONIZATION_CHECK == sync);
+        return SUCCESS;
+    }
+
+
+    /**
+     * @hide
+     * CANDIDATE FOR PUBLIC API
+     * Creates a {@link MetadataEditor} for updating metadata values of the editable keys of
+     * the current {@link RemoteControlClient}.
+     * @return a new MetadataEditor instance.
+     */
+    public MetadataEditor editMetadata() {
+        MetadataEditor editor = new MetadataEditor();
+        editor.mEditorMetadata = new Bundle();
+        editor.mEditorArtwork = null;
+        editor.mMetadataChanged = true;
+        editor.mArtworkChanged = true;
+        editor.mEditableKeys = 0;
+        return editor;
+    }
+
+
+    /**
+     * @hide
+     * CANDIDATE FOR PUBLIC API
+     * Used to read the metadata published by a {@link RemoteControlClient}, or send a
+     * {@link RemoteControlClient} new values for keys that can be edited.
+     */
+    public class MetadataEditor extends MediaMetadataEditor {
+        /**
+         * @hide
+         */
+        protected MetadataEditor() { }
+
+        /**
+         * @hide
+         */
+        protected MetadataEditor(Bundle metadata, long editableKeys) {
+            mEditorMetadata = metadata;
+            mEditableKeys = editableKeys;
+            mEditorArtwork = null;
+            mMetadataChanged = true;
+            mArtworkChanged = true;
+            mApplied = false;
+        }
+
+        /**
+         * @hide
+         * CANDIDATE FOR PUBLIC API
+         */
+        public synchronized void apply() {
+            // "applying" a metadata bundle in RemoteController is only for sending edited
+            // key values back to the RemoteControlClient, so here we only care about the only
+            // editable key we support: RATING_KEY_BY_USER
+            if (!mMetadataChanged) {
+                return;
+            }
+            final int genId;
+            synchronized(mGenLock) {
+                genId = mClientGenerationIdCurrent;
+            }
+            synchronized(mInfoLock) {
+                if (mEditorMetadata.containsKey(
+                        String.valueOf(MediaMetadataEditor.RATING_KEY_BY_USER))) {
+                    Rating rating = (Rating) getObject(
+                            MediaMetadataEditor.RATING_KEY_BY_USER, null);
+                    mAudioManager.updateRemoteControlClientMetadata(genId,
+                          MediaMetadataEditor.RATING_KEY_BY_USER,
+                          rating);
+                } else {
+                    Log.e(TAG, "no metadata to apply");
+                }
+                // NOT setting mApplied to true as this type of MetadataEditor will be applied
+                // multiple times, whenever the user of a RemoteController needs to change the
+                // metadata (e.g. user changes the rating of a song more than once during playback)
+                mApplied = false;
+            }
+        }
+
+    }
+
+
+    //==================================================
+    // Implementation of IRemoteControlDisplay interface
+    private class RcDisplay extends IRemoteControlDisplay.Stub {
+        /**
+         * @hide
+         */
+        public void setCurrentClientId(int genId, PendingIntent clientMediaIntent,
+                boolean clearing) {
+            boolean isNew = false;
+            synchronized(mGenLock) {
+                if (mClientGenerationIdCurrent != genId) {
+                    mClientGenerationIdCurrent = genId;
+                    isNew = true;
+                }
+            }
+            if (clientMediaIntent != null) {
+                sendMsg(mEventHandler, MSG_NEW_PENDING_INTENT, SENDMSG_REPLACE,
+                        genId /*arg1*/, 0, clientMediaIntent /*obj*/, 0 /*delay*/);
+            }
+            if (isNew || clearing) {
+                sendMsg(mEventHandler, MSG_CLIENT_RESET, SENDMSG_REPLACE,
+                        genId /*arg1*/, clearing ? 1 : 0, null /*obj*/, 0 /*delay*/);
+            }
+        }
+
+        /**
+         * @hide
+         */
+        public void setPlaybackState(int genId, int state,
+                long stateChangeTimeMs, long currentPosMs, float speed) {
+            if (DEBUG) {
+                Log.d(TAG, "> new playback state: genId="+genId
+                        + " state="+ state
+                        + " changeTime="+ stateChangeTimeMs
+                        + " pos=" + currentPosMs
+                        + "ms speed=" + speed);
+            }
+
+            synchronized(mGenLock) {
+                if (mClientGenerationIdCurrent != genId) {
+                    return;
+                }
+            }
+            final PlaybackInfo playbackInfo =
+                    new PlaybackInfo(state, stateChangeTimeMs, currentPosMs, speed);
+            sendMsg(mEventHandler, MSG_NEW_PLAYBACK_INFO, SENDMSG_REPLACE,
+                    genId /*arg1*/, 0, playbackInfo /*obj*/, 0 /*delay*/);
+
+        }
+
+        /**
+         * @hide
+         */
+        public void setTransportControlInfo(int genId, int transportControlFlags,
+                int posCapabilities) {
+            synchronized(mGenLock) {
+                if (mClientGenerationIdCurrent != genId) {
+                    return;
+                }
+            }
+            sendMsg(mEventHandler, MSG_NEW_TRANSPORT_INFO, SENDMSG_REPLACE,
+                    genId /*arg1*/, transportControlFlags /*arg2*/,
+                    null /*obj*/, 0 /*delay*/);
+        }
+
+        /**
+         * @hide
+         */
+        public void setMetadata(int genId, Bundle metadata) {
+            if (DEBUG) { Log.e(TAG, "setMetadata("+genId+")"); }
+            if (metadata == null) {
+                return;
+            }
+            synchronized(mGenLock) {
+                if (mClientGenerationIdCurrent != genId) {
+                    return;
+                }
+            }
+            sendMsg(mEventHandler, MSG_NEW_METADATA, SENDMSG_QUEUE,
+                    genId /*arg1*/, 0 /*arg2*/,
+                    metadata /*obj*/, 0 /*delay*/);
+        }
+
+        /**
+         * @hide
+         */
+        public void setArtwork(int genId, Bitmap artwork) {
+            if (DEBUG) { Log.v(TAG, "setArtwork("+genId+")"); }
+            if (artwork == null) {
+                return;
+            }
+            synchronized(mGenLock) {
+                if (mClientGenerationIdCurrent != genId) {
+                    return;
+                }
+            }
+            Bundle metadata = new Bundle(1);
+            metadata.putParcelable(String.valueOf(MediaMetadataEditor.BITMAP_KEY_ARTWORK), artwork);
+            sendMsg(mEventHandler, MSG_NEW_METADATA, SENDMSG_QUEUE,
+                    genId /*arg1*/, 0 /*arg2*/,
+                    metadata /*obj*/, 0 /*delay*/);
+        }
+
+        /**
+         * @hide
+         */
+        public void setAllMetadata(int genId, Bundle metadata, Bitmap artwork) {
+            if (DEBUG) { Log.e(TAG, "setAllMetadata("+genId+")"); }
+            if ((metadata == null) && (artwork == null)) {
+                return;
+            }
+            synchronized(mGenLock) {
+                if (mClientGenerationIdCurrent != genId) {
+                    return;
+                }
+            }
+            if (metadata == null) {
+                metadata = new Bundle(1);
+            }
+            if (artwork != null) {
+                metadata.putParcelable(String.valueOf(MediaMetadataEditor.BITMAP_KEY_ARTWORK),
+                        artwork);
+            }
+            sendMsg(mEventHandler, MSG_NEW_METADATA, SENDMSG_QUEUE,
+                    genId /*arg1*/, 0 /*arg2*/,
+                    metadata /*obj*/, 0 /*delay*/);
+        }
+    }
+
+    //==================================================
+    // Event handling
+    private EventHandler mEventHandler;
+    private final static int MSG_NEW_PENDING_INTENT = 0;
+    private final static int MSG_NEW_PLAYBACK_INFO =  1;
+    private final static int MSG_NEW_TRANSPORT_INFO = 2;
+    private final static int MSG_NEW_METADATA       = 3; // msg always has non-null obj parameter
+    private final static int MSG_CLIENT_RESET       = 4;
+
+    private class EventHandler extends Handler {
+
+        public EventHandler(RemoteController rc, Looper looper) {
+            super(looper);
+        }
+
+        @Override
+        public void handleMessage(Message msg) {
+            switch(msg.what) {
+                case MSG_NEW_PENDING_INTENT:
+                    onNewPendingIntent(msg.arg1, (PendingIntent) msg.obj);
+                    break;
+                case MSG_NEW_PLAYBACK_INFO:
+                    onNewPlaybackInfo(msg.arg1, (PlaybackInfo) msg.obj);
+                    break;
+                case MSG_NEW_TRANSPORT_INFO:
+                    onNewTransportInfo(msg.arg1, msg.arg2);
+                    break;
+                case MSG_NEW_METADATA:
+                    onNewMetadata(msg.arg1, (Bundle)msg.obj);
+                    break;
+                case MSG_CLIENT_RESET:
+                    onClientReset(msg.arg1, msg.arg2 == 1);
+                    break;
+                default:
+                    Log.e(TAG, "unknown event " + msg.what);
+            }
+        }
+    }
+
+    /** If the msg is already queued, replace it with this one. */
+    private static final int SENDMSG_REPLACE = 0;
+    /** If the msg is already queued, ignore this one and leave the old. */
+    private static final int SENDMSG_NOOP = 1;
+    /** If the msg is already queued, queue this one and leave the old. */
+    private static final int SENDMSG_QUEUE = 2;
+
+    private static void sendMsg(Handler handler, int msg, int existingMsgPolicy,
+            int arg1, int arg2, Object obj, int delayMs) {
+        if (handler == null) {
+            Log.e(TAG, "null event handler, will not deliver message " + msg);
+            return;
+        }
+        if (existingMsgPolicy == SENDMSG_REPLACE) {
+            handler.removeMessages(msg);
+        } else if (existingMsgPolicy == SENDMSG_NOOP && handler.hasMessages(msg)) {
+            return;
+        }
+        handler.sendMessageDelayed(handler.obtainMessage(msg, arg1, arg2, obj), delayMs);
+    }
+
+    private void onNewPendingIntent(int genId, PendingIntent pi) {
+        synchronized(mGenLock) {
+            if (mClientGenerationIdCurrent != genId) {
+                return;
+            }
+        }
+        synchronized(mInfoLock) {
+            mClientPendingIntentCurrent = pi;
+        }
+    }
+
+    private void onNewPlaybackInfo(int genId, PlaybackInfo pi) {
+        synchronized(mGenLock) {
+            if (mClientGenerationIdCurrent != genId) {
+                return;
+            }
+        }
+        final OnClientUpdateListener l;
+        synchronized(mInfoLock) {
+            l = this.mOnClientUpdateListener;
+            mLastPlaybackInfo = pi;
+        }
+        if (l != null) {
+            if (pi.mCurrentPosMs == RemoteControlClient.PLAYBACK_POSITION_ALWAYS_UNKNOWN) {
+                l.onClientPlaybackStateUpdate(pi.mState);
+            } else {
+                l.onClientPlaybackStateUpdate(pi.mState, pi.mStateChangeTimeMs, pi.mCurrentPosMs,
+                        pi.mSpeed);
+            }
+        }
+    }
+
+    private void onNewTransportInfo(int genId, int transportControlFlags) {
+        synchronized(mGenLock) {
+            if (mClientGenerationIdCurrent != genId) {
+                return;
+            }
+        }
+        final OnClientUpdateListener l;
+        synchronized(mInfoLock) {
+            l = mOnClientUpdateListener;
+            mLastTransportControlFlags = transportControlFlags;
+        }
+        if (l != null) {
+            l.onClientTransportControlUpdate(transportControlFlags);
+        }
+    }
+
+    /**
+     * @param genId
+     * @param metadata guaranteed to be always non-null
+     */
+    private void onNewMetadata(int genId, Bundle metadata) {
+        synchronized(mGenLock) {
+            if (mClientGenerationIdCurrent != genId) {
+                return;
+            }
+        }
+        final OnClientUpdateListener l;
+        final MetadataEditor metadataEditor;
+        // prepare the received Bundle to be used inside a MetadataEditor
+        final long editableKeys = metadata.getLong(
+                String.valueOf(MediaMetadataEditor.KEY_EDITABLE_MASK), 0);
+        if (editableKeys != 0) {
+            metadata.remove(String.valueOf(MediaMetadataEditor.KEY_EDITABLE_MASK));
+        }
+        synchronized(mInfoLock) {
+            l = mOnClientUpdateListener;
+            if ((mMetadataEditor != null) && (mMetadataEditor.mEditorMetadata != null)) {
+                if (mMetadataEditor.mEditorMetadata != metadata) {
+                    // existing metadata, merge existing and new
+                    mMetadataEditor.mEditorMetadata.putAll(metadata);
+                }
+            } else {
+                mMetadataEditor = new MetadataEditor(metadata, editableKeys);
+            }
+            metadataEditor = mMetadataEditor;
+        }
+        if (l != null) {
+            l.onClientMetadataUpdate(metadataEditor);
+        }
+    }
+
+    private void onClientReset(int genId, boolean clearing) {
+        synchronized(mGenLock) {
+            if (mClientGenerationIdCurrent != genId) {
+                return;
+            }
+        }
+        final OnClientUpdateListener l;
+        synchronized(mInfoLock) {
+            l = mOnClientUpdateListener;
+        }
+        if (l != null) {
+            l.onClientReset(clearing);
+        }
+    }
+
+
+    //==================================================
+    private static class PlaybackInfo {
+        int mState;
+        long mStateChangeTimeMs;
+        long mCurrentPosMs;
+        float mSpeed;
+
+        PlaybackInfo(int state, long stateChangeTimeMs, long currentPosMs, float speed) {
+            mState = state;
+            mStateChangeTimeMs = stateChangeTimeMs;
+            mCurrentPosMs = currentPosMs;
+            mSpeed = speed;
+        }
+    }
+
+    /**
+     * @hide
+     * Used by AudioManager to mark this instance as registered.
+     * @param registered
+     */
+    protected void setIsRegistered(boolean registered) {
+        synchronized (mInfoLock) {
+            mIsRegistered = registered;
+        }
+    }
+
+    /**
+     * @hide
+     * Used by AudioManager to access binder to be registered/unregistered inside MediaFocusControl
+     * @return
+     */
+    protected RcDisplay getRcDisplay() {
+        return mRcd;
+    }
+}
diff --git a/packages/Keyguard/AndroidManifest.xml b/packages/Keyguard/AndroidManifest.xml
index 7d77c48..c05d527 100644
--- a/packages/Keyguard/AndroidManifest.xml
+++ b/packages/Keyguard/AndroidManifest.xml
@@ -37,6 +37,7 @@
     <uses-permission android:name="android.permission.INTERNAL_SYSTEM_WINDOW" />
     <uses-permission android:name="android.permission.BIND_DEVICE_ADMIN" />
     <uses-permission android:name="android.permission.CHANGE_COMPONENT_ENABLED_STATE" />
+    <uses-permission android:name="android.permission.MEDIA_CONTENT_CONTROL" />
 
     <application android:label="@string/app_name"
         android:process="com.android.systemui"