Add the remaining CarTelemetryManager API

This change adds the remaining APIs to CarTelemetryManager
(go/st-communication-api), which allows the client (cloud app) to send
manifest as well as receive script outputs

CarTelemetryService is enabled with
> adb shell cmd car_service enable-feature car_telemetry_service

Bug: 184087869

Test: m CarService
Test: atest CarTelemetryManagerTest

Change-Id: Ic2fd311c884bc52773a9c8fe000618a971fcbc7d
Merged-In: Ic2fd311c884bc52773a9c8fe000618a971fcbc7d
(cherry picked from commit 16d571374d60bc2920f5c0f8ccdb7e6a5eb386c8)
diff --git a/car-lib/src/android/car/telemetry/CarTelemetryManager.java b/car-lib/src/android/car/telemetry/CarTelemetryManager.java
index 24d787b..91df1dd 100644
--- a/car-lib/src/android/car/telemetry/CarTelemetryManager.java
+++ b/car-lib/src/android/car/telemetry/CarTelemetryManager.java
@@ -17,6 +17,7 @@
 package android.car.telemetry;
 
 import android.annotation.CallbackExecutor;
+import android.annotation.IntDef;
 import android.annotation.NonNull;
 import android.annotation.RequiresPermission;
 import android.car.Car;
@@ -28,6 +29,8 @@
 
 import com.android.internal.annotations.GuardedBy;
 
+import java.lang.annotation.Retention;
+import java.lang.annotation.RetentionPolicy;
 import java.lang.ref.WeakReference;
 import java.util.concurrent.Executor;
 
@@ -41,6 +44,7 @@
 
     private static final boolean DEBUG = false;
     private static final String TAG = CarTelemetryManager.class.getSimpleName();
+    private static final int MANIFEST_MAX_SIZE_BYTES = 10 * 1024; // 10 kb
 
     private final CarTelemetryServiceListener mCarTelemetryServiceListener =
             new CarTelemetryServiceListener(this);
