AutomotiveCarClimate.java

/*
 * Copyright 2022 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 androidx.car.app.hardware.climate;

import static android.car.VehiclePropertyIds.HVAC_AC_ON;
import static android.car.VehiclePropertyIds.HVAC_AUTO_ON;
import static android.car.VehiclePropertyIds.HVAC_AUTO_RECIRC_ON;
import static android.car.VehiclePropertyIds.HVAC_DEFROSTER;
import static android.car.VehiclePropertyIds.HVAC_DUAL_ON;
import static android.car.VehiclePropertyIds.HVAC_FAN_DIRECTION;
import static android.car.VehiclePropertyIds.HVAC_FAN_SPEED;
import static android.car.VehiclePropertyIds.HVAC_MAX_AC_ON;
import static android.car.VehiclePropertyIds.HVAC_MAX_DEFROST_ON;
import static android.car.VehiclePropertyIds.HVAC_POWER_ON;
import static android.car.VehiclePropertyIds.HVAC_RECIRC_ON;
import static android.car.VehiclePropertyIds.HVAC_SEAT_TEMPERATURE;
import static android.car.VehiclePropertyIds.HVAC_SEAT_VENTILATION;
import static android.car.VehiclePropertyIds.HVAC_STEERING_WHEEL_HEAT;
import static android.car.VehiclePropertyIds.HVAC_TEMPERATURE_SET;

import static androidx.annotation.RestrictTo.Scope.LIBRARY;
import static androidx.car.app.hardware.climate.ClimateProfileRequest.FEATURE_CABIN_TEMPERATURE;
import static androidx.car.app.hardware.climate.ClimateProfileRequest.FEATURE_FAN_DIRECTION;
import static androidx.car.app.hardware.climate.ClimateProfileRequest.FEATURE_FAN_SPEED;
import static androidx.car.app.hardware.climate.ClimateProfileRequest.FEATURE_HVAC_AC;
import static androidx.car.app.hardware.climate.ClimateProfileRequest.FEATURE_HVAC_AUTO_MODE;
import static androidx.car.app.hardware.climate.ClimateProfileRequest.FEATURE_HVAC_AUTO_RECIRCULATION;
import static androidx.car.app.hardware.climate.ClimateProfileRequest.FEATURE_HVAC_DEFROSTER;
import static androidx.car.app.hardware.climate.ClimateProfileRequest.FEATURE_HVAC_DUAL_MODE;
import static androidx.car.app.hardware.climate.ClimateProfileRequest.FEATURE_HVAC_ELECTRIC_DEFROSTER;
import static androidx.car.app.hardware.climate.ClimateProfileRequest.FEATURE_HVAC_MAX_AC;
import static androidx.car.app.hardware.climate.ClimateProfileRequest.FEATURE_HVAC_MAX_DEFROSTER;
import static androidx.car.app.hardware.climate.ClimateProfileRequest.FEATURE_HVAC_POWER;
import static androidx.car.app.hardware.climate.ClimateProfileRequest.FEATURE_HVAC_RECIRCULATION;
import static androidx.car.app.hardware.climate.ClimateProfileRequest.FEATURE_SEAT_TEMPERATURE_LEVEL;
import static androidx.car.app.hardware.climate.ClimateProfileRequest.FEATURE_SEAT_VENTILATION_LEVEL;
import static androidx.car.app.hardware.climate.ClimateProfileRequest.FEATURE_STEERING_WHEEL_HEAT;
import static androidx.car.app.hardware.common.CarValueUtils.getCarValue;
import static androidx.car.app.hardware.common.PropertyUtils.getMinMaxProfileFloatMap;
import static androidx.car.app.hardware.common.PropertyUtils.getMinMaxProfileIntegerMap;

import static java.util.Objects.requireNonNull;

import android.util.Log;

import androidx.annotation.NonNull;
import androidx.annotation.RestrictTo;
import androidx.annotation.VisibleForTesting;
import androidx.car.app.annotations.ExperimentalCarApi;
import androidx.car.app.hardware.common.CarPropertyProfile;
import androidx.car.app.hardware.common.CarPropertyResponse;
import androidx.car.app.hardware.common.CarSetOperationStatusCallback;
import androidx.car.app.hardware.common.CarValue;
import androidx.car.app.hardware.common.CarZone;
import androidx.car.app.hardware.common.OnCarPropertyResponseListener;
import androidx.car.app.hardware.common.PropertyManager;
import androidx.car.app.utils.LogTags;

import com.google.common.collect.ImmutableBiMap;
import com.google.common.util.concurrent.ListenableFuture;

import java.util.ArrayList;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
import java.util.concurrent.ExecutionException;
import java.util.concurrent.Executor;

/**
 * Manages access to car climate system such as cabin temperatures, fan speeds and fan directions.
 *
 * @hide
 */
