Implementation of the policing of transport control key press
events among multiple applications competing for the remote control
focus.
AudioManager defines a new API for applications to use in order to
register their BroadcastReceiver for the media button as the one
to receive the corresponding intent, but all applications at the
same time (in an ordered broadcast).
AudioService handles a stack of remote control focus owners. It
traps ACTION_MEDIA_BUTTON intents and sends a new intent to the
remote control focus owner.

Change-Id: I3c109221ecfb160cbb1ec0e40a71b241aad73812
diff --git a/media/java/android/media/AudioManager.java b/media/java/android/media/AudioManager.java
index 4fa3327..8462889 100644
--- a/media/java/android/media/AudioManager.java
+++ b/media/java/android/media/AudioManager.java
@@ -18,6 +18,7 @@
 
 import android.annotation.SdkConstant;
 import android.annotation.SdkConstant.SdkConstantType;
+import android.content.ComponentName;
 import android.content.Context;
 import android.database.ContentObserver;
 import android.os.Binder;
@@ -29,6 +30,7 @@
 import android.os.ServiceManager;
 import android.provider.Settings;
 import android.util.Log;
+import android.view.KeyEvent;
 
 import java.util.Iterator;
 import java.util.HashMap;
@@ -1179,7 +1181,7 @@
      * Map to convert focus event listener IDs, as used in the AudioService audio focus stack,
      * to actual listener objects.
      */
-    private HashMap<String, OnAudioFocusChangeListener> mFocusIdListenerMap =
+    private HashMap<String, OnAudioFocusChangeListener> mAudioFocusIdListenerMap =
             new HashMap<String, OnAudioFocusChangeListener>();
     /**
      * Lock to prevent concurrent changes to the list of focus listeners for this AudioManager
@@ -1188,13 +1190,14 @@
     private final Object mFocusListenerLock = new Object();
 
     private OnAudioFocusChangeListener findFocusListener(String id) {
-        return mFocusIdListenerMap.get(id);
+        return mAudioFocusIdListenerMap.get(id);
     }
 
     /**
      * Handler for audio focus events coming from the audio service.
      */
-    private FocusEventHandlerDelegate mFocusEventHandlerDelegate = new FocusEventHandlerDelegate();
+    private FocusEventHandlerDelegate mAudioFocusEventHandlerDelegate =
+            new FocusEventHandlerDelegate();
     /**
      * Event id denotes a loss of focus
      */
@@ -1239,16 +1242,16 @@
         }
     }
 
-    private IAudioFocusDispatcher mFocusDispatcher = new IAudioFocusDispatcher.Stub() {
+    private IAudioFocusDispatcher mAudioFocusDispatcher = new IAudioFocusDispatcher.Stub() {
 
         public void dispatchAudioFocusChange(int focusChange, String id) {
-            Message m = mFocusEventHandlerDelegate.getHandler().obtainMessage(focusChange, id);
-            mFocusEventHandlerDelegate.getHandler().sendMessage(m);
+            Message m = mAudioFocusEventHandlerDelegate.getHandler().obtainMessage(focusChange, id);
+            mAudioFocusEventHandlerDelegate.getHandler().sendMessage(m);
         }
 
     };
 
-    private String getIdForFocusListener(OnAudioFocusChangeListener l) {
+    private String getIdForAudioFocusListener(OnAudioFocusChangeListener l) {
         if (l == null) {
             return new String();
         } else {
@@ -1264,10 +1267,10 @@
             return;
         }
         synchronized(mFocusListenerLock) {
-            if (mFocusIdListenerMap.containsKey(getIdForFocusListener(l))) {
+            if (mAudioFocusIdListenerMap.containsKey(getIdForAudioFocusListener(l))) {
                 return;
             }
-            mFocusIdListenerMap.put(getIdForFocusListener(l), l);
+            mAudioFocusIdListenerMap.put(getIdForAudioFocusListener(l), l);
         }
     }
 
@@ -1278,13 +1281,13 @@
         // notify service to remove it from audio focus stack
         IAudioService service = getService();
         try {
-            service.unregisterFocusClient(getIdForFocusListener(l));
+            service.unregisterAudioFocusClient(getIdForAudioFocusListener(l));
         } catch (RemoteException e) {
             Log.e(TAG, "Can't call unregisterFocusClient() from AudioService due to "+e);
         }
         // remove locally
         synchronized(mFocusListenerLock) {
-            mFocusIdListenerMap.remove(getIdForFocusListener(l));
+            mAudioFocusIdListenerMap.remove(getIdForAudioFocusListener(l));
         }
     }
 
@@ -1318,7 +1321,7 @@
         IAudioService service = getService();
         try {
             status = service.requestAudioFocus(streamType, durationHint, mICallBack,
-                    mFocusDispatcher, getIdForFocusListener(l));
+                    mAudioFocusDispatcher, getIdForAudioFocusListener(l));
         } catch (RemoteException e) {
             Log.e(TAG, "Can't call requestAudioFocus() from AudioService due to "+e);
         }
@@ -1336,7 +1339,8 @@
         registerAudioFocusListener(l);
         IAudioService service = getService();
         try {
-            status = service.abandonAudioFocus(mFocusDispatcher, getIdForFocusListener(l));
+            status = service.abandonAudioFocus(mAudioFocusDispatcher,
+                    getIdForAudioFocusListener(l));
         } catch (RemoteException e) {
             Log.e(TAG, "Can't call abandonAudioFocus() from AudioService due to "+e);
         }
@@ -1344,6 +1348,63 @@
     }
 
 
