• Home
  • Line#
  • Scopes#
  • Navigate#
  • Raw
  • Download
1 /*
2  * Copyright (C) 2021 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 
17 package android.car.builtin.media;
18 
19 import static android.media.AudioAttributes.USAGE_VIRTUAL_SOURCE;
20 import static android.media.AudioManager.EXTRA_VOLUME_STREAM_TYPE;
21 import static android.media.AudioManager.GET_DEVICES_INPUTS;
22 import static android.media.AudioManager.GET_DEVICES_OUTPUTS;
23 import static android.media.AudioManager.MASTER_MUTE_CHANGED_ACTION;
24 import static android.media.AudioManager.VOLUME_CHANGED_ACTION;
25 
26 import android.annotation.NonNull;
27 import android.annotation.SystemApi;
28 import android.car.builtin.annotation.AddedIn;
29 import android.car.builtin.annotation.PlatformVersion;
30 import android.car.builtin.util.Slogf;
31 import android.content.BroadcastReceiver;
32 import android.content.Context;
33 import android.content.Intent;
34 import android.content.IntentFilter;
35 import android.media.AudioAttributes;
36 import android.media.AudioAttributes.AttributeUsage;
37 import android.media.AudioDeviceInfo;
38 import android.media.AudioDevicePort;
39 import android.media.AudioFormat;
40 import android.media.AudioGain;
41 import android.media.AudioGainConfig;
42 import android.media.AudioManager;
43 import android.media.AudioPatch;
44 import android.media.AudioPortConfig;
45 import android.media.AudioSystem;
46 
47 import com.android.internal.util.Preconditions;
48 
49 import java.util.ArrayList;
50 import java.util.Objects;
51 
52 /**
53  * Helper for Audio related operations.
54  *
55  * @hide
56  */
57 @SystemApi(client = SystemApi.Client.MODULE_LIBRARIES)
58 public final class AudioManagerHelper {
59 
60     @AddedIn(PlatformVersion.TIRAMISU_0)
61     public static final int UNDEFINED_STREAM_TYPE = -1;
62     private static final String TAG = "AudioServiceHelper";
63 
AudioManagerHelper()64     private AudioManagerHelper() {
65         throw new UnsupportedOperationException();
66     }
67 
68     /**
69      * Set the audio device gain for device with {@code address}
70      * @param audioManager audio manager
71      * @param address Address for device to set gain
72      * @param gainInMillibels gain in millibels to set
73      * @param isOutput is the device an output device
74      * @return true if the gain was successfully set
75      */
76     @AddedIn(PlatformVersion.TIRAMISU_0)
setAudioDeviceGain(@onNull AudioManager audioManager, @NonNull String address, int gainInMillibels, boolean isOutput)77     public static boolean setAudioDeviceGain(@NonNull AudioManager audioManager,
78             @NonNull String address, int gainInMillibels, boolean isOutput) {
79         Preconditions.checkNotNull(audioManager,
80                 "Audio Manager can not be null in set device gain, device address %s", address);
81         AudioDeviceInfo deviceInfo = getAudioDeviceInfo(audioManager, address, isOutput);
82 
83         AudioGain audioGain = getAudioGain(deviceInfo.getPort());
84 
85         // size of gain values is 1 in MODE_JOINT
86         AudioGainConfig audioGainConfig = audioGain.buildConfig(
87                 AudioGain.MODE_JOINT,
88                 audioGain.channelMask(),
89                 new int[] { gainInMillibels },
90                 0);
91         if (audioGainConfig == null) {
92             throw new IllegalStateException("Failed to construct AudioGainConfig for device "
93                     + address);
94         }
95 
96         int r = AudioManager.setAudioPortGain(deviceInfo.getPort(), audioGainConfig);
97         if (r == AudioManager.SUCCESS) {
98             return true;
99         }
100         return false;
101     }
102 
getAudioDeviceInfo(@onNull AudioManager audioManager, @NonNull String address, boolean isOutput)103     private static AudioDeviceInfo getAudioDeviceInfo(@NonNull AudioManager audioManager,
104             @NonNull String address, boolean isOutput) {
105         Objects.requireNonNull(address, "Device address can not be null");
106         Preconditions.checkStringNotEmpty(address, "Device Address can not be empty");
107 
108         AudioDeviceInfo[] devices =
109                 audioManager.getDevices(isOutput ? GET_DEVICES_OUTPUTS : GET_DEVICES_INPUTS);
110 
111         for (int index = 0; index < devices.length; index++) {
112             AudioDeviceInfo device = devices[index];
113             if (address.equals(device.getAddress())) {
114                 return device;
115             }
116         }
117 
118         throw new IllegalStateException((isOutput ? "Output" : "Input")
119                 + " Audio device info not found for device address " + address);
120     }
121 
getAudioGain(@onNull AudioDevicePort deviceport)122     private static AudioGain getAudioGain(@NonNull AudioDevicePort deviceport) {
123         Objects.requireNonNull(deviceport, "Audio device port can not be null");
124         Preconditions.checkArgument(deviceport.gains().length > 0,
125                 "Audio device must have gains defined");
126         for (int index = 0; index < deviceport.gains().length; index++) {
127             AudioGain gain = deviceport.gains()[index];
128             if ((gain.mode() & AudioGain.MODE_JOINT) != 0) {
129                 return checkAudioGainConfiguration(gain);
130             }
131         }
132         throw new IllegalStateException("Audio device does not have a valid audio gain");
133     }
134 
checkAudioGainConfiguration(@onNull AudioGain audioGain)135     private static AudioGain checkAudioGainConfiguration(@NonNull AudioGain audioGain) {
136         Preconditions.checkArgument(audioGain.maxValue() >= audioGain.minValue(),
137                 "Max gain %d is lower than min gain %d",
138                 audioGain.maxValue(), audioGain.minValue());
139         Preconditions.checkArgument((audioGain.defaultValue() >= audioGain.minValue())
140                 && (audioGain.defaultValue() <= audioGain.maxValue()),
141                 "Default gain %d not in range (%d,%d)", audioGain.defaultValue(),
142                 audioGain.minValue(), audioGain.maxValue());
143         Preconditions.checkArgument(
144                 ((audioGain.maxValue() - audioGain.minValue()) % audioGain.stepValue()) == 0,
145                 "Gain step value %d greater than min gain to max gain range %d",
146                 audioGain.stepValue(), audioGain.maxValue() - audioGain.minValue());
147         Preconditions.checkArgument(
148                 ((audioGain.defaultValue() - audioGain.minValue()) % audioGain.stepValue()) == 0,
149                 "Gain step value %d greater than min gain to default gain range %d",
150                 audioGain.stepValue(), audioGain.defaultValue() - audioGain.minValue());
151         return audioGain;
152     }
153 
154     /**
155      * Returns the audio gain information for the specified device.
156      * @param deviceInfo
157      * @return
158      */
159     @AddedIn(PlatformVersion.TIRAMISU_0)
getAudioGainInfo(@onNull AudioDeviceInfo deviceInfo)160     public static AudioGainInfo getAudioGainInfo(@NonNull AudioDeviceInfo deviceInfo) {
161         Objects.requireNonNull(deviceInfo);
162         return new AudioGainInfo(getAudioGain(deviceInfo.getPort()));
163     }
164 
165     /**
166      * Creates an audio patch from source and sink source
167      * @param sourceDevice Source device for the patch
168      * @param sinkDevice Sink device of the patch
169      * @param gainInMillibels gain to apply to the source device
170      * @return The audio patch information that was created
171      */
172     @AddedIn(PlatformVersion.TIRAMISU_0)
createAudioPatch(@onNull AudioDeviceInfo sourceDevice, @NonNull AudioDeviceInfo sinkDevice, int gainInMillibels)173     public static AudioPatchInfo createAudioPatch(@NonNull AudioDeviceInfo sourceDevice,
174             @NonNull AudioDeviceInfo sinkDevice, int gainInMillibels) {
175         Preconditions.checkNotNull(sourceDevice,
176                 "Source device can not be null, sink info %s", sinkDevice);
177         Preconditions.checkNotNull(sinkDevice,
178                 "Sink device can not be null, source info %s", sourceDevice);
179 
180         AudioDevicePort sinkPort = Preconditions.checkNotNull(sinkDevice.getPort(),
181                 "Sink device [%s] does not contain an audio port", sinkDevice);
182 
183         // {@link android.media.AudioPort#activeConfig()} is valid for mixer port only,
184         // since audio framework has no clue what's active on the device ports.
185         // Therefore we construct an empty / default configuration here, which the audio HAL
186         // implementation should ignore.
187         AudioPortConfig sinkConfig = sinkPort.buildConfig(0,
188                 AudioFormat.CHANNEL_OUT_DEFAULT, AudioFormat.ENCODING_DEFAULT, null);
189         Slogf.d(TAG, "createAudioPatch sinkConfig: " + sinkConfig);
190 
191         // Configure the source port to match the output port except for a gain adjustment
192         AudioGain audioGain = Objects.requireNonNull(getAudioGain(sourceDevice.getPort()),
193                 "Gain controller not available for source port");
194 
195         // size of gain values is 1 in MODE_JOINT
196         AudioGainConfig audioGainConfig = audioGain.buildConfig(AudioGain.MODE_JOINT,
197                 audioGain.channelMask(), new int[] { gainInMillibels }, 0);
198         // Construct an empty / default configuration excepts gain config here and it's up to the
199         // audio HAL how to interpret this configuration, which the audio HAL
200         // implementation should ignore.
201         AudioPortConfig sourceConfig = sourceDevice.getPort().buildConfig(0,
202                 AudioFormat.CHANNEL_IN_DEFAULT, AudioFormat.ENCODING_DEFAULT, audioGainConfig);
203 
204         // Create an audioPatch to connect the two ports
205         AudioPatch[] patch = new AudioPatch[] { null };
206         int result = AudioManager.createAudioPatch(patch,
207                 new AudioPortConfig[] { sourceConfig },
208                 new AudioPortConfig[] { sinkConfig });
209         if (result != AudioManager.SUCCESS) {
210             throw new RuntimeException("createAudioPatch failed with code " + result);
211         }
212 
213         Preconditions.checkNotNull(patch[0],
214                 "createAudioPatch didn't provide expected single handle [source: %s,sink: %s]",
215                 sinkDevice, sourceDevice);
216         Slogf.d(TAG, "Audio patch created: " + patch[0]);
217 
218         return createAudioPatchInfo(patch[0]);
219     }
220 
createAudioPatchInfo(AudioPatch patch)221     private static AudioPatchInfo createAudioPatchInfo(AudioPatch patch) {
222         Preconditions.checkArgument(patch.sources().length == 1
223                         && patch.sources()[0].port() instanceof AudioDevicePort,
224                 "Accepts exactly one device port as source");
225         Preconditions.checkArgument(patch.sinks().length == 1
226                         && patch.sinks()[0].port() instanceof AudioDevicePort,
227                 "Accepts exactly one device port as sink");
228 
229         return new AudioPatchInfo(((AudioDevicePort) patch.sources()[0].port()).address(),
230                 ((AudioDevicePort) patch.sinks()[0].port()).address(),
231                 patch.id());
232     }
233 
234     /**
235      * Releases audio patch handle
236      * @param audioManager manager to call for releasing of handle
237      * @param info patch information to release
238      * @return returns true if the patch was successfully removed
239      */
240     @AddedIn(PlatformVersion.TIRAMISU_0)
releaseAudioPatch(@onNull AudioManager audioManager, @NonNull AudioPatchInfo info)241     public static boolean releaseAudioPatch(@NonNull AudioManager audioManager,
242             @NonNull AudioPatchInfo info) {
243         Preconditions.checkNotNull(audioManager,
244                 "Audio Manager can not be null in release audio patch for %s", info);
245         Preconditions.checkNotNull(info,
246                 "Audio Patch Info can not be null in release audio patch for %s", info);
247         // NOTE:  AudioPolicyService::removeNotificationClient will take care of this automatically
248         //        if the client that created a patch quits.
249         ArrayList<AudioPatch> patches = new ArrayList<>();
250         int result = audioManager.listAudioPatches(patches);
251         if (result != AudioManager.SUCCESS) {
252             throw new RuntimeException("listAudioPatches failed with code " + result);
253         }
254 
255         // Look for a patch that matches the provided user side handle
256         for (AudioPatch patch : patches) {
257             if (info.represents(patch)) {
258                 // Found it!
259                 result = AudioManager.releaseAudioPatch(patch);
260                 if (result != AudioManager.SUCCESS) {
261                     throw new RuntimeException("releaseAudioPatch failed with code " + result);
262                 }
263                 return true;
264             }
265         }
266         return false;
267     }
268 
269     /**
270      * Returns the string representation of {@link android.media.AudioAttributes.AttributeUsage}.
271      *
272      * <p>See {@link android.media.AudioAttributes.usageToString}.
273      */
274     @AddedIn(PlatformVersion.TIRAMISU_0)
usageToString(@ttributeUsage int usage)275     public static String usageToString(@AttributeUsage int usage) {
276         return AudioAttributes.usageToString(usage);
277     }
278 
279     /**
280      * Returns the xsd string representation of
281      * {@link android.media.AudioAttributes.AttributeUsage}.
282      *
283      * <p>See {@link android.media.AudioAttributes.usageToXsdString}.
284      */
285     @AddedIn(PlatformVersion.TIRAMISU_0)
usageToXsdString(@ttributeUsage int usage)286     public static String usageToXsdString(@AttributeUsage int usage) {
287         return AudioAttributes.usageToXsdString(usage);
288     }
289 
290     /**
291      * Returns {@link android.media.AudioAttributes.AttributeUsage} representation of
292      * xsd usage string.
293      *
294      * <p>See {@link android.media.AudioAttributes.xsdStringToUsage}.
295      */
296     @AddedIn(PlatformVersion.TIRAMISU_0)
xsdStringToUsage(String usage)297     public static int xsdStringToUsage(String usage) {
298         return AudioAttributes.xsdStringToUsage(usage);
299     }
300 
301     /**
302      * Returns {@link android.media.AudioAttributes.AttributeUsage} for
303      * {@link android.media.AudioAttributes.AttributeUsage.USAGE_VIRTUAL_SOURCE}.
304      */
305     @AddedIn(PlatformVersion.TIRAMISU_0)
getUsageVirtualSource()306     public static int getUsageVirtualSource() {
307         return USAGE_VIRTUAL_SOURCE;
308     }
309 
310     /**
311      * Returns the string representation of volume adjustment.
312      *
313      * <p>See {@link android.media.AudioManager#adjustToString(int)}
314      */
315     @AddedIn(PlatformVersion.TIRAMISU_0)
adjustToString(int adjustment)316     public static String adjustToString(int adjustment) {
317         return AudioManager.adjustToString(adjustment);
318     }
319 
320     /**
321      * Sets the system master mute state.
322      *
323      * <p>See {@link android.media.AudioManager#setMasterMute(boolean, int)}.
324      */
325     @AddedIn(PlatformVersion.TIRAMISU_0)
setMasterMute(@onNull AudioManager audioManager, boolean mute, int flags)326     public static void setMasterMute(@NonNull AudioManager audioManager, boolean mute, int flags) {
327         Objects.requireNonNull(audioManager, "AudioManager must not be null.");
328         audioManager.setMasterMute(mute, flags);
329     }
330 
331     /**
332      * Gets system master mute state.
333      *
334      * <p>See {@link android.media.AudioManager#isMasterMute()}.
335      */
336     @AddedIn(PlatformVersion.TIRAMISU_0)
isMasterMute(@onNull AudioManager audioManager)337     public static boolean isMasterMute(@NonNull AudioManager audioManager) {
338         Objects.requireNonNull(audioManager, "AudioManager must not be null.");
339         return audioManager.isMasterMute();
340     }
341 
342     /**
343      * Registers volume and mute receiver
344      */
345     @AddedIn(PlatformVersion.TIRAMISU_0)
registerVolumeAndMuteReceiver(Context context, VolumeAndMuteReceiver audioAndMuteHelper)346     public static void registerVolumeAndMuteReceiver(Context context,
347             VolumeAndMuteReceiver audioAndMuteHelper) {
348         Objects.requireNonNull(context, "Context can not be null.");
349         Objects.requireNonNull(audioAndMuteHelper, "Audio and Mute helper can not be null.");
350 
351         IntentFilter intentFilter = new IntentFilter();
352         intentFilter.addAction(VOLUME_CHANGED_ACTION);
353         intentFilter.addAction(MASTER_MUTE_CHANGED_ACTION);
354         context.registerReceiver(audioAndMuteHelper.getReceiver(), intentFilter,
355                 Context.RECEIVER_NOT_EXPORTED);
356     }
357 
358     /**
359      * Unregisters volume and mute receiver
360      */
361     @AddedIn(PlatformVersion.TIRAMISU_0)
unregisterVolumeAndMuteReceiver(Context context, VolumeAndMuteReceiver audioAndMuteHelper)362     public static void unregisterVolumeAndMuteReceiver(Context context,
363             VolumeAndMuteReceiver audioAndMuteHelper) {
364         Objects.requireNonNull(context, "Context can not be null.");
365         Objects.requireNonNull(audioAndMuteHelper, "Audio and Mute helper can not be null.");
366 
367         context.unregisterReceiver(audioAndMuteHelper.getReceiver());
368     }
369 
370     /**
371      * Checks if the client id is equal to the telephony's focus client id.
372      */
373     @AddedIn(PlatformVersion.TIRAMISU_0)
isCallFocusRequestClientId(String clientId)374     public static boolean isCallFocusRequestClientId(String clientId) {
375         return AudioSystem.IN_VOICE_COMM_FOCUS_ID.equals(clientId);
376     }
377 
378 
379     /**
380      * Audio gain information for a particular device:
381      * Contains Max, Min, Default gain and the step value between gain changes
382      */
383     public static class AudioGainInfo {
384 
385         private final int mMinGain;
386         private final int mMaxGain;
387         private final int mDefaultGain;
388         private final int mStepValue;
389 
AudioGainInfo(AudioGain gain)390         private AudioGainInfo(AudioGain gain) {
391             mMinGain = gain.minValue();
392             mMaxGain = gain.maxValue();
393             mDefaultGain = gain.defaultValue();
394             mStepValue = gain.stepValue();
395         }
396 
397         @AddedIn(PlatformVersion.TIRAMISU_0)
getMinGain()398         public int getMinGain() {
399             return mMinGain;
400         }
401 
402         @AddedIn(PlatformVersion.TIRAMISU_0)
getMaxGain()403         public int getMaxGain() {
404             return mMaxGain;
405         }
406 
407         @AddedIn(PlatformVersion.TIRAMISU_0)
getDefaultGain()408         public int getDefaultGain() {
409             return mDefaultGain;
410         }
411 
412         @AddedIn(PlatformVersion.TIRAMISU_0)
getStepValue()413         public int getStepValue() {
414             return mStepValue;
415         }
416     }
417 
418     /**
419      * Contains the audio patch information for the created audio patch:
420      * Patch handle id, source device address, sink device address
421      */
422     public static class AudioPatchInfo {
423         private final int mHandleId;
424 
425         private final String mSourceAddress;
426         private final String mSinkAddress;
427 
428 
AudioPatchInfo(@onNull String sourceAddress, @NonNull String sinkAddress, int handleId)429         public AudioPatchInfo(@NonNull String sourceAddress, @NonNull String sinkAddress,
430                 int handleId) {
431             mSourceAddress = Preconditions.checkNotNull(sourceAddress,
432                     "Source Address can not be null for patch id %d", handleId);
433             mSinkAddress = Preconditions.checkNotNull(sinkAddress,
434                     "Sink Address can not be null for patch id %d", handleId);
435             mHandleId = handleId;
436         }
437 
438         @AddedIn(PlatformVersion.TIRAMISU_0)
getHandleId()439         public int getHandleId() {
440             return mHandleId;
441         }
442 
443         @AddedIn(PlatformVersion.TIRAMISU_0)
getSourceAddress()444         public String getSourceAddress() {
445             return mSourceAddress;
446         }
447 
448         @AddedIn(PlatformVersion.TIRAMISU_0)
getSinkAddress()449         public String getSinkAddress() {
450             return mSinkAddress;
451         }
452 
453         @Override
454         @AddedIn(PlatformVersion.TIRAMISU_0)
toString()455         public String toString() {
456             StringBuilder builder = new StringBuilder();
457             builder.append("Source{ ");
458             builder.append(mSourceAddress);
459             builder.append("} Sink{ ");
460             builder.append(mSinkAddress);
461             builder.append("} Handle{ ");
462             builder.append(mHandleId);
463             builder.append("}");
464             return builder.toString();
465         }
466 
represents(AudioPatch patch)467         private boolean represents(AudioPatch patch) {
468             return patch.id() == mHandleId;
469         }
470     }
471 
472     /**
473      * Class to manage volume and mute changes from audio manager
474      */
475     public abstract static class VolumeAndMuteReceiver {
476 
477         private final BroadcastReceiver mReceiver = new BroadcastReceiver() {
478 
479             @Override
480             public void onReceive(Context context, Intent intent) {
481                 switch (intent.getAction()) {
482                     case VOLUME_CHANGED_ACTION:
483                         int streamType =
484                                 intent.getIntExtra(EXTRA_VOLUME_STREAM_TYPE, UNDEFINED_STREAM_TYPE);
485                         onVolumeChanged(streamType);
486                         break;
487                     case MASTER_MUTE_CHANGED_ACTION:
488                         onMuteChanged();
489                         break;
490                 }
491             }
492         };
493 
getReceiver()494         private BroadcastReceiver getReceiver() {
495             return mReceiver;
496         }
497 
498         /**
499          * Called on volume changes
500          * @param streamType type of stream for the volume change
501          */
502         @AddedIn(PlatformVersion.TIRAMISU_0)
onVolumeChanged(int streamType)503         public abstract void onVolumeChanged(int streamType);
504 
505         /**
506          * Called on mute changes
507          */
508         @AddedIn(PlatformVersion.TIRAMISU_0)
onMuteChanged()509         public abstract void onMuteChanged();
510     }
511 }
512