blob: 7a18989497098339d572c4c7739d3bc4455dd0b0 [file] [log] [blame]
/*
* Copyright (C) 2012 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.BluetoothA2dp;
import android.bluetooth.BluetoothAdapter;
import android.bluetooth.BluetoothCodecConfig;
import android.bluetooth.BluetoothCodecStatus;
import android.bluetooth.BluetoothDevice;
import android.bluetooth.BluetoothProfile;
import android.bluetooth.BluetoothUuid;
import android.content.Context;
import android.content.Intent;
import android.content.res.Resources;
import android.content.res.Resources.NotFoundException;
import android.media.AudioManager;
import android.os.Handler;
import android.os.Message;
import android.os.ParcelUuid;
import android.os.PowerManager;
import android.os.PowerManager.WakeLock;
import android.util.Log;
import com.android.bluetooth.R;
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 boolean DBG = false;
private static final String TAG = "A2dpStateMachine";
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 final AudioManager mAudioManager;
private IntentBroadcastHandler mIntentBroadcastHandler;
private final WakeLock mWakeLock;
private BluetoothCodecConfig[] mCodecConfigPriorities;
private static final int MSG_CONNECTION_STATE_CHANGED = 0;
// 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;
private BluetoothCodecStatus mCodecStatus = null;
private int mA2dpSourceCodecPrioritySbc = BluetoothCodecConfig.CODEC_PRIORITY_DEFAULT;
private int mA2dpSourceCodecPriorityAac = BluetoothCodecConfig.CODEC_PRIORITY_DEFAULT;
private int mA2dpSourceCodecPriorityAptx = BluetoothCodecConfig.CODEC_PRIORITY_DEFAULT;
private int mA2dpSourceCodecPriorityAptxHd = BluetoothCodecConfig.CODEC_PRIORITY_DEFAULT;
private int mA2dpSourceCodecPriorityLdac = BluetoothCodecConfig.CODEC_PRIORITY_DEFAULT;
static {
classInitNative();
}
private A2dpStateMachine(A2dpService svc, Context context) {
super("A2dpStateMachine");
mService = svc;
mContext = context;
mAdapter = BluetoothAdapter.getDefaultAdapter();
mCodecConfigPriorities = assignCodecConfigPriorities();
initNative(mCodecConfigPriorities);
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, "BluetoothA2dpService");
mIntentBroadcastHandler = new IntentBroadcastHandler();
mAudioManager = (AudioManager) context.getSystemService(Context.AUDIO_SERVICE);
}
static A2dpStateMachine make(A2dpService svc, Context context) {
Log.d(TAG, "make");
A2dpStateMachine a2dpSm = new A2dpStateMachine(svc, context);
a2dpSm.start();
return a2dpSm;
}
// Assign the A2DP Source codec config priorities
private BluetoothCodecConfig[] assignCodecConfigPriorities() {
Resources resources = mContext.getResources();
if (resources == null) {
return null;
}
int value;
try {
value = resources.getInteger(R.integer.a2dp_source_codec_priority_sbc);
} catch (NotFoundException e) {
value = BluetoothCodecConfig.CODEC_PRIORITY_DEFAULT;
}
if ((value >= BluetoothCodecConfig.CODEC_PRIORITY_DISABLED)
&& (value < BluetoothCodecConfig.CODEC_PRIORITY_HIGHEST)) {
mA2dpSourceCodecPrioritySbc = value;
}
try {
value = resources.getInteger(R.integer.a2dp_source_codec_priority_aac);
} catch (NotFoundException e) {
value = BluetoothCodecConfig.CODEC_PRIORITY_DEFAULT;
}
if ((value >= BluetoothCodecConfig.CODEC_PRIORITY_DISABLED)
&& (value < BluetoothCodecConfig.CODEC_PRIORITY_HIGHEST)) {
mA2dpSourceCodecPriorityAac = value;
}
try {
value = resources.getInteger(R.integer.a2dp_source_codec_priority_aptx);
} catch (NotFoundException e) {
value = BluetoothCodecConfig.CODEC_PRIORITY_DEFAULT;
}
if ((value >= BluetoothCodecConfig.CODEC_PRIORITY_DISABLED)
&& (value < BluetoothCodecConfig.CODEC_PRIORITY_HIGHEST)) {
mA2dpSourceCodecPriorityAptx = value;
}
try {
value = resources.getInteger(R.integer.a2dp_source_codec_priority_aptx_hd);
} catch (NotFoundException e) {
value = BluetoothCodecConfig.CODEC_PRIORITY_DEFAULT;
}
if ((value >= BluetoothCodecConfig.CODEC_PRIORITY_DISABLED)
&& (value < BluetoothCodecConfig.CODEC_PRIORITY_HIGHEST)) {
mA2dpSourceCodecPriorityAptxHd = value;
}
try {
value = resources.getInteger(R.integer.a2dp_source_codec_priority_ldac);
} catch (NotFoundException e) {
value = BluetoothCodecConfig.CODEC_PRIORITY_DEFAULT;
}
if ((value >= BluetoothCodecConfig.CODEC_PRIORITY_DISABLED)
&& (value < BluetoothCodecConfig.CODEC_PRIORITY_HIGHEST)) {
mA2dpSourceCodecPriorityLdac = value;
}
BluetoothCodecConfig codecConfig;
BluetoothCodecConfig[] codecConfigArray =
new BluetoothCodecConfig[BluetoothCodecConfig.SOURCE_CODEC_TYPE_MAX];
codecConfig = new BluetoothCodecConfig(BluetoothCodecConfig.SOURCE_CODEC_TYPE_SBC,
mA2dpSourceCodecPrioritySbc, BluetoothCodecConfig.SAMPLE_RATE_NONE,
BluetoothCodecConfig.BITS_PER_SAMPLE_NONE, BluetoothCodecConfig.CHANNEL_MODE_NONE,
0 /* codecSpecific1 */, 0 /* codecSpecific2 */, 0 /* codecSpecific3 */,
0 /* codecSpecific4 */);
codecConfigArray[0] = codecConfig;
codecConfig = new BluetoothCodecConfig(BluetoothCodecConfig.SOURCE_CODEC_TYPE_AAC,
mA2dpSourceCodecPriorityAac, BluetoothCodecConfig.SAMPLE_RATE_NONE,
BluetoothCodecConfig.BITS_PER_SAMPLE_NONE, BluetoothCodecConfig.CHANNEL_MODE_NONE,
0 /* codecSpecific1 */, 0 /* codecSpecific2 */, 0 /* codecSpecific3 */,
0 /* codecSpecific4 */);
codecConfigArray[1] = codecConfig;
codecConfig = new BluetoothCodecConfig(BluetoothCodecConfig.SOURCE_CODEC_TYPE_APTX,
mA2dpSourceCodecPriorityAptx, BluetoothCodecConfig.SAMPLE_RATE_NONE,
BluetoothCodecConfig.BITS_PER_SAMPLE_NONE, BluetoothCodecConfig.CHANNEL_MODE_NONE,
0 /* codecSpecific1 */, 0 /* codecSpecific2 */, 0 /* codecSpecific3 */,
0 /* codecSpecific4 */);
codecConfigArray[2] = codecConfig;
codecConfig = new BluetoothCodecConfig(BluetoothCodecConfig.SOURCE_CODEC_TYPE_APTX_HD,
mA2dpSourceCodecPriorityAptxHd, BluetoothCodecConfig.SAMPLE_RATE_NONE,
BluetoothCodecConfig.BITS_PER_SAMPLE_NONE, BluetoothCodecConfig.CHANNEL_MODE_NONE,
0 /* codecSpecific1 */, 0 /* codecSpecific2 */, 0 /* codecSpecific3 */,
0 /* codecSpecific4 */);
codecConfigArray[3] = codecConfig;
codecConfig = new BluetoothCodecConfig(BluetoothCodecConfig.SOURCE_CODEC_TYPE_LDAC,
mA2dpSourceCodecPriorityLdac, BluetoothCodecConfig.SAMPLE_RATE_NONE,
BluetoothCodecConfig.BITS_PER_SAMPLE_NONE, BluetoothCodecConfig.CHANNEL_MODE_NONE,
0 /* codecSpecific1 */, 0 /* codecSpecific2 */, 0 /* codecSpecific3 */,
0 /* codecSpecific4 */);
codecConfigArray[4] = codecConfig;
return codecConfigArray;
}
public void doQuit() {
if ((mTargetDevice != null) &&
(getConnectionState(mTargetDevice) == BluetoothProfile.STATE_CONNECTING)) {
log("doQuit()- Move A2DP State to DISCONNECTED");
broadcastConnectionState(mTargetDevice, BluetoothProfile.STATE_DISCONNECTED,
BluetoothProfile.STATE_CONNECTING);
}
quitNow();
}
public void cleanup() {
cleanupNative();
}
private class Disconnected extends State {
@Override
public void enter() {
log("Enter Disconnected: " + getCurrentMessage().what);
if (mCurrentDevice != null || mTargetDevice != null || mIncomingDevice != null) {
loge("ERROR: enter() inconsistent state in Disconnected: current = "
+ mCurrentDevice + " target = " + mTargetDevice + " incoming = "
+ mIncomingDevice);
}
}
@Override
public boolean processMessage(Message message) {
log("Disconnected process message: " + message.what);
if (mCurrentDevice != null || mTargetDevice != null || mIncomingDevice != null) {
loge("ERROR: not null state in Disconnected: current = " + mCurrentDevice
+ " target = " + mTargetDevice + " incoming = " + mIncomingDevice);
mCurrentDevice = null;
mTargetDevice = null;
mIncomingDevice = null;
}
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:
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 (A2dpStateMachine.this) {
mIncomingDevice = device;
transitionTo(mPending);
}
} else {
//reject the connection and stay in Disconnected state itself
logi("Incoming A2DP rejected");
disconnectA2dpNative(getByteAddress(device));
}
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 (A2dpStateMachine.this) {
mCurrentDevice = device;
transitionTo(mConnected);
}
} else {
//reject the connection and stay in Disconnected state itself
logi("Incoming A2DP rejected");
disconnectA2dpNative(getByteAddress(device));
}
break;
case CONNECTION_STATE_DISCONNECTING:
logw("Ignore A2dp 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);
if (mTargetDevice != null && mIncomingDevice != null) {
loge("ERROR: enter() inconsistent state in Pending: current = " + mCurrentDevice
+ " target = " + mTargetDevice + " incoming = " + mIncomingDevice);
}
}
@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:
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:
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);
// check if there is some incoming connection request
if (mIncomingDevice != null) {
logi("disconnect for outgoing in pending state");
synchronized (A2dpStateMachine.this) {
mTargetDevice = null;
}
break;
}
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 {
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 (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);
// check for a2dp connection allowed for this device in race condition
if (okToConnect(mIncomingDevice)) {
logi("Ready to connect incoming Connection from pending state");
synchronized (A2dpStateMachine.this) {
mCurrentDevice = mIncomingDevice;
mIncomingDevice = null;
transitionTo(mConnected);
}
} else {
// A2dp connection unchecked for this device
loge("Incoming A2DP rejected from pending state");
disconnectA2dpNative(getByteAddress(device));
}
} 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 (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
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, accept it");
broadcastConnectionState(device, BluetoothProfile.STATE_CONNECTING,
BluetoothProfile.STATE_DISCONNECTED);
mIncomingDevice = device;
}
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 unknow device: " + device);
}
break;
default:
loge("Incorrect state: " + state);
break;
}
}
}
private class Connected extends State {
@Override
public void enter() {
// Remove pending connection attempts that were deferred during the pending
// state. This is to prevent auto connect attempts from disconnecting
// devices that previously successfully connected.
// TODO: This needs to check for multiple A2DP connections, once supported...
removeDeferredMessages(CONNECT);
log("Enter Connected: " + getCurrentMessage().what);
if (mTargetDevice != null || mIncomingDevice != null) {
loge("ERROR: enter() inconsistent state in Connected: current = " + mCurrentDevice
+ " target = " + mTargetDevice + " incoming = " + mIncomingDevice);
}
// 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 (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;
} else {
broadcastConnectionState(mCurrentDevice,
BluetoothProfile.STATE_DISCONNECTING,
BluetoothProfile.STATE_CONNECTED);
}
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_DISCONNECTING);
break;
}
synchronized (A2dpStateMachine.this) {
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:
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:
if (mCurrentDevice.equals(device)) {
broadcastConnectionState(mCurrentDevice, BluetoothProfile.STATE_DISCONNECTED,
BluetoothProfile.STATE_CONNECTED);
synchronized (A2dpStateMachine.this) {
mCurrentDevice = null;
transitionTo(mDisconnected);
}
} else if (mTargetDevice != null && mTargetDevice.equals(device)) {
broadcastConnectionState(device, BluetoothProfile.STATE_DISCONNECTED,
BluetoothProfile.STATE_CONNECTING);
synchronized (A2dpStateMachine.this) {
mTargetDevice = null;
}
logi("Disconnected from mTargetDevice in connected state device: " + device);
} 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;
}
switch (state) {
case AUDIO_STATE_STARTED:
if (mPlayingA2dpDevice == null) {
mPlayingA2dpDevice = device;
mService.setAvrcpAudioState(BluetoothA2dp.STATE_PLAYING);
broadcastAudioState(device, BluetoothA2dp.STATE_PLAYING,
BluetoothA2dp.STATE_NOT_PLAYING);
}
break;
case AUDIO_STATE_REMOTE_SUSPEND:
case AUDIO_STATE_STOPPED:
if (mPlayingA2dpDevice != null) {
mPlayingA2dpDevice = null;
mService.setAvrcpAudioState(BluetoothA2dp.STATE_NOT_PLAYING);
broadcastAudioState(device, BluetoothA2dp.STATE_NOT_PLAYING,
BluetoothA2dp.STATE_PLAYING);
}
break;
default:
loge("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 {
loge("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;
}
BluetoothCodecStatus getCodecStatus() {
synchronized (this) {
return mCodecStatus;
}
}
private void onCodecConfigChanged(BluetoothCodecConfig newCodecConfig,
BluetoothCodecConfig[] codecsLocalCapabilities,
BluetoothCodecConfig[] codecsSelectableCapabilities) {
BluetoothCodecConfig prevCodecConfig = null;
synchronized (this) {
if (mCodecStatus != null) {
prevCodecConfig = mCodecStatus.getCodecConfig();
}
mCodecStatus = new BluetoothCodecStatus(
newCodecConfig, codecsLocalCapabilities, codecsSelectableCapabilities);
}
Intent intent = new Intent(BluetoothA2dp.ACTION_CODEC_CONFIG_CHANGED);
intent.putExtra(BluetoothCodecStatus.EXTRA_CODEC_STATUS, mCodecStatus);
intent.addFlags(Intent.FLAG_RECEIVER_REGISTERED_ONLY_BEFORE_BOOT);
log("A2DP Codec Config: " + prevCodecConfig + "->" + newCodecConfig);
for (BluetoothCodecConfig codecConfig : codecsLocalCapabilities) {
log("A2DP Codec Local Capability: " + codecConfig);
}
for (BluetoothCodecConfig codecConfig : codecsSelectableCapabilities) {
log("A2DP Codec Selectable Capability: " + codecConfig);
}
// Inform the Audio Service about the codec configuration change,
// so the Audio Service can reset accordingly the audio feeding
// parameters in the Audio HAL to the Bluetooth stack.
if (!newCodecConfig.sameAudioFeedingParameters(prevCodecConfig) && (mCurrentDevice != null)
&& (getCurrentState() == mConnected)) {
// Add the device only if it is currently connected
intent.putExtra(BluetoothDevice.EXTRA_DEVICE, mCurrentDevice);
mAudioManager.handleBluetoothA2dpDeviceConfigChange(mCurrentDevice);
}
mContext.sendBroadcast(intent, A2dpService.BLUETOOTH_PERM);
}
void setCodecConfigPreference(BluetoothCodecConfig codecConfig) {
BluetoothCodecConfig[] codecConfigArray = new BluetoothCodecConfig[1];
codecConfigArray[0] = codecConfig;
setCodecConfigPreferenceNative(codecConfigArray);
}
void enableOptionalCodecs() {
BluetoothCodecConfig[] codecConfigArray = assignCodecConfigPriorities();
if (codecConfigArray == null) {
return;
}
// Set the mandatory codec's priority to default, and remove the rest
for (int i = 0; i < codecConfigArray.length; i++) {
BluetoothCodecConfig codecConfig = codecConfigArray[i];
if (!codecConfig.isMandatoryCodec()) {
codecConfigArray[i] = null;
}
}
setCodecConfigPreferenceNative(codecConfigArray);
}
void disableOptionalCodecs() {
BluetoothCodecConfig[] codecConfigArray = assignCodecConfigPriorities();
if (codecConfigArray == null) {
return;
}
// Set the mandatory codec's priority to highest, and ignore the rest
for (int i = 0; i < codecConfigArray.length; i++) {
BluetoothCodecConfig codecConfig = codecConfigArray[i];
if (codecConfig.isMandatoryCodec()) {
codecConfig.setCodecPriority(BluetoothCodecConfig.CODEC_PRIORITY_HIGHEST);
} else {
codecConfigArray[i] = null;
}
}
setCodecConfigPreferenceNative(codecConfigArray);
}
boolean okToConnect(BluetoothDevice device) {
AdapterService adapterService = AdapterService.getAdapterService();
int priority = mService.getPriority(device);
boolean ret = false;
//check if this is an incoming connection in Quiet mode.
if((adapterService == null) ||
((adapterService.isQuietModeEnabled() == true) &&
(mTargetDevice == null))){
ret = false;
}
// check priority and accept or reject the connection. if priority is undefined
// it is likely that our SDP has not completed and peer is initiating the
// connection. Allow this connection, provided the device is bonded
else if((BluetoothProfile.PRIORITY_OFF < priority) ||
((BluetoothProfile.PRIORITY_UNDEFINED == priority) &&
(device.getBondState() != BluetoothDevice.BOND_NONE))){
if (device.getBondState() == BluetoothDevice.BOND_BONDED) {
ret = true;
}
}
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.AudioSink)) {
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) {
mAudioManager.setBluetoothA2dpDeviceConnectionState(
device, newState, BluetoothProfile.A2DP);
mWakeLock.acquire();
mIntentBroadcastHandler.sendMessage(mIntentBroadcastHandler.obtainMessage(
MSG_CONNECTION_STATE_CHANGED, prevState, newState, device));
}
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);
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 class StackEvent {
int type = EVENT_TYPE_NONE;
int valueInt = 0;
BluetoothDevice device = 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(BluetoothA2dp.ACTION_CONNECTION_STATE_CHANGED);
intent.putExtra(BluetoothProfile.EXTRA_PREVIOUS_STATE, prevState);
intent.putExtra(BluetoothProfile.EXTRA_STATE, state);
intent.putExtra(BluetoothDevice.EXTRA_DEVICE, device);
intent.addFlags(Intent.FLAG_RECEIVER_REGISTERED_ONLY_BEFORE_BOOT
| Intent.FLAG_RECEIVER_INCLUDE_BACKGROUND);
mContext.sendBroadcast(intent, ProfileService.BLUETOOTH_PERM);
log("Connection state " + device + ": " + prevState + "->" + state);
}
@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 void dump(StringBuilder sb) {
ProfileService.println(sb, "mCurrentDevice: " + mCurrentDevice);
ProfileService.println(sb, "mTargetDevice: " + mTargetDevice);
ProfileService.println(sb, "mIncomingDevice: " + mIncomingDevice);
ProfileService.println(sb, "mPlayingA2dpDevice: " + mPlayingA2dpDevice);
ProfileService.println(sb, "StateMachine: " + this.toString());
}
// 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(BluetoothCodecConfig[] codecConfigPriorites);
private native void cleanupNative();
private native boolean connectA2dpNative(byte[] address);
private native boolean disconnectA2dpNative(byte[] address);
private native boolean setCodecConfigPreferenceNative(BluetoothCodecConfig[] codecConfigArray);
}