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

Commit 5b5465ea authored by Wenyu Zhang's avatar Wenyu Zhang Committed by Android (Google) Code Review
Browse files

Merge "Support selecting input devices" into main

parents afde520f 03a3dbdc
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);
    }
}