• Home
  • Line#
  • Scopes#
  • Navigate#
  • Raw
  • Download
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.synchronizeCurrentGainIndex();
222             current.updateVolumeDevices();
223         }
224     }
225 
init()226     void init() {
227         int defaultConfig = -1;
228         for (int index = 0; index < mCarAudioZoneConfigs.size(); index++) {
229             CarAudioZoneConfig config = mCarAudioZoneConfigs.valueAt(index);
230             // mCurrentConfigId should be the default config, but this may change in the future
231             // The configuration could be loaded from audio settings instead
232             if (!config.isDefault()) {
233                 continue;
234             }
235             defaultConfig = config.getZoneConfigId();
236             config.setIsSelected(true);
237             config.synchronizeCurrentGainIndex();
238             config.updateVolumeDevices();
239             break;
240         }
241         synchronized (mLock) {
242             mCurrentConfigId = defaultConfig;
243         }
244     }
245 
246     @ExcludeFromCodeCoverageGeneratedReport(reason = DUMP_INFO)
dump(IndentingPrintWriter writer)247     void dump(IndentingPrintWriter writer) {
248         writer.printf("CarAudioZone(%s:%d) isPrimary? %b\n", mName, mId,
249                 isPrimaryZone());
250         writer.increaseIndent();
251         writer.printf("Current Config Id: %d\n", getCurrentConfigId());
252         writer.printf("Input Audio Device Addresses\n");
253         writer.increaseIndent();
254         for (int index = 0; index < mInputAudioDevice.size(); index++) {
255             writer.printf("Device Address(%s)\n", mInputAudioDevice.get(index).getAddress());
256         }
257         writer.decreaseIndent();
258         writer.println();
259         writer.printf("Audio Zone Configurations[%d]\n", mCarAudioZoneConfigs.size());
260         writer.increaseIndent();
261         for (int i = 0; i < mCarAudioZoneConfigs.size(); i++) {
262             mCarAudioZoneConfigs.valueAt(i).dump(writer);
263         }
264         writer.decreaseIndent();
265         writer.println();
266         writer.decreaseIndent();
267     }
268 
269     @ExcludeFromCodeCoverageGeneratedReport(reason = DUMP_INFO)
dumpProto(ProtoOutputStream proto)270     void dumpProto(ProtoOutputStream proto) {
271         long carAudioZonesToken = proto.start(CarAudioDumpProto.CAR_AUDIO_ZONES);
272         proto.write(CarAudioZoneProto.NAME, mName);
273         proto.write(CarAudioZoneProto.ID, mId);
274         proto.write(CarAudioZoneProto.PRIMARY_ZONE, isPrimaryZone());
275         proto.write(CarAudioZoneProto.CURRENT_ZONE_CONFIG_ID, getCurrentConfigId());
276         for (int index = 0; index < mInputAudioDevice.size(); index++) {
277             proto.write(CarAudioZoneProto.INPUT_AUDIO_DEVICE_ADDRESSES,
278                     mInputAudioDevice.get(index).getAddress());
279         }
280         for (int i = 0; i < mCarAudioZoneConfigs.size(); i++) {
281             mCarAudioZoneConfigs.valueAt(i).dumpProto(proto);
282         }
283         proto.end(carAudioZonesToken);
284     }
285 
286     /**
287      * Return the audio device address mapping to a car audio context
288      */
getAddressForContext(int audioContext)289     public String getAddressForContext(int audioContext) {
290         mCarAudioContext.preconditionCheckAudioContext(audioContext);
291         String deviceAddress = null;
292         for (CarVolumeGroup volumeGroup : getCurrentVolumeGroups()) {
293             deviceAddress = volumeGroup.getAddressForContext(audioContext);
294             if (deviceAddress != null) {
295                 return deviceAddress;
296             }
297         }
298         // This should not happen unless something went wrong.
299         // Device address are unique per zone and all contexts are assigned in a zone.
300         throw new IllegalStateException("Could not find output device in zone " + mId
301                 + " for audio context " + audioContext);
302     }
303 
getAudioDeviceForContext(int audioContext)304     AudioDeviceAttributes getAudioDeviceForContext(int audioContext) {
305         mCarAudioContext.preconditionCheckAudioContext(audioContext);
306         for (CarVolumeGroup volumeGroup : getCurrentVolumeGroups()) {
307             AudioDeviceAttributes audioDeviceAttributes =
308                     volumeGroup.getAudioDeviceForContext(audioContext);
309             if (audioDeviceAttributes != null) {
310                 return audioDeviceAttributes;
311             }
312         }
313         // This should not happen unless something went wrong.
314         // Device address are unique per zone and all contexts are assigned in a zone.
315         throw new IllegalStateException("Could not find output device in zone " + mId
316                 + " for audio context " + audioContext);
317     }
318 
319     /**
320      * Update the volume groups for the new user
321      * @param userId user id to update to
322      */
updateVolumeGroupsSettingsForUser(int userId)323     public void updateVolumeGroupsSettingsForUser(int userId) {
324         for (int index = 0; index < mCarAudioZoneConfigs.size(); index++) {
325             CarAudioZoneConfig config = mCarAudioZoneConfigs.valueAt(index);
326             if (!config.isSelected()) {
327                 continue;
328             }
329             config.updateVolumeGroupsSettingsForUser(userId);
330             break;
331         }
332     }
333 
addInputAudioDevice(AudioDeviceAttributes device)334     void addInputAudioDevice(AudioDeviceAttributes device) {
335         mInputAudioDevice.add(device);
336     }
337 
getInputAudioDevices()338     List<AudioDeviceAttributes> getInputAudioDevices() {
339         return mInputAudioDevice;
340     }
341 
addZoneConfig(CarAudioZoneConfig zoneConfig)342     void addZoneConfig(CarAudioZoneConfig zoneConfig) {
343         mCarAudioZoneConfigs.put(zoneConfig.getZoneConfigId(), zoneConfig);
344         if (zoneConfig.isDefault()) {
345             synchronized (mLock) {
346                 mCurrentConfigId = zoneConfig.getZoneConfigId();
347             }
348         }
349     }
350 
findActiveAudioAttributesFromPlaybackConfigurations( List<AudioPlaybackConfiguration> configurations)351     public List<AudioAttributes> findActiveAudioAttributesFromPlaybackConfigurations(
352             List<AudioPlaybackConfiguration> configurations) {
353         Objects.requireNonNull(configurations, "Audio playback configurations can not be null");
354         List<AudioAttributes> audioAttributes = new ArrayList<>();
355         for (int index = 0; index < configurations.size(); index++) {
356             AudioPlaybackConfiguration configuration = configurations.get(index);
357             if (configuration.isActive()) {
358                 if (isAudioDeviceInfoValidForZone(configuration.getAudioDeviceInfo())) {
359                     // Note that address's context and the context actually supplied could be
360                     // different
361                     audioAttributes.add(configuration.getAudioAttributes());
362                 }
363             }
364         }
365         return audioAttributes;
366     }
367 
isAudioDeviceInfoValidForZone(AudioDeviceInfo info)368     boolean isAudioDeviceInfoValidForZone(AudioDeviceInfo info) {
369         return getCurrentCarAudioZoneConfig().isAudioDeviceInfoValidForZone(info);
370     }
371 
372     @Nullable
getVolumeGroupForAudioAttributes(AudioAttributes audioAttributes)373     CarVolumeGroup getVolumeGroupForAudioAttributes(AudioAttributes audioAttributes) {
374         return getCurrentCarAudioZoneConfig().getVolumeGroupForAudioAttributes(audioAttributes);
375     }
376 
onAudioGainChanged(List<Integer> halReasons, List<CarAudioGainConfigInfo> gainInfos)377     List<CarVolumeGroupEvent> onAudioGainChanged(List<Integer> halReasons,
378             List<CarAudioGainConfigInfo> gainInfos) {
379         List<CarVolumeGroupEvent> events = new ArrayList<>();
380         for (int index = 0; index < mCarAudioZoneConfigs.size(); index++) {
381             List<CarVolumeGroupEvent> eventsForZoneConfig = mCarAudioZoneConfigs.valueAt(index)
382                     .onAudioGainChanged(halReasons, gainInfos);
383             // use events for callback only if current zone configuration
384             if (mCarAudioZoneConfigs.keyAt(index) == getCurrentConfigId()) {
385                 events.addAll(eventsForZoneConfig);
386             }
387         }
388         return events;
389     }
390 
onAudioPortsChanged(List<HalAudioDeviceInfo> deviceInfos)391     List<CarVolumeGroupEvent> onAudioPortsChanged(List<HalAudioDeviceInfo> deviceInfos) {
392         List<CarVolumeGroupEvent> events = new ArrayList<>();
393         for (int index = 0; index < mCarAudioZoneConfigs.size(); index++) {
394             List<CarVolumeGroupEvent> eventsForZoneConfig = mCarAudioZoneConfigs.valueAt(index)
395                     .onAudioPortsChanged(deviceInfos);
396             // Use events for callback only if current zone configuration
397             if (mCarAudioZoneConfigs.keyAt(index) == getCurrentConfigId()) {
398                 events.addAll(eventsForZoneConfig);
399             }
400         }
401         return events;
402     }
403 
404     /**
405      * Returns the car audio context set for the car audio zone
406      */
getCarAudioContext()407     public CarAudioContext getCarAudioContext() {
408         return mCarAudioContext;
409     }
410 
411     /**
412      * Returns the car volume infos for all the volume groups in the audio zone
413      */
getCurrentVolumeGroupInfos()414     List<CarVolumeGroupInfo> getCurrentVolumeGroupInfos() {
415         return getCurrentCarAudioZoneConfig().getVolumeGroupInfos();
416     }
417 
418     /**
419      * Returns all audio zone config info in the audio zone
420      */
getCarAudioZoneConfigInfos()421     List<CarAudioZoneConfigInfo> getCarAudioZoneConfigInfos() {
422         List<CarAudioZoneConfigInfo> zoneConfigInfos = new ArrayList<>(mCarAudioZoneConfigs.size());
423         for (int index = 0; index < mCarAudioZoneConfigs.size(); index++) {
424             zoneConfigInfos.add(mCarAudioZoneConfigs.valueAt(index).getCarAudioZoneConfigInfo());
425         }
426 
427         return zoneConfigInfos;
428     }
429 
audioDevicesAdded(List<AudioDeviceInfo> devices)430     boolean audioDevicesAdded(List<AudioDeviceInfo> devices) {
431         Objects.requireNonNull(devices, "Audio devices can not be null");
432         boolean updated = false;
433         for (int c = 0; c < mCarAudioZoneConfigs.size(); c++) {
434             if (!mCarAudioZoneConfigs.valueAt(c).audioDevicesAdded(devices)) {
435                 continue;
436             }
437             updated = true;
438         }
439         return updated;
440     }
441 
audioDevicesRemoved(List<AudioDeviceInfo> devices)442     boolean audioDevicesRemoved(List<AudioDeviceInfo> devices) {
443         Objects.requireNonNull(devices, "Audio devices can not be null");
444         boolean updated = false;
445         for (int c = 0; c < mCarAudioZoneConfigs.size(); c++) {
446             if (!mCarAudioZoneConfigs.valueAt(c).audioDevicesRemoved(devices)) {
447                 continue;
448             }
449             updated = true;
450         }
451         return updated;
452     }
453 }
454