1/*
2 * Copyright (C) 2015 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 */
16package com.android.car;
17
18import android.annotation.NonNull;
19import android.annotation.Nullable;
20import android.car.Car;
21import android.car.media.CarAudioPatchHandle;
22import android.car.media.ICarAudio;
23import android.car.media.ICarVolumeCallback;
24import android.content.BroadcastReceiver;
25import android.content.Context;
26import android.content.Intent;
27import android.content.IntentFilter;
28import android.content.pm.PackageManager;
29import android.hardware.automotive.audiocontrol.V1_0.ContextNumber;
30import android.hardware.automotive.audiocontrol.V1_0.IAudioControl;
31import android.media.AudioAttributes;
32import android.media.AudioDeviceInfo;
33import android.media.AudioDevicePort;
34import android.media.AudioFormat;
35import android.media.AudioGain;
36import android.media.AudioGainConfig;
37import android.media.AudioManager;
38import android.media.AudioPatch;
39import android.media.AudioPlaybackConfiguration;
40import android.media.AudioPortConfig;
41import android.media.AudioSystem;
42import android.media.audiopolicy.AudioMix;
43import android.media.audiopolicy.AudioMixingRule;
44import android.media.audiopolicy.AudioPolicy;
45import android.os.IBinder;
46import android.os.Looper;
47import android.os.RemoteException;
48import android.telephony.TelephonyManager;
49import android.text.TextUtils;
50import android.util.Log;
51import android.util.SparseArray;
52import android.util.SparseIntArray;
53
54import com.android.internal.util.Preconditions;
55
56import java.io.PrintWriter;
57import java.util.ArrayList;
58import java.util.Arrays;
59import java.util.HashSet;
60import java.util.List;
61import java.util.NoSuchElementException;
62import java.util.Set;
63import java.util.stream.Collectors;
64
65public class CarAudioService extends ICarAudio.Stub implements CarServiceBase {
66
67    private static final int DEFAULT_AUDIO_USAGE = AudioAttributes.USAGE_MEDIA;
68
69    private static final int[] CONTEXT_NUMBERS = new int[] {
70            ContextNumber.MUSIC,
71            ContextNumber.NAVIGATION,
72            ContextNumber.VOICE_COMMAND,
73            ContextNumber.CALL_RING,
74            ContextNumber.CALL,
75            ContextNumber.ALARM,
76            ContextNumber.NOTIFICATION,
77            ContextNumber.SYSTEM_SOUND
78    };
79
80    private static final SparseIntArray USAGE_TO_CONTEXT = new SparseIntArray();
81
82    // For legacy stream type based volume control.
83    // Values in STREAM_TYPES and STREAM_TYPE_USAGES should be aligned.
84    private static final int[] STREAM_TYPES = new int[] {
85            AudioManager.STREAM_MUSIC,
86            AudioManager.STREAM_ALARM,
87            AudioManager.STREAM_RING
88    };
89    private static final int[] STREAM_TYPE_USAGES = new int[] {
90            AudioAttributes.USAGE_MEDIA,
91            AudioAttributes.USAGE_ALARM,
92            AudioAttributes.USAGE_NOTIFICATION_RINGTONE
93    };
94
95    static {
96        USAGE_TO_CONTEXT.put(AudioAttributes.USAGE_UNKNOWN, ContextNumber.MUSIC);
97        USAGE_TO_CONTEXT.put(AudioAttributes.USAGE_MEDIA, ContextNumber.MUSIC);
98        USAGE_TO_CONTEXT.put(AudioAttributes.USAGE_VOICE_COMMUNICATION, ContextNumber.CALL);
99        USAGE_TO_CONTEXT.put(AudioAttributes.USAGE_VOICE_COMMUNICATION_SIGNALLING,
100                ContextNumber.CALL);
101        USAGE_TO_CONTEXT.put(AudioAttributes.USAGE_ALARM, ContextNumber.ALARM);
102        USAGE_TO_CONTEXT.put(AudioAttributes.USAGE_NOTIFICATION, ContextNumber.NOTIFICATION);
103        USAGE_TO_CONTEXT.put(AudioAttributes.USAGE_NOTIFICATION_RINGTONE, ContextNumber.CALL_RING);
104        USAGE_TO_CONTEXT.put(AudioAttributes.USAGE_NOTIFICATION_COMMUNICATION_REQUEST,
105                ContextNumber.NOTIFICATION);
106        USAGE_TO_CONTEXT.put(AudioAttributes.USAGE_NOTIFICATION_COMMUNICATION_INSTANT,
107                ContextNumber.NOTIFICATION);
108        USAGE_TO_CONTEXT.put(AudioAttributes.USAGE_NOTIFICATION_COMMUNICATION_DELAYED,
109                ContextNumber.NOTIFICATION);
110        USAGE_TO_CONTEXT.put(AudioAttributes.USAGE_NOTIFICATION_EVENT, ContextNumber.NOTIFICATION);
111        USAGE_TO_CONTEXT.put(AudioAttributes.USAGE_ASSISTANCE_ACCESSIBILITY,
112                ContextNumber.VOICE_COMMAND);
113        USAGE_TO_CONTEXT.put(AudioAttributes.USAGE_ASSISTANCE_NAVIGATION_GUIDANCE,
114                ContextNumber.NAVIGATION);
115        USAGE_TO_CONTEXT.put(AudioAttributes.USAGE_ASSISTANCE_SONIFICATION,
116                ContextNumber.SYSTEM_SOUND);
117        USAGE_TO_CONTEXT.put(AudioAttributes.USAGE_GAME, ContextNumber.MUSIC);
118        USAGE_TO_CONTEXT.put(AudioAttributes.USAGE_VIRTUAL_SOURCE, ContextNumber.INVALID);
119        USAGE_TO_CONTEXT.put(AudioAttributes.USAGE_ASSISTANT, ContextNumber.VOICE_COMMAND);
120    }
121
122    private final Object mImplLock = new Object();
123
124    private final Context mContext;
125    private final TelephonyManager mTelephonyManager;
126    private final AudioManager mAudioManager;
127    private final boolean mUseDynamicRouting;
128    private final SparseIntArray mContextToBus = new SparseIntArray();
129    private final SparseArray<CarAudioDeviceInfo> mCarAudioDeviceInfos = new SparseArray<>();
130
131    private final AudioPolicy.AudioPolicyVolumeCallback mAudioPolicyVolumeCallback =
132            new AudioPolicy.AudioPolicyVolumeCallback() {
133        @Override
134        public void onVolumeAdjustment(int adjustment) {
135            final int usage = getSuggestedAudioUsage();
136            Log.v(CarLog.TAG_AUDIO,
137                    "onVolumeAdjustment: " + AudioManager.adjustToString(adjustment)
138                            + " suggested usage: " + AudioAttributes.usageToString(usage));
139            final int groupId = getVolumeGroupIdForUsage(usage);
140            final int currentVolume = getGroupVolume(groupId);
141            final int flags = AudioManager.FLAG_FROM_KEY | AudioManager.FLAG_SHOW_UI;
142            switch (adjustment) {
143                case AudioManager.ADJUST_LOWER:
144                    if (currentVolume > getGroupMinVolume(groupId)) {
145                        setGroupVolume(groupId, currentVolume - 1, flags);
146                    }
147                    break;
148                case AudioManager.ADJUST_RAISE:
149                    if (currentVolume < getGroupMaxVolume(groupId)) {
150                        setGroupVolume(groupId, currentVolume + 1, flags);
151                    }
152                    break;
153                case AudioManager.ADJUST_MUTE:
154                    mAudioManager.setMasterMute(true, flags);
155                    callbackMasterMuteChange(flags);
156                    break;
157                case AudioManager.ADJUST_UNMUTE:
158                    mAudioManager.setMasterMute(false, flags);
159                    callbackMasterMuteChange(flags);
160                    break;
161                case AudioManager.ADJUST_TOGGLE_MUTE:
162                    mAudioManager.setMasterMute(!mAudioManager.isMasterMute(), flags);
163                    callbackMasterMuteChange(flags);
164                    break;
165                case AudioManager.ADJUST_SAME:
166                default:
167                    break;
168            }
169        }
170    };
171
172    private final BinderInterfaceContainer<ICarVolumeCallback> mVolumeCallbackContainer =
173            new BinderInterfaceContainer<>();
174
175    /**
176     * Simulates {@link ICarVolumeCallback} when it's running in legacy mode.
177     */
178    private final BroadcastReceiver mLegacyVolumeChangedReceiver = new BroadcastReceiver() {
179        @Override
180        public void onReceive(Context context, Intent intent) {
181            switch (intent.getAction()) {
182                case AudioManager.VOLUME_CHANGED_ACTION:
183                    int streamType = intent.getIntExtra(AudioManager.EXTRA_VOLUME_STREAM_TYPE, -1);
184                    int groupId = getVolumeGroupIdForStreamType(streamType);
185                    if (groupId == -1) {
186                        Log.w(CarLog.TAG_AUDIO, "Unknown stream type: " + streamType);
187                    } else {
188                        callbackGroupVolumeChange(groupId, 0);
189                    }
190                    break;
191                case AudioManager.MASTER_MUTE_CHANGED_ACTION:
192                    callbackMasterMuteChange(0);
193                    break;
194            }
195        }
196    };
197
198    private AudioPolicy mAudioPolicy;
199    private CarVolumeGroup[] mCarVolumeGroups;
200
201    public CarAudioService(Context context) {
202        mContext = context;
203        mTelephonyManager = (TelephonyManager) mContext.getSystemService(Context.TELEPHONY_SERVICE);
204        mAudioManager = (AudioManager) mContext.getSystemService(Context.AUDIO_SERVICE);
205        mUseDynamicRouting = mContext.getResources().getBoolean(R.bool.audioUseDynamicRouting);
206    }
207
208    /**
209     * Dynamic routing and volume groups are set only if
210     * {@link #mUseDynamicRouting} is {@code true}. Otherwise, this service runs in legacy mode.
211     */
212    @Override
213    public void init() {
214        synchronized (mImplLock) {
215            if (!mUseDynamicRouting) {
216                Log.i(CarLog.TAG_AUDIO, "Audio dynamic routing not configured, run in legacy mode");
217                setupLegacyVolumeChangedListener();
218            } else {
219                setupDynamicRouting();
220                setupVolumeGroups();
221            }
222        }
223    }
224
225    @Override
226    public void release() {
227        synchronized (mImplLock) {
228            if (mUseDynamicRouting) {
229                if (mAudioPolicy != null) {
230                    mAudioManager.unregisterAudioPolicyAsync(mAudioPolicy);
231                    mAudioPolicy = null;
232                }
233            } else {
234                mContext.unregisterReceiver(mLegacyVolumeChangedReceiver);
235            }
236
237            mVolumeCallbackContainer.clear();
238        }
239    }
240
241    @Override
242    public void dump(PrintWriter writer) {
243        writer.println("*CarAudioService*");
244        writer.println("\tRun in legacy mode? " + (!mUseDynamicRouting));
245        writer.println("\tMaster mute? " + mAudioManager.isMasterMute());
246        // Empty line for comfortable reading
247        writer.println();
248        if (mUseDynamicRouting) {
249            for (CarVolumeGroup group : mCarVolumeGroups) {
250                group.dump(writer);
251            }
252        }
253    }
254
255    /**
256     * @see {@link android.car.media.CarAudioManager#setGroupVolume(int, int, int)}
257     */
258    @Override
259    public void setGroupVolume(int groupId, int index, int flags) {
260        synchronized (mImplLock) {
261            enforcePermission(Car.PERMISSION_CAR_CONTROL_AUDIO_VOLUME);
262
263            callbackGroupVolumeChange(groupId, flags);
264            // For legacy stream type based volume control
265            if (!mUseDynamicRouting) {
266                mAudioManager.setStreamVolume(STREAM_TYPES[groupId], index, flags);
267                return;
268            }
269
270            CarVolumeGroup group = getCarVolumeGroup(groupId);
271            group.setCurrentGainIndex(index);
272        }
273    }
274
275    private void callbackGroupVolumeChange(int groupId, int flags) {
276        for (BinderInterfaceContainer.BinderInterface<ICarVolumeCallback> callback :
277                mVolumeCallbackContainer.getInterfaces()) {
278            try {
279                callback.binderInterface.onGroupVolumeChanged(groupId, flags);
280            } catch (RemoteException e) {
281                Log.e(CarLog.TAG_AUDIO, "Failed to callback onGroupVolumeChanged", e);
282            }
283        }
284    }
285
286    private void callbackMasterMuteChange(int flags) {
287        for (BinderInterfaceContainer.BinderInterface<ICarVolumeCallback> callback :
288                mVolumeCallbackContainer.getInterfaces()) {
289            try {
290                callback.binderInterface.onMasterMuteChanged(flags);
291            } catch (RemoteException e) {
292                Log.e(CarLog.TAG_AUDIO, "Failed to callback onMasterMuteChanged", e);
293            }
294        }
295    }
296
297    /**
298     * @see {@link android.car.media.CarAudioManager#getGroupMaxVolume(int)}
299     */
300    @Override
301    public int getGroupMaxVolume(int groupId) {
302        synchronized (mImplLock) {
303            enforcePermission(Car.PERMISSION_CAR_CONTROL_AUDIO_VOLUME);
304
305            // For legacy stream type based volume control
306            if (!mUseDynamicRouting) {
307                return mAudioManager.getStreamMaxVolume(STREAM_TYPES[groupId]);
308            }
309
310            CarVolumeGroup group = getCarVolumeGroup(groupId);
311            return group.getMaxGainIndex();
312        }
313    }
314
315    /**
316     * @see {@link android.car.media.CarAudioManager#getGroupMinVolume(int)}
317     */
318    @Override
319    public int getGroupMinVolume(int groupId) {
320        synchronized (mImplLock) {
321            enforcePermission(Car.PERMISSION_CAR_CONTROL_AUDIO_VOLUME);
322
323            // For legacy stream type based volume control
324            if (!mUseDynamicRouting) {
325                return mAudioManager.getStreamMinVolume(STREAM_TYPES[groupId]);
326            }
327
328            CarVolumeGroup group = getCarVolumeGroup(groupId);
329            return group.getMinGainIndex();
330        }
331    }
332
333    /**
334     * @see {@link android.car.media.CarAudioManager#getGroupVolume(int)}
335     */
336    @Override
337    public int getGroupVolume(int groupId) {
338        synchronized (mImplLock) {
339            enforcePermission(Car.PERMISSION_CAR_CONTROL_AUDIO_VOLUME);
340
341            // For legacy stream type based volume control
342            if (!mUseDynamicRouting) {
343                return mAudioManager.getStreamVolume(STREAM_TYPES[groupId]);
344            }
345
346            CarVolumeGroup group = getCarVolumeGroup(groupId);
347            return group.getCurrentGainIndex();
348        }
349    }
350
351    private CarVolumeGroup getCarVolumeGroup(int groupId) {
352        Preconditions.checkNotNull(mCarVolumeGroups);
353        Preconditions.checkArgument(groupId >= 0 && groupId < mCarVolumeGroups.length,
354                "groupId out of range: " + groupId);
355        return mCarVolumeGroups[groupId];
356    }
357
358    private void setupLegacyVolumeChangedListener() {
359        IntentFilter intentFilter = new IntentFilter();
360        intentFilter.addAction(AudioManager.VOLUME_CHANGED_ACTION);
361        intentFilter.addAction(AudioManager.MASTER_MUTE_CHANGED_ACTION);
362        mContext.registerReceiver(mLegacyVolumeChangedReceiver, intentFilter);
363    }
364
365    private void setupDynamicRouting() {
366        final IAudioControl audioControl = getAudioControl();
367        if (audioControl == null) {
368            return;
369        }
370        AudioPolicy audioPolicy = getDynamicAudioPolicy(audioControl);
371        int r = mAudioManager.registerAudioPolicy(audioPolicy);
372        if (r != AudioManager.SUCCESS) {
373            throw new RuntimeException("registerAudioPolicy failed " + r);
374        }
375        mAudioPolicy = audioPolicy;
376    }
377
378    private void setupVolumeGroups() {
379        Preconditions.checkArgument(mCarAudioDeviceInfos.size() > 0,
380                "No bus device is configured to setup volume groups");
381        final CarVolumeGroupsHelper helper = new CarVolumeGroupsHelper(
382                mContext, R.xml.car_volume_groups);
383        mCarVolumeGroups = helper.loadVolumeGroups();
384        for (CarVolumeGroup group : mCarVolumeGroups) {
385            for (int contextNumber : group.getContexts()) {
386                int busNumber = mContextToBus.get(contextNumber);
387                group.bind(contextNumber, busNumber, mCarAudioDeviceInfos.get(busNumber));
388            }
389
390            // Now that we have all our contexts, ensure the HAL gets our intial value
391            group.setCurrentGainIndex(group.getCurrentGainIndex());
392
393            Log.v(CarLog.TAG_AUDIO, "Processed volume group: " + group);
394        }
395        // Perform validation after all volume groups are processed
396        if (!validateVolumeGroups()) {
397            throw new RuntimeException("Invalid volume groups configuration");
398        }
399    }
400
401    /**
402     * Constraints applied here:
403     *
404     * - One context should not appear in two groups
405     * - All contexts are assigned
406     * - One bus should not appear in two groups
407     * - All gain controllers in the same group have same step value
408     *
409     * Note that it is fine that there are buses not appear in any group, those buses may be
410     * reserved for other usages.
411     * Step value validation is done in {@link CarVolumeGroup#bind(int, int, CarAudioDeviceInfo)}
412     *
413     * See also the car_volume_groups.xml configuration
414     */
415    private boolean validateVolumeGroups() {
416        Set<Integer> contextSet = new HashSet<>();
417        Set<Integer> busNumberSet = new HashSet<>();
418        for (CarVolumeGroup group : mCarVolumeGroups) {
419            // One context should not appear in two groups
420            for (int context : group.getContexts()) {
421                if (contextSet.contains(context)) {
422                    Log.e(CarLog.TAG_AUDIO, "Context appears in two groups: " + context);
423                    return false;
424                }
425                contextSet.add(context);
426            }
427
428            // One bus should not appear in two groups
429            for (int busNumber : group.getBusNumbers()) {
430                if (busNumberSet.contains(busNumber)) {
431                    Log.e(CarLog.TAG_AUDIO, "Bus appears in two groups: " + busNumber);
432                    return false;
433                }
434                busNumberSet.add(busNumber);
435            }
436        }
437
438        // All contexts are assigned
439        if (contextSet.size() != CONTEXT_NUMBERS.length) {
440            Log.e(CarLog.TAG_AUDIO, "Some contexts are not assigned to group");
441            Log.e(CarLog.TAG_AUDIO, "Assigned contexts "
442                    + Arrays.toString(contextSet.toArray(new Integer[contextSet.size()])));
443            Log.e(CarLog.TAG_AUDIO, "All contexts " + Arrays.toString(CONTEXT_NUMBERS));
444            return false;
445        }
446
447        return true;
448    }
449
450    @Nullable
451    private AudioPolicy getDynamicAudioPolicy(@NonNull IAudioControl audioControl) {
452        AudioPolicy.Builder builder = new AudioPolicy.Builder(mContext);
453        builder.setLooper(Looper.getMainLooper());
454
455        // 1st, enumerate all output bus device ports
456        AudioDeviceInfo[] deviceInfos = mAudioManager.getDevices(AudioManager.GET_DEVICES_OUTPUTS);
457        if (deviceInfos.length == 0) {
458            Log.e(CarLog.TAG_AUDIO, "getDynamicAudioPolicy, no output device available, ignore");
459            return null;
460        }
461        for (AudioDeviceInfo info : deviceInfos) {
462            Log.v(CarLog.TAG_AUDIO, String.format("output id=%d address=%s type=%s",
463                    info.getId(), info.getAddress(), info.getType()));
464            if (info.getType() == AudioDeviceInfo.TYPE_BUS) {
465                final CarAudioDeviceInfo carInfo = new CarAudioDeviceInfo(info);
466                // See also the audio_policy_configuration.xml and getBusForContext in
467                // audio control HAL, the bus number should be no less than zero.
468                if (carInfo.getBusNumber() >= 0) {
469                    mCarAudioDeviceInfos.put(carInfo.getBusNumber(), carInfo);
470                    Log.i(CarLog.TAG_AUDIO, "Valid bus found " + carInfo);
471                }
472            }
473        }
474
475        // 2nd, map context to physical bus
476        try {
477            for (int contextNumber : CONTEXT_NUMBERS) {
478                int busNumber = audioControl.getBusForContext(contextNumber);
479                mContextToBus.put(contextNumber, busNumber);
480                CarAudioDeviceInfo info = mCarAudioDeviceInfos.get(busNumber);
481                if (info == null) {
482                    Log.w(CarLog.TAG_AUDIO, "No bus configured for context: " + contextNumber);
483                }
484            }
485        } catch (RemoteException e) {
486            Log.e(CarLog.TAG_AUDIO, "Error mapping context to physical bus", e);
487        }
488
489        // 3rd, enumerate all physical buses and build the routing policy.
490        // Note that one can not register audio mix for same bus more than once.
491        for (int i = 0; i < mCarAudioDeviceInfos.size(); i++) {
492            int busNumber = mCarAudioDeviceInfos.keyAt(i);
493            boolean hasContext = false;
494            CarAudioDeviceInfo info = mCarAudioDeviceInfos.valueAt(i);
495            AudioFormat mixFormat = new AudioFormat.Builder()
496                    .setSampleRate(info.getSampleRate())
497                    .setEncoding(info.getEncodingFormat())
498                    .setChannelMask(info.getChannelCount())
499                    .build();
500            AudioMixingRule.Builder mixingRuleBuilder = new AudioMixingRule.Builder();
501            for (int j = 0; j < mContextToBus.size(); j++) {
502                if (mContextToBus.valueAt(j) == busNumber) {
503                    hasContext = true;
504                    int contextNumber = mContextToBus.keyAt(j);
505                    int[] usages = getUsagesForContext(contextNumber);
506                    for (int usage : usages) {
507                        mixingRuleBuilder.addRule(
508                                new AudioAttributes.Builder().setUsage(usage).build(),
509                                AudioMixingRule.RULE_MATCH_ATTRIBUTE_USAGE);
510                    }
511                    Log.i(CarLog.TAG_AUDIO, "Bus number: " + busNumber
512                            + " contextNumber: " + contextNumber
513                            + " sampleRate: " + info.getSampleRate()
514                            + " channels: " + info.getChannelCount()
515                            + " usages: " + Arrays.toString(usages));
516                }
517            }
518            if (hasContext) {
519                // It's a valid case that an audio output bus is defined in
520                // audio_policy_configuration and no context is assigned to it.
521                // In such case, do not build a policy mix with zero rules.
522                AudioMix audioMix = new AudioMix.Builder(mixingRuleBuilder.build())
523                        .setFormat(mixFormat)
524                        .setDevice(info.getAudioDeviceInfo())
525                        .setRouteFlags(AudioMix.ROUTE_FLAG_RENDER)
526                        .build();
527                builder.addMix(audioMix);
528            }
529        }
530
531        // 4th, attach the {@link AudioPolicyVolumeCallback}
532        builder.setAudioPolicyVolumeCallback(mAudioPolicyVolumeCallback);
533
534        return builder.build();
535    }
536
537    private int[] getUsagesForContext(int contextNumber) {
538        final List<Integer> usages = new ArrayList<>();
539        for (int i = 0; i < USAGE_TO_CONTEXT.size(); i++) {
540            if (USAGE_TO_CONTEXT.valueAt(i) == contextNumber) {
541                usages.add(USAGE_TO_CONTEXT.keyAt(i));
542            }
543        }
544        return usages.stream().mapToInt(i -> i).toArray();
545    }
546
547    @Override
548    public void setFadeTowardFront(float value) {
549        synchronized (mImplLock) {
550            enforcePermission(Car.PERMISSION_CAR_CONTROL_AUDIO_VOLUME);
551            final IAudioControl audioControlHal = getAudioControl();
552            if (audioControlHal != null) {
553                try {
554                    audioControlHal.setFadeTowardFront(value);
555                } catch (RemoteException e) {
556                    Log.e(CarLog.TAG_AUDIO, "setFadeTowardFront failed", e);
557                }
558            }
559        }
560    }
561
562    @Override
563    public void setBalanceTowardRight(float value) {
564        synchronized (mImplLock) {
565            enforcePermission(Car.PERMISSION_CAR_CONTROL_AUDIO_VOLUME);
566            final IAudioControl audioControlHal = getAudioControl();
567            if (audioControlHal != null) {
568                try {
569                    audioControlHal.setBalanceTowardRight(value);
570                } catch (RemoteException e) {
571                    Log.e(CarLog.TAG_AUDIO, "setBalanceTowardRight failed", e);
572                }
573            }
574        }
575    }
576
577    /**
578     * @return Array of accumulated device addresses, empty array if we found nothing
579     */
580    @Override
581    public @NonNull String[] getExternalSources() {
582        synchronized (mImplLock) {
583            enforcePermission(Car.PERMISSION_CAR_CONTROL_AUDIO_SETTINGS);
584            List<String> sourceAddresses = new ArrayList<>();
585
586            AudioDeviceInfo[] devices = mAudioManager.getDevices(AudioManager.GET_DEVICES_INPUTS);
587            if (devices.length == 0) {
588                Log.w(CarLog.TAG_AUDIO, "getExternalSources, no input devices found.");
589            }
590
591            // Collect the list of non-microphone input ports
592            for (AudioDeviceInfo info : devices) {
593                switch (info.getType()) {
594                    // TODO:  Can we trim this set down? Especially duplicates like FM vs FM_TUNER?
595                    case AudioDeviceInfo.TYPE_FM:
596                    case AudioDeviceInfo.TYPE_FM_TUNER:
597                    case AudioDeviceInfo.TYPE_TV_TUNER:
598                    case AudioDeviceInfo.TYPE_HDMI:
599                    case AudioDeviceInfo.TYPE_AUX_LINE:
600                    case AudioDeviceInfo.TYPE_LINE_ANALOG:
601                    case AudioDeviceInfo.TYPE_LINE_DIGITAL:
602                    case AudioDeviceInfo.TYPE_USB_ACCESSORY:
603                    case AudioDeviceInfo.TYPE_USB_DEVICE:
604                    case AudioDeviceInfo.TYPE_USB_HEADSET:
605                    case AudioDeviceInfo.TYPE_IP:
606                    case AudioDeviceInfo.TYPE_BUS:
607                        String address = info.getAddress();
608                        if (TextUtils.isEmpty(address)) {
609                            Log.w(CarLog.TAG_AUDIO,
610                                    "Discarded device with empty address, type=" + info.getType());
611                        } else {
612                            sourceAddresses.add(address);
613                        }
614                }
615            }
616
617            return sourceAddresses.toArray(new String[sourceAddresses.size()]);
618        }
619    }
620
621    @Override
622    public CarAudioPatchHandle createAudioPatch(String sourceAddress,
623            @AudioAttributes.AttributeUsage int usage, int gainInMillibels) {
624        synchronized (mImplLock) {
625            enforcePermission(Car.PERMISSION_CAR_CONTROL_AUDIO_SETTINGS);
626            return createAudioPatchLocked(sourceAddress, usage, gainInMillibels);
627        }
628    }
629
630    @Override
631    public void releaseAudioPatch(CarAudioPatchHandle carPatch) {
632        synchronized (mImplLock) {
633            enforcePermission(Car.PERMISSION_CAR_CONTROL_AUDIO_SETTINGS);
634            releaseAudioPatchLocked(carPatch);
635        }
636    }
637
638    private CarAudioPatchHandle createAudioPatchLocked(String sourceAddress,
639            @AudioAttributes.AttributeUsage int usage, int gainInMillibels) {
640        // Find the named source port
641        AudioDeviceInfo sourcePortInfo = null;
642        AudioDeviceInfo[] deviceInfos = mAudioManager.getDevices(AudioManager.GET_DEVICES_INPUTS);
643        for (AudioDeviceInfo info : deviceInfos) {
644            if (sourceAddress.equals(info.getAddress())) {
645                // This is the one for which we're looking
646                sourcePortInfo = info;
647                break;
648            }
649        }
650        Preconditions.checkNotNull(sourcePortInfo,
651                "Specified source is not available: " + sourceAddress);
652
653        // Find the output port associated with the given carUsage
654        AudioDevicePort sinkPort = Preconditions.checkNotNull(getAudioPort(usage),
655                "Sink not available for usage: " + AudioAttributes.usageToString(usage));
656
657        // {@link android.media.AudioPort#activeConfig()} is valid for mixer port only,
658        // since audio framework has no clue what's active on the device ports.
659        // Therefore we construct an empty / default configuration here, which the audio HAL
660        // implementation should ignore.
661        AudioPortConfig sinkConfig = sinkPort.buildConfig(0,
662                AudioFormat.CHANNEL_OUT_DEFAULT, AudioFormat.ENCODING_DEFAULT, null);
663        Log.d(CarLog.TAG_AUDIO, "createAudioPatch sinkConfig: " + sinkConfig);
664
665        // Configure the source port to match the output port except for a gain adjustment
666        final CarAudioDeviceInfo helper = new CarAudioDeviceInfo(sourcePortInfo);
667        AudioGain audioGain = Preconditions.checkNotNull(helper.getAudioGain(),
668                "Gain controller not available for source port");
669
670        // size of gain values is 1 in MODE_JOINT
671        AudioGainConfig audioGainConfig = audioGain.buildConfig(AudioGain.MODE_JOINT,
672                audioGain.channelMask(), new int[] { gainInMillibels }, 0);
673        // Construct an empty / default configuration excepts gain config here and it's up to the
674        // audio HAL how to interpret this configuration, which the audio HAL
675        // implementation should ignore.
676        AudioPortConfig sourceConfig = sourcePortInfo.getPort().buildConfig(0,
677                AudioFormat.CHANNEL_IN_DEFAULT, AudioFormat.ENCODING_DEFAULT, audioGainConfig);
678
679        // Create an audioPatch to connect the two ports
680        AudioPatch[] patch = new AudioPatch[] { null };
681        int result = AudioManager.createAudioPatch(patch,
682                new AudioPortConfig[] { sourceConfig },
683                new AudioPortConfig[] { sinkConfig });
684        if (result != AudioManager.SUCCESS) {
685            throw new RuntimeException("createAudioPatch failed with code " + result);
686        }
687
688        Preconditions.checkNotNull(patch[0],
689                "createAudioPatch didn't provide expected single handle");
690        Log.d(CarLog.TAG_AUDIO, "Audio patch created: " + patch[0]);
691        return new CarAudioPatchHandle(patch[0]);
692    }
693
694    private void releaseAudioPatchLocked(CarAudioPatchHandle carPatch) {
695        // NOTE:  AudioPolicyService::removeNotificationClient will take care of this automatically
696        //        if the client that created a patch quits.
697
698        // FIXME {@link AudioManager#listAudioPatches(ArrayList)} returns old generation of
699        // audio patches after creation
700        ArrayList<AudioPatch> patches = new ArrayList<>();
701        int result = AudioSystem.listAudioPatches(patches, new int[1]);
702        if (result != AudioManager.SUCCESS) {
703            throw new RuntimeException("listAudioPatches failed with code " + result);
704        }
705
706        // Look for a patch that matches the provided user side handle
707        for (AudioPatch patch : patches) {
708            if (carPatch.represents(patch)) {
709                // Found it!
710                result = AudioManager.releaseAudioPatch(patch);
711                if (result != AudioManager.SUCCESS) {
712                    throw new RuntimeException("releaseAudioPatch failed with code " + result);
713                }
714                return;
715            }
716        }
717
718        // If we didn't find a match, then something went awry, but it's probably not fatal...
719        Log.e(CarLog.TAG_AUDIO, "releaseAudioPatch found no match for " + carPatch);
720    }
721
722    @Override
723    public int getVolumeGroupCount() {
724        synchronized (mImplLock) {
725            enforcePermission(Car.PERMISSION_CAR_CONTROL_AUDIO_VOLUME);
726
727            // For legacy stream type based volume control
728            if (!mUseDynamicRouting) return STREAM_TYPES.length;
729
730            return mCarVolumeGroups == null ? 0 : mCarVolumeGroups.length;
731        }
732    }
733
734    @Override
735    public int getVolumeGroupIdForUsage(@AudioAttributes.AttributeUsage int usage) {
736        synchronized (mImplLock) {
737            enforcePermission(Car.PERMISSION_CAR_CONTROL_AUDIO_VOLUME);
738
739            if (mCarVolumeGroups == null) {
740                return -1;
741            }
742
743            for (int i = 0; i < mCarVolumeGroups.length; i++) {
744                int[] contexts = mCarVolumeGroups[i].getContexts();
745                for (int context : contexts) {
746                    if (USAGE_TO_CONTEXT.get(usage) == context) {
747                        return i;
748                    }
749                }
750            }
751            return -1;
752        }
753    }
754
755    @Override
756    public @NonNull int[] getUsagesForVolumeGroupId(int groupId) {
757        synchronized (mImplLock) {
758            enforcePermission(Car.PERMISSION_CAR_CONTROL_AUDIO_VOLUME);
759
760            // For legacy stream type based volume control
761            if (!mUseDynamicRouting) {
762                return new int[] { STREAM_TYPE_USAGES[groupId] };
763            }
764
765            CarVolumeGroup group = getCarVolumeGroup(groupId);
766            Set<Integer> contexts =
767                    Arrays.stream(group.getContexts()).boxed().collect(Collectors.toSet());
768            final List<Integer> usages = new ArrayList<>();
769            for (int i = 0; i < USAGE_TO_CONTEXT.size(); i++) {
770                if (contexts.contains(USAGE_TO_CONTEXT.valueAt(i))) {
771                    usages.add(USAGE_TO_CONTEXT.keyAt(i));
772                }
773            }
774            return usages.stream().mapToInt(i -> i).toArray();
775        }
776    }
777
778    /**
779     * See {@link android.car.media.CarAudioManager#registerVolumeCallback(IBinder)}
780     */
781    @Override
782    public void registerVolumeCallback(@NonNull IBinder binder) {
783        synchronized (mImplLock) {
784            enforcePermission(Car.PERMISSION_CAR_CONTROL_AUDIO_VOLUME);
785
786            mVolumeCallbackContainer.addBinder(ICarVolumeCallback.Stub.asInterface(binder));
787        }
788    }
789
790    /**
791     * See {@link android.car.media.CarAudioManager#unregisterVolumeCallback(IBinder)}
792     */
793    @Override
794    public void unregisterVolumeCallback(@NonNull IBinder binder) {
795        synchronized (mImplLock) {
796            enforcePermission(Car.PERMISSION_CAR_CONTROL_AUDIO_VOLUME);
797
798            mVolumeCallbackContainer.removeBinder(ICarVolumeCallback.Stub.asInterface(binder));
799        }
800    }
801
802    private void enforcePermission(String permissionName) {
803        if (mContext.checkCallingOrSelfPermission(permissionName)
804                != PackageManager.PERMISSION_GRANTED) {
805            throw new SecurityException("requires permission " + permissionName);
806        }
807    }
808
809    /**
810     * @return {@link AudioDevicePort} that handles the given car audio usage.
811     * Multiple usages may share one {@link AudioDevicePort}
812     */
813    private @Nullable AudioDevicePort getAudioPort(@AudioAttributes.AttributeUsage int usage) {
814        final int groupId = getVolumeGroupIdForUsage(usage);
815        final CarVolumeGroup group = Preconditions.checkNotNull(mCarVolumeGroups[groupId],
816                "Can not find CarVolumeGroup by usage: "
817                        + AudioAttributes.usageToString(usage));
818        return group.getAudioDevicePortForContext(USAGE_TO_CONTEXT.get(usage));
819    }
820
821    /**
822     * @return The suggested {@link AudioAttributes} usage to which the volume key events apply
823     */
824    private @AudioAttributes.AttributeUsage int getSuggestedAudioUsage() {
825        int callState = mTelephonyManager.getCallState();
826        if (callState == TelephonyManager.CALL_STATE_RINGING) {
827            return AudioAttributes.USAGE_NOTIFICATION_RINGTONE;
828        } else if (callState == TelephonyManager.CALL_STATE_OFFHOOK) {
829            return AudioAttributes.USAGE_VOICE_COMMUNICATION;
830        } else {
831            List<AudioPlaybackConfiguration> playbacks = mAudioManager
832                    .getActivePlaybackConfigurations()
833                    .stream()
834                    .filter(AudioPlaybackConfiguration::isActive)
835                    .collect(Collectors.toList());
836            if (!playbacks.isEmpty()) {
837                // Get audio usage from active playbacks if there is any, last one if multiple
838                return playbacks.get(playbacks.size() - 1).getAudioAttributes().getUsage();
839            } else {
840                // TODO(b/72695246): Otherwise, get audio usage from foreground activity/window
841                return DEFAULT_AUDIO_USAGE;
842            }
843        }
844    }
845
846    /**
847     * Gets volume group by a given legacy stream type
848     * @param streamType Legacy stream type such as {@link AudioManager#STREAM_MUSIC}
849     * @return volume group id mapped from stream type
850     */
851    private int getVolumeGroupIdForStreamType(int streamType) {
852        int groupId = -1;
853        for (int i = 0; i < STREAM_TYPES.length; i++) {
854            if (streamType == STREAM_TYPES[i]) {
855                groupId = i;
856                break;
857            }
858        }
859        return groupId;
860    }
861
862    @Nullable
863    private static IAudioControl getAudioControl() {
864        try {
865            return IAudioControl.getService();
866        } catch (RemoteException e) {
867            Log.e(CarLog.TAG_AUDIO, "Failed to get IAudioControl service", e);
868        } catch (NoSuchElementException e) {
869            Log.e(CarLog.TAG_AUDIO, "IAudioControl service not registered yet");
870        }
871        return null;
872    }
873}
874