diff options
author | 2024-09-13 11:29:28 +0000 | |
---|---|---|
committer | 2024-09-13 11:29:28 +0000 | |
commit | ada98fa12e255db3a6a9a4a791c1aedbe1143e4f (patch) | |
tree | 2b3e320c64ad982e19beb37c02fa7ed7b227683e | |
parent | f9a051da904e9b9e40a643a46a769abefe5c3437 (diff) | |
parent | 7bfb54f7f4ff381dedc0b2cfbc1b6c34c7ea1ff6 (diff) |
Merge "Add InputRouteManager and InputMediaDevice to support input routing" into main
5 files changed, 549 insertions, 0 deletions
diff --git a/packages/SettingsLib/res/values/strings.xml b/packages/SettingsLib/res/values/strings.xml index 4d771c0f8d71..feee89a51e7c 100644 --- a/packages/SettingsLib/res/values/strings.xml +++ b/packages/SettingsLib/res/values/strings.xml @@ -1411,6 +1411,8 @@ <string name="media_transfer_this_device_name_tablet">This tablet</string> <!-- Name of the default media output of the TV. [CHAR LIMIT=30] --> <string name="media_transfer_this_device_name_tv">@string/tv_media_transfer_default</string> + <!-- Name of the internal mic. [CHAR LIMIT=30] --> + <string name="media_transfer_internal_mic">Microphone (internal)</string> <!-- Name of the dock device. [CHAR LIMIT=30] --> <string name="media_transfer_dock_speaker_device_name">Dock speaker</string> <!-- Default name of the external device. [CHAR LIMIT=30] --> @@ -1637,6 +1639,12 @@ <!-- Name of the 3.5mm and usb audio device. [CHAR LIMIT=50] --> <string name="media_transfer_wired_usb_device_name">Wired headphone</string> + <!-- Name of the 3.5mm audio device mic. [CHAR LIMIT=50] --> + <string name="media_transfer_wired_device_mic_name">Mic jack</string> + + <!-- Name of the usb audio device mic. [CHAR LIMIT=50] --> + <string name="media_transfer_usb_device_mic_name">USB mic</string> + <!-- Label for Wifi hotspot switch on. Toggles hotspot on [CHAR LIMIT=30] --> <string name="wifi_hotspot_switch_on_text">On</string> <!-- Label for Wifi hotspot switch off. Toggles hotspot off [CHAR LIMIT=30] --> diff --git a/packages/SettingsLib/src/com/android/settingslib/media/InputMediaDevice.java b/packages/SettingsLib/src/com/android/settingslib/media/InputMediaDevice.java new file mode 100644 index 000000000000..766cd438a811 --- /dev/null +++ b/packages/SettingsLib/src/com/android/settingslib/media/InputMediaDevice.java @@ -0,0 +1,161 @@ +/* + * Copyright 2024 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package com.android.settingslib.media; + +import static android.media.AudioDeviceInfo.TYPE_BUILTIN_MIC; +import static android.media.AudioDeviceInfo.TYPE_USB_ACCESSORY; +import static android.media.AudioDeviceInfo.TYPE_USB_DEVICE; +import static android.media.AudioDeviceInfo.TYPE_USB_HEADSET; +import static android.media.AudioDeviceInfo.TYPE_WIRED_HEADSET; + +import static com.android.settingslib.media.MediaDevice.SelectionBehavior.SELECTION_BEHAVIOR_TRANSFER; + +import android.content.Context; +import android.graphics.drawable.Drawable; +import android.media.AudioDeviceInfo.AudioDeviceType; + +import androidx.annotation.NonNull; +import androidx.annotation.Nullable; +import androidx.annotation.VisibleForTesting; + +import com.android.settingslib.R; + +/** {@link MediaDevice} implementation that represents an input device. */ +public class InputMediaDevice extends MediaDevice { + + private static final String TAG = "InputMediaDevice"; + + private final String mId; + + private final @AudioDeviceType int mAudioDeviceInfoType; + + private final int mMaxVolume; + + private final int mCurrentVolume; + + private final boolean mIsVolumeFixed; + + private InputMediaDevice( + @NonNull Context context, + @NonNull String id, + @AudioDeviceType int audioDeviceInfoType, + int maxVolume, + int currentVolume, + boolean isVolumeFixed) { + super(context, /* info= */ null, /* item= */ null); + mId = id; + mAudioDeviceInfoType = audioDeviceInfoType; + mMaxVolume = maxVolume; + mCurrentVolume = currentVolume; + mIsVolumeFixed = isVolumeFixed; + initDeviceRecord(); + } + + @Nullable + public static InputMediaDevice create( + @NonNull Context context, + @NonNull String id, + @AudioDeviceType int audioDeviceInfoType, + int maxVolume, + int currentVolume, + boolean isVolumeFixed) { + if (!isSupportedInputDevice(audioDeviceInfoType)) { + return null; + } + + return new InputMediaDevice( + context, id, audioDeviceInfoType, maxVolume, currentVolume, isVolumeFixed); + } + + public static boolean isSupportedInputDevice(@AudioDeviceType int audioDeviceInfoType) { + return switch (audioDeviceInfoType) { + case TYPE_BUILTIN_MIC, + TYPE_WIRED_HEADSET, + TYPE_USB_DEVICE, + TYPE_USB_HEADSET, + TYPE_USB_ACCESSORY -> + true; + default -> false; + }; + } + + @Override + public @NonNull String getName() { + CharSequence name = + switch (mAudioDeviceInfoType) { + case TYPE_WIRED_HEADSET -> + mContext.getString(R.string.media_transfer_wired_device_mic_name); + case TYPE_USB_DEVICE, TYPE_USB_HEADSET, TYPE_USB_ACCESSORY -> + mContext.getString(R.string.media_transfer_usb_device_mic_name); + default -> mContext.getString(R.string.media_transfer_internal_mic); + }; + return name.toString(); + } + + @Override + public @SelectionBehavior int getSelectionBehavior() { + // We don't allow apps to override the selection behavior of system routes. + return SELECTION_BEHAVIOR_TRANSFER; + } + + @Override + public @NonNull String getSummary() { + return ""; + } + + @Override + public @Nullable Drawable getIcon() { + return getIconWithoutBackground(); + } + + @Override + public @Nullable Drawable getIconWithoutBackground() { + return mContext.getDrawable(getDrawableResId()); + } + + @VisibleForTesting + int getDrawableResId() { + // TODO(b/357122624): check with UX to obtain the icon for desktop devices. + return R.drawable.ic_media_tablet; + } + + @Override + public @NonNull String getId() { + return mId; + } + + @Override + public boolean isConnected() { + // Indicating if the device is connected and thus showing the status of STATE_CONNECTED. + // Upon creation, this device is already connected. + return true; + } + + @Override + public int getMaxVolume() { + return mMaxVolume; + } + + @Override + public int getCurrentVolume() { + return mCurrentVolume; + } + + @Override + public boolean isVolumeFixed() { + return mIsVolumeFixed; + } +} diff --git a/packages/SettingsLib/src/com/android/settingslib/media/InputRouteManager.java b/packages/SettingsLib/src/com/android/settingslib/media/InputRouteManager.java new file mode 100644 index 000000000000..548eb3fd4b8f --- /dev/null +++ b/packages/SettingsLib/src/com/android/settingslib/media/InputRouteManager.java @@ -0,0 +1,126 @@ +/* + * Copyright 2024 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package com.android.settingslib.media; + +import android.content.Context; +import android.media.AudioDeviceCallback; +import android.media.AudioDeviceInfo; +import android.media.AudioManager; +import android.os.Handler; + +import androidx.annotation.NonNull; + +import com.android.internal.annotations.VisibleForTesting; + +import java.util.ArrayList; +import java.util.Collection; +import java.util.List; +import java.util.concurrent.CopyOnWriteArrayList; + +/** Provides functionalities to get/observe input routes, control input routing and volume gain. */ +public final class InputRouteManager { + + private static final String TAG = "InputRouteManager"; + + private final Context mContext; + + private final AudioManager mAudioManager; + + @VisibleForTesting final List<MediaDevice> mInputMediaDevices = new CopyOnWriteArrayList<>(); + + private final Collection<InputDeviceCallback> mCallbacks = new CopyOnWriteArrayList<>(); + + @VisibleForTesting + final AudioDeviceCallback mAudioDeviceCallback = + new AudioDeviceCallback() { + @Override + public void onAudioDevicesAdded(@NonNull AudioDeviceInfo[] addedDevices) { + dispatchInputDeviceListUpdate(); + } + + @Override + public void onAudioDevicesRemoved(@NonNull AudioDeviceInfo[] removedDevices) { + dispatchInputDeviceListUpdate(); + } + }; + + /* package */ InputRouteManager(@NonNull Context context, @NonNull AudioManager audioManager) { + mContext = context; + mAudioManager = audioManager; + Handler handler = new Handler(context.getMainLooper()); + + mAudioManager.registerAudioDeviceCallback(mAudioDeviceCallback, handler); + } + + public void registerCallback(@NonNull InputDeviceCallback callback) { + if (!mCallbacks.contains(callback)) { + mCallbacks.add(callback); + dispatchInputDeviceListUpdate(); + } + } + + public void unregisterCallback(@NonNull InputDeviceCallback callback) { + mCallbacks.remove(callback); + } + + private void dispatchInputDeviceListUpdate() { + // TODO (b/360175574): Get selected input device. + + // Get all input devices. + AudioDeviceInfo[] audioDeviceInfos = + mAudioManager.getDevices(AudioManager.GET_DEVICES_INPUTS); + mInputMediaDevices.clear(); + for (AudioDeviceInfo info : audioDeviceInfos) { + MediaDevice mediaDevice = + InputMediaDevice.create( + mContext, + String.valueOf(info.getId()), + info.getType(), + getMaxInputGain(), + getCurrentInputGain(), + isInputGainFixed()); + if (mediaDevice != null) { + mInputMediaDevices.add(mediaDevice); + } + } + + final List<MediaDevice> inputMediaDevices = new ArrayList<>(mInputMediaDevices); + for (InputDeviceCallback callback : mCallbacks) { + callback.onInputDeviceListUpdated(inputMediaDevices); + } + } + + public int getMaxInputGain() { + // TODO (b/357123335): use real input gain implementation. + // Using 15 for now since it matches the max index for output. + return 15; + } + + public int getCurrentInputGain() { + // TODO (b/357123335): use real input gain implementation. + return 8; + } + + public boolean isInputGainFixed() { + // TODO (b/357123335): use real input gain implementation. + return true; + } + + /** Callback for listening to input device changes. */ + public interface InputDeviceCallback { + void onInputDeviceListUpdated(@NonNull List<MediaDevice> devices); + } +} diff --git a/packages/SettingsLib/tests/robotests/src/com/android/settingslib/media/InputMediaDeviceTest.java b/packages/SettingsLib/tests/robotests/src/com/android/settingslib/media/InputMediaDeviceTest.java new file mode 100644 index 000000000000..bc1ea6c42fa3 --- /dev/null +++ b/packages/SettingsLib/tests/robotests/src/com/android/settingslib/media/InputMediaDeviceTest.java @@ -0,0 +1,114 @@ +/* + * Copyright 2024 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.android.settingslib.media; + +import static com.google.common.truth.Truth.assertThat; + +import static org.mockito.Mockito.mock; +import static org.mockito.Mockito.when; + +import android.content.Context; +import android.media.AudioDeviceInfo; +import android.platform.test.flag.junit.SetFlagsRule; + +import com.android.settingslib.R; + +import org.junit.Before; +import org.junit.Rule; +import org.junit.Test; +import org.junit.runner.RunWith; +import org.robolectric.RobolectricTestRunner; +import org.robolectric.RuntimeEnvironment; + +@RunWith(RobolectricTestRunner.class) +public class InputMediaDeviceTest { + + private final int BUILTIN_MIC_ID = 1; + private final int WIRED_HEADSET_ID = 2; + private final int USB_HEADSET_ID = 3; + private final int MAX_VOLUME = 1; + private final int CURRENT_VOLUME = 0; + private final boolean IS_VOLUME_FIXED = true; + + @Rule public final SetFlagsRule mSetFlagsRule = new SetFlagsRule(); + + private Context mContext; + + @Before + public void setUp() { + mContext = RuntimeEnvironment.application; + } + + @Test + public void getDrawableResId_returnCorrectResId() { + InputMediaDevice builtinMediaDevice = + InputMediaDevice.create( + mContext, + String.valueOf(BUILTIN_MIC_ID), + AudioDeviceInfo.TYPE_BUILTIN_MIC, + MAX_VOLUME, + CURRENT_VOLUME, + IS_VOLUME_FIXED); + assertThat(builtinMediaDevice).isNotNull(); + assertThat(builtinMediaDevice.getDrawableResId()).isEqualTo(R.drawable.ic_media_tablet); + } + + @Test + public void getName_returnCorrectName_builtinMic() { + InputMediaDevice builtinMediaDevice = + InputMediaDevice.create( + mContext, + String.valueOf(BUILTIN_MIC_ID), + AudioDeviceInfo.TYPE_BUILTIN_MIC, + MAX_VOLUME, + CURRENT_VOLUME, + IS_VOLUME_FIXED); + assertThat(builtinMediaDevice).isNotNull(); + assertThat(builtinMediaDevice.getName()) + .isEqualTo(mContext.getString(R.string.media_transfer_internal_mic)); + } + + @Test + public void getName_returnCorrectName_wiredHeadset() { + InputMediaDevice wiredMediaDevice = + InputMediaDevice.create( + mContext, + String.valueOf(WIRED_HEADSET_ID), + AudioDeviceInfo.TYPE_WIRED_HEADSET, + MAX_VOLUME, + CURRENT_VOLUME, + IS_VOLUME_FIXED); + assertThat(wiredMediaDevice).isNotNull(); + assertThat(wiredMediaDevice.getName()) + .isEqualTo(mContext.getString(R.string.media_transfer_wired_device_mic_name)); + } + + @Test + public void getName_returnCorrectName_usbHeadset() { + InputMediaDevice usbMediaDevice = + InputMediaDevice.create( + mContext, + String.valueOf(USB_HEADSET_ID), + AudioDeviceInfo.TYPE_USB_HEADSET, + MAX_VOLUME, + CURRENT_VOLUME, + IS_VOLUME_FIXED); + assertThat(usbMediaDevice).isNotNull(); + assertThat(usbMediaDevice.getName()) + .isEqualTo(mContext.getString(R.string.media_transfer_usb_device_mic_name)); + } +} 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 new file mode 100644 index 000000000000..2501ae6769b6 --- /dev/null +++ b/packages/SettingsLib/tests/robotests/src/com/android/settingslib/media/InputRouteManagerTest.java @@ -0,0 +1,140 @@ +/* + * Copyright 2024 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.android.settingslib.media; + +import static com.google.common.truth.Truth.assertThat; + +import static org.mockito.Mockito.mock; +import static org.mockito.Mockito.spy; +import static org.mockito.Mockito.when; + +import android.content.Context; +import android.media.AudioDeviceInfo; +import android.media.AudioManager; + +import com.android.settingslib.testutils.shadow.ShadowRouter2Manager; + +import org.junit.Before; +import org.junit.Test; +import org.junit.runner.RunWith; +import org.mockito.MockitoAnnotations; +import org.robolectric.RobolectricTestRunner; +import org.robolectric.RuntimeEnvironment; +import org.robolectric.annotation.Config; + +@RunWith(RobolectricTestRunner.class) +@Config(shadows = {ShadowRouter2Manager.class}) +public class InputRouteManagerTest { + private static final int BUILTIN_MIC_ID = 1; + private static final int INPUT_WIRED_HEADSET_ID = 2; + 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 final Context mContext = spy(RuntimeEnvironment.application); + private InputRouteManager mInputRouteManager; + + @Before + public void setUp() { + MockitoAnnotations.initMocks(this); + + final AudioManager audioManager = mock(AudioManager.class); + mInputRouteManager = new InputRouteManager(mContext, audioManager); + } + + @Test + public void onAudioDevicesAdded_shouldUpdateInputMediaDevice() { + final AudioDeviceInfo info1 = mock(AudioDeviceInfo.class); + when(info1.getType()).thenReturn(AudioDeviceInfo.TYPE_BUILTIN_MIC); + when(info1.getId()).thenReturn(BUILTIN_MIC_ID); + + final AudioDeviceInfo info2 = mock(AudioDeviceInfo.class); + when(info2.getType()).thenReturn(AudioDeviceInfo.TYPE_WIRED_HEADSET); + when(info2.getId()).thenReturn(INPUT_WIRED_HEADSET_ID); + + final AudioDeviceInfo info3 = mock(AudioDeviceInfo.class); + when(info3.getType()).thenReturn(AudioDeviceInfo.TYPE_USB_DEVICE); + when(info3.getId()).thenReturn(INPUT_USB_DEVICE_ID); + + final AudioDeviceInfo info4 = mock(AudioDeviceInfo.class); + when(info4.getType()).thenReturn(AudioDeviceInfo.TYPE_USB_HEADSET); + when(info4.getId()).thenReturn(INPUT_USB_HEADSET_ID); + + final AudioDeviceInfo info5 = mock(AudioDeviceInfo.class); + when(info5.getType()).thenReturn(AudioDeviceInfo.TYPE_USB_ACCESSORY); + when(info5.getId()).thenReturn(INPUT_USB_ACCESSORY_ID); + + final AudioDeviceInfo unsupportedInfo = mock(AudioDeviceInfo.class); + when(unsupportedInfo.getType()).thenReturn(AudioDeviceInfo.TYPE_HDMI); + + final AudioManager audioManager = mock(AudioManager.class); + AudioDeviceInfo[] devices = {info1, info2, info3, info4, info5, unsupportedInfo}; + when(audioManager.getDevices(AudioManager.GET_DEVICES_INPUTS)).thenReturn(devices); + + InputRouteManager inputRouteManager = new InputRouteManager(mContext, audioManager); + + assertThat(inputRouteManager.mInputMediaDevices).isEmpty(); + + inputRouteManager.mAudioDeviceCallback.onAudioDevicesAdded(devices); + + // The unsupported info should be filtered out. + assertThat(inputRouteManager.mInputMediaDevices).hasSize(devices.length - 1); + assertThat(inputRouteManager.mInputMediaDevices.get(0).getId()) + .isEqualTo(String.valueOf(BUILTIN_MIC_ID)); + assertThat(inputRouteManager.mInputMediaDevices.get(1).getId()) + .isEqualTo(String.valueOf(INPUT_WIRED_HEADSET_ID)); + assertThat(inputRouteManager.mInputMediaDevices.get(2).getId()) + .isEqualTo(String.valueOf(INPUT_USB_DEVICE_ID)); + assertThat(inputRouteManager.mInputMediaDevices.get(3).getId()) + .isEqualTo(String.valueOf(INPUT_USB_HEADSET_ID)); + assertThat(inputRouteManager.mInputMediaDevices.get(4).getId()) + .isEqualTo(String.valueOf(INPUT_USB_ACCESSORY_ID)); + } + + @Test + public void onAudioDevicesRemoved_shouldUpdateInputMediaDevice() { + final AudioManager audioManager = mock(AudioManager.class); + when(audioManager.getDevices(AudioManager.GET_DEVICES_INPUTS)) + .thenReturn(new AudioDeviceInfo[] {}); + + InputRouteManager inputRouteManager = new InputRouteManager(mContext, audioManager); + + final MediaDevice device = mock(MediaDevice.class); + inputRouteManager.mInputMediaDevices.add(device); + + final AudioDeviceInfo info = mock(AudioDeviceInfo.class); + when(info.getType()).thenReturn(AudioDeviceInfo.TYPE_WIRED_HEADSET); + inputRouteManager.mAudioDeviceCallback.onAudioDevicesRemoved(new AudioDeviceInfo[] {info}); + + assertThat(inputRouteManager.mInputMediaDevices).isEmpty(); + } + + @Test + public void getMaxInputGain_returnMaxInputGain() { + assertThat(mInputRouteManager.getMaxInputGain()).isEqualTo(15); + } + + @Test + public void getCurrentInputGain_returnCurrentInputGain() { + assertThat(mInputRouteManager.getCurrentInputGain()).isEqualTo(8); + } + + @Test + public void isInputGainFixed() { + assertThat(mInputRouteManager.isInputGainFixed()).isTrue(); + } +} |