@RestrictTo(LIBRARY)
@ExperimentalCarApi
public class AutomotiveCarClimate implements CarClimate {

    @VisibleForTesting
    static final float DEFAULT_SAMPLE_RATE_HZ = 5f;
    public static final int HVAC_ELECTRIC_DEFROSTER_ON_PROPERTY_ID = 320865556;

    // TODO(b/240347704): replace FEATURE_HVAC_ELECTRIC_DEFROSTER value with
    //  HVAC_ELECTRIC_DEFROSTER_ON if it becomes available.
    static ImmutableBiMap<Integer, Integer> sFeatureToPropertyId =
            new ImmutableBiMap.Builder<Integer,
                    Integer>()
                    .put(FEATURE_HVAC_POWER, HVAC_POWER_ON)
                    .put(FEATURE_HVAC_AC, HVAC_AC_ON)
                    .put(FEATURE_HVAC_MAX_AC, HVAC_MAX_AC_ON)
                    .put(FEATURE_CABIN_TEMPERATURE, HVAC_TEMPERATURE_SET)
                    .put(FEATURE_FAN_SPEED, HVAC_FAN_SPEED)
                    .put(FEATURE_FAN_DIRECTION, HVAC_FAN_DIRECTION)
                    .put(FEATURE_SEAT_TEMPERATURE_LEVEL, HVAC_SEAT_TEMPERATURE)
                    .put(FEATURE_SEAT_VENTILATION_LEVEL, HVAC_SEAT_VENTILATION)
                    .put(FEATURE_STEERING_WHEEL_HEAT, HVAC_STEERING_WHEEL_HEAT)
                    .put(FEATURE_HVAC_RECIRCULATION, HVAC_RECIRC_ON)
                    .put(FEATURE_HVAC_AUTO_RECIRCULATION, HVAC_AUTO_RECIRC_ON)
                    .put(FEATURE_HVAC_AUTO_MODE, HVAC_AUTO_ON)
                    .put(FEATURE_HVAC_DUAL_MODE, HVAC_DUAL_ON)
                    .put(FEATURE_HVAC_DEFROSTER, HVAC_DEFROSTER)
                    .put(FEATURE_HVAC_MAX_DEFROSTER, HVAC_MAX_DEFROST_ON)
                    .put(FEATURE_HVAC_ELECTRIC_DEFROSTER, HVAC_ELECTRIC_DEFROSTER_ON_PROPERTY_ID)
                    .buildOrThrow();

    private final Map<CarClimateStateCallback, OnCarPropertyResponseListener> mListenerMap =
            new HashMap<>();

    private final PropertyManager mPropertyManager;

    public AutomotiveCarClimate(@NonNull PropertyManager manager) {
        mPropertyManager = requireNonNull(manager);
    }

