1 /* 2 * Copyright (C) 2021 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 17 package com.android.systemui.car.hvac; 18 19 import static android.car.VehiclePropertyIds.HVAC_ACTUAL_FAN_SPEED_RPM; 20 import static android.car.VehiclePropertyIds.HVAC_AC_ON; 21 import static android.car.VehiclePropertyIds.HVAC_AUTO_ON; 22 import static android.car.VehiclePropertyIds.HVAC_AUTO_RECIRC_ON; 23 import static android.car.VehiclePropertyIds.HVAC_DEFROSTER; 24 import static android.car.VehiclePropertyIds.HVAC_DUAL_ON; 25 import static android.car.VehiclePropertyIds.HVAC_ELECTRIC_DEFROSTER_ON; 26 import static android.car.VehiclePropertyIds.HVAC_FAN_DIRECTION; 27 import static android.car.VehiclePropertyIds.HVAC_FAN_DIRECTION_AVAILABLE; 28 import static android.car.VehiclePropertyIds.HVAC_FAN_SPEED; 29 import static android.car.VehiclePropertyIds.HVAC_MAX_AC_ON; 30 import static android.car.VehiclePropertyIds.HVAC_MAX_DEFROST_ON; 31 import static android.car.VehiclePropertyIds.HVAC_POWER_ON; 32 import static android.car.VehiclePropertyIds.HVAC_RECIRC_ON; 33 import static android.car.VehiclePropertyIds.HVAC_SEAT_TEMPERATURE; 34 import static android.car.VehiclePropertyIds.HVAC_SEAT_VENTILATION; 35 import static android.car.VehiclePropertyIds.HVAC_SIDE_MIRROR_HEAT; 36 import static android.car.VehiclePropertyIds.HVAC_STEERING_WHEEL_HEAT; 37 import static android.car.VehiclePropertyIds.HVAC_TEMPERATURE_CURRENT; 38 import static android.car.VehiclePropertyIds.HVAC_TEMPERATURE_DISPLAY_UNITS; 39 import static android.car.VehiclePropertyIds.HVAC_TEMPERATURE_SET; 40 41 import android.annotation.IntDef; 42 import android.car.Car; 43 import android.car.VehicleUnit; 44 import android.car.hardware.CarPropertyValue; 45 import android.car.hardware.property.CarPropertyManager; 46 import android.content.res.Resources; 47 import android.util.Log; 48 import android.view.View; 49 import android.view.ViewGroup; 50 51 import androidx.annotation.Nullable; 52 import androidx.annotation.VisibleForTesting; 53 54 import com.android.systemui.R; 55 import com.android.systemui.car.CarServiceProvider; 56 import com.android.systemui.dagger.qualifiers.Main; 57 import com.android.systemui.dagger.qualifiers.UiBackground; 58 import com.android.systemui.statusbar.policy.ConfigurationController; 59 60 import java.lang.annotation.ElementType; 61 import java.lang.annotation.Target; 62 import java.util.ArrayList; 63 import java.util.HashMap; 64 import java.util.List; 65 import java.util.Map; 66 import java.util.concurrent.Executor; 67 68 import javax.inject.Inject; 69 70 /** 71 * A controller that connects to {@link CarPropertyManager} to subscribe to HVAC property change 72 * events and propagate them to subscribing {@link HvacView}s by property ID and area ID. 73 * 74 * Grants {@link HvacView}s access to {@link HvacPropertySetter} with API's to write new values 75 * for HVAC properties. 76 */ 77 public class HvacController implements HvacPropertySetter, 78 ConfigurationController.ConfigurationListener { 79 private static final String TAG = HvacController.class.getSimpleName(); 80 private static final boolean DEBUG = false; 81 private static final int[] HVAC_PROPERTIES = 82 {HVAC_FAN_SPEED, HVAC_FAN_DIRECTION, HVAC_TEMPERATURE_CURRENT, HVAC_TEMPERATURE_SET, 83 HVAC_DEFROSTER, HVAC_AC_ON, HVAC_MAX_AC_ON, HVAC_MAX_DEFROST_ON, HVAC_RECIRC_ON, 84 HVAC_DUAL_ON, HVAC_AUTO_ON, HVAC_SEAT_TEMPERATURE, HVAC_SIDE_MIRROR_HEAT, 85 HVAC_STEERING_WHEEL_HEAT, HVAC_TEMPERATURE_DISPLAY_UNITS, 86 HVAC_ACTUAL_FAN_SPEED_RPM, HVAC_POWER_ON, HVAC_FAN_DIRECTION_AVAILABLE, 87 HVAC_AUTO_RECIRC_ON, HVAC_SEAT_VENTILATION, HVAC_ELECTRIC_DEFROSTER_ON}; 88 private static final int[] HVAC_PROPERTIES_TO_GET_ON_INIT = {HVAC_POWER_ON, HVAC_AUTO_ON}; 89 90 @IntDef(value = {HVAC_FAN_SPEED, HVAC_FAN_DIRECTION, HVAC_TEMPERATURE_CURRENT, 91 HVAC_TEMPERATURE_SET, HVAC_DEFROSTER, HVAC_AC_ON, HVAC_MAX_AC_ON, HVAC_MAX_DEFROST_ON, 92 HVAC_RECIRC_ON, HVAC_DUAL_ON, HVAC_AUTO_ON, HVAC_SEAT_TEMPERATURE, 93 HVAC_SIDE_MIRROR_HEAT, HVAC_STEERING_WHEEL_HEAT, HVAC_TEMPERATURE_DISPLAY_UNITS, 94 HVAC_ACTUAL_FAN_SPEED_RPM, HVAC_POWER_ON, HVAC_FAN_DIRECTION_AVAILABLE, 95 HVAC_AUTO_RECIRC_ON, HVAC_SEAT_VENTILATION, HVAC_ELECTRIC_DEFROSTER_ON}) 96 @Target({ElementType.TYPE_PARAMETER, ElementType.TYPE_USE}) 97 public @interface HvacProperty { 98 } 99 100 @Target({ElementType.TYPE_PARAMETER, ElementType.TYPE_USE}) 101 public @interface AreaId { 102 } 103 104 private Executor mExecutor; 105 private CarPropertyManager mCarPropertyManager; 106 private boolean mIsConnectedToCar; 107 @Nullable 108 private Integer mHvacGlobalAreaId; 109 110 /** 111 * Contains views to init until car service is connected. 112 * This must be accessed via {@link #mExecutor} to ensure thread safety. 113 */ 114 private final ArrayList<View> mViewsToInit = new ArrayList<>(); 115 private final Map<@HvacProperty Integer, Map<@AreaId Integer, List<HvacView>>> 116 mHvacPropertyViewMap = new HashMap<>(); 117 118 private final CarPropertyManager.CarPropertyEventCallback mPropertyEventCallback = 119 new CarPropertyManager.CarPropertyEventCallback() { 120 @Override 121 public void onChangeEvent(CarPropertyValue value) { 122 mExecutor.execute(() -> handleHvacPropertyChange(value.getPropertyId(), value)); 123 } 124 125 @Override 126 public void onErrorEvent(int propId, int zone) { 127 Log.w(TAG, "Could not handle " + propId + " change event in zone " + zone); 128 } 129 }; 130 131 @UiBackground 132 @VisibleForTesting 133 final CarServiceProvider.CarServiceOnConnectedListener mCarServiceLifecycleListener = 134 car -> { 135 try { 136 mExecutor.execute(() -> { 137 mIsConnectedToCar = true; 138 mCarPropertyManager = 139 (CarPropertyManager) car.getCarManager(Car.PROPERTY_SERVICE); 140 registerHvacPropertyEventListeners(); 141 mViewsToInit.forEach(this::registerHvacViews); 142 mViewsToInit.clear(); 143 }); 144 } catch (Exception e) { 145 Log.e(TAG, "Failed to connect to HVAC", e); 146 mIsConnectedToCar = false; 147 } 148 }; 149 150 @Inject HvacController(CarServiceProvider carServiceProvider, @UiBackground Executor executor, @Main Resources resources, ConfigurationController configurationController)151 public HvacController(CarServiceProvider carServiceProvider, 152 @UiBackground Executor executor, 153 @Main Resources resources, 154 ConfigurationController configurationController) { 155 mExecutor = executor; 156 if (!mIsConnectedToCar) { 157 carServiceProvider.addListener(mCarServiceLifecycleListener); 158 } 159 configurationController.addCallback(this); 160 mHvacGlobalAreaId = resources.getInteger(R.integer.hvac_global_area_id); 161 } 162 163 @Override setHvacProperty(@vacProperty Integer propertyId, int areaId, int val)164 public void setHvacProperty(@HvacProperty Integer propertyId, int areaId, int val) { 165 mExecutor.execute(() -> { 166 try { 167 mCarPropertyManager.setIntProperty(propertyId, areaId, val); 168 } catch (RuntimeException e) { 169 Log.w(TAG, "Error while setting HVAC property: ", e); 170 } 171 }); 172 } 173 174 @Override setHvacProperty(@vacProperty Integer propertyId, int areaId, float val)175 public void setHvacProperty(@HvacProperty Integer propertyId, int areaId, float val) { 176 mExecutor.execute(() -> { 177 try { 178 mCarPropertyManager.setFloatProperty(propertyId, areaId, val); 179 } catch (RuntimeException e) { 180 Log.w(TAG, "Error while setting HVAC property: ", e); 181 } 182 }); 183 } 184 185 @Override setHvacProperty(@vacProperty Integer propertyId, int areaId, boolean val)186 public void setHvacProperty(@HvacProperty Integer propertyId, int areaId, boolean val) { 187 mExecutor.execute(() -> { 188 try { 189 mCarPropertyManager.setBooleanProperty(propertyId, areaId, val); 190 } catch (RuntimeException e) { 191 Log.w(TAG, "Error while setting HVAC property: ", e); 192 } 193 }); 194 } 195 196 /** 197 * Registers all {@link HvacView}s in the {@code rootView} and its descendants. 198 */ 199 @UiBackground registerHvacViews(View rootView)200 public void registerHvacViews(View rootView) { 201 if (!mIsConnectedToCar) { 202 mExecutor.execute(() -> mViewsToInit.add(rootView)); 203 return; 204 } 205 206 if (rootView instanceof HvacView) { 207 try { 208 HvacView hvacView = (HvacView) rootView; 209 @HvacProperty Integer propId = hvacView.getHvacPropertyToView(); 210 @AreaId Integer areaId = hvacView.getAreaId(); 211 hvacView.setHvacPropertySetter(this); 212 213 addHvacViewToMap(propId, areaId, hvacView); 214 215 if (mCarPropertyManager != null) { 216 CarPropertyValue initValue = mCarPropertyManager.getProperty(propId, areaId); 217 boolean usesFahrenheit = mCarPropertyManager.getIntProperty( 218 HVAC_TEMPERATURE_DISPLAY_UNITS, 219 mCarPropertyManager.getAreaId(HVAC_TEMPERATURE_DISPLAY_UNITS, 220 areaId)) == VehicleUnit.FAHRENHEIT; 221 222 // Initialize the view with the initial value. 223 hvacView.onPropertyChanged(initValue); 224 hvacView.onHvacTemperatureUnitChanged(usesFahrenheit); 225 for (int propToGetOnInitId : HVAC_PROPERTIES_TO_GET_ON_INIT) { 226 CarPropertyValue propToGetOnInitValue = mCarPropertyManager.getProperty( 227 propToGetOnInitId, mHvacGlobalAreaId); 228 hvacView.onPropertyChanged(propToGetOnInitValue); 229 } 230 } 231 } catch (IllegalArgumentException ex) { 232 Log.e(TAG, "Can't register HVAC view", ex); 233 } 234 } 235 236 if (rootView instanceof ViewGroup) { 237 ViewGroup viewGroup = (ViewGroup) rootView; 238 for (int i = 0; i < viewGroup.getChildCount(); i++) { 239 registerHvacViews(viewGroup.getChildAt(i)); 240 } 241 } 242 } 243 244 /** 245 * Unregisters all {@link HvacView}s in the {@code rootView} and its descendants. 246 */ unregisterViews(View rootView)247 public void unregisterViews(View rootView) { 248 if (rootView instanceof HvacView) { 249 HvacView hvacView = (HvacView) rootView; 250 @HvacProperty Integer propId = hvacView.getHvacPropertyToView(); 251 @AreaId Integer areaId = hvacView.getAreaId(); 252 253 removeHvacViewFromMap(propId, areaId, hvacView); 254 } 255 256 if (rootView instanceof ViewGroup) { 257 ViewGroup viewGroup = (ViewGroup) rootView; 258 for (int i = 0; i < viewGroup.getChildCount(); i++) { 259 unregisterViews(viewGroup.getChildAt(i)); 260 } 261 } 262 } 263 264 @VisibleForTesting handleHvacPropertyChange(@vacProperty int propertyId, CarPropertyValue value)265 void handleHvacPropertyChange(@HvacProperty int propertyId, CarPropertyValue value) { 266 List<HvacView> viewsToNotify = null; 267 268 if (value.getAreaId() == mHvacGlobalAreaId) { 269 mHvacPropertyViewMap.forEach((propId, areaIds) -> { 270 areaIds.forEach((areaId, views) -> { 271 views.forEach(v -> v.onPropertyChanged(value)); 272 }); 273 }); 274 return; 275 } 276 277 if (value.getPropertyId() == HVAC_TEMPERATURE_DISPLAY_UNITS) { 278 mHvacPropertyViewMap.forEach((propId, areaIds) -> { 279 areaIds.forEach((areaId, views) -> { 280 views.forEach(v -> v.onHvacTemperatureUnitChanged( 281 (Integer) value.getValue() == VehicleUnit.FAHRENHEIT)); 282 }); 283 }); 284 return; 285 } 286 287 Map<Integer, List<HvacView>> viewsRegisteredForProp = mHvacPropertyViewMap.get(propertyId); 288 if (viewsRegisteredForProp != null) { 289 viewsToNotify = viewsRegisteredForProp.get(value.getAreaId()); 290 if (viewsToNotify != null) { 291 viewsToNotify.forEach(v -> v.onPropertyChanged(value)); 292 } 293 } 294 } 295 296 @VisibleForTesting getHvacPropertyViewMap()297 Map<@HvacProperty Integer, Map<@AreaId Integer, List<HvacView>>> getHvacPropertyViewMap() { 298 return mHvacPropertyViewMap; 299 } 300 301 @Override onLocaleListChanged()302 public void onLocaleListChanged() { 303 // Call {@link HvacView#onLocaleListChanged} on all {@link HvacView} instances. 304 for (Map<@AreaId Integer, List<HvacView>> subMap : mHvacPropertyViewMap.values()) { 305 for (List<HvacView> views : subMap.values()) { 306 for (HvacView view : views) { 307 view.onLocaleListChanged(); 308 } 309 } 310 } 311 } 312 registerHvacPropertyEventListeners()313 private void registerHvacPropertyEventListeners() { 314 for (int i = 0; i < HVAC_PROPERTIES.length; i++) { 315 @HvacProperty Integer propertyId = HVAC_PROPERTIES[i]; 316 mCarPropertyManager.registerCallback(mPropertyEventCallback, propertyId, 317 CarPropertyManager.SENSOR_RATE_ONCHANGE); 318 } 319 } 320 addHvacViewToMap(@vacProperty int propId, @AreaId int areaId, HvacView v)321 private void addHvacViewToMap(@HvacProperty int propId, @AreaId int areaId, 322 HvacView v) { 323 mHvacPropertyViewMap.computeIfAbsent(propId, k -> new HashMap<>()) 324 .computeIfAbsent(areaId, k -> new ArrayList<>()) 325 .add(v); 326 } 327 removeHvacViewFromMap(@vacProperty int propId, @AreaId int areaId, HvacView v)328 private void removeHvacViewFromMap(@HvacProperty int propId, @AreaId int areaId, HvacView v) { 329 Map<Integer, List<HvacView>> viewsRegisteredForProp = mHvacPropertyViewMap.get(propId); 330 if (viewsRegisteredForProp != null) { 331 List<HvacView> registeredViews = viewsRegisteredForProp.get(areaId); 332 if (registeredViews != null) { 333 registeredViews.remove(v); 334 if (registeredViews.isEmpty()) { 335 viewsRegisteredForProp.remove(areaId); 336 if (viewsRegisteredForProp.isEmpty()) { 337 mHvacPropertyViewMap.remove(propId); 338 } 339 } 340 } 341 } 342 } 343 } 344