+    //====================================================================
+    // Remote Control
+    /**
+     * @hide
+     * TODO unhide for SDK
+     * TODO document for SDK
+     * @param eventReceiver identifier of a {@link android.content.BroadcastReceiver}
+     *      that will receive the media button intent. This broadcast receiver must be declared
+     *      in the application manifest.
+     */
+    public void registerMediaButtonEventReceiver(ComponentName eventReceiver) {
+        //TODO enforce the rule about the receiver being declared in the manifest
+        IAudioService service = getService();
+        try {
+            service.registerMediaButtonEventReceiver(eventReceiver);
+        } catch (RemoteException e) {
+            Log.e(TAG, "Dead object in registerMediaButtonEventReceiver"+e);
+        }
+    }
+
+    /**
+     * @hide
+     * TODO unhide for SDK
+     * TODO document for SDK
+     * @param eventReceiverClass class of a {@link android.content.BroadcastReceiver} that will
+     *     receive the media button intent. This broadcast receiver must be declared in the
+     *     application manifest.
+     */
+    public void registerMediaButtonEventReceiver(Class<?> eventReceiverClass) {
+        registerMediaButtonEventReceiver(new ComponentName(
+                eventReceiverClass.getPackage().getName(), eventReceiverClass.getName()));
+    }
+
+    /**
+     * @hide
+     * TODO unhide for SDK
+     * TODO document for SDK
+     */
+    public void unregisterMediaButtonEventReceiver(ComponentName eventReceiver) {
+        IAudioService service = getService();
+        try {
+            service.unregisterMediaButtonEventReceiver(eventReceiver);
+        } catch (RemoteException e) {
+            Log.e(TAG, "Dead object in unregisterMediaButtonEventReceiver"+e);
+        }
+    }
+
+    /**
+     * @hide
+     * TODO unhide for SDK
+     * TODO document for SDK
+     */
+    public void unregisterMediaButtonEventReceiver(Class<?> eventReceiverClass) {
+        unregisterMediaButtonEventReceiver(new ComponentName(
+                eventReceiverClass.getPackage().getName(), eventReceiverClass.getName()));
+    }
+
     /**
      *  @hide
      *  Reload audio settings. This method is called by Settings backup
diff --git a/media/java/android/media/AudioService.java b/media/java/android/media/AudioService.java
index 618f239..81e17b7 100644
--- a/media/java/android/media/AudioService.java
+++ b/media/java/android/media/AudioService.java
@@ -18,6 +18,7 @@
 
 import android.app.ActivityManagerNative;
 import android.content.BroadcastReceiver;
+import android.content.ComponentName;
 import android.content.ContentResolver;
 import android.content.Context;
 import android.content.Intent;
@@ -41,6 +42,7 @@
 import android.provider.Settings;
 import android.provider.Settings.System;
 import android.util.Log;
+import android.view.KeyEvent;
 import android.view.VolumePanel;
 import android.os.SystemProperties;
 
@@ -224,6 +226,10 @@
     // Broadcast receiver for device connections intent broadcasts
     private final BroadcastReceiver mReceiver = new AudioServiceBroadcastReceiver();
 
+    //  Broadcast receiver for media button broadcasts (separate from mReceiver to
+    //  independently change its priority)
+    private final BroadcastReceiver mMediaButtonReceiver = new MediaButtonBroadcastReceiver();
+
     // Devices currently connected
     private HashMap <Integer, String> mConnectedDevices = new HashMap <Integer, String>();
 
@@ -269,6 +275,10 @@
         intentFilter.addAction(Intent.ACTION_DOCK_EVENT);
         context.registerReceiver(mReceiver, intentFilter);
 
+        // Register for media button intent broadcasts.
+        intentFilter = new IntentFilter(Intent.ACTION_MEDIA_BUTTON);
+        intentFilter.setPriority(IntentFilter.SYSTEM_HIGH_PRIORITY);
+        context.registerReceiver(mMediaButtonReceiver, intentFilter);
     }
 
     private void createAudioSystemThread() {
@@ -1667,7 +1677,7 @@
             if (signal) {
                 // notify the new top of the stack it gained focus
                 if (!mFocusStack.empty() && (mFocusStack.peek().mFocusDispatcher != null)
-                        && canReassignFocus()) {
+                        && canReassignAudioFocus()) {
                     try {
                         mFocusStack.peek().mFocusDispatcher.dispatchAudioFocusChange(
                                 AudioManager.AUDIOFOCUS_GAIN, mFocusStack.peek().mClientId);
@@ -1714,7 +1724,7 @@
      * Helper function:
      * Returns true if the system is in a state where the focus can be reevaluated, false otherwise.
      */
