1 /* 2 * Copyright (C) 2018 The Android Open Source Project 3 * 4 * Licensed under the Apache License, Version 2.0 (the "License"); 5 * you may not use this file except in compliance with the License. 6 * You may obtain a copy of the License at 7 * 8 * http://www.apache.org/licenses/LICENSE-2.0 9 * 10 * Unless required by applicable law or agreed to in writing, software 11 * distributed under the License is distributed on an "AS IS" BASIS, 12 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 * See the License for the specific language governing permissions and 14 * limitations under the License. 15 */ 16 package com.android.car.audio; 17 18 import static com.android.car.internal.ExcludeFromCodeCoverageGeneratedReport.BOILERPLATE_CODE; 19 import static com.android.car.internal.ExcludeFromCodeCoverageGeneratedReport.DUMP_INFO; 20 21 import android.annotation.NonNull; 22 import android.annotation.Nullable; 23 import android.annotation.UserIdInt; 24 import android.car.media.CarAudioManager; 25 import android.media.AudioDevicePort; 26 import android.os.UserHandle; 27 import android.util.IndentingPrintWriter; 28 import android.util.Slog; 29 import android.util.SparseArray; 30 31 import com.android.car.CarLog; 32 import com.android.car.audio.CarAudioContext.AudioContext; 33 import com.android.car.internal.ExcludeFromCodeCoverageGeneratedReport; 34 import com.android.internal.annotations.GuardedBy; 35 import com.android.internal.annotations.VisibleForTesting; 36 import com.android.internal.util.Preconditions; 37 38 import java.util.ArrayList; 39 import java.util.Arrays; 40 import java.util.HashMap; 41 import java.util.List; 42 import java.util.Map; 43 44 /** 45 * A class encapsulates a volume group in car. 46 * 47 * Volume in a car is controlled by group. A group holds one or more car audio contexts. 48 * Call {@link CarAudioManager#getVolumeGroupCount()} to get the count of {@link CarVolumeGroup} 49 * supported in a car. 50 */ 51 /* package */ final class CarVolumeGroup { 52 53 private final boolean mUseCarVolumeGroupMute; 54 private final boolean mHasCriticalAudioContexts; 55 private final CarAudioSettings mSettingsManager; 56 private final int mDefaultGain; 57 private final int mId; 58 private final int mMaxGain; 59 private final int mMinGain; 60 private final int mStepSize; 61 private final int mZoneId; 62 private final SparseArray<String> mContextToAddress; 63 private final Map<String, CarAudioDeviceInfo> mAddressToCarAudioDeviceInfo; 64 65 private final Object mLock = new Object(); 66 67 @GuardedBy("mLock") 68 private int mStoredGainIndex; 69 @GuardedBy("mLock") 70 private int mCurrentGainIndex = -1; 71 @GuardedBy("mLock") 72 private boolean mIsMuted; 73 @GuardedBy("mLock") 74 private @UserIdInt int mUserId = UserHandle.USER_CURRENT; 75 CarVolumeGroup(int zoneId, int id, CarAudioSettings settingsManager, int stepSize, int defaultGain, int minGain, int maxGain, SparseArray<String> contextToAddress, Map<String, CarAudioDeviceInfo> addressToCarAudioDeviceInfo, boolean useCarVolumeGroupMute)76 private CarVolumeGroup(int zoneId, int id, CarAudioSettings settingsManager, int stepSize, 77 int defaultGain, int minGain, int maxGain, SparseArray<String> contextToAddress, 78 Map<String, CarAudioDeviceInfo> addressToCarAudioDeviceInfo, 79 boolean useCarVolumeGroupMute) { 80 81 mSettingsManager = settingsManager; 82 mZoneId = zoneId; 83 mId = id; 84 mStepSize = stepSize; 85 mDefaultGain = defaultGain; 86 mMinGain = minGain; 87 mMaxGain = maxGain; 88 mContextToAddress = contextToAddress; 89 mAddressToCarAudioDeviceInfo = addressToCarAudioDeviceInfo; 90 mUseCarVolumeGroupMute = useCarVolumeGroupMute; 91 92 mHasCriticalAudioContexts = containsCriticalAudioContext(contextToAddress); 93 } 94 init()95 void init() { 96 mStoredGainIndex = mSettingsManager.getStoredVolumeGainIndexForUser(mUserId, mZoneId, mId); 97 updateCurrentGainIndexLocked(); 98 } 99 100 @Nullable getCarAudioDeviceInfoForAddress(String address)101 CarAudioDeviceInfo getCarAudioDeviceInfoForAddress(String address) { 102 return mAddressToCarAudioDeviceInfo.get(address); 103 } 104 105 @AudioContext getContexts()106 int[] getContexts() { 107 final int[] carAudioContexts = new int[mContextToAddress.size()]; 108 for (int i = 0; i < carAudioContexts.length; i++) { 109 carAudioContexts[i] = mContextToAddress.keyAt(i); 110 } 111 return carAudioContexts; 112 } 113 114 /** 115 * Returns the devices address for the given context 116 * or {@code null} if the context does not exist in the volume group 117 */ 118 @Nullable getAddressForContext(int audioContext)119 String getAddressForContext(int audioContext) { 120 return mContextToAddress.get(audioContext); 121 } 122 123 @AudioContext getContextsForAddress(@onNull String address)124 List<Integer> getContextsForAddress(@NonNull String address) { 125 List<Integer> carAudioContexts = new ArrayList<>(); 126 for (int i = 0; i < mContextToAddress.size(); i++) { 127 String value = mContextToAddress.valueAt(i); 128 if (address.equals(value)) { 129 carAudioContexts.add(mContextToAddress.keyAt(i)); 130 } 131 } 132 return carAudioContexts; 133 } 134 getAddresses()135 List<String> getAddresses() { 136 return new ArrayList<>(mAddressToCarAudioDeviceInfo.keySet()); 137 } 138 getMaxGainIndex()139 int getMaxGainIndex() { 140 synchronized (mLock) { 141 return getIndexForGain(mMaxGain); 142 } 143 } 144 getMinGainIndex()145 int getMinGainIndex() { 146 synchronized (mLock) { 147 return getIndexForGain(mMinGain); 148 } 149 } 150 getCurrentGainIndex()151 int getCurrentGainIndex() { 152 synchronized (mLock) { 153 return mCurrentGainIndex; 154 } 155 } 156 157 /** 158 * Sets the gain on this group, gain will be set on all devices within volume group. 159 */ setCurrentGainIndex(int gainIndex)160 void setCurrentGainIndex(int gainIndex) { 161 int gainInMillibels = getGainForIndex(gainIndex); 162 Preconditions.checkArgument(isValidGainIndex(gainIndex), 163 "Gain out of range (%d:%d) %d index %d", mMinGain, mMaxGain, 164 gainInMillibels, gainIndex); 165 synchronized (mLock) { 166 for (String address : mAddressToCarAudioDeviceInfo.keySet()) { 167 CarAudioDeviceInfo info = mAddressToCarAudioDeviceInfo.get(address); 168 info.setCurrentGain(gainInMillibels); 169 } 170 171 mCurrentGainIndex = gainIndex; 172 173 storeGainIndexForUserLocked(mCurrentGainIndex, mUserId); 174 } 175 } 176 177 @Nullable getAudioDevicePortForContext(int carAudioContext)178 AudioDevicePort getAudioDevicePortForContext(int carAudioContext) { 179 final String address = mContextToAddress.get(carAudioContext); 180 if (address == null || mAddressToCarAudioDeviceInfo.get(address) == null) { 181 return null; 182 } 183 184 return mAddressToCarAudioDeviceInfo.get(address).getAudioDevicePort(); 185 } 186 hasCriticalAudioContexts()187 boolean hasCriticalAudioContexts() { 188 return mHasCriticalAudioContexts; 189 } 190 191 @Override 192 @ExcludeFromCodeCoverageGeneratedReport(reason = BOILERPLATE_CODE) toString()193 public String toString() { 194 return "CarVolumeGroup id: " + mId 195 + " currentGainIndex: " + mCurrentGainIndex 196 + " contexts: " + Arrays.toString(getContexts()) 197 + " addresses: " + String.join(", ", getAddresses()); 198 } 199 200 @ExcludeFromCodeCoverageGeneratedReport(reason = DUMP_INFO) dump(IndentingPrintWriter writer)201 void dump(IndentingPrintWriter writer) { 202 synchronized (mLock) { 203 writer.printf("CarVolumeGroup(%d)\n", mId); 204 writer.increaseIndent(); 205 writer.printf("Zone Id(%b)\n", mZoneId); 206 writer.printf("Is Muted(%b)\n", mIsMuted); 207 writer.printf("UserId(%d)\n", mUserId); 208 writer.printf("Persist Volume Group Mute(%b)\n", 209 mSettingsManager.isPersistVolumeGroupMuteEnabled(mUserId)); 210 writer.printf("Step size: %d\n", mStepSize); 211 writer.printf("Gain values (min / max / default/ current): %d %d %d %d\n", mMinGain, 212 mMaxGain, mDefaultGain, getGainForIndex(mCurrentGainIndex)); 213 writer.printf("Gain indexes (min / max / default / current): %d %d %d %d\n", 214 getMinGainIndex(), getMaxGainIndex(), getDefaultGainIndex(), mCurrentGainIndex); 215 for (int i = 0; i < mContextToAddress.size(); i++) { 216 writer.printf("Context: %s -> Address: %s\n", 217 CarAudioContext.toString(mContextToAddress.keyAt(i)), 218 mContextToAddress.valueAt(i)); 219 } 220 mAddressToCarAudioDeviceInfo.keySet().stream() 221 .map(mAddressToCarAudioDeviceInfo::get) 222 .forEach((info -> info.dump(writer))); 223 224 // Empty line for comfortable reading 225 writer.println(); 226 writer.decreaseIndent(); 227 } 228 } 229 loadVolumesSettingsForUser(@serIdInt int userId)230 void loadVolumesSettingsForUser(@UserIdInt int userId) { 231 synchronized (mLock) { 232 //Update the volume for the new user 233 updateUserIdLocked(userId); 234 //Update the current gain index 235 updateCurrentGainIndexLocked(); 236 //Reset devices with current gain index 237 updateGroupMuteLocked(); 238 } 239 setCurrentGainIndex(getCurrentGainIndex()); 240 } 241 setMute(boolean mute)242 void setMute(boolean mute) { 243 synchronized (mLock) { 244 mIsMuted = mute; 245 if (mSettingsManager.isPersistVolumeGroupMuteEnabled(mUserId)) { 246 mSettingsManager.storeVolumeGroupMuteForUser(mUserId, mZoneId, mId, mute); 247 } 248 } 249 } 250 isMuted()251 boolean isMuted() { 252 synchronized (mLock) { 253 return mIsMuted; 254 } 255 } 256 containsCriticalAudioContext(SparseArray<String> contextToAddress)257 private static boolean containsCriticalAudioContext(SparseArray<String> contextToAddress) { 258 for (int i = 0; i < contextToAddress.size(); i++) { 259 int audioContext = contextToAddress.keyAt(i); 260 if (CarAudioContext.isCriticalAudioContext(audioContext)) { 261 return true; 262 } 263 } 264 return false; 265 } 266 267 @GuardedBy("mLock") updateUserIdLocked(@serIdInt int userId)268 private void updateUserIdLocked(@UserIdInt int userId) { 269 mUserId = userId; 270 mStoredGainIndex = getCurrentGainIndexForUserLocked(); 271 } 272 273 @GuardedBy("mLock") getCurrentGainIndexForUserLocked()274 private int getCurrentGainIndexForUserLocked() { 275 int gainIndexForUser = mSettingsManager.getStoredVolumeGainIndexForUser(mUserId, mZoneId, 276 mId); 277 Slog.i(CarLog.TAG_AUDIO, "updateUserId userId " + mUserId 278 + " gainIndexForUser " + gainIndexForUser); 279 return gainIndexForUser; 280 } 281 282 /** 283 * Update the current gain index based on the stored gain index 284 */ 285 @GuardedBy("mLock") updateCurrentGainIndexLocked()286 private void updateCurrentGainIndexLocked() { 287 if (isValidGainIndex(mStoredGainIndex)) { 288 mCurrentGainIndex = mStoredGainIndex; 289 } else { 290 mCurrentGainIndex = getIndexForGain(mDefaultGain); 291 } 292 } 293 isValidGainIndex(int gainIndex)294 private boolean isValidGainIndex(int gainIndex) { 295 return gainIndex >= getIndexForGain(mMinGain) 296 && gainIndex <= getIndexForGain(mMaxGain); 297 } 298 getDefaultGainIndex()299 private int getDefaultGainIndex() { 300 synchronized (mLock) { 301 return getIndexForGain(mDefaultGain); 302 } 303 } 304 305 @GuardedBy("mLock") storeGainIndexForUserLocked(int gainIndex, @UserIdInt int userId)306 private void storeGainIndexForUserLocked(int gainIndex, @UserIdInt int userId) { 307 mSettingsManager.storeVolumeGainIndexForUser(userId, 308 mZoneId, mId, gainIndex); 309 } 310 getGainForIndex(int gainIndex)311 private int getGainForIndex(int gainIndex) { 312 return mMinGain + gainIndex * mStepSize; 313 } 314 getIndexForGain(int gainInMillibel)315 private int getIndexForGain(int gainInMillibel) { 316 return (gainInMillibel - mMinGain) / mStepSize; 317 } 318 319 @GuardedBy("mLock") updateGroupMuteLocked()320 private void updateGroupMuteLocked() { 321 if (!mUseCarVolumeGroupMute) { 322 return; 323 } 324 if (!mSettingsManager.isPersistVolumeGroupMuteEnabled(mUserId)) { 325 mIsMuted = false; 326 return; 327 } 328 mIsMuted = mSettingsManager.getVolumeGroupMuteForUser(mUserId, mZoneId, mId); 329 } 330 331 static final class Builder { 332 private static final int UNSET_STEP_SIZE = -1; 333 334 private final int mId; 335 private final int mZoneId; 336 private final boolean mUseCarVolumeGroupMute; 337 private final CarAudioSettings mCarAudioSettings; 338 private final SparseArray<String> mContextToAddress = new SparseArray<>(); 339 private final Map<String, CarAudioDeviceInfo> mAddressToCarAudioDeviceInfo = 340 new HashMap<>(); 341 342 @VisibleForTesting 343 int mStepSize = UNSET_STEP_SIZE; 344 @VisibleForTesting 345 int mDefaultGain = Integer.MIN_VALUE; 346 @VisibleForTesting 347 int mMaxGain = Integer.MIN_VALUE; 348 @VisibleForTesting 349 int mMinGain = Integer.MAX_VALUE; 350 Builder(int zoneId, int id, CarAudioSettings carAudioSettings, boolean useCarVolumeGroupMute)351 Builder(int zoneId, int id, CarAudioSettings carAudioSettings, 352 boolean useCarVolumeGroupMute) { 353 mZoneId = zoneId; 354 mId = id; 355 mCarAudioSettings = carAudioSettings; 356 mUseCarVolumeGroupMute = useCarVolumeGroupMute; 357 } 358 setDeviceInfoForContext(int carAudioContext, CarAudioDeviceInfo info)359 Builder setDeviceInfoForContext(int carAudioContext, CarAudioDeviceInfo info) { 360 Preconditions.checkArgument(mContextToAddress.get(carAudioContext) == null, 361 "Context %s has already been set to %s", 362 CarAudioContext.toString(carAudioContext), 363 mContextToAddress.get(carAudioContext)); 364 365 if (mAddressToCarAudioDeviceInfo.isEmpty()) { 366 mStepSize = info.getStepValue(); 367 } else { 368 Preconditions.checkArgument( 369 info.getStepValue() == mStepSize, 370 "Gain controls within one group must have same step value"); 371 } 372 373 mAddressToCarAudioDeviceInfo.put(info.getAddress(), info); 374 mContextToAddress.put(carAudioContext, info.getAddress()); 375 376 if (info.getDefaultGain() > mDefaultGain) { 377 // We're arbitrarily selecting the highest 378 // device default gain as the group's default. 379 mDefaultGain = info.getDefaultGain(); 380 } 381 if (info.getMaxGain() > mMaxGain) { 382 mMaxGain = info.getMaxGain(); 383 } 384 if (info.getMinGain() < mMinGain) { 385 mMinGain = info.getMinGain(); 386 } 387 388 return this; 389 } 390 build()391 CarVolumeGroup build() { 392 Preconditions.checkArgument(mStepSize != UNSET_STEP_SIZE, 393 "setDeviceInfoForContext has to be called at least once before building"); 394 CarVolumeGroup group = new CarVolumeGroup(mZoneId, mId, mCarAudioSettings, mStepSize, 395 mDefaultGain, mMinGain, mMaxGain, mContextToAddress, 396 mAddressToCarAudioDeviceInfo, mUseCarVolumeGroupMute); 397 group.init(); 398 return group; 399 } 400 } 401 } 402