From f315d3c599f369f3cb24a2ba9cca4504492dcbc8 Mon Sep 17 00:00:00 2001 From: Wolfgang Karl Date: Wed, 24 May 2023 11:00:36 +0200 Subject: [PATCH 1/2] Add mandatory namespace constructor argument to separate local storage when using multiple JsBridge instances --- README.md | 7 ++- .../oasisjsbridge/JsBridgeJavaTest.java | 2 +- .../oasisjsbridge/JsBridgeTest.kt | 44 ++++++++++++++----- .../oasisjsbridge/ReadmeTest.kt | 2 +- .../oasisjsbridge/JsBridge.kt | 8 +++- .../oasisjsbridge/extensions/LocalStorage.kt | 4 +- .../extensions/LocalStorageExtension.kt | 10 ++--- 7 files changed, 54 insertions(+), 23 deletions(-) diff --git a/README.md b/README.md index c69f0ff..73716c0 100644 --- a/README.md +++ b/README.md @@ -7,7 +7,7 @@ Evaluate JavaScript code and map values, objects and functions between Kotlin/Java and JavaScript on Android. ```kotlin -val jsBridge = JsBridge(JsBridgeConfig.bareConfig()) +val jsBridge = JsBridge(JsBridgeConfig.bareConfig(), context, "namespace") val msg: String = jsBridge.evaluate("'Hello world!'.toUpperCase()") println(msg) // HELLO WORLD! ``` @@ -304,6 +304,9 @@ Other network clients are not tested but should work as well (polyfill for Support for ES6 promises (Duktape: via polyfill, QuickJS: built-in). Pending jobs are triggered after each evaluation. +- **LocalStorage:**
+Support for browser-like local storage. + - **JS Debugger:**
JS debugger support (Duktape only via Visual Studio Code plugin) @@ -401,7 +404,7 @@ val nativeApi = object: NativeApi { Bridging JavaScript and Kotlin: ```kotlin -val jsBridge = JsBridge(JsBridgeConfig.standardConfig()) +val jsBridge = JsBridge(JsBridgeConfig.standardConfig(), context, "namespace") jsBridge.evaluateLocalFileUnsync(context, "js/api.js") // JS "proxy" to native API diff --git a/jsbridge/src/androidTest/kotlin/de/prosiebensat1digital/oasisjsbridge/JsBridgeJavaTest.java b/jsbridge/src/androidTest/kotlin/de/prosiebensat1digital/oasisjsbridge/JsBridgeJavaTest.java index f5c345d..9bc2c88 100644 --- a/jsbridge/src/androidTest/kotlin/de/prosiebensat1digital/oasisjsbridge/JsBridgeJavaTest.java +++ b/jsbridge/src/androidTest/kotlin/de/prosiebensat1digital/oasisjsbridge/JsBridgeJavaTest.java @@ -153,7 +153,7 @@ public void triggerCallback(TestAidlCallback cb) throws RemoteException { // --- private JsBridge createAndSetUpJsBridge() { - JsBridge jsBridge = new JsBridge(JsBridgeConfig.standardConfig(), context); + JsBridge jsBridge = new JsBridge(JsBridgeConfig.standardConfig(), context, "test_namespace"); this.jsBridge = jsBridge; return jsBridge; } diff --git a/jsbridge/src/androidTest/kotlin/de/prosiebensat1digital/oasisjsbridge/JsBridgeTest.kt b/jsbridge/src/androidTest/kotlin/de/prosiebensat1digital/oasisjsbridge/JsBridgeTest.kt index 57c1e3d..ffae2b1 100644 --- a/jsbridge/src/androidTest/kotlin/de/prosiebensat1digital/oasisjsbridge/JsBridgeTest.kt +++ b/jsbridge/src/androidTest/kotlin/de/prosiebensat1digital/oasisjsbridge/JsBridgeTest.kt @@ -76,6 +76,8 @@ interface JsExpectationsNativeApi : JsToNativeInterface { fun addExpectation(name: String, value: JsValue) } +private const val NAMESPACE = "test_namespace" + class JsBridgeTest { private var jsBridge: JsBridge? = null private val context: Context = InstrumentationRegistry.getInstrumentation().context @@ -1180,7 +1182,7 @@ class JsBridgeTest { val config = JsBridgeConfig.standardConfig().apply { xhrConfig.okHttpClient = okHttpClient } - val subject = JsBridge(config, context) + val subject = JsBridge(config, context, NAMESPACE) val jsExpectations = JsExpectations() val jsExpectationsJsValue = JsValue.fromNativeObject(subject, jsExpectations) @@ -1551,7 +1553,7 @@ class JsBridgeTest { val config = JsBridgeConfig.standardConfig().apply { xhrConfig.okHttpClient = okHttpClient } - val subject = JsBridge(config, context) + val subject = JsBridge(config, context, NAMESPACE) val jsExpectations = JsExpectations() val jsExpectationsJsValue = JsValue.fromNativeObject(subject, jsExpectations) @@ -2591,7 +2593,7 @@ class JsBridgeTest { messages.add(priority to message) } } - val subject = JsBridge(config, context) + val subject = JsBridge(config, context, NAMESPACE) jsBridge = subject // WHEN @@ -2629,7 +2631,7 @@ class JsBridgeTest { messages.add(priority to message) } } - val subject = JsBridge(config, context) + val subject = JsBridge(config, context, NAMESPACE) jsBridge = subject // WHEN @@ -2667,7 +2669,7 @@ class JsBridgeTest { hasMessage = true } } - val subject = JsBridge(config, context) + val subject = JsBridge(config, context, NAMESPACE) jsBridge = subject // WHEN @@ -2883,11 +2885,11 @@ class JsBridgeTest { val subject = createAndSetUpJsBridge() // WHEN - subject.evaluateBlocking("""localStorage.setItem("foo", "bar");""") - val result = subject.evaluateBlocking("""localStorage.getItem("foo");""") + subject.evaluateBlocking("""localStorage.setItem("key", "value");""") + val result = subject.evaluateBlocking("""localStorage.getItem("key");""") // THEN - assertEquals(result, "bar") + assertEquals("value", result) assertTrue(errors.isEmpty()) // GIVEN @@ -2903,6 +2905,27 @@ class JsBridgeTest { assertTrue(errors.isEmpty()) } + @Test + fun testLocalStorageNamespaces() { + // GIVEN + val subject1 = createAndSetUpJsBridge() + val subject2 = createAndSetUpJsBridge(namespace = "other_namespace") + + // WHEN + subject1.evaluateBlocking("""localStorage.setItem("key", "value");""") + subject1.evaluateBlocking("""localStorage.setItem("key2", "value");""") + val result1 = subject1.evaluateBlocking("""localStorage.getItem("key");""") + subject2.evaluateBlocking("""localStorage.setItem("key", "TEST");""") + val result2 = subject2.evaluateBlocking("""localStorage.getItem("key");""") + val result3 = subject2.evaluateBlocking("""localStorage.getItem("key2");""") + + // THEN + assertEquals("value", result1) + assertEquals("TEST", result2) + assertNull(result3) + assertTrue(errors.isEmpty()) + } + // JsExpectations // --- @@ -2947,10 +2970,11 @@ class JsBridgeTest { private fun createAndSetUpJsBridge( config: JsBridgeConfig = JsBridgeConfig.standardConfig().apply { xhrConfig.okHttpClient = okHttpClient - } + }, + namespace: String = NAMESPACE, ): JsBridge { - return JsBridge(config, context).also { jsBridge -> + return JsBridge(config, context, namespace).also { jsBridge -> this@JsBridgeTest.jsBridge = jsBridge jsBridge.registerErrorListener(createErrorListener()) diff --git a/jsbridge/src/androidTest/kotlin/de/prosiebensat1digital/oasisjsbridge/ReadmeTest.kt b/jsbridge/src/androidTest/kotlin/de/prosiebensat1digital/oasisjsbridge/ReadmeTest.kt index 6aa6ae8..8b0f68e 100644 --- a/jsbridge/src/androidTest/kotlin/de/prosiebensat1digital/oasisjsbridge/ReadmeTest.kt +++ b/jsbridge/src/androidTest/kotlin/de/prosiebensat1digital/oasisjsbridge/ReadmeTest.kt @@ -39,7 +39,7 @@ class ReadmeTest { @Before fun setUp() { - jsBridge = JsBridge(JsBridgeConfig.standardConfig(), context) + jsBridge = JsBridge(JsBridgeConfig.standardConfig(), context,"test_namespace") } @After diff --git a/jsbridge/src/main/kotlin/de/prosiebensat1digital/oasisjsbridge/JsBridge.kt b/jsbridge/src/main/kotlin/de/prosiebensat1digital/oasisjsbridge/JsBridge.kt index 4093c2e..f743841 100644 --- a/jsbridge/src/main/kotlin/de/prosiebensat1digital/oasisjsbridge/JsBridge.kt +++ b/jsbridge/src/main/kotlin/de/prosiebensat1digital/oasisjsbridge/JsBridge.kt @@ -57,9 +57,13 @@ import java.lang.reflect.Proxy * Note: all the public methods are asynchronous and will not block the caller threads. They * can be safely called in a "synchronous" way. though, because their executions are guaranteed * to be performed sequentially (via an internal queue). + * + * @param config JsBridge configuration + * @param context Context needed for built in implementation of local storage + * @param namespace arbitrary string for separation of local storage between multiple JsBridge instances */ class JsBridge - constructor(config: JsBridgeConfig, context: Context) : CoroutineScope { + constructor(config: JsBridgeConfig, context: Context, namespace: String) : CoroutineScope { companion object { private var isLibraryLoaded = false @@ -158,7 +162,7 @@ class JsBridge if (config.xhrConfig.enabled) xhrExtension = XMLHttpRequestExtension(this@JsBridge, config.xhrConfig) if (config.localStorageConfig.enabled) - localStorageExtension = LocalStorageExtension(this@JsBridge, config.localStorageConfig, context.applicationContext) + localStorageExtension = LocalStorageExtension(this@JsBridge, config.localStorageConfig, context.applicationContext, namespace) config.jvmConfig.customClassLoader?.let { customClassLoader = it } } } diff --git a/jsbridge/src/main/kotlin/de/prosiebensat1digital/oasisjsbridge/extensions/LocalStorage.kt b/jsbridge/src/main/kotlin/de/prosiebensat1digital/oasisjsbridge/extensions/LocalStorage.kt index 6fb186d..8f2a192 100644 --- a/jsbridge/src/main/kotlin/de/prosiebensat1digital/oasisjsbridge/extensions/LocalStorage.kt +++ b/jsbridge/src/main/kotlin/de/prosiebensat1digital/oasisjsbridge/extensions/LocalStorage.kt @@ -13,10 +13,10 @@ interface LocalStorageInteface : JsToNativeInterface { fun clear() } -class LocalStorage(context: Context) : LocalStorageInteface { +class LocalStorage(context: Context, namespace: String) : LocalStorageInteface { private val localStoragePreferences = context.getSharedPreferences( - context.applicationInfo.packageName + ".LOCAL_STORAGE_PREFERENCE_FILE_KEY", + "${namespace.takeIf { it.isNotEmpty() } ?: "default"}.LOCAL_STORAGE_PREFERENCES", Context.MODE_PRIVATE ) diff --git a/jsbridge/src/main/kotlin/de/prosiebensat1digital/oasisjsbridge/extensions/LocalStorageExtension.kt b/jsbridge/src/main/kotlin/de/prosiebensat1digital/oasisjsbridge/extensions/LocalStorageExtension.kt index 5c979ec..2591494 100644 --- a/jsbridge/src/main/kotlin/de/prosiebensat1digital/oasisjsbridge/extensions/LocalStorageExtension.kt +++ b/jsbridge/src/main/kotlin/de/prosiebensat1digital/oasisjsbridge/extensions/LocalStorageExtension.kt @@ -21,16 +21,16 @@ import de.prosiebensat1digital.oasisjsbridge.JsBridgeConfig import de.prosiebensat1digital.oasisjsbridge.JsValue internal class LocalStorageExtension( - private val jsBridge: JsBridge, - val config: JsBridgeConfig.LocalStorageConfig, + jsBridge: JsBridge, + config: JsBridgeConfig.LocalStorageConfig, context: Context, + namespace: String, ) { init { if (config.useDefaultLocalStorage) { - val localStorageJsValue: JsValue - val localStorage: LocalStorageInteface = LocalStorage(context) - localStorageJsValue = JsValue.fromNativeObject(jsBridge, localStorage) + val localStorage: LocalStorageInteface = LocalStorage(context, namespace) + val localStorageJsValue = JsValue.fromNativeObject(jsBridge, localStorage) localStorageJsValue.assignToGlobal("localStorage") } } From 47b902b70fb2daa9a7ab181c041bbede3c6bde78 Mon Sep 17 00:00:00 2001 From: Wolfgang Karl Date: Wed, 14 Jun 2023 11:34:25 +0200 Subject: [PATCH 2/2] Add config option to use old file name for local storage preferences instead of namespaces for backward compatibility in existing apps --- .../oasisjsbridge/JsBridgeConfig.kt | 10 ++++++++++ .../oasisjsbridge/extensions/LocalStorage.kt | 5 +++-- .../oasisjsbridge/extensions/LocalStorageExtension.kt | 2 +- 3 files changed, 14 insertions(+), 3 deletions(-) diff --git a/jsbridge/src/main/kotlin/de/prosiebensat1digital/oasisjsbridge/JsBridgeConfig.kt b/jsbridge/src/main/kotlin/de/prosiebensat1digital/oasisjsbridge/JsBridgeConfig.kt index 80f80d0..ffaf564 100644 --- a/jsbridge/src/main/kotlin/de/prosiebensat1digital/oasisjsbridge/JsBridgeConfig.kt +++ b/jsbridge/src/main/kotlin/de/prosiebensat1digital/oasisjsbridge/JsBridgeConfig.kt @@ -62,6 +62,16 @@ private constructor() { class LocalStorageConfig { var enabled: Boolean = false var useDefaultLocalStorage: Boolean = true + + /** + * Only disable namespaces if a particular instance of JsBridge requires access to local + * storage key/value pairs that were saved with a previous version of the library. + * + * You should try to avoid using multiple unrelated instances of JsBridge without namespaces + * or with an identical namespace. An exception would be if you want to explicitly share data + * between instances and the possibility of key name collisions is not an issue. + */ + var useNamespaces: Boolean = true } class JvmConfig { diff --git a/jsbridge/src/main/kotlin/de/prosiebensat1digital/oasisjsbridge/extensions/LocalStorage.kt b/jsbridge/src/main/kotlin/de/prosiebensat1digital/oasisjsbridge/extensions/LocalStorage.kt index 8f2a192..13d7b5c 100644 --- a/jsbridge/src/main/kotlin/de/prosiebensat1digital/oasisjsbridge/extensions/LocalStorage.kt +++ b/jsbridge/src/main/kotlin/de/prosiebensat1digital/oasisjsbridge/extensions/LocalStorage.kt @@ -13,10 +13,11 @@ interface LocalStorageInteface : JsToNativeInterface { fun clear() } -class LocalStorage(context: Context, namespace: String) : LocalStorageInteface { +class LocalStorage(context: Context, namespace: String?) : LocalStorageInteface { private val localStoragePreferences = context.getSharedPreferences( - "${namespace.takeIf { it.isNotEmpty() } ?: "default"}.LOCAL_STORAGE_PREFERENCES", + namespace?.let { "${it.takeIf { it.isNotEmpty() } ?: "default"}.LOCAL_STORAGE_PREFERENCES" } + ?: "${context.applicationInfo.packageName}.LOCAL_STORAGE_PREFERENCE_FILE_KEY", Context.MODE_PRIVATE ) diff --git a/jsbridge/src/main/kotlin/de/prosiebensat1digital/oasisjsbridge/extensions/LocalStorageExtension.kt b/jsbridge/src/main/kotlin/de/prosiebensat1digital/oasisjsbridge/extensions/LocalStorageExtension.kt index 2591494..fbd29da 100644 --- a/jsbridge/src/main/kotlin/de/prosiebensat1digital/oasisjsbridge/extensions/LocalStorageExtension.kt +++ b/jsbridge/src/main/kotlin/de/prosiebensat1digital/oasisjsbridge/extensions/LocalStorageExtension.kt @@ -29,7 +29,7 @@ internal class LocalStorageExtension( init { if (config.useDefaultLocalStorage) { - val localStorage: LocalStorageInteface = LocalStorage(context, namespace) + val localStorage: LocalStorageInteface = LocalStorage(context, namespace.takeIf { config.useNamespaces }) val localStorageJsValue = JsValue.fromNativeObject(jsBridge, localStorage) localStorageJsValue.assignToGlobal("localStorage") }