• Home
  • Line#
  • Scopes#
  • Navigate#
  • Raw
  • Download
1 /*
2  * Copyright 2024 The Android Open Source Project
3  *
4  * Licensed under the Apache License, Version 2.0 (the "License");
5  * you may not use this file except in compliance with the License.
6  * You may obtain a copy of the License at
7  *
8  *      http://www.apache.org/licenses/LICENSE-2.0
9  *
10  * Unless required by applicable law or agreed to in writing, software
11  * distributed under the License is distributed on an "AS IS" BASIS,
12  * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13  * See the License for the specific language governing permissions and
14  * limitations under the License.
15  */
16 package com.android.settingslib.media;
17 
18 import static com.android.settingslib.media.LocalMediaManager.MediaDeviceState.STATE_SELECTED;
19 
20 import android.content.Context;
21 import android.media.AudioAttributes;
22 import android.media.AudioDeviceAttributes;
23 import android.media.AudioDeviceCallback;
24 import android.media.AudioDeviceInfo;
25 import android.media.AudioDeviceInfo.AudioDeviceType;
26 import android.media.AudioManager;
27 import android.media.MediaRecorder;
28 import android.os.Handler;
29 import android.os.HandlerExecutor;
30 import android.util.Slog;
31 
32 import androidx.annotation.NonNull;
33 import androidx.annotation.Nullable;
34 
35 import com.android.internal.annotations.VisibleForTesting;
36 
37 import java.util.ArrayList;
38 import java.util.Collection;
39 import java.util.List;
40 import java.util.concurrent.CopyOnWriteArrayList;
41 
42 /** Provides functionalities to get/observe input routes, control input routing and volume gain. */
43 public final class InputRouteManager {
44 
45     private static final String TAG = "InputRouteManager";
46 
47     @VisibleForTesting
48     static final AudioAttributes INPUT_ATTRIBUTES =
49             new AudioAttributes.Builder().setCapturePreset(MediaRecorder.AudioSource.MIC).build();
50 
51     @VisibleForTesting
52     static final int[] PRESETS = {
53         MediaRecorder.AudioSource.MIC,
54         MediaRecorder.AudioSource.CAMCORDER,
55         MediaRecorder.AudioSource.VOICE_RECOGNITION,
56         MediaRecorder.AudioSource.VOICE_COMMUNICATION,
57         MediaRecorder.AudioSource.UNPROCESSED,
58         MediaRecorder.AudioSource.VOICE_PERFORMANCE
59     };
60 
61     private final Context mContext;
62 
63     private final AudioManager mAudioManager;
64 
65     @VisibleForTesting final List<MediaDevice> mInputMediaDevices = new CopyOnWriteArrayList<>();
66 
67     private @AudioDeviceType int mSelectedInputDeviceType;
68 
69     private final Collection<InputDeviceCallback> mCallbacks = new CopyOnWriteArrayList<>();
70     private final Object mCallbackLock = new Object();
71 
72     @VisibleForTesting
73     final AudioDeviceCallback mAudioDeviceCallback =
74             new AudioDeviceCallback() {
75                 @Override
76                 public void onAudioDevicesAdded(@NonNull AudioDeviceInfo[] addedDevices) {
77                     applyDefaultSelectedTypeToAllPresets();
78 
79                     // Activate the last hot plugged valid input device, to match the output device
80                     // behavior.
81                     @AudioDeviceType int deviceTypeToActivate = mSelectedInputDeviceType;
82                     for (AudioDeviceInfo info : addedDevices) {
83                         @AudioDeviceType int type = info.getType();
84                         // Since onAudioDevicesAdded is called not only when new device is hot
85                         // plugged, but also when the switcher dialog is opened, make sure to check
86                         // against existing device list and only activate if the device does not
87                         // exist previously.
88                         if (InputMediaDevice.isSupportedInputDevice(type)
89                                 && findDeviceByType(type) == null) {
90                             deviceTypeToActivate = type;
91                         }
92                     }
93 
94                     // Only activate if we find a different valid input device. e.g. if none of the
95                     // addedDevices is supported input device, we don't need to activate anything.
96                     if (mSelectedInputDeviceType != deviceTypeToActivate) {
97                         mSelectedInputDeviceType = deviceTypeToActivate;
98                         AudioDeviceAttributes deviceAttributes =
99                                 createInputDeviceAttributes(mSelectedInputDeviceType);
100                         setPreferredDeviceForAllPresets(deviceAttributes);
101                     }
102                 }
103 
104                 @Override
105                 public void onAudioDevicesRemoved(@NonNull AudioDeviceInfo[] removedDevices) {
106                     applyDefaultSelectedTypeToAllPresets();
107                 }
108             };
109 
InputRouteManager(@onNull Context context, @NonNull AudioManager audioManager)110     public InputRouteManager(@NonNull Context context, @NonNull AudioManager audioManager) {
111         mContext = context;
112         mAudioManager = audioManager;
113         Handler handler = new Handler(context.getMainLooper());
114 
115         mAudioManager.registerAudioDeviceCallback(mAudioDeviceCallback, handler);
116 
117         mAudioManager.addOnPreferredDevicesForCapturePresetChangedListener(
118                 new HandlerExecutor(handler),
119                 this::onPreferredDevicesForCapturePresetChangedListener);
120 
121         applyDefaultSelectedTypeToAllPresets();
122     }
123 
124     @VisibleForTesting
onPreferredDevicesForCapturePresetChangedListener( @ediaRecorder.SystemSource int capturePreset, @NonNull List<AudioDeviceAttributes> devices)125     void onPreferredDevicesForCapturePresetChangedListener(
126             @MediaRecorder.SystemSource int capturePreset,
127             @NonNull List<AudioDeviceAttributes> devices) {
128         if (capturePreset == MediaRecorder.AudioSource.MIC) {
129             dispatchInputDeviceListUpdate();
130         }
131     }
132 
registerCallback(@onNull InputDeviceCallback callback)133     public void registerCallback(@NonNull InputDeviceCallback callback) {
134         synchronized (mCallbackLock) {
135             if (!mCallbacks.contains(callback)) {
136                 mCallbacks.add(callback);
137                 dispatchInputDeviceListUpdate();
138             }
139         }
140     }
141 
unregisterCallback(@onNull InputDeviceCallback callback)142     public void unregisterCallback(@NonNull InputDeviceCallback callback) {
143         synchronized (mCallbackLock) {
144             mCallbacks.remove(callback);
145         }
146     }
147 
148     // TODO(b/355684672): handle edge case where there are two devices with the same type. Only
149     // using a single type might not be enough to recognize the correct device.
150     @Nullable
findDeviceByType(@udioDeviceType int type)151     private MediaDevice findDeviceByType(@AudioDeviceType int type) {
152         for (MediaDevice device : mInputMediaDevices) {
153             if (((InputMediaDevice) device).getAudioDeviceInfoType() == type) {
154                 return device;
155             }
156         }
157         return null;
158     }
159 
160     @Nullable
getSelectedInputDevice()161     public MediaDevice getSelectedInputDevice() {
162         return findDeviceByType(mSelectedInputDeviceType);
163     }
164 
applyDefaultSelectedTypeToAllPresets()165     private void applyDefaultSelectedTypeToAllPresets() {
166         mSelectedInputDeviceType = retrieveDefaultSelectedDeviceType();
167         AudioDeviceAttributes deviceAttributes =
168                 createInputDeviceAttributes(mSelectedInputDeviceType);
169         setPreferredDeviceForAllPresets(deviceAttributes);
170     }
171 
createInputDeviceAttributes(@udioDeviceType int type)172     private AudioDeviceAttributes createInputDeviceAttributes(@AudioDeviceType int type) {
173         // Address is not used.
174         return new AudioDeviceAttributes(AudioDeviceAttributes.ROLE_INPUT, type, /* address= */ "");
175     }
176 
retrieveDefaultSelectedDeviceType()177     private @AudioDeviceType int retrieveDefaultSelectedDeviceType() {
178         List<AudioDeviceAttributes> attributesOfSelectedInputDevices =
179                 mAudioManager.getDevicesForAttributes(INPUT_ATTRIBUTES);
180         int selectedInputDeviceAttributesType;
181         if (attributesOfSelectedInputDevices.isEmpty()) {
182             Slog.e(TAG, "Unexpected empty list of input devices. Using built-in mic.");
183             selectedInputDeviceAttributesType = AudioDeviceInfo.TYPE_BUILTIN_MIC;
184         } else {
185             if (attributesOfSelectedInputDevices.size() > 1) {
186                 Slog.w(
187                         TAG,
188                         "AudioManager.getDevicesForAttributes returned more than one element."
189                                 + " Using the first one.");
190             }
191             selectedInputDeviceAttributesType = attributesOfSelectedInputDevices.get(0).getType();
192         }
193         return selectedInputDeviceAttributesType;
194     }
195 
dispatchInputDeviceListUpdate()196     private void dispatchInputDeviceListUpdate() {
197         // Get all input devices.
198         AudioDeviceInfo[] audioDeviceInfos =
199                 mAudioManager.getDevices(AudioManager.GET_DEVICES_INPUTS);
200         mInputMediaDevices.clear();
201         for (AudioDeviceInfo info : audioDeviceInfos) {
202             MediaDevice mediaDevice =
203                     InputMediaDevice.create(
204                             mContext,
205                             String.valueOf(info.getId()),
206                             info.getType(),
207                             getMaxInputGain(),
208                             getCurrentInputGain(),
209                             isInputGainFixed(),
210                             getProductNameFromAudioDeviceInfo(info));
211             if (mediaDevice != null) {
212                 if (info.getType() == mSelectedInputDeviceType) {
213                     mediaDevice.setState(STATE_SELECTED);
214                 }
215                 mInputMediaDevices.add(mediaDevice);
216             }
217         }
218 
219         final List<MediaDevice> inputMediaDevices = new ArrayList<>(mInputMediaDevices);
220         synchronized (mCallbackLock) {
221             for (InputDeviceCallback callback : mCallbacks) {
222                 callback.onInputDeviceListUpdated(inputMediaDevices);
223             }
224         }
225     }
226 
227     /**
228      * Gets the product name for the given {@link AudioDeviceInfo}.
229      *
230      * @return The product name for the given {@link AudioDeviceInfo}, or null if a suitable name
231      *     cannot be found.
232      */
233     @Nullable
getProductNameFromAudioDeviceInfo(AudioDeviceInfo deviceInfo)234     private String getProductNameFromAudioDeviceInfo(AudioDeviceInfo deviceInfo) {
235         CharSequence productName = deviceInfo.getProductName();
236         if (productName == null) {
237             return null;
238         }
239         String productNameString = productName.toString();
240         if (productNameString.isBlank()) {
241             return null;
242         }
243         return productNameString;
244     }
245 
selectDevice(@onNull MediaDevice device)246     public void selectDevice(@NonNull MediaDevice device) {
247         if (!(device instanceof InputMediaDevice inputMediaDevice)) {
248             Slog.w(TAG, "This device is not an InputMediaDevice: " + device.getName());
249             return;
250         }
251 
252         if (inputMediaDevice.getAudioDeviceInfoType() == mSelectedInputDeviceType) {
253             Slog.w(TAG, "This device is already selected: " + device.getName());
254             return;
255         }
256 
257         // Handle edge case where the targeting device is not available, e.g. disconnected.
258         if (!mInputMediaDevices.contains(device)) {
259             Slog.w(TAG, "This device is not available: " + device.getName());
260             return;
261         }
262 
263         // Update mSelectedInputDeviceType directly based on user action.
264         mSelectedInputDeviceType = inputMediaDevice.getAudioDeviceInfoType();
265 
266         AudioDeviceAttributes deviceAttributes =
267                 createInputDeviceAttributes(inputMediaDevice.getAudioDeviceInfoType());
268         try {
269             setPreferredDeviceForAllPresets(deviceAttributes);
270         } catch (IllegalArgumentException e) {
271             Slog.e(
272                     TAG,
273                     "Illegal argument exception while setPreferredDeviceForAllPreset: "
274                             + device.getName(),
275                     e);
276         }
277     }
278 
setPreferredDeviceForAllPresets(@onNull AudioDeviceAttributes deviceAttributes)279     private void setPreferredDeviceForAllPresets(@NonNull AudioDeviceAttributes deviceAttributes) {
280         // The input routing via system setting takes effect on all capture presets.
281         for (@MediaRecorder.Source int preset : PRESETS) {
282             mAudioManager.setPreferredDeviceForCapturePreset(preset, deviceAttributes);
283         }
284     }
285 
getMaxInputGain()286     public int getMaxInputGain() {
287         // TODO (b/357123335): use real input gain implementation.
288         // Using 100 for now since it matches the maximum input gain index in classic ChromeOS.
289         return 100;
290     }
291 
getCurrentInputGain()292     public int getCurrentInputGain() {
293         // TODO (b/357123335): use real input gain implementation.
294         // Show a fixed full gain in UI before it really works per UX requirement.
295         return 100;
296     }
297 
isInputGainFixed()298     public boolean isInputGainFixed() {
299         // TODO (b/357123335): use real input gain implementation.
300         return true;
301     }
302 
303     /** Callback for listening to input device changes. */
304     public interface InputDeviceCallback {
onInputDeviceListUpdated(@onNull List<MediaDevice> devices)305         void onInputDeviceListUpdated(@NonNull List<MediaDevice> devices);
306     }
307 }
308