Donate to e Foundation | Murena handsets with /e/OS | Own a part of Murena! Learn more

Commit 9e94241e authored by Pranav Madapurmath's avatar Pranav Madapurmath
Browse files

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 96cb64ba)
Merged-In: I14be322037ad968009850d69387ad1aca76a0e05
parent 20cb6b5a
Loading
Loading
Loading
Loading
+24 −1
Original line number Diff line number Diff line
@@ -1986,6 +1986,29 @@ public class CallAudioRouteStateMachine extends StateMachine {
        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 @@ public class CallAudioRouteStateMachine extends StateMachine {
        }
        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;
+40 −3
Original line number Diff line number Diff line
@@ -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 @@ public class BluetoothRouteManager extends StateMachine {
        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 @@ public class BluetoothRouteManager extends StateMachine {
                ? 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;
            }
+49 −0
Original line number Diff line number Diff line
@@ -95,6 +95,7 @@ public class CallAudioRouteStateMachineTest extends TelecomTestCase {
    @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 @@ public class CallAudioRouteStateMachineTest extends TelecomTestCase {
        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(