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

Commit 03a3dbdc authored by Wenyu Zhang's avatar Wenyu Zhang
Browse files

Support selecting input devices

InputRouteManager calls AudioManager.setPreferredDeviceForCapturePreset
to select preferred input device.
InputRouteManager adds OnPreferredDevicesForCapturePresetChangedListener
to listen to preferred input device changes and then updates UI.

Change-Id: I8eefceb62699aca547dec438c00d2aada41cedc2
Bug: b/355684672, b/357123258
Test atest MediaSwitchingControllerTest,InputRouteManagerTest
Flag: com.android.media.flags.enable_audio_input_device_routing_and_volume_control
parent 33c87751
Loading
Loading
Loading
Loading
+77 −6
Original line number Diff line number Diff line
@@ -25,6 +25,7 @@ import android.media.AudioDeviceInfo;
import android.media.AudioManager;
import android.media.MediaRecorder;
import android.os.Handler;
import android.os.HandlerExecutor;
import android.util.Slog;

import androidx.annotation.NonNull;
@@ -46,6 +47,16 @@ public final class InputRouteManager {
    static final AudioAttributes INPUT_ATTRIBUTES =
            new AudioAttributes.Builder().setCapturePreset(MediaRecorder.AudioSource.MIC).build();

    @VisibleForTesting
    static final int[] PRESETS = {
        MediaRecorder.AudioSource.MIC,
        MediaRecorder.AudioSource.CAMCORDER,
        MediaRecorder.AudioSource.VOICE_RECOGNITION,
        MediaRecorder.AudioSource.VOICE_COMMUNICATION,
        MediaRecorder.AudioSource.UNPROCESSED,
        MediaRecorder.AudioSource.VOICE_PERFORMANCE
    };

    private final Context mContext;

    private final AudioManager mAudioManager;
@@ -55,6 +66,7 @@ public final class InputRouteManager {
    private MediaDevice mSelectedInputDevice;

    private final Collection<InputDeviceCallback> mCallbacks = new CopyOnWriteArrayList<>();
    private final Object mCallbackLock = new Object();

    @VisibleForTesting
    final AudioDeviceCallback mAudioDeviceCallback =
@@ -76,18 +88,34 @@ public final class InputRouteManager {
        Handler handler = new Handler(context.getMainLooper());

        mAudioManager.registerAudioDeviceCallback(mAudioDeviceCallback, handler);

        mAudioManager.addOnPreferredDevicesForCapturePresetChangedListener(
                new HandlerExecutor(handler),
                this::onPreferredDevicesForCapturePresetChangedListener);
    }

    private void onPreferredDevicesForCapturePresetChangedListener(
            @MediaRecorder.SystemSource int capturePreset,
            @NonNull List<AudioDeviceAttributes> devices) {
        if (capturePreset == MediaRecorder.AudioSource.MIC) {
            dispatchInputDeviceListUpdate();
        }
    }

    public void registerCallback(@NonNull InputDeviceCallback callback) {
        synchronized (mCallbackLock) {
            if (!mCallbacks.contains(callback)) {
                mCallbacks.add(callback);
                dispatchInputDeviceListUpdate();
            }
        }
    }

    public void unregisterCallback(@NonNull InputDeviceCallback callback) {
        synchronized (mCallbackLock) {
            mCallbacks.remove(callback);
        }
    }

    public @Nullable MediaDevice getSelectedInputDevice() {
        return mSelectedInputDevice;
@@ -134,10 +162,53 @@ public final class InputRouteManager {
        }

        final List<MediaDevice> inputMediaDevices = new ArrayList<>(mInputMediaDevices);
        synchronized (mCallbackLock) {
            for (InputDeviceCallback callback : mCallbacks) {
                callback.onInputDeviceListUpdated(inputMediaDevices);
            }
        }
    }

    public void selectDevice(@NonNull MediaDevice device) {
        if (!(device instanceof InputMediaDevice)) {
            Slog.w(TAG, "This device is not an InputMediaDevice: " + device.getName());
            return;
        }

        if (device.equals(mSelectedInputDevice)) {
            Slog.w(TAG, "This device is already selected: " + device.getName());
            return;
        }

        // Handle edge case where the targeting device is not available, e.g. disconnected.
        if (!mInputMediaDevices.contains(device)) {
            Slog.w(TAG, "This device is not available: " + device.getName());
            return;
        }

        // TODO(b/355684672): apply address for BT devices.
        AudioDeviceAttributes deviceAttributes =
                new AudioDeviceAttributes(
                        AudioDeviceAttributes.ROLE_INPUT,
                        ((InputMediaDevice) device).getAudioDeviceInfoType(),
                        /* address= */ "");
        try {
            setPreferredDeviceForAllPresets(deviceAttributes);
        } catch (IllegalArgumentException e) {
            Slog.e(
                    TAG,
                    "Illegal argument exception while setPreferredDeviceForAllPreset: "
                            + device.getName(),
                    e);
        }
    }

    private void setPreferredDeviceForAllPresets(@NonNull AudioDeviceAttributes deviceAttributes) {
        // The input routing via system setting takes effect on all capture presets.
        for (@MediaRecorder.Source int preset : PRESETS) {
            mAudioManager.setPreferredDeviceForCapturePreset(preset, deviceAttributes);
        }
    }

    public int getMaxInputGain() {
        // TODO (b/357123335): use real input gain implementation.
+32 −0
Original line number Diff line number Diff line
@@ -17,17 +17,21 @@
package com.android.settingslib.media;

import static com.android.settingslib.media.InputRouteManager.INPUT_ATTRIBUTES;
import static com.android.settingslib.media.InputRouteManager.PRESETS;

import static com.google.common.truth.Truth.assertThat;

import static org.mockito.Mockito.atLeastOnce;
import static org.mockito.Mockito.mock;
import static org.mockito.Mockito.spy;
import static org.mockito.Mockito.verify;
import static org.mockito.Mockito.when;

import android.content.Context;
import android.media.AudioDeviceAttributes;
import android.media.AudioDeviceInfo;
import android.media.AudioManager;
import android.media.MediaRecorder;

import com.android.settingslib.testutils.shadow.ShadowRouter2Manager;

@@ -51,6 +55,9 @@ public class InputRouteManagerTest {
    private static final int INPUT_USB_DEVICE_ID = 3;
    private static final int INPUT_USB_HEADSET_ID = 4;
    private static final int INPUT_USB_ACCESSORY_ID = 5;
    private static final int MAX_VOLUME = 1;
    private static final int CURRENT_VOLUME = 0;
    private static final boolean VOLUME_FIXED_TRUE = true;

    private final Context mContext = spy(RuntimeEnvironment.application);
    private InputRouteManager mInputRouteManager;
@@ -221,6 +228,31 @@ public class InputRouteManagerTest {
                .isEqualTo(AudioDeviceInfo.TYPE_BUILTIN_MIC);
    }

    @Test
    public void selectDevice() {
        final AudioManager audioManager = mock(AudioManager.class);
        InputRouteManager inputRouteManager = new InputRouteManager(mContext, audioManager);
        final MediaDevice inputMediaDevice =
                InputMediaDevice.create(
                        mContext,
                        String.valueOf(BUILTIN_MIC_ID),
                        AudioDeviceInfo.TYPE_BUILTIN_MIC,
                        MAX_VOLUME,
                        CURRENT_VOLUME,
                        VOLUME_FIXED_TRUE);
        inputRouteManager.selectDevice(inputMediaDevice);

        AudioDeviceAttributes deviceAttributes =
                new AudioDeviceAttributes(
                        AudioDeviceAttributes.ROLE_INPUT,
                        AudioDeviceInfo.TYPE_BUILTIN_MIC,
                        /* address= */ "");
        for (@MediaRecorder.Source int preset : PRESETS) {
            verify(audioManager, atLeastOnce())
                    .setPreferredDeviceForCapturePreset(preset, deviceAttributes);
        }
    }

    @Test
    public void getMaxInputGain_returnMaxInputGain() {
        assertThat(mInputRouteManager.getMaxInputGain()).isEqualTo(15);
+12 −0
Original line number Diff line number Diff line
@@ -77,6 +77,7 @@ import com.android.settingslib.bluetooth.LocalBluetoothLeBroadcastAssistant;
import com.android.settingslib.bluetooth.LocalBluetoothLeBroadcastMetadata;
import com.android.settingslib.bluetooth.LocalBluetoothManager;
import com.android.settingslib.media.InfoMediaManager;
import com.android.settingslib.media.InputMediaDevice;
import com.android.settingslib.media.InputRouteManager;
import com.android.settingslib.media.LocalMediaManager;
import com.android.settingslib.media.MediaDevice;
@@ -875,6 +876,17 @@ public class MediaSwitchingController
    }

    protected void connectDevice(MediaDevice device) {
        // If input routing is supported and the device is an input device, call mInputRouteManager
        // to handle routing.
        if (enableInputRouting() && device instanceof InputMediaDevice) {
            var unused =
                    ThreadUtils.postOnBackgroundThread(
                            () -> {
                                mInputRouteManager.selectDevice(device);
                            });
            return;
        }

        mMetricLogger.updateOutputEndPoints(getCurrentConnectedMediaDevice(), device);

        ThreadUtils.postOnBackgroundThread(() -> {
+40 −0
Original line number Diff line number Diff line
@@ -76,6 +76,7 @@ import com.android.settingslib.media.InputMediaDevice;
import com.android.settingslib.media.InputRouteManager;
import com.android.settingslib.media.LocalMediaManager;
import com.android.settingslib.media.MediaDevice;
import com.android.settingslib.utils.ThreadUtils;
import com.android.systemui.SysuiTestCase;
import com.android.systemui.SysuiTestCaseExtKt;
import com.android.systemui.animation.ActivityTransitionAnimator;
@@ -103,6 +104,8 @@ import org.mockito.MockitoAnnotations;
import java.util.ArrayList;
import java.util.Collections;
import java.util.List;
import java.util.concurrent.CountDownLatch;
import java.util.concurrent.TimeUnit;

@SmallTest
@RunWith(AndroidJUnit4.class)
@@ -120,6 +123,8 @@ public class MediaSwitchingControllerTest extends SysuiTestCase {
    private static final int MAX_VOLUME = 1;
    private static final int CURRENT_VOLUME = 0;
    private static final boolean VOLUME_FIXED_TRUE = true;
    private static final int LATCH_COUNT_DOWN_TIME_IN_SECOND = 5;
    private static final int LATCH_TIME_OUT_TIME_IN_SECOND = 10;

    @Mock
    private DialogTransitionAnimator mDialogTransitionAnimator;
@@ -1339,4 +1344,39 @@ public class MediaSwitchingControllerTest extends SysuiTestCase {
        assertThat(selectedMediaDevices)
                .containsExactly(selectedOutputMediaDevice, selectedInputMediaDevice);
    }

    @EnableFlags(Flags.FLAG_ENABLE_AUDIO_INPUT_DEVICE_ROUTING_AND_VOLUME_CONTROL)
    @Test
    public void selectInputDevice() throws InterruptedException {
        final MediaDevice inputMediaDevice =
                InputMediaDevice.create(
                        mContext,
                        TEST_DEVICE_1_ID,
                        AudioDeviceInfo.TYPE_BUILTIN_MIC,
                        MAX_VOLUME,
                        CURRENT_VOLUME,
                        VOLUME_FIXED_TRUE);
        mMediaSwitchingController.connectDevice(inputMediaDevice);

        CountDownLatch latch = new CountDownLatch(LATCH_COUNT_DOWN_TIME_IN_SECOND);
        var unused = ThreadUtils.postOnBackgroundThread(latch::countDown);
        latch.await(LATCH_TIME_OUT_TIME_IN_SECOND, TimeUnit.SECONDS);

        verify(mInputRouteManager, atLeastOnce()).selectDevice(inputMediaDevice);
        verify(mLocalMediaManager, never()).connectDevice(inputMediaDevice);
    }

    @EnableFlags(Flags.FLAG_ENABLE_AUDIO_INPUT_DEVICE_ROUTING_AND_VOLUME_CONTROL)
    @Test
    public void selectOutputDevice() throws InterruptedException {
        final MediaDevice outputMediaDevice = mock(MediaDevice.class);
        mMediaSwitchingController.connectDevice(outputMediaDevice);

        CountDownLatch latch = new CountDownLatch(LATCH_COUNT_DOWN_TIME_IN_SECOND);
        var unused = ThreadUtils.postOnBackgroundThread(latch::countDown);
        latch.await(LATCH_TIME_OUT_TIME_IN_SECOND, TimeUnit.SECONDS);

        verify(mInputRouteManager, never()).selectDevice(outputMediaDevice);
        verify(mLocalMediaManager, atLeastOnce()).connectDevice(outputMediaDevice);
    }
}