Merge "Remove need for onActivityResult from KeyChain API"
diff --git a/api/current.txt b/api/current.txt
index 8c61922..6854965 100644
--- a/api/current.txt
+++ b/api/current.txt
@@ -9687,7 +9687,8 @@
     method public void unloadSoundEffects();
     method public void unregisterMediaButtonEventReceiver(android.content.ComponentName);
     field public static final java.lang.String ACTION_AUDIO_BECOMING_NOISY = "android.media.AUDIO_BECOMING_NOISY";
-    field public static final java.lang.String ACTION_SCO_AUDIO_STATE_CHANGED = "android.media.SCO_AUDIO_STATE_CHANGED";
+    field public static final deprecated java.lang.String ACTION_SCO_AUDIO_STATE_CHANGED = "android.media.SCO_AUDIO_STATE_CHANGED";
+    field public static final java.lang.String ACTION_SCO_AUDIO_STATE_UPDATED = "android.media.ACTION_SCO_AUDIO_STATE_UPDATED";
     field public static final int ADJUST_LOWER = -1; // 0xffffffff
     field public static final int ADJUST_RAISE = 1; // 0x1
     field public static final int ADJUST_SAME = 0; // 0x0
@@ -9700,6 +9701,7 @@
     field public static final int AUDIOFOCUS_REQUEST_FAILED = 0; // 0x0
     field public static final int AUDIOFOCUS_REQUEST_GRANTED = 1; // 0x1
     field public static final java.lang.String EXTRA_RINGER_MODE = "android.media.EXTRA_RINGER_MODE";
+    field public static final java.lang.String EXTRA_SCO_AUDIO_PREVIOUS_STATE = "android.media.extra.SCO_AUDIO_PREVIOUS_STATE";
     field public static final java.lang.String EXTRA_SCO_AUDIO_STATE = "android.media.extra.SCO_AUDIO_STATE";
     field public static final java.lang.String EXTRA_VIBRATE_SETTING = "android.media.EXTRA_VIBRATE_SETTING";
     field public static final java.lang.String EXTRA_VIBRATE_TYPE = "android.media.EXTRA_VIBRATE_TYPE";
@@ -9736,6 +9738,7 @@
     field public static final deprecated int ROUTE_HEADSET = 8; // 0x8
     field public static final deprecated int ROUTE_SPEAKER = 2; // 0x2
     field public static final int SCO_AUDIO_STATE_CONNECTED = 1; // 0x1
+    field public static final int SCO_AUDIO_STATE_CONNECTING = 2; // 0x2
     field public static final int SCO_AUDIO_STATE_DISCONNECTED = 0; // 0x0
     field public static final int SCO_AUDIO_STATE_ERROR = -1; // 0xffffffff
     field public static final int STREAM_ALARM = 4; // 0x4
diff --git a/core/java/android/speech/tts/PlaybackSynthesisRequest.java b/core/java/android/speech/tts/PlaybackSynthesisRequest.java
index 6f4c15b..d698b54 100644
--- a/core/java/android/speech/tts/PlaybackSynthesisRequest.java
+++ b/core/java/android/speech/tts/PlaybackSynthesisRequest.java
@@ -18,6 +18,7 @@
 import android.media.AudioFormat;
 import android.media.AudioTrack;
 import android.os.Bundle;
