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

Commit 9e6cef6f authored by Treehugger Robot's avatar Treehugger Robot Committed by Gerrit Code Review
Browse files

Merge "A2DP sink audio focus"

parents 97e25765 e9d2261d
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);
    }
}