OSDN Git Service

Issue 2416481: Support Voice Dialer over BT SCO.
authorEric Laurent <elaurent@google.com>
Thu, 18 Mar 2010 06:26:26 +0000 (23:26 -0700)
committerEric Laurent <elaurent@google.com>
Thu, 18 Mar 2010 07:26:46 +0000 (00:26 -0700)
Added public methods to AudioManager API so that unbundled applications can use bluetooth
SCO audio when the phone is not incall.
Without this change, the only way to activate and use bluetooth SCO is via the BluetoothHeadset API
which is not public yet.

Change-Id: Ia1680f219ea1d0943092d475d5be7d6638983ebb

media/java/android/media/AudioManager.java
media/java/android/media/AudioService.java
media/java/android/media/IAudioService.aidl

index 4d364ab..3a3c66b 100644 (file)
@@ -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.
+     * <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>
@@ -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);
index 81e17b7..75e51f9 100644 (file)
@@ -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 <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
     ///////////////////////////////////////////////////////////////////////////
@@ -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);
+                        }
+                    }
+                }
             }
         }
     }
index 953892b..384b8da 100644 (file)
@@ -82,4 +82,8 @@ interface IAudioService {
     void registerMediaButtonEventReceiver(in ComponentName eventReceiver);
 
     void unregisterMediaButtonEventReceiver(in ComponentName eventReceiver);
+
+    void startBluetoothSco(IBinder cb);
+
+    void stopBluetoothSco(IBinder cb);
 }