@@ -53,6 +57,51 @@
     private Executor mExecutor;
 
     /**
+     * Status to indicate that manifest was added successfully.
+     */
+    public static final int ERROR_NONE = 0;
+
+    /**
+     * Status to indicate that add manifest failed because the same manifest based on the
+     * ManifestKey already exists.
+     */
+    public static final int ERROR_SAME_MANIFEST_EXISTS = 1;
+
+    /**
+     * Status to indicate that add manifest failed because a newer version of the manifest exists.
+     */
+    public static final int ERROR_NEWER_MANIFEST_EXISTS = 2;
+
+    /**
+     * Status to indicate that add manifest failed because CarTelemetryService is unable to parse
+     * the given byte array into a Manifest.
+     */
+    public static final int ERROR_PARSE_MANIFEST_FAILED = 3;
+
+    /**
+     * Status to indicate that add manifest failed because of failure to verify the signature of
+     * the manifest.
+     */
+    public static final int ERROR_SIGNATURE_VERIFICATION_FAILED = 4;
+
+    /**
+     * Status to indicate that add manifest failed because of a general error in cars.
+     */
+    public static final int ERROR_UNKNOWN = 5;
+
+    /** @hide */
+    @IntDef(prefix = {"ERROR_"}, value = {
+            ERROR_NONE,
+            ERROR_SAME_MANIFEST_EXISTS,
+            ERROR_NEWER_MANIFEST_EXISTS,
+            ERROR_PARSE_MANIFEST_FAILED,
+            ERROR_SIGNATURE_VERIFICATION_FAILED,
+            ERROR_UNKNOWN
+    })
+    @Retention(RetentionPolicy.SOURCE)
+    public @interface AddManifestError {}
+
+    /**
      * Application registers {@link CarTelemetryResultsListener} object to receive data from
      * {@link com.android.car.telemetry.CarTelemetryService}.
      *
@@ -169,4 +218,115 @@
             handleRemoteExceptionFromCarService(e);
         }
     }
+
+    /**
+     * Called by client to send telemetry manifest. The size of the manifest cannot exceed a
+     * predefined size. Otherwise an exception is thrown.
+     * The {@link ManifestKey} is used to uniquely identify a manifest. If a manifest of the same
+     * name already exists in {@link com.android.car.telemetry.CarTelemetryService}, then the
+     * version will be compared. If the version is strictly higher, the existing manifest will be
+     * replaced by the new one.
+     * TODO(b/185420981): Update javadoc after CarTelemetryService has concrete implementation.
+     *
+     * @param key      the unique key to identify the manifest.
+     * @param manifest the serialized bytes of a Manifest object.
+     * @return {@link #AddManifestError} to tell the result of the request.
+     * @throws IllegalArgumentException if the manifest size exceeds limit.
+     *
+     * @hide
+     */
+    @RequiresPermission(Car.PERMISSION_USE_CAR_TELEMETRY_SERVICE)
+    public @AddManifestError int addManifest(@NonNull ManifestKey key, @NonNull byte[] manifest) {
+        if (manifest.length > MANIFEST_MAX_SIZE_BYTES) {
+            throw new IllegalArgumentException("Manifest size exceeds limit.");
+        }
+        try {
+            return mService.addManifest(key, manifest);
+        } catch (RemoteException e) {
+            handleRemoteExceptionFromCarService(e);
+        }
+        return ERROR_UNKNOWN;
+    }
+
+    /**
+     * Removes a manifest from {@link com.android.car.telemetry.CarTelemetryService}. If the
+     * manifest does not exist, nothing will be removed but the status will be indicated in the
+     * return value.
+     *
+     * @param key the unique key to identify the manifest. Name and version must be exact.
+     * @return true for success, false otherwise.
+     * @hide
+     */
+    @RequiresPermission(Car.PERMISSION_USE_CAR_TELEMETRY_SERVICE)
+    public boolean removeManifest(@NonNull ManifestKey key) {
+        try {
+            return mService.removeManifest(key);
+        } catch (RemoteException e) {
+            handleRemoteExceptionFromCarService(e);
+        }
+        return false;
+    }
+
+    /**
+     * Removes all manifests from {@link com.android.car.telemetry.CarTelemetryService}.
+     *
+     * @hide
+     */
+    @RequiresPermission(Car.PERMISSION_USE_CAR_TELEMETRY_SERVICE)
+    public void removeAllManifests() {
+        try {
+            mService.removeAllManifests();
+        } catch (RemoteException e) {
+            handleRemoteExceptionFromCarService(e);
+        }
+    }
+
+    /**
+     * An asynchronous API for the client to get script execution results of a specific manifest
+     * from the {@link com.android.car.telemetry.CarTelemetryService} through the listener.
+     * This call is destructive. The returned results will be deleted from CarTelemetryService.
+     *
+     * @param key the unique key to identify the manifest.
+     * @hide
+     */
+    @RequiresPermission(Car.PERMISSION_USE_CAR_TELEMETRY_SERVICE)
+    public void sendFinishedReports(@NonNull ManifestKey key) {
+        try {
+            mService.sendFinishedReports(key);
+        } catch (RemoteException e) {
+            handleRemoteExceptionFromCarService(e);
+        }
+    }
+
+    /**
+     * An asynchronous API for the client to get all script execution results
+     * from the {@link com.android.car.telemetry.CarTelemetryService} through the listener.
+     * This call is destructive. The returned results will be deleted from CarTelemetryService.
+     *
+     * @hide
+     */
+    @RequiresPermission(Car.PERMISSION_USE_CAR_TELEMETRY_SERVICE)
+    public void sendAllFinishedReports() {
+        try {
+            mService.sendAllFinishedReports();
+        } catch (RemoteException e) {
+            handleRemoteExceptionFromCarService(e);
+        }
+    }
+
+    /**
+     * An asynchronous API for the client to get all script execution errors
+     * from the {@link com.android.car.telemetry.CarTelemetryService} through the listener.
+     * This call is destructive. The returned results will be deleted from CarTelemetryService.
+     *
+     * @hide
+     */
+    @RequiresPermission(Car.PERMISSION_USE_CAR_TELEMETRY_SERVICE)
+    public void sendScriptExecutionErrors() {
+        try {
+            mService.sendScriptExecutionErrors();
+        } catch (RemoteException e) {
+            handleRemoteExceptionFromCarService(e);
+        }
+    }
 }
