1 /*
2  * Copyright (C) 2018 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.property;
18 
19 import static java.lang.Integer.toHexString;
20 
21 import android.annotation.FloatRange;
22 import android.annotation.NonNull;
23 import android.annotation.Nullable;
24 import android.car.Car;
25 import android.car.CarManagerBase;
26 import android.car.hardware.CarPropertyConfig;
27 import android.car.hardware.CarPropertyValue;
28 import android.os.Handler;
29 import android.os.RemoteException;
30 import android.util.ArraySet;
31 import android.util.Log;
32 import android.util.SparseArray;
33 
34 import com.android.car.internal.CarRatedFloatListeners;
35 import com.android.car.internal.SingleMessageHandler;
36 
37 import java.lang.ref.WeakReference;
38 import java.util.ArrayList;
39 import java.util.List;
40 import java.util.function.Consumer;
41 
42 
43 /**
44  * Provides an application interface for interacting with the Vehicle specific properties.
45  * For details about the individual properties, see the descriptions in
46  * hardware/interfaces/automotive/vehicle/types.hal
47  */
48 public class CarPropertyManager extends CarManagerBase {
49     private static final boolean DBG = false;
50     private static final String TAG = "CarPropertyManager";
51     private static final int MSG_GENERIC_EVENT = 0;
52     private final SingleMessageHandler<CarPropertyEvent> mHandler;
53     private final ICarProperty mService;
54 
55     private CarPropertyEventListenerToService mCarPropertyEventToService;
56 
57     /** Record of locally active properties. Key is propertyId */
58     private final SparseArray<CarPropertyListeners> mActivePropertyListener =
59             new SparseArray<>();
60     /** Record of properties' configs. Key is propertyId */
61     private final SparseArray<CarPropertyConfig> mConfigMap = new SparseArray<>();
62 
63     /**
64      * Application registers {@link CarPropertyEventCallback} object to receive updates and changes
65      * to subscribed Vehicle specific properties.
66      */
67     public interface CarPropertyEventCallback {
68         /**
69          * Called when a property is updated
70          * @param value Property that has been updated.
71          */
onChangeEvent(CarPropertyValue value)72         void onChangeEvent(CarPropertyValue value);
73 
74         /**
75          * Called when an error is detected with a property
76          * @param propId Property ID which is detected an error.
77          * @param zone Zone which is detected an error.
78          */
onErrorEvent(int propId, int zone)79         void onErrorEvent(int propId, int zone);
80     }
81 
82     /** Read ON_CHANGE sensors */
83     public static final float SENSOR_RATE_ONCHANGE = 0f;
84     /** Read sensors at the rate of  1 hertz */
85     public static final float SENSOR_RATE_NORMAL = 1f;
86     /** Read sensors at the rate of 5 hertz */
87     public static final float SENSOR_RATE_UI = 5f;
88     /** Read sensors at the rate of 10 hertz */
89     public static final float SENSOR_RATE_FAST = 10f;
90     /** Read sensors at the rate of 100 hertz */
91     public static final float SENSOR_RATE_FASTEST = 100f;
92 
93     /**
94      * Get an instance of the CarPropertyManager.
95      *
96      * Should not be obtained directly by clients, use {@link Car#getCarManager(String)} instead.
97      * @param car Car instance
98      * @param service ICarProperty instance
99      * @hide
100      */
CarPropertyManager(Car car, @NonNull ICarProperty service)101     public CarPropertyManager(Car car, @NonNull ICarProperty service) {
102         super(car);
103         mService = service;
104         try {
105             List<CarPropertyConfig> configs = mService.getPropertyList();
106             for (CarPropertyConfig carPropertyConfig : configs) {
107                 mConfigMap.put(carPropertyConfig.getPropertyId(), carPropertyConfig);
108             }
109         } catch (Exception e) {
110             Log.e(TAG, "getPropertyList exception ", e);
111             throw new RuntimeException(e);
112         }
113         Handler eventHandler = getEventHandler();
114         if (eventHandler == null) {
115             mHandler = null;
116             return;
117         }
118         mHandler = new SingleMessageHandler<CarPropertyEvent>(eventHandler.getLooper(),
119             MSG_GENERIC_EVENT) {
120             @Override
121             protected void handleEvent(CarPropertyEvent event) {
122                 CarPropertyListeners listeners;
123                 synchronized (mActivePropertyListener) {
124                     listeners = mActivePropertyListener.get(
125                         event.getCarPropertyValue().getPropertyId());
126                 }
127                 if (listeners != null) {
128                     switch (event.getEventType()) {
129                         case CarPropertyEvent.PROPERTY_EVENT_PROPERTY_CHANGE:
130                             listeners.onPropertyChanged(event);
131                             break;
132                         case CarPropertyEvent.PROPERTY_EVENT_ERROR:
133                             listeners.onErrorEvent(event);
134                             break;
135                         default:
136                             throw new IllegalArgumentException();
137                     }
138                 }
139             }
140         };
141     }
142 
143     /**
144      * Register {@link CarPropertyEventCallback} to get property updates. Multiple listeners
145      * can be registered for a single property or the same listener can be used for different
146      * properties. If the same listener is registered again for the same property, it will be
147      * updated to new rate.
148      * <p>Rate could be one of the following:
149      * <ul>
150      *   <li>{@link CarPropertyManager#SENSOR_RATE_ONCHANGE}</li>
151      *   <li>{@link CarPropertyManager#SENSOR_RATE_NORMAL}</li>
152      *   <li>{@link CarPropertyManager#SENSOR_RATE_UI}</li>
153      *   <li>{@link CarPropertyManager#SENSOR_RATE_FAST}</li>
154      *   <li>{@link CarPropertyManager#SENSOR_RATE_FASTEST}</li>
155      * </ul>
156      * <p>
157      * <b>Note:</b>Rate has no effect if the property has one of the following change modes:
158      * <ul>
159      *   <li>{@link CarPropertyConfig#VEHICLE_PROPERTY_CHANGE_MODE_STATIC}</li>
160      *   <li>{@link CarPropertyConfig#VEHICLE_PROPERTY_CHANGE_MODE_ONCHANGE}</li>
161      * </ul>
162      * See {@link CarPropertyConfig#getChangeMode()} for details.
163      * If rate is higher than {@link CarPropertyConfig#getMaxSampleRate()}, it will be registered
164      * with max sample rate.
165      * If rate is lower than {@link CarPropertyConfig#getMinSampleRate()}, it will be registered
166      * with min sample rate.
167      *
168      * @param callback CarPropertyEventCallback to be registered.
169      * @param propertyId PropertyId to subscribe
170      * @param rate how fast the property events are delivered in Hz.
171      * @return true if the listener is successfully registered.
172      * @throws SecurityException if missing the appropriate permission.
173      */
registerCallback(@onNull CarPropertyEventCallback callback, int propertyId, @FloatRange(from = 0.0, to = 100.0) float rate)174     public boolean registerCallback(@NonNull CarPropertyEventCallback callback,
175             int propertyId, @FloatRange(from = 0.0, to = 100.0) float rate) {
176         synchronized (mActivePropertyListener) {
177             if (mCarPropertyEventToService == null) {
178                 mCarPropertyEventToService = new CarPropertyEventListenerToService(this);
179             }
180             CarPropertyConfig config = mConfigMap.get(propertyId);
181             if (config == null) {
182                 Log.e(TAG, "registerListener:  propId is not in config list:  " + propertyId);
183                 return false;
184             }
185             if (config.getChangeMode() == CarPropertyConfig.VEHICLE_PROPERTY_CHANGE_MODE_ONCHANGE) {
186                 rate = SENSOR_RATE_ONCHANGE;
187             }
188             boolean needsServerUpdate = false;
189             CarPropertyListeners listeners;
190             listeners = mActivePropertyListener.get(propertyId);
191             if (listeners == null) {
192                 listeners = new CarPropertyListeners(rate);
193                 mActivePropertyListener.put(propertyId, listeners);
194                 needsServerUpdate = true;
195             }
196             if (listeners.addAndUpdateRate(callback, rate)) {
197                 needsServerUpdate = true;
198             }
199             if (needsServerUpdate) {
200                 if (!registerOrUpdatePropertyListener(propertyId, rate)) {
201                     return false;
202                 }
203             }
204         }
205         return true;
206     }
207 
registerOrUpdatePropertyListener(int propertyId, float rate)208     private boolean registerOrUpdatePropertyListener(int propertyId, float rate) {
209         try {
210             mService.registerListener(propertyId, rate, mCarPropertyEventToService);
211         } catch (RemoteException e) {
212             return handleRemoteExceptionFromCarService(e, false);
213         }
214         return true;
215     }
216 
217     private static class CarPropertyEventListenerToService extends ICarPropertyEventListener.Stub{
218         private final WeakReference<CarPropertyManager> mMgr;
219 
CarPropertyEventListenerToService(CarPropertyManager mgr)220         CarPropertyEventListenerToService(CarPropertyManager mgr) {
221             mMgr = new WeakReference<>(mgr);
222         }
223 
224         @Override
onEvent(List<CarPropertyEvent> events)225         public void onEvent(List<CarPropertyEvent> events) throws RemoteException {
226             CarPropertyManager manager = mMgr.get();
227             if (manager != null) {
228                 manager.handleEvent(events);
229             }
230         }
231     }
232 
handleEvent(List<CarPropertyEvent> events)233     private void handleEvent(List<CarPropertyEvent> events) {
234         if (mHandler != null) {
235             mHandler.sendEvents(events);
236         }
237     }
238 
239     /**
240      * Stop getting property update for the given callback. If there are multiple registrations for
241      * this callback, all listening will be stopped.
242      * @param callback CarPropertyEventCallback to be unregistered.
243      */
unregisterCallback(@onNull CarPropertyEventCallback callback)244     public void unregisterCallback(@NonNull CarPropertyEventCallback callback) {
245         synchronized (mActivePropertyListener) {
246             int [] propertyIds = new int[mActivePropertyListener.size()];
247             for (int i = 0; i < mActivePropertyListener.size(); i++) {
248                 propertyIds[i] = mActivePropertyListener.keyAt(i);
249             }
250             for (int prop : propertyIds) {
251                 doUnregisterListenerLocked(callback, prop);
252             }
253         }
254     }
255 
256     /**
257      * Stop getting property update for the given callback and property. If the same callback is
258      * used for other properties, those subscriptions will not be affected.
259      *
260      * @param callback CarPropertyEventCallback to be unregistered.
261      * @param propertyId PropertyId to be unregistered.
262      */
unregisterCallback(@onNull CarPropertyEventCallback callback, int propertyId)263     public void unregisterCallback(@NonNull CarPropertyEventCallback callback, int propertyId) {
264         synchronized (mActivePropertyListener) {
265             doUnregisterListenerLocked(callback, propertyId);
266         }
267     }
268 
doUnregisterListenerLocked(CarPropertyEventCallback listener, int propertyId)269     private void doUnregisterListenerLocked(CarPropertyEventCallback listener, int propertyId) {
270         CarPropertyListeners listeners = mActivePropertyListener.get(propertyId);
271         if (listeners != null) {
272             boolean needsServerUpdate = false;
273             if (listeners.contains(listener)) {
274                 needsServerUpdate = listeners.remove(listener);
275             }
276             if (listeners.isEmpty()) {
277                 try {
278                     mService.unregisterListener(propertyId, mCarPropertyEventToService);
279                 } catch (RemoteException e) {
280                     handleRemoteExceptionFromCarService(e);
281                     // continue for local clean-up
282                 }
283                 mActivePropertyListener.remove(propertyId);
284             } else if (needsServerUpdate) {
285                 registerOrUpdatePropertyListener(propertyId, listeners.getRate());
286             }
287         }
288     }
289 
290     /**
291      * @return List of properties implemented by this car that the application may access.
292      */
293     @NonNull
getPropertyList()294     public List<CarPropertyConfig> getPropertyList() {
295         List<CarPropertyConfig> configs = new ArrayList<>(mConfigMap.size());
296         for (int i = 0; i < mConfigMap.size(); i++) {
297             configs.add(mConfigMap.valueAt(i));
298         }
299         return configs;
300     }
301 
302     /**
303      * @param propertyIds property ID list
304      * @return List of properties implemented by this car in given property ID list that application
305      *          may access.
306      */
307     @NonNull
getPropertyList(@onNull ArraySet<Integer> propertyIds)308     public List<CarPropertyConfig> getPropertyList(@NonNull ArraySet<Integer> propertyIds) {
309         List<CarPropertyConfig> configs = new ArrayList<>();
310         for (int propId : propertyIds) {
311             CarPropertyConfig config = mConfigMap.get(propId);
312             if (config != null) {
313                 configs.add(config);
314             }
315         }
316         return configs;
317     }
318 
319     /**
320      * Return read permission string for given property ID.
321      *
322      * @param propId Property ID to query
323      * @return String Permission needed to read this property.  NULL if propId not available.
324      * @hide
325      */
326     @Nullable
getReadPermission(int propId)327     public String getReadPermission(int propId) {
328         if (DBG) {
329             Log.d(TAG, "getReadPermission, propId: 0x" + toHexString(propId));
330         }
331         try {
332             return mService.getReadPermission(propId);
333         } catch (RemoteException e) {
334             return handleRemoteExceptionFromCarService(e, "");
335         }
336     }
337 
338     /**
339      * Return write permission string for given property ID.
340      *
341      * @param propId Property ID to query
342      * @return String Permission needed to write this property.  NULL if propId not available.
343      * @hide
344      */
345     @Nullable
getWritePermission(int propId)346     public String getWritePermission(int propId) {
347         if (DBG) {
348             Log.d(TAG, "getWritePermission, propId: 0x" + toHexString(propId));
349         }
350         try {
351             return mService.getWritePermission(propId);
352         } catch (RemoteException e) {
353             return handleRemoteExceptionFromCarService(e, "");
354         }
355     }
356 
357 
358     /**
359      * Check whether a given property is available or disabled based on the car's current state.
360      * @param propId Property Id
361      * @param area AreaId of property
362      * @return true if STATUS_AVAILABLE, false otherwise (eg STATUS_UNAVAILABLE)
363      */
isPropertyAvailable(int propId, int area)364     public boolean isPropertyAvailable(int propId, int area) {
365         try {
366             CarPropertyValue propValue = mService.getProperty(propId, area);
367             return (propValue != null)
368                     && (propValue.getStatus() == CarPropertyValue.STATUS_AVAILABLE);
369         } catch (RemoteException e) {
370             return handleRemoteExceptionFromCarService(e, false);
371         }
372     }
373 
374     /**
375      * Returns value of a bool property
376      *
377      * @param prop Property ID to get
378      * @param area Area of the property to get
379      * @return value of a bool property.
380      */
getBooleanProperty(int prop, int area)381     public boolean getBooleanProperty(int prop, int area) {
382         CarPropertyValue<Boolean> carProp = getProperty(Boolean.class, prop, area);
383         return carProp != null ? carProp.getValue() : false;
384     }
385 
386     /**
387      * Returns value of a float property
388      *
389      * @param prop Property ID to get
390      * @param area Area of the property to get
391      */
getFloatProperty(int prop, int area)392     public float getFloatProperty(int prop, int area) {
393         CarPropertyValue<Float> carProp = getProperty(Float.class, prop, area);
394         return carProp != null ? carProp.getValue() : 0f;
395     }
396 
397     /**
398      * Returns value of a integer property
399      *
400      * @param prop Property ID to get
401      * @param area Zone of the property to get
402      */
getIntProperty(int prop, int area)403     public int getIntProperty(int prop, int area) {
404         CarPropertyValue<Integer> carProp = getProperty(Integer.class, prop, area);
405         return carProp != null ? carProp.getValue() : 0;
406     }
407 
408     /**
409      * Returns value of a integer array property
410      *
411      * @param prop Property ID to get
412      * @param area Zone of the property to get
413      */
414     @NonNull
getIntArrayProperty(int prop, int area)415     public int[] getIntArrayProperty(int prop, int area) {
416         CarPropertyValue<Integer[]> carProp = getProperty(Integer[].class, prop, area);
417         return carProp != null ? toIntArray(carProp.getValue()) : new int[0];
418     }
419 
toIntArray(Integer[] input)420     private static int[] toIntArray(Integer[] input) {
421         int len = input.length;
422         int[] arr = new int[len];
423         for (int i = 0; i < len; i++) {
424             arr[i] = input[i];
425         }
426         return arr;
427     }
428 
429     /**
430      * Return CarPropertyValue
431      *
432      * @param clazz The class object for the CarPropertyValue
433      * @param propId Property ID to get
434      * @param areaId Zone of the property to get
435      * @throws IllegalArgumentException if there is invalid property type.
436      * @return CarPropertyValue. Null if property's id is invalid.
437      */
438     @SuppressWarnings("unchecked")
439     @Nullable
getProperty(@onNull Class<E> clazz, int propId, int areaId)440     public <E> CarPropertyValue<E> getProperty(@NonNull Class<E> clazz, int propId, int areaId) {
441         if (DBG) {
442             Log.d(TAG, "getProperty, propId: 0x" + toHexString(propId)
443                     + ", areaId: 0x" + toHexString(areaId) + ", class: " + clazz);
444         }
445         try {
446             CarPropertyValue<E> propVal = mService.getProperty(propId, areaId);
447             if (propVal != null && propVal.getValue() != null) {
448                 Class<?> actualClass = propVal.getValue().getClass();
449                 if (actualClass != clazz) {
450                     throw new IllegalArgumentException("Invalid property type. " + "Expected: "
451                             + clazz + ", but was: " + actualClass);
452                 }
453             }
454             return propVal;
455         } catch (RemoteException e) {
456             return handleRemoteExceptionFromCarService(e, null);
457         }
458     }
459 
460     /**
461      * Query CarPropertyValue with property id and areaId.
462      * @param propId Property Id
463      * @param areaId areaId
464      * @param <E>
465      * @return CarPropertyValue. Null if property's id is invalid.
466      */
467     @Nullable
getProperty(int propId, int areaId)468     public <E> CarPropertyValue<E> getProperty(int propId, int areaId) {
469         try {
470             CarPropertyValue<E> propVal = mService.getProperty(propId, areaId);
471             return propVal;
472         } catch (RemoteException e) {
473             return handleRemoteExceptionFromCarService(e, null);
474         }
475     }
476 
477     /**
478      * Set value of car property by areaId.
479      * @param clazz The class object for the CarPropertyValue
480      * @param propId Property ID
481      * @param areaId areaId
482      * @param val Value of CarPropertyValue
483      * @param <E> data type of the given property, for example property that was
484      * defined as {@code VEHICLE_VALUE_TYPE_INT32} in vehicle HAL could be accessed using
485      * {@code Integer.class}
486      */
setProperty(@onNull Class<E> clazz, int propId, int areaId, @NonNull E val)487     public <E> void setProperty(@NonNull Class<E> clazz, int propId, int areaId, @NonNull E val) {
488         if (DBG) {
489             Log.d(TAG, "setProperty, propId: 0x" + toHexString(propId)
490                     + ", areaId: 0x" + toHexString(areaId) + ", class: " + clazz + ", val: " + val);
491         }
492         try {
493             mService.setProperty(new CarPropertyValue<>(propId, areaId, val));
494         } catch (RemoteException e) {
495             handleRemoteExceptionFromCarService(e);
496         }
497     }
498 
499     /**
500      * Modifies a property.  If the property modification doesn't occur, an error event shall be
501      * generated and propagated back to the application.
502      *
503      * @param prop Property ID to modify
504      * @param areaId AreaId to apply the modification.
505      * @param val Value to set
506      */
setBooleanProperty(int prop, int areaId, boolean val)507     public void setBooleanProperty(int prop, int areaId, boolean val) {
508         setProperty(Boolean.class, prop, areaId, val);
509     }
510 
511     /**
512      * Set float value of property
513      *
514      * @param prop Property ID to modify
515      * @param areaId AreaId to apply the modification
516      * @param val Value to set
517      */
setFloatProperty(int prop, int areaId, float val)518     public void setFloatProperty(int prop, int areaId, float val) {
519         setProperty(Float.class, prop, areaId, val);
520     }
521 
522     /**
523      * Set int value of property
524      *
525      * @param prop Property ID to modify
526      * @param areaId AreaId to apply the modification
527      * @param val Value to set
528      */
setIntProperty(int prop, int areaId, int val)529     public void setIntProperty(int prop, int areaId, int val) {
530         setProperty(Integer.class, prop, areaId, val);
531     }
532 
533 
534     private class CarPropertyListeners extends CarRatedFloatListeners<CarPropertyEventCallback> {
CarPropertyListeners(float rate)535         CarPropertyListeners(float rate) {
536             super(rate);
537         }
onPropertyChanged(final CarPropertyEvent event)538         void onPropertyChanged(final CarPropertyEvent event) {
539             // throw away old sensor data as oneway binder call can change order.
540             long updateTime = event.getCarPropertyValue().getTimestamp();
541             int areaId = event.getCarPropertyValue().getAreaId();
542             if (!needUpdateForAreaId(areaId, updateTime)) {
543                 Log.w(TAG, "dropping old property data");
544                 return;
545             }
546             List<CarPropertyEventCallback> listeners;
547             synchronized (mActivePropertyListener) {
548                 listeners = new ArrayList<>(getListeners());
549             }
550             listeners.forEach(new Consumer<CarPropertyEventCallback>() {
551                 @Override
552                 public void accept(CarPropertyEventCallback listener) {
553                     if (needUpdateForSelectedListener(listener, updateTime)) {
554                         listener.onChangeEvent(event.getCarPropertyValue());
555                     }
556                 }
557             });
558         }
559 
onErrorEvent(final CarPropertyEvent event)560         void onErrorEvent(final CarPropertyEvent event) {
561             List<CarPropertyEventCallback> listeners;
562             CarPropertyValue value = event.getCarPropertyValue();
563             synchronized (mActivePropertyListener) {
564                 listeners = new ArrayList<>(getListeners());
565             }
566             listeners.forEach(new Consumer<CarPropertyEventCallback>() {
567                 @Override
568                 public void accept(CarPropertyEventCallback listener) {
569                     if (DBG) {
570                         Log.d(TAG, new StringBuilder().append("onErrorEvent for ")
571                                         .append("property: ").append(value.getPropertyId())
572                                         .append(" areaId: ").append(value.getAreaId())
573                                         .toString());
574                     }
575                     listener.onErrorEvent(value.getPropertyId(), value.getAreaId());
576                 }
577             });
578         }
579     }
580 
581     /** @hide */
582     @Override
onCarDisconnected()583     public void onCarDisconnected() {
584         synchronized (mActivePropertyListener) {
585             mActivePropertyListener.clear();
586             mCarPropertyEventToService = null;
587         }
588     }
589 }
590