Add helper methods to create a pending intent for media button event

Introduced helper methods to create a broadcast pending intent which sends
a media button event to the media button receiver with specified media action.
It will be helpful for developers because it can save lots of code.

Bug: 22718016
Change-Id: I08d2ffd4c666c22d4b57aa5a1e6b57dc8d02382f
diff --git a/api/current.txt b/api/current.txt
index 67359fa..53434f5 100644
--- a/api/current.txt
+++ b/api/current.txt
@@ -5312,6 +5312,8 @@
     ctor public MediaSessionCompat(android.content.Context, java.lang.String);
     ctor public MediaSessionCompat(android.content.Context, java.lang.String, android.content.ComponentName, android.app.PendingIntent);
     method public void addOnActiveChangeListener(android.support.v4.media.session.MediaSessionCompat.OnActiveChangeListener);
+    method public static android.app.PendingIntent buildMediaButtonPendingIntent(android.content.Context, long);
+    method public static android.app.PendingIntent buildMediaButtonPendingIntent(android.content.Context, android.content.ComponentName, long);
     method public android.support.v4.media.session.MediaControllerCompat getController();
     method public java.lang.Object getMediaSession();
     method public java.lang.Object getRemoteControlClient();
@@ -5414,6 +5416,7 @@
     method public java.lang.Object getPlaybackState();
     method public long getPosition();
     method public int getState();
+    method public static int toKeyCode(long);
     method public void writeToParcel(android.os.Parcel, int);
     field public static final long ACTION_FAST_FORWARD = 64L; // 0x40L
     field public static final long ACTION_PAUSE = 2L; // 0x2L
diff --git a/media-compat/java/android/support/v4/media/session/MediaSessionCompat.java b/media-compat/java/android/support/v4/media/session/MediaSessionCompat.java
index e66b894..117eae2 100644
--- a/media-compat/java/android/support/v4/media/session/MediaSessionCompat.java
+++ b/media-compat/java/android/support/v4/media/session/MediaSessionCompat.java
@@ -44,6 +44,7 @@
 import android.support.v4.media.MediaMetadataCompat;
 import android.support.v4.media.RatingCompat;
 import android.support.v4.media.VolumeProviderCompat;
+import android.support.v4.media.session.PlaybackStateCompat.MediaKeyActions;
 import android.text.TextUtils;
 import android.util.Log;
 import android.view.KeyEvent;