    @Override
    public void registerClimateStateCallback(@NonNull Executor executor,
            @NonNull RegisterClimateStateRequest request,
            @NonNull CarClimateStateCallback callback) {
        Map<Integer, List<CarZone>> propertyIdsWithCarZones = new HashMap<>();
        for (CarClimateFeature feature : request.getClimateRegisterFeatures()) {
            int propertyId = requireNonNull(sFeatureToPropertyId.get(feature.getFeature()));
            propertyIdsWithCarZones.put(propertyId, feature.getCarZones());
        }
        PropertyListener listener = new PropertyListener(callback, executor);
        mPropertyManager.submitRegisterListenerRequest(propertyIdsWithCarZones,
                DEFAULT_SAMPLE_RATE_HZ,
                listener,
                executor);
        mListenerMap.put(callback, listener);
    }

    @Override
    public void unregisterClimateStateCallback(@NonNull CarClimateStateCallback callback) {
        OnCarPropertyResponseListener responseListener = mListenerMap.remove(callback);
        if (responseListener != null) {
            mPropertyManager.submitUnregisterListenerRequest(responseListener);
        }
    }

    @Override
    public void fetchClimateProfile(@NonNull Executor executor,
            @NonNull ClimateProfileRequest request,
            @NonNull CarClimateProfileCallback callback) {
        if (request.getClimateProfileFeatures().isEmpty()) {
            Log.e(LogTags.TAG_CAR_HARDWARE,
                    "ClimateProfileRequest does not contain features.");
            return;
        }
        List<Integer> propertyIds = new ArrayList<>();
        for (CarClimateFeature feature : request.getClimateProfileFeatures()) {
            propertyIds.add(requireNonNull(sFeatureToPropertyId.get(
                    feature.getFeature())));
        }
        ListenableFuture<List<CarPropertyProfile<?>>> future =
                mPropertyManager.fetchSupportedZonesResponse(
                        propertyIds, executor);
        populateData(executor, callback, future);
    }

    @Override
    public <E> void setClimateState(@NonNull Executor executor,
            @NonNull ClimateStateRequest<E> request,
            @NonNull CarSetOperationStatusCallback callback) {

    }

    private static class PropertyListener implements OnCarPropertyResponseListener {
        private final Executor mExecutor;
        private final CarClimateStateCallback mCarClimateStateCallback;

        PropertyListener(CarClimateStateCallback callback, Executor executor) {
            mCarClimateStateCallback = callback;
            mExecutor = executor;
        }

