diff options
author | Jean-Michel Trivi <jmtrivi@google.com> | 2010-01-28 11:56:42 -0800 |
---|---|---|
committer | Jean-Michel Trivi <jmtrivi@google.com> | 2010-03-02 08:39:26 -0800 |
commit | d5176cfe6eae954e9cef1e2ec17859a5089e1330 (patch) | |
tree | f1266d2fc2eef8d9114e3c7af403a65c43da8b24 /media/java | |
parent | 07863ab6bc6f5ecf9a8454357667af6862588fc2 (diff) | |
download | frameworks_base-d5176cfe6eae954e9cef1e2ec17859a5089e1330.zip frameworks_base-d5176cfe6eae954e9cef1e2ec17859a5089e1330.tar.gz frameworks_base-d5176cfe6eae954e9cef1e2ec17859a5089e1330.tar.bz2 |
First implementation of the audio focus management as an extension
of AudioManager and AudioService.
Diffstat (limited to 'media/java')
-rw-r--r-- | media/java/android/media/AudioManager.java | 240 | ||||
-rw-r--r-- | media/java/android/media/AudioService.java | 225 | ||||
-rwxr-xr-x | media/java/android/media/IAudioFocusDispatcher.aidl | 28 | ||||
-rw-r--r-- | media/java/android/media/IAudioService.aidl | 9 |
4 files changed, 501 insertions, 1 deletions
diff --git a/media/java/android/media/AudioManager.java b/media/java/android/media/AudioManager.java index 32c5c23..b0a179f 100644 --- a/media/java/android/media/AudioManager.java +++ b/media/java/android/media/AudioManager.java @@ -23,11 +23,16 @@ import android.database.ContentObserver; import android.os.Binder; import android.os.Handler; import android.os.IBinder; +import android.os.Looper; +import android.os.Message; import android.os.RemoteException; import android.os.ServiceManager; import android.provider.Settings; import android.util.Log; +import java.util.Iterator; +import java.util.HashMap; + /** * AudioManager provides access to volume and ringer mode control. * <p> @@ -1127,6 +1132,241 @@ public class AudioManager { } /** + * TODO unhide for SDK + * Used to indicate a loss of audio focus of unknown duration. + * @see OnAudioFocusChangeListener#onAudioFocusChanged(int) + * {@hide} + */ + public static final int AUDIOFOCUS_LOSS = -1; + /** + * TODO unhide for SDK + * Used to indicate a transient loss of audio focus. + * @see OnAudioFocusChangeListener#onAudioFocusChanged(int) + * {@hide} + */ + public static final int AUDIOFOCUS_LOSS_TRANSIENT = -2; + /** + * TODO unhide for SDK + * Used to indicate a gain of audio focus, or a request of audio focus, of unknown duration. + * @see OnAudioFocusChangeListener#onAudioFocusChanged(int) + * @see #requestAudioFocus(OnAudioFocusChangeListener, int, int) + * {@hide} + */ + public static final int AUDIOFOCUS_GAIN = 1; + /** + * TODO unhide for SDK + * Used to indicate a temporary gain or request of audio focus, anticipated to last a short + * amount of time. Examples of temporary changes are the playback of driving directions, or an + * event notification. + * @see OnAudioFocusChangeListener#onAudioFocusChanged(int) + * @see #requestAudioFocus(OnAudioFocusChangeListener, int, int) + * {@hide} + */ + public static final int AUDIOFOCUS_GAIN_TRANSIENT = 2; + + /** + * TODO unhide for SDK + * {@hide} + * Interface definition for a callback to be invoked when the audio focus of the system is + * updated. + */ + public interface OnAudioFocusChangeListener { + /** + * Called on the listener to notify it the audio focus for this listener has been changed. + * The focusChange value indicates whether the focus was gained, + * whether the focus was lost, and whether that loss is transient, or whether the new focus + * holder will hold it for an unknown amount of time. + * When losing focus, listeners can use the duration hint to decide what + * behavior to adopt when losing focus. A music player could for instance elect to duck its + * music stream for transient focus losses, and pause otherwise. + * @param focusChange one of {@link AudioManager#AUDIOFOCUS_GAIN}, + * {@link AudioManager#AUDIOFOCUS_LOSS}, {@link AudioManager#AUDIOFOCUS_LOSS_TRANSIENT}. + */ + public void onAudioFocusChanged(int focusChange); + } + + /** + * Map to convert focus event listener IDs, as used in the AudioService audio focus stack, + * to actual listener objects. + */ + private HashMap<String, OnAudioFocusChangeListener> mFocusIdListenerMap = + new HashMap<String, OnAudioFocusChangeListener>(); + /** + * Lock to prevent concurrent changes to the list of focus listeners for this AudioManager + * instance. + */ + private final Object mFocusListenerLock = new Object(); + + private OnAudioFocusChangeListener findFocusListener(String id) { + return mFocusIdListenerMap.get(id); + } + + /** + * Handler for audio focus events coming from the audio service. + */ + private FocusEventHandlerDelegate mFocusEventHandlerDelegate = new FocusEventHandlerDelegate(); + /** + * Event id denotes a loss of focus + */ + private static final int AUDIOFOCUS_EVENT_LOSS = 0; + /** + * Event id denotes a gain of focus + */ + private static final int AUDIOFOCUS_EVENT_GAIN = 1; + /** + * Helper class to handle the forwarding of audio focus events to the appropriate listener + */ + private class FocusEventHandlerDelegate { + private final Handler mHandler; + + FocusEventHandlerDelegate() { + Looper looper; + if ((looper = Looper.myLooper()) == null) { + looper = Looper.getMainLooper(); + } + + if (looper != null) { + // implement the event handler delegate to receive audio focus events + mHandler = new Handler(looper) { + @Override + public void handleMessage(Message msg) { + OnAudioFocusChangeListener listener = null; + synchronized(mFocusListenerLock) { + listener = findFocusListener((String)msg.obj); + } + if (listener != null) { + listener.onAudioFocusChanged(msg.what); + } + } + }; + } else { + mHandler = null; + } + } + + Handler getHandler() { + return mHandler; + } + } + + private IAudioFocusDispatcher mFocusDispatcher = new IAudioFocusDispatcher.Stub() { + + public void dispatchAudioFocusChange(int focusChange, String id) { + Message m = mFocusEventHandlerDelegate.getHandler().obtainMessage(focusChange, id); + mFocusEventHandlerDelegate.getHandler().sendMessage(m); + } + + }; + + private String getIdForFocusListener(OnAudioFocusChangeListener l) { + if (l == null) { + return new String(); + } else { + return new String(this.toString() + l.toString()); + } + } + + /** + * TODO unhide for SDK + * {@hide} + * Register a listener for audio focus updates. + */ + public void registerAudioFocusListener(OnAudioFocusChangeListener l) { + if (l == null) { + return; + } + synchronized(mFocusListenerLock) { + if (mFocusIdListenerMap.containsKey(getIdForFocusListener(l))) { + return; + } + mFocusIdListenerMap.put(getIdForFocusListener(l), l); + } + } + + /** + * TODO unhide for SDK + * TODO document for SDK + * {@hide} + */ + public void unregisterAudioFocusListener(OnAudioFocusChangeListener l) { + // notify service to remove it from audio focus stack + IAudioService service = getService(); + try { + service.unregisterFocusClient(getIdForFocusListener(l)); + } catch (RemoteException e) { + Log.e(TAG, "Can't call unregisterFocusClient() from AudioService due to "+e); + } + // remove locally + synchronized(mFocusListenerLock) { + mFocusIdListenerMap.remove(getIdForFocusListener(l)); + } + } + + + /** + * TODO unhide for SDK + * TODO document for SDK + * {@hide} + */ + public static final int AUDIOFOCUS_REQUEST_FAILED = 0; + /** + * TODO unhide for SDK + * TODO document for SDK + * {@hide} + */ + public static final int AUDIOFOCUS_REQUEST_GRANTED = 1; + + + /** + * TODO unhide for SDK + * {@hide} + * Request audio focus. + * Send a request to obtain the audio focus for a specific stream type + * @param l the listener to be notified of audio focus changes + * @param streamType the main audio stream type affected by the focus request + * @param durationHint use {@link #AUDIOFOCUS_GAIN_TRANSIENT} to indicate this focus request + * is temporary, and focus will be abandonned shortly. Examples of transient requests are + * for the playback of driving directions, or notifications sounds. Use + * {@link #AUDIOFOCUS_GAIN} for a focus request of unknown duration such + * as the playback of a song or a video. + * @return {@link #AUDIOFOCUS_REQUEST_FAILED} or {@link #AUDIOFOCUS_REQUEST_GRANTED} + */ + public int requestAudioFocus(OnAudioFocusChangeListener l, int streamType, int durationHint) { + int status = AUDIOFOCUS_REQUEST_FAILED; + registerAudioFocusListener(l); + //TODO protect request by permission check? + IAudioService service = getService(); + try { + status = service.requestAudioFocus(streamType, durationHint, mICallBack, + mFocusDispatcher, getIdForFocusListener(l)); + } catch (RemoteException e) { + Log.e(TAG, "Can't call requestAudioFocus() from AudioService due to "+e); + } + return status; + } + + + /** + * TODO unhide for SDK + * TODO document for SDK + * {@hide} + * Abandon audio focus. + * @return {@link #AUDIOFOCUS_REQUEST_FAILED} or {@link #AUDIOFOCUS_REQUEST_GRANTED} + */ + public int abandonAudioFocus(OnAudioFocusChangeListener l) { + int status = AUDIOFOCUS_REQUEST_FAILED; + registerAudioFocusListener(l); + IAudioService service = getService(); + try { + status = service.abandonAudioFocus(mFocusDispatcher, getIdForFocusListener(l)); + } catch (RemoteException e) { + Log.e(TAG, "Can't call abandonAudioFocus() from AudioService due to "+e); + } + return status; + } + + + /** * @hide * Reload audio settings. This method is called by Settings backup * agent when audio settings are restored and causes the AudioService diff --git a/media/java/android/media/AudioService.java b/media/java/android/media/AudioService.java index 947307d..a36ee85 100644 --- a/media/java/android/media/AudioService.java +++ b/media/java/android/media/AudioService.java @@ -26,7 +26,6 @@ import android.bluetooth.BluetoothA2dp; import android.bluetooth.BluetoothClass; import android.bluetooth.BluetoothDevice; import android.bluetooth.BluetoothHeadset; - import android.content.pm.PackageManager; import android.database.ContentObserver; import android.media.MediaPlayer.OnCompletionListener; @@ -47,12 +46,15 @@ import android.os.SystemProperties; import com.android.internal.telephony.ITelephony; +import java.io.FileDescriptor; import java.io.IOException; +import java.io.PrintWriter; import java.util.ArrayList; import java.util.HashMap; import java.util.Iterator; import java.util.Map; import java.util.Set; +import java.util.Stack; /** * The implementation of the volume manager service. @@ -76,6 +78,7 @@ public class AudioService extends IAudioService.Stub { private Context mContext; private ContentResolver mContentResolver; + /** The UI */ private VolumePanel mVolumePanel; @@ -888,6 +891,7 @@ public class AudioService extends IAudioService.Stub { } } + /////////////////////////////////////////////////////////////////////////// // Internal methods /////////////////////////////////////////////////////////////////////////// @@ -1600,4 +1604,223 @@ public class AudioService extends IAudioService.Stub { } } } + + //========================================================================================== + // AudioFocus + //========================================================================================== + private static class FocusStackEntry { + public int mStreamType = -1;// no stream type + public boolean mIsTransportControlReceiver = false; + public IAudioFocusDispatcher mFocusDispatcher = null; + public IBinder mSourceRef = null; + public String mClientId; + public int mDurationHint; + + public FocusStackEntry() { + } + + public FocusStackEntry(int streamType, int duration, boolean isTransportControlReceiver, + IAudioFocusDispatcher afl, IBinder source, String id) { + mStreamType = streamType; + mIsTransportControlReceiver = isTransportControlReceiver; + mFocusDispatcher = afl; + mSourceRef = source; + mClientId = id; + mDurationHint = duration; + } + } + + private Stack<FocusStackEntry> mFocusStack = new Stack<FocusStackEntry>(); + + /** + * Helper function: + * Display in the log the current entries in the audio focus stack + */ + private void dumpFocusStack(PrintWriter pw) { + pw.println("Audio Focus stack entries:"); + synchronized(mFocusStack) { + Iterator<FocusStackEntry> stackIterator = mFocusStack.iterator(); + while(stackIterator.hasNext()) { + FocusStackEntry fse = stackIterator.next(); + pw.println(" source:" + fse.mSourceRef + " -- client: " + fse.mClientId + + " -- duration: " +fse.mDurationHint); + } + } + } + + /** + * Helper function: + * Remove a focus listener from the focus stack. + * @param focusListenerToRemove the focus listener + * @param signal if true and the listener was at the top of the focus stack, i.e. it was holding + * focus, notify the next item in the stack it gained focus. + */ + private void removeFocusStackEntry(String clientToRemove, boolean signal) { + // is the current top of the focus stack abandoning focus? (because of death or request) + if (!mFocusStack.empty() && mFocusStack.peek().mClientId.equals(clientToRemove)) + { + //Log.i(TAG, " removeFocusStackEntry() removing top of stack"); + mFocusStack.pop(); + if (signal) { + // notify the new top of the stack it gained focus + if (!mFocusStack.empty() && (mFocusStack.peek().mFocusDispatcher != null) + && canReassignFocus()) { + try { + mFocusStack.peek().mFocusDispatcher.dispatchAudioFocusChange( + AudioManager.AUDIOFOCUS_GAIN, mFocusStack.peek().mClientId); + } catch (RemoteException e) { + Log.e(TAG, " Failure to signal gain of focus due to "+ e); + e.printStackTrace(); + } + } + } + } else { + // focus is abandoned by a client that's not at the top of the stack, + // no need to update focus. + Iterator<FocusStackEntry> stackIterator = mFocusStack.iterator(); + while(stackIterator.hasNext()) { + FocusStackEntry fse = (FocusStackEntry)stackIterator.next(); + if(fse.mClientId.equals(clientToRemove)) { + Log.i(TAG, " AudioFocus abandonAudioFocus(): removing entry for " + + fse.mClientId); + mFocusStack.remove(fse); + } + } + } + } + + /** + * Helper function: + * Remove focus listeners from the focus stack for a particular client. + */ + private void removeFocusStackEntryForClient(IBinder cb) { + // focus is abandoned by a client that's not at the top of the stack, + // no need to update focus. + Iterator<FocusStackEntry> stackIterator = mFocusStack.iterator(); + while(stackIterator.hasNext()) { + FocusStackEntry fse = (FocusStackEntry)stackIterator.next(); + if(fse.mSourceRef.equals(cb)) { + Log.i(TAG, " AudioFocus abandonAudioFocus(): removing entry for " + + fse.mClientId); + mFocusStack.remove(fse); + } + } + } + + /** + * Helper function: + * Returns true if the system is in a state where the focus can be reevaluated, false otherwise. + */ + private boolean canReassignFocus() { + // 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"); + return false; + } + return true; + } + + /** + * Inner class to monitor audio focus client deaths, and remove them from the audio focus + * stack if necessary. + */ + private class AudioFocusDeathHandler implements IBinder.DeathRecipient { + private IBinder mCb; // To be notified of client's death + + AudioFocusDeathHandler(IBinder cb) { + mCb = cb; + } + + public void binderDied() { + synchronized(mFocusStack) { + Log.w(TAG, " AudioFocus audio focus client died"); + removeFocusStackEntryForClient(mCb); + } + } + + public IBinder getBinder() { + return mCb; + } + } + + + /** @see AudioManager#requestAudioFocus(int, int, IBinder, IAudioFocusDispatcher, String) */ + public int requestAudioFocus(int mainStreamType, int durationHint, IBinder cb, + IAudioFocusDispatcher fd, String clientId) { + Log.i(TAG, " AudioFocus requestAudioFocus() from " + clientId); + // the main stream type for the audio focus request is currently not used. It may + // potentially be used to handle multiple stream type-dependent audio focuses. + + if ((cb == null) || !cb.pingBinder()) { + Log.i(TAG, " AudioFocus DOA client for requestAudioFocus(), exiting"); + return AudioManager.AUDIOFOCUS_REQUEST_FAILED; + } + + if (!canReassignFocus()) { + return AudioManager.AUDIOFOCUS_REQUEST_FAILED; + } + + synchronized(mFocusStack) { + if (!mFocusStack.empty() && mFocusStack.peek().mClientId.equals(clientId)) { + mFocusStack.peek().mDurationHint = durationHint; + // if focus is already owned by this client, don't do anything + return AudioManager.AUDIOFOCUS_REQUEST_GRANTED; + } + + // notify current top of stack it is losing focus + if (!mFocusStack.empty() && (mFocusStack.peek().mFocusDispatcher != null)) { + try { + mFocusStack.peek().mFocusDispatcher.dispatchAudioFocusChange( + (durationHint == AudioManager.AUDIOFOCUS_GAIN) ? + AudioManager.AUDIOFOCUS_LOSS : + AudioManager.AUDIOFOCUS_LOSS_TRANSIENT, + mFocusStack.peek().mClientId); + } catch (RemoteException e) { + Log.e(TAG, " Failure to signal loss of focus due to "+ e); + e.printStackTrace(); + } + } + + // push focus requester at the top of the audio focus stack + mFocusStack.push(new FocusStackEntry(mainStreamType, durationHint, false, fd, cb, + clientId)); + }//synchronized(mFocusStack) + + // handle the potential premature death of the new holder of the focus + // (premature death == death before abandoning focus) + // Register for client death notification + AudioFocusDeathHandler afdh = new AudioFocusDeathHandler(cb); + try { + cb.linkToDeath(afdh, 0); + } catch (RemoteException e) { + // client has already died! + Log.w(TAG, " AudioFocus requestAudioFocus() could not link to "+cb+" binder death"); + } + + return AudioManager.AUDIOFOCUS_REQUEST_GRANTED; + } + + /** @see AudioManager#abandonAudioFocus(IBinder, IAudioFocusDispatcher, String) */ + public int abandonAudioFocus(IAudioFocusDispatcher fl, String clientId) { + Log.i(TAG, " AudioFocus abandonAudioFocus() from " + clientId); + + // this will take care of notifying the new focus owner if needed + removeFocusStackEntry(clientId, true); + + return AudioManager.AUDIOFOCUS_REQUEST_GRANTED; + } + + + public void unregisterFocusClient(String clientId) { + removeFocusStackEntry(clientId, false); + } + + + @Override + protected void dump(FileDescriptor fd, PrintWriter pw, String[] args) { + // TODO probably a lot more to do here than just the audio focus stack + dumpFocusStack(pw); + } + + } diff --git a/media/java/android/media/IAudioFocusDispatcher.aidl b/media/java/android/media/IAudioFocusDispatcher.aidl new file mode 100755 index 0000000..09575f7 --- /dev/null +++ b/media/java/android/media/IAudioFocusDispatcher.aidl @@ -0,0 +1,28 @@ +/* + * Copyright (C) 2010 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; + +/** + * AIDL for the AudioService to signal audio focus listeners of focus updates. + * + * {@hide} + */ +oneway interface IAudioFocusDispatcher { + + void dispatchAudioFocusChange(int focusChange, String clientId); + +} diff --git a/media/java/android/media/IAudioService.aidl b/media/java/android/media/IAudioService.aidl index 83581d2..b275488 100644 --- a/media/java/android/media/IAudioService.aidl +++ b/media/java/android/media/IAudioService.aidl @@ -16,6 +16,8 @@ package android.media; +import android.media.IAudioFocusDispatcher; + /** * {@hide} */ @@ -68,4 +70,11 @@ interface IAudioService { void setBluetoothScoOn(boolean on); boolean isBluetoothScoOn(); + + int requestAudioFocus(int mainStreamType, int durationHint, IBinder cb, IAudioFocusDispatcher l, + String clientId); + + int abandonAudioFocus(IAudioFocusDispatcher l, String clientId); + + void unregisterFocusClient(String clientId); } |