+import android.os.Handler;
 import android.util.Log;
 
 /**
@@ -49,16 +50,20 @@
     private final float mPan;
 
     private final Object mStateLock = new Object();
-    private AudioTrack mAudioTrack = null;
+    private final Handler mAudioTrackHandler;
+    private volatile AudioTrack mAudioTrack = null;
     private boolean mStopped = false;
     private boolean mDone = false;
+    private volatile boolean mWriteErrorOccured;
 
     PlaybackSynthesisRequest(String text, Bundle params,
-            int streamType, float volume, float pan) {
+            int streamType, float volume, float pan, Handler audioTrackHandler) {
         super(text, params);
         mStreamType = streamType;
         mVolume = volume;
         mPan = pan;
+        mAudioTrackHandler = audioTrackHandler;
+        mWriteErrorOccured = false;
     }
 
     @Override
@@ -70,14 +75,28 @@
         }
     }
 
+    // Always guarded by mStateLock.
     private void cleanUp() {
         if (DBG) Log.d(TAG, "cleanUp()");
-        if (mAudioTrack != null) {
-            mAudioTrack.flush();
-            mAudioTrack.stop();
-            mAudioTrack.release();
-            mAudioTrack = null;
+        if (mAudioTrack == null) {
+            return;
         }
+
+        final AudioTrack audioTrack = mAudioTrack;
+        mAudioTrack = null;
+
+        // Clean up on the audiotrack handler thread.
+        //
+        // NOTE: It isn't very clear whether AudioTrack is thread safe.
+        // If it is we can clean up on the current (synthesis) thread.
+        mAudioTrackHandler.post(new Runnable() {
+            @Override
+            public void run() {
+                audioTrack.flush();
+                audioTrack.stop();
+                audioTrack.release();
+            }
+        });
     }
 
     @Override
@@ -146,10 +165,15 @@
             Log.d(TAG, "audioAvailable(byte[" + buffer.length + "],"
                     + offset + "," + length + ")");
         }
-        if (length > getMaxBufferSize()) {
-            throw new IllegalArgumentException("buffer is too large (" + length + " bytes)");
+        if (length > getMaxBufferSize() || length <= 0) {
+            throw new IllegalArgumentException("buffer is too large or of zero length (" +
+                    + length + " bytes)");
         }
         synchronized (mStateLock) {
+            if (mWriteErrorOccured) {
+                if (DBG) Log.d(TAG, "Error writing to audio track, count < 0");
+                return TextToSpeech.ERROR;
+            }
             if (mStopped) {
                 if (DBG) Log.d(TAG, "Request has been aborted.");
                 return TextToSpeech.ERROR;
@@ -158,22 +182,33 @@
                 Log.e(TAG, "audioAvailable(): Not started");
                 return TextToSpeech.ERROR;
             }
-            int playState = mAudioTrack.getPlayState();
-            if (playState == AudioTrack.PLAYSTATE_STOPPED) {
-                if (DBG) Log.d(TAG, "AudioTrack stopped, restarting");
-                mAudioTrack.play();
-            }
-            // TODO: loop until all data is written?
-            if (DBG) Log.d(TAG, "AudioTrack.write()");
-            int count = mAudioTrack.write(buffer, offset, length);
-            if (DBG) Log.d(TAG, "AudioTrack.write() returned " + count);
-            if (count < 0) {
-                Log.e(TAG, "Writing to AudioTrack failed: " + count);
-                cleanUp();
-                return TextToSpeech.ERROR;
-            } else {
-                return TextToSpeech.SUCCESS;
-            }
+            final AudioTrack audioTrack = mAudioTrack;
+            // Sigh, another copy.
+            final byte[] bufferCopy = new byte[length];
+            System.arraycopy(buffer, offset, bufferCopy, 0, length);
+
+            mAudioTrackHandler.post(new Runnable() {
+                @Override
+                public void run() {
+                    int playState = audioTrack.getPlayState();
+                    if (playState == AudioTrack.PLAYSTATE_STOPPED) {
+                        if (DBG) Log.d(TAG, "AudioTrack stopped, restarting");
+                        audioTrack.play();
+                    }
+                    // TODO: loop until all data is written?
+                    if (DBG) Log.d(TAG, "AudioTrack.write()");
+                    int count = audioTrack.write(bufferCopy, 0, bufferCopy.length);
+                    // The semantics of this change very slightly. Earlier, we would
+                    // report an error immediately, Now we will return an error on
+                    // the next API call, usually done( ) or another audioAvailable( )
+                    // call.
+                    if (count < 0) {
+                        mWriteErrorOccured = true;
+                    }
+                }
+            });
+
+            return TextToSpeech.SUCCESS;
         }
     }
 
@@ -181,6 +216,10 @@
     public int done() {
         if (DBG) Log.d(TAG, "done()");
         synchronized (mStateLock) {
+            if (mWriteErrorOccured) {
+                if (DBG) Log.d(TAG, "Error writing to audio track, count < 0");
+                return TextToSpeech.ERROR;
+            }
             if (mStopped) {
                 if (DBG) Log.d(TAG, "Request has been aborted.");
                 return TextToSpeech.ERROR;
diff --git a/core/java/android/speech/tts/TextToSpeechService.java b/core/java/android/speech/tts/TextToSpeechService.java
index f32474f..717dde8 100644
--- a/core/java/android/speech/tts/TextToSpeechService.java
+++ b/core/java/android/speech/tts/TextToSpeechService.java
@@ -51,6 +51,7 @@
     private static final String SYNTH_THREAD_NAME = "SynthThread";
 
     private SynthHandler mSynthHandler;
+    private Handler mAudioTrackHandler;
 
     private CallbackMap mCallbacks;
 
@@ -63,6 +64,10 @@
         synthThread.start();
         mSynthHandler = new SynthHandler(synthThread.getLooper());
 
+        HandlerThread audioTrackThread = new HandlerThread("TTS.audioTrackThread");
+        audioTrackThread.start();
+        mAudioTrackHandler = new Handler(audioTrackThread.getLooper());
+
         mCallbacks = new CallbackMap();
 
         // Load default language
@@ -75,6 +80,7 @@
 
         // Tell the synthesizer to stop
         mSynthHandler.quit();
+        mAudioTrackHandler.getLooper().quit();
 
         // Unregister all callbacks.
         mCallbacks.kill();
@@ -444,7 +450,7 @@
 
         protected SynthesisRequest createSynthesisRequest() {
             return new PlaybackSynthesisRequest(mText, mParams,
-                    getStreamType(), getVolume(), getPan());
+                    getStreamType(), getVolume(), getPan(), mAudioTrackHandler);
         }
 
         private void setRequestParams(SynthesisRequest request) {
diff --git a/core/res/res/values/strings.xml b/core/res/res/values/strings.xml
index ccb3518..7f4cc81 100755
--- a/core/res/res/values/strings.xml
+++ b/core/res/res/values/strings.xml
@@ -1852,7 +1852,7 @@
 
     <!-- Do not translate.  WebView User Agent string -->
     <string name="web_user_agent" translatable="false">Mozilla/5.0 (Linux; U; <xliff:g id="x">Android %s</xliff:g>)
-        AppleWebKit/534.16 (KHTML, like Gecko) Version/4.0 <xliff:g id="mobile">%s</xliff:g>Safari/534.16</string>
+        AppleWebKit/534.20 (KHTML, like Gecko) Version/4.0 <xliff:g id="mobile">%s</xliff:g>Safari/534.20</string>
     <!-- Do not translate.  WebView User Agent targeted content -->
     <string name="web_user_agent_target_content" translatable="false">"Mobile "</string>
 
diff --git a/media/java/android/media/AudioManager.java b/media/java/android/media/AudioManager.java
index e1daede..253010c 100644
--- a/media/java/android/media/AudioManager.java
+++ b/media/java/android/media/AudioManager.java
@@ -828,29 +828,64 @@
      * or {@link #SCO_AUDIO_STATE_CONNECTED}
      *
      * @see #startBluetoothSco()
+     * @deprecated Use  {@link #ACTION_SCO_AUDIO_STATE_UPDATED} instead
      */
