| /* |
| * Copyright (C) 2018 The Android Open Source Project |
| * |
| * Licensed under the Apache License, Version 2.0 (the "License"); |
| * you may not use this file except in compliance with the License. |
| * You may obtain a copy of the License at |
| * |
| * http://www.apache.org/licenses/LICENSE-2.0 |
| * |
| * Unless required by applicable law or agreed to in writing, software |
| * distributed under the License is distributed on an "AS IS" BASIS, |
| * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. |
| * See the License for the specific language governing permissions and |
| * limitations under the License. |
| */ |
| |
| package com.android.car; |
| |
| import static java.lang.Integer.toHexString; |
| |
| import android.car.hardware.CarPropertyConfig; |
| import android.car.hardware.CarPropertyValue; |
| import android.car.hardware.property.CarPropertyEvent; |
| import android.car.hardware.property.ICarProperty; |
| import android.car.hardware.property.ICarPropertyEventListener; |
| import android.content.Context; |
| import android.os.IBinder; |
| import android.os.RemoteException; |
| import android.util.Log; |
| import android.util.Pair; |
| import android.util.SparseArray; |
| |
| import com.android.car.hal.PropertyHalService; |
| |
| import java.io.PrintWriter; |
| import java.util.ArrayList; |
| import java.util.HashMap; |
| import java.util.LinkedList; |
| import java.util.List; |
| import java.util.Map; |
| import java.util.concurrent.ConcurrentHashMap; |
| import java.util.concurrent.CopyOnWriteArrayList; |
| |
| /** |
| * This class implements the binder interface for ICarProperty.aidl to make it easier to create |
| * multiple managers that deal with Vehicle Properties. To create a new service, simply extend |
| * this class and call the super() constructor with the appropriate arguments for the new service. |
| * {@link CarHvacService} shows the basic usage. |
| */ |
| public class CarPropertyService extends ICarProperty.Stub |
| implements CarServiceBase, PropertyHalService.PropertyHalListener { |
| private static final boolean DBG = true; |
| private static final String TAG = "Property.service"; |
| private final Context mContext; |
| private final Map<IBinder, Client> mClientMap = new ConcurrentHashMap<>(); |
| private Map<Integer, CarPropertyConfig<?>> mConfigs; |
| private final PropertyHalService mHal; |
| private boolean mListenerIsSet = false; |
| private final Map<Integer, List<Client>> mPropIdClientMap = new ConcurrentHashMap<>(); |
| private final Object mLock = new Object(); |
| |
| public CarPropertyService(Context context, PropertyHalService hal) { |
| if (DBG) { |
| Log.d(TAG, "CarPropertyService started!"); |
| } |
| mHal = hal; |
| mContext = context; |
| } |
| |
| // Helper class to keep track of listeners to this service |
| private class Client implements IBinder.DeathRecipient { |
| private final ICarPropertyEventListener mListener; |
| private final IBinder mListenerBinder; |
| private final SparseArray<Float> mRateMap = new SparseArray<Float>(); // key is propId |
| |
| Client(ICarPropertyEventListener listener) { |
| mListener = listener; |
| mListenerBinder = listener.asBinder(); |
| |
| try { |
| mListenerBinder.linkToDeath(this, 0); |
| } catch (RemoteException e) { |
| throw new IllegalStateException("Client already dead", e); |
| } |
| mClientMap.put(mListenerBinder, this); |
| } |
| |
| void addProperty(int propId, float rate) { |
| mRateMap.put(propId, rate); |
| } |
| |
| /** |
| * Client died. Remove the listener from HAL service and unregister if this is the last |
| * client. |
| */ |
| @Override |
| public void binderDied() { |
| if (DBG) { |
| Log.d(TAG, "binderDied " + mListenerBinder); |
| } |
| |
| for (int i = 0; i < mRateMap.size(); i++) { |
| int propId = mRateMap.keyAt(i); |
| CarPropertyService.this.unregisterListenerBinderLocked(propId, mListenerBinder); |
| } |
| this.release(); |
| } |
| |
| ICarPropertyEventListener getListener() { |
| return mListener; |
| } |
| |
| IBinder getListenerBinder() { |
| return mListenerBinder; |
| } |
| |
| float getRate(int propId) { |
| // Return 0 if no key found, since that is the slowest rate. |
| return mRateMap.get(propId, (float) 0); |
| } |
| |
| void release() { |
| mListenerBinder.unlinkToDeath(this, 0); |
| mClientMap.remove(mListenerBinder); |
| } |
| |
| void removeProperty(int propId) { |
| mRateMap.remove(propId); |
| if (mRateMap.size() == 0) { |
| // Last property was released, remove the client. |
| this.release(); |
| } |
| } |
| } |
| |
| @Override |
| public void init() { |
| if (mConfigs == null) { |
| // Cache the configs list to avoid subsequent binder calls |
| mConfigs = mHal.getPropertyList(); |
| if (DBG) { |
| Log.d(TAG, "cache CarPropertyConfigs " + mConfigs.size()); |
| } |
| } |
| } |
| |
| @Override |
| public void release() { |
| for (Client c : mClientMap.values()) { |
| c.release(); |
| } |
| mClientMap.clear(); |
| mPropIdClientMap.clear(); |
| mHal.setListener(null); |
| mListenerIsSet = false; |
| } |
| |
| @Override |
| public void dump(PrintWriter writer) { |
| } |
| |
| @Override |
| public void registerListener(int propId, float rate, ICarPropertyEventListener listener) { |
| if (DBG) { |
| Log.d(TAG, "registerListener: propId=0x" + toHexString(propId) + " rate=" + rate); |
| } |
| if (mConfigs.get(propId) == null) { |
| // Do not attempt to register an invalid propId |
| Log.e(TAG, "registerListener: propId is not in config list: " + propId); |
| return; |
| } |
| ICarImpl.assertPermission(mContext, mHal.getReadPermission(propId)); |
| if (listener == null) { |
| Log.e(TAG, "registerListener: Listener is null."); |
| throw new IllegalArgumentException("listener cannot be null."); |
| } |
| |
| IBinder listenerBinder = listener.asBinder(); |
| |
| synchronized (mLock) { |
| // Get the client for this listener |
| Client client = mClientMap.get(listenerBinder); |
| if (client == null) { |
| client = new Client(listener); |
| } |
| client.addProperty(propId, rate); |
| // Insert the client into the propId --> clients map |
| List<Client> clients = mPropIdClientMap.get(propId); |
| if (clients == null) { |
| clients = new CopyOnWriteArrayList<Client>(); |
| mPropIdClientMap.put(propId, clients); |
| } |
| if (!clients.contains(client)) { |
| clients.add(client); |
| } |
| // Set the HAL listener if necessary |
| if (!mListenerIsSet) { |
| mHal.setListener(this); |
| } |
| // Set the new rate |
| if (rate > mHal.getSampleRate(propId)) { |
| mHal.subscribeProperty(propId, rate); |
| } |
| } |
| // Send the latest value(s) to the registering listener only |
| List<CarPropertyEvent> events = new LinkedList<CarPropertyEvent>(); |
| if (mConfigs.get(propId).isGlobalProperty()) { |
| CarPropertyValue value = mHal.getProperty(propId, 0); |
| // CarPropertyEvent without a CarPropertyValue can not be used by any listeners. |
| if (value != null) { |
| CarPropertyEvent event = new CarPropertyEvent( |
| CarPropertyEvent.PROPERTY_EVENT_PROPERTY_CHANGE, value); |
| events.add(event); |
| } |
| } else { |
| for (int areaId : mConfigs.get(propId).getAreaIds()) { |
| CarPropertyValue value = mHal.getProperty(propId, areaId); |
| if (value != null) { |
| CarPropertyEvent event = new CarPropertyEvent( |
| CarPropertyEvent.PROPERTY_EVENT_PROPERTY_CHANGE, value); |
| events.add(event); |
| } |
| } |
| } |
| try { |
| listener.onEvent(events); |
| } catch (RemoteException ex) { |
| // If we cannot send a record, its likely the connection snapped. Let the binder |
| // death handle the situation. |
| Log.e(TAG, "onEvent calling failed: " + ex); |
| } |
| } |
| |
| @Override |
| public void unregisterListener(int propId, ICarPropertyEventListener listener) { |
| if (DBG) { |
| Log.d(TAG, "unregisterListener propId=0x" + toHexString(propId)); |
| } |
| ICarImpl.assertPermission(mContext, mHal.getReadPermission(propId)); |
| if (listener == null) { |
| Log.e(TAG, "unregisterListener: Listener is null."); |
| throw new IllegalArgumentException("Listener is null"); |
| } |
| |
| IBinder listenerBinder = listener.asBinder(); |
| synchronized (mLock) { |
| unregisterListenerBinderLocked(propId, listenerBinder); |
| } |
| } |
| |
| private void unregisterListenerBinderLocked(int propId, IBinder listenerBinder) { |
| Client client = mClientMap.get(listenerBinder); |
| List<Client> propertyClients = mPropIdClientMap.get(propId); |
| if (mConfigs.get(propId) == null) { |
| // Do not attempt to register an invalid propId |
| Log.e(TAG, "unregisterListener: propId is not in config list:0x" + toHexString(propId)); |
| return; |
| } |
| if ((client == null) || (propertyClients == null)) { |
| Log.e(TAG, "unregisterListenerBinderLocked: Listener was not previously registered."); |
| } else { |
| if (propertyClients.remove(client)) { |
| client.removeProperty(propId); |
| } else { |
| Log.e(TAG, "unregisterListenerBinderLocked: Listener was not registered for " |
| + "propId=0x" + toHexString(propId)); |
| } |
| |
| if (propertyClients.isEmpty()) { |
| // Last listener for this property unsubscribed. Clean up |
| mHal.unsubscribeProperty(propId); |
| mPropIdClientMap.remove(propId); |
| if (mPropIdClientMap.isEmpty()) { |
| // No more properties are subscribed. Turn off the listener. |
| mHal.setListener(null); |
| mListenerIsSet = false; |
| } |
| } else { |
| // Other listeners are still subscribed. Calculate the new rate |
| float maxRate = 0; |
| for (Client c : propertyClients) { |
| float rate = c.getRate(propId); |
| if (rate > maxRate) { |
| maxRate = rate; |
| } |
| } |
| // Set the new rate |
| mHal.subscribeProperty(propId, maxRate); |
| } |
| } |
| } |
| |
| /** |
| * Return the list of properties that the caller may access. |
| */ |
| @Override |
| public List<CarPropertyConfig> getPropertyList() { |
| List<CarPropertyConfig> returnList = new ArrayList<CarPropertyConfig>(); |
| for (CarPropertyConfig c : mConfigs.values()) { |
| if (ICarImpl.hasPermission(mContext, mHal.getReadPermission(c.getPropertyId()))) { |
| // Only add properties the list if the process has permissions to read it |
| returnList.add(c); |
| } |
| } |
| if (DBG) { |
| Log.d(TAG, "getPropertyList returns " + returnList.size() + " configs"); |
| } |
| return returnList; |
| } |
| |
| @Override |
| public CarPropertyValue getProperty(int prop, int zone) { |
| if (mConfigs.get(prop) == null) { |
| // Do not attempt to register an invalid propId |
| Log.e(TAG, "getProperty: propId is not in config list:0x" + toHexString(prop)); |
| return null; |
| } |
| ICarImpl.assertPermission(mContext, mHal.getReadPermission(prop)); |
| return mHal.getProperty(prop, zone); |
| } |
| |
| @Override |
| public String getReadPermission(int propId) { |
| if (mConfigs.get(propId) == null) { |
| // Property ID does not exist |
| Log.e(TAG, "getReadPermission: propId is not in config list:0x" + toHexString(propId)); |
| return null; |
| } |
| return mHal.getReadPermission(propId); |
| } |
| |
| @Override |
| public String getWritePermission(int propId) { |
| if (mConfigs.get(propId) == null) { |
| // Property ID does not exist |
| Log.e(TAG, "getWritePermission: propId is not in config list:0x" + toHexString(propId)); |
| return null; |
| } |
| return mHal.getWritePermission(propId); |
| } |
| |
| @Override |
| public void setProperty(CarPropertyValue prop) { |
| int propId = prop.getPropertyId(); |
| if (mConfigs.get(propId) == null) { |
| // Do not attempt to register an invalid propId |
| Log.e(TAG, "setProperty: propId is not in config list:0x" + toHexString(propId)); |
| return; |
| } |
| ICarImpl.assertPermission(mContext, mHal.getWritePermission(propId)); |
| mHal.setProperty(prop); |
| } |
| |
| // Implement PropertyHalListener interface |
| @Override |
| public void onPropertyChange(List<CarPropertyEvent> events) { |
| Map<IBinder, Pair<ICarPropertyEventListener, List<CarPropertyEvent>>> eventsToDispatch = |
| new HashMap<>(); |
| |
| for (CarPropertyEvent event : events) { |
| int propId = event.getCarPropertyValue().getPropertyId(); |
| List<Client> clients = mPropIdClientMap.get(propId); |
| if (clients == null) { |
| Log.e(TAG, "onPropertyChange: no listener registered for propId=0x" |
| + toHexString(propId)); |
| continue; |
| } |
| |
| for (Client c : clients) { |
| IBinder listenerBinder = c.getListenerBinder(); |
| Pair<ICarPropertyEventListener, List<CarPropertyEvent>> p = |
| eventsToDispatch.get(listenerBinder); |
| if (p == null) { |
| // Initialize the linked list for the listener |
| p = new Pair<>(c.getListener(), new LinkedList<CarPropertyEvent>()); |
| eventsToDispatch.put(listenerBinder, p); |
| } |
| p.second.add(event); |
| } |
| } |
| // Parse the dispatch list to send events |
| for (Pair<ICarPropertyEventListener, List<CarPropertyEvent>> p: eventsToDispatch.values()) { |
| try { |
| p.first.onEvent(p.second); |
| } catch (RemoteException ex) { |
| // If we cannot send a record, its likely the connection snapped. Let binder |
| // death handle the situation. |
| Log.e(TAG, "onEvent calling failed: " + ex); |
| } |
| } |
| } |
| |
| @Override |
| public void onPropertySetError(int property, int area) { |
| List<Client> clients = mPropIdClientMap.get(property); |
| if (clients != null) { |
| List<CarPropertyEvent> eventList = new LinkedList<>(); |
| eventList.add(CarPropertyEvent.createErrorEvent(property, area)); |
| for (Client c : clients) { |
| try { |
| c.getListener().onEvent(eventList); |
| } catch (RemoteException ex) { |
| // If we cannot send a record, its likely the connection snapped. Let the binder |
| // death handle the situation. |
| Log.e(TAG, "onEvent calling failed: " + ex); |
| } |
| } |
| } else { |
| Log.e(TAG, "onPropertySetError called with no listener registered for propId=0x" |
| + toHexString(property)); |
| } |
| } |
| } |