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 an a run time exception otherwise. 46 * 47 * <p>The current rules that apply are: 48 * <ul> 49 * <li>There must be a zone defined 50 * <li>Has valid zone configuration, see 51 * {@link CarAudioZoneConfig#validateVolumeGroups(CarAudioContext, boolean)}) for further 52 * information. 53 * <li>Configurations can be routed by dynamic audio policy if core routing is not used, see 54 * {@link CarAudioZoneConfig#validateCanUseDynamicMixRouting(boolean)} for further information. 55 * <li>Device addresses are not shared across zones 56 * <li>Device addresses are not shared across volume groups in same config 57 * <li>Device addresses can be shared across configs in the same zone 58 * </ul> 59 * 60 * @param carAudioZones Audio zones to validate 61 * @param useCoreAudioRouting If the service is using core audio routing 62 * @throws RuntimeException when ever there is a failure when validating the audio zones 63 */ validate(SparseArray<CarAudioZone> carAudioZones, boolean useCoreAudioRouting)64 static void validate(SparseArray<CarAudioZone> carAudioZones, boolean useCoreAudioRouting) 65 throws RuntimeException { 66 validateAtLeastOneZoneDefined(carAudioZones); 67 validateZoneConfigsForEachZone(carAudioZones, useCoreAudioRouting); 68 if (!useCoreAudioRouting) { 69 validateEachAddressAppearsAtMostOnceInOneConfig(carAudioZones); 70 } 71 validatePrimaryZoneHasInputDevice(carAudioZones); 72 } 73 validatePrimaryZoneHasInputDevice(SparseArray<CarAudioZone> carAudioZones)74 private static void validatePrimaryZoneHasInputDevice(SparseArray<CarAudioZone> carAudioZones) { 75 CarAudioZone primaryZone = carAudioZones.get(PRIMARY_AUDIO_ZONE); 76 List<AudioDeviceAttributes> devices = primaryZone.getInputAudioDevices(); 77 Preconditions.checkCollectionNotEmpty(devices, "Primary Zone Input Devices"); 78 for (int index = 0; index < devices.size(); index++) { 79 AudioDeviceAttributes device = devices.get(index); 80 if (device.getType() == TYPE_BUILTIN_MIC) { 81 return; 82 } 83 } 84 throw new RuntimeException("Primary Zone must have at least one microphone input device"); 85 } 86 validateAtLeastOneZoneDefined(SparseArray<CarAudioZone> carAudioZones)87 private static void validateAtLeastOneZoneDefined(SparseArray<CarAudioZone> carAudioZones) { 88 if (carAudioZones.size() == 0) { 89 throw new RuntimeException("At least one zone should be defined"); 90 } 91 } 92 validateZoneConfigsForEachZone(SparseArray<CarAudioZone> carAudioZones, boolean useCoreAudioRouting)93 private static void validateZoneConfigsForEachZone(SparseArray<CarAudioZone> carAudioZones, 94 boolean useCoreAudioRouting) { 95 for (int i = 0; i < carAudioZones.size(); i++) { 96 CarAudioZone zone = carAudioZones.valueAt(i); 97 if (!zone.validateZoneConfigs(useCoreAudioRouting)) { 98 throw new RuntimeException( 99 "Invalid zone configurations for zone " + zone.getId()); 100 } 101 // TODO(b/301391301) use fore routing for all zones. 102 // Currently force "useCoreAudioRouting" to be false for non primary zones as only 103 // primary zone supports core routing 104 if (!zone.validateCanUseDynamicMixRouting( 105 zone.isPrimaryZone() && useCoreAudioRouting)) { 106 throw new RuntimeException( 107 "Invalid Configuration to use Dynamic Mix for zone " + zone.getId()); 108 } 109 } 110 } 111 validateEachAddressAppearsAtMostOnceInOneConfig( SparseArray<CarAudioZone> carAudioZones)112 private static void validateEachAddressAppearsAtMostOnceInOneConfig( 113 SparseArray<CarAudioZone> carAudioZones) { 114 Set<String> addresses = new ArraySet<>(); 115 for (int i = 0; i < carAudioZones.size(); i++) { 116 List<CarAudioZoneConfig> zoneConfigs = 117 carAudioZones.valueAt(i).getAllCarAudioZoneConfigs(); 118 ArraySet<String> addressesPerZone = new ArraySet<>(); 119 for (int configIndex = 0; configIndex < zoneConfigs.size(); configIndex++) { 120 Set<String> addressesPerConfig = new ArraySet<>(); 121 CarAudioZoneConfig config = zoneConfigs.get(configIndex); 122 CarVolumeGroup[] groups = config.getVolumeGroups(); 123 for (CarVolumeGroup carVolumeGroup : groups) { 124 validateVolumeGroupAddresses(addressesPerConfig, carVolumeGroup.getAddresses()); 125 } 126 // No need to check for addresses shared among configurations 127 // as that is allowed 128 addressesPerZone.addAll(addressesPerConfig); 129 } 130 131 for (int c = 0; c < addressesPerZone.size(); c++) { 132 String address = addressesPerZone.valueAt(c); 133 if (addresses.add(address)) { 134 continue; 135 } 136 throw new IllegalStateException("Address " + address + " repeats among multiple" 137 + " zones in car_audio_configuration.xml"); 138 } 139 } 140 } 141 validateVolumeGroupAddresses(Set<String> addressesPerConfig, List<String> groupAddresses)142 private static void validateVolumeGroupAddresses(Set<String> addressesPerConfig, 143 List<String> groupAddresses) { 144 for (int c = 0; c < groupAddresses.size(); c++) { 145 String address = groupAddresses.get(c); 146 // Ignore dynamic devices as they may not have addresses until the device is connected 147 if (address == null || address.isEmpty()) { 148 continue; 149 } 150 if (!addressesPerConfig.add(address)) { 151 throw new RuntimeException("Device with address " + address 152 + " appears in multiple volume groups in the same configuration"); 153 } 154 } 155 } 156 } 157