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