Donate to e Foundation | Murena handsets with /e/OS | Own a part of Murena! Learn more

Commit d5176cfe authored by Jean-Michel Trivi's avatar Jean-Michel Trivi
Browse files

First implementation of the audio focus management as an extension

of AudioManager and AudioService.
parent 07863ab6
Loading
Loading
Loading
Loading
+1 −0
Original line number Diff line number Diff line
@@ -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 \
+240 −0
Original line number Diff line number Diff line
@@ -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
+224 −1
Original line number Diff line number Diff line
@@ -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);
    }


}
+28 −0
Original line number Diff line number Diff line
/*
 * 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);

}
+9 −0
Original line number Diff line number Diff line
@@ -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);
}