        @Override
        @SuppressWarnings({"unchecked", "unsafe"})
        public void onCarPropertyResponses(
                @NonNull List<CarPropertyResponse<?>> carPropertyResponses) {
            mExecutor.execute(() -> {
                for (CarPropertyResponse<?> response : carPropertyResponses) {
                    Integer mFeature = sFeatureToPropertyId.inverse().get(response.getPropertyId());
                    if (mFeature == null) {
                        Log.e(LogTags.TAG_CAR_HARDWARE, "Feature not found for property Id "
                                + response.getPropertyId());
                        continue;
                    }
                    CarValue<?> mCarValue = getCarValue(response, response.getValue());
                    switch (mFeature) {
                        case FEATURE_HVAC_POWER:
                            mCarClimateStateCallback.onHvacPowerStateAvailable(
                                    (CarValue<Boolean>) mCarValue);
                            break;
                        case FEATURE_HVAC_AC:
                            mCarClimateStateCallback.onHvacAcStateAvailable(
                                    (CarValue<Boolean>) mCarValue);
                            break;
                        case FEATURE_HVAC_MAX_AC:
                            mCarClimateStateCallback.onHvacMaxAcModeStateAvailable(
                                    (CarValue<Boolean>) mCarValue);
                            break;
                        case FEATURE_CABIN_TEMPERATURE:
                            mCarClimateStateCallback.onCabinTemperatureStateAvailable(
                                    (CarValue<Float>) mCarValue);
                            break;
                        case FEATURE_FAN_SPEED:
                            mCarClimateStateCallback.onFanSpeedLevelStateAvailable(
                                    (CarValue<Integer>) mCarValue);
                            break;
                        case FEATURE_FAN_DIRECTION:
                            mCarClimateStateCallback.onFanDirectionStateAvailable(
                                    (CarValue<Integer>) mCarValue);
                            break;
                        case FEATURE_SEAT_TEMPERATURE_LEVEL:
                            mCarClimateStateCallback.onSeatTemperatureLevelStateAvailable(
                                    (CarValue<Integer>) mCarValue);
                            break;
                        case FEATURE_SEAT_VENTILATION_LEVEL:
                            mCarClimateStateCallback.onSeatVentilationLevelStateAvailable(
                                    (CarValue<Integer>) mCarValue);
                            break;
                        case FEATURE_STEERING_WHEEL_HEAT:
                            mCarClimateStateCallback.onSteeringWheelHeatStateAvailable(
                                    (CarValue<Boolean>) mCarValue);
                            break;
                        case FEATURE_HVAC_RECIRCULATION:
                            mCarClimateStateCallback.onHvacRecirculationStateAvailable(
                                    (CarValue<Boolean>) mCarValue);
                            break;
                        case FEATURE_HVAC_AUTO_RECIRCULATION:
                            mCarClimateStateCallback.onHvacAutoRecirculationStateAvailable(
                                    (CarValue<Boolean>) mCarValue);
                            break;
                        case FEATURE_HVAC_AUTO_MODE:
                            mCarClimateStateCallback.onHvacAutoModeStateAvailable(
                                    (CarValue<Boolean>) mCarValue);
                            break;
                        case FEATURE_HVAC_DUAL_MODE:
                            mCarClimateStateCallback.onHvacDualModeStateAvailable(
                                    (CarValue<Boolean>) mCarValue);
                            break;
                        case FEATURE_HVAC_DEFROSTER:
                            mCarClimateStateCallback.onDefrosterStateAvailable(
                                    (CarValue<Boolean>) mCarValue);
                            break;
                        case FEATURE_HVAC_MAX_DEFROSTER:
                            mCarClimateStateCallback.onMaxDefrosterStateAvailable(
                                    (CarValue<Boolean>) mCarValue);
                            break;
                        case FEATURE_HVAC_ELECTRIC_DEFROSTER:
                            mCarClimateStateCallback.onElectricDefrosterStateAvailable(
                                    (CarValue<Boolean>) mCarValue);
                            break;
                        default:
                            Log.e(LogTags.TAG_CAR_HARDWARE,
                                    "Invalid response callback in PropertyListener with "
                                            + "feature value: " + mFeature);
                            break;
                    }
                }
            });
        }
    }

