[CarTelemetryService] Add hello world kitchensink test

Bug: 200092147
Test: atest CarServiceUnitTest:DataBrokerTest
Test: manual testing
1. Flash seahawk
2. Open kitchen sink app
3. Click on Send MetricsConfig onGearChange
4. Verify status code is 0 (success)
5. Inject vehicle property event adb shell cmd car_service inject-vhal-event 0x11400400 2
6. Click on Get onGearChange Report
7. Wait a few moments and verify output is "Hello World!"

Change-Id: Ia599c19dbb927a6a5aed0c0c221e9648d6ae4c66
diff --git a/car-lib/src/android/car/telemetry/MetricsConfigKey.java b/car-lib/src/android/car/telemetry/MetricsConfigKey.java
index 6e4614b..ddc9718 100644
--- a/car-lib/src/android/car/telemetry/MetricsConfigKey.java
+++ b/car-lib/src/android/car/telemetry/MetricsConfigKey.java
@@ -20,6 +20,8 @@
 import android.os.Parcel;
 import android.os.Parcelable;
 
+import java.util.Objects;
+
 /**
  * A parcelable that wraps around the Manifest name and version.
  *
@@ -61,6 +63,20 @@
         return 0;
     }
 
+    @Override
+    public boolean equals(Object o) {
+        if (!(o instanceof MetricsConfigKey)) {
+            return false;
+        }
+        MetricsConfigKey other = (MetricsConfigKey) o;
+        return mName.equals(other.getName()) && mVersion == other.getVersion();
+    }
+
+    @Override
+    public int hashCode() {
+        return Objects.hash(mName, mVersion);
+    }
+
     public static final @NonNull Parcelable.Creator<MetricsConfigKey> CREATOR =
             new Parcelable.Creator<MetricsConfigKey>() {
                 @Override
diff --git a/packages/ScriptExecutor/src/ScriptExecutorJni.cpp b/packages/ScriptExecutor/src/ScriptExecutorJni.cpp
index 6cb0670..c11a30b 100644
--- a/packages/ScriptExecutor/src/ScriptExecutorJni.cpp
+++ b/packages/ScriptExecutor/src/ScriptExecutorJni.cpp
@@ -111,9 +111,7 @@
     // TODO(b/189241508): Provide implementation to parse publishedData input,
     // convert it into Lua table and push into Lua stack.
     if (publishedData) {
-        env->ThrowNew(env->FindClass("java/lang/RuntimeException"),
-                      "Parsing of publishedData is not implemented yet.");
-        return;
+        LOG(WARNING) << "Parsing of publishedData is not implemented yet.";
     }
 
     // Unpack bundle in savedState, convert to Lua table and push it to Lua
diff --git a/service/src/com/android/car/telemetry/MetricsConfigStore.java b/service/src/com/android/car/telemetry/MetricsConfigStore.java
index 7a5512a..fac0b2e 100644
--- a/service/src/com/android/car/telemetry/MetricsConfigStore.java
+++ b/service/src/com/android/car/telemetry/MetricsConfigStore.java
@@ -106,6 +106,7 @@
     /** Deletes the MetricsConfig from disk. Returns the success status. */
     boolean deleteMetricsConfig(String metricsConfigName) {
         mActiveConfigs.remove(metricsConfigName);
+        mNameVersionMap.remove(metricsConfigName);
         return new File(mConfigDirectory, metricsConfigName).delete();
     }
 
diff --git a/service/src/com/android/car/telemetry/databroker/DataBrokerImpl.java b/service/src/com/android/car/telemetry/databroker/DataBrokerImpl.java
index f1fbd24..f4b3e6a 100644
--- a/service/src/com/android/car/telemetry/databroker/DataBrokerImpl.java
+++ b/service/src/com/android/car/telemetry/databroker/DataBrokerImpl.java
@@ -59,6 +59,9 @@
     private static final int MSG_HANDLE_TASK = 1;
     private static final int MSG_BIND_TO_SCRIPT_EXECUTOR = 2;
 
+    /** Bind to script executor 5 times before entering disabled state. */
+    private static final int MAX_BIND_SCRIPT_EXECUTOR_ATTEMPTS = 5;
+
     private static final String SCRIPT_EXECUTOR_PACKAGE = "com.android.car.scriptexecutor";
     private static final String SCRIPT_EXECUTOR_CLASS =
             "com.android.car.scriptexecutor.ScriptExecutor";
