Prevent auto-routing to wearable devices.
Currently, if BT is supported, Telecom will auto-route to one of the
available devices. Consequently, a watch that is paired with the phone
will receive the call audio when it is not explicitly requested.
This CL ensures that implicit routing to watches is ignored. When
connecting an arbitrary BT device, the wearable devices need to be
filtered out. Wearable (Pixel) devices are distinguishable by their
class/type (BluetoothClass.Device.WEARABLE_WRIST_WATCH,
BluetoothDevice.DEVICE_TYPE_WATCH). In addition to that, if there are no
devices to connect to, fallback is required to ensure that the audio
route is properly handled.
Bug: 294378768
Bug: 322330341
Test: Manual test with wired headset plugged in and Pixel watch paired.
Unplugged headset during call to verify that audio is routed to earpiece
and that there aren't any issues with user switching to the watch.
Test: Unit test verifying that the audio is routed into another
supported route when there are no non-wearable devices to arbitrarily
connect to.
Change-Id: I14be322037ad968009850d69387ad1aca76a0e05
(cherry picked from commit 96cb64ba5d8fa35fab1b2276185ed9443cbfa8b4)
Merged-In: I14be322037ad968009850d69387ad1aca76a0e05
diff --git a/src/com/android/server/telecom/CallAudioRouteStateMachine.java b/src/com/android/server/telecom/CallAudioRouteStateMachine.java
index dbc6d6a..46743be 100644
--- a/src/com/android/server/telecom/CallAudioRouteStateMachine.java
+++ b/src/com/android/server/telecom/CallAudioRouteStateMachine.java
@@ -1986,6 +1986,29 @@
return false;
}
+ private boolean isWatchActiveOrOnlyWatchesAvailable() {
+ boolean containsWatchDevice = false;
+ boolean containsNonWatchDevice = false;
+ Collection<BluetoothDevice> connectedBtDevices =
+ mBluetoothRouteManager.getConnectedDevices();
+
+ for (BluetoothDevice connectedDevice: connectedBtDevices) {
+ if (mBluetoothRouteManager.isWatch(connectedDevice)) {
+ containsWatchDevice = true;
+ } else {
+ containsNonWatchDevice = true;
+ }
+ }
+
+ // Don't ignore switch if watch is already the active device.
+ boolean isActiveDeviceWatch = mBluetoothRouteManager.isWatch(
+ mBluetoothRouteManager.getBluetoothAudioConnectedDevice());
+ Log.i(this, "isWatchActiveOrOnlyWatchesAvailable: contains watch: %s, contains "
+ + "non-wearable device: %s, is active device a watch: %s.",
+ containsWatchDevice, containsNonWatchDevice, isActiveDeviceWatch);
+ return containsWatchDevice && !containsNonWatchDevice && !isActiveDeviceWatch;
+ }
+
private int calculateBaselineRouteMessage(boolean isExplicitUserRequest,
boolean includeBluetooth) {
boolean isSkipEarpiece = false;
@@ -1998,7 +2021,7 @@
}
if ((mAvailableRoutes & ROUTE_BLUETOOTH) != 0
&& !mHasUserExplicitlyLeftBluetooth
- && includeBluetooth) {
+ && includeBluetooth && !isWatchActiveOrOnlyWatchesAvailable()) {
return isExplicitUserRequest ? USER_SWITCH_BLUETOOTH : SWITCH_BLUETOOTH;
} else if ((mAvailableRoutes & ROUTE_EARPIECE) != 0 && !isSkipEarpiece) {
return isExplicitUserRequest ? USER_SWITCH_EARPIECE : SWITCH_EARPIECE;
diff --git a/src/com/android/server/telecom/bluetooth/BluetoothRouteManager.java b/src/com/android/server/telecom/bluetooth/BluetoothRouteManager.java
index bce6e99..2db81f1 100644
--- a/src/com/android/server/telecom/bluetooth/BluetoothRouteManager.java
+++ b/src/com/android/server/telecom/bluetooth/BluetoothRouteManager.java
@@ -17,6 +17,7 @@
package com.android.server.telecom.bluetooth;
import android.bluetooth.BluetoothAdapter;
+import android.bluetooth.BluetoothClass;
import android.bluetooth.BluetoothDevice;
import android.bluetooth.BluetoothHeadset;
import android.bluetooth.BluetoothHearingAid;
@@ -663,6 +664,33 @@
return mDeviceManager.getUniqueConnectedDevices();
}
+ public boolean isWatch(BluetoothDevice device) {
+ if (device == null) {
+ Log.i(this, "isWatch: device is null. Returning false");
+ return false;
+ }
+
+ BluetoothClass deviceClass = device.getBluetoothClass();
+ if (deviceClass != null && deviceClass.getDeviceClass()
+ == BluetoothClass.Device.WEARABLE_WRIST_WATCH) {
+ Log.i(this, "isWatch: bluetooth class component is a WEARABLE_WRIST_WATCH.");
+ return true;
+ }
+
+ // Check metadata
+ byte[] deviceType = device.getMetadata(BluetoothDevice.METADATA_DEVICE_TYPE);
+ if (deviceType == null) {
+ return false;
+ }
+ String deviceTypeStr = new String(deviceType);
+ if (deviceTypeStr.equals(BluetoothDevice.DEVICE_TYPE_WATCH)) {
+ Log.i(this, "isWatch: bluetooth device type is DEVICE_TYPE_WATCH.");
+ return true;
+ }
+
+ return false;
+ }
+
private String connectBtAudio(String address, boolean switchingBtDevices) {
return connectBtAudio(address, 0, switchingBtDevices);
}
@@ -692,10 +720,19 @@
? address : getActiveDeviceAddress();
if (actualAddress == null) {
Log.i(this, "No device specified and BT stack has no active device."
- + " Using arbitrary device");
+ + " Using arbitrary device - except watch");
if (deviceList.size() > 0) {
- actualAddress = deviceList.iterator().next().getAddress();
- } else {
+ for (BluetoothDevice device : deviceList) {
+ if (isWatch(device)) {
+ Log.i(this, "Skipping a watch device: " + device);
+ continue;
+ }
+ actualAddress = device.getAddress();
+ break;
+ }
+ }
+
+ if (actualAddress == null) {
Log.i(this, "No devices available at all. Not connecting.");
return null;
}
diff --git a/tests/src/com/android/server/telecom/tests/CallAudioRouteStateMachineTest.java b/tests/src/com/android/server/telecom/tests/CallAudioRouteStateMachineTest.java
index 4a9b742..431a253 100644
--- a/tests/src/com/android/server/telecom/tests/CallAudioRouteStateMachineTest.java
+++ b/tests/src/com/android/server/telecom/tests/CallAudioRouteStateMachineTest.java
@@ -95,6 +95,7 @@
@Mock Call fakeSelfManagedCall;
@Mock Call fakeCall;
@Mock CallAudioManager mockCallAudioManager;
+ @Mock BluetoothDevice mockWatchDevice;
private CallAudioManager.AudioServiceFactory mAudioServiceFactory;
private static final int TEST_TIMEOUT = 500;
@@ -830,6 +831,54 @@
assertEquals(expectedState, stateMachine.getCurrentCallAudioState());
}
+ @MediumTest
+ @Test
+ public void testIgnoreImplicitBTSwitchWhenDeviceIsWatch() {
+ CallAudioRouteStateMachine stateMachine = new CallAudioRouteStateMachine(
+ mContext,
+ mockCallsManager,
+ mockBluetoothRouteManager,
+ mockWiredHeadsetManager,
+ mockStatusBarNotifier,
+ mAudioServiceFactory,
+ CallAudioRouteStateMachine.EARPIECE_FORCE_ENABLED,
+ mThreadHandler.getLooper(),
+ Runnable::run /** do async stuff sync for test purposes */);
+ stateMachine.setCallAudioManager(mockCallAudioManager);
+
+ CallAudioState initState = new CallAudioState(false,
+ CallAudioState.ROUTE_WIRED_HEADSET, CallAudioState.ROUTE_WIRED_HEADSET
+ | CallAudioState.ROUTE_EARPIECE | CallAudioState.ROUTE_BLUETOOTH);
+ stateMachine.initialize(initState);
+
+ // Switch to active
+ stateMachine.sendMessageWithSessionInfo(CallAudioRouteStateMachine.SWITCH_FOCUS,
+ CallAudioRouteStateMachine.ACTIVE_FOCUS);
+ waitForHandlerAction(stateMachine.getHandler(), TEST_TIMEOUT);
+
+ // Make sure that we've successfully switched to the active headset.
+ assertTrue(stateMachine.isInActiveState());
+
+ // Set up watch device as only available BT device.
+ Collection<BluetoothDevice> availableDevices = Collections.singleton(mockWatchDevice);
+
+ when(mockBluetoothRouteManager.isBluetoothAudioConnectedOrPending()).thenReturn(false);
+ when(mockBluetoothRouteManager.isBluetoothAvailable()).thenReturn(true);
+ when(mockBluetoothRouteManager.getConnectedDevices()).thenReturn(availableDevices);
+ when(mockBluetoothRouteManager.isWatch(any(BluetoothDevice.class))).thenReturn(true);
+
+ // Disconnect wired headset to force switch to BT (verify that we ignore the implicit switch
+ // to BT when the watch is the only connected device and that we move into the next
+ // available route.
+ stateMachine.sendMessageWithSessionInfo(
+ CallAudioRouteStateMachine.DISCONNECT_WIRED_HEADSET);
+ waitForHandlerAction(stateMachine.getHandler(), TEST_TIMEOUT);
+ CallAudioState expectedState = new CallAudioState(false, CallAudioState.ROUTE_EARPIECE,
+ CallAudioState.ROUTE_EARPIECE | CallAudioState.ROUTE_BLUETOOTH,
+ null, availableDevices);
+ assertEquals(expectedState, stateMachine.getCurrentCallAudioState());
+ }
+
private void initializationTestHelper(CallAudioState expectedState,
int earpieceControl) {
when(mockWiredHeadsetManager.isPluggedIn()).thenReturn(