@@ -160,6 +161,94 @@
             "android.support.v4.media.session.action.ARGUMENT_EXTRAS";
 
     /**
+     * Creates a broadcast pending intent that will send a media button event. The {@code action}
+     * will be translated to the appropriate {@link KeyEvent}, and it will be sent to the
+     * registered media button receiver in the given context. The {@code action} should be one of
+     * the following:
+     * <ul>
+     * <li>{@link PlaybackStateCompat#ACTION_PLAY}</li>
+     * <li>{@link PlaybackStateCompat#ACTION_PAUSE}</li>
+     * <li>{@link PlaybackStateCompat#ACTION_SKIP_TO_NEXT}</li>
+     * <li>{@link PlaybackStateCompat#ACTION_SKIP_TO_PREVIOUS}</li>
+     * <li>{@link PlaybackStateCompat#ACTION_STOP}</li>
+     * <li>{@link PlaybackStateCompat#ACTION_FAST_FORWARD}</li>
+     * <li>{@link PlaybackStateCompat#ACTION_REWIND}</li>
+     * <li>{@link PlaybackStateCompat#ACTION_PLAY_PAUSE}</li>
+     * </ul>
+     *
+     * @param context The context of the application.
+     * @param action The action to be sent via the pending intent.
+     * @return Created pending intent, or null if cannot find a unique registered media button
+     *         receiver or if the {@code action} is unsupported/invalid.
+     */
+    public static PendingIntent buildMediaButtonPendingIntent(Context context,
+            @MediaKeyActions long action) {
+        ComponentName mbrComponent = getMediaButtonReceiverComponent(context);
+        if (mbrComponent == null) {
+            Log.w(TAG, "A unique media button receiver could not be found in the given context, so "
+                    + "couldn't build a pending intent.");
+            return null;
+        }
+        return buildMediaButtonPendingIntent(context, mbrComponent, action);
+    }
+
+    /**
+     * Creates a broadcast pending intent that will send a media button event. The {@code action}
+     * will be translated to the appropriate {@link KeyEvent}, and sent to the provided media
+     * button receiver via the pending intent. The {@code action} should be one of the following:
+     * <ul>
+     * <li>{@link PlaybackStateCompat#ACTION_PLAY}</li>
+     * <li>{@link PlaybackStateCompat#ACTION_PAUSE}</li>
+     * <li>{@link PlaybackStateCompat#ACTION_SKIP_TO_NEXT}</li>
+     * <li>{@link PlaybackStateCompat#ACTION_SKIP_TO_PREVIOUS}</li>
+     * <li>{@link PlaybackStateCompat#ACTION_STOP}</li>
+     * <li>{@link PlaybackStateCompat#ACTION_FAST_FORWARD}</li>
+     * <li>{@link PlaybackStateCompat#ACTION_REWIND}</li>
+     * <li>{@link PlaybackStateCompat#ACTION_PLAY_PAUSE}</li>
+     * </ul>
+     *
+     * @param context The context of the application.
+     * @param mbrComponent The full component name of a media button receiver where you want to send
+     *            this intent.
+     * @param action The action to be sent via the pending intent.
+     * @return Created pending intent, or null if cannot find a unique registered media button
+     *         receiver or if the {@code action} is unsupported/invalid.
+     */
+    public static PendingIntent buildMediaButtonPendingIntent(Context context,
+            ComponentName mbrComponent, @MediaKeyActions long action) {
+        if (mbrComponent == null) {
+            Log.w(TAG, "The component name of media button receiver should be provided.");
+            return null;
+        }
+        int keyCode = PlaybackStateCompat.toKeyCode(action);
+        if (keyCode == KeyEvent.KEYCODE_UNKNOWN) {
+            Log.w(TAG,
+                    "Cannot build a media button pending intent with the given action: " + action);
+            return null;
+        }
+        Intent intent = new Intent(Intent.ACTION_MEDIA_BUTTON);
+        intent.setComponent(mbrComponent);
+        intent.putExtra(Intent.EXTRA_KEY_EVENT, new KeyEvent(KeyEvent.ACTION_DOWN, keyCode));
+        return PendingIntent.getBroadcast(context, keyCode, intent, 0);
+    }
+
+    private static ComponentName getMediaButtonReceiverComponent(Context context) {
+        Intent queryIntent = new Intent(Intent.ACTION_MEDIA_BUTTON);
+        queryIntent.setPackage(context.getPackageName());
+        PackageManager pm = context.getPackageManager();
+        List<ResolveInfo> resolveInfos = pm.queryBroadcastReceivers(queryIntent, 0);
+        if (resolveInfos.size() == 1) {
+            ResolveInfo resolveInfo = resolveInfos.get(0);
+            return new ComponentName(resolveInfo.activityInfo.packageName,
+                    resolveInfo.activityInfo.name);
+        } else if (resolveInfos.size() > 1) {
+            Log.w(TAG, "More than one BroadcastReceiver that handles "
+                    + Intent.ACTION_MEDIA_BUTTON + " was found, returning null.");
+        }
+        return null;
+    }
+
+    /**
      * Creates a new session. You must call {@link #release()} when finished with the session.
      * <p>
      * The session will automatically be registered with the system but will not be published
@@ -1220,22 +1309,10 @@
         public MediaSessionImplBase(Context context, String tag, ComponentName mbrComponent,
                 PendingIntent mbrIntent) {
             if (mbrComponent == null) {
-                Intent queryIntent = new Intent(Intent.ACTION_MEDIA_BUTTON);
-                queryIntent.setPackage(context.getPackageName());
-                PackageManager pm = context.getPackageManager();
-                List<ResolveInfo> resolveInfos = pm.queryBroadcastReceivers(queryIntent, 0);
-                // If none are found, assume we are running on a newer platform version that does
-                // not require a media button receiver ComponentName. Later code will double check
-                // this assumption and throw an error if needed
-                if (resolveInfos.size() == 1) {
-                    ResolveInfo resolveInfo = resolveInfos.get(0);
-                    mbrComponent = new ComponentName(resolveInfo.activityInfo.packageName,
-                            resolveInfo.activityInfo.name);
-                } else if (resolveInfos.size() > 1) {
-                    Log.w(TAG, "More than one BroadcastReceiver that handles "
-                            + Intent.ACTION_MEDIA_BUTTON + " was found, using null. Provide a "
-                            + "specific ComponentName to use as this session's media button "
-                            + "receiver");
+                mbrComponent = getMediaButtonReceiverComponent(context);
+                if (mbrComponent == null) {
+                    Log.w(TAG, "Couldn't find a unique registered media button receiver in the "
+                            + "given context.");
                 }
             }
             if (mbrComponent != null && mbrIntent == null) {
diff --git a/media-compat/java/android/support/v4/media/session/PlaybackStateCompat.java b/media-compat/java/android/support/v4/media/session/PlaybackStateCompat.java
index e3bda8c..8f9d3b0 100644
--- a/media-compat/java/android/support/v4/media/session/PlaybackStateCompat.java
+++ b/media-compat/java/android/support/v4/media/session/PlaybackStateCompat.java
@@ -24,6 +24,7 @@
 import android.support.annotation.IntDef;
 import android.support.annotation.Nullable;
 import android.text.TextUtils;
+import android.view.KeyEvent;
 
 import java.lang.annotation.Retention;
 import java.lang.annotation.RetentionPolicy;
@@ -49,6 +50,14 @@
     public @interface Actions {}
 
     /**
+     * @hide
+     */
+    @IntDef(flag=true, value={ACTION_STOP, ACTION_PAUSE, ACTION_PLAY, ACTION_REWIND,
+            ACTION_SKIP_TO_PREVIOUS, ACTION_SKIP_TO_NEXT, ACTION_FAST_FORWARD, ACTION_PLAY_PAUSE})
+    @Retention(RetentionPolicy.SOURCE)
+    public @interface MediaKeyActions {}
+
+    /**
      * Indicates this session supports the stop command.
      *
      * @see Builder#setActions(long)
@@ -285,6 +294,49 @@
      */
     public final static long PLAYBACK_POSITION_UNKNOWN = -1;
 
