/* * Copyright (C) 2019 The Android Open Source Project * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. * You may obtain a copy of the License at * * http://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, software * distributed under the License is distributed on an "AS IS" BASIS, * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. * See the License for the specific language governing permissions and * limitations under the License. */ package com.android.car.audio; import static android.car.media.CarAudioManager.PRIMARY_AUDIO_ZONE; import static android.media.AudioDeviceInfo.TYPE_BUILTIN_MIC; import static com.android.car.internal.ExcludeFromCodeCoverageGeneratedReport.PRIVATE_CONSTRUCTOR; import android.media.AudioDeviceAttributes; import android.util.ArraySet; import android.util.SparseArray; import com.android.car.internal.ExcludeFromCodeCoverageGeneratedReport; import com.android.internal.util.Preconditions; import java.util.List; import java.util.Set; /* * Class to help validate audio zones are constructed correctly. */ final class CarAudioZonesValidator { @ExcludeFromCodeCoverageGeneratedReport(reason = PRIVATE_CONSTRUCTOR) private CarAudioZonesValidator() { throw new UnsupportedOperationException( "CarAudioZonesValidator class is non-instantiable, contains static members only"); } /** * Returns {@code true} if validation succeeds, throws an a run time exception otherwise. * *

The current rules that apply are: *

* * @param carAudioZones Audio zones to validate * @param useCoreAudioRouting If the service is using core audio routing * @throws RuntimeException when ever there is a failure when validating the audio zones */ static void validate(SparseArray carAudioZones, boolean useCoreAudioRouting) throws RuntimeException { validateAtLeastOneZoneDefined(carAudioZones); validateZoneConfigsForEachZone(carAudioZones, useCoreAudioRouting); if (!useCoreAudioRouting) { validateEachAddressAppearsAtMostOnceInOneConfig(carAudioZones); } validatePrimaryZoneHasInputDevice(carAudioZones); } private static void validatePrimaryZoneHasInputDevice(SparseArray carAudioZones) { CarAudioZone primaryZone = carAudioZones.get(PRIMARY_AUDIO_ZONE); List devices = primaryZone.getInputAudioDevices(); Preconditions.checkCollectionNotEmpty(devices, "Primary Zone Input Devices"); for (int index = 0; index < devices.size(); index++) { AudioDeviceAttributes device = devices.get(index); if (device.getType() == TYPE_BUILTIN_MIC) { return; } } throw new RuntimeException("Primary Zone must have at least one microphone input device"); } private static void validateAtLeastOneZoneDefined(SparseArray carAudioZones) { if (carAudioZones.size() == 0) { throw new RuntimeException("At least one zone should be defined"); } } private static void validateZoneConfigsForEachZone(SparseArray carAudioZones, boolean useCoreAudioRouting) { for (int i = 0; i < carAudioZones.size(); i++) { CarAudioZone zone = carAudioZones.valueAt(i); if (!zone.validateZoneConfigs(useCoreAudioRouting)) { throw new RuntimeException( "Invalid zone configurations for zone " + zone.getId()); } // TODO(b/301391301) use fore routing for all zones. // Currently force "useCoreAudioRouting" to be false for non primary zones as only // primary zone supports core routing if (!zone.validateCanUseDynamicMixRouting( zone.isPrimaryZone() && useCoreAudioRouting)) { throw new RuntimeException( "Invalid Configuration to use Dynamic Mix for zone " + zone.getId()); } } } private static void validateEachAddressAppearsAtMostOnceInOneConfig( SparseArray carAudioZones) { Set addresses = new ArraySet<>(); for (int i = 0; i < carAudioZones.size(); i++) { List zoneConfigs = carAudioZones.valueAt(i).getAllCarAudioZoneConfigs(); ArraySet addressesPerZone = new ArraySet<>(); for (int configIndex = 0; configIndex < zoneConfigs.size(); configIndex++) { Set addressesPerConfig = new ArraySet<>(); CarAudioZoneConfig config = zoneConfigs.get(configIndex); CarVolumeGroup[] groups = config.getVolumeGroups(); for (CarVolumeGroup carVolumeGroup : groups) { validateVolumeGroupAddresses(addressesPerConfig, carVolumeGroup.getAddresses()); } // No need to check for addresses shared among configurations // as that is allowed addressesPerZone.addAll(addressesPerConfig); } for (int c = 0; c < addressesPerZone.size(); c++) { String address = addressesPerZone.valueAt(c); if (addresses.add(address)) { continue; } throw new IllegalStateException("Address " + address + " repeats among multiple" + " zones in car_audio_configuration.xml"); } } } private static void validateVolumeGroupAddresses(Set addressesPerConfig, List groupAddresses) { for (int c = 0; c < groupAddresses.size(); c++) { String address = groupAddresses.get(c); // Ignore dynamic devices as they may not have addresses until the device is connected if (address == null || address.isEmpty()) { continue; } if (!addressesPerConfig.add(address)) { throw new RuntimeException("Device with address " + address + " appears in multiple volume groups in the same configuration"); } } } }