}
}
+ //====================================================================
+ // Bluetooth SCO control
+ /**
+ * @hide
+ * TODO unhide for SDK
+ * Sticky broadcast intent action indicating that the bluetoooth SCO audio
+ * connection state has changed. The intent contains on extra {@link EXTRA_SCO_AUDIO_STATE}
+ * indicating the new state which is either {@link #SCO_AUDIO_STATE_DISCONNECTED}
+ * or {@link #SCO_AUDIO_STATE_CONNECTED}
+ *
+ * @see #startBluetoothSco()
+ */
+ @SdkConstant(SdkConstantType.BROADCAST_INTENT_ACTION)
+ public static final String ACTION_SCO_AUDIO_STATE_CHANGED =
+ "android.media.SCO_AUDIO_STATE_CHANGED";
+ /**
+ * @hide
+ * TODO unhide for SDK
+ * Extra for intent {@link #ACTION_SCO_AUDIO_STATE_CHANGED} containing the new
+ * bluetooth SCO connection state.
+ */
+ public static final String EXTRA_SCO_AUDIO_STATE =
+ "android.media.extra.SCO_AUDIO_STATE";
+
+ /**
+ * @hide
+ * TODO unhide for SDK
+ * Value for extra {@link #EXTRA_SCO_AUDIO_STATE} indicating that the
+ * SCO audio channel is not established
+ */
+ public static final int SCO_AUDIO_STATE_DISCONNECTED = 0;
+ /**
+ * @hide
+ * TODO unhide for SDK
+ * Value for extra {@link #EXTRA_SCO_AUDIO_STATE} indicating that the
+ * SCO audio channel is established
+ */
+ public static final int SCO_AUDIO_STATE_CONNECTED = 1;
+ /**
+ * @hide
+ * TODO unhide for SDK
+ * Value for extra {@link #EXTRA_SCO_AUDIO_STATE} indicating that
+ * there was an error trying to obtain the state
+ */
+ public static final int SCO_AUDIO_STATE_ERROR = -1;
+
+
+ /**
+ * @hide
+ * TODO unhide for SDK
+ * Indicates if current platform supports use of SCO for off call use cases.
+ * Application wanted to use bluetooth SCO audio when the phone is not in call
+ * must first call thsi method to make sure that the platform supports this
+ * feature.
+ * @return true if bluetooth SCO can be used for audio when not in call
+ * false otherwise
+ * @see #startBluetoothSco()
+ */
+ public boolean isBluetoothScoAvailableOffCall() {
+ return mContext.getResources().getBoolean(
+ com.android.internal.R.bool.config_bluetooth_sco_off_call);
+ }
+
+ /**
+ * @hide
+ * TODO unhide for SDK
+ * Start bluetooth SCO audio connection.
+ * <p>Requires Permission:
+ * {@link android.Manifest.permission#MODIFY_AUDIO_SETTINGS}.
+ * <p>This method can be used by applications wanting to send and received audio
+ * to/from a bluetooth SCO headset while the phone is not in call.
+ * <p>As the SCO connection establishment can take several seconds,
+ * applications should not rely on the connection to be available when the method
+ * returns but instead register to receive the intent {@link #ACTION_SCO_AUDIO_STATE_CHANGED}
+ * and wait for the state to be {@link #SCO_AUDIO_STATE_CONNECTED}.
+ * <p>As the connection is not guaranteed to succeed, applications must wait for this intent with
+ * a timeout.
+ * <p>When finished with the SCO connection or if the establishment times out,
+ * the application must call {@link #stopBluetoothSco()} to clear the request and turn
+ * down the bluetooth connection.
+ * <p>Even if a SCO connection is established, the following restrictions apply on audio
+ * output streams so that they can be routed to SCO headset:
+ * - the stream type must be {@link #STREAM_VOICE_CALL} or {@link #STREAM_BLUETOOTH_SCO}
+ * - the format must be mono
+ * - the sampling must be 16kHz or 8kHz
+ * <p>The following restrictions apply on input streams:
+ * - the format must be mono
+ * - the sampling must be 8kHz
+ *
+ * <p>Note that the phone application always has the priority on the usage of the SCO
+ * connection for telephony. If this method is called while the phone is in call
+ * it will be ignored. Similarly, if a call is received or sent while an application
+ * is using the SCO connection, the connection will be lost for the application and NOT
+ * returned automatically when the call ends.
+ * @see #stopBluetoothSco()
+ * @see #ACTION_SCO_AUDIO_STATE_CHANGED
+ */
+ public void startBluetoothSco(){
+ IAudioService service = getService();
+ try {
+ service.startBluetoothSco(mICallBack);
+ } catch (RemoteException e) {
+ Log.e(TAG, "Dead object in startBluetoothSco", e);
+ }
+ }
+
+ /**
+ * @hide
+ * TODO unhide for SDK
+ * Stop bluetooth SCO audio connection.
+ * <p>Requires Permission:
+ * {@link android.Manifest.permission#MODIFY_AUDIO_SETTINGS}.
+ * <p>This method must be called by applications having requested the use of
+ * bluetooth SCO audio with {@link #startBluetoothSco()}
+ * when finished with the SCO connection or if the establishment times out.
+ * @see #startBluetoothSco()
+ */
+ public void stopBluetoothSco(){
+ IAudioService service = getService();
+ try {
+ service.stopBluetoothSco(mICallBack);
+ } catch (RemoteException e) {
+ Log.e(TAG, "Dead object in stopBluetoothSco", e);
+ }
+ }
+
/**
* Request use of Bluetooth SCO headset for communications.
* <p>
* When losing focus, listeners can use the duration hint to decide what
* behavior to adopt when losing focus. A music player could for instance elect to duck its
* music stream for transient focus losses, and pause otherwise.
- * @param focusChange one of {@link AudioManager#AUDIOFOCUS_GAIN},
+ * @param focusChange one of {@link AudioManager#AUDIOFOCUS_GAIN},
* {@link AudioManager#AUDIOFOCUS_LOSS}, {@link AudioManager#AUDIOFOCUS_LOSS_TRANSIENT}.
*/
public void onAudioFocusChanged(int focusChange);
// The last process to have called setMode() is at the top of the list.
private ArrayList <SetModeDeathHandler> mSetModeDeathHandlers = new ArrayList <SetModeDeathHandler>();
+ // List of clients having issued a SCO start request
+ private ArrayList <ScoClient> mScoClients = new ArrayList <ScoClient>();
+
+ // BluetoothHeadset API to control SCO connection
+ private BluetoothHeadset mBluetoothHeadset;
+
+ // Bluetooth headset connection state
+ private boolean mBluetoothHeadsetConnected;
+
///////////////////////////////////////////////////////////////////////////
// Construction
///////////////////////////////////////////////////////////////////////////
AudioSystem.setErrorCallback(mAudioSystemCallback);
loadSoundEffects();
+ mBluetoothHeadsetConnected = false;
+ mBluetoothHeadset = new BluetoothHeadset(context,
+ mBluetoothHeadsetServiceListener);
+
// Register for device connection intent broadcasts.
IntentFilter intentFilter =
new IntentFilter(Intent.ACTION_HEADSET_PLUG);
intentFilter.addAction(BluetoothA2dp.ACTION_SINK_STATE_CHANGED);
intentFilter.addAction(BluetoothHeadset.ACTION_STATE_CHANGED);
intentFilter.addAction(Intent.ACTION_DOCK_EVENT);
+ intentFilter.addAction(BluetoothHeadset.ACTION_AUDIO_STATE_CHANGED);
context.registerReceiver(mReceiver, intentFilter);
// Register for media button intent broadcasts.
mSetModeDeathHandlers.add(0, hdlr);
hdlr.setMode(mode);
}
+
+ if (mode != AudioSystem.MODE_NORMAL) {
+ clearAllScoClients();
+ }
}
}
int streamType = getActiveStreamType(AudioManager.USE_DEFAULT_STREAM_TYPE);
}
}
+ /** @see AudioManager#startBluetoothSco() */
+ public void startBluetoothSco(IBinder cb){
+ if (!checkAudioSettingsPermission("startBluetoothSco()")) {
+ return;
+ }
+ ScoClient client = getScoClient(cb);
+ client.incCount();
+ }
+
+ /** @see AudioManager#stopBluetoothSco() */
+ public void stopBluetoothSco(IBinder cb){
+ if (!checkAudioSettingsPermission("stopBluetoothSco()")) {
+ return;
+ }
+ ScoClient client = getScoClient(cb);
+ client.decCount();
+ }
+
+ private class ScoClient implements IBinder.DeathRecipient {
+ private IBinder mCb; // To be notified of client's death
+ private int mStartcount; // number of SCO connections started by this client
+
+ ScoClient(IBinder cb) {
+ mCb = cb;
+ mStartcount = 0;
+ }
+
+ public void binderDied() {
+ synchronized(mScoClients) {
+ Log.w(TAG, "SCO client died");
+ int index = mScoClients.indexOf(this);
+ if (index < 0) {
+ Log.w(TAG, "unregistered SCO client died");
+ } else {
+ clearCount(true);
+ mScoClients.remove(this);
+ }
+ }
+ }
+
+ public void incCount() {
+ synchronized(mScoClients) {
+ requestScoState(BluetoothHeadset.AUDIO_STATE_CONNECTED);
+ if (mStartcount == 0) {
+ try {
+ mCb.linkToDeath(this, 0);
+ } catch (RemoteException e) {
+ // client has already died!
+ Log.w(TAG, "ScoClient incCount() could not link to "+mCb+" binder death");
+ }
+ }
+ mStartcount++;
+ }
+ }
+
+ public void decCount() {
+ synchronized(mScoClients) {
+ if (mStartcount == 0) {
+ Log.w(TAG, "ScoClient.decCount() already 0");
+ } else {
+ mStartcount--;
+ if (mStartcount == 0) {
+ mCb.unlinkToDeath(this, 0);
+ }
+ requestScoState(BluetoothHeadset.AUDIO_STATE_DISCONNECTED);
+ }
+ }
+ }
+
+ public void clearCount(boolean stopSco) {
+ synchronized(mScoClients) {
+ mStartcount = 0;
+ mCb.unlinkToDeath(this, 0);
+ if (stopSco) {
+ requestScoState(BluetoothHeadset.AUDIO_STATE_DISCONNECTED);
+ }
+ }
+ }
+
+ public int getCount() {
+ return mStartcount;
+ }
+
+ public IBinder getBinder() {
+ return mCb;
+ }
+
+ public int totalCount() {
+ synchronized(mScoClients) {
+ int count = 0;
+ int size = mScoClients.size();
+ for (int i = 0; i < size; i++) {
+ count += mScoClients.get(i).getCount();
+ }
+ return count;
+ }
+ }
+
+ private void requestScoState(int state) {
+ if (totalCount() == 0 &&
+ mBluetoothHeadsetConnected &&
+ AudioService.this.mMode == AudioSystem.MODE_NORMAL) {
+ if (state == BluetoothHeadset.AUDIO_STATE_CONNECTED) {
+ mBluetoothHeadset.startVoiceRecognition();
+ } else {
+ mBluetoothHeadset.stopVoiceRecognition();
+ }
+ }
+ }
+ }
+
+ public ScoClient getScoClient(IBinder cb) {
+ synchronized(mScoClients) {
+ ScoClient client;
+ int size = mScoClients.size();
+ for (int i = 0; i < size; i++) {
+ client = mScoClients.get(i);
+ if (client.getBinder() == cb)
+ return client;
+ }
+ client = new ScoClient(cb);
+ mScoClients.add(client);
+ return client;
+ }
+ }
+
+ public void clearAllScoClients() {
+ synchronized(mScoClients) {
+ int size = mScoClients.size();
+ for (int i = 0; i < size; i++) {
+ mScoClients.get(i).clearCount(false);
+ }
+ }
+ }
+
+ private BluetoothHeadset.ServiceListener mBluetoothHeadsetServiceListener =
+ new BluetoothHeadset.ServiceListener() {
+ public void onServiceConnected() {
+ if (mBluetoothHeadset != null &&
+ mBluetoothHeadset.getState() == BluetoothHeadset.STATE_CONNECTED) {
+ mBluetoothHeadsetConnected = true;
+ }
+ }
+ public void onServiceDisconnected() {
+ if (mBluetoothHeadset != null &&
+ mBluetoothHeadset.getState() == BluetoothHeadset.STATE_DISCONNECTED) {
+ mBluetoothHeadsetConnected = false;
+ clearAllScoClients();
+ }
+ }
+ };
///////////////////////////////////////////////////////////////////////////
// Internal methods
AudioSystem.DEVICE_STATE_UNAVAILABLE,
address);
mConnectedDevices.remove(device);
+ mBluetoothHeadsetConnected = false;
+ clearAllScoClients();
} else if (!isConnected && state == BluetoothHeadset.STATE_CONNECTED) {
AudioSystem.setDeviceConnectionState(device,
AudioSystem.DEVICE_STATE_AVAILABLE,
address);
mConnectedDevices.put(new Integer(device), address);
+ mBluetoothHeadsetConnected = true;
}
} else if (action.equals(Intent.ACTION_HEADSET_PLUG)) {
int state = intent.getIntExtra("state", 0);
mConnectedDevices.put( new Integer(AudioSystem.DEVICE_OUT_WIRED_HEADPHONE), "");
}
}
+ } else if (action.equals(BluetoothHeadset.ACTION_AUDIO_STATE_CHANGED)) {
+ int state = intent.getIntExtra(BluetoothHeadset.EXTRA_AUDIO_STATE,
+ BluetoothHeadset.STATE_ERROR);
+ synchronized (mScoClients) {
+ if (!mScoClients.isEmpty()) {
+ switch (state) {
+ case BluetoothHeadset.AUDIO_STATE_CONNECTED:
+ state = AudioManager.SCO_AUDIO_STATE_CONNECTED;
+ break;
+ case BluetoothHeadset.AUDIO_STATE_DISCONNECTED:
+ state = AudioManager.SCO_AUDIO_STATE_DISCONNECTED;
+ break;
+ default:
+ state = AudioManager.SCO_AUDIO_STATE_ERROR;
+ break;
+ }
+ if (state != AudioManager.SCO_AUDIO_STATE_ERROR) {
+ Intent newIntent = new Intent(AudioManager.ACTION_SCO_AUDIO_STATE_CHANGED);
+ newIntent.putExtra(AudioManager.EXTRA_SCO_AUDIO_STATE, state);
+ mContext.sendStickyBroadcast(newIntent);
+ }
+ }
+ }
}
}
}