BluetoothControllerImpl: track bluetooth better

Match settings behavior better:
 - Get connection info from profiles so it is for all devices
   (CONNECTION_STATE_CHANGED only updates one)
 - Disconnect from all known connections so that 'x' does something
 - When device is disconnected without CONNECTION_STATE_CHANGED
   then find a new 'last' device that is connected
 - Listen to a bunch more broadcasts to make sure info is always
   up to date.

Bug: 18210666
Bug: 18638010
Change-Id: Ie003b67bbdd0d452b7472c644d90bca929e5cd29
diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/policy/BluetoothControllerImpl.java b/packages/SystemUI/src/com/android/systemui/statusbar/policy/BluetoothControllerImpl.java
index 80fec5b..076cfe2 100644
--- a/packages/SystemUI/src/com/android/systemui/statusbar/policy/BluetoothControllerImpl.java
+++ b/packages/SystemUI/src/com/android/systemui/statusbar/policy/BluetoothControllerImpl.java
@@ -19,15 +19,21 @@
 import static android.bluetooth.BluetoothAdapter.ERROR;
 import static com.android.systemui.statusbar.policy.BluetoothUtil.connectionStateToString;
 import static com.android.systemui.statusbar.policy.BluetoothUtil.deviceToString;
-import static com.android.systemui.statusbar.policy.BluetoothUtil.profileStateToString;
 import static com.android.systemui.statusbar.policy.BluetoothUtil.profileToString;
 import static com.android.systemui.statusbar.policy.BluetoothUtil.uuidToProfile;
 import static com.android.systemui.statusbar.policy.BluetoothUtil.uuidToString;
 import static com.android.systemui.statusbar.policy.BluetoothUtil.uuidsToString;
 
+import android.bluetooth.BluetoothA2dp;
+import android.bluetooth.BluetoothA2dpSink;
 import android.bluetooth.BluetoothAdapter;
 import android.bluetooth.BluetoothDevice;
+import android.bluetooth.BluetoothHeadset;
+import android.bluetooth.BluetoothHeadsetClient;
+import android.bluetooth.BluetoothInputDevice;
 import android.bluetooth.BluetoothManager;
+import android.bluetooth.BluetoothMap;
+import android.bluetooth.BluetoothPan;
 import android.bluetooth.BluetoothProfile;
 import android.bluetooth.BluetoothProfile.ServiceListener;
 import android.content.BroadcastReceiver;
@@ -38,24 +44,37 @@
 import android.util.ArrayMap;
 import android.util.ArraySet;
 import android.util.Log;
-import android.util.SparseBooleanArray;
+import android.util.SparseArray;
 
 import com.android.systemui.statusbar.policy.BluetoothUtil.Profile;
 
 import java.io.FileDescriptor;
 import java.io.PrintWriter;
 import java.util.ArrayList;
