Skip to content

Commit 4eb776e

Browse files
authored
Add storage to js engine (#26)
* implement JSStorage * add unit test * add unit test for other types * fix string double quote issue * fix long conversion issue * fix jsObject conversion issue * fix jsArray conversion issue * add removeValue * add unit test for remove value
1 parent dbf61df commit 4eb776e

File tree

3 files changed

+383
-0
lines changed

3 files changed

+383
-0
lines changed
Lines changed: 262 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,262 @@
1+
package com.segment.analytics.liveplugins.kotlin
2+
3+
import androidx.test.ext.junit.runners.AndroidJUnit4
4+
import com.segment.analytics.kotlin.core.utilities.getInt
5+
import com.segment.analytics.kotlin.core.utilities.getString
6+
import com.segment.analytics.liveplugins.kotlin.utils.MemorySharedPreferences
7+
import com.segment.analytics.substrata.kotlin.JSArray
8+
import com.segment.analytics.substrata.kotlin.JSObject
9+
import com.segment.analytics.substrata.kotlin.JSScope
10+
import com.segment.analytics.substrata.kotlin.JsonElementConverter
11+
import kotlinx.coroutines.ExperimentalCoroutinesApi
12+
import kotlinx.serialization.json.jsonArray
13+
import kotlinx.serialization.json.jsonObject
14+
import kotlinx.serialization.json.jsonPrimitive
15+
import org.junit.Assert.assertEquals
16+
import org.junit.Assert.assertNull
17+
import org.junit.Before
18+
import org.junit.Test
19+
import org.junit.runner.RunWith
20+
21+
22+
@OptIn(ExperimentalCoroutinesApi::class)
23+
@RunWith(AndroidJUnit4::class)
24+
class JSStorageTest {
25+
26+
private lateinit var engine: JSScope
27+
private lateinit var jsStorage: JSStorage
28+
private var exceptionThrown: Throwable? = null
29+
30+
@Before
31+
fun setUp() {
32+
exceptionThrown = null
33+
34+
engine = JSScope{ exception ->
35+
exceptionThrown = exception
36+
}
37+
jsStorage = JSStorage(MemorySharedPreferences(), engine)
38+
// Setup the engine similar to LivePlugins.configureEngine
39+
engine.sync {
40+
export(jsStorage, "Storage", "storage")
41+
}
42+
}
43+
44+
@Test
45+
fun testJSStorageWithInt() {
46+
// set from js
47+
var value = engine.await {
48+
evaluate("""storage.setValue("int", 1)""")
49+
evaluate("""storage.getValue("int")""")
50+
}
51+
assertNull(exceptionThrown)
52+
assertEquals(1, value)
53+
assertEquals(1, jsStorage.getValue("int"))
54+
55+
// set from native
56+
jsStorage.setValue("int", 2)
57+
value = engine.await {
58+
evaluate("""storage.getValue("int")""")
59+
}
60+
assertEquals(2, value)
61+
assertEquals(2, jsStorage.getValue("int"))
62+
}
63+
64+
@Test
65+
fun testJSStorageWithBoolean() {
66+
// set from js
67+
var value = engine.await {
68+
evaluate("""storage.setValue("boolean", true)""")
69+
evaluate("""storage.getValue("boolean")""")
70+
}
71+
assertNull(exceptionThrown)
72+
assertEquals(true, value)
73+
assertEquals(true, jsStorage.getValue("boolean"))
74+
75+
// set from native
76+
jsStorage.setValue("boolean", false)
77+
value = engine.await {
78+
evaluate("""storage.getValue("boolean")""")
79+
}
80+
assertEquals(false, value)
81+
assertEquals(false, jsStorage.getValue("boolean"))
82+
}
83+
84+
@Test
85+
fun testJSStorageWithDouble() {
86+
// set from js
87+
var value = engine.await {
88+
evaluate("""storage.setValue("double", 3.14)""")
89+
evaluate("""storage.getValue("double")""")
90+
}
91+
assertNull(exceptionThrown)
92+
assertEquals(3.14, value)
93+
assertEquals(3.14, jsStorage.getValue("double"))
94+
95+
// set from native
96+
jsStorage.setValue("double", 2.71)
97+
value = engine.await {
98+
evaluate("""storage.getValue("double")""")
99+
}
100+
assertEquals(2.71, value)
101+
assertEquals(2.71, jsStorage.getValue("double"))
102+
}
103+
104+
@Test
105+
fun testJSStorageWithString() {
106+
// set from js
107+
var value = engine.await {
108+
evaluate("""storage.setValue("string", "hello")""")
109+
evaluate("""storage.getValue("string")""")
110+
}
111+
assertNull(exceptionThrown)
112+
assertEquals("hello", value)
113+
assertEquals("hello", jsStorage.getValue("string"))
114+
115+
// set from native
116+
jsStorage.setValue("string", "world")
117+
value = engine.await {
118+
evaluate("""storage.getValue("string")""")
119+
}
120+
assertEquals("world", value)
121+
assertEquals("world", jsStorage.getValue("string"))
122+
}
123+
124+
@Test
125+
fun testJSStorageWithLong() {
126+
// set from js
127+
var value = engine.await {
128+
evaluate("""storage.setValue("long", 1234567890123)""")
129+
evaluate("""storage.getValue("long")""")
130+
}
131+
assertNull(exceptionThrown)
132+
assertEquals(1234567890123L.toDouble(), value)
133+
assertEquals(1234567890123L.toDouble(), jsStorage.getValue("long"))
134+
135+
// set from native
136+
jsStorage.setValue("long", 9876543210987L)
137+
value = engine.await {
138+
evaluate("""storage.getValue("long")""")
139+
}
140+
assertEquals(9876543210987L.toDouble(), value)
141+
assertEquals(9876543210987L.toDouble(), jsStorage.getValue("long"))
142+
}
143+
144+
@Test
145+
fun testJSStorageWithJSObject() {
146+
// set from js
147+
var value = engine.await(true) {
148+
evaluate("""storage.setValue("object", {name: "test", value: 42})""")
149+
evaluate("""storage.getValue("object")""")
150+
}
151+
assertNull(exceptionThrown)
152+
val jsonObject = JsonElementConverter.read(value).jsonObject
153+
assertEquals("test", jsonObject.getString("name"))
154+
assertEquals(42, jsonObject.getInt("value"))
155+
156+
// set from native
157+
val nativeObject = engine.await(true) {
158+
evaluate("""
159+
let obj = {name: "native", value: 100}
160+
obj
161+
""".trimIndent())
162+
}
163+
jsStorage.setValue("object", nativeObject as JSObject)
164+
value = engine.await {
165+
evaluate("""
166+
let obj2 = storage.getValue("object")
167+
obj.name == obj2.name && obj.value == obj2.value
168+
""".trimIndent())
169+
}
170+
assertEquals(true, value)
171+
val jsValue = jsStorage.getValue("object")
172+
val retrievedObject = JsonElementConverter.read(jsValue).jsonObject
173+
assertEquals("native", retrievedObject.getString("name"))
174+
assertEquals(100, retrievedObject.getInt("value"))
175+
}
176+
177+
@Test
178+
fun testJSStorageWithJSArray() {
179+
// set from js
180+
var value = engine.await(true) {
181+
evaluate("""storage.setValue("array", [1, "test", true])""")
182+
evaluate("""storage.getValue("array")""")
183+
}
184+
assertNull(exceptionThrown)
185+
val jsonArray = JsonElementConverter.read(value).jsonArray
186+
assertEquals(3, jsonArray.size)
187+
assertEquals(1, jsonArray[0].jsonPrimitive.content.toInt())
188+
assertEquals("test", jsonArray[1].jsonPrimitive.content)
189+
assertEquals(true, jsonArray[2].jsonPrimitive.content.toBoolean())
190+
191+
// set from native
192+
val nativeArray = engine.await(true) {
193+
evaluate("""
194+
let arr = [42, "native", false]
195+
arr
196+
""".trimIndent())
197+
}
198+
jsStorage.setValue("array", nativeArray as JSArray)
199+
value = engine.await {
200+
evaluate("""
201+
let arr2 = storage.getValue("array")
202+
arr.length == arr2.length && arr[0] == arr2[0] && arr[1] == arr2[1] && arr[2] == arr2[2]
203+
""".trimIndent())
204+
}
205+
assertEquals(true, value)
206+
val jsValue = jsStorage.getValue("array")
207+
val retrievedArray = JsonElementConverter.read(jsValue).jsonArray
208+
assertEquals(3, retrievedArray.size)
209+
assertEquals(42, retrievedArray[0].jsonPrimitive.content.toInt())
210+
assertEquals("native", retrievedArray[1].jsonPrimitive.content)
211+
assertEquals(false, retrievedArray[2].jsonPrimitive.content.toBoolean())
212+
}
213+
214+
@Test
215+
fun testJSStorageRemoveValue() {
216+
// 1. set from js and remove from js
217+
engine.sync {
218+
evaluate("""storage.setValue("jsJs", "value1")""")
219+
evaluate("""storage.removeValue("jsJs")""")
220+
}
221+
assertNull(exceptionThrown)
222+
assertNull(jsStorage.getValue("jsJs"))
223+
val jsJsValue = engine.await(true) {
224+
evaluate("""storage.getValue("jsJs")""")
225+
}
226+
assertNull(jsJsValue)
227+
228+
// 2. set from native and remove from native
229+
jsStorage.setValue("nativeNative", "value2")
230+
assertEquals("value2", jsStorage.getValue("nativeNative"))
231+
jsStorage.removeValue("nativeNative")
232+
assertNull(jsStorage.getValue("nativeNative"))
233+
val nativeNativeValue = engine.await {
234+
evaluate("""storage.getValue("nativeNative")""")
235+
}
236+
assertNull(nativeNativeValue)
237+
238+
// 3. set from js and remove from native
239+
engine.sync {
240+
evaluate("""storage.setValue("jsNative", "value3")""")
241+
}
242+
assertEquals("value3", jsStorage.getValue("jsNative"))
243+
jsStorage.removeValue("jsNative")
244+
assertNull(jsStorage.getValue("jsNative"))
245+
val jsNativeValue = engine.await(true) {
246+
evaluate("""storage.getValue("jsNative")""")
247+
}
248+
assertNull(jsNativeValue)
249+
250+
// 4. set from native and remove from js
251+
jsStorage.setValue("nativeJs", "value4")
252+
assertEquals("value4", jsStorage.getValue("nativeJs"))
253+
engine.sync {
254+
evaluate("""storage.removeValue("nativeJs")""")
255+
}
256+
assertNull(jsStorage.getValue("nativeJs"))
257+
val nativeJsValue = engine.await(true) {
258+
evaluate("""storage.getValue("nativeJs")""")
259+
}
260+
assertNull(nativeJsValue)
261+
}
262+
}
Lines changed: 119 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,119 @@
1+
package com.segment.analytics.liveplugins.kotlin
2+
3+
import android.content.SharedPreferences
4+
import androidx.core.content.edit
5+
import com.segment.analytics.kotlin.core.utilities.getBoolean
6+
import com.segment.analytics.kotlin.core.utilities.getDouble
7+
import com.segment.analytics.kotlin.core.utilities.getInt
8+
import com.segment.analytics.kotlin.core.utilities.getLong
9+
import com.segment.analytics.kotlin.core.utilities.getString
10+
import com.segment.analytics.substrata.kotlin.JSArray
11+
import com.segment.analytics.substrata.kotlin.JSObject
12+
import com.segment.analytics.substrata.kotlin.JSScope
13+
import com.segment.analytics.substrata.kotlin.JsonElementConverter
14+
import kotlinx.serialization.encodeToString
15+
import kotlinx.serialization.json.Json
16+
import kotlinx.serialization.json.JsonElement
17+
import kotlinx.serialization.json.JsonObject
18+
import kotlinx.serialization.json.buildJsonObject
19+
import kotlinx.serialization.json.put
20+
21+
class JSStorage {
22+
23+
internal var sharedPreferences: SharedPreferences? = null
24+
25+
private var engine: JSScope? = null
26+
27+
// JSEngine requires an empty constructor to be able to export this class
28+
constructor() {}
29+
30+
constructor(sharedPreferences: SharedPreferences, engine: JSScope) {
31+
this.sharedPreferences = sharedPreferences
32+
this.engine = engine
33+
}
34+
35+
fun setValue(key: String, value: Boolean) {
36+
save(key, value, TYPE_BOOLEAN)
37+
}
38+
39+
fun setValue(key: String, value: Double) {
40+
save(key, value, TYPE_DOUBLE)
41+
}
42+
43+
fun setValue(key: String, value: Int) {
44+
save(key, value, TYPE_INT)
45+
}
46+
47+
fun setValue(key: String, value: String) {
48+
save(key, value, TYPE_STRING)
49+
}
50+
51+
fun setValue(key: String, value: Long) {
52+
save(key, value, TYPE_LONG)
53+
}
54+
55+
fun setValue(key: String, value: JSObject) {
56+
save(
57+
key,
58+
JsonElementConverter.read(value),
59+
TYPE_OBJECT
60+
)
61+
}
62+
63+
fun setValue(key: String, value: JSArray) {
64+
save(
65+
key,
66+
JsonElementConverter.read(value),
67+
TYPE_ARRAY
68+
)
69+
}
70+
71+
fun getValue(key: String): Any? {
72+
return this.sharedPreferences?.getString(key, null)?.let {
73+
Json.decodeFromString<JsonObject>(it).unwrap()
74+
}
75+
}
76+
77+
fun removeValue(key: String) {
78+
this.sharedPreferences?.edit(commit = true) { remove(key) }
79+
}
80+
81+
private inline fun <reified T> save(key: String, value: T, type: String) {
82+
val jsonObject = buildJsonObject {
83+
put(PROP_TYPE, type)
84+
put(PROP_VALUE, Json.encodeToString(value))
85+
}
86+
87+
this.sharedPreferences?.edit(commit = true) { putString(key, Json.encodeToString(jsonObject)) }
88+
}
89+
90+
private fun JsonObject.unwrap(): Any? {
91+
return when(this.getString(PROP_TYPE)) {
92+
TYPE_BOOLEAN -> this.getBoolean(PROP_VALUE)
93+
TYPE_INT -> this.getInt(PROP_VALUE)
94+
TYPE_DOUBLE -> this.getDouble(PROP_VALUE)
95+
TYPE_STRING -> this.getString(PROP_VALUE)?.let { Json.decodeFromString<String>(it) }
96+
TYPE_LONG -> this.getLong(PROP_VALUE)?.toDouble()
97+
else -> {
98+
this.getString(PROP_VALUE)?.let {
99+
val json = Json.decodeFromString<JsonElement>(it)
100+
engine?.await(true) {
101+
JsonElementConverter.write(json, context)
102+
}
103+
}
104+
}
105+
}
106+
}
107+
108+
companion object {
109+
const val PROP_TYPE = "type"
110+
const val PROP_VALUE = "value"
111+
const val TYPE_BOOLEAN = "boolean"
112+
const val TYPE_INT = "int"
113+
const val TYPE_DOUBLE = "double"
114+
const val TYPE_STRING = "string"
115+
const val TYPE_LONG = "long"
116+
const val TYPE_OBJECT = "object"
117+
const val TYPE_ARRAY = "array"
118+
}
119+
}

analytics-kotlin-live/src/main/java/com/segment/analytics/liveplugins/kotlin/LivePlugins.kt

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -123,6 +123,8 @@ class LivePlugins(
123123
private fun configureEngine() = engine.sync {
124124
val jsAnalytics = JSAnalytics(analytics, engine)
125125
export(jsAnalytics, "Analytics","analytics")
126+
val jsStorage = JSStorage(sharedPreferences, engine)
127+
export(jsStorage, "Storage", "storage")
126128

127129
evaluate(EmbeddedJS.ENUM_SETUP_SCRIPT)
128130
evaluate(EmbeddedJS.LIVE_PLUGINS_BASE_SETUP_SCRIPT)

0 commit comments

Comments
 (0)