diff --git a/car-lib/src/android/car/telemetry/ICarTelemetryService.aidl b/car-lib/src/android/car/telemetry/ICarTelemetryService.aidl
index e2eb3e9..09743d8 100644
--- a/car-lib/src/android/car/telemetry/ICarTelemetryService.aidl
+++ b/car-lib/src/android/car/telemetry/ICarTelemetryService.aidl
@@ -1,6 +1,7 @@
 package android.car.telemetry;
 
 import android.car.telemetry.ICarTelemetryServiceListener;
+import android.car.telemetry.ManifestKey;
 
 /**
  * Internal binder interface for {@code CarTelemetryService}, used by {@code CarTelemetryManager}.
@@ -18,4 +19,35 @@
      * Clears the listener registered with CarTelemetryService.
      */
     void clearListener();
+
+    /**
+     * Sends telemetry manifests to CarTelemetryService.
+     */
+    int addManifest(in ManifestKey key, in byte[] manifest);
+
+    /**
+     * Removes a manifest based on the key.
+     */
+    boolean removeManifest(in ManifestKey key);
+
+    /**
+     * Removes all manifests.
+     */
+    void removeAllManifests();
+
+    /**
+     * Sends script results associated with the given key using the
+     * {@code ICarTelemetryServiceListener}.
+     */
+    void sendFinishedReports(in ManifestKey key);
+
+    /**
+     * Sends all script results associated using the {@code ICarTelemetryServiceListener}.
+     */
+    void sendAllFinishedReports();
+
+    /**
+     * Sends all errors using the {@code ICarTelemetryServiceListener}.
+     */
+    void sendScriptExecutionErrors();
 }
\ No newline at end of file
diff --git a/car-lib/src/android/car/telemetry/ManifestKey.aidl b/car-lib/src/android/car/telemetry/ManifestKey.aidl
new file mode 100644
index 0000000..25097df
--- /dev/null
+++ b/car-lib/src/android/car/telemetry/ManifestKey.aidl
@@ -0,0 +1,22 @@
+/*
+ * 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 android.car.telemetry;
+
+/**
+ * @hide
+ */
+parcelable ManifestKey;
\ No newline at end of file
diff --git a/car-lib/src/android/car/telemetry/ManifestKey.java b/car-lib/src/android/car/telemetry/ManifestKey.java
new file mode 100644
index 0000000..b0a69c2
--- /dev/null
+++ b/car-lib/src/android/car/telemetry/ManifestKey.java
@@ -0,0 +1,76 @@
+/*
+ * 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 android.car.telemetry;
+
+import android.annotation.NonNull;
+import android.os.Parcel;
+import android.os.Parcelable;
+
+/**
+ * A parcelable that wraps around the Manifest name and version.
+ *
+ * @hide
+ */
+public final class ManifestKey implements Parcelable {
+
+    @NonNull
+    private String mName;
+    private int mVersion;
+
+    @NonNull
+    public String getName() {
+        return mName;
+    }
+
+    public int getVersion() {
+        return mVersion;
+    }
+
+    @Override
+    public void writeToParcel(@NonNull Parcel out, int flags) {
+        out.writeString(mName);
+        out.writeInt(mVersion);
+    }
+
+    private ManifestKey(Parcel in) {
+        mName = in.readString();
+        mVersion = in.readInt();
+    }
+
+    public ManifestKey(@NonNull String name, int version) {
+        mName = name;
+        mVersion = version;
+    }
+
+    @Override
+    public int describeContents() {
+        return 0;
+    }
+
+    public static final @NonNull Parcelable.Creator<ManifestKey> CREATOR =
+            new Parcelable.Creator<ManifestKey>() {
+                @Override
+                public ManifestKey createFromParcel(Parcel in) {
+                    return new ManifestKey(in);
+                }
+
+                @Override
+                public ManifestKey[] newArray(int size) {
+                    return new ManifestKey[size];
+                }
+            };
+}
diff --git a/car-test-lib/src/android/car/testapi/CarTelemetryController.java b/car-test-lib/src/android/car/testapi/CarTelemetryController.java
index 1f57497..e4df3f3 100644
--- a/car-test-lib/src/android/car/testapi/CarTelemetryController.java
+++ b/car-test-lib/src/android/car/testapi/CarTelemetryController.java
@@ -16,6 +16,8 @@
 
 package android.car.testapi;
 
