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 android.annotation.NonNull; 19 import android.annotation.Nullable; 20 import android.annotation.UserIdInt; 21 import android.car.media.CarAudioManager; 22 import android.content.Context; 23 import android.media.AudioDevicePort; 24 import android.os.UserHandle; 25 import android.util.Log; 26 import android.util.SparseArray; 27 28 import com.android.car.CarLog; 29 import com.android.internal.util.Preconditions; 30 31 import java.io.PrintWriter; 32 import java.util.ArrayList; 33 import java.util.Arrays; 34 import java.util.HashMap; 35 import java.util.List; 36 import java.util.Map; 37 38 /** 39 * A class encapsulates a volume group in car. 40 * 41 * Volume in a car is controlled by group. A group holds one or more car audio contexts. 42 * Call {@link CarAudioManager#getVolumeGroupCount()} to get the count of {@link CarVolumeGroup} 43 * supported in a car. 44 */ 45 /* package */ final class CarVolumeGroup { 46 47 private CarAudioSettings mSettingsManager; 48 private final int mZoneId; 49 private final int mId; 50 private final SparseArray<String> mContextToAddress = new SparseArray<>(); 51 private final Map<String, CarAudioDeviceInfo> mAddressToCarAudioDeviceInfo = new HashMap<>(); 52 53 private final Object mLock = new Object(); 54 55 private int mDefaultGain = Integer.MIN_VALUE; 56 private int mMaxGain = Integer.MIN_VALUE; 57 private int mMinGain = Integer.MAX_VALUE; 58 private int mStepSize = 0; 59 private int mStoredGainIndex; 60 private int mCurrentGainIndex = -1; 61 private @UserIdInt int mUserId = UserHandle.USER_CURRENT; 62 63 /** 64 * Constructs a {@link CarVolumeGroup} instance 65 * @param Settings {@link CarAudioSettings} instance 66 * @param zoneId Audio zone this volume group belongs to 67 * @param id ID of this volume group 68 */ CarVolumeGroup(CarAudioSettings settings, int zoneId, int id)69 CarVolumeGroup(CarAudioSettings settings, int zoneId, int id) { 70 mSettingsManager = settings; 71 mZoneId = zoneId; 72 mId = id; 73 mStoredGainIndex = mSettingsManager.getStoredVolumeGainIndexForUser(mUserId, mZoneId, mId); 74 } 75 76 /** 77 * Constructs a {@link CarVolumeGroup} instance 78 * @param context {@link Context} instance 79 * @param zoneId Audio zone this volume group belongs to 80 * @param id ID of this volume group 81 * @param contexts Pre-populated array of car contexts, for legacy car_volume_groups.xml only 82 * @deprecated In favor of {@link #CarVolumeGroup(Context, int, int)} 83 */ 84 @Deprecated CarVolumeGroup(CarAudioSettings settings, int zoneId, int id, @NonNull int[] contexts)85 CarVolumeGroup(CarAudioSettings settings, int zoneId, int id, @NonNull int[] contexts) { 86 this(settings, zoneId, id); 87 // Deal with the pre-populated car audio contexts 88 for (int audioContext : contexts) { 89 mContextToAddress.put(audioContext, null); 90 } 91 } 92 93 /** 94 * @param address Physical address for the audio device 95 * @return {@link CarAudioDeviceInfo} associated with a given address 96 */ getCarAudioDeviceInfoForAddress(String address)97 CarAudioDeviceInfo getCarAudioDeviceInfoForAddress(String address) { 98 return mAddressToCarAudioDeviceInfo.get(address); 99 } 100 101 /** 102 * @return Array of car audio contexts {@link CarAudioContext} in this {@link CarVolumeGroup} 103 */ getContexts()104 int[] getContexts() { 105 final int[] carAudioContexts = new int[mContextToAddress.size()]; 106 for (int i = 0; i < carAudioContexts.length; i++) { 107 carAudioContexts[i] = mContextToAddress.keyAt(i); 108 } 109 return carAudioContexts; 110 } 111 112 /** 113 * Returns the devices address for the given context 114 * or {@code null} if the context does not exist in the volume group 115 */ 116 @Nullable getAddressForContext(int audioContext)117 String getAddressForContext(int audioContext) { 118 return mContextToAddress.get(audioContext); 119 } 120 121 /** 122 * @param address Physical address for the audio device 123 * @return Array of car audio contexts {@link CarAudioContext} assigned to a given address 124 */ getContextsForAddress(@onNull String address)125 int[] getContextsForAddress(@NonNull String address) { 126 List<Integer> carAudioContexts = new ArrayList<>(); 127 for (int i = 0; i < mContextToAddress.size(); i++) { 128 String value = mContextToAddress.valueAt(i); 129 if (address.equals(value)) { 130 carAudioContexts.add(mContextToAddress.keyAt(i)); 131 } 132 } 133 return carAudioContexts.stream().mapToInt(i -> i).toArray(); 134 } 135 136 /** 137 * @return Array of addresses in this {@link CarVolumeGroup} 138 */ getAddresses()139 List<String> getAddresses() { 140 return new ArrayList<>(mAddressToCarAudioDeviceInfo.keySet()); 141 } 142 143 /** 144 * Binds the context number to physical address and audio device port information. 145 * Because this may change the groups min/max values, thus invalidating an index computed from 146 * a gain before this call, all calls to this function must happen at startup before any 147 * set/getGainIndex calls. 148 * 149 * @param carAudioContext Context to bind audio to {@link CarAudioContext} 150 * @param info {@link CarAudioDeviceInfo} instance relates to the physical address 151 */ bind(int carAudioContext, CarAudioDeviceInfo info)152 void bind(int carAudioContext, CarAudioDeviceInfo info) { 153 Preconditions.checkArgument(mContextToAddress.get(carAudioContext) == null, 154 String.format("Context %s has already been bound to %s", 155 CarAudioContext.toString(carAudioContext), 156 mContextToAddress.get(carAudioContext))); 157 158 synchronized (mLock) { 159 if (mAddressToCarAudioDeviceInfo.size() == 0) { 160 mStepSize = info.getStepValue(); 161 } else { 162 Preconditions.checkArgument( 163 info.getStepValue() == mStepSize, 164 "Gain controls within one group must have same step value"); 165 } 166 167 mAddressToCarAudioDeviceInfo.put(info.getAddress(), info); 168 mContextToAddress.put(carAudioContext, info.getAddress()); 169 170 if (info.getDefaultGain() > mDefaultGain) { 171 // We're arbitrarily selecting the highest 172 // device default gain as the group's default. 173 mDefaultGain = info.getDefaultGain(); 174 } 175 if (info.getMaxGain() > mMaxGain) { 176 mMaxGain = info.getMaxGain(); 177 } 178 if (info.getMinGain() < mMinGain) { 179 mMinGain = info.getMinGain(); 180 } 181 updateCurrentGainIndexLocked(); 182 } 183 } 184 185 /** 186 * Update the user with the a new user 187 * @param userId new user 188 * @note also reloads the store gain index for the user 189 */ updateUserIdLocked(@serIdInt int userId)190 private void updateUserIdLocked(@UserIdInt int userId) { 191 mUserId = userId; 192 mStoredGainIndex = getCurrentGainIndexForUserLocked(); 193 } 194 getCurrentGainIndexForUserLocked()195 private int getCurrentGainIndexForUserLocked() { 196 int gainIndexForUser = mSettingsManager.getStoredVolumeGainIndexForUser(mUserId, mZoneId, 197 mId); 198 Log.i(CarLog.TAG_AUDIO, "updateUserId userId " + mUserId 199 + " gainIndexForUser " + gainIndexForUser); 200 return gainIndexForUser; 201 } 202 203 /** 204 * Update the current gain index based on the stored gain index 205 */ updateCurrentGainIndexLocked()206 private void updateCurrentGainIndexLocked() { 207 synchronized (mLock) { 208 if (mStoredGainIndex < getIndexForGainLocked(mMinGain) 209 || mStoredGainIndex > getIndexForGainLocked(mMaxGain)) { 210 // We expected to load a value from last boot, but if we didn't (perhaps this is the 211 // first boot ever?), then use the highest "default" we've seen to initialize 212 // ourselves. 213 mCurrentGainIndex = getIndexForGainLocked(mDefaultGain); 214 } else { 215 // Just use the gain index we stored last time the gain was 216 // set (presumably during our last boot cycle). 217 mCurrentGainIndex = mStoredGainIndex; 218 } 219 } 220 } 221 getDefaultGainIndex()222 private int getDefaultGainIndex() { 223 synchronized (mLock) { 224 return getIndexForGainLocked(mDefaultGain); 225 } 226 } 227 getMaxGainIndex()228 int getMaxGainIndex() { 229 synchronized (mLock) { 230 return getIndexForGainLocked(mMaxGain); 231 } 232 } 233 getMinGainIndex()234 int getMinGainIndex() { 235 synchronized (mLock) { 236 return getIndexForGainLocked(mMinGain); 237 } 238 } 239 getCurrentGainIndex()240 int getCurrentGainIndex() { 241 synchronized (mLock) { 242 return mCurrentGainIndex; 243 } 244 } 245 246 /** 247 * Sets the gain on this group, gain will be set on all devices within volume group. 248 * @param gainIndex The gain index 249 */ setCurrentGainIndex(int gainIndex)250 void setCurrentGainIndex(int gainIndex) { 251 synchronized (mLock) { 252 int gainInMillibels = getGainForIndexLocked(gainIndex); 253 Preconditions.checkArgument( 254 gainInMillibels >= mMinGain && gainInMillibels <= mMaxGain, 255 "Gain out of range (" 256 + mMinGain + ":" 257 + mMaxGain + ") " 258 + gainInMillibels + "index " 259 + gainIndex); 260 261 for (String address : mAddressToCarAudioDeviceInfo.keySet()) { 262 CarAudioDeviceInfo info = mAddressToCarAudioDeviceInfo.get(address); 263 info.setCurrentGain(gainInMillibels); 264 } 265 266 mCurrentGainIndex = gainIndex; 267 268 storeGainIndexForUserLocked(mCurrentGainIndex, mUserId); 269 } 270 } 271 storeGainIndexForUserLocked(int gainIndex, @UserIdInt int userId)272 private void storeGainIndexForUserLocked(int gainIndex, @UserIdInt int userId) { 273 mSettingsManager.storeVolumeGainIndexForUser(userId, 274 mZoneId, mId, gainIndex); 275 } 276 277 // Given a group level gain index, return the computed gain in millibells 278 // TODO (randolphs) If we ever want to add index to gain curves other than lock-stepped 279 // linear, this would be the place to do it. getGainForIndexLocked(int gainIndex)280 private int getGainForIndexLocked(int gainIndex) { 281 return mMinGain + gainIndex * mStepSize; 282 } 283 284 // TODO (randolphs) if we ever went to a non-linear index to gain curve mapping, we'd need to 285 // revisit this as it assumes (at the least) that getGainForIndex is reversible. Luckily, 286 // this is an internal implementation details we could factor out if/when necessary. getIndexForGainLocked(int gainInMillibel)287 private int getIndexForGainLocked(int gainInMillibel) { 288 return (gainInMillibel - mMinGain) / mStepSize; 289 } 290 291 /** 292 * Gets {@link AudioDevicePort} from a context number 293 */ 294 @Nullable getAudioDevicePortForContext(int carAudioContext)295 AudioDevicePort getAudioDevicePortForContext(int carAudioContext) { 296 final String address = mContextToAddress.get(carAudioContext); 297 if (address == null || mAddressToCarAudioDeviceInfo.get(address) == null) { 298 return null; 299 } 300 301 return mAddressToCarAudioDeviceInfo.get(address).getAudioDevicePort(); 302 } 303 304 @Override toString()305 public String toString() { 306 return "CarVolumeGroup id: " + mId 307 + " currentGainIndex: " + mCurrentGainIndex 308 + " contexts: " + Arrays.toString(getContexts()) 309 + " addresses: " + String.join(", ", getAddresses()); 310 } 311 312 /** Writes to dumpsys output */ dump(String indent, PrintWriter writer)313 void dump(String indent, PrintWriter writer) { 314 synchronized (mLock) { 315 writer.printf("%sCarVolumeGroup(%d)\n", indent, mId); 316 writer.printf("%sUserId(%d)\n", indent, mUserId); 317 writer.printf("%sGain values (min / max / default/ current): %d %d %d %d\n", 318 indent, mMinGain, mMaxGain, 319 mDefaultGain, getGainForIndexLocked(mCurrentGainIndex)); 320 writer.printf("%sGain indexes (min / max / default / current): %d %d %d %d\n", 321 indent, getMinGainIndex(), getMaxGainIndex(), 322 getDefaultGainIndex(), mCurrentGainIndex); 323 for (int i = 0; i < mContextToAddress.size(); i++) { 324 writer.printf("%sContext: %s -> Address: %s\n", indent, 325 CarAudioContext.toString(mContextToAddress.keyAt(i)), 326 mContextToAddress.valueAt(i)); 327 } 328 mAddressToCarAudioDeviceInfo.keySet().stream() 329 .map(mAddressToCarAudioDeviceInfo::get) 330 .forEach((info -> info.dump(indent, writer))); 331 332 // Empty line for comfortable reading 333 writer.println(); 334 } 335 } 336 337 /** 338 * Load volumes for new user 339 * @param userId new user to load 340 */ loadVolumesForUser(@serIdInt int userId)341 void loadVolumesForUser(@UserIdInt int userId) { 342 synchronized (mLock) { 343 //Update the volume for the new user 344 updateUserIdLocked(userId); 345 //Update the current gain index 346 updateCurrentGainIndexLocked(); 347 //Reset devices with current gain index 348 } 349 setCurrentGainIndex(getCurrentGainIndex()); 350 } 351 } 352