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 if (mIsMuted) { 154 return getIndexForGain(mMinGain); 155 } 156 return getCurrentGainIndexLocked(); 157 } 158 } 159 getCurrentGainIndexLocked()160 private int getCurrentGainIndexLocked() { 161 return mCurrentGainIndex; 162 } 163 164 /** 165 * Sets the gain on this group, gain will be set on all devices within volume group. 166 */ setCurrentGainIndex(int gainIndex)167 void setCurrentGainIndex(int gainIndex) { 168 Preconditions.checkArgument(isValidGainIndex(gainIndex), 169 "Gain out of range (%d:%d) index %d", mMinGain, mMaxGain, gainIndex); 170 synchronized (mLock) { 171 if (mIsMuted) { 172 setMuteLocked(false); 173 } 174 setCurrentGainIndexLocked(gainIndex); 175 } 176 } 177 setCurrentGainIndexLocked(int gainIndex)178 private void setCurrentGainIndexLocked(int gainIndex) { 179 int gainInMillibels = getGainForIndex(gainIndex); 180 for (String address : mAddressToCarAudioDeviceInfo.keySet()) { 181 CarAudioDeviceInfo info = mAddressToCarAudioDeviceInfo.get(address); 182 info.setCurrentGain(gainInMillibels); 183 } 184 185 mCurrentGainIndex = gainIndex; 186 187 storeGainIndexForUserLocked(mCurrentGainIndex, mUserId); 188 } 189 190 @Nullable getAudioDevicePortForContext(int carAudioContext)191 AudioDevicePort getAudioDevicePortForContext(int carAudioContext) { 192 final String address = mContextToAddress.get(carAudioContext); 193 if (address == null || mAddressToCarAudioDeviceInfo.get(address) == null) { 194 return null; 195 } 196 197 return mAddressToCarAudioDeviceInfo.get(address).getAudioDevicePort(); 198 } 199 hasCriticalAudioContexts()200 boolean hasCriticalAudioContexts() { 201 return mHasCriticalAudioContexts; 202 } 203 204 @Override 205 @ExcludeFromCodeCoverageGeneratedReport(reason = BOILERPLATE_CODE) toString()206 public String toString() { 207 return "CarVolumeGroup id: " + mId 208 + " currentGainIndex: " + mCurrentGainIndex 209 + " contexts: " + Arrays.toString(getContexts()) 210 + " addresses: " + String.join(", ", getAddresses()); 211 } 212 213 @ExcludeFromCodeCoverageGeneratedReport(reason = DUMP_INFO) dump(IndentingPrintWriter writer)214 void dump(IndentingPrintWriter writer) { 215 synchronized (mLock) { 216 writer.printf("CarVolumeGroup(%d)\n", mId); 217 writer.increaseIndent(); 218 writer.printf("Zone Id(%b)\n", mZoneId); 219 writer.printf("Is Muted(%b)\n", mIsMuted); 220 writer.printf("UserId(%d)\n", mUserId); 221 writer.printf("Persist Volume Group Mute(%b)\n", 222 mSettingsManager.isPersistVolumeGroupMuteEnabled(mUserId)); 223 writer.printf("Step size: %d\n", mStepSize); 224 writer.printf("Gain values (min / max / default/ current): %d %d %d %d\n", mMinGain, 225 mMaxGain, mDefaultGain, getGainForIndex(mCurrentGainIndex)); 226 writer.printf("Gain indexes (min / max / default / current): %d %d %d %d\n", 227 getMinGainIndex(), getMaxGainIndex(), getDefaultGainIndex(), mCurrentGainIndex); 228 for (int i = 0; i < mContextToAddress.size(); i++) { 229 writer.printf("Context: %s -> Address: %s\n", 230 CarAudioContext.toString(mContextToAddress.keyAt(i)), 231 mContextToAddress.valueAt(i)); 232 } 233 mAddressToCarAudioDeviceInfo.keySet().stream() 234 .map(mAddressToCarAudioDeviceInfo::get) 235 .forEach((info -> info.dump(writer))); 236 237 // Empty line for comfortable reading 238 writer.println(); 239 writer.decreaseIndent(); 240 } 241 } 242 loadVolumesSettingsForUser(@serIdInt int userId)243 void loadVolumesSettingsForUser(@UserIdInt int userId) { 244 synchronized (mLock) { 245 //Update the volume for the new user 246 updateUserIdLocked(userId); 247 //Update the current gain index 248 updateCurrentGainIndexLocked(); 249 setCurrentGainIndexLocked(getCurrentGainIndexLocked()); 250 //Reset devices with current gain index 251 updateGroupMuteLocked(); 252 } 253 } 254 setMute(boolean mute)255 void setMute(boolean mute) { 256 synchronized (mLock) { 257 setMuteLocked(mute); 258 } 259 } 260 setMuteLocked(boolean mute)261 void setMuteLocked(boolean mute) { 262 mIsMuted = mute; 263 if (mSettingsManager.isPersistVolumeGroupMuteEnabled(mUserId)) { 264 mSettingsManager.storeVolumeGroupMuteForUser(mUserId, mZoneId, mId, mute); 265 } 266 } 267 isMuted()268 boolean isMuted() { 269 synchronized (mLock) { 270 return mIsMuted; 271 } 272 } 273 containsCriticalAudioContext(SparseArray<String> contextToAddress)274 private static boolean containsCriticalAudioContext(SparseArray<String> contextToAddress) { 275 for (int i = 0; i < contextToAddress.size(); i++) { 276 int audioContext = contextToAddress.keyAt(i); 277 if (CarAudioContext.isCriticalAudioContext(audioContext)) { 278 return true; 279 } 280 } 281 return false; 282 } 283 284 @GuardedBy("mLock") updateUserIdLocked(@serIdInt int userId)285 private void updateUserIdLocked(@UserIdInt int userId) { 286 mUserId = userId; 287 mStoredGainIndex = getCurrentGainIndexForUserLocked(); 288 } 289 290 @GuardedBy("mLock") getCurrentGainIndexForUserLocked()291 private int getCurrentGainIndexForUserLocked() { 292 int gainIndexForUser = mSettingsManager.getStoredVolumeGainIndexForUser(mUserId, mZoneId, 293 mId); 294 Slog.i(CarLog.TAG_AUDIO, "updateUserId userId " + mUserId 295 + " gainIndexForUser " + gainIndexForUser); 296 return gainIndexForUser; 297 } 298 299 /** 300 * Update the current gain index based on the stored gain index 301 */ 302 @GuardedBy("mLock") updateCurrentGainIndexLocked()303 private void updateCurrentGainIndexLocked() { 304 if (isValidGainIndex(mStoredGainIndex)) { 305 mCurrentGainIndex = mStoredGainIndex; 306 } else { 307 mCurrentGainIndex = getIndexForGain(mDefaultGain); 308 } 309 } 310 isValidGainIndex(int gainIndex)311 private boolean isValidGainIndex(int gainIndex) { 312 return gainIndex >= getIndexForGain(mMinGain) 313 && gainIndex <= getIndexForGain(mMaxGain); 314 } 315 getDefaultGainIndex()316 private int getDefaultGainIndex() { 317 synchronized (mLock) { 318 return getIndexForGain(mDefaultGain); 319 } 320 } 321 322 @GuardedBy("mLock") storeGainIndexForUserLocked(int gainIndex, @UserIdInt int userId)323 private void storeGainIndexForUserLocked(int gainIndex, @UserIdInt int userId) { 324 mSettingsManager.storeVolumeGainIndexForUser(userId, 325 mZoneId, mId, gainIndex); 326 } 327 getGainForIndex(int gainIndex)328 private int getGainForIndex(int gainIndex) { 329 return mMinGain + gainIndex * mStepSize; 330 } 331 getIndexForGain(int gainInMillibel)332 private int getIndexForGain(int gainInMillibel) { 333 return (gainInMillibel - mMinGain) / mStepSize; 334 } 335 336 @GuardedBy("mLock") updateGroupMuteLocked()337 private void updateGroupMuteLocked() { 338 if (!mUseCarVolumeGroupMute) { 339 return; 340 } 341 if (!mSettingsManager.isPersistVolumeGroupMuteEnabled(mUserId)) { 342 mIsMuted = false; 343 return; 344 } 345 mIsMuted = mSettingsManager.getVolumeGroupMuteForUser(mUserId, mZoneId, mId); 346 } 347 348 static final class Builder { 349 private static final int UNSET_STEP_SIZE = -1; 350 351 private final int mId; 352 private final int mZoneId; 353 private final boolean mUseCarVolumeGroupMute; 354 private final CarAudioSettings mCarAudioSettings; 355 private final SparseArray<String> mContextToAddress = new SparseArray<>(); 356 private final Map<String, CarAudioDeviceInfo> mAddressToCarAudioDeviceInfo = 357 new HashMap<>(); 358 359 @VisibleForTesting 360 int mStepSize = UNSET_STEP_SIZE; 361 @VisibleForTesting 362 int mDefaultGain = Integer.MIN_VALUE; 363 @VisibleForTesting 364 int mMaxGain = Integer.MIN_VALUE; 365 @VisibleForTesting 366 int mMinGain = Integer.MAX_VALUE; 367 Builder(int zoneId, int id, CarAudioSettings carAudioSettings, boolean useCarVolumeGroupMute)368 Builder(int zoneId, int id, CarAudioSettings carAudioSettings, 369 boolean useCarVolumeGroupMute) { 370 mZoneId = zoneId; 371 mId = id; 372 mCarAudioSettings = carAudioSettings; 373 mUseCarVolumeGroupMute = useCarVolumeGroupMute; 374 } 375 setDeviceInfoForContext(int carAudioContext, CarAudioDeviceInfo info)376 Builder setDeviceInfoForContext(int carAudioContext, CarAudioDeviceInfo info) { 377 Preconditions.checkArgument(mContextToAddress.get(carAudioContext) == null, 378 "Context %s has already been set to %s", 379 CarAudioContext.toString(carAudioContext), 380 mContextToAddress.get(carAudioContext)); 381 382 if (mAddressToCarAudioDeviceInfo.isEmpty()) { 383 mStepSize = info.getStepValue(); 384 } else { 385 Preconditions.checkArgument( 386 info.getStepValue() == mStepSize, 387 "Gain controls within one group must have same step value"); 388 } 389 390 mAddressToCarAudioDeviceInfo.put(info.getAddress(), info); 391 mContextToAddress.put(carAudioContext, info.getAddress()); 392 393 if (info.getDefaultGain() > mDefaultGain) { 394 // We're arbitrarily selecting the highest 395 // device default gain as the group's default. 396 mDefaultGain = info.getDefaultGain(); 397 } 398 if (info.getMaxGain() > mMaxGain) { 399 mMaxGain = info.getMaxGain(); 400 } 401 if (info.getMinGain() < mMinGain) { 402 mMinGain = info.getMinGain(); 403 } 404 405 return this; 406 } 407 build()408 CarVolumeGroup build() { 409 Preconditions.checkArgument(mStepSize != UNSET_STEP_SIZE, 410 "setDeviceInfoForContext has to be called at least once before building"); 411 CarVolumeGroup group = new CarVolumeGroup(mZoneId, mId, mCarAudioSettings, mStepSize, 412 mDefaultGain, mMinGain, mMaxGain, mContextToAddress, 413 mAddressToCarAudioDeviceInfo, mUseCarVolumeGroupMute); 414 group.init(); 415 return group; 416 } 417 } 418 } 419