+import java.util.List;
 import java.util.Set;
 
 public class BluetoothControllerImpl implements BluetoothController {
     private static final String TAG = "BluetoothController";
     private static final boolean DEBUG = Log.isLoggable(TAG, Log.DEBUG);
+    // This controls the order in which we check the states.  Since a device can only have
+    // one state on screen, but can have multiple profiles, the later states override the
+    // value of earlier states.  So if a device has a profile in CONNECTING and one in
+    // CONNECTED, it will show as CONNECTED, theoretically this shouldn't really happen often,
+    // but seemed worth noting.
+    private static final int[] CONNECTION_STATES = {
+        BluetoothProfile.STATE_DISCONNECTED,
+        BluetoothProfile.STATE_DISCONNECTING,
+        BluetoothProfile.STATE_CONNECTING,
+        BluetoothProfile.STATE_CONNECTED,
+    };
 
     private final Context mContext;
     private final ArrayList<Callback> mCallbacks = new ArrayList<Callback>();
     private final BluetoothAdapter mAdapter;
     private final Receiver mReceiver = new Receiver();
     private final ArrayMap<BluetoothDevice, DeviceInfo> mDeviceInfo = new ArrayMap<>();
+    private final SparseArray<BluetoothProfile> mProfiles = new SparseArray<>();
 
     private boolean mEnabled;
     private boolean mConnecting;
@@ -73,7 +92,8 @@
 
         mReceiver.register();
         setAdapterState(mAdapter.getState());
-        updateBondedBluetoothDevices();
+        updateBluetoothDevices();
+        bindAllProfiles();
     }
 
     public void dump(FileDescriptor fd, PrintWriter pw, String[] args) {
@@ -83,6 +103,7 @@
         pw.print("  mConnecting="); pw.println(mConnecting);
         pw.print("  mLastDevice="); pw.println(mLastDevice);
         pw.print("  mCallbacks.size="); pw.println(mCallbacks.size());
+        pw.print("  mProfiles="); pw.println(profilesToString(mProfiles));
         pw.print("  mDeviceInfo.size="); pw.println(mDeviceInfo.size());
         for (int i = 0; i < mDeviceInfo.size(); i++) {
             final BluetoothDevice device = mDeviceInfo.keyAt(i);
@@ -95,7 +116,22 @@
 
     private static String infoToString(DeviceInfo info) {
         return info == null ? null : ("connectionState=" +
-                connectionStateToString(info.connectionState) + ",bonded=" + info.bonded);
+                connectionStateToString(info.connectionState) + ",bonded=" + info.bonded
+                + ",profiles=" + profilesToString(info.connectedProfiles));
+    }
+
+    private static String profilesToString(SparseArray<?> profiles) {
+        final int N = profiles.size();
+        final StringBuffer buffer = new StringBuffer();
+        buffer.append('[');
+        for (int i = 0; i < N; i++) {
+            if (i != 0) {
+                buffer.append(',');
+            }
+            buffer.append(BluetoothUtil.profileToString(profiles.keyAt(i)));
+        }
+        buffer.append(']');
+        return buffer.toString();
     }
 
     public void addStateChangedCallback(Callback cb) {
@@ -178,6 +214,7 @@
     private void connect(PairedDevice pd, final boolean connect) {
         if (mAdapter == null || pd == null || pd.tag == null) return;
         final BluetoothDevice device = (BluetoothDevice) pd.tag;
+        final DeviceInfo info = mDeviceInfo.get(device);
         final String action = connect ? "connect" : "disconnect";
         if (DEBUG) Log.d(TAG, action + " " + deviceToString(device));
         final ParcelUuid[] uuids = device.getUuids();
@@ -185,43 +222,35 @@
             Log.w(TAG, "No uuids returned, aborting " + action + " for " + deviceToString(device));
             return;
         }
-        final SparseBooleanArray profiles = new SparseBooleanArray();
-        for (ParcelUuid uuid : uuids) {
-            final int profile = uuidToProfile(uuid);
-            if (profile == 0) {
-                Log.w(TAG, "Device " + deviceToString(device) + " has an unsupported uuid: "
-                        + uuidToString(uuid));
-                continue;
+        SparseArray<Boolean> profiles = new SparseArray<>();
+        if (connect) {
+            // When connecting add every profile we can recognize by uuid.
+            for (ParcelUuid uuid : uuids) {
+                final int profile = uuidToProfile(uuid);
+                if (profile == 0) {
+                    Log.w(TAG, "Device " + deviceToString(device) + " has an unsupported uuid: "
+                            + uuidToString(uuid));
+                    continue;
+                }
+                final boolean connected = info.connectedProfiles.get(profile, false);
+                if (!connected) {
+                    profiles.put(profile, true);
+                }
             }
-            final int profileState = mAdapter.getProfileConnectionState(profile);
-            if (DEBUG && !profiles.get(profile)) Log.d(TAG, "Profile " + profileToString(profile)
-                    + " state = " + profileStateToString(profileState));
-            final boolean connected = profileState == BluetoothProfile.STATE_CONNECTED;
-            if (connect != connected) {
-                profiles.put(profile, true);
-            }
+        } else {
+            // When disconnecting, just add every profile we know they are connected to.
+            profiles = info.connectedProfiles;
         }
         for (int i = 0; i < profiles.size(); i++) {
             final int profile = profiles.keyAt(i);
-            mAdapter.getProfileProxy(mContext, new ServiceListener() {
-                @Override
-                public void onServiceConnected(int profile, BluetoothProfile proxy) {
-                    if (DEBUG) Log.d(TAG, "onServiceConnected " + profileToString(profile));
-                    final Profile p = BluetoothUtil.getProfile(proxy);
-                    if (p == null) {
-                        Log.w(TAG, "Unable get get Profile for " + profileToString(profile));
-                    } else {
-                        final boolean ok = connect ? p.connect(device) : p.disconnect(device);
-                        if (DEBUG) Log.d(TAG, action + " " + profileToString(profile) + " "
-                                + (ok ? "succeeded" : "failed"));
-                    }
-                }
-
-                @Override
-                public void onServiceDisconnected(int profile) {
-                    if (DEBUG) Log.d(TAG, "onServiceDisconnected " + profileToString(profile));
-                }
-            }, profile);
+            if (mProfiles.indexOfKey(profile) >= 0) {
+                final Profile p = BluetoothUtil.getProfile(mProfiles.get(profile));
+                final boolean ok = connect ? p.connect(device) : p.disconnect(device);
+                if (DEBUG) Log.d(TAG, action + " " + profileToString(profile) + " "
+                        + (ok ? "succeeded" : "failed"));
+            } else {
+                Log.w(TAG, "Unable get get Profile for " + profileToString(profile));
+            }
         }
     }
 
@@ -230,11 +259,13 @@
         return mLastDevice != null ? mLastDevice.getAliasName() : null;
     }
 
-    private void updateBondedBluetoothDevices() {
+    private void updateBluetoothDevices() {
         if (mAdapter == null) return;
         final Set<BluetoothDevice> bondedDevices = mAdapter.getBondedDevices();
         for (DeviceInfo info : mDeviceInfo.values()) {
             info.bonded = false;
+            info.connectionState = ERROR;
+            info.connectedProfiles.clear();
         }
         int bondedCount = 0;
         BluetoothDevice lastBonded = null;
@@ -248,12 +279,62 @@
                 }
             }
         }
+        final int N = mProfiles.size();
+        final int[] connectionType = new int[1];
+        for (int i = 0; i < CONNECTION_STATES.length; i++) {
+            connectionType[0] = CONNECTION_STATES[i];
+            for (int j = 0; j < N; j++) {
+                int profile = mProfiles.keyAt(j);
+                List<BluetoothDevice> devices = mProfiles.get(profile)
+                        .getDevicesMatchingConnectionStates(connectionType);
+                for (int k = 0; k < devices.size(); k++) {
+                    DeviceInfo info = mDeviceInfo.get(devices.get(k));
+                    info.connectionState = CONNECTION_STATES[i];
+                    if (CONNECTION_STATES[i] == BluetoothProfile.STATE_CONNECTED) {
+                        info.connectedProfiles.put(profile, true);
+                    }
+                }
+            }
+        }
         if (mLastDevice == null && bondedCount == 1) {
             mLastDevice = lastBonded;
         }
+        // If we are no longer connected to the current device, see if we are connected to
+        // something else, so we don't display a name we aren't connected to.
+        if (mLastDevice != null &&
+                mDeviceInfo.get(mLastDevice).connectionState != BluetoothProfile.STATE_CONNECTED) {
+            // Make sure we don't keep this device while it isn't connected.
+            mLastDevice = null;
+            // Look for anything else connected.
+            final int size = mDeviceInfo.size();
+            for (int i = 0; i < size; i++) {
+                BluetoothDevice device = mDeviceInfo.keyAt(i);
+                DeviceInfo info = mDeviceInfo.valueAt(i);
+                if (info.connectionState == BluetoothProfile.STATE_CONNECTED) {
+                    mLastDevice = device;
+                    break;
+                }
+            }
+        }
         firePairedDevicesChanged();
     }
 
