| /* |
| * 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.annotation.NonNull; |
| import android.annotation.Nullable; |
| import android.car.Car; |
| 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.Handler; |
| import android.os.HandlerThread; |
| import android.os.IBinder; |
| import android.os.RemoteException; |
| import android.util.ArrayMap; |
| import android.util.IndentingPrintWriter; |
| import android.util.Pair; |
| import android.util.Slog; |
| import android.util.SparseArray; |
| |
| import com.android.car.hal.PropertyHalService; |
| import com.android.internal.annotations.GuardedBy; |
| import com.android.server.utils.Slogf; |
| |
| import java.util.ArrayList; |
| import java.util.HashSet; |
| import java.util.List; |
| import java.util.Map; |
| import java.util.Set; |
| |
| /** |
| * This class implements the binder interface for ICarProperty.aidl to make it easier to create |
| * multiple managers that deal with Vehicle Properties. The property Ids in this class are IDs in |
| * manager level. |
| */ |
| public class CarPropertyService extends ICarProperty.Stub |
| implements CarServiceBase, PropertyHalService.PropertyHalListener { |
| private static final boolean DBG = true; |
| private static final String TAG = CarLog.tagFor(CarPropertyService.class); |
| private final Context mContext; |
| private final PropertyHalService mHal; |
| private final Object mLock = new Object(); |
| @GuardedBy("mLock") |
| private final Map<IBinder, Client> mClientMap = new ArrayMap<>(); |
| @GuardedBy("mLock") |
| private final SparseArray<List<Client>> mPropIdClientMap = new SparseArray<>(); |
| @GuardedBy("mLock") |
| private final SparseArray<SparseArray<Client>> mSetOperationClientMap = new SparseArray<>(); |
| private final HandlerThread mHandlerThread = |
| CarServiceUtils.getHandlerThread(getClass().getSimpleName()); |
| private final Handler mHandler = new Handler(mHandlerThread.getLooper()); |
| // Use SparseArray instead of map to save memory. |
| @GuardedBy("mLock") |
| private SparseArray<CarPropertyConfig<?>> mConfigs = new SparseArray<>(); |
| @GuardedBy("mLock") |
| private SparseArray<Pair<String, String>> mPropToPermission = new SparseArray<>(); |
| |
| public CarPropertyService(Context context, PropertyHalService hal) { |
| if (DBG) { |
| Slog.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 Object mLock = new Object(); |
| // propId->rate map. |
| @GuardedBy("mLock") |
| private final SparseArray<Float> mRateMap = new SparseArray<Float>(); |
| @GuardedBy("mLock") |
| private boolean mIsDead = false; |
| |
| Client(ICarPropertyEventListener listener) { |
| mListener = listener; |
| mListenerBinder = listener.asBinder(); |
| |
| try { |
| mListenerBinder.linkToDeath(this, 0); |
| } catch (RemoteException e) { |
| mIsDead = true; |
| } |
| } |
| |
| /** |
| * Returns whether this client is already dead. |
| * |
| * Caller should not assume this client is alive after getting True response because the |
| * binder might die after this function checks the status. Caller should only use this |
| * function to fail early. |
| */ |
| boolean isDead() { |
| synchronized (mLock) { |
| return mIsDead; |
| } |
| } |
| |
| void addProperty(int propId, float rate) { |
| synchronized (mLock) { |
| if (mIsDead) { |
| return; |
| } |
| mRateMap.put(propId, rate); |
| } |
| } |
| |
| float getRate(int propId) { |
| synchronized (mLock) { |
| // Return 0 if no key found, since that is the slowest rate. |
| return mRateMap.get(propId, 0.0f); |
| } |
| } |
| |
| int removeProperty(int propId) { |
| synchronized (mLock) { |
| mRateMap.remove(propId); |
| return mRateMap.size(); |
| } |
| } |
| |
| /** |
| * Handler to be called when client died. |
| * |
| * Remove the listener from HAL service and unregister if this is the last client. |
| */ |
| @Override |
| public void binderDied() { |
| List<Integer> propIds = new ArrayList<>(); |
| synchronized (mLock) { |
| mIsDead = true; |
| |
| if (DBG) { |
| Slogf.d(TAG, "binderDied %s", mListenerBinder); |
| } |
| |
| // Because we set mIsDead to true here, we are sure mRateMap would not have new |
| // elements. The propIds here is going to cover all the prop Ids that we need to |
| // unregister. |
| for (int i = 0; i < mRateMap.size(); i++) { |
| propIds.add(mRateMap.keyAt(i)); |
| } |
| } |
| |
| CarPropertyService.this.unregisterListenerBinderForProps(propIds, mListenerBinder); |
| } |
| |
| /** |
| * Calls onEvent function on the listener if the binder is alive. |
| * |
| * There is still chance when onEvent might fail because binderDied is not called before |
| * this function. |
| */ |
| void onEvent(List<CarPropertyEvent> events) throws RemoteException { |
| synchronized (mLock) { |
| if (mIsDead) { |
| return; |
| } |
| } |
| mListener.onEvent(events); |
| } |
| } |
| |
| @Override |
| public void init() { |
| synchronized (mLock) { |
| // Cache the configs list and permissions to avoid subsequent binder calls |
| mConfigs = mHal.getPropertyList(); |
| mPropToPermission = mHal.getPermissionsForAllProperties(); |
| } |
| if (DBG) { |
| Slog.d(TAG, "cache CarPropertyConfigs " + mConfigs.size()); |
| } |
| mHal.setListener(this); |
| } |
| |
| @Override |
| public void release() { |
| synchronized (mLock) { |
| mClientMap.clear(); |
| mPropIdClientMap.clear(); |
| mHal.setListener(null); |
| mSetOperationClientMap.clear(); |
| } |
| } |
| |
| @Override |
| public void dump(IndentingPrintWriter writer) { |
| writer.println("*CarPropertyService*"); |
| writer.increaseIndent(); |
| synchronized (mLock) { |
| writer.println(String.format("There are %d clients using CarPropertyService.", |
| mClientMap.size())); |
| writer.println("Properties registered: "); |
| writer.increaseIndent(); |
| for (int i = 0; i < mPropIdClientMap.size(); i++) { |
| int propId = mPropIdClientMap.keyAt(i); |
| writer.println("propId: 0x" + toHexString(propId) |
| + " is registered by " + mPropIdClientMap.valueAt(i).size() |
| + " client(s)."); |
| } |
| writer.decreaseIndent(); |
| writer.println("Properties changed by CarPropertyService: "); |
| writer.increaseIndent(); |
| for (int i = 0; i < mSetOperationClientMap.size(); i++) { |
| int propId = mSetOperationClientMap.keyAt(i); |
| SparseArray areaIdToClient = mSetOperationClientMap.valueAt(i); |
| for (int j = 0; j < areaIdToClient.size(); j++) { |
| int areaId = areaIdToClient.keyAt(j); |
| writer.println(String.format("propId: 0x%s areaId: 0x%s by client: %s", |
| toHexString(propId), toHexString(areaId), areaIdToClient.valueAt(j))); |
| } |
| } |
| writer.decreaseIndent(); |
| } |
| writer.decreaseIndent(); |
| } |
| |
| @Override |
| public void registerListener(int propId, float rate, ICarPropertyEventListener listener) { |
| if (DBG) { |
| Slog.d(TAG, "registerListener: propId=0x" + toHexString(propId) + " rate=" + rate); |
| } |
| if (listener == null) { |
| Slog.e(TAG, "registerListener: Listener is null."); |
| throw new IllegalArgumentException("listener cannot be null."); |
| } |
| |
| IBinder listenerBinder = listener.asBinder(); |
| CarPropertyConfig propertyConfig; |
| Client finalClient; |
| synchronized (mLock) { |
| propertyConfig = mConfigs.get(propId); |
| if (propertyConfig == null) { |
| // Do not attempt to register an invalid propId |
| Slog.e(TAG, "registerListener: propId is not in config list: 0x" + toHexString( |
| propId)); |
| return; |
| } |
| ICarImpl.assertPermission(mContext, mHal.getReadPermission(propId)); |
| // Get or create the client for this listener |
| Client client = mClientMap.get(listenerBinder); |
| if (client == null) { |
| client = new Client(listener); |
| if (client.isDead()) { |
| Slogf.w(TAG, "the ICarPropertyEventListener is already dead"); |
| return; |
| } |
| mClientMap.put(listenerBinder, client); |
| } |
| client.addProperty(propId, rate); |
| // Insert the client into the propId --> clients map |
| List<Client> clients = mPropIdClientMap.get(propId); |
| if (clients == null) { |
| clients = new ArrayList<Client>(); |
| mPropIdClientMap.put(propId, clients); |
| } |
| if (!clients.contains(client)) { |
| clients.add(client); |
| } |
| // Set the new rate |
| if (rate > mHal.getSampleRate(propId)) { |
| mHal.subscribeProperty(propId, rate); |
| } |
| finalClient = client; |
| } |
| |
| // propertyConfig and client are NonNull. |
| mHandler.post(() -> |
| getAndDispatchPropertyInitValue(propertyConfig, finalClient)); |
| } |
| |
| private void getAndDispatchPropertyInitValue(CarPropertyConfig config, Client client) { |
| List<CarPropertyEvent> events = new ArrayList<>(); |
| int propId = config.getPropertyId(); |
| if (config.isGlobalProperty()) { |
| CarPropertyValue value = mHal.getPropertySafe(propId, 0); |
| if (value != null) { |
| CarPropertyEvent event = new CarPropertyEvent( |
| CarPropertyEvent.PROPERTY_EVENT_PROPERTY_CHANGE, value); |
| events.add(event); |
| } |
| } else { |
| for (int areaId : config.getAreaIds()) { |
| CarPropertyValue value = mHal.getPropertySafe(propId, areaId); |
| if (value != null) { |
| CarPropertyEvent event = new CarPropertyEvent( |
| CarPropertyEvent.PROPERTY_EVENT_PROPERTY_CHANGE, value); |
| events.add(event); |
| } |
| } |
| } |
| try { |
| client.onEvent(events); |
| } catch (RemoteException ex) { |
| // If we cannot send a record, its likely the connection snapped. Let the binder |
| // death handle the situation. |
| Slogf.e(TAG, "onEvent calling failed", ex); |
| } |
| } |
| |
| @Override |
| public void unregisterListener(int propId, ICarPropertyEventListener listener) { |
| if (DBG) { |
| Slog.d(TAG, "unregisterListener propId=0x" + toHexString(propId)); |
| } |
| ICarImpl.assertPermission(mContext, mHal.getReadPermission(propId)); |
| if (listener == null) { |
| Slog.e(TAG, "unregisterListener: Listener is null."); |
| throw new IllegalArgumentException("Listener is null"); |
| } |
| |
| IBinder listenerBinder = listener.asBinder(); |
| unregisterListenerBinderForProps(List.of(propId), listenerBinder); |
| } |
| |
| @GuardedBy("mLock") |
| private void unregisterListenerBinderLocked(int propId, IBinder listenerBinder) { |
| float updateMaxRate = 0f; |
| Client client = mClientMap.get(listenerBinder); |
| List<Client> propertyClients = mPropIdClientMap.get(propId); |
| if (mConfigs.get(propId) == null) { |
| // Do not attempt to unregister an invalid propId |
| Slogf.e(TAG, "unregisterListener: propId is not in config list:0x%s", |
| toHexString(propId)); |
| return; |
| } |
| if ((client == null) || (propertyClients == null)) { |
| Slogf.e(TAG, "unregisterListenerBinderLocked: Listener was not previously " |
| + "registered."); |
| } else { |
| if (propertyClients.remove(client)) { |
| int propLeft = client.removeProperty(propId); |
| if (propLeft == 0) { |
| mClientMap.remove(listenerBinder); |
| } |
| clearSetOperationRecorderLocked(propId, client); |
| |
| } else { |
| Slogf.e(TAG, "unregisterListenerBinderLocked: Listener was not registered for " |
| + "propId=0x" + toHexString(propId)); |
| } |
| |
| if (propertyClients.isEmpty()) { |
| // Last listener for this property unsubscribed. Clean up |
| mPropIdClientMap.remove(propId); |
| mSetOperationClientMap.remove(propId); |
| } else { |
| // Other listeners are still subscribed. Calculate the new rate |
| for (int i = 0; i < propertyClients.size(); i++) { |
| Client c = propertyClients.get(i); |
| float rate = c.getRate(propId); |
| updateMaxRate = Math.max(rate, updateMaxRate); |
| } |
| } |
| } |
| if (Float.compare(updateMaxRate, 0f) == 0) { |
| // Unsubscribe property if we did not find any other client register to this property |
| mHal.unsubscribeProperty(propId); |
| } else if (Float.compare(updateMaxRate, mHal.getSampleRate(propId)) != 0) { |
| try { |
| // Only reset the sample rate if needed |
| mHal.subscribeProperty(propId, updateMaxRate); |
| } catch (IllegalArgumentException e) { |
| Slogf.e(TAG, "failed to subscribe to propId=0x" + toHexString(propId) |
| + ", error: " + e); |
| } |
| } |
| } |
| |
| private void unregisterListenerBinderForProps(List<Integer> propIds, IBinder listenerBinder) { |
| synchronized (mLock) { |
| for (int i = 0; i < propIds.size(); i++) { |
| int propId = propIds.get(i); |
| unregisterListenerBinderLocked(propId, listenerBinder); |
| } |
| } |
| } |
| |
| /** |
| * Return the list of properties' configs that the caller may access. |
| */ |
| @NonNull |
| @Override |
| public List<CarPropertyConfig> getPropertyList() { |
| int[] allPropId; |
| // Avoid permission checking under lock. |
| synchronized (mLock) { |
| allPropId = new int[mConfigs.size()]; |
| for (int i = 0; i < mConfigs.size(); i++) { |
| allPropId[i] = mConfigs.keyAt(i); |
| } |
| } |
| return getPropertyConfigList(allPropId); |
| } |
| |
| /** |
| * |
| * @param propIds Array of property Ids |
| * @return the list of properties' configs that the caller may access. |
| */ |
| @NonNull |
| @Override |
| public List<CarPropertyConfig> getPropertyConfigList(int[] propIds) { |
| // Cache the granted permissions |
| Set<String> grantedPermission = new HashSet<>(); |
| List<CarPropertyConfig> availableProp = new ArrayList<>(); |
| if (propIds == null) { |
| return availableProp; |
| } |
| for (int propId : propIds) { |
| String readPermission = getReadPermission(propId); |
| if (readPermission == null) { |
| continue; |
| } |
| // Check if context already granted permission first |
| if (grantedPermission.contains(readPermission) |
| || ICarImpl.hasPermission(mContext, readPermission)) { |
| grantedPermission.add(readPermission); |
| synchronized (mLock) { |
| availableProp.add(mConfigs.get(propId)); |
| } |
| } |
| } |
| if (DBG) { |
| Slog.d(TAG, "getPropertyList returns " + availableProp.size() + " configs"); |
| } |
| return availableProp; |
| } |
| |
| @Override |
| public CarPropertyValue getProperty(int prop, int zone) { |
| synchronized (mLock) { |
| if (mConfigs.get(prop) == null) { |
| // Do not attempt to register an invalid propId |
| Slog.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); |
| } |
| |
| /** |
| * Get property value for car service's internal usage. |
| * @param prop property id |
| * @param zone area id |
| * @return null if property is not implemented or there is an exception in the vehicle. |
| */ |
| public CarPropertyValue getPropertySafe(int prop, int zone) { |
| synchronized (mLock) { |
| if (mConfigs.get(prop) == null) { |
| // Do not attempt to register an invalid propId |
| Slog.e(TAG, "getPropertySafe: propId is not in config list:0x" |
| + toHexString(prop)); |
| return null; |
| } |
| } |
| ICarImpl.assertPermission(mContext, mHal.getReadPermission(prop)); |
| return mHal.getPropertySafe(prop, zone); |
| } |
| |
| @Nullable |
| @Override |
| public String getReadPermission(int propId) { |
| Pair<String, String> permissions; |
| synchronized (mLock) { |
| permissions = mPropToPermission.get(propId); |
| } |
| if (permissions == null) { |
| // Property ID does not exist |
| Slog.e(TAG, |
| "getReadPermission: propId is not in config list:0x" + toHexString(propId)); |
| return null; |
| } |
| return permissions.first; |
| } |
| |
| @Nullable |
| @Override |
| public String getWritePermission(int propId) { |
| Pair<String, String> permissions; |
| synchronized (mLock) { |
| permissions = mPropToPermission.get(propId); |
| } |
| if (permissions == null) { |
| // Property ID does not exist |
| Slog.e(TAG, |
| "getWritePermission: propId is not in config list:0x" + toHexString(propId)); |
| return null; |
| } |
| return permissions.second; |
| } |
| |
| @Override |
| public void setProperty(CarPropertyValue prop, ICarPropertyEventListener listener) { |
| int propId = prop.getPropertyId(); |
| checkPropertyAccessibility(propId); |
| // need an extra permission for writing display units properties. |
| if (mHal.isDisplayUnitsProperty(propId)) { |
| ICarImpl.assertPermission(mContext, Car.PERMISSION_VENDOR_EXTENSION); |
| } |
| mHal.setProperty(prop); |
| IBinder listenerBinder = listener.asBinder(); |
| synchronized (mLock) { |
| Client client = mClientMap.get(listenerBinder); |
| if (client == null) { |
| client = new Client(listener); |
| } |
| if (client.isDead()) { |
| Slogf.w(TAG, "the ICarPropertyEventListener is already dead"); |
| return; |
| } |
| updateSetOperationRecorderLocked(propId, prop.getAreaId(), client); |
| } |
| } |
| |
| // The helper method checks if the vehicle has implemented this property and the property |
| // is accessible or not for platform and client. |
| private void checkPropertyAccessibility(int propId) { |
| // Checks if the car implemented the property or not. |
| synchronized (mLock) { |
| if (mConfigs.get(propId) == null) { |
| throw new IllegalArgumentException("Property Id: 0x" + Integer.toHexString(propId) |
| + " does not exist in the vehicle"); |
| } |
| } |
| |
| // Checks if android has permission to write property. |
| String propertyWritePermission = mHal.getWritePermission(propId); |
| if (propertyWritePermission == null) { |
| throw new SecurityException("Platform does not have permission to change value for " |
| + "property Id: 0x" + Integer.toHexString(propId)); |
| } |
| // Checks if the client has the permission. |
| ICarImpl.assertPermission(mContext, propertyWritePermission); |
| } |
| |
| // Updates recorder for set operation. |
| @GuardedBy("mLock") |
| private void updateSetOperationRecorderLocked(int propId, int areaId, Client client) { |
| if (mSetOperationClientMap.get(propId) != null) { |
| mSetOperationClientMap.get(propId).put(areaId, client); |
| } else { |
| SparseArray<Client> areaIdToClient = new SparseArray<>(); |
| areaIdToClient.put(areaId, client); |
| mSetOperationClientMap.put(propId, areaIdToClient); |
| } |
| } |
| |
| // Clears map when client unregister for property. |
| private void clearSetOperationRecorderLocked(int propId, Client client) { |
| SparseArray<Client> areaIdToClient = mSetOperationClientMap.get(propId); |
| if (areaIdToClient != null) { |
| List<Integer> indexNeedToRemove = new ArrayList<>(); |
| for (int index = 0; index < areaIdToClient.size(); index++) { |
| if (client.equals(areaIdToClient.valueAt(index))) { |
| indexNeedToRemove.add(index); |
| } |
| } |
| |
| for (int index : indexNeedToRemove) { |
| if (DBG) { |
| Slog.d("ErrorEvent", " Clear propId:0x" + toHexString(propId) |
| + " areaId: 0x" + toHexString(areaIdToClient.keyAt(index))); |
| } |
| areaIdToClient.removeAt(index); |
| } |
| } |
| } |
| |
| // Implement PropertyHalListener interface |
| @Override |
| public void onPropertyChange(List<CarPropertyEvent> events) { |
| Map<Client, List<CarPropertyEvent>> eventsToDispatch = new ArrayMap<>(); |
| |
| synchronized (mLock) { |
| for (int i = 0; i < events.size(); i++) { |
| CarPropertyEvent event = events.get(i); |
| int propId = event.getCarPropertyValue().getPropertyId(); |
| List<Client> clients = mPropIdClientMap.get(propId); |
| if (clients == null) { |
| Slogf.e(TAG, "onPropertyChange: no listener registered for propId=0x%s", |
| toHexString(propId)); |
| continue; |
| } |
| |
| for (int j = 0; j < clients.size(); j++) { |
| Client c = clients.get(j); |
| List<CarPropertyEvent> p = eventsToDispatch.get(c); |
| if (p == null) { |
| // Initialize the linked list for the listener |
| p = new ArrayList<CarPropertyEvent>(); |
| eventsToDispatch.put(c, p); |
| } |
| p.add(event); |
| } |
| } |
| |
| // Parse the dispatch list to send events |
| for (Client client : eventsToDispatch.keySet()) { |
| try { |
| client.onEvent(eventsToDispatch.get(client)); |
| } catch (RemoteException ex) { |
| // If we cannot send a record, its likely the connection snapped. Let binder |
| // death handle the situation. |
| Slogf.e(TAG, "onEvent calling failed", ex); |
| } |
| } |
| } |
| } |
| |
| @Override |
| public void onPropertySetError(int property, int areaId, int errorCode) { |
| Client lastOperatedClient = null; |
| synchronized (mLock) { |
| if (mSetOperationClientMap.get(property) != null |
| && mSetOperationClientMap.get(property).get(areaId) != null) { |
| lastOperatedClient = mSetOperationClientMap.get(property).get(areaId); |
| } else { |
| Slog.e(TAG, "Can not find the client changed propertyId: 0x" |
| + toHexString(property) + " in areaId: 0x" + toHexString(areaId)); |
| } |
| |
| } |
| if (lastOperatedClient != null) { |
| dispatchToLastClient(property, areaId, errorCode, lastOperatedClient); |
| } |
| } |
| |
| private void dispatchToLastClient(int property, int areaId, int errorCode, |
| Client lastOperatedClient) { |
| try { |
| List<CarPropertyEvent> eventList = new ArrayList<>(); |
| eventList.add( |
| CarPropertyEvent.createErrorEventWithErrorCode(property, areaId, |
| errorCode)); |
| lastOperatedClient.onEvent(eventList); |
| } catch (RemoteException ex) { |
| Slogf.e(TAG, "onEvent calling failed", ex); |
| } |
| } |
| } |