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

Commit a0fe0932 authored by Sal Savage's avatar Sal Savage
Browse files

Make pause/stop cancel playback reestablishment during transient loss

Our goal in a transient loss is to pause the remote player as a courtesy
and then attempt to reestablish playback once we gain focus back.
However, in rare cases, an application (like assistant) can request and
send a pause while we're in a transient loss and already paused. This
should cause playback to stop, however we don't currently react to the
pause and cancel our note to reestablish playback.

This change moves the play/pause decision logic to AVRCP Controller, and
allows local requests to transport controls to cancel our future attempt
to establish playback out of a transient loss if a user sents a pause or
stop.

Tag: #stability
Bug: 254536123
Test: atest BluetoothInstrumentationTests
Change-Id: I277e35f7b432fc7d287cbd4ab19c2ef5f3b870bb
parent e5fb9b36
Loading
Loading
Loading
Loading
+11 −54
Original line number Diff line number Diff line
@@ -16,7 +16,6 @@

package com.android.bluetooth.a2dpsink;

import android.bluetooth.BluetoothDevice;
import android.content.pm.PackageManager;
import android.media.AudioAttributes;
import android.media.AudioFocusRequest;
@@ -25,15 +24,10 @@ import android.media.AudioManager.OnAudioFocusChangeListener;
import android.media.MediaPlayer;
import android.os.Handler;
import android.os.Message;
import android.support.v4.media.session.PlaybackStateCompat;
import android.util.Log;

import com.android.bluetooth.R;
import com.android.bluetooth.avrcpcontroller.BluetoothMediaBrowserService;
import com.android.bluetooth.hfpclient.HeadsetClientService;
import com.android.bluetooth.hfpclient.HfpClientCall;

import java.util.List;
import com.android.bluetooth.avrcpcontroller.AvrcpControllerService;

