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.DUMP_INFO;
19 
20 import android.annotation.Nullable;
21 import android.car.builtin.util.Slogf;
22 import android.car.media.CarAudioManager;
23 import android.car.media.CarAudioZoneConfigInfo;
24 import android.car.media.CarVolumeGroupEvent;
25 import android.car.media.CarVolumeGroupInfo;
26 import android.media.AudioAttributes;
27 import android.media.AudioDeviceAttributes;
28 import android.media.AudioDeviceInfo;
29 import android.media.AudioPlaybackConfiguration;
30 import android.util.SparseArray;
31 import android.util.proto.ProtoOutputStream;
32 
33 import com.android.car.CarLog;
34 import com.android.car.audio.CarAudioDumpProto.CarAudioZoneProto;
35 import com.android.car.audio.hal.HalAudioDeviceInfo;
36 import com.android.car.internal.ExcludeFromCodeCoverageGeneratedReport;
37 import com.android.car.internal.util.IndentingPrintWriter;
38 import com.android.internal.annotations.GuardedBy;
39 
40 import java.util.ArrayList;
41 import java.util.List;
42 import java.util.Objects;
43 
44 /**
45  * A class encapsulates an audio zone in car.
46  *
47  * An audio zone can contain multiple {@link CarAudioZoneConfig}s, and each zone has its own
48  * {@link CarAudioFocus} instance. Additionally, there may be dedicated hardware volume keys
49  * attached to each zone.
50  *
51  * See also the unified car_audio_configuration.xml
52  */
53 public class CarAudioZone {
54 
55     private final int mId;
56     private final String mName;
57     private final CarAudioContext mCarAudioContext;
58     private final List<AudioDeviceAttributes> mInputAudioDevice;
59     // zone configuration id to zone configuration mapping
60     // We don't protect mCarAudioZoneConfigs by a lock because it's only written at XML parsing.
61     private final SparseArray<CarAudioZoneConfig> mCarAudioZoneConfigs;
62     private final Object mLock = new Object();
63 
64     @GuardedBy("mLock")
65     private int mCurrentConfigId;
66 
CarAudioZone(CarAudioContext carAudioContext, String name, int id)67     CarAudioZone(CarAudioContext carAudioContext, String name, int id) {
68         mCarAudioContext = Objects.requireNonNull(carAudioContext,
69                 "Car audio context can not be null");
70         mName = name;
71         mId = id;
72         mCurrentConfigId = 0;
73         mInputAudioDevice = new ArrayList<>();
74         mCarAudioZoneConfigs = new SparseArray<>();
75     }
76 
getCurrentConfigId()77     private int getCurrentConfigId() {
78         synchronized (mLock) {
79             return mCurrentConfigId;
80         }
81     }
82 
getId()83     int getId() {
84         return mId;
85     }
86 
getName()87     String getName() {
88         return mName;
89     }
90 
isPrimaryZone()91     boolean isPrimaryZone() {
92         return mId == CarAudioManager.PRIMARY_AUDIO_ZONE;
93     }
94 
getCurrentCarAudioZoneConfig()95     CarAudioZoneConfig getCurrentCarAudioZoneConfig() {
96         synchronized (mLock) {
97             return mCarAudioZoneConfigs.get(mCurrentConfigId);
98         }
99     }
100 
101     @Nullable
getDefaultAudioZoneConfigInfo()102     CarAudioZoneConfigInfo getDefaultAudioZoneConfigInfo() {
103         for (int c = 0; c < mCarAudioZoneConfigs.size(); c++) {
104             if (!mCarAudioZoneConfigs.valueAt(c).isDefault()) {
105                 continue;
106             }
107             return mCarAudioZoneConfigs.valueAt(c).getCarAudioZoneConfigInfo();
108         }
109         // Should not be able to get here, for fully validated configuration.
110         Slogf.wtf(CarLog.TAG_AUDIO, "Audio zone " + mId
111                 + " does not have a default configuration");
112         return null;
113     }
114 
getAllCarAudioZoneConfigs()115     List<CarAudioZoneConfig> getAllCarAudioZoneConfigs() {
116         List<CarAudioZoneConfig> zoneConfigList = new ArrayList<>(mCarAudioZoneConfigs.size());
117         for (int index = 0; index < mCarAudioZoneConfigs.size(); index++) {
118             zoneConfigList.add(mCarAudioZoneConfigs.valueAt(index));
119         }
120         return zoneConfigList;
121     }
122 
123     @Nullable
getCurrentVolumeGroup(String groupName)124     CarVolumeGroup getCurrentVolumeGroup(String groupName) {
125         return getCurrentCarAudioZoneConfig().getVolumeGroup(groupName);
126     }
127 
getCurrentVolumeGroup(int groupId)128     CarVolumeGroup getCurrentVolumeGroup(int groupId) {
129         return getCurrentCarAudioZoneConfig().getVolumeGroup(groupId);
130     }
131 
132     /**
133      * @return Snapshot of available {@link AudioDeviceInfo}s in List.
134      */
getCurrentAudioDevices()135     List<AudioDeviceAttributes> getCurrentAudioDevices() {
136         return getCurrentCarAudioZoneConfig().getAudioDevice();
137     }
138 
getCurrentAudioDeviceSupportingDynamicMix()139     List<AudioDeviceAttributes> getCurrentAudioDeviceSupportingDynamicMix() {
140         return getCurrentCarAudioZoneConfig().getAudioDeviceSupportingDynamicMix();
141     }
142 
getCurrentVolumeGroupCount()143     int getCurrentVolumeGroupCount() {
144         return getCurrentCarAudioZoneConfig().getVolumeGroupCount();
145     }
146 
147     /**
148      * @return Snapshot of available {@link CarVolumeGroup}s in array.
149      */
getCurrentVolumeGroups()150     CarVolumeGroup[] getCurrentVolumeGroups() {
151         return getCurrentCarAudioZoneConfig().getVolumeGroups();
152     }
153 
validateCanUseDynamicMixRouting(boolean useCoreAudioRouting)154     boolean validateCanUseDynamicMixRouting(boolean useCoreAudioRouting) {
155         return getCurrentCarAudioZoneConfig().validateCanUseDynamicMixRouting(useCoreAudioRouting);
156     }
157 
158     /**
159      * Constraints applied here:
160      *
161      * <ul>
162      * <li>At least one zone configuration exists.
163      * <li>Current zone configuration exists.
164      * <li>The zone id of all zone configurations matches zone id of the zone.
165      * <li>Exactly one zone configuration is default.
166      * <li>Volume groups for each zone configuration is valid (see
167      * {@link CarAudioZoneConfig#validateVolumeGroups(CarAudioContext, boolean)}).
168      * </ul>
169      */
validateZoneConfigs(boolean useCoreAudioRouting)170     boolean validateZoneConfigs(boolean useCoreAudioRouting) {
171         if (mCarAudioZoneConfigs.size() == 0) {
172             Slogf.w(CarLog.TAG_AUDIO, "No zone configurations for zone %d", mId);
173             return false;
174         }
175         boolean isDefaultConfigFound = false;
176         for (int index = 0; index < mCarAudioZoneConfigs.size(); index++) {
177             CarAudioZoneConfig zoneConfig = mCarAudioZoneConfigs.valueAt(index);
178             if (zoneConfig.getZoneId() != mId) {
179                 Slogf.w(CarLog.TAG_AUDIO,
180                         "Zone id %d of zone configuration %d does not match zone id %d",
181                         zoneConfig.getZoneId(),
182                         mCarAudioZoneConfigs.keyAt(index), mId);
183                 return false;
184             }
185             if (zoneConfig.isDefault()) {
186                 if (isDefaultConfigFound) {
187                     Slogf.w(CarLog.TAG_AUDIO,
188                             "Multiple default zone configurations exist in zone %d", mId);
189                     return false;
190                 }
191                 isDefaultConfigFound = true;
192             }
193             if (!zoneConfig.validateVolumeGroups(mCarAudioContext, useCoreAudioRouting)) {
194                 return false;
195             }
196         }
197         if (!isDefaultConfigFound) {
198             Slogf.w(CarLog.TAG_AUDIO, "No default zone configuration exists in zone %d", mId);
199             return false;
200         }
201         return true;
202     }
203 
isCurrentZoneConfig(CarAudioZoneConfigInfo configInfoSwitchedTo)204     boolean isCurrentZoneConfig(CarAudioZoneConfigInfo configInfoSwitchedTo) {
205         synchronized (mLock) {
206             return configInfoSwitchedTo.equals(mCarAudioZoneConfigs.get(mCurrentConfigId)
207                     .getCarAudioZoneConfigInfo());
208         }
209     }
210 
setCurrentCarZoneConfig(CarAudioZoneConfigInfo configInfoSwitchedTo)211     void setCurrentCarZoneConfig(CarAudioZoneConfigInfo configInfoSwitchedTo) {
212         synchronized (mLock) {
213             if (mCurrentConfigId == configInfoSwitchedTo.getConfigId()) {
214                 return;
215             }
216             CarAudioZoneConfig previousConfig = mCarAudioZoneConfigs.get(mCurrentConfigId);
217             previousConfig.setIsSelected(false);
218             mCurrentConfigId = configInfoSwitchedTo.getConfigId();
219             CarAudioZoneConfig current = mCarAudioZoneConfigs.get(mCurrentConfigId);
220             current.setIsSelected(true);
221             current.updateVolumeDevices(mCarAudioContext.useCoreAudioRouting());
222         }
223     }
224 
init()225     void init() {
226         for (int index = 0; index < mCarAudioZoneConfigs.size(); index++) {
227             CarAudioZoneConfig config = mCarAudioZoneConfigs.valueAt(index);
228             config.synchronizeCurrentGainIndex();
229             // mCurrentConfigId should be the default config, but this may change in the future
230             // The configuration could be loaded from audio settings instead
231             if (!config.isDefault()) {
232                 continue;
233             }
234             synchronized (mLock) {
235                 mCurrentConfigId = config.getZoneConfigId();
236             }
237             config.setIsSelected(true);
238             config.updateVolumeDevices(mCarAudioContext.useCoreAudioRouting());
239         }
240     }
241 
242     @ExcludeFromCodeCoverageGeneratedReport(reason = DUMP_INFO)
dump(IndentingPrintWriter writer)243     void dump(IndentingPrintWriter writer) {
244         writer.printf("CarAudioZone(%s:%d) isPrimary? %b\n", mName, mId,
245                 isPrimaryZone());
246         writer.increaseIndent();
247         writer.printf("Current Config Id: %d\n", getCurrentConfigId());
248         writer.printf("Input Audio Device Addresses\n");
249         writer.increaseIndent();
250         for (int index = 0; index < mInputAudioDevice.size(); index++) {
251             writer.printf("Device Address(%s)\n", mInputAudioDevice.get(index).getAddress());
252         }
253         writer.decreaseIndent();
254         writer.println();
255         writer.printf("Audio Zone Configurations[%d]\n", mCarAudioZoneConfigs.size());
256         writer.increaseIndent();
257         for (int i = 0; i < mCarAudioZoneConfigs.size(); i++) {
258             mCarAudioZoneConfigs.valueAt(i).dump(writer);
259         }
260         writer.decreaseIndent();
261         writer.println();
262         writer.decreaseIndent();
263     }
264 
265     @ExcludeFromCodeCoverageGeneratedReport(reason = DUMP_INFO)
dumpProto(ProtoOutputStream proto)266     void dumpProto(ProtoOutputStream proto) {
267         long carAudioZonesToken = proto.start(CarAudioDumpProto.CAR_AUDIO_ZONES);
268         proto.write(CarAudioZoneProto.NAME, mName);
269         proto.write(CarAudioZoneProto.ID, mId);
270         proto.write(CarAudioZoneProto.PRIMARY_ZONE, isPrimaryZone());
271         proto.write(CarAudioZoneProto.CURRENT_ZONE_CONFIG_ID, getCurrentConfigId());
272         for (int index = 0; index < mInputAudioDevice.size(); index++) {
273             proto.write(CarAudioZoneProto.INPUT_AUDIO_DEVICE_ADDRESSES,
274                     mInputAudioDevice.get(index).getAddress());
275         }
276         for (int i = 0; i < mCarAudioZoneConfigs.size(); i++) {
277             mCarAudioZoneConfigs.valueAt(i).dumpProto(proto);
278         }
279         proto.end(carAudioZonesToken);
280     }
281 
282     /**
283      * Return the audio device address mapping to a car audio context
284      */
getAddressForContext(int audioContext)285     public String getAddressForContext(int audioContext) {
286         mCarAudioContext.preconditionCheckAudioContext(audioContext);
287         String deviceAddress = null;
288         for (CarVolumeGroup volumeGroup : getCurrentVolumeGroups()) {
289             deviceAddress = volumeGroup.getAddressForContext(audioContext);
290             if (deviceAddress != null) {
291                 return deviceAddress;
292             }
293         }
294         // This should not happen unless something went wrong.
295         // Device address are unique per zone and all contexts are assigned in a zone.
296         throw new IllegalStateException("Could not find output device in zone " + mId
297                 + " for audio context " + audioContext);
298     }
299 
getAudioDeviceForContext(int audioContext)300     AudioDeviceAttributes getAudioDeviceForContext(int audioContext) {
301         mCarAudioContext.preconditionCheckAudioContext(audioContext);
302         for (CarVolumeGroup volumeGroup : getCurrentVolumeGroups()) {
303             AudioDeviceAttributes audioDeviceAttributes =
304                     volumeGroup.getAudioDeviceForContext(audioContext);
305             if (audioDeviceAttributes != null) {
306                 return audioDeviceAttributes;
307             }
308         }
309         // This should not happen unless something went wrong.
310         // Device address are unique per zone and all contexts are assigned in a zone.
311         throw new IllegalStateException("Could not find output device in zone " + mId
312                 + " for audio context " + audioContext);
313     }
314 
315     /**
316      * Update the volume groups for the new user
317      * @param userId user id to update to
318      */
updateVolumeGroupsSettingsForUser(int userId)319     public void updateVolumeGroupsSettingsForUser(int userId) {
320         for (int index = 0; index < mCarAudioZoneConfigs.size(); index++) {
321             CarAudioZoneConfig config = mCarAudioZoneConfigs.valueAt(index);
322             if (!config.isSelected()) {
323                 continue;
324             }
325             config.updateVolumeGroupsSettingsForUser(userId);
326             break;
327         }
328     }
329 
addInputAudioDevice(AudioDeviceAttributes device)330     void addInputAudioDevice(AudioDeviceAttributes device) {
331         mInputAudioDevice.add(device);
332     }
333 
getInputAudioDevices()334     List<AudioDeviceAttributes> getInputAudioDevices() {
335         return mInputAudioDevice;
336     }
337 
addZoneConfig(CarAudioZoneConfig zoneConfig)338     void addZoneConfig(CarAudioZoneConfig zoneConfig) {
339         mCarAudioZoneConfigs.put(zoneConfig.getZoneConfigId(), zoneConfig);
340         if (zoneConfig.isDefault()) {
341             synchronized (mLock) {
342                 mCurrentConfigId = zoneConfig.getZoneConfigId();
343             }
344         }
345     }
346 
findActiveAudioAttributesFromPlaybackConfigurations( List<AudioPlaybackConfiguration> configurations)347     public List<AudioAttributes> findActiveAudioAttributesFromPlaybackConfigurations(
348             List<AudioPlaybackConfiguration> configurations) {
349         Objects.requireNonNull(configurations, "Audio playback configurations can not be null");
350         List<AudioAttributes> audioAttributes = new ArrayList<>();
351         for (int index = 0; index < configurations.size(); index++) {
352             AudioPlaybackConfiguration configuration = configurations.get(index);
353             if (configuration.isActive()) {
354                 if (isAudioDeviceInfoValidForZone(configuration.getAudioDeviceInfo())) {
355                     // Note that address's context and the context actually supplied could be
356                     // different
357                     audioAttributes.add(configuration.getAudioAttributes());
358                 }
359             }
360         }
361         return audioAttributes;
362     }
363 
isAudioDeviceInfoValidForZone(AudioDeviceInfo info)364     boolean isAudioDeviceInfoValidForZone(AudioDeviceInfo info) {
365         return getCurrentCarAudioZoneConfig().isAudioDeviceInfoValidForZone(info);
366     }
367 
368     @Nullable
getVolumeGroupForAudioAttributes(AudioAttributes audioAttributes)369     CarVolumeGroup getVolumeGroupForAudioAttributes(AudioAttributes audioAttributes) {
370         return getCurrentCarAudioZoneConfig().getVolumeGroupForAudioAttributes(audioAttributes);
371     }
372 
onAudioGainChanged(List<Integer> halReasons, List<CarAudioGainConfigInfo> gainInfos)373     List<CarVolumeGroupEvent> onAudioGainChanged(List<Integer> halReasons,
374             List<CarAudioGainConfigInfo> gainInfos) {
375         List<CarVolumeGroupEvent> events = new ArrayList<>();
376         for (int index = 0; index < mCarAudioZoneConfigs.size(); index++) {
377             List<CarVolumeGroupEvent> eventsForZoneConfig = mCarAudioZoneConfigs.valueAt(index)
378                     .onAudioGainChanged(halReasons, gainInfos);
379             // use events for callback only if current zone configuration
380             if (mCarAudioZoneConfigs.keyAt(index) == getCurrentConfigId()) {
381                 events.addAll(eventsForZoneConfig);
382             }
383         }
384         return events;
385     }
386 
onAudioPortsChanged(List<HalAudioDeviceInfo> deviceInfos)387     List<CarVolumeGroupEvent> onAudioPortsChanged(List<HalAudioDeviceInfo> deviceInfos) {
388         List<CarVolumeGroupEvent> events = new ArrayList<>();
389         for (int index = 0; index < mCarAudioZoneConfigs.size(); index++) {
390             List<CarVolumeGroupEvent> eventsForZoneConfig = mCarAudioZoneConfigs.valueAt(index)
391                     .onAudioPortsChanged(deviceInfos);
392             // Use events for callback only if current zone configuration
393             if (mCarAudioZoneConfigs.keyAt(index) == getCurrentConfigId()) {
394                 events.addAll(eventsForZoneConfig);
395             }
396         }
397         return events;
398     }
399 
400     /**
401      * Returns the car audio context set for the car audio zone
402      */
getCarAudioContext()403     public CarAudioContext getCarAudioContext() {
404         return mCarAudioContext;
405     }
406 
407     /**
408      * Returns the car volume infos for all the volume groups in the audio zone
409      */
getCurrentVolumeGroupInfos()410     List<CarVolumeGroupInfo> getCurrentVolumeGroupInfos() {
411         return getCurrentCarAudioZoneConfig().getVolumeGroupInfos();
412     }
413 
414     /**
415      * Returns all audio zone config info in the audio zone
416      */
getCarAudioZoneConfigInfos()417     List<CarAudioZoneConfigInfo> getCarAudioZoneConfigInfos() {
418         List<CarAudioZoneConfigInfo> zoneConfigInfos = new ArrayList<>(mCarAudioZoneConfigs.size());
419         for (int index = 0; index < mCarAudioZoneConfigs.size(); index++) {
420             zoneConfigInfos.add(mCarAudioZoneConfigs.valueAt(index).getCarAudioZoneConfigInfo());
421         }
422 
423         return zoneConfigInfos;
424     }
425 
audioDevicesAdded(List<AudioDeviceInfo> devices)426     boolean audioDevicesAdded(List<AudioDeviceInfo> devices) {
427         Objects.requireNonNull(devices, "Audio devices can not be null");
428         boolean updated = false;
429         for (int c = 0; c < mCarAudioZoneConfigs.size(); c++) {
430             if (!mCarAudioZoneConfigs.valueAt(c).audioDevicesAdded(devices)) {
431                 continue;
432             }
433             updated = true;
434         }
435         return updated;
436     }
437 
audioDevicesRemoved(List<AudioDeviceInfo> devices)438     boolean audioDevicesRemoved(List<AudioDeviceInfo> devices) {
439         Objects.requireNonNull(devices, "Audio devices can not be null");
440         boolean updated = false;
441         for (int c = 0; c < mCarAudioZoneConfigs.size(); c++) {
442             if (!mCarAudioZoneConfigs.valueAt(c).audioDevicesRemoved(devices)) {
443                 continue;
444             }
445             updated = true;
446         }
447         return updated;
448     }
449 }
450