blob: fb62c95cce327e8473e333742cb28fc58e42f62f [file] [log] [blame]
/*
* Copyright (C) 2016 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 android.bluetooth.BluetoothA2dpSink;
import android.bluetooth.BluetoothAvrcpController;
import android.bluetooth.BluetoothDevice;
import android.content.Context;
import android.media.AudioManager;
import android.media.AudioManager.OnAudioFocusChangeListener;
import android.os.Message;
import android.util.Log;
import com.android.bluetooth.avrcp.AvrcpControllerService;
import com.android.bluetooth.R;
import com.android.internal.util.IState;
import com.android.internal.util.State;
import com.android.internal.util.StateMachine;
/**
* Bluetooth A2DP SINK Streaming StateMachine.
*
* This state machine defines how the stack behaves once the A2DP connection is established and both
* devices are ready for streaming. For simplification we assume that the connection can either
* stream music immediately (i.e. data packets coming in or have potential to come in) or it cannot
* stream (i.e. Idle and Open states are treated alike). See Fig 4-1 of GAVDP Spec 1.0.
*
* Legend:
* SRC: Source (Remote)
* (SRC, F) - Remote is not streaming. F stands for false.
* (SRC, T) - Remote is intent to streaming. T stands for true.
* ACT: Action
* (ACT, F) - Local/Remote user intent is to pause/stop (AVRCP pause/stop).
* (ACT, T) - Local/Remote user intent is to play (AVRCP play).
* The way to detect action is two fold:
* -- We can detect action on the SNK side by directly monitoring the AVRCP controller service.
* -- On the SRC side, any AVRCP action will be accompanied by an update via AVRCP and hence we can
* update our action state.
*
* A state will be a combination of SRC and ACT state. Hence a state such as:
* (F, T) will mean that user has shown intent to play on local or remote device (second T) but the
* connection is not in streaming state yet.
*
* -----------------------------------------------------------------------------------------------
* Start State | End State | Transition(s)
* -----------------------------------------------------------------------------------------------
* (F, F) (F, T) ACT Play (No streaming in either states)
* (F, F) (T, F) Remote streams (play depends on policy)
* (T, F) (F, F) Remote stops streaming.
* (T, F) (T, T) ACT Play (streaming already existed).
* (F, T) (F, F) ACT Pause.
* (F, T) (T, T) Remote starts streaming (ACT Play already existed)
* (T, T) (F, T) Remote stops streaming.
* (T, T) (F, F) ACT stop.
* (T, T) (T, F) ACT pause.
*
* -----------------------------------------------------------------------------------------------
* State | Action(s)
* -----------------------------------------------------------------------------------------------
* (F, F) 1. Lose audio focus (if it exists) and notify fluoride of audio focus loss.
* 2. Stop AVRCP from pushing updates to UI.
* (T, F) 1. If policy is opt-in then get focus and stream (get audio focus etc).
* 2. Else throw away the data (lose audio focus etc).
* (F, T) In this state the source does not stream although we have play intent.
* 1. Show a spinny that data will come through.
* (T, T) 1. Request Audio Focus and on success update AVRCP to show UI updates.
* 2. On Audio focus enable streaming in Fluoride.
*/
final class A2dpSinkStreamingStateMachine extends StateMachine {
private static final boolean DBG = true;
private static final String TAG = "A2dpSinkStreamingStateMachine";
private static final int ACT_PLAY_NUM_RETRIES = 5;
private static final int ACT_PLAY_RETRY_DELAY = 2000; // millis.
private static final int DEFAULT_DUCK_PERCENT = 25;
// Streaming states (see the description above).
private SRC_F_ACT_F mSrcFActF;
private SRC_F_ACT_T mSrcFActT;
private SRC_T_ACT_F mSrcTActF;
private SRC_T_ACT_T mSrcTActT;
// Transitions.
public static final int SRC_STR_START = 0;
public static final int SRC_STR_STOP = 1;
public static final int SRC_STR_STOP_JITTER_WAIT_OVER = 2;
public static final int ACT_PLAY = 3;
public static final int ACT_PLAY_RETRY = 4;
public static final int ACT_PAUSE = 5;
public static final int AUDIO_FOCUS_CHANGE = 6;
public static final int DISCONNECT = 7;
// Private variables.
private A2dpSinkStateMachine mA2dpSinkSm;
private Context mContext;
private AudioManager mAudioManager;
// Set default focus to loss since we have never requested it before.
private int mCurrentAudioFocus = AudioManager.AUDIOFOCUS_LOSS;
/* 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;
/* Wait in millis before the ACT loses focus on SRC jitter when streaming */
private static final int SRC_STR_JITTER_WAIT = 5 * 1000; // 5sec
/* Focus changes when we are currently holding focus (i.e. we're in SRC_T_ACT_T state). */
private OnAudioFocusChangeListener mAudioFocusListener = new OnAudioFocusChangeListener() {
public void onAudioFocusChange(int focusChange){
if (DBG) {
Log.d(TAG, "onAudioFocusChangeListener focuschange " + focusChange);
}
A2dpSinkStreamingStateMachine.this.sendMessage(AUDIO_FOCUS_CHANGE, focusChange);
}
};
private A2dpSinkStreamingStateMachine(A2dpSinkStateMachine a2dpSinkSm, Context context) {
super("A2dpSinkStreamingStateMachine");
mA2dpSinkSm = a2dpSinkSm;
mContext = context;
mAudioManager = (AudioManager) context.getSystemService(Context.AUDIO_SERVICE);
mSrcFActF = new SRC_F_ACT_F();
mSrcFActT = new SRC_F_ACT_T();
mSrcTActF = new SRC_T_ACT_F();
mSrcTActT = new SRC_T_ACT_T();
// States are independent of each other. We simply use transitionTo.
addState(mSrcFActF);
addState(mSrcFActT);
addState(mSrcTActF);
addState(mSrcTActT);
setInitialState(mSrcFActF);
}
public static A2dpSinkStreamingStateMachine make(
A2dpSinkStateMachine a2dpSinkSm, Context context) {
if (DBG) {
Log.d(TAG, "make");
}
A2dpSinkStreamingStateMachine a2dpStrStateMachine =
new A2dpSinkStreamingStateMachine(a2dpSinkSm, context);
a2dpStrStateMachine.start();
return a2dpStrStateMachine;
}
/**
* Utility functions that can be used by all states.
*/
private boolean requestAudioFocus() {
return (mAudioManager.requestAudioFocus(
mAudioFocusListener, AudioManager.STREAM_MUSIC, AudioManager.AUDIOFOCUS_GAIN) ==
AudioManager.AUDIOFOCUS_REQUEST_GRANTED);
}
private void startAvrcpUpdates() {
// Since AVRCP gets started after A2DP we may need to request it later in cycle.
AvrcpControllerService avrcpService = AvrcpControllerService.getAvrcpControllerService();
if (DBG) {
Log.d(TAG, "startAvrcpUpdates");
}
if (avrcpService != null && avrcpService.getConnectedDevices().size() == 1) {
avrcpService.startAvrcpUpdates();
} else {
Log.e(TAG, "startAvrcpUpdates failed because of connection.");
}
}
private void stopAvrcpUpdates() {
// Since AVRCP gets started after A2DP we may need to request it later in cycle.
AvrcpControllerService avrcpService = AvrcpControllerService.getAvrcpControllerService();
if (DBG) {
Log.d(TAG, "stopAvrcpUpdates");
}
if (avrcpService != null && avrcpService.getConnectedDevices().size() == 1) {
avrcpService.stopAvrcpUpdates();
} else {
Log.e(TAG, "stopAvrcpUpdates failed because of connection.");
}
}
private void sendAvrcpPause() {
// Since AVRCP gets started after A2DP we may need to request it later in cycle.
AvrcpControllerService avrcpService = AvrcpControllerService.getAvrcpControllerService();
if (DBG) {
Log.d(TAG, "sendAvrcpPause");
}
if (avrcpService != null && avrcpService.getConnectedDevices().size() == 1) {
if (DBG) {
Log.d(TAG, "Pausing AVRCP.");
}
avrcpService.sendPassThroughCmd(
avrcpService.getConnectedDevices().get(0),
BluetoothAvrcpController.PASS_THRU_CMD_ID_PAUSE,
BluetoothAvrcpController.KEY_STATE_PRESSED);
avrcpService.sendPassThroughCmd(
avrcpService.getConnectedDevices().get(0),
BluetoothAvrcpController.PASS_THRU_CMD_ID_PAUSE,
BluetoothAvrcpController.KEY_STATE_RELEASED);
} else {
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),
BluetoothAvrcpController.PASS_THRU_CMD_ID_PLAY,
BluetoothAvrcpController.KEY_STATE_PRESSED);
avrcpService.sendPassThroughCmd(
avrcpService.getConnectedDevices().get(0),
BluetoothAvrcpController.PASS_THRU_CMD_ID_PLAY,
BluetoothAvrcpController.KEY_STATE_RELEASED);
} else {
Log.e(TAG, "Passthrough not sent, connection un-available.");
}
}
private void startFluorideStreaming() {
mA2dpSinkSm.informAudioFocusStateNative(STATE_FOCUS_GRANTED);
mA2dpSinkSm.informAudioTrackGainNative(1.0f);
}
private void stopFluorideStreaming() {
mA2dpSinkSm.informAudioFocusStateNative(STATE_FOCUS_LOST);
}
private void setFluorideAudioTrackGain(float gain) {
mA2dpSinkSm.informAudioTrackGainNative(gain);
}
private class SRC_F_ACT_F extends State {
private static final String STATE_TAG = A2dpSinkStreamingStateMachine.TAG + ".SRC_F_ACT_F";
@Override
public void enter() {
if (DBG) {
Log.d(STATE_TAG, "Enter: " + getCurrentMessage().what);
}
}
@Override
public boolean processMessage(Message message) {
if (DBG) {
Log.d(STATE_TAG, " process message: " + message.what);
}
switch (message.what) {
case SRC_STR_START:
// Opt out of all sounds without AVRCP play. We simply throw away.
transitionTo(mSrcTActF);
break;
case ACT_PLAY:
// Wait in next state for actual playback. We defer the message so that the next
// state (SRC_F_ACT_T) can execute the retry logic.
deferMessage(message);
transitionTo(mSrcFActT);
break;
case DISCONNECT:
mAudioManager.abandonAudioFocus(mAudioFocusListener);
mCurrentAudioFocus = AudioManager.AUDIOFOCUS_LOSS;
break;
case AUDIO_FOCUS_CHANGE:
// If we are regaining focus after transient loss this indicates that we should
// press play again.
int newAudioFocus = message.arg1;
if (DBG) {
Log.d(STATE_TAG,
"prev focus " + mCurrentAudioFocus + " new focus " + newAudioFocus);
}
if (mCurrentAudioFocus == AudioManager.AUDIOFOCUS_LOSS_TRANSIENT &&
newAudioFocus == AudioManager.AUDIOFOCUS_GAIN) {
sendAvrcpPlay();
// We should transition to SRC_F_ACT_T after this message. We also send some
// retries here because after phone calls we may have race conditions.
sendMessageDelayed(
ACT_PLAY_RETRY, ACT_PLAY_NUM_RETRIES, ACT_PLAY_RETRY_DELAY);
}
mCurrentAudioFocus = newAudioFocus;
break;
default:
Log.e(TAG, "Don't know how to handle " + message.what);
}
return HANDLED;
}
}
private class SRC_F_ACT_T extends State {
private static final String STATE_TAG = A2dpSinkStreamingStateMachine.TAG + ".SRC_F_ACT_T";
private boolean mPlay = false;
@Override
public void enter() {
if (DBG) {
Log.d(STATE_TAG, "Enter: " + getCurrentMessage().what);
}
}
@Override
public boolean processMessage(Message message) {
if (DBG) {
Log.d(STATE_TAG, " process message: " + message.what);
}
switch (message.what) {
case SRC_STR_START:
deferMessage(message);
transitionTo(mSrcTActT);
break;
case ACT_PAUSE:
transitionTo(mSrcFActF);
break;
case ACT_PLAY:
// Retry if the remote has not yet started playing music. This is seen in some
// devices where after the phone call it requires multiple play commands to
// start music.
break;
case ACT_PLAY_RETRY:
if (message.arg1 > 0) {
Log.d(STATE_TAG, "Retry " + message.arg1);
sendAvrcpPlay();
sendMessageDelayed(ACT_PLAY_RETRY, message.arg1 - 1, ACT_PLAY_RETRY_DELAY);
}
break;
case DISCONNECT:
deferMessage(message);
transitionTo(mSrcFActF);
mPlay = false;
break;
case AUDIO_FOCUS_CHANGE:
int newAudioFocus = message.arg1;
if (DBG) {
Log.d(STATE_TAG,
"prev focus " + mCurrentAudioFocus + " new focus " + newAudioFocus);
}
if (newAudioFocus == AudioManager.AUDIOFOCUS_GAIN) {
sendAvrcpPlay();
mCurrentAudioFocus = newAudioFocus;
} else if (newAudioFocus == AudioManager.AUDIOFOCUS_LOSS_TRANSIENT) {
sendAvrcpPause();
mCurrentAudioFocus = newAudioFocus;
} else if (newAudioFocus == AudioManager.AUDIOFOCUS_LOSS) {
mAudioManager.abandonAudioFocus(mAudioFocusListener);
mCurrentAudioFocus = AudioManager.AUDIOFOCUS_LOSS;
}
break;
default:
Log.e(TAG, "Don't know how to handle " + message.what);
}
return HANDLED;
}
}
private class SRC_T_ACT_F extends State {
private static final String STATE_TAG = A2dpSinkStreamingStateMachine.TAG + ".SRC_T_ACT_F";
@Override
public void enter() {
if (DBG) {
Log.d(STATE_TAG, "Enter: " + getCurrentMessage().what);
}
}
@Override
public boolean processMessage(Message message) {
if (DBG) {
Log.d(STATE_TAG, " process message: " + message.what);
}
switch (message.what) {
case SRC_STR_STOP:
transitionTo(mSrcFActF);
break;
case ACT_PLAY:
deferMessage(message);
transitionTo(mSrcTActT);
break;
case DISCONNECT:
deferMessage(message);
transitionTo(mSrcFActF);
break;
case AUDIO_FOCUS_CHANGE:
// If we regain focus from TRANSIENT that means that the remote was playing all
// this while although we must have sent a PAUSE (see focus loss in SRC_T_ACT_T
// state). In any case, we should resume music here if that is the case.
int newAudioFocus = message.arg1;
if (DBG) {
Log.d(STATE_TAG,
"prev focus " + mCurrentAudioFocus + " new focus " + newAudioFocus);
}
if (newAudioFocus == AudioManager.AUDIOFOCUS_LOSS ||
newAudioFocus == AudioManager.AUDIOFOCUS_LOSS_TRANSIENT) {
sendAvrcpPause();
} else if (newAudioFocus == AudioManager.AUDIOFOCUS_GAIN) {
sendAvrcpPlay();
}
mCurrentAudioFocus = newAudioFocus;
break;
default:
Log.e(TAG, "Don't know how to handle " + message.what);
}
return HANDLED;
}
}
private class SRC_T_ACT_T extends State {
private static final String STATE_TAG = A2dpSinkStreamingStateMachine.TAG + ".SRC_T_ACT_T";
private boolean mWaitForJitter = false;
@Override
public void enter() {
if (DBG) {
Log.d(STATE_TAG, "Enter: " + getCurrentMessage().what);
}
}
@Override
public boolean processMessage(Message message) {
if (DBG) {
Log.d(STATE_TAG, " process message: " + message.what);
}
switch (message.what) {
case ACT_PAUSE:
// Stop avrcp updates.
stopAvrcpUpdates();
stopFluorideStreaming();
transitionTo(mSrcTActF);
if (mCurrentAudioFocus == AudioManager.AUDIOFOCUS_GAIN) {
// If we have focus gain and we still get pause that means that we must have
// gotten a PAUSE by user explicitly pressing PAUSE on Car or Phone. Hence
// we release focus.
mAudioManager.abandonAudioFocus(mAudioFocusListener);
mCurrentAudioFocus = AudioManager.AUDIOFOCUS_LOSS;
}
break;
case SRC_STR_STOP:
stopAvrcpUpdates();
stopFluorideStreaming();
transitionTo(mSrcFActT);
// This could be variety of reasons including that the remote is going to
// (eventually send us pause) or the device is going to go into a call state
// etc. Also it may simply be stutter of music. Instead of sending pause
// prematurely we wait for either a Pause from remote or AudioFocus change owing
// an ongoing call.
break;
case SRC_STR_START:
case ACT_PLAY:
Log.d(STATE_TAG, "Current Audio Focus " + mCurrentAudioFocus);
boolean startStream = true;
if (mCurrentAudioFocus == AudioManager.AUDIOFOCUS_LOSS) {
if (!requestAudioFocus()) {
Log.e(STATE_TAG, "Cannot get focus, hence not starting streaming.");
startStream = false;
} else {
mCurrentAudioFocus = AudioManager.AUDIOFOCUS_GAIN;
}
}
if (startStream) {
startAvrcpUpdates();
startFluorideStreaming();
}
// If we did not get focus, it may mean that the device in a call state and
// hence we should wait for an audio focus event.
break;
// On Audio Focus events we stay in the same state but this can potentially change
// if we playback.
case AUDIO_FOCUS_CHANGE:
int newAudioFocus = (int) message.arg1;
if (DBG) {
Log.d(STATE_TAG,
"prev focus " + mCurrentAudioFocus + " new focus " + newAudioFocus);
}
if (newAudioFocus == AudioManager.AUDIOFOCUS_GAIN) {
// We have gained focus so play with 1.0 gain.
sendAvrcpPlay();
startAvrcpUpdates();
startFluorideStreaming();
setFluorideAudioTrackGain(1.0f);
} else if (newAudioFocus == AudioManager.AUDIOFOCUS_LOSS_TRANSIENT_CAN_DUCK) {
// Make the volume duck.
int duckPercent =
mContext.getResources().getInteger(R.integer.a2dp_sink_duck_percent);
if (duckPercent < 0 || duckPercent > 100) {
Log.e(STATE_TAG, "Invalid duck percent using default.");
duckPercent = DEFAULT_DUCK_PERCENT;
}
float duckRatio = (float) ((duckPercent * 1.0f) / 100);
Log.d(STATE_TAG,
"Setting reduce gain on transient loss gain=" + duckRatio);
setFluorideAudioTrackGain(duckRatio);
} else {
// We either are in transient loss or we are in permanent loss,
// either ways we should stop streaming.
sendAvrcpPause();
stopAvrcpUpdates();
stopFluorideStreaming();
// If it is permanent focus loss then we should abandon focus here and wait
// for user to explicitly play again.
if (newAudioFocus == AudioManager.AUDIOFOCUS_LOSS) {
mAudioManager.abandonAudioFocus(mAudioFocusListener);
mCurrentAudioFocus = AudioManager.AUDIOFOCUS_LOSS;
}
}
mCurrentAudioFocus = newAudioFocus;
break;
case DISCONNECT:
deferMessage(message);
transitionTo(mSrcFActF);
break;
default:
Log.e(TAG, "Don't know how to handle " + message.what);
}
return HANDLED;
}
}
}