+    private void bindAllProfiles() {
+        // Note: This needs to contain all of the types that can be returned by BluetoothUtil
+        // otherwise we can't find the profiles we need when we connect/disconnect.
+        mAdapter.getProfileProxy(mContext, mProfileListener, BluetoothProfile.A2DP);
+        mAdapter.getProfileProxy(mContext, mProfileListener, BluetoothProfile.A2DP_SINK);
+        mAdapter.getProfileProxy(mContext, mProfileListener, BluetoothProfile.AVRCP_CONTROLLER);
+        mAdapter.getProfileProxy(mContext, mProfileListener, BluetoothProfile.HEADSET);
+        mAdapter.getProfileProxy(mContext, mProfileListener, BluetoothProfile.HEADSET_CLIENT);
+        mAdapter.getProfileProxy(mContext, mProfileListener, BluetoothProfile.INPUT_DEVICE);
+        mAdapter.getProfileProxy(mContext, mProfileListener, BluetoothProfile.MAP);
+        mAdapter.getProfileProxy(mContext, mProfileListener, BluetoothProfile.PAN);
+        // Note Health is not in this list because health devices aren't 'connected'.
+        // If profiles are expanded to use more than just connection state and connect/disconnect
+        // then it should be added.
+    }
+
     private void firePairedDevicesChanged() {
         for (Callback cb : mCallbacks) {
             cb.onBluetoothPairedDevicesChanged();
@@ -283,6 +364,20 @@
         cb.onBluetoothStateChange(mEnabled, mConnecting);
     }
 
+    private final ServiceListener mProfileListener = new ServiceListener() {
+        @Override
+        public void onServiceDisconnected(int profile) {
+            mProfiles.remove(profile);
+            updateBluetoothDevices();
+        }
+
+        @Override
+        public void onServiceConnected(int profile, BluetoothProfile proxy) {
+            mProfiles.put(profile, proxy);
+            updateBluetoothDevices();
+        }
+    };
+
     private final class Receiver extends BroadcastReceiver {
         public void register() {
             final IntentFilter filter = new IntentFilter();
@@ -290,6 +385,15 @@
             filter.addAction(BluetoothAdapter.ACTION_CONNECTION_STATE_CHANGED);
             filter.addAction(BluetoothDevice.ACTION_BOND_STATE_CHANGED);
             filter.addAction(BluetoothDevice.ACTION_ALIAS_CHANGED);
+            filter.addAction(BluetoothDevice.ACTION_CLASS_CHANGED);
+            filter.addAction(BluetoothDevice.ACTION_UUID);
+            filter.addAction(BluetoothA2dp.ACTION_CONNECTION_STATE_CHANGED);
+            filter.addAction(BluetoothHeadset.ACTION_CONNECTION_STATE_CHANGED);
+            filter.addAction(BluetoothA2dpSink.ACTION_CONNECTION_STATE_CHANGED);
+            filter.addAction(BluetoothHeadsetClient.ACTION_CONNECTION_STATE_CHANGED);
+            filter.addAction(BluetoothInputDevice.ACTION_CONNECTION_STATE_CHANGED);
+            filter.addAction(BluetoothMap.ACTION_CONNECTION_STATE_CHANGED);
+            filter.addAction(BluetoothPan.ACTION_CONNECTION_STATE_CHANGED);
             mContext.registerReceiver(this, filter);
         }
 
@@ -301,16 +405,13 @@
                 setAdapterState(intent.getIntExtra(BluetoothAdapter.EXTRA_STATE, ERROR));
                 if (DEBUG) Log.d(TAG, "ACTION_STATE_CHANGED " + mEnabled);
             } else if (action.equals(BluetoothAdapter.ACTION_CONNECTION_STATE_CHANGED)) {
-                final DeviceInfo info = updateInfo(device);
+                updateInfo(device);
                 final int state = intent.getIntExtra(BluetoothAdapter.EXTRA_CONNECTION_STATE,
                         ERROR);
-                if (state != ERROR) {
-                    info.connectionState = state;
-                }
                 mLastDevice = device;
                 if (DEBUG) Log.d(TAG, "ACTION_CONNECTION_STATE_CHANGED "
                         + connectionStateToString(state) + " " + deviceToString(device));
-                setConnecting(info.connectionState == BluetoothAdapter.STATE_CONNECTING);
+                setConnecting(state == BluetoothAdapter.STATE_CONNECTING);
             } else if (action.equals(BluetoothDevice.ACTION_ALIAS_CHANGED)) {
                 updateInfo(device);
                 mLastDevice = device;
@@ -318,7 +419,8 @@
                 if (DEBUG) Log.d(TAG, "ACTION_BOND_STATE_CHANGED " + device);
                 // we'll update all bonded devices below
             }
-            updateBondedBluetoothDevices();
+            // Always update bluetooth devices state.
+            updateBluetoothDevices();
         }
     }
 
@@ -332,5 +434,6 @@
     private static class DeviceInfo {
         int connectionState = BluetoothAdapter.STATE_DISCONNECTED;
         boolean bonded;  // per getBondedDevices
+        SparseArray<Boolean> connectedProfiles = new SparseArray<>();
     }
 }