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  */
16 package com.android.car.audio;
17 
18 import android.annotation.NonNull;
19 import android.annotation.Nullable;
20 import android.car.Car;
21 import android.car.media.CarAudioManager;
22 import android.car.media.CarAudioPatchHandle;
23 import android.car.media.ICarAudio;
24 import android.car.media.ICarVolumeCallback;
25 import android.content.BroadcastReceiver;
26 import android.content.Context;
27 import android.content.Intent;
28 import android.content.IntentFilter;
29 import android.content.pm.PackageManager;
30 import android.hardware.automotive.audiocontrol.V1_0.IAudioControl;
31 import android.media.AudioAttributes;
32 import android.media.AudioDeviceInfo;
33 import android.media.AudioDevicePort;
34 import android.media.AudioFocusInfo;
35 import android.media.AudioFormat;
36 import android.media.AudioGain;
37 import android.media.AudioGainConfig;
38 import android.media.AudioManager;
39 import android.media.AudioPatch;
40 import android.media.AudioPlaybackConfiguration;
41 import android.media.AudioPortConfig;
42 import android.media.AudioSystem;
43 import android.media.audiopolicy.AudioPolicy;
44 import android.os.IBinder;
45 import android.os.Looper;
46 import android.os.RemoteException;
47 import android.provider.Settings;
48 import android.telephony.TelephonyManager;
49 import android.text.TextUtils;
50 import android.util.Log;
51 import android.util.SparseArray;
52 import android.view.DisplayAddress;
53 import android.view.KeyEvent;
54 
55 import com.android.car.BinderInterfaceContainer;
56 import com.android.car.CarLog;
57 import com.android.car.CarServiceBase;
58 import com.android.car.R;
59 import com.android.internal.util.Preconditions;
60 
61 import org.xmlpull.v1.XmlPullParserException;
62 
63 import java.io.File;
64 import java.io.FileInputStream;
65 import java.io.IOException;
66 import java.io.InputStream;
67 import java.io.PrintWriter;
68 import java.util.ArrayList;
69 import java.util.Arrays;
70 import java.util.HashMap;
71 import java.util.List;
72 import java.util.Map;
73 import java.util.NoSuchElementException;
74 import java.util.Set;
75 import java.util.stream.Collectors;
76 
77 /**
78  * Service responsible for interaction with car's audio system.
79  */
80 public class CarAudioService extends ICarAudio.Stub implements CarServiceBase {
81 
82     // Turning this off will result in falling back to the default focus policy of Android
83     // (which boils down to "grant if not in a phone call, else deny").
84     // Aside from the obvious effect of ignoring the logic in CarAudioFocus, this will also
85     // result in the framework taking over responsibility for ducking in TRANSIENT_LOSS cases.
86     // Search for "DUCK_VSHAPE" in PLaybackActivityMonitor.java to see where this happens.
87     private static boolean sUseCarAudioFocus = true;
88 
89     // Key to persist global mute state in system settings
90     private static final String VOLUME_SETTINGS_KEY_MASTER_MUTE = "android.car.MASTER_MUTE";
91 
92     // The trailing slash forms a directory-liked hierarchy and
93     // allows listening for both GROUP/MEDIA and GROUP/NAVIGATION.
94     private static final String VOLUME_SETTINGS_KEY_FOR_GROUP_PREFIX = "android.car.VOLUME_GROUP/";
95 
96     // CarAudioService reads configuration from the following paths respectively.
97     // If the first one is found, all others are ignored.
98     // If no one is found, it fallbacks to car_volume_groups.xml resource file.
99     private static final String[] AUDIO_CONFIGURATION_PATHS = new String[] {
100             "/vendor/etc/car_audio_configuration.xml",
101             "/system/etc/car_audio_configuration.xml"
102     };
103 
104     /**
105      * Gets the key to persist volume for a volume group in settings
106      *
107      * @param zoneId The audio zone id
108      * @param groupId The volume group id
109      * @return Key to persist volume index for volume group in system settings
110      */
getVolumeSettingsKeyForGroup(int zoneId, int groupId)111     static String getVolumeSettingsKeyForGroup(int zoneId, int groupId) {
112         final int maskedGroupId = (zoneId << 8) + groupId;
113         return VOLUME_SETTINGS_KEY_FOR_GROUP_PREFIX + maskedGroupId;
114     }
115 
116     private final Object mImplLock = new Object();
117 
118     private final Context mContext;
119     private final TelephonyManager mTelephonyManager;
120     private final AudioManager mAudioManager;
121     private final boolean mUseDynamicRouting;
122     private final boolean mPersistMasterMuteState;
123 
124     private final AudioPolicy.AudioPolicyVolumeCallback mAudioPolicyVolumeCallback =
125             new AudioPolicy.AudioPolicyVolumeCallback() {
126         @Override
127         public void onVolumeAdjustment(int adjustment) {
128             final int usage = getSuggestedAudioUsage();
129             Log.v(CarLog.TAG_AUDIO,
130                     "onVolumeAdjustment: " + AudioManager.adjustToString(adjustment)
131                             + " suggested usage: " + AudioAttributes.usageToString(usage));
132             // TODO: Pass zone id into this callback.
133             final int zoneId = CarAudioManager.PRIMARY_AUDIO_ZONE;
134             final int groupId = getVolumeGroupIdForUsage(zoneId, usage);
135             final int currentVolume = getGroupVolume(zoneId, groupId);
136             final int flags = AudioManager.FLAG_FROM_KEY | AudioManager.FLAG_SHOW_UI;
137             switch (adjustment) {
138                 case AudioManager.ADJUST_LOWER:
139                     int minValue = Math.max(currentVolume - 1, getGroupMinVolume(zoneId, groupId));
140                     setGroupVolume(zoneId, groupId, minValue , flags);
141                     break;
142                 case AudioManager.ADJUST_RAISE:
143                     int maxValue =  Math.min(currentVolume + 1, getGroupMaxVolume(zoneId, groupId));
144                     setGroupVolume(zoneId, groupId, maxValue, flags);
145                     break;
146                 case AudioManager.ADJUST_MUTE:
147                     setMasterMute(true, flags);
148                     callbackMasterMuteChange(zoneId, flags);
149                     break;
150                 case AudioManager.ADJUST_UNMUTE:
151                     setMasterMute(false, flags);
152                     callbackMasterMuteChange(zoneId, flags);
153                     break;
154                 case AudioManager.ADJUST_TOGGLE_MUTE:
155                     setMasterMute(!mAudioManager.isMasterMute(), flags);
156                     callbackMasterMuteChange(zoneId, flags);
157                     break;
158                 case AudioManager.ADJUST_SAME:
159                 default:
160                     break;
161             }
162         }
163     };
164 
165     private final BinderInterfaceContainer<ICarVolumeCallback> mVolumeCallbackContainer =
166             new BinderInterfaceContainer<>();
167 
168     /**
169      * Simulates {@link ICarVolumeCallback} when it's running in legacy mode.
170      * This receiver assumes the intent is sent to {@link CarAudioManager#PRIMARY_AUDIO_ZONE}.
171      */
172     private final BroadcastReceiver mLegacyVolumeChangedReceiver = new BroadcastReceiver() {
173         @Override
174         public void onReceive(Context context, Intent intent) {
175             final int zoneId = CarAudioManager.PRIMARY_AUDIO_ZONE;
176             switch (intent.getAction()) {
177                 case AudioManager.VOLUME_CHANGED_ACTION:
178                     int streamType = intent.getIntExtra(AudioManager.EXTRA_VOLUME_STREAM_TYPE, -1);
179                     int groupId = getVolumeGroupIdForStreamType(streamType);
180                     if (groupId == -1) {
181                         Log.w(CarLog.TAG_AUDIO, "Unknown stream type: " + streamType);
182                     } else {
183                         callbackGroupVolumeChange(zoneId, groupId, 0);
184                     }
185                     break;
186                 case AudioManager.MASTER_MUTE_CHANGED_ACTION:
187                     callbackMasterMuteChange(zoneId, 0);
188                     break;
189             }
190         }
191     };
192 
193     private AudioPolicy mAudioPolicy;
194     private CarZonesAudioFocus mFocusHandler;
195     private String mCarAudioConfigurationPath;
196     private CarAudioZone[] mCarAudioZones;
197 
198     // TODO do not store uid mapping here instead use the uid
199     //  device affinity in audio policy when available
200     private Map<Integer, Integer> mUidToZoneMap;
201 
CarAudioService(Context context)202     public CarAudioService(Context context) {
203         mContext = context;
204         mTelephonyManager = (TelephonyManager) mContext.getSystemService(Context.TELEPHONY_SERVICE);
205         mAudioManager = (AudioManager) mContext.getSystemService(Context.AUDIO_SERVICE);
206         mUseDynamicRouting = mContext.getResources().getBoolean(R.bool.audioUseDynamicRouting);
207         mPersistMasterMuteState = mContext.getResources().getBoolean(
208                 R.bool.audioPersistMasterMuteState);
209         mUidToZoneMap = new HashMap<>();
210     }
211 
212     /**
213      * Dynamic routing and volume groups are set only if
214      * {@link #mUseDynamicRouting} is {@code true}. Otherwise, this service runs in legacy mode.
215      */
216     @Override
init()217     public void init() {
218         synchronized (mImplLock) {
219             if (mUseDynamicRouting) {
220                 // Enumerate all output bus device ports
221                 AudioDeviceInfo[] deviceInfos = mAudioManager.getDevices(
222                         AudioManager.GET_DEVICES_OUTPUTS);
223                 if (deviceInfos.length == 0) {
224                     Log.e(CarLog.TAG_AUDIO, "No output device available, ignore");
225                     return;
226                 }
227                 SparseArray<CarAudioDeviceInfo> busToCarAudioDeviceInfo = new SparseArray<>();
228                 for (AudioDeviceInfo info : deviceInfos) {
229                     Log.v(CarLog.TAG_AUDIO, String.format("output id=%d address=%s type=%s",
230                             info.getId(), info.getAddress(), info.getType()));
231                     if (info.getType() == AudioDeviceInfo.TYPE_BUS) {
232                         final CarAudioDeviceInfo carInfo = new CarAudioDeviceInfo(info);
233                         // See also the audio_policy_configuration.xml,
234                         // the bus number should be no less than zero.
235                         if (carInfo.getBusNumber() >= 0) {
236                             busToCarAudioDeviceInfo.put(carInfo.getBusNumber(), carInfo);
237                             Log.i(CarLog.TAG_AUDIO, "Valid bus found " + carInfo);
238                         }
239                     }
240                 }
241                 setupDynamicRouting(busToCarAudioDeviceInfo);
242             } else {
243                 Log.i(CarLog.TAG_AUDIO, "Audio dynamic routing not enabled, run in legacy mode");
244                 setupLegacyVolumeChangedListener();
245             }
246 
247             // Restore global mute state if applicable
248             if (mPersistMasterMuteState) {
249                 boolean storedMasterMute = Settings.Global.getInt(mContext.getContentResolver(),
250                         VOLUME_SETTINGS_KEY_MASTER_MUTE, 0) != 0;
251                 setMasterMute(storedMasterMute, 0);
252             }
253         }
254     }
255 
256     @Override
release()257     public void release() {
258         synchronized (mImplLock) {
259             if (mUseDynamicRouting) {
260                 if (mAudioPolicy != null) {
261                     mAudioManager.unregisterAudioPolicyAsync(mAudioPolicy);
262                     mAudioPolicy = null;
263                     mFocusHandler.setOwningPolicy(null, null);
264                     mFocusHandler = null;
265                 }
266             } else {
267                 mContext.unregisterReceiver(mLegacyVolumeChangedReceiver);
268             }
269 
270             mVolumeCallbackContainer.clear();
271         }
272     }
273 
274     @Override
dump(PrintWriter writer)275     public void dump(PrintWriter writer) {
276         writer.println("*CarAudioService*");
277         writer.println("\tRun in legacy mode? " + (!mUseDynamicRouting));
278         writer.println("\tPersist master mute state? " + mPersistMasterMuteState);
279         writer.println("\tMaster muted? " + mAudioManager.isMasterMute());
280         if (mCarAudioConfigurationPath != null) {
281             writer.println("\tCar audio configuration path: " + mCarAudioConfigurationPath);
282         }
283         // Empty line for comfortable reading
284         writer.println();
285         if (mUseDynamicRouting) {
286             for (CarAudioZone zone : mCarAudioZones) {
287                 zone.dump("\t", writer);
288             }
289             writer.println();
290             writer.println("\tUID to Zone Mapping:");
291             for (int callingId : mUidToZoneMap.keySet()) {
292                 writer.printf("\t\tUID %d mapped to zone %d\n",
293                         callingId,
294                         mUidToZoneMap.get(callingId));
295             }
296             //Print focus handler info
297             writer.println();
298             mFocusHandler.dump("\t", writer);
299         }
300 
301     }
302 
303     @Override
isDynamicRoutingEnabled()304     public boolean isDynamicRoutingEnabled() {
305         return mUseDynamicRouting;
306     }
307 
308     /**
309      * @see {@link android.car.media.CarAudioManager#setGroupVolume(int, int, int, int)}
310      */
311     @Override
setGroupVolume(int zoneId, int groupId, int index, int flags)312     public void setGroupVolume(int zoneId, int groupId, int index, int flags) {
313         synchronized (mImplLock) {
314             enforcePermission(Car.PERMISSION_CAR_CONTROL_AUDIO_VOLUME);
315 
316             callbackGroupVolumeChange(zoneId, groupId, flags);
317             // For legacy stream type based volume control
318             if (!mUseDynamicRouting) {
319                 mAudioManager.setStreamVolume(
320                         CarAudioDynamicRouting.STREAM_TYPES[groupId], index, flags);
321                 return;
322             }
323 
324             CarVolumeGroup group = getCarVolumeGroup(zoneId, groupId);
325             group.setCurrentGainIndex(index);
326         }
327     }
328 
callbackGroupVolumeChange(int zoneId, int groupId, int flags)329     private void callbackGroupVolumeChange(int zoneId, int groupId, int flags) {
330         for (BinderInterfaceContainer.BinderInterface<ICarVolumeCallback> callback :
331                 mVolumeCallbackContainer.getInterfaces()) {
332             try {
333                 callback.binderInterface.onGroupVolumeChanged(zoneId, groupId, flags);
334             } catch (RemoteException e) {
335                 Log.e(CarLog.TAG_AUDIO, "Failed to callback onGroupVolumeChanged", e);
336             }
337         }
338     }
339 
setMasterMute(boolean mute, int flags)340     private void setMasterMute(boolean mute, int flags) {
341         mAudioManager.setMasterMute(mute, flags);
342 
343         // When the global mute is turned ON, we want the playing app to get a "pause" command.
344         // When the volume is unmuted, we want to resume playback.
345         int keycode = mute ? KeyEvent.KEYCODE_MEDIA_PAUSE : KeyEvent.KEYCODE_MEDIA_PLAY;
346         mAudioManager.dispatchMediaKeyEvent(new KeyEvent(KeyEvent.ACTION_DOWN, keycode));
347         mAudioManager.dispatchMediaKeyEvent(new KeyEvent(KeyEvent.ACTION_UP, keycode));
348     }
349 
callbackMasterMuteChange(int zoneId, int flags)350     private void callbackMasterMuteChange(int zoneId, int flags) {
351         for (BinderInterfaceContainer.BinderInterface<ICarVolumeCallback> callback :
352                 mVolumeCallbackContainer.getInterfaces()) {
353             try {
354                 callback.binderInterface.onMasterMuteChanged(zoneId, flags);
355             } catch (RemoteException e) {
356                 Log.e(CarLog.TAG_AUDIO, "Failed to callback onMasterMuteChanged", e);
357             }
358         }
359 
360         // Persists global mute state if applicable
361         if (mPersistMasterMuteState) {
362             Settings.Global.putInt(mContext.getContentResolver(),
363                     VOLUME_SETTINGS_KEY_MASTER_MUTE,
364                     mAudioManager.isMasterMute() ? 1 : 0);
365         }
366     }
367 
368     /**
369      * @see {@link android.car.media.CarAudioManager#getGroupMaxVolume(int, int)}
370      */
371     @Override
getGroupMaxVolume(int zoneId, int groupId)372     public int getGroupMaxVolume(int zoneId, int groupId) {
373         synchronized (mImplLock) {
374             enforcePermission(Car.PERMISSION_CAR_CONTROL_AUDIO_VOLUME);
375 
376             // For legacy stream type based volume control
377             if (!mUseDynamicRouting) {
378                 return mAudioManager.getStreamMaxVolume(
379                         CarAudioDynamicRouting.STREAM_TYPES[groupId]);
380             }
381 
382             CarVolumeGroup group = getCarVolumeGroup(zoneId, groupId);
383             return group.getMaxGainIndex();
384         }
385     }
386 
387     /**
388      * @see {@link android.car.media.CarAudioManager#getGroupMinVolume(int, int)}
389      */
390     @Override
getGroupMinVolume(int zoneId, int groupId)391     public int getGroupMinVolume(int zoneId, int groupId) {
392         synchronized (mImplLock) {
393             enforcePermission(Car.PERMISSION_CAR_CONTROL_AUDIO_VOLUME);
394 
395             // For legacy stream type based volume control
396             if (!mUseDynamicRouting) {
397                 return mAudioManager.getStreamMinVolume(
398                         CarAudioDynamicRouting.STREAM_TYPES[groupId]);
399             }
400 
401             CarVolumeGroup group = getCarVolumeGroup(zoneId, groupId);
402             return group.getMinGainIndex();
403         }
404     }
405 
406     /**
407      * @see {@link android.car.media.CarAudioManager#getGroupVolume(int, int)}
408      */
409     @Override
getGroupVolume(int zoneId, int groupId)410     public int getGroupVolume(int zoneId, int groupId) {
411         synchronized (mImplLock) {
412             enforcePermission(Car.PERMISSION_CAR_CONTROL_AUDIO_VOLUME);
413 
414             // For legacy stream type based volume control
415             if (!mUseDynamicRouting) {
416                 return mAudioManager.getStreamVolume(
417                         CarAudioDynamicRouting.STREAM_TYPES[groupId]);
418             }
419 
420             CarVolumeGroup group = getCarVolumeGroup(zoneId, groupId);
421             return group.getCurrentGainIndex();
422         }
423     }
424 
getCarVolumeGroup(int zoneId, int groupId)425     private CarVolumeGroup getCarVolumeGroup(int zoneId, int groupId) {
426         Preconditions.checkNotNull(mCarAudioZones);
427         Preconditions.checkArgumentInRange(zoneId, 0, mCarAudioZones.length - 1,
428                 "zoneId out of range: " + zoneId);
429         return mCarAudioZones[zoneId].getVolumeGroup(groupId);
430     }
431 
setupLegacyVolumeChangedListener()432     private void setupLegacyVolumeChangedListener() {
433         IntentFilter intentFilter = new IntentFilter();
434         intentFilter.addAction(AudioManager.VOLUME_CHANGED_ACTION);
435         intentFilter.addAction(AudioManager.MASTER_MUTE_CHANGED_ACTION);
436         mContext.registerReceiver(mLegacyVolumeChangedReceiver, intentFilter);
437     }
438 
setupDynamicRouting(SparseArray<CarAudioDeviceInfo> busToCarAudioDeviceInfo)439     private void setupDynamicRouting(SparseArray<CarAudioDeviceInfo> busToCarAudioDeviceInfo) {
440         final AudioPolicy.Builder builder = new AudioPolicy.Builder(mContext);
441         builder.setLooper(Looper.getMainLooper());
442 
443         mCarAudioConfigurationPath = getAudioConfigurationPath();
444         if (mCarAudioConfigurationPath != null) {
445             try (InputStream inputStream = new FileInputStream(mCarAudioConfigurationPath)) {
446                 CarAudioZonesHelper zonesHelper = new CarAudioZonesHelper(mContext, inputStream,
447                         busToCarAudioDeviceInfo);
448                 mCarAudioZones = zonesHelper.loadAudioZones();
449             } catch (IOException | XmlPullParserException e) {
450                 throw new RuntimeException("Failed to parse audio zone configuration", e);
451             }
452         } else {
453             // In legacy mode, context -> bus mapping is done by querying IAudioControl HAL.
454             final IAudioControl audioControl = getAudioControl();
455             if (audioControl == null) {
456                 throw new RuntimeException(
457                         "Dynamic routing requested but audioControl HAL not available");
458             }
459             CarAudioZonesHelperLegacy legacyHelper = new CarAudioZonesHelperLegacy(mContext,
460                     R.xml.car_volume_groups, busToCarAudioDeviceInfo, audioControl);
461             mCarAudioZones = legacyHelper.loadAudioZones();
462         }
463         for (CarAudioZone zone : mCarAudioZones) {
464             if (!zone.validateVolumeGroups()) {
465                 throw new RuntimeException("Invalid volume groups configuration");
466             }
467             // Ensure HAL gets our initial value
468             zone.synchronizeCurrentGainIndex();
469             Log.v(CarLog.TAG_AUDIO, "Processed audio zone: " + zone);
470         }
471 
472         // Setup dynamic routing rules by usage
473         final CarAudioDynamicRouting dynamicRouting = new CarAudioDynamicRouting(mCarAudioZones);
474         dynamicRouting.setupAudioDynamicRouting(builder);
475 
476         // Attach the {@link AudioPolicyVolumeCallback}
477         builder.setAudioPolicyVolumeCallback(mAudioPolicyVolumeCallback);
478 
479         if (sUseCarAudioFocus) {
480             // Configure our AudioPolicy to handle focus events.
481             // This gives us the ability to decide which audio focus requests to accept and bypasses
482             // the framework ducking logic.
483             mFocusHandler = new CarZonesAudioFocus(mAudioManager,
484                     mContext.getPackageManager(),
485                     mCarAudioZones);
486             builder.setAudioPolicyFocusListener(mFocusHandler);
487             builder.setIsAudioFocusPolicy(true);
488         }
489 
490         mAudioPolicy = builder.build();
491         if (sUseCarAudioFocus) {
492             // Connect the AudioPolicy and the focus listener
493             mFocusHandler.setOwningPolicy(this, mAudioPolicy);
494         }
495 
496         int r = mAudioManager.registerAudioPolicy(mAudioPolicy);
497         if (r != AudioManager.SUCCESS) {
498             throw new RuntimeException("registerAudioPolicy failed " + r);
499         }
500     }
501 
502     /**
503      * Read from {@link #AUDIO_CONFIGURATION_PATHS} respectively.
504      * @return File path of the first hit in {@link #AUDIO_CONFIGURATION_PATHS}
505      */
506     @Nullable
getAudioConfigurationPath()507     private String getAudioConfigurationPath() {
508         for (String path : AUDIO_CONFIGURATION_PATHS) {
509             File configuration = new File(path);
510             if (configuration.exists()) {
511                 return path;
512             }
513         }
514         return null;
515     }
516 
517     /**
518      * @return Context number for a given audio usage, 0 if the given usage is unrecognized.
519      */
getContextForUsage(int audioUsage)520     int getContextForUsage(int audioUsage) {
521         return CarAudioDynamicRouting.USAGE_TO_CONTEXT.get(audioUsage);
522     }
523 
524     @Override
setFadeTowardFront(float value)525     public void setFadeTowardFront(float value) {
526         synchronized (mImplLock) {
527             enforcePermission(Car.PERMISSION_CAR_CONTROL_AUDIO_VOLUME);
528             final IAudioControl audioControlHal = getAudioControl();
529             if (audioControlHal != null) {
530                 try {
531                     audioControlHal.setFadeTowardFront(value);
532                 } catch (RemoteException e) {
533                     Log.e(CarLog.TAG_AUDIO, "setFadeTowardFront failed", e);
534                 }
535             }
536         }
537     }
538 
539     @Override
setBalanceTowardRight(float value)540     public void setBalanceTowardRight(float value) {
541         synchronized (mImplLock) {
542             enforcePermission(Car.PERMISSION_CAR_CONTROL_AUDIO_VOLUME);
543             final IAudioControl audioControlHal = getAudioControl();
544             if (audioControlHal != null) {
545                 try {
546                     audioControlHal.setBalanceTowardRight(value);
547                 } catch (RemoteException e) {
548                     Log.e(CarLog.TAG_AUDIO, "setBalanceTowardRight failed", e);
549                 }
550             }
551         }
552     }
553 
554     /**
555      * @return Array of accumulated device addresses, empty array if we found nothing
556      */
557     @Override
getExternalSources()558     public @NonNull String[] getExternalSources() {
559         synchronized (mImplLock) {
560             enforcePermission(Car.PERMISSION_CAR_CONTROL_AUDIO_SETTINGS);
561             List<String> sourceAddresses = new ArrayList<>();
562 
563             AudioDeviceInfo[] devices = mAudioManager.getDevices(AudioManager.GET_DEVICES_INPUTS);
564             if (devices.length == 0) {
565                 Log.w(CarLog.TAG_AUDIO, "getExternalSources, no input devices found.");
566             }
567 
568             // Collect the list of non-microphone input ports
569             for (AudioDeviceInfo info : devices) {
570                 switch (info.getType()) {
571                     // TODO:  Can we trim this set down? Especially duplicates like FM vs FM_TUNER?
572                     case AudioDeviceInfo.TYPE_FM:
573                     case AudioDeviceInfo.TYPE_FM_TUNER:
574                     case AudioDeviceInfo.TYPE_TV_TUNER:
575                     case AudioDeviceInfo.TYPE_HDMI:
576                     case AudioDeviceInfo.TYPE_AUX_LINE:
577                     case AudioDeviceInfo.TYPE_LINE_ANALOG:
578                     case AudioDeviceInfo.TYPE_LINE_DIGITAL:
579                     case AudioDeviceInfo.TYPE_USB_ACCESSORY:
580                     case AudioDeviceInfo.TYPE_USB_DEVICE:
581                     case AudioDeviceInfo.TYPE_USB_HEADSET:
582                     case AudioDeviceInfo.TYPE_IP:
583                     case AudioDeviceInfo.TYPE_BUS:
584                         String address = info.getAddress();
585                         if (TextUtils.isEmpty(address)) {
586                             Log.w(CarLog.TAG_AUDIO,
587                                     "Discarded device with empty address, type=" + info.getType());
588                         } else {
589                             sourceAddresses.add(address);
590                         }
591                 }
592             }
593 
594             return sourceAddresses.toArray(new String[0]);
595         }
596     }
597 
598     @Override
createAudioPatch(String sourceAddress, @AudioAttributes.AttributeUsage int usage, int gainInMillibels)599     public CarAudioPatchHandle createAudioPatch(String sourceAddress,
600             @AudioAttributes.AttributeUsage int usage, int gainInMillibels) {
601         synchronized (mImplLock) {
602             enforcePermission(Car.PERMISSION_CAR_CONTROL_AUDIO_SETTINGS);
603             return createAudioPatchLocked(sourceAddress, usage, gainInMillibels);
604         }
605     }
606 
607     @Override
releaseAudioPatch(CarAudioPatchHandle carPatch)608     public void releaseAudioPatch(CarAudioPatchHandle carPatch) {
609         synchronized (mImplLock) {
610             enforcePermission(Car.PERMISSION_CAR_CONTROL_AUDIO_SETTINGS);
611             releaseAudioPatchLocked(carPatch);
612         }
613     }
614 
createAudioPatchLocked(String sourceAddress, @AudioAttributes.AttributeUsage int usage, int gainInMillibels)615     private CarAudioPatchHandle createAudioPatchLocked(String sourceAddress,
616             @AudioAttributes.AttributeUsage int usage, int gainInMillibels) {
617         // Find the named source port
618         AudioDeviceInfo sourcePortInfo = null;
619         AudioDeviceInfo[] deviceInfos = mAudioManager.getDevices(AudioManager.GET_DEVICES_INPUTS);
620         for (AudioDeviceInfo info : deviceInfos) {
621             if (sourceAddress.equals(info.getAddress())) {
622                 // This is the one for which we're looking
623                 sourcePortInfo = info;
624                 break;
625             }
626         }
627         Preconditions.checkNotNull(sourcePortInfo,
628                 "Specified source is not available: " + sourceAddress);
629 
630         // Find the output port associated with the given carUsage
631         AudioDevicePort sinkPort = Preconditions.checkNotNull(getAudioPort(usage),
632                 "Sink not available for usage: " + AudioAttributes.usageToString(usage));
633 
634         // {@link android.media.AudioPort#activeConfig()} is valid for mixer port only,
635         // since audio framework has no clue what's active on the device ports.
636         // Therefore we construct an empty / default configuration here, which the audio HAL
637         // implementation should ignore.
638         AudioPortConfig sinkConfig = sinkPort.buildConfig(0,
639                 AudioFormat.CHANNEL_OUT_DEFAULT, AudioFormat.ENCODING_DEFAULT, null);
640         Log.d(CarLog.TAG_AUDIO, "createAudioPatch sinkConfig: " + sinkConfig);
641 
642         // Configure the source port to match the output port except for a gain adjustment
643         final CarAudioDeviceInfo helper = new CarAudioDeviceInfo(sourcePortInfo);
644         AudioGain audioGain = Preconditions.checkNotNull(helper.getAudioGain(),
645                 "Gain controller not available for source port");
646 
647         // size of gain values is 1 in MODE_JOINT
648         AudioGainConfig audioGainConfig = audioGain.buildConfig(AudioGain.MODE_JOINT,
649                 audioGain.channelMask(), new int[] { gainInMillibels }, 0);
650         // Construct an empty / default configuration excepts gain config here and it's up to the
651         // audio HAL how to interpret this configuration, which the audio HAL
652         // implementation should ignore.
653         AudioPortConfig sourceConfig = sourcePortInfo.getPort().buildConfig(0,
654                 AudioFormat.CHANNEL_IN_DEFAULT, AudioFormat.ENCODING_DEFAULT, audioGainConfig);
655 
656         // Create an audioPatch to connect the two ports
657         AudioPatch[] patch = new AudioPatch[] { null };
658         int result = AudioManager.createAudioPatch(patch,
659                 new AudioPortConfig[] { sourceConfig },
660                 new AudioPortConfig[] { sinkConfig });
661         if (result != AudioManager.SUCCESS) {
662             throw new RuntimeException("createAudioPatch failed with code " + result);
663         }
664 
665         Preconditions.checkNotNull(patch[0],
666                 "createAudioPatch didn't provide expected single handle");
667         Log.d(CarLog.TAG_AUDIO, "Audio patch created: " + patch[0]);
668 
669         // Ensure the initial volume on output device port
670         int groupId = getVolumeGroupIdForUsage(CarAudioManager.PRIMARY_AUDIO_ZONE, usage);
671         setGroupVolume(CarAudioManager.PRIMARY_AUDIO_ZONE, groupId,
672                 getGroupVolume(CarAudioManager.PRIMARY_AUDIO_ZONE, groupId), 0);
673 
674         return new CarAudioPatchHandle(patch[0]);
675     }
676 
releaseAudioPatchLocked(CarAudioPatchHandle carPatch)677     private void releaseAudioPatchLocked(CarAudioPatchHandle carPatch) {
678         // NOTE:  AudioPolicyService::removeNotificationClient will take care of this automatically
679         //        if the client that created a patch quits.
680 
681         // FIXME {@link AudioManager#listAudioPatches(ArrayList)} returns old generation of
682         // audio patches after creation
683         ArrayList<AudioPatch> patches = new ArrayList<>();
684         int result = AudioSystem.listAudioPatches(patches, new int[1]);
685         if (result != AudioManager.SUCCESS) {
686             throw new RuntimeException("listAudioPatches failed with code " + result);
687         }
688 
689         // Look for a patch that matches the provided user side handle
690         for (AudioPatch patch : patches) {
691             if (carPatch.represents(patch)) {
692                 // Found it!
693                 result = AudioManager.releaseAudioPatch(patch);
694                 if (result != AudioManager.SUCCESS) {
695                     throw new RuntimeException("releaseAudioPatch failed with code " + result);
696                 }
697                 return;
698             }
699         }
700 
701         // If we didn't find a match, then something went awry, but it's probably not fatal...
702         Log.e(CarLog.TAG_AUDIO, "releaseAudioPatch found no match for " + carPatch);
703     }
704 
705     @Override
getVolumeGroupCount(int zoneId)706     public int getVolumeGroupCount(int zoneId) {
707         synchronized (mImplLock) {
708             enforcePermission(Car.PERMISSION_CAR_CONTROL_AUDIO_VOLUME);
709             // For legacy stream type based volume control
710             if (!mUseDynamicRouting) return CarAudioDynamicRouting.STREAM_TYPES.length;
711 
712             Preconditions.checkArgumentInRange(zoneId, 0, mCarAudioZones.length - 1,
713                     "zoneId out of range: " + zoneId);
714             return mCarAudioZones[zoneId].getVolumeGroupCount();
715         }
716     }
717 
718     @Override
getVolumeGroupIdForUsage(int zoneId, @AudioAttributes.AttributeUsage int usage)719     public int getVolumeGroupIdForUsage(int zoneId, @AudioAttributes.AttributeUsage int usage) {
720         synchronized (mImplLock) {
721             enforcePermission(Car.PERMISSION_CAR_CONTROL_AUDIO_VOLUME);
722             Preconditions.checkArgumentInRange(zoneId, 0, mCarAudioZones.length - 1,
723                     "zoneId out of range: " + zoneId);
724 
725             CarVolumeGroup[] groups = mCarAudioZones[zoneId].getVolumeGroups();
726             for (int i = 0; i < groups.length; i++) {
727                 int[] contexts = groups[i].getContexts();
728                 for (int context : contexts) {
729                     if (getContextForUsage(usage) == context) {
730                         return i;
731                     }
732                 }
733             }
734             return -1;
735         }
736     }
737 
738     @Override
getUsagesForVolumeGroupId(int zoneId, int groupId)739     public @NonNull int[] getUsagesForVolumeGroupId(int zoneId, int groupId) {
740         synchronized (mImplLock) {
741             enforcePermission(Car.PERMISSION_CAR_CONTROL_AUDIO_VOLUME);
742 
743             // For legacy stream type based volume control
744             if (!mUseDynamicRouting) {
745                 return new int[] { CarAudioDynamicRouting.STREAM_TYPE_USAGES[groupId] };
746             }
747 
748             CarVolumeGroup group = getCarVolumeGroup(zoneId, groupId);
749             Set<Integer> contexts =
750                     Arrays.stream(group.getContexts()).boxed().collect(Collectors.toSet());
751             final List<Integer> usages = new ArrayList<>();
752             for (int i = 0; i < CarAudioDynamicRouting.USAGE_TO_CONTEXT.size(); i++) {
753                 if (contexts.contains(CarAudioDynamicRouting.USAGE_TO_CONTEXT.valueAt(i))) {
754                     usages.add(CarAudioDynamicRouting.USAGE_TO_CONTEXT.keyAt(i));
755                 }
756             }
757             return usages.stream().mapToInt(i -> i).toArray();
758         }
759     }
760 
761     /**
762      * Gets the ids of all available audio zones
763      *
764      * @return Array of available audio zones ids
765      */
766     @Override
getAudioZoneIds()767     public @NonNull int[] getAudioZoneIds() {
768         enforcePermission(Car.PERMISSION_CAR_CONTROL_AUDIO_SETTINGS);
769         synchronized (mImplLock) {
770             return Arrays.stream(mCarAudioZones).mapToInt(CarAudioZone::getId).toArray();
771         }
772     }
773 
774     /**
775      * Gets the audio zone id currently mapped to uid,
776      * defaults to PRIMARY_AUDIO_ZONE if no mapping exist
777      *
778      * @param uid The uid
779      * @return zone id mapped to uid
780      */
781     @Override
getZoneIdForUid(int uid)782     public int getZoneIdForUid(int uid) {
783         enforcePermission(Car.PERMISSION_CAR_CONTROL_AUDIO_SETTINGS);
784         synchronized (mImplLock) {
785             if (!mUidToZoneMap.containsKey(uid)) {
786                 Log.i(CarLog.TAG_AUDIO, "getZoneIdForUid uid "
787                         + uid + " does not have a zone. Defaulting to PRIMARY_AUDIO_ZONE: "
788                         + CarAudioManager.PRIMARY_AUDIO_ZONE);
789 
790                 // Must be added to PRIMARY_AUDIO_ZONE otherwise
791                 // audio may be routed to other devices
792                 // that match the audio criterion (i.e. usage)
793                 setZoneIdForUidNoCheckLocked(CarAudioManager.PRIMARY_AUDIO_ZONE, uid);
794             }
795 
796             return mUidToZoneMap.get(uid);
797         }
798     }
799     /**
800      * Maps the audio zone id to uid
801      *
802      * @param zoneId The audio zone id
803      * @param uid The uid to map
804      * @return true if the device affinities, for devices in zone, are successfully set
805      */
806     @Override
setZoneIdForUid(int zoneId, int uid)807     public boolean setZoneIdForUid(int zoneId, int uid) {
808         enforcePermission(Car.PERMISSION_CAR_CONTROL_AUDIO_SETTINGS);
809         synchronized (mImplLock) {
810             Log.i(CarLog.TAG_AUDIO, "setZoneIdForUid Calling uid "
811                     + uid + " mapped to : "
812                     + zoneId);
813 
814             // Figure out if anything is currently holding focus,
815             // This will change the focus to transient loss while we are switching zones
816             Integer currentZoneId = mUidToZoneMap.get(uid);
817             ArrayList<AudioFocusInfo> currentFocusHoldersForUid = new ArrayList<>();
818             ArrayList<AudioFocusInfo> currentFocusLosersForUid = new ArrayList<>();
819             if (currentZoneId != null) {
820                 currentFocusHoldersForUid = mFocusHandler.getAudioFocusHoldersForUid(uid,
821                         currentZoneId.intValue());
822                 currentFocusLosersForUid = mFocusHandler.getAudioFocusLosersForUid(uid,
823                         currentZoneId.intValue());
824                 if (!currentFocusHoldersForUid.isEmpty() || !currentFocusLosersForUid.isEmpty()) {
825                     // Order matters here: Remove the focus losers first
826                     // then do the current holder to prevent loser from popping up while
827                     // the focus is being remove for current holders
828                     // Remove focus for current focus losers
829                     mFocusHandler.transientlyLoseInFocusInZone(currentFocusLosersForUid,
830                             currentZoneId.intValue());
831                     // Remove focus for current holders
832                     mFocusHandler.transientlyLoseInFocusInZone(currentFocusHoldersForUid,
833                             currentZoneId.intValue());
834                 }
835             }
836 
837             // if the current uid is in the list
838             // remove it from the list
839 
840             if (checkAndRemoveUidLocked(uid)) {
841                 if (setZoneIdForUidNoCheckLocked(zoneId, uid)) {
842                     // Order matters here: Regain focus for
843                     // Previously lost focus holders then regain
844                     // focus for holders that had it last
845                     // Regain focus for the focus losers from previous zone
846                     if (!currentFocusLosersForUid.isEmpty()) {
847                         regainAudioFocusLocked(currentFocusLosersForUid, zoneId);
848                     }
849                     // Regain focus for the focus holders from previous zone
850                     if (!currentFocusHoldersForUid.isEmpty()) {
851                         regainAudioFocusLocked(currentFocusHoldersForUid, zoneId);
852                     }
853                     return true;
854                 }
855             }
856             return false;
857         }
858     }
859 
860     /**
861      * Regain focus for the focus list passed in
862      * @param afiList focus info list to regain
863      * @param zoneId zone id where the focus holder belong
864      */
regainAudioFocusLocked(ArrayList<AudioFocusInfo> afiList, int zoneId)865     void regainAudioFocusLocked(ArrayList<AudioFocusInfo> afiList, int zoneId) {
866         for (AudioFocusInfo info : afiList) {
867             if (mFocusHandler.reevaluateAndRegainAudioFocus(info)
868                     != AudioManager.AUDIOFOCUS_REQUEST_GRANTED) {
869                 Log.i(CarLog.TAG_AUDIO,
870                         " Focus could not be granted for entry "
871                                 + info.getClientId()
872                                 + " uid " + info.getClientUid()
873                                 + " in zone " + zoneId);
874             }
875         }
876     }
877 
878     /**
879      * Removes the current mapping of the uid, focus will be lost in zone
880      * @param uid The uid to remove
881      * return true if all the devices affinities currently
882      *            mapped to uid are successfully removed
883      */
884     @Override
clearZoneIdForUid(int uid)885     public boolean clearZoneIdForUid(int uid) {
886         enforcePermission(Car.PERMISSION_CAR_CONTROL_AUDIO_SETTINGS);
887         synchronized (mImplLock) {
888             return checkAndRemoveUidLocked(uid);
889         }
890     }
891 
892     /**
893      * Sets the zone id for uid
894      * @param zoneId zone id to map to uid
895      * @param uid uid to map
896      * @return true if setting uid device affinity is successful
897      */
setZoneIdForUidNoCheckLocked(int zoneId, int uid)898     private boolean setZoneIdForUidNoCheckLocked(int zoneId, int uid) {
899         Log.d(CarLog.TAG_AUDIO, "setZoneIdForUidNoCheck Calling uid "
900                 + uid + " mapped to " + zoneId);
901         //Request to add uid device affinity
902         if (mAudioPolicy.setUidDeviceAffinity(uid, mCarAudioZones[zoneId].getAudioDeviceInfos())) {
903             // TODO do not store uid mapping here instead use the uid
904             //  device affinity in audio policy when available
905             mUidToZoneMap.put(uid, zoneId);
906             return true;
907         }
908         Log.w(CarLog.TAG_AUDIO, "setZoneIdForUidNoCheck Failed set device affinity for uid "
909                 + uid + " in zone " + zoneId);
910         return false;
911     }
912 
913     /**
914      * Check if uid is attached to a zone and remove it
915      * @param uid unique id to remove
916      * @return true if the uid was successfully removed or mapping was not assigned
917      */
checkAndRemoveUidLocked(int uid)918     private boolean checkAndRemoveUidLocked(int uid) {
919         Integer zoneId = mUidToZoneMap.get(uid);
920         if (zoneId != null) {
921             Log.i(CarLog.TAG_AUDIO, "checkAndRemoveUid removing Calling uid "
922                     + uid + " from zone " + zoneId);
923             if (mAudioPolicy.removeUidDeviceAffinity(uid)) {
924                 // TODO use the uid device affinity in audio policy when available
925                 mUidToZoneMap.remove(uid);
926                 return true;
927             }
928             //failed to remove device affinity from zone devices
929             Log.w(CarLog.TAG_AUDIO,
930                     "checkAndRemoveUid Failed remove device affinity for uid "
931                             + uid + " in zone " +  zoneId);
932             return false;
933         }
934         return true;
935     }
936 
937     /**
938      * Gets the zone id for the display port id.
939      * @param displayPortId display port id to match
940      * @return zone id for the display port id or
941      * CarAudioManager.PRIMARY_AUDIO_ZONE if none are found
942      */
943     @Override
getZoneIdForDisplayPortId(byte displayPortId)944     public int getZoneIdForDisplayPortId(byte displayPortId) {
945         enforcePermission(Car.PERMISSION_CAR_CONTROL_AUDIO_SETTINGS);
946         synchronized (mImplLock) {
947             for (int index = 0; index < mCarAudioZones.length; index++) {
948                 CarAudioZone zone = mCarAudioZones[index];
949                 List<DisplayAddress.Physical> displayAddresses = zone.getPhysicalDisplayAddresses();
950                 if (displayAddresses.stream().anyMatch(displayAddress->
951                         displayAddress.getPort() == displayPortId)) {
952                     return index;
953                 }
954             }
955 
956             // Everything else defaults to primary audio zone
957             return CarAudioManager.PRIMARY_AUDIO_ZONE;
958         }
959     }
960 
961     @Override
registerVolumeCallback(@onNull IBinder binder)962     public void registerVolumeCallback(@NonNull IBinder binder) {
963         synchronized (mImplLock) {
964             enforcePermission(Car.PERMISSION_CAR_CONTROL_AUDIO_VOLUME);
965 
966             mVolumeCallbackContainer.addBinder(ICarVolumeCallback.Stub.asInterface(binder));
967         }
968     }
969 
970     @Override
unregisterVolumeCallback(@onNull IBinder binder)971     public void unregisterVolumeCallback(@NonNull IBinder binder) {
972         synchronized (mImplLock) {
973             enforcePermission(Car.PERMISSION_CAR_CONTROL_AUDIO_VOLUME);
974 
975             mVolumeCallbackContainer.removeBinder(ICarVolumeCallback.Stub.asInterface(binder));
976         }
977     }
978 
enforcePermission(String permissionName)979     private void enforcePermission(String permissionName) {
980         if (mContext.checkCallingOrSelfPermission(permissionName)
981                 != PackageManager.PERMISSION_GRANTED) {
982             throw new SecurityException("requires permission " + permissionName);
983         }
984     }
985 
986     /**
987      * @return {@link AudioDevicePort} that handles the given car audio usage.
988      * Multiple usages may share one {@link AudioDevicePort}
989      */
getAudioPort(@udioAttributes.AttributeUsage int usage)990     private @Nullable AudioDevicePort getAudioPort(@AudioAttributes.AttributeUsage int usage) {
991         int zoneId = CarAudioManager.PRIMARY_AUDIO_ZONE;
992         final int groupId = getVolumeGroupIdForUsage(zoneId, usage);
993         final CarVolumeGroup group = Preconditions.checkNotNull(
994                 mCarAudioZones[zoneId].getVolumeGroup(groupId),
995                 "Can not find CarVolumeGroup by usage: "
996                         + AudioAttributes.usageToString(usage));
997         return group.getAudioDevicePortForContext(getContextForUsage(usage));
998     }
999 
1000     /**
1001      * @return The suggested {@link AudioAttributes} usage to which the volume key events apply
1002      */
getSuggestedAudioUsage()1003     private @AudioAttributes.AttributeUsage int getSuggestedAudioUsage() {
1004         int callState = mTelephonyManager.getCallState();
1005         if (callState == TelephonyManager.CALL_STATE_RINGING) {
1006             return AudioAttributes.USAGE_NOTIFICATION_RINGTONE;
1007         } else if (callState == TelephonyManager.CALL_STATE_OFFHOOK) {
1008             return AudioAttributes.USAGE_VOICE_COMMUNICATION;
1009         } else {
1010             List<AudioPlaybackConfiguration> playbacks = mAudioManager
1011                     .getActivePlaybackConfigurations()
1012                     .stream()
1013                     .filter(AudioPlaybackConfiguration::isActive)
1014                     .collect(Collectors.toList());
1015             if (!playbacks.isEmpty()) {
1016                 // Get audio usage from active playbacks if there is any, last one if multiple
1017                 return playbacks.get(playbacks.size() - 1).getAudioAttributes().getUsage();
1018             } else {
1019                 // TODO(b/72695246): Otherwise, get audio usage from foreground activity/window
1020                 return CarAudioDynamicRouting.DEFAULT_AUDIO_USAGE;
1021             }
1022         }
1023     }
1024 
1025     /**
1026      * Gets volume group by a given legacy stream type
1027      * @param streamType Legacy stream type such as {@link AudioManager#STREAM_MUSIC}
1028      * @return volume group id mapped from stream type
1029      */
getVolumeGroupIdForStreamType(int streamType)1030     private int getVolumeGroupIdForStreamType(int streamType) {
1031         int groupId = -1;
1032         for (int i = 0; i < CarAudioDynamicRouting.STREAM_TYPES.length; i++) {
1033             if (streamType == CarAudioDynamicRouting.STREAM_TYPES[i]) {
1034                 groupId = i;
1035                 break;
1036             }
1037         }
1038         return groupId;
1039     }
1040 
1041     @Nullable
getAudioControl()1042     private static IAudioControl getAudioControl() {
1043         try {
1044             return IAudioControl.getService();
1045         } catch (RemoteException e) {
1046             Log.e(CarLog.TAG_AUDIO, "Failed to get IAudioControl service", e);
1047         } catch (NoSuchElementException e) {
1048             Log.e(CarLog.TAG_AUDIO, "IAudioControl service not registered yet");
1049         }
1050         return null;
1051     }
1052 }
1053