+    // KeyEvent constants only available on API 11+
+    private static final int KEYCODE_MEDIA_PAUSE = 127;
+    private static final int KEYCODE_MEDIA_PLAY = 126;
+
+    /**
+     * Translates a given action into a matched key code defined in {@link KeyEvent}. The given
+     * action should be one of the following:
+     * <ul>
+     * <li>{@link PlaybackStateCompat#ACTION_PLAY}</li>
+     * <li>{@link PlaybackStateCompat#ACTION_PAUSE}</li>
+     * <li>{@link PlaybackStateCompat#ACTION_SKIP_TO_NEXT}</li>
+     * <li>{@link PlaybackStateCompat#ACTION_SKIP_TO_PREVIOUS}</li>
+     * <li>{@link PlaybackStateCompat#ACTION_STOP}</li>
+     * <li>{@link PlaybackStateCompat#ACTION_FAST_FORWARD}</li>
+     * <li>{@link PlaybackStateCompat#ACTION_REWIND}</li>
+     * <li>{@link PlaybackStateCompat#ACTION_PLAY_PAUSE}</li>
+     * </ul>
+     *
+     * @param action The action to be translated.
+     *
+     * @return the key code matched to the given action.
+     */
+    public static int toKeyCode(@MediaKeyActions long action) {
+        if (action == ACTION_PLAY) {
+            return KEYCODE_MEDIA_PLAY;
+        } else if (action == ACTION_PAUSE) {
+            return KEYCODE_MEDIA_PAUSE;
+        } else if (action == ACTION_SKIP_TO_NEXT) {
+            return KeyEvent.KEYCODE_MEDIA_NEXT;
+        } else if (action == ACTION_SKIP_TO_PREVIOUS) {
+            return KeyEvent.KEYCODE_MEDIA_PREVIOUS;
+        } else if (action == ACTION_STOP) {
+            return KeyEvent.KEYCODE_MEDIA_STOP;
+        } else if (action == ACTION_FAST_FORWARD) {
+            return KeyEvent.KEYCODE_MEDIA_FAST_FORWARD;
+        } else if (action == ACTION_REWIND) {
+            return KeyEvent.KEYCODE_MEDIA_REWIND;
+        } else if (action == ACTION_PLAY_PAUSE) {
+            return KeyEvent.KEYCODE_MEDIA_PLAY_PAUSE;
+        }
+        return KeyEvent.KEYCODE_UNKNOWN;
+    }
+
     private final int mState;
     private final long mPosition;
     private final long mBufferedPosition;