1 /*
2  * Copyright (C) 2022 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 
17 package com.android.car.audio;
18 
19 import static com.android.car.internal.ExcludeFromCodeCoverageGeneratedReport.PRIVATE_CONSTRUCTOR;
20 
21 import android.annotation.Nullable;
22 import android.car.builtin.media.AudioManagerHelper;
23 import android.car.builtin.util.Slogf;
24 import android.media.AudioAttributes;
25 import android.media.audiopolicy.AudioProductStrategy;
26 import android.media.audiopolicy.AudioVolumeGroup;
27 import android.util.SparseArray;
28 
29 import com.android.car.internal.ExcludeFromCodeCoverageGeneratedReport;
30 import com.android.internal.util.Preconditions;
31 
32 import java.util.List;
33 import java.util.Objects;
34 
35 /**
36  * Helper for audio related operations for core audio routing and volume management
37  * based on {@link AudioProductStrategy} and {@link AudioVolumeGroup}
38  */
39 final class CoreAudioHelper {
40     static final String TAG = "CoreAudioHelper";
41 
42     private static final boolean DEBUG = false;
43 
44     @ExcludeFromCodeCoverageGeneratedReport(reason = PRIVATE_CONSTRUCTOR)
CoreAudioHelper()45     private CoreAudioHelper() {
46         throw new UnsupportedOperationException("CoreAudioHelper class is non-instantiable, "
47                 + "contains static members only");
48     }
49 
50     /** Invalid strategy id returned when none matches a given request. */
51     static final int INVALID_STRATEGY = -1;
52 
53     /** Invalid group id returned when none matches a given request. */
54     static final int INVALID_GROUP_ID = -1;
55 
56     /**
57      * Default {@link AudioAttributes} used to identify the default {@link AudioProductStrategy}
58      * and {@link AudioVolumeGroup}.
59      */
60     static final AudioAttributes DEFAULT_ATTRIBUTES = new AudioAttributes.Builder().build();
61 
62     /**
63      * Due to testing issue with static mock, use lazy initialize pattern for static variables
64      */
getAudioProductStrategies()65     private static List<AudioProductStrategy> getAudioProductStrategies() {
66         return StaticLazyInitializer.sAudioProductStrategies;
67     }
getAudioVolumeGroups()68     private static List<AudioVolumeGroup> getAudioVolumeGroups() {
69         return StaticLazyInitializer.sAudioVolumeGroups;
70     }
getGroupIdToNames()71     private static SparseArray<String> getGroupIdToNames() {
72         return StaticLazyInitializer.sGroupIdToNames;
73     }
74 
75     private static class StaticLazyInitializer {
76         /**
77          * @see AudioProductStrategy
78          */
79         static final List<AudioProductStrategy> sAudioProductStrategies =
80                 AudioManagerWrapper.getAudioProductStrategies();
81         /**
82          * @see AudioVolumeGroup
83          */
84         static final List<AudioVolumeGroup> sAudioVolumeGroups =
85                 AudioManagerWrapper.getAudioVolumeGroups();
86         static final SparseArray<String> sGroupIdToNames = new SparseArray<>() {
87             {
88                 for (int index = 0; index < sAudioVolumeGroups.size(); index++) {
89                     AudioVolumeGroup group = sAudioVolumeGroups.get(index);
90                     put(group.getId(), group.name());
91                 }
92             }
93         };
94     }
95 
96     /**
97      * Identifies the {@link AudioProductStrategy} supporting the given {@link AudioAttributes}.
98      *
99      * @param attributes {@link AudioAttributes} to look for.
100      * @return the id of the {@link AudioProductStrategy} supporting the
101      * given {@link AudioAttributes} if found, {@link #INVALID_STRATEGY} id otherwise.
102      */
getStrategyForAudioAttributes(AudioAttributes attributes)103     public static int getStrategyForAudioAttributes(AudioAttributes attributes) {
104         Preconditions.checkNotNull(attributes, "Audio Attributes can not be null");
105         for (int index = 0; index < getAudioProductStrategies().size(); index++) {
106             AudioProductStrategy strategy = getAudioProductStrategies().get(index);
107             if (strategy.supportsAudioAttributes(attributes)) {
108                 return strategy.getId();
109             }
110         }
111         return INVALID_STRATEGY;
112     }
113 
getStrategyForContextName(String contextName)114     public static int getStrategyForContextName(String contextName) {
115         Preconditions.checkNotNull(contextName, "Context name can not be null");
116         for (int index = 0; index < getAudioProductStrategies().size(); index++) {
117             AudioProductStrategy strategy = getAudioProductStrategies().get(index);
118             if (Objects.equals(strategy.getName(), contextName)) {
119                 return strategy.getId();
120             }
121         }
122         return INVALID_STRATEGY;
123     }
124 
125     /**
126      * Identifies the {@link AudioProductStrategy} supporting the given {@link AudioAttributes}
127      * and fallbacking on the default strategy supporting {@code DEFAULT_ATTRIBUTES} otherwise.
128      *
129      * @param attributes {@link AudioAttributes} supported by the
130      * {@link AudioProductStrategy} to look for.
131      * @return the id of the {@link AudioProductStrategy} supporting the
132      * given {@link AudioAttributes}, otherwise the id of the default strategy, aka the
133      * strategy supporting {@code DEFAULT_ATTRIBUTES}, {@code INVALID_STRATEGY} id otherwise.
134      */
getStrategyForAudioAttributesOrDefault(AudioAttributes attributes)135     public static int getStrategyForAudioAttributesOrDefault(AudioAttributes attributes) {
136         int strategyId = getStrategyForAudioAttributes(attributes);
137         return strategyId == INVALID_STRATEGY
138                 ? getStrategyForAudioAttributes(DEFAULT_ATTRIBUTES) : strategyId;
139     }
140 
141     @Nullable
getProductStrategyForAudioAttributes( AudioAttributes attributes)142     static AudioProductStrategy getProductStrategyForAudioAttributes(
143             AudioAttributes attributes) {
144         Preconditions.checkNotNull(attributes, "Audio attributes can not be null");
145         for (int index = 0; index < getAudioProductStrategies().size(); index++) {
146             AudioProductStrategy strategy = getAudioProductStrategies().get(index);
147             if (!strategy.supportsAudioAttributes(attributes)) {
148                 continue;
149             }
150             return strategy;
151         }
152         return null;
153     }
154 
155     /**
156      * Gets the {@link AudioProductStrategy} referred by its unique identifier.
157      *
158      * @param strategyId id of the {@link AudioProductStrategy} to look for
159      * @return the {@link AudioProductStrategy} referred by the given id if found, {@code null}
160      * otherwise.
161      */
162     @Nullable
getStrategy(int strategyId)163     public static AudioProductStrategy getStrategy(int strategyId) {
164         for (int index = 0; index < getAudioProductStrategies().size(); index++) {
165             AudioProductStrategy strategy = getAudioProductStrategies().get(index);
166             if (strategy.getId() == strategyId) {
167                 return strategy;
168             }
169         }
170         return null;
171     }
172 
173     /**
174      * Checks if the {@link AudioProductStrategy} referred by it id is the default.
175      *
176      * @param strategyId to look for
177      * @return {@code true} if the {@link AudioProductStrategy} referred by
178      * its id is the default, aka supports {@code DEFAULT_ATTRIBUTES}, {@code false} otherwise.
179      */
isDefaultStrategy(int strategyId)180     public static boolean isDefaultStrategy(int strategyId) {
181         for (int index = 0; index < getAudioProductStrategies().size(); index++) {
182             AudioProductStrategy strategy = getAudioProductStrategies().get(index);
183             if (strategy.getId() == strategyId) {
184                 return strategy.supportsAudioAttributes(DEFAULT_ATTRIBUTES);
185             }
186         }
187         return false;
188     }
189 
190     /**
191      * Gets the {@link AudioVolumeGroup} referred by it name.
192      *
193      * @param groupName name of the {@link AudioVolumeGroup} to look for.
194      * @return the {@link AudioVolumeGroup} referred by the given id if found, {@code null}
195      * otherwise.
196      */
197     @Nullable
getVolumeGroup(String groupName)198     public static AudioVolumeGroup getVolumeGroup(String groupName) {
199         for (int index = 0; index < getAudioVolumeGroups().size(); index++) {
200             AudioVolumeGroup group = getAudioVolumeGroups().get(index);
201             if (DEBUG) {
202                 Slogf.d(TAG, "requested %s has %s,", groupName, group);
203             }
204             if (group.name().equals(groupName)) {
205                 return group;
206             }
207         }
208         return null;
209     }
210 
211     /**
212      * Gets the most representative {@link AudioAttributes} of a given {@link AudioVolumeGroup}
213      * referred by it s name.
214      * <p>When relying on core audio to control volume, Volume APIs are based on AudioAttributes,
215      * thus, selecting the most representative attributes (not default without tag, with tag as
216      * fallback, {@link #DEFAULT_ATTRIBUTES} otherwise) will help identify the request.
217      *
218      * @param groupName name of the {@link AudioVolumeGroup} to look for.
219      * @return the best {@link AudioAttributes} for a given volume group id,
220      * {@link #DEFAULT_ATTRIBUTES} otherwise.
221      */
selectAttributesForVolumeGroupName(String groupName)222     public static AudioAttributes selectAttributesForVolumeGroupName(String groupName) {
223         AudioVolumeGroup group = getVolumeGroup(groupName);
224         AudioAttributes bestAttributes = DEFAULT_ATTRIBUTES;
225         if (group == null) {
226             return bestAttributes;
227         }
228         for (int index = 0; index < group.getAudioAttributes().size(); index++) {
229             AudioAttributes attributes = group.getAudioAttributes().get(index);
230             // bestAttributes attributes are not default and without tag (most generic as possible)
231             if (!attributes.equals(DEFAULT_ATTRIBUTES)) {
232                 bestAttributes = attributes;
233                 if (Objects.equals(AudioManagerHelper.getFormattedTags(attributes), "")) {
234                     break;
235                 }
236             }
237         }
238         return bestAttributes;
239     }
240 
241     /**
242      * Gets the name of the {@link AudioVolumeGroup} supporting given {@link AudioAttributes},
243      * {@code null} is returned if none is found.
244      *
245      * @param attributes {@link AudioAttributes} supported by the group to look for.
246      *
247      * @return the name of the {@link AudioVolumeGroup} supporting the given audio attributes,
248      * {@code null} otherwise.
249      */
250     @Nullable
getVolumeGroupNameForAudioAttributes(AudioAttributes attributes)251     public static String getVolumeGroupNameForAudioAttributes(AudioAttributes attributes) {
252         Preconditions.checkNotNull(attributes, "Audio Attributes can not be null");
253         int volumeGroupId = getVolumeGroupIdForAudioAttributes(attributes);
254         return volumeGroupId != AudioVolumeGroup.DEFAULT_VOLUME_GROUP
255                 ? getVolumeGroupNameFromCoreId(volumeGroupId) : null;
256     }
257 
258     /**
259      * Gets the name of the {@link AudioVolumeGroup} referred by its id.
260      *
261      * @param coreGroupId id of the volume group to look for.
262      * @return the volume group id referred by its name if found, throws an exception otherwise.
263      */
264     @Nullable
getVolumeGroupNameFromCoreId(int coreGroupId)265     public static String getVolumeGroupNameFromCoreId(int coreGroupId) {
266         return getGroupIdToNames().get(coreGroupId);
267     }
268 
269     /**
270      * Gets the {@link AudioVolumeGroup} id associated to the given {@link AudioAttributes}.
271      *
272      * @param attributes {@link AudioAttributes} to be considered
273      * @return the id of the {@link AudioVolumeGroup} supporting the given {@link AudioAttributes}
274      * if found, {@link #INVALID_GROUP_ID} otherwise.
275      */
getVolumeGroupIdForAudioAttributes(AudioAttributes attributes)276     public static int getVolumeGroupIdForAudioAttributes(AudioAttributes attributes) {
277         Preconditions.checkNotNull(attributes, "Audio Attributes can not be null");
278         for (int index = 0; index < getAudioProductStrategies().size(); index++) {
279             AudioProductStrategy strategy = getAudioProductStrategies().get(index);
280             int volumeGroupId =
281                     AudioManagerHelper.getVolumeGroupIdForAudioAttributes(strategy, attributes);
282             Slogf.d(TAG, "getVolumeGroupIdForAudioAttributes %s %s,", volumeGroupId, strategy);
283             if (volumeGroupId != AudioVolumeGroup.DEFAULT_VOLUME_GROUP) {
284                 return volumeGroupId;
285             }
286         }
287         return INVALID_GROUP_ID;
288     }
289 }
290