diff --git a/media/java/android/media/AudioManager.java b/media/java/android/media/AudioManager.java index 4d364ab7044af..3a3c66b6c2ad0 100644 --- a/media/java/android/media/AudioManager.java +++ b/media/java/android/media/AudioManager.java @@ -690,6 +690,132 @@ public class AudioManager { } } + //==================================================================== + // 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. + *

Requires Permission: + * {@link android.Manifest.permission#MODIFY_AUDIO_SETTINGS}. + *

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. + *

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}. + *

As the connection is not guaranteed to succeed, applications must wait for this intent with + * a timeout. + *

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. + *

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 + *

The following restrictions apply on input streams: + * - the format must be mono + * - the sampling must be 8kHz + * + *

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. + *

Requires Permission: + * {@link android.Manifest.permission#MODIFY_AUDIO_SETTINGS}. + *

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. *

@@ -1171,7 +1297,7 @@ public class AudioManager { * 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); diff --git a/media/java/android/media/AudioService.java b/media/java/android/media/AudioService.java index 81e17b7f22255..75e51f9b8d923 100644 --- a/media/java/android/media/AudioService.java +++ b/media/java/android/media/AudioService.java @@ -240,6 +240,15 @@ public class AudioService extends IAudioService.Stub { // The last process to have called setMode() is at the top of the list. private ArrayList mSetModeDeathHandlers = new ArrayList (); + // List of clients having issued a SCO start request + private ArrayList mScoClients = new ArrayList (); + + // BluetoothHeadset API to control SCO connection + private BluetoothHeadset mBluetoothHeadset; + + // Bluetooth headset connection state + private boolean mBluetoothHeadsetConnected; + /////////////////////////////////////////////////////////////////////////// // Construction /////////////////////////////////////////////////////////////////////////// @@ -267,12 +276,17 @@ public class AudioService extends IAudioService.Stub { 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. @@ -705,6 +719,10 @@ public class AudioService extends IAudioService.Stub { mSetModeDeathHandlers.add(0, hdlr); hdlr.setMode(mode); } + + if (mode != AudioSystem.MODE_NORMAL) { + clearAllScoClients(); + } } } int streamType = getActiveStreamType(AudioManager.USE_DEFAULT_STREAM_TYPE); @@ -909,6 +927,157 @@ public class AudioService extends IAudioService.Stub { } } + /** @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 @@ -1577,11 +1746,14 @@ public class AudioService extends IAudioService.Stub { 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); @@ -1614,6 +1786,29 @@ public class AudioService extends IAudioService.Stub { 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); + } + } + } } } } diff --git a/media/java/android/media/IAudioService.aidl b/media/java/android/media/IAudioService.aidl index 953892b644839..384b8da9d3f2e 100644 --- a/media/java/android/media/IAudioService.aidl +++ b/media/java/android/media/IAudioService.aidl @@ -82,4 +82,8 @@ interface IAudioService { void registerMediaButtonEventReceiver(in ComponentName eventReceiver); void unregisterMediaButtonEventReceiver(in ComponentName eventReceiver); + + void startBluetoothSco(IBinder cb); + + void stopBluetoothSco(IBinder cb); }