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 com.android.car;
18 
19 import static java.lang.Integer.toHexString;
20 
21 import android.annotation.NonNull;
22 import android.annotation.Nullable;
23 import android.car.Car;
24 import android.car.hardware.CarPropertyConfig;
25 import android.car.hardware.CarPropertyValue;
26 import android.car.hardware.property.CarPropertyEvent;
27 import android.car.hardware.property.ICarProperty;
28 import android.car.hardware.property.ICarPropertyEventListener;
29 import android.content.Context;
30 import android.os.Handler;
31 import android.os.HandlerThread;
32 import android.os.IBinder;
33 import android.os.RemoteException;
34 import android.util.IndentingPrintWriter;
35 import android.util.Pair;
36 import android.util.Slog;
37 import android.util.SparseArray;
38 
39 import com.android.car.hal.PropertyHalService;
40 import com.android.internal.annotations.GuardedBy;
41 
42 import java.util.ArrayList;
43 import java.util.HashMap;
44 import java.util.HashSet;
45 import java.util.LinkedList;
46 import java.util.List;
47 import java.util.Map;
48 import java.util.Set;
49 import java.util.concurrent.ConcurrentHashMap;
50 import java.util.concurrent.CopyOnWriteArrayList;
51 
52 /**
53  * This class implements the binder interface for ICarProperty.aidl to make it easier to create
54  * multiple managers that deal with Vehicle Properties. The property Ids in this class are IDs in
55  * manager level.
56  */
57 public class CarPropertyService extends ICarProperty.Stub
58         implements CarServiceBase, PropertyHalService.PropertyHalListener {
59     private static final boolean DBG = true;
60     private static final String TAG = CarLog.tagFor(CarPropertyService.class);
61     private final Context mContext;
62     private final Map<IBinder, Client> mClientMap = new ConcurrentHashMap<>();
63     private final PropertyHalService mHal;
64     private boolean mListenerIsSet = false;
65     private final Map<Integer, List<Client>> mPropIdClientMap = new ConcurrentHashMap<>();
66     private final Object mLock = new Object();
67     @GuardedBy("mLock")
68     private final SparseArray<SparseArray<Client>> mSetOperationClientMap = new SparseArray<>();
69     private final HandlerThread mHandlerThread =
70             CarServiceUtils.getHandlerThread(getClass().getSimpleName());
71     private final Handler mHandler = new Handler(mHandlerThread.getLooper());
72     // Use SparseArray instead of map to save memory.
73     @GuardedBy("mLock")
74     private SparseArray<CarPropertyConfig<?>> mConfigs = new SparseArray<>();
75     @GuardedBy("mLock")
76     private SparseArray<Pair<String, String>> mPropToPermission = new SparseArray<>();
77 
CarPropertyService(Context context, PropertyHalService hal)78     public CarPropertyService(Context context, PropertyHalService hal) {
79         if (DBG) {
80             Slog.d(TAG, "CarPropertyService started!");
81         }
82         mHal = hal;
83         mContext = context;
84     }
85 
86     // Helper class to keep track of listeners to this service
87     private class Client implements IBinder.DeathRecipient {
88         private final ICarPropertyEventListener mListener;
89         private final IBinder mListenerBinder;
90         private final SparseArray<Float> mRateMap = new SparseArray<Float>();   // key is propId
91 
Client(ICarPropertyEventListener listener)92         Client(ICarPropertyEventListener listener) {
93             mListener = listener;
94             mListenerBinder = listener.asBinder();
95 
96             try {
97                 mListenerBinder.linkToDeath(this, 0);
98             } catch (RemoteException e) {
99                 throw new IllegalStateException("Client already dead", e);
100             }
101             mClientMap.put(mListenerBinder, this);
102         }
103 
addProperty(int propId, float rate)104         void addProperty(int propId, float rate) {
105             mRateMap.put(propId, rate);
106         }
107 
108         /**
109          * Client died. Remove the listener from HAL service and unregister if this is the last
110          * client.
111          */
112         @Override
binderDied()113         public void binderDied() {
114             if (DBG) {
115                 Slog.d(TAG, "binderDied " + mListenerBinder);
116             }
117 
118             for (int i = 0; i < mRateMap.size(); i++) {
119                 int propId = mRateMap.keyAt(i);
120                 CarPropertyService.this.unregisterListenerBinderLocked(propId, mListenerBinder);
121             }
122             this.release();
123         }
124 
getListener()125         ICarPropertyEventListener getListener() {
126             return mListener;
127         }
128 
getListenerBinder()129         IBinder getListenerBinder() {
130             return mListenerBinder;
131         }
132 
getRate(int propId)133         float getRate(int propId) {
134             // Return 0 if no key found, since that is the slowest rate.
135             return mRateMap.get(propId, (float) 0);
136         }
137 
release()138         void release() {
139             mListenerBinder.unlinkToDeath(this, 0);
140             mClientMap.remove(mListenerBinder);
141         }
142 
removeProperty(int propId)143         void removeProperty(int propId) {
144             mRateMap.remove(propId);
145             if (mRateMap.size() == 0) {
146                 // Last property was released, remove the client.
147                 this.release();
148             }
149         }
150     }
151 
152     @Override
init()153     public void init() {
154         synchronized (mLock) {
155             // Cache the configs list and permissions to avoid subsequent binder calls
156             mConfigs = mHal.getPropertyList();
157             mPropToPermission = mHal.getPermissionsForAllProperties();
158         }
159         if (DBG) {
160             Slog.d(TAG, "cache CarPropertyConfigs " + mConfigs.size());
161         }
162     }
163 
164     @Override
release()165     public void release() {
166         for (Client c : mClientMap.values()) {
167             c.release();
168         }
169         mClientMap.clear();
170         mPropIdClientMap.clear();
171         mHal.setListener(null);
172         mListenerIsSet = false;
173         synchronized (mLock) {
174             mSetOperationClientMap.clear();
175         }
176     }
177 
178     @Override
dump(IndentingPrintWriter writer)179     public void dump(IndentingPrintWriter writer) {
180         writer.println("*CarPropertyService*");
181         writer.increaseIndent();
182         synchronized (mLock) {
183             writer.println(String.format("Listener is set for PropertyHalService: %s",
184                     mListenerIsSet));
185             writer.println(String.format("There are %d clients using CarPropertyService.",
186                     mClientMap.size()));
187             writer.println("Properties registered: ");
188             writer.increaseIndent();
189             for (int propId : mPropIdClientMap.keySet()) {
190                 writer.println("propId: 0x" + toHexString(propId)
191                         + " is registered by " + mPropIdClientMap.get(propId).size()
192                         + " client(s).");
193             }
194             writer.decreaseIndent();
195             writer.println("Properties changed by CarPropertyService: ");
196             writer.increaseIndent();
197             for (int i = 0; i < mSetOperationClientMap.size(); i++) {
198                 int propId = mSetOperationClientMap.keyAt(i);
199                 SparseArray areaIdToClient = mSetOperationClientMap.valueAt(i);
200                 for (int j = 0; j < areaIdToClient.size(); j++) {
201                     int areaId = areaIdToClient.keyAt(j);
202                     writer.println(String.format("propId: 0x%s areaId: 0x%s by client: %s",
203                             toHexString(propId), toHexString(areaId), areaIdToClient.valueAt(j)));
204                 }
205             }
206             writer.decreaseIndent();
207         }
208         writer.decreaseIndent();
209     }
210 
211     @Override
registerListener(int propId, float rate, ICarPropertyEventListener listener)212     public void registerListener(int propId, float rate, ICarPropertyEventListener listener) {
213         if (DBG) {
214             Slog.d(TAG, "registerListener: propId=0x" + toHexString(propId) + " rate=" + rate);
215         }
216         if (listener == null) {
217             Slog.e(TAG, "registerListener: Listener is null.");
218             throw new IllegalArgumentException("listener cannot be null.");
219         }
220 
221         IBinder listenerBinder = listener.asBinder();
222         CarPropertyConfig propertyConfig;
223         Client finalClient;
224         synchronized (mLock) {
225             propertyConfig = mConfigs.get(propId);
226             if (propertyConfig == null) {
227                 // Do not attempt to register an invalid propId
228                 Slog.e(TAG, "registerListener:  propId is not in config list: 0x" + toHexString(
229                         propId));
230                 return;
231             }
232             ICarImpl.assertPermission(mContext, mHal.getReadPermission(propId));
233             // Get or create the client for this listener
234             Client client = mClientMap.get(listenerBinder);
235             if (client == null) {
236                 client = new Client(listener);
237             }
238             client.addProperty(propId, rate);
239             // Insert the client into the propId --> clients map
240             List<Client> clients = mPropIdClientMap.get(propId);
241             if (clients == null) {
242                 clients = new CopyOnWriteArrayList<Client>();
243                 mPropIdClientMap.put(propId, clients);
244             }
245             if (!clients.contains(client)) {
246                 clients.add(client);
247             }
248             // Set the HAL listener if necessary
249             if (!mListenerIsSet) {
250                 mHal.setListener(this);
251             }
252             // Set the new rate
253             if (rate > mHal.getSampleRate(propId)) {
254                 mHal.subscribeProperty(propId, rate);
255             }
256             finalClient = client;
257         }
258 
259         // propertyConfig and client are NonNull.
260         mHandler.post(() ->
261                 getAndDispatchPropertyInitValue(propertyConfig, finalClient));
262     }
263 
getAndDispatchPropertyInitValue(CarPropertyConfig config, Client client)264     private void getAndDispatchPropertyInitValue(CarPropertyConfig config, Client client) {
265         List<CarPropertyEvent> events = new LinkedList<>();
266         int propId = config.getPropertyId();
267         if (config.isGlobalProperty()) {
268             CarPropertyValue value = mHal.getPropertySafe(propId, 0);
269             if (value != null) {
270                 CarPropertyEvent event = new CarPropertyEvent(
271                         CarPropertyEvent.PROPERTY_EVENT_PROPERTY_CHANGE, value);
272                 events.add(event);
273             }
274         } else {
275             for (int areaId : config.getAreaIds()) {
276                 CarPropertyValue value = mHal.getPropertySafe(propId, areaId);
277                 if (value != null) {
278                     CarPropertyEvent event = new CarPropertyEvent(
279                             CarPropertyEvent.PROPERTY_EVENT_PROPERTY_CHANGE, value);
280                     events.add(event);
281                 }
282             }
283         }
284         try {
285             client.getListener().onEvent(events);
286         } catch (RemoteException ex) {
287             // If we cannot send a record, its likely the connection snapped. Let the binder
288             // death handle the situation.
289             Slog.e(TAG, "onEvent calling failed: " + ex);
290         }
291     }
292 
293     @Override
unregisterListener(int propId, ICarPropertyEventListener listener)294     public void unregisterListener(int propId, ICarPropertyEventListener listener) {
295         if (DBG) {
296             Slog.d(TAG, "unregisterListener propId=0x" + toHexString(propId));
297         }
298         ICarImpl.assertPermission(mContext, mHal.getReadPermission(propId));
299         if (listener == null) {
300             Slog.e(TAG, "unregisterListener: Listener is null.");
301             throw new IllegalArgumentException("Listener is null");
302         }
303 
304         IBinder listenerBinder = listener.asBinder();
305         synchronized (mLock) {
306             unregisterListenerBinderLocked(propId, listenerBinder);
307         }
308     }
309 
unregisterListenerBinderLocked(int propId, IBinder listenerBinder)310     private void unregisterListenerBinderLocked(int propId, IBinder listenerBinder) {
311         Client client = mClientMap.get(listenerBinder);
312         List<Client> propertyClients = mPropIdClientMap.get(propId);
313         synchronized (mLock) {
314             if (mConfigs.get(propId) == null) {
315                 // Do not attempt to register an invalid propId
316                 Slog.e(TAG, "unregisterListener: propId is not in config list:0x" + toHexString(
317                         propId));
318                 return;
319             }
320         }
321         if ((client == null) || (propertyClients == null)) {
322             Slog.e(TAG, "unregisterListenerBinderLocked: Listener was not previously registered.");
323         } else {
324             if (propertyClients.remove(client)) {
325                 client.removeProperty(propId);
326                 clearSetOperationRecorderLocked(propId, client);
327             } else {
328                 Slog.e(TAG, "unregisterListenerBinderLocked: Listener was not registered for "
329                            + "propId=0x" + toHexString(propId));
330             }
331 
332             if (propertyClients.isEmpty()) {
333                 // Last listener for this property unsubscribed.  Clean up
334                 mHal.unsubscribeProperty(propId);
335                 mPropIdClientMap.remove(propId);
336                 mSetOperationClientMap.remove(propId);
337                 if (mPropIdClientMap.isEmpty()) {
338                     // No more properties are subscribed.  Turn off the listener.
339                     mHal.setListener(null);
340                     mListenerIsSet = false;
341                 }
342             } else {
343                 // Other listeners are still subscribed.  Calculate the new rate
344                 float maxRate = 0;
345                 for (Client c : propertyClients) {
346                     float rate = c.getRate(propId);
347                     if (rate > maxRate) {
348                         maxRate = rate;
349                     }
350                 }
351                 // Set the new rate
352                 mHal.subscribeProperty(propId, maxRate);
353             }
354         }
355     }
356 
357     /**
358      * Return the list of properties' configs that the caller may access.
359      */
360     @NonNull
361     @Override
getPropertyList()362     public List<CarPropertyConfig> getPropertyList() {
363         int[] allPropId;
364         // Avoid permission checking under lock.
365         synchronized (mLock) {
366             allPropId = new int[mConfigs.size()];
367             for (int i = 0; i < mConfigs.size(); i++) {
368                 allPropId[i] = mConfigs.keyAt(i);
369             }
370         }
371         return getPropertyConfigList(allPropId);
372     }
373 
374     /**
375      *
376      * @param propIds Array of property Ids
377      * @return the list of properties' configs that the caller may access.
378      */
379     @NonNull
380     @Override
getPropertyConfigList(int[] propIds)381     public List<CarPropertyConfig> getPropertyConfigList(int[] propIds) {
382         // Cache the granted permissions
383         Set<String> grantedPermission = new HashSet<>();
384         List<CarPropertyConfig> availableProp = new ArrayList<>();
385         if (propIds == null) {
386             return availableProp;
387         }
388         for (int propId : propIds) {
389             String readPermission = getReadPermission(propId);
390             if (readPermission == null) {
391                 continue;
392             }
393             // Check if context already granted permission first
394             if (grantedPermission.contains(readPermission)
395                     || ICarImpl.hasPermission(mContext, readPermission)) {
396                 grantedPermission.add(readPermission);
397                 synchronized (mLock) {
398                     availableProp.add(mConfigs.get(propId));
399                 }
400             }
401         }
402         if (DBG) {
403             Slog.d(TAG, "getPropertyList returns " + availableProp.size() + " configs");
404         }
405         return availableProp;
406     }
407 
408     @Override
getProperty(int prop, int zone)409     public CarPropertyValue getProperty(int prop, int zone) {
410         synchronized (mLock) {
411             if (mConfigs.get(prop) == null) {
412                 // Do not attempt to register an invalid propId
413                 Slog.e(TAG, "getProperty: propId is not in config list:0x" + toHexString(prop));
414                 return null;
415             }
416         }
417         ICarImpl.assertPermission(mContext, mHal.getReadPermission(prop));
418         return mHal.getProperty(prop, zone);
419     }
420 
421     /**
422      * Get property value for car service's internal usage.
423      * @param prop property id
424      * @param zone area id
425      * @return null if property is not implemented or there is an exception in the vehicle.
426      */
getPropertySafe(int prop, int zone)427     public CarPropertyValue getPropertySafe(int prop, int zone) {
428         synchronized (mLock) {
429             if (mConfigs.get(prop) == null) {
430                 // Do not attempt to register an invalid propId
431                 Slog.e(TAG, "getPropertySafe: propId is not in config list:0x"
432                         + toHexString(prop));
433                 return null;
434             }
435         }
436         ICarImpl.assertPermission(mContext, mHal.getReadPermission(prop));
437         return mHal.getPropertySafe(prop, zone);
438     }
439 
440     @Nullable
441     @Override
getReadPermission(int propId)442     public String getReadPermission(int propId) {
443         Pair<String, String> permissions;
444         synchronized (mLock) {
445             permissions = mPropToPermission.get(propId);
446         }
447         if (permissions == null) {
448             // Property ID does not exist
449             Slog.e(TAG,
450                     "getReadPermission: propId is not in config list:0x" + toHexString(propId));
451             return null;
452         }
453         return permissions.first;
454     }
455 
456     @Nullable
457     @Override
getWritePermission(int propId)458     public String getWritePermission(int propId) {
459         Pair<String, String> permissions;
460         synchronized (mLock) {
461             permissions = mPropToPermission.get(propId);
462         }
463         if (permissions == null) {
464             // Property ID does not exist
465             Slog.e(TAG,
466                     "getWritePermission: propId is not in config list:0x" + toHexString(propId));
467             return null;
468         }
469         return permissions.second;
470     }
471 
472     @Override
setProperty(CarPropertyValue prop, ICarPropertyEventListener listener)473     public void setProperty(CarPropertyValue prop, ICarPropertyEventListener listener) {
474         int propId = prop.getPropertyId();
475         checkPropertyAccessibility(propId);
476         // need an extra permission for writing display units properties.
477         if (mHal.isDisplayUnitsProperty(propId)) {
478             ICarImpl.assertPermission(mContext, Car.PERMISSION_VENDOR_EXTENSION);
479         }
480         mHal.setProperty(prop);
481         IBinder listenerBinder = listener.asBinder();
482         synchronized (mLock) {
483             Client client = mClientMap.get(listenerBinder);
484             if (client == null) {
485                 client = new Client(listener);
486             }
487             updateSetOperationRecorder(propId, prop.getAreaId(), client);
488         }
489     }
490 
491     // The helper method checks if the vehicle has implemented this property and the property
492     // is accessible or not for platform and client.
checkPropertyAccessibility(int propId)493     private void checkPropertyAccessibility(int propId) {
494         // Checks if the car implemented the property or not.
495         synchronized (mLock) {
496             if (mConfigs.get(propId) == null) {
497                 throw new IllegalArgumentException("Property Id: 0x" + Integer.toHexString(propId)
498                         + " does not exist in the vehicle");
499             }
500         }
501 
502         // Checks if android has permission to write property.
503         String propertyWritePermission = mHal.getWritePermission(propId);
504         if (propertyWritePermission == null) {
505             throw new SecurityException("Platform does not have permission to change value for "
506                     + "property Id: 0x" + Integer.toHexString(propId));
507         }
508         // Checks if the client has the permission.
509         ICarImpl.assertPermission(mContext, propertyWritePermission);
510     }
511 
512     // Updates recorder for set operation.
updateSetOperationRecorder(int propId, int areaId, Client client)513     private void updateSetOperationRecorder(int propId, int areaId, Client client) {
514         if (mSetOperationClientMap.get(propId) != null) {
515             mSetOperationClientMap.get(propId).put(areaId, client);
516         } else {
517             SparseArray<Client> areaIdToClient = new SparseArray<>();
518             areaIdToClient.put(areaId, client);
519             mSetOperationClientMap.put(propId, areaIdToClient);
520         }
521     }
522 
523     // Clears map when client unregister for property.
clearSetOperationRecorderLocked(int propId, Client client)524     private void clearSetOperationRecorderLocked(int propId, Client client) {
525         SparseArray<Client> areaIdToClient = mSetOperationClientMap.get(propId);
526         if (areaIdToClient != null) {
527             List<Integer> indexNeedToRemove = new ArrayList<>();
528             for (int index = 0; index < areaIdToClient.size(); index++) {
529                 if (client.equals(areaIdToClient.valueAt(index))) {
530                     indexNeedToRemove.add(index);
531                 }
532             }
533 
534             for (int index : indexNeedToRemove) {
535                 if (DBG) {
536                     Slog.d("ErrorEvent", " Clear propId:0x" + toHexString(propId)
537                             + " areaId: 0x" + toHexString(areaIdToClient.keyAt(index)));
538                 }
539                 areaIdToClient.removeAt(index);
540             }
541         }
542     }
543 
544     // Implement PropertyHalListener interface
545     @Override
onPropertyChange(List<CarPropertyEvent> events)546     public void onPropertyChange(List<CarPropertyEvent> events) {
547         Map<IBinder, Pair<ICarPropertyEventListener, List<CarPropertyEvent>>> eventsToDispatch =
548                 new HashMap<>();
549 
550         for (CarPropertyEvent event : events) {
551             int propId = event.getCarPropertyValue().getPropertyId();
552             List<Client> clients = mPropIdClientMap.get(propId);
553             if (clients == null) {
554                 Slog.e(TAG, "onPropertyChange: no listener registered for propId=0x"
555                         + toHexString(propId));
556                 continue;
557             }
558 
559             for (Client c : clients) {
560                 IBinder listenerBinder = c.getListenerBinder();
561                 Pair<ICarPropertyEventListener, List<CarPropertyEvent>> p =
562                         eventsToDispatch.get(listenerBinder);
563                 if (p == null) {
564                     // Initialize the linked list for the listener
565                     p = new Pair<>(c.getListener(), new LinkedList<CarPropertyEvent>());
566                     eventsToDispatch.put(listenerBinder, p);
567                 }
568                 p.second.add(event);
569             }
570         }
571         // Parse the dispatch list to send events
572         for (Pair<ICarPropertyEventListener, List<CarPropertyEvent>> p: eventsToDispatch.values()) {
573             try {
574                 p.first.onEvent(p.second);
575             } catch (RemoteException ex) {
576                 // If we cannot send a record, its likely the connection snapped. Let binder
577                 // death handle the situation.
578                 Slog.e(TAG, "onEvent calling failed: " + ex);
579             }
580         }
581     }
582 
583     @Override
onPropertySetError(int property, int areaId, int errorCode)584     public void onPropertySetError(int property, int areaId, int errorCode) {
585         Client lastOperatedClient = null;
586         synchronized (mLock) {
587             if (mSetOperationClientMap.get(property) != null
588                     && mSetOperationClientMap.get(property).get(areaId) != null) {
589                 lastOperatedClient = mSetOperationClientMap.get(property).get(areaId);
590             } else {
591                 Slog.e(TAG, "Can not find the client changed propertyId: 0x"
592                         + toHexString(property) + " in areaId: 0x" + toHexString(areaId));
593             }
594 
595         }
596         if (lastOperatedClient != null) {
597             dispatchToLastClient(property, areaId, errorCode, lastOperatedClient);
598         }
599     }
600 
dispatchToLastClient(int property, int areaId, int errorCode, Client lastOperatedClient)601     private void dispatchToLastClient(int property, int areaId, int errorCode,
602             Client lastOperatedClient) {
603         try {
604             List<CarPropertyEvent> eventList = new LinkedList<>();
605             eventList.add(
606                     CarPropertyEvent.createErrorEventWithErrorCode(property, areaId,
607                             errorCode));
608             lastOperatedClient.getListener().onEvent(eventList);
609         } catch (RemoteException ex) {
610             Slog.e(TAG, "onEvent calling failed: " + ex);
611         }
612     }
613 }
614