Introduce ability for the script to send the results back.

Implements OnSuccess API method of ScriptExecutorListener.
Adds routines to convert Lua table to Java Bundle.
Adds test cases to verify that the script can process inputs and return
the expected results back.
Additionally, changes naming of methods in native code for
ScriptExecutor to be lower-case.

Bug: 187517415
Test: atest CarServiceUnitTest:ScriptExecutorTest
Change-Id: I23efe323f520e9ab17e27c8f4117068839d4a10e
diff --git a/cpp/telemetry/script_executor/Android.bp b/cpp/telemetry/script_executor/Android.bp
index 6eaec62..9d8a05b 100644
--- a/cpp/telemetry/script_executor/Android.bp
+++ b/cpp/telemetry/script_executor/Android.bp
@@ -34,6 +34,7 @@
         "scriptexecutor_defaults",
     ],
     srcs: [
+        "src/BundleWrapper.cpp",
         "src/JniUtils.cpp",
         "src/LuaEngine.cpp",
         "src/ScriptExecutorListener.cpp",
diff --git a/cpp/telemetry/script_executor/src/BundleWrapper.cpp b/cpp/telemetry/script_executor/src/BundleWrapper.cpp
new file mode 100644
index 0000000..77d2a5f
--- /dev/null
+++ b/cpp/telemetry/script_executor/src/BundleWrapper.cpp
@@ -0,0 +1,79 @@
+/*
+ * 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 "BundleWrapper.h"
+
+#include <android-base/logging.h>
+#include <android_runtime/AndroidRuntime.h>
+
+namespace android {
+namespace automotive {
+namespace telemetry {
+namespace script_executor {
+
+BundleWrapper::BundleWrapper(JNIEnv* env) {
+    mJNIEnv = env;
+    mBundleClass =
+            static_cast<jclass>(mJNIEnv->NewGlobalRef(mJNIEnv->FindClass("android/os/Bundle")));
+    jmethodID bundleConstructor = mJNIEnv->GetMethodID(mBundleClass, "<init>", "()V");
+    mBundle = mJNIEnv->NewGlobalRef(mJNIEnv->NewObject(mBundleClass, bundleConstructor));
+}
+
+BundleWrapper::~BundleWrapper() {
+    // Delete global JNI references.
+    if (mBundle != NULL) {
+        mJNIEnv->DeleteGlobalRef(mBundle);
+    }
+    if (mBundleClass != NULL) {
+        mJNIEnv->DeleteGlobalRef(mBundleClass);
+    }
+}
+
+void BundleWrapper::putBoolean(const char* key, bool value) {
+    jmethodID putBooleanMethod =
+            mJNIEnv->GetMethodID(mBundleClass, "putBoolean", "(Ljava/lang/String;Z)V");
+    mJNIEnv->CallVoidMethod(mBundle, putBooleanMethod, mJNIEnv->NewStringUTF(key),
+                            static_cast<jboolean>(value));
+}
+
+void BundleWrapper::putInteger(const char* key, int value) {
+    jmethodID putIntMethod = mJNIEnv->GetMethodID(mBundleClass, "putInt", "(Ljava/lang/String;I)V");
+    mJNIEnv->CallVoidMethod(mBundle, putIntMethod, mJNIEnv->NewStringUTF(key),
+                            static_cast<jint>(value));
+}
+
+void BundleWrapper::putDouble(const char* key, double value) {
+    jmethodID putDoubleMethod =
+            mJNIEnv->GetMethodID(mBundleClass, "putDouble", "(Ljava/lang/String;D)V");
+    mJNIEnv->CallVoidMethod(mBundle, putDoubleMethod, mJNIEnv->NewStringUTF(key),
+                            static_cast<jdouble>(value));
+}
+
+void BundleWrapper::putString(const char* key, const char* value) {
+    jmethodID putStringMethod = mJNIEnv->GetMethodID(mBundleClass, "putString",
+                                                     "(Ljava/lang/String;Ljava/lang/String;)V");
+    mJNIEnv->CallVoidMethod(mBundle, putStringMethod, mJNIEnv->NewStringUTF(key),
+                            mJNIEnv->NewStringUTF(value));
+}
+
+jobject BundleWrapper::getBundle() {
+    return mBundle;
+}
+
+}  // namespace script_executor
+}  // namespace telemetry
+}  // namespace automotive
+}  // namespace android
diff --git a/cpp/telemetry/script_executor/src/BundleWrapper.h b/cpp/telemetry/script_executor/src/BundleWrapper.h
new file mode 100644
index 0000000..8c42c46
--- /dev/null
+++ b/cpp/telemetry/script_executor/src/BundleWrapper.h
@@ -0,0 +1,63 @@
+/*
+ * 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.
+ */
+
+#ifndef CPP_TELEMETRY_SCRIPT_EXECUTOR_SRC_BUNDLEWRAPPER_H_
+#define CPP_TELEMETRY_SCRIPT_EXECUTOR_SRC_BUNDLEWRAPPER_H_
+
+#include "jni.h"
+
+namespace android {
+namespace automotive {
+namespace telemetry {
+namespace script_executor {
+
+// Used to create a java bundle object and populate its fields one at a time.
+class BundleWrapper {
+public:
+    explicit BundleWrapper(JNIEnv* env);
+    // BundleWrapper is not copyable.
+    BundleWrapper(const BundleWrapper&) = delete;
+    BundleWrapper& operator=(const BundleWrapper&) = delete;
+
+    virtual ~BundleWrapper();
+
+    // Family of methods that puts the provided 'value' into the Bundle under provided 'key'.
+    void putBoolean(const char* key, bool value);
+    void putInteger(const char* key, int value);
+    void putDouble(const char* key, double value);
+    void putString(const char* key, const char* value);
+
+    jobject getBundle();
+
+private:
+    // The class asks Java to create Bundle object and stores the reference.
+    // When the instance of this class is destroyed the actual Java Bundle object behind
+    // this reference stays on and is managed by Java.
+    jobject mBundle;
+
+    // Reference to java Bundle class cached for performance reasons.
+    jclass mBundleClass;
+
+    // Stores a JNIEnv* pointer.
+    JNIEnv* mJNIEnv;  // not owned
+};
+
+}  // namespace script_executor
+}  // namespace telemetry
+}  // namespace automotive
+}  // namespace android
+
+#endif  // CPP_TELEMETRY_SCRIPT_EXECUTOR_SRC_BUNDLEWRAPPER_H_
diff --git a/cpp/telemetry/script_executor/src/JniUtils.cpp b/cpp/telemetry/script_executor/src/JniUtils.cpp
index 93c1af8..cfe1da4 100644
--- a/cpp/telemetry/script_executor/src/JniUtils.cpp
+++ b/cpp/telemetry/script_executor/src/JniUtils.cpp
@@ -21,8 +21,8 @@
 namespace telemetry {
 namespace script_executor {
 
-void PushBundleToLuaTable(JNIEnv* env, LuaEngine* luaEngine, jobject bundle) {
-    lua_newtable(luaEngine->GetLuaState());
+void pushBundleToLuaTable(JNIEnv* env, LuaEngine* luaEngine, jobject bundle) {
+    lua_newtable(luaEngine->getLuaState());
     // null bundle object is allowed. We will treat it as an empty table.
     if (bundle == nullptr) {
         return;
@@ -62,19 +62,19 @@
         if (env->IsInstanceOf(value, booleanClass)) {
             jmethodID boolMethod = env->GetMethodID(booleanClass, "booleanValue", "()Z");
             bool boolValue = static_cast<bool>(env->CallBooleanMethod(value, boolMethod));
-            lua_pushboolean(luaEngine->GetLuaState(), boolValue);
+            lua_pushboolean(luaEngine->getLuaState(), boolValue);
         } else if (env->IsInstanceOf(value, integerClass)) {
             jmethodID intMethod = env->GetMethodID(integerClass, "intValue", "()I");
-            lua_pushinteger(luaEngine->GetLuaState(), env->CallIntMethod(value, intMethod));
+            lua_pushinteger(luaEngine->getLuaState(), env->CallIntMethod(value, intMethod));
         } else if (env->IsInstanceOf(value, numberClass)) {
             // Condense other numeric types using one class. Because lua supports only
             // integer or double, and we handled integer in previous if clause.
             jmethodID numberMethod = env->GetMethodID(numberClass, "doubleValue", "()D");
             /* Pushes a double onto the stack */
-            lua_pushnumber(luaEngine->GetLuaState(), env->CallDoubleMethod(value, numberMethod));
+            lua_pushnumber(luaEngine->getLuaState(), env->CallDoubleMethod(value, numberMethod));
         } else if (env->IsInstanceOf(value, stringClass)) {
             const char* rawStringValue = env->GetStringUTFChars((jstring)value, nullptr);
-            lua_pushstring(luaEngine->GetLuaState(), rawStringValue);
+            lua_pushstring(luaEngine->getLuaState(), rawStringValue);
             env->ReleaseStringUTFChars((jstring)value, rawStringValue);
         } else {
             // Other types are not implemented yet, skipping.
@@ -84,7 +84,7 @@
         const char* rawKey = env->GetStringUTFChars(key, nullptr);
         // table[rawKey] = value, where value is on top of the stack,
         // and the table is the next element in the stack.
-        lua_setfield(luaEngine->GetLuaState(), /* idx= */ -2, rawKey);
+        lua_setfield(luaEngine->getLuaState(), /* idx= */ -2, rawKey);
         env->ReleaseStringUTFChars(key, rawKey);
     }
 }
diff --git a/cpp/telemetry/script_executor/src/JniUtils.h b/cpp/telemetry/script_executor/src/JniUtils.h
index c3ef677..85034d7 100644
--- a/cpp/telemetry/script_executor/src/JniUtils.h
+++ b/cpp/telemetry/script_executor/src/JniUtils.h
@@ -29,7 +29,7 @@
 // converted to the corresponding key-value pairs of the Lua table as long as
 // the Bundle value types are supported. At this point, we support boolean,
 // integer, double and String types in Java.
-void PushBundleToLuaTable(JNIEnv* env, LuaEngine* luaEngine, jobject bundle);
+void pushBundleToLuaTable(JNIEnv* env, LuaEngine* luaEngine, jobject bundle);
 
 }  // namespace script_executor
 }  // namespace telemetry
diff --git a/cpp/telemetry/script_executor/src/LuaEngine.cpp b/cpp/telemetry/script_executor/src/LuaEngine.cpp
index 1a074f2..e2bcef7 100644
--- a/cpp/telemetry/script_executor/src/LuaEngine.cpp
+++ b/cpp/telemetry/script_executor/src/LuaEngine.cpp
@@ -16,10 +16,15 @@
 
 #include "LuaEngine.h"
 
+#include "BundleWrapper.h"
+
+#include <android-base/logging.h>
+
 #include <utility>
 
 extern "C" {
 #include "lauxlib.h"
+#include "lua.h"
 #include "lualib.h"
 }
 
@@ -28,6 +33,16 @@
 namespace telemetry {
 namespace script_executor {
 
+namespace {
+
+enum LuaNumReturnedResults {
+    ZERO_RETURNED_RESULTS = 0,
+};
+
+}  // namespace
+
+ScriptExecutorListener* LuaEngine::sListener = nullptr;
+
 LuaEngine::LuaEngine() {
     // Instantiate Lua environment
     mLuaState = luaL_newstate();
@@ -38,15 +53,18 @@
     lua_close(mLuaState);
 }
 
-lua_State* LuaEngine::GetLuaState() {
+lua_State* LuaEngine::getLuaState() {
     return mLuaState;
 }
 
-void LuaEngine::ResetListener(ScriptExecutorListener* listener) {
-    mListener.reset(listener);
+void LuaEngine::resetListener(ScriptExecutorListener* listener) {
+    if (sListener != nullptr) {
+        delete sListener;
+    }
+    sListener = listener;
 }
 
-int LuaEngine::LoadScript(const char* scriptBody) {
+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.
@@ -61,11 +79,15 @@
         // 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;
     }
+
+    // Register limited set of reserved methods for Lua to call native side.
+    lua_register(mLuaState, "on_success", LuaEngine::onSuccess);
     return status;
 }
 
-bool LuaEngine::PushFunction(const char* functionName) {
+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
@@ -78,7 +100,7 @@
     return status;
 }
 
-int LuaEngine::Run() {
+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
@@ -89,6 +111,54 @@
     return lua_pcall(mLuaState, /* nargs= */ 1, /* nresults= */ 0, /*errfunc= */ 0);
 }
 
+int LuaEngine::onSuccess(lua_State* lua) {
+    if (sListener == nullptr) {
+        LOG(FATAL) << "sListener object must not be null";
+    }
+    // Any script we run can call on_success only with a single argument of Lua table type.
+    if (lua_gettop(lua) != 1 || !lua_istable(lua, /* index =*/-1)) {
+        // TODO(b/193565932): Return programming error through binder callback interface.
+        LOG(ERROR) << "Only a single input argument, a Lua table object, expected here";
+    }
+
+    // Helper object to create and populate Java Bundle object.
+    BundleWrapper bundleWrapper(sListener->getCurrentJNIEnv());
+    // Iterate over Lua table which is expected to be at the top of Lua stack.
+    // lua_next call pops the key from the top of the stack and finds the next
+    // key-value pair for the popped key. It returns 0 if the next pair was not found.
+    // More on lua_next in: https://www.lua.org/manual/5.3/manual.html#lua_next
+    lua_pushnil(lua);  // First key is a null value.
+    while (lua_next(lua, /* index =*/-2) != 0) {
+        //  'key' is at index -2 and 'value' is at index -1
+        // -1 index is the top of the stack.
+        // remove 'value' and keep 'key' for next iteration
+        // Process each key-value depending on a type and push it to Java Bundle.
+        const char* key = lua_tostring(lua, /* index =*/-2);
+        if (lua_isboolean(lua, /* index =*/-1)) {
+            bundleWrapper.putBoolean(key, static_cast<bool>(lua_toboolean(lua, /* index =*/-1)));
+        } else if (lua_isinteger(lua, /* index =*/-1)) {
+            bundleWrapper.putInteger(key, static_cast<int>(lua_tointeger(lua, /* index =*/-1)));
+        } else if (lua_isnumber(lua, /* index =*/-1)) {
+            bundleWrapper.putDouble(key, static_cast<double>(lua_tonumber(lua, /* index =*/-1)));
+        } else if (lua_isstring(lua, /* index =*/-1)) {
+            bundleWrapper.putString(key, lua_tostring(lua, /* index =*/-1));
+        } else {
+            // not supported yet...
+            LOG(WARNING) << "key=" << key << " has a Lua type which is not supported yet. "
+                         << "The bundle object will not have this key-value pair.";
+        }
+        // Pop 1 element from the stack.
+        lua_pop(lua, 1);
+        // The key is at index -1, the table is at index -2 now.
+    }
+
+    // Forward the populated Bundle object to Java callback.
+    sListener->onSuccess(bundleWrapper.getBundle());
+    // We explicitly must tell Lua how many results we return, which is 0 in this case.
+    // More on the topic: https://www.lua.org/manual/5.3/manual.html#lua_CFunction
+    return ZERO_RETURNED_RESULTS;
+}
+
 }  // 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 a1d6e48..39f3a9f 100644
--- a/cpp/telemetry/script_executor/src/LuaEngine.h
+++ b/cpp/telemetry/script_executor/src/LuaEngine.h
@@ -38,30 +38,47 @@
     virtual ~LuaEngine();
 
     // Returns pointer to Lua state object.
-    lua_State* GetLuaState();
+    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);
+    int loadScript(const char* scriptBody);
 
     // Pushes a Lua function under provided name into the stack.
     // Returns true if successful.
-    bool PushFunction(const char* functionName);
+    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
+    // Assumes that the script body has been already loaded and successfully
     // 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();
+    int run();
 
     // Updates stored listener and destroys the previous one.
-    void ResetListener(ScriptExecutorListener* listener);
+    static void resetListener(ScriptExecutorListener* listener);
 
 private:
-    lua_State* mLuaState;  // owned
+    // Invoked by running Lua script to store intermediate results.
+    // The script will provide the results as a Lua table.
+    // We currently support only non-nested fields in the table and the fields can be the following
+    // Lua types: boolean, number, integer, and string.
+    // The result is converted to Android Bundle and forwarded to
+    // ScriptExecutor service via callback interface.
+    static int onSuccess(lua_State* lua);
 
-    std::unique_ptr<ScriptExecutorListener> mListener;
+    // Points to the current listener object.
+    // Lua cannot call non-static class methods. We need to access listener object instance in
+    // Lua callbacks. Therefore, callbacks callable by Lua are static class methods and the pointer
+    // to a listener object needs to be static, since static methods cannot access non-static
+    // members.
+    // Only one listener is supported at any given time.
+    // Since listeners are heap-allocated, the destructor does not need to run at shutdown
+    // of the service because the memory allocated to the current listener object will be
+    // reclaimed by the OS.
+    static ScriptExecutorListener* sListener;
+
+    lua_State* mLuaState;  // owned
 };
 
 }  // namespace script_executor
diff --git a/cpp/telemetry/script_executor/src/ScriptExecutorJni.cpp b/cpp/telemetry/script_executor/src/ScriptExecutorJni.cpp
index 500b8e2..0bdc692 100644
--- a/cpp/telemetry/script_executor/src/ScriptExecutorJni.cpp
+++ b/cpp/telemetry/script_executor/src/ScriptExecutorJni.cpp
@@ -65,9 +65,8 @@
 // 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.
+// Finally, once parsing and pushing to Lua stack is complete, we go on to the final step,
+// 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) {
@@ -88,7 +87,7 @@
 
     // Load and parse the script
     const char* scriptStr = env->GetStringUTFChars(scriptBody, nullptr);
-    auto status = engine->LoadScript(scriptStr);
+    auto status = engine->loadScript(scriptStr);
     env->ReleaseStringUTFChars(scriptBody, scriptStr);
     // status == 0 if the script loads successfully.
     if (status) {
@@ -96,11 +95,11 @@
                       "Failed to load the script.");
         return;
     }
-    engine->ResetListener(new ScriptExecutorListener(env, listener));
+    LuaEngine::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);
+    status = engine->pushFunction(functionNameStr);
     env->ReleaseStringUTFChars(functionName, functionNameStr);
     // status == 1 if the name is indeed a function.
     if (!status) {
@@ -119,10 +118,10 @@
 
     // Unpack bundle in savedState, convert to Lua table and push it to Lua
     // stack.
-    PushBundleToLuaTable(env, engine, savedState);
+    pushBundleToLuaTable(env, engine, savedState);
 
     // Execute the function. This will block until complete or error.
-    if (engine->Run()) {
+    if (engine->run()) {
         env->ThrowNew(env->FindClass("java/lang/RuntimeException"),
                       "Runtime error occurred while running the function.");
         return;
diff --git a/cpp/telemetry/script_executor/src/ScriptExecutorListener.cpp b/cpp/telemetry/script_executor/src/ScriptExecutorListener.cpp
index 8c10aa4..d80aae8 100644
--- a/cpp/telemetry/script_executor/src/ScriptExecutorListener.cpp
+++ b/cpp/telemetry/script_executor/src/ScriptExecutorListener.cpp
@@ -17,7 +17,6 @@
 #include "ScriptExecutorListener.h"
 
 #include <android-base/logging.h>
-#include <android_runtime/AndroidRuntime.h>
 
 namespace android {
 namespace automotive {
@@ -25,14 +24,27 @@
 namespace script_executor {
 
 ScriptExecutorListener::~ScriptExecutorListener() {
-    if (mScriptExecutorListener != NULL) {
-        JNIEnv* env = AndroidRuntime::getJNIEnv();
+    JNIEnv* env = getCurrentJNIEnv();
+    if (mScriptExecutorListener != nullptr) {
         env->DeleteGlobalRef(mScriptExecutorListener);
     }
 }
 
 ScriptExecutorListener::ScriptExecutorListener(JNIEnv* env, jobject script_executor_listener) {
     mScriptExecutorListener = env->NewGlobalRef(script_executor_listener);
+    env->GetJavaVM(&mJavaVM);
+}
+
+void ScriptExecutorListener::onSuccess(jobject bundle) {
+    JNIEnv* env = getCurrentJNIEnv();
+    if (mScriptExecutorListener == nullptr) {
+        env->FatalError(
+                "mScriptExecutorListener must point to a valid listener object, not nullptr.");
+    }
+    jclass listenerClass = env->GetObjectClass(mScriptExecutorListener);
+    jmethodID onSuccessMethod =
+            env->GetMethodID(listenerClass, "onSuccess", "(Landroid/os/Bundle;)V");
+    env->CallVoidMethod(mScriptExecutorListener, onSuccessMethod, bundle);
 }
 
 void ScriptExecutorListener::onError(const int errorType, const std::string& message,
@@ -41,6 +53,14 @@
               << ", stackTrace: " << stackTrace;
 }
 
+JNIEnv* ScriptExecutorListener::getCurrentJNIEnv() {
+    JNIEnv* env;
+    if (mJavaVM->GetEnv(reinterpret_cast<void**>(&env), JNI_VERSION_1_6) != JNI_OK) {
+        LOG(FATAL) << "Unable to return JNIEnv from JavaVM";
+    }
+    return env;
+}
+
 }  // namespace script_executor
 }  // namespace telemetry
 }  // namespace automotive
diff --git a/cpp/telemetry/script_executor/src/ScriptExecutorListener.h b/cpp/telemetry/script_executor/src/ScriptExecutorListener.h
index 1e5c7d7..5f6c380 100644
--- a/cpp/telemetry/script_executor/src/ScriptExecutorListener.h
+++ b/cpp/telemetry/script_executor/src/ScriptExecutorListener.h
@@ -35,13 +35,20 @@
 
     void onScriptFinished() {}
 
-    void onSuccess() {}
+    void onSuccess(jobject bundle);
 
     void onError(const int errorType, const std::string& message, const std::string& stackTrace);
 
+    JNIEnv* getCurrentJNIEnv();
+
 private:
     // Stores a jni global reference to Java Script Executor listener object.
     jobject mScriptExecutorListener;
+
+    // Stores JavaVM pointer in order to be able to get JNIEnv pointer.
+    // This is done because JNIEnv cannot be shared between threads.
+    // https://developer.android.com/training/articles/perf-jni.html#javavm-and-jnienv
+    JavaVM* mJavaVM;
 };
 
 }  // namespace script_executor
diff --git a/cpp/telemetry/script_executor/src/tests/JniUtilsTestHelper.cpp b/cpp/telemetry/script_executor/src/tests/JniUtilsTestHelper.cpp
index 9e2c43a..8cdf87a 100644
--- a/cpp/telemetry/script_executor/src/tests/JniUtilsTestHelper.cpp
+++ b/cpp/telemetry/script_executor/src/tests/JniUtilsTestHelper.cpp
@@ -44,21 +44,21 @@
 
 JNIEXPORT void JNICALL Java_com_android_car_telemetry_JniUtilsTest_nativePushBundleToLuaTableCaller(
         JNIEnv* env, jobject object, jlong luaEnginePtr, jobject bundle) {
-    PushBundleToLuaTable(env, reinterpret_cast<LuaEngine*>(static_cast<intptr_t>(luaEnginePtr)),
+    pushBundleToLuaTable(env, reinterpret_cast<LuaEngine*>(static_cast<intptr_t>(luaEnginePtr)),
                          bundle);
 }
 
 JNIEXPORT jint JNICALL Java_com_android_car_telemetry_JniUtilsTest_nativeGetObjectSize(
         JNIEnv* env, jobject object, jlong luaEnginePtr, jint index) {
     LuaEngine* engine = reinterpret_cast<LuaEngine*>(static_cast<intptr_t>(luaEnginePtr));
-    return lua_rawlen(engine->GetLuaState(), static_cast<int>(index));
+    return lua_rawlen(engine->getLuaState(), static_cast<int>(index));
 }
 
 JNIEXPORT bool JNICALL Java_com_android_car_telemetry_JniUtilsTest_nativeHasBooleanValue(
         JNIEnv* env, jobject object, jlong luaEnginePtr, jstring key, jboolean value) {
     const char* rawKey = env->GetStringUTFChars(key, nullptr);
     LuaEngine* engine = reinterpret_cast<LuaEngine*>(static_cast<intptr_t>(luaEnginePtr));
-    auto* luaState = engine->GetLuaState();
+    auto* luaState = engine->getLuaState();
     lua_pushstring(luaState, rawKey);
     env->ReleaseStringUTFChars(key, rawKey);
     lua_gettable(luaState, -2);
@@ -76,7 +76,7 @@
     const char* rawKey = env->GetStringUTFChars(key, nullptr);
     LuaEngine* engine = reinterpret_cast<LuaEngine*>(static_cast<intptr_t>(luaEnginePtr));
     // Assumes the table is on top of the stack.
-    auto* luaState = engine->GetLuaState();
+    auto* luaState = engine->getLuaState();
     lua_pushstring(luaState, rawKey);
     env->ReleaseStringUTFChars(key, rawKey);
     lua_gettable(luaState, -2);
@@ -94,7 +94,7 @@
     const char* rawKey = env->GetStringUTFChars(key, nullptr);
     LuaEngine* engine = reinterpret_cast<LuaEngine*>(static_cast<intptr_t>(luaEnginePtr));
     // Assumes the table is on top of the stack.
-    auto* luaState = engine->GetLuaState();
+    auto* luaState = engine->getLuaState();
     lua_pushstring(luaState, rawKey);
     env->ReleaseStringUTFChars(key, rawKey);
     lua_gettable(luaState, -2);
@@ -112,7 +112,7 @@
     const char* rawKey = env->GetStringUTFChars(key, nullptr);
     LuaEngine* engine = reinterpret_cast<LuaEngine*>(static_cast<intptr_t>(luaEnginePtr));
     // Assumes the table is on top of the stack.
-    auto* luaState = engine->GetLuaState();
+    auto* luaState = engine->getLuaState();
     lua_pushstring(luaState, rawKey);
     env->ReleaseStringUTFChars(key, rawKey);
     lua_gettable(luaState, -2);
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
index 774da25..344fb04 100644
--- a/tests/carservice_unit_test/src/com/android/car/telemetry/ScriptExecutorTest.java
+++ b/tests/carservice_unit_test/src/com/android/car/telemetry/ScriptExecutorTest.java
@@ -16,6 +16,8 @@
 
 package com.android.car.telemetry;
 
+import static com.google.common.truth.Truth.assertThat;
+
 import static org.junit.Assert.fail;
 
 import android.car.telemetry.IScriptExecutor;
@@ -47,12 +49,17 @@
 
 
     private static final class ScriptExecutorListener extends IScriptExecutorListener.Stub {
+        public Bundle mSavedBundle;
+        public final CountDownLatch mSuccessLatch = new CountDownLatch(1);
+
         @Override
         public void onScriptFinished(byte[] result) {
         }
 
         @Override
         public void onSuccess(Bundle stateToPersist) {
+            mSavedBundle = stateToPersist;
+            mSuccessLatch.countDown();
         }
 
         @Override
@@ -60,7 +67,7 @@
         }
     }
 
-    private final IScriptExecutorListener mFakeScriptExecutorListener =
+    private final ScriptExecutorListener mFakeScriptExecutorListener =
             new ScriptExecutorListener();
 
     // TODO(b/189241508). Parsing of publishedData is not implemented yet.
@@ -69,15 +76,16 @@
     private final Bundle mSavedState = new Bundle();
 
     private static final String LUA_SCRIPT =
-            "function hello(data, state)\n"
-            + "    print(\"Hello World\")\n"
-            + "end\n";
+            "function hello(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 static final int SCRIPT_SUCCESS_TIMEOUT_SEC = 10;
 
     private final ServiceConnection mScriptExecutorConnection =
             new ServiceConnection() {
@@ -93,6 +101,22 @@
                 }
             };
 
+    // Helper method to invoke the script and wait for it to complete and return the result.
+    public void runScriptAndWaitForResult(String script, String function, Bundle previousState)
+            throws RemoteException {
+        mScriptExecutor.invokeScript(script, function, mPublishedData, previousState,
+                mFakeScriptExecutorListener);
+        try {
+            if (!mFakeScriptExecutorListener.mSuccessLatch.await(SCRIPT_SUCCESS_TIMEOUT_SEC,
+                    TimeUnit.SECONDS)) {
+                fail("Failed to get on_success called by the script on time");
+            }
+        } catch (InterruptedException e) {
+            e.printStackTrace();
+            fail(e.getMessage());
+        }
+    }
+
     @Before
     public void setUp() throws InterruptedException {
         mContext.bindIsolatedService(new Intent(mContext, ScriptExecutor.class),
@@ -119,5 +143,129 @@
             fail(e.getMessage());
         }
     }
+
+    @Test
+    public void invokeScript_returnsResult() throws RemoteException {
+        String returnResultScript =
+                "function hello(state)\n"
+                        + "    result = {hello=\"world\"}\n"
+                        + "    on_success(result)\n"
+                        + "end\n";
+
+
+        runScriptAndWaitForResult(returnResultScript, "hello", mSavedState);
+
+        // Expect to get back a bundle with a single string key: string value pair:
+        // {"hello": "world"}
+        assertThat(mFakeScriptExecutorListener.mSavedBundle.size()).isEqualTo(1);
+        assertThat(mFakeScriptExecutorListener.mSavedBundle.getString("hello")).isEqualTo("world");
+    }
+
+    @Test
+    public void invokeScript_allSupportedTypes() throws RemoteException {
+        String script =
+                "function knows(state)\n"
+                        + "    result = {string=\"hello\", boolean=true, integer=1, number=1.1}\n"
+                        + "    on_success(result)\n"
+                        + "end\n";
+
+
+        runScriptAndWaitForResult(script, "knows", mSavedState);
+
+        // Expect to get back a bundle with 4 keys, each corresponding to a distinct supported type.
+        assertThat(mFakeScriptExecutorListener.mSavedBundle.size()).isEqualTo(4);
+        assertThat(mFakeScriptExecutorListener.mSavedBundle.getString("string")).isEqualTo("hello");
+        assertThat(mFakeScriptExecutorListener.mSavedBundle.getBoolean("boolean")).isEqualTo(true);
+        assertThat(mFakeScriptExecutorListener.mSavedBundle.getInt("integer")).isEqualTo(1);
+        assertThat(mFakeScriptExecutorListener.mSavedBundle.getDouble("number")).isEqualTo(1.1);
+    }
+
+    @Test
+    public void invokeScript_skipsUnsupportedTypes() throws RemoteException {
+        String script =
+                "function nested(state)\n"
+                        + "    result = {string=\"hello\", boolean=true, integer=1, number=1.1}\n"
+                        + "    result.nested_table = {x=0, y=0}\n"
+                        + "    on_success(result)\n"
+                        + "end\n";
+
+
+        runScriptAndWaitForResult(script, "nested", mSavedState);
+
+        // Bundle does not contain any value under "nested_table" key, because nested tables are
+        // not supported yet.
+        assertThat(mFakeScriptExecutorListener.mSavedBundle.size()).isEqualTo(4);
+        assertThat(mFakeScriptExecutorListener.mSavedBundle.getString("nested_table")).isNull();
+    }
+
+    @Test
+    public void invokeScript_emptyBundle() throws RemoteException {
+        String script =
+                "function empty(state)\n"
+                        + "    result = {}\n"
+                        + "    on_success(result)\n"
+                        + "end\n";
+
+
+        runScriptAndWaitForResult(script, "empty", mSavedState);
+
+        // If a script returns empty table as the result, we get an empty bundle.
+        assertThat(mFakeScriptExecutorListener.mSavedBundle).isNotNull();
+        assertThat(mFakeScriptExecutorListener.mSavedBundle.size()).isEqualTo(0);
+    }
+
+    @Test
+    public void invokeScript_processPreviousStateAndReturnResult() throws RemoteException {
+        // Here we verify that the script actually processes provided state from a previous run
+        // and makes calculation based on that and returns the result.
+        // TODO(b/189241508): update function signatures.
+        String script =
+                "function update(state)\n"
+                        + "    result = {y = state.x+1}\n"
+                        + "    on_success(result)\n"
+                        + "end\n";
+        Bundle previousState = new Bundle();
+        previousState.putInt("x", 1);
+
+
+        runScriptAndWaitForResult(script, "update", previousState);
+
+        // Verify that y = 2, because y = x + 1 and x = 1.
+        assertThat(mFakeScriptExecutorListener.mSavedBundle.size()).isEqualTo(1);
+        assertThat(mFakeScriptExecutorListener.mSavedBundle.getInt("y")).isEqualTo(2);
+    }
+
+    @Test
+    public void invokeScript_allSupportedTypesWorkRoundTripWithKeyNamesPreserved()
+            throws RemoteException {
+        // Here we verify that all supported types in supplied previous state Bundle are interpreted
+        // by the script as expected.
+        // TODO(b/189241508): update function signatures.
+        String script =
+                "function update_all(state)\n"
+                        + "    result = {}\n"
+                        + "    result.integer = state.integer + 1\n"
+                        + "    result.number = state.number + 0.1\n"
+                        + "    result.boolean = not state.boolean\n"
+                        + "    result.string = state.string .. \"CADABRA\"\n"
+                        + "    on_success(result)\n"
+                        + "end\n";
+        Bundle previousState = new Bundle();
+        previousState.putInt("integer", 1);
+        previousState.putDouble("number", 0.1);
+        previousState.putBoolean("boolean", false);
+        previousState.putString("string", "ABRA");
+
+
+        runScriptAndWaitForResult(script, "update_all", previousState);
+
+        // Verify that keys are preserved but the values are modified as expected.
+        assertThat(mFakeScriptExecutorListener.mSavedBundle.size()).isEqualTo(4);
+        assertThat(mFakeScriptExecutorListener.mSavedBundle.getInt("integer")).isEqualTo(2);
+        assertThat(mFakeScriptExecutorListener.mSavedBundle.getDouble("number")).isEqualTo(0.2);
+        assertThat(mFakeScriptExecutorListener.mSavedBundle.getBoolean("boolean")).isEqualTo(true);
+        assertThat(mFakeScriptExecutorListener.mSavedBundle.getString("string")).isEqualTo(
+                "ABRACADABRA");
+    }
 }