/* * 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 Sink StateMachine * (Disconnected) * | ^ * CONNECT | | DISCONNECTED * V | * (Pending) * | ^ * CONNECTED | | CONNECT * V | * (Connected -- See A2dpSinkStreamHandler) */ package com.android.bluetooth.a2dpsink; import android.bluetooth.BluetoothA2dpSink; import android.bluetooth.BluetoothAdapter; import android.bluetooth.BluetoothAudioConfig; import android.bluetooth.BluetoothDevice; import android.bluetooth.BluetoothProfile; import android.bluetooth.BluetoothUuid; import android.content.BroadcastReceiver; import android.content.Context; import android.content.Intent; import android.media.AudioFormat; 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.Utils; import com.android.bluetooth.btservice.AdapterService; import com.android.bluetooth.btservice.ProfileService; import com.android.bluetooth.avrcpcontroller.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; public class A2dpSinkStateMachine extends StateMachine { private static final boolean DBG = false; static final int CONNECT = 1; static final int DISCONNECT = 2; private static final int STACK_EVENT = 101; private static final int CONNECT_TIMEOUT = 201; public static final int EVENT_AVRCP_CT_PLAY = 301; public static final int EVENT_AVRCP_CT_PAUSE = 302; public static final int EVENT_AVRCP_TG_PLAY = 303; public static final int EVENT_AVRCP_TG_PAUSE = 304; 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; // Connection states. // 1. Disconnected: The connection does not exist. // 2. Pending: The connection is being established. // 3. Connected: The connection is established. The audio connection is in Idle state. private Disconnected mDisconnected; private Pending mPending; private Connected mConnected; private A2dpSinkService mService; private Context mContext; private BluetoothAdapter mAdapter; private IntentBroadcastHandler mIntentBroadcastHandler; private static final int MSG_CONNECTION_STATE_CHANGED = 0; private final Object mLockForPatch = new Object(); // 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 A2dpSinkStreamHandler mStreaming = null; private final HashMap mAudioConfigs = new HashMap(); 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); mIntentBroadcastHandler = new IntentBroadcastHandler(); } static A2dpSinkStateMachine make(A2dpSinkService svc, Context context) { Log.d("A2dpSinkStateMachine", "make"); A2dpSinkStateMachine a2dpSm = new A2dpSinkStateMachine(svc, context); a2dpSm.start(); return a2dpSm; } public void doQuit() { if(DBG) { Log.d("A2dpSinkStateMachine", "Quit"); } synchronized (A2dpSinkStateMachine.this) { mStreaming = null; } quitNow(); } public void cleanup() { cleanupNative(); mAudioConfigs.clear(); } 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); } @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 A2DP 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)); } 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)); } 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); } @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; log("STACK_EVENT " + event.type); 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) { log("processConnectionEvent state " + state); log("Devices curr: " + mCurrentDevice + " target: " + mTargetDevice + " incoming: " + mIncomingDevice + " device: " + 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)) { loge("current device is not null"); // 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)) { loge("target device is not null"); broadcastConnectionState(mTargetDevice, BluetoothProfile.STATE_CONNECTED, BluetoothProfile.STATE_CONNECTING); synchronized (A2dpSinkStateMachine.this) { mCurrentDevice = mTargetDevice; mTargetDevice = null; transitionTo(mConnected); } } else if (mIncomingDevice != null && mIncomingDevice.equals(device)) { loge("incoming device is not null"); 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 by connecting to // the new device and disconnect from previous device. broadcastConnectionState(device, BluetoothProfile.STATE_CONNECTED, BluetoothProfile.STATE_DISCONNECTED); broadcastConnectionState(mCurrentDevice, BluetoothProfile.STATE_DISCONNECTED, BluetoothProfile.STATE_CONNECTING); 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); synchronized (A2dpSinkStateMachine.this) { if (mStreaming == null) { if(DBG) { log("Creating New A2dpSinkStreamHandler"); } mStreaming = new A2dpSinkStreamHandler(A2dpSinkStateMachine.this, mContext); } } } @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; } 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; mStreaming.obtainMessage(A2dpSinkStreamHandler.DISCONNECT).sendToTarget(); 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; } mPlayingDevice = null; mStreaming.obtainMessage(A2dpSinkStreamHandler.DISCONNECT).sendToTarget(); 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; case EVENT_TYPE_AUDIO_CONFIG_CHANGED: processAudioConfigEvent(event.audioConfig, event.device); break; default: loge("Unexpected stack event: " + event.type); break; } break; case EVENT_AVRCP_CT_PLAY: mStreaming.obtainMessage(A2dpSinkStreamHandler.SNK_PLAY).sendToTarget(); break; case EVENT_AVRCP_TG_PLAY: mStreaming.obtainMessage(A2dpSinkStreamHandler.SRC_PLAY).sendToTarget(); break; case EVENT_AVRCP_CT_PAUSE: mStreaming.obtainMessage(A2dpSinkStreamHandler.SNK_PAUSE).sendToTarget(); break; case EVENT_AVRCP_TG_PAUSE: mStreaming.obtainMessage(A2dpSinkStreamHandler.SRC_PAUSE).sendToTarget(); break; default: return NOT_HANDLED; } return HANDLED; } // 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) { // Take care of existing audio focus in the streaming state machine. mStreaming.obtainMessage(A2dpSinkStreamHandler.DISCONNECT) .sendToTarget(); mCurrentDevice = null; transitionTo(mDisconnected); } } 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: mStreaming.obtainMessage(A2dpSinkStreamHandler.SRC_STR_START).sendToTarget(); break; case AUDIO_STATE_REMOTE_SUSPEND: case AUDIO_STATE_STOPPED: mStreaming.obtainMessage(A2dpSinkStreamHandler.SRC_STR_STOP).sendToTarget(); break; default: loge("Audio State Device: " + device + " bad state: " + state); break; } } } 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 getConnectedDevices() { List devices = new ArrayList(); 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; } // Utility Functions boolean okToConnect(BluetoothDevice device) { AdapterService adapterService = AdapterService.getAdapterService(); int priority = mService.getPriority(device); // 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 if((BluetoothProfile.PRIORITY_OFF < priority) || ((BluetoothProfile.PRIORITY_UNDEFINED == priority) && (device.getBondState() != BluetoothDevice.BOND_NONE))){ return true; } logw("okToConnect not OK to connect " + device); 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.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; 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 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); } @Override public void handleMessage(Message msg) { switch (msg.what) { case MSG_CONNECTION_STATE_CHANGED: onConnectionStateChanged((BluetoothDevice) msg.obj, msg.arg1, msg.arg2); break; } } } public boolean SendPassThruPlay(BluetoothDevice mDevice) { log("SendPassThruPlay + "); AvrcpControllerService avrcpCtrlService = AvrcpControllerService.getAvrcpControllerService(); if ((avrcpCtrlService != null) && (mDevice != null) && (avrcpCtrlService.getConnectedDevices().contains(mDevice))){ avrcpCtrlService.sendPassThroughCmd(mDevice, AvrcpControllerService.PASS_THRU_CMD_ID_PLAY, AvrcpControllerService.KEY_STATE_PRESSED); avrcpCtrlService.sendPassThroughCmd(mDevice, AvrcpControllerService.PASS_THRU_CMD_ID_PLAY, AvrcpControllerService.KEY_STATE_RELEASED); log(" SendPassThruPlay command sent - "); return true; } else { log("passthru command not sent, connection unavailable"); return false; } } // 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); public native void informAudioFocusStateNative(int focusGranted); public native void informAudioTrackGainNative(float focusGranted); }