summaryrefslogtreecommitdiffstats
path: root/media/java
diff options
context:
space:
mode:
authorJean-Michel Trivi <jmtrivi@google.com>2010-01-28 11:56:42 -0800
committerJean-Michel Trivi <jmtrivi@google.com>2010-03-02 08:39:26 -0800
commitd5176cfe6eae954e9cef1e2ec17859a5089e1330 (patch)
treef1266d2fc2eef8d9114e3c7af403a65c43da8b24 /media/java
parent07863ab6bc6f5ecf9a8454357667af6862588fc2 (diff)
downloadframeworks_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.java240
-rw-r--r--media/java/android/media/AudioService.java225
-rwxr-xr-xmedia/java/android/media/IAudioFocusDispatcher.aidl28
-rw-r--r--media/java/android/media/IAudioService.aidl9
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);
}