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