Add InputRouteManager and InputMediaDevice to support input routing

InputRouteManager interacts with AudioManager to get/observe
available input routes.

Change-Id: I5503aa3f7a51420c0af87ca225c03818e8451056
Bug: b/355684672, b/357122624
Test: atest MediaOutputControllerTest, LocalMediaManagerTest
      atest InputRouteManagerTest, InputMediaDeviceTest
Flag: com.android.media.flags.enable_audio_input_device_routing_and_volume_control
diff --git a/packages/SettingsLib/res/values/strings.xml b/packages/SettingsLib/res/values/strings.xml
index 4d771c0..feee89a 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 0000000..766cd43
--- /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 0000000..548eb3f
--- /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 0000000..bc1ea6c
--- /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 0000000..2501ae6
--- /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();
+    }
+}