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