Serialize all commands for a particular profile.

Change-Id: I843ea9ab0bb2372c8316e99e8c083a9939ad774a
diff --git a/core/java/android/bluetooth/BluetoothProfileConnectionState.java b/core/java/android/bluetooth/BluetoothDeviceProfileState.java
similarity index 93%
rename from core/java/android/bluetooth/BluetoothProfileConnectionState.java
rename to core/java/android/bluetooth/BluetoothDeviceProfileState.java
index a58b858..8e655e2 100644
--- a/core/java/android/bluetooth/BluetoothProfileConnectionState.java
+++ b/core/java/android/bluetooth/BluetoothDeviceProfileState.java
@@ -1,5 +1,5 @@
 /*
- * Copyright (C) 2009 The Android Open Source Project
+ * Copyright (C) 2010 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.
@@ -54,8 +54,8 @@
  * Todo(): Write tests for this class, when the Android Mock support is completed.
  * @hide
  */
-public final class BluetoothProfileConnectionState extends HierarchicalStateMachine {
-    private static final String TAG = "BluetoothProfileConnectionState";
+public final class BluetoothDeviceProfileState extends HierarchicalStateMachine {
+    private static final String TAG = "BluetoothDeviceProfileState";
     private static final boolean DBG = true; //STOPSHIP - Change to false
 
     public static final int CONNECT_HFP_OUTGOING = 1;
@@ -72,7 +72,7 @@
     public static final int AUTO_CONNECT_PROFILES = 10;
     public static final int TRANSITION_TO_STABLE = 11;
 
-    private static final int AUTO_CONNECT_DELAY = 8000; // 8 secs
+    private static final int AUTO_CONNECT_DELAY = 6000; // 6 secs
 
     private BondedDevice mBondedDevice = new BondedDevice();
     private OutgoingHandsfree mOutgoingHandsfree = new OutgoingHandsfree();
@@ -137,7 +137,22 @@
       }
     };
 