@@ -87,9 +90,15 @@
      */
     private boolean mDisabled = false;
 
+    /** Current number of attempts to bind to ScriptExecutor. */
+    private int mBindScriptExecutorAttempts = 0;
+
     /** Priority of current system to determine if a {@link ScriptExecutionTask} can run. */
     private int mPriority = 1;
 
+    /** Waiting period between attempts to bind script executor. Can be shortened for tests. */
+    @VisibleForTesting long mBindScriptExecutorDelayMillis = 3_000L;
+
     /**
      * Name of the script that's currently running. If no script is running, value is null.
      * A non-null script name indicates a script is running, which means DataBroker should not
@@ -144,9 +153,21 @@
                 mServiceConnection,
                 Context.BIND_AUTO_CREATE,
                 UserHandle.SYSTEM);
-        if (!success) {
-            Slog.w(CarLog.TAG_TELEMETRY, "failed to get valid connection to ScriptExecutor");
-            unbindScriptExecutor();
+        if (success) {
+            mBindScriptExecutorAttempts = 0; // reset
+            return;
+        }
+        unbindScriptExecutor();
+        mBindScriptExecutorAttempts++;
+        if (mBindScriptExecutorAttempts < MAX_BIND_SCRIPT_EXECUTOR_ATTEMPTS) {
+            Slog.w(CarLog.TAG_TELEMETRY,
+                    "failed to get valid connection to ScriptExecutor, retrying in "
+                            + mBindScriptExecutorDelayMillis + "ms.");
+            mTelemetryHandler.sendEmptyMessageDelayed(MSG_BIND_TO_SCRIPT_EXECUTOR,
+                    mBindScriptExecutorDelayMillis);
+        } else {
+            Slog.w(CarLog.TAG_TELEMETRY, "failed to get valid connection to ScriptExecutor, "
+                    + "disabling DataBroker");
             disableBroker();
         }
     }
@@ -166,7 +187,10 @@
         }
     }
 
-    /** Enters into a disabled state because something irrecoverable happened. */
+    /**
+     * Enters into a disabled state because something irrecoverable happened.
+     * TODO(b/200841260): expose the state to the caller.
+     */
     private void disableBroker() {
         mDisabled = true;
         // remove all MetricConfigs, disable all publishers, stop receiving data
@@ -323,6 +347,7 @@
                 // upon successful binding, a task will be scheduled to run if there are any
                 mTelemetryHandler.sendEmptyMessage(MSG_BIND_TO_SCRIPT_EXECUTOR);
             } else {
+                Slog.d(CarLog.TAG_TELEMETRY, "invoking script executor");
                 // update current name because a script is currently running
                 mCurrentScriptName = task.getMetricsConfig().getName();
                 mScriptExecutor.invokeScript(
diff --git a/tests/EmbeddedKitchenSinkApp/Android.bp b/tests/EmbeddedKitchenSinkApp/Android.bp
index ef70cc9..3226c9e 100644
--- a/tests/EmbeddedKitchenSinkApp/Android.bp
+++ b/tests/EmbeddedKitchenSinkApp/Android.bp
@@ -52,6 +52,7 @@
         "com.android.car.keventreader-client",
         "guava",
         "android.car.cluster.navigation",
+        "cartelemetry-protos",
         "car-experimental-api-static-lib",
     ],
 
diff --git a/tests/EmbeddedKitchenSinkApp/res/layout/car_telemetry_test.xml b/tests/EmbeddedKitchenSinkApp/res/layout/car_telemetry_test.xml
new file mode 100644
index 0000000..2576b7d
--- /dev/null
+++ b/tests/EmbeddedKitchenSinkApp/res/layout/car_telemetry_test.xml
@@ -0,0 +1,47 @@
+<!--
+  ~ 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.
+  -->
+
+<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
+              android:layout_width="match_parent"
+              android:layout_height="match_parent"
+              android:orientation="vertical">
+    <LinearLayout
+        android:id="@+id/on_gear_change_layout"
+        android:layout_width="match_parent"
+        android:layout_height="wrap_content"
+        android:orientation="horizontal">
+        <Button
+            android:id="@+id/send_on_gear_change_config"
+            android:layout_width="wrap_content"
+            android:layout_height="wrap_content"
+            android:text="@string/send_on_gear_change"/>
+        <Button
+            android:id="@+id/remove_on_gear_change_config"
+            android:layout_width="wrap_content"
+            android:layout_height="wrap_content"
+            android:text="@string/remove_on_gear_change"/>
+        <Button
+            android:id="@+id/get_on_gear_change_report"
+            android:layout_width="wrap_content"
+            android:layout_height="wrap_content"
+            android:text="@string/get_on_gear_change"/>
+    </LinearLayout>
+    <TextView
+        android:id="@+id/output_textview"
+        android:layout_width="wrap_content"
+        android:layout_height="wrap_content"
+        android:layout_marginTop="20dp"/>
+</LinearLayout>
diff --git a/tests/EmbeddedKitchenSinkApp/res/values/strings.xml b/tests/EmbeddedKitchenSinkApp/res/values/strings.xml
index 2fb2de5..d43e715 100644
--- a/tests/EmbeddedKitchenSinkApp/res/values/strings.xml
+++ b/tests/EmbeddedKitchenSinkApp/res/values/strings.xml
@@ -381,4 +381,9 @@
     <!-- Fullscreen Activity Test -->
     <string name="nav_to_full_screen" translatable="false">Navigate to Full Screen</string>
     <string name="cancel" translatable="false">Cancel</string>
+
+    <!-- CarTelemetryService Test -->
+    <string name="send_on_gear_change" translatable="false">Send MetricsConfig on_gear_change</string>
+    <string name="remove_on_gear_change" translatable="false">Remove MetricsConfig on_gear_change</string>
+    <string name="get_on_gear_change" translatable="false">Get on_gear_change Report</string>
 </resources>
diff --git a/tests/EmbeddedKitchenSinkApp/src/com/google/android/car/kitchensink/KitchenSinkActivity.java b/tests/EmbeddedKitchenSinkApp/src/com/google/android/car/kitchensink/KitchenSinkActivity.java
index beeac2a..57ea5f6 100644
--- a/tests/EmbeddedKitchenSinkApp/src/com/google/android/car/kitchensink/KitchenSinkActivity.java
+++ b/tests/EmbeddedKitchenSinkApp/src/com/google/android/car/kitchensink/KitchenSinkActivity.java
@@ -23,6 +23,7 @@
 import android.car.hardware.hvac.CarHvacManager;
 import android.car.hardware.power.CarPowerManager;
 import android.car.hardware.property.CarPropertyManager;
+import android.car.telemetry.CarTelemetryManager;
 import android.car.watchdog.CarWatchdogManager;
 import android.content.Context;
 import android.content.Intent;
@@ -72,6 +73,7 @@
 import com.google.android.car.kitchensink.storagevolumes.StorageVolumesFragment;
 import com.google.android.car.kitchensink.systembars.SystemBarsFragment;
 import com.google.android.car.kitchensink.systemfeatures.SystemFeaturesFragment;
+import com.google.android.car.kitchensink.telemetry.CarTelemetryTestFragment;
 import com.google.android.car.kitchensink.touch.TouchTestFragment;
 import com.google.android.car.kitchensink.users.ProfileUserFragment;
 import com.google.android.car.kitchensink.users.UserFragment;
@@ -205,6 +207,7 @@
             new FragmentMenuEntry("storage volumes", StorageVolumesFragment.class),
             new FragmentMenuEntry("system bars", SystemBarsFragment.class),
             new FragmentMenuEntry("system features", SystemFeaturesFragment.class),
+            new FragmentMenuEntry("telemetry", CarTelemetryTestFragment.class),
             new FragmentMenuEntry("touch test", TouchTestFragment.class),
             new FragmentMenuEntry("users", UserFragment.class),
             new FragmentMenuEntry("user restrictions", UserRestrictionsFragment.class),
@@ -223,6 +226,7 @@
     private CarSensorManager mSensorManager;
     private CarAppFocusManager mCarAppFocusManager;
     private CarProjectionManager mCarProjectionManager;
+    private CarTelemetryManager mCarTelemetryManager;
     private CarWatchdogManager mCarWatchdogManager;
     private Object mPropertyManagerReady = new Object();
 
@@ -250,6 +254,10 @@
         return mCarProjectionManager;
     }
 
