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
17package com.android.car;
18
19import static java.lang.Integer.toHexString;
20
21import android.car.Car;
22import android.car.hardware.CarPropertyConfig;
23import android.car.hardware.CarPropertyValue;
24import android.car.hardware.property.CarPropertyEvent;
25import android.car.hardware.property.ICarProperty;
26import android.car.hardware.property.ICarPropertyEventListener;
27import android.content.Context;
28import android.os.IBinder;
29import android.os.RemoteException;
30import android.util.Log;
31import android.util.Pair;
32import android.util.SparseArray;
33
34import com.android.car.hal.PropertyHalService;
35
36import java.io.PrintWriter;
37import java.util.ArrayList;
38import java.util.HashMap;
39import java.util.LinkedList;
40import java.util.List;
41import java.util.Map;
42import java.util.concurrent.ConcurrentHashMap;
43import java.util.concurrent.CopyOnWriteArrayList;
44
45/**
46 * This class implements the binder interface for ICarProperty.aidl to make it easier to create
47 * multiple managers that deal with Vehicle Properties. To create a new service, simply extend
48 * this class and call the super() constructor with the appropriate arguments for the new service.
49 * {@link CarHvacService} shows the basic usage.
50 */
51public class CarPropertyService extends ICarProperty.Stub
52        implements CarServiceBase, PropertyHalService.PropertyHalListener {
53    private static final boolean DBG = true;
54    private static final String TAG = "Property.service";
55    private final Context mContext;
56    private final Map<IBinder, Client> mClientMap = new ConcurrentHashMap<>();
57    private Map<Integer, CarPropertyConfig<?>> mConfigs;
58    private final PropertyHalService mHal;
59    private boolean mListenerIsSet = false;
60    private final Map<Integer, List<Client>> mPropIdClientMap = new ConcurrentHashMap<>();
61    private final Object mLock = new Object();
62
63    public CarPropertyService(Context context, PropertyHalService hal) {
64        if (DBG) {
65            Log.d(TAG, "CarPropertyService started!");
66        }
67        mHal = hal;
68        mContext = context;
69    }
70
71    // Helper class to keep track of listeners to this service
72    private class Client implements IBinder.DeathRecipient {
73        private final ICarPropertyEventListener mListener;
74        private final IBinder mListenerBinder;
75        private final SparseArray<Float> mRateMap = new SparseArray<Float>();   // key is propId
76
77        Client(ICarPropertyEventListener listener) {
78            mListener = listener;
79            mListenerBinder = listener.asBinder();
80
81            try {
82                mListenerBinder.linkToDeath(this, 0);
83            } catch (RemoteException e) {
84                Log.e(TAG, "Failed to link death for recipient. " + e);
85                throw new IllegalStateException(Car.CAR_NOT_CONNECTED_EXCEPTION_MSG);
86            }
87            mClientMap.put(mListenerBinder, this);
88        }
89
90        void addProperty(int propId, float rate) {
91            mRateMap.put(propId, rate);
92        }
93
94        /**
95         * Client died. Remove the listener from HAL service and unregister if this is the last
96         * client.
97         */
98        @Override
99        public void binderDied() {
100            if (DBG) {
101                Log.d(TAG, "binderDied " + mListenerBinder);
102            }
103
104            for (int i = 0; i < mRateMap.size(); i++) {
105                int propId = mRateMap.keyAt(i);
106                CarPropertyService.this.unregisterListenerBinderLocked(propId, mListenerBinder);
107            }
108            this.release();
109        }
110
111        ICarPropertyEventListener getListener() {
112            return mListener;
113        }
114
115        IBinder getListenerBinder() {
116            return mListenerBinder;
117        }
118
119        float getRate(int propId) {
120            // Return 0 if no key found, since that is the slowest rate.
121            return mRateMap.get(propId, (float) 0);
122        }
123
124        void release() {
125            mListenerBinder.unlinkToDeath(this, 0);
126            mClientMap.remove(mListenerBinder);
127        }
128
129        void removeProperty(int propId) {
130            mRateMap.remove(propId);
131            if (mRateMap.size() == 0) {
132                // Last property was released, remove the client.
133                this.release();
134            }
135        }
136    }
137
138    @Override
139    public void init() {
140    }
141
142    @Override
143    public void release() {
144        for (Client c : mClientMap.values()) {
145            c.release();
146        }
147        mClientMap.clear();
148        mPropIdClientMap.clear();
149        mHal.setListener(null);
150        mListenerIsSet = false;
151    }
152
153    @Override
154    public void dump(PrintWriter writer) {
155    }
156
157    @Override
158    public void registerListener(int propId, float rate, ICarPropertyEventListener listener) {
159        if (DBG) {
160            Log.d(TAG, "registerListener: propId=0x" + toHexString(propId) + " rate=" + rate);
161        }
162        if (mConfigs.get(propId) == null) {
163            // Do not attempt to register an invalid propId
164            Log.e(TAG, "registerListener:  propId is not in config list:  " + propId);
165            return;
166        }
167        ICarImpl.assertPermission(mContext, mHal.getReadPermission(propId));
168        if (listener == null) {
169            Log.e(TAG, "registerListener: Listener is null.");
170            throw new IllegalArgumentException("listener cannot be null.");
171        }
172
173        IBinder listenerBinder = listener.asBinder();
174
175        synchronized (mLock) {
176            // Get the client for this listener
177            Client client = mClientMap.get(listenerBinder);
178            if (client == null) {
179                client = new Client(listener);
180            }
181            client.addProperty(propId, rate);
182            // Insert the client into the propId --> clients map
183            List<Client> clients = mPropIdClientMap.get(propId);
184            if (clients == null) {
185                clients = new CopyOnWriteArrayList<Client>();
186                mPropIdClientMap.put(propId, clients);
187            }
188            if (!clients.contains(client)) {
189                clients.add(client);
190            }
191            // Set the HAL listener if necessary
192            if (!mListenerIsSet) {
193                mHal.setListener(this);
194            }
195            // Set the new rate
196            if (rate > mHal.getSampleRate(propId)) {
197                mHal.subscribeProperty(propId, rate);
198            }
199        }
200
201        // Send the latest value(s) to the registering listener only
202        List<CarPropertyEvent> events = new LinkedList<CarPropertyEvent>();
203        for (int areaId : mConfigs.get(propId).getAreaIds()) {
204            CarPropertyValue value = mHal.getProperty(propId, areaId);
205            CarPropertyEvent event = new CarPropertyEvent(
206                    CarPropertyEvent.PROPERTY_EVENT_PROPERTY_CHANGE, value);
207            events.add(event);
208        }
209        try {
210            listener.onEvent(events);
211        } catch (RemoteException ex) {
212            // If we cannot send a record, its likely the connection snapped. Let the binder
213            // death handle the situation.
214            Log.e(TAG, "onEvent calling failed: " + ex);
215        }
216    }
217
218    @Override
219    public void unregisterListener(int propId, ICarPropertyEventListener listener) {
220        if (DBG) {
221            Log.d(TAG, "unregisterListener propId=0x" + toHexString(propId));
222        }
223        ICarImpl.assertPermission(mContext, mHal.getReadPermission(propId));
224        if (listener == null) {
225            Log.e(TAG, "unregisterListener: Listener is null.");
226            throw new IllegalArgumentException("Listener is null");
227        }
228
229        IBinder listenerBinder = listener.asBinder();
230        synchronized (mLock) {
231            unregisterListenerBinderLocked(propId, listenerBinder);
232        }
233    }
234
235    private void unregisterListenerBinderLocked(int propId, IBinder listenerBinder) {
236        Client client = mClientMap.get(listenerBinder);
237        List<Client> propertyClients = mPropIdClientMap.get(propId);
238        if (mConfigs.get(propId) == null) {
239            // Do not attempt to register an invalid propId
240            Log.e(TAG, "unregisterListener: propId is not in config list:0x" + toHexString(propId));
241            return;
242        }
243        if ((client == null) || (propertyClients == null)) {
244            Log.e(TAG, "unregisterListenerBinderLocked: Listener was not previously registered.");
245        } else {
246            if (propertyClients.remove(client)) {
247                client.removeProperty(propId);
248            } else {
249                Log.e(TAG, "unregisterListenerBinderLocked: Listener was not registered for "
250                           + "propId=0x" + toHexString(propId));
251            }
252
253            if (propertyClients.isEmpty()) {
254                // Last listener for this property unsubscribed.  Clean up
255                mHal.unsubscribeProperty(propId);
256                mPropIdClientMap.remove(propId);
257                if (mPropIdClientMap.isEmpty()) {
258                    // No more properties are subscribed.  Turn off the listener.
259                    mHal.setListener(null);
260                    mListenerIsSet = false;
261                }
262            } else {
263                // Other listeners are still subscribed.  Calculate the new rate
264                float maxRate = 0;
265                for (Client c : propertyClients) {
266                    float rate = c.getRate(propId);
267                    if (rate > maxRate) {
268                        maxRate = rate;
269                    }
270                }
271                // Set the new rate
272                mHal.subscribeProperty(propId, maxRate);
273            }
274        }
275    }
276
277    /**
278     * Return the list of properties that the caller may access.
279     */
280    @Override
281    public List<CarPropertyConfig> getPropertyList() {
282        List<CarPropertyConfig> returnList = new ArrayList<CarPropertyConfig>();
283        if (mConfigs == null) {
284            // Cache the configs list to avoid subsequent binder calls
285            mConfigs = mHal.getPropertyList();
286        }
287        for (CarPropertyConfig c : mConfigs.values()) {
288            if (ICarImpl.hasPermission(mContext, mHal.getReadPermission(c.getPropertyId()))) {
289                // Only add properties the list if the process has permissions to read it
290                returnList.add(c);
291            }
292        }
293        if (DBG) {
294            Log.d(TAG, "getPropertyList returns " + returnList.size() + " configs");
295        }
296        return returnList;
297    }
298
299    @Override
300    public CarPropertyValue getProperty(int prop, int zone) {
301        if (mConfigs.get(prop) == null) {
302            // Do not attempt to register an invalid propId
303            Log.e(TAG, "getProperty: propId is not in config list:0x" + toHexString(prop));
304            return null;
305        }
306        ICarImpl.assertPermission(mContext, mHal.getReadPermission(prop));
307        return mHal.getProperty(prop, zone);
308    }
309
310    @Override
311    public void setProperty(CarPropertyValue prop) {
312        int propId = prop.getPropertyId();
313        if (mConfigs.get(propId) == null) {
314            // Do not attempt to register an invalid propId
315            Log.e(TAG, "setProperty:  propId is not in config list:0x" + toHexString(propId));
316            return;
317        }
318        ICarImpl.assertPermission(mContext, mHal.getWritePermission(propId));
319        mHal.setProperty(prop);
320    }
321
322    // Implement PropertyHalListener interface
323    @Override
324    public void onPropertyChange(List<CarPropertyEvent> events) {
325        Map<IBinder, Pair<ICarPropertyEventListener, List<CarPropertyEvent>>> eventsToDispatch =
326                new HashMap<>();
327
328        for (CarPropertyEvent event : events) {
329            int propId = event.getCarPropertyValue().getPropertyId();
330            List<Client> clients = mPropIdClientMap.get(propId);
331            if (clients == null) {
332                Log.e(TAG, "onPropertyChange: no listener registered for propId=0x"
333                        + toHexString(propId));
334                continue;
335            }
336
337            for (Client c : clients) {
338                IBinder listenerBinder = c.getListenerBinder();
339                Pair<ICarPropertyEventListener, List<CarPropertyEvent>> p =
340                        eventsToDispatch.get(listenerBinder);
341                if (p == null) {
342                    // Initialize the linked list for the listener
343                    p = new Pair<>(c.getListener(), new LinkedList<CarPropertyEvent>());
344                    eventsToDispatch.put(listenerBinder, p);
345                }
346                p.second.add(event);
347            }
348        }
349        // Parse the dispatch list to send events
350        for (Pair<ICarPropertyEventListener, List<CarPropertyEvent>> p: eventsToDispatch.values()) {
351            try {
352                p.first.onEvent(p.second);
353            } catch (RemoteException ex) {
354                // If we cannot send a record, its likely the connection snapped. Let binder
355                // death handle the situation.
356                Log.e(TAG, "onEvent calling failed: " + ex);
357            }
358        }
359    }
360
361    @Override
362    public void onPropertySetError(int property, int area) {
363        List<Client> clients = mPropIdClientMap.get(property);
364        if (clients != null) {
365            List<CarPropertyEvent> eventList = new LinkedList<>();
366            eventList.add(createErrorEvent(property, area));
367            for (Client c : clients) {
368                try {
369                    c.getListener().onEvent(eventList);
370                } catch (RemoteException ex) {
371                    // If we cannot send a record, its likely the connection snapped. Let the binder
372                    // death handle the situation.
373                    Log.e(TAG, "onEvent calling failed: " + ex);
374                }
375            }
376        } else {
377            Log.e(TAG, "onPropertySetError called with no listener registered for propId=0x"
378                    + toHexString(property));
379        }
380    }
381
382    private static CarPropertyEvent createErrorEvent(int property, int area) {
383        return new CarPropertyEvent(CarPropertyEvent.PROPERTY_EVENT_ERROR,
384                new CarPropertyValue<>(property, area, null));
385    }
386}
387