| /* |
| * 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 com.android.systemui.car.hvac; |
| |
| import static android.car.VehiclePropertyIds.HVAC_ACTUAL_FAN_SPEED_RPM; |
| 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_ELECTRIC_DEFROSTER_ON; |
| import static android.car.VehiclePropertyIds.HVAC_FAN_DIRECTION; |
| import static android.car.VehiclePropertyIds.HVAC_FAN_DIRECTION_AVAILABLE; |
| 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_SIDE_MIRROR_HEAT; |
| import static android.car.VehiclePropertyIds.HVAC_STEERING_WHEEL_HEAT; |
| import static android.car.VehiclePropertyIds.HVAC_TEMPERATURE_CURRENT; |
| import static android.car.VehiclePropertyIds.HVAC_TEMPERATURE_DISPLAY_UNITS; |
| import static android.car.VehiclePropertyIds.HVAC_TEMPERATURE_SET; |
| |
| import android.annotation.IntDef; |
| import android.car.Car; |
| import android.car.VehicleUnit; |
| import android.car.hardware.CarPropertyConfig; |
| import android.car.hardware.CarPropertyValue; |
| import android.car.hardware.property.CarPropertyManager; |
| import android.content.res.Resources; |
| import android.util.Log; |
| import android.view.View; |
| import android.view.ViewGroup; |
| |
| import androidx.annotation.VisibleForTesting; |
| |
| import com.android.systemui.car.CarServiceProvider; |
| import com.android.systemui.dagger.qualifiers.Main; |
| import com.android.systemui.dagger.qualifiers.UiBackground; |
| import com.android.systemui.statusbar.policy.ConfigurationController; |
| |
| import java.lang.annotation.ElementType; |
| import java.lang.annotation.Target; |
| import java.util.ArrayList; |
| import java.util.HashMap; |
| import java.util.List; |
| import java.util.Map; |
| import java.util.concurrent.Executor; |
| |
| import javax.inject.Inject; |
| |
| /** |
| * A controller that connects to {@link CarPropertyManager} to subscribe to HVAC property change |
| * events and propagate them to subscribing {@link HvacView}s by property ID and area ID. |
| * |
| * Grants {@link HvacView}s access to {@link HvacPropertySetter} with API's to write new values |
| * for HVAC properties. |
| */ |
| public class HvacController implements HvacPropertySetter, |
| ConfigurationController.ConfigurationListener { |
| private static final String TAG = HvacController.class.getSimpleName(); |
| private static final boolean DEBUG = false; |
| private static final int[] HVAC_PROPERTIES = |
| {HVAC_FAN_SPEED, HVAC_FAN_DIRECTION, HVAC_TEMPERATURE_CURRENT, HVAC_TEMPERATURE_SET, |
| HVAC_DEFROSTER, HVAC_AC_ON, HVAC_MAX_AC_ON, HVAC_MAX_DEFROST_ON, HVAC_RECIRC_ON, |
| HVAC_DUAL_ON, HVAC_AUTO_ON, HVAC_SEAT_TEMPERATURE, HVAC_SIDE_MIRROR_HEAT, |
| HVAC_STEERING_WHEEL_HEAT, HVAC_TEMPERATURE_DISPLAY_UNITS, |
| HVAC_ACTUAL_FAN_SPEED_RPM, HVAC_POWER_ON, HVAC_FAN_DIRECTION_AVAILABLE, |
| HVAC_AUTO_RECIRC_ON, HVAC_SEAT_VENTILATION, HVAC_ELECTRIC_DEFROSTER_ON}; |
| private static final int[] HVAC_PROPERTIES_TO_GET_ON_INIT = {HVAC_POWER_ON, HVAC_AUTO_ON}; |
| private static final int GLOBAL_AREA_ID = 0; |
| private static final int VEHICLE_AREA_GLOBAL = 0x01000000; |
| private static final int VEHICLE_AREA_SEAT = 0x05000000; |
| |
| @IntDef(value = {HVAC_FAN_SPEED, HVAC_FAN_DIRECTION, HVAC_TEMPERATURE_CURRENT, |
| HVAC_TEMPERATURE_SET, HVAC_DEFROSTER, HVAC_AC_ON, HVAC_MAX_AC_ON, HVAC_MAX_DEFROST_ON, |
| HVAC_RECIRC_ON, HVAC_DUAL_ON, HVAC_AUTO_ON, HVAC_SEAT_TEMPERATURE, |
| HVAC_SIDE_MIRROR_HEAT, HVAC_STEERING_WHEEL_HEAT, HVAC_TEMPERATURE_DISPLAY_UNITS, |
| HVAC_ACTUAL_FAN_SPEED_RPM, HVAC_POWER_ON, HVAC_FAN_DIRECTION_AVAILABLE, |
| HVAC_AUTO_RECIRC_ON, HVAC_SEAT_VENTILATION, HVAC_ELECTRIC_DEFROSTER_ON}) |
| @Target({ElementType.TYPE_PARAMETER, ElementType.TYPE_USE}) |
| public @interface HvacProperty { |
| } |
| |
| @Target({ElementType.TYPE_PARAMETER, ElementType.TYPE_USE}) |
| public @interface AreaId { |
| } |
| |
| private Executor mExecutor; |
| private CarPropertyManager mCarPropertyManager; |
| private boolean mIsConnectedToCar; |
| |
| /** |
| * Contains views to init until car service is connected. |
| * This must be accessed via {@link #mExecutor} to ensure thread safety. |
| */ |
| private final ArrayList<View> mViewsToInit = new ArrayList<>(); |
| private final Map<@HvacProperty Integer, Map<@AreaId Integer, List<HvacView>>> |
| mHvacPropertyViewMap = new HashMap<>(); |
| |
| private final CarPropertyManager.CarPropertyEventCallback mPropertyEventCallback = |
| new CarPropertyManager.CarPropertyEventCallback() { |
| @Override |
| public void onChangeEvent(CarPropertyValue value) { |
| mExecutor.execute(() -> { |
| handleHvacPropertyChange(value.getPropertyId(), value); |
| }); |
| } |
| |
| @Override |
| public void onErrorEvent(int propId, int zone) { |
| Log.w(TAG, "Could not handle " + propId + " change event in zone " + zone); |
| } |
| }; |
| |
| @UiBackground |
| @VisibleForTesting |
| final CarServiceProvider.CarServiceOnConnectedListener mCarServiceLifecycleListener = |
| car -> { |
| try { |
| mExecutor.execute(() -> { |
| mIsConnectedToCar = true; |
| mCarPropertyManager = |
| (CarPropertyManager) car.getCarManager(Car.PROPERTY_SERVICE); |
| registerHvacPropertyEventListeners(); |
| mViewsToInit.forEach(this::registerHvacViews); |
| mViewsToInit.clear(); |
| }); |
| } catch (Exception e) { |
| Log.e(TAG, "Failed to connect to HVAC", e); |
| mIsConnectedToCar = false; |
| } |
| }; |
| |
| @Inject |
| public HvacController(CarServiceProvider carServiceProvider, |
| @UiBackground Executor executor, |
| @Main Resources resources, |
| ConfigurationController configurationController) { |
| mExecutor = executor; |
| if (!mIsConnectedToCar) { |
| carServiceProvider.addListener(mCarServiceLifecycleListener); |
| } |
| configurationController.addCallback(this); |
| } |
| |
| private int[] getSupportedAreaIds(int propertyId) { |
| CarPropertyConfig config = mCarPropertyManager.getCarPropertyConfig(propertyId); |
| if (config == null) { |
| // This property isn't supported/exposed by the CarPropertyManager. So an empty array is |
| // returned here to signify that no areaIds with this propertyId are going to be |
| // registered or updated. |
| return new int[] {}; |
| } |
| return config.getAreaIds(); |
| } |
| |
| private ArrayList<Integer> getAreaIdsFromTargetAreaId(int propertyId, int targetAreaId) { |
| ArrayList<Integer> areaIdsFromTargetAreaId = new ArrayList<Integer>(); |
| int[] supportedAreaIds = getSupportedAreaIds(propertyId); |
| |
| for (int supportedAreaId : supportedAreaIds) { |
| if (targetAreaId == GLOBAL_AREA_ID || (targetAreaId & supportedAreaId) != 0) { |
| areaIdsFromTargetAreaId.add(supportedAreaId); |
| } |
| } |
| |
| return areaIdsFromTargetAreaId; |
| } |
| |
| @Override |
| public void setHvacProperty(@HvacProperty Integer propertyId, int targetAreaId, |
| int val) { |
| mExecutor.execute(() -> { |
| try { |
| ArrayList<Integer> supportedAreaIds = getAreaIdsFromTargetAreaId( |
| propertyId.intValue(), targetAreaId); |
| for (int areaId : supportedAreaIds) { |
| mCarPropertyManager.setIntProperty(propertyId, areaId, val); |
| } |
| } catch (RuntimeException e) { |
| Log.w(TAG, "Error while setting HVAC property: ", e); |
| } |
| }); |
| } |
| |
| @Override |
| public void setHvacProperty(@HvacProperty Integer propertyId, int targetAreaId, |
| float val) { |
| mExecutor.execute(() -> { |
| try { |
| ArrayList<Integer> supportedAreaIds = getAreaIdsFromTargetAreaId( |
| propertyId.intValue(), targetAreaId); |
| for (int areaId : supportedAreaIds) { |
| mCarPropertyManager.setFloatProperty(propertyId, areaId, val); |
| } |
| } catch (RuntimeException e) { |
| Log.w(TAG, "Error while setting HVAC property: ", e); |
| } |
| }); |
| } |
| |
| @Override |
| public void setHvacProperty(@HvacProperty Integer propertyId, int targetAreaId, |
| boolean val) { |
| mExecutor.execute(() -> { |
| try { |
| ArrayList<Integer> supportedAreaIds = getAreaIdsFromTargetAreaId( |
| propertyId.intValue(), targetAreaId); |
| for (int areaId : supportedAreaIds) { |
| mCarPropertyManager.setBooleanProperty(propertyId, areaId, val); |
| } |
| } catch (RuntimeException e) { |
| Log.w(TAG, "Error while setting HVAC property: ", e); |
| } |
| }); |
| } |
| |
| /** |
| * Registers all {@link HvacView}s in the {@code rootView} and its descendents. |
| */ |
| @UiBackground |
| public void registerHvacViews(View rootView) { |
| if (!mIsConnectedToCar) { |
| mExecutor.execute(() -> mViewsToInit.add(rootView)); |
| return; |
| } |
| |
| if (rootView instanceof HvacView) { |
| try { |
| HvacView hvacView = (HvacView) rootView; |
| @HvacProperty Integer propId = hvacView.getHvacPropertyToView(); |
| @AreaId Integer targetAreaId = hvacView.getAreaId(); |
| hvacView.setHvacPropertySetter(this); |
| |
| ArrayList<Integer> supportedAreaIds = getAreaIdsFromTargetAreaId(propId.intValue(), |
| targetAreaId.intValue()); |
| for (Integer areaId : supportedAreaIds) { |
| addHvacViewToMap(propId.intValue(), areaId.intValue(), hvacView); |
| } |
| |
| if (mCarPropertyManager != null) { |
| boolean usesFahrenheit = mCarPropertyManager.getIntProperty( |
| HVAC_TEMPERATURE_DISPLAY_UNITS, GLOBAL_AREA_ID) |
| == VehicleUnit.FAHRENHEIT; |
| for (Integer areaId : supportedAreaIds) { |
| CarPropertyValue initValue = mCarPropertyManager.getProperty(propId, |
| areaId); |
| |
| // Initialize the view with the initial value. |
| hvacView.onPropertyChanged(initValue); |
| hvacView.onHvacTemperatureUnitChanged(usesFahrenheit); |
| |
| if (mCarPropertyManager.getCarPropertyConfig(propId).getAreaType() |
| != VEHICLE_AREA_SEAT) { |
| continue; |
| } |
| |
| for (int propToGetOnInitId : HVAC_PROPERTIES_TO_GET_ON_INIT) { |
| int[] propToGetOnInitSupportedAreaIds = getSupportedAreaIds( |
| propToGetOnInitId); |
| |
| int areaIdToFind = areaId.intValue(); |
| |
| for (int supportedAreaId : propToGetOnInitSupportedAreaIds) { |
| if ((supportedAreaId & areaIdToFind) == areaIdToFind) { |
| CarPropertyValue propToGetOnInitValue = |
| mCarPropertyManager.getProperty(propToGetOnInitId, |
| supportedAreaId); |
| hvacView.onPropertyChanged(propToGetOnInitValue); |
| break; |
| } |
| } |
| } |
| } |
| } |
| } catch (IllegalArgumentException ex) { |
| Log.e(TAG, "Can't register HVAC view", ex); |
| } |
| } |
| |
| if (rootView instanceof ViewGroup) { |
| ViewGroup viewGroup = (ViewGroup) rootView; |
| for (int i = 0; i < viewGroup.getChildCount(); i++) { |
| registerHvacViews(viewGroup.getChildAt(i)); |
| } |
| } |
| } |
| |
| /** |
| * Unregisters all {@link HvacView}s in the {@code rootView} and its descendents. |
| */ |
| public void unregisterViews(View rootView) { |
| if (rootView instanceof HvacView) { |
| HvacView hvacView = (HvacView) rootView; |
| @HvacProperty Integer propId = hvacView.getHvacPropertyToView(); |
| @AreaId Integer targetAreaId = hvacView.getAreaId(); |
| |
| ArrayList<Integer> supportedAreaIds = getAreaIdsFromTargetAreaId(propId.intValue(), |
| targetAreaId.intValue()); |
| for (Integer areaId : supportedAreaIds) { |
| removeHvacViewFromMap(propId.intValue(), areaId.intValue(), hvacView); |
| } |
| } |
| |
| if (rootView instanceof ViewGroup) { |
| ViewGroup viewGroup = (ViewGroup) rootView; |
| for (int i = 0; i < viewGroup.getChildCount(); i++) { |
| unregisterViews(viewGroup.getChildAt(i)); |
| } |
| } |
| } |
| |
| @VisibleForTesting |
| void handleHvacPropertyChange(@HvacProperty int propertyId, CarPropertyValue value) { |
| if (value.getPropertyId() == HVAC_TEMPERATURE_DISPLAY_UNITS) { |
| mHvacPropertyViewMap.forEach((propId, areaIds) -> { |
| areaIds.forEach((areaId, views) -> { |
| views.forEach(v -> v.onHvacTemperatureUnitChanged( |
| (Integer) value.getValue() == VehicleUnit.FAHRENHEIT)); |
| }); |
| }); |
| return; |
| } |
| |
| int valueAreaType = mCarPropertyManager.getCarPropertyConfig(value.getPropertyId()) |
| .getAreaType(); |
| if (valueAreaType == VEHICLE_AREA_GLOBAL) { |
| mHvacPropertyViewMap.forEach((propId, areaIds) -> { |
| areaIds.forEach((areaId, views) -> { |
| views.forEach(v -> v.onPropertyChanged(value)); |
| }); |
| }); |
| } else { |
| mHvacPropertyViewMap.forEach((propId, areaIds) -> { |
| if (valueAreaType |
| == mCarPropertyManager.getCarPropertyConfig(propId).getAreaType()) { |
| areaIds.forEach((areaId, views) -> { |
| if ((value.getAreaId() & areaId) == areaId) { |
| views.forEach(v -> v.onPropertyChanged(value)); |
| } |
| }); |
| } |
| }); |
| } |
| } |
| |
| @VisibleForTesting |
| Map<@HvacProperty Integer, Map<@AreaId Integer, List<HvacView>>> getHvacPropertyViewMap() { |
| return mHvacPropertyViewMap; |
| } |
| |
| @Override |
| public void onLocaleListChanged() { |
| // Call {@link HvacView#onLocaleListChanged} on all {@link HvacView} instances. |
| for (Map<@AreaId Integer, List<HvacView>> subMap : mHvacPropertyViewMap.values()) { |
| for (List<HvacView> views : subMap.values()) { |
| for (HvacView view : views) { |
| view.onLocaleListChanged(); |
| } |
| } |
| } |
| } |
| |
| private void registerHvacPropertyEventListeners() { |
| for (int i = 0; i < HVAC_PROPERTIES.length; i++) { |
| @HvacProperty Integer propertyId = HVAC_PROPERTIES[i]; |
| mCarPropertyManager.registerCallback(mPropertyEventCallback, propertyId, |
| CarPropertyManager.SENSOR_RATE_ONCHANGE); |
| } |
| } |
| |
| private void addHvacViewToMap(@HvacProperty int propId, @AreaId int areaId, |
| HvacView v) { |
| mHvacPropertyViewMap.computeIfAbsent(propId, k -> new HashMap<>()) |
| .computeIfAbsent(areaId, k -> new ArrayList<>()) |
| .add(v); |
| } |
| |
| private void removeHvacViewFromMap(@HvacProperty int propId, @AreaId int areaId, HvacView v) { |
| Map<Integer, List<HvacView>> viewsRegisteredForProp = mHvacPropertyViewMap.get(propId); |
| if (viewsRegisteredForProp != null) { |
| List<HvacView> registeredViews = viewsRegisteredForProp.get(areaId); |
| if (registeredViews != null) { |
| registeredViews.remove(v); |
| if (registeredViews.isEmpty()) { |
| viewsRegisteredForProp.remove(areaId); |
| if (viewsRegisteredForProp.isEmpty()) { |
| mHvacPropertyViewMap.remove(propId); |
| } |
| } |
| } |
| } |
| } |
| } |