• Home
  • Line#
  • Scopes#
  • Navigate#
  • Raw
  • Download
1 /*
2  * Copyright (C) 2019 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 android.car.media.CarAudioManager.PRIMARY_AUDIO_ZONE;
19 import static android.media.AudioDeviceInfo.TYPE_BUILTIN_MIC;
20 
21 import static com.android.car.internal.ExcludeFromCodeCoverageGeneratedReport.PRIVATE_CONSTRUCTOR;
22 
23 import android.media.AudioDeviceAttributes;
24 import android.util.ArraySet;
25 import android.util.SparseArray;
26 
27 import com.android.car.internal.ExcludeFromCodeCoverageGeneratedReport;
28 import com.android.internal.util.Preconditions;
29 
30 import java.util.List;
31 import java.util.Set;
32 
33 /*
34  * Class to help validate audio zones are constructed correctly.
35  */
36 final class CarAudioZonesValidator {
37 
38     @ExcludeFromCodeCoverageGeneratedReport(reason = PRIVATE_CONSTRUCTOR)
CarAudioZonesValidator()39     private CarAudioZonesValidator() {
40         throw new UnsupportedOperationException(
41                 "CarAudioZonesValidator class is non-instantiable, contains static members only");
42     }
43 
44     /**
45      * Returns {@code true} if validation succeeds, throws a run time exception otherwise.
46      *
47      * <p>The current rules that apply are:
48      * <ul>
49      *  <li>There must be a zone defined</li>
50      *  <li>Has valid zone configuration,
51      *   @see CarAudioZoneConfig#validateVolumeGroups(CarAudioContext, boolean) for further
52      *   information.
53      *  </li>
54      *  <li>Configurations can be routed by dynamic audio policy if core routing is not used,
55      *   @see CarAudioZoneConfig#validateCanUseDynamicMixRouting(boolean) for further information.
56      *  </li>
57      *  <li>Device addresses are not shared across zones</li>
58      *  <li>Device addresses are not shared across volume groups in same config</li>
59      *  <li>Device addresses can be shared across configs in the same zone</li>
60      * </ul>
61      * </p>
62      *
63      * @param carAudioZones Audio zones to validate
64      * @param useCoreAudioRouting If the service is using core audio routing
65      * @throws RuntimeException when ever there is a failure when validating the audio zones
66      */
validate(SparseArray<CarAudioZone> carAudioZones, boolean useCoreAudioRouting)67     static void validate(SparseArray<CarAudioZone> carAudioZones, boolean useCoreAudioRouting)
68             throws RuntimeException {
69         validateWithoutInputDevicesCheck(carAudioZones, useCoreAudioRouting);
70         validatePrimaryZoneHasInputDevice(carAudioZones);
71     }
72 
73     /**
74      * Returns {@code true} if validation succeeds without checking input devices for each zone,
75      *  throws a run time exception otherwise.
76      *
77      * <p>The current rules that apply are:
78      * <ul>
79      * <li>There must be a zone defined</li>
80      * <li>Has valid zone configuration,
81      * @see CarAudioZoneConfig#validateVolumeGroups(CarAudioContext, boolean) for further
82      * information.
83      * </li>
84      * <li>Configurations can be routed by dynamic audio policy if core routing is not used,
85      * @see CarAudioZoneConfig#validateCanUseDynamicMixRouting(boolean) for further information.
86      * </li>
87      * <li>Device addresses are not shared across zones</li>
88      * <li>Device addresses are not shared across volume groups in same config</li>
89      * <li>Device addresses can be shared across configs in the same zone</li>
90      * </ul>
91      * </p>
92      *
93      * @param carAudioZones Audio zones to validate
94      * @param useCoreAudioRouting If the service is using core audio routing
95      * @throws RuntimeException when ever there is a failure when validating the audio zones
96      */
validateWithoutInputDevicesCheck(SparseArray<CarAudioZone> carAudioZones, boolean useCoreAudioRouting)97     static void validateWithoutInputDevicesCheck(SparseArray<CarAudioZone> carAudioZones,
98             boolean useCoreAudioRouting)
99             throws RuntimeException {
100         validateAtLeastOneZoneDefined(carAudioZones);
101         validateZoneConfigsForEachZone(carAudioZones, useCoreAudioRouting);
102         if (!useCoreAudioRouting) {
103             validateEachAddressAppearsAtMostOnceInOneConfig(carAudioZones);
104         }
105     }
106 
validatePrimaryZoneHasInputDevice(SparseArray<CarAudioZone> carAudioZones)107     private static void validatePrimaryZoneHasInputDevice(SparseArray<CarAudioZone> carAudioZones) {
108         CarAudioZone primaryZone = carAudioZones.get(PRIMARY_AUDIO_ZONE);
109         List<AudioDeviceAttributes> devices = primaryZone.getInputAudioDevices();
110         Preconditions.checkCollectionNotEmpty(devices, "Primary Zone Input Devices");
111         for (int index = 0; index < devices.size(); index++) {
112             AudioDeviceAttributes device = devices.get(index);
113             if (device.getType() == TYPE_BUILTIN_MIC) {
114                 return;
115             }
116         }
117         throw new RuntimeException("Primary Zone must have at least one microphone input device");
118     }
119 
validateAtLeastOneZoneDefined(SparseArray<CarAudioZone> carAudioZones)120     private static void validateAtLeastOneZoneDefined(SparseArray<CarAudioZone> carAudioZones) {
121         if (carAudioZones.size() == 0) {
122             throw new RuntimeException("At least one zone should be defined");
123         }
124     }
125 
validateZoneConfigsForEachZone(SparseArray<CarAudioZone> carAudioZones, boolean useCoreAudioRouting)126     private static void validateZoneConfigsForEachZone(SparseArray<CarAudioZone> carAudioZones,
127             boolean useCoreAudioRouting) {
128         for (int i = 0; i < carAudioZones.size(); i++) {
129             CarAudioZone zone = carAudioZones.valueAt(i);
130             if (!zone.validateZoneConfigs(useCoreAudioRouting)) {
131                 throw new RuntimeException(
132                         "Invalid zone configurations for zone " + zone.getId());
133             }
134             // TODO(b/301391301) use fore routing for all zones.
135             // Currently force "useCoreAudioRouting" to be false for non primary zones as only
136             // primary zone supports core routing
137             if (!zone.validateCanUseDynamicMixRouting(
138                     zone.isPrimaryZone() && useCoreAudioRouting)) {
139                 throw new RuntimeException(
140                         "Invalid Configuration to use Dynamic Mix for zone " + zone.getId());
141             }
142         }
143     }
144 
validateEachAddressAppearsAtMostOnceInOneConfig( SparseArray<CarAudioZone> carAudioZones)145     private static void validateEachAddressAppearsAtMostOnceInOneConfig(
146             SparseArray<CarAudioZone> carAudioZones) {
147         Set<String> addresses = new ArraySet<>();
148         for (int i = 0; i < carAudioZones.size(); i++) {
149             List<CarAudioZoneConfig> zoneConfigs =
150                     carAudioZones.valueAt(i).getAllCarAudioZoneConfigs();
151             ArraySet<String> addressesPerZone = new ArraySet<>();
152             for (int configIndex = 0; configIndex < zoneConfigs.size(); configIndex++) {
153                 Set<String> addressesPerConfig = new ArraySet<>();
154                 CarAudioZoneConfig config = zoneConfigs.get(configIndex);
155                 CarVolumeGroup[] groups = config.getVolumeGroups();
156                 for (CarVolumeGroup carVolumeGroup : groups) {
157                     validateVolumeGroupAddresses(addressesPerConfig, carVolumeGroup.getAddresses());
158                 }
159                 // No need to check for addresses shared among configurations
160                 // as that is allowed
161                 addressesPerZone.addAll(addressesPerConfig);
162             }
163 
164             for (int c = 0; c < addressesPerZone.size(); c++) {
165                 String address = addressesPerZone.valueAt(c);
166                 if (addresses.add(address)) {
167                     continue;
168                 }
169                 throw  new IllegalStateException("Address " + address + " repeats among multiple"
170                         + " zones in car_audio_configuration.xml");
171             }
172         }
173     }
174 
validateVolumeGroupAddresses(Set<String> addressesPerConfig, List<String> groupAddresses)175     private static void validateVolumeGroupAddresses(Set<String> addressesPerConfig,
176                                                      List<String> groupAddresses) {
177         for (int c = 0; c < groupAddresses.size(); c++) {
178             String address = groupAddresses.get(c);
179             // Ignore dynamic devices as they may not have addresses until the device is connected
180             if (address == null || address.isEmpty()) {
181                 continue;
182             }
183             if (!addressesPerConfig.add(address)) {
184                 throw new RuntimeException("Device with address " + address
185                         + " appears in multiple volume groups in the same configuration");
186             }
187         }
188     }
189 }
190