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