-    private boolean canReassignFocus() {
+    private boolean canReassignAudioFocus() {
         // focus requests are rejected during a phone call
         if (getMode() == AudioSystem.MODE_IN_CALL) {
             Log.i(TAG, " AudioFocus  can't be reassigned during a call, exiting");
@@ -1747,7 +1757,7 @@
     }
 
 
-    /** @see AudioManager#requestAudioFocus(int, int, IBinder, IAudioFocusDispatcher, String) */
+    /** @see AudioManager#requestAudioFocus(IAudioFocusDispatcher, int, int) */
     public int requestAudioFocus(int mainStreamType, int durationHint, IBinder cb,
             IAudioFocusDispatcher fd, String clientId) {
         Log.i(TAG, " AudioFocus  requestAudioFocus() from " + clientId);
@@ -1759,7 +1769,7 @@
             return AudioManager.AUDIOFOCUS_REQUEST_FAILED;
         }
 
-        if (!canReassignFocus()) {
+        if (!canReassignAudioFocus()) {
             return AudioManager.AUDIOFOCUS_REQUEST_FAILED;
         }
 
@@ -1803,7 +1813,7 @@
         return AudioManager.AUDIOFOCUS_REQUEST_GRANTED;
     }
 
-    /** @see AudioManager#abandonAudioFocus(IBinder, IAudioFocusDispatcher, String) */
+    /** @see AudioManager#abandonAudioFocus(IAudioFocusDispatcher) */
     public int abandonAudioFocus(IAudioFocusDispatcher fl, String clientId) {
         Log.i(TAG, " AudioFocus  abandonAudioFocus() from " + clientId);
 
@@ -1814,15 +1824,141 @@
     }
 
 
-    public void unregisterFocusClient(String clientId) {
+    public void unregisterAudioFocusClient(String clientId) {
         removeFocusStackEntry(clientId, false);
     }
 
 
+    //==========================================================================================
+    // RemoteControl
+    //==========================================================================================
+    /**
+     * Receiver for media button intents. Handles the dispatching of the media button event
+     * to one of the registered listeners, or if there was none, resumes the intent broadcast
+     * to the rest of the system.
+     */
+    private class MediaButtonBroadcastReceiver extends BroadcastReceiver {
+        @Override
+        public void onReceive(Context context, Intent intent) {
+            String action = intent.getAction();
+            if (!Intent.ACTION_MEDIA_BUTTON.equals(action)) {
+                return;
+            }
+            KeyEvent event = (KeyEvent) intent.getParcelableExtra(Intent.EXTRA_KEY_EVENT);
+            if (event != null) {
+                // if in a call or ringing, do not break the current phone app behavior
+                // TODO modify this to let the phone app specifically get the RC focus
+                //      add modify the phone app to take advantage of the new API
+                if ((getMode() == AudioSystem.MODE_IN_CALL) ||
+                        (getMode() == AudioSystem.MODE_RINGTONE)) {
+                    return;
+                }
+                synchronized(mRCStack) {
+                    if (!mRCStack.empty()) {
+                        // create a new intent specifically aimed at the current registered listener
+                        Intent targetedIntent = new Intent(Intent.ACTION_MEDIA_BUTTON);
+                        targetedIntent.putExtras(intent.getExtras());
+                        targetedIntent.setComponent(mRCStack.peek().mReceiverComponent);
+                        // trap the current broadcast
+                        abortBroadcast();
+                        //Log.v(TAG, " Sending intent" + targetedIntent);
+                        context.sendBroadcast(targetedIntent, null);
+                    }
+                }
+            }
+        }
+    }
+
+    private static class RemoteControlStackEntry {
+        public ComponentName mReceiverComponent;// always non null
+        // TODO implement registration expiration?
+        //public int mRegistrationTime;
+
+        public RemoteControlStackEntry() {
+        }
+
+        public RemoteControlStackEntry(ComponentName r) {
+            mReceiverComponent = r;
+        }
+    }
+
+    private Stack<RemoteControlStackEntry> mRCStack = new Stack<RemoteControlStackEntry>();
+
+    /**
+     * Helper function:
+     * Display in the log the current entries in the remote control focus stack
+     */
+    private void dumpRCStack(PrintWriter pw) {
+        pw.println("Remote Control stack entries:");
+        synchronized(mRCStack) {
+            Iterator<RemoteControlStackEntry> stackIterator = mRCStack.iterator();
+            while(stackIterator.hasNext()) {
+                RemoteControlStackEntry fse = stackIterator.next();
+                pw.println("     receiver:" + fse.mReceiverComponent);
+            }
+        }
+    }
+
+    /**
+     * Helper function:
+     * Set the new remote control receiver at the top of the RC focus stack
+     */
+    private void pushMediaButtonReceiver(ComponentName newReceiver) {
+        // already at top of stack?
+        if (!mRCStack.empty() && mRCStack.peek().mReceiverComponent.equals(newReceiver)) {
+            return;
+        }
+        Iterator<RemoteControlStackEntry> stackIterator = mRCStack.iterator();
+        while(stackIterator.hasNext()) {
+            RemoteControlStackEntry rcse = (RemoteControlStackEntry)stackIterator.next();
+            if(rcse.mReceiverComponent.equals(newReceiver)) {
+                mRCStack.remove(rcse);
+                break;
+            }
+        }
+        mRCStack.push(new RemoteControlStackEntry(newReceiver));
+    }
+
+    /**
+     * Helper function:
+     * Remove the remote control receiver from the RC focus stack
+     */
+    private void removeMediaButtonReceiver(ComponentName newReceiver) {
+        Iterator<RemoteControlStackEntry> stackIterator = mRCStack.iterator();
+        while(stackIterator.hasNext()) {
+            RemoteControlStackEntry rcse = (RemoteControlStackEntry)stackIterator.next();
+            if(rcse.mReceiverComponent.equals(newReceiver)) {
+                mRCStack.remove(rcse);
+                break;
+            }
+        }
+    }
+
+
+    /** see AudioManager.registerMediaButtonEventReceiver(ComponentName eventReceiver) */
+    public void registerMediaButtonEventReceiver(ComponentName eventReceiver) {
+        Log.i(TAG, "  Remote Control   registerMediaButtonEventReceiver() for " + eventReceiver);
+
+        synchronized(mRCStack) {
+            pushMediaButtonReceiver(eventReceiver);
+        }
+    }
+
+    /** see AudioManager.unregisterMediaButtonEventReceiver(ComponentName eventReceiver) */
+    public void unregisterMediaButtonEventReceiver(ComponentName eventReceiver) {
+        Log.i(TAG, "  Remote Control   registerMediaButtonEventReceiver() for " + eventReceiver);
+
+        synchronized(mRCStack) {
+            removeMediaButtonReceiver(eventReceiver);
+        }
+    }
+
+
     @Override
     protected void dump(FileDescriptor fd, PrintWriter pw, String[] args) {
-        // TODO probably a lot more to do here than just the audio focus stack
+        // TODO probably a lot more to do here than just the audio focus and remote control stacks
         dumpFocusStack(pw);
+        dumpRCStack(pw);
     }
 
 
diff --git a/media/java/android/media/IAudioService.aidl b/media/java/android/media/IAudioService.aidl
index b275488..953892b6 100644
--- a/media/java/android/media/IAudioService.aidl
+++ b/media/java/android/media/IAudioService.aidl
@@ -16,6 +16,7 @@
 
 package android.media;
 
+import android.content.ComponentName;
 import android.media.IAudioFocusDispatcher;
 
 /**
@@ -76,5 +77,9 @@
 
     int abandonAudioFocus(IAudioFocusDispatcher l, String clientId);
     
-    void unregisterFocusClient(String clientId);
+    void unregisterAudioFocusClient(String clientId);
+
+    void registerMediaButtonEventReceiver(in ComponentName eventReceiver);
+
+    void unregisterMediaButtonEventReceiver(in ComponentName eventReceiver);
 }