/**
 * Bluetooth A2DP SINK Streaming Handler.
@@ -71,7 +65,6 @@ public class A2dpSinkStreamHandler extends Handler {
    public static final int DISCONNECT = 6; // Remote device was disconnected
    public static final int AUDIO_FOCUS_CHANGE = 7; // Audio focus callback with associated change
    public static final int REQUEST_FOCUS = 8; // Request focus when the media service is active
    public static final int DELAYED_PAUSE = 9; // If a call just started allow stack time to settle

    // Used to indicate focus lost
    private static final int STATE_FOCUS_LOST = 0;
@@ -84,7 +77,6 @@ public class A2dpSinkStreamHandler extends Handler {
    private AudioManager mAudioManager;
    // Keep track if the remote device is providing audio
    private boolean mStreamAvailable = false;
    private boolean mSentPause = false;
    // Keep track of the relevant audio focus (None, Transient, Gain)
    private int mAudioFocus = AudioManager.AUDIOFOCUS_NONE;

@@ -187,7 +179,7 @@ public class A2dpSinkStreamHandler extends Handler {

            case DISCONNECT:
                // Remote device has disconnected, restore everything to default state.
                mSentPause = false;
                mStreamAvailable = false;
                break;

            case AUDIO_FOCUS_CHANGE:
@@ -195,13 +187,8 @@ public class A2dpSinkStreamHandler extends Handler {
                // message.obj is the newly granted audio focus.
                switch (mAudioFocus) {
                    case AudioManager.AUDIOFOCUS_GAIN:
                        removeMessages(DELAYED_PAUSE);
                        // Begin playing audio, if we paused the remote, send a play now.
                        // Begin playing audio
                        startFluorideStreaming();
                        if (mSentPause) {
                            sendAvrcpPlay();
                            mSentPause = false;
                        }
                        break;

                    case AudioManager.AUDIOFOCUS_LOSS_TRANSIENT_CAN_DUCK:
@@ -220,27 +207,23 @@ public class A2dpSinkStreamHandler extends Handler {
                        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.
                        sendMessageDelayed(obtainMessage(DELAYED_PAUSE), SETTLE_TIMEOUT);
                        // Temporary loss of focus. Set gain to zero.
                        setFluorideAudioTrackGain(0);
                        break;

                    case AudioManager.AUDIOFOCUS_LOSS:
                        // Permanent loss of focus probably due to another audio app, abandon focus
                        // and stop playback.
                        abandonAudioFocus();
                        sendAvrcpPause();
                        break;
                }
                break;

            case DELAYED_PAUSE:
                if (BluetoothMediaBrowserService.getPlaybackState()
                            == PlaybackStateCompat.STATE_PLAYING && !inCallFromStreamingDevice()) {
                    sendAvrcpPause();
                    mSentPause = true;
                    mStreamAvailable = false;
                // Route new focus state to AVRCP Controller to handle media player states
                AvrcpControllerService avrcpControllerService =
                        AvrcpControllerService.getAvrcpControllerService();
                if (avrcpControllerService != null) {
                    avrcpControllerService.onAudioFocusStateChanged(mAudioFocus);
                } else {
                    Log.w(TAG, "AVRCP Controller Service not available to send focus events to.");
                }
                break;

@@ -316,7 +299,6 @@ public class A2dpSinkStreamHandler extends Handler {
        }

        mMediaPlayer.start();
        BluetoothMediaBrowserService.setActive(true);
    }

    private synchronized void abandonAudioFocus() {
@@ -335,7 +317,6 @@ public class A2dpSinkStreamHandler extends Handler {
        if (mMediaPlayer == null) {
            return;
        }
        BluetoothMediaBrowserService.setActive(false);
        mMediaPlayer.stop();
        mMediaPlayer.release();
        mMediaPlayer = null;
@@ -356,30 +337,6 @@ public class A2dpSinkStreamHandler extends Handler {
        mNativeInterface.informAudioTrackGain(gain);
    }

    private void sendAvrcpPause() {
        BluetoothMediaBrowserService.pause();
    }

    private void sendAvrcpPlay() {
        BluetoothMediaBrowserService.play();
    }

    private boolean inCallFromStreamingDevice() {
        BluetoothDevice targetDevice = null;
        List<BluetoothDevice> connectedDevices = mA2dpSinkService.getConnectedDevices();
        if (!connectedDevices.isEmpty()) {
            targetDevice = connectedDevices.get(0);
        }
        HeadsetClientService headsetClientService = HeadsetClientService.getHeadsetClientService();
        if (targetDevice != null && headsetClientService != null) {
            List<HfpClientCall> currentCalls =
                    headsetClientService.getCurrentCalls(targetDevice);
            if (currentCalls == null) return false;
            return currentCalls.size() > 0;
        }
        return false;
    }

    private boolean isIotDevice() {
        return mA2dpSinkService.getPackageManager().hasSystemFeature(
                PackageManager.FEATURE_EMBEDDED);
+29 −1
Original line number Diff line number Diff line
@@ -23,7 +23,6 @@ import android.bluetooth.BluetoothDevice;
import android.bluetooth.BluetoothProfile;
import android.bluetooth.IBluetoothAvrcpController;
import android.content.AttributionSource;
import android.content.ComponentName;
import android.content.Intent;
import android.support.v4.media.MediaBrowserCompat.MediaItem;
import android.support.v4.media.session.PlaybackStateCompat;
@@ -503,11 +502,13 @@ public class AvrcpControllerService extends ProfileService {
            // The first device to connect gets to be the active device
            if (getActiveDevice() == null) {
                setActiveDevice(device);
                BluetoothMediaBrowserService.setActive(true);
            }
        } else {
            stateMachine.disconnect();
            if (device.equals(getActiveDevice())) {
                setActiveDevice(null);
                BluetoothMediaBrowserService.setActive(false);
            }
        }
    }
@@ -559,6 +560,33 @@ public class AvrcpControllerService extends ProfileService {
        }
    }

    /**
     * Notify AVRCP Controller of an audio focus state change so we can make requests of the active
     * player to stop and start playing.
     */
    public void onAudioFocusStateChanged(int state) {
        if (DBG) {
            Log.d(TAG, "onAudioFocusStateChanged(state=" + state + ")");
        }

        // Make sure the active device isn't changed while we're processing the event so play/pause
        // commands get routed to the correct device
        synchronized (mActiveDeviceLock) {
            BluetoothDevice device = getActiveDevice();
            if (device == null) {
                Log.w(TAG, "No active device set, ignore focus change");
                return;
            }

            AvrcpControllerStateMachine stateMachine = mDeviceStateMap.get(device);
            if (stateMachine == null) {
                Log.w(TAG, "No state machine for active device.");
                return;
            }
            stateMachine.sendMessage(AvrcpControllerStateMachine.AUDIO_FOCUS_STATE_CHANGE, state);
        }
    }

    // Called by JNI when a track changes and local AvrcpController is registered for updates.
    private synchronized void onTrackChanged(byte[] address, byte numAttributes, int[] attributes,
            String[] attribVals) {
+78 −9
Original line number Diff line number Diff line
@@ -58,6 +58,7 @@ class AvrcpControllerStateMachine extends StateMachine {
    public static final int CONNECT = 1;
    public static final int DISCONNECT = 2;
    public static final int ACTIVE_DEVICE_CHANGE = 3;
    public static final int AUDIO_FOCUS_STATE_CHANGE = 4;

    //100->199 Internal Events
    protected static final int CLEANUP = 100;
@@ -110,6 +111,7 @@ class AvrcpControllerStateMachine extends StateMachine {

    private static BluetoothDevice sActiveDevice;
    private final AudioManager mAudioManager;
    private boolean mShouldSendPlayOnFocusRecovery = false;
    private final boolean mIsVolumeFixed;

    protected final BluetoothDevice mDevice;
@@ -480,9 +482,59 @@ class AvrcpControllerStateMachine extends StateMachine {
                        BluetoothMediaBrowserService.notifyChanged(
                                mAddressedPlayer.getPlaybackState());
                        BluetoothMediaBrowserService.notifyChanged(mBrowseTree.mNowPlayingNode);

                        // If we switch to a device that is playing and we don't have focus, pause
                        int focusState = getFocusState();
                        if (mAddressedPlayer.getPlaybackState().getState()
                                == PlaybackStateCompat.STATE_PLAYING
                                && focusState == AudioManager.AUDIOFOCUS_NONE) {
                            sendMessage(MSG_AVRCP_PASSTHRU,
                                    AvrcpControllerService.PASS_THRU_CMD_ID_PAUSE);
                        }
                    } else {
                        sendMessage(MSG_AVRCP_PASSTHRU,
                                AvrcpControllerService.PASS_THRU_CMD_ID_PAUSE);
                        mShouldSendPlayOnFocusRecovery = false;
                    }
                    return true;

                case AUDIO_FOCUS_STATE_CHANGE:
                    int newState = msg.arg1;
                    logD("Audio focus changed -> " + newState);
                    switch (newState) {
                        case AudioManager.AUDIOFOCUS_GAIN:
                            // Begin playing audio again if we paused the remote
                            if (mShouldSendPlayOnFocusRecovery) {
                                logD("Regained focus, establishing play status");
                                sendMessage(MSG_AVRCP_PASSTHRU,
                                        AvrcpControllerService.PASS_THRU_CMD_ID_PLAY);
                            }
                            mShouldSendPlayOnFocusRecovery = false;
                            break;

                        case AudioManager.AUDIOFOCUS_LOSS_TRANSIENT:
                            // Temporary loss of focus. Send a courtesy pause if we are playing and
                            // note we should recover
                            if (mAddressedPlayer.getPlaybackState().getState()
                                    == PlaybackStateCompat.STATE_PLAYING) {
                                logD("Transient loss, temporarily pause with intent to recover");
                                sendMessage(MSG_AVRCP_PASSTHRU,
                                        AvrcpControllerService.PASS_THRU_CMD_ID_PAUSE);
                                mShouldSendPlayOnFocusRecovery = true;
                            }
                            break;

                        case AudioManager.AUDIOFOCUS_LOSS:
                            // Permanent loss of focus probably due to another audio app. Send a
                            // courtesy pause
                            logD("Lost focus, send a courtesy pause");
                            if (mAddressedPlayer.getPlaybackState().getState()
                                    == PlaybackStateCompat.STATE_PLAYING) {
                                sendMessage(MSG_AVRCP_PASSTHRU,
                                        AvrcpControllerService.PASS_THRU_CMD_ID_PAUSE);
                            }
                            mShouldSendPlayOnFocusRecovery = false;
                            break;
                    }
                    return true;

@@ -537,6 +589,7 @@ class AvrcpControllerStateMachine extends StateMachine {
                    return true;

                case MESSAGE_PROCESS_PLAY_STATUS_CHANGED:
                    logd("Playback status changed to " + msg.arg1);
                    mAddressedPlayer.setPlayStatus(msg.arg1);
                    if (!isActive()) {
                        sendMessage(MSG_AVRCP_PASSTHRU,
@@ -544,22 +597,17 @@ class AvrcpControllerStateMachine extends StateMachine {
                        return true;
                    }

                    PlaybackStateCompat playbackState = mAddressedPlayer.getPlaybackState();
                    BluetoothMediaBrowserService.notifyChanged(playbackState);

                    int focusState = AudioManager.ERROR;
                    A2dpSinkService a2dpSinkService = A2dpSinkService.getA2dpSinkService();
                    if (a2dpSinkService != null) {
                        focusState = a2dpSinkService.getFocusState();
                    }
                    BluetoothMediaBrowserService.notifyChanged(mAddressedPlayer.getPlaybackState());

                    int focusState = getFocusState();
                    if (focusState == AudioManager.ERROR) {
                        sendMessage(MSG_AVRCP_PASSTHRU,
                                AvrcpControllerService.PASS_THRU_CMD_ID_PAUSE);
                        return true;
                    }

                    if (playbackState.getState() == PlaybackStateCompat.STATE_PLAYING
                    if (mAddressedPlayer.getPlaybackState().getState()
                            == PlaybackStateCompat.STATE_PLAYING
                            && focusState == AudioManager.AUDIOFOCUS_NONE) {
                        if (shouldRequestFocus()) {
                            mSessionCallbacks.onPrepare();
@@ -1141,6 +1189,15 @@ class AvrcpControllerStateMachine extends StateMachine {
        }
    }

    private int getFocusState() {
        int focusState = AudioManager.ERROR;
        A2dpSinkService a2dpSinkService = A2dpSinkService.getA2dpSinkService();
        if (a2dpSinkService != null) {
            focusState = a2dpSinkService.getFocusState();
        }
        return focusState;
    }

    MediaSessionCompat.Callback mSessionCallbacks = new MediaSessionCompat.Callback() {
        @Override
        public void onPlay() {
@@ -1152,6 +1209,12 @@ class AvrcpControllerStateMachine extends StateMachine {
        @Override
        public void onPause() {
            logD("onPause");
            // If we receive a local pause/stop request and send it out then we need to signal that
            // the intent is to stay paused if we recover focus from a transient loss
            if (getFocusState() == AudioManager.AUDIOFOCUS_LOSS_TRANSIENT) {
                logD("Received a pause while in a transient loss. Do not recover anymore.");
                mShouldSendPlayOnFocusRecovery = false;
            }
            sendMessage(MSG_AVRCP_PASSTHRU, AvrcpControllerService.PASS_THRU_CMD_ID_PAUSE);
        }

@@ -1182,6 +1245,12 @@ class AvrcpControllerStateMachine extends StateMachine {
        @Override
        public void onStop() {
            logD("onStop");
            // If we receive a local pause/stop request and send it out then we need to signal that
            // the intent is to stay paused if we recover focus from a transient loss
            if (getFocusState() == AudioManager.AUDIOFOCUS_LOSS_TRANSIENT) {
                logD("Received a stop while in a transient loss. Do not recover anymore.");
                mShouldSendPlayOnFocusRecovery = false;
            }
            sendMessage(MSG_AVRCP_PASSTHRU, AvrcpControllerService.PASS_THRU_CMD_ID_STOP);
        }

+4 −10
Original line number Diff line number Diff line
@@ -233,20 +233,14 @@ public class A2dpSinkStreamHandlerTest {
    }

    @Test
    public void testFocusGainTransient() {
        // Focus was lost then regained.
        testSnkPlay();
        mStreamHandler.handleMessage(
                mStreamHandler.obtainMessage(A2dpSinkStreamHandler.AUDIO_FOCUS_CHANGE,
                        AudioManager.AUDIOFOCUS_LOSS_TRANSIENT));
        mStreamHandler.handleMessage(
                mStreamHandler.obtainMessage(A2dpSinkStreamHandler.DELAYED_PAUSE));
    public void testFocusGainFromTransientLoss() {
        // Focus was lost transiently and then regained.
        testFocusLostTransient();

        mStreamHandler.handleMessage(
                mStreamHandler.obtainMessage(A2dpSinkStreamHandler.AUDIO_FOCUS_CHANGE,
                        AudioManager.AUDIOFOCUS_GAIN));
        verify(mMockAudioManager, times(0)).abandonAudioFocus(any());
        verify(mMockNativeInterface, times(0)).informAudioFocusState(0);
        verify(mMockNativeInterface, times(1)).informAudioTrackGain(0);
        verify(mMockNativeInterface, times(2)).informAudioTrackGain(1.0f);

        TestUtils.waitForLooperToFinishScheduledTask(mHandlerThread.getLooper());
+173 −0

File changed.

Preview size limit exceeded, changes collapsed.