diff --git a/analytics-kotlin-live/src/androidTest/java/com/segment/analytics/liveplugins/kotlin/JSStorageTest.kt b/analytics-kotlin-live/src/androidTest/java/com/segment/analytics/liveplugins/kotlin/JSStorageTest.kt new file mode 100644 index 0000000..c4d9f7b --- /dev/null +++ b/analytics-kotlin-live/src/androidTest/java/com/segment/analytics/liveplugins/kotlin/JSStorageTest.kt @@ -0,0 +1,262 @@ +package com.segment.analytics.liveplugins.kotlin + +import androidx.test.ext.junit.runners.AndroidJUnit4 +import com.segment.analytics.kotlin.core.utilities.getInt +import com.segment.analytics.kotlin.core.utilities.getString +import com.segment.analytics.liveplugins.kotlin.utils.MemorySharedPreferences +import com.segment.analytics.substrata.kotlin.JSArray +import com.segment.analytics.substrata.kotlin.JSObject +import com.segment.analytics.substrata.kotlin.JSScope +import com.segment.analytics.substrata.kotlin.JsonElementConverter +import kotlinx.coroutines.ExperimentalCoroutinesApi +import kotlinx.serialization.json.jsonArray +import kotlinx.serialization.json.jsonObject +import kotlinx.serialization.json.jsonPrimitive +import org.junit.Assert.assertEquals +import org.junit.Assert.assertNull +import org.junit.Before +import org.junit.Test +import org.junit.runner.RunWith + + +@OptIn(ExperimentalCoroutinesApi::class) +@RunWith(AndroidJUnit4::class) +class JSStorageTest { + + private lateinit var engine: JSScope + private lateinit var jsStorage: JSStorage + private var exceptionThrown: Throwable? = null + + @Before + fun setUp() { + exceptionThrown = null + + engine = JSScope{ exception -> + exceptionThrown = exception + } + jsStorage = JSStorage(MemorySharedPreferences(), engine) + // Setup the engine similar to LivePlugins.configureEngine + engine.sync { + export(jsStorage, "Storage", "storage") + } + } + + @Test + fun testJSStorageWithInt() { + // set from js + var value = engine.await { + evaluate("""storage.setValue("int", 1)""") + evaluate("""storage.getValue("int")""") + } + assertNull(exceptionThrown) + assertEquals(1, value) + assertEquals(1, jsStorage.getValue("int")) + + // set from native + jsStorage.setValue("int", 2) + value = engine.await { + evaluate("""storage.getValue("int")""") + } + assertEquals(2, value) + assertEquals(2, jsStorage.getValue("int")) + } + + @Test + fun testJSStorageWithBoolean() { + // set from js + var value = engine.await { + evaluate("""storage.setValue("boolean", true)""") + evaluate("""storage.getValue("boolean")""") + } + assertNull(exceptionThrown) + assertEquals(true, value) + assertEquals(true, jsStorage.getValue("boolean")) + + // set from native + jsStorage.setValue("boolean", false) + value = engine.await { + evaluate("""storage.getValue("boolean")""") + } + assertEquals(false, value) + assertEquals(false, jsStorage.getValue("boolean")) + } + + @Test + fun testJSStorageWithDouble() { + // set from js + var value = engine.await { + evaluate("""storage.setValue("double", 3.14)""") + evaluate("""storage.getValue("double")""") + } + assertNull(exceptionThrown) + assertEquals(3.14, value) + assertEquals(3.14, jsStorage.getValue("double")) + + // set from native + jsStorage.setValue("double", 2.71) + value = engine.await { + evaluate("""storage.getValue("double")""") + } + assertEquals(2.71, value) + assertEquals(2.71, jsStorage.getValue("double")) + } + + @Test + fun testJSStorageWithString() { + // set from js + var value = engine.await { + evaluate("""storage.setValue("string", "hello")""") + evaluate("""storage.getValue("string")""") + } + assertNull(exceptionThrown) + assertEquals("hello", value) + assertEquals("hello", jsStorage.getValue("string")) + + // set from native + jsStorage.setValue("string", "world") + value = engine.await { + evaluate("""storage.getValue("string")""") + } + assertEquals("world", value) + assertEquals("world", jsStorage.getValue("string")) + } + + @Test + fun testJSStorageWithLong() { + // set from js + var value = engine.await { + evaluate("""storage.setValue("long", 1234567890123)""") + evaluate("""storage.getValue("long")""") + } + assertNull(exceptionThrown) + assertEquals(1234567890123L.toDouble(), value) + assertEquals(1234567890123L.toDouble(), jsStorage.getValue("long")) + + // set from native + jsStorage.setValue("long", 9876543210987L) + value = engine.await { + evaluate("""storage.getValue("long")""") + } + assertEquals(9876543210987L.toDouble(), value) + assertEquals(9876543210987L.toDouble(), jsStorage.getValue("long")) + } + + @Test + fun testJSStorageWithJSObject() { + // set from js + var value = engine.await(true) { + evaluate("""storage.setValue("object", {name: "test", value: 42})""") + evaluate("""storage.getValue("object")""") + } + assertNull(exceptionThrown) + val jsonObject = JsonElementConverter.read(value).jsonObject + assertEquals("test", jsonObject.getString("name")) + assertEquals(42, jsonObject.getInt("value")) + + // set from native + val nativeObject = engine.await(true) { + evaluate(""" + let obj = {name: "native", value: 100} + obj + """.trimIndent()) + } + jsStorage.setValue("object", nativeObject as JSObject) + value = engine.await { + evaluate(""" + let obj2 = storage.getValue("object") + obj.name == obj2.name && obj.value == obj2.value + """.trimIndent()) + } + assertEquals(true, value) + val jsValue = jsStorage.getValue("object") + val retrievedObject = JsonElementConverter.read(jsValue).jsonObject + assertEquals("native", retrievedObject.getString("name")) + assertEquals(100, retrievedObject.getInt("value")) + } + + @Test + fun testJSStorageWithJSArray() { + // set from js + var value = engine.await(true) { + evaluate("""storage.setValue("array", [1, "test", true])""") + evaluate("""storage.getValue("array")""") + } + assertNull(exceptionThrown) + val jsonArray = JsonElementConverter.read(value).jsonArray + assertEquals(3, jsonArray.size) + assertEquals(1, jsonArray[0].jsonPrimitive.content.toInt()) + assertEquals("test", jsonArray[1].jsonPrimitive.content) + assertEquals(true, jsonArray[2].jsonPrimitive.content.toBoolean()) + + // set from native + val nativeArray = engine.await(true) { + evaluate(""" + let arr = [42, "native", false] + arr + """.trimIndent()) + } + jsStorage.setValue("array", nativeArray as JSArray) + value = engine.await { + evaluate(""" + let arr2 = storage.getValue("array") + arr.length == arr2.length && arr[0] == arr2[0] && arr[1] == arr2[1] && arr[2] == arr2[2] + """.trimIndent()) + } + assertEquals(true, value) + val jsValue = jsStorage.getValue("array") + val retrievedArray = JsonElementConverter.read(jsValue).jsonArray + assertEquals(3, retrievedArray.size) + assertEquals(42, retrievedArray[0].jsonPrimitive.content.toInt()) + assertEquals("native", retrievedArray[1].jsonPrimitive.content) + assertEquals(false, retrievedArray[2].jsonPrimitive.content.toBoolean()) + } + + @Test + fun testJSStorageRemoveValue() { + // 1. set from js and remove from js + engine.sync { + evaluate("""storage.setValue("jsJs", "value1")""") + evaluate("""storage.removeValue("jsJs")""") + } + assertNull(exceptionThrown) + assertNull(jsStorage.getValue("jsJs")) + val jsJsValue = engine.await(true) { + evaluate("""storage.getValue("jsJs")""") + } + assertNull(jsJsValue) + + // 2. set from native and remove from native + jsStorage.setValue("nativeNative", "value2") + assertEquals("value2", jsStorage.getValue("nativeNative")) + jsStorage.removeValue("nativeNative") + assertNull(jsStorage.getValue("nativeNative")) + val nativeNativeValue = engine.await { + evaluate("""storage.getValue("nativeNative")""") + } + assertNull(nativeNativeValue) + + // 3. set from js and remove from native + engine.sync { + evaluate("""storage.setValue("jsNative", "value3")""") + } + assertEquals("value3", jsStorage.getValue("jsNative")) + jsStorage.removeValue("jsNative") + assertNull(jsStorage.getValue("jsNative")) + val jsNativeValue = engine.await(true) { + evaluate("""storage.getValue("jsNative")""") + } + assertNull(jsNativeValue) + + // 4. set from native and remove from js + jsStorage.setValue("nativeJs", "value4") + assertEquals("value4", jsStorage.getValue("nativeJs")) + engine.sync { + evaluate("""storage.removeValue("nativeJs")""") + } + assertNull(jsStorage.getValue("nativeJs")) + val nativeJsValue = engine.await(true) { + evaluate("""storage.getValue("nativeJs")""") + } + assertNull(nativeJsValue) + } +} \ No newline at end of file diff --git a/analytics-kotlin-live/src/main/java/com/segment/analytics/liveplugins/kotlin/JSStorage.kt b/analytics-kotlin-live/src/main/java/com/segment/analytics/liveplugins/kotlin/JSStorage.kt new file mode 100644 index 0000000..783ea21 --- /dev/null +++ b/analytics-kotlin-live/src/main/java/com/segment/analytics/liveplugins/kotlin/JSStorage.kt @@ -0,0 +1,119 @@ +package com.segment.analytics.liveplugins.kotlin + +import android.content.SharedPreferences +import androidx.core.content.edit +import com.segment.analytics.kotlin.core.utilities.getBoolean +import com.segment.analytics.kotlin.core.utilities.getDouble +import com.segment.analytics.kotlin.core.utilities.getInt +import com.segment.analytics.kotlin.core.utilities.getLong +import com.segment.analytics.kotlin.core.utilities.getString +import com.segment.analytics.substrata.kotlin.JSArray +import com.segment.analytics.substrata.kotlin.JSObject +import com.segment.analytics.substrata.kotlin.JSScope +import com.segment.analytics.substrata.kotlin.JsonElementConverter +import kotlinx.serialization.encodeToString +import kotlinx.serialization.json.Json +import kotlinx.serialization.json.JsonElement +import kotlinx.serialization.json.JsonObject +import kotlinx.serialization.json.buildJsonObject +import kotlinx.serialization.json.put + +class JSStorage { + + internal var sharedPreferences: SharedPreferences? = null + + private var engine: JSScope? = null + + // JSEngine requires an empty constructor to be able to export this class + constructor() {} + + constructor(sharedPreferences: SharedPreferences, engine: JSScope) { + this.sharedPreferences = sharedPreferences + this.engine = engine + } + + fun setValue(key: String, value: Boolean) { + save(key, value, TYPE_BOOLEAN) + } + + fun setValue(key: String, value: Double) { + save(key, value, TYPE_DOUBLE) + } + + fun setValue(key: String, value: Int) { + save(key, value, TYPE_INT) + } + + fun setValue(key: String, value: String) { + save(key, value, TYPE_STRING) + } + + fun setValue(key: String, value: Long) { + save(key, value, TYPE_LONG) + } + + fun setValue(key: String, value: JSObject) { + save( + key, + JsonElementConverter.read(value), + TYPE_OBJECT + ) + } + + fun setValue(key: String, value: JSArray) { + save( + key, + JsonElementConverter.read(value), + TYPE_ARRAY + ) + } + + fun getValue(key: String): Any? { + return this.sharedPreferences?.getString(key, null)?.let { + Json.decodeFromString(it).unwrap() + } + } + + fun removeValue(key: String) { + this.sharedPreferences?.edit(commit = true) { remove(key) } + } + + private inline fun save(key: String, value: T, type: String) { + val jsonObject = buildJsonObject { + put(PROP_TYPE, type) + put(PROP_VALUE, Json.encodeToString(value)) + } + + this.sharedPreferences?.edit(commit = true) { putString(key, Json.encodeToString(jsonObject)) } + } + + private fun JsonObject.unwrap(): Any? { + return when(this.getString(PROP_TYPE)) { + TYPE_BOOLEAN -> this.getBoolean(PROP_VALUE) + TYPE_INT -> this.getInt(PROP_VALUE) + TYPE_DOUBLE -> this.getDouble(PROP_VALUE) + TYPE_STRING -> this.getString(PROP_VALUE)?.let { Json.decodeFromString(it) } + TYPE_LONG -> this.getLong(PROP_VALUE)?.toDouble() + else -> { + this.getString(PROP_VALUE)?.let { + val json = Json.decodeFromString(it) + engine?.await(true) { + JsonElementConverter.write(json, context) + } + } + } + } + } + + companion object { + const val PROP_TYPE = "type" + const val PROP_VALUE = "value" + const val TYPE_BOOLEAN = "boolean" + const val TYPE_INT = "int" + const val TYPE_DOUBLE = "double" + const val TYPE_STRING = "string" + const val TYPE_LONG = "long" + const val TYPE_OBJECT = "object" + const val TYPE_ARRAY = "array" + } +} \ No newline at end of file diff --git a/analytics-kotlin-live/src/main/java/com/segment/analytics/liveplugins/kotlin/LivePlugins.kt b/analytics-kotlin-live/src/main/java/com/segment/analytics/liveplugins/kotlin/LivePlugins.kt index 4157bd9..d1e8588 100644 --- a/analytics-kotlin-live/src/main/java/com/segment/analytics/liveplugins/kotlin/LivePlugins.kt +++ b/analytics-kotlin-live/src/main/java/com/segment/analytics/liveplugins/kotlin/LivePlugins.kt @@ -123,6 +123,8 @@ class LivePlugins( private fun configureEngine() = engine.sync { val jsAnalytics = JSAnalytics(analytics, engine) export(jsAnalytics, "Analytics","analytics") + val jsStorage = JSStorage(sharedPreferences, engine) + export(jsStorage, "Storage", "storage") evaluate(EmbeddedJS.ENUM_SETUP_SCRIPT) evaluate(EmbeddedJS.LIVE_PLUGINS_BASE_SETUP_SCRIPT)