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