    private static void populateData(@NonNull Executor executor,
            @NonNull CarClimateProfileCallback onCarClimateProfileCallback,
            ListenableFuture<List<CarPropertyProfile<?>>> future) {
        future.addListener(() -> {
            List<CarPropertyProfile<?>> carPropertyProfiles;
            try {
                carPropertyProfiles = future.get();
            } catch (ExecutionException e) {
                Log.e(LogTags.TAG_CAR_HARDWARE,
                        "Failed to get CarPropertyResponse due to error", e);
                return;
            } catch (InterruptedException e) {
                Log.e(LogTags.TAG_CAR_HARDWARE,
                        "Failed to get CarPropertyResponse due to error", e);
                Thread.currentThread().interrupt();
                return;
            }

            // Extract all car zones corresponding to each feature.
            for (CarPropertyProfile<?> carPropertyProfile : carPropertyProfiles) {
                Integer feature = sFeatureToPropertyId.inverse().get(
                        carPropertyProfile.getPropertyId());
                if (feature == null) {
                    Log.e(LogTags.TAG_CAR_HARDWARE, "Feature not found for property Id "
                            + carPropertyProfile.getPropertyId());
                    continue;
                }

                switch (feature) {
                    case FEATURE_HVAC_POWER:
                        onCarClimateProfileCallback.onHvacPowerProfileAvailable(
                                new HvacPowerProfile.Builder(carPropertyProfile.getCarZones())
                                        .build());
                        break;
                    case FEATURE_CABIN_TEMPERATURE:
                        if ((carPropertyProfile.getCelsiusRange() == null
                                || carPropertyProfile.getFahrenheitRange() == null)
                                && (carPropertyProfile.getCarZoneSetsToMinMaxRange() == null)) {
                            Log.e(LogTags.TAG_CAR_HARDWARE, "Failed to fetch cabin "
                                    + "temperature value with profile: " + carPropertyProfile);
                            break;
                        }
                        CabinTemperatureProfile.Builder cabinTemperatureProfileBuilder =
                                new CabinTemperatureProfile.Builder();
                        if (carPropertyProfile.getCelsiusRange() != null) {
                            cabinTemperatureProfileBuilder.setSupportedMinMaxCelsiusRange(
                                    carPropertyProfile.getCelsiusRange());
                        }
                        if (carPropertyProfile.getFahrenheitRange() != null) {
                            cabinTemperatureProfileBuilder.setSupportedMinMaxFahrenheitRange(
                                    carPropertyProfile.getFahrenheitRange());
                        }
                        if (carPropertyProfile.getCarZoneSetsToMinMaxRange() != null) {
                            cabinTemperatureProfileBuilder
                                    .setCarZoneSetsToCabinCelsiusTemperatureRanges(
                                            getMinMaxProfileFloatMap(
                                                    carPropertyProfile
                                                            .getCarZoneSetsToMinMaxRange()));
                        }
                        if (carPropertyProfile.getCelsiusIncrement() != -1f) {
                            cabinTemperatureProfileBuilder.setCelsiusSupportedIncrement(
                                    carPropertyProfile.getCelsiusIncrement());
                        }
                        if (carPropertyProfile.getCelsiusIncrement() != -1f) {
                            cabinTemperatureProfileBuilder.setFahrenheitSupportedIncrement(
                                    carPropertyProfile.getFahrenheitIncrement());
                        }
                        onCarClimateProfileCallback.onCabinTemperatureProfileAvailable(
                                cabinTemperatureProfileBuilder.build());
                        break;
                    case FEATURE_FAN_SPEED:
                        if (carPropertyProfile.getCarZoneSetsToMinMaxRange() == null) {
                            Log.e(LogTags.TAG_CAR_HARDWARE, "Failed to fetch fan speed value"
                                    + " with profile: " + carPropertyProfile);
                            break;
                        }
                        onCarClimateProfileCallback.onFanSpeedLevelProfileAvailable(
                                new FanSpeedLevelProfile.Builder(getMinMaxProfileIntegerMap(
                                        carPropertyProfile.getCarZoneSetsToMinMaxRange()))
                                        .build()
                        );
                        break;
                    case FEATURE_FAN_DIRECTION:
                        if (carPropertyProfile.getHvacFanDirection() == null) {
                            Log.e(LogTags.TAG_CAR_HARDWARE, "Failed to fetch direction value"
                                    + " with profile: " + carPropertyProfile);
                            break;
                        }
                        onCarClimateProfileCallback.onFanDirectionProfileAvailable(
                                new FanDirectionProfile.Builder(
                                        carPropertyProfile.getHvacFanDirection())
                                        .build()
                        );
                        break;
                    case FEATURE_SEAT_TEMPERATURE_LEVEL:
                        if (carPropertyProfile.getCarZoneSetsToMinMaxRange() == null) {
                            Log.e(LogTags.TAG_CAR_HARDWARE, "Failed to fetch seat temperature"
                                    + " value with profile: " + carPropertyProfile);
                            break;
                        }
                        onCarClimateProfileCallback.onSeatTemperatureLevelProfileAvailable(
                                new SeatTemperatureProfile.Builder(getMinMaxProfileIntegerMap(
                                        carPropertyProfile.getCarZoneSetsToMinMaxRange()))
                                        .build()
                        );
                        break;
                    case FEATURE_SEAT_VENTILATION_LEVEL:
                        if (carPropertyProfile.getCarZoneSetsToMinMaxRange() == null) {
                            Log.e(LogTags.TAG_CAR_HARDWARE, "Failed to fetch seat ventilation"
                                    + " value with profile: " + carPropertyProfile);
                            break;
                        }
                        onCarClimateProfileCallback.onSeatVentilationLevelProfileAvailable(
                                new SeatVentilationProfile.Builder(getMinMaxProfileIntegerMap(
                                        carPropertyProfile.getCarZoneSetsToMinMaxRange()))
                                        .build()
                        );
                        break;
                    case FEATURE_STEERING_WHEEL_HEAT:
                        if (carPropertyProfile.getCarZoneSetsToMinMaxRange() == null) {
                            Log.e(LogTags.TAG_CAR_HARDWARE, "Failed to fetch steering wheel"
                                    + " heat value with profile: " + carPropertyProfile);
                            break;
                        }
                        onCarClimateProfileCallback.onSteeringWheelHeatProfileAvailable(
                                new SteeringWheelHeatProfile.Builder(getMinMaxProfileIntegerMap(
                                        carPropertyProfile.getCarZoneSetsToMinMaxRange()))
                                        .build()
                        );
                        break;
                    case FEATURE_HVAC_AC:
                        onCarClimateProfileCallback.onHvacAcProfileAvailable(
                                new HvacAcProfile.Builder(carPropertyProfile.getCarZones())
                                        .build());
                        break;
                    case FEATURE_HVAC_MAX_AC:
                        onCarClimateProfileCallback.onHvacMaxAcModeProfileAvailable(
                                new HvacMaxAcModeProfile.Builder(carPropertyProfile.getCarZones())
                                        .build());
                        break;
                    case FEATURE_HVAC_RECIRCULATION:
                        onCarClimateProfileCallback.onHvacRecirculationProfileAvailable(
                                new HvacRecirculationProfile.Builder(
                                        carPropertyProfile.getCarZones())
                                        .build());
                        break;
                    case FEATURE_HVAC_AUTO_RECIRCULATION:
                        onCarClimateProfileCallback.onHvacAutoRecirculationProfileAvailable(
                                new HvacAutoRecirculationProfile.Builder(
                                        carPropertyProfile.getCarZones())
                                        .build());
                        break;
                    case FEATURE_HVAC_AUTO_MODE:
                        onCarClimateProfileCallback.onHvacAutoModeProfileAvailable(
                                new HvacAutoModeProfile.Builder(carPropertyProfile.getCarZones())
                                        .build());
                        break;
                    case FEATURE_HVAC_DUAL_MODE:
                        onCarClimateProfileCallback.onHvacDualModeProfileAvailable(
                                new HvacDualModeProfile.Builder(carPropertyProfile.getCarZones())
                                        .build());
                        break;
                    case FEATURE_HVAC_DEFROSTER:
                        onCarClimateProfileCallback.onDefrosterProfileAvailable(
                                new DefrosterProfile.Builder(carPropertyProfile.getCarZones())
                                        .build());
                        break;
                    case FEATURE_HVAC_MAX_DEFROSTER:
                        onCarClimateProfileCallback.onMaxDefrosterProfileAvailable(
                                new MaxDefrosterProfile.Builder(carPropertyProfile.getCarZones())
                                        .build());
                        break;
                    case FEATURE_HVAC_ELECTRIC_DEFROSTER:
                        onCarClimateProfileCallback.onElectricDefrosterProfileAvailable(
                                new ElectricDefrosterProfile.Builder(
                                        carPropertyProfile.getCarZones())
                                        .build());
                        break;
                    default:
                        Log.e(LogTags.TAG_CAR_HARDWARE,
                                "Invalid response callback while populating data for "
                                        + "feature value: " + feature);
                        break;
                }
            }
        }, executor);
    }
}