Add BluetoothDeviceManager (multi-hfp part 1)
Adds BluetoothDeviceManager, a class to manage connected HFP devices for
Telecom. This class separates out part of the current functionality of
BluetoothManager and generalizes it to support multiple connected HFP
devices. See design doc for details.
Test: added unit tests
Change-Id: I03ca6e3827aa80b18e64a9c736097ca8dd4e01b8
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;
+ }
+}