Merge "Add utility function that takes Java Bundle, converts it and pushes it to Lua table." into sc-dev
diff --git a/cpp/telemetry/script_executor/Android.bp b/cpp/telemetry/script_executor/Android.bp
index 734d4dd..78b639a 100644
--- a/cpp/telemetry/script_executor/Android.bp
+++ b/cpp/telemetry/script_executor/Android.bp
@@ -16,13 +16,47 @@
     default_applicable_licenses: ["Android-Apache-2.0"],
 }
 
-cc_library {
-   name: "script_executor",
-   srcs: [
-       "src/LuaEngine.cpp",
-   ],
-   static_libs: [
-       "liblua",
-   ],
+cc_defaults {
+    name: "scriptexecutor_defaults",
+    cflags: [
+        "-Wno-unused-parameter",
+    ],
+    static_libs: [
+        "libbase",
+        "liblog",
+        "liblua",
+    ],
 }
 
+cc_library {
+    name: "libscriptexecutor",
+    defaults: [
+        "scriptexecutor_defaults",
+    ],
+    srcs: [
+        "src/JniUtils.cpp",
+        "src/LuaEngine.cpp",
+        "src/ScriptExecutorListener.cpp",
+    ],
+    shared_libs: [
+        "libnativehelper",
+    ],
+    // Allow dependents to use the header files.
+    export_include_dirs: [
+        "src",
+    ],
+}
+
+cc_library_shared {
+    name: "libscriptexecutorjniutils-test",
+    defaults: [
+        "scriptexecutor_defaults",
+    ],
+    srcs: [
+        "src/tests/JniUtilsTestHelper.cpp",
+    ],
+    shared_libs: [
+        "libnativehelper",
+        "libscriptexecutor",
+    ],
+}
diff --git a/cpp/telemetry/script_executor/src/JniUtils.cpp b/cpp/telemetry/script_executor/src/JniUtils.cpp
new file mode 100644
index 0000000..93c1af8
--- /dev/null
+++ b/cpp/telemetry/script_executor/src/JniUtils.cpp
@@ -0,0 +1,95 @@
+/*
+ * 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"
+
+namespace android {
+namespace automotive {
+namespace telemetry {
+namespace script_executor {
+
+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;
+    }
+
+    // TODO(b/188832769): Consider caching some of these JNI references for
+    // performance reasons.
+    jclass bundleClass = env->FindClass("android/os/Bundle");
+    jmethodID getKeySetMethod = env->GetMethodID(bundleClass, "keySet", "()Ljava/util/Set;");
+    jobject keys = env->CallObjectMethod(bundle, getKeySetMethod);
+    jclass setClass = env->FindClass("java/util/Set");
+    jmethodID iteratorMethod = env->GetMethodID(setClass, "iterator", "()Ljava/util/Iterator;");
+    jobject keySetIteratorObject = env->CallObjectMethod(keys, iteratorMethod);
+
+    jclass iteratorClass = env->FindClass("java/util/Iterator");
+    jmethodID hasNextMethod = env->GetMethodID(iteratorClass, "hasNext", "()Z");
+    jmethodID nextMethod = env->GetMethodID(iteratorClass, "next", "()Ljava/lang/Object;");
+
+    jclass booleanClass = env->FindClass("java/lang/Boolean");
+    jclass integerClass = env->FindClass("java/lang/Integer");
+    jclass numberClass = env->FindClass("java/lang/Number");
+    jclass stringClass = env->FindClass("java/lang/String");
+    // TODO(b/188816922): Handle more types such as float and integer arrays,
+    // and perhaps nested Bundles.
+
+    jmethodID getMethod =
+            env->GetMethodID(bundleClass, "get", "(Ljava/lang/String;)Ljava/lang/Object;");
+
+    // Iterate over key set of the bundle one key at a time.
+    while (env->CallBooleanMethod(keySetIteratorObject, hasNextMethod)) {
+        // Read the value object that corresponds to this key.
+        jstring key = (jstring)env->CallObjectMethod(keySetIteratorObject, nextMethod);
+        jobject value = env->CallObjectMethod(bundle, getMethod, key);
+
+        // Get the value of the type, extract it accordingly from the bundle and
+        // push the extracted value and the key to the Lua table.
+        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);
+        } else if (env->IsInstanceOf(value, integerClass)) {
+            jmethodID intMethod = env->GetMethodID(integerClass, "intValue", "()I");
+            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));
+        } else if (env->IsInstanceOf(value, stringClass)) {
+            const char* rawStringValue = env->GetStringUTFChars((jstring)value, nullptr);
+            lua_pushstring(luaEngine->GetLuaState(), rawStringValue);
+            env->ReleaseStringUTFChars((jstring)value, rawStringValue);
+        } else {
+            // Other types are not implemented yet, skipping.
+            continue;
+        }
+
+        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);
+        env->ReleaseStringUTFChars(key, rawKey);
+    }
+}
+
+}  // namespace script_executor
+}  // namespace telemetry
+}  // namespace automotive
+}  // namespace android
diff --git a/cpp/telemetry/script_executor/src/JniUtils.h b/cpp/telemetry/script_executor/src/JniUtils.h
new file mode 100644
index 0000000..c3ef677
--- /dev/null
+++ b/cpp/telemetry/script_executor/src/JniUtils.h
@@ -0,0 +1,39 @@
+/*
+ * 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_JNIUTILS_H_
+#define CPP_TELEMETRY_SCRIPT_EXECUTOR_SRC_JNIUTILS_H_
+
+#include "LuaEngine.h"
+#include "jni.h"
+
+namespace android {
+namespace automotive {
+namespace telemetry {
+namespace script_executor {
+
+// Helper function which takes android.os.Bundle object in "bundle" argument
+// and converts it to Lua table on top of Lua stack. All key-value pairs are
+// 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);
+
+}  // namespace script_executor
+}  // namespace telemetry
+}  // namespace automotive
+}  // namespace android
+
+#endif  // CPP_TELEMETRY_SCRIPT_EXECUTOR_SRC_JNIUTILS_H_
diff --git a/cpp/telemetry/script_executor/src/LuaEngine.cpp b/cpp/telemetry/script_executor/src/LuaEngine.cpp
index a8cace3..cc1d0b8 100644
--- a/cpp/telemetry/script_executor/src/LuaEngine.cpp
+++ b/cpp/telemetry/script_executor/src/LuaEngine.cpp
@@ -28,8 +28,7 @@
 namespace telemetry {
 namespace script_executor {
 
-LuaEngine::LuaEngine(std::unique_ptr<ScriptExecutorListener> listener) :
-      mListener(std::move(listener)) {
+LuaEngine::LuaEngine() {
     mLuaState = luaL_newstate();
     luaL_openlibs(mLuaState);
 }
@@ -38,6 +37,10 @@
     lua_close(mLuaState);
 }
 
+lua_State* LuaEngine::GetLuaState() {
+    return mLuaState;
+}
+
 }  // 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 086dbfe..a0f3978 100644
--- a/cpp/telemetry/script_executor/src/LuaEngine.h
+++ b/cpp/telemetry/script_executor/src/LuaEngine.h
@@ -33,14 +33,15 @@
 // Encapsulates Lua script execution environment.
 class LuaEngine {
 public:
-    explicit LuaEngine(std::unique_ptr<ScriptExecutorListener> listener);
+    LuaEngine();
 
     virtual ~LuaEngine();
 
+    // Returns pointer to Lua state object.
+    lua_State* GetLuaState();
+
 private:
     lua_State* mLuaState;  // owned
-
-    std::unique_ptr<ScriptExecutorListener> mListener;
 };
 
 }  // namespace script_executor
diff --git a/cpp/telemetry/script_executor/src/tests/JniUtilsTestHelper.cpp b/cpp/telemetry/script_executor/src/tests/JniUtilsTestHelper.cpp
new file mode 100644
index 0000000..9e2c43a
--- /dev/null
+++ b/cpp/telemetry/script_executor/src/tests/JniUtilsTestHelper.cpp
@@ -0,0 +1,138 @@
+/*
+ * 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 "jni.h"
+
+#include <cstdint>
+#include <cstring>
+
+namespace android {
+namespace automotive {
+namespace telemetry {
+namespace script_executor {
+namespace {
+
+extern "C" {
+
+#include "lua.h"
+
+JNIEXPORT jlong JNICALL
+Java_com_android_car_telemetry_JniUtilsTest_nativeCreateLuaEngine(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_JniUtilsTest_nativeDestroyLuaEngine(
+        JNIEnv* env, jobject object, jlong luaEnginePtr) {
+    delete reinterpret_cast<LuaEngine*>(static_cast<intptr_t>(luaEnginePtr));
+}
+
+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)),
+                         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));
+}
+
+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();
+    lua_pushstring(luaState, rawKey);
+    env->ReleaseStringUTFChars(key, rawKey);
+    lua_gettable(luaState, -2);
+    bool result = false;
+    if (!lua_isboolean(luaState, -1))
+        result = false;
+    else
+        result = static_cast<bool>(lua_toboolean(luaState, -1)) == static_cast<bool>(value);
+    lua_pop(luaState, 1);
+    return result;
+}
+
+JNIEXPORT bool JNICALL Java_com_android_car_telemetry_JniUtilsTest_nativeHasIntValue(
+        JNIEnv* env, jobject object, jlong luaEnginePtr, jstring key, jint value) {
+    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();
+    lua_pushstring(luaState, rawKey);
+    env->ReleaseStringUTFChars(key, rawKey);
+    lua_gettable(luaState, -2);
+    bool result = false;
+    if (!lua_isinteger(luaState, -1))
+        result = false;
+    else
+        result = lua_tointeger(luaState, -1) == static_cast<int>(value);
+    lua_pop(luaState, 1);
+    return result;
+}
+
+JNIEXPORT bool JNICALL Java_com_android_car_telemetry_JniUtilsTest_nativeHasDoubleValue(
+        JNIEnv* env, jobject object, jlong luaEnginePtr, jstring key, jdouble value) {
+    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();
+    lua_pushstring(luaState, rawKey);
+    env->ReleaseStringUTFChars(key, rawKey);
+    lua_gettable(luaState, -2);
+    bool result = false;
+    if (!lua_isnumber(luaState, -1))
+        result = false;
+    else
+        result = static_cast<double>(lua_tonumber(luaState, -1)) == static_cast<double>(value);
+    lua_pop(luaState, 1);
+    return result;
+}
+
+JNIEXPORT bool JNICALL Java_com_android_car_telemetry_JniUtilsTest_nativeHasStringValue(
+        JNIEnv* env, jobject object, jlong luaEnginePtr, jstring key, jstring value) {
+    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();
+    lua_pushstring(luaState, rawKey);
+    env->ReleaseStringUTFChars(key, rawKey);
+    lua_gettable(luaState, -2);
+    bool result = false;
+    if (!lua_isstring(luaState, -1)) {
+        result = false;
+    } else {
+        std::string s = lua_tostring(luaState, -1);
+        const char* rawValue = env->GetStringUTFChars(value, nullptr);
+        result = strcmp(lua_tostring(luaState, -1), rawValue) == 0;
+        env->ReleaseStringUTFChars(value, rawValue);
+    }
+    lua_pop(luaState, 1);
+    return result;
+}
+
+}  //  extern "C"
+
+}  //  namespace
+}  //  namespace script_executor
+}  //  namespace telemetry
+}  //  namespace automotive
+}  //  namespace android
diff --git a/tests/carservice_unit_test/Android.bp b/tests/carservice_unit_test/Android.bp
index 57e14d9..cf9d238 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",
+        "libscriptexecutorjniutils-test",
         "libstaticjvmtiagent",
     ],
 }
diff --git a/tests/carservice_unit_test/src/com/android/car/telemetry/JniUtilsTest.java b/tests/carservice_unit_test/src/com/android/car/telemetry/JniUtilsTest.java
new file mode 100644
index 0000000..67a5ac8
--- /dev/null
+++ b/tests/carservice_unit_test/src/com/android/car/telemetry/JniUtilsTest.java
@@ -0,0 +1,124 @@
+/*
+ * 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 com.google.common.truth.Truth.assertThat;
+
+import android.os.Bundle;
+import android.util.Log;
+
+import org.junit.After;
+import org.junit.Before;
+import org.junit.Test;
+import org.junit.runner.RunWith;
+import org.junit.runners.JUnit4;
+
+@RunWith(JUnit4.class)
+public final class JniUtilsTest {
+
+    private static final String TAG = JniUtilsTest.class.getSimpleName();
+
+    private static final String BOOLEAN_KEY = "boolean_key";
+    private static final String INT_KEY = "int_key";
+    private static final String STRING_KEY = "string_key";
+    private static final String NUMBER_KEY = "number_key";
+
+    private static final boolean BOOLEAN_VALUE = true;
+    private static final double NUMBER_VALUE = 0.1;
+    private static final int INT_VALUE = 10;
+    private static final String STRING_VALUE = "test";
+
+    // Pointer to Lua Engine instantiated in native space.
+    private long mLuaEnginePtr = 0;
+
+    static {
+        System.loadLibrary("scriptexecutorjniutils-test");
+    }
+
+    @Before
+    public void setUp() {
+        mLuaEnginePtr = nativeCreateLuaEngine();
+    }
+
+    @After
+    public void tearDown() {
+        nativeDestroyLuaEngine(mLuaEnginePtr);
+    }
+
+    // Simply invokes PushBundleToLuaTable native method under test.
+    private native void nativePushBundleToLuaTableCaller(long luaEnginePtr, Bundle bundle);
+
+    // Creates an instance of LuaEngine on the heap and returns the pointer.
+    private native long nativeCreateLuaEngine();
+
+    // Destroys instance of LuaEngine on the native side at provided memory address.
+    private native void nativeDestroyLuaEngine(long luaEnginePtr);
+
+    // Returns size of a Lua object located at the specified position on the stack.
+    private native int nativeGetObjectSize(long luaEnginePtr, int index);
+
+    /*
+     * Family of methods to check if the table on top of the stack has
+     * the given value under provided key.
+     */
+    private native boolean nativeHasBooleanValue(long luaEnginePtr, String key, boolean value);
+    private native boolean nativeHasStringValue(long luaEnginePtr, String key, String value);
+    private native boolean nativeHasIntValue(long luaEnginePtr, String key, int value);
+    private native boolean nativeHasDoubleValue(long luaEnginePtr, String key, double value);
+
+    @Test
+    public void pushBundleToLuaTable_nullBundleMakesEmptyLuaTable() {
+        Log.d(TAG, "Using Lua Engine with address " + mLuaEnginePtr);
+        nativePushBundleToLuaTableCaller(mLuaEnginePtr, null);
+        // Get the size of the object on top of the stack,
+        // which is where our table is supposed to be.
+        assertThat(nativeGetObjectSize(mLuaEnginePtr, 1)).isEqualTo(0);
+    }
+
+    @Test
+    public void pushBundleToLuaTable_valuesOfDifferentTypes() {
+        Bundle bundle = new Bundle();
+        bundle.putBoolean(BOOLEAN_KEY, BOOLEAN_VALUE);
+        bundle.putInt(INT_KEY, INT_VALUE);
+        bundle.putDouble(NUMBER_KEY, NUMBER_VALUE);
+        bundle.putString(STRING_KEY, STRING_VALUE);
+
+        // Invokes the corresponding helper method to convert the bundle
+        // to Lua table on Lua stack.
+        nativePushBundleToLuaTableCaller(mLuaEnginePtr, bundle);
+
+        // Check contents of Lua table.
+        assertThat(nativeHasBooleanValue(mLuaEnginePtr, BOOLEAN_KEY, BOOLEAN_VALUE)).isTrue();
+        assertThat(nativeHasIntValue(mLuaEnginePtr, INT_KEY, INT_VALUE)).isTrue();
+        assertThat(nativeHasDoubleValue(mLuaEnginePtr, NUMBER_KEY, NUMBER_VALUE)).isTrue();
+        assertThat(nativeHasStringValue(mLuaEnginePtr, STRING_KEY, STRING_VALUE)).isTrue();
+    }
+
+
+    @Test
+    public void pushBundleToLuaTable_wrongKey() {
+        Bundle bundle = new Bundle();
+        bundle.putBoolean(BOOLEAN_KEY, BOOLEAN_VALUE);
+
+        // Invokes the corresponding helper method to convert the bundle
+        // to Lua table on Lua stack.
+        nativePushBundleToLuaTableCaller(mLuaEnginePtr, bundle);
+
+        // Check contents of Lua table.
+        assertThat(nativeHasBooleanValue(mLuaEnginePtr, "wrong key", BOOLEAN_VALUE)).isFalse();
+    }
+}