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
diff --git a/src/com/android/bluetooth/a2dpsink/A2dpSinkStateMachine.java b/src/com/android/bluetooth/a2dpsink/A2dpSinkStateMachine.java
index 5102369..02f8f1d 100644
--- a/src/com/android/bluetooth/a2dpsink/A2dpSinkStateMachine.java
+++ b/src/com/android/bluetooth/a2dpsink/A2dpSinkStateMachine.java
@@ -58,7 +58,7 @@
 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 @@
                     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;
diff --git a/src/com/android/bluetooth/a2dpsink/A2dpSinkStreamHandler.java b/src/com/android/bluetooth/a2dpsink/A2dpSinkStreamHandler.java
index deb0563..33fe1ec 100644
--- a/src/com/android/bluetooth/a2dpsink/A2dpSinkStreamHandler.java
+++ b/src/com/android/bluetooth/a2dpsink/A2dpSinkStreamHandler.java
@@ -37,9 +37,7 @@
  *
  * 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 @@
  * 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 @@
         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 ACT_PLAY:
-                playRequested = true;
-                startAvrcpUpdates();
-                if (streamAvailable && audioFocus == AudioManager.AUDIOFOCUS_NONE) {
+            case SNK_PLAY:
+                // Local play command, gain focus and start avrcp updates.
+                if (mAudioFocus == AudioManager.AUDIOFOCUS_NONE) {
                     requestAudioFocus();
                 }
+                startAvrcpUpdates();
                 break;
 
-            case ACT_PAUSE:
-                playRequested = false;
+            case SNK_PAUSE:
+                // Local pause command, maintain focus but stop avrcp updates.
+                stopAvrcpUpdates();
+                break;
+
+            case SRC_PLAY:
+                // Remote play command, if we have audio focus update avrcp, otherwise send pause.
+                if (mAudioFocus == AudioManager.AUDIOFOCUS_NONE) {
+                    sendAvrcpPause();
+                } else {
+                    startAvrcpUpdates();
+                }
+                break;
+
+            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;
-                            startAvrcpUpdates();
-                            startFluorideStreaming();
+                        // Begin playing audio, if we paused the remote, send a play now.
+                        startAvrcpUpdates();
+                        startFluorideStreaming();
+                        if (mSentPause) {
+                            sendAvrcpPlay();
+                            mSentPause = false;
                         }
                         break;
 
@@ -191,19 +178,24 @@
                             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 @@
      * 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 @@
             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.");
+        }
+    }
 }
diff --git a/tests/src/com/android/bluetooth/a2dpsink/A2dpSinkStreamHandlerTest.java b/tests/src/com/android/bluetooth/a2dpsink/A2dpSinkStreamHandlerTest.java
new file mode 100644
index 0000000..2f18960
--- /dev/null
+++ b/tests/src/com/android/bluetooth/a2dpsink/A2dpSinkStreamHandlerTest.java
@@ -0,0 +1,189 @@
+/*
+ * 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);
+    }
+}