From d327f21626217aa3c9c0cdb7a84a742c531e59a3 Mon Sep 17 00:00:00 2001 From: Jean-Michel Trivi Date: Tue, 16 Mar 2010 21:44:33 -0700 Subject: [PATCH] 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 --- media/java/android/media/AudioManager.java | 87 ++++++++++-- media/java/android/media/AudioService.java | 150 +++++++++++++++++++- media/java/android/media/IAudioService.aidl | 7 +- 3 files changed, 223 insertions(+), 21 deletions(-) diff --git a/media/java/android/media/AudioManager.java b/media/java/android/media/AudioManager.java index 4fa33279a6912..8462889935349 100644 --- a/media/java/android/media/AudioManager.java +++ b/media/java/android/media/AudioManager.java @@ -18,6 +18,7 @@ package android.media; 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.RemoteException; 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 @@ public class AudioManager { * Map to convert focus event listener IDs, as used in the AudioService audio focus stack, * to actual listener objects. */ - private HashMap mFocusIdListenerMap = + private HashMap mAudioFocusIdListenerMap = new HashMap(); /** * Lock to prevent concurrent changes to the list of focus listeners for this AudioManager @@ -1188,13 +1190,14 @@ public class AudioManager { 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 @@ public class AudioManager { } } - 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 @@ public class AudioManager { 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 @@ public class AudioManager { // 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 @@ public class AudioManager { 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 @@ public class AudioManager { 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 @@ public class AudioManager { } + //==================================================================== + // 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 618f239653c3e..81e17b7f22255 100644 --- a/media/java/android/media/AudioService.java +++ b/media/java/android/media/AudioService.java @@ -18,6 +18,7 @@ package android.media; 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.os.ServiceManager; 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 @@ public class AudioService extends IAudioService.Stub { // 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 mConnectedDevices = new HashMap (); @@ -269,6 +275,10 @@ public class AudioService extends IAudioService.Stub { 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 @@ public class AudioService extends IAudioService.Stub { 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 @@ public class AudioService extends IAudioService.Stub { * 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 @@ public class AudioService extends IAudioService.Stub { } - /** @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 @@ public class AudioService extends IAudioService.Stub { return AudioManager.AUDIOFOCUS_REQUEST_FAILED; } - if (!canReassignFocus()) { + if (!canReassignAudioFocus()) { return AudioManager.AUDIOFOCUS_REQUEST_FAILED; } @@ -1803,7 +1813,7 @@ public class AudioService extends IAudioService.Stub { 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 class AudioService extends IAudioService.Stub { } - 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 mRCStack = new Stack(); + + /** + * 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 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 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 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 b275488ca5ac4..953892b644839 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 @@ interface IAudioService { int abandonAudioFocus(IAudioFocusDispatcher l, String clientId); - void unregisterFocusClient(String clientId); + void unregisterAudioFocusClient(String clientId); + + void registerMediaButtonEventReceiver(in ComponentName eventReceiver); + + void unregisterMediaButtonEventReceiver(in ComponentName eventReceiver); }