blob: 2e8d902c21c89c5c8be88dba6d4bb644ed28f282 [file] [log] [blame]
/*
* Copyright (C) 2012 Google Inc.
*/
/**
* Bluetooth A2dp StateMachine
* (Disconnected)
* | ^
* CONNECT | | DISCONNECTED
* V |
* (Pending)
* | ^
* CONNECTED | | CONNECT
* V |
* (Connected)
*/
package com.android.bluetooth.a2dp;
import android.bluetooth.BluetoothA2dp;
import android.bluetooth.BluetoothAdapter;
import android.bluetooth.BluetoothDevice;
import android.bluetooth.BluetoothProfile;
import android.bluetooth.BluetoothUuid;
import android.bluetooth.IBluetooth;
import android.content.Context;
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.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.Set;
final class A2dpStateMachine extends StateMachine {
private static final String TAG = "A2dpStateMachine";
private static final boolean DBG = true;
static final int CONNECT = 1;
static final int DISCONNECT = 2;
private static final int STACK_EVENT = 101;
private static final int CONNECT_TIMEOUT = 201;
private Disconnected mDisconnected;
private Pending mPending;
private Connected mConnected;
private A2dpService mService;
private Context mContext;
private BluetoothAdapter mAdapter;
private static final ParcelUuid[] A2DP_UUIDS = {
BluetoothUuid.AudioSink
};
// 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 mPlayingA2dpDevice = null;
static {
classInitNative();
}
A2dpStateMachine(A2dpService svc, Context context) {
super(TAG);
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);
}
public void cleanup() {
cleanupNative();
if(mService != null)
mService = null;
if (mContext != null)
mContext = null;
if(mAdapter != null)
mAdapter = null;
}
private class Disconnected extends State {
@Override
public void enter() {
log("Enter Disconnected: " + getCurrentMessage().what);
}
@Override
public boolean processMessage(Message message) {
log("Disconnected process message: " + message.what);
if (DBG) {
if (mCurrentDevice != null || mTargetDevice != null || mIncomingDevice != null) {
log("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 (A2dpStateMachine.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;
default:
Log.e(TAG, "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:
Log.w(TAG, "Ignore HF DISCONNECTED event, device: " + device);
break;
case CONNECTION_STATE_CONNECTING:
// check priority and accept or reject the connection
// Since the state changes to Connecting or directly Connected in some cases.Have the check both in
// CONNECTION_STATE_CONNECTING and CONNECTION_STATE_CONNECTED.
if (BluetoothProfile.PRIORITY_OFF < mService.getPriority(device)) {
Log.i(TAG,"Incoming A2DP accepted");
broadcastConnectionState(device, BluetoothProfile.STATE_CONNECTING,
BluetoothProfile.STATE_DISCONNECTED);
synchronized (A2dpStateMachine.this) {
mIncomingDevice = device;
transitionTo(mPending);
}
} else {
//reject the connection and stay in Disconnected state itself
Log.i(TAG,"Incoming A2DP rejected");
disconnectA2dpNative(getByteAddress(device));
}
break;
case CONNECTION_STATE_CONNECTED:
Log.w(TAG, "A2DP Connected from Disconnected state");
if (BluetoothProfile.PRIORITY_OFF < mService.getPriority(device)) {
Log.i(TAG,"Incoming A2DP accepted");
broadcastConnectionState(device, BluetoothProfile.STATE_CONNECTED,
BluetoothProfile.STATE_DISCONNECTED);
synchronized (A2dpStateMachine.this) {
mCurrentDevice = device;
transitionTo(mConnected);
}
} else {
//reject the connection and stay in Disconnected state itself
Log.i(TAG,"Incoming A2DP rejected");
disconnectA2dpNative(getByteAddress(device));
}
break;
case CONNECTION_STATE_DISCONNECTING:
Log.w(TAG, "Ignore HF DISCONNECTING event, device: " + device);
break;
default:
Log.e(TAG, "Incorrect state: " + state);
break;
}
}
}
private class Pending extends State {
@Override
public void enter() {
log("Enter Pending: " + getCurrentMessage().what);
}
@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 (A2dpStateMachine.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;
default:
Log.e(TAG, "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:
if ((mCurrentDevice != null) && mCurrentDevice.equals(device)) {
broadcastConnectionState(mCurrentDevice,
BluetoothProfile.STATE_DISCONNECTED,
BluetoothProfile.STATE_DISCONNECTING);
synchronized (A2dpStateMachine.this) {
mCurrentDevice = null;
}
if (mTargetDevice != null) {
if (!connectA2dpNative(getByteAddress(mTargetDevice))) {
broadcastConnectionState(mTargetDevice,
BluetoothProfile.STATE_DISCONNECTED,
BluetoothProfile.STATE_CONNECTING);
synchronized (A2dpStateMachine.this) {
mTargetDevice = null;
transitionTo(mDisconnected);
}
}
} else {
synchronized (A2dpStateMachine.this) {
mIncomingDevice = null;
transitionTo(mDisconnected);
}
}
} else if (mTargetDevice != null && mTargetDevice.equals(device)) {
// outgoing connection failed
broadcastConnectionState(mTargetDevice, BluetoothProfile.STATE_DISCONNECTED,
BluetoothProfile.STATE_CONNECTING);
synchronized (A2dpStateMachine.this) {
mTargetDevice = null;
transitionTo(mDisconnected);
}
} else if (mIncomingDevice != null && mIncomingDevice.equals(device)) {
broadcastConnectionState(mIncomingDevice,
BluetoothProfile.STATE_DISCONNECTED,
BluetoothProfile.STATE_CONNECTING);
synchronized (A2dpStateMachine.this) {
mIncomingDevice = null;
transitionTo(mDisconnected);
}
} else {
Log.e(TAG, "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 (A2dpStateMachine.this) {
mTargetDevice = null;
transitionTo(mConnected);
}
} else if (mTargetDevice != null && mTargetDevice.equals(device)) {
broadcastConnectionState(mTargetDevice, BluetoothProfile.STATE_CONNECTED,
BluetoothProfile.STATE_CONNECTING);
synchronized (A2dpStateMachine.this) {
mCurrentDevice = mTargetDevice;
mTargetDevice = null;
transitionTo(mConnected);
}
} else if (mIncomingDevice != null && mIncomingDevice.equals(device)) {
broadcastConnectionState(mIncomingDevice, BluetoothProfile.STATE_CONNECTED,
BluetoothProfile.STATE_CONNECTING);
synchronized (A2dpStateMachine.this) {
mCurrentDevice = mIncomingDevice;
mIncomingDevice = null;
transitionTo(mConnected);
}
} else {
Log.e(TAG, "Unknown device Connected: " + device);
// something is wrong here, but sync our state with stack
broadcastConnectionState(device, BluetoothProfile.STATE_CONNECTED,
BluetoothProfile.STATE_DISCONNECTED);
synchronized (A2dpStateMachine.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
if (DBG) {
log("Stack and target device are connecting");
}
}
else if (mIncomingDevice != null && mIncomingDevice.equals(device)) {
Log.e(TAG, "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)) {
Log.e(TAG, "TargetDevice is getting disconnected");
} else if (mIncomingDevice != null && mIncomingDevice.equals(device)) {
Log.e(TAG, "IncomingDevice is getting disconnected");
} else {
Log.e(TAG, "Disconnecting unknow device: " + device);
}
break;
default:
Log.e(TAG, "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, BluetoothA2dp.STATE_NOT_PLAYING,
BluetoothA2dp.STATE_PLAYING);
}
@Override
public boolean processMessage(Message message) {
log("Connected process message: " + message.what);
if (DBG) {
if (mCurrentDevice == null) {
log("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 (A2dpStateMachine.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;
}
transitionTo(mPending);
}
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;
default:
Log.e(TAG, "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:
if (mCurrentDevice.equals(device)) {
broadcastConnectionState(mCurrentDevice, BluetoothProfile.STATE_DISCONNECTED,
BluetoothProfile.STATE_CONNECTED);
synchronized (A2dpStateMachine.this) {
mCurrentDevice = null;
transitionTo(mDisconnected);
}
} else {
Log.e(TAG, "Disconnected from unknown device: " + device);
}
break;
default:
Log.e(TAG, "Connection State Device: " + device + " bad state: " + state);
break;
}
}
private void processAudioStateEvent(int state, BluetoothDevice device) {
if (!mCurrentDevice.equals(device)) {
Log.e(TAG, "Audio State Device:" + device + "is different from ConnectedDevice:" +
mCurrentDevice);
return;
}
switch (state) {
case AUDIO_STATE_STARTED:
if (mPlayingA2dpDevice == null) {
mPlayingA2dpDevice = device;
broadcastAudioState(device, BluetoothA2dp.STATE_PLAYING,
BluetoothA2dp.STATE_NOT_PLAYING);
}
break;
case AUDIO_STATE_STOPPED:
if(mPlayingA2dpDevice != null) {
mPlayingA2dpDevice = null;
broadcastAudioState(device, BluetoothA2dp.STATE_NOT_PLAYING,
BluetoothA2dp.STATE_PLAYING);
}
break;
default:
Log.e(TAG, "Audio State Device: " + device + " bad state: " + state);
break;
}
}
}
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 {
Log.e(TAG, "Bad currentState: " + currentState);
return BluetoothProfile.STATE_DISCONNECTED;
}
}
}
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 (device.equals(mPlayingA2dpDevice)) {
return true;
}
}
return false;
}
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.containsAnyUuid(featureUuids, A2DP_UUIDS)) {
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) {
/* Notifying the connection state change of the profile before sending the intent for
connection state change, as it was causing a race condition, with the UI not being
updated with the correct connection state. */
if (DBG) log("Connection state " + device + ": " + prevState + "->" + newState);
mService.notifyProfileConnectionStateChanged(device, BluetoothProfile.A2DP,
newState, prevState);
Intent intent = new Intent(BluetoothA2dp.ACTION_CONNECTION_STATE_CHANGED);
intent.putExtra(BluetoothProfile.EXTRA_PREVIOUS_STATE, prevState);
intent.putExtra(BluetoothProfile.EXTRA_STATE, newState);
intent.putExtra(BluetoothDevice.EXTRA_DEVICE, device);
mContext.sendBroadcast(intent, ProfileService.BLUETOOTH_PERM);
}
private void broadcastAudioState(BluetoothDevice device, int state, int prevState) {
Intent intent = new Intent(BluetoothA2dp.ACTION_PLAYING_STATE_CHANGED);
intent.putExtra(BluetoothDevice.EXTRA_DEVICE, device);
intent.putExtra(BluetoothProfile.EXTRA_PREVIOUS_STATE, prevState);
intent.putExtra(BluetoothProfile.EXTRA_STATE, state);
intent.addFlags(Intent.FLAG_RECEIVER_REGISTERED_ONLY_BEFORE_BOOT);
mContext.sendBroadcast(intent, A2dpService.BLUETOOTH_PERM);
if (DBG) log("A2DP Playing state : device: " + device + " State:" + prevState + "->" + state);
}
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 BluetoothDevice getDevice(byte[] address) {
return mAdapter.getRemoteDevice(Utils.getAddressStringFromByte(address));
}
private void log(String msg) {
if (DBG) {
Log.d(TAG, msg);
}
}
private class StackEvent {
int type = EVENT_TYPE_NONE;
int valueInt = 0;
BluetoothDevice device = null;
private StackEvent(int type) {
this.type = type;
}
}
// 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;
// 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);
}