+import android.car.telemetry.ManifestKey;
+
 /**
  * Controller to manipulate and verify {@link android.car.telemetry.CarTelemetryManager} in
  * unit tests.
@@ -27,4 +29,20 @@
      * registered with the manager, otherwise returns {@code false}.
      */
     boolean isListenerSet();
+
+    /**
+     * Returns the number of valid manifests registered with the manager.
+     */
+    int getValidManifestsCount();
+
+    /**
+     * Associate a blob of data with the given key, used for testing the flush reports APIs.
+     */
+    void addDataForKey(ManifestKey key, byte[] data);
+
+    /**
+     * Configure the blob of data to be flushed with the
+     * {@code FakeCarTelemetryService#flushScriptExecutionErrors()} API.
+     */
+    void setErrorData(byte[] error);
 }
diff --git a/car-test-lib/src/android/car/testapi/FakeCarTelemetryService.java b/car-test-lib/src/android/car/testapi/FakeCarTelemetryService.java
index af8a2b4..407d29f 100644
--- a/car-test-lib/src/android/car/testapi/FakeCarTelemetryService.java
+++ b/car-test-lib/src/android/car/testapi/FakeCarTelemetryService.java
@@ -16,8 +16,18 @@
 
 package android.car.testapi;
 
+import static android.car.telemetry.CarTelemetryManager.ERROR_NEWER_MANIFEST_EXISTS;
+import static android.car.telemetry.CarTelemetryManager.ERROR_NONE;
+import static android.car.telemetry.CarTelemetryManager.ERROR_SAME_MANIFEST_EXISTS;
+
+import android.car.telemetry.CarTelemetryManager.AddManifestError;
 import android.car.telemetry.ICarTelemetryService;
 import android.car.telemetry.ICarTelemetryServiceListener;
+import android.car.telemetry.ManifestKey;
+import android.os.RemoteException;
+
+import java.util.HashMap;
+import java.util.Map;
 
 /**
  * A fake implementation of {@link ICarTelemetryService.Stub} to facilitate the use of
@@ -28,8 +38,13 @@
 public class FakeCarTelemetryService extends ICarTelemetryService.Stub implements
         CarTelemetryController {
 
+    private byte[] mErrorBytes;
     private ICarTelemetryServiceListener mListener;
 
+    private final Map<String, Integer> mNameVersionMap = new HashMap<>();
+    private final Map<ManifestKey, byte[]> mManifestMap = new HashMap<>();
+    private final Map<ManifestKey, byte[]> mScriptResultMap = new HashMap<>();
+
     @Override
     public void setListener(ICarTelemetryServiceListener listener) {
         mListener = listener;
@@ -40,9 +55,74 @@
         mListener = null;
     }
 
+    @Override
+    public @AddManifestError int addManifest(ManifestKey key, byte[] manifest) {
+        if (mNameVersionMap.getOrDefault(key.getName(), 0) > key.getVersion()) {
+            return ERROR_NEWER_MANIFEST_EXISTS;
+        } else if (mNameVersionMap.getOrDefault(key.getName(), 0) == key.getVersion()) {
+            return ERROR_SAME_MANIFEST_EXISTS;
+        }
+        mNameVersionMap.put(key.getName(), key.getVersion());
+        mManifestMap.put(key, manifest);
+        return ERROR_NONE;
+    }
+
+    @Override
+    public boolean removeManifest(ManifestKey key) {
+        if (!mManifestMap.containsKey(key)) {
+            return false;
+        }
+        mNameVersionMap.remove(key.getName());
+        mManifestMap.remove(key);
+        return true;
+    }
+
+    @Override
+    public void removeAllManifests() {
+        mNameVersionMap.clear();
+        mManifestMap.clear();
+    }
+
+    @Override
+    public void sendFinishedReports(ManifestKey key) throws RemoteException {
+        if (!mScriptResultMap.containsKey(key)) {
+            return;
+        }
+        mListener.onDataReceived(mScriptResultMap.get(key));
+        mScriptResultMap.remove(key);
+    }
+
+    @Override
+    public void sendAllFinishedReports() throws RemoteException {
+        for (byte[] data : mScriptResultMap.values()) {
+            mListener.onDataReceived(data);
+        }
+        mScriptResultMap.clear();
+    }
+
+    @Override
+    public void sendScriptExecutionErrors() throws RemoteException {
+        mListener.onDataReceived(mErrorBytes);
+    }
+
     /**************************** CarTelemetryController impl ********************************/
     @Override
     public boolean isListenerSet() {
         return mListener != null;
     }
