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.car.media.CarAudioManager;
21 import android.content.ContentResolver;
22 import android.content.Context;
23 import android.hardware.automotive.audiocontrol.V1_0.ContextNumber;
24 import android.media.AudioDevicePort;
25 import android.provider.Settings;
26 import android.util.SparseArray;
27 import android.util.SparseIntArray;
28 
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.List;
35 
36 /**
37  * A class encapsulates a volume group in car.
38  *
39  * Volume in a car is controlled by group. A group holds one or more car audio contexts.
40  * Call {@link CarAudioManager#getVolumeGroupCount()} to get the count of {@link CarVolumeGroup}
41  * supported in a car.
42  */
43 /* package */ final class CarVolumeGroup {
44 
45     private final ContentResolver mContentResolver;
46     private final int mZoneId;
47     private final int mId;
48     private final SparseIntArray mContextToBus = new SparseIntArray();
49     private final SparseArray<CarAudioDeviceInfo> mBusToCarAudioDeviceInfo = new SparseArray<>();
50 
51     private int mDefaultGain = Integer.MIN_VALUE;
52     private int mMaxGain = Integer.MIN_VALUE;
53     private int mMinGain = Integer.MAX_VALUE;
54     private int mStepSize = 0;
55     private int mStoredGainIndex;
56     private int mCurrentGainIndex = -1;
57 
58     /**
59      * Constructs a {@link CarVolumeGroup} instance
60      * @param context {@link Context} instance
61      * @param zoneId Audio zone this volume group belongs to
62      * @param id ID of this volume group
63      */
CarVolumeGroup(Context context, int zoneId, int id)64     CarVolumeGroup(Context context, int zoneId, int id) {
65         mContentResolver = context.getContentResolver();
66         mZoneId = zoneId;
67         mId = id;
68         mStoredGainIndex = Settings.Global.getInt(mContentResolver,
69                 CarAudioService.getVolumeSettingsKeyForGroup(mZoneId, mId), -1);
70     }
71 
72     /**
73      * Constructs a {@link CarVolumeGroup} instance
74      * @param context {@link Context} instance
75      * @param zoneId Audio zone this volume group belongs to
76      * @param id ID of this volume group
77      * @param contexts Pre-populated array of car contexts, for legacy car_volume_groups.xml only
78      * @deprecated In favor of {@link #CarVolumeGroup(Context, int, int)}
79      */
80     @Deprecated
CarVolumeGroup(Context context, int zoneId, int id, @NonNull int[] contexts)81     CarVolumeGroup(Context context, int zoneId, int id, @NonNull int[] contexts) {
82         this(context, zoneId, id);
83         // Deal with the pre-populated car audio contexts
84         for (int audioContext : contexts) {
85             mContextToBus.put(audioContext, -1);
86         }
87     }
88 
89     /**
90      * @param busNumber Physical bus number for the audio device port
91      * @return {@link CarAudioDeviceInfo} associated with a given bus number
92      */
getCarAudioDeviceInfoForBus(int busNumber)93     CarAudioDeviceInfo getCarAudioDeviceInfoForBus(int busNumber) {
94         return mBusToCarAudioDeviceInfo.get(busNumber);
95     }
96 
97     /**
98      * @return Array of context numbers in this {@link CarVolumeGroup}
99      */
getContexts()100     int[] getContexts() {
101         final int[] contextNumbers = new int[mContextToBus.size()];
102         for (int i = 0; i < contextNumbers.length; i++) {
103             contextNumbers[i] = mContextToBus.keyAt(i);
104         }
105         return contextNumbers;
106     }
107 
108     /**
109      * @param busNumber Physical bus number for the audio device port
110      * @return Array of context numbers assigned to a given bus number
111      */
getContextsForBus(int busNumber)112     int[] getContextsForBus(int busNumber) {
113         List<Integer> contextNumbers = new ArrayList<>();
114         for (int i = 0; i < mContextToBus.size(); i++) {
115             int value = mContextToBus.valueAt(i);
116             if (value == busNumber) {
117                 contextNumbers.add(mContextToBus.keyAt(i));
118             }
119         }
120         return contextNumbers.stream().mapToInt(i -> i).toArray();
121     }
122 
123     /**
124      * @return Array of bus numbers in this {@link CarVolumeGroup}
125      */
getBusNumbers()126     int[] getBusNumbers() {
127         final int[] busNumbers = new int[mBusToCarAudioDeviceInfo.size()];
128         for (int i = 0; i < busNumbers.length; i++) {
129             busNumbers[i] = mBusToCarAudioDeviceInfo.keyAt(i);
130         }
131         return busNumbers;
132     }
133 
134     /**
135      * Binds the context number to physical bus number and audio device port information.
136      * Because this may change the groups min/max values, thus invalidating an index computed from
137      * a gain before this call, all calls to this function must happen at startup before any
138      * set/getGainIndex calls.
139      *
140      * @param contextNumber Context number as defined in audio control HAL
141      * @param busNumber Physical bus number for the audio device port
142      * @param info {@link CarAudioDeviceInfo} instance relates to the physical bus
143      */
bind(int contextNumber, int busNumber, CarAudioDeviceInfo info)144     void bind(int contextNumber, int busNumber, CarAudioDeviceInfo info) {
145         if (mBusToCarAudioDeviceInfo.size() == 0) {
146             mStepSize = info.getAudioGain().stepValue();
147         } else {
148             Preconditions.checkArgument(
149                     info.getAudioGain().stepValue() == mStepSize,
150                     "Gain controls within one group must have same step value");
151         }
152 
153         mContextToBus.put(contextNumber, busNumber);
154         mBusToCarAudioDeviceInfo.put(busNumber, info);
155 
156         if (info.getDefaultGain() > mDefaultGain) {
157             // We're arbitrarily selecting the highest bus default gain as the group's default.
158             mDefaultGain = info.getDefaultGain();
159         }
160         if (info.getMaxGain() > mMaxGain) {
161             mMaxGain = info.getMaxGain();
162         }
163         if (info.getMinGain() < mMinGain) {
164             mMinGain = info.getMinGain();
165         }
166         if (mStoredGainIndex < getMinGainIndex() || mStoredGainIndex > getMaxGainIndex()) {
167             // We expected to load a value from last boot, but if we didn't (perhaps this is the
168             // first boot ever?), then use the highest "default" we've seen to initialize
169             // ourselves.
170             mCurrentGainIndex = getIndexForGain(mDefaultGain);
171         } else {
172             // Just use the gain index we stored last time the gain was set (presumably during our
173             // last boot cycle).
174             mCurrentGainIndex = mStoredGainIndex;
175         }
176     }
177 
getDefaultGainIndex()178     private int getDefaultGainIndex() {
179         return getIndexForGain(mDefaultGain);
180     }
181 
getMaxGainIndex()182     int getMaxGainIndex() {
183         return getIndexForGain(mMaxGain);
184     }
185 
getMinGainIndex()186     int getMinGainIndex() {
187         return getIndexForGain(mMinGain);
188     }
189 
getCurrentGainIndex()190     int getCurrentGainIndex() {
191         return mCurrentGainIndex;
192     }
193 
194     /**
195      * Sets the gain on this group, gain will be set on all buses within same bus.
196      * @param gainIndex The gain index
197      */
setCurrentGainIndex(int gainIndex)198     void setCurrentGainIndex(int gainIndex) {
199         int gainInMillibels = getGainForIndex(gainIndex);
200 
201         Preconditions.checkArgument(
202                 gainInMillibels >= mMinGain && gainInMillibels <= mMaxGain,
203                 "Gain out of range ("
204                         + mMinGain + ":"
205                         + mMaxGain + ") "
206                         + gainInMillibels + "index "
207                         + gainIndex);
208 
209         for (int i = 0; i < mBusToCarAudioDeviceInfo.size(); i++) {
210             CarAudioDeviceInfo info = mBusToCarAudioDeviceInfo.valueAt(i);
211             info.setCurrentGain(gainInMillibels);
212         }
213 
214         mCurrentGainIndex = gainIndex;
215         Settings.Global.putInt(mContentResolver,
216                 CarAudioService.getVolumeSettingsKeyForGroup(mZoneId, mId), gainIndex);
217     }
218 
219     // Given a group level gain index, return the computed gain in millibells
220     // TODO (randolphs) If we ever want to add index to gain curves other than lock-stepped
221     // linear, this would be the place to do it.
getGainForIndex(int gainIndex)222     private int getGainForIndex(int gainIndex) {
223         return mMinGain + gainIndex * mStepSize;
224     }
225 
226     // TODO (randolphs) if we ever went to a non-linear index to gain curve mapping, we'd need to
227     // revisit this as it assumes (at the least) that getGainForIndex is reversible.  Luckily,
228     // this is an internal implementation details we could factor out if/when necessary.
getIndexForGain(int gainInMillibel)229     private int getIndexForGain(int gainInMillibel) {
230         return (gainInMillibel - mMinGain) / mStepSize;
231     }
232 
233     /**
234      * Gets {@link AudioDevicePort} from a context number
235      */
236     @Nullable
getAudioDevicePortForContext(int contextNumber)237     AudioDevicePort getAudioDevicePortForContext(int contextNumber) {
238         final int busNumber = mContextToBus.get(contextNumber, -1);
239         if (busNumber < 0 || mBusToCarAudioDeviceInfo.get(busNumber) == null) {
240             return null;
241         }
242         return mBusToCarAudioDeviceInfo.get(busNumber).getAudioDevicePort();
243     }
244 
245     @Override
toString()246     public String toString() {
247         return "CarVolumeGroup id: " + mId
248                 + " currentGainIndex: " + mCurrentGainIndex
249                 + " contexts: " + Arrays.toString(getContexts())
250                 + " buses: " + Arrays.toString(getBusNumbers());
251     }
252 
253     /** Writes to dumpsys output */
dump(String indent, PrintWriter writer)254     void dump(String indent, PrintWriter writer) {
255         writer.printf("%sCarVolumeGroup(%d)\n", indent, mId);
256         writer.printf("%sGain values (min / max / default/ current): %d %d %d %d\n",
257                 indent, mMinGain, mMaxGain,
258                 mDefaultGain, getGainForIndex(mCurrentGainIndex));
259         writer.printf("%sGain indexes (min / max / default / current): %d %d %d %d\n",
260                 indent, getMinGainIndex(), getMaxGainIndex(),
261                 getDefaultGainIndex(), mCurrentGainIndex);
262         for (int i = 0; i < mContextToBus.size(); i++) {
263             writer.printf("%sContext: %s -> Bus: %d\n", indent,
264                     ContextNumber.toString(mContextToBus.keyAt(i)), mContextToBus.valueAt(i));
265         }
266         for (int i = 0; i < mBusToCarAudioDeviceInfo.size(); i++) {
267             mBusToCarAudioDeviceInfo.valueAt(i).dump(indent, writer);
268         }
269         // Empty line for comfortable reading
270         writer.println();
271     }
272 }
273