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