First implementation of the audio focus management as an extension

of AudioManager and AudioService.
This commit is contained in:
Jean-Michel Trivi
2010-01-28 11:56:42 -08:00
parent 07863ab6bc
commit d5176cfe6e
5 changed files with 502 additions and 1 deletions

View File

@@ -167,6 +167,7 @@ LOCAL_SRC_FILES += \
location/java/android/location/ILocationProvider.aidl \
location/java/android/location/INetInitiatedListener.aidl \
media/java/android/media/IAudioService.aidl \
media/java/android/media/IAudioFocusDispatcher.aidl \
media/java/android/media/IMediaScannerListener.aidl \
media/java/android/media/IMediaScannerService.aidl \
telephony/java/com/android/internal/telephony/IPhoneStateListener.aidl \

View File

@@ -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>
@@ -1126,6 +1131,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

View File

@@ -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);
}
}

View File

@@ -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);
}

View File

@@ -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);
}