blob: 3372260749dd2261fa7ec9528c09ee4814fdf6e7 [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 static android.car.builtin.media.AudioManagerHelper.UNDEFINED_STREAM_TYPE;
import static android.car.builtin.media.AudioManagerHelper.isMasterMute;
import static android.car.media.CarAudioManager.AUDIO_FEATURE_AUDIO_MIRRORING;
import static android.car.media.CarAudioManager.AUDIO_FEATURE_DYNAMIC_ROUTING;
import static android.car.media.CarAudioManager.AUDIO_FEATURE_OEM_AUDIO_SERVICE;
import static android.car.media.CarAudioManager.AUDIO_FEATURE_VOLUME_GROUP_EVENTS;
import static android.car.media.CarAudioManager.AUDIO_FEATURE_VOLUME_GROUP_MUTING;
import static android.car.media.CarAudioManager.CarAudioFeature;
import static android.car.media.CarAudioManager.INVALID_REQUEST_ID;
import static android.car.media.CarAudioManager.INVALID_VOLUME_GROUP_ID;
import static android.car.media.CarAudioManager.PRIMARY_AUDIO_ZONE;
import static android.car.media.CarVolumeGroupEvent.EVENT_TYPE_MUTE_CHANGED;
import static android.car.media.CarVolumeGroupEvent.EVENT_TYPE_VOLUME_GAIN_INDEX_CHANGED;
import static android.car.media.CarVolumeGroupEvent.EVENT_TYPE_VOLUME_MAX_INDEX_CHANGED;
import static android.car.media.CarVolumeGroupEvent.EVENT_TYPE_VOLUME_MIN_INDEX_CHANGED;
import static android.media.AudioAttributes.USAGE_MEDIA;
import static android.media.AudioManager.ADJUST_LOWER;
import static android.media.AudioManager.ADJUST_RAISE;
import static android.media.AudioManager.ADJUST_SAME;
import static android.media.AudioManager.ADJUST_TOGGLE_MUTE;
import static android.media.AudioManager.FLAG_FROM_KEY;
import static android.media.AudioManager.FLAG_PLAY_SOUND;
import static android.media.AudioManager.FLAG_SHOW_UI;
import static android.view.KeyEvent.ACTION_DOWN;
import static android.view.KeyEvent.KEYCODE_VOLUME_DOWN;
import static android.view.KeyEvent.KEYCODE_VOLUME_MUTE;
import static android.view.KeyEvent.KEYCODE_VOLUME_UP;
import static com.android.car.audio.CarAudioUtils.convertVolumeChangeToEvent;
import static com.android.car.audio.CarVolume.VERSION_TWO;
import static com.android.car.audio.hal.AudioControlWrapper.AUDIOCONTROL_FEATURE_AUDIO_DUCKING;
import static com.android.car.audio.hal.AudioControlWrapper.AUDIOCONTROL_FEATURE_AUDIO_FOCUS;
import static com.android.car.audio.hal.AudioControlWrapper.AUDIOCONTROL_FEATURE_AUDIO_GAIN_CALLBACK;
import static com.android.car.audio.hal.AudioControlWrapper.AUDIOCONTROL_FEATURE_AUDIO_MODULE_CALLBACK;
import static com.android.car.internal.ExcludeFromCodeCoverageGeneratedReport.DEBUGGING_CODE;
import static com.android.car.internal.ExcludeFromCodeCoverageGeneratedReport.DEPRECATED_CODE;
import static com.android.car.internal.ExcludeFromCodeCoverageGeneratedReport.DUMP_INFO;
import android.annotation.NonNull;
import android.annotation.Nullable;
import android.annotation.UserIdInt;
import android.car.Car;
import android.car.CarOccupantZoneManager;
import android.car.ICarOccupantZoneCallback;
import android.car.builtin.media.AudioManagerHelper;
import android.car.builtin.media.AudioManagerHelper.AudioPatchInfo;
import android.car.builtin.media.AudioManagerHelper.VolumeAndMuteReceiver;
import android.car.builtin.os.TraceHelper;
import android.car.builtin.os.UserManagerHelper;
import android.car.builtin.util.Slogf;
import android.car.builtin.util.TimingsTraceLog;
import android.car.media.AudioZonesMirrorStatusCallback;
import android.car.media.CarAudioManager;
import android.car.media.CarAudioPatchHandle;
import android.car.media.CarAudioZoneConfigInfo;
import android.car.media.CarVolumeGroupEvent;
import android.car.media.CarVolumeGroupInfo;
import android.car.media.IAudioZonesMirrorStatusCallback;
import android.car.media.ICarAudio;
import android.car.media.ICarVolumeCallback;
import android.car.media.ICarVolumeEventCallback;
import android.car.media.IMediaAudioRequestStatusCallback;
import android.car.media.IPrimaryZoneMediaAudioRequestCallback;
import android.car.media.ISwitchAudioZoneConfigCallback;
import android.content.Context;
import android.content.pm.PackageManager;
import android.media.AudioAttributes;
import android.media.AudioDeviceAttributes;
import android.media.AudioDeviceInfo;
import android.media.AudioFocusInfo;
import android.media.AudioManager;
import android.media.audiopolicy.AudioPolicy;
import android.os.Binder;
import android.os.Handler;
import android.os.HandlerThread;
import android.os.IBinder;
import android.os.Looper;
import android.os.RemoteException;
import android.os.SystemClock;
import android.os.SystemProperties;
import android.os.UserHandle;
import android.telephony.TelephonyManager;
import android.text.TextUtils;
import android.util.ArraySet;
import android.util.SparseArray;
import android.util.SparseIntArray;
import android.view.KeyEvent;
import com.android.car.CarInputService;
import com.android.car.CarInputService.KeyEventListener;
import com.android.car.CarLocalServices;
import com.android.car.CarLog;
import com.android.car.CarOccupantZoneService;
import com.android.car.CarServiceBase;
import com.android.car.CarServiceUtils;
import com.android.car.R;
import com.android.car.audio.CarAudioContext.AudioContext;
import com.android.car.audio.CarAudioPolicyVolumeCallback.AudioPolicyVolumeCallbackInternal;
import com.android.car.audio.hal.AudioControlFactory;
import com.android.car.audio.hal.AudioControlWrapper;
import com.android.car.audio.hal.AudioControlWrapperV1;
import com.android.car.audio.hal.HalAudioDeviceInfo;
import com.android.car.audio.hal.HalAudioFocus;
import com.android.car.audio.hal.HalAudioGainCallback;
import com.android.car.audio.hal.HalAudioModuleChangeCallback;
import com.android.car.internal.ExcludeFromCodeCoverageGeneratedReport;
import com.android.car.internal.annotation.AttributeUsage;
import com.android.car.internal.util.ArrayUtils;
import com.android.car.internal.util.IndentingPrintWriter;
import com.android.car.oem.CarOemProxyService;
import com.android.internal.annotations.GuardedBy;
import com.android.internal.annotations.VisibleForTesting;
import com.android.internal.util.Preconditions;
import org.xmlpull.v1.XmlPullParserException;
import java.io.BufferedInputStream;
import java.io.File;
import java.io.FileInputStream;
import java.io.IOException;
import java.io.InputStream;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.Collections;
import java.util.HashMap;
import java.util.HashSet;
import java.util.List;
import java.util.Map;
import java.util.Objects;
import java.util.Set;
import java.util.concurrent.Executor;
/**
* Service responsible for interaction with car's audio system.
*/
public final class CarAudioService extends ICarAudio.Stub implements CarServiceBase {
static final String TAG = CarLog.TAG_AUDIO;
private static final String CAR_AUDIO_SERVICE_THREAD_NAME = "CarAudioService";
static final AudioAttributes CAR_DEFAULT_AUDIO_ATTRIBUTE =
CarAudioContext.getAudioAttributeFromUsage(USAGE_MEDIA);
private static final String PROPERTY_RO_ENABLE_AUDIO_PATCH =
"ro.android.car.audio.enableaudiopatch";
// 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"
};
private static final List<Integer> KEYCODES_OF_INTEREST = List.of(
KEYCODE_VOLUME_DOWN,
KEYCODE_VOLUME_UP,
KEYCODE_VOLUME_MUTE
);
private static final AudioAttributes MEDIA_AUDIO_ATTRIBUTE =
CarAudioContext.getAudioAttributeFromUsage(USAGE_MEDIA);
private final HandlerThread mHandlerThread = CarServiceUtils.getHandlerThread(
CAR_AUDIO_SERVICE_THREAD_NAME);
private final Handler mHandler = new Handler(mHandlerThread.getLooper());
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 mUseCoreAudioVolume;
private final boolean mUseCoreAudioRouting;
private final boolean mUseCarVolumeGroupEvents;
private final boolean mUseCarVolumeGroupMuting;
private final boolean mUseHalDuckingSignals;
private final @CarVolume.CarVolumeListVersion int mAudioVolumeAdjustmentContextsVersion;
private final boolean mPersistMasterMuteState;
private final CarAudioSettings mCarAudioSettings;
private final int mKeyEventTimeoutMs;
private final MediaRequestHandler mMediaRequestHandler = new MediaRequestHandler();
private final CarAudioMirrorRequestHandler mCarAudioMirrorRequestHandler =
new CarAudioMirrorRequestHandler();
private final CarVolumeEventHandler mCarVolumeEventHandler = new CarVolumeEventHandler();
private AudioControlWrapper mAudioControlWrapper;
private CarDucking mCarDucking;
private CarVolumeGroupMuting mCarVolumeGroupMuting;
private HalAudioFocus mHalAudioFocus;
private @Nullable CarAudioGainMonitor mCarAudioGainMonitor;
@GuardedBy("mImplLock")
private @Nullable CoreAudioVolumeGroupCallback mCoreAudioVolumeGroupCallback;
private CarOccupantZoneService mOccupantZoneService;
private CarAudioModuleChangeMonitor mCarAudioModuleChangeMonitor;
/**
* 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 VolumeAndMuteReceiver mLegacyVolumeChangedHelper =
new AudioManagerHelper.VolumeAndMuteReceiver() {
@Override
public void onVolumeChanged(int streamType) {
if (streamType == UNDEFINED_STREAM_TYPE) {
Slogf.w(TAG, "Invalid stream type: %d", streamType);
}
int groupId = getVolumeGroupIdForStreamType(streamType);
if (groupId == INVALID_VOLUME_GROUP_ID) {
Slogf.w(TAG, "Unknown stream type: %d", streamType);
} else {
callbackGroupVolumeChange(PRIMARY_AUDIO_ZONE, groupId,
FLAG_FROM_KEY | FLAG_SHOW_UI);
}
}
@Override
public void onMuteChanged() {
callbackMasterMuteChange(PRIMARY_AUDIO_ZONE, FLAG_FROM_KEY | FLAG_SHOW_UI);
}
};
private final KeyEventListener mCarKeyEventListener = new KeyEventListener() {
@Override
public void onKeyEvent(KeyEvent event, int displayType, int seat) {
Slogf.i(TAG, "On key event for audio with display type: %d and seat %d", displayType,
seat);
if (event.getAction() != ACTION_DOWN) {
return;
}
int audioZoneId = mOccupantZoneService.getAudioZoneIdForOccupant(
mOccupantZoneService.getOccupantZoneIdForSeat(seat));
if (!isAudioZoneIdValid(audioZoneId)) {
Slogf.e(TAG, "Audio zone is invalid for event %s, displayType %d, and seat %d",
event, displayType, seat);
return;
}
int adjustment;
switch (event.getKeyCode()) {
case KEYCODE_VOLUME_DOWN:
adjustment = ADJUST_LOWER;
break;
case KEYCODE_VOLUME_UP:
adjustment = ADJUST_RAISE;
break;
case KEYCODE_VOLUME_MUTE:
adjustment = ADJUST_TOGGLE_MUTE;
break;
default:
adjustment = ADJUST_SAME;
break;
}
synchronized (mImplLock) {
mCarAudioPolicyVolumeCallback.onVolumeAdjustment(adjustment, audioZoneId);
}
}
};
private AudioPolicy mAudioPolicy;
private CarZonesAudioFocus mFocusHandler;
private String mCarAudioConfigurationPath;
private SparseIntArray mAudioZoneIdToOccupantZoneIdMapping;
@GuardedBy("mImplLock")
private SparseArray<CarAudioZone> mCarAudioZones;
@GuardedBy("mImplLock")
private CarVolume mCarVolume;
@GuardedBy("mImplLock")
private CarAudioContext mCarAudioContext;
private final CarVolumeCallbackHandler mCarVolumeCallbackHandler;
private final SparseIntArray mAudioZoneIdToUserIdMapping;
private final SystemClockWrapper mClock = new SystemClockWrapper();
@GuardedBy("mImplLock")
private final SparseArray<DeathRecipient>
mUserAssignedToPrimaryZoneToCallbackDeathRecipient = new SparseArray<>();
// TODO do not store uid mapping here instead use the uid
// device affinity in audio policy when available
private Map<Integer, Integer> mUidToZoneMap;
private CarAudioPlaybackCallback mCarAudioPlaybackCallback;
private CarAudioPowerListener mCarAudioPowerListener;
private CarInputService mCarInputService;
private final HalAudioGainCallback mHalAudioGainCallback =
new HalAudioGainCallback() {
@Override
public void onAudioDeviceGainsChanged(
List<Integer> halReasons, List<CarAudioGainConfigInfo> gains) {
synchronized (mImplLock) {
handleAudioDeviceGainsChangedLocked(halReasons, gains);
}
}
};
private final ICarOccupantZoneCallback mOccupantZoneCallback =
new ICarOccupantZoneCallback.Stub() {
@Override
public void onOccupantZoneConfigChanged(int flags) {
Slogf.d(TAG, "onOccupantZoneConfigChanged(%d)", flags);
if (((flags & CarOccupantZoneManager.ZONE_CONFIG_CHANGE_FLAG_USER)
!= CarOccupantZoneManager.ZONE_CONFIG_CHANGE_FLAG_USER)
&& ((flags & CarOccupantZoneManager.ZONE_CONFIG_CHANGE_FLAG_DISPLAY)
!= CarOccupantZoneManager.ZONE_CONFIG_CHANGE_FLAG_DISPLAY)) {
return;
}
handleOccupantZoneUserChanged();
}
};
@GuardedBy("mImplLock")
private CarAudioPolicyVolumeCallback mCarAudioPolicyVolumeCallback;
private final HalAudioModuleChangeCallback mHalAudioModuleChangeCallback =
new HalAudioModuleChangeCallback() {
@Override
public void onAudioPortsChanged(List<HalAudioDeviceInfo> deviceInfos) {
synchronized (mImplLock) {
handleAudioPortsChangedLocked(deviceInfos);
}
}
};
public CarAudioService(Context context) {
this(context, getAudioConfigurationPath(), new CarVolumeCallbackHandler());
}
@VisibleForTesting
CarAudioService(Context context, @Nullable String audioConfigurationPath,
CarVolumeCallbackHandler carVolumeCallbackHandler) {
mContext = Objects.requireNonNull(context,
"Context to create car audio service can not be null");
mCarAudioConfigurationPath = audioConfigurationPath;
mTelephonyManager = (TelephonyManager) mContext.getSystemService(Context.TELEPHONY_SERVICE);
mAudioManager = (AudioManager) mContext.getSystemService(Context.AUDIO_SERVICE);
mUseDynamicRouting = mContext.getResources().getBoolean(R.bool.audioUseDynamicRouting);
mUseCoreAudioVolume = mContext.getResources().getBoolean(R.bool.audioUseCoreVolume);
mUseCoreAudioRouting = mContext.getResources().getBoolean(R.bool.audioUseCoreRouting);
mKeyEventTimeoutMs =
mContext.getResources().getInteger(R.integer.audioVolumeKeyEventTimeoutMs);
mUseHalDuckingSignals = mContext.getResources().getBoolean(
R.bool.audioUseHalDuckingSignals);
mUidToZoneMap = new HashMap<>();
mCarVolumeCallbackHandler = carVolumeCallbackHandler;
mCarAudioSettings = new CarAudioSettings(mContext);
mAudioZoneIdToUserIdMapping = new SparseIntArray();
mAudioVolumeAdjustmentContextsVersion =
mContext.getResources().getInteger(R.integer.audioVolumeAdjustmentContextsVersion);
boolean useCarVolumeGroupMuting = mUseDynamicRouting && mContext.getResources().getBoolean(
R.bool.audioUseCarVolumeGroupMuting);
if (mAudioVolumeAdjustmentContextsVersion != VERSION_TWO && useCarVolumeGroupMuting) {
throw new IllegalArgumentException("audioUseCarVolumeGroupMuting is enabled but "
+ "this requires audioVolumeAdjustmentContextsVersion 2,"
+ " instead version " + mAudioVolumeAdjustmentContextsVersion + " was found");
}
mUseCarVolumeGroupEvents = mUseDynamicRouting && mContext.getResources().getBoolean(
R.bool.audioUseCarVolumeGroupEvent);
mUseCarVolumeGroupMuting = useCarVolumeGroupMuting;
mPersistMasterMuteState = !mUseCarVolumeGroupMuting && 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) {
mOccupantZoneService = CarLocalServices.getService(CarOccupantZoneService.class);
mCarInputService = CarLocalServices.getService(CarInputService.class);
if (mUseDynamicRouting) {
setupDynamicRoutingLocked();
setupHalAudioFocusListenerLocked();
setupHalAudioGainCallbackLocked();
setupHalAudioModuleChangeCallbackLocked();
setupAudioConfigurationCallbackLocked();
setupPowerPolicyListener();
mCarInputService.registerKeyEventListener(mCarKeyEventListener,
KEYCODES_OF_INTEREST);
} else {
Slogf.i(TAG, "Audio dynamic routing not enabled, run in legacy mode");
setupLegacyVolumeChangedListener();
}
mAudioManager.setSupportedSystemUsages(CarAudioContext.getSystemUsages());
}
restoreMasterMuteState();
}
private void setupPowerPolicyListener() {
mCarAudioPowerListener = CarAudioPowerListener.newCarAudioPowerListener(this);
mCarAudioPowerListener.startListeningForPolicyChanges();
}
private void restoreMasterMuteState() {
if (mUseCarVolumeGroupMuting) {
return;
}
// Restore master mute state if applicable
if (mPersistMasterMuteState) {
boolean storedMasterMute = mCarAudioSettings.isMasterMute();
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 {
AudioManagerHelper.unregisterVolumeAndMuteReceiver(mContext,
mLegacyVolumeChangedHelper);
}
if (mCoreAudioVolumeGroupCallback != null) {
mCoreAudioVolumeGroupCallback.release();
}
mCarVolumeCallbackHandler.release();
mOccupantZoneService.unregisterCallback(mOccupantZoneCallback);
if (mHalAudioFocus != null) {
mHalAudioFocus.unregisterFocusListener();
}
if (mAudioControlWrapper != null) {
mAudioControlWrapper.unlinkToDeath();
mAudioControlWrapper = null;
}
if (mCarAudioPowerListener != null) {
mCarAudioPowerListener.stopListeningForPolicyChanges();
}
}
}
@Override
@ExcludeFromCodeCoverageGeneratedReport(reason = DUMP_INFO)
public void dump(IndentingPrintWriter writer) {
synchronized (mImplLock) {
writer.println("*CarAudioService*");
writer.increaseIndent();
writer.println("Configurations:");
writer.increaseIndent();
writer.printf("Run in legacy mode? %b\n", !mUseDynamicRouting);
writer.printf("Rely on core audio for volume(%b)\n", mUseCoreAudioVolume);
writer.printf("Rely on core audio for routing(%b)\n", mUseCoreAudioRouting);
writer.printf("Audio Patch APIs enabled? %b\n", areAudioPatchAPIsEnabled());
writer.printf("Persist master mute state? %b\n", mPersistMasterMuteState);
writer.printf("Use hal ducking signals %b\n", mUseHalDuckingSignals);
writer.printf("Volume key event timeout ms: %d\n", mKeyEventTimeoutMs);
if (mCarAudioConfigurationPath != null) {
writer.printf("Car audio configuration path: %s\n", mCarAudioConfigurationPath);
}
writer.decreaseIndent();
writer.println();
writer.println("Current State:");
writer.increaseIndent();
writer.printf("Master muted? %b\n", isMasterMute(mAudioManager));
if (mCarAudioPowerListener != null) {
writer.printf("Audio enabled? %b\n", mCarAudioPowerListener.isAudioEnabled());
}
writer.decreaseIndent();
writer.println();
if (mUseDynamicRouting) {
writer.printf("Volume Group Mute Enabled? %b\n", mUseCarVolumeGroupMuting);
writer.printf("Volume Group Events Enabled? %b\n", mUseCarVolumeGroupEvents);
writer.println();
mCarVolume.dump(writer);
writer.println();
mCarAudioContext.dump(writer);
writer.println();
for (int i = 0; i < mCarAudioZones.size(); i++) {
CarAudioZone zone = mCarAudioZones.valueAt(i);
zone.dump(writer);
}
writer.println();
writer.println("UserId to Zone Mapping:");
writer.increaseIndent();
for (int index = 0; index < mAudioZoneIdToUserIdMapping.size(); index++) {
int audioZoneId = mAudioZoneIdToUserIdMapping.keyAt(index);
writer.printf("UserId %d mapped to zone %d\n",
mAudioZoneIdToUserIdMapping.get(audioZoneId),
audioZoneId);
}
writer.decreaseIndent();
writer.println();
writer.println("Audio Zone to Occupant Zone Mapping:");
writer.increaseIndent();
for (int index = 0; index < mAudioZoneIdToOccupantZoneIdMapping.size(); index++) {
int audioZoneId = mAudioZoneIdToOccupantZoneIdMapping.keyAt(index);
writer.printf("AudioZoneId %d mapped to OccupantZoneId %d\n", audioZoneId,
mAudioZoneIdToOccupantZoneIdMapping.get(audioZoneId));
}
writer.decreaseIndent();
writer.println();
writer.println("UID to Zone Mapping:");
writer.increaseIndent();
for (int callingId : mUidToZoneMap.keySet()) {
writer.printf("UID %d mapped to zone %d\n",
callingId,
mUidToZoneMap.get(callingId));
}
writer.decreaseIndent();
writer.println();
mFocusHandler.dump(writer);
writer.println();
getAudioControlWrapperLocked().dump(writer);
if (mHalAudioFocus != null) {
writer.println();
mHalAudioFocus.dump(writer);
} else {
writer.println("No HalAudioFocus instance\n");
}
if (mCarDucking != null) {
writer.println();
mCarDucking.dump(writer);
}
if (mCarVolumeGroupMuting != null) {
mCarVolumeGroupMuting.dump(writer);
}
if (mCarAudioPlaybackCallback != null) {
mCarAudioPlaybackCallback.dump(writer);
}
mCarAudioMirrorRequestHandler.dump(writer);
}
writer.decreaseIndent();
}
}
@Override
public boolean isAudioFeatureEnabled(@CarAudioFeature int audioFeatureType) {
switch (audioFeatureType) {
case AUDIO_FEATURE_DYNAMIC_ROUTING:
return mUseDynamicRouting;
case AUDIO_FEATURE_VOLUME_GROUP_MUTING:
return mUseCarVolumeGroupMuting;
case AUDIO_FEATURE_OEM_AUDIO_SERVICE:
return isAnyOemFeatureEnabled();
case AUDIO_FEATURE_VOLUME_GROUP_EVENTS:
return mUseCarVolumeGroupEvents;
case AUDIO_FEATURE_AUDIO_MIRRORING:
return mCarAudioMirrorRequestHandler.isMirrorAudioEnabled();
default:
throw new IllegalArgumentException("Unknown Audio Feature type: "
+ audioFeatureType);
}
}
private boolean isAnyOemFeatureEnabled() {
CarOemProxyService proxy = CarLocalServices.getService(CarOemProxyService.class);
return proxy != null && proxy.isOemServiceEnabled()
&& (proxy.getCarOemAudioFocusService() != null
|| proxy.getCarOemAudioVolumeService() != null
|| proxy.getCarOemAudioDuckingService() != null);
}
/**
* {@link android.car.media.CarAudioManager#setGroupVolume(int, int, int, int)}
*/
@Override
public void setGroupVolume(int zoneId, int groupId, int index, int flags) {
enforcePermission(Car.PERMISSION_CAR_CONTROL_AUDIO_VOLUME);
callbackGroupVolumeChange(zoneId, groupId, flags);
int callbackFlags = flags;
int eventTypes = EVENT_TYPE_VOLUME_GAIN_INDEX_CHANGED;
// For legacy stream type based volume control
boolean wasMute;
if (!mUseDynamicRouting) {
mAudioManager.setStreamVolume(
CarAudioDynamicRouting.STREAM_TYPES[groupId], index, flags);
return;
}
synchronized (mImplLock) {
CarVolumeGroup group = getCarVolumeGroupLocked(zoneId, groupId);
wasMute = group.isMuted();
group.setCurrentGainIndex(index);
}
if (wasMute) {
handleMuteChanged(zoneId, groupId, flags);
eventTypes |= EVENT_TYPE_MUTE_CHANGED;
}
if (mUseDynamicRouting && !isPlaybackOnVolumeGroupActive(zoneId, groupId)) {
callbackFlags |= FLAG_PLAY_SOUND;
}
callbackVolumeGroupEvent(List.of(convertVolumeChangeToEvent(
getVolumeGroupInfo(zoneId, groupId), callbackFlags, eventTypes)));
}
private void handleMuteChanged(int zoneId, int groupId, int flags) {
callbackGroupMuteChanged(zoneId, groupId, flags);
mCarVolumeGroupMuting.carMuteChanged();
}
private void callbackGroupVolumeChange(int zoneId, int groupId, int flags) {
int callbackFlags = flags;
if (mUseDynamicRouting && !isPlaybackOnVolumeGroupActive(zoneId, groupId)) {
callbackFlags |= FLAG_PLAY_SOUND;
}
mCarVolumeCallbackHandler.onVolumeGroupChange(zoneId, groupId, callbackFlags);
}
private void callbackGroupMuteChanged(int zoneId, int groupId, int flags) {
mCarVolumeCallbackHandler.onGroupMuteChange(zoneId, groupId, flags);
}
void setMasterMute(boolean mute, int flags) {
AudioManagerHelper.setMasterMute(mAudioManager, mute, flags);
// Master Mute only applies to primary zone
callbackMasterMuteChange(PRIMARY_AUDIO_ZONE, flags);
}
void callbackMasterMuteChange(int zoneId, int flags) {
mCarVolumeCallbackHandler.onMasterMuteChanged(zoneId, flags);
// Persists master mute state if applicable
if (mPersistMasterMuteState) {
mCarAudioSettings.storeMasterMute(isMasterMute(mAudioManager));
}
}
void callbackVolumeGroupEvent(List<CarVolumeGroupEvent> events) {
if (events.isEmpty()) {
Slogf.w(TAG, "Callback not initiated for empty events list");
return;
}
mCarVolumeEventHandler.onVolumeGroupEvent(events);
}
/**
* {@link android.car.media.CarAudioManager#getGroupMaxVolume(int, int)}
*/
@Override
public int getGroupMaxVolume(int zoneId, int groupId) {
enforcePermission(Car.PERMISSION_CAR_CONTROL_AUDIO_VOLUME);
if (!mUseDynamicRouting) {
return mAudioManager.getStreamMaxVolume(
CarAudioDynamicRouting.STREAM_TYPES[groupId]);
}
synchronized (mImplLock) {
CarVolumeGroup group = getCarVolumeGroupLocked(zoneId, groupId);
return group.getMaxGainIndex();
}
}
/**
* {@link android.car.media.CarAudioManager#getGroupMinVolume(int, int)}
*/
@Override
public int getGroupMinVolume(int zoneId, int groupId) {
enforcePermission(Car.PERMISSION_CAR_CONTROL_AUDIO_VOLUME);
if (!mUseDynamicRouting) {
return mAudioManager.getStreamMinVolume(
CarAudioDynamicRouting.STREAM_TYPES[groupId]);
}
synchronized (mImplLock) {
CarVolumeGroup group = getCarVolumeGroupLocked(zoneId, groupId);
return group.getMinGainIndex();
}
}
/**
* {@link android.car.media.CarAudioManager#getGroupVolume(int, int)}
*/
@Override
public int getGroupVolume(int zoneId, int groupId) {
enforcePermission(Car.PERMISSION_CAR_CONTROL_AUDIO_VOLUME);
// For legacy stream type based volume control
if (!mUseDynamicRouting) {
return mAudioManager.getStreamVolume(
CarAudioDynamicRouting.STREAM_TYPES[groupId]);
}
synchronized (mImplLock) {
CarVolumeGroup group = getCarVolumeGroupLocked(zoneId, groupId);
return group.getCurrentGainIndex();
}
}
/**
* {@link android.car.media.CarAudioManager#setPrimaryZoneMediaAudioRequestCallback()}
*/
@Override
public boolean registerPrimaryZoneMediaAudioRequestCallback(
IPrimaryZoneMediaAudioRequestCallback callback) {
enforcePermission(Car.PERMISSION_CAR_CONTROL_AUDIO_SETTINGS);
requireDynamicRouting();
return mMediaRequestHandler.registerPrimaryZoneMediaAudioRequestCallback(callback);
}
/**
* {@link android.car.media.CarAudioManager#clearPrimaryZoneMediaAudioRequestCallback()}
*/
@Override
public void unregisterPrimaryZoneMediaAudioRequestCallback(
IPrimaryZoneMediaAudioRequestCallback callback) {
enforcePermission(Car.PERMISSION_CAR_CONTROL_AUDIO_SETTINGS);
requireDynamicRouting();
List<Long> ownedRequests = mMediaRequestHandler.getRequestsOwnedByApprover(callback);
for (int index = 0; index < ownedRequests.size(); index++) {
long requestId = ownedRequests.get(index);
handleUnassignAudioFromUserIdOnPrimaryAudioZone(requestId);
}
if (!mMediaRequestHandler.unregisterPrimaryZoneMediaAudioRequestCallback(callback)) {
Slogf.e(TAG,
"unregisterPrimaryZoneMediaAudioRequestCallback could not remove callback");
}
}
/**
* {@link android.car.media.CarAudioManager#requestMediaAudioOnPrimaryZone(
* MediaAudioRequest)}
*/
@Override
public long requestMediaAudioOnPrimaryZone(IMediaAudioRequestStatusCallback callback,
CarOccupantZoneManager.OccupantZoneInfo info) {
enforcePermission(Car.PERMISSION_CAR_CONTROL_AUDIO_SETTINGS);
requireDynamicRouting();
Objects.requireNonNull(callback, "Media audio request callback can not be null");
Objects.requireNonNull(info, "Occupant zone info can not be null");
synchronized (mImplLock) {
int audioZoneId = mOccupantZoneService.getAudioZoneIdForOccupant(info.zoneId);
if (audioZoneId == PRIMARY_AUDIO_ZONE) {
throw new IllegalArgumentException("Occupant " + info
+ " already owns the primary audio zone");
}
int index = mAudioZoneIdToUserIdMapping.indexOfKey(audioZoneId);
if (index < 0) {
Slogf.w(TAG, "Audio zone id %d is not mapped to any user id", audioZoneId);
return INVALID_REQUEST_ID;
}
}
return mMediaRequestHandler.requestMediaAudioOnPrimaryZone(callback, info);
}
/**
* {@link android.car.media.CarAudioManager#allowMediaAudioOnPrimaryZone(
* android.car.media.CarAudioManager.MediaRequestToken, long, boolean)}
*/
@Override
public boolean allowMediaAudioOnPrimaryZone(IBinder token, long requestId, boolean allow) {
Objects.requireNonNull(token, "Media request token must not be null");
enforcePermission(Car.PERMISSION_CAR_CONTROL_AUDIO_SETTINGS);
requireDynamicRouting();
boolean canApprove = mMediaRequestHandler.isAudioMediaCallbackRegistered(token);
if (!allow || !canApprove) {
if (!canApprove) {
Slogf.w(TAG, "allowMediaAudioOnPrimaryZone Request %d can not be approved by "
+ "token %s",
requestId, token);
}
return mMediaRequestHandler.rejectMediaAudioRequest(requestId);
}
CarOccupantZoneManager.OccupantZoneInfo info =
mMediaRequestHandler.getOccupantForRequest(requestId);
if (info == null) {
Slogf.w(TAG, "allowMediaAudioOnPrimaryZone Request %d is no longer present",
requestId);
return false;
}
synchronized (mImplLock) {
int userId = mOccupantZoneService.getUserForOccupant(info.zoneId);
int audioZoneId = mOccupantZoneService.getAudioZoneIdForOccupant(info.zoneId);
return handleAssignAudioFromUserIdToPrimaryAudioZoneLocked(token,
userId, audioZoneId, requestId);
}
}
/**
* {@link android.car.media.CarAudioManager#isMediaAudioAllowedInPrimaryZone(
* CarOccupantZoneManager.OccupantZoneInfo)}
*/
@Override
public boolean isMediaAudioAllowedInPrimaryZone(CarOccupantZoneManager.OccupantZoneInfo info) {
Objects.requireNonNull(info, "Occupant zone info can not be null");
enforcePermission(Car.PERMISSION_CAR_CONTROL_AUDIO_SETTINGS);
requireDynamicRouting();
return mMediaRequestHandler.isMediaAudioAllowedInPrimaryZone(info);
}
/**
* {@link android.car.media.CarAudioManager#resetMediaAudioOnPrimaryZone(
* CarOccupantZoneManager.OccupantZoneInfo)}
*/
@Override
public boolean resetMediaAudioOnPrimaryZone(CarOccupantZoneManager.OccupantZoneInfo info) {
enforcePermission(Car.PERMISSION_CAR_CONTROL_AUDIO_SETTINGS);
requireDynamicRouting();
long requestId = mMediaRequestHandler.getRequestIdForOccupant(info);
if (requestId == INVALID_REQUEST_ID) {
Slogf.w(TAG, "resetMediaAudioOnPrimaryZone no request id for occupant %s", info);
return false;
}
return handleUnassignAudioFromUserIdOnPrimaryAudioZone(requestId);
}
/**
* {@link android.car.media.CarAudioManager#cancelMediaAudioOnPrimaryZone(long)}
*/
@Override
public boolean cancelMediaAudioOnPrimaryZone(long requestId) {
enforcePermission(Car.PERMISSION_CAR_CONTROL_AUDIO_SETTINGS);
requireDynamicRouting();
CarOccupantZoneManager.OccupantZoneInfo info =
mMediaRequestHandler.getOccupantForRequest(requestId);
if (info == null) {
Slogf.w(TAG, "cancelMediaAudioOnPrimaryZone no occupant for request %d",
requestId);
return false;
}
if (!mMediaRequestHandler.isMediaAudioAllowedInPrimaryZone(info)) {
return mMediaRequestHandler.cancelMediaAudioOnPrimaryZone(requestId);
}
return handleUnassignAudioFromUserIdOnPrimaryAudioZone(requestId);
}
/**
* {@link CarAudioManager#setAudioZoneMirrorStatusCallback(Executor,
* AudioZonesMirrorStatusCallback)}
*/
@Override
public boolean registerAudioZonesMirrorStatusCallback(
IAudioZonesMirrorStatusCallback callback) {
enforcePermission(Car.PERMISSION_CAR_CONTROL_AUDIO_SETTINGS);
requireDynamicRouting();
requireAudioMirroring();
return mCarAudioMirrorRequestHandler.registerAudioZonesMirrorStatusCallback(callback);
}
/**
* {@link CarAudioManager#clearAudioZonesMirrorStatusCallback()}
*/
@Override
public void unregisterAudioZonesMirrorStatusCallback(IAudioZonesMirrorStatusCallback callback) {
enforcePermission(Car.PERMISSION_CAR_CONTROL_AUDIO_SETTINGS);
requireDynamicRouting();
requireAudioMirroring();
if (!mCarAudioMirrorRequestHandler.unregisterAudioZonesMirrorStatusCallback(callback)) {
Slogf.w(TAG, "Could not unregister audio zones mirror status callback ,"
+ "callback could have died before unregister was called.");
}
}
/**
* {@link CarAudioManager#enableMirrorForAudioZones(List)}
*/
@Override
public long enableMirrorForAudioZones(int[] audioZones) {
enforcePermission(Car.PERMISSION_CAR_CONTROL_AUDIO_SETTINGS);
requireDynamicRouting();
requireAudioMirroring();
verifyCanMirrorToAudioZones(audioZones, /* forExtension= */ false);
long requestId = mCarAudioMirrorRequestHandler.getUniqueRequestId();
mHandler.post(() -> handleEnableAudioMirrorForZones(audioZones, requestId));
return requestId;
}
/**
* {@link CarAudioManager#extendAudioMirrorRequest(long, List)}
*/
@Override
public void extendAudioMirrorRequest(long mirrorId, int[] audioZones) {
enforcePermission(Car.PERMISSION_CAR_CONTROL_AUDIO_SETTINGS);
requireDynamicRouting();
requireAudioMirroring();
verifyCanMirrorToAudioZones(audioZones, /* forExtension= */ true);
mCarAudioMirrorRequestHandler.verifyValidRequestId(mirrorId);
mHandler.post(() -> handleEnableAudioMirrorForZones(audioZones, mirrorId));
}
/**
* {@link CarAudioManager#disableAudioMirrorForZone(int)}
*/
@Override
public void disableAudioMirrorForZone(int zoneId) {
enforcePermission(Car.PERMISSION_CAR_CONTROL_AUDIO_SETTINGS);
requireDynamicRouting();
requireAudioMirroring();
checkAudioZoneId(zoneId);
long requestId = mCarAudioMirrorRequestHandler.getRequestIdForAudioZone(zoneId);
if (requestId == INVALID_REQUEST_ID) {
Slogf.w(TAG, "Could not disable audio mirror for zone %d, zone was not mirroring",
zoneId);
return;
}
mHandler.post(() -> handleDisableAudioMirrorForZonesInConfig(new int[]{zoneId}, requestId));
}
/**
* {@link CarAudioManager#disableAudioMirrorRequest(List)}
*/
public void disableAudioMirror(long mirrorId) {
enforcePermission(Car.PERMISSION_CAR_CONTROL_AUDIO_SETTINGS);
requireDynamicRouting();
requireAudioMirroring();
Preconditions.checkArgument(mirrorId != INVALID_REQUEST_ID,
"Mirror id can not be INVALID_REQUEST_ID");
int[] config = mCarAudioMirrorRequestHandler.getMirrorAudioZonesForRequest(mirrorId);
if (config == null) {
Slogf.w(TAG, "disableAudioMirror mirror id %d no longer exist",
mirrorId);
return;
}
mHandler.post(() -> handleDisableAudioMirrorForZonesInConfig(config, mirrorId));
}
/**
* {@link CarAudioManager#getMirrorAudioZonesForAudioZone(int)}
*/
@Override
public int[] getMirrorAudioZonesForAudioZone(int zoneId) {
enforcePermission(Car.PERMISSION_CAR_CONTROL_AUDIO_SETTINGS);
requireDynamicRouting();
requireAudioMirroring();
long requestId = mCarAudioMirrorRequestHandler.getRequestIdForAudioZone(zoneId);
return requestId == INVALID_REQUEST_ID ? new int[0] :
mCarAudioMirrorRequestHandler.getMirrorAudioZonesForRequest(requestId);
}
/**
* {@link CarAudioManager#getMirrorAudioZonesForMirrorRequest(long)}
*/
@Override
public int[] getMirrorAudioZonesForMirrorRequest(long mirrorId) {
enforcePermission(Car.PERMISSION_CAR_CONTROL_AUDIO_SETTINGS);
requireDynamicRouting();
requireAudioMirroring();
Preconditions.checkArgument(mirrorId != INVALID_REQUEST_ID,
"Mirror request id can not be INVALID_REQUEST_ID");
int[] config = mCarAudioMirrorRequestHandler.getMirrorAudioZonesForRequest(mirrorId);
return config == null ? new int[0] : config;
}
@GuardedBy("mImplLock")
private CarVolumeGroup getCarVolumeGroupLocked(int zoneId, int groupId) {
return getCarAudioZoneLocked(zoneId).getCurrentVolumeGroup(groupId);
}
@GuardedBy("mImplLock")
@Nullable
private CarVolumeGroup getCarVolumeGroupLocked(int zoneId, String groupName) {
return getCarAudioZoneLocked(zoneId).getCurrentVolumeGroup(groupName);
}
private void verifyCanMirrorToAudioZones(int[] audioZones, boolean forExtension) {
Objects.requireNonNull(audioZones, "Mirror audio zones can not be null");
int minSize = 2;
if (forExtension) {
minSize = 1;
}
Preconditions.checkArgument(audioZones.length >= minSize,
"Mirror audio zones needs to have at least " + minSize + " zones");
ArraySet<Integer> zones = CarServiceUtils.toIntArraySet(audioZones);
if (zones.size() != audioZones.length) {
throw new IllegalArgumentException(
"Audio zones in mirror configuration must be unique "
+ Arrays.toString(audioZones));
}
if (zones.contains(PRIMARY_AUDIO_ZONE)) {
throw new IllegalArgumentException(
"Audio mirroring not allowed for primary audio zone");
}
for (int c = 0; c < audioZones.length; c++) {
int zoneId = audioZones[c];
checkAudioZoneId(zoneId);
int userId = getUserIdForZone(zoneId);
if (userId == UserManagerHelper.USER_NULL) {
throw new IllegalStateException(
"Audio zone must have an active user to allow mirroring");
}
CarOccupantZoneManager.OccupantZoneInfo info = mOccupantZoneService
.getOccupantForAudioZoneId(zoneId);
if (mMediaRequestHandler.isMediaAudioAllowedInPrimaryZone(info)) {
throw new IllegalStateException(
"Occupant " + info + " in audio zone " + zoneId
+ " is currently sharing to primary zone, "
+ "undo audio sharing in primary zone before setting up mirroring");
}
long zoneRequestId = mCarAudioMirrorRequestHandler.getRequestIdForAudioZone(zoneId);
if (zoneRequestId == INVALID_REQUEST_ID) {
continue;
}
throw new IllegalStateException(
"Audio zone " + zoneId + " is already mirroring");
}
}
private void handleEnableAudioMirrorForZones(int[] audioZoneIds, long requestId) {
boolean alreadySet = true;
int[] config = mCarAudioMirrorRequestHandler.getMirrorAudioZonesForRequest(requestId);
// Check it is same configuration as requested, order is preserved as it is assumed
// that the first zone id is the source and other zones are the receiver of the audio
// mirror
if (Arrays.equals(audioZoneIds, config)) {
Slogf.i(TAG, "handleEnableAudioMirrorForZones audio mirror already set for zones %s",
Arrays.toString(audioZoneIds));
mCarAudioMirrorRequestHandler.enableMirrorForZones(requestId, audioZoneIds);
return;
}
ArrayList<Integer> zones = new ArrayList<>();
if (config != null) {
zones.addAll(CarServiceUtils.asList(config));
}
for (int index = 0; index < audioZoneIds.length; index++) {
int audioZoneId = audioZoneIds[index];
int userId = getUserIdForZone(audioZoneId);
if (userId == UserManagerHelper.USER_NULL) {
Slogf.w(TAG, "handleEnableAudioMirrorForZones failed,"
+ " audio mirror not allowed for unassigned audio zone %d", audioZoneId);
mCarAudioMirrorRequestHandler.rejectMirrorForZones(audioZoneIds);
return;
}
long zoneRequestId = mCarAudioMirrorRequestHandler.getRequestIdForAudioZone(
audioZoneId);
if (zoneRequestId != INVALID_REQUEST_ID && zoneRequestId != requestId) {
Slogf.w(TAG, "handleEnableAudioMirrorForZones failed,"
+ " audio mirror not allowed for already mirroring audio zone %d",
audioZoneId);
mCarAudioMirrorRequestHandler.rejectMirrorForZones(audioZoneIds);
return;
}
CarOccupantZoneManager.OccupantZoneInfo info = mOccupantZoneService
.getOccupantForAudioZoneId(audioZoneId);
if (mMediaRequestHandler.isMediaAudioAllowedInPrimaryZone(info)) {
Slogf.w(TAG, "handleEnableAudioMirrorForZones failed,"
+ " audio mirror not allowed for audio zone %d sharing to primary zone",
audioZoneId);
mCarAudioMirrorRequestHandler.rejectMirrorForZones(audioZoneIds);
return;
}
zones.add(audioZoneId);
}
int[] audioZoneIdsToAdd = CarServiceUtils.toIntArray(zones);
TimingsTraceLog t = new TimingsTraceLog(TAG, TraceHelper.TRACE_TAG_CAR_SERVICE);
t.traceBegin("audio-mirror-" + Arrays.toString(audioZoneIdsToAdd));
synchronized (mImplLock) {
List<AudioFocusStackRequest> mediaFocusStacks = new ArrayList<>();
t.traceBegin("audio-mirror-focus-loss-" + Arrays.toString(audioZoneIdsToAdd));
for (int index = 0; index < audioZoneIdsToAdd.length; index++) {
int zoneId = audioZoneIdsToAdd[index];
int userId = getUserIdForZone(zoneId);
t.traceBegin("audio-mirror-focus-loss-zone-" + zoneId);
mediaFocusStacks.add(new AudioFocusStackRequest(mFocusHandler
.transientlyLoseMediaAudioFocusForUser(userId, zoneId), zoneId));
t.traceEnd();
}
t.traceEnd();
t.traceBegin("audio-mirror-routing-" + Arrays.toString(audioZoneIdsToAdd));
if (!setupAudioRoutingForUserInMirrorDeviceLocked(audioZoneIdsToAdd)) {
for (int index = 0; index < mediaFocusStacks.size(); index++) {
AudioFocusStackRequest request = mediaFocusStacks.get(index);
mFocusHandler.regainMediaAudioFocusInZone(request.mStack,
request.mOriginalZoneId);
}
mCarAudioMirrorRequestHandler.rejectMirrorForZones(audioZoneIdsToAdd);
return;
}
t.traceEnd();
// TODO(b/268383539): Implement multi zone focus for mirror
// Currently only selecting the source zone as focus manager
t.traceBegin("audio-mirror-focus-gain-" + Arrays.toString(audioZoneIdsToAdd));
int zoneId = audioZoneIdsToAdd[0];
for (int index = 0; index < mediaFocusStacks.size(); index++) {
AudioFocusStackRequest request = mediaFocusStacks.get(index);
t.traceBegin("audio-mirror-focus-gain-" + index + "-zone-" + zoneId);
mFocusHandler.regainMediaAudioFocusInZone(request.mStack, zoneId);
t.traceEnd();
}
t.traceEnd();
}
t.traceEnd();
mCarAudioMirrorRequestHandler.enableMirrorForZones(requestId, audioZoneIdsToAdd);
}
private void handleDisableAudioMirrorForZonesInConfig(int[] audioZoneIds, long requestId) {
int[] oldConfigs = mCarAudioMirrorRequestHandler.getMirrorAudioZonesForRequest(requestId);
if (oldConfigs == null) {
Slogf.w(TAG, "Could not disable audio mirror for zones %s,"
+ " %d request id was no longer mirroring",
Arrays.toString(audioZoneIds), requestId);
return;
}
for (int index = 0; index < audioZoneIds.length; index++) {
int zoneId = audioZoneIds[index];
if (!mCarAudioMirrorRequestHandler.isMirrorEnabledForZone(zoneId)) {
Slogf.w(TAG, "Could not disable audio mirror for zone %d,"
+ " zone was no longer mirroring",
zoneId);
return;
}
long currentRequestId = mCarAudioMirrorRequestHandler.getRequestIdForAudioZone(zoneId);
// The configuration to remove must be the same for the zones
if (currentRequestId != requestId) {
Slogf.w(TAG, "Could not disable audio mirror for zone %d,"
+ " found non matching configuration",
zoneId);
return;
}
}
int[] newConfig = mCarAudioMirrorRequestHandler
.calculateAudioConfigurationAfterRemovingZonesFromRequestId(requestId, audioZoneIds
);
if (newConfig == null) {
Slogf.w(TAG, " handleDisableAudioMirrorForZone could not disable audio "
+ "mirror for zones %s, configuration not found",
Arrays.toString(audioZoneIds));
return;
}
// If there are less than two zones mirroring, remove all the zones
if (newConfig.length < 2) {
newConfig = new int[0];
}
modifyAudioMirrorForZones(oldConfigs, newConfig);
mCarAudioMirrorRequestHandler.updateRemoveMirrorConfigurationForZones(requestId, newConfig);
}
private void modifyAudioMirrorForZones(int[] audioZoneIds, int[] newConfig) {
TimingsTraceLog t = new TimingsTraceLog(TAG, TraceHelper.TRACE_TAG_CAR_SERVICE);
ArraySet<Integer> newConfigSet = CarServiceUtils.toIntArraySet(newConfig);
int focusZoneId = audioZoneIds[0];
List<AudioFocusStackRequest> mediaFocusStacks = new ArrayList<>();
ArrayList<Integer> zonesToUndoRouting = new ArrayList<>(audioZoneIds.length
- newConfig.length);
t.traceBegin("audio-remove-mirror-" + Arrays.toString(audioZoneIds));
synchronized (mImplLock) {
t.traceBegin("audio-remove-mirror-focus-loss-" + Arrays.toString(audioZoneIds));
for (int index = 0; index < audioZoneIds.length; index++) {
int zoneId = audioZoneIds[index];
int newFocusZoneId = newConfig.length > 0 ? newConfig[0] : zoneId;
// Focus for zones not in the new config remove focus and routing
if (!newConfigSet.contains(zoneId)) {
newFocusZoneId = zoneId;
zonesToUndoRouting.add(zoneId);
}
int userId = getUserIdForZone(zoneId);
t.traceBegin("audio-remove-mirror-focus-loss-zone-" + zoneId);
mediaFocusStacks.add(new AudioFocusStackRequest(mFocusHandler
.transientlyLoseMediaAudioFocusForUser(userId, focusZoneId),
newFocusZoneId));
t.traceEnd();
}
t.traceEnd();
t.traceBegin("audio-remove-mirror-routing-" + zonesToUndoRouting);
setupAudioRoutingForUsersZoneLocked(zonesToUndoRouting);
t.traceEnd();
t.traceBegin("audio-remove-mirror-focus-gain-" + Arrays.toString(audioZoneIds));
for (int index = 0; index < mediaFocusStacks.size(); index++) {
AudioFocusStackRequest request = mediaFocusStacks.get(index);
t.traceBegin("audio-remove-mirror-focus-gain-" + index + "-zone-"
+ request.mOriginalZoneId);
mFocusHandler.regainMediaAudioFocusInZone(request.mStack, request.mOriginalZoneId);
t.traceEnd();
}
t.traceEnd();
}
t.traceEnd();
}
@GuardedBy("mImplLock")
private void setupAudioRoutingForUsersZoneLocked(ArrayList<Integer> audioZoneIds) {
for (int index = 0; index < audioZoneIds.size(); index++) {
int zoneId = audioZoneIds.get(index);
int userId = getUserIdForZone(zoneId);
if (userId == UserManagerHelper.USER_NULL) {
continue;
}
CarAudioZone audioZone = getCarAudioZone(zoneId);
setUserIdDeviceAffinitiesLocked(audioZone, userId, zoneId);
}
}
@GuardedBy("mImplLock")
private boolean setupAudioRoutingForUserInMirrorDeviceLocked(int[] audioZones) {
int index;
boolean succeeded = true;
for (index = 0; index < audioZones.length; index++) {
int zoneId = audioZones[index];
int userId = getUserIdForZone(zoneId);
CarAudioZone audioZone = getCarAudioZone(zoneId);
boolean enabled = setupMirrorDeviceForUserIdLocked(userId, audioZone,
mCarAudioMirrorRequestHandler.getAudioDeviceInfo());
if (!enabled) {
succeeded = false;
Slogf.w(TAG, "setupAudioRoutingForUserInMirrorDeviceLocked failed for zone "
+ "id %d and user id %d", zoneId, userId);
break;
}
}
if (succeeded) {
return true;
}
// Attempt to reset user id routing for other mirror zones
for (int count = 0; count < index; count++) {
int zoneId = audioZones[count];
int userId = getUserIdForZone(zoneId);
CarAudioZone audioZone = getCarAudioZone(zoneId);
setUserIdDeviceAffinitiesLocked(audioZone, userId, zoneId);
}
return false;
}
private void setupLegacyVolumeChangedListener() {
AudioManagerHelper.registerVolumeAndMuteReceiver(mContext, mLegacyVolumeChangedHelper);
}
private List<CarAudioDeviceInfo> generateCarAudioDeviceInfos() {
AudioDeviceInfo[] deviceInfos = mAudioManager.getDevices(
AudioManager.GET_DEVICES_OUTPUTS);
List<CarAudioDeviceInfo> infos = new ArrayList<>();
for (int index = 0; index < deviceInfos.length; index++) {
if (deviceInfos[index].getType() == AudioDeviceInfo.TYPE_BUS) {
infos.add(new CarAudioDeviceInfo(mAudioManager, deviceInfos[index]));
}
}
return infos;
}
private AudioDeviceInfo[] getAllInputDevices() {
return mAudioManager.getDevices(
AudioManager.GET_DEVICES_INPUTS);
}
@GuardedBy("mImplLock")
private SparseArray<CarAudioZone> loadCarAudioConfigurationLocked(
List<CarAudioDeviceInfo> carAudioDeviceInfos, AudioDeviceInfo[] inputDevices) {
try (InputStream fileStream = new FileInputStream(mCarAudioConfigurationPath);
InputStream inputStream = new BufferedInputStream(fileStream)) {
CarAudioZonesHelper zonesHelper = new CarAudioZonesHelper(mAudioManager,
mCarAudioSettings, inputStream, carAudioDeviceInfos, inputDevices,
mUseCarVolumeGroupMuting, mUseCoreAudioVolume, mUseCoreAudioRouting);
mAudioZoneIdToOccupantZoneIdMapping =
zonesHelper.getCarAudioZoneIdToOccupantZoneIdMapping();
SparseArray<CarAudioZone> zones = zonesHelper.loadAudioZones();
mCarAudioMirrorRequestHandler.setMirrorDeviceInfos(zonesHelper.getMirrorDeviceInfos());
mCarAudioContext = zonesHelper.getCarAudioContext();
return zones;
} catch (IOException | XmlPullParserException e) {
throw new RuntimeException("Failed to parse audio zone configuration", e);
}
}
@GuardedBy("mImplLock")
@ExcludeFromCodeCoverageGeneratedReport(reason = DEPRECATED_CODE)
private SparseArray<CarAudioZone> loadVolumeGroupConfigurationWithAudioControlLocked(
List<CarAudioDeviceInfo> carAudioDeviceInfos, AudioDeviceInfo[] inputDevices) {
AudioControlWrapper audioControlWrapper = getAudioControlWrapperLocked();
if (!(audioControlWrapper instanceof AudioControlWrapperV1)) {
throw new IllegalStateException(
"Updated version of IAudioControl no longer supports CarAudioZonesHelperLegacy."
+ " Please provide car_audio_configuration.xml.");
}
mCarAudioContext = new CarAudioContext(CarAudioContext.getAllContextsInfo(),
mUseCoreAudioVolume);
CarAudioZonesHelperLegacy legacyHelper = new CarAudioZonesHelperLegacy(mContext,
mCarAudioContext, R.xml.car_volume_groups, carAudioDeviceInfos,
(AudioControlWrapperV1) audioControlWrapper,
mCarAudioSettings, inputDevices);
return legacyHelper.loadAudioZones();
}
@GuardedBy("mImplLock")
private void loadCarAudioZonesLocked() {
List<CarAudioDeviceInfo> carAudioDeviceInfos = generateCarAudioDeviceInfos();
AudioDeviceInfo[] inputDevices = getAllInputDevices();
if (mCarAudioConfigurationPath != null) {
mCarAudioZones = loadCarAudioConfigurationLocked(carAudioDeviceInfos, inputDevices);
} else {
mCarAudioZones =
loadVolumeGroupConfigurationWithAudioControlLocked(carAudioDeviceInfos,
inputDevices);
}
CarAudioZonesValidator.validate(mCarAudioZones, mUseCoreAudioRouting);
}
@GuardedBy("mImplLock")
private void setupDynamicRoutingLocked() {
AudioPolicy.Builder builder = new AudioPolicy.Builder(mContext);
builder.setLooper(Looper.getMainLooper());
loadCarAudioZonesLocked();
mCarVolume = new CarVolume(mCarAudioContext, mClock,
mAudioVolumeAdjustmentContextsVersion, mKeyEventTimeoutMs);
for (int i = 0; i < mCarAudioZones.size(); i++) {
CarAudioZone zone = mCarAudioZones.valueAt(i);
// Ensure HAL gets our initial value
zone.init();
Slogf.v(TAG, "Processed audio zone: %s", zone);
}
// Mirror policy has to be set before general audio policy
setupMirrorDevicePolicyLocked(builder);
CarAudioDynamicRouting.setupAudioDynamicRouting(builder, mCarAudioZones, mCarAudioContext);
AudioPolicyVolumeCallbackInternal volumeCallbackInternal =
new AudioPolicyVolumeCallbackInternal() {
@Override
public void onMuteChange(boolean mute, int zoneId, int groupId, int flags) {
if (mUseCarVolumeGroupMuting) {
setVolumeGroupMute(zoneId, groupId, mute, flags);
return;
}
setMasterMute(mute, flags);
}
@Override
public void onGroupVolumeChange(int zoneId, int groupId, int volumeValue, int flags) {
setGroupVolume(zoneId, groupId, volumeValue, flags);
}
};
mCarAudioPolicyVolumeCallback = new CarAudioPolicyVolumeCallback(volumeCallbackInternal,
mAudioManager, new CarVolumeInfoWrapper(this), mUseCarVolumeGroupMuting);
// Attach the {@link AudioPolicyVolumeCallback}
CarAudioPolicyVolumeCallback.addVolumeCallbackToPolicy(builder,
mCarAudioPolicyVolumeCallback);
AudioControlWrapper audioControlWrapper = getAudioControlWrapperLocked();
if (mUseHalDuckingSignals) {
if (audioControlWrapper.supportsFeature(AUDIOCONTROL_FEATURE_AUDIO_DUCKING)) {
mCarDucking = new CarDucking(mCarAudioZones, audioControlWrapper);
}
}
if (mUseCarVolumeGroupMuting) {
mCarVolumeGroupMuting = new CarVolumeGroupMuting(mCarAudioZones, audioControlWrapper);
}
// 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 = CarZonesAudioFocus.createCarZonesAudioFocus(mAudioManager,
mContext.getPackageManager(),
mCarAudioZones,
mCarAudioSettings,
mCarDucking,
new CarVolumeInfoWrapper(this));
builder.setAudioPolicyFocusListener(mFocusHandler);
builder.setIsAudioFocusPolicy(true);
mAudioPolicy = builder.build();
// 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);
}
setupOccupantZoneInfoLocked();
if (mUseCoreAudioVolume) {
mCoreAudioVolumeGroupCallback = new CoreAudioVolumeGroupCallback(
new CarVolumeInfoWrapper(this), mAudioManager);
mCoreAudioVolumeGroupCallback.init(mContext.getMainExecutor());
}
}
@GuardedBy("mImplLock")
private void setupMirrorDevicePolicyLocked(AudioPolicy.Builder mirrorPolicyBuilder) {
if (mCarAudioMirrorRequestHandler.isMirrorAudioEnabled()) {
Slogf.i(TAG, "setupMirrorDevicePolicyLocked Audio mirroring is not enabled");
return;
}
CarAudioDynamicRouting.setupAudioDynamicRoutingForMirrorDevice(mirrorPolicyBuilder,
mCarAudioMirrorRequestHandler.getMirroringDeviceInfos());
}
@GuardedBy("mImplLock")
private void setupAudioConfigurationCallbackLocked() {
mCarAudioPlaybackCallback =
new CarAudioPlaybackCallback(mCarAudioZones, mClock, mKeyEventTimeoutMs);
mAudioManager.registerAudioPlaybackCallback(mCarAudioPlaybackCallback, null);
}
@GuardedBy("mImplLock")
private void setupOccupantZoneInfoLocked() {
CarOccupantZoneService occupantZoneService;
SparseIntArray audioZoneIdToOccupantZoneMapping;
audioZoneIdToOccupantZoneMapping = mAudioZoneIdToOccupantZoneIdMapping;
occupantZoneService = mOccupantZoneService;
occupantZoneService.setAudioZoneIdsForOccupantZoneIds(audioZoneIdToOccupantZoneMapping);
occupantZoneService.registerCallback(mOccupantZoneCallback);
}
@GuardedBy("mImplLock")
private void setupHalAudioFocusListenerLocked() {
AudioControlWrapper audioControlWrapper = getAudioControlWrapperLocked();
if (!audioControlWrapper.supportsFeature(AUDIOCONTROL_FEATURE_AUDIO_FOCUS)) {
Slogf.d(TAG, "HalAudioFocus is not supported on this device");
return;
}
mHalAudioFocus = new HalAudioFocus(mAudioManager, mAudioControlWrapper, getAudioZoneIds());
mHalAudioFocus.registerFocusListener();
}
@GuardedBy("mImplLock")
private void setupHalAudioGainCallbackLocked() {
AudioControlWrapper audioControlWrapper = getAudioControlWrapperLocked();
if (!audioControlWrapper.supportsFeature(AUDIOCONTROL_FEATURE_AUDIO_GAIN_CALLBACK)) {
Slogf.d(CarLog.TAG_AUDIO, "HalAudioGainCallback is not supported on this device");
return;
}
mCarAudioGainMonitor = new CarAudioGainMonitor(mAudioControlWrapper, mCarAudioZones);
mCarAudioGainMonitor.registerAudioGainListener(mHalAudioGainCallback);
}
@GuardedBy("mImplLock")
private void setupHalAudioModuleChangeCallbackLocked() {
AudioControlWrapper audioControlWrapper = getAudioControlWrapperLocked();
if (!audioControlWrapper.supportsFeature(AUDIOCONTROL_FEATURE_AUDIO_MODULE_CALLBACK)) {
Slogf.w(CarLog.TAG_AUDIO, "HalModuleChangeCallback is not supported on this device");
return;
}
mCarAudioModuleChangeMonitor = new CarAudioModuleChangeMonitor(mAudioControlWrapper,
new CarVolumeInfoWrapper(this), mCarAudioZones);
mCarAudioModuleChangeMonitor.setModuleChangeCallback(mHalAudioModuleChangeCallback);
}
/**
* Read from {@link #AUDIO_CONFIGURATION_PATHS} respectively.
* @return File path of the first hit in {@link #AUDIO_CONFIGURATION_PATHS}
*/
@Nullable
private static String getAudioConfigurationPath() {
for (String path : AUDIO_CONFIGURATION_PATHS) {
File configuration = new File(path);
if (configuration.exists()) {
return path;
}
}
return null;
}
@Override
public void setFadeTowardFront(float value) {
synchronized (mImplLock) {
enforcePermission(Car.PERMISSION_CAR_CONTROL_AUDIO_VOLUME);
requireValidFadeRange(value);
getAudioControlWrapperLocked().setFadeTowardFront(value);
}
}
@Override
public void setBalanceTowardRight(float value) {
synchronized (mImplLock) {
enforcePermission(Car.PERMISSION_CAR_CONTROL_AUDIO_VOLUME);
requireValidBalanceRange(value);
getAudioControlWrapperLocked().setBalanceTowardRight(value);
}
}
/**
* @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) {
Slogf.w(TAG, "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)) {
Slogf.w(TAG, "Discarded device with empty address, type=%d",
info.getType());
} else {
sourceAddresses.add(address);
}
}
}
return sourceAddresses.toArray(new String[0]);
}
}
@Override
public CarAudioPatchHandle createAudioPatch(String sourceAddress,
@AttributeUsage int usage, int gainInMillibels) {
enforcePermission(Car.PERMISSION_CAR_CONTROL_AUDIO_SETTINGS);
enforceCanUseAudioPatchAPI();
synchronized (mImplLock) {
return createAudioPatchLocked(sourceAddress, usage, gainInMillibels);
}
}
@Override
public void releaseAudioPatch(CarAudioPatchHandle carPatch) {
enforcePermission(Car.PERMISSION_CAR_CONTROL_AUDIO_SETTINGS);
enforceCanUseAudioPatchAPI();
synchronized (mImplLock) {
releaseAudioPatchLocked(carPatch);
}
}
private void enforceCanUseAudioPatchAPI() {
if (!areAudioPatchAPIsEnabled()) {
throw new IllegalStateException("Audio Patch APIs not enabled, see "
+ PROPERTY_RO_ENABLE_AUDIO_PATCH);
}
}
private boolean areAudioPatchAPIsEnabled() {
return SystemProperties.getBoolean(PROPERTY_RO_ENABLE_AUDIO_PATCH, /* default= */ false);
}
@GuardedBy("mImplLock")
private CarAudioPatchHandle createAudioPatchLocked(String sourceAddress,
@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;
}
}
Objects.requireNonNull(sourcePortInfo,
"Specified source is not available: " + sourceAddress);
AudioAttributes audioAttributes = CarAudioContext.getAudioAttributeFromUsage(usage);
AudioPatchInfo audioPatchInfo = AudioManagerHelper.createAudioPatch(sourcePortInfo,
getOutputDeviceForAudioAttributeLocked(PRIMARY_AUDIO_ZONE, audioAttributes),
gainInMillibels);
Slogf.d(TAG, "Audio patch created: %s", audioPatchInfo);
// Ensure the initial volume on output device port
int groupId = getVolumeGroupIdForAudioAttributeLocked(PRIMARY_AUDIO_ZONE, audioAttributes);
setGroupVolume(PRIMARY_AUDIO_ZONE, groupId,
getGroupVolume(PRIMARY_AUDIO_ZONE, groupId), 0);
return new CarAudioPatchHandle(audioPatchInfo.getHandleId(),
audioPatchInfo.getSourceAddress(), audioPatchInfo.getSinkAddress());
}
@GuardedBy("mImplLock")
private void releaseAudioPatchLocked(CarAudioPatchHandle carPatch) {
Objects.requireNonNull(carPatch);
if (AudioManagerHelper.releaseAudioPatch(mAudioManager, getAudioPatchInfo(carPatch))) {
Slogf.d(TAG, "releaseAudioPatch %s successfully", carPatch);
}
// If we didn't find a match, then something went awry, but it's probably not fatal...
Slogf.e(TAG, "releaseAudioPatch found no match for %s", carPatch);
}
private static AudioPatchInfo getAudioPatchInfo(CarAudioPatchHandle carPatch) {
return new AudioPatchInfo(carPatch.getSourceAddress(),
carPatch.getSinkAddress(),
carPatch.getHandleId());
}
@Override
public int getVolumeGroupCount(int zoneId) {
enforcePermission(Car.PERMISSION_CAR_CONTROL_AUDIO_VOLUME);
if (!mUseDynamicRouting) {
return CarAudioDynamicRouting.STREAM_TYPES.length;
}
synchronized (mImplLock) {
return getCarAudioZoneLocked(zoneId).getCurrentVolumeGroupCount();
}
}
@Override
public int getVolumeGroupIdForUsage(int zoneId, @AttributeUsage int usage) {
enforcePermission(Car.PERMISSION_CAR_CONTROL_AUDIO_VOLUME);
if (!CarAudioContext.isValidAudioAttributeUsage(usage)) {
return INVALID_VOLUME_GROUP_ID;
}
synchronized (mImplLock) {
return getVolumeGroupIdForAudioAttributeLocked(zoneId,
CarAudioContext.getAudioAttributeFromUsage(usage));
}
}
@Override
public CarVolumeGroupInfo getVolumeGroupInfo(int zoneId, int groupId) {
enforcePermission(Car.PERMISSION_CAR_CONTROL_AUDIO_VOLUME);
if (!mUseDynamicRouting) {
return null;
}
synchronized (mImplLock) {
return getCarVolumeGroupLocked(zoneId, groupId).getCarVolumeGroupInfo();
}
}
@Override
public List<CarVolumeGroupInfo> getVolumeGroupInfosForZone(int zoneId) {
enforcePermission(Car.PERMISSION_CAR_CONTROL_AUDIO_VOLUME);
if (!mUseDynamicRouting) {
return Collections.EMPTY_LIST;
}
synchronized (mImplLock) {
return getCarAudioZoneLocked(zoneId).getCurrentVolumeGroupInfos();
}
}
@Override
public List<AudioAttributes> getAudioAttributesForVolumeGroup(CarVolumeGroupInfo groupInfo) {
Objects.requireNonNull(groupInfo, "Car volume group info can not be null");
enforcePermission(Car.PERMISSION_CAR_CONTROL_AUDIO_VOLUME);
if (!mUseDynamicRouting) {
return Collections.EMPTY_LIST;
}
synchronized (mImplLock) {
return getCarAudioZoneLocked(groupInfo.getZoneId())
.getCurrentVolumeGroup(groupInfo.getId()).getAudioAttributes();
}
}
@GuardedBy("mImplLock")
private int getVolumeGroupIdForAudioAttributeLocked(int zoneId,
AudioAttributes audioAttributes) {
if (!mUseDynamicRouting) {
return getStreamTypeFromAudioAttribute(audioAttributes);
}
@AudioContext int audioContext =
mCarAudioContext.getContextForAudioAttribute(audioAttributes);
return getVolumeGroupIdForAudioContextLocked(zoneId, audioContext);
}
private static int getStreamTypeFromAudioAttribute(AudioAttributes audioAttributes) {
int usage = audioAttributes.getSystemUsage();
for (int i = 0; i < CarAudioDynamicRouting.STREAM_TYPE_USAGES.length; i++) {
if (usage == CarAudioDynamicRouting.STREAM_TYPE_USAGES[i]) {
return i;
}
}
return INVALID_VOLUME_GROUP_ID;
}
@GuardedBy("mImplLock")
private int getVolumeGroupIdForAudioContextLocked(int zoneId, @AudioContext int audioContext) {
CarVolumeGroup[] groups = getCarAudioZoneLocked(zoneId).getCurrentVolumeGroups();
for (int i = 0; i < groups.length; i++) {
int[] groupAudioContexts = groups[i].getContexts();
for (int groupAudioContext : groupAudioContexts) {
if (audioContext == groupAudioContext) {
return i;
}
}
}
return INVALID_VOLUME_GROUP_ID;
}
@Override
public @NonNull int[] getUsagesForVolumeGroupId(int zoneId, int groupId) {
enforcePermission(Car.PERMISSION_CAR_CONTROL_AUDIO_VOLUME);
if (!mUseDynamicRouting) {
return new int[] { CarAudioDynamicRouting.STREAM_TYPE_USAGES[groupId] };
}
synchronized (mImplLock) {
CarVolumeGroup group = getCarVolumeGroupLocked(zoneId, groupId);
int[] contexts = group.getContexts();
List<Integer> usages = new ArrayList<>();
for (int index = 0; index < contexts.length; index++) {
AudioAttributes[] attributesForContext =
mCarAudioContext.getAudioAttributesForContext(contexts[index]);
for (int counter = 0; counter < attributesForContext.length; counter++) {
usages.add(attributesForContext[counter].getSystemUsage());
}
}
int[] usagesArray = CarServiceUtils.toIntArray(usages);
return usagesArray;
}
}
@Override
public boolean isPlaybackOnVolumeGroupActive(int zoneId, int groupId) {
enforcePermission(Car.PERMISSION_CAR_CONTROL_AUDIO_VOLUME);
requireDynamicRouting();
Preconditions.checkArgument(isAudioZoneIdValid(zoneId),
"Invalid audio zone id %d", zoneId);
CarVolume carVolume;
synchronized (mImplLock) {
carVolume = mCarVolume;
}
return carVolume.isAnyContextActive(getContextsForVolumeGroupId(zoneId, groupId),
getActiveAttributesFromPlaybackConfigurations(zoneId),
getCallStateForZone(zoneId), getActiveHalAudioAttributesForZone(zoneId));
}
/**
*
* returns the current call state ({@code CALL_STATE_OFFHOOK}, {@code CALL_STATE_RINGING},
* {@code CALL_STATE_IDLE}) from the telephony manager.
*/
int getCallStateForZone(int zoneId) {
synchronized (mImplLock) {
// Only driver can use telephony stack
if (getUserIdForZoneLocked(zoneId) == mOccupantZoneService.getDriverUserId()) {
return mTelephonyManager.getCallState();
}
}
return TelephonyManager.CALL_STATE_IDLE;
}
private List<AudioAttributes> getActiveAttributesFromPlaybackConfigurations(int zoneId) {
return getCarAudioZone(zoneId)
.findActiveAudioAttributesFromPlaybackConfigurations(mAudioManager
.getActivePlaybackConfigurations());
}
private @NonNull @AudioContext int[] getContextsForVolumeGroupId(int zoneId, int groupId) {
synchronized (mImplLock) {
CarVolumeGroup group = getCarVolumeGroupLocked(zoneId, groupId);
return group.getContexts();
}
}
/**
* Gets the ids of all available audio zones
*
* @return Array of available audio zones ids
*/
@Override
public @NonNull int[] getAudioZoneIds() {
enforcePermission(Car.PERMISSION_CAR_CONTROL_AUDIO_SETTINGS);
requireDynamicRouting();
synchronized (mImplLock) {
int[] zoneIds = new int[mCarAudioZones.size()];
for (int i = 0; i < mCarAudioZones.size(); i++) {
zoneIds[i] = mCarAudioZones.keyAt(i);
}
return zoneIds;
}
}
/**
* Gets the audio zone id currently mapped to uid,
*
* <p><b>Note:</b> Will use uid mapping first, followed by uid's {@userId} mapping.
* defaults to PRIMARY_AUDIO_ZONE if no mapping exist
*
* @param uid The uid
* @return zone id mapped to uid
*/
@Override
public int getZoneIdForUid(int uid) {
enforcePermission(Car.PERMISSION_CAR_CONTROL_AUDIO_SETTINGS);
requireDynamicRouting();
synchronized (mImplLock) {
return getZoneIdForUidLocked(uid);
}
}
@GuardedBy("mImplLock")
private int getZoneIdForUidLocked(int uid) {
if (mUidToZoneMap.containsKey(uid)) {
return mUidToZoneMap.get(uid);
}
return getZoneIdForUserLocked(UserHandle.getUserHandleForUid(uid));
}
@GuardedBy("mImplLock")
private int getZoneIdForUserLocked(UserHandle handle) {
CarOccupantZoneManager.OccupantZoneInfo info =
mOccupantZoneService.getOccupantZoneForUser(handle);
int audioZoneId = CarAudioManager.INVALID_AUDIO_ZONE;
if (info != null) {
audioZoneId = mOccupantZoneService.getAudioZoneIdForOccupant(info.zoneId);
}
return audioZoneId == CarAudioManager.INVALID_AUDIO_ZONE ? PRIMARY_AUDIO_ZONE : audioZoneId;
}
/**
* Maps the audio zone id to uid
*
* @param zoneId The audio zone id
* @param uid The uid to map
*
* <p><b>Note:</b> Will throw if occupant zone mapping exist, as uid and occupant zone mapping
* do not work in conjunction.
*
* @return true if the device affinities, for devices in zone, are successfully set
*/
@Override
public boolean setZoneIdForUid(int zoneId, int uid) {
enforcePermission(Car.PERMISSION_CAR_CONTROL_AUDIO_SETTINGS);
requireDynamicRouting();
Slogf.i(TAG, "setZoneIdForUid Calling uid %d mapped to : %d", uid, zoneId);
synchronized (mImplLock) {
checkAudioZoneIdLocked(zoneId);
// If occupant mapping exist uid routing can not be used
requiredOccupantZoneMappingDisabledLocked();
// Figure out if anything is currently holding focus,
// This will change the focus to transient loss while we are switching zones
Integer currentZoneId = mUidToZoneMap.get(uid);
ArrayList<AudioFocusInfo> currentFocusHoldersForUid = new ArrayList<>();
ArrayList<AudioFocusInfo> currentFocusLosersForUid = new ArrayList<>();
if (currentZoneId != null) {
currentFocusHoldersForUid = mFocusHandler.getAudioFocusHoldersForUid(uid,
currentZoneId.intValue());
currentFocusLosersForUid = mFocusHandler.getAudioFocusLosersForUid(uid,
currentZoneId.intValue());
if (!currentFocusHoldersForUid.isEmpty() || !currentFocusLosersForUid.isEmpty()) {
// Order matters here: Remove the focus losers first
// then do the current holder to prevent loser from popping up while
// the focus is being remove for current holders
// Remove focus for current focus losers
mFocusHandler.transientlyLoseInFocusInZone(currentFocusLosersForUid,
currentZoneId.intValue());
// Remove focus for current holders
mFocusHandler.transientlyLoseInFocusInZone(currentFocusHoldersForUid,
currentZoneId.intValue());
}
}
// if the current uid is in the list
// remove it from the list
if (checkAndRemoveUidLocked(uid)) {
if (setZoneIdForUidNoCheckLocked(zoneId, uid)) {
// Order matters here: Regain focus for
// Previously lost focus holders then regain
// focus for holders that had it last
// Regain focus for the focus losers from previous zone
if (!currentFocusLosersForUid.isEmpty()) {
regainAudioFocusLocked(currentFocusLosersForUid, zoneId);
}
// Regain focus for the focus holders from previous zone
if (!currentFocusHoldersForUid.isEmpty()) {
regainAudioFocusLocked(currentFocusHoldersForUid, zoneId);
}
return true;
}
}
return false;
}
}
@GuardedBy("mImplLock")
private boolean handleAssignAudioFromUserIdToPrimaryAudioZoneLocked(
IBinder token, int userId, int zoneId, long requestId) {
AudioFocusStack mediaFocusStack =
mFocusHandler.transientlyLoseMediaAudioFocusForUser(userId, zoneId);
if (!shareAudioRoutingForUserInPrimaryAudioZoneLocked(userId, zoneId)) {
Slogf.w(TAG, "Can not route user id %s to primary audio zone", userId);
mFocusHandler.regainMediaAudioFocusInZone(mediaFocusStack, zoneId);
return false;
}
DeathRecipient deathRecipient = () -> handleAssignedAudioFromUserDeath(requestId);
try {
token.linkToDeath(deathRecipient, /* flags= */ 0);
} catch (RemoteException e) {
Slogf.e(TAG, e, "Can not route user id %d to primary audio zone, caller died", userId);
mFocusHandler.regainMediaAudioFocusInZone(mediaFocusStack, zoneId);
return false;
}
mFocusHandler.regainMediaAudioFocusInZone(mediaFocusStack, PRIMARY_AUDIO_ZONE);
mUserAssignedToPrimaryZoneToCallbackDeathRecipient.put(userId, deathRecipient);
mMediaRequestHandler.acceptMediaAudioRequest(token, requestId);
Slogf.d(TAG, "Assigning user id %d from primary audio zone", userId);
return true;
}
@GuardedBy("mImplLock")
private boolean shareAudioRoutingForUserInPrimaryAudioZoneLocked(int userId, int zoneId) {
CarAudioZone zone = mCarAudioZones.get(zoneId);
return shareUserIdMediaInMainZoneLocked(userId, zone);
}
@GuardedBy("mImplLock")
private boolean shareUserIdMediaInMainZoneLocked(int userId, CarAudioZone audioZone) {
List<AudioDeviceInfo> devices = audioZone.getCurrentAudioDeviceInfos();
CarAudioZone primaryAudioZone = getCarAudioZoneLocked(PRIMARY_AUDIO_ZONE);
devices.add(primaryAudioZone.getAudioDeviceForContext(mCarAudioContext
.getContextForAudioAttribute(MEDIA_AUDIO_ATTRIBUTE)));
return setUserIdDeviceAffinityLocked(devices, userId, audioZone.getId());
}
@GuardedBy("mImplLock")
private boolean setupMirrorDeviceForUserIdLocked(int userId, CarAudioZone audioZone,
AudioDeviceInfo mirrorDevice) {
List<AudioDeviceInfo> devices = audioZone.getCurrentAudioDeviceInfos();
devices.add(mirrorDevice);
Slogf.d(TAG, "setupMirrorDeviceForUserIdLocked for userId %d in zone %d", userId,
audioZone.getId());
return setUserIdDeviceAffinityLocked(devices, userId, audioZone.getId());
}
@GuardedBy("mImplLock")
private boolean setUserIdDeviceAffinityLocked(List<AudioDeviceInfo> devices,
int userId, int zoneId) {
boolean results = mAudioPolicy.setUserIdDeviceAffinity(userId, devices);
if (!results) {
Slogf.w(TAG, "setUserIdDeviceAffinityLocked for userId %d in zone %d Failed,"
+ " could not set audio routing.", userId, zoneId);
}
return results;
}
private void handleAssignedAudioFromUserDeath(long requestId) {
Slogf.e(TAG, "IBinder for request %d died", requestId);
handleUnassignAudioFromUserIdOnPrimaryAudioZone(requestId);
}
private boolean handleUnassignAudioFromUserIdOnPrimaryAudioZone(long requestId) {
CarOccupantZoneManager.OccupantZoneInfo info =
mMediaRequestHandler.getOccupantForRequest(requestId);
if (info == null) {
Slogf.w(TAG, "Occupant %s is not mapped to any audio zone", info);
return false;
}
int userId = mOccupantZoneService.getUserForOccupant(info.zoneId);
int audioZoneId = mOccupantZoneService.getAudioZoneIdForOccupant(info.zoneId);
synchronized (mImplLock) {
CarAudioZone audioZone = getCarAudioZoneLocked(audioZoneId);
AudioFocusStack mediaFocusStack =
mFocusHandler.transientlyLoseMediaAudioFocusForUser(userId, PRIMARY_AUDIO_ZONE);
if (!resetUserIdMediaInMainZoneLocked(userId, audioZone)) {
Slogf.w(TAG, "Can not remove route for user id %d to primary audio zone", userId);
mFocusHandler.regainMediaAudioFocusInZone(mediaFocusStack, PRIMARY_AUDIO_ZONE);
return false;
}
mFocusHandler.regainMediaAudioFocusInZone(mediaFocusStack, audioZoneId);
removeAssignedUserInfoLocked(userId);
}
Slogf.d(TAG, "Unassigned user id %d from primary audio zone", userId);
return mMediaRequestHandler.stopMediaAudioOnPrimaryZone(requestId);
}
@GuardedBy("mImplLock")
private void removeAssignedUserInfoLocked(int userId) {
mUserAssignedToPrimaryZoneToCallbackDeathRecipient.remove(userId);
}
@GuardedBy("mImplLock")
private boolean resetUserIdMediaInMainZoneLocked(int userId, CarAudioZone audioZone) {
List<AudioDeviceInfo> devices = audioZone.getCurrentAudioDeviceInfos();
return setUserIdDeviceAffinityLocked(devices, userId, audioZone.getId());
}
@GuardedBy("mImplLock")
private AudioDeviceInfo getOutputDeviceForAudioAttributeLocked(int zoneId,
AudioAttributes audioAttributes) {
enforcePermission(Car.PERMISSION_CAR_CONTROL_AUDIO_SETTINGS);
requireDynamicRouting();
int contextForUsage = mCarAudioContext.getContextForAudioAttribute(audioAttributes);
Preconditions.checkArgument(!CarAudioContext.isInvalidContextId(contextForUsage),
"Invalid audio attribute usage %s", audioAttributes);
return getCarAudioZoneLocked(zoneId).getAudioDeviceForContext(contextForUsage);
}
@Override
public String getOutputDeviceAddressForUsage(int zoneId, @AttributeUsage int usage) {
enforcePermission(Car.PERMISSION_CAR_CONTROL_AUDIO_SETTINGS);
requireDynamicRouting();
CarAudioContext.checkAudioAttributeUsage(usage);
int contextForUsage = getCarAudioContext()
.getContextForAudioAttribute(CarAudioContext.getAudioAttributeFromUsage(usage));
return getCarAudioZone(zoneId).getAddressForContext(contextForUsage);
}
/**
* Regain focus for the focus list passed in
* @param afiList focus info list to regain
* @param zoneId zone id where the focus holder belong
*/
@GuardedBy("mImplLock")
void regainAudioFocusLocked(ArrayList<AudioFocusInfo> afiList, int zoneId) {
for (AudioFocusInfo info : afiList) {
if (mFocusHandler.reevaluateAndRegainAudioFocus(info)
!= AudioManager.AUDIOFOCUS_REQUEST_GRANTED) {
Slogf.i(TAG,
" Focus could not be granted for entry %s uid %d in zone %d",
info.getClientId(), info.getClientUid(), zoneId);
}
}
}
/**
* Removes the current mapping of the uid, focus will be lost in zone
* @param uid The uid to remove
*
* <p><b>Note:</b> Will throw if occupant zone mapping exist, as uid and occupant zone mapping
* do not work in conjunction.
*
* return true if all the devices affinities currently
* mapped to uid are successfully removed
*/
@Override
public boolean clearZoneIdForUid(int uid) {
enforcePermission(Car.PERMISSION_CAR_CONTROL_AUDIO_SETTINGS);
requireDynamicRouting();
synchronized (mImplLock) {
// Throw so as to not set the wrong expectation,
// that routing will be changed if clearZoneIdForUid is called.
requiredOccupantZoneMappingDisabledLocked();
return checkAndRemoveUidLocked(uid);
}
}
/**
* Sets the zone id for uid
* @param zoneId zone id to map to uid
* @param uid uid to map
* @return true if setting uid device affinity is successful
*/
@GuardedBy("mImplLock")
private boolean setZoneIdForUidNoCheckLocked(int zoneId, int uid) {
Slogf.d(TAG, "setZoneIdForUidNoCheck Calling uid %d mapped to %d", uid, zoneId);
//Request to add uid device affinity
List<AudioDeviceInfo> deviceInfos =
getCarAudioZoneLocked(zoneId).getCurrentAudioDeviceInfos();
if (mAudioPolicy.setUidDeviceAffinity(uid, deviceInfos)) {
// TODO do not store uid mapping here instead use the uid
// device affinity in audio policy when available
mUidToZoneMap.put(uid, zoneId);
return true;
}
Slogf.w(TAG, "setZoneIdForUidNoCheck Failed set device affinity for uid %d in zone %d",
uid, zoneId);
return false;
}
/**
* Check if uid is attached to a zone and remove it
* @param uid unique id to remove
* @return true if the uid was successfully removed or mapping was not assigned
*/
@GuardedBy("mImplLock")
private boolean checkAndRemoveUidLocked(int uid) {
Integer zoneId = mUidToZoneMap.get(uid);
if (zoneId != null) {
Slogf.i(TAG, "checkAndRemoveUid removing Calling uid %d from zone %d", uid, zoneId);
if (mAudioPolicy.removeUidDeviceAffinity(uid)) {
// TODO use the uid device affinity in audio policy when available
mUidToZoneMap.remove(uid);
return true;
}
//failed to remove device affinity from zone devices
Slogf.w(TAG, "checkAndRemoveUid Failed remove device affinity for uid %d in zone %d",
uid, zoneId);
return false;
}
return true;
}
/*
* {@link android.car.media.CarAudioManager#registerCarVolumeGroupEventCallback()}
*/
@Override
public boolean registerCarVolumeEventCallback(ICarVolumeEventCallback callback) {
enforcePermission(Car.PERMISSION_CAR_CONTROL_AUDIO_VOLUME);
requireDynamicRouting();
requireVolumeGroupEvents();
int uid = Binder.getCallingUid();
mCarVolumeEventHandler.registerCarVolumeEventCallback(callback, uid);
mCarVolumeCallbackHandler.checkAndRepriotize(uid, false);
return true;
}
/*
* {@link android.car.media.CarAudioManager#unregisterCarVolumeGroupEventCallback()}
*/
@Override
public boolean unregisterCarVolumeEventCallback(ICarVolumeEventCallback callback) {
enforcePermission(Car.PERMISSION_CAR_CONTROL_AUDIO_VOLUME);
requireDynamicRouting();
requireVolumeGroupEvents();
int uid = Binder.getCallingUid();
mCarVolumeEventHandler.unregisterCarVolumeEventCallback(callback, uid);
mCarVolumeCallbackHandler.checkAndRepriotize(uid, true);
return true;
}
@Override
public void registerVolumeCallback(@NonNull IBinder binder) {
synchronized (mImplLock) {
enforcePermission(Car.PERMISSION_CAR_CONTROL_AUDIO_VOLUME);
int uid = Binder.getCallingUid();
mCarVolumeCallbackHandler.registerCallback(binder, uid,
!mCarVolumeEventHandler.checkIfUidIsRegistered(uid));
}
}
@Override
public void unregisterVolumeCallback(@NonNull IBinder binder) {
synchronized (mImplLock) {
enforcePermission(Car.PERMISSION_CAR_CONTROL_AUDIO_VOLUME);
mCarVolumeCallbackHandler.unregisterCallback(binder, Binder.getCallingUid());
}
}
/**
* {@link android.car.media.CarAudioManager#isVolumeGroupMuted(int, int)}
*/
@Override
public boolean isVolumeGroupMuted(int zoneId, int groupId) {
enforcePermission(Car.PERMISSION_CAR_CONTROL_AUDIO_VOLUME);
requireDynamicRouting();
if (!mUseCarVolumeGroupMuting) {
return false;
}
synchronized (mImplLock) {
CarVolumeGroup group = getCarVolumeGroupLocked(zoneId, groupId);
return group.isMuted();
}
}
/**
* {@link android.car.media.CarAudioManager#setVolumeGroupMute(int, int, boolean, int)}
*/
@Override
public void setVolumeGroupMute(int zoneId, int groupId, boolean mute, int flags) {
enforcePermission(Car.PERMISSION_CAR_CONTROL_AUDIO_VOLUME);
requireDynamicRouting();
requireVolumeGroupMuting();
synchronized (mImplLock) {
CarVolumeGroup group = getCarVolumeGroupLocked(zoneId, groupId);
group.setMute(mute);
}
handleMuteChanged(zoneId, groupId, flags);
callbackVolumeGroupEvent(List.of(convertVolumeChangeToEvent(
getVolumeGroupInfo(zoneId, groupId), flags, EVENT_TYPE_MUTE_CHANGED)));
}
@Override
public @NonNull List<AudioDeviceAttributes> getInputDevicesForZoneId(int zoneId) {
enforcePermission(Car.PERMISSION_CAR_CONTROL_AUDIO_SETTINGS);
requireDynamicRouting();
return getCarAudioZone(zoneId).getInputAudioDevices();
}
@Override
public CarAudioZoneConfigInfo getCurrentAudioZoneConfigInfo(int zoneId) {
enforcePermission(Car.PERMISSION_CAR_CONTROL_AUDIO_SETTINGS);
requireDynamicRouting();
synchronized (mImplLock) {
return getCarAudioZoneLocked(zoneId).getCurrentCarAudioZoneConfig()
.getCarAudioZoneConfigInfo();
}
}
@Override
public List<CarAudioZoneConfigInfo> getAudioZoneConfigInfos(int zoneId) {
enforcePermission(Car.PERMISSION_CAR_CONTROL_AUDIO_SETTINGS);
requireDynamicRouting();
synchronized (mImplLock) {
return getCarAudioZoneLocked(zoneId).getCarAudioZoneConfigInfos();
}
}
@Override
public void switchZoneToConfig(CarAudioZoneConfigInfo zoneConfig,
ISwitchAudioZoneConfigCallback callback) {
enforcePermission(Car.PERMISSION_CAR_CONTROL_AUDIO_SETTINGS);
requireDynamicRouting();
Objects.requireNonNull(zoneConfig, "Car audio zone config to switch to can not be null");
verifyCanSwitchZoneConfigs(zoneConfig);
mHandler.post(() -> {
boolean isSuccessful = handleSwitchZoneConfig(zoneConfig);
try {
callback.onAudioZoneConfigSwitched(zoneConfig, isSuccessful);
} catch (RemoteException e) {
Slogf.e(TAG, e, "Could not inform zone configuration %s switch result",
zoneConfig);
}
});
}
private void verifyCanSwitchZoneConfigs(CarAudioZoneConfigInfo zoneConfig) {
int zoneId = zoneConfig.getZoneId();
synchronized (mImplLock) {
checkAudioZoneIdLocked(zoneId);
}
int userId = getUserIdForZone(zoneId);
if (userId == UserManagerHelper.USER_NULL) {
throw new IllegalStateException(
"Audio zone must have an active user to allow switching zone configuration");
}
CarOccupantZoneManager.OccupantZoneInfo info =
mOccupantZoneService.getOccupantForAudioZoneId(zoneId);
if (mMediaRequestHandler.isMediaAudioAllowedInPrimaryZone(info)) {
throw new IllegalStateException(
"Occupant " + info + " in audio zone " + zoneId
+ " is currently sharing to primary zone, undo audio sharing in "
+ "primary zone before switching zone configuration");
}
if (mCarAudioMirrorRequestHandler.isMirrorAudioEnabled()
&& mCarAudioMirrorRequestHandler.isMirrorEnabledForZone(zoneId)) {
throw new IllegalStateException("Audio zone " + zoneId + " is currently in a mirroring"
+ " configuration, undo audio mirroring before switching zone configuration");
}
}
private boolean handleSwitchZoneConfig(CarAudioZoneConfigInfo zoneConfig) {
int zoneId = zoneConfig.getZoneId();
CarAudioZone zone;
synchronized (mImplLock) {
zone = getCarAudioZoneLocked(zoneId);
}
if (zone.isCurrentZoneConfig(zoneConfig)) {
Slogf.w(TAG, "handleSwitchZoneConfig switch current zone configuration");
return true;
}
CarOccupantZoneManager.OccupantZoneInfo info =
mOccupantZoneService.getOccupantForAudioZoneId(zoneId);
if (mMediaRequestHandler.isMediaAudioAllowedInPrimaryZone(info)) {
Slogf.w(TAG, "handleSwitchZoneConfig failed, occupant %s in audio zone %d is "
+ "currently sharing to primary zone, undo audio sharing in primary "
+ "zone before switching zone configuration", info, zoneId);
return false;
}
if (mCarAudioMirrorRequestHandler.isMirrorAudioEnabled()
&& mCarAudioMirrorRequestHandler.isMirrorEnabledForZone(zoneId)) {
Slogf.w(TAG, "handleSwitchZoneConfig failed, audio zone %d is currently in a mirroring"
+ "configuration, undo audio mirroring before switching zone configuration",
zoneId);
return false;
}
synchronized (mImplLock) {
int userId = getUserIdForZoneLocked(zoneId);
if (userId == UserManagerHelper.USER_NULL) {
Slogf.w(TAG, "handleSwitchZoneConfig failed, audio zone configuration switching "
+ "not allowed for unassigned audio zone %d", zoneId);
return false;
}
List<AudioFocusInfo> pendingFocusInfos =
mFocusHandler.transientlyLoseAllFocusInZone(zoneId);
boolean succeeded = true;
CarAudioZoneConfig prevZoneConfig = zone.getCurrentCarAudioZoneConfig();
try {
zone.setCurrentCarZoneConfig(zoneConfig);
setUserIdDeviceAffinitiesLocked(zone, userId, zoneId);
} catch (IllegalStateException e) {
zone.setCurrentCarZoneConfig(prevZoneConfig.getCarAudioZoneConfigInfo());
succeeded = false;
}
mFocusHandler.reevaluateAndRegainAudioFocusList(pendingFocusInfos);
return succeeded;
}
}
void setAudioEnabled(boolean isAudioEnabled) {
Slogf.i(TAG, "Setting isAudioEnabled to %b", isAudioEnabled);
mFocusHandler.setRestrictFocus(/* isFocusRestricted= */ !isAudioEnabled);
if (mUseCarVolumeGroupMuting) {
mCarVolumeGroupMuting.setRestrictMuting(/* isMutingRestricted= */ !isAudioEnabled);
}
// TODO(b/176258537) if not using group volume, then set master mute accordingly
}
private void enforcePermission(String permissionName) {
if (mContext.checkCallingOrSelfPermission(permissionName)
!= PackageManager.PERMISSION_GRANTED) {
throw new SecurityException("requires permission " + permissionName);
}
}
private void requireDynamicRouting() {
Preconditions.checkState(mUseDynamicRouting, "Dynamic routing is required");
}
private void requireAudioMirroring() {
Preconditions.checkState(mCarAudioMirrorRequestHandler.isMirrorAudioEnabled(),
"Audio zones mirroring is required");
}
private void requireVolumeGroupMuting() {
Preconditions.checkState(mUseCarVolumeGroupMuting,
"Car Volume Group Muting is required");
}
private void requireVolumeGroupEvents() {
Preconditions.checkState(mUseCarVolumeGroupEvents,
"Car Volume Group Event is required");
}
private void requireValidFadeRange(float value) {
Preconditions.checkArgumentInRange(value, -1f, 1f, "Fade");
}
private void requireValidBalanceRange(float value) {
Preconditions.checkArgumentInRange(value, -1f, 1f, "Balance");
}
@GuardedBy("mImplLock")
private void requiredOccupantZoneMappingDisabledLocked() {
if (isOccupantZoneMappingAvailableLocked()) {
throw new IllegalStateException(
"UID based routing is not supported while using occupant zone mapping");
}
}
@AudioContext int getSuggestedAudioContextForZone(int zoneId) {
if (!isAudioZoneIdValid(zoneId)) {
return CarAudioContext.getInvalidContext();
}
CarVolume carVolume;
synchronized (mImplLock) {
carVolume = mCarVolume;
}
return carVolume.getSuggestedAudioContextAndSaveIfFound(
getAllActiveAttributesForZone(zoneId), getCallStateForZone(zoneId),
getActiveHalAudioAttributesForZone(zoneId));
}
private List<AudioAttributes> getActiveHalAudioAttributesForZone(int zoneId) {
if (mHalAudioFocus == null) {
return new ArrayList<>(0);
}
return mHalAudioFocus.getActiveAudioAttributesForZone(zoneId);
}
/**
* 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 = INVALID_VOLUME_GROUP_ID;
for (int i = 0; i < CarAudioDynamicRouting.STREAM_TYPES.length; i++) {
if (streamType == CarAudioDynamicRouting.STREAM_TYPES[i]) {
groupId = i;
break;
}
}
return groupId;
}
private void handleOccupantZoneUserChanged() {
int driverUserId = mOccupantZoneService.getDriverUserId();
synchronized (mImplLock) {
if (!isOccupantZoneMappingAvailableLocked()) {
adjustZonesToUserIdLocked(driverUserId);
return;
}
int occupantZoneForDriver = getOccupantZoneIdForDriver();
Set<Integer> assignedZones = new HashSet<Integer>();
for (int index = 0; index < mAudioZoneIdToOccupantZoneIdMapping.size(); index++) {
int audioZoneId = mAudioZoneIdToOccupantZoneIdMapping.keyAt(index);
int occupantZoneId = mAudioZoneIdToOccupantZoneIdMapping.get(audioZoneId);
assignedZones.add(audioZoneId);
updateUserForOccupantZoneLocked(occupantZoneId, audioZoneId, driverUserId,
occupantZoneForDriver);
}
assignMissingZonesToDriverLocked(driverUserId, assignedZones);
}
restoreVolumeGroupMuteState();
}
private void restoreVolumeGroupMuteState() {
if (!mUseCarVolumeGroupMuting) {
return;
}
mCarVolumeGroupMuting.carMuteChanged();
}
@GuardedBy("mImplLock")
private void assignMissingZonesToDriverLocked(@UserIdInt int driverUserId,
Set<Integer> assignedZones) {
for (int i = 0; i < mCarAudioZones.size(); i++) {
CarAudioZone zone = mCarAudioZones.valueAt(i);
if (assignedZones.contains(zone.getId())) {
continue;
}
assignUserIdToAudioZoneLocked(zone, driverUserId);
}
}
@GuardedBy("mImplLock")
private void adjustZonesToUserIdLocked(@UserIdInt int userId) {
for (int i = 0; i < mCarAudioZones.size(); i++) {
CarAudioZone zone = mCarAudioZones.valueAt(i);
assignUserIdToAudioZoneLocked(zone, userId);
}
}
@GuardedBy("mImplLock")
private void assignUserIdToAudioZoneLocked(CarAudioZone zone, @UserIdInt int userId) {
if (userId == getUserIdForZoneLocked(zone.getId())) {
Slogf.d(TAG, "assignUserIdToAudioZone userId(%d) already assigned to audioZoneId(%d)",
userId, zone.getId());
return;
}
Slogf.d(TAG, "assignUserIdToAudioZone assigning userId(%d) to audioZoneId(%d)",
userId, zone.getId());
zone.updateVolumeGroupsSettingsForUser(userId);
mFocusHandler.updateUserForZoneId(zone.getId(), userId);
setUserIdForAudioZoneLocked(userId, zone.getId());
}
@GuardedBy("mImplLock")
private boolean isOccupantZoneMappingAvailableLocked() {
return mAudioZoneIdToOccupantZoneIdMapping.size() > 0;
}
@GuardedBy("mImplLock")
private void updateUserForOccupantZoneLocked(int occupantZoneId, int audioZoneId,
@UserIdInt int driverUserId, int occupantZoneForDriver) {
CarAudioZone audioZone = getCarAudioZoneLocked(audioZoneId);
int userId = mOccupantZoneService.getUserForOccupant(occupantZoneId);
int prevUserId = getUserIdForZoneLocked(audioZoneId);
if (userId == prevUserId) {
Slogf.d(TAG, "updateUserForOccupantZone userId(%d) already assigned to audioZoneId(%d)",
userId, audioZoneId);
return;
}
Slogf.d(TAG, "updateUserForOccupantZone assigning userId(%d) to audioZoneId(%d)",
userId, audioZoneId);
// If the user has changed, be sure to remove from current routing
// This would be true even if the new user is UserManagerHelper.USER_NULL,
// as that indicates the user has logged out.
removeUserIdDeviceAffinitiesLocked(prevUserId);
if (userId == UserManagerHelper.USER_NULL) {
// Reset zone back to driver user id
resetZoneToDefaultUser(audioZone, driverUserId);
setUserIdForAudioZoneLocked(userId, audioZoneId);
return;
}
// Only set user id device affinities for driver when it is the driver's occupant zone
if (userId != driverUserId || occupantZoneId == occupantZoneForDriver) {
setUserIdDeviceAffinitiesLocked(audioZone, userId, audioZoneId);
}
audioZone.updateVolumeGroupsSettingsForUser(userId);
mFocusHandler.updateUserForZoneId(audioZoneId, userId);
setUserIdForAudioZoneLocked(userId, audioZoneId);
}
private int getOccupantZoneIdForDriver() {
List<CarOccupantZoneManager.OccupantZoneInfo> occupantZoneInfos =
mOccupantZoneService.getAllOccupantZones();
for (CarOccupantZoneManager.OccupantZoneInfo info: occupantZoneInfos) {
if (info.occupantType == CarOccupantZoneManager.OCCUPANT_TYPE_DRIVER) {
return info.zoneId;
}
}
return CarOccupantZoneManager.OccupantZoneInfo.INVALID_ZONE_ID;
}
@GuardedBy("mImplLock")
private void setUserIdDeviceAffinitiesLocked(CarAudioZone zone, @UserIdInt int userId,
int audioZoneId) {
List<AudioDeviceInfo> infos = zone.getCurrentAudioDeviceInfosSupportingDynamicMix();
if (!infos.isEmpty() && !mAudioPolicy.setUserIdDeviceAffinity(userId, infos)) {
throw new IllegalStateException(String.format(
"setUserIdDeviceAffinity for userId %d in zone %d Failed,"
+ " could not set audio routing.",
userId, audioZoneId));
}
}
private void resetZoneToDefaultUser(CarAudioZone zone, @UserIdInt int driverUserId) {
resetCarZonesAudioFocus(zone.getId(), driverUserId);
zone.updateVolumeGroupsSettingsForUser(driverUserId);
}
private void resetCarZonesAudioFocus(int audioZoneId, @UserIdInt int driverUserId) {
mFocusHandler.updateUserForZoneId(audioZoneId, driverUserId);
}
@GuardedBy("mImplLock")
private void removeUserIdDeviceAffinitiesLocked(@UserIdInt int userId) {
Slogf.d(TAG, "removeUserIdDeviceAffinities(%d) Succeeded", userId);
if (userId == UserManagerHelper.USER_NULL) {
return;
}
if (!mAudioPolicy.removeUserIdDeviceAffinity(userId)) {
Slogf.e(TAG, "removeUserIdDeviceAffinities(%d) Failed", userId);
return;
}
}
@VisibleForTesting
@UserIdInt int getUserIdForZone(int audioZoneId) {
synchronized (mImplLock) {
return getUserIdForZoneLocked(audioZoneId);
}
}
@GuardedBy("mImplLock")
private @UserIdInt int getUserIdForZoneLocked(int audioZoneId) {
return mAudioZoneIdToUserIdMapping.get(audioZoneId, UserManagerHelper.USER_NULL);
}
@GuardedBy("mImplLock")
private void setUserIdForAudioZoneLocked(@UserIdInt int userId, int audioZoneId) {
mAudioZoneIdToUserIdMapping.put(audioZoneId, userId);
}
@GuardedBy("mImplLock")
private AudioControlWrapper getAudioControlWrapperLocked() {
if (mAudioControlWrapper == null) {
mAudioControlWrapper = AudioControlFactory.newAudioControl();
mAudioControlWrapper.linkToDeath(this::audioControlDied);
}
return mAudioControlWrapper;
}
private void resetHalAudioFocus() {
if (mHalAudioFocus != null) {
mHalAudioFocus.reset();
mHalAudioFocus.registerFocusListener();
}
}
private void resetHalAudioGain() {
if (mCarAudioGainMonitor != null) {
mCarAudioGainMonitor.reset();
mCarAudioGainMonitor.registerAudioGainListener(mHalAudioGainCallback);
}
}
private void resetHalAudioModuleChange() {
if (mCarAudioModuleChangeMonitor != null) {
mCarAudioModuleChangeMonitor.setModuleChangeCallback(mHalAudioModuleChangeCallback);
}
}
private void handleAudioDeviceGainsChangedLocked(
List<Integer> halReasons, List<CarAudioGainConfigInfo> gains) {
mCarAudioGainMonitor.handleAudioDeviceGainsChanged(halReasons, gains);
}
private void handleAudioPortsChangedLocked(List<HalAudioDeviceInfo> deviceInfos) {
mCarAudioModuleChangeMonitor.handleAudioPortsChanged(deviceInfos);
}
private void audioControlDied() {
resetHalAudioFocus();
resetHalAudioGain();
resetHalAudioModuleChange();
}
boolean isAudioZoneIdValid(int zoneId) {
synchronized (mImplLock) {
return mCarAudioZones.contains(zoneId);
}
}
private CarAudioZone getCarAudioZone(int zoneId) {
synchronized (mImplLock) {
return getCarAudioZoneLocked(zoneId);
}
}
@GuardedBy("mImplLock")
private CarAudioZone getCarAudioZoneLocked(int zoneId) {
checkAudioZoneIdLocked(zoneId);
return mCarAudioZones.get(zoneId);
}
private void checkAudioZoneId(int zoneId) {
synchronized (mImplLock) {
checkAudioZoneIdLocked(zoneId);
}
}
@GuardedBy("mImplLock")
private void checkAudioZoneIdLocked(int zoneId) {
Preconditions.checkArgument(mCarAudioZones.contains(zoneId),
"Invalid audio zone Id " + zoneId);
}
int getVolumeGroupIdForAudioContext(int zoneId, int suggestedContext) {
synchronized (mImplLock) {
return getVolumeGroupIdForAudioContextLocked(zoneId, suggestedContext);
}
}
/**
* Resets the last selected volume context.
*/
@ExcludeFromCodeCoverageGeneratedReport(reason = DEBUGGING_CODE)
public void resetSelectedVolumeContext() {
enforcePermission(Car.PERMISSION_CAR_CONTROL_AUDIO_VOLUME);
synchronized (mImplLock) {
mCarVolume.resetSelectedVolumeContext();
mCarAudioPlaybackCallback.resetStillActiveContexts();
}
}
@VisibleForTesting
CarAudioContext getCarAudioContext() {
synchronized (mImplLock) {
return mCarAudioContext;
}
}
@VisibleForTesting
void requestAudioFocusForTest(AudioFocusInfo audioFocusInfo, int audioFocusResult) {
mFocusHandler.onAudioFocusRequest(audioFocusInfo, audioFocusResult);
}
int getZoneIdForAudioFocusInfo(AudioFocusInfo focusInfo) {
if (isAllowedInPrimaryZone(focusInfo)) {
return PRIMARY_AUDIO_ZONE;
}
int audioZoneId;
synchronized (mImplLock) {
audioZoneId = getZoneIdForUidLocked(focusInfo.getClientUid());
}
if (isAudioZoneMirroringEnabledForMedia(focusInfo, audioZoneId)) {
long requestId = mCarAudioMirrorRequestHandler.getRequestIdForAudioZone(audioZoneId);
int[] mirrorZones = mCarAudioMirrorRequestHandler.getMirrorAudioZonesForRequest(
requestId);
return ArrayUtils.isEmpty(mirrorZones) ? audioZoneId : mirrorZones[0];
}
return audioZoneId;
}
private boolean isAllowedInPrimaryZone(AudioFocusInfo focusInfo) {
boolean isMedia = CarAudioContext.AudioAttributesWrapper.audioAttributeMatches(
CarAudioContext.getAudioAttributeFromUsage(USAGE_MEDIA),
focusInfo.getAttributes());
return isMedia && mMediaRequestHandler
.isMediaAudioAllowedInPrimaryZone(mOccupantZoneService
.getOccupantZoneForUser(UserHandle
.getUserHandleForUid(focusInfo.getClientUid())));
}
private boolean isAudioZoneMirroringEnabledForMedia(AudioFocusInfo focusInfo, int zoneId) {
boolean isMedia = CarAudioContext.AudioAttributesWrapper.audioAttributeMatches(
CarAudioContext.getAudioAttributeFromUsage(USAGE_MEDIA),
focusInfo.getAttributes());
return isMedia && mCarAudioMirrorRequestHandler.isMirrorEnabledForZone(zoneId);
}
private List<AudioAttributes> getAllActiveAttributesForZone(int zoneId) {
synchronized (mImplLock) {
return mCarAudioPlaybackCallback.getAllActiveAudioAttributesForZone(zoneId);
}
}
List<CarVolumeGroupInfo> getMutedVolumeGroups(int zoneId) {
List<CarVolumeGroupInfo> mutedGroups = new ArrayList<>();
if (!mUseCarVolumeGroupMuting || !isAudioZoneIdValid(zoneId)) {
return mutedGroups;
}
synchronized (mImplLock) {
int groupCount = getCarAudioZoneLocked(zoneId).getCurrentVolumeGroupCount();
for (int groupId = 0; groupId < groupCount; groupId++) {
CarVolumeGroup group = getCarVolumeGroupLocked(zoneId, groupId);
if (!group.isMuted()) {
continue;
}
mutedGroups.add(group.getCarVolumeGroupInfo());
}
}
return mutedGroups;
}
List<AudioAttributes> getActiveAudioAttributesForZone(int zoneId) {
List<AudioAttributes> activeAudioAttributes = new ArrayList<>();
activeAudioAttributes.addAll(getAllActiveAttributesForZone(zoneId));
activeAudioAttributes.addAll(getActiveHalAudioAttributesForZone(zoneId));
return activeAudioAttributes;
}
static final class SystemClockWrapper {
public long uptimeMillis() {
return SystemClock.uptimeMillis();
}
}
void onAudioVolumeGroupChanged(int zoneId, String groupName, int flags) {
int callbackFlags = flags;
synchronized (mImplLock) {
CarVolumeGroup group = getCarVolumeGroupLocked(zoneId, groupName);
if (group == null) {
Slogf.w(TAG, "onAudioVolumeGroupChanged reported on unmanaged group (%s)",
groupName);
return;
}
int volumeEventFlags = group.onAudioVolumeGroupChanged(flags);
if (CarVolumeEventFlag.hasInvalidFlag(volumeEventFlags)) {
Slogf.e(TAG, "onAudioVolumeGroupChanged has invalid flag(%s)",
CarVolumeEventFlag.flagsToString(volumeEventFlags));
return;
}
int eventTypes = (EVENT_TYPE_VOLUME_MIN_INDEX_CHANGED
| EVENT_TYPE_VOLUME_MAX_INDEX_CHANGED);
if ((volumeEventFlags & CarVolumeEventFlag.FLAG_EVENT_VOLUME_CHANGE) != 0) {
callbackGroupVolumeChange(zoneId, group.getId(), /* flags= */ 0);
volumeEventFlags &= ~CarVolumeEventFlag.FLAG_EVENT_VOLUME_CHANGE;
eventTypes |= EVENT_TYPE_VOLUME_GAIN_INDEX_CHANGED;
if (mUseDynamicRouting && !isPlaybackOnVolumeGroupActive(zoneId, group.getId())) {
callbackFlags |= FLAG_PLAY_SOUND;
}
}
if ((volumeEventFlags & CarVolumeEventFlag.FLAG_EVENT_VOLUME_MUTE) != 0) {
callbackGroupMuteChanged(zoneId, group.getId(), /* flags= */ 0);
volumeEventFlags &= ~CarVolumeEventFlag.FLAG_EVENT_VOLUME_MUTE;
eventTypes |= EVENT_TYPE_MUTE_CHANGED;
}
callbackVolumeGroupEvent(List.of(convertVolumeChangeToEvent(
getVolumeGroupInfo(zoneId, group.getId()), callbackFlags, eventTypes)));
}
}
private static final class AudioFocusStackRequest {
private final AudioFocusStack mStack;
private final int mOriginalZoneId;
AudioFocusStackRequest(AudioFocusStack stack, int originalZoneId) {
mOriginalZoneId = originalZoneId;
mStack = stack;
}
}
}