1 /* 2 * Copyright (C) 2021 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 android.car.telemetry; 18 19 import android.annotation.CallbackExecutor; 20 import android.annotation.IntDef; 21 import android.annotation.NonNull; 22 import android.annotation.Nullable; 23 import android.annotation.RequiresPermission; 24 import android.annotation.SystemApi; 25 import android.car.Car; 26 import android.car.CarManagerBase; 27 import android.car.annotation.AddedInOrBefore; 28 import android.car.annotation.RequiredFeature; 29 import android.car.builtin.util.Slogf; 30 import android.os.Bundle; 31 import android.os.IBinder; 32 import android.os.ParcelFileDescriptor; 33 import android.os.PersistableBundle; 34 import android.os.RemoteException; 35 import android.os.ResultReceiver; 36 37 import libcore.io.IoUtils; 38 39 import java.io.ByteArrayInputStream; 40 import java.io.DataInputStream; 41 import java.io.EOFException; 42 import java.io.IOException; 43 import java.lang.annotation.Retention; 44 import java.lang.annotation.RetentionPolicy; 45 import java.lang.ref.WeakReference; 46 import java.util.ArrayList; 47 import java.util.List; 48 import java.util.Objects; 49 import java.util.concurrent.Executor; 50 import java.util.concurrent.atomic.AtomicReference; 51 52 /** 53 * Provides an application interface for interacting with the Car Telemetry Service. 54 * 55 * @hide 56 */ 57 @RequiredFeature(Car.CAR_TELEMETRY_SERVICE) 58 @SystemApi 59 public final class CarTelemetryManager extends CarManagerBase { 60 61 private static final boolean DEBUG = false; 62 private static final String TAG = CarTelemetryManager.class.getSimpleName(); 63 private static final int METRICS_CONFIG_MAX_SIZE_BYTES = 10 * 1024; // 10 kb 64 65 private final ICarTelemetryService mService; 66 private final AtomicReference<Executor> mReportReadyListenerExecutor; 67 private final AtomicReference<ReportReadyListener> mReportReadyListener; 68 69 /** Status to indicate that MetricsConfig was added successfully. */ 70 @AddedInOrBefore(majorVersion = 33) 71 public static final int STATUS_ADD_METRICS_CONFIG_SUCCEEDED = 0; 72 73 /** 74 * Status to indicate that add MetricsConfig failed because the same MetricsConfig of the same 75 * name and version already exists. 76 */ 77 @AddedInOrBefore(majorVersion = 33) 78 public static final int STATUS_ADD_METRICS_CONFIG_ALREADY_EXISTS = 1; 79 80 /** 81 * Status to indicate that add MetricsConfig failed because a newer version of the MetricsConfig 82 * exists. 83 */ 84 @AddedInOrBefore(majorVersion = 33) 85 public static final int STATUS_ADD_METRICS_CONFIG_VERSION_TOO_OLD = 2; 86 87 /** 88 * Status to indicate that add MetricsConfig failed because CarTelemetryService is unable to 89 * parse the given byte array into a MetricsConfig. 90 */ 91 @AddedInOrBefore(majorVersion = 33) 92 public static final int STATUS_ADD_METRICS_CONFIG_PARSE_FAILED = 3; 93 94 /** 95 * Status to indicate that add MetricsConfig failed because of failure to verify the signature 96 * of the MetricsConfig. 97 */ 98 @AddedInOrBefore(majorVersion = 33) 99 public static final int STATUS_ADD_METRICS_CONFIG_SIGNATURE_VERIFICATION_FAILED = 4; 100 101 /** Status to indicate that add MetricsConfig failed because of a general error in cars. */ 102 @AddedInOrBefore(majorVersion = 33) 103 public static final int STATUS_ADD_METRICS_CONFIG_UNKNOWN = 5; 104 105 /** @hide */ 106 @IntDef( 107 prefix = {"STATUS_ADD_METRICS_CONFIG_"}, 108 value = { 109 STATUS_ADD_METRICS_CONFIG_SUCCEEDED, 110 STATUS_ADD_METRICS_CONFIG_ALREADY_EXISTS, 111 STATUS_ADD_METRICS_CONFIG_VERSION_TOO_OLD, 112 STATUS_ADD_METRICS_CONFIG_PARSE_FAILED, 113 STATUS_ADD_METRICS_CONFIG_SIGNATURE_VERIFICATION_FAILED, 114 STATUS_ADD_METRICS_CONFIG_UNKNOWN 115 }) 116 @Retention(RetentionPolicy.SOURCE) 117 public @interface MetricsConfigStatus {} 118 119 /** Status to indicate that MetricsConfig produced a report. */ 120 @AddedInOrBefore(majorVersion = 33) 121 public static final int STATUS_GET_METRICS_CONFIG_FINISHED = 0; 122 123 /** 124 * Status to indicate a MetricsConfig exists but has produced neither interim/final report nor 125 * runtime execution errors. 126 */ 127 @AddedInOrBefore(majorVersion = 33) 128 public static final int STATUS_GET_METRICS_CONFIG_PENDING = 1; 129 130 /** Status to indicate a MetricsConfig exists and produced interim results. */ 131 @AddedInOrBefore(majorVersion = 33) 132 public static final int STATUS_GET_METRICS_CONFIG_INTERIM_RESULTS = 2; 133 134 /** Status to indicate the MetricsConfig produced a runtime execution error. */ 135 @AddedInOrBefore(majorVersion = 33) 136 public static final int STATUS_GET_METRICS_CONFIG_RUNTIME_ERROR = 3; 137 138 /** Status to indicate a MetricsConfig does not exist and hence no report can be found. */ 139 @AddedInOrBefore(majorVersion = 33) 140 public static final int STATUS_GET_METRICS_CONFIG_DOES_NOT_EXIST = 4; 141 142 /** @hide */ 143 @IntDef( 144 prefix = {"STATUS_GET_METRICS_CONFIG_"}, 145 value = { 146 STATUS_GET_METRICS_CONFIG_FINISHED, 147 STATUS_GET_METRICS_CONFIG_PENDING, 148 STATUS_GET_METRICS_CONFIG_INTERIM_RESULTS, 149 STATUS_GET_METRICS_CONFIG_RUNTIME_ERROR, 150 STATUS_GET_METRICS_CONFIG_DOES_NOT_EXIST 151 }) 152 @Retention(RetentionPolicy.SOURCE) 153 public @interface MetricsReportStatus {} 154 155 /** 156 * Application must pass a {@link AddMetricsConfigCallback} to use {@link 157 * #addMetricsConfig(String, byte[], Executor, AddMetricsConfigCallback)} 158 * 159 * @hide 160 */ 161 @SystemApi 162 public interface AddMetricsConfigCallback { 163 /** 164 * Sends the {@link #addMetricsConfig(String, byte[], Executor, AddMetricsConfigCallback)} 165 * status to the client. 166 * 167 * @param metricsConfigName name of the MetricsConfig that the status is associated with. 168 * @param statusCode See {@link MetricsConfigStatus}. 169 */ 170 @AddedInOrBefore(majorVersion = 33) onAddMetricsConfigStatus( @onNull String metricsConfigName, @MetricsConfigStatus int statusCode)171 void onAddMetricsConfigStatus( 172 @NonNull String metricsConfigName, @MetricsConfigStatus int statusCode); 173 } 174 175 /** 176 * Application must pass a {@link MetricsReportCallback} object to receive finished reports from 177 * {@link #getFinishedReport(String, Executor, MetricsReportCallback)} and {@link 178 * #getAllFinishedReports(Executor, MetricsReportCallback)}. 179 * 180 * @hide 181 */ 182 @SystemApi 183 public interface MetricsReportCallback { 184 /** 185 * Provides the metrics report associated with metricsConfigName. If there is a metrics 186 * report, it provides the metrics report. If the metrics report calculation failed due to a 187 * runtime error during the execution of reporting script, it provides the runtime error in 188 * the error parameter. The status parameter provides more information on the state of the 189 * metrics report. 190 * 191 * TODO(b/184964661): Publish the documentation for the format of the finished reports. 192 * 193 * @param metricsConfigName name of the MetricsConfig that the report is associated with. 194 * @param report the car telemetry report. Null if there is no report. 195 * @param telemetryError the serialized telemetry metrics configuration runtime execution 196 * error. 197 * @param status of the metrics report. See {@link MetricsReportStatus}. 198 */ 199 @AddedInOrBefore(majorVersion = 33) onResult( @onNull String metricsConfigName, @Nullable PersistableBundle report, @Nullable byte[] telemetryError, @MetricsReportStatus int status)200 void onResult( 201 @NonNull String metricsConfigName, 202 @Nullable PersistableBundle report, 203 @Nullable byte[] telemetryError, 204 @MetricsReportStatus int status); 205 } 206 207 /** 208 * Application can optionally use {@link #setReportReadyListener(Executor, ReportReadyListener)} 209 * to receive report ready notifications. Upon receiving the notification, client can use 210 * {@link #getFinishedReport(String, Executor, MetricsReportCallback)} on the received 211 * metricsConfigName. 212 * 213 * @hide 214 */ 215 @SystemApi 216 public interface ReportReadyListener { 217 /** 218 * Sends the report ready notification to the client. 219 * 220 * @param metricsConfigName name of the MetricsConfig whose report is ready. 221 */ 222 @AddedInOrBefore(majorVersion = 33) onReady(@onNull String metricsConfigName)223 void onReady(@NonNull String metricsConfigName); 224 } 225 226 /** 227 * Gets an instance of CarTelemetryManager. 228 * 229 * <p>CarTelemetryManager manages {@link com.android.car.telemetry.CarTelemetryService} and 230 * provides APIs so the client can use the car telemetry service. 231 * 232 * <p>There is only one client to this manager, which is OEM's cloud application. It uses the 233 * APIs to send config to and receive data from CarTelemetryService. 234 * 235 * @hide 236 */ CarTelemetryManager(Car car, IBinder service)237 public CarTelemetryManager(Car car, IBinder service) { 238 super(car); 239 mService = ICarTelemetryService.Stub.asInterface(service); 240 mReportReadyListenerExecutor = new AtomicReference<>(null); 241 mReportReadyListener = new AtomicReference<>(null); 242 if (DEBUG) { 243 Slogf.d(TAG, "starting car telemetry manager"); 244 } 245 } 246 247 /** @hide */ 248 @Override 249 @AddedInOrBefore(majorVersion = 33) onCarDisconnected()250 public void onCarDisconnected() {} 251 252 /** 253 * Adds a MetricsConfig to CarTelemetryService. The size of the MetricsConfig cannot exceed a 254 * {@link #METRICS_CONFIG_MAX_SIZE_BYTES}, otherwise an exception is thrown. 255 * 256 * <p>The MetricsConfig will be uniquely identified by its name and version. If a MetricsConfig 257 * of the same name already exists in {@link com.android.car.telemetry.CarTelemetryService}, the 258 * config version will be compared. If the version is strictly higher, the existing 259 * MetricsConfig will be replaced by the new one. All legacy data will be cleared if replaced. 260 * 261 * <p>Client should use {@link #getFinishedReport(String, Executor, MetricsReportCallback)} to 262 * get the report before replacing a MetricsConfig. 263 * 264 * <p>The status of this API is sent back asynchronously via {@link AddMetricsConfigCallback}. 265 * 266 * @param metricsConfigName name of the MetricsConfig, must match {@link 267 * TelemetryProto.MetricsConfig#getName()}. 268 * @param metricsConfig the serialized bytes of a MetricsConfig object. 269 * @param executor The {@link Executor} on which the callback will be invoked. 270 * @param callback A callback for receiving addMetricsConfig status codes. 271 * @throws IllegalArgumentException if the MetricsConfig size exceeds limit. 272 * @hide 273 */ 274 @SystemApi 275 @RequiresPermission(Car.PERMISSION_USE_CAR_TELEMETRY_SERVICE) 276 @AddedInOrBefore(majorVersion = 33) addMetricsConfig( @onNull String metricsConfigName, @NonNull byte[] metricsConfig, @CallbackExecutor @NonNull Executor executor, @NonNull AddMetricsConfigCallback callback)277 public void addMetricsConfig( 278 @NonNull String metricsConfigName, 279 @NonNull byte[] metricsConfig, 280 @CallbackExecutor @NonNull Executor executor, 281 @NonNull AddMetricsConfigCallback callback) { 282 if (metricsConfig.length > METRICS_CONFIG_MAX_SIZE_BYTES) { 283 throw new IllegalArgumentException("MetricsConfig size exceeds limit."); 284 } 285 try { 286 mService.addMetricsConfig(metricsConfigName, metricsConfig, new ResultReceiver(null) { 287 @Override 288 protected void onReceiveResult(int resultCode, Bundle resultData) { 289 executor.execute(() -> 290 callback.onAddMetricsConfigStatus(metricsConfigName, resultCode)); 291 } 292 }); 293 } catch (RemoteException e) { 294 handleRemoteExceptionFromCarService(e); 295 } 296 } 297 298 /** 299 * Removes a MetricsConfig from {@link com.android.car.telemetry.CarTelemetryService}. This will 300 * also remove outputs produced by the MetricsConfig. If the MetricsConfig does not exist, 301 * nothing will be removed. 302 * 303 * @param metricsConfigName that identify the MetricsConfig. 304 * @hide 305 */ 306 @SystemApi 307 @RequiresPermission(Car.PERMISSION_USE_CAR_TELEMETRY_SERVICE) 308 @AddedInOrBefore(majorVersion = 33) removeMetricsConfig(@onNull String metricsConfigName)309 public void removeMetricsConfig(@NonNull String metricsConfigName) { 310 try { 311 mService.removeMetricsConfig(metricsConfigName); 312 } catch (RemoteException e) { 313 handleRemoteExceptionFromCarService(e); 314 } 315 } 316 317 /** 318 * Removes all MetricsConfigs from {@link com.android.car.telemetry.CarTelemetryService}. This 319 * will also remove all MetricsConfig outputs. 320 * 321 * @hide 322 */ 323 @SystemApi 324 @RequiresPermission(Car.PERMISSION_USE_CAR_TELEMETRY_SERVICE) 325 @AddedInOrBefore(majorVersion = 33) removeAllMetricsConfigs()326 public void removeAllMetricsConfigs() { 327 try { 328 mService.removeAllMetricsConfigs(); 329 } catch (RemoteException e) { 330 handleRemoteExceptionFromCarService(e); 331 } 332 } 333 334 /** 335 * Gets script execution reports of a MetricsConfig as from the {@link 336 * com.android.car.telemetry.CarTelemetryService}. This API is asynchronous and the report is 337 * sent back asynchronously via the {@link MetricsReportCallback}. This call is destructive. The 338 * returned report will be deleted from CarTelemetryService. 339 * 340 * @param metricsConfigName to identify the MetricsConfig. 341 * @param executor The {@link Executor} on which the callback will be invoked. 342 * @param callback A callback for receiving finished reports. 343 * @hide 344 */ 345 @SystemApi 346 @RequiresPermission(Car.PERMISSION_USE_CAR_TELEMETRY_SERVICE) 347 @AddedInOrBefore(majorVersion = 33) getFinishedReport( @onNull String metricsConfigName, @CallbackExecutor @NonNull Executor executor, @NonNull MetricsReportCallback callback)348 public void getFinishedReport( 349 @NonNull String metricsConfigName, 350 @CallbackExecutor @NonNull Executor executor, 351 @NonNull MetricsReportCallback callback) { 352 try { 353 mService.getFinishedReport( 354 metricsConfigName, new CarTelemetryReportListenerImpl(executor, callback)); 355 } catch (RemoteException e) { 356 handleRemoteExceptionFromCarService(e); 357 } 358 } 359 360 /** 361 * Gets all script execution reports from {@link com.android.car.telemetry.CarTelemetryService} 362 * asynchronously via the {@link MetricsReportCallback}. The callback will be invoked multiple 363 * times if there are multiple reports. This call is destructive. The returned reports will be 364 * deleted from CarTelemetryService. 365 * 366 * @param executor The {@link Executor} on which the callback will be invoked. 367 * @param callback A callback for receiving finished reports. 368 * @hide 369 */ 370 @SystemApi 371 @RequiresPermission(Car.PERMISSION_USE_CAR_TELEMETRY_SERVICE) 372 @AddedInOrBefore(majorVersion = 33) getAllFinishedReports( @allbackExecutor @onNull Executor executor, @NonNull MetricsReportCallback callback)373 public void getAllFinishedReports( 374 @CallbackExecutor @NonNull Executor executor, @NonNull MetricsReportCallback callback) { 375 try { 376 mService.getAllFinishedReports(new CarTelemetryReportListenerImpl(executor, callback)); 377 } catch (RemoteException e) { 378 handleRemoteExceptionFromCarService(e); 379 } 380 } 381 382 /** 383 * Registers a listener to receive report ready notifications. This is an optional feature that 384 * helps clients decide when is a good time to call {@link 385 * #getFinishedReport(String, Executor, MetricsReportCallback)}. 386 * 387 * <p>When a listener is set, it will receive notifications for reports or errors that are 388 * already produced before the listener is registered. 389 * 390 * <p>Clients who do not register a listener should use {@link 391 * #getFinishedReport(String, Executor, MetricsReportCallback)} periodically to check for 392 * report. 393 * 394 * @param executor The {@link Executor} on which the callback will be invoked. 395 * @param listener The listener to receive report ready notifications. 396 * @throws IllegalStateException if the listener is already set. 397 * @hide 398 */ 399 @SystemApi 400 @RequiresPermission(Car.PERMISSION_USE_CAR_TELEMETRY_SERVICE) 401 @AddedInOrBefore(majorVersion = 33) setReportReadyListener( @allbackExecutor @onNull Executor executor, @NonNull ReportReadyListener listener)402 public void setReportReadyListener( 403 @CallbackExecutor @NonNull Executor executor, @NonNull ReportReadyListener listener) { 404 if (mReportReadyListener.get() != null) { 405 throw new IllegalStateException("ReportReadyListener is already set."); 406 } 407 mReportReadyListenerExecutor.set(executor); 408 mReportReadyListener.set(listener); 409 try { 410 mService.setReportReadyListener(new CarTelemetryReportReadyListenerImpl(this)); 411 } catch (RemoteException e) { 412 handleRemoteExceptionFromCarService(e); 413 } 414 } 415 416 /** 417 * Clears the listener for receiving telemetry report ready notifications. 418 * 419 * @hide 420 */ 421 @SystemApi 422 @RequiresPermission(Car.PERMISSION_USE_CAR_TELEMETRY_SERVICE) 423 @AddedInOrBefore(majorVersion = 33) clearReportReadyListener()424 public void clearReportReadyListener() { 425 mReportReadyListenerExecutor.set(null); 426 mReportReadyListener.set(null); 427 try { 428 mService.clearReportReadyListener(); 429 } catch (RemoteException e) { 430 handleRemoteExceptionFromCarService(e); 431 } 432 } 433 434 /** Listens for report ready notifications. 435 * Atomic variables (mReportReadyListenerExecutor and mReportReadyListener) 436 * can be accessed from different threads simultaneously. 437 * Both of these variables can be set to null by {@link #clearReportReadyListener()} 438 * and simultaneously {@link #onReady(String)} may try to access the null value. 439 * So, to avoid possible NullPointerException due to this race condition, 440 * these atomic variables are needed to be retrieved in local variables 441 * and verified those are not null before accessing. */ 442 private static final class CarTelemetryReportReadyListenerImpl 443 extends ICarTelemetryReportReadyListener.Stub { 444 private final WeakReference<CarTelemetryManager> mManager; 445 CarTelemetryReportReadyListenerImpl(CarTelemetryManager manager)446 private CarTelemetryReportReadyListenerImpl(CarTelemetryManager manager) { 447 mManager = new WeakReference<>(manager); 448 } 449 450 @Override onReady(@onNull String metricsConfigName)451 public void onReady(@NonNull String metricsConfigName) { 452 CarTelemetryManager manager = mManager.get(); 453 if (manager == null) { 454 return; 455 } 456 Executor executor = manager.mReportReadyListenerExecutor.get(); 457 if (executor == null) { 458 return; 459 } 460 ReportReadyListener reportReadyListener = manager.mReportReadyListener.get(); 461 if (reportReadyListener == null) { 462 return; 463 } 464 executor.execute( 465 () -> reportReadyListener.onReady(metricsConfigName)); 466 } 467 } 468 469 /** 470 * Receives responses to {@link #getFinishedReport(String, Executor, MetricsReportCallback)} 471 * requests. 472 */ 473 private static final class CarTelemetryReportListenerImpl 474 extends ICarTelemetryReportListener.Stub { 475 476 private final Executor mExecutor; 477 private final MetricsReportCallback mMetricsReportCallback; 478 CarTelemetryReportListenerImpl(Executor executor, MetricsReportCallback callback)479 private CarTelemetryReportListenerImpl(Executor executor, MetricsReportCallback callback) { 480 Objects.requireNonNull(executor); 481 Objects.requireNonNull(callback); 482 mExecutor = executor; 483 mMetricsReportCallback = callback; 484 } 485 486 @Override onResult( @onNull String metricsConfigName, @Nullable ParcelFileDescriptor reportFileDescriptor, @Nullable byte[] telemetryError, @MetricsReportStatus int status)487 public void onResult( 488 @NonNull String metricsConfigName, 489 @Nullable ParcelFileDescriptor reportFileDescriptor, 490 @Nullable byte[] telemetryError, 491 @MetricsReportStatus int status) { 492 // return early if no need to stream reports 493 if (reportFileDescriptor == null) { 494 mExecutor.execute(() -> mMetricsReportCallback.onResult( 495 metricsConfigName, null, telemetryError, status)); 496 return; 497 } 498 // getting to this line means the reportFileDescriptor is non-null 499 ParcelFileDescriptor dup = null; 500 try { 501 dup = reportFileDescriptor.dup(); 502 } catch (IOException e) { 503 Slogf.w(TAG, "Could not dup ParcelFileDescriptor", e); 504 return; 505 } finally { 506 IoUtils.closeQuietly(reportFileDescriptor); 507 } 508 final ParcelFileDescriptor readFd = dup; 509 mExecutor.execute(() -> { 510 // read PersistableBundles from the pipe, this method will also close the fd 511 List<PersistableBundle> reports = parseReports(readFd); 512 // if a readFd is non-null, CarTelemetryService will write at least 1 report 513 // to the pipe, so something must have gone wrong to get 0 report 514 if (reports.size() == 0) { 515 mMetricsReportCallback.onResult(metricsConfigName, null, null, 516 STATUS_GET_METRICS_CONFIG_RUNTIME_ERROR); 517 return; 518 } 519 for (PersistableBundle report : reports) { 520 mMetricsReportCallback 521 .onResult(metricsConfigName, report, telemetryError, status); 522 } 523 }); 524 } 525 526 /** Helper method to parse reports (PersistableBundles) from the file descriptor. */ parseReports(ParcelFileDescriptor reportFileDescriptor)527 private List<PersistableBundle> parseReports(ParcelFileDescriptor reportFileDescriptor) { 528 List<PersistableBundle> reports = new ArrayList<>(); 529 try (DataInputStream dataInputStream = new DataInputStream( 530 new ParcelFileDescriptor.AutoCloseInputStream(reportFileDescriptor))) { 531 while (true) { 532 // read integer which tells us how many bytes to read for the PersistableBundle 533 int size = dataInputStream.readInt(); 534 byte[] bundleBytes = dataInputStream.readNBytes(size); 535 if (bundleBytes.length != size) { 536 Slogf.e(TAG, "Expected to read " + size 537 + " bytes from the pipe, but only read " 538 + bundleBytes.length + " bytes"); 539 break; 540 } 541 PersistableBundle report = PersistableBundle.readFromStream( 542 new ByteArrayInputStream(bundleBytes)); 543 reports.add(report); 544 } 545 } catch (EOFException e) { 546 // a graceful exit from the while true loop, thrown by DataInputStream#readInt(), 547 // every successful parse should naturally reach this line 548 if (DEBUG) { 549 Slogf.d(TAG, "parseReports reached end of file"); 550 } 551 } catch (IOException e) { 552 Slogf.e(TAG, "Failed to read metrics reports from pipe", e); 553 } 554 return reports; 555 } 556 } 557 } 558