Create minimal ScriptExecutor java class to invoke script.

Amends IScriptExecutor invokeScript API to accept publishedData as a
bundle instead of byte[].
We do not parse the published input yet. It will be implemented
separately (see b/189241508).
Also adds necessary JNI layer.

Adds a unit test ScriptExecutorTest which starts ScriptExecutor
as an isolated process and binds to it.

Test cases push a simple Hello world script and test that it can be
loaded and run without exceptions. We don't test for output at this
point because it will be pushed asyncronously to CarTelemetryService
using IScriptExecutorListener callback. This functionality will be
implemented in the follow up CLs.

Bug: 187517413
Test: atest ScriptExecutorTest
Change-Id: Ida7c6ba9332532f3704a83a4ebfaf12dbec83bcb
diff --git a/car-lib/src/android/car/telemetry/IScriptExecutor.aidl b/car-lib/src/android/car/telemetry/IScriptExecutor.aidl
index d7b967b..83ea3f0 100644
--- a/car-lib/src/android/car/telemetry/IScriptExecutor.aidl
+++ b/car-lib/src/android/car/telemetry/IScriptExecutor.aidl
@@ -37,7 +37,7 @@
    */
   void invokeScript(String scriptBody,
                     String functionName,
-                    in byte[] publishedData,
+                    in Bundle publishedData,
                     in @nullable Bundle savedState,
                     in IScriptExecutorListener listener);
 }
diff --git a/cpp/telemetry/script_executor/Android.bp b/cpp/telemetry/script_executor/Android.bp
index 78b639a..6eaec62 100644
--- a/cpp/telemetry/script_executor/Android.bp
+++ b/cpp/telemetry/script_executor/Android.bp
@@ -39,6 +39,7 @@
         "src/ScriptExecutorListener.cpp",
     ],
     shared_libs: [
+        "libandroid_runtime",
         "libnativehelper",
     ],
     // Allow dependents to use the header files.
@@ -60,3 +61,17 @@
         "libscriptexecutor",
     ],
 }
+
+cc_library {
+    name: "libscriptexecutorjni",
+    defaults: [
+        "scriptexecutor_defaults",
+    ],
+    srcs: [
+        "src/ScriptExecutorJni.cpp",
+    ],
+    shared_libs: [
+        "libnativehelper",
+        "libscriptexecutor",
+    ],
+}
diff --git a/cpp/telemetry/script_executor/src/LuaEngine.cpp b/cpp/telemetry/script_executor/src/LuaEngine.cpp
index cc1d0b8..1a074f2 100644
--- a/cpp/telemetry/script_executor/src/LuaEngine.cpp
+++ b/cpp/telemetry/script_executor/src/LuaEngine.cpp
@@ -29,6 +29,7 @@
 namespace script_executor {
 
 LuaEngine::LuaEngine() {
+    // Instantiate Lua environment
     mLuaState = luaL_newstate();
     luaL_openlibs(mLuaState);
 }
@@ -41,6 +42,53 @@
     return mLuaState;
 }
 
