Merge "Add BluetoothDeviceManager (multi-hfp part 1)"
diff --git a/src/com/android/server/telecom/bluetooth/BluetoothDeviceManager.java b/src/com/android/server/telecom/bluetooth/BluetoothDeviceManager.java
new file mode 100644
index 0000000..5feacbc
--- /dev/null
+++ b/src/com/android/server/telecom/bluetooth/BluetoothDeviceManager.java
@@ -0,0 +1,161 @@
+/*
+ * Copyright (C) 2016 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 com.android.server.telecom.bluetooth;
+
+import android.bluetooth.BluetoothDevice;
+import android.bluetooth.BluetoothHeadset;
+import android.bluetooth.BluetoothProfile;
+import android.content.BroadcastReceiver;
+import android.content.Context;
+import android.content.Intent;
+import android.content.IntentFilter;
+import android.telecom.Log;
+
+import com.android.server.telecom.BluetoothAdapterProxy;
+import com.android.server.telecom.BluetoothHeadsetProxy;
+import com.android.server.telecom.TelecomSystem;
+
+import java.util.LinkedHashMap;
+import java.util.Objects;
+
+public class BluetoothDeviceManager {
+    private final BluetoothProfile.ServiceListener mBluetoothProfileServiceListener =
+            new BluetoothProfile.ServiceListener() {
+                @Override
+                public void onServiceConnected(int profile, BluetoothProfile proxy) {
+                    Log.startSession("BMSL.oSC");
+                    try {
+                        synchronized (mLock) {
+                            if (profile == BluetoothProfile.HEADSET) {
+                                mBluetoothHeadsetService =
+                                        new BluetoothHeadsetProxy((BluetoothHeadset) proxy);
+                                Log.i(this, "- Got BluetoothHeadset: " + mBluetoothHeadsetService);
+                            } else {
+                                Log.w(this, "Connected to non-headset bluetooth service." +
+                                        " Not changing bluetooth headset.");
+                            }
+                        }
+                    } finally {
+                        Log.endSession();
+                    }
+                }
+
+                @Override
+                public void onServiceDisconnected(int profile) {
+                    Log.startSession("BMSL.oSD");
+                    try {
+                        synchronized (mLock) {
+                            mBluetoothHeadsetService = null;
+                            Log.i(BluetoothDeviceManager.this, "Lost BluetoothHeadset service.");
+                        }
+                    } finally {
+                        Log.endSession();
+                    }
+                }
+           };
+
+    private final BroadcastReceiver mReceiver = new BroadcastReceiver() {
+        @Override
+        public void onReceive(Context context, Intent intent) {
+            Log.startSession("BM.oR");
+            try {
+                String action = intent.getAction();
+
+                if (action.equals(BluetoothHeadset.ACTION_CONNECTION_STATE_CHANGED)) {
+                    int bluetoothHeadsetState = intent.getIntExtra(BluetoothHeadset.EXTRA_STATE,
+                            BluetoothHeadset.STATE_DISCONNECTED);
+                    BluetoothDevice device =
+                            intent.getParcelableExtra(BluetoothDevice.EXTRA_DEVICE);
+
+                    if (device == null) {
+                        Log.w(BluetoothDeviceManager.this, "Got null device from broadcast. " +
+                                "Ignoring.");
+                        return;
+                    }
+
+                    Log.i(BluetoothDeviceManager.this, "Device %s changed state to %d",
+                            device.getAddress(), bluetoothHeadsetState);
+
+                    synchronized (mLock) {
+                        if (bluetoothHeadsetState == BluetoothHeadset.STATE_CONNECTED) {
+                            if (!mConnectedDevicesByAddress.containsKey(device.getAddress())) {
+                                mConnectedDevicesByAddress.put(device.getAddress(), device);
+                                mBluetoothRouteManager.onDeviceAdded(device);
+                            }
+                        } else if (bluetoothHeadsetState == BluetoothHeadset.STATE_DISCONNECTED
+                                || bluetoothHeadsetState == BluetoothHeadset.STATE_DISCONNECTING) {
+                            if (mConnectedDevicesByAddress.containsKey(device.getAddress())) {
+                                mConnectedDevicesByAddress.remove(device.getAddress());
+                                mBluetoothRouteManager.onDeviceLost(device);
+                            }
+                        }
+                    }
+                }
+            } finally {
+                Log.endSession();
+            }
+        }
+    };
+
+    private final LinkedHashMap<String, BluetoothDevice> mConnectedDevicesByAddress =
+            new LinkedHashMap<>();
+    private final TelecomSystem.SyncRoot mLock;
+
+    private BluetoothRouteManager mBluetoothRouteManager;
+    private BluetoothHeadsetProxy mBluetoothHeadsetService;
+
+    public BluetoothDeviceManager(Context context, BluetoothAdapterProxy bluetoothAdapter,
+            TelecomSystem.SyncRoot lock) {
+        mLock = lock;
+
+        if (bluetoothAdapter != null) {
+            bluetoothAdapter.getProfileProxy(context, mBluetoothProfileServiceListener,
+                    BluetoothProfile.HEADSET);
+        }
+        IntentFilter intentFilter =
+                new IntentFilter(BluetoothHeadset.ACTION_CONNECTION_STATE_CHANGED);
+        context.registerReceiver(mReceiver, intentFilter);
+    }
+
+    public void setBluetoothRouteManager(BluetoothRouteManager brm) {
+        mBluetoothRouteManager = brm;
+    }
+
+    public int getNumConnectedDevices() {
+        return mConnectedDevicesByAddress.size();
+    }
+
+    public String getMostRecentlyConnectedDevice(String excludeAddress) {
+        String result = null;
+        synchronized (mLock) {
+            for (String addr : mConnectedDevicesByAddress.keySet()) {
+                if (!Objects.equals(addr, excludeAddress)) {
+                    result = addr;
+                }
+            }
+        }
+        return result;
+    }
+
+    public BluetoothHeadsetProxy getHeadsetService() {
+        return mBluetoothHeadsetService;
+    }
+
+    public void setHeadsetServiceForTesting(BluetoothHeadsetProxy bluetoothHeadset) {
+        mBluetoothHeadsetService = bluetoothHeadset;
+    }
+}
diff --git a/src/com/android/server/telecom/bluetooth/BluetoothRouteManager.java b/src/com/android/server/telecom/bluetooth/BluetoothRouteManager.java
new file mode 100644
index 0000000..3197caa
--- /dev/null
+++ b/src/com/android/server/telecom/bluetooth/BluetoothRouteManager.java
@@ -0,0 +1,30 @@
+/*
+ * Copyright (C) 2016 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 com.android.server.telecom.bluetooth;
+
+import android.bluetooth.BluetoothDevice;
+
+public class BluetoothRouteManager {
+    // TODO: implement
+    public void onDeviceLost(BluetoothDevice device) {
+
+    }
+
+    public void onDeviceAdded(BluetoothDevice device) {
+
+    }
+}
diff --git a/tests/src/com/android/server/telecom/tests/BluetoothDeviceManagerTest.java b/tests/src/com/android/server/telecom/tests/BluetoothDeviceManagerTest.java
new file mode 100644
index 0000000..c8cde98
--- /dev/null
+++ b/tests/src/com/android/server/telecom/tests/BluetoothDeviceManagerTest.java
@@ -0,0 +1,149 @@
+/*
+ * Copyright (C) 2016 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 com.android.server.telecom.tests;
+
+import android.bluetooth.BluetoothDevice;
+import android.bluetooth.BluetoothHeadset;
+import android.bluetooth.BluetoothProfile;
+import android.content.BroadcastReceiver;
+import android.content.Intent;
+import android.content.IntentFilter;
+import android.os.Parcel;
+import android.test.suitebuilder.annotation.SmallTest;
+
+import com.android.server.telecom.BluetoothAdapterProxy;
+import com.android.server.telecom.BluetoothHeadsetProxy;
+import com.android.server.telecom.TelecomSystem;
+import com.android.server.telecom.bluetooth.BluetoothDeviceManager;
+import com.android.server.telecom.bluetooth.BluetoothRouteManager;
+
+import org.mockito.ArgumentCaptor;
+import org.mockito.Mock;
+
+import static org.mockito.Matchers.eq;
+import static org.mockito.Mockito.verify;
+
+public class BluetoothDeviceManagerTest extends TelecomTestCase {
+    @Mock BluetoothRouteManager mRouteManager;
+    @Mock BluetoothHeadsetProxy mHeadsetProxy;
+    @Mock BluetoothAdapterProxy mAdapterProxy;
+
+    BluetoothDeviceManager mBluetoothDeviceManager;
+    BluetoothProfile.ServiceListener serviceListenerUnderTest;
+    BroadcastReceiver receiverUnderTest;
+
+    private BluetoothDevice device1;
+    private BluetoothDevice device2;
+    private BluetoothDevice device3;
+
+    public void setUp() throws Exception {
+        super.setUp();
+        device1 = makeBluetoothDevice("00:00:00:00:00:01");
+        device2 = makeBluetoothDevice("00:00:00:00:00:02");
+        device3 = makeBluetoothDevice("00:00:00:00:00:03");
+
+        mContext = mComponentContextFixture.getTestDouble().getApplicationContext();
+        mBluetoothDeviceManager = new BluetoothDeviceManager(mContext, mAdapterProxy,
+                new TelecomSystem.SyncRoot() { });
+        mBluetoothDeviceManager.setBluetoothRouteManager(mRouteManager);
+
+        ArgumentCaptor<BluetoothProfile.ServiceListener> serviceCaptor =
+                ArgumentCaptor.forClass(BluetoothProfile.ServiceListener.class);
+        verify(mAdapterProxy).getProfileProxy(eq(mContext),
+                serviceCaptor.capture(), eq(BluetoothProfile.HEADSET));
+        serviceListenerUnderTest = serviceCaptor.getValue();
+
+        ArgumentCaptor<BroadcastReceiver> receiverCaptor =
+                ArgumentCaptor.forClass(BroadcastReceiver.class);
+        ArgumentCaptor<IntentFilter> intentFilterCaptor =
+                ArgumentCaptor.forClass(IntentFilter.class);
+        verify(mContext).registerReceiver(receiverCaptor.capture(), intentFilterCaptor.capture());
+        assertTrue(intentFilterCaptor.getValue().hasAction(
+                BluetoothHeadset.ACTION_CONNECTION_STATE_CHANGED));
+        receiverUnderTest = receiverCaptor.getValue();
+
+        mBluetoothDeviceManager.setHeadsetServiceForTesting(mHeadsetProxy);
+    }
+
+    @SmallTest
+    public void testSingleDeviceConnectAndDisconnect() {
+        receiverUnderTest.onReceive(mContext,
+                buildConnectionActionIntent(BluetoothHeadset.STATE_CONNECTED, device1));
+        assertEquals(1, mBluetoothDeviceManager.getNumConnectedDevices());
+        assertEquals(device1.getAddress(),
+                mBluetoothDeviceManager.getMostRecentlyConnectedDevice(null));
+        receiverUnderTest.onReceive(mContext,
+                buildConnectionActionIntent(BluetoothHeadset.STATE_DISCONNECTED, device1));
+        assertEquals(0, mBluetoothDeviceManager.getNumConnectedDevices());
+        assertNull(mBluetoothDeviceManager.getMostRecentlyConnectedDevice(null));
+    }
+
+    @SmallTest
+    public void testMultiDeviceConnectAndDisconnect() {
+        receiverUnderTest.onReceive(mContext,
+                buildConnectionActionIntent(BluetoothHeadset.STATE_CONNECTED, device1));
+        receiverUnderTest.onReceive(mContext,
+                buildConnectionActionIntent(BluetoothHeadset.STATE_CONNECTED, device2));
+        receiverUnderTest.onReceive(mContext,
+                buildConnectionActionIntent(BluetoothHeadset.STATE_DISCONNECTED, device1));
+        receiverUnderTest.onReceive(mContext,
+                buildConnectionActionIntent(BluetoothHeadset.STATE_CONNECTED, device3));
+        receiverUnderTest.onReceive(mContext,
+                buildConnectionActionIntent(BluetoothHeadset.STATE_CONNECTED, device2));
+        assertEquals(2, mBluetoothDeviceManager.getNumConnectedDevices());
+        assertEquals(device3.getAddress(),
+                mBluetoothDeviceManager.getMostRecentlyConnectedDevice(null));
+        receiverUnderTest.onReceive(mContext,
+                buildConnectionActionIntent(BluetoothHeadset.STATE_DISCONNECTED, device3));
+        assertEquals(1, mBluetoothDeviceManager.getNumConnectedDevices());
+        assertEquals(device2.getAddress(),
+                mBluetoothDeviceManager.getMostRecentlyConnectedDevice(null));
+    }
+
+    @SmallTest
+    public void testExclusionaryGetRecentDevices() {
+        receiverUnderTest.onReceive(mContext,
+                buildConnectionActionIntent(BluetoothHeadset.STATE_CONNECTED, device1));
+        receiverUnderTest.onReceive(mContext,
+                buildConnectionActionIntent(BluetoothHeadset.STATE_CONNECTED, device2));
+        receiverUnderTest.onReceive(mContext,
+                buildConnectionActionIntent(BluetoothHeadset.STATE_DISCONNECTED, device1));
+        receiverUnderTest.onReceive(mContext,
+                buildConnectionActionIntent(BluetoothHeadset.STATE_CONNECTED, device3));
+        receiverUnderTest.onReceive(mContext,
+                buildConnectionActionIntent(BluetoothHeadset.STATE_CONNECTED, device2));
+        assertEquals(2, mBluetoothDeviceManager.getNumConnectedDevices());
+        assertEquals(device2.getAddress(),
+                mBluetoothDeviceManager.getMostRecentlyConnectedDevice(device3.getAddress()));
+    }
+
+    private Intent buildConnectionActionIntent(int state, BluetoothDevice device) {
+        Intent i = new Intent(BluetoothHeadset.ACTION_CONNECTION_STATE_CHANGED);
+        i.putExtra(BluetoothHeadset.EXTRA_STATE, state);
+        i.putExtra(BluetoothDevice.EXTRA_DEVICE, device);
+        return i;
+    }
+
+    private BluetoothDevice makeBluetoothDevice(String address) {
+        Parcel p1 = Parcel.obtain();
+        p1.writeString(address);
+        p1.setDataPosition(0);
+        BluetoothDevice device = BluetoothDevice.CREATOR.createFromParcel(p1);
+        p1.recycle();
+        return device;
+    }
+}