-    public BluetoothProfileConnectionState(Context context, String address,
+    private boolean isPhoneDocked(BluetoothDevice autoConnectDevice) {
+      // This works only because these broadcast intents are "sticky"
+      Intent i = mContext.registerReceiver(null, new IntentFilter(Intent.ACTION_DOCK_EVENT));
+      if (i != null) {
+          int state = i.getIntExtra(Intent.EXTRA_DOCK_STATE, Intent.EXTRA_DOCK_STATE_UNDOCKED);
+          if (state != Intent.EXTRA_DOCK_STATE_UNDOCKED) {
+              BluetoothDevice device = i.getParcelableExtra(BluetoothDevice.EXTRA_DEVICE);
+              if (device != null && autoConnectDevice.equals(device)) {
+                  return true;
+              }
+          }
+      }
+      return false;
+  }
+
+    public BluetoothDeviceProfileState(Context context, String address,
           BluetoothService service, BluetoothA2dpService a2dpService) {
         super(address);
         mContext = context;
@@ -168,12 +183,12 @@
             mHeadsetService = new BluetoothHeadset(mContext, this);
         }
         public void onServiceConnected() {
-            synchronized(BluetoothProfileConnectionState.this) {
+            synchronized(BluetoothDeviceProfileState.this) {
                 mHeadsetServiceConnected = true;
             }
         }
         public void onServiceDisconnected() {
-            synchronized(BluetoothProfileConnectionState.this) {
+            synchronized(BluetoothDeviceProfileState.this) {
                 mHeadsetServiceConnected = false;
             }
         }
@@ -222,16 +237,21 @@
                     processCommand(UNPAIR);
                     break;
                 case AUTO_CONNECT_PROFILES:
-                    if (!mHeadsetServiceConnected) {
+                    if (isPhoneDocked(mDevice)) {
+                        // Don't auto connect to docks.
+                        break;
+                    } else if (!mHeadsetServiceConnected) {
                         deferMessage(message);
                     } else {
                         if (mHeadsetService.getPriority(mDevice) ==
-                            BluetoothHeadset.PRIORITY_AUTO_CONNECT) {
+                              BluetoothHeadset.PRIORITY_AUTO_CONNECT &&
+                              !mHeadsetService.isConnected(mDevice)) {
                             mHeadsetService.connectHeadset(mDevice);
                         }
                         if (mA2dpService != null &&
-                            mA2dpService.getSinkPriority(mDevice) ==
-                              BluetoothA2dp.PRIORITY_AUTO_CONNECT) {
+                              mA2dpService.getSinkPriority(mDevice) ==
+                              BluetoothA2dp.PRIORITY_AUTO_CONNECT &&
+                              mA2dpService.getConnectedSinks().length == 0) {
                             mA2dpService.connectSink(mDevice);
                         }
                     }
@@ -633,6 +653,10 @@
         return false;
     }
 
+    /*package*/ BluetoothDevice getDevice() {
+        return mDevice;
+    }
+
     private void log(String message) {
         if (DBG) {
             Log.i(TAG, "Device:" + mDevice + " Message:" + message);
diff --git a/core/java/android/bluetooth/BluetoothProfileState.java b/core/java/android/bluetooth/BluetoothProfileState.java
new file mode 100644
index 0000000..946dcaa
--- /dev/null
+++ b/core/java/android/bluetooth/BluetoothProfileState.java
@@ -0,0 +1,144 @@
+/*
+ * Copyright (C) 2010 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.
+ */
+package android.bluetooth;
+
+import android.content.BroadcastReceiver;
+import android.content.Context;
+import android.content.Intent;
+import android.content.IntentFilter;
+import android.os.Message;
+import android.util.Log;
+
+import com.android.internal.util.HierarchicalState;
+import com.android.internal.util.HierarchicalStateMachine;
+
+/**
+ * This state machine is used to serialize the connections
+ * to a particular profile. Currently, we only allow one device
+ * to be connected to a particular profile.
+ * States:
+ *      {@link StableState} : No pending commands. Send the
+ *      command to the appropriate remote device specific state machine.
+ *
+ *      {@link PendingCommandState} : A profile connection / disconnection
+ *      command is being executed. This will result in a profile state
+ *      change. Defer all commands.
+ * @hide
+ */
+
+public class BluetoothProfileState extends HierarchicalStateMachine {
+    private static final boolean DBG = true; // STOPSHIP - change to false.
+    private static final String TAG = "BluetoothProfileState";
+
+    public static int HFP = 0;
+    public static int A2DP = 1;
+
+    private static int TRANSITION_TO_STABLE = 100;
+
+    private int mProfile;
+    private BluetoothDevice mPendingDevice;
+    private PendingCommandState mPendingCommandState = new PendingCommandState();
+    private StableState mStableState = new StableState();
+
+    private BroadcastReceiver mBroadcastReceiver = new BroadcastReceiver() {
+        @Override
+        public void onReceive(Context context, Intent intent) {
+            String action = intent.getAction();
+
+            if (action.equals(BluetoothHeadset.ACTION_STATE_CHANGED)) {
+                int newState = intent.getIntExtra(BluetoothHeadset.EXTRA_STATE, 0);
+                if (mProfile == HFP && (newState == BluetoothHeadset.STATE_CONNECTED ||
+                    newState == BluetoothHeadset.STATE_DISCONNECTED)) {
+                    sendMessage(TRANSITION_TO_STABLE);
+                }
+            } else if (action.equals(BluetoothA2dp.ACTION_SINK_STATE_CHANGED)) {
+                int newState = intent.getIntExtra(BluetoothA2dp.EXTRA_SINK_STATE, 0);
+                if (mProfile == A2DP && (newState == BluetoothA2dp.STATE_CONNECTED ||
+                    newState == BluetoothA2dp.STATE_DISCONNECTED)) {
+                    sendMessage(TRANSITION_TO_STABLE);
+                }
+            }
+        }
+    };
+
+    public BluetoothProfileState(Context context, int profile) {
+        super("BluetoothProfileState:" + profile);
+        mProfile = profile;
+        addState(mStableState);
+        addState(mPendingCommandState);
+        setInitialState(mStableState);
+
+        IntentFilter filter = new IntentFilter();
+        filter.addAction(BluetoothA2dp.ACTION_SINK_STATE_CHANGED);
+        filter.addAction(BluetoothHeadset.ACTION_STATE_CHANGED);
+        context.registerReceiver(mBroadcastReceiver, filter);
+    }
+
+    private class StableState extends HierarchicalState {
+        @Override
+        protected void enter() {
+            log("Entering Stable State");
+            mPendingDevice = null;
+        }
+
+        @Override
+        protected boolean processMessage(Message msg) {
+            if (msg.what != TRANSITION_TO_STABLE) {
+                transitionTo(mPendingCommandState);
+            }
+            return true;
+        }
+    }
+
+    private class PendingCommandState extends HierarchicalState {
+        @Override
+        protected void enter() {
+            log("Entering PendingCommandState State");
+            dispatchMessage(getCurrentMessage());
+        }
+
+        @Override
+        protected boolean processMessage(Message msg) {
+            if (msg.what == TRANSITION_TO_STABLE) {
+                transitionTo(mStableState);
+            } else {
+                dispatchMessage(msg);
+            }
+            return true;
+        }
+
+        private void dispatchMessage(Message msg) {
+            BluetoothDeviceProfileState deviceProfileMgr =
+              (BluetoothDeviceProfileState)msg.obj;
+            int cmd = msg.arg1;
+            if (mPendingDevice == null || mPendingDevice.equals(deviceProfileMgr.getDevice())) {
+                mPendingDevice = deviceProfileMgr.getDevice();
+                deviceProfileMgr.sendMessage(cmd);
+            } else {
+                Message deferMsg = new Message();
+                deferMsg.arg1 = cmd;
+                deferMsg.obj = deviceProfileMgr;
+                deferMessage(deferMsg);
+            }
+        }
+    }
+
+    private void log(String message) {
+        if (DBG) {
+            Log.i(TAG, "Message:" + message);
+        }
+    }
+}
diff --git a/core/java/android/server/BluetoothService.java b/core/java/android/server/BluetoothService.java
index 6720145..31e5a7b 100644
--- a/core/java/android/server/BluetoothService.java
+++ b/core/java/android/server/BluetoothService.java
@@ -28,7 +28,8 @@
 import android.bluetooth.BluetoothClass;
 import android.bluetooth.BluetoothDevice;
 import android.bluetooth.BluetoothHeadset;
-import android.bluetooth.BluetoothProfileConnectionState;
+import android.bluetooth.BluetoothDeviceProfileState;
+import android.bluetooth.BluetoothProfileState;
 import android.bluetooth.BluetoothSocket;
 import android.bluetooth.BluetoothUuid;
 import android.bluetooth.IBluetooth;
@@ -123,7 +124,9 @@
 
     private final HashMap<Integer, Integer> mServiceRecordToPid;
 
-    private final HashMap<String, BluetoothProfileConnectionState> mProfileConnectionMgr;
+    private final HashMap<String, BluetoothDeviceProfileState> mDeviceProfileState;
+    private final BluetoothProfileState mA2dpProfileState;
+    private final BluetoothProfileState mHfpProfileState;
 
     private BluetoothA2dpService mA2dpService;
     private static String mDockAddress;
@@ -183,7 +186,12 @@
         mUuidIntentTracker = new ArrayList<String>();
         mUuidCallbackTracker = new HashMap<RemoteService, IBluetoothCallback>();
         mServiceRecordToPid = new HashMap<Integer, Integer>();
-        mProfileConnectionMgr = new HashMap<String, BluetoothProfileConnectionState>();
+        mDeviceProfileState = new HashMap<String, BluetoothDeviceProfileState>();
+        mA2dpProfileState = new BluetoothProfileState(mContext, BluetoothProfileState.A2DP);
+        mHfpProfileState = new BluetoothProfileState(mContext, BluetoothProfileState.HFP);
+
+        mHfpProfileState.start();
+        mA2dpProfileState.start();
 
         IntentFilter filter = new IntentFilter();
         registerForAirplaneMode(filter);
@@ -1179,9 +1187,9 @@
         if (!BluetoothAdapter.checkBluetoothAddress(address)) {
             return false;
         }
-        BluetoothProfileConnectionState state = mProfileConnectionMgr.get(address);
+        BluetoothDeviceProfileState state = mDeviceProfileState.get(address);
         if (state != null) {
-            state.sendMessage(BluetoothProfileConnectionState.UNPAIR);
+            state.sendMessage(BluetoothDeviceProfileState.UNPAIR);
             return true;
         } else {
             return false;
@@ -1942,53 +1950,65 @@
     }
 
     public boolean connectHeadset(String address) {
-        BluetoothProfileConnectionState state = mProfileConnectionMgr.get(address);
+        BluetoothDeviceProfileState state = mDeviceProfileState.get(address);
         if (state != null) {
-          state.sendMessage(BluetoothProfileConnectionState.CONNECT_HFP_OUTGOING);
-          return true;
+            Message msg = new Message();
+            msg.arg1 = BluetoothDeviceProfileState.CONNECT_HFP_OUTGOING;
+            msg.obj = state;
+            mHfpProfileState.sendMessage(msg);
+            return true;
         }
         return false;
     }
 
     public boolean disconnectHeadset(String address) {
-        BluetoothProfileConnectionState state = mProfileConnectionMgr.get(address);
+        BluetoothDeviceProfileState state = mDeviceProfileState.get(address);
         if (state != null) {
-            state.sendMessage(BluetoothProfileConnectionState.DISCONNECT_HFP_OUTGOING);
+            Message msg = new Message();
+            msg.arg1 = BluetoothDeviceProfileState.DISCONNECT_HFP_OUTGOING;
+            msg.obj = state;
+            mHfpProfileState.sendMessage(msg);
             return true;
         }
         return false;
     }
 
     public boolean connectSink(String address) {
-        BluetoothProfileConnectionState state = mProfileConnectionMgr.get(address);
+        BluetoothDeviceProfileState state = mDeviceProfileState.get(address);
         if (state != null) {
-          state.sendMessage(BluetoothProfileConnectionState.CONNECT_A2DP_OUTGOING);
-          return true;
-        }
-        return false;
-    }
-
-    public boolean disconnectSink(String address) {
-        BluetoothProfileConnectionState state = mProfileConnectionMgr.get(address);
-        if (state != null) {
-            state.sendMessage(BluetoothProfileConnectionState.DISCONNECT_A2DP_OUTGOING);
+            Message msg = new Message();
+            msg.arg1 = BluetoothDeviceProfileState.CONNECT_A2DP_OUTGOING;
+            msg.obj = state;
+            mA2dpProfileState.sendMessage(msg);
             return true;
         }
         return false;
     }
 
-    private BluetoothProfileConnectionState addProfileState(String address) {
-        BluetoothProfileConnectionState state = mProfileConnectionMgr.get(address);
+    public boolean disconnectSink(String address) {
+        BluetoothDeviceProfileState state = mDeviceProfileState.get(address);
+        if (state != null) {
+            Message msg = new Message();
+            msg.arg1 = BluetoothDeviceProfileState.DISCONNECT_A2DP_OUTGOING;
+            msg.obj = state;
+            mA2dpProfileState.sendMessage(msg);
+            return true;
+        }
+        return false;
+    }
+
+    private BluetoothDeviceProfileState addProfileState(String address) {
+        BluetoothDeviceProfileState state = mDeviceProfileState.get(address);
         if (state != null) return state;
 
-        state = new BluetoothProfileConnectionState(mContext, address, this, mA2dpService);
-        mProfileConnectionMgr.put(address, state);
+        state = new BluetoothDeviceProfileState(mContext, address, this, mA2dpService);
+        mDeviceProfileState.put(address, state);
         state.start();
         return state;
     }
 
     private void removeProfileState(String address) {
-        mProfileConnectionMgr.remove(address);
+        mDeviceProfileState.remove(address);
     }
 
     private void initProfileState() {
@@ -2003,20 +2023,20 @@
 
         for (String path : bonds) {
             String address = getAddressFromObjectPath(path);
-            BluetoothProfileConnectionState state = addProfileState(address);
+            BluetoothDeviceProfileState state = addProfileState(address);
             // Allow 8 secs for SDP records to get registered.
             Message msg = new Message();
-            msg.what = BluetoothProfileConnectionState.AUTO_CONNECT_PROFILES;
+            msg.what = BluetoothDeviceProfileState.AUTO_CONNECT_PROFILES;
             state.sendMessageDelayed(msg, 8000);
         }
     }
 
     public boolean notifyIncomingConnection(String address) {
-        BluetoothProfileConnectionState state =
-             mProfileConnectionMgr.get(address);
+        BluetoothDeviceProfileState state =
+             mDeviceProfileState.get(address);
         if (state != null) {
             Message msg = new Message();
-            msg.what = BluetoothProfileConnectionState.CONNECT_HFP_INCOMING;
+            msg.what = BluetoothDeviceProfileState.CONNECT_HFP_INCOMING;
             state.sendMessage(msg);
             return true;
         }
@@ -2024,11 +2044,11 @@
     }
 
     /*package*/ boolean notifyIncomingA2dpConnection(String address) {
-       BluetoothProfileConnectionState state =
-            mProfileConnectionMgr.get(address);
+       BluetoothDeviceProfileState state =
+            mDeviceProfileState.get(address);
        if (state != null) {
            Message msg = new Message();
-           msg.what = BluetoothProfileConnectionState.CONNECT_A2DP_INCOMING;
+           msg.what = BluetoothDeviceProfileState.CONNECT_A2DP_INCOMING;
            state.sendMessage(msg);
            return true;
        }