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

Commit 85660ca1 authored by Pranav Madapurmath's avatar Pranav Madapurmath
Browse files

Support dynamic call audio route updates

This CL leverages the audio fwk API (AudioDeviceCallback) in order to
dynamically update the call audio routes. These were previously only
created once during initialization of the controller but it's possible
that the native audio server hasn't been instantiated yet which causes
issues where the user is only able to route to speaker (and not
earpiece) for example. By using the callback, we can add the supported
routes once they are reported to us by the audio fwk.

Bug: 410037709
Flag: EXEMPT bug fix
Test: atest CallAudioRouteControllerTest
Test: manual verification upon initialization to check that route
calculations come via the callback

Change-Id: I02ffcd516b07da67333df5ff455f9fab6e0e59f3
parent 7009ae13
Loading
Loading
Loading
Loading
+131 −27
Original line number Diff line number Diff line
@@ -31,6 +31,7 @@ import android.content.Intent;
import android.content.IntentFilter;
import android.media.AudioAttributes;
import android.media.AudioDeviceAttributes;
import android.media.AudioDeviceCallback;
import android.media.AudioDeviceInfo;
import android.media.AudioManager;
import android.media.IAudioService;
@@ -126,6 +127,62 @@ public class CallAudioRouteController implements CallAudioRouteAdapter {
    private final TelecomSystem.SyncRoot mTelecomLock;
    private CountDownLatch mAudioOperationsCompleteLatch;
    private CountDownLatch mAudioActiveCompleteLatch;

    /** Receiver for added/removed device outputs that are reported by the audio fwk */
    public class AudioRoutesCallback extends AudioDeviceCallback {
        @Override
        public void onAudioDevicesAdded(AudioDeviceInfo[] addedDevices) {
            Log.startSession("ARC.oADA");
            try {
                updateAudioRoutes(addedDevices, true);
            } finally {
                Log.endSession();
            }
        }

        @Override
        public void onAudioDevicesRemoved(AudioDeviceInfo[] devices) {
            Log.startSession("ARC.oADR");
            try {
                updateAudioRoutes(devices, false);
            } finally {
                Log.endSession();
            }
        }

        private void updateAudioRoutes(AudioDeviceInfo[] devices, boolean addDevices) {
            Log.i(this, "updateAudioRoutes: add devices? %b", addDevices);
            for (AudioDeviceInfo deviceInfo: devices) {
                int audioRouteType = getAudioType(deviceInfo);
                Log.i(this, "updateAudioRoutes: audioDeviceInfo: %s, audioRouteType: %d",
                        deviceInfo, audioRouteType);
                // We should really only worry about handling earpiece and speaker. Bluetooth and
                // wired headset routes are already dynamically updated. This logic can be updated
                // once we support call audio route centralization.
                if (audioRouteType == TYPE_INVALID || audioRouteType == AudioRoute.TYPE_WIRED
                        || BT_AUDIO_ROUTE_TYPES.contains(audioRouteType)) {
                    Log.i(this, "updateAudioRoutes: skipping route.");
                    continue;
                }
                if (addDevices) {
                    switch(audioRouteType) {
                        case AudioRoute.TYPE_SPEAKER:
                            createSpeakerRoute();
                            break;
                        case AudioRoute.TYPE_EARPIECE:
                            createEarpieceRoute();
                            break;
                        default:
                            break;
                    }
                } else {
                    AudioRoute route = mTypeRoutes.remove(audioRouteType);
                    updateAvailableRoutes(route, false);
                }
            }
        }
    }

    private final BroadcastReceiver mSpeakerPhoneChangeReceiver = new BroadcastReceiver() {
        @Override
        public void onReceive(Context context, Intent intent) {
@@ -187,6 +244,7 @@ public class CallAudioRouteController implements CallAudioRouteAdapter {
    private boolean mIsPending;
    private boolean mIsActive;
    private boolean mWasOnSpeaker;
    private AudioRoutesCallback mAudioRoutesCallback;
    private final TelecomMetricsController mMetricsController;

    public CallAudioRouteController(
@@ -404,23 +462,7 @@ public class CallAudioRouteController implements CallAudioRouteAdapter {
        int supportMask = calculateSupportedRouteMaskInit();
        if ((supportMask & CallAudioState.ROUTE_SPEAKER) != 0) {
            int audioRouteType = AudioRoute.TYPE_SPEAKER;
            // Create speaker routes
            mSpeakerDockRoute = mAudioRouteFactory.create(AudioRoute.TYPE_SPEAKER, null,
                    mAudioManager);
            if (mSpeakerDockRoute == null){
                Log.i(this, "Can't find available audio device info for route TYPE_SPEAKER, trying"
                        + " for TYPE_BUS");
                mSpeakerDockRoute = mAudioRouteFactory.create(AudioRoute.TYPE_BUS, null,
                        mAudioManager);
                audioRouteType = AudioRoute.TYPE_BUS;
            }
            if (mSpeakerDockRoute != null) {
                mTypeRoutes.put(audioRouteType, mSpeakerDockRoute);
                updateAvailableRoutes(mSpeakerDockRoute, true);
            } else {
                Log.w(this, "Can't find available audio device info for route TYPE_SPEAKER "
                        + "or TYPE_BUS.");
            }
            createSpeakerRoute();
        }

        if ((supportMask & CallAudioState.ROUTE_WIRED_HEADSET) != 0) {
@@ -434,15 +476,7 @@ public class CallAudioRouteController implements CallAudioRouteAdapter {
                updateAvailableRoutes(mEarpieceWiredRoute, true);
            }
        } else if ((supportMask & CallAudioState.ROUTE_EARPIECE) != 0) {
            // Create earpiece routes
            mEarpieceWiredRoute = mAudioRouteFactory.create(AudioRoute.TYPE_EARPIECE, null,
                    mAudioManager);
            if (mEarpieceWiredRoute == null) {
                Log.w(this, "Can't find available audio device info for route TYPE_EARPIECE");
            } else {
                mTypeRoutes.put(AudioRoute.TYPE_EARPIECE, mEarpieceWiredRoute);
                updateAvailableRoutes(mEarpieceWiredRoute, true);
            }
            createEarpieceRoute();
        }

        // set current route
@@ -464,6 +498,8 @@ public class CallAudioRouteController implements CallAudioRouteAdapter {
                supportMask, null, new HashSet<>());
        mAudioManager.addOnCommunicationDeviceChangedListener(
                mCommunicationDeviceChangedExecutor, mCommunicationDeviceListener);
        mAudioRoutesCallback = new AudioRoutesCallback();
        mAudioManager.registerAudioDeviceCallback(mAudioRoutesCallback, null);
    }

    @Override
@@ -1427,8 +1463,15 @@ public class CallAudioRouteController implements CallAudioRouteAdapter {
        } else {
            AudioDeviceInfo[] deviceList = mAudioManager.getDevices(
                    AudioManager.GET_DEVICES_OUTPUTS);
            // For debugging purposes in cases where the device list returned by the API fwk is
            // empty and we don't end up adding the earpiece route upon init.
            Log.i(this, "calculateSupportedRouteMaskInit: is device list size: %d",
                    deviceList.length);
            for (AudioDeviceInfo device: deviceList) {
                if (device.getType() == AudioDeviceInfo.TYPE_BUILTIN_EARPIECE) {
                Log.i(this, "calculateSupportedRouteMaskInit: audio route type from audio "
                        + "device info: %d", device != null ? DEVICE_INFO_TYPE_TO_AUDIO_ROUTE_TYPE
                        .getOrDefault(device.getType(), TYPE_INVALID) : TYPE_INVALID);
                if (device != null && device.getType() == AudioDeviceInfo.TYPE_BUILTIN_EARPIECE) {
                    routeMask |= CallAudioState.ROUTE_EARPIECE;
                    break;
                }
@@ -1692,6 +1735,9 @@ public class CallAudioRouteController implements CallAudioRouteAdapter {
    }

    private void updateAvailableRoutes(AudioRoute route, boolean includeRoute) {
        if (route == null) {
            return;
        }
        if (includeRoute) {
            mAvailableRoutes.add(route);
        } else {
@@ -1781,4 +1827,62 @@ public class CallAudioRouteController implements CallAudioRouteAdapter {
        }
        return isDestRouteActive;
    }

    private void createSpeakerRoute() {
        int audioRouteType = TYPE_SPEAKER;
        if (mSpeakerDockRoute == null) {
            //create type speaker
            mSpeakerDockRoute = mAudioRouteFactory.create(audioRouteType, null,
                    mAudioManager);
            // If speaker route couldn't be instantiated, try for TYPE_BUS
            if (mSpeakerDockRoute == null) {
                Log.i(this, "createSpeakerRoute: Can't find available audio device info for "
                        + "route TYPE_SPEAKER, trying for TYPE_BUS");
                mSpeakerDockRoute = mAudioRouteFactory.create(AudioRoute.TYPE_BUS, null,
                        mAudioManager);
                audioRouteType = AudioRoute.TYPE_BUS;
            }
            if (mSpeakerDockRoute == null) {
                Log.w(this, "createSpeakerRoute: Can't find available audio device info "
                        + "for route TYPE_SPEAKER or TYPE_BUS.");
            } else {
                // Update available routes
                mTypeRoutes.put(audioRouteType, mSpeakerDockRoute);
                updateAvailableRoutes(mSpeakerDockRoute, true);
            }
        } else {
            Log.i(this, "createSpeakerRoute: route already created. Skipping.");
        }
    }

    private void createEarpieceRoute() {
        // Create earpiece route
        if (mEarpieceWiredRoute != null) {
            Log.i(this, "createEarpieceRoute: route already created. Skipping.");
            return;
        }
        mEarpieceWiredRoute = mAudioRouteFactory.create(AudioRoute.TYPE_EARPIECE, null,
                mAudioManager);
        if (mEarpieceWiredRoute == null) {
            Log.w(this, "createEarpieceRoute: Can't find available audio device info for "
                    + "route TYPE_EARPIECE");
        } else {
            mTypeRoutes.put(AudioRoute.TYPE_EARPIECE, mEarpieceWiredRoute);
            updateAvailableRoutes(mEarpieceWiredRoute, true);
        }
    }

    @VisibleForTesting
    public AudioRoute getAudioRouteForTesting(int audioRouteType) {
        return switch (audioRouteType) {
            case AudioRoute.TYPE_EARPIECE, AudioRoute.TYPE_WIRED -> mEarpieceWiredRoute;
            case AudioRoute.TYPE_SPEAKER -> mSpeakerDockRoute;
            default -> DUMMY_ROUTE;
        };
    }

    @VisibleForTesting
    public AudioRoutesCallback getAudioRoutesCallback() {
        return mAudioRoutesCallback;
    }
}
+41 −0
Original line number Diff line number Diff line
@@ -46,6 +46,8 @@ import static com.android.server.telecom.CallAudioRouteAdapter.USER_SWITCH_SPEAK
import static com.android.server.telecom.CallAudioRouteController.INCLUDE_BLUETOOTH_IN_BASELINE;
import static org.junit.Assert.assertEquals;
import static org.junit.Assert.assertFalse;
import static org.junit.Assert.assertNotNull;
import static org.junit.Assert.assertNull;
import static org.junit.Assert.assertTrue;
import static org.mockito.ArgumentMatchers.anyBoolean;
import static org.mockito.ArgumentMatchers.anyInt;
@@ -1535,6 +1537,45 @@ public class CallAudioRouteControllerTest extends TelecomTestCase {
        BLUETOOTH_DEVICES.remove(scoDevice);
    }

    @Test
    @SmallTest
    public void testAddAudioRoutesDynamic() {
        AudioRoute.Factory audioRouteFactory = new AudioRoute.Factory() {
            @Override
            public AudioRoute create(@AudioRoute.AudioRouteType int type, String bluetoothAddress,
                    AudioManager audioManager) {
                if (mOverrideSpeakerToBus && type == AudioRoute.TYPE_SPEAKER) {
                    type = AudioRoute.TYPE_BUS;
                }
                // Purposely return null to mimic audio routes not being created upon
                // initialization.
                return null;
            }
        };
        mController.setAudioRouteFactory(audioRouteFactory);
        mController.initialize();
        // Verify that the earpiece/speaker routes aren't created upon initialization of the
        // controller.
        assertNull(mController.getAudioRouteForTesting(AudioRoute.TYPE_SPEAKER));
        assertNull(mController.getAudioRouteForTesting(AudioRoute.TYPE_EARPIECE));

        // Set up the AudioDeviceCallback to signal to the controller of the newly added devices
        // (earpiece + speaker).
        CallAudioRouteController.AudioRoutesCallback callback = mController
                .getAudioRoutesCallback();
        AudioDeviceInfo earpieceDeviceInfo = mock(AudioDeviceInfo.class);
        when(earpieceDeviceInfo.getType()).thenReturn(AudioDeviceInfo.TYPE_BUILTIN_EARPIECE);
        AudioDeviceInfo speakerDeviceInfo = mock(AudioDeviceInfo.class);
        when(speakerDeviceInfo.getType()).thenReturn(AudioDeviceInfo.TYPE_BUILTIN_SPEAKER);

        // Reset the audio route factory so that the route creation can be successful now.
        mController.setAudioRouteFactory(mAudioRouteFactory);
        callback.onAudioDevicesAdded(new AudioDeviceInfo[] {earpieceDeviceInfo, speakerDeviceInfo});
        // Verify that the earpiece/speaker routes are created this time around.
        assertNotNull(mController.getAudioRouteForTesting(AudioRoute.TYPE_SPEAKER));
        assertNotNull(mController.getAudioRouteForTesting(AudioRoute.TYPE_EARPIECE));
    }

    private void verifyConnectBluetoothDevice(int audioType) {
        mController.initialize();
        mController.setActive(true);