blob: 974dc3fd3ccf4068979f4fbd4dfbe85f55c5034b [file] [log] [blame]
/*
* Copyright (C) 2021 The Android Open Source Project
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package com.android.car.telemetry;
import static android.car.telemetry.CarTelemetryManager.STATUS_ADD_METRICS_CONFIG_PARSE_FAILED;
import static android.car.telemetry.CarTelemetryManager.STATUS_ADD_METRICS_CONFIG_SUCCEEDED;
import static android.car.telemetry.CarTelemetryManager.STATUS_GET_METRICS_CONFIG_DOES_NOT_EXIST;
import static android.car.telemetry.CarTelemetryManager.STATUS_GET_METRICS_CONFIG_FINISHED;
import static android.car.telemetry.CarTelemetryManager.STATUS_GET_METRICS_CONFIG_INTERIM_RESULTS;
import static android.car.telemetry.CarTelemetryManager.STATUS_GET_METRICS_CONFIG_PENDING;
import static android.car.telemetry.CarTelemetryManager.STATUS_GET_METRICS_CONFIG_RUNTIME_ERROR;
import static java.util.stream.Collectors.toList;
import android.annotation.NonNull;
import android.annotation.Nullable;
import android.app.ActivityManager;
import android.car.Car;
import android.car.telemetry.CarTelemetryManager;
import android.car.telemetry.ICarTelemetryReportListener;
import android.car.telemetry.ICarTelemetryReportReadyListener;
import android.car.telemetry.ICarTelemetryService;
import android.content.Context;
import android.os.Handler;
import android.os.HandlerThread;
import android.os.PersistableBundle;
import android.os.RemoteException;
import android.os.ResultReceiver;
import android.os.Trace;
import android.util.ArrayMap;
import android.util.IndentingPrintWriter;
import android.util.TimingsTraceLog;
import com.android.car.CarLocalServices;
import com.android.car.CarLog;
import com.android.car.CarPropertyService;
import com.android.car.CarServiceBase;
import com.android.car.CarServiceUtils;
import com.android.car.OnShutdownReboot;
import com.android.car.systeminterface.SystemInterface;
import com.android.car.telemetry.databroker.DataBroker;
import com.android.car.telemetry.databroker.DataBrokerController;
import com.android.car.telemetry.databroker.DataBrokerImpl;
import com.android.car.telemetry.publisher.PublisherFactory;
import com.android.car.telemetry.sessioncontroller.SessionController;
import com.android.car.telemetry.systemmonitor.SystemMonitor;
import com.android.internal.annotations.VisibleForTesting;
import com.android.server.utils.Slogf;
import com.google.protobuf.InvalidProtocolBufferException;
import java.io.File;
import java.util.Arrays;
import java.util.List;
import java.util.Set;
/**
* CarTelemetryService manages OEM telemetry collection, processing and communication
* with a data upload service.
*/
public class CarTelemetryService extends ICarTelemetryService.Stub implements CarServiceBase {
public static final boolean DEBUG = true; // STOPSHIP if true
private static final String PUBLISHER_DIR = "publisher";
public static final String TELEMETRY_DIR = "telemetry";
private final Context mContext;
private final CarPropertyService mCarPropertyService;
private final HandlerThread mTelemetryThread = CarServiceUtils.getHandlerThread(
CarTelemetryService.class.getSimpleName());
private final Handler mTelemetryHandler = new Handler(mTelemetryThread.getLooper());
// accessed and updated on the main thread
private boolean mReleased = false;
// all the following fields are accessed and updated on the telemetry thread
private DataBroker mDataBroker;
private DataBrokerController mDataBrokerController;
private ICarTelemetryReportReadyListener mReportReadyListener;
private MetricsConfigStore mMetricsConfigStore;
private OnShutdownReboot mOnShutdownReboot;
private PublisherFactory mPublisherFactory;
private ResultStore mResultStore;
private SessionController mSessionController;
private SystemMonitor mSystemMonitor;
private TimingsTraceLog mTelemetryThreadTraceLog; // can only be used on telemetry thread
public CarTelemetryService(Context context, CarPropertyService carPropertyService) {
mContext = context;
mCarPropertyService = carPropertyService;
}
@Override
public void init() {
mTelemetryHandler.post(() -> {
mTelemetryThreadTraceLog = new TimingsTraceLog(
CarLog.TAG_TELEMETRY, Trace.TRACE_TAG_SYSTEM_SERVER);
mTelemetryThreadTraceLog.traceBegin("init");
SystemInterface systemInterface = CarLocalServices.getService(SystemInterface.class);
// full root directory path is /data/system/car/telemetry
File rootDirectory = new File(systemInterface.getSystemCarDir(), TELEMETRY_DIR);
File publisherDirectory = new File(rootDirectory, PUBLISHER_DIR);
publisherDirectory.mkdirs();
// initialize all necessary components
mMetricsConfigStore = new MetricsConfigStore(rootDirectory);
mResultStore = new ResultStore(rootDirectory);
mSessionController = new SessionController(mContext);
mPublisherFactory = new PublisherFactory(mCarPropertyService, mTelemetryHandler,
mContext, publisherDirectory);
mDataBroker = new DataBrokerImpl(mContext, mPublisherFactory, mResultStore,
mTelemetryThreadTraceLog);
ActivityManager activityManager = mContext.getSystemService(ActivityManager.class);
mSystemMonitor = SystemMonitor.create(activityManager, mTelemetryHandler);
// controller starts metrics collection after boot complete
mDataBrokerController = new DataBrokerController(mDataBroker, mTelemetryHandler,
mMetricsConfigStore, this::onReportReady, mSystemMonitor,
systemInterface.getSystemStateInterface(), mSessionController);
mTelemetryThreadTraceLog.traceEnd();
// save state at reboot and shutdown
mOnShutdownReboot = new OnShutdownReboot(mContext);
mOnShutdownReboot.addAction((context, intent) -> release());
});
}
@Override
public void release() {
if (mReleased) {
return;
}
mReleased = true;
mTelemetryHandler.post(() -> {
mTelemetryThreadTraceLog.traceBegin("release");
mResultStore.flushToDisk();
mOnShutdownReboot.release();
mSessionController.release();
mTelemetryThreadTraceLog.traceEnd();
});
mTelemetryThread.quitSafely();
}
@Override
public void dump(IndentingPrintWriter writer) {
writer.println("*CarTelemetryService*");
writer.println();
// Print active configs with their interim results and errors.
writer.println("Active Configs");
writer.println();
for (TelemetryProto.MetricsConfig config : mMetricsConfigStore.getActiveMetricsConfigs()) {
writer.println(" Name: " + config.getName());
writer.println(" Version: " + config.getVersion());
PersistableBundle interimResult = mResultStore.getInterimResult(config.getName());
if (interimResult != null) {
writer.println(" Interim Result");
writer.println(" Bundle keys: "
+ Arrays.toString(interimResult.keySet().toArray()));
}
writer.println();
}
// Print info on stored final results. Configs are inactive after producing final result.
ArrayMap<String, PersistableBundle> finalResults = mResultStore.getAllFinalResults();
writer.println("Final Results");
writer.println();
for (int i = 0; i < finalResults.size(); i++) {
writer.println(" Config name: " + finalResults.keyAt(i));
writer.println(" Bundle keys: "
+ Arrays.toString(finalResults.valueAt(i).keySet().toArray()));
writer.println();
}
// Print info on stored errors. Configs are inactive after producing errors.
ArrayMap<String, TelemetryProto.TelemetryError> errors = mResultStore.getAllErrorResults();
writer.println("Errors");
writer.println();
for (int i = 0; i < errors.size(); i++) {
writer.println(" Config name: " + errors.keyAt(i));
TelemetryProto.TelemetryError error = errors.valueAt(i);
writer.println(" Error");
writer.println(" Type: " + error.getErrorType());
writer.println(" Message: " + error.getMessage());
if (error.hasStackTrace() && !error.getStackTrace().isEmpty()) {
writer.println(" Stack trace: " + error.getStackTrace());
}
writer.println();
}
}
/**
* Send a telemetry metrics config to the service.
* @param metricsConfigName name of the MetricsConfig.
* @param config the serialized bytes of a MetricsConfig object.
* @param callback to send status code to CarTelemetryManager.
*/
@Override
public void addMetricsConfig(@NonNull String metricsConfigName, @NonNull byte[] config,
@NonNull ResultReceiver callback) {
mContext.enforceCallingOrSelfPermission(
Car.PERMISSION_USE_CAR_TELEMETRY_SERVICE, "addMetricsConfig");
mTelemetryHandler.post(() -> {
mTelemetryThreadTraceLog.traceBegin("addMetricsConfig");
int status = addMetricsConfigInternal(metricsConfigName, config);
callback.send(status, null);
mTelemetryThreadTraceLog.traceEnd();
});
}
/** Adds the MetricsConfig and returns the status. */
private int addMetricsConfigInternal(
@NonNull String metricsConfigName, @NonNull byte[] config) {
Slogf.d(CarLog.TAG_TELEMETRY,
"Adding metrics config: " + metricsConfigName + " to car telemetry service");
TelemetryProto.MetricsConfig metricsConfig;
try {
metricsConfig = TelemetryProto.MetricsConfig.parseFrom(config);
} catch (InvalidProtocolBufferException e) {
Slogf.e(CarLog.TAG_TELEMETRY, "Failed to parse MetricsConfig.", e);
return STATUS_ADD_METRICS_CONFIG_PARSE_FAILED;
}
if (!metricsConfig.getName().equals(metricsConfigName)) {
Slogf.e(CarLog.TAG_TELEMETRY, "Argument config name " + metricsConfigName
+ " doesn't match name in MetricsConfig (" + metricsConfig.getName() + ").");
return STATUS_ADD_METRICS_CONFIG_PARSE_FAILED;
}
int status = mMetricsConfigStore.addMetricsConfig(metricsConfig);
if (status != STATUS_ADD_METRICS_CONFIG_SUCCEEDED) {
return status;
}
// If no error (config is added to the MetricsConfigStore), remove previously collected data
// for this config and add config to the DataBroker for metrics collection.
mResultStore.removeResult(metricsConfigName);
mDataBroker.removeMetricsConfig(metricsConfigName);
mDataBroker.addMetricsConfig(metricsConfigName, metricsConfig);
// TODO(b/199410900): update logic once metrics configs have expiration dates
return STATUS_ADD_METRICS_CONFIG_SUCCEEDED;
}
/**
* Removes a metrics config based on the name. This will also remove outputs produced by the
* MetricsConfig.
*
* @param metricsConfigName the unique identifier of a MetricsConfig.
*/
@Override
public void removeMetricsConfig(@NonNull String metricsConfigName) {
mContext.enforceCallingOrSelfPermission(
Car.PERMISSION_USE_CAR_TELEMETRY_SERVICE, "removeMetricsConfig");
mTelemetryHandler.post(() -> {
if (DEBUG) {
Slogf.d(CarLog.TAG_TELEMETRY, "Removing metrics config " + metricsConfigName
+ " from car telemetry service");
}
mTelemetryThreadTraceLog.traceBegin("removeMetricsConfig");
if (mMetricsConfigStore.removeMetricsConfig(metricsConfigName)) {
mDataBroker.removeMetricsConfig(metricsConfigName);
mResultStore.removeResult(metricsConfigName);
}
mTelemetryThreadTraceLog.traceEnd();
});
}
/**
* Removes all MetricsConfigs. This will also remove all MetricsConfig outputs.
*/
@Override
public void removeAllMetricsConfigs() {
mContext.enforceCallingOrSelfPermission(
Car.PERMISSION_USE_CAR_TELEMETRY_SERVICE, "removeAllMetricsConfigs");
mTelemetryHandler.post(() -> {
mTelemetryThreadTraceLog.traceBegin("removeAllMetricsConfig");
Slogf.d(CarLog.TAG_TELEMETRY,
"Removing all metrics config from car telemetry service");
mDataBroker.removeAllMetricsConfigs();
mMetricsConfigStore.removeAllMetricsConfigs();
mResultStore.removeAllResults();
mTelemetryThreadTraceLog.traceEnd();
});
}
/**
* Sends telemetry reports associated with the given config name using the
* {@link ICarTelemetryReportListener}.
*
* @param metricsConfigName the unique identifier of a MetricsConfig.
* @param listener to receive finished report or error.
*/
@Override
public void getFinishedReport(@NonNull String metricsConfigName,
@NonNull ICarTelemetryReportListener listener) {
mContext.enforceCallingOrSelfPermission(
Car.PERMISSION_USE_CAR_TELEMETRY_SERVICE, "getFinishedReport");
mTelemetryHandler.post(() -> {
if (DEBUG) {
Slogf.d(CarLog.TAG_TELEMETRY,
"Getting report for metrics config " + metricsConfigName);
}
mTelemetryThreadTraceLog.traceBegin("getFinishedReport");
PersistableBundle report;
TelemetryProto.TelemetryError error;
if ((report = mResultStore.getFinalResult(metricsConfigName, true)) != null) {
sendResult(listener, metricsConfigName, /* report = */ report, /* error = */ null,
/* status = */ STATUS_GET_METRICS_CONFIG_FINISHED);
} else if (mResultStore.getInterimResult(metricsConfigName) != null) {
sendResult(listener, metricsConfigName, /* report = */ null, /* error = */null,
/* status = */ STATUS_GET_METRICS_CONFIG_INTERIM_RESULTS);
} else if ((error = mResultStore.getErrorResult(metricsConfigName, true)) != null) {
sendResult(listener, metricsConfigName, /* report = */ null, /* error = */ error,
/* status = */ STATUS_GET_METRICS_CONFIG_RUNTIME_ERROR);
} else if (mMetricsConfigStore.containsConfig(metricsConfigName)) {
sendResult(listener, metricsConfigName, /* report = */ null, /* error = */ null,
/* status = */ STATUS_GET_METRICS_CONFIG_PENDING);
} else {
sendResult(listener, metricsConfigName, /* report = */ null, /* error = */ null,
/* status = */ STATUS_GET_METRICS_CONFIG_DOES_NOT_EXIST);
}
mTelemetryThreadTraceLog.traceEnd();
});
}
/**
* Sends all script reports or errors using the {@link ICarTelemetryReportListener}.
*/
@Override
public void getAllFinishedReports(@NonNull ICarTelemetryReportListener listener) {
mContext.enforceCallingOrSelfPermission(
Car.PERMISSION_USE_CAR_TELEMETRY_SERVICE, "getAllFinishedReports");
mTelemetryHandler.post(() -> {
if (DEBUG) {
Slogf.d(CarLog.TAG_TELEMETRY, "Getting all reports");
}
mTelemetryThreadTraceLog.traceBegin("getAllFinishedReports");
ArrayMap<String, PersistableBundle> reports = mResultStore.getAllFinalResults();
for (int i = 0; i < reports.size(); i++) {
mResultStore.removeResult(reports.keyAt(i));
sendResult(listener, reports.keyAt(i), reports.valueAt(i),
/* error = */ null, STATUS_GET_METRICS_CONFIG_FINISHED);
}
ArrayMap<String, TelemetryProto.TelemetryError> errors =
mResultStore.getAllErrorResults();
for (int i = 0; i < errors.size(); i++) {
mResultStore.removeResult(errors.keyAt(i));
sendResult(listener, errors.keyAt(i), /* report = */ null,
errors.valueAt(i), STATUS_GET_METRICS_CONFIG_RUNTIME_ERROR);
}
mTelemetryThreadTraceLog.traceEnd();
});
}
/**
* Sets a listener for report ready notifications.
*/
@Override
public void setReportReadyListener(@NonNull ICarTelemetryReportReadyListener listener) {
mContext.enforceCallingOrSelfPermission(
Car.PERMISSION_USE_CAR_TELEMETRY_SERVICE, "setReportReadyListener");
mTelemetryHandler.post(() -> {
mReportReadyListener = listener;
Set<String> configNames = mResultStore.getFinishedMetricsConfigNames();
for (String name : configNames) {
try {
mReportReadyListener.onReady(name);
} catch (RemoteException e) {
Slogf.w(CarLog.TAG_TELEMETRY, "error with ICarTelemetryReportReadyListener", e);
}
}
});
}
/**
* Clears the listener to stop report ready notifications.
*/
@Override
public void clearReportReadyListener() {
mContext.enforceCallingOrSelfPermission(
Car.PERMISSION_USE_CAR_TELEMETRY_SERVICE, "clearReportReadyListener");
mTelemetryHandler.post(() -> mReportReadyListener = null);
}
/**
* Implementation of the functional interface {@link DataBrokerController.ReportReadyListener}.
* Invoked from {@link DataBrokerController} when a script produces a report or a runtime error.
*/
private void onReportReady(@NonNull String metricsConfigName) {
if (mReportReadyListener == null) {
return;
}
try {
mReportReadyListener.onReady(metricsConfigName);
} catch (RemoteException e) {
Slogf.w(CarLog.TAG_TELEMETRY, "error with ICarTelemetryReportReadyListener", e);
}
}
/**
* Returns the list of config names and versions. This methods is expected to be used only by
* {@code CarShellCommand} class. Other usages are not supported.
*/
@NonNull
public List<String> getActiveMetricsConfigDetails() {
return mMetricsConfigStore.getActiveMetricsConfigs().stream()
.map((config) -> config.getName() + " version=" + config.getVersion())
.collect(toList());
}
@Nullable
private byte[] getBytes(@Nullable TelemetryProto.TelemetryError error) {
if (error == null) {
return null;
}
return error.toByteArray();
}
private void sendResult(
@NonNull ICarTelemetryReportListener listener,
@NonNull String metricsConfigName,
@Nullable PersistableBundle report,
@Nullable TelemetryProto.TelemetryError error,
@CarTelemetryManager.MetricsReportStatus int status) {
try {
listener.onResult(metricsConfigName, report, getBytes(error), status);
} catch (RemoteException e) {
Slogf.w(CarLog.TAG_TELEMETRY, "error with ICarTelemetryReportListener", e);
}
}
@VisibleForTesting
Handler getTelemetryHandler() {
return mTelemetryHandler;
}
@VisibleForTesting
ResultStore getResultStore() {
return mResultStore;
}
@VisibleForTesting
MetricsConfigStore getMetricsConfigStore() {
return mMetricsConfigStore;
}
}