+
+    @Override
+    public int getValidManifestsCount() {
+        return mManifestMap.size();
+    }
+
+    @Override
+    public void addDataForKey(ManifestKey key, byte[] data) {
+        mScriptResultMap.put(key, data);
+    }
+
+    @Override
+    public void setErrorData(byte[] error) {
+        mErrorBytes = error;
+    }
 }
diff --git a/service/src/com/android/car/telemetry/CarTelemetryService.java b/service/src/com/android/car/telemetry/CarTelemetryService.java
index b68e020..74f4509 100644
--- a/service/src/com/android/car/telemetry/CarTelemetryService.java
+++ b/service/src/com/android/car/telemetry/CarTelemetryService.java
@@ -15,10 +15,14 @@
  */
 package com.android.car.telemetry;
 
+import static android.car.telemetry.CarTelemetryManager.ERROR_NONE;
+
 import android.annotation.NonNull;
 import android.car.Car;
+import android.car.telemetry.CarTelemetryManager.AddManifestError;
 import android.car.telemetry.ICarTelemetryService;
 import android.car.telemetry.ICarTelemetryServiceListener;
+import android.car.telemetry.ManifestKey;
 import android.content.Context;
 import android.util.IndentingPrintWriter;
 import android.util.Slog;
@@ -62,7 +66,7 @@
      */
     @Override
     public void setListener(@NonNull ICarTelemetryServiceListener listener) {
-        // TODO(b/150978930): verify that only a hardcoded app can set the listener
+        // TODO(b/184890506): verify that only a hardcoded app can set the listener
         mContext.enforceCallingOrSelfPermission(
                 Car.PERMISSION_USE_CAR_TELEMETRY_SERVICE, "setListener");
         if (DEBUG) {
@@ -83,4 +87,89 @@
         }
         mListener = null;
     }