+void LuaEngine::ResetListener(ScriptExecutorListener* listener) {
+    mListener.reset(listener);
+}
+
+int LuaEngine::LoadScript(const char* scriptBody) {
+    // As the first step in Lua script execution we want to load
+    // the body of the script into Lua stack and have it processed by Lua
+    // to catch any errors.
+    // More on luaL_dostring: https://www.lua.org/manual/5.3/manual.html#lual_dostring
+    // If error, pushes the error object into the stack.
+    const auto status = luaL_dostring(mLuaState, scriptBody);
+    if (status) {
+        // Removes error object from the stack.
+        // Lua stack must be properly maintained due to its limited size,
+        // ~20 elements and its critical function because all interaction with
+        // Lua happens via the stack.
+        // Starting read about Lua stack: https://www.lua.org/pil/24.2.html
+        // TODO(b/192284232): add test case to trigger this.
+        lua_pop(mLuaState, 1);
+    }
+    return status;
+}
+
+bool LuaEngine::PushFunction(const char* functionName) {
+    // Interaction between native code and Lua happens via Lua stack.
+    // In such model, a caller first pushes the name of the function
+    // that needs to be called, followed by the function's input
+    // arguments, one input value pushed at a time.
+    // More info: https://www.lua.org/pil/24.2.html
+    lua_getglobal(mLuaState, functionName);
+    const auto status = lua_isfunction(mLuaState, /*idx= */ -1);
+    // TODO(b/192284785): add test case for wrong function name in Lua.
+    if (status == 0) lua_pop(mLuaState, 1);
+    return status;
+}
+
+int LuaEngine::Run() {
+    // Performs blocking call of the provided Lua function. Assumes all
+    // input arguments are in the Lua stack as well in proper order.
+    // On how to call Lua functions: https://www.lua.org/pil/25.2.html
+    // Doc on lua_pcall: https://www.lua.org/manual/5.3/manual.html#lua_pcall
+    // TODO(b/189241508): Once we implement publishedData parsing, nargs should
+    // change from 1 to 2.
+    // TODO(b/192284612): add test case for failed call.
+    return lua_pcall(mLuaState, /* nargs= */ 1, /* nresults= */ 0, /*errfunc= */ 0);
+}
+
 }  // namespace script_executor
 }  // namespace telemetry
 }  // namespace automotive
diff --git a/cpp/telemetry/script_executor/src/LuaEngine.h b/cpp/telemetry/script_executor/src/LuaEngine.h
index a0f3978..a1d6e48 100644
--- a/cpp/telemetry/script_executor/src/LuaEngine.h
+++ b/cpp/telemetry/script_executor/src/LuaEngine.h
@@ -40,8 +40,28 @@
     // Returns pointer to Lua state object.
     lua_State* GetLuaState();
 
+    // Loads Lua script provided as scriptBody string.
+    // Returns 0 if successful. Otherwise returns non-zero Lua error code.
+    int LoadScript(const char* scriptBody);
+
+    // Pushes a Lua function under provided name into the stack.
+    // Returns true if successful.
+    bool PushFunction(const char* functionName);
+
+    // Invokes function with the inputs provided in the stack.
+    // Assumes that the script body has been already loaded and successully
+    // compiled and run, and all input arguments, and the function have been
+    // pushed to the stack.
+    // Returns 0 if successful. Otherwise returns non-zero Lua error code.
+    int Run();
+
+    // Updates stored listener and destroys the previous one.
+    void ResetListener(ScriptExecutorListener* listener);
+
 private:
     lua_State* mLuaState;  // owned
+
+    std::unique_ptr<ScriptExecutorListener> mListener;
 };
 
 }  // namespace script_executor