+    @Deprecated
     @SdkConstant(SdkConstantType.BROADCAST_INTENT_ACTION)
     public static final String ACTION_SCO_AUDIO_STATE_CHANGED =
             "android.media.SCO_AUDIO_STATE_CHANGED";
+
+     /**
+     * Sticky broadcast intent action indicating that the bluetoooth SCO audio
+     * connection state has been updated.
+     * <p>This intent has two extras:
+     * <ul>
+     *   <li> {@link #EXTRA_SCO_AUDIO_STATE} - The new SCO audio state. </li>
+     *   <li> {@link #EXTRA_SCO_AUDIO_PREVIOUS_STATE}- The previous SCO audio state. </li>
+     * </ul>
+     * <p> EXTRA_SCO_AUDIO_STATE or EXTRA_SCO_AUDIO_PREVIOUS_STATE can be any of:
+     * <ul>
+     *   <li> {@link #SCO_AUDIO_STATE_DISCONNECTED}, </li>
+     *   <li> {@link #SCO_AUDIO_STATE_CONNECTING} or </li>
+     *   <li> {@link #SCO_AUDIO_STATE_CONNECTED}, </li>
+     * </ul>
+     * @see #startBluetoothSco()
+     */
+    @SdkConstant(SdkConstantType.BROADCAST_INTENT_ACTION)
+    public static final String ACTION_SCO_AUDIO_STATE_UPDATED =
+            "android.media.ACTION_SCO_AUDIO_STATE_UPDATED";
+
     /**
-     * Extra for intent {@link #ACTION_SCO_AUDIO_STATE_CHANGED} containing the new
-     * bluetooth SCO connection state.
+     * Extra for intent {@link #ACTION_SCO_AUDIO_STATE_CHANGED} or
+     * {@link #ACTION_SCO_AUDIO_STATE_UPDATED} containing the new bluetooth SCO connection state.
      */
     public static final String EXTRA_SCO_AUDIO_STATE =
             "android.media.extra.SCO_AUDIO_STATE";
 
     /**
-     * Value for extra {@link #EXTRA_SCO_AUDIO_STATE} indicating that the
-     * SCO audio channel is not established
+     * Extra for intent {@link #ACTION_SCO_AUDIO_STATE_UPDATED} containing the previous
+     * bluetooth SCO connection state.
+     */
+    public static final String EXTRA_SCO_AUDIO_PREVIOUS_STATE =
+            "android.media.extra.SCO_AUDIO_PREVIOUS_STATE";
+
+    /**
+     * Value for extra EXTRA_SCO_AUDIO_STATE or EXTRA_SCO_AUDIO_PREVIOUS_STATE
+     * indicating that the SCO audio channel is not established
      */
     public static final int SCO_AUDIO_STATE_DISCONNECTED = 0;
     /**
-     * Value for extra {@link #EXTRA_SCO_AUDIO_STATE} indicating that the
-     * SCO audio channel is established
+     * Value for extra {@link #EXTRA_SCO_AUDIO_STATE} or {@link #EXTRA_SCO_AUDIO_PREVIOUS_STATE}
+     * indicating that the SCO audio channel is established
      */
     public static final int SCO_AUDIO_STATE_CONNECTED = 1;
     /**
-     * Value for extra {@link #EXTRA_SCO_AUDIO_STATE} indicating that
+     * Value for extra EXTRA_SCO_AUDIO_STATE or EXTRA_SCO_AUDIO_PREVIOUS_STATE
+     * indicating that the SCO audio channel is being established
+     */
+    public static final int SCO_AUDIO_STATE_CONNECTING = 2;
+    /**
+     * Value for extra EXTRA_SCO_AUDIO_STATE indicating that
      * there was an error trying to obtain the state
      */
     public static final int SCO_AUDIO_STATE_ERROR = -1;