+    public CarTelemetryManager getCarTelemetryManager() {
+        return mCarTelemetryManager;
+    }
+
     public CarWatchdogManager getCarWatchdogManager() {
         return mCarWatchdogManager;
     }
@@ -411,6 +419,8 @@
                     (CarAppFocusManager) car.getCarManager(Car.APP_FOCUS_SERVICE);
             mCarProjectionManager =
                     (CarProjectionManager) car.getCarManager(Car.PROJECTION_SERVICE);
+            mCarTelemetryManager =
+                    (CarTelemetryManager) car.getCarManager(Car.CAR_TELEMETRY_SERVICE);
             mCarWatchdogManager =
                     (CarWatchdogManager) car.getCarManager(Car.CAR_WATCHDOG_SERVICE);
             mPropertyManagerReady.notifyAll();
diff --git a/tests/EmbeddedKitchenSinkApp/src/com/google/android/car/kitchensink/telemetry/CarTelemetryTestFragment.java b/tests/EmbeddedKitchenSinkApp/src/com/google/android/car/kitchensink/telemetry/CarTelemetryTestFragment.java
new file mode 100644
index 0000000..7c4a4b1
--- /dev/null
+++ b/tests/EmbeddedKitchenSinkApp/src/com/google/android/car/kitchensink/telemetry/CarTelemetryTestFragment.java
@@ -0,0 +1,170 @@
+/*
+ * 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.google.android.car.kitchensink.telemetry;
+
+import android.annotation.NonNull;
+import android.annotation.Nullable;
+import android.car.telemetry.CarTelemetryManager;
+import android.car.telemetry.MetricsConfigKey;
+import android.hardware.automotive.vehicle.V2_0.VehicleProperty;
+import android.os.Bundle;
+import android.os.PersistableBundle;
+import android.view.LayoutInflater;
+import android.view.View;
+import android.view.ViewGroup;
+import android.widget.Button;
+import android.widget.TextView;
+
+import androidx.fragment.app.Fragment;
+
+import com.android.car.telemetry.TelemetryProto;
+
+import com.google.android.car.kitchensink.KitchenSinkActivity;
+import com.google.android.car.kitchensink.R;
+
+import java.io.ByteArrayInputStream;
+import java.io.IOException;
+import java.util.concurrent.Executor;
+import java.util.concurrent.Executors;
+
+public class CarTelemetryTestFragment extends Fragment {
+    private static final String LUA_SCRIPT_ON_GEAR_CHANGE =
+            "function onGearChange(state)\n"
+                    + "    result = {data = \"Hello World!\"}\n"
+                    + "    on_script_finished(result)\n"
+                    + "end\n";
+    private static final TelemetryProto.Publisher VEHICLE_PROPERTY_PUBLISHER =
+            TelemetryProto.Publisher.newBuilder()
+                    .setVehicleProperty(
+                            TelemetryProto.VehiclePropertyPublisher.newBuilder()
+                                    .setVehiclePropertyId(VehicleProperty.GEAR_SELECTION)
+                                    .setReadRate(0f)
+                                    .build()
+                    ).build();
+    private static final TelemetryProto.Subscriber VEHICLE_PROPERTY_SUBSCRIBER =
+            TelemetryProto.Subscriber.newBuilder()
+                    .setHandler("onGearChange")
+                    .setPublisher(VEHICLE_PROPERTY_PUBLISHER)
+                    .setPriority(0)
+                    .build();
+    private static final TelemetryProto.MetricsConfig METRICS_CONFIG_ON_GEAR_CHANGE_V1 =
+            TelemetryProto.MetricsConfig.newBuilder()
+                    .setName("my_metrics_config")
+                    .setVersion(1)
+                    .setScript(LUA_SCRIPT_ON_GEAR_CHANGE)
+                    .addSubscribers(VEHICLE_PROPERTY_SUBSCRIBER)
+                    .build();
+    private static final MetricsConfigKey KEY_V1 = new MetricsConfigKey(
+            METRICS_CONFIG_ON_GEAR_CHANGE_V1.getName(),
+            METRICS_CONFIG_ON_GEAR_CHANGE_V1.getVersion());
+
+    private final Executor mExecutor = Executors.newSingleThreadExecutor();
+
+    private CarTelemetryManager mCarTelemetryManager;
+    private CarTelemetryResultsListenerImpl mListener;
+    private KitchenSinkActivity mActivity;
+    private TextView mOutputTextView;
+
+    @Override
+    public void onCreate(@Nullable Bundle savedInstanceState) {
+        mActivity = (KitchenSinkActivity) getActivity();
+        mCarTelemetryManager = mActivity.getCarTelemetryManager();
+        mListener = new CarTelemetryResultsListenerImpl();
+        mCarTelemetryManager.setListener(mExecutor, mListener);
+        super.onCreate(savedInstanceState);
+    }
+
+    @Nullable
+    @Override
+    public View onCreateView(
+            @NonNull LayoutInflater inflater,
+            @Nullable ViewGroup container,
+            @Nullable Bundle savedInstanceState) {
+        View view = inflater.inflate(R.layout.car_telemetry_test, container, false);
+
+        mOutputTextView = view.findViewById(R.id.output_textview);
+        Button sendGearConfigBtn = view.findViewById(R.id.send_on_gear_change_config);
+        Button getGearReportBtn = view.findViewById(R.id.get_on_gear_change_report);
+        Button removeGearConfigBtn = view.findViewById(R.id.remove_on_gear_change_config);
+
+        sendGearConfigBtn.setOnClickListener(this::onSendGearChangeConfigBtnClick);
+        removeGearConfigBtn.setOnClickListener(this::onRemoveGearChangeConfigBtnClick);
+        getGearReportBtn.setOnClickListener(this::onGetGearChangeReportBtnClick);
+
+        return view;
+    }
+
+    private void showOutput(String s) {
+        mActivity.runOnUiThread(() -> mOutputTextView.setText(s));
+    }
+
+    private void onSendGearChangeConfigBtnClick(View view) {
+        showOutput("Sending MetricsConfig that listen for gear change...");
+        mCarTelemetryManager.addMetricsConfig(KEY_V1,
+                METRICS_CONFIG_ON_GEAR_CHANGE_V1.toByteArray());
+    }
+
+    private void onRemoveGearChangeConfigBtnClick(View view) {
+        showOutput("Removing MetricsConfig that listens for gear change...");
+        mCarTelemetryManager.removeMetricsConfig(KEY_V1);
+    }
+
+    private void onGetGearChangeReportBtnClick(View view) {
+        showOutput("Fetching report... If nothing shows up after a few seconds, "
+                + "then no result exists");
+        mCarTelemetryManager.sendFinishedReports(KEY_V1);
+    }
+
+    @Override
+    public void onDestroyView() {
+        super.onDestroyView();
+    }
+
+    /**
+     * Implementation of the {@link CarTelemetryManager.CarTelemetryResultsListener}. They update
+     * the view to show the outputs from the APIs of {@link CarTelemetryManager}.
+     * The callbacks are executed in {@link mExecutor}.
+     */
+    private final class CarTelemetryResultsListenerImpl
+            implements CarTelemetryManager.CarTelemetryResultsListener {
+
+        @Override
+        public void onResult(@NonNull MetricsConfigKey key, @NonNull byte[] result) {
+            PersistableBundle bundle;
+            try (ByteArrayInputStream bis = new ByteArrayInputStream(result)) {
+                bundle = PersistableBundle.readFromStream(bis);
+            } catch (IOException e) {
+                bundle = null;
+            }
+            showOutput("Result is " + bundle.toString());
+        }
+
+        @Override
+        public void onError(@NonNull MetricsConfigKey key, @NonNull byte[] error) {
+        }
+
+        @Override
+        public void onAddMetricsConfigStatus(@NonNull MetricsConfigKey key, int statusCode) {
+            showOutput("Add MetricsConfig status: " + statusCode);
+        }
+
+        @Override
+        public void onRemoveMetricsConfigStatus(@NonNull MetricsConfigKey key, boolean success) {
+            showOutput("Remove MetricsConfig status: " + success);
+        }
+    }
+}
diff --git a/tests/carservice_unit_test/src/com/android/car/telemetry/databroker/DataBrokerTest.java b/tests/carservice_unit_test/src/com/android/car/telemetry/databroker/DataBrokerTest.java
index d4f61a9..82f7e5e 100644
--- a/tests/carservice_unit_test/src/com/android/car/telemetry/databroker/DataBrokerTest.java
+++ b/tests/carservice_unit_test/src/com/android/car/telemetry/databroker/DataBrokerTest.java
@@ -50,7 +50,9 @@
 import org.junit.runner.RunWith;
 import org.mockito.Mock;
 import org.mockito.Mockito;
