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

Commit 0d3bd4b3 authored by Joseph Pirozzo's avatar Joseph Pirozzo
Browse files

A2DP sink audio focus

Resolved a bug where BT media fails to pause and subsequently resume during
a transient audio focus loss as is seen during voice recognition.
Resolved a bug where BT media may not resume after a phone call due to an
audio focus race condition.
Resolved a bug where BT phone notifications could interrupt and disrupt
an ongoing media session such as radio or local media player.

Bug: 34853256
Bug: 36529639
Bug: 37288772
Test: runtest bluetooth -c
com.android.bluetooth.a2dpsink.A2dpSinkStreamHandlerTest
Change-Id: I5261d24fd7bbe49bf61c48fdf2c9ae86934dcd3d

(cherry picked from commit e9d2261d)
parent 37571912
Loading
Loading
Loading
Loading
+9 −4
Original line number Diff line number Diff line
@@ -58,7 +58,7 @@ import java.util.List;
import java.util.HashMap;
import java.util.Set;

final class A2dpSinkStateMachine extends StateMachine {
public class A2dpSinkStateMachine extends StateMachine {
    private static final boolean DBG = false;

    static final int CONNECT = 1;
@@ -573,15 +573,20 @@ final class A2dpSinkStateMachine extends StateMachine {
                    break;

                case EVENT_AVRCP_CT_PLAY:
                    mStreaming.obtainMessage(A2dpSinkStreamHandler.SNK_PLAY).sendToTarget();
                    break;

                case EVENT_AVRCP_TG_PLAY:
                    mStreaming.obtainMessage(A2dpSinkStreamHandler.ACT_PLAY).sendToTarget();
                    mStreaming.obtainMessage(A2dpSinkStreamHandler.SRC_PLAY).sendToTarget();
                    break;

                case EVENT_AVRCP_CT_PAUSE:
                case EVENT_AVRCP_TG_PAUSE:
                    mStreaming.obtainMessage(A2dpSinkStreamHandler.ACT_PAUSE).sendToTarget();
                    mStreaming.obtainMessage(A2dpSinkStreamHandler.SNK_PAUSE).sendToTarget();
                    break;

                case EVENT_AVRCP_TG_PAUSE:
                    mStreaming.obtainMessage(A2dpSinkStreamHandler.SRC_PAUSE).sendToTarget();
                    break;

                default:
                    return NOT_HANDLED;
+86 −82
Original line number Diff line number Diff line
@@ -37,9 +37,7 @@ import com.android.bluetooth.R;
 *
 * Note: There are several different audio tracks that a connected phone may like to transmit over
 * the A2DP stream including Music, Navigation, Assistant, and Notifications.  Music is the only
 * track that is almost always accompanied with an AVRCP play/pause command.  The following handler
 * is configurable at compile time through the PLAY_WITHOUT_AVRCP_COMMAND flag to allow all of these
 * audio tracks to be played trough without an explicit play command.
 * track that is almost always accompanied with an AVRCP play/pause command.
 *
 * Streaming is initiated by either an explicit play command from user interaction or audio coming
 * from the phone.  Streaming is terminated when either the user pauses the audio, the audio stream
@@ -47,44 +45,37 @@ import com.android.bluetooth.R;
 * a change to audio focus playback may be temporarily paused and then resumed when focus is
 * restored.
 */
final class A2dpSinkStreamHandler extends Handler {
public class A2dpSinkStreamHandler extends Handler {
    private static final boolean DBG = false;
    private static final String TAG = "A2dpSinkStreamHandler";

    // Configuration Variables
    private static final int DEFAULT_DUCK_PERCENT = 25;
    // Allows any audio to stream from phone without requiring AVRCP play command,
    // this lets navigation and other non music streams through.
    private static final boolean PLAY_WITHOUT_AVRCP_COMMAND = true;

    // Incoming events.
    public static final int SRC_STR_START = 0;
    public static final int SRC_STR_STOP = 1;
    public static final int ACT_PLAY = 2;
    public static final int ACT_PAUSE = 3;
    public static final int DISCONNECT = 4;
    public static final int UPGRADE_FOCUS = 5;
    public static final int AUDIO_FOCUS_CHANGE = 7;
    public static final int SRC_STR_START = 0; // Audio stream from remote device started
    public static final int SRC_STR_STOP = 1; // Audio stream from remote device stopped
    public static final int SNK_PLAY = 2; // Play command was generated from local device
    public static final int SNK_PAUSE = 3; // Pause command was generated from local device
    public static final int SRC_PLAY = 4; // Play command was generated from remote device
    public static final int SRC_PAUSE = 5; // Pause command was generated from remote device
    public static final int DISCONNECT = 6; // Remote device was disconnected
    public static final int AUDIO_FOCUS_CHANGE = 7; // Audio focus callback with associated change

    // Used to indicate focus lost
    private static final int STATE_FOCUS_LOST = 0;
    // Used to inform bluedroid that focus is granted
    private static final int STATE_FOCUS_GRANTED = 1;
    // Timeout in milliseconds before upgrading a transient audio focus to full focus;
    // This allows notifications and other intermittent sounds from impacting other sources.
    private static final int TRANSIENT_FOCUS_DELAY = 10000; // 10 seconds

    // Private variables.
    private A2dpSinkStateMachine mA2dpSinkSm;
    private Context mContext;
    private AudioManager mAudioManager;
    private AudioAttributes mStreamAttributes;
    // Keep track if play was requested
    private boolean playRequested = false;
    // Keep track if the remote device is providing audio
    private boolean streamAvailable = false;
    private boolean mStreamAvailable = false;
    private boolean mSentPause = false;
    // Keep track of the relevant audio focus (None, Transient, Gain)
    private int audioFocus = AudioManager.AUDIOFOCUS_NONE;
    private int mAudioFocus = AudioManager.AUDIOFOCUS_NONE;

    // Focus changes when we are currently holding focus.
    private OnAudioFocusChangeListener mAudioFocusListener = new OnAudioFocusChangeListener() {
@@ -101,80 +92,76 @@ final class A2dpSinkStreamHandler extends Handler {
        mA2dpSinkSm = a2dpSinkSm;
        mContext = context;
        mAudioManager = (AudioManager) context.getSystemService(Context.AUDIO_SERVICE);
        mStreamAttributes = new AudioAttributes.Builder()
                                    .setUsage(AudioAttributes.USAGE_MEDIA)
                                    .setContentType(AudioAttributes.CONTENT_TYPE_MUSIC)
                                    .build();
    }

    @Override
    public void handleMessage(Message message) {
        if (DBG) {
            Log.d(TAG, " process message: " + message.what);
            Log.d(TAG, " audioFocus =  " + audioFocus + " playRequested = " + playRequested);
            Log.d(TAG, " audioFocus =  " + mAudioFocus);
        }
        switch (message.what) {
            case SRC_STR_START:
                streamAvailable = true;
                if ((playRequested || PLAY_WITHOUT_AVRCP_COMMAND)
                        && audioFocus == AudioManager.AUDIOFOCUS_NONE) {
                    requestAudioFocus();
                // Audio stream has started, stop it if we don't have focus.
                mStreamAvailable = true;
                if (mAudioFocus == AudioManager.AUDIOFOCUS_NONE) {
                    sendAvrcpPause();
                } else {
                    startAvrcpUpdates();
                }
                break;

            case SRC_STR_STOP:
                streamAvailable = false;
                if (audioFocus != AudioManager.AUDIOFOCUS_NONE) {
                    abandonAudioFocus();
                // Audio stream has stopped, maintain focus but stop avrcp updates.
                mStreamAvailable = false;
                stopAvrcpUpdates();
                break;

            case SNK_PLAY:
                // Local play command, gain focus and start avrcp updates.
                if (mAudioFocus == AudioManager.AUDIOFOCUS_NONE) {
                    requestAudioFocus();
                }
                startAvrcpUpdates();
                break;

            case SNK_PAUSE:
                // Local pause command, maintain focus but stop avrcp updates.
                stopAvrcpUpdates();
                break;

            case ACT_PLAY:
                playRequested = true;
            case SRC_PLAY:
                // Remote play command, if we have audio focus update avrcp, otherwise send pause.
                if (mAudioFocus == AudioManager.AUDIOFOCUS_NONE) {
                    sendAvrcpPause();
                } else {
                    startAvrcpUpdates();
                if (streamAvailable && audioFocus == AudioManager.AUDIOFOCUS_NONE) {
                    requestAudioFocus();
                }
                break;

            case ACT_PAUSE:
                playRequested = false;
            case SRC_PAUSE:
                // Remote pause command, stop avrcp updates.
                stopAvrcpUpdates();
                break;

            case DISCONNECT:
                playRequested = false;
                // Remote device has disconnected, restore everything to default state.
                sendAvrcpPause();
                stopAvrcpUpdates();
                stopFluorideStreaming();
                abandonAudioFocus();
                break;

            case UPGRADE_FOCUS:
                upgradeAudioFocus();
                mSentPause = false;
                break;

            case AUDIO_FOCUS_CHANGE:
                // message.obj is the newly granted audio focus.
                switch ((int) message.obj) {
                    case AudioManager.AUDIOFOCUS_GAIN_TRANSIENT:
                        setFluorideAudioTrackGain(1.0f);
                        sendMessageDelayed(obtainMessage(UPGRADE_FOCUS), TRANSIENT_FOCUS_DELAY);
                        // Begin playing audio
                        if (audioFocus == AudioManager.AUDIOFOCUS_NONE) {
                            audioFocus = (int) message.obj;
                            startAvrcpUpdates();
                            startFluorideStreaming();
                        }
                        break;

                    case AudioManager.AUDIOFOCUS_GAIN:
                        setFluorideAudioTrackGain(1.0f);
                        // Begin playing audio
                        if (audioFocus == AudioManager.AUDIOFOCUS_NONE) {
                            audioFocus = (int) message.obj;
                        // Begin playing audio, if we paused the remote, send a play now.
                        startAvrcpUpdates();
                        startFluorideStreaming();
                        if (mSentPause) {
                            sendAvrcpPlay();
                            mSentPause = false;
                        }
                        break;

@@ -191,19 +178,24 @@ final class A2dpSinkStreamHandler extends Handler {
                            Log.d(TAG, "Setting reduce gain on transient loss gain=" + duckRatio);
                        }
                        setFluorideAudioTrackGain(duckRatio);
                        removeMessages(UPGRADE_FOCUS);
                        break;

                    case AudioManager.AUDIOFOCUS_LOSS_TRANSIENT:
                        // Temporary loss of focus, if we are actively streaming pause the remote
                        // and make sure we resume playback when we regain focus.
                        if (mStreamAvailable) {
                            sendAvrcpPause();
                            mSentPause = true;
                        }
                        stopFluorideStreaming();
                        removeMessages(UPGRADE_FOCUS);
                        break;

                    case AudioManager.AUDIOFOCUS_LOSS:
                        // Permanent loss of focus probably due to another audio app, abandon focus
                        // and stop playback.
                        mAudioFocus = AudioManager.AUDIOFOCUS_NONE;
                        abandonAudioFocus();
                        sendAvrcpPause();
                        stopAvrcpUpdates();
                        stopFluorideStreaming();
                        break;
                }
                break;
@@ -217,32 +209,22 @@ final class A2dpSinkStreamHandler extends Handler {
     * Utility functions.
     */
    private int requestAudioFocus() {
        int focusRequestStatus = mAudioManager.requestAudioFocus(mAudioFocusListener,
                mStreamAttributes, AudioManager.AUDIOFOCUS_GAIN_TRANSIENT,
                AudioManager.AUDIOFOCUS_FLAG_DELAY_OK);
        int focusRequestStatus = mAudioManager.requestAudioFocus(
                mAudioFocusListener, AudioManager.STREAM_MUSIC, AudioManager.AUDIOFOCUS_GAIN);
        // If the request is granted begin streaming immediately and schedule an upgrade.
        if (focusRequestStatus == AudioManager.AUDIOFOCUS_REQUEST_GRANTED) {
            startAvrcpUpdates();
            setFluorideAudioTrackGain(1.0f);
            startFluorideStreaming();
            audioFocus = AudioManager.AUDIOFOCUS_GAIN_TRANSIENT;
            sendMessageDelayed(obtainMessage(UPGRADE_FOCUS), TRANSIENT_FOCUS_DELAY);
            mAudioFocus = AudioManager.AUDIOFOCUS_GAIN;
        }
        return focusRequestStatus;
    }

    private boolean upgradeAudioFocus() {
        return (mAudioManager.requestAudioFocus(mAudioFocusListener, AudioManager.STREAM_MUSIC,
                        AudioManager.AUDIOFOCUS_GAIN)
                == AudioManager.AUDIOFOCUS_REQUEST_GRANTED);
    }

    private void abandonAudioFocus() {
        removeMessages(UPGRADE_FOCUS);
        stopAvrcpUpdates();
        stopFluorideStreaming();
        mAudioManager.abandonAudioFocus(mAudioFocusListener);
        audioFocus = AudioManager.AUDIOFOCUS_NONE;
        mAudioFocus = AudioManager.AUDIOFOCUS_NONE;
    }

    private void startFluorideStreaming() {
@@ -307,4 +289,26 @@ final class A2dpSinkStreamHandler extends Handler {
            Log.e(TAG, "Passthrough not sent, connection un-available.");
        }
    }

    private void sendAvrcpPlay() {
        // Since AVRCP gets started after A2DP we may need to request it later in cycle.
        AvrcpControllerService avrcpService = AvrcpControllerService.getAvrcpControllerService();

        if (DBG) {
            Log.d(TAG, "sendAvrcpPlay");
        }
        if (avrcpService != null && avrcpService.getConnectedDevices().size() == 1) {
            if (DBG) {
                Log.d(TAG, "Playing AVRCP.");
            }
            avrcpService.sendPassThroughCmd(avrcpService.getConnectedDevices().get(0),
                    AvrcpControllerService.PASS_THRU_CMD_ID_PLAY,
                    AvrcpControllerService.KEY_STATE_PRESSED);
            avrcpService.sendPassThroughCmd(avrcpService.getConnectedDevices().get(0),
                    AvrcpControllerService.PASS_THRU_CMD_ID_PLAY,
                    AvrcpControllerService.KEY_STATE_RELEASED);
        } else {
            Log.e(TAG, "Passthrough not sent, connection un-available.");
        }
    }
}
+189 −0
Original line number Diff line number Diff line
/*
 * Copyright (C) 2017 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 com.android.bluetooth.a2dpsink;

import static org.mockito.Mockito.any;
import static org.mockito.Mockito.anyFloat;
import static org.mockito.Mockito.anyInt;
import static org.mockito.Mockito.doNothing;
import static org.mockito.Mockito.eq;
import static org.mockito.Mockito.spy;
import static org.mockito.Mockito.times;
import static org.mockito.Mockito.verify;
import static org.mockito.Mockito.when;

import android.content.Context;
import android.content.res.Resources;
import android.media.AudioManager;
import android.media.AudioManager.OnAudioFocusChangeListener;
import android.os.HandlerThread;
import android.os.Looper;
import android.test.AndroidTestCase;

import org.junit.Before;
import org.junit.Test;
import org.junit.runner.RunWith;
import org.mockito.ArgumentCaptor;
import org.mockito.Mock;
import org.mockito.runners.MockitoJUnitRunner;

@RunWith(MockitoJUnitRunner.class)
public class A2dpSinkStreamHandlerTest extends AndroidTestCase {
    static final int DUCK_PERCENT = 75;
    private HandlerThread mHandlerThread;
    A2dpSinkStreamHandler streamHandler;
    ArgumentCaptor<OnAudioFocusChangeListener> audioFocusChangeListenerArgumentCaptor;

    @Mock Context mockContext;

    @Mock A2dpSinkStateMachine mockA2dpSink;

    @Mock AudioManager mockAudioManager;

    @Mock Resources mockResources;

    @Before
    public void setUp() {
        // Mock the looper
        if (Looper.myLooper() == null) {
            Looper.prepare();
        }

        mHandlerThread = new HandlerThread("A2dpSinkStreamHandlerTest");
        mHandlerThread.start();

        audioFocusChangeListenerArgumentCaptor =
                ArgumentCaptor.forClass(OnAudioFocusChangeListener.class);
        when(mockContext.getSystemService(Context.AUDIO_SERVICE)).thenReturn(mockAudioManager);
        when(mockContext.getResources()).thenReturn(mockResources);
        when(mockResources.getInteger(anyInt())).thenReturn(DUCK_PERCENT);
        when(mockAudioManager.requestAudioFocus(audioFocusChangeListenerArgumentCaptor.capture(),
                     eq(AudioManager.STREAM_MUSIC), eq(AudioManager.AUDIOFOCUS_GAIN)))
                .thenReturn(AudioManager.AUDIOFOCUS_REQUEST_GRANTED);
        when(mockAudioManager.abandonAudioFocus(any())).thenReturn(AudioManager.AUDIOFOCUS_GAIN);
        doNothing().when(mockA2dpSink).informAudioTrackGainNative(anyFloat());
        when(mockContext.getMainLooper()).thenReturn(mHandlerThread.getLooper());

        streamHandler = spy(new A2dpSinkStreamHandler(mockA2dpSink, mockContext));
    }

    @Test
    public void testSrcStart() {
        // Stream started without local play, expect no change in streaming.
        streamHandler.handleMessage(
                streamHandler.obtainMessage(A2dpSinkStreamHandler.SRC_STR_START));
        verify(mockAudioManager, times(0)).requestAudioFocus(any(), anyInt(), anyInt());
        verify(mockA2dpSink, times(0)).informAudioFocusStateNative(1);
        verify(mockA2dpSink, times(0)).informAudioTrackGainNative(1.0f);
    }

    @Test
    public void testSrcStop() {
        // Stream stopped without local play, expect no change in streaming.
        streamHandler.handleMessage(
                streamHandler.obtainMessage(A2dpSinkStreamHandler.SRC_STR_STOP));
        verify(mockAudioManager, times(0)).requestAudioFocus(any(), anyInt(), anyInt());
        verify(mockA2dpSink, times(0)).informAudioFocusStateNative(1);
        verify(mockA2dpSink, times(0)).informAudioTrackGainNative(1.0f);
    }

    @Test
    public void testSnkPlay() {
        // Play was pressed locally, expect streaming to start.
        streamHandler.handleMessage(streamHandler.obtainMessage(A2dpSinkStreamHandler.SNK_PLAY));
        verify(mockAudioManager, times(1)).requestAudioFocus(any(), anyInt(), anyInt());
        verify(mockA2dpSink, times(1)).informAudioFocusStateNative(1);
        verify(mockA2dpSink, times(1)).informAudioTrackGainNative(1.0f);
    }

    @Test
    public void testSnkPause() {
        // Pause was pressed locally, expect streaming to stop.
        streamHandler.handleMessage(streamHandler.obtainMessage(A2dpSinkStreamHandler.SNK_PAUSE));
        verify(mockAudioManager, times(0)).requestAudioFocus(any(), anyInt(), anyInt());
        verify(mockA2dpSink, times(0)).informAudioFocusStateNative(1);
        verify(mockA2dpSink, times(0)).informAudioTrackGainNative(1.0f);
    }

    @Test
    public void testDisconnect() {
        // Remote device was disconnected, expect streaming to stop.
        testSnkPlay();
        streamHandler.handleMessage(streamHandler.obtainMessage(A2dpSinkStreamHandler.DISCONNECT));
        verify(mockAudioManager, times(1)).abandonAudioFocus(any());
        verify(mockA2dpSink, times(1)).informAudioFocusStateNative(0);
    }

    @Test
    public void testSrcPlay() {
        // Play was pressed remotely, expect no streaming due to lack of audio focus.
        streamHandler.handleMessage(streamHandler.obtainMessage(A2dpSinkStreamHandler.SRC_PLAY));
        verify(mockAudioManager, times(0)).requestAudioFocus(any(), anyInt(), anyInt());
        verify(mockA2dpSink, times(0)).informAudioFocusStateNative(1);
        verify(mockA2dpSink, times(0)).informAudioTrackGainNative(1.0f);
    }

    @Test
    public void testSrcPause() {
        // Play was pressed locally, expect streaming to start.
        streamHandler.handleMessage(streamHandler.obtainMessage(A2dpSinkStreamHandler.SRC_PLAY));
        verify(mockAudioManager, times(0)).requestAudioFocus(any(), anyInt(), anyInt());
        verify(mockA2dpSink, times(0)).informAudioFocusStateNative(1);
        verify(mockA2dpSink, times(0)).informAudioTrackGainNative(1.0f);
    }

    @Test
    public void testFocusGain() {
        // Focus was gained, expect streaming to resume.
        testSnkPlay();
        streamHandler.handleMessage(streamHandler.obtainMessage(
                A2dpSinkStreamHandler.AUDIO_FOCUS_CHANGE, AudioManager.AUDIOFOCUS_GAIN));
        verify(mockAudioManager, times(1)).requestAudioFocus(any(), anyInt(), anyInt());
        verify(mockA2dpSink, times(2)).informAudioFocusStateNative(1);
        verify(mockA2dpSink, times(2)).informAudioTrackGainNative(1.0f);
    }

    @Test
    public void testFocusTransientMayDuck() {
        // TransientMayDuck focus was gained, expect audio stream to duck.
        testSnkPlay();
        streamHandler.handleMessage(
                streamHandler.obtainMessage(A2dpSinkStreamHandler.AUDIO_FOCUS_CHANGE,
                        AudioManager.AUDIOFOCUS_LOSS_TRANSIENT_CAN_DUCK));
        verify(mockA2dpSink, times(1)).informAudioTrackGainNative(DUCK_PERCENT / 100.0f);
    }

    @Test
    public void testFocusLostTransient() {
        // Focus was lost transiently, expect streaming to stop.
        testSnkPlay();
        streamHandler.handleMessage(streamHandler.obtainMessage(
                A2dpSinkStreamHandler.AUDIO_FOCUS_CHANGE, AudioManager.AUDIOFOCUS_LOSS_TRANSIENT));
        verify(mockAudioManager, times(0)).abandonAudioFocus(any());
        verify(mockA2dpSink, times(1)).informAudioFocusStateNative(0);
    }

    @Test
    public void testFocusLost() {
        // Focus was lost permanently, expect streaming to stop.
        testSnkPlay();
        streamHandler.handleMessage(streamHandler.obtainMessage(
                A2dpSinkStreamHandler.AUDIO_FOCUS_CHANGE, AudioManager.AUDIOFOCUS_LOSS));
        verify(mockAudioManager, times(1)).abandonAudioFocus(any());
        verify(mockA2dpSink, times(1)).informAudioFocusStateNative(0);
    }
}