@@ -878,29 +913,37 @@
      * 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}
+     * returns but instead register to receive the intent {@link #ACTION_SCO_AUDIO_STATE_UPDATED}
      * 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>As the ACTION_SCO_AUDIO_STATE_UPDATED intent is sticky, the application can check the SCO
+     * audio state before calling startBluetoothSco() by reading the intent returned by the receiver
+     * registration. If the state is already CONNECTED, no state change will be received via the
+     * intent after calling startBluetoothSco(). It is however useful to call startBluetoothSco()
+     * so that the connection stays active in case the current initiator stops the connection.
+     * <p>Unless the connection is already active as described above, the state will always
+     * transition from DISCONNECTED to CONNECTING and then either to CONNECTED if the connection
+     * succeeds or back to DISCONNECTED if the connection fails (e.g no headset is connected).
+     * <p>When finished with the SCO connection or if the establishment fails, 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}
-     * - the format must be mono
-     * - the sampling must be 16kHz or 8kHz
+     * <ul>
+     *   <li> the stream type must be {@link #STREAM_VOICE_CALL} </li>
+     *   <li> the format must be mono </li>
+     *   <li> the sampling must be 16kHz or 8kHz </li>
+     * </ul>
      * <p>The following restrictions apply on input streams:
-     * - the format must be mono
-     * - the sampling must be 8kHz
-     *
+     * <ul>
+     *   <li> the format must be mono </li>
+     *   <li> the sampling must be 8kHz </li>
+     * </ul>
      * <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
+     * @see #ACTION_SCO_AUDIO_STATE_UPDATED
      */
     public void startBluetoothSco(){
         IAudioService service = getService();
@@ -917,7 +960,7 @@
      *   {@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.
+     * when finished with the SCO connection or if connection fails.
      * @see #startBluetoothSco()
      */
     public void stopBluetoothSco(){
diff --git a/media/java/android/media/AudioService.java b/media/java/android/media/AudioService.java
index 504cfde..4e77fcb 100644
--- a/media/java/android/media/AudioService.java
+++ b/media/java/android/media/AudioService.java
@@ -112,9 +112,12 @@
     private static final int MSG_LOAD_SOUND_EFFECTS = 9;
     private static final int MSG_SET_FORCE_USE = 10;
     private static final int MSG_PERSIST_MEDIABUTTONRECEIVER = 11;
-
+    private static final int MSG_BT_HEADSET_CNCT_FAILED = 12;
 
     private static final int BTA2DP_DOCK_TIMEOUT_MILLIS = 8000;
+    // Timeout for connection to bluetooth headset service
+    private static final int BT_HEADSET_CNCT_TIMEOUT_MS = 3000;
+
 
     /** @see AudioSystemThread */
     private AudioSystemThread mAudioSystemThread;
@@ -273,11 +276,22 @@
     private int mScoAudioState;
     // SCO audio state is not active
     private static final int SCO_STATE_INACTIVE = 0;
+    // SCO audio activation request waiting for headset service to connect
+    private static final int SCO_STATE_ACTIVATE_REQ = 1;
     // SCO audio state is active or starting due to a local request to start a virtual call
-    private static final int SCO_STATE_ACTIVE_INTERNAL = 1;
+    private static final int SCO_STATE_ACTIVE_INTERNAL = 3;
+    // SCO audio deactivation request waiting for headset service to connect
+    private static final int SCO_STATE_DEACTIVATE_REQ = 5;
+
     // SCO audio state is active due to an action in BT handsfree (either voice recognition or
     // in call audio)
     private static final int SCO_STATE_ACTIVE_EXTERNAL = 2;
+    // Deactivation request for all SCO connections (initiated by audio mode change)
+    // waiting for headset service to connect
+    private static final int SCO_STATE_DEACTIVATE_EXT_REQ = 4;
+
+    // Current connection state indicated by bluetooth headset
+    private int mScoConnectionState;
 
     // true if boot sequence has been completed
     private boolean mBootCompleted;
@@ -335,13 +349,6 @@
 
         AudioSystem.setErrorCallback(mAudioSystemCallback);
 
-        mBluetoothHeadsetDevice = null;
-        BluetoothAdapter adapter = BluetoothAdapter.getDefaultAdapter();
-        if (adapter != null) {
-            adapter.getProfileProxy(mContext, mBluetoothProfileServiceListener,
-                                    BluetoothProfile.HEADSET);
-        }
-
         // Register for device connection intent broadcasts.
         IntentFilter intentFilter =
                 new IntentFilter(Intent.ACTION_HEADSET_PLUG);
@@ -768,17 +775,7 @@
                             if (AudioSystem.setPhoneState(mode) == AudioSystem.AUDIO_STATUS_OK) {
                                 AudioService.this.mMode = mode;
                                 if (mode != AudioSystem.MODE_NORMAL) {
-                                    synchronized(mScoClients) {
-                                        checkScoAudioState();
-                                        if (mScoAudioState == SCO_STATE_ACTIVE_EXTERNAL) {
-                                            mBluetoothHeadset.stopVoiceRecognition(
-                                                    mBluetoothHeadsetDevice);
-                                            mBluetoothHeadset.stopScoUsingVirtualVoiceCall(
-                                                    mBluetoothHeadsetDevice);
-                                        } else {
-                                            clearAllScoClients(mCb, true);
-                                        }
-                                    }
+                                    disconnectBluetoothSco(mCb);
                                 }
                             }
                         }
@@ -856,16 +853,7 @@
                     // when entering RINGTONE, IN_CALL or IN_COMMUNICATION mode, clear all
                     // SCO connections not started by the application changing the mode
                     if (mode != AudioSystem.MODE_NORMAL) {
-                        synchronized(mScoClients) {
-                            checkScoAudioState();
-                            if (mScoAudioState == SCO_STATE_ACTIVE_EXTERNAL) {
-                                mBluetoothHeadset.stopVoiceRecognition(mBluetoothHeadsetDevice);
-                                mBluetoothHeadset.stopScoUsingVirtualVoiceCall(
-                                    mBluetoothHeadsetDevice);
-                            } else {
-                                clearAllScoClients(cb, true);
-                            }
-                        }
+                        disconnectBluetoothSco(cb);
                     }
                 }
             }
@@ -1213,7 +1201,8 @@
 
     /** @see AudioManager#startBluetoothSco() */
     public void startBluetoothSco(IBinder cb){
-        if (!checkAudioSettingsPermission("startBluetoothSco()")) {
+        if (!checkAudioSettingsPermission("startBluetoothSco()") ||
+                !mBootCompleted) {
             return;
         }
         ScoClient client = getScoClient(cb, true);
@@ -1222,7 +1211,8 @@
 
     /** @see AudioManager#stopBluetoothSco() */
     public void stopBluetoothSco(IBinder cb){
-        if (!checkAudioSettingsPermission("stopBluetoothSco()")) {
+        if (!checkAudioSettingsPermission("stopBluetoothSco()") ||
+                !mBootCompleted) {
             return;
         }
         ScoClient client = getScoClient(cb, false);
@@ -1322,25 +1312,57 @@
         }
 
         private void requestScoState(int state) {
-            if (mBluetoothHeadset == null) {
-                return;
-            }
-
             checkScoAudioState();
-
-            if (totalCount() == 0 &&
-                mBluetoothHeadsetDevice != null) {
-                // Accept SCO audio activation only in NORMAL audio mode or if the mode is
-                // currently controlled by the same client.
-                if ((AudioService.this.mMode == AudioSystem.MODE_NORMAL ||
-                        mSetModeDeathHandlers.get(0).getBinder() == mCb) &&
-                        state == BluetoothHeadset.STATE_AUDIO_CONNECTED &&
-                        mScoAudioState == SCO_STATE_INACTIVE) {
-                    mScoAudioState = SCO_STATE_ACTIVE_INTERNAL;
-                    mBluetoothHeadset.startScoUsingVirtualVoiceCall(mBluetoothHeadsetDevice);
+            if (totalCount() == 0) {
+                if (state == BluetoothHeadset.STATE_AUDIO_CONNECTED) {
+                    // Make sure that the state transitions to CONNECTING even if we cannot initiate
+                    // the connection.
+                    broadcastScoConnectionState(AudioManager.SCO_AUDIO_STATE_CONNECTING);
+                    // Accept SCO audio activation only in NORMAL audio mode or if the mode is
+                    // currently controlled by the same client.
+                    if ((AudioService.this.mMode == AudioSystem.MODE_NORMAL ||
+                            mSetModeDeathHandlers.get(0).getBinder() == mCb) &&
+                            mBluetoothHeadsetDevice != null &&
+                            (mScoAudioState == SCO_STATE_INACTIVE ||
+                             mScoAudioState == SCO_STATE_DEACTIVATE_REQ)) {
+                        if (mScoAudioState == SCO_STATE_INACTIVE) {
+                            if (mBluetoothHeadset != null) {
+                                if (mBluetoothHeadset.startScoUsingVirtualVoiceCall(
+                                        mBluetoothHeadsetDevice)) {
+                                    mScoAudioState = SCO_STATE_ACTIVE_INTERNAL;
+                                } else {
+                                    broadcastScoConnectionState(
+                                            AudioManager.SCO_AUDIO_STATE_DISCONNECTED);
+                                }
+                            } else if (getBluetoothHeadset()) {
+                                mScoAudioState = SCO_STATE_ACTIVATE_REQ;
+                            }
+                        } else {
+                            mScoAudioState = SCO_STATE_ACTIVE_INTERNAL;
+                            broadcastScoConnectionState(AudioManager.SCO_AUDIO_STATE_CONNECTED);
+                        }
+                    } else {
+                        broadcastScoConnectionState(AudioManager.SCO_AUDIO_STATE_DISCONNECTED);
+                    }
                 } else if (state == BluetoothHeadset.STATE_AUDIO_DISCONNECTED &&
-                        mScoAudioState == SCO_STATE_ACTIVE_INTERNAL){
-                    mBluetoothHeadset.stopScoUsingVirtualVoiceCall(mBluetoothHeadsetDevice);
+                              mBluetoothHeadsetDevice != null &&
+                              (mScoAudioState == SCO_STATE_ACTIVE_INTERNAL ||
+                               mScoAudioState == SCO_STATE_ACTIVATE_REQ)) {
+                    if (mScoAudioState == SCO_STATE_ACTIVE_INTERNAL) {
+                        if (mBluetoothHeadset != null) {
+                            if (!mBluetoothHeadset.stopScoUsingVirtualVoiceCall(
+                                    mBluetoothHeadsetDevice)) {
+                                mScoAudioState = SCO_STATE_INACTIVE;
+                                broadcastScoConnectionState(
+                                        AudioManager.SCO_AUDIO_STATE_DISCONNECTED);
+                            }
+                        } else if (getBluetoothHeadset()) {
+                            mScoAudioState = SCO_STATE_DEACTIVATE_REQ;
+                        }
+                    } else {
+                        mScoAudioState = SCO_STATE_INACTIVE;
+                        broadcastScoConnectionState(AudioManager.SCO_AUDIO_STATE_DISCONNECTED);
+                    }
                 }
             }
         }
@@ -1348,7 +1370,7 @@
 
     private void checkScoAudioState() {
         if (mBluetoothHeadset != null && mBluetoothHeadsetDevice != null &&
-                mScoAudioState != SCO_STATE_ACTIVE_INTERNAL &&
+                mScoAudioState == SCO_STATE_INACTIVE &&
                 mBluetoothHeadset.getAudioState(mBluetoothHeadsetDevice)
                 != BluetoothHeadset.STATE_AUDIO_DISCONNECTED) {
             mScoAudioState = SCO_STATE_ACTIVE_EXTERNAL;
@@ -1391,10 +1413,72 @@
         }
     }
 
+    private boolean getBluetoothHeadset() {
+        boolean result = false;
+        BluetoothAdapter adapter = BluetoothAdapter.getDefaultAdapter();
+        if (adapter != null) {
+            result = adapter.getProfileProxy(mContext, mBluetoothProfileServiceListener,
+                                    BluetoothProfile.HEADSET);
+        }
+        // If we could not get a bluetooth headset proxy, send a failure message
+        // without delay to reset the SCO audio state and clear SCO clients.
+        // If we could get a proxy, send a delayed failure message that will reset our state
+        // in case we don't receive onServiceConnected().
+        sendMsg(mAudioHandler, MSG_BT_HEADSET_CNCT_FAILED, 0,
+                SENDMSG_REPLACE, 0, 0, null, result ? BT_HEADSET_CNCT_TIMEOUT_MS : 0);
+        return result;
+    }
+
+    private void disconnectBluetoothSco(IBinder exceptBinder) {
+        synchronized(mScoClients) {
+            checkScoAudioState();
+            if (mScoAudioState == SCO_STATE_ACTIVE_EXTERNAL ||
+                    mScoAudioState == SCO_STATE_DEACTIVATE_EXT_REQ) {
+                if (mBluetoothHeadsetDevice != null) {
+                    if (mBluetoothHeadset != null) {
+                        if (!mBluetoothHeadset.stopVoiceRecognition(
+                                mBluetoothHeadsetDevice) ||
+                                !mBluetoothHeadset.stopScoUsingVirtualVoiceCall(
+                                        mBluetoothHeadsetDevice)) {
+                            sendMsg(mAudioHandler, MSG_BT_HEADSET_CNCT_FAILED, 0,
+                                    SENDMSG_REPLACE, 0, 0, null, 0);
+                        }
+                    } else if (mScoAudioState == SCO_STATE_ACTIVE_EXTERNAL &&
+                            getBluetoothHeadset()) {
+                        mScoAudioState = SCO_STATE_DEACTIVATE_EXT_REQ;
+                    }
+                }
+            } else {
+                clearAllScoClients(exceptBinder, true);
+            }
+        }
+    }
+
+    private void resetBluetoothSco() {
+        synchronized(mScoClients) {
+            clearAllScoClients(null, false);
+            mScoAudioState = SCO_STATE_INACTIVE;
+            broadcastScoConnectionState(AudioManager.SCO_AUDIO_STATE_DISCONNECTED);
+        }
+    }
+
+    private void broadcastScoConnectionState(int state) {
+        if (state != mScoConnectionState) {
+            Intent newIntent = new Intent(AudioManager.ACTION_SCO_AUDIO_STATE_UPDATED);
+            newIntent.putExtra(AudioManager.EXTRA_SCO_AUDIO_STATE, state);
+            newIntent.putExtra(AudioManager.EXTRA_SCO_AUDIO_PREVIOUS_STATE,
+                    mScoConnectionState);
+            mContext.sendStickyBroadcast(newIntent);
+            mScoConnectionState = state;
+        }
+    }
+
     private BluetoothProfile.ServiceListener mBluetoothProfileServiceListener =
         new BluetoothProfile.ServiceListener() {
         public void onServiceConnected(int profile, BluetoothProfile proxy) {
             synchronized (mScoClients) {
+                // Discard timeout message
+                mAudioHandler.removeMessages(MSG_BT_HEADSET_CNCT_FAILED);
                 mBluetoothHeadset = (BluetoothHeadset) proxy;
                 List<BluetoothDevice> deviceList = mBluetoothHeadset.getConnectedDevices();
                 if (deviceList.size() > 0) {
@@ -1402,19 +1486,41 @@
                 } else {
                     mBluetoothHeadsetDevice = null;
                 }
+                // Refresh SCO audio state
+                checkScoAudioState();
+                // Continue pending action if any
+                if (mScoAudioState == SCO_STATE_ACTIVATE_REQ ||
+                        mScoAudioState == SCO_STATE_DEACTIVATE_REQ ||
+                        mScoAudioState == SCO_STATE_DEACTIVATE_EXT_REQ) {
+                    boolean status = false;
+                    if (mBluetoothHeadsetDevice != null) {
+                        switch (mScoAudioState) {
+                        case SCO_STATE_ACTIVATE_REQ:
+                            mScoAudioState = SCO_STATE_ACTIVE_INTERNAL;
+                            status = mBluetoothHeadset.startScoUsingVirtualVoiceCall(
+                                    mBluetoothHeadsetDevice);
+                            break;
+                        case SCO_STATE_DEACTIVATE_REQ:
+                            status = mBluetoothHeadset.stopScoUsingVirtualVoiceCall(
+                                    mBluetoothHeadsetDevice);
+                            break;
+                        case SCO_STATE_DEACTIVATE_EXT_REQ:
+                            status = mBluetoothHeadset.stopVoiceRecognition(
+                                    mBluetoothHeadsetDevice) &&
+                                    mBluetoothHeadset.stopScoUsingVirtualVoiceCall(
+                                    mBluetoothHeadsetDevice);
+                        }
+                    }
+                    if (!status) {
+                        sendMsg(mAudioHandler, MSG_BT_HEADSET_CNCT_FAILED, 0,
+                                SENDMSG_REPLACE, 0, 0, null, 0);
+                    }
+                }
             }
         }
         public void onServiceDisconnected(int profile) {
             synchronized (mScoClients) {
-                if (mBluetoothHeadset != null) {
-                    List<BluetoothDevice> devices = mBluetoothHeadset.getConnectedDevices();
-                    if (devices.size() == 0) {
-                        mBluetoothHeadsetDevice = null;
-                        clearAllScoClients(null, false);
-                        mScoAudioState = SCO_STATE_INACTIVE;
-                    }
-                    mBluetoothHeadset = null;
-                }
+                mBluetoothHeadset = null;
             }
         }
     };
@@ -2041,6 +2147,10 @@
                 case MSG_PERSIST_MEDIABUTTONRECEIVER:
                     persistMediaButtonReceiver( (ComponentName) msg.obj );
                     break;
+
+                case MSG_BT_HEADSET_CNCT_FAILED:
+                    resetBluetoothSco();
+                    break;
             }
         }
     }
@@ -2241,8 +2351,7 @@
                                                              address);
                         mConnectedDevices.remove(device);
                         mBluetoothHeadsetDevice = null;
-                        clearAllScoClients(null, false);
-                        mScoAudioState = SCO_STATE_INACTIVE;
+                        resetBluetoothSco();
                     } else if (!isConnected && state == BluetoothProfile.STATE_CONNECTED) {
                         AudioSystem.setDeviceConnectionState(device,
                                                              AudioSystem.DEVICE_STATE_AVAILABLE,
@@ -2332,26 +2441,34 @@
                 }
             } else if (action.equals(BluetoothHeadset.ACTION_AUDIO_STATE_CHANGED)) {
                 boolean broadcast = false;
-                int audioState = AudioManager.SCO_AUDIO_STATE_ERROR;
+                int state = AudioManager.SCO_AUDIO_STATE_ERROR;
                 synchronized (mScoClients) {
                     int btState = intent.getIntExtra(BluetoothProfile.EXTRA_STATE, -1);
-                    if (!mScoClients.isEmpty() && mScoAudioState == SCO_STATE_ACTIVE_INTERNAL) {
+                    // broadcast intent if the connection was initated by AudioService
+                    if (!mScoClients.isEmpty() &&
+                            (mScoAudioState == SCO_STATE_ACTIVE_INTERNAL ||
+                             mScoAudioState == SCO_STATE_ACTIVATE_REQ ||
+                             mScoAudioState == SCO_STATE_DEACTIVATE_REQ)) {
                         broadcast = true;
                     }
                     switch (btState) {
                     case BluetoothHeadset.STATE_AUDIO_CONNECTED:
-                        audioState = AudioManager.SCO_AUDIO_STATE_CONNECTED;
-                        if (mScoAudioState != SCO_STATE_ACTIVE_INTERNAL) {
+                        state = AudioManager.SCO_AUDIO_STATE_CONNECTED;
+                        if (mScoAudioState != SCO_STATE_ACTIVE_INTERNAL &&
+                            mScoAudioState != SCO_STATE_DEACTIVATE_REQ &&
+                            mScoAudioState != SCO_STATE_DEACTIVATE_EXT_REQ) {
                             mScoAudioState = SCO_STATE_ACTIVE_EXTERNAL;
                         }
                         break;
                     case BluetoothHeadset.STATE_AUDIO_DISCONNECTED:
-                        audioState = AudioManager.SCO_AUDIO_STATE_DISCONNECTED;
+                        state = AudioManager.SCO_AUDIO_STATE_DISCONNECTED;
                         mScoAudioState = SCO_STATE_INACTIVE;
                         clearAllScoClients(null, false);
                         break;
                     case BluetoothHeadset.STATE_AUDIO_CONNECTING:
-                        if (mScoAudioState != SCO_STATE_ACTIVE_INTERNAL) {
+                        if (mScoAudioState != SCO_STATE_ACTIVE_INTERNAL &&
+                            mScoAudioState != SCO_STATE_DEACTIVATE_REQ &&
+                            mScoAudioState != SCO_STATE_DEACTIVATE_EXT_REQ) {
                             mScoAudioState = SCO_STATE_ACTIVE_EXTERNAL;
                         }
                     default:
@@ -2361,14 +2478,23 @@
                     }
                 }
                 if (broadcast) {
+                    broadcastScoConnectionState(state);
+                    //FIXME: this is to maintain compatibility with deprecated intent
+                    // AudioManager.ACTION_SCO_AUDIO_STATE_CHANGED. Remove when appropriate.
                     Intent newIntent = new Intent(AudioManager.ACTION_SCO_AUDIO_STATE_CHANGED);
-                    newIntent.putExtra(AudioManager.EXTRA_SCO_AUDIO_STATE, audioState);
+                    newIntent.putExtra(AudioManager.EXTRA_SCO_AUDIO_STATE, state);
                     mContext.sendStickyBroadcast(newIntent);
                 }
             } else if (action.equals(Intent.ACTION_BOOT_COMPLETED)) {
                 mBootCompleted = true;
                 sendMsg(mAudioHandler, MSG_LOAD_SOUND_EFFECTS, SHARED_MSG, SENDMSG_NOOP,
                         0, 0, null, 0);
+
+                mScoConnectionState = AudioManager.SCO_AUDIO_STATE_ERROR;
+                resetBluetoothSco();
+                getBluetoothHeadset();
+                //FIXME: this is to maintain compatibility with deprecated intent
+                // AudioManager.ACTION_SCO_AUDIO_STATE_CHANGED. Remove when appropriate.
                 Intent newIntent = new Intent(AudioManager.ACTION_SCO_AUDIO_STATE_CHANGED);
                 newIntent.putExtra(AudioManager.EXTRA_SCO_AUDIO_STATE,
                         AudioManager.SCO_AUDIO_STATE_DISCONNECTED);