diff --git a/cpp/telemetry/script_executor/src/ScriptExecutorJni.cpp b/cpp/telemetry/script_executor/src/ScriptExecutorJni.cpp
new file mode 100644
index 0000000..500b8e2
--- /dev/null
+++ b/cpp/telemetry/script_executor/src/ScriptExecutorJni.cpp
@@ -0,0 +1,137 @@
+/*
+ * 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.
+ */
+
+#include "JniUtils.h"
+#include "LuaEngine.h"
+#include "ScriptExecutorListener.h"
+#include "jni.h"
+
+#include <android-base/logging.h>
+
+#include <cstdint>
+
+namespace android {
+namespace automotive {
+namespace telemetry {
+namespace script_executor {
+
+extern "C" {
+
+JNIEXPORT jlong JNICALL
+Java_com_android_car_telemetry_ScriptExecutor_nativeInitLuaEngine(JNIEnv* env, jobject object) {
+    // Cast first to intptr_t to ensure int can hold the pointer without loss.
+    return static_cast<jlong>(reinterpret_cast<intptr_t>(new LuaEngine()));
+}
+
+JNIEXPORT void JNICALL Java_com_android_car_telemetry_ScriptExecutor_nativeDestroyLuaEngine(
+        JNIEnv* env, jobject object, jlong luaEnginePtr) {
+    delete reinterpret_cast<LuaEngine*>(static_cast<intptr_t>(luaEnginePtr));
+}
+
+// Parses the inputs and loads them to Lua one at a time.
+// Loading of data into Lua also triggers checks on Lua side to verify the
+// inputs are valid. For example, pushing "functionName" into Lua stack verifies
+// that the function name actually exists in the previously loaded body of the
+// script.
+//
+// The steps are:
+// Step 1: Parse the inputs for obvious programming errors.
+// Step 2: Parse and load the body of the script.
+// Step 3: Parse and push function name we want to execute in the provided
+// script body to Lua stack. If the function name doesn't exist, we exit.
+// Step 4: Parse publishedData, convert it into Lua table and push it to the
+// stack.
+// Step 5: Parse savedState Bundle object, convert it into Lua table and push it
+// to the stack.
+// Any errors that occur at the stage above result in quick exit or crash.
+//
+// All interaction with Lua happens via Lua stack. Therefore, order of how the
+// inputs are parsed and processed is critical because Lua API methods such as
+// lua_pcall assume specific order between function name and the input arguments
+// on the stack.
+// More information about how to work with Lua stack: https://www.lua.org/pil/24.2.html
+// and how Lua functions are called via Lua API: https://www.lua.org/pil/25.2.html
+//
+// Finally, once parsing and pushing to Lua stack is complete, we do
+//
+// Step 6: attempt to run the provided function.
+JNIEXPORT void JNICALL Java_com_android_car_telemetry_ScriptExecutor_nativeInvokeScript(
+        JNIEnv* env, jobject object, jlong luaEnginePtr, jstring scriptBody, jstring functionName,
+        jobject publishedData, jobject savedState, jobject listener) {
+    if (!luaEnginePtr) {
+        env->FatalError("luaEnginePtr parameter cannot be nil");
+    }
+    if (scriptBody == nullptr) {
+        env->FatalError("scriptBody parameter cannot be null");
+    }
+    if (functionName == nullptr) {
+        env->FatalError("functionName parameter cannot be null");
+    }
+    if (listener == nullptr) {
+        env->FatalError("listener parameter cannot be null");
+    }
+
+    LuaEngine* engine = reinterpret_cast<LuaEngine*>(static_cast<intptr_t>(luaEnginePtr));
+
+    // Load and parse the script
+    const char* scriptStr = env->GetStringUTFChars(scriptBody, nullptr);
+    auto status = engine->LoadScript(scriptStr);
+    env->ReleaseStringUTFChars(scriptBody, scriptStr);
+    // status == 0 if the script loads successfully.
+    if (status) {
+        env->ThrowNew(env->FindClass("java/lang/IllegalArgumentException"),
+                      "Failed to load the script.");
+        return;
+    }
+    engine->ResetListener(new ScriptExecutorListener(env, listener));
+
+    // Push the function name we want to invoke to Lua stack
+    const char* functionNameStr = env->GetStringUTFChars(functionName, nullptr);
+    status = engine->PushFunction(functionNameStr);
+    env->ReleaseStringUTFChars(functionName, functionNameStr);
+    // status == 1 if the name is indeed a function.
+    if (!status) {
+        env->ThrowNew(env->FindClass("java/lang/IllegalArgumentException"),
+                      "symbol functionName does not correspond to a function.");
+        return;
+    }
+
+    // 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;
+    }
+
+    // Unpack bundle in savedState, convert to Lua table and push it to Lua
+    // stack.
+    PushBundleToLuaTable(env, engine, savedState);
+
+    // Execute the function. This will block until complete or error.
+    if (engine->Run()) {
+        env->ThrowNew(env->FindClass("java/lang/RuntimeException"),
+                      "Runtime error occurred while running the function.");
+        return;
+    }
+}
+
+}  // extern "C"
+
+}  // namespace script_executor
+}  // namespace telemetry
+}  // namespace automotive
+}  // namespace android
diff --git a/cpp/telemetry/script_executor/src/ScriptExecutorListener.cpp b/cpp/telemetry/script_executor/src/ScriptExecutorListener.cpp
index 8a46253..8c10aa4 100644
--- a/cpp/telemetry/script_executor/src/ScriptExecutorListener.cpp
+++ b/cpp/telemetry/script_executor/src/ScriptExecutorListener.cpp
@@ -17,12 +17,24 @@
 #include "ScriptExecutorListener.h"
 
 #include <android-base/logging.h>
+#include <android_runtime/AndroidRuntime.h>
 
 namespace android {
 namespace automotive {
 namespace telemetry {
 namespace script_executor {
 
+ScriptExecutorListener::~ScriptExecutorListener() {
+    if (mScriptExecutorListener != NULL) {
+        JNIEnv* env = AndroidRuntime::getJNIEnv();
+        env->DeleteGlobalRef(mScriptExecutorListener);
+    }
+}
+
+ScriptExecutorListener::ScriptExecutorListener(JNIEnv* env, jobject script_executor_listener) {
+    mScriptExecutorListener = env->NewGlobalRef(script_executor_listener);
+}
+
 void ScriptExecutorListener::onError(const int errorType, const std::string& message,
                                      const std::string& stackTrace) {
     LOG(INFO) << "errorType: " << errorType << ", message: " << message
diff --git a/cpp/telemetry/script_executor/src/ScriptExecutorListener.h b/cpp/telemetry/script_executor/src/ScriptExecutorListener.h
index d9d8213..1e5c7d7 100644
--- a/cpp/telemetry/script_executor/src/ScriptExecutorListener.h
+++ b/cpp/telemetry/script_executor/src/ScriptExecutorListener.h
@@ -17,6 +17,8 @@
 #ifndef CPP_TELEMETRY_SCRIPT_EXECUTOR_SRC_SCRIPTEXECUTORLISTENER_H_
 #define CPP_TELEMETRY_SCRIPT_EXECUTOR_SRC_SCRIPTEXECUTORLISTENER_H_
 
+#include "jni.h"
+
 #include <string>
 
 namespace android {
@@ -27,11 +29,19 @@
 //  Wrapper class for IScriptExecutorListener.aidl.
 class ScriptExecutorListener {
 public:
+    ScriptExecutorListener(JNIEnv* jni, jobject script_executor_listener);
+
+    virtual ~ScriptExecutorListener();
+
     void onScriptFinished() {}
 
     void onSuccess() {}
 
     void onError(const int errorType, const std::string& message, const std::string& stackTrace);
+
+private:
+    // Stores a jni global reference to Java Script Executor listener object.
+    jobject mScriptExecutorListener;
 };
 
 }  // namespace script_executor
diff --git a/service/Android.bp b/service/Android.bp
index 1bc18eb..398177f 100644
--- a/service/Android.bp
+++ b/service/Android.bp
@@ -83,7 +83,10 @@
         "com.android.car.internal.system",
     ],
 
-    jni_libs: ["libcarservicejni"],
+    jni_libs: [
+        "libcarservicejni",
+        "libscriptexecutorjni",
+    ],
 
     required: ["allowed_privapp_com.android.car"],
 
diff --git a/service/AndroidManifest.xml b/service/AndroidManifest.xml
index 8f90480..0e1d12a 100644
--- a/service/AndroidManifest.xml
+++ b/service/AndroidManifest.xml
@@ -935,7 +935,10 @@
             </intent-filter>
         </service>
         <service android:name=".PerUserCarService"
-             android:exported="false"/>
+            android:exported="false"/>
+        <service android:name=".telemetry.ScriptExecutor"
+            android:exported="false"
+            android:isolatedProcess="true"/>
 
         <activity android:name="com.android.car.pm.ActivityBlockingActivity"
              android:documentLaunchMode="always"
diff --git a/service/src/com/android/car/telemetry/ScriptExecutor.java b/service/src/com/android/car/telemetry/ScriptExecutor.java
new file mode 100644
index 0000000..d791481
--- /dev/null
+++ b/service/src/com/android/car/telemetry/ScriptExecutor.java
@@ -0,0 +1,115 @@
+/*
+ * 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 android.app.Service;
+import android.car.telemetry.IScriptExecutor;
+import android.car.telemetry.IScriptExecutorListener;
+import android.content.Intent;
+import android.os.Bundle;
+import android.os.Handler;
+import android.os.HandlerThread;
+import android.os.IBinder;
+import android.util.Log;
+
+import com.android.car.CarServiceUtils;
+
+/**
+ * Executes Lua code in an isolated process with provided source code
+ * and input arguments.
+ */
+public final class ScriptExecutor extends Service {
+
+    static {
+        System.loadLibrary("scriptexecutorjni");
+    }
+
+    private static final String TAG = ScriptExecutor.class.getSimpleName();
+
+    // Dedicated "worker" thread to handle all calls related to native code.
+    private HandlerThread mNativeHandlerThread;
+    // Handler associated with the native worker thread.
+    private Handler mNativeHandler;
+
+    private final class IScriptExecutorImpl extends IScriptExecutor.Stub {
+        @Override
+        public void invokeScript(String scriptBody, String functionName, Bundle publishedData,
+                Bundle savedState, IScriptExecutorListener listener) {
+            mNativeHandler.post(() ->
+                    nativeInvokeScript(mLuaEnginePtr, scriptBody, functionName, publishedData,
+                            savedState, listener));
+        }
+    }
+    private IScriptExecutorImpl mScriptExecutorBinder;
+
+    // Memory location of Lua Engine object which is allocated in native code.
+    private long mLuaEnginePtr;
+
+    @Override
+    public void onCreate() {
+        super.onCreate();
+
+        mNativeHandlerThread = CarServiceUtils.getHandlerThread(
+            ScriptExecutor.class.getSimpleName());
+        // TODO(b/192284628): Remove this once start handling all recoverable errors via onError
+        // callback.
+        mNativeHandlerThread.setUncaughtExceptionHandler((t, e) -> Log.e(TAG, e.getMessage()));
+        mNativeHandler = new Handler(mNativeHandlerThread.getLooper());
+
+        mLuaEnginePtr = nativeInitLuaEngine();
+        mScriptExecutorBinder = new IScriptExecutorImpl();
+    }
+
+    @Override
+    public void onDestroy() {
+        super.onDestroy();
+        nativeDestroyLuaEngine(mLuaEnginePtr);
+        mNativeHandlerThread.quit();
+    }
+
+    @Override
+    public IBinder onBind(Intent intent) {
+        return mScriptExecutorBinder;
+    }
+
+    /**
+    * Initializes Lua Engine.
+    *
+    * <p>Returns memory location of Lua Engine.
+    */
+    private native long nativeInitLuaEngine();
+
+    /**
+     * Destroys LuaEngine at the provided memory address.
+     */
+    private native void nativeDestroyLuaEngine(long luaEnginePtr);
+
+    /**
+     * Calls provided Lua function.
+     *
+     * @param luaEnginePtr memory address of the stored LuaEngine instance.
+     * @param scriptBody complete body of Lua script that also contains the function to be invoked.
+     * @param functionName the name of the function to execute.
+     * @param publishedData input data provided by the source which the function handles.
+     * @param savedState key-value pairs preserved from the previous invocation of the function.
+     * @param listener callback for the sandboxed environent to report back script execution results
+     * and errors.
+     */
+    private native void nativeInvokeScript(long luaEnginePtr, String scriptBody,
+            String functionName, Bundle publishedData, Bundle savedState,
+            IScriptExecutorListener listener);
+}
diff --git a/tests/carservice_unit_test/Android.bp b/tests/carservice_unit_test/Android.bp
index cf9d238..1f22098 100644
--- a/tests/carservice_unit_test/Android.bp
+++ b/tests/carservice_unit_test/Android.bp
@@ -69,6 +69,7 @@
     // mockito-target-inline dependency
     jni_libs: [
         "libdexmakerjvmtiagent",
+	"libscriptexecutorjni",
         "libscriptexecutorjniutils-test",
         "libstaticjvmtiagent",
     ],
diff --git a/tests/carservice_unit_test/src/com/android/car/telemetry/ScriptExecutorTest.java b/tests/carservice_unit_test/src/com/android/car/telemetry/ScriptExecutorTest.java
new file mode 100644
index 0000000..774da25
--- /dev/null
+++ b/tests/carservice_unit_test/src/com/android/car/telemetry/ScriptExecutorTest.java
@@ -0,0 +1,123 @@
+/*
+ * 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 org.junit.Assert.fail;
+
+import android.car.telemetry.IScriptExecutor;
+import android.car.telemetry.IScriptExecutorListener;
+import android.content.ComponentName;
+import android.content.Context;
+import android.content.Intent;
+import android.content.ServiceConnection;
+import android.os.Bundle;
+import android.os.IBinder;
+import android.os.RemoteException;
+
+import androidx.test.platform.app.InstrumentationRegistry;
+
+import org.junit.Before;
+import org.junit.Test;
+import org.junit.runner.RunWith;
+import org.junit.runners.JUnit4;
+
+import java.util.concurrent.CountDownLatch;
+import java.util.concurrent.TimeUnit;
+
+@RunWith(JUnit4.class)
+public final class ScriptExecutorTest {
+
+    private IScriptExecutor mScriptExecutor;
+    private ScriptExecutor mInstance;
+    private Context mContext = InstrumentationRegistry.getInstrumentation().getTargetContext();
+
+
+    private static final class ScriptExecutorListener extends IScriptExecutorListener.Stub {
+        @Override
+        public void onScriptFinished(byte[] result) {
+        }
+
+        @Override
+        public void onSuccess(Bundle stateToPersist) {
+        }
+
+        @Override
+        public void onError(int errorType, String message, String stackTrace) {
+        }
+    }
+
+    private final IScriptExecutorListener mFakeScriptExecutorListener =
+            new ScriptExecutorListener();
+
+    // TODO(b/189241508). Parsing of publishedData is not implemented yet.
+    // Null is the only accepted input.
+    private final Bundle mPublishedData = null;
+    private final Bundle mSavedState = new Bundle();
+
+    private static final String LUA_SCRIPT =
+            "function hello(data, state)\n"
+            + "    print(\"Hello World\")\n"
+            + "end\n";
+
+    private static final String LUA_METHOD = "hello";
+
+    private final CountDownLatch mBindLatch = new CountDownLatch(1);
+
+    private static final int BIND_SERVICE_TIMEOUT_SEC = 5;
+
+    private final ServiceConnection mScriptExecutorConnection =
+            new ServiceConnection() {
+                @Override
+                public void onServiceConnected(ComponentName className, IBinder service) {
+                    mScriptExecutor = IScriptExecutor.Stub.asInterface(service);
+                    mBindLatch.countDown();
+                }
+
+                @Override
+                public void onServiceDisconnected(ComponentName className) {
+                    fail("Service unexpectedly disconnected");
+                }
+            };
+
+    @Before
+    public void setUp() throws InterruptedException {
+        mContext.bindIsolatedService(new Intent(mContext, ScriptExecutor.class),
+                Context.BIND_AUTO_CREATE, "scriptexecutor", mContext.getMainExecutor(),
+                mScriptExecutorConnection);
+        if (!mBindLatch.await(BIND_SERVICE_TIMEOUT_SEC, TimeUnit.SECONDS)) {
+            fail("Failed to bind to ScripExecutor service");
+        }
+    }
+
+    @Test
+    public void invokeScript_helloWorld() throws RemoteException {
+        // Expect to load "hello world" script successfully and push the function.
+        mScriptExecutor.invokeScript(LUA_SCRIPT, LUA_METHOD, mPublishedData, mSavedState,
+                mFakeScriptExecutorListener);
+        // Sleep, otherwise the test case will complete before the script loads
+        // because invokeScript is non-blocking.
+        try {
+            // TODO(b/192285332): Replace sleep logic with waiting for specific callbacks
+            // to be called once they are implemented. Otherwise, this could be a flaky test.
+            TimeUnit.SECONDS.sleep(10);
+        } catch (InterruptedException e) {
+            e.printStackTrace();
+            fail(e.getMessage());
+        }
+    }
+}
+