/* * 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: // TODO(BT) Assume it's incoming connection // Do we need to check priority and accept/reject accordingly? broadcastConnectionState(device, BluetoothProfile.STATE_CONNECTING, BluetoothProfile.STATE_DISCONNECTED); synchronized (A2dpStateMachine.this) { mIncomingDevice = device; transitionTo(mPending); } break; case CONNECTION_STATE_CONNECTED: Log.w(TAG, "A2DP Connected from Disconnected state"); broadcastConnectionState(device, BluetoothProfile.STATE_CONNECTED, BluetoothProfile.STATE_DISCONNECTED); synchronized (A2dpStateMachine.this) { mCurrentDevice = device; transitionTo(mConnected); } 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 getConnectedDevices() { List devices = new ArrayList(); 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 getDevicesMatchingConnectionStates(int[] states) { List deviceList = new ArrayList(); Set 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) { 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); if (DBG) log("Connection state " + device + ": " + prevState + "->" + newState); mService.notifyProfileConnectionStateChanged(device, BluetoothProfile.A2DP, newState, prevState); } 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); }