blob: b19fda5b844c2a29a888b9a3e98c6db66d378bb4 [file] [log] [blame]
/*
* Copyright (C) 2015 The Android Open Source Project
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package com.android.car.audio;
import android.annotation.NonNull;
import android.annotation.Nullable;
import android.car.Car;
import android.car.media.CarAudioManager;
import android.car.media.CarAudioPatchHandle;
import android.car.media.ICarAudio;
import android.car.media.ICarVolumeCallback;
import android.content.BroadcastReceiver;
import android.content.Context;
import android.content.Intent;
import android.content.IntentFilter;
import android.content.pm.PackageManager;
import android.hardware.automotive.audiocontrol.V1_0.IAudioControl;
import android.media.AudioAttributes;
import android.media.AudioDeviceInfo;
import android.media.AudioDevicePort;
import android.media.AudioFormat;
import android.media.AudioGain;
import android.media.AudioGainConfig;
import android.media.AudioManager;
import android.media.AudioPatch;
import android.media.AudioPlaybackConfiguration;
import android.media.AudioPortConfig;
import android.media.AudioSystem;
import android.media.audiopolicy.AudioPolicy;
import android.os.IBinder;
import android.os.Looper;
import android.os.RemoteException;
import android.provider.Settings;
import android.telephony.TelephonyManager;
import android.text.TextUtils;
import android.util.Log;
import android.util.SparseArray;
import android.view.KeyEvent;
import com.android.car.BinderInterfaceContainer;
import com.android.car.CarLog;
import com.android.car.CarServiceBase;
import com.android.car.R;
import com.android.internal.util.Preconditions;
import java.io.File;
import java.io.PrintWriter;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.List;
import java.util.NoSuchElementException;
import java.util.Set;
import java.util.stream.Collectors;
/**
* Service responsible for interaction with car's audio system.
*/
public class CarAudioService extends ICarAudio.Stub implements CarServiceBase {
// Turning this off will result in falling back to the default focus policy of Android
// (which boils down to "grant if not in a phone call, else deny").
// Aside from the obvious effect of ignoring the logic in CarAudioFocus, this will also
// result in the framework taking over responsibility for ducking in TRANSIENT_LOSS cases.
// Search for "DUCK_VSHAPE" in PLaybackActivityMonitor.java to see where this happens.
private static boolean sUseCarAudioFocus = true;
// Key to persist master mute state in system settings
private static final String VOLUME_SETTINGS_KEY_MASTER_MUTE = "android.car.MASTER_MUTE";
// The trailing slash forms a directory-liked hierarchy and
// allows listening for both GROUP/MEDIA and GROUP/NAVIGATION.
private static final String VOLUME_SETTINGS_KEY_FOR_GROUP_PREFIX = "android.car.VOLUME_GROUP/";
// CarAudioService reads configuration from the following paths respectively.
// If the first one is found, all others are ignored.
// If no one is found, it fallbacks to car_volume_groups.xml resource file.
private static final String[] AUDIO_CONFIGURATION_PATHS = new String[] {
"/vendor/etc/car_audio_configuration.xml",
"/system/etc/car_audio_configuration.xml"
};
/**
* Gets the key to persist volume for a volume group in settings
*
* @param zoneId The audio zone id
* @param groupId The volume group id
* @return Key to persist volume index for volume group in system settings
*/
static String getVolumeSettingsKeyForGroup(int zoneId, int groupId) {
final int maskedGroupId = (zoneId << 8) + groupId;
return VOLUME_SETTINGS_KEY_FOR_GROUP_PREFIX + maskedGroupId;
}
private final Object mImplLock = new Object();
private final Context mContext;
private final TelephonyManager mTelephonyManager;
private final AudioManager mAudioManager;
private final boolean mUseDynamicRouting;
private final boolean mPersistMasterMuteState;
private final AudioPolicy.AudioPolicyVolumeCallback mAudioPolicyVolumeCallback =
new AudioPolicy.AudioPolicyVolumeCallback() {
@Override
public void onVolumeAdjustment(int adjustment) {
final int usage = getSuggestedAudioUsage();
Log.v(CarLog.TAG_AUDIO,
"onVolumeAdjustment: " + AudioManager.adjustToString(adjustment)
+ " suggested usage: " + AudioAttributes.usageToString(usage));
// TODO: Pass zone id into this callback.
final int zoneId = CarAudioManager.PRIMARY_AUDIO_ZONE;
final int groupId = getVolumeGroupIdForUsage(zoneId, usage);
final int currentVolume = getGroupVolume(zoneId, groupId);
final int flags = AudioManager.FLAG_FROM_KEY | AudioManager.FLAG_SHOW_UI;
switch (adjustment) {
case AudioManager.ADJUST_LOWER:
int minValue = Math.max(currentVolume - 1, getGroupMinVolume(zoneId, groupId));
setGroupVolume(zoneId, groupId, minValue , flags);
break;
case AudioManager.ADJUST_RAISE:
int maxValue = Math.min(currentVolume + 1, getGroupMaxVolume(zoneId, groupId));
setGroupVolume(zoneId, groupId, maxValue, flags);
break;
case AudioManager.ADJUST_MUTE:
setMasterMute(true, flags);
callbackMasterMuteChange(zoneId, flags);
break;
case AudioManager.ADJUST_UNMUTE:
setMasterMute(false, flags);
callbackMasterMuteChange(zoneId, flags);
break;
case AudioManager.ADJUST_TOGGLE_MUTE:
setMasterMute(!mAudioManager.isMasterMute(), flags);
callbackMasterMuteChange(zoneId, flags);
break;
case AudioManager.ADJUST_SAME:
default:
break;
}
}
};
private final BinderInterfaceContainer<ICarVolumeCallback> mVolumeCallbackContainer =
new BinderInterfaceContainer<>();
/**
* Simulates {@link ICarVolumeCallback} when it's running in legacy mode.
* This receiver assumes the intent is sent to {@link CarAudioManager#PRIMARY_AUDIO_ZONE}.
*/
private final BroadcastReceiver mLegacyVolumeChangedReceiver = new BroadcastReceiver() {
@Override
public void onReceive(Context context, Intent intent) {
final int zoneId = CarAudioManager.PRIMARY_AUDIO_ZONE;
switch (intent.getAction()) {
case AudioManager.VOLUME_CHANGED_ACTION:
int streamType = intent.getIntExtra(AudioManager.EXTRA_VOLUME_STREAM_TYPE, -1);
int groupId = getVolumeGroupIdForStreamType(streamType);
if (groupId == -1) {
Log.w(CarLog.TAG_AUDIO, "Unknown stream type: " + streamType);
} else {
callbackGroupVolumeChange(zoneId, groupId, 0);
}
break;
case AudioManager.MASTER_MUTE_CHANGED_ACTION:
callbackMasterMuteChange(zoneId, 0);
break;
}
}
};
private AudioPolicy mAudioPolicy;
private CarAudioFocus mFocusHandler;
private String mCarAudioConfigurationPath;
private CarAudioZone[] mCarAudioZones;
public CarAudioService(Context context) {
mContext = context;
mTelephonyManager = (TelephonyManager) mContext.getSystemService(Context.TELEPHONY_SERVICE);
mAudioManager = (AudioManager) mContext.getSystemService(Context.AUDIO_SERVICE);
mUseDynamicRouting = mContext.getResources().getBoolean(R.bool.audioUseDynamicRouting);
mPersistMasterMuteState = mContext.getResources().getBoolean(
R.bool.audioPersistMasterMuteState);
}
/**
* Dynamic routing and volume groups are set only if
* {@link #mUseDynamicRouting} is {@code true}. Otherwise, this service runs in legacy mode.
*/
@Override
public void init() {
synchronized (mImplLock) {
if (mUseDynamicRouting) {
// Enumerate all output bus device ports
AudioDeviceInfo[] deviceInfos = mAudioManager.getDevices(
AudioManager.GET_DEVICES_OUTPUTS);
if (deviceInfos.length == 0) {
Log.e(CarLog.TAG_AUDIO, "No output device available, ignore");
return;
}
SparseArray<CarAudioDeviceInfo> busToCarAudioDeviceInfo = new SparseArray<>();
for (AudioDeviceInfo info : deviceInfos) {
Log.v(CarLog.TAG_AUDIO, String.format("output id=%d address=%s type=%s",
info.getId(), info.getAddress(), info.getType()));
if (info.getType() == AudioDeviceInfo.TYPE_BUS) {
final CarAudioDeviceInfo carInfo = new CarAudioDeviceInfo(info);
// See also the audio_policy_configuration.xml,
// the bus number should be no less than zero.
if (carInfo.getBusNumber() >= 0) {
busToCarAudioDeviceInfo.put(carInfo.getBusNumber(), carInfo);
Log.i(CarLog.TAG_AUDIO, "Valid bus found " + carInfo);
}
}
}
setupDynamicRouting(busToCarAudioDeviceInfo);
} else {
Log.i(CarLog.TAG_AUDIO, "Audio dynamic routing not enabled, run in legacy mode");
setupLegacyVolumeChangedListener();
}
// Restore master mute state if applicable
if (mPersistMasterMuteState) {
boolean storedMasterMute = Settings.Global.getInt(mContext.getContentResolver(),
VOLUME_SETTINGS_KEY_MASTER_MUTE, 0) != 0;
setMasterMute(storedMasterMute, 0);
}
}
}
@Override
public void release() {
synchronized (mImplLock) {
if (mUseDynamicRouting) {
if (mAudioPolicy != null) {
mAudioManager.unregisterAudioPolicyAsync(mAudioPolicy);
mAudioPolicy = null;
mFocusHandler.setOwningPolicy(null, null);
mFocusHandler = null;
}
} else {
mContext.unregisterReceiver(mLegacyVolumeChangedReceiver);
}
mVolumeCallbackContainer.clear();
}
}
@Override
public void dump(PrintWriter writer) {
writer.println("*CarAudioService*");
writer.println("\tRun in legacy mode? " + (!mUseDynamicRouting));
writer.println("\tPersist master mute state? " + mPersistMasterMuteState);
writer.println("\tMaster muted? " + mAudioManager.isMasterMute());
if (mCarAudioConfigurationPath != null) {
writer.println("\tCar audio configuration path: " + mCarAudioConfigurationPath);
}
// Empty line for comfortable reading
writer.println();
if (mUseDynamicRouting) {
for (CarAudioZone zone : mCarAudioZones) {
zone.dump("\t", writer);
}
}
}
@Override
public boolean isDynamicRoutingEnabled() {
return mUseDynamicRouting;
}
/**
* @see {@link android.car.media.CarAudioManager#setGroupVolume(int, int, int, int)}
*/
@Override
public void setGroupVolume(int zoneId, int groupId, int index, int flags) {
synchronized (mImplLock) {
enforcePermission(Car.PERMISSION_CAR_CONTROL_AUDIO_VOLUME);
callbackGroupVolumeChange(zoneId, groupId, flags);
// For legacy stream type based volume control
if (!mUseDynamicRouting) {
mAudioManager.setStreamVolume(
CarAudioDynamicRouting.STREAM_TYPES[groupId], index, flags);
return;
}
CarVolumeGroup group = getCarVolumeGroup(zoneId, groupId);
group.setCurrentGainIndex(index);
}
}
private void callbackGroupVolumeChange(int zoneId, int groupId, int flags) {
for (BinderInterfaceContainer.BinderInterface<ICarVolumeCallback> callback :
mVolumeCallbackContainer.getInterfaces()) {
try {
callback.binderInterface.onGroupVolumeChanged(zoneId, groupId, flags);
} catch (RemoteException e) {
Log.e(CarLog.TAG_AUDIO, "Failed to callback onGroupVolumeChanged", e);
}
}
}
private void setMasterMute(boolean mute, int flags) {
mAudioManager.setMasterMute(mute, flags);
// When the master mute is turned ON, we want the playing app to get a "pause" command.
// When the volume is unmuted, we want to resume playback.
int keycode = mute ? KeyEvent.KEYCODE_MEDIA_PAUSE : KeyEvent.KEYCODE_MEDIA_PLAY;
mAudioManager.dispatchMediaKeyEvent(new KeyEvent(KeyEvent.ACTION_DOWN, keycode));
mAudioManager.dispatchMediaKeyEvent(new KeyEvent(KeyEvent.ACTION_UP, keycode));
}
private void callbackMasterMuteChange(int zoneId, int flags) {
for (BinderInterfaceContainer.BinderInterface<ICarVolumeCallback> callback :
mVolumeCallbackContainer.getInterfaces()) {
try {
callback.binderInterface.onMasterMuteChanged(zoneId, flags);
} catch (RemoteException e) {
Log.e(CarLog.TAG_AUDIO, "Failed to callback onMasterMuteChanged", e);
}
}
// Persists master mute state if applicable
if (mPersistMasterMuteState) {
Settings.Global.putInt(mContext.getContentResolver(),
VOLUME_SETTINGS_KEY_MASTER_MUTE,
mAudioManager.isMasterMute() ? 1 : 0);
}
}
/**
* @see {@link android.car.media.CarAudioManager#getGroupMaxVolume(int, int)}
*/
@Override
public int getGroupMaxVolume(int zoneId, int groupId) {
synchronized (mImplLock) {
enforcePermission(Car.PERMISSION_CAR_CONTROL_AUDIO_VOLUME);
// For legacy stream type based volume control
if (!mUseDynamicRouting) {
return mAudioManager.getStreamMaxVolume(
CarAudioDynamicRouting.STREAM_TYPES[groupId]);
}
CarVolumeGroup group = getCarVolumeGroup(zoneId, groupId);
return group.getMaxGainIndex();
}
}
/**
* @see {@link android.car.media.CarAudioManager#getGroupMinVolume(int, int)}
*/
@Override
public int getGroupMinVolume(int zoneId, int groupId) {
synchronized (mImplLock) {
enforcePermission(Car.PERMISSION_CAR_CONTROL_AUDIO_VOLUME);
// For legacy stream type based volume control
if (!mUseDynamicRouting) {
return mAudioManager.getStreamMinVolume(
CarAudioDynamicRouting.STREAM_TYPES[groupId]);
}
CarVolumeGroup group = getCarVolumeGroup(zoneId, groupId);
return group.getMinGainIndex();
}
}
/**
* @see {@link android.car.media.CarAudioManager#getGroupVolume(int, int)}
*/
@Override
public int getGroupVolume(int zoneId, int groupId) {
synchronized (mImplLock) {
enforcePermission(Car.PERMISSION_CAR_CONTROL_AUDIO_VOLUME);
// For legacy stream type based volume control
if (!mUseDynamicRouting) {
return mAudioManager.getStreamVolume(
CarAudioDynamicRouting.STREAM_TYPES[groupId]);
}
CarVolumeGroup group = getCarVolumeGroup(zoneId, groupId);
return group.getCurrentGainIndex();
}
}
private CarVolumeGroup getCarVolumeGroup(int zoneId, int groupId) {
Preconditions.checkNotNull(mCarAudioZones);
Preconditions.checkArgumentInRange(zoneId, 0, mCarAudioZones.length - 1,
"zoneId out of range: " + zoneId);
return mCarAudioZones[zoneId].getVolumeGroup(groupId);
}
private void setupLegacyVolumeChangedListener() {
IntentFilter intentFilter = new IntentFilter();
intentFilter.addAction(AudioManager.VOLUME_CHANGED_ACTION);
intentFilter.addAction(AudioManager.MASTER_MUTE_CHANGED_ACTION);
mContext.registerReceiver(mLegacyVolumeChangedReceiver, intentFilter);
}
private void setupDynamicRouting(SparseArray<CarAudioDeviceInfo> busToCarAudioDeviceInfo) {
final AudioPolicy.Builder builder = new AudioPolicy.Builder(mContext);
builder.setLooper(Looper.getMainLooper());
final CarAudioZonesLoader zonesLoader;
mCarAudioConfigurationPath = getAudioConfigurationPath();
if (mCarAudioConfigurationPath != null) {
zonesLoader = new CarAudioZonesHelper(mContext, mCarAudioConfigurationPath,
busToCarAudioDeviceInfo);
} else {
// In legacy mode, context -> bus mapping is done by querying IAudioControl HAL.
final IAudioControl audioControl = getAudioControl();
if (audioControl == null) {
throw new RuntimeException(
"Dynamic routing requested but audioControl HAL not available");
}
zonesLoader = new CarAudioZonesHelperLegacy(mContext, R.xml.car_volume_groups,
busToCarAudioDeviceInfo, audioControl);
}
mCarAudioZones = zonesLoader.loadAudioZones();
for (CarAudioZone zone : mCarAudioZones) {
if (!zone.validateVolumeGroups()) {
throw new RuntimeException("Invalid volume groups configuration");
}
// Ensure HAL gets our initial value
zone.synchronizeCurrentGainIndex();
Log.v(CarLog.TAG_AUDIO, "Processed audio zone: " + zone);
}
// Setup dynamic routing rules by usage
final CarAudioDynamicRouting dynamicRouting = new CarAudioDynamicRouting(mCarAudioZones);
dynamicRouting.setupAudioDynamicRouting(builder);
// Attach the {@link AudioPolicyVolumeCallback}
builder.setAudioPolicyVolumeCallback(mAudioPolicyVolumeCallback);
if (sUseCarAudioFocus) {
// Configure our AudioPolicy to handle focus events.
// This gives us the ability to decide which audio focus requests to accept and bypasses
// the framework ducking logic.
mFocusHandler = new CarAudioFocus(mAudioManager);
builder.setAudioPolicyFocusListener(mFocusHandler);
builder.setIsAudioFocusPolicy(true);
}
mAudioPolicy = builder.build();
if (sUseCarAudioFocus) {
// Connect the AudioPolicy and the focus listener
mFocusHandler.setOwningPolicy(this, mAudioPolicy);
}
int r = mAudioManager.registerAudioPolicy(mAudioPolicy);
if (r != AudioManager.SUCCESS) {
throw new RuntimeException("registerAudioPolicy failed " + r);
}
}
/**
* Read from {@link #AUDIO_CONFIGURATION_PATHS} respectively.
* @return File path of the first hit in {@link #AUDIO_CONFIGURATION_PATHS}
*/
@Nullable
private String getAudioConfigurationPath() {
for (String path : AUDIO_CONFIGURATION_PATHS) {
File configuration = new File(path);
if (configuration.exists()) {
return path;
}
}
return null;
}
/**
* @return Context number for a given audio usage, 0 if the given usage is unrecognized.
*/
int getContextForUsage(int audioUsage) {
return CarAudioDynamicRouting.USAGE_TO_CONTEXT.get(audioUsage);
}
@Override
public void setFadeTowardFront(float value) {
synchronized (mImplLock) {
enforcePermission(Car.PERMISSION_CAR_CONTROL_AUDIO_VOLUME);
final IAudioControl audioControlHal = getAudioControl();
if (audioControlHal != null) {
try {
audioControlHal.setFadeTowardFront(value);
} catch (RemoteException e) {
Log.e(CarLog.TAG_AUDIO, "setFadeTowardFront failed", e);
}
}
}
}
@Override
public void setBalanceTowardRight(float value) {
synchronized (mImplLock) {
enforcePermission(Car.PERMISSION_CAR_CONTROL_AUDIO_VOLUME);
final IAudioControl audioControlHal = getAudioControl();
if (audioControlHal != null) {
try {
audioControlHal.setBalanceTowardRight(value);
} catch (RemoteException e) {
Log.e(CarLog.TAG_AUDIO, "setBalanceTowardRight failed", e);
}
}
}
}
/**
* @return Array of accumulated device addresses, empty array if we found nothing
*/
@Override
public @NonNull String[] getExternalSources() {
synchronized (mImplLock) {
enforcePermission(Car.PERMISSION_CAR_CONTROL_AUDIO_SETTINGS);
List<String> sourceAddresses = new ArrayList<>();
AudioDeviceInfo[] devices = mAudioManager.getDevices(AudioManager.GET_DEVICES_INPUTS);
if (devices.length == 0) {
Log.w(CarLog.TAG_AUDIO, "getExternalSources, no input devices found.");
}
// Collect the list of non-microphone input ports
for (AudioDeviceInfo info : devices) {
switch (info.getType()) {
// TODO: Can we trim this set down? Especially duplicates like FM vs FM_TUNER?
case AudioDeviceInfo.TYPE_FM:
case AudioDeviceInfo.TYPE_FM_TUNER:
case AudioDeviceInfo.TYPE_TV_TUNER:
case AudioDeviceInfo.TYPE_HDMI:
case AudioDeviceInfo.TYPE_AUX_LINE:
case AudioDeviceInfo.TYPE_LINE_ANALOG:
case AudioDeviceInfo.TYPE_LINE_DIGITAL:
case AudioDeviceInfo.TYPE_USB_ACCESSORY:
case AudioDeviceInfo.TYPE_USB_DEVICE:
case AudioDeviceInfo.TYPE_USB_HEADSET:
case AudioDeviceInfo.TYPE_IP:
case AudioDeviceInfo.TYPE_BUS:
String address = info.getAddress();
if (TextUtils.isEmpty(address)) {
Log.w(CarLog.TAG_AUDIO,
"Discarded device with empty address, type=" + info.getType());
} else {
sourceAddresses.add(address);
}
}
}
return sourceAddresses.toArray(new String[0]);
}
}
@Override
public CarAudioPatchHandle createAudioPatch(String sourceAddress,
@AudioAttributes.AttributeUsage int usage, int gainInMillibels) {
synchronized (mImplLock) {
enforcePermission(Car.PERMISSION_CAR_CONTROL_AUDIO_SETTINGS);
return createAudioPatchLocked(sourceAddress, usage, gainInMillibels);
}
}
@Override
public void releaseAudioPatch(CarAudioPatchHandle carPatch) {
synchronized (mImplLock) {
enforcePermission(Car.PERMISSION_CAR_CONTROL_AUDIO_SETTINGS);
releaseAudioPatchLocked(carPatch);
}
}
private CarAudioPatchHandle createAudioPatchLocked(String sourceAddress,
@AudioAttributes.AttributeUsage int usage, int gainInMillibels) {
// Find the named source port
AudioDeviceInfo sourcePortInfo = null;
AudioDeviceInfo[] deviceInfos = mAudioManager.getDevices(AudioManager.GET_DEVICES_INPUTS);
for (AudioDeviceInfo info : deviceInfos) {
if (sourceAddress.equals(info.getAddress())) {
// This is the one for which we're looking
sourcePortInfo = info;
break;
}
}
Preconditions.checkNotNull(sourcePortInfo,
"Specified source is not available: " + sourceAddress);
// Find the output port associated with the given carUsage
AudioDevicePort sinkPort = Preconditions.checkNotNull(getAudioPort(usage),
"Sink not available for usage: " + AudioAttributes.usageToString(usage));
// {@link android.media.AudioPort#activeConfig()} is valid for mixer port only,
// since audio framework has no clue what's active on the device ports.
// Therefore we construct an empty / default configuration here, which the audio HAL
// implementation should ignore.
AudioPortConfig sinkConfig = sinkPort.buildConfig(0,
AudioFormat.CHANNEL_OUT_DEFAULT, AudioFormat.ENCODING_DEFAULT, null);
Log.d(CarLog.TAG_AUDIO, "createAudioPatch sinkConfig: " + sinkConfig);
// Configure the source port to match the output port except for a gain adjustment
final CarAudioDeviceInfo helper = new CarAudioDeviceInfo(sourcePortInfo);
AudioGain audioGain = Preconditions.checkNotNull(helper.getAudioGain(),
"Gain controller not available for source port");
// size of gain values is 1 in MODE_JOINT
AudioGainConfig audioGainConfig = audioGain.buildConfig(AudioGain.MODE_JOINT,
audioGain.channelMask(), new int[] { gainInMillibels }, 0);
// Construct an empty / default configuration excepts gain config here and it's up to the
// audio HAL how to interpret this configuration, which the audio HAL
// implementation should ignore.
AudioPortConfig sourceConfig = sourcePortInfo.getPort().buildConfig(0,
AudioFormat.CHANNEL_IN_DEFAULT, AudioFormat.ENCODING_DEFAULT, audioGainConfig);
// Create an audioPatch to connect the two ports
AudioPatch[] patch = new AudioPatch[] { null };
int result = AudioManager.createAudioPatch(patch,
new AudioPortConfig[] { sourceConfig },
new AudioPortConfig[] { sinkConfig });
if (result != AudioManager.SUCCESS) {
throw new RuntimeException("createAudioPatch failed with code " + result);
}
Preconditions.checkNotNull(patch[0],
"createAudioPatch didn't provide expected single handle");
Log.d(CarLog.TAG_AUDIO, "Audio patch created: " + patch[0]);
// Ensure the initial volume on output device port
int groupId = getVolumeGroupIdForUsage(CarAudioManager.PRIMARY_AUDIO_ZONE, usage);
setGroupVolume(CarAudioManager.PRIMARY_AUDIO_ZONE, groupId,
getGroupVolume(CarAudioManager.PRIMARY_AUDIO_ZONE, groupId), 0);
return new CarAudioPatchHandle(patch[0]);
}
private void releaseAudioPatchLocked(CarAudioPatchHandle carPatch) {
// NOTE: AudioPolicyService::removeNotificationClient will take care of this automatically
// if the client that created a patch quits.
// FIXME {@link AudioManager#listAudioPatches(ArrayList)} returns old generation of
// audio patches after creation
ArrayList<AudioPatch> patches = new ArrayList<>();
int result = AudioSystem.listAudioPatches(patches, new int[1]);
if (result != AudioManager.SUCCESS) {
throw new RuntimeException("listAudioPatches failed with code " + result);
}
// Look for a patch that matches the provided user side handle
for (AudioPatch patch : patches) {
if (carPatch.represents(patch)) {
// Found it!
result = AudioManager.releaseAudioPatch(patch);
if (result != AudioManager.SUCCESS) {
throw new RuntimeException("releaseAudioPatch failed with code " + result);
}
return;
}
}
// If we didn't find a match, then something went awry, but it's probably not fatal...
Log.e(CarLog.TAG_AUDIO, "releaseAudioPatch found no match for " + carPatch);
}
@Override
public int getVolumeGroupCount(int zoneId) {
synchronized (mImplLock) {
enforcePermission(Car.PERMISSION_CAR_CONTROL_AUDIO_VOLUME);
// For legacy stream type based volume control
if (!mUseDynamicRouting) return CarAudioDynamicRouting.STREAM_TYPES.length;
Preconditions.checkArgumentInRange(zoneId, 0, mCarAudioZones.length - 1,
"zoneId out of range: " + zoneId);
return mCarAudioZones[zoneId].getVolumeGroupCount();
}
}
@Override
public int getVolumeGroupIdForUsage(int zoneId, @AudioAttributes.AttributeUsage int usage) {
synchronized (mImplLock) {
enforcePermission(Car.PERMISSION_CAR_CONTROL_AUDIO_VOLUME);
Preconditions.checkArgumentInRange(zoneId, 0, mCarAudioZones.length - 1,
"zoneId out of range: " + zoneId);
CarVolumeGroup[] groups = mCarAudioZones[zoneId].getVolumeGroups();
for (int i = 0; i < groups.length; i++) {
int[] contexts = groups[i].getContexts();
for (int context : contexts) {
if (getContextForUsage(usage) == context) {
return i;
}
}
}
return -1;
}
}
@Override
public @NonNull int[] getUsagesForVolumeGroupId(int zoneId, int groupId) {
synchronized (mImplLock) {
enforcePermission(Car.PERMISSION_CAR_CONTROL_AUDIO_VOLUME);
// For legacy stream type based volume control
if (!mUseDynamicRouting) {
return new int[] { CarAudioDynamicRouting.STREAM_TYPE_USAGES[groupId] };
}
CarVolumeGroup group = getCarVolumeGroup(zoneId, groupId);
Set<Integer> contexts =
Arrays.stream(group.getContexts()).boxed().collect(Collectors.toSet());
final List<Integer> usages = new ArrayList<>();
for (int i = 0; i < CarAudioDynamicRouting.USAGE_TO_CONTEXT.size(); i++) {
if (contexts.contains(CarAudioDynamicRouting.USAGE_TO_CONTEXT.valueAt(i))) {
usages.add(CarAudioDynamicRouting.USAGE_TO_CONTEXT.keyAt(i));
}
}
return usages.stream().mapToInt(i -> i).toArray();
}
}
@Override
public void registerVolumeCallback(@NonNull IBinder binder) {
synchronized (mImplLock) {
enforcePermission(Car.PERMISSION_CAR_CONTROL_AUDIO_VOLUME);
mVolumeCallbackContainer.addBinder(ICarVolumeCallback.Stub.asInterface(binder));
}
}
@Override
public void unregisterVolumeCallback(@NonNull IBinder binder) {
synchronized (mImplLock) {
enforcePermission(Car.PERMISSION_CAR_CONTROL_AUDIO_VOLUME);
mVolumeCallbackContainer.removeBinder(ICarVolumeCallback.Stub.asInterface(binder));
}
}
private void enforcePermission(String permissionName) {
if (mContext.checkCallingOrSelfPermission(permissionName)
!= PackageManager.PERMISSION_GRANTED) {
throw new SecurityException("requires permission " + permissionName);
}
}
/**
* @return {@link AudioDevicePort} that handles the given car audio usage.
* Multiple usages may share one {@link AudioDevicePort}
*/
private @Nullable AudioDevicePort getAudioPort(@AudioAttributes.AttributeUsage int usage) {
int zoneId = CarAudioManager.PRIMARY_AUDIO_ZONE;
final int groupId = getVolumeGroupIdForUsage(zoneId, usage);
final CarVolumeGroup group = Preconditions.checkNotNull(
mCarAudioZones[zoneId].getVolumeGroup(groupId),
"Can not find CarVolumeGroup by usage: "
+ AudioAttributes.usageToString(usage));
return group.getAudioDevicePortForContext(getContextForUsage(usage));
}
/**
* @return The suggested {@link AudioAttributes} usage to which the volume key events apply
*/
private @AudioAttributes.AttributeUsage int getSuggestedAudioUsage() {
int callState = mTelephonyManager.getCallState();
if (callState == TelephonyManager.CALL_STATE_RINGING) {
return AudioAttributes.USAGE_NOTIFICATION_RINGTONE;
} else if (callState == TelephonyManager.CALL_STATE_OFFHOOK) {
return AudioAttributes.USAGE_VOICE_COMMUNICATION;
} else {
List<AudioPlaybackConfiguration> playbacks = mAudioManager
.getActivePlaybackConfigurations()
.stream()
.filter(AudioPlaybackConfiguration::isActive)
.collect(Collectors.toList());
if (!playbacks.isEmpty()) {
// Get audio usage from active playbacks if there is any, last one if multiple
return playbacks.get(playbacks.size() - 1).getAudioAttributes().getUsage();
} else {
// TODO(b/72695246): Otherwise, get audio usage from foreground activity/window
return CarAudioDynamicRouting.DEFAULT_AUDIO_USAGE;
}
}
}
/**
* Gets volume group by a given legacy stream type
* @param streamType Legacy stream type such as {@link AudioManager#STREAM_MUSIC}
* @return volume group id mapped from stream type
*/
private int getVolumeGroupIdForStreamType(int streamType) {
int groupId = -1;
for (int i = 0; i < CarAudioDynamicRouting.STREAM_TYPES.length; i++) {
if (streamType == CarAudioDynamicRouting.STREAM_TYPES[i]) {
groupId = i;
break;
}
}
return groupId;
}
@Nullable
private static IAudioControl getAudioControl() {
try {
return IAudioControl.getService();
} catch (RemoteException e) {
Log.e(CarLog.TAG_AUDIO, "Failed to get IAudioControl service", e);
} catch (NoSuchElementException e) {
Log.e(CarLog.TAG_AUDIO, "IAudioControl service not registered yet");
}
return null;
}
interface CarAudioZonesLoader {
CarAudioZone[] loadAudioZones();
}
}