1 /*
2  * Copyright (C) 2015 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 android.car.hardware.hvac;
18 
19 import static java.lang.Integer.toHexString;
20 
21 import android.annotation.IntDef;
22 import android.annotation.Nullable;
23 import android.annotation.SystemApi;
24 import android.car.Car;
25 import android.car.CarManagerBase;
26 import android.car.CarNotConnectedException;
27 import android.car.hardware.CarPropertyConfig;
28 import android.car.hardware.CarPropertyValue;
29 import android.content.Context;
30 import android.os.Handler;
31 import android.os.IBinder;
32 import android.os.Looper;
33 import android.os.Message;
34 import android.os.RemoteException;
35 import android.util.ArraySet;
36 import android.util.Log;
37 
38 import java.lang.ref.WeakReference;
39 import java.util.Collection;
40 import java.util.List;
41 
42 /**
43  * API for controlling HVAC system in cars
44  * @hide
45  */
46 @SystemApi
47 public class CarHvacManager implements CarManagerBase {
48     public final static boolean DBG = true;
49     public final static String TAG = "CarHvacManager";
50 
51     /**
52      * HVAC property IDs for get/set methods
53      */
54     @IntDef({
55             HvacPropertyId.MIRROR_DEFROSTER_ON,
56             HvacPropertyId.STEERING_WHEEL_TEMP,
57             HvacPropertyId.MAX_GLOBAL_PROPERTY_ID,
58             HvacPropertyId.ZONED_TEMP_SETPOINT,
59             HvacPropertyId.ZONED_TEMP_ACTUAL,
60             HvacPropertyId.ZONED_TEMP_IS_FAHRENHEIT,
61             HvacPropertyId.ZONED_FAN_SPEED_SETPOINT,
62             HvacPropertyId.ZONED_FAN_SPEED_RPM,
63             HvacPropertyId.ZONED_FAN_POSITION_AVAILABLE,
64             HvacPropertyId.ZONED_FAN_POSITION,
65             HvacPropertyId.ZONED_SEAT_TEMP,
66             HvacPropertyId.ZONED_AC_ON,
67             HvacPropertyId.ZONED_AUTOMATIC_MODE_ON,
68             HvacPropertyId.ZONED_AIR_RECIRCULATION_ON,
69             HvacPropertyId.WINDOW_DEFROSTER_ON,
70     })
71     public @interface HvacPropertyId {
72         /**
73          * Global HVAC properties.  There is only a single instance in a car.
74          * Global properties are in the range of 0-0x3FFF.
75          */
76         /** Mirror defrosters state, bool. */
77         int MIRROR_DEFROSTER_ON = 0x0001;
78         /** Steering wheel temp:  negative values indicate cooling, positive values indicate
79          * heat, int. */
80         int STEERING_WHEEL_TEMP = 0x0002;
81 
82         /** The maximum id that can be assigned to global (non-zoned) property. */
83         int MAX_GLOBAL_PROPERTY_ID = 0x3fff;
84 
85         /**
86          * ZONED_* represents properties available on a per-zone basis.  All zones in a car are
87          * not required to have the same properties.  Zone specific properties start at 0x4000.
88          */
89         /** Temperature setpoint desired by the user, in terms of F or C, depending on
90          * TEMP_IS_FAHRENHEIT, int */
91         int ZONED_TEMP_SETPOINT = 0x4001;
92         /** Actual zone temperature is read only integer, in terms of F or C, int. */
93         int ZONED_TEMP_ACTUAL = 0x4002;
94         /** Temperature is in degrees fahrenheit if this is true, bool. */
95         int ZONED_TEMP_IS_FAHRENHEIT = 0x4003;
96         /** Fan speed setpoint is an integer from 0-n, depending on the number of fan speeds
97          * available. Selection determines the fan position, int. */
98         int ZONED_FAN_SPEED_SETPOINT = 0x4004;
99         /** Actual fan speed is a read-only value, expressed in RPM, int. */
100         int ZONED_FAN_SPEED_RPM = 0x4005;
101         /** Fan position available is a bitmask of positions available for each zone, int. */
102         int ZONED_FAN_POSITION_AVAILABLE = 0x4006;
103         /** Current fan position setting, int. */
104         int ZONED_FAN_POSITION = 0x4007;
105         /** Seat temperature is negative for cooling, positive for heating.  Temperature is a
106          * setting, i.e. -3 to 3 for 3 levels of cooling and 3 levels of heating.  int. */
107         int ZONED_SEAT_TEMP = 0x4008;
108         /** Air conditioner state, bool */
109         int ZONED_AC_ON = 0x4009;
110         /** HVAC is in automatic mode, bool. */
111         int ZONED_AUTOMATIC_MODE_ON = 0x400A;
112         /** Air recirculation is active, bool. */
113         int ZONED_AIR_RECIRCULATION_ON = 0x400B;
114         /** Defroster is based off of window position, bool */
115         int WINDOW_DEFROSTER_ON = 0x5001;
116     }
117 
118     // Constants handled in the handler (see mHandler below).
119     private final static int MSG_HVAC_EVENT = 0;
120 
121     /** Callback functions for HVAC events */
122     public interface CarHvacEventListener {
123         /** Called when an HVAC property is updated */
onChangeEvent(final CarPropertyValue value)124         void onChangeEvent(final CarPropertyValue value);
125 
126         /** Called when an error is detected with a property */
onErrorEvent(final int propertyId, final int zone)127         void onErrorEvent(final int propertyId, final int zone);
128     }
129 
130     private final ICarHvac mService;
131     private final ArraySet<CarHvacEventListener> mListeners = new ArraySet<>();
132     private CarHvacEventListenerToService mListenerToService = null;
133 
134     private static final class EventCallbackHandler extends Handler {
135         WeakReference<CarHvacManager> mMgr;
136 
EventCallbackHandler(CarHvacManager mgr, Looper looper)137         EventCallbackHandler(CarHvacManager mgr, Looper looper) {
138             super(looper);
139             mMgr = new WeakReference<>(mgr);
140         }
141 
142         @Override
handleMessage(Message msg)143         public void handleMessage(Message msg) {
144             switch (msg.what) {
145                 case MSG_HVAC_EVENT:
146                     CarHvacManager mgr = mMgr.get();
147                     if (mgr != null) {
148                         mgr.dispatchEventToClient((CarHvacEvent) msg.obj);
149                     }
150                     break;
151                 default:
152                     Log.e(TAG, "Event type not handled?" + msg);
153                     break;
154             }
155         }
156     }
157 
158     private final Handler mHandler;
159 
160     private static class CarHvacEventListenerToService extends ICarHvacEventListener.Stub {
161         private final WeakReference<CarHvacManager> mManager;
162 
CarHvacEventListenerToService(CarHvacManager manager)163         public CarHvacEventListenerToService(CarHvacManager manager) {
164             mManager = new WeakReference<>(manager);
165         }
166 
167         @Override
onEvent(CarHvacEvent event)168         public void onEvent(CarHvacEvent event) {
169             CarHvacManager manager = mManager.get();
170             if (manager != null) {
171                 manager.handleEvent(event);
172             }
173         }
174     }
175 
176     /**
177      * Get an instance of the CarHvacManager.
178      *
179      * Should not be obtained directly by clients, use {@link Car#getCarManager(String)} instead.
180      * @hide
181      */
CarHvacManager(IBinder service, Context context, Looper looper)182     public CarHvacManager(IBinder service, Context context, Looper looper) {
183         mService = ICarHvac.Stub.asInterface(service);
184         mHandler = new EventCallbackHandler(this, looper);
185     }
186 
187     /** Returns true if the property is a zoned type. */
isZonedProperty(int propertyId)188     public static boolean isZonedProperty(int propertyId) {
189         return propertyId > HvacPropertyId.MAX_GLOBAL_PROPERTY_ID;
190     }
191 
192     /**
193      * Register {@link CarHvacEventListener} to get HVAC property changes
194      *
195      * @param listener Implements onEvent() for property change updates
196      */
registerListener(CarHvacEventListener listener)197     public synchronized void registerListener(CarHvacEventListener listener)
198             throws CarNotConnectedException {
199         if(mListeners.isEmpty()) {
200             try {
201                 mListenerToService = new CarHvacEventListenerToService(this);
202                 mService.registerListener(mListenerToService);
203             } catch (RemoteException ex) {
204                 Log.e(TAG, "Could not connect: " + ex.toString());
205                 throw new CarNotConnectedException(ex);
206             } catch (IllegalStateException ex) {
207                 Car.checkCarNotConnectedExceptionFromCarService(ex);
208             }
209         }
210         mListeners.add(listener);
211     }
212 
213     /**
214      * Unregister {@link CarHvacEventListener}.
215      * @param listener CarHvacEventListener to unregister
216      */
unregisterListener(CarHvacEventListener listener)217     public synchronized void unregisterListener(CarHvacEventListener listener)
218             throws CarNotConnectedException {
219         if (DBG) {
220             Log.d(TAG, "unregisterListener");
221         }
222         try {
223             mService.unregisterListener(mListenerToService);
224         } catch (RemoteException e) {
225             Log.e(TAG, "Could not unregister: " + e.toString());
226             throw new CarNotConnectedException(e);
227 
228         }
229         mListeners.remove(listener);
230         if(mListeners.isEmpty()) {
231             mListenerToService = null;
232         }
233     }
234 
235     /**
236      * Returns the list of HVAC properties available.
237      *
238      * @return Caller must check the property type and typecast to the appropriate subclass
239      * (CarHvacBooleanProperty, CarHvacFloatProperty, CarrHvacIntProperty)
240      */
getPropertyList()241     public List<CarPropertyConfig> getPropertyList()  throws CarNotConnectedException {
242         List<CarPropertyConfig> carProps;
243         try {
244             carProps = mService.getHvacProperties();
245         } catch (RemoteException e) {
246             Log.w(TAG, "Exception in getPropertyList", e);
247             throw new CarNotConnectedException(e);
248         }
249         return carProps;
250     }
251 
252     /**
253      * Returns value of a bool property
254      *
255      * @param prop Property ID to get
256      * @param area Area of the property to get
257      */
getBooleanProperty(int prop, int area)258     public boolean getBooleanProperty(int prop, int area) throws CarNotConnectedException {
259         CarPropertyValue<Boolean> carProp = getProperty(Boolean.class, prop, area);
260         return carProp != null ? carProp.getValue() : false;
261     }
262 
263     /**
264      * Returns value of a float property
265      *
266      * @param prop Property ID to get
267      * @param area Area of the property to get
268      */
getFloatProperty(int prop, int area)269     public float getFloatProperty(int prop, int area) throws CarNotConnectedException {
270         CarPropertyValue<Float> carProp = getProperty(Float.class, prop, area);
271         return carProp != null ? carProp.getValue() : 0f;
272     }
273 
274     /**
275      * Returns value of a integer property
276      *
277      * @param prop Property ID to get
278      * @param area Zone of the property to get
279      */
getIntProperty(int prop, int area)280     public int getIntProperty(int prop, int area) throws CarNotConnectedException {
281         CarPropertyValue<Integer> carProp = getProperty(Integer.class, prop, area);
282         return carProp != null ? carProp.getValue() : 0;
283     }
284 
285     @Nullable
286     @SuppressWarnings("unchecked")
getProperty(Class<E> clazz, int prop, int area)287     private <E> CarPropertyValue<E> getProperty(Class<E> clazz, int prop, int area)
288             throws CarNotConnectedException {
289         if (DBG) {
290             Log.d(TAG, "getProperty, prop: 0x" + toHexString(prop)
291                     + ", area: 0x" + toHexString(area) + ", clazz: " + clazz);
292         }
293         try {
294             CarPropertyValue<E> hvacProperty = mService.getProperty(prop, area);
295             if (hvacProperty != null && hvacProperty.getValue() != null) {
296                 Class<?> actualClass = hvacProperty.getValue().getClass();
297                 if (actualClass != clazz) {
298                 throw new IllegalArgumentException("Invalid property type. "
299                         + "Expected: " + clazz + ", but was: " + actualClass);
300                 }
301             }
302             return hvacProperty;
303         } catch (RemoteException e) {
304             Log.e(TAG, "getProperty failed with " + e.toString()
305                     + ", propId: 0x" + toHexString(prop) + ", area: 0x" + toHexString(area), e);
306             throw new CarNotConnectedException(e);
307         }
308     }
309 
310     /**
311      * Modifies a property.  If the property modification doesn't occur, an error event shall be
312      * generated and propagated back to the application.
313      *
314      * @param prop Property ID to modify
315      * @param area Area to apply the modification.
316      * @param val Value to set
317      */
setBooleanProperty(int prop, int area, boolean val)318     public void setBooleanProperty(int prop, int area, boolean val)
319             throws CarNotConnectedException {
320         if (DBG) {
321             Log.d(TAG, "setBooleanProperty:  prop = " + prop + " area = " + area + " val = " + val);
322         }
323         try {
324             mService.setProperty(new CarPropertyValue<>(prop, area, val));
325         } catch (RemoteException e) {
326             Log.e(TAG, "setBooleanProperty failed with " + e.toString(), e);
327             throw new CarNotConnectedException(e);
328         }
329     }
330 
setFloatProperty(int prop, int area, float val)331     public void setFloatProperty(int prop, int area, float val) throws CarNotConnectedException {
332         if (DBG) {
333             Log.d(TAG, "setFloatProperty:  prop = " + prop + " area = " + area + " val = " + val);
334         }
335         try {
336             mService.setProperty(new CarPropertyValue<>(prop, area, val));
337         } catch (RemoteException e) {
338             Log.e(TAG, "setBooleanProperty failed with " + e.toString(), e);
339             throw new CarNotConnectedException(e);
340         }
341     }
342 
setIntProperty(int prop, int area, int val)343     public void setIntProperty(int prop, int area, int val) throws CarNotConnectedException {
344         if (DBG) {
345             Log.d(TAG, "setIntProperty:  prop = " + prop + " area = " + area + " val = " + val);
346         }
347         try {
348             mService.setProperty(new CarPropertyValue<>(prop, area, val));
349         } catch (RemoteException e) {
350             Log.e(TAG, "setIntProperty failed with " + e.toString(), e);
351             throw new CarNotConnectedException(e);
352         }
353     }
354 
dispatchEventToClient(CarHvacEvent event)355     private void dispatchEventToClient(CarHvacEvent event) {
356         Collection<CarHvacEventListener> listeners;
357         synchronized (this) {
358             listeners = mListeners;
359         }
360         if (!listeners.isEmpty()) {
361             CarPropertyValue hvacProperty = event.getCarPropertyValue();
362             switch(event.getEventType()) {
363                 case CarHvacEvent.HVAC_EVENT_PROPERTY_CHANGE:
364                     for (CarHvacEventListener l: listeners) {
365                         l.onChangeEvent(hvacProperty);
366                     }
367                 case CarHvacEvent.HVAC_EVENT_ERROR:
368                     for (CarHvacEventListener l: listeners) {
369                         l.onErrorEvent(hvacProperty.getPropertyId(), hvacProperty.getAreaId());
370                     }
371                     break;
372                 default:
373                     throw new IllegalArgumentException();
374             }
375         } else {
376             Log.e(TAG, "Listener died, not dispatching event.");
377         }
378     }
379 
handleEvent(CarHvacEvent event)380     private void handleEvent(CarHvacEvent event) {
381         mHandler.sendMessage(mHandler.obtainMessage(MSG_HVAC_EVENT, event));
382     }
383 
384     /** @hide */
385     @Override
onCarDisconnected()386     public void onCarDisconnected() {
387         for(CarHvacEventListener l: mListeners) {
388             try {
389                 unregisterListener(l);
390             } catch (CarNotConnectedException e) {
391                 // Ignore, car is disconnecting.
392             }
393         }
394     }
395 }
396