+import org.mockito.invocation.InvocationOnMock;
 import org.mockito.junit.MockitoJUnitRunner;
+import org.mockito.stubbing.Answer;
 
 import java.nio.file.Files;
 import java.util.Collections;
@@ -114,8 +116,6 @@
     public void setUp() throws Exception {
         when(mMockCarPropertyService.getPropertyList())
                 .thenReturn(Collections.singletonList(PROP_CONFIG));
-        // bind service should return true, otherwise broker is disabled
-        when(mMockContext.bindServiceAsUser(any(), any(), anyInt(), any())).thenReturn(true);
         PublisherFactory factory = new PublisherFactory(
                 mMockCarPropertyService, mMockHandler, mMockStatsManager,
                 Files.createTempDirectory("telemetry_test").toFile());
@@ -295,11 +295,24 @@
     }
 
     @Test
-    public void testScheduleNextTask_whenBindScriptExecutorFailed_shouldDisableBroker()
+    public void testScheduleNextTask_bindScriptExecutorFailedOnce_shouldRebind()
             throws Exception {
-        // fail all future attempts to bind to it
         Mockito.reset(mMockContext);
-        when(mMockContext.bindServiceAsUser(any(), any(), anyInt(), any())).thenReturn(false);
+        when(mMockContext.bindServiceAsUser(any(), any(), anyInt(), any())).thenAnswer(
+                new Answer() {
+                    private int mCount = 0;
+
+                    @Override
+                    public Object answer(InvocationOnMock invocation) {
+                        if (mCount++ == 1) {
+                            return false; // fail first attempt
+                        }
+                        ServiceConnection conn = invocation.getArgument(1);
+                        conn.onServiceConnected(null, mMockScriptExecutorBinder);
+                        return true; // second attempt should succeed
+                    }
+                });
+        mDataBroker.mBindScriptExecutorDelayMillis = 0L; // immediately rebind for testing purpose
         mDataBroker.addMetricsConfiguration(METRICS_CONFIG_FOO);
         PriorityBlockingQueue<ScriptExecutionTask> taskQueue = mDataBroker.getTaskQueue();
         taskQueue.add(mHighPriorityTask);
@@ -308,7 +321,27 @@
         mDataBroker.scheduleNextTask();
 
         waitForHandlerThreadToFinish();
-        // all subscribers should have been removed
+        assertThat(taskQueue.peek()).isNull();
+        assertThat(mFakeScriptExecutor.getApiInvocationCount()).isEqualTo(1);
+    }
+
+    @Test
+    public void testScheduleNextTask_bindScriptExecutorFailedMultipleTimes_shouldDisableBroker()
+            throws Exception {
+        // fail 6 future attempts to bind to it
+        Mockito.reset(mMockContext);
+        when(mMockContext.bindServiceAsUser(any(), any(), anyInt(), any()))
+                .thenReturn(false, false, false, false, false, false);
+        mDataBroker.mBindScriptExecutorDelayMillis = 0L; // immediately rebind for testing purpose
+        mDataBroker.addMetricsConfiguration(METRICS_CONFIG_FOO);
+        PriorityBlockingQueue<ScriptExecutionTask> taskQueue = mDataBroker.getTaskQueue();
+        taskQueue.add(mHighPriorityTask);
+
+        // will rebind to ScriptExecutor if it is null
+        mDataBroker.scheduleNextTask();
+
+        waitForHandlerThreadToFinish();
+        // broker disabled, all subscribers should have been removed
         assertThat(mDataBroker.getSubscriptionMap()).hasSize(0);
         assertThat(mFakeScriptExecutor.getApiInvocationCount()).isEqualTo(0);
     }