blob: 29b8bc8597d662b2bca204bdae76f5caa5298e32 [file] [log] [blame]
/*
* Copyright (C) 2014 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.
*/
/**
* Bluetooth A2dp StateMachine
* (Disconnected)
* | ^
* CONNECT | | DISCONNECTED
* V |
* (Pending)
* | ^
* CONNECTED | | CONNECT
* V |
* (Connected)
*/
package com.android.bluetooth.a2dp;
import android.bluetooth.BluetoothA2dpSink;
import android.bluetooth.BluetoothAvrcpController;
import android.bluetooth.BluetoothAdapter;
import android.bluetooth.BluetoothAudioConfig;
import android.bluetooth.BluetoothDevice;
import android.bluetooth.BluetoothProfile;
import android.bluetooth.BluetoothUuid;
import android.bluetooth.IBluetooth;
import android.content.BroadcastReceiver;
import android.content.Context;
import android.media.AudioFormat;
import android.media.AudioManager;
import android.media.AudioManager.OnAudioFocusChangeListener;
import android.media.AudioDevicePort;
import android.media.AudioPatch;
import android.media.AudioSystem;
import android.media.AudioPortConfig;
import android.media.AudioPort;
import android.os.Handler;
import android.os.Message;
import android.os.ParcelUuid;
import android.os.PowerManager;
import android.os.PowerManager.WakeLock;
import android.content.Intent;
import android.os.Message;
import android.os.RemoteException;
import android.os.ServiceManager;
import android.os.ParcelUuid;
import android.util.Log;
import com.android.bluetooth.Utils;
import com.android.bluetooth.btservice.AdapterService;
import com.android.bluetooth.btservice.ProfileService;
import com.android.bluetooth.avrcp.AvrcpControllerService;
import com.android.internal.util.IState;
import com.android.internal.util.State;
import com.android.internal.util.StateMachine;
import java.util.ArrayList;
import java.util.List;
import java.util.HashMap;
import java.util.Set;
final class A2dpSinkStateMachine extends StateMachine {
private static final boolean DBG = true;
static final int CONNECT = 1;
static final int DISCONNECT = 2;
final private static int EVENT_TYPE_REQUEST_AUDIO_FOCUS = 4;
private static final int STACK_EVENT = 101;
private static final int CONNECT_TIMEOUT = 201;
private static final int IS_INVALID_DEVICE = 0;
private static final int IS_VALID_DEVICE = 1;
public static final int AVRC_ID_PLAY = 0x44;
public static final int AVRC_ID_PAUSE = 0x46;
public static final int KEY_STATE_PRESSED = 0;
public static final int KEY_STATE_RELEASED = 1;
private Disconnected mDisconnected;
private Pending mPending;
private Connected mConnected;
private A2dpSinkService mService;
private Context mContext;
private BluetoothAdapter mAdapter;
private final AudioManager mAudioManager;
private IntentBroadcastHandler mIntentBroadcastHandler;
private final WakeLock mWakeLock;
private static final int MSG_CONNECTION_STATE_CHANGED = 0;
private static final int AUDIO_FOCUS_LOSS = 0;
private static final int AUDIO_FOCUS_GAIN = 1;
private static final int AUDIO_FOCUS_LOSS_TRANSIENT = 2;
private static final int AUDIO_FOCUS_LOSS_CAN_DUCK = 3;
private static final int AUDIO_FOCUS_REQUEST_MESSAGE_DELAYED = 500;
private static final boolean USE_AUDIOTRACK = true;
private static final boolean BLOCK_REMOTE_INITIATED_STREAMING = true;
private static boolean mAvrcpPlaySent = false;
private int mAudioFocusAcquired = AUDIO_FOCUS_LOSS;
/* Used to indicate focus lost */
private static final int STATE_FOCUS_LOST = 0;
/* Used to inform bluedroid about AVRCP State changes */
private static final int STATE_FOCUS_READY = 1;
/* Used to inform bluedroid that focus is granted */
private static final int STATE_FOCUS_GRANTED = 3;
private final Object mLockForPatch = new Object();
private AudioDevicePort mInPortA2dpSink;
private AudioDevicePort mOutPortSpeaker;
private AudioPatch mA2dpSinkAudioPatch;
// mCurrentDevice is the device connected before the state changes
// mTargetDevice is the device to be connected
// mIncomingDevice is the device connecting to us, valid only in Pending state
// when mIncomingDevice is not null, both mCurrentDevice
// and mTargetDevice are null
// when either mCurrentDevice or mTargetDevice is not null,
// mIncomingDevice is null
// Stable states
// No connection, Disconnected state
// both mCurrentDevice and mTargetDevice are null
// Connected, Connected state
// mCurrentDevice is not null, mTargetDevice is null
// Interim states
// Connecting to a device, Pending
// mCurrentDevice is null, mTargetDevice is not null
// Disconnecting device, Connecting to new device
// Pending
// Both mCurrentDevice and mTargetDevice are not null
// Disconnecting device Pending
// mCurrentDevice is not null, mTargetDevice is null
// Incoming connections Pending
// Both mCurrentDevice and mTargetDevice are null
private BluetoothDevice mCurrentDevice = null;
private BluetoothDevice mTargetDevice = null;
private BluetoothDevice mIncomingDevice = null;
private BluetoothDevice mPlayingDevice = null;
private final HashMap<BluetoothDevice,BluetoothAudioConfig> mAudioConfigs
= new HashMap<BluetoothDevice,BluetoothAudioConfig>();
static {
classInitNative();
}
private A2dpSinkStateMachine(A2dpSinkService svc, Context context) {
super("A2dpSinkStateMachine");
mService = svc;
mContext = context;
mAdapter = BluetoothAdapter.getDefaultAdapter();
initNative();
mDisconnected = new Disconnected();
mPending = new Pending();
mConnected = new Connected();
addState(mDisconnected);
addState(mPending);
addState(mConnected);
setInitialState(mDisconnected);
PowerManager pm = (PowerManager)context.getSystemService(Context.POWER_SERVICE);
mWakeLock = pm.newWakeLock(PowerManager.PARTIAL_WAKE_LOCK, "BluetoothA2dpSinkService");
mIntentBroadcastHandler = new IntentBroadcastHandler();
mAudioManager = (AudioManager) context.getSystemService(Context.AUDIO_SERVICE);
if(!USE_AUDIOTRACK)
mAudioManager.registerAudioPortUpdateListener(mAudioPortListener);
}
private void releasePatch() {
if(USE_AUDIOTRACK) {
log(" releasePatch: AudioTrack, inform focus loss");
informAudioFocusStateNative(STATE_FOCUS_LOST);
return;
}
synchronized (mLockForPatch){
log("releaseAudioPatch AudioPatch = " + mA2dpSinkAudioPatch);
if(mA2dpSinkAudioPatch != null) {
mAudioManager.releaseAudioPatch(mA2dpSinkAudioPatch);
mA2dpSinkAudioPatch = null;
}
}
}
private void patchPorts() {
/* If we are using AudioTrack, we need to broadcast and inform bluedroid from here */
if(USE_AUDIOTRACK){
log(" patchPorts: AudioTrack, inform focus gain");
broadcastAudioState(mPlayingDevice, BluetoothA2dpSink.STATE_PLAYING,
BluetoothA2dpSink.STATE_NOT_PLAYING);
informAudioFocusStateNative(STATE_FOCUS_GRANTED);
return;
}
synchronized (mLockForPatch) {
log("patchPorts : mA2dpSinkAudioPatch: " + mA2dpSinkAudioPatch +
" mPlayingDevice " + mPlayingDevice + "mOutPortSpeaker" + mOutPortSpeaker);
if((mA2dpSinkAudioPatch == null) && (mPlayingDevice != null) &&
(mOutPortSpeaker != null) && (mInPortA2dpSink != null)) {
if((mAudioConfigs == null)||(!mAudioConfigs.containsKey(mPlayingDevice))) {
log(" AudioConfigs not yet received, returning");
return;
}
int sampleRate = getAudioConfig(mPlayingDevice).getSampleRate();
int channelMask = getAudioConfig(mPlayingDevice).getChannelConfig();
int format = getAudioConfig(mPlayingDevice).getAudioFormat();
AudioPortConfig sourcePortArray[] =
{mInPortA2dpSink.buildConfig(sampleRate, channelMask, format, null)};
AudioPortConfig sinkPortArray[] =
{mOutPortSpeaker.buildConfig(sampleRate, channelMask, format, null)};
AudioPatch patchPortArray[] = {null};
/* broadCast Audio State */
broadcastAudioState(mPlayingDevice, BluetoothA2dpSink.STATE_PLAYING,
BluetoothA2dpSink.STATE_NOT_PLAYING);
int ret = mAudioManager.createAudioPatch(patchPortArray, sourcePortArray,
sinkPortArray);
if (ret == 0) {
mA2dpSinkAudioPatch = patchPortArray[0];
log("PatchCreated success: " + ret + " mA2dpSinkAudioPatch: "
+ mA2dpSinkAudioPatch);
} else {
log("PatchCreated failed returned: " + ret);
}
}
}
}
private final AudioManager.OnAudioPortUpdateListener mAudioPortListener =
new AudioManager.OnAudioPortUpdateListener(){
public void onAudioPortListUpdate(AudioPort[] portList) {
synchronized (mLockForPatch){
log("onAudioPortListUpdate");
mOutPortSpeaker = null;
mInPortA2dpSink = null;
for (int i = 0; i < portList.length; i++) {
AudioPort port = portList[i];
if(port instanceof AudioDevicePort) {
AudioDevicePort devicePort = (AudioDevicePort)port;
if(devicePort.type() == AudioSystem.DEVICE_OUT_SPEAKER) {
log("Updating Speaker Port");
mOutPortSpeaker = devicePort;
} else if(devicePort.type() == AudioSystem.DEVICE_IN_BLUETOOTH_A2DP) {
log("Updating In Port A2DP Sink");
mInPortA2dpSink = devicePort;
/* Check if we still have focus */
if ((mAudioFocusAcquired == AUDIO_FOCUS_GAIN) &&
(mInPortA2dpSink != null) && (mA2dpSinkAudioPatch == null)) {
/* This is the case of Port available
* later than focus acquired. Try patching ports now
*/
log(" Sink Port updated, but patch not made");
patchPorts();
}
}
}
}
}
}
public void onAudioPatchListUpdate(AudioPatch[] patchList){
log("onAudioPatchListUpdate");
for(int i = 0; i < patchList.length; i++) {
log("Patch List " + i +" : "+ patchList[i]);
}
}
public void onServiceDied() {
log(" Service Died");
}
};
static A2dpSinkStateMachine make(A2dpSinkService svc, Context context) {
Log.d("A2dpSinkStateMachine", "make");
A2dpSinkStateMachine a2dpSm = new A2dpSinkStateMachine(svc, context);
a2dpSm.start();
return a2dpSm;
}
public void doQuit() {
quitNow();
}
public void cleanup() {
/*
* remove cleanup of resources
* not acquired in AudioTrack approach
*/
if(!USE_AUDIOTRACK)
releasePatch();
cleanupNative();
mAudioConfigs.clear();
if(USE_AUDIOTRACK) return;
if (mContext != null)
mContext.unregisterReceiver(mA2dpReceiver);
mAudioManager.unregisterAudioPortUpdateListener(mAudioPortListener);
}
public void dump(StringBuilder sb) {
ProfileService.println(sb, "mCurrentDevice: " + mCurrentDevice);
ProfileService.println(sb, "mTargetDevice: " + mTargetDevice);
ProfileService.println(sb, "mIncomingDevice: " + mIncomingDevice);
ProfileService.println(sb, "StateMachine: " + this.toString());
}
private class Disconnected extends State {
@Override
public void enter() {
log("Enter Disconnected: " + getCurrentMessage().what);
/*
* Remove audio focus request when not in connected state
*/
removeMessages(EVENT_TYPE_REQUEST_AUDIO_FOCUS);
mAvrcpPlaySent = false;
}
@Override
public boolean processMessage(Message message) {
log("Disconnected process message: " + message.what);
if (mCurrentDevice != null || mTargetDevice != null || mIncomingDevice != null) {
loge("ERROR: current, target, or mIncomingDevice not null in Disconnected");
return NOT_HANDLED;
}
boolean retValue = HANDLED;
switch(message.what) {
case CONNECT:
BluetoothDevice device = (BluetoothDevice) message.obj;
broadcastConnectionState(device, BluetoothProfile.STATE_CONNECTING,
BluetoothProfile.STATE_DISCONNECTED);
if (!connectA2dpNative(getByteAddress(device)) ) {
broadcastConnectionState(device, BluetoothProfile.STATE_DISCONNECTED,
BluetoothProfile.STATE_CONNECTING);
break;
}
synchronized (A2dpSinkStateMachine.this) {
mTargetDevice = device;
transitionTo(mPending);
}
// TODO(BT) remove CONNECT_TIMEOUT when the stack
// sends back events consistently
sendMessageDelayed(CONNECT_TIMEOUT, 30000);
break;
case DISCONNECT:
// ignore
break;
case STACK_EVENT:
StackEvent event = (StackEvent) message.obj;
switch (event.type) {
case EVENT_TYPE_CONNECTION_STATE_CHANGED:
processConnectionEvent(event.valueInt, event.device);
break;
case EVENT_TYPE_AUDIO_CONFIG_CHANGED:
processAudioConfigEvent(event.audioConfig, event.device);
break;
default:
loge("Unexpected stack event: " + event.type);
break;
}
break;
default:
return NOT_HANDLED;
}
return retValue;
}
@Override
public void exit() {
log("Exit Disconnected: " + getCurrentMessage().what);
}
// in Disconnected state
private void processConnectionEvent(int state, BluetoothDevice device) {
switch (state) {
case CONNECTION_STATE_DISCONNECTED:
logw("Ignore HF DISCONNECTED event, device: " + device);
break;
case CONNECTION_STATE_CONNECTING:
if (okToConnect(device)){
logi("Incoming A2DP accepted");
broadcastConnectionState(device, BluetoothProfile.STATE_CONNECTING,
BluetoothProfile.STATE_DISCONNECTED);
synchronized (A2dpSinkStateMachine.this) {
mIncomingDevice = device;
transitionTo(mPending);
}
} else {
//reject the connection and stay in Disconnected state itself
logi("Incoming A2DP rejected");
disconnectA2dpNative(getByteAddress(device));
// the other profile connection should be initiated
AdapterService adapterService = AdapterService.getAdapterService();
if (adapterService != null) {
adapterService.connectOtherProfile(device,
AdapterService.PROFILE_CONN_REJECTED);
}
}
break;
case CONNECTION_STATE_CONNECTED:
logw("A2DP Connected from Disconnected state");
if (okToConnect(device)){
logi("Incoming A2DP accepted");
broadcastConnectionState(device, BluetoothProfile.STATE_CONNECTED,
BluetoothProfile.STATE_DISCONNECTED);
synchronized (A2dpSinkStateMachine.this) {
mCurrentDevice = device;
transitionTo(mConnected);
}
} else {
//reject the connection and stay in Disconnected state itself
logi("Incoming A2DP rejected");
disconnectA2dpNative(getByteAddress(device));
// the other profile connection should be initiated
AdapterService adapterService = AdapterService.getAdapterService();
if (adapterService != null) {
adapterService.connectOtherProfile(device,
AdapterService.PROFILE_CONN_REJECTED);
}
}
break;
case CONNECTION_STATE_DISCONNECTING:
logw("Ignore HF DISCONNECTING event, device: " + device);
break;
default:
loge("Incorrect state: " + state);
break;
}
}
}
private class Pending extends State {
@Override
public void enter() {
log("Enter Pending: " + getCurrentMessage().what);
/*
* Remove audio focus request when not in connected state
*/
removeMessages(EVENT_TYPE_REQUEST_AUDIO_FOCUS);
mAvrcpPlaySent = false;
}
@Override
public boolean processMessage(Message message) {
log("Pending process message: " + message.what);
boolean retValue = HANDLED;
switch(message.what) {
case CONNECT:
deferMessage(message);
break;
case CONNECT_TIMEOUT:
onConnectionStateChanged(CONNECTION_STATE_DISCONNECTED,
getByteAddress(mTargetDevice));
break;
case DISCONNECT:
BluetoothDevice device = (BluetoothDevice) message.obj;
if (mCurrentDevice != null && mTargetDevice != null &&
mTargetDevice.equals(device) ) {
// cancel connection to the mTargetDevice
broadcastConnectionState(device, BluetoothProfile.STATE_DISCONNECTED,
BluetoothProfile.STATE_CONNECTING);
synchronized (A2dpSinkStateMachine.this) {
mTargetDevice = null;
}
} else {
deferMessage(message);
}
break;
case STACK_EVENT:
StackEvent event = (StackEvent) message.obj;
switch (event.type) {
case EVENT_TYPE_CONNECTION_STATE_CHANGED:
removeMessages(CONNECT_TIMEOUT);
processConnectionEvent(event.valueInt, event.device);
break;
case EVENT_TYPE_AUDIO_CONFIG_CHANGED:
processAudioConfigEvent(event.audioConfig, event.device);
break;
default:
loge("Unexpected stack event: " + event.type);
break;
}
break;
default:
return NOT_HANDLED;
}
return retValue;
}
// in Pending state
private void processConnectionEvent(int state, BluetoothDevice device) {
switch (state) {
case CONNECTION_STATE_DISCONNECTED:
mAudioConfigs.remove(device);
if ((mCurrentDevice != null) && mCurrentDevice.equals(device)) {
broadcastConnectionState(mCurrentDevice,
BluetoothProfile.STATE_DISCONNECTED,
BluetoothProfile.STATE_DISCONNECTING);
synchronized (A2dpSinkStateMachine.this) {
mCurrentDevice = null;
}
if (mTargetDevice != null) {
if (!connectA2dpNative(getByteAddress(mTargetDevice))) {
broadcastConnectionState(mTargetDevice,
BluetoothProfile.STATE_DISCONNECTED,
BluetoothProfile.STATE_CONNECTING);
synchronized (A2dpSinkStateMachine.this) {
mTargetDevice = null;
transitionTo(mDisconnected);
}
}
} else {
synchronized (A2dpSinkStateMachine.this) {
mIncomingDevice = null;
transitionTo(mDisconnected);
}
}
} else if (mTargetDevice != null && mTargetDevice.equals(device)) {
// outgoing connection failed
broadcastConnectionState(mTargetDevice, BluetoothProfile.STATE_DISCONNECTED,
BluetoothProfile.STATE_CONNECTING);
synchronized (A2dpSinkStateMachine.this) {
mTargetDevice = null;
transitionTo(mDisconnected);
}
} else if (mIncomingDevice != null && mIncomingDevice.equals(device)) {
broadcastConnectionState(mIncomingDevice,
BluetoothProfile.STATE_DISCONNECTED,
BluetoothProfile.STATE_CONNECTING);
synchronized (A2dpSinkStateMachine.this) {
mIncomingDevice = null;
transitionTo(mDisconnected);
}
} else {
loge("Unknown device Disconnected: " + device);
}
break;
case CONNECTION_STATE_CONNECTED:
if ((mCurrentDevice != null) && mCurrentDevice.equals(device)) {
// disconnection failed
broadcastConnectionState(mCurrentDevice, BluetoothProfile.STATE_CONNECTED,
BluetoothProfile.STATE_DISCONNECTING);
if (mTargetDevice != null) {
broadcastConnectionState(mTargetDevice, BluetoothProfile.STATE_DISCONNECTED,
BluetoothProfile.STATE_CONNECTING);
}
synchronized (A2dpSinkStateMachine.this) {
mTargetDevice = null;
transitionTo(mConnected);
}
} else if (mTargetDevice != null && mTargetDevice.equals(device)) {
broadcastConnectionState(mTargetDevice, BluetoothProfile.STATE_CONNECTED,
BluetoothProfile.STATE_CONNECTING);
synchronized (A2dpSinkStateMachine.this) {
mCurrentDevice = mTargetDevice;
mTargetDevice = null;
transitionTo(mConnected);
}
} else if (mIncomingDevice != null && mIncomingDevice.equals(device)) {
broadcastConnectionState(mIncomingDevice, BluetoothProfile.STATE_CONNECTED,
BluetoothProfile.STATE_CONNECTING);
synchronized (A2dpSinkStateMachine.this) {
mCurrentDevice = mIncomingDevice;
mIncomingDevice = null;
transitionTo(mConnected);
}
} else {
loge("Unknown device Connected: " + device);
// something is wrong here, but sync our state with stack
broadcastConnectionState(device, BluetoothProfile.STATE_CONNECTED,
BluetoothProfile.STATE_DISCONNECTED);
synchronized (A2dpSinkStateMachine.this) {
mCurrentDevice = device;
mTargetDevice = null;
mIncomingDevice = null;
transitionTo(mConnected);
}
}
break;
case CONNECTION_STATE_CONNECTING:
if ((mCurrentDevice != null) && mCurrentDevice.equals(device)) {
log("current device tries to connect back");
// TODO(BT) ignore or reject
} else if (mTargetDevice != null && mTargetDevice.equals(device)) {
// The stack is connecting to target device or
// there is an incoming connection from the target device at the same time
// we already broadcasted the intent, doing nothing here
log("Stack and target device are connecting");
}
else if (mIncomingDevice != null && mIncomingDevice.equals(device)) {
loge("Another connecting event on the incoming device");
} else {
// We get an incoming connecting request while Pending
// TODO(BT) is stack handing this case? let's ignore it for now
log("Incoming connection while pending, ignore");
}
break;
case CONNECTION_STATE_DISCONNECTING:
if ((mCurrentDevice != null) && mCurrentDevice.equals(device)) {
// we already broadcasted the intent, doing nothing here
if (DBG) {
log("stack is disconnecting mCurrentDevice");
}
} else if (mTargetDevice != null && mTargetDevice.equals(device)) {
loge("TargetDevice is getting disconnected");
} else if (mIncomingDevice != null && mIncomingDevice.equals(device)) {
loge("IncomingDevice is getting disconnected");
} else {
loge("Disconnecting unknown device: " + device);
}
break;
default:
loge("Incorrect state: " + state);
break;
}
}
}
private class Connected extends State {
@Override
public void enter() {
log("Enter Connected: " + getCurrentMessage().what);
// Upon connected, the audio starts out as stopped
broadcastAudioState(mCurrentDevice, BluetoothA2dpSink.STATE_NOT_PLAYING,
BluetoothA2dpSink.STATE_PLAYING);
mAvrcpPlaySent = false;
}
@Override
public boolean processMessage(Message message) {
log("Connected process message: " + message.what);
if (mCurrentDevice == null) {
loge("ERROR: mCurrentDevice is null in Connected");
return NOT_HANDLED;
}
boolean retValue = HANDLED;
switch(message.what) {
case CONNECT:
{
BluetoothDevice device = (BluetoothDevice) message.obj;
if (mCurrentDevice.equals(device)) {
break;
}
broadcastConnectionState(device, BluetoothProfile.STATE_CONNECTING,
BluetoothProfile.STATE_DISCONNECTED);
if (!disconnectA2dpNative(getByteAddress(mCurrentDevice))) {
broadcastConnectionState(device, BluetoothProfile.STATE_DISCONNECTED,
BluetoothProfile.STATE_CONNECTING);
break;
}
synchronized (A2dpSinkStateMachine.this) {
mTargetDevice = device;
transitionTo(mPending);
}
}
break;
case DISCONNECT:
{
BluetoothDevice device = (BluetoothDevice) message.obj;
if (!mCurrentDevice.equals(device)) {
break;
}
broadcastConnectionState(device, BluetoothProfile.STATE_DISCONNECTING,
BluetoothProfile.STATE_CONNECTED);
if (!disconnectA2dpNative(getByteAddress(device))) {
broadcastConnectionState(device, BluetoothProfile.STATE_CONNECTED,
BluetoothProfile.STATE_DISCONNECTED);
break;
}
if (mAudioFocusAcquired != AUDIO_FOCUS_LOSS) {
releasePatch();
int status = mAudioManager.abandonAudioFocus(mAudioFocusListener);
if (status == AudioManager.AUDIOFOCUS_REQUEST_GRANTED) {
mAudioFocusAcquired = AUDIO_FOCUS_LOSS;
}
}
mPlayingDevice = null;
transitionTo(mPending);
}
break;
case EVENT_TYPE_REQUEST_AUDIO_FOCUS:
processAudioFocusRequestEvent(1, (BluetoothDevice) message.obj);
break;
case STACK_EVENT:
StackEvent event = (StackEvent) message.obj;
switch (event.type) {
case EVENT_TYPE_CONNECTION_STATE_CHANGED:
processConnectionEvent(event.valueInt, event.device);
break;
case EVENT_TYPE_AUDIO_STATE_CHANGED:
processAudioStateEvent(event.valueInt, event.device);
break;
case EVENT_TYPE_AUDIO_CONFIG_CHANGED:
processAudioConfigEvent(event.audioConfig, event.device);
break;
default:
loge("Unexpected stack event: " + event.type);
break;
}
break;
default:
return NOT_HANDLED;
}
return retValue;
}
// in Connected state
private void processConnectionEvent(int state, BluetoothDevice device) {
switch (state) {
case CONNECTION_STATE_DISCONNECTED:
mAudioConfigs.remove(device);
if ((mPlayingDevice != null) && (device.equals(mPlayingDevice))) {
mPlayingDevice = null;
}
if (mCurrentDevice.equals(device)) {
broadcastConnectionState(mCurrentDevice, BluetoothProfile.STATE_DISCONNECTED,
BluetoothProfile.STATE_CONNECTED);
synchronized (A2dpSinkStateMachine.this) {
mCurrentDevice = null;
transitionTo(mDisconnected);
}
releasePatch();
int status = mAudioManager.abandonAudioFocus(mAudioFocusListener);
if (status == AudioManager.AUDIOFOCUS_REQUEST_GRANTED) {
mAudioFocusAcquired = AUDIO_FOCUS_LOSS;
/* Check if we need to release patch here
* Would we ever get disconnect directly without
* Suspend or Stoppped being called before */
}
} else {
loge("Disconnected from unknown device: " + device);
}
break;
default:
loge("Connection State Device: " + device + " bad state: " + state);
break;
}
}
private void processAudioStateEvent(int state, BluetoothDevice device) {
if (!mCurrentDevice.equals(device)) {
loge("Audio State Device:" + device + "is different from ConnectedDevice:" +
mCurrentDevice);
return;
}
log(" processAudioStateEvent in state " + state);
switch (state) {
case AUDIO_STATE_STARTED:
if (mPlayingDevice == null) {
mPlayingDevice = device;
}
if(!USE_AUDIOTRACK)
requestAudioFocus(true, device, 0);
break;
case AUDIO_STATE_REMOTE_SUSPEND:
case AUDIO_STATE_STOPPED:
mPlayingDevice = null;
releasePatch();
broadcastAudioState(device, BluetoothA2dpSink.STATE_NOT_PLAYING,
BluetoothA2dpSink.STATE_PLAYING);
if (mAudioFocusAcquired == AUDIO_FOCUS_LOSS) {
/* Audio Focus was already lost, no need to do it again */
return;
}
if ((mAudioFocusAcquired == AUDIO_FOCUS_LOSS_TRANSIENT) &&
(state == AUDIO_STATE_REMOTE_SUSPEND)) {
log(" Dont't Loose audiofocus in case of suspend ");
break;
}
int status = mAudioManager.abandonAudioFocus(mAudioFocusListener);
if (status == AudioManager.AUDIOFOCUS_REQUEST_GRANTED) {
mAudioFocusAcquired = AUDIO_FOCUS_LOSS;
}
break;
default:
loge("Audio State Device: " + device + " bad state: " + state);
break;
}
}
private void processAudioFocusRequestEvent(int enable, BluetoothDevice device) {
if ((mCurrentDevice != null) && (mCurrentDevice.equals(device))
&& (1 == enable)) {
if (BLOCK_REMOTE_INITIATED_STREAMING) {
/*
* If play command is not send from UI
* don't rquest focus.
*/
if (!mAvrcpPlaySent) {
log(" AVRCP Play not sent from UI, send Pause");
SendPassThruPause(device);
if(USE_AUDIOTRACK)
informAudioFocusStateNative(STATE_FOCUS_LOST);
/*
* TODO, any additional intent that we want to send
*/
return;
}
}
if (mAudioFocusAcquired == AUDIO_FOCUS_LOSS_TRANSIENT) {
log(" Transient Loss Still, Don't acquire focus, Send PAUSE");
SendPassThruPause(device);
if(USE_AUDIOTRACK)
informAudioFocusStateNative(STATE_FOCUS_LOST);
return;
}
else if (mAudioFocusAcquired == AUDIO_FOCUS_GAIN) {
patchPorts();
return; /* if we already have focus, don't request again */
}
int status = mAudioManager.requestAudioFocus(mAudioFocusListener,
AudioManager.STREAM_MUSIC,AudioManager.AUDIOFOCUS_GAIN);
log(" Audio Focus Request returned " + status);
if (status == AudioManager.AUDIOFOCUS_REQUEST_GRANTED) {
mAudioFocusAcquired = AUDIO_FOCUS_GAIN;
patchPorts();
}
else {
log("Can't acquire Focus, request with delay");
requestAudioFocus(true, device, AUDIO_FOCUS_REQUEST_MESSAGE_DELAYED);
}
}
}
}
private void processAudioConfigEvent(BluetoothAudioConfig audioConfig, BluetoothDevice device) {
log("processAudioConfigEvent: " + device);
mAudioConfigs.put(device, audioConfig);
broadcastAudioConfig(device, audioConfig);
}
int getConnectionState(BluetoothDevice device) {
if (getCurrentState() == mDisconnected) {
return BluetoothProfile.STATE_DISCONNECTED;
}
synchronized (this) {
IState currentState = getCurrentState();
if (currentState == mPending) {
if ((mTargetDevice != null) && mTargetDevice.equals(device)) {
return BluetoothProfile.STATE_CONNECTING;
}
if ((mCurrentDevice != null) && mCurrentDevice.equals(device)) {
return BluetoothProfile.STATE_DISCONNECTING;
}
if ((mIncomingDevice != null) && mIncomingDevice.equals(device)) {
return BluetoothProfile.STATE_CONNECTING; // incoming connection
}
return BluetoothProfile.STATE_DISCONNECTED;
}
if (currentState == mConnected) {
if (mCurrentDevice.equals(device)) {
return BluetoothProfile.STATE_CONNECTED;
}
return BluetoothProfile.STATE_DISCONNECTED;
} else {
loge("Bad currentState: " + currentState);
return BluetoothProfile.STATE_DISCONNECTED;
}
}
}
BluetoothAudioConfig getAudioConfig(BluetoothDevice device) {
return mAudioConfigs.get(device);
}
List<BluetoothDevice> getConnectedDevices() {
List<BluetoothDevice> devices = new ArrayList<BluetoothDevice>();
synchronized(this) {
if (getCurrentState() == mConnected) {
devices.add(mCurrentDevice);
}
}
return devices;
}
boolean isPlaying(BluetoothDevice device) {
synchronized(this) {
if ((mPlayingDevice != null) && (device.equals(mPlayingDevice))) {
return true;
}
}
return false;
}
void informAvrcpStatePlaying(BluetoothDevice device) {
if ((mPlayingDevice != null) && (device.equals(mPlayingDevice))) {
log(" AVRCP State is Playing");
/* If call is ongoing, Focus already acquired, no need to
* send this info to bluedroid
*/
/*
* If Flag is set, this info should not be used
* to request focus.
*/
if (BLOCK_REMOTE_INITIATED_STREAMING)
return;
if(mAudioFocusAcquired == AUDIO_FOCUS_LOSS_TRANSIENT)
SendPassThruPause(device);
if((USE_AUDIOTRACK) && (mAudioFocusAcquired == AUDIO_FOCUS_LOSS))
informAudioFocusStateNative(STATE_FOCUS_READY);
}
}
void informAvrcpPassThroughCmd(BluetoothDevice device, int keyCode,
int keyState) {
if (!BLOCK_REMOTE_INITIATED_STREAMING)
return;
log(" informAvrcpPassThroughCmd device: " + device + " key: " + keyCode
+ " keyState: " + keyState);
if ((keyCode == BluetoothAvrcpController.PASS_THRU_CMD_ID_PLAY) &&
(keyState == BluetoothAvrcpController.KEY_STATE_RELEASED)) {
mAvrcpPlaySent = true;
/*
* SO that if packets are received, we can request for focus
* from bluedroid
*/
informAudioFocusStateNative(STATE_FOCUS_READY);
}
if (((keyCode == BluetoothAvrcpController.PASS_THRU_CMD_ID_PAUSE)||
(keyCode == BluetoothAvrcpController.PASS_THRU_CMD_ID_STOP)) &&
(keyState == BluetoothAvrcpController.KEY_STATE_RELEASED)) {
mAvrcpPlaySent = false;
/*
* emulate a remote initiaited suspend.
*/
onAudioStateChanged(AUDIO_STATE_REMOTE_SUSPEND,
getByteAddress(device));
}
}
boolean okToConnect(BluetoothDevice device) {
AdapterService adapterService = AdapterService.getAdapterService();
boolean ret = true;
//check if this is an incoming connection in Quiet mode.
if((adapterService == null) ||
((adapterService.isQuietModeEnabled() == true) &&
(mTargetDevice == null))){
ret = false;
}
return ret;
}
synchronized List<BluetoothDevice> getDevicesMatchingConnectionStates(int[] states) {
List<BluetoothDevice> deviceList = new ArrayList<BluetoothDevice>();
Set<BluetoothDevice> bondedDevices = mAdapter.getBondedDevices();
int connectionState;
for (BluetoothDevice device : bondedDevices) {
ParcelUuid[] featureUuids = device.getUuids();
if (!BluetoothUuid.isUuidPresent(featureUuids, BluetoothUuid.AudioSource)) {
continue;
}
connectionState = getConnectionState(device);
for(int i = 0; i < states.length; i++) {
if (connectionState == states[i]) {
deviceList.add(device);
}
}
}
return deviceList;
}
// This method does not check for error conditon (newState == prevState)
private void broadcastConnectionState(BluetoothDevice device, int newState, int prevState) {
int delay = 0;
if(!USE_AUDIOTRACK)
delay = mAudioManager.setBluetoothA2dpDeviceConnectionState(device, newState,
BluetoothProfile.A2DP_SINK);
mWakeLock.acquire();
mIntentBroadcastHandler.sendMessageDelayed(mIntentBroadcastHandler.obtainMessage(
MSG_CONNECTION_STATE_CHANGED,
prevState,
newState,
device),
delay);
}
private void broadcastAudioState(BluetoothDevice device, int state, int prevState) {
Intent intent = new Intent(BluetoothA2dpSink.ACTION_PLAYING_STATE_CHANGED);
intent.putExtra(BluetoothDevice.EXTRA_DEVICE, device);
intent.putExtra(BluetoothProfile.EXTRA_PREVIOUS_STATE, prevState);
intent.putExtra(BluetoothProfile.EXTRA_STATE, state);
//FIXME intent.addFlags(Intent.FLAG_RECEIVER_REGISTERED_ONLY_BEFORE_BOOT);
mContext.sendBroadcast(intent, ProfileService.BLUETOOTH_PERM);
log("A2DP Playing state : device: " + device + " State:" + prevState + "->" + state);
}
private void broadcastAudioConfig(BluetoothDevice device, BluetoothAudioConfig audioConfig) {
Intent intent = new Intent(BluetoothA2dpSink.ACTION_AUDIO_CONFIG_CHANGED);
intent.putExtra(BluetoothDevice.EXTRA_DEVICE, device);
intent.putExtra(BluetoothA2dpSink.EXTRA_AUDIO_CONFIG, audioConfig);
//FIXME intent.addFlags(Intent.FLAG_RECEIVER_REGISTERED_ONLY_BEFORE_BOOT);
mContext.sendBroadcast(intent, ProfileService.BLUETOOTH_PERM);
log("A2DP Audio Config : device: " + device + " config: " + audioConfig);
}
private byte[] getByteAddress(BluetoothDevice device) {
return Utils.getBytesFromAddress(device.getAddress());
}
private void onConnectionStateChanged(int state, byte[] address) {
StackEvent event = new StackEvent(EVENT_TYPE_CONNECTION_STATE_CHANGED);
event.valueInt = state;
event.device = getDevice(address);
sendMessage(STACK_EVENT, event);
}
private void onAudioStateChanged(int state, byte[] address) {
StackEvent event = new StackEvent(EVENT_TYPE_AUDIO_STATE_CHANGED);
event.valueInt = state;
event.device = getDevice(address);
sendMessage(STACK_EVENT, event);
}
private void onAudioConfigChanged(byte[] address, int sampleRate, int channelCount) {
StackEvent event = new StackEvent(EVENT_TYPE_AUDIO_CONFIG_CHANGED);
int channelConfig = (channelCount == 1 ? AudioFormat.CHANNEL_IN_MONO
: AudioFormat.CHANNEL_IN_STEREO);
event.audioConfig = new BluetoothAudioConfig(sampleRate, channelConfig,
AudioFormat.ENCODING_PCM_16BIT);
event.device = getDevice(address);
sendMessage(STACK_EVENT, event);
}
private void onAudioFocusRequested(byte[] address) {
BluetoothDevice device = getDevice(address);
if (BLOCK_REMOTE_INITIATED_STREAMING) {
/*
* IN this case we mPlayingDevice can be null
*/
requestAudioFocus(true, device, 0);
}
else if(device.equals(mPlayingDevice))
requestAudioFocus(true, device, 0);
}
private void requestAudioFocus(boolean enable, BluetoothDevice device, int delay) {
log(" requestAudioFocus for " + device + " enable " + enable);
if (enable) {
// send a request for audio_focus
Message posMsg = obtainMessage(EVENT_TYPE_REQUEST_AUDIO_FOCUS, device);
sendMessageDelayed(posMsg, delay);
}
}
private BluetoothDevice getDevice(byte[] address) {
return mAdapter.getRemoteDevice(Utils.getAddressStringFromByte(address));
}
private class StackEvent {
int type = EVENT_TYPE_NONE;
int valueInt = 0;
BluetoothDevice device = null;
BluetoothAudioConfig audioConfig = null;
private StackEvent(int type) {
this.type = type;
}
}
/** Handles A2DP connection state change intent broadcasts. */
private class IntentBroadcastHandler extends Handler {
private void onConnectionStateChanged(BluetoothDevice device, int prevState, int state) {
Intent intent = new Intent(BluetoothA2dpSink.ACTION_CONNECTION_STATE_CHANGED);
intent.putExtra(BluetoothProfile.EXTRA_PREVIOUS_STATE, prevState);
intent.putExtra(BluetoothProfile.EXTRA_STATE, state);
intent.putExtra(BluetoothDevice.EXTRA_DEVICE, device);
//FIXME intent.addFlags(Intent.FLAG_RECEIVER_REGISTERED_ONLY_BEFORE_BOOT);
mContext.sendBroadcast(intent, ProfileService.BLUETOOTH_PERM);
log("Connection state " + device + ": " + prevState + "->" + state);
mService.notifyProfileConnectionStateChanged(device, BluetoothProfile.A2DP_SINK,
state, prevState);
}
@Override
public void handleMessage(Message msg) {
switch (msg.what) {
case MSG_CONNECTION_STATE_CHANGED:
onConnectionStateChanged((BluetoothDevice) msg.obj, msg.arg1, msg.arg2);
mWakeLock.release();
break;
}
}
}
public boolean SendPassThruPlay(BluetoothDevice mDevice) {
log("SendPassThruPlay + ");
AvrcpControllerService avrcpCtrlService = AvrcpControllerService.getAvrcpControllerService();
if ((avrcpCtrlService != null) && (mDevice != null) &&
(avrcpCtrlService.getConnectedDevices().contains(mDevice))){
avrcpCtrlService.sendPassThroughCmd(mDevice,
BluetoothAvrcpController.PASS_THRU_CMD_ID_PLAY,
BluetoothAvrcpController.KEY_STATE_PRESSED);
avrcpCtrlService.sendPassThroughCmd(mDevice,
BluetoothAvrcpController.PASS_THRU_CMD_ID_PLAY,
BluetoothAvrcpController.KEY_STATE_RELEASED);
log(" SendPassThruPlay command sent - ");
return true;
} else {
log("passthru command not sent, connection unavailable");
return false;
}
}
public boolean SendPassThruPause(BluetoothDevice mDevice) {
log("SendPassThruPause + ");
AvrcpControllerService avrcpCtrlService = AvrcpControllerService.getAvrcpControllerService();
if ((avrcpCtrlService != null) && (mDevice != null) &&
(avrcpCtrlService.getConnectedDevices().contains(mDevice))){
avrcpCtrlService.sendPassThroughCmd(mDevice,
BluetoothAvrcpController.PASS_THRU_CMD_ID_PAUSE,
BluetoothAvrcpController.KEY_STATE_PRESSED);
avrcpCtrlService.sendPassThroughCmd(mDevice,
BluetoothAvrcpController.PASS_THRU_CMD_ID_PAUSE,
BluetoothAvrcpController.KEY_STATE_RELEASED);
log(" SendPassThruPause command sent - ");
return true;
} else {
log("passthru command not sent, connection unavailable");
return false;
}
}
private final BroadcastReceiver mA2dpReceiver = new BroadcastReceiver() {
@Override
public void onReceive(Context context, Intent intent) {
String action = intent.getAction();
log("onReceive " + action);
if (action.equals("com.android.music.musicservicecommand")) {
String cmd = intent.getStringExtra("command");
log("Command Received " + cmd);
if (cmd.equals("pause")) {
if (mCurrentDevice != null) {
if (SendPassThruPause(mCurrentDevice)) {
log(" Sending AVRCP Pause");
} else {
log(" Sending Disconnect AVRCP Not Up");
disconnectA2dpNative(getByteAddress(mCurrentDevice));
}
releasePatch();
if (mAudioFocusAcquired != AUDIO_FOCUS_LOSS) {
int status = mAudioManager.abandonAudioFocus(mAudioFocusListener);
log("abandonAudioFocus returned" + status);
if (status == AudioManager.AUDIOFOCUS_REQUEST_GRANTED) {
mAudioFocusAcquired = AUDIO_FOCUS_LOSS;
}
}
}
}
}
}
};
private OnAudioFocusChangeListener mAudioFocusListener = new OnAudioFocusChangeListener() {
public void onAudioFocusChange(int focusChange){
log("onAudioFocusChangeListener focuschange " + focusChange);
switch(focusChange){
case AudioManager.AUDIOFOCUS_LOSS:
if (mCurrentDevice != null) {
/* First Release the Patch Anyways */
if (SendPassThruPause(mCurrentDevice)) {
log(" Sending AVRCP Pause");
} else {
log(" Sending Disconnect AVRCP Not Up");
disconnectA2dpNative(getByteAddress(mCurrentDevice));
}
if(USE_AUDIOTRACK)
informAudioFocusStateNative(STATE_FOCUS_LOST);
int status = mAudioManager.abandonAudioFocus(mAudioFocusListener);
log("abandonAudioFocus returned" + status);
if (status == AudioManager.AUDIOFOCUS_REQUEST_GRANTED) {
mAudioFocusAcquired = AUDIO_FOCUS_LOSS;
}
}
break;
case AudioManager.AUDIOFOCUS_LOSS_TRANSIENT:
if ((mCurrentDevice != null) && (getCurrentState() == mConnected)) {
/* don't abandon focus, but fake focus loss */
mAudioFocusAcquired = AUDIO_FOCUS_LOSS_TRANSIENT;
if (SendPassThruPause(mCurrentDevice)) {
log(" Sending AVRCP Pause");
} else {
log(" AVRCP Connection not UP");
disconnectA2dpNative(getByteAddress(mCurrentDevice));
/* TODO what shld we do in case AVRCP connection is not there */
}
if(USE_AUDIOTRACK)
informAudioFocusStateNative(STATE_FOCUS_LOST);
}
break;
case AudioManager.AUDIOFOCUS_LOSS_TRANSIENT_CAN_DUCK:
log(" Received AUDIOFOCUS_LOSS_TRANSIENT_CAN_DUCK ");
mAudioFocusAcquired = AUDIO_FOCUS_LOSS_CAN_DUCK;
// TODO(sanketa): Currently we do not honor this and keep playing music at the
// previous volume. This should be changed to implement ducking.
break;
case AudioManager.AUDIOFOCUS_GAIN:
// we got focus gain
if ((mCurrentDevice != null) && (getCurrentState() == mConnected)) {
if (mAudioFocusAcquired == AUDIO_FOCUS_LOSS_CAN_DUCK) {
log(" Received Can_Duck earlier, Ignore Now ");
mAudioFocusAcquired = AUDIO_FOCUS_GAIN;
break;
}
mAudioFocusAcquired = AUDIO_FOCUS_GAIN;
if (SendPassThruPlay(mCurrentDevice)) {
log(" Sending AVRCP Play");
} else {
log(" AVRCP Connection not up");
/* TODO what shld we do in case AVRCP connection is not there */
}
if(USE_AUDIOTRACK)
informAudioFocusStateNative(STATE_FOCUS_GRANTED);
}
break;
default:
break;
}
}
};
// Event types for STACK_EVENT message
final private static int EVENT_TYPE_NONE = 0;
final private static int EVENT_TYPE_CONNECTION_STATE_CHANGED = 1;
final private static int EVENT_TYPE_AUDIO_STATE_CHANGED = 2;
final private static int EVENT_TYPE_AUDIO_CONFIG_CHANGED = 3;
// Do not modify without updating the HAL bt_av.h files.
// match up with btav_connection_state_t enum of bt_av.h
final static int CONNECTION_STATE_DISCONNECTED = 0;
final static int CONNECTION_STATE_CONNECTING = 1;
final static int CONNECTION_STATE_CONNECTED = 2;
final static int CONNECTION_STATE_DISCONNECTING = 3;
// match up with btav_audio_state_t enum of bt_av.h
final static int AUDIO_STATE_REMOTE_SUSPEND = 0;
final static int AUDIO_STATE_STOPPED = 1;
final static int AUDIO_STATE_STARTED = 2;
private native static void classInitNative();
private native void initNative();
private native void cleanupNative();
private native boolean connectA2dpNative(byte[] address);
private native boolean disconnectA2dpNative(byte[] address);
private native void informAudioFocusStateNative(int focusGranted);
}