Loading android/app/src/com/android/bluetooth/a2dpsink/A2dpSinkStateMachine.java +9 −4 Original line number Diff line number Diff line Loading @@ -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; Loading Loading @@ -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; Loading android/app/src/com/android/bluetooth/a2dpsink/A2dpSinkStreamHandler.java +86 −82 Original line number Diff line number Diff line Loading @@ -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 Loading @@ -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() { Loading @@ -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; Loading @@ -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; Loading @@ -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() { Loading Loading @@ -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."); } } } android/app/tests/src/com/android/bluetooth/a2dpsink/A2dpSinkStreamHandlerTest.java 0 → 100644 +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); } } Loading
android/app/src/com/android/bluetooth/a2dpsink/A2dpSinkStateMachine.java +9 −4 Original line number Diff line number Diff line Loading @@ -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; Loading Loading @@ -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; Loading
android/app/src/com/android/bluetooth/a2dpsink/A2dpSinkStreamHandler.java +86 −82 Original line number Diff line number Diff line Loading @@ -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 Loading @@ -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() { Loading @@ -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; Loading @@ -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; Loading @@ -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() { Loading Loading @@ -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."); } } }
android/app/tests/src/com/android/bluetooth/a2dpsink/A2dpSinkStreamHandlerTest.java 0 → 100644 +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); } }