diff options
| author | 2024-09-24 22:43:48 +0000 | |
|---|---|---|
| committer | 2024-09-24 22:43:48 +0000 | |
| commit | 5b5465ea63f997518a1bec7f2c386f43cc1230bb (patch) | |
| tree | 2e74559f5f6f1967d4a046acf9a0dcc1326efbe7 | |
| parent | afde520ff12a789e16053868ba483a59a3087012 (diff) | |
| parent | 03a3dbdcaaf462b60a87256ec80409afa43d3131 (diff) | |
Merge "Support selecting input devices" into main
4 files changed, 161 insertions, 6 deletions
diff --git a/packages/SettingsLib/src/com/android/settingslib/media/InputRouteManager.java b/packages/SettingsLib/src/com/android/settingslib/media/InputRouteManager.java index 0c50166fff8b..9164b64057fd 100644 --- a/packages/SettingsLib/src/com/android/settingslib/media/InputRouteManager.java +++ b/packages/SettingsLib/src/com/android/settingslib/media/InputRouteManager.java @@ -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,17 +88,33 @@ public final class InputRouteManager { Handler handler = new Handler(context.getMainLooper()); mAudioManager.registerAudioDeviceCallback(mAudioDeviceCallback, handler); + + mAudioManager.addOnPreferredDevicesForCapturePresetChangedListener( + new HandlerExecutor(handler), + this::onPreferredDevicesForCapturePresetChangedListener); } - public void registerCallback(@NonNull InputDeviceCallback callback) { - if (!mCallbacks.contains(callback)) { - mCallbacks.add(callback); + 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) { - mCallbacks.remove(callback); + synchronized (mCallbackLock) { + mCallbacks.remove(callback); + } } public @Nullable MediaDevice getSelectedInputDevice() { @@ -134,8 +162,51 @@ public final class InputRouteManager { } final List<MediaDevice> inputMediaDevices = new ArrayList<>(mInputMediaDevices); - for (InputDeviceCallback callback : mCallbacks) { - callback.onInputDeviceListUpdated(inputMediaDevices); + 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); } } diff --git a/packages/SettingsLib/tests/robotests/src/com/android/settingslib/media/InputRouteManagerTest.java b/packages/SettingsLib/tests/robotests/src/com/android/settingslib/media/InputRouteManagerTest.java index 8a18d0714e0a..16c0c1c71d79 100644 --- a/packages/SettingsLib/tests/robotests/src/com/android/settingslib/media/InputRouteManagerTest.java +++ b/packages/SettingsLib/tests/robotests/src/com/android/settingslib/media/InputRouteManagerTest.java @@ -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; @@ -222,6 +229,31 @@ public class InputRouteManagerTest { } @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); } diff --git a/packages/SystemUI/src/com/android/systemui/media/dialog/MediaSwitchingController.java b/packages/SystemUI/src/com/android/systemui/media/dialog/MediaSwitchingController.java index f7b73534d35c..b69b25dbddf2 100644 --- a/packages/SystemUI/src/com/android/systemui/media/dialog/MediaSwitchingController.java +++ b/packages/SystemUI/src/com/android/systemui/media/dialog/MediaSwitchingController.java @@ -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(() -> { diff --git a/packages/SystemUI/tests/src/com/android/systemui/media/dialog/MediaSwitchingControllerTest.java b/packages/SystemUI/tests/src/com/android/systemui/media/dialog/MediaSwitchingControllerTest.java index 53f0800a7d13..c4f5d621cb6c 100644 --- a/packages/SystemUI/tests/src/com/android/systemui/media/dialog/MediaSwitchingControllerTest.java +++ b/packages/SystemUI/tests/src/com/android/systemui/media/dialog/MediaSwitchingControllerTest.java @@ -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); + } } |