+
+    /**
+     * Allows client to send telemetry manifests.
+     *
+     * @param key      the unique key to identify the manifest.
+     * @param manifest the serialized bytes of a Manifest object.
+     * @return {@link AddManifestError} the error code.
+     */
+    @Override
+    public @AddManifestError int addManifest(@NonNull ManifestKey key, @NonNull byte[] manifest) {
+        // TODO(b/184087869): Implement
+        mContext.enforceCallingOrSelfPermission(
+                Car.PERMISSION_USE_CAR_TELEMETRY_SERVICE, "setListener");
+        if (DEBUG) {
+            Slog.d(TAG, "Adding manifest to car telemetry service");
+        }
+        return ERROR_NONE;
+    }
+
+    /**
+     * Removes a manifest based on the key.
+     */
+    @Override
+    public boolean removeManifest(@NonNull ManifestKey key) {
+        // TODO(b/184087869): Implement
+        mContext.enforceCallingOrSelfPermission(
+                Car.PERMISSION_USE_CAR_TELEMETRY_SERVICE, "setListener");
+        if (DEBUG) {
+            Slog.d(TAG, "Removing manifest from car telemetry service");
+        }
+        return true;
+    }
+
+    /**
+     * Removes all manifests.
+     */
+    @Override
+    public void removeAllManifests() {
+        // TODO(b/184087869): Implement
+        mContext.enforceCallingOrSelfPermission(
+                Car.PERMISSION_USE_CAR_TELEMETRY_SERVICE, "setListener");
+        if (DEBUG) {
+            Slog.d(TAG, "Removing all manifest from car telemetry service");
+        }
+    }
+
+    /**
+     * Sends script results associated with the given key using the
+     * {@link ICarTelemetryServiceListener}.
+     */
+    @Override
+    public void sendFinishedReports(@NonNull ManifestKey key) {
+        // TODO(b/184087869): Implement
+        mContext.enforceCallingOrSelfPermission(
+                Car.PERMISSION_USE_CAR_TELEMETRY_SERVICE, "setListener");
+        if (DEBUG) {
+            Slog.d(TAG, "Flushing reports for a manifest");
+        }
+    }
+
+    /**
+     * Sends all script results associated using the {@link ICarTelemetryServiceListener}.
+     */
+    @Override
+    public void sendAllFinishedReports() {
+        // TODO(b/184087869): Implement
+        mContext.enforceCallingOrSelfPermission(
+                Car.PERMISSION_USE_CAR_TELEMETRY_SERVICE, "setListener");
+        if (DEBUG) {
+            Slog.d(TAG, "Flushing all reports");
+        }
+    }
+
+    /**
+     * Sends all errors using the {@link ICarTelemetryServiceListener}.
+     */
+    @Override
+    public void sendScriptExecutionErrors() {
+        // TODO(b/184087869): Implement
+        mContext.enforceCallingOrSelfPermission(
+                Car.PERMISSION_USE_CAR_TELEMETRY_SERVICE, "setListener");
+        if (DEBUG) {
+            Slog.d(TAG, "Flushing script execution errors");
+        }
+    }
 }
diff --git a/tests/CarLibTests/src/android/car/CarTelemetryManagerTest.java b/tests/CarLibTests/src/android/car/CarTelemetryManagerTest.java
index 90b5f7a..eb037af 100644
--- a/tests/CarLibTests/src/android/car/CarTelemetryManagerTest.java
+++ b/tests/CarLibTests/src/android/car/CarTelemetryManagerTest.java
@@ -16,10 +16,17 @@
 
 package android.car;
 
+import static android.car.telemetry.CarTelemetryManager.ERROR_NONE;
+import static android.car.telemetry.CarTelemetryManager.ERROR_SAME_MANIFEST_EXISTS;
+
 import static com.google.common.truth.Truth.assertThat;
 
+import static org.mockito.Mockito.times;
+import static org.mockito.Mockito.verify;
+
 import android.app.Application;
 import android.car.telemetry.CarTelemetryManager;
+import android.car.telemetry.ManifestKey;
 import android.car.testapi.CarTelemetryController;
 import android.car.testapi.FakeCar;
 
@@ -43,10 +50,16 @@
     @Rule
     public MockitoRule rule = MockitoJUnit.rule();
 
+    private static final byte[] ERROR_BYTES = "ERROR".getBytes();
+    private static final byte[] MANIFEST_BYTES = "MANIFEST".getBytes();
+    private static final byte[] SCRIPT_RESULT_BYTES = "SCRIPT RESULT".getBytes();
+    private static final ManifestKey DEFAULT_MANIFEST_KEY =
+            new ManifestKey("NAME", 1);
     private static final Executor DIRECT_EXECUTOR = Runnable::run;
 
     private CarTelemetryController mCarTelemetryController;
     private CarTelemetryManager mCarTelemetryManager;
+
     @Mock
     private CarTelemetryManager.CarTelemetryResultsListener mListener;
 
@@ -77,4 +90,85 @@
 
         assertThat(mCarTelemetryController.isListenerSet()).isFalse();
     }
+
+    @Test
+    public void addManifest_whenNew_shouldSucceed() {
+        int result = mCarTelemetryManager.addManifest(DEFAULT_MANIFEST_KEY, MANIFEST_BYTES);
+
+        assertThat(result).isEqualTo(ERROR_NONE);
+        assertThat(mCarTelemetryController.getValidManifestsCount()).isEqualTo(1);
+    }
+
+    @Test
+    public void addManifest_whenDuplicate_shouldIgnore() {
+        int firstResult =
+                mCarTelemetryManager.addManifest(DEFAULT_MANIFEST_KEY, MANIFEST_BYTES);
+        int secondResult =
+                mCarTelemetryManager.addManifest(DEFAULT_MANIFEST_KEY, MANIFEST_BYTES);
+
+        assertThat(firstResult).isEqualTo(ERROR_NONE);
+        assertThat(secondResult).isEqualTo(ERROR_SAME_MANIFEST_EXISTS);
+        assertThat(mCarTelemetryController.getValidManifestsCount()).isEqualTo(1);
+    }
+
+    @Test
+    public void removeManifest_whenValid_shouldSucceed() {
+        mCarTelemetryManager.addManifest(DEFAULT_MANIFEST_KEY, MANIFEST_BYTES);
+
+        boolean result = mCarTelemetryManager.removeManifest(DEFAULT_MANIFEST_KEY);
+
+        assertThat(result).isTrue();
+        assertThat(mCarTelemetryController.getValidManifestsCount()).isEqualTo(0);
+    }
+
+    @Test
+    public void removeManifest_whenInvalid_shouldIgnore() {
+        mCarTelemetryManager.addManifest(DEFAULT_MANIFEST_KEY, MANIFEST_BYTES);
+
+        boolean result = mCarTelemetryManager.removeManifest(new ManifestKey("NAME", 100));
+
+        assertThat(result).isFalse();
+        assertThat(mCarTelemetryController.getValidManifestsCount()).isEqualTo(1);
+    }
+
+    @Test
+    public void removeAllManifests_shouldSucceed() {
+        mCarTelemetryManager.addManifest(DEFAULT_MANIFEST_KEY, MANIFEST_BYTES);
+        mCarTelemetryManager.addManifest(new ManifestKey("NAME", 100), MANIFEST_BYTES);
+
+        mCarTelemetryManager.removeAllManifests();
+
+        assertThat(mCarTelemetryController.getValidManifestsCount()).isEqualTo(0);
+    }
+
+    @Test
+    public void sendFinishedReports_shouldSucceed() {
+        mCarTelemetryManager.setListener(DIRECT_EXECUTOR, mListener);
+        mCarTelemetryController.addDataForKey(DEFAULT_MANIFEST_KEY, SCRIPT_RESULT_BYTES);
+
+        mCarTelemetryManager.sendFinishedReports(DEFAULT_MANIFEST_KEY);
+
+        verify(mListener).onDataReceived(SCRIPT_RESULT_BYTES);
+    }
+
+    @Test
+    public void sendAllFinishedReports_shouldSucceed() {
+        mCarTelemetryManager.setListener(DIRECT_EXECUTOR, mListener);
+        mCarTelemetryController.addDataForKey(DEFAULT_MANIFEST_KEY, SCRIPT_RESULT_BYTES);
+        mCarTelemetryController.addDataForKey(new ManifestKey("key name", 1), SCRIPT_RESULT_BYTES);
+
+        mCarTelemetryManager.sendAllFinishedReports();
+
+        verify(mListener, times(2)).onDataReceived(SCRIPT_RESULT_BYTES);
+    }
+
+    @Test
+    public void sendScriptExecutionErrors_shouldSucceed() {
+        mCarTelemetryManager.setListener(DIRECT_EXECUTOR, mListener);
+        mCarTelemetryController.setErrorData(ERROR_BYTES);
+
+        mCarTelemetryManager.sendScriptExecutionErrors();
+
+        verify(mListener).onDataReceived(ERROR_BYTES);
+    }
 }