/* * Copyright (C) 2018 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.feature.Flags.carAudioDynamicDevices; import static android.car.feature.Flags.carAudioMinMaxActivationVolume; import static android.car.feature.Flags.carAudioMuteAmbiguity; import static android.car.media.CarVolumeGroupEvent.EVENT_TYPE_ATTENUATION_CHANGED; import static android.car.media.CarVolumeGroupEvent.EVENT_TYPE_MUTE_CHANGED; import static android.car.media.CarVolumeGroupEvent.EVENT_TYPE_VOLUME_BLOCKED_CHANGED; import static android.car.media.CarVolumeGroupEvent.EVENT_TYPE_VOLUME_GAIN_INDEX_CHANGED; import static android.media.AudioDeviceInfo.TYPE_AUX_LINE; import static android.media.AudioDeviceInfo.TYPE_BLE_BROADCAST; import static android.media.AudioDeviceInfo.TYPE_BLE_HEADSET; import static android.media.AudioDeviceInfo.TYPE_BLE_SPEAKER; import static android.media.AudioDeviceInfo.TYPE_BLUETOOTH_A2DP; import static android.media.AudioDeviceInfo.TYPE_BUILTIN_SPEAKER; import static android.media.AudioDeviceInfo.TYPE_BUS; import static android.media.AudioDeviceInfo.TYPE_HDMI; import static android.media.AudioDeviceInfo.TYPE_USB_ACCESSORY; import static android.media.AudioDeviceInfo.TYPE_USB_DEVICE; import static android.media.AudioDeviceInfo.TYPE_USB_HEADSET; import static android.media.AudioDeviceInfo.TYPE_WIRED_HEADPHONES; import static android.media.AudioDeviceInfo.TYPE_WIRED_HEADSET; import static com.android.car.audio.CarActivationVolumeConfig.ActivationVolumeInvocationType; import static com.android.car.audio.hal.HalAudioGainCallback.reasonToString; import static com.android.car.internal.ExcludeFromCodeCoverageGeneratedReport.BOILERPLATE_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.builtin.util.Slogf; import android.car.media.CarVolumeGroupInfo; import android.media.AudioAttributes; import android.media.AudioDeviceAttributes; import android.media.AudioDeviceInfo; import android.media.AudioManager; import android.os.UserHandle; import android.util.ArrayMap; import android.util.ArraySet; import android.util.SparseArray; import android.util.proto.ProtoOutputStream; import com.android.car.CarLog; import com.android.car.audio.CarAudioContext.AudioContext; import com.android.car.audio.CarAudioDumpProto.CarAudioZoneConfigProto; import com.android.car.audio.CarAudioDumpProto.CarVolumeGroupProto; import com.android.car.audio.CarAudioDumpProto.CarVolumeGroupProto.ContextToAddress; import com.android.car.audio.CarAudioDumpProto.CarVolumeGroupProto.GainInfo; import com.android.car.audio.hal.HalAudioDeviceInfo; import com.android.car.internal.ExcludeFromCodeCoverageGeneratedReport; import com.android.car.internal.util.DebugUtils; import com.android.car.internal.util.IndentingPrintWriter; import com.android.internal.annotations.GuardedBy; import com.android.internal.util.Preconditions; import java.util.ArrayList; import java.util.Arrays; import java.util.List; import java.util.Objects; import java.util.Set; /** * A class encapsulates a volume group in car. * * Interface holding volume interface APIs and also common code for: * * -volume groups using {@link AudioManager#setAudioPortGain} to control the volume * while the audioserver resource config_useFixedVolume is set. * * -volume groups relying on audioserver to control the volume and access using * {@link AudioManager#setVolumeIndexForAttributes(AudioAttributes, int, int)} and all other * related volume APIs. * Gain may either be controlled on hardware amplifier using Audio HAL setaudioPortConfig if the * correlated audio device port defines a gain controller with attribute name="useForVolume" set * or in software using the port id in Audio flinger. * Gains are set only when activity is detected on the given audio device port (Mixer thread, or * {@link android.media.HwAudioSource} realized through a software bridge or hardware bridge. * */ /* package */ abstract class CarVolumeGroup { public static final int UNINITIALIZED = -1; private final boolean mUseCarVolumeGroupMute; private final boolean mHasCriticalAudioContexts; private final CarAudioSettings mSettingsManager; protected final int mId; private final String mName; protected final int mZoneId; protected final int mConfigId; protected final SparseArray mContextToDevices; protected final Object mLock = new Object(); private final CarAudioContext mCarAudioContext; private final CarActivationVolumeConfig mCarActivationVolumeConfig; @GuardedBy("mLock") protected final SparseArray mContextToAddress; @GuardedBy("mLock") protected final ArrayMap mAddressToCarAudioDeviceInfo; @GuardedBy("mLock") protected int mStoredGainIndex; @GuardedBy("mLock") protected int mCurrentGainIndex = UNINITIALIZED; /** * Mute state for requests coming from clients. See {@link #mIsHalMuted} for state of requests * coming from HAL. */ @GuardedBy("mLock") protected boolean mIsMuted; @GuardedBy("mLock") protected @UserIdInt int mUserId = UserHandle.CURRENT.getIdentifier(); /** * Attenuated gain is set to {@link #UNINITIALIZED} till attenuation explicitly reported by * {@link com.android.car.audio.hal.HalAudioGainCallback#onAudioDeviceGainsChanged} for one or * more {@link android.hardware.automotive.audiocontrol.Reasons}. When the reason is cleared, * it returns back to {@link #UNINITIALIZED}. */ @GuardedBy("mLock") protected int mAttenuatedGainIndex = UNINITIALIZED; /** * Limitation gain is set to max gain value till limitation explicitly reported by {@link * com.android.car.audio.hal.HalAudioGainCallback#onAudioDeviceGainsChanged} for one or more * {@link android.hardware.automotive.audiocontrol.Reasons}. When the reason is cleared, it * returns back to max. */ @GuardedBy("mLock") protected int mLimitedGainIndex; /** * Blocked gain is set to {@link #UNINITIALIZED} till blocking case explicitly reported by * {@link com.android.car.audio.hal.HalAudioGainCallback#onAudioDeviceGainsChanged} for one or * more {@link android.hardware.automotive.audiocontrol.Reasons}. When the reason is cleared, * it returns back to {@link #UNINITIALIZED}. */ @GuardedBy("mLock") protected int mBlockedGainIndex = UNINITIALIZED; /** * The default state of HAL mute is {@code false} until HAL explicitly reports through * {@link com.android.car.audio.hal.HalAudioGainCallback#onAudioDeviceGainsChanged} for one or * more {@link android.hardware.automotive.audiocontrol.Reasons}. When the reason * is cleared, it is reset. See {@link #mIsMuted} for state of requests coming from clients. */ @GuardedBy("mLock") private boolean mIsHalMuted = false; /** * Reasons list currently reported for this port by {@link * com.android.car.audio.hal.HalAudioGainCallback#onAudioDeviceGainsChanged}. */ protected List mReasons = new ArrayList<>(); protected CarVolumeGroup(CarAudioContext carAudioContext, CarAudioSettings settingsManager, SparseArray contextToDevices, int zoneId, int configId, int volumeGroupId, String name, boolean useCarVolumeGroupMute, CarActivationVolumeConfig carActivationVolumeConfig) { mSettingsManager = settingsManager; mCarAudioContext = carAudioContext; mContextToDevices = contextToDevices; mZoneId = zoneId; mConfigId = configId; mId = volumeGroupId; mName = Objects.requireNonNull(name, "Volume group name cannot be null"); mUseCarVolumeGroupMute = useCarVolumeGroupMute; mContextToAddress = new SparseArray<>(contextToDevices.size()); mAddressToCarAudioDeviceInfo = new ArrayMap<>(contextToDevices.size()); List volumeAttributes = new ArrayList<>(); for (int index = 0; index < contextToDevices.size(); index++) { int context = contextToDevices.keyAt(index); CarAudioDeviceInfo info = contextToDevices.valueAt(index); List audioAttributes = Arrays.asList(mCarAudioContext.getAudioAttributesForContext(context)); volumeAttributes.addAll(audioAttributes); mContextToAddress.put(context, info.getAddress()); mAddressToCarAudioDeviceInfo.put(info.getAddress(), info); } mHasCriticalAudioContexts = containsCriticalAttributes(volumeAttributes); mCarActivationVolumeConfig = Objects.requireNonNull(carActivationVolumeConfig, "Activation volume config can not be null"); } void init() { synchronized (mLock) { mStoredGainIndex = mSettingsManager.getStoredVolumeGainIndexForUser( mUserId, mZoneId, mConfigId, mId); updateCurrentGainIndexLocked(); } } @GuardedBy("mLock") protected void setBlockedLocked(int blockedIndex) { mBlockedGainIndex = blockedIndex; } @GuardedBy("mLock") protected void resetBlockedLocked() { setBlockedLocked(UNINITIALIZED); } @GuardedBy("mLock") protected boolean isBlockedLocked() { return mBlockedGainIndex != UNINITIALIZED; } @GuardedBy("mLock") protected void setLimitLocked(int limitIndex) { int minActivationGainIndex = getMinActivationGainIndex(); if (limitIndex < minActivationGainIndex) { Slogf.w(CarLog.TAG_AUDIO, "Limit cannot be set lower than min activation volume index", minActivationGainIndex); } mLimitedGainIndex = limitIndex; } @GuardedBy("mLock") protected void resetLimitLocked() { setLimitLocked(getMaxGainIndex()); } @GuardedBy("mLock") protected boolean isLimitedLocked() { return mLimitedGainIndex != getMaxGainIndex(); } @GuardedBy("mLock") protected boolean isOverLimitLocked() { return isOverLimitLocked(mCurrentGainIndex); } @GuardedBy("mLock") protected boolean isOverLimitLocked(int index) { return isLimitedLocked() && (index > mLimitedGainIndex); } @GuardedBy("mLock") protected void setAttenuatedGainLocked(int attenuatedGainIndex) { mAttenuatedGainIndex = attenuatedGainIndex; } @GuardedBy("mLock") protected void resetAttenuationLocked() { setAttenuatedGainLocked(UNINITIALIZED); } @GuardedBy("mLock") protected boolean isAttenuatedLocked() { return mAttenuatedGainIndex != UNINITIALIZED; } @GuardedBy("mLock") private void setHalMuteLocked(boolean mute) { mIsHalMuted = mute; } @GuardedBy("mLock") protected boolean isHalMutedLocked() { return mIsHalMuted; } boolean isHalMuted() { synchronized (mLock) { return isHalMutedLocked(); } } void setBlocked(int blockedIndex) { synchronized (mLock) { setBlockedLocked(blockedIndex); } } void resetBlocked() { synchronized (mLock) { resetBlockedLocked(); } } boolean isBlocked() { synchronized (mLock) { return isBlockedLocked(); } } void setLimit(int limitIndex) { synchronized (mLock) { setLimitLocked(limitIndex); } } void resetLimit() { synchronized (mLock) { resetLimitLocked(); } } boolean isLimited() { synchronized (mLock) { return isLimitedLocked(); } } boolean isOverLimit() { synchronized (mLock) { return isOverLimitLocked(); } } void setAttenuatedGain(int attenuatedGainIndex) { synchronized (mLock) { setAttenuatedGainLocked(attenuatedGainIndex); } } void resetAttenuation() { synchronized (mLock) { resetAttenuationLocked(); } } boolean isAttenuated() { synchronized (mLock) { return isAttenuatedLocked(); } } @Nullable CarAudioDeviceInfo getCarAudioDeviceInfoForAddress(String address) { synchronized (mLock) { return mAddressToCarAudioDeviceInfo.get(address); } } int[] getContexts() { int[] carAudioContexts = new int[mContextToDevices.size()]; for (int i = 0; i < mContextToDevices.size(); i++) { carAudioContexts[i] = mContextToDevices.keyAt(i); } return carAudioContexts; } protected AudioAttributes[] getAudioAttributesForContext(int context) { return mCarAudioContext.getAudioAttributesForContext(context); } /** * Returns the id of the volume group. *

Note that all clients are already developed in the way that when they get the number of * volume group, they will then address a given volume group using its id as if the id was the * index of the array of group (aka 0 to length - 1). */ int getId() { return mId; } String getName() { return mName; } /** * Returns the devices address for the given context * or {@code null} if the context does not exist in the volume group */ @Nullable String getAddressForContext(int audioContext) { synchronized (mLock) { return mContextToAddress.get(audioContext); } } /** * Returns the audio devices for the given context * or {@code null} if the context does not exist in the volume group */ @Nullable AudioDeviceAttributes getAudioDeviceForContext(int audioContext) { String address = getAddressForContext(audioContext); if (address == null) { return null; } CarAudioDeviceInfo info; synchronized (mLock) { info = mAddressToCarAudioDeviceInfo.get(address); } if (info == null) { return null; } return info.getAudioDevice(); } @AudioContext List getContextsForAddress(@NonNull String address) { List carAudioContexts = new ArrayList<>(); synchronized (mLock) { for (int i = 0; i < mContextToAddress.size(); i++) { String value = mContextToAddress.valueAt(i); if (address.equals(value)) { carAudioContexts.add(mContextToAddress.keyAt(i)); } } } return carAudioContexts; } List getAddresses() { synchronized (mLock) { return new ArrayList<>(mAddressToCarAudioDeviceInfo.keySet()); } } List getAllSupportedUsagesForAddress(@NonNull String address) { List supportedUsagesForAddress = new ArrayList<>(); List contextsForAddress = getContextsForAddress(address); for (int contextIndex = 0; contextIndex < contextsForAddress.size(); contextIndex++) { int contextId = contextsForAddress.get(contextIndex); AudioAttributes[] attributes = mCarAudioContext.getAudioAttributesForContext(contextId); for (int attrIndex = 0; attrIndex < attributes.length; attrIndex++) { int usage = attributes[attrIndex].getSystemUsage(); if (!supportedUsagesForAddress.contains(usage)) { supportedUsagesForAddress.add(usage); } } } return supportedUsagesForAddress; } abstract int getMaxGainIndex(); abstract int getMinGainIndex(); int getMaxActivationGainIndex() { int maxGainIndex = getMaxGainIndex(); int minGainIndex = getMinGainIndex(); return minGainIndex + (int) Math.round( mCarActivationVolumeConfig.getMaxActivationVolumePercentage() / 100.0 * (maxGainIndex - minGainIndex)); } int getMinActivationGainIndex() { int maxGainIndex = getMaxGainIndex(); int minGainIndex = getMinGainIndex(); return minGainIndex + (int) Math.round( mCarActivationVolumeConfig.getMinActivationVolumePercentage() / 100.0 * (maxGainIndex - minGainIndex)); } int getActivationVolumeInvocationType() { return mCarActivationVolumeConfig.getInvocationType(); } int getCurrentGainIndex() { synchronized (mLock) { if (isMutedLocked()) { return getMinGainIndex(); } return getRestrictedGainForIndexLocked(getCurrentGainIndexLocked()); } } @GuardedBy("mLock") protected int getCurrentGainIndexLocked() { return mCurrentGainIndex; } @GuardedBy("mLock") protected int getRestrictedGainForIndexLocked(int index) { if (isBlockedLocked()) { return mBlockedGainIndex; } if (isOverLimitLocked()) { return mLimitedGainIndex; } if (isAttenuatedLocked()) { // Need to figure out if attenuation shall be hidden to end user // as while ducked from IAudioControl // TODO(b/) clarify in case of volume adjustment if the reference index is the // ducked index or the current index. Taking current may lead to gap of index > 1. return mAttenuatedGainIndex; } return index; } /** * Sets the gain on this group, gain will be set on all devices within volume group. */ void setCurrentGainIndex(int gainIndex) { synchronized (mLock) { int currentgainIndex = gainIndex; Preconditions.checkArgument(isValidGainIndexLocked(gainIndex), "Gain out of range (%d:%d) index %d", getMinGainIndex(), getMaxGainIndex(), gainIndex); if (isBlockedLocked()) { // prevent any volume change while {@link IAudioGainCallback} reported block event. return; } if (isOverLimitLocked(currentgainIndex)) { currentgainIndex = mLimitedGainIndex; } if (isAttenuatedLocked()) { resetAttenuationLocked(); } // In case of attenuation/Limitation, requested index is now the new reference for // cached current index. mCurrentGainIndex = currentgainIndex; if (mIsMuted) { setMuteLocked(false); } setCurrentGainIndexLocked(mCurrentGainIndex); } } @GuardedBy("mLock") protected void setCurrentGainIndexLocked(int gainIndex) { storeGainIndexForUserLocked(gainIndex, mUserId); } boolean handleActivationVolume( @ActivationVolumeInvocationType int activationVolumeInvocationType) { if (!carAudioMinMaxActivationVolume() || (getActivationVolumeInvocationType() & activationVolumeInvocationType) == 0) { // Min/max activation volume is not invoked if the given invocation type is not allowed // for the volume group. return false; } boolean invokeVolumeGainIndexChanged = true; synchronized (mLock) { int minActivationGainIndex = getMinActivationGainIndex(); int maxActivationGainIndex = getMaxActivationGainIndex(); int curGainIndex = getCurrentGainIndexLocked(); int activationVolume; if (curGainIndex > maxActivationGainIndex) { activationVolume = maxActivationGainIndex; } else if (curGainIndex < minActivationGainIndex) { activationVolume = minActivationGainIndex; } else { return false; } if (isMutedLocked() || isBlockedLocked()) { invokeVolumeGainIndexChanged = false; } else { if (isOverLimitLocked(activationVolume)) { // Limit index is used as min activation gain index if limit is lower than min // activation gain index. invokeVolumeGainIndexChanged = !isOverLimitLocked(curGainIndex); } if (isAttenuatedLocked()) { // Attenuation state should be maintained and not reset for min/max activation. invokeVolumeGainIndexChanged = false; } } mCurrentGainIndex = activationVolume; setCurrentGainIndexLocked(mCurrentGainIndex); } return invokeVolumeGainIndexChanged; } boolean hasCriticalAudioContexts() { return mHasCriticalAudioContexts; } @Override @ExcludeFromCodeCoverageGeneratedReport(reason = BOILERPLATE_CODE) public String toString() { synchronized (mLock) { return "CarVolumeGroup id: " + mId + " currentGainIndex: " + mCurrentGainIndex + " contexts: " + Arrays.toString(getContexts()) + " addresses: " + String.join(", ", getAddresses()); } } @ExcludeFromCodeCoverageGeneratedReport(reason = DUMP_INFO) protected abstract void dumpLocked(IndentingPrintWriter writer); @ExcludeFromCodeCoverageGeneratedReport(reason = DUMP_INFO) void dump(IndentingPrintWriter writer) { synchronized (mLock) { writer.printf("CarVolumeGroup(%d)\n", mId); writer.increaseIndent(); writer.printf("Name(%s)\n", mName); writer.printf("Zone Id(%d)\n", mZoneId); writer.printf("Configuration Id(%d)\n", mConfigId); writer.printf("Is Muted(%b)\n", isMutedLocked()); writer.printf("UserId(%d)\n", mUserId); writer.printf("Persist Volume Group Mute(%b)\n", mSettingsManager.isPersistVolumeGroupMuteEnabled(mUserId)); dumpLocked(writer); writer.printf("Gain indexes (min / max / default / current): %d %d %d %d\n", getMinGainIndex(), getMaxGainIndex(), getDefaultGainIndex(), mCurrentGainIndex); writer.printf("Activation gain (min index / max index / invocation type): %d %d %d\n", getMinActivationGainIndex(), getMaxActivationGainIndex(), getActivationVolumeInvocationType()); for (int i = 0; i < mContextToAddress.size(); i++) { writer.printf("Context: %s -> Address: %s\n", mCarAudioContext.toString(mContextToAddress.keyAt(i)), mContextToAddress.valueAt(i)); } for (int i = 0; i < mContextToDevices.size(); i++) { CarAudioDeviceInfo info = mContextToDevices.valueAt(i); info.dump(writer); } writer.printf("Reported reasons:\n"); writer.increaseIndent(); for (int index = 0; index < mReasons.size(); index++) { int reason = mReasons.get(index); writer.printf("%s\n", reasonToString(reason)); } writer.decreaseIndent(); writer.printf("Gain infos:\n"); writer.increaseIndent(); writer.printf( "Blocked: %b%s\n", isBlockedLocked(), (isBlockedLocked() ? " (at: " + mBlockedGainIndex + ")" : "")); writer.printf( "Limited: %b%s\n", isLimitedLocked(), (isLimitedLocked() ? " (at: " + mLimitedGainIndex + ")" : "")); writer.printf( "Attenuated: %b%s\n", isAttenuatedLocked(), (isAttenuatedLocked() ? " (at: " + mAttenuatedGainIndex + ")" : "")); writer.printf("Muted by HAL: %b\n", isHalMutedLocked()); writer.decreaseIndent(); // Empty line for comfortable reading writer.println(); writer.decreaseIndent(); } } @ExcludeFromCodeCoverageGeneratedReport(reason = DUMP_INFO) void dumpProto(ProtoOutputStream proto) { long volumeGroupToken = proto.start(CarAudioZoneConfigProto.VOLUME_GROUPS); synchronized (mLock) { proto.write(CarVolumeGroupProto.ID, mId); proto.write(CarVolumeGroupProto.NAME, mName); proto.write(CarVolumeGroupProto.ZONE_ID, mZoneId); proto.write(CarVolumeGroupProto.CONFIG_ID, mConfigId); proto.write(CarVolumeGroupProto.MUTED, isMutedLocked()); proto.write(CarVolumeGroupProto.USER_ID, mUserId); proto.write(CarVolumeGroupProto.PERSIST_VOLUME_GROUP_MUTE_ENABLED, mSettingsManager.isPersistVolumeGroupMuteEnabled(mUserId)); long volumeGainToken = proto.start(CarVolumeGroupProto.VOLUME_GAIN); proto.write(CarAudioDumpProto.CarVolumeGain.MIN_GAIN_INDEX, getMinGainIndex()); proto.write(CarAudioDumpProto.CarVolumeGain.MAX_GAIN_INDEX, getMaxGainIndex()); proto.write(CarAudioDumpProto.CarVolumeGain.DEFAULT_GAIN_INDEX, getDefaultGainIndex()); proto.write(CarAudioDumpProto.CarVolumeGain.CURRENT_GAIN_INDEX, mCurrentGainIndex); proto.write(CarAudioDumpProto.CarVolumeGain.MIN_ACTIVATION_GAIN_INDEX, getMinActivationGainIndex()); proto.write(CarAudioDumpProto.CarVolumeGain.MAX_ACTIVATION_GAIN_INDEX, getMaxActivationGainIndex()); proto.write(CarAudioDumpProto.CarVolumeGain.ACTIVATION_INVOCATION_TYPE, getActivationVolumeInvocationType()); proto.end(volumeGainToken); for (int i = 0; i < mContextToAddress.size(); i++) { long contextToAddressMappingToken = proto.start(CarVolumeGroupProto .CONTEXT_TO_ADDRESS_MAPPINGS); proto.write(ContextToAddress.CONTEXT, mCarAudioContext.toString(mContextToAddress.keyAt(i))); proto.write(ContextToAddress.ADDRESS, mContextToAddress.valueAt(i)); proto.end(contextToAddressMappingToken); } for (int i = 0; i < mContextToDevices.size(); i++) { CarAudioDeviceInfo info = mContextToDevices.valueAt(i); info.dumpProto(CarVolumeGroupProto.CAR_AUDIO_DEVICE_INFOS, proto); } for (int index = 0; index < mReasons.size(); index++) { int reason = mReasons.get(index); proto.write(CarVolumeGroupProto.REPORTED_REASONS, reasonToString(reason)); } long gainInfoToken = proto.start(CarVolumeGroupProto.GAIN_INFOS); proto.write(GainInfo.BLOCKED, isBlockedLocked()); if (isBlockedLocked()) { proto.write(GainInfo.BLOCKED_GAIN_INDEX, mBlockedGainIndex); } proto.write(GainInfo.LIMITED, isLimitedLocked()); if (isLimitedLocked()) { proto.write(GainInfo.LIMITED_GAIN_INDEX, mLimitedGainIndex); } proto.write(GainInfo.ATTENUATED, isAttenuatedLocked()); if (isAttenuatedLocked()) { proto.write(GainInfo.ATTENUATED_GAIN_INDEX, mAttenuatedGainIndex); } proto.write(GainInfo.HAL_MUTED, isHalMutedLocked()); proto.write(GainInfo.IS_ACTIVE, isActive()); proto.end(gainInfoToken); } proto.end(volumeGroupToken); } void loadVolumesSettingsForUser(@UserIdInt int userId) { synchronized (mLock) { //Update the volume for the new user updateUserIdLocked(userId); //Update the current gain index updateCurrentGainIndexLocked(); setCurrentGainIndexLocked(getCurrentGainIndexLocked()); //Reset devices with current gain index updateGroupMuteLocked(); } } /** * Set the mute state of the Volume Group * * @param mute state requested * @return true if mute state has changed, false otherwiser (already set or change not allowed) */ boolean setMute(boolean mute) { synchronized (mLock) { // if hal muted the audio devices, then do not allow other incoming requests // to perform unmute. if (!mute && isHalMutedLocked()) { Slogf.e(CarLog.TAG_AUDIO, "Un-mute request cannot be processed due to active " + "hal mute restriction!"); return false; } applyMuteLocked(mute); return setMuteLocked(mute); } } @GuardedBy("mLock") protected boolean setMuteLocked(boolean mute) { boolean hasChanged = mIsMuted != mute; mIsMuted = mute; if (mSettingsManager.isPersistVolumeGroupMuteEnabled(mUserId)) { mSettingsManager.storeVolumeGroupMuteForUser(mUserId, mZoneId, mConfigId, mId, mute); } return hasChanged; } @GuardedBy("mLock") protected void applyMuteLocked(boolean mute) { } boolean isMuted() { synchronized (mLock) { return isMutedLocked(); } } @GuardedBy("mLock") protected boolean isMutedLocked() { // if either of the mute states is set, it results in group being muted. return isUserMutedLocked() || isHalMutedLocked(); } @GuardedBy("mLock") protected boolean isUserMutedLocked() { return mIsMuted; } @GuardedBy("mLock") protected boolean isFullyMutedLocked() { return isUserMutedLocked() || isHalMutedLocked() || isBlockedLocked(); } private static boolean containsCriticalAttributes(List volumeAttributes) { for (int index = 0; index < volumeAttributes.size(); index++) { if (CarAudioContext.isCriticalAudioAudioAttribute(volumeAttributes.get(index))) { return true; } } return false; } @GuardedBy("mLock") private void updateUserIdLocked(@UserIdInt int userId) { mUserId = userId; mStoredGainIndex = getCurrentGainIndexForUserLocked(); } @GuardedBy("mLock") private int getCurrentGainIndexForUserLocked() { int gainIndexForUser = mSettingsManager.getStoredVolumeGainIndexForUser(mUserId, mZoneId, mConfigId, mId); Slogf.i(CarLog.TAG_AUDIO, "updateUserId userId " + mUserId + " gainIndexForUser " + gainIndexForUser); return gainIndexForUser; } /** * Update the current gain index based on the stored gain index */ @GuardedBy("mLock") private void updateCurrentGainIndexLocked() { if (isValidGainIndexLocked(mStoredGainIndex)) { mCurrentGainIndex = mStoredGainIndex; } else { mCurrentGainIndex = getDefaultGainIndex(); } } protected boolean isValidGainIndex(int gainIndex) { synchronized (mLock) { return isValidGainIndexLocked(gainIndex); } } protected abstract boolean isValidGainIndexLocked(int gainIndex); protected abstract int getDefaultGainIndex(); @GuardedBy("mLock") private void storeGainIndexForUserLocked(int gainIndex, @UserIdInt int userId) { mSettingsManager.storeVolumeGainIndexForUser(userId, mZoneId, mConfigId, mId, gainIndex); } @GuardedBy("mLock") private void updateGroupMuteLocked() { if (!mUseCarVolumeGroupMute) { return; } if (!mSettingsManager.isPersistVolumeGroupMuteEnabled(mUserId)) { mIsMuted = false; return; } mIsMuted = mSettingsManager.getVolumeGroupMuteForUser(mUserId, mZoneId, mConfigId, mId); applyMuteLocked(isFullyMutedLocked()); } /** * Updates volume group states (index, mute, blocked etc) on callback from audio control hal. * *

If gain config info carries duplicate info, do not generate events (i.e. eventType = 0) * @param halReasons reasons for change to gain config info * @param gain updated gain config info * @return one or more of {@link android.car.media.CarVolumeGroupEvent.EventTypeEnum} or 0 for * duplicate gain config info */ int onAudioGainChanged(List halReasons, CarAudioGainConfigInfo gain) { int eventType = 0; int halIndex = gain.getVolumeIndex(); if (getCarAudioDeviceInfoForAddress(gain.getDeviceAddress()) == null || !isValidGainIndex(halIndex)) { Slogf.e(CarLog.TAG_AUDIO, "onAudioGainChanged invalid CarAudioGainConfigInfo: " + gain + " for group id: " + mId); return eventType; } synchronized (mLock) { int previousRestrictedIndex = getRestrictedGainForIndexLocked(mCurrentGainIndex); mReasons = new ArrayList<>(halReasons); boolean shouldBlock = CarAudioGainMonitor.shouldBlockVolumeRequest(halReasons); if ((shouldBlock != isBlockedLocked()) || (shouldBlock && (halIndex != mBlockedGainIndex))) { setBlockedLocked(shouldBlock ? halIndex : UNINITIALIZED); eventType |= EVENT_TYPE_VOLUME_BLOCKED_CHANGED; } boolean shouldLimit = CarAudioGainMonitor.shouldLimitVolume(halReasons); if ((shouldLimit != isLimitedLocked()) || (shouldLimit && (halIndex != mLimitedGainIndex))) { setLimitLocked(shouldLimit ? halIndex : getMaxGainIndex()); eventType |= EVENT_TYPE_ATTENUATION_CHANGED; } boolean shouldDuck = CarAudioGainMonitor.shouldDuckGain(halReasons); if ((shouldDuck != isAttenuatedLocked()) || (shouldDuck && (halIndex != mAttenuatedGainIndex))) { setAttenuatedGainLocked(shouldDuck ? halIndex : UNINITIALIZED); eventType |= EVENT_TYPE_ATTENUATION_CHANGED; } // Accept mute callbacks from hal only if group mute is enabled. // If disabled, such callbacks will be considered as blocking restriction only. boolean shouldMute = CarAudioGainMonitor.shouldMuteVolumeGroup(halReasons); if (mUseCarVolumeGroupMute && (shouldMute != isHalMutedLocked())) { setHalMuteLocked(shouldMute); eventType |= EVENT_TYPE_MUTE_CHANGED; } if (CarAudioGainMonitor.shouldUpdateVolumeIndex(halReasons) && (halIndex != getRestrictedGainForIndexLocked(mCurrentGainIndex))) { mCurrentGainIndex = halIndex; eventType |= EVENT_TYPE_VOLUME_GAIN_INDEX_CHANGED; } // Blocked/Attenuated index shall have been already apply by Audio HAL on HW. // However, keep in sync & broadcast to all ports this volume group deals with. // // Do not update current gain cache, keep it for restoring rather using reported index // when the event is cleared. int newRestrictedIndex = getRestrictedGainForIndexLocked(mCurrentGainIndex); setCurrentGainIndexLocked(newRestrictedIndex); // Hal or user mute state can change (only user mute enabled while hal muted allowed). // Force a sync of mute application. applyMuteLocked(isFullyMutedLocked()); if (newRestrictedIndex != previousRestrictedIndex) { eventType |= EVENT_TYPE_VOLUME_GAIN_INDEX_CHANGED; } } return eventType; } CarVolumeGroupInfo getCarVolumeGroupInfo() { int gainIndex; boolean isMuted; boolean isHalMuted; boolean isBlocked; boolean isAttenuated; synchronized (mLock) { gainIndex = getRestrictedGainForIndexLocked(mCurrentGainIndex); isMuted = isMutedLocked(); isHalMuted = isHalMutedLocked(); isBlocked = isBlockedLocked(); isAttenuated = isAttenuatedLocked() || isLimitedLocked(); } String name = mName.isEmpty() ? "group id " + mId : mName; CarVolumeGroupInfo.Builder builder = new CarVolumeGroupInfo.Builder(name, mZoneId, mId) .setVolumeGainIndex(gainIndex).setMaxVolumeGainIndex(getMaxGainIndex()) .setMinVolumeGainIndex(getMinGainIndex()).setMuted(isMuted).setBlocked(isBlocked) .setAttenuated(isAttenuated).setAudioAttributes(getAudioAttributes()); if (carAudioDynamicDevices()) { builder.setAudioDeviceAttributes(getAudioDeviceAttributes()); } if (carAudioMinMaxActivationVolume()) { builder.setMaxActivationVolumeGainIndex(getMaxActivationGainIndex()) .setMinActivationVolumeGainIndex(getMinActivationGainIndex()); } if (carAudioMuteAmbiguity()) { builder.setMutedBySystem(isHalMuted); } return builder.build(); } private List getAudioDeviceAttributes() { ArraySet set = new ArraySet<>(); int[] contexts = getContexts(); for (int index = 0; index < contexts.length; index++) { AudioDeviceAttributes device = getAudioDeviceForContext(contexts[index]); if (device == null) { Slogf.w(CarLog.TAG_AUDIO, "getAudioDeviceAttributes: Could not find audio device for context " + mCarAudioContext.toString(contexts[index])); continue; } set.add(device); } return new ArrayList<>(set); } boolean hasAudioAttributes(AudioAttributes audioAttributes) { synchronized (mLock) { return mContextToAddress.contains(mCarAudioContext.getContextForAttributes( audioAttributes)); } } List getAudioAttributes() { List audioAttributes = new ArrayList<>(); synchronized (mLock) { for (int index = 0; index < mContextToAddress.size(); index++) { int context = mContextToAddress.keyAt(index); AudioAttributes[] contextAttributes = mCarAudioContext.getAudioAttributesForContext(context); for (int attrIndex = 0; attrIndex < contextAttributes.length; attrIndex++) { audioAttributes.add(contextAttributes[attrIndex]); } } } return audioAttributes; } /** * @return one or more {@link android.car.media.CarVolumeGroupEvent.EventTypeEnum} */ public int onAudioVolumeGroupChanged(int flags) { return 0; } /** * Updates car audio device info with the hal audio device info */ void updateAudioDeviceInfo(HalAudioDeviceInfo halDeviceInfo) { synchronized (mLock) { CarAudioDeviceInfo info = mAddressToCarAudioDeviceInfo.get(halDeviceInfo.getAddress()); if (info == null) { Slogf.w(CarLog.TAG_AUDIO, "No matching car audio device info found for address: %s", halDeviceInfo.getAddress()); return; } info.updateAudioDeviceInfo(halDeviceInfo); } } void updateDevices(boolean useCoreAudioRouting) { } /** * Calculates the new gain stages from list of assigned audio device infos * *

Used to update audio device gain stages dynamically. * * @return one or more of {@link android.car.media.CarVolumeGroupEvent.EventTypeEnum}, or 0 if * dynamic updates are not supported */ int calculateNewGainStageFromDeviceInfos() { return 0; } boolean isActive() { synchronized (mLock) { for (int c = 0; c < mAddressToCarAudioDeviceInfo.size(); c++) { CarAudioDeviceInfo info = mAddressToCarAudioDeviceInfo.valueAt(c); if (info.isActive()) { continue; } return false; } } return true; } public boolean audioDevicesAdded(List devices) { Objects.requireNonNull(devices, "Audio devices can not be null"); if (isActive()) { return false; } boolean updated = false; for (int c = 0; c < mContextToDevices.size(); c++) { if (!mContextToDevices.valueAt(c).audioDevicesAdded(devices)) { continue; } updated = true; } if (!updated) { return false; } synchronized (mLock) { updateAudioDevicesMappingLocked(); } return true; } public boolean audioDevicesRemoved(List devices) { Objects.requireNonNull(devices, "Audio devices can not be null"); boolean updated = false; for (int c = 0; c < mContextToDevices.size(); c++) { if (!mContextToDevices.valueAt(c).audioDevicesRemoved(devices)) { continue; } updated = true; } if (!updated) { return false; } synchronized (mLock) { updateAudioDevicesMappingLocked(); } return true; } @GuardedBy("mLock") private void updateAudioDevicesMappingLocked() { mAddressToCarAudioDeviceInfo.clear(); mContextToAddress.clear(); for (int c = 0; c < mContextToDevices.size(); c++) { CarAudioDeviceInfo info = mContextToDevices.valueAt(c); int audioContext = mContextToDevices.keyAt(c); mAddressToCarAudioDeviceInfo.put(info.getAddress(), info); mContextToAddress.put(audioContext, info.getAddress()); } } /** * Determines if device types assign to volume groups are valid based on the following rules: *

    *
  • Dynamic device types (non BUS) for this group should not appear in the * {@code dynamicDeviceTypesInConfig} passed in parameter
  • *
  • Dynamic device types should appear alone in volume group
  • *
* * @param dynamicDeviceTypesInConfig Devices already seen in other volume groups for the same * configuration, groups checks if the device types for the volume group already exists here * and return {@code false} if so. Also adds any non-existing device types for the group. * @return {@code true} if the rules defined above are valid for the group, {@code false} * otherwise */ boolean validateDeviceTypes(Set dynamicDeviceTypesInConfig) { List devices = getAudioDeviceAttributes(); boolean hasNonBusDevice = false; for (int c = 0; c < devices.size(); c++) { int deviceType = devices.get(c).getType(); // BUS devices are handled by address name check if (deviceType == TYPE_BUS) { continue; } hasNonBusDevice = true; int convertedType = convertDeviceType(deviceType); if (dynamicDeviceTypesInConfig.add(convertedType)) { continue; } Slogf.e(CarLog.TAG_AUDIO, "Car volume groups defined in" + " car_audio_configuration.xml shared the dynamic device type " + DebugUtils.constantToString(AudioDeviceInfo.class, /* prefix= */ "TYPE_", deviceType) + " in multiple volume groups in the same configuration"); return false; } if (!hasNonBusDevice || devices.size() == 1) { return true; } Slogf.e(CarLog.TAG_AUDIO, "Car volume group " + getName() + " defined in car_audio_configuration.xml" + " has multiple devices for a dynamic device group." + " Groups with dynamic devices can only have a single device."); return false; } // Given the current limitation in BT stack where there can only be one BT device available // of any type, we need to consider all BT types as the same, we are picking TYPE_BLUETOOTH_A2DP // for verification purposes, could pick any of them. private static int convertDeviceType(int type) { switch (type) { case TYPE_BLUETOOTH_A2DP: // fall through case TYPE_BLE_HEADSET: // fall through case TYPE_BLE_SPEAKER: // fall through case TYPE_BLE_BROADCAST: return TYPE_BLUETOOTH_A2DP; case TYPE_BUILTIN_SPEAKER: // fall through case TYPE_WIRED_HEADSET: // fall through case TYPE_WIRED_HEADPHONES: // fall through case TYPE_HDMI: // fall through case TYPE_USB_ACCESSORY: // fall through case TYPE_USB_DEVICE: // fall through case TYPE_USB_HEADSET: // fall through case TYPE_AUX_LINE: // fall through case TYPE_BUS: default: return type; } } }