Merge "Introduce ability for the script to send the results back." into sc-v2-dev
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");
+    }
 }