| /* |
| * Copyright (C) 2021 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 android.car.cts.utils; |
| |
| import static com.google.common.truth.Truth.assertWithMessage; |
| |
| import static org.junit.Assume.assumeNotNull; |
| |
| import android.car.VehicleAreaMirror; |
| import android.car.VehicleAreaSeat; |
| import android.car.VehicleAreaType; |
| import android.car.VehicleAreaWheel; |
| import android.car.VehicleAreaWindow; |
| import android.car.VehiclePropertyIds; |
| import android.car.VehiclePropertyType; |
| import android.car.hardware.CarPropertyConfig; |
| import android.car.hardware.CarPropertyValue; |
| import android.car.hardware.property.CarPropertyManager; |
| import android.os.SystemClock; |
| |
| import com.google.common.collect.ImmutableSet; |
| import com.google.common.collect.Sets; |
| |
| import java.util.ArrayList; |
| import java.util.Arrays; |
| import java.util.Collection; |
| import java.util.Collections; |
| import java.util.List; |
| import java.util.Optional; |
| import java.util.concurrent.CountDownLatch; |
| import java.util.concurrent.TimeUnit; |
| import java.util.stream.Collectors; |
| import java.util.stream.IntStream; |
| |
| public class VehiclePropertyVerifier<T> { |
| private final static String CAR_PROPERTY_VALUE_SOURCE_GETTER = "Getter"; |
| private final static String CAR_PROPERTY_VALUE_SOURCE_CALLBACK = "Callback"; |
| private static final float FLOAT_INEQUALITY_THRESHOLD = 0.00001f; |
| private static final ImmutableSet<Integer> WHEEL_AREAS = ImmutableSet.of( |
| VehicleAreaWheel.WHEEL_LEFT_FRONT, VehicleAreaWheel.WHEEL_LEFT_REAR, |
| VehicleAreaWheel.WHEEL_RIGHT_FRONT, VehicleAreaWheel.WHEEL_RIGHT_REAR); |
| private static final ImmutableSet<Integer> ALL_POSSIBLE_WHEEL_AREA_IDS = |
| generateAllPossibleAreaIds(WHEEL_AREAS); |
| private static final ImmutableSet<Integer> WINDOW_AREAS = ImmutableSet.of( |
| VehicleAreaWindow.WINDOW_FRONT_WINDSHIELD, VehicleAreaWindow.WINDOW_REAR_WINDSHIELD, |
| VehicleAreaWindow.WINDOW_ROW_1_LEFT, VehicleAreaWindow.WINDOW_ROW_1_RIGHT, |
| VehicleAreaWindow.WINDOW_ROW_2_LEFT, VehicleAreaWindow.WINDOW_ROW_2_RIGHT, |
| VehicleAreaWindow.WINDOW_ROW_3_LEFT, VehicleAreaWindow.WINDOW_ROW_3_RIGHT, |
| VehicleAreaWindow.WINDOW_ROOF_TOP_1, VehicleAreaWindow.WINDOW_ROOF_TOP_2); |
| private static final ImmutableSet<Integer> ALL_POSSIBLE_WINDOW_AREA_IDS = |
| generateAllPossibleAreaIds(WINDOW_AREAS); |
| private static final ImmutableSet<Integer> MIRROR_AREAS = ImmutableSet.of( |
| VehicleAreaMirror.MIRROR_DRIVER_LEFT, VehicleAreaMirror.MIRROR_DRIVER_RIGHT, |
| VehicleAreaMirror.MIRROR_DRIVER_CENTER); |
| private static final ImmutableSet<Integer> ALL_POSSIBLE_MIRROR_AREA_IDS = |
| generateAllPossibleAreaIds(MIRROR_AREAS); |
| private static final ImmutableSet<Integer> SEAT_AREAS = ImmutableSet.of( |
| VehicleAreaSeat.SEAT_ROW_1_LEFT, VehicleAreaSeat.SEAT_ROW_1_CENTER, |
| VehicleAreaSeat.SEAT_ROW_1_RIGHT, VehicleAreaSeat.SEAT_ROW_2_LEFT, |
| VehicleAreaSeat.SEAT_ROW_2_CENTER, VehicleAreaSeat.SEAT_ROW_2_RIGHT, |
| VehicleAreaSeat.SEAT_ROW_3_LEFT, VehicleAreaSeat.SEAT_ROW_3_CENTER, |
| VehicleAreaSeat.SEAT_ROW_3_RIGHT); |
| private static final ImmutableSet<Integer> ALL_POSSIBLE_SEAT_AREA_IDS = |
| generateAllPossibleAreaIds(SEAT_AREAS); |
| |
| |
| private final int mPropertyId; |
| private final String mPropertyName; |
| private final int mAccess; |
| private final int mAreaType; |
| private final int mChangeMode; |
| private final Class<T> mPropertyType; |
| private final boolean mRequiredProperty; |
| private final Optional<ConfigArrayVerifier> mConfigArrayVerifier; |
| private final Optional<CarPropertyValueVerifier> mCarPropertyValueVerifier; |
| private final Optional<AreaIdsVerifier> mAreaIdsVerifier; |
| private final ImmutableSet<Integer> mPossibleConfigArrayValues; |
| private final ImmutableSet<T> mPossibleCarPropertyValues; |
| private final boolean mRequirePropertyValueToBeInConfigArray; |
| private final boolean mVerifySetterWithConfigArrayValues; |
| private final boolean mRequireMinMaxValues; |
| private final boolean mRequireMinValuesToBeZero; |
| private final boolean mRequireZeroToBeContainedInMinMaxRanges; |
| private final boolean mPossiblyDependentOnHvacPowerOn; |
| |
| private VehiclePropertyVerifier(int propertyId, int access, int areaType, int changeMode, |
| Class<T> propertyType, boolean requiredProperty, |
| Optional<ConfigArrayVerifier> configArrayVerifier, |
| Optional<CarPropertyValueVerifier> carPropertyValueVerifier, |
| Optional<AreaIdsVerifier> areaIdsVerifier, |
| ImmutableSet<Integer> possibleConfigArrayValues, |
| ImmutableSet<T> possibleCarPropertyValues, |
| boolean requirePropertyValueToBeInConfigArray, |
| boolean verifySetterWithConfigArrayValues, |
| boolean requireMinMaxValues, |
| boolean requireMinValuesToBeZero, |
| boolean requireZeroToBeContainedInMinMaxRanges, |
| boolean possiblyDependentOnHvacPowerOn) { |
| mPropertyId = propertyId; |
| mPropertyName = VehiclePropertyIds.toString(propertyId); |
| mAccess = access; |
| mAreaType = areaType; |
| mChangeMode = changeMode; |
| mPropertyType = propertyType; |
| mRequiredProperty = requiredProperty; |
| mConfigArrayVerifier = configArrayVerifier; |
| mCarPropertyValueVerifier = carPropertyValueVerifier; |
| mAreaIdsVerifier = areaIdsVerifier; |
| mPossibleConfigArrayValues = possibleConfigArrayValues; |
| mPossibleCarPropertyValues = possibleCarPropertyValues; |
| mRequirePropertyValueToBeInConfigArray = requirePropertyValueToBeInConfigArray; |
| mVerifySetterWithConfigArrayValues = verifySetterWithConfigArrayValues; |
| mRequireMinMaxValues = requireMinMaxValues; |
| mRequireMinValuesToBeZero = requireMinValuesToBeZero; |
| mRequireZeroToBeContainedInMinMaxRanges = requireZeroToBeContainedInMinMaxRanges; |
| mPossiblyDependentOnHvacPowerOn = possiblyDependentOnHvacPowerOn; |
| } |
| |
| public static <T> Builder<T> newBuilder(int propertyId, int access, int areaType, |
| int changeMode, |
| Class<T> propertyType) { |
| return new Builder<>(propertyId, access, areaType, changeMode, propertyType); |
| } |
| |
| private static String accessToString(int access) { |
| switch (access) { |
| case CarPropertyConfig.VEHICLE_PROPERTY_ACCESS_NONE: |
| return "VEHICLE_PROPERTY_ACCESS_NONE"; |
| case CarPropertyConfig.VEHICLE_PROPERTY_ACCESS_READ: |
| return "VEHICLE_PROPERTY_ACCESS_READ"; |
| case CarPropertyConfig.VEHICLE_PROPERTY_ACCESS_WRITE: |
| return "VEHICLE_PROPERTY_ACCESS_WRITE"; |
| case CarPropertyConfig.VEHICLE_PROPERTY_ACCESS_READ_WRITE: |
| return "VEHICLE_PROPERTY_ACCESS_READ_WRITE"; |
| default: |
| return Integer.toString(access); |
| } |
| } |
| |
| private static String areaTypeToString(int areaType) { |
| switch (areaType) { |
| case VehicleAreaType.VEHICLE_AREA_TYPE_GLOBAL: |
| return "VEHICLE_AREA_TYPE_GLOBAL"; |
| case VehicleAreaType.VEHICLE_AREA_TYPE_WINDOW: |
| return "VEHICLE_AREA_TYPE_WINDOW"; |
| case VehicleAreaType.VEHICLE_AREA_TYPE_DOOR: |
| return "VEHICLE_AREA_TYPE_DOOR"; |
| case VehicleAreaType.VEHICLE_AREA_TYPE_MIRROR: |
| return "VEHICLE_AREA_TYPE_MIRROR"; |
| case VehicleAreaType.VEHICLE_AREA_TYPE_SEAT: |
| return "VEHICLE_AREA_TYPE_SEAT"; |
| case VehicleAreaType.VEHICLE_AREA_TYPE_WHEEL: |
| return "VEHICLE_AREA_TYPE_WHEEL"; |
| default: |
| return Integer.toString(areaType); |
| } |
| } |
| |
| private static String changeModeToString(int changeMode) { |
| switch (changeMode) { |
| case CarPropertyConfig.VEHICLE_PROPERTY_CHANGE_MODE_STATIC: |
| return "VEHICLE_PROPERTY_CHANGE_MODE_STATIC"; |
| case CarPropertyConfig.VEHICLE_PROPERTY_CHANGE_MODE_ONCHANGE: |
| return "VEHICLE_PROPERTY_CHANGE_MODE_ONCHANGE"; |
| case CarPropertyConfig.VEHICLE_PROPERTY_CHANGE_MODE_CONTINUOUS: |
| return "VEHICLE_PROPERTY_CHANGE_MODE_CONTINUOUS"; |
| default: |
| return Integer.toString(changeMode); |
| } |
| } |
| |
| public void verify(CarPropertyManager carPropertyManager) { |
| CarPropertyConfig<?> carPropertyConfig = carPropertyManager.getCarPropertyConfig( |
| mPropertyId); |
| if (mRequiredProperty) { |
| assertWithMessage("Must support " + mPropertyName).that(carPropertyConfig) |
| .isNotNull(); |
| } else { |
| assumeNotNull(carPropertyConfig); |
| } |
| |
| verifyCarPropertyConfig(carPropertyConfig); |
| |
| if (mPossiblyDependentOnHvacPowerOn) { |
| CarPropertyConfig<?> hvacPowerOnCarPropertyConfig = |
| carPropertyManager.getCarPropertyConfig(VehiclePropertyIds.HVAC_POWER_ON); |
| if (hvacPowerOnCarPropertyConfig != null |
| && hvacPowerOnCarPropertyConfig.getConfigArray().contains(mPropertyId)) { |
| turnOnHvacPower(carPropertyManager, hvacPowerOnCarPropertyConfig); |
| } |
| } |
| |
| verifyCarPropertyValueGetter(carPropertyConfig, carPropertyManager); |
| verifyCarPropertyValueCallback(carPropertyConfig, carPropertyManager); |
| verifyCarPropertyValueSetter(carPropertyConfig, carPropertyManager); |
| } |
| |
| private void turnOnHvacPower(CarPropertyManager carPropertyManager, |
| CarPropertyConfig<?> hvacPowerOnCarPropertyConfig) { |
| for (int areaId : hvacPowerOnCarPropertyConfig.getAreaIds()) { |
| if (carPropertyManager.getProperty(VehiclePropertyIds.HVAC_POWER_ON, |
| areaId).getValue().equals(true)) { |
| continue; |
| } |
| verifySetProperty((CarPropertyConfig<Boolean>) hvacPowerOnCarPropertyConfig, |
| carPropertyManager, areaId, Boolean.TRUE); |
| } |
| } |
| |
| private void verifyCarPropertyValueSetter(CarPropertyConfig<?> carPropertyConfig, |
| CarPropertyManager carPropertyManager) { |
| if (carPropertyConfig.getAccess() == CarPropertyConfig.VEHICLE_PROPERTY_ACCESS_READ) { |
| return; |
| } |
| if (Boolean.class.equals(carPropertyConfig.getPropertyType())) { |
| verifyBooleanPropertySetter(carPropertyConfig, carPropertyManager); |
| } else if (Integer.class.equals(carPropertyConfig.getPropertyType())) { |
| verifyIntegerPropertySetter(carPropertyConfig, carPropertyManager); |
| } else if (Float.class.equals(carPropertyConfig.getPropertyType())) { |
| verifyFloatPropertySetter(carPropertyConfig, carPropertyManager); |
| } |
| } |
| |
| private void verifyBooleanPropertySetter(CarPropertyConfig<?> carPropertyConfig, |
| CarPropertyManager carPropertyManager) { |
| for (int areaId : carPropertyConfig.getAreaIds()) { |
| CarPropertyValue<Boolean> currentCarPropertyValue = carPropertyManager.getProperty( |
| mPropertyId, areaId); |
| verifyCarPropertyValue(carPropertyConfig, currentCarPropertyValue, areaId, |
| CAR_PROPERTY_VALUE_SOURCE_GETTER); |
| Boolean valueToSet = !currentCarPropertyValue.getValue(); |
| verifySetProperty((CarPropertyConfig<Boolean>) carPropertyConfig, carPropertyManager, |
| areaId, valueToSet); |
| verifySetProperty((CarPropertyConfig<Boolean>) carPropertyConfig, carPropertyManager, |
| areaId, !valueToSet); |
| } |
| } |
| |
| private void verifyIntegerPropertySetter(CarPropertyConfig<?> carPropertyConfig, |
| CarPropertyManager carPropertyManager) { |
| if (mVerifySetterWithConfigArrayValues) { |
| verifySetterWithValues((CarPropertyConfig<T>) carPropertyConfig, carPropertyManager, |
| (Collection<T>) carPropertyConfig.getConfigArray()); |
| } else if (!mPossibleCarPropertyValues.isEmpty()) { |
| verifySetterWithValues((CarPropertyConfig<T>) carPropertyConfig, carPropertyManager, |
| mPossibleCarPropertyValues); |
| } else { |
| verifySetterWithMinMaxValues(carPropertyConfig, carPropertyManager); |
| } |
| } |
| |
| private void verifySetterWithValues(CarPropertyConfig<T> carPropertyConfig, |
| CarPropertyManager carPropertyManager, Collection<T> valuesToSet) { |
| for (T valueToSet : valuesToSet) { |
| for (int areaId : carPropertyConfig.getAreaIds()) { |
| verifySetProperty(carPropertyConfig, carPropertyManager, areaId, valueToSet); |
| } |
| } |
| } |
| |
| private void verifySetterWithMinMaxValues(CarPropertyConfig<?> carPropertyConfig, |
| CarPropertyManager carPropertyManager) { |
| for (int areaId : carPropertyConfig.getAreaIds()) { |
| if (carPropertyConfig.getMinValue(areaId) == null || carPropertyConfig.getMinValue( |
| areaId) == null) { |
| continue; |
| } |
| List<Integer> valuesToSet = IntStream.rangeClosed( |
| ((Integer) carPropertyConfig.getMinValue(areaId)).intValue(), |
| ((Integer) carPropertyConfig.getMaxValue(areaId)).intValue()).boxed().collect( |
| Collectors.toList()); |
| |
| for (Integer valueToSet : valuesToSet) { |
| verifySetProperty((CarPropertyConfig<Integer>) carPropertyConfig, |
| carPropertyManager, areaId, valueToSet); |
| } |
| } |
| } |
| |
| private void verifyFloatPropertySetter(CarPropertyConfig<?> carPropertyConfig, |
| CarPropertyManager carPropertyManager) { |
| if (!mPossibleCarPropertyValues.isEmpty()) { |
| verifySetterWithValues((CarPropertyConfig<T>) carPropertyConfig, carPropertyManager, |
| mPossibleCarPropertyValues); |
| } |
| } |
| |
| private <T> void verifySetProperty(CarPropertyConfig<T> carPropertyConfig, |
| CarPropertyManager carPropertyManager, int areaId, T valueToSet) { |
| CarPropertyValue<T> currentCarPropertyValue = carPropertyManager.getProperty( |
| mPropertyId, areaId); |
| verifyCarPropertyValue(carPropertyConfig, currentCarPropertyValue, areaId, |
| CAR_PROPERTY_VALUE_SOURCE_GETTER); |
| if (valueEquals(valueToSet, currentCarPropertyValue.getValue())) { |
| return; |
| } |
| SetterCallback setterCallback = new SetterCallback(mPropertyId, mPropertyName, areaId, |
| valueToSet); |
| assertWithMessage("Failed to register setter callback for " + mPropertyName).that( |
| carPropertyManager.registerCallback(setterCallback, mPropertyId, |
| CarPropertyManager.SENSOR_RATE_FASTEST)).isTrue(); |
| carPropertyManager.setProperty(carPropertyConfig.getPropertyType(), mPropertyId, areaId, |
| valueToSet); |
| CarPropertyValue<?> updatedCarPropertyValue = |
| setterCallback.waitForUpdatedCarPropertyValue(); |
| verifyCarPropertyValue(carPropertyConfig, updatedCarPropertyValue, areaId, |
| CAR_PROPERTY_VALUE_SOURCE_CALLBACK); |
| carPropertyManager.unregisterCallback(setterCallback, mPropertyId); |
| } |
| |
| private void verifyCarPropertyValueCallback(CarPropertyConfig<?> carPropertyConfig, |
| CarPropertyManager carPropertyManager) { |
| if (mChangeMode == CarPropertyConfig.VEHICLE_PROPERTY_CHANGE_MODE_STATIC |
| || mChangeMode == CarPropertyConfig.VEHICLE_PROPERTY_CHANGE_MODE_ONCHANGE) { |
| CarPropertyValueCallback carPropertyValueCallback = |
| new CarPropertyValueCallback(mPropertyName, |
| carPropertyConfig.getAreaIds().length); |
| assertWithMessage("Failed to register callback for " + mPropertyName).that( |
| carPropertyManager.registerCallback(carPropertyValueCallback, mPropertyId, |
| CarPropertyManager.SENSOR_RATE_ONCHANGE)).isTrue(); |
| List<CarPropertyValue<?>> carPropertyValues = |
| carPropertyValueCallback.getCarPropertyValues(); |
| carPropertyManager.unregisterCallback(carPropertyValueCallback, mPropertyId); |
| |
| for (CarPropertyValue<?> carPropertyValue : carPropertyValues) { |
| verifyCarPropertyValue(carPropertyConfig, carPropertyValue, |
| carPropertyValue.getAreaId(), CAR_PROPERTY_VALUE_SOURCE_CALLBACK); |
| } |
| assertWithMessage(mPropertyName |
| + " callback values did not cover all the property's area IDs").that( |
| carPropertyValues.stream().map(CarPropertyValue::getAreaId).collect( |
| Collectors.toList()) |
| ).containsExactlyElementsIn( |
| Arrays.stream(carPropertyConfig.getAreaIds()).boxed().collect( |
| Collectors.toList())); |
| |
| } else if (mChangeMode == CarPropertyConfig.VEHICLE_PROPERTY_CHANGE_MODE_CONTINUOUS) { |
| int updatesPerAreaId = 2; |
| int minimumTotalUpdates = updatesPerAreaId * carPropertyConfig.getAreaIds().length; |
| float secondsToMillis = 1_000; |
| long bufferMillis = 1_000; // 1 second |
| long timeoutMillis = |
| ((long) ((1.0f / carPropertyConfig.getMinSampleRate()) * secondsToMillis |
| * minimumTotalUpdates)) + bufferMillis; |
| CarPropertyValueCallback carPropertyValueCallback = new CarPropertyValueCallback( |
| mPropertyName, minimumTotalUpdates, timeoutMillis); |
| assertWithMessage("Failed to register callback for " + mPropertyName).that( |
| carPropertyManager.registerCallback(carPropertyValueCallback, mPropertyId, |
| carPropertyConfig.getMaxSampleRate())).isTrue(); |
| List<CarPropertyValue<?>> carPropertyValues = |
| carPropertyValueCallback.getCarPropertyValues(); |
| carPropertyManager.unregisterCallback(carPropertyValueCallback, mPropertyId); |
| |
| for (CarPropertyValue<?> carPropertyValue : carPropertyValues) { |
| verifyCarPropertyValue(carPropertyConfig, carPropertyValue, |
| carPropertyValue.getAreaId(), CAR_PROPERTY_VALUE_SOURCE_CALLBACK); |
| } |
| |
| for (int areaId : carPropertyConfig.getAreaIds()) { |
| assertWithMessage( |
| mPropertyName + " callback values did not receive " + updatesPerAreaId |
| + " updates for area ID: " + areaId).that( |
| carPropertyValues.stream().filter( |
| carPropertyValue -> carPropertyValue.getAreaId() |
| == areaId).count()).isAtLeast(updatesPerAreaId); |
| } |
| } |
| } |
| |
| private void verifyCarPropertyConfig(CarPropertyConfig<?> carPropertyConfig) { |
| assertWithMessage(mPropertyName + " CarPropertyConfig must have correct property ID") |
| .that(carPropertyConfig.getPropertyId()) |
| .isEqualTo(mPropertyId); |
| if (mAccess == CarPropertyConfig.VEHICLE_PROPERTY_ACCESS_READ_WRITE) { |
| assertWithMessage(mPropertyName + " must be " + accessToString( |
| CarPropertyConfig.VEHICLE_PROPERTY_ACCESS_READ) + ", " + accessToString( |
| CarPropertyConfig.VEHICLE_PROPERTY_ACCESS_WRITE) + ", or " + accessToString( |
| CarPropertyConfig.VEHICLE_PROPERTY_ACCESS_READ_WRITE)) |
| .that(carPropertyConfig.getAccess()) |
| .isIn(ImmutableSet.of(CarPropertyConfig.VEHICLE_PROPERTY_ACCESS_READ, |
| CarPropertyConfig.VEHICLE_PROPERTY_ACCESS_WRITE, |
| CarPropertyConfig.VEHICLE_PROPERTY_ACCESS_READ_WRITE)); |
| } else { |
| assertWithMessage(mPropertyName + " must be " + accessToString(mAccess)) |
| .that(carPropertyConfig.getAccess()).isEqualTo(mAccess); |
| } |
| assertWithMessage(mPropertyName + " must be " + areaTypeToString(mAreaType)) |
| .that(carPropertyConfig.getAreaType()) |
| .isEqualTo(mAreaType); |
| assertWithMessage(mPropertyName + " must be " + changeModeToString(mChangeMode)) |
| .that(carPropertyConfig.getChangeMode()) |
| .isEqualTo(mChangeMode); |
| assertWithMessage(mPropertyName + " must be " + mPropertyType + " type property") |
| .that(carPropertyConfig.getPropertyType()).isEqualTo(mPropertyType); |
| |
| assertWithMessage(mPropertyName + "'s must have at least 1 area ID defined").that( |
| carPropertyConfig.getAreaIds().length).isAtLeast(1); |
| assertWithMessage(mPropertyName + "'s area IDs must all be unique: " + Arrays.toString( |
| carPropertyConfig.getAreaIds())).that(ImmutableSet.copyOf(Arrays.stream( |
| carPropertyConfig.getAreaIds()).boxed().collect(Collectors.toList())).size() |
| == carPropertyConfig.getAreaIds().length).isTrue(); |
| if (mAreaIdsVerifier.isPresent()) { |
| mAreaIdsVerifier.get().verify(carPropertyConfig.getAreaIds()); |
| } else if (mAreaType == VehicleAreaType.VEHICLE_AREA_TYPE_GLOBAL) { |
| assertWithMessage( |
| mPropertyName + "'s AreaIds must contain a single 0 since it is " |
| + areaTypeToString(mAreaType)) |
| .that(carPropertyConfig.getAreaIds()).isEqualTo(new int[]{0}); |
| } else if (mAreaType == VehicleAreaType.VEHICLE_AREA_TYPE_WHEEL) { |
| verifyValidAreaIdsForAreaType(carPropertyConfig, ALL_POSSIBLE_WHEEL_AREA_IDS); |
| verifyNoAreaOverlapInAreaIds(carPropertyConfig, WHEEL_AREAS); |
| } else if (mAreaType == VehicleAreaType.VEHICLE_AREA_TYPE_WINDOW) { |
| verifyValidAreaIdsForAreaType(carPropertyConfig, ALL_POSSIBLE_WINDOW_AREA_IDS); |
| verifyNoAreaOverlapInAreaIds(carPropertyConfig, WINDOW_AREAS); |
| } else if (mAreaType == VehicleAreaType.VEHICLE_AREA_TYPE_MIRROR) { |
| verifyValidAreaIdsForAreaType(carPropertyConfig, ALL_POSSIBLE_MIRROR_AREA_IDS); |
| verifyNoAreaOverlapInAreaIds(carPropertyConfig, MIRROR_AREAS); |
| } else if (mAreaType == VehicleAreaType.VEHICLE_AREA_TYPE_SEAT) { |
| verifyValidAreaIdsForAreaType(carPropertyConfig, ALL_POSSIBLE_SEAT_AREA_IDS); |
| verifyNoAreaOverlapInAreaIds(carPropertyConfig, SEAT_AREAS); |
| } |
| |
| if (mChangeMode == CarPropertyConfig.VEHICLE_PROPERTY_CHANGE_MODE_CONTINUOUS) { |
| verifyContinuousCarPropertyConfig(carPropertyConfig); |
| } else { |
| verifyNonContinuousCarPropertyConfig(carPropertyConfig); |
| } |
| |
| if (!mPossibleConfigArrayValues.isEmpty()) { |
| assertWithMessage( |
| mPropertyName + " configArray must specify supported values") |
| .that(carPropertyConfig.getConfigArray().size()) |
| .isGreaterThan(0); |
| for (Integer supportedValue : carPropertyConfig.getConfigArray()) { |
| assertWithMessage( |
| mPropertyName + " configArray value must be a defined " |
| + "value: " |
| + supportedValue).that( |
| supportedValue).isIn(mPossibleConfigArrayValues); |
| } |
| } |
| |
| mConfigArrayVerifier.ifPresent(configArrayVerifier -> configArrayVerifier.verify( |
| carPropertyConfig.getConfigArray())); |
| |
| if (mPossibleConfigArrayValues.isEmpty() && !mConfigArrayVerifier.isPresent()) { |
| assertWithMessage(mPropertyName + " configArray is undefined, so it must be empty") |
| .that(carPropertyConfig.getConfigArray().size()).isEqualTo(0); |
| } |
| |
| for (int areaId : carPropertyConfig.getAreaIds()) { |
| T areaIdMinValue = (T) carPropertyConfig.getMinValue(areaId); |
| T areaIdMaxValue = (T) carPropertyConfig.getMaxValue(areaId); |
| if (mRequireMinMaxValues) { |
| assertWithMessage(mPropertyName + " - area ID: " + areaId |
| + " must have min value defined").that(areaIdMinValue).isNotNull(); |
| assertWithMessage(mPropertyName + " - area ID: " + areaId |
| + " must have max value defined").that(areaIdMaxValue).isNotNull(); |
| } |
| if (mRequireMinValuesToBeZero) { |
| assertWithMessage( |
| mPropertyName + " - area ID: " + areaId + " min value must be zero").that( |
| areaIdMinValue).isEqualTo(0); |
| } |
| if (mRequireZeroToBeContainedInMinMaxRanges) { |
| assertWithMessage(mPropertyName + " - areaId: " + areaId |
| + "'s max and min range must contain zero").that( |
| verifyMaxAndMinRangeContainsZero(areaIdMinValue, areaIdMaxValue)).isTrue(); |
| |
| } |
| if (areaIdMinValue == null || areaIdMaxValue == null) { |
| continue; |
| } |
| assertWithMessage( |
| mPropertyName + " - areaId: " + areaId + "'s max value must be >= min value") |
| .that(verifyMaxAndMin(areaIdMinValue, areaIdMaxValue)).isTrue(); |
| } |
| } |
| |
| private boolean verifyMaxAndMinRangeContainsZero(T min, T max) { |
| int propertyType = mPropertyId & VehiclePropertyType.MASK; |
| switch (propertyType) { |
| case VehiclePropertyType.INT32: |
| return (Integer) max >= 0 && (Integer) min <= 0; |
| case VehiclePropertyType.INT64: |
| return (Long) max >= 0 && (Long) min <= 0; |
| case VehiclePropertyType.FLOAT: |
| return (Float) max >= 0 && (Float) min <= 0; |
| default: |
| return false; |
| } |
| } |
| |
| private boolean verifyMaxAndMin(T min, T max) { |
| int propertyType = mPropertyId & VehiclePropertyType.MASK; |
| switch (propertyType) { |
| case VehiclePropertyType.INT32: |
| return (Integer) max >= (Integer) min; |
| case VehiclePropertyType.INT64: |
| return (Long) max >= (Long) min; |
| case VehiclePropertyType.FLOAT: |
| return (Float) max >= (Float) min; |
| default: |
| return false; |
| } |
| } |
| |
| private void verifyContinuousCarPropertyConfig(CarPropertyConfig<?> carPropertyConfig) { |
| assertWithMessage( |
| mPropertyName + " must define max sample rate since change mode is " |
| + changeModeToString(mChangeMode)) |
| .that(carPropertyConfig.getMaxSampleRate()).isGreaterThan(0); |
| assertWithMessage( |
| mPropertyName + " must define min sample rate since change mode is " |
| + changeModeToString(mChangeMode)) |
| .that(carPropertyConfig.getMinSampleRate()).isGreaterThan(0); |
| assertWithMessage(mPropertyName + " max sample rate must be >= min sample rate") |
| .that(carPropertyConfig.getMaxSampleRate() >= |
| carPropertyConfig.getMinSampleRate()) |
| .isTrue(); |
| } |
| |
| private void verifyNonContinuousCarPropertyConfig(CarPropertyConfig<?> carPropertyConfig) { |
| assertWithMessage(mPropertyName + " must define max sample rate as 0 since change mode is " |
| + changeModeToString(mChangeMode)) |
| .that(carPropertyConfig.getMaxSampleRate()).isEqualTo(0); |
| assertWithMessage(mPropertyName + " must define min sample rate as 0 since change mode is " |
| + changeModeToString(mChangeMode)) |
| .that(carPropertyConfig.getMinSampleRate()).isEqualTo(0); |
| } |
| |
| private void verifyCarPropertyValueGetter(CarPropertyConfig<?> carPropertyConfig, |
| CarPropertyManager carPropertyManager) { |
| for (int areaId : carPropertyConfig.getAreaIds()) { |
| CarPropertyValue<?> carPropertyValue = |
| carPropertyManager.getProperty( |
| mPropertyId, areaId); |
| |
| verifyCarPropertyValue(carPropertyConfig, carPropertyValue, areaId, |
| CAR_PROPERTY_VALUE_SOURCE_GETTER); |
| } |
| } |
| |
| private void verifyCarPropertyValue(CarPropertyConfig<?> carPropertyConfig, |
| CarPropertyValue<?> carPropertyValue, int areaId, String source) { |
| assertWithMessage( |
| mPropertyName + " - areaId: " + areaId + " - source: " + source |
| + " value must have correct property ID") |
| .that(carPropertyValue.getPropertyId()).isEqualTo(mPropertyId); |
| assertWithMessage( |
| mPropertyName + " - areaId: " + areaId + " - source: " + source |
| + " value must have correct area id: " |
| + areaId) |
| .that(carPropertyValue.getAreaId()) |
| .isEqualTo(areaId); |
| assertWithMessage(mPropertyName + " - areaId: " + areaId + " - source: " + source |
| + " area ID must be in carPropertyConfig#getAreaIds()").that(Arrays.stream( |
| carPropertyConfig.getAreaIds()).boxed().collect(Collectors.toList()).contains( |
| carPropertyValue.getAreaId())).isTrue(); |
| assertWithMessage( |
| mPropertyName + " - areaId: " + areaId + " - source: " + source |
| + " value's status must be valid") |
| .that(carPropertyValue.getStatus()).isIn( |
| ImmutableSet.of(CarPropertyValue.STATUS_AVAILABLE, |
| CarPropertyValue.STATUS_UNAVAILABLE, CarPropertyValue.STATUS_ERROR)); |
| assertWithMessage(mPropertyName + " - areaId: " + areaId + |
| " - source: " + source |
| + " timestamp must use the SystemClock.elapsedRealtimeNanos() time base") |
| .that(carPropertyValue.getTimestamp()).isAtLeast(0); |
| assertWithMessage(mPropertyName + " - areaId: " + areaId + |
| " - source: " + source |
| + " timestamp must use the SystemClock.elapsedRealtimeNanos() time base") |
| .that(carPropertyValue.getTimestamp()).isLessThan( |
| SystemClock.elapsedRealtimeNanos()); |
| assertWithMessage( |
| mPropertyName + " - areaId: " + areaId + " - source: " + source + " must return " |
| + mPropertyType |
| + " type value") |
| .that(carPropertyValue.getValue().getClass()).isEqualTo(mPropertyType); |
| |
| if (mRequirePropertyValueToBeInConfigArray) { |
| assertWithMessage(mPropertyName + " - areaId: " + areaId + " - source: " + source + |
| " value must be listed in configArray") |
| .that(carPropertyConfig.getConfigArray().contains( |
| carPropertyValue.getValue())).isTrue(); |
| } |
| |
| if (!mPossibleCarPropertyValues.isEmpty()) { |
| if (Float.class.equals(mPropertyType)) { |
| boolean foundInPossibleValues = false; |
| for (Float possibleValue : (Collection<Float>) mPossibleCarPropertyValues) { |
| if (floatEquals(possibleValue, (Float) carPropertyValue.getValue())) { |
| foundInPossibleValues = true; |
| break; |
| } |
| } |
| assertWithMessage( |
| mPropertyName + " - areaId: " + areaId + " - source: " + source + " value: " |
| + carPropertyValue.getValue() + " must be listed in the Float set: " |
| + mPossibleCarPropertyValues).that(foundInPossibleValues).isTrue(); |
| } else { |
| assertWithMessage(mPropertyName + " - areaId: " + areaId + " - source: " + source |
| + " value must be listed in the set").that( |
| carPropertyValue.getValue()).isIn(mPossibleCarPropertyValues); |
| } |
| } |
| |
| mCarPropertyValueVerifier.ifPresent( |
| propertyValueVerifier -> propertyValueVerifier.verify(carPropertyConfig, |
| carPropertyValue)); |
| |
| T areaIdMinValue = (T) carPropertyConfig.getMinValue(areaId); |
| T areaIdMaxValue = (T) carPropertyConfig.getMaxValue(areaId); |
| if (areaIdMinValue != null && areaIdMaxValue != null) { |
| assertWithMessage( |
| "carPropertyValue must be between the max and min values") |
| .that(verifyValueInRange(areaIdMinValue, areaIdMaxValue, |
| (T) carPropertyValue.getValue())).isTrue(); |
| } |
| } |
| |
| private boolean verifyValueInRange(T min, T max, T value) { |
| int propertyType = mPropertyId & VehiclePropertyType.MASK; |
| switch (propertyType) { |
| case VehiclePropertyType.INT32: |
| return ((Integer) value >= (Integer) min && (Integer) value <= (Integer) max); |
| case VehiclePropertyType.INT64: |
| return ((Long) value >= (Long) min && (Long) value <= (Long) max); |
| case VehiclePropertyType.FLOAT: |
| return ((Float) value >= (Float) min && (Float) value <= (Float) max); |
| default: |
| return false; |
| } |
| } |
| |
| private static ImmutableSet<Integer> generateAllPossibleAreaIds(ImmutableSet<Integer> areas) { |
| ImmutableSet.Builder<Integer> allPossibleAreaIdsBuilder = ImmutableSet.builder(); |
| for (int i = 1; i <= areas.size(); i++) { |
| allPossibleAreaIdsBuilder.addAll(Sets.combinations(areas, i).stream().map(areaCombo -> { |
| Integer possibleAreaId = 0; |
| for (Integer area : areaCombo) { |
| possibleAreaId |= area; |
| } |
| return possibleAreaId; |
| }).collect(Collectors.toList())); |
| } |
| return allPossibleAreaIdsBuilder.build(); |
| } |
| |
| private void verifyValidAreaIdsForAreaType(CarPropertyConfig<?> carPropertyConfig, |
| ImmutableSet<Integer> allPossibleAreaIds) { |
| for (int areaId : carPropertyConfig.getAreaIds()) { |
| assertWithMessage( |
| mPropertyName + "'s area ID must be a valid " + areaTypeToString(mAreaType) |
| + " area ID").that(areaId).isIn(allPossibleAreaIds); |
| } |
| } |
| |
| private void verifyNoAreaOverlapInAreaIds(CarPropertyConfig<?> carPropertyConfig, |
| ImmutableSet<Integer> areas) { |
| if (carPropertyConfig.getAreaIds().length < 2) { |
| return; |
| } |
| ImmutableSet<Integer> areaIds = ImmutableSet.copyOf(Arrays.stream( |
| carPropertyConfig.getAreaIds()).boxed().collect(Collectors.toList())); |
| List<Integer> areaIdOverlapCheckResults = Sets.combinations(areaIds, 2).stream().map( |
| areaIdPair -> { |
| List<Integer> areaIdPairAsList = areaIdPair.stream().collect( |
| Collectors.toList()); |
| return areaIdPairAsList.get(0) & areaIdPairAsList.get(1); |
| }).collect(Collectors.toList()); |
| |
| assertWithMessage( |
| mPropertyName + " area IDs: " + Arrays.toString(carPropertyConfig.getAreaIds()) |
| + " must contain each area only once (e.g. no bitwise AND overlap) for " |
| + "the area type: " + areaTypeToString(mAreaType)).that( |
| Collections.frequency(areaIdOverlapCheckResults, 0) |
| == areaIdOverlapCheckResults.size()).isTrue(); |
| } |
| |
| public interface ConfigArrayVerifier { |
| void verify(List<Integer> configArray); |
| } |
| |
| public interface CarPropertyValueVerifier { |
| void verify(CarPropertyConfig<?> carPropertyConfig, CarPropertyValue<?> carPropertyValue); |
| } |
| |
| public interface AreaIdsVerifier { |
| void verify(int[] areaIds); |
| } |
| |
| public static class Builder<T> { |
| private final int mPropertyId; |
| private final int mAccess; |
| private final int mAreaType; |
| private final int mChangeMode; |
| private final Class<T> mPropertyType; |
| private boolean mRequiredProperty = false; |
| private Optional<ConfigArrayVerifier> mConfigArrayVerifier = Optional.empty(); |
| private Optional<CarPropertyValueVerifier> mCarPropertyValueVerifier = Optional.empty(); |
| private Optional<AreaIdsVerifier> mAreaIdsVerifier = Optional.empty(); |
| private ImmutableSet<Integer> mPossibleConfigArrayValues = ImmutableSet.of(); |
| private ImmutableSet<T> mPossibleCarPropertyValues = ImmutableSet.of(); |
| private boolean mRequirePropertyValueToBeInConfigArray = false; |
| private boolean mVerifySetterWithConfigArrayValues = false; |
| private boolean mRequireMinMaxValues = false; |
| private boolean mRequireMinValuesToBeZero = false; |
| private boolean mRequireZeroToBeContainedInMinMaxRanges = false; |
| private boolean mPossiblyDependentOnHvacPowerOn = false; |
| |
| private Builder(int propertyId, int access, int areaType, int changeMode, |
| Class<T> propertyType) { |
| mPropertyId = propertyId; |
| mAccess = access; |
| mAreaType = areaType; |
| mChangeMode = changeMode; |
| mPropertyType = propertyType; |
| } |
| |
| public Builder<T> requireProperty() { |
| mRequiredProperty = true; |
| return this; |
| } |
| |
| public Builder<T> setConfigArrayVerifier(ConfigArrayVerifier configArrayVerifier) { |
| mConfigArrayVerifier = Optional.of(configArrayVerifier); |
| return this; |
| } |
| |
| public Builder<T> setCarPropertyValueVerifier( |
| CarPropertyValueVerifier carPropertyValueVerifier) { |
| mCarPropertyValueVerifier = Optional.of(carPropertyValueVerifier); |
| return this; |
| } |
| |
| public Builder<T> setAreaIdsVerifier(AreaIdsVerifier areaIdsVerifier) { |
| mAreaIdsVerifier = Optional.of(areaIdsVerifier); |
| return this; |
| } |
| |
| public Builder<T> setPossibleConfigArrayValues( |
| ImmutableSet<Integer> possibleConfigArrayValues) { |
| mPossibleConfigArrayValues = possibleConfigArrayValues; |
| return this; |
| } |
| |
| public Builder<T> setPossibleCarPropertyValues( |
| ImmutableSet<T> possibleCarPropertyValues) { |
| mPossibleCarPropertyValues = possibleCarPropertyValues; |
| return this; |
| } |
| |
| public Builder<T> requirePropertyValueTobeInConfigArray() { |
| mRequirePropertyValueToBeInConfigArray = true; |
| return this; |
| } |
| |
| public Builder<T> verifySetterWithConfigArrayValues() { |
| mVerifySetterWithConfigArrayValues = true; |
| return this; |
| } |
| |
| public Builder<T> requireMinMaxValues() { |
| mRequireMinMaxValues = true; |
| return this; |
| } |
| |
| public Builder<T> requireMinValuesToBeZero() { |
| mRequireMinValuesToBeZero = true; |
| return this; |
| } |
| |
| public Builder<T> requireZeroToBeContainedInMinMaxRanges() { |
| mRequireZeroToBeContainedInMinMaxRanges = true; |
| return this; |
| } |
| |
| public Builder<T> setPossiblyDependentOnHvacPowerOn() { |
| mPossiblyDependentOnHvacPowerOn = true; |
| return this; |
| } |
| |
| public VehiclePropertyVerifier<T> build() { |
| return new VehiclePropertyVerifier<>(mPropertyId, mAccess, mAreaType, mChangeMode, |
| mPropertyType, mRequiredProperty, mConfigArrayVerifier, |
| mCarPropertyValueVerifier, mAreaIdsVerifier, mPossibleConfigArrayValues, |
| mPossibleCarPropertyValues, mRequirePropertyValueToBeInConfigArray, |
| mVerifySetterWithConfigArrayValues, mRequireMinMaxValues, |
| mRequireMinValuesToBeZero, mRequireZeroToBeContainedInMinMaxRanges, |
| mPossiblyDependentOnHvacPowerOn); |
| } |
| } |
| |
| private static class CarPropertyValueCallback implements |
| CarPropertyManager.CarPropertyEventCallback { |
| private final String mPropertyName; |
| private final int mTotalOnChangeEvents; |
| private final CountDownLatch mCountDownLatch; |
| private final List<CarPropertyValue<?>> mCarPropertyValues = new ArrayList<>(); |
| private final long mTimeoutMillis; |
| |
| CarPropertyValueCallback(String propertyName, int totalOnChangeEvents) { |
| this(propertyName, totalOnChangeEvents, 1500); |
| } |
| |
| CarPropertyValueCallback(String propertyName, int totalOnChangeEvents, |
| long timeoutMillis) { |
| mPropertyName = propertyName; |
| mTotalOnChangeEvents = totalOnChangeEvents; |
| mCountDownLatch = new CountDownLatch(totalOnChangeEvents); |
| mTimeoutMillis = timeoutMillis; |
| } |
| |
| public List<CarPropertyValue<?>> getCarPropertyValues() { |
| try { |
| assertWithMessage( |
| "Never received " + mTotalOnChangeEvents + " onChangeEvent(s) for " |
| + mPropertyName + " callback before " + mTimeoutMillis |
| + " ms timeout").that( |
| mCountDownLatch.await(mTimeoutMillis, TimeUnit.MILLISECONDS)).isTrue(); |
| } catch (InterruptedException e) { |
| assertWithMessage("Waiting for onChangeEvent callback(s) for " + mPropertyName |
| + " threw an exception: " + e).fail(); |
| } |
| return mCarPropertyValues; |
| } |
| |
| @Override |
| public void onChangeEvent(CarPropertyValue value) { |
| mCarPropertyValues.add(value); |
| mCountDownLatch.countDown(); |
| } |
| |
| @Override |
| public void onErrorEvent(int propId, int zone) { |
| } |
| |
| @Override |
| public void onErrorEvent(int propId, int areaId, int errorCode) { |
| } |
| } |
| |
| |
| private static class SetterCallback<T> implements CarPropertyManager.CarPropertyEventCallback { |
| private final int mPropertyId; |
| private final String mPropertyName; |
| private final int mAreaId; |
| private final T mExpectedSetValue; |
| private final CountDownLatch mCountDownLatch = new CountDownLatch(1); |
| private final long mCreationTimeNanos = SystemClock.elapsedRealtimeNanos(); |
| private CarPropertyValue<?> mUpdatedCarPropertyValue = null; |
| |
| SetterCallback(int propertyId, String propertyName, int areaId, T expectedSetValue) { |
| mPropertyId = propertyId; |
| mPropertyName = propertyName; |
| mAreaId = areaId; |
| mExpectedSetValue = expectedSetValue; |
| } |
| |
| public CarPropertyValue<?> waitForUpdatedCarPropertyValue() { |
| try { |
| assertWithMessage( |
| "Never received onChangeEvent(s) for " + mPropertyName + " new value: " |
| + mExpectedSetValue + " before 5s timeout").that( |
| mCountDownLatch.await(5, TimeUnit.SECONDS)).isTrue(); |
| } catch (InterruptedException e) { |
| assertWithMessage("Waiting for onChangeEvent set callback for " + mPropertyName |
| + " threw an exception: " + e).fail(); |
| } |
| return mUpdatedCarPropertyValue; |
| } |
| |
| @Override |
| public void onChangeEvent(CarPropertyValue carPropertyValue) { |
| if (mUpdatedCarPropertyValue != null || carPropertyValue.getPropertyId() != mPropertyId |
| || carPropertyValue.getAreaId() != mAreaId |
| || carPropertyValue.getStatus() != CarPropertyValue.STATUS_AVAILABLE |
| || carPropertyValue.getTimestamp() <= mCreationTimeNanos |
| || carPropertyValue.getTimestamp() >= SystemClock.elapsedRealtimeNanos() |
| || !valueEquals(mExpectedSetValue, (T) carPropertyValue.getValue())) { |
| return; |
| } |
| mUpdatedCarPropertyValue = carPropertyValue; |
| mCountDownLatch.countDown(); |
| } |
| |
| @Override |
| public void onErrorEvent(int propId, int zone) { |
| } |
| } |
| |
| private static <V> boolean valueEquals(V v1, V v2) { |
| return (v1 instanceof Float && floatEquals((Float) v1, (Float) v2)) || v1.equals(v2); |
| } |
| |
| private static boolean floatEquals(float f1, float f2) { |
| return Math.abs(f1 - f2) < FLOAT_INEQUALITY_THRESHOLD; |
| } |
| } |