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