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