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

Commit 47dc4bbd authored by Pranav Madapurmath's avatar Pranav Madapurmath
Browse files

Resolve call audio refactoring issues.

As part of the new call audio refactor changes that were introduced,
this CL resolves the issues that were found during testing as well as
other known issues that were originally found in the legacy code.

In the new code path, BT SCO was leveraging setCommunicationDevice but
this currently isn't available so explicit connection has to be
established as is done in the legacy path. There were also problems with
switching between LE/HFP devices which is being addressed in this CL. It
also takes care of ignoring auto routing to wearable devices as well as
ensuring that audio routing to HA devices is working as intended.

Bug: 328287261
Test: Manual testing with single case scenario of
connecting/disconnecting HFP/LE/HA devices as well as testing switching
between LE/HFP devices. Testing was also done to ensure auto-routing to
wearable devices is ignored unless explicitly requested by the user.
Test: atest CallAudioRouteControllerTest

Change-Id: I989b2d018512e73b11a597a1d688faf88a8433c2
parent 32260704
Loading
Loading
Loading
Loading
+127 −16
Original line number Diff line number Diff line
@@ -23,11 +23,14 @@ import static com.android.server.telecom.CallAudioRouteAdapter.SPEAKER_OFF;
import static com.android.server.telecom.CallAudioRouteAdapter.SPEAKER_ON;

import android.annotation.IntDef;
import android.bluetooth.BluetoothDevice;
import android.bluetooth.BluetoothStatusCodes;
import android.media.AudioDeviceInfo;
import android.media.AudioManager;
import android.telecom.Log;

import com.android.internal.annotations.VisibleForTesting;
import com.android.server.telecom.bluetooth.BluetoothRouteManager;

import java.lang.annotation.Retention;
import java.lang.annotation.RetentionPolicy;
@@ -38,6 +41,7 @@ import java.util.Objects;
import java.util.Set;
import java.util.concurrent.CompletableFuture;
import java.util.concurrent.ExecutionException;
import java.util.concurrent.RejectedExecutionException;
import java.util.concurrent.ScheduledExecutorService;
import java.util.concurrent.ScheduledThreadPoolExecutor;
import java.util.concurrent.TimeUnit;
@@ -46,9 +50,10 @@ public class AudioRoute {
    public static class Factory {
        private final ScheduledExecutorService mScheduledExecutorService =
                new ScheduledThreadPoolExecutor(1);
        private final CompletableFuture<AudioRoute> mAudioRouteFuture = new CompletableFuture<>();
        private CompletableFuture<AudioRoute> mAudioRouteFuture;
        public AudioRoute create(@AudioRouteType int type, String bluetoothAddress,
                                 AudioManager audioManager) throws RuntimeException {
            mAudioRouteFuture = new CompletableFuture();
            createRetry(type, bluetoothAddress, audioManager, MAX_CONNECTION_RETRIES);
            try {
                return mAudioRouteFuture.get();
@@ -58,8 +63,10 @@ public class AudioRoute {
        }
        private void createRetry(@AudioRouteType int type, String bluetoothAddress,
                                       AudioManager audioManager, int retryCount) {
            // Early exit if exceeded max number of retries (and complete the future).
            if (retryCount == 0) {
                mAudioRouteFuture.complete(null);
                return;
            }

            Log.i(this, "creating AudioRoute with type %s and address %s, retry count %d",
@@ -81,10 +88,17 @@ public class AudioRoute {
                    }
                }
            }
            if (routeInfo == null) {
            // Try connecting BT device anyway (to handle wearables not showing as available
            // communication device or LE device not showing up since it may not be the lead
            // device).
            if (routeInfo == null && bluetoothAddress == null) {
                try {
                    mScheduledExecutorService.schedule(
                            () -> createRetry(type, bluetoothAddress, audioManager, retryCount - 1),
                            RETRY_TIME_DELAY, TimeUnit.MILLISECONDS);
                } catch (RejectedExecutionException e) {
                    Log.e(this, e, "Could not schedule retry for audio routing.");
                }
            } else {
                mAudioRouteFuture.complete(new AudioRoute(type, bluetoothAddress, routeInfo));
            }
@@ -222,30 +236,81 @@ public class AudioRoute {

    // Invoked when entered pending route whose dest route is this route
    void onDestRouteAsPendingRoute(boolean active, PendingAudioRoute pendingAudioRoute,
                                   AudioManager audioManager) {
            BluetoothDevice device, AudioManager audioManager,
            BluetoothRouteManager bluetoothRouteManager) {
        Log.i(this, "onDestRouteAsPendingRoute: active (%b), type (%d)", active, mAudioRouteType);
        if (pendingAudioRoute.isActive() && !active) {
            Log.i(this, "clearCommunicationDevice");
            audioManager.clearCommunicationDevice();
            clearCommunicationDevice(pendingAudioRoute, bluetoothRouteManager, audioManager);
        } else if (active) {
            // Handle BT routing case.
            if (BT_AUDIO_ROUTE_TYPES.contains(mAudioRouteType)) {
                boolean connectedBtAudio = connectBtAudio(pendingAudioRoute, device,
                        audioManager, bluetoothRouteManager);
                // Special handling for SCO case.
                if (mAudioRouteType == TYPE_BLUETOOTH_SCO) {
                    // Check if the communication device was set for the device, even if
                    // BluetoothHeadset#connectAudio reports that the SCO connection wasn't
                    // successfully established.
                    boolean scoConnected = audioManager.getCommunicationDevice().equals(mInfo);
                    if (connectedBtAudio || scoConnected) {
                        pendingAudioRoute.setCommunicationDeviceType(mAudioRouteType);
                    }
                    if (connectedBtAudio) {
                        pendingAudioRoute.addMessage(BT_AUDIO_CONNECTED);
                    } else if (!scoConnected) {
                        pendingAudioRoute.onMessageReceived(
                                PENDING_ROUTE_FAILED, mBluetoothAddress);
                    }
                    return;
                }
            } else if (mAudioRouteType == TYPE_SPEAKER) {
                pendingAudioRoute.addMessage(SPEAKER_ON);
            }
            if (!audioManager.setCommunicationDevice(mInfo)) {
                pendingAudioRoute.onMessageReceived(PENDING_ROUTE_FAILED);

            boolean result = false;
            List<AudioDeviceInfo> devices = audioManager.getAvailableCommunicationDevices();
            for (AudioDeviceInfo deviceInfo : devices) {
                // It's possible for the AudioDeviceInfo to be updated for the BT device so adjust
                // mInfo accordingly.
                if (BT_AUDIO_ROUTE_TYPES.contains(mAudioRouteType) && mBluetoothAddress
                        .equals(deviceInfo.getAddress())) {
                    mInfo = deviceInfo;
                }
                if (deviceInfo.equals(mInfo)) {
                    result = audioManager.setCommunicationDevice(mInfo);
                    if (result) {
                        pendingAudioRoute.setCommunicationDeviceType(mAudioRouteType);
                    }
                    Log.i(this, "Result of setting communication device for audio "
                            + "route (%s) - %b", this, result);
                    break;
                }
            }

            // It's possible that BluetoothStateReceiver needs to report that the device is active
            // before being able to successfully set the communication device. Refrain from sending
            // pending route failed message for BT route until the second attempt fails.
            if (!result && !BT_AUDIO_ROUTE_TYPES.contains(mAudioRouteType)) {
                pendingAudioRoute.onMessageReceived(PENDING_ROUTE_FAILED, null);
            }
        }
    }

    // Takes care of cleaning up original audio route (i.e. clearCommunicationDevice,
    // sending SPEAKER_OFF, or disconnecting SCO).
    void onOrigRouteAsPendingRoute(boolean active, PendingAudioRoute pendingAudioRoute,
                                   AudioManager audioManager) {
            AudioManager audioManager, BluetoothRouteManager bluetoothRouteManager) {
        Log.i(this, "onOrigRouteAsPendingRoute: active (%b), type (%d)", active, mAudioRouteType);
        if (active) {
            if (mAudioRouteType == TYPE_BLUETOOTH_SCO) {
                pendingAudioRoute.addMessage(BT_AUDIO_DISCONNECTED);
            } else if (mAudioRouteType == TYPE_SPEAKER) {
            if (mAudioRouteType == TYPE_SPEAKER) {
                pendingAudioRoute.addMessage(SPEAKER_OFF);
            }
            int result = clearCommunicationDevice(pendingAudioRoute, bluetoothRouteManager,
                    audioManager);
            // Only send BT_AUDIO_DISCONNECTED for SCO if disconnect was successful.
            if (mAudioRouteType == TYPE_BLUETOOTH_SCO && result == BluetoothStatusCodes.SUCCESS) {
                pendingAudioRoute.addMessage(BT_AUDIO_DISCONNECTED);
            }
        }
    }

@@ -282,4 +347,50 @@ public class AudioRoute {
                + ", Address=" + ((mBluetoothAddress != null) ? mBluetoothAddress : "invalid")
                + "]";
    }

    private boolean connectBtAudio(PendingAudioRoute pendingAudioRoute, BluetoothDevice device,
            AudioManager audioManager, BluetoothRouteManager bluetoothRouteManager) {
        // Ensure that if another BT device was set, it is disconnected before connecting
        // the new one.
        AudioRoute currentRoute = pendingAudioRoute.getOrigRoute();
        if (currentRoute.getBluetoothAddress() != null &&
                !currentRoute.getBluetoothAddress().equals(device.getAddress())) {
            clearCommunicationDevice(pendingAudioRoute, bluetoothRouteManager, audioManager);
        }

        // Connect to the device (explicit handling for HFP devices).
        boolean success = false;
        if (device != null) {
            success = bluetoothRouteManager.getDeviceManager()
                    .connectAudio(device, mAudioRouteType);
        }

        Log.i(this, "connectBtAudio: routeToConnectTo = %s, successful = %b",
                this, success);
        return success;
    }

    private int clearCommunicationDevice(PendingAudioRoute pendingAudioRoute,
            BluetoothRouteManager bluetoothRouteManager, AudioManager audioManager) {
        // Try to see if there's a previously set device for communication that should be cleared.
        // This only serves to help in the SCO case to ensure that we disconnect the headset.
        if (pendingAudioRoute.getCommunicationDeviceType() == AudioRoute.TYPE_INVALID) {
            return -1;
        }

        int result = BluetoothStatusCodes.SUCCESS;
        if (pendingAudioRoute.getCommunicationDeviceType() == TYPE_BLUETOOTH_SCO) {
            Log.i(this, "Disconnecting SCO device.");
            result = bluetoothRouteManager.getDeviceManager().disconnectSco();
        } else {
            Log.i(this, "Clearing communication device for audio type %d.",
                    pendingAudioRoute.getCommunicationDeviceType());
            audioManager.clearCommunicationDevice();
        }

        if (result == BluetoothStatusCodes.SUCCESS) {
            pendingAudioRoute.setCommunicationDeviceType(AudioRoute.TYPE_INVALID);
        }
        return result;
    }
}
+1 −0
Original line number Diff line number Diff line
@@ -134,5 +134,6 @@ public interface CallAudioRouteAdapter {
    CallAudioState getCurrentCallAudioState();
    boolean isHfpDeviceAvailable();
    Handler getAdapterHandler();
    PendingAudioRoute getPendingAudioRoute();
    void dump(IndentingPrintWriter pw);
}
Loading