From bd8a6a0b23188555b7a7149551d286ef23820227 Mon Sep 17 00:00:00 2001 From: Hur Ali Date: Wed, 25 Feb 2026 13:48:40 +0500 Subject: [PATCH 01/23] chore: scaffold android --- packages/brownie/.gitignore | 1 + packages/brownie/android/build.gradle | 67 +++++++++++++++++++ .../android/src/main/AndroidManifest.xml | 2 + .../com/callstack/brownie/BrownieModule.kt | 10 +++ .../com/callstack/brownie/BrowniePackage.kt | 21 ++++++ packages/brownie/package.json | 6 ++ packages/brownie/react-native.config.js | 7 -- 7 files changed, 107 insertions(+), 7 deletions(-) create mode 100644 packages/brownie/android/build.gradle create mode 100644 packages/brownie/android/src/main/AndroidManifest.xml create mode 100644 packages/brownie/android/src/main/java/com/callstack/brownie/BrownieModule.kt create mode 100644 packages/brownie/android/src/main/java/com/callstack/brownie/BrowniePackage.kt delete mode 100644 packages/brownie/react-native.config.js diff --git a/packages/brownie/.gitignore b/packages/brownie/.gitignore index ed24048c..10f0045b 100644 --- a/packages/brownie/.gitignore +++ b/packages/brownie/.gitignore @@ -1,2 +1,3 @@ # Generated store types (created by brownie codegen) ios/Generated/ +android/build/ diff --git a/packages/brownie/android/build.gradle b/packages/brownie/android/build.gradle new file mode 100644 index 00000000..79d4d2c8 --- /dev/null +++ b/packages/brownie/android/build.gradle @@ -0,0 +1,67 @@ +buildscript { + ext.brownie = [ + kotlinVersion: "2.0.21", + minSdkVersion: 24, + compileSdkVersion: 36, + targetSdkVersion: 36 + ] + + ext.getExtOrDefault = { prop -> + if (rootProject.ext.has(prop)) { + return rootProject.ext.get(prop) + } + + return brownie[prop] + } + + repositories { + google() + mavenCentral() + } + + dependencies { + classpath "com.android.tools.build:gradle:8.7.2" + // noinspection DifferentKotlinGradleVersion + classpath "org.jetbrains.kotlin:kotlin-gradle-plugin:${getExtOrDefault('kotlinVersion')}" + } +} + + +apply plugin: "com.android.library" +apply plugin: "kotlin-android" + +apply plugin: "com.facebook.react" + +android { + namespace "com.callstack.brownie" + + compileSdkVersion getExtOrDefault("compileSdkVersion") + + defaultConfig { + minSdkVersion getExtOrDefault("minSdkVersion") + targetSdkVersion getExtOrDefault("targetSdkVersion") + } + + buildFeatures { + buildConfig true + } + + buildTypes { + release { + minifyEnabled false + } + } + + lint { + disable "GradleCompatible" + } + + compileOptions { + sourceCompatibility JavaVersion.VERSION_1_8 + targetCompatibility JavaVersion.VERSION_1_8 + } +} + +dependencies { + implementation "com.facebook.react:react-android" +} diff --git a/packages/brownie/android/src/main/AndroidManifest.xml b/packages/brownie/android/src/main/AndroidManifest.xml new file mode 100644 index 00000000..2d331664 --- /dev/null +++ b/packages/brownie/android/src/main/AndroidManifest.xml @@ -0,0 +1,2 @@ + diff --git a/packages/brownie/android/src/main/java/com/callstack/brownie/BrownieModule.kt b/packages/brownie/android/src/main/java/com/callstack/brownie/BrownieModule.kt new file mode 100644 index 00000000..c9207f99 --- /dev/null +++ b/packages/brownie/android/src/main/java/com/callstack/brownie/BrownieModule.kt @@ -0,0 +1,10 @@ +package com.callstack.brownie + +import com.facebook.react.bridge.ReactApplicationContext + +class BrownieModule(reactContext: ReactApplicationContext) : + NativeBrownieModuleSpec(reactContext) { + override fun getName(): String { + return "Brownie" + } +} diff --git a/packages/brownie/android/src/main/java/com/callstack/brownie/BrowniePackage.kt b/packages/brownie/android/src/main/java/com/callstack/brownie/BrowniePackage.kt new file mode 100644 index 00000000..dffc2573 --- /dev/null +++ b/packages/brownie/android/src/main/java/com/callstack/brownie/BrowniePackage.kt @@ -0,0 +1,21 @@ +package com.callstack.brownie + +import android.view.View +import com.facebook.react.ReactPackage +import com.facebook.react.bridge.NativeModule +import com.facebook.react.bridge.ReactApplicationContext +import com.facebook.react.uimanager.ReactShadowNode +import com.facebook.react.uimanager.ViewManager +import java.util.Collections + +class BrowniePackage : ReactPackage { + override fun createViewManagers(reactContext: ReactApplicationContext): MutableList>> { + return Collections.emptyList() + } + + override fun createNativeModules(reactContext: ReactApplicationContext): MutableList { + val modules = ArrayList() + modules.add(BrownieModule(reactContext)) + return modules + } +} diff --git a/packages/brownie/package.json b/packages/brownie/package.json index 26e1f997..4ab31220 100644 --- a/packages/brownie/package.json +++ b/packages/brownie/package.json @@ -50,10 +50,16 @@ "files": [ "src", "lib", + "android", "ios", "cpp", "*.podspec", "!ios/build", + "!android/build", + "!android/gradle", + "!android/gradlew", + "!android/gradlew.bat", + "!android/local.properties", "!**/__tests__", "!**/__fixtures__", "!**/__mocks__", diff --git a/packages/brownie/react-native.config.js b/packages/brownie/react-native.config.js deleted file mode 100644 index 22b2e13d..00000000 --- a/packages/brownie/react-native.config.js +++ /dev/null @@ -1,7 +0,0 @@ -module.exports = { - dependency: { - platforms: { - android: null, - }, - }, -}; From a25543a37cfc3fa5fa79d66a746ed63887baf8a2 Mon Sep 17 00:00:00 2001 From: Hur Ali Date: Wed, 25 Feb 2026 14:30:47 +0500 Subject: [PATCH 02/23] feat: add cpp bindings --- packages/brownie/android/build.gradle | 16 +- .../android/src/main/cpp/CMakeLists.txt | 43 ++++ .../src/main/cpp/JNIBrownieStoreBridge.cpp | 226 ++++++++++++++++++ .../com/callstack/brownie/BrownieModule.kt | 51 ++++ .../com/callstack/brownie/BrownieStore.kt | 163 +++++++++++++ .../callstack/brownie/BrownieStoreBridge.kt | 85 +++++++ 6 files changed, 583 insertions(+), 1 deletion(-) create mode 100644 packages/brownie/android/src/main/cpp/CMakeLists.txt create mode 100644 packages/brownie/android/src/main/cpp/JNIBrownieStoreBridge.cpp create mode 100644 packages/brownie/android/src/main/java/com/callstack/brownie/BrownieStore.kt create mode 100644 packages/brownie/android/src/main/java/com/callstack/brownie/BrownieStoreBridge.kt diff --git a/packages/brownie/android/build.gradle b/packages/brownie/android/build.gradle index 79d4d2c8..162484f5 100644 --- a/packages/brownie/android/build.gradle +++ b/packages/brownie/android/build.gradle @@ -3,7 +3,8 @@ buildscript { kotlinVersion: "2.0.21", minSdkVersion: 24, compileSdkVersion: 36, - targetSdkVersion: 36 + targetSdkVersion: 36, + cmakeVersion: "3.22.1" ] ext.getExtOrDefault = { prop -> @@ -40,6 +41,12 @@ android { defaultConfig { minSdkVersion getExtOrDefault("minSdkVersion") targetSdkVersion getExtOrDefault("targetSdkVersion") + + externalNativeBuild { + cmake { + cppFlags "-std=c++20 -fexceptions -frtti" + } + } } buildFeatures { @@ -60,6 +67,13 @@ android { sourceCompatibility JavaVersion.VERSION_1_8 targetCompatibility JavaVersion.VERSION_1_8 } + + externalNativeBuild { + cmake { + path "src/main/cpp/CMakeLists.txt" + version getExtOrDefault("cmakeVersion") + } + } } dependencies { diff --git a/packages/brownie/android/src/main/cpp/CMakeLists.txt b/packages/brownie/android/src/main/cpp/CMakeLists.txt new file mode 100644 index 00000000..df43446a --- /dev/null +++ b/packages/brownie/android/src/main/cpp/CMakeLists.txt @@ -0,0 +1,43 @@ +cmake_minimum_required(VERSION 3.13) + +project(brownie) + +set(CMAKE_CXX_STANDARD 20) +set(CMAKE_CXX_STANDARD_REQUIRED ON) + +set(PACKAGE_ROOT "${CMAKE_CURRENT_SOURCE_DIR}/../../..") + +add_library( + brownie + SHARED + JNIBrownieStoreBridge.cpp + "${PACKAGE_ROOT}/cpp/BrownieHostObject.cpp" + "${PACKAGE_ROOT}/cpp/BrownieInstaller.cpp" + "${PACKAGE_ROOT}/cpp/BrownieStore.cpp" + "${PACKAGE_ROOT}/cpp/BrownieStoreManager.cpp" +) + +target_include_directories( + brownie + PRIVATE + "${PACKAGE_ROOT}/cpp" +) + +find_library(log-lib log) +find_package(fbjni REQUIRED CONFIG) +find_package(ReactAndroid QUIET CONFIG) + +target_link_libraries( + brownie + PRIVATE + ${log-lib} + fbjni::fbjni +) + +if(TARGET ReactAndroid::folly_runtime) + target_link_libraries(brownie PRIVATE ReactAndroid::folly_runtime) +endif() + +if(TARGET ReactAndroid::jsi) + target_link_libraries(brownie PRIVATE ReactAndroid::jsi) +endif() diff --git a/packages/brownie/android/src/main/cpp/JNIBrownieStoreBridge.cpp b/packages/brownie/android/src/main/cpp/JNIBrownieStoreBridge.cpp new file mode 100644 index 00000000..ad0edf07 --- /dev/null +++ b/packages/brownie/android/src/main/cpp/JNIBrownieStoreBridge.cpp @@ -0,0 +1,226 @@ +#include +#include +#include +#include +#include +#include +#include +#include "BrownieInstaller.h" +#include "BrownieStore.h" +#include "BrownieStoreManager.h" + +namespace { + +constexpr auto kBridgeClassName = "com/callstack/brownie/BrownieStoreBridge"; +constexpr auto kOnStoreDidChangeMethod = "onStoreDidChange"; +constexpr auto kOnStoreDidChangeSignature = "(Ljava/lang/String;)V"; + +JavaVM *g_vm = nullptr; +jclass g_bridgeClass = nullptr; +jmethodID g_onStoreDidChangeMethod = nullptr; +std::once_flag g_initMethodOnce; + +std::string fromJString(JNIEnv *env, jstring value) { + if (value == nullptr) { + return ""; + } + + const char *chars = env->GetStringUTFChars(value, nullptr); + if (chars == nullptr) { + return ""; + } + + std::string result(chars); + env->ReleaseStringUTFChars(value, chars); + return result; +} + +jstring toJString(JNIEnv *env, const std::string &value) { + return env->NewStringUTF(value.c_str()); +} + +bool initOnStoreDidChangeMethod(JNIEnv *env) { + bool success = true; + std::call_once(g_initMethodOnce, [env, &success]() { + auto localBridgeClass = env->FindClass(kBridgeClassName); + if (localBridgeClass == nullptr) { + success = false; + return; + } + + g_bridgeClass = reinterpret_cast(env->NewGlobalRef(localBridgeClass)); + env->DeleteLocalRef(localBridgeClass); + if (g_bridgeClass == nullptr) { + success = false; + return; + } + + g_onStoreDidChangeMethod = env->GetStaticMethodID( + g_bridgeClass, kOnStoreDidChangeMethod, kOnStoreDidChangeSignature); + if (g_onStoreDidChangeMethod == nullptr) { + success = false; + } + }); + + return success && g_bridgeClass != nullptr && g_onStoreDidChangeMethod != nullptr; +} + +void emitStoreDidChange(const std::string &storeKey) { + if (g_vm == nullptr) { + return; + } + + JNIEnv *env = nullptr; + bool didAttachCurrentThread = false; + + if (g_vm->GetEnv(reinterpret_cast(&env), JNI_VERSION_1_6) != JNI_OK) { + if (g_vm->AttachCurrentThread(&env, nullptr) != JNI_OK) { + return; + } + didAttachCurrentThread = true; + } + + if (!initOnStoreDidChangeMethod(env)) { + if (didAttachCurrentThread) { + g_vm->DetachCurrentThread(); + } + return; + } + + auto jStoreKey = toJString(env, storeKey); + env->CallStaticVoidMethod(g_bridgeClass, g_onStoreDidChangeMethod, jStoreKey); + env->DeleteLocalRef(jStoreKey); + + if (didAttachCurrentThread) { + g_vm->DetachCurrentThread(); + } +} + +std::shared_ptr getStoreOrNull(const std::string &storeKey) { + return brownie::BrownieStoreManager::shared().getStore(storeKey); +} + +} // namespace + +extern "C" JNIEXPORT jint JNICALL JNI_OnLoad(JavaVM *vm, void *) { + g_vm = vm; + return JNI_VERSION_1_6; +} + +extern "C" JNIEXPORT void JNICALL +Java_com_callstack_brownie_BrownieStoreBridge_nativeInstallJSIBindings(JNIEnv *, + jclass, + jlong runtimePointer) { + auto *runtime = reinterpret_cast(runtimePointer); + if (runtime == nullptr) { + return; + } + + brownie::BrownieInstaller::install(*runtime); +} + +extern "C" JNIEXPORT void JNICALL +Java_com_callstack_brownie_BrownieStoreBridge_nativeRegisterStore(JNIEnv *env, + jclass, + jstring storeKey) { + auto store = std::make_shared(); + auto key = fromJString(env, storeKey); + store->setChangeCallback([key]() { emitStoreDidChange(key); }); + brownie::BrownieStoreManager::shared().registerStore(key, store); +} + +extern "C" JNIEXPORT void JNICALL +Java_com_callstack_brownie_BrownieStoreBridge_nativeRemoveStore(JNIEnv *env, + jclass, + jstring storeKey) { + brownie::BrownieStoreManager::shared().removeStore(fromJString(env, storeKey)); +} + +extern "C" JNIEXPORT void JNICALL +Java_com_callstack_brownie_BrownieStoreBridge_nativeSetValue(JNIEnv *env, + jclass, + jstring valueJson, + jstring propKey, + jstring storeKey) { + auto store = getStoreOrNull(fromJString(env, storeKey)); + if (!store) { + return; + } + + auto key = fromJString(env, propKey); + auto json = fromJString(env, valueJson); + try { + store->set(key, folly::parseJson(json)); + } catch (const std::exception &) { + // Keep native bridge resilient to malformed payloads from Kotlin callers. + } +} + +extern "C" JNIEXPORT jstring JNICALL +Java_com_callstack_brownie_BrownieStoreBridge_nativeGetValue(JNIEnv *env, + jclass, + jstring propKey, + jstring storeKey) { + auto store = getStoreOrNull(fromJString(env, storeKey)); + if (!store) { + return nullptr; + } + + auto key = fromJString(env, propKey); + auto value = store->get(key); + + try { + return toJString(env, folly::toJson(value)); + } catch (const std::exception &) { + return nullptr; + } +} + +extern "C" JNIEXPORT jstring JNICALL +Java_com_callstack_brownie_BrownieStoreBridge_nativeGetSnapshot(JNIEnv *env, + jclass, + jstring storeKey) { + auto store = getStoreOrNull(fromJString(env, storeKey)); + if (!store) { + return nullptr; + } + + try { + return toJString(env, folly::toJson(store->getSnapshot())); + } catch (const std::exception &) { + return nullptr; + } +} + +extern "C" JNIEXPORT void JNICALL +Java_com_callstack_brownie_BrownieStoreBridge_nativeSetState(JNIEnv *env, + jclass, + jstring stateJson, + jstring storeKey) { + auto store = getStoreOrNull(fromJString(env, storeKey)); + if (!store) { + return; + } + + auto json = fromJString(env, stateJson); + try { + store->setState(folly::parseJson(json)); + } catch (const std::exception &) { + // Keep native bridge resilient to malformed payloads from Kotlin callers. + } +} + +extern "C" JNIEXPORT void JNICALL JNI_OnUnload(JavaVM *, void *) { + if (g_vm == nullptr || g_bridgeClass == nullptr) { + return; + } + + JNIEnv *env = nullptr; + if (g_vm->GetEnv(reinterpret_cast(&env), JNI_VERSION_1_6) != JNI_OK) { + return; + } + + env->DeleteGlobalRef(g_bridgeClass); + g_bridgeClass = nullptr; + g_onStoreDidChangeMethod = nullptr; +} diff --git a/packages/brownie/android/src/main/java/com/callstack/brownie/BrownieModule.kt b/packages/brownie/android/src/main/java/com/callstack/brownie/BrownieModule.kt index c9207f99..f6da15b4 100644 --- a/packages/brownie/android/src/main/java/com/callstack/brownie/BrownieModule.kt +++ b/packages/brownie/android/src/main/java/com/callstack/brownie/BrownieModule.kt @@ -1,9 +1,60 @@ package com.callstack.brownie +import android.os.Handler +import android.os.Looper +import com.facebook.react.bridge.Arguments import com.facebook.react.bridge.ReactApplicationContext +import java.util.concurrent.atomic.AtomicBoolean class BrownieModule(reactContext: ReactApplicationContext) : NativeBrownieModuleSpec(reactContext) { + private val mainHandler = Handler(Looper.getMainLooper()) + private val didInstallJSI = AtomicBoolean(false) + + private val storeDidChangeListener: (String) -> Unit = { storeKey -> + val eventPayload = + Arguments.createMap().apply { + putString("storeKey", storeKey) + putString("key", "storeKey") + putString("value", storeKey) + } + + if (Looper.myLooper() == Looper.getMainLooper()) { + emitNativeStoreDidChange(eventPayload) + } else { + mainHandler.post { emitNativeStoreDidChange(eventPayload) } + } + } + + init { + BrownieStoreBridge.setStoreDidChangeListener(storeDidChangeListener) + } + + override fun initialize() { + super.initialize() + installJSIBindingsIfNeeded() + } + + override fun invalidate() { + BrownieStoreBridge.setStoreDidChangeListener(null) + StoreManager.shared.clear() + super.invalidate() + } + + private fun installJSIBindingsIfNeeded() { + if (didInstallJSI.get()) { + return + } + + val runtimePointer = reactApplicationContext.javaScriptContextHolder?.get() ?: 0L + if (runtimePointer == 0L) { + return + } + + BrownieStoreBridge.installJSIBindings(runtimePointer) + didInstallJSI.set(true) + } + override fun getName(): String { return "Brownie" } diff --git a/packages/brownie/android/src/main/java/com/callstack/brownie/BrownieStore.kt b/packages/brownie/android/src/main/java/com/callstack/brownie/BrownieStore.kt new file mode 100644 index 00000000..83f50e5a --- /dev/null +++ b/packages/brownie/android/src/main/java/com/callstack/brownie/BrownieStore.kt @@ -0,0 +1,163 @@ +package com.callstack.brownie + +import java.util.concurrent.CopyOnWriteArraySet +import java.util.concurrent.atomic.AtomicBoolean +import java.util.concurrent.locks.ReentrantLock +import kotlin.concurrent.withLock + +interface BrownieStoreSerializer { + fun encode(state: State): String + + fun decode(snapshotJson: String): State +} + +interface BrownieStoreDefinition { + val storeName: String + val serializer: BrownieStoreSerializer +} + +fun BrownieStoreDefinition.register(initialState: State): Store { + return Store(initialState, storeName, serializer) +} + +class StoreManager private constructor() { + companion object { + val shared: StoreManager = StoreManager() + + fun get(key: String): Store? = shared.store(key) + } + + private val lock = ReentrantLock() + private val stores: MutableMap = mutableMapOf() + + fun register(store: Store, key: String) { + lock.withLock { + stores[key] = store + } + } + + @Suppress("UNCHECKED_CAST") + fun store(key: String): Store? { + lock.withLock { + return stores[key] as? Store + } + } + + fun removeStore(key: String) { + val store = lock.withLock { + stores.remove(key) as? Store<*> + } + + store?.dispose() + BrownieStoreBridge.removeStore(key) + } + + fun snapshot(key: String): String? = BrownieStoreBridge.getSnapshot(key) + + fun setValue(key: String, property: String, valueJson: String) { + BrownieStoreBridge.setValue(valueJson, property, key) + } + + fun clear() { + val keys = lock.withLock { + stores.keys.toList() + } + + keys.forEach { key -> + removeStore(key) + } + } +} + +class Store( + initialState: State, + private val storeKey: String, + private val serializer: BrownieStoreSerializer, +) : AutoCloseable { + private val stateLock = ReentrantLock() + private val listeners = CopyOnWriteArraySet<(State) -> Unit>() + private val disposed = AtomicBoolean(false) + + @Volatile + private var _state: State = initialState + + val state: State + get() = stateLock.withLock { _state } + + private val bridgeListenerId: String + + init { + BrownieStoreBridge.registerStore(storeKey) + pushStateToCxx() + + bridgeListenerId = + BrownieStoreBridge.addStoreDidChangeListener { updatedStoreKey -> + if (updatedStoreKey == storeKey) { + rebuildState() + } + } + + StoreManager.shared.register(this, storeKey) + } + + fun set(updater: (State) -> State) { + val newState = + stateLock.withLock { + _state = updater(_state) + _state + } + + pushStateToCxx(newState) + notifyListeners(newState) + } + + fun set(value: State) { + set { value } + } + + fun setValue(property: String, valueJson: String) { + BrownieStoreBridge.setValue(valueJson, property, storeKey) + } + + fun getValue(property: String): String? = BrownieStoreBridge.getValue(property, storeKey) + + fun subscribe(onChange: (State) -> Unit): () -> Unit { + listeners.add(onChange) + onChange(state) + return { listeners.remove(onChange) } + } + + internal fun dispose() { + if (!disposed.compareAndSet(false, true)) { + return + } + + BrownieStoreBridge.removeStoreDidChangeListener(bridgeListenerId) + listeners.clear() + } + + override fun close() { + StoreManager.shared.removeStore(storeKey) + } + + private fun pushStateToCxx(state: State = this.state) { + BrownieStoreBridge.setState(serializer.encode(state), storeKey) + } + + private fun rebuildState() { + val snapshot = BrownieStoreBridge.getSnapshot(storeKey) ?: return + val rebuiltState = runCatching { serializer.decode(snapshot) }.getOrNull() ?: return + + stateLock.withLock { + _state = rebuiltState + } + + notifyListeners(rebuiltState) + } + + private fun notifyListeners(state: State) { + listeners.forEach { listener -> + listener(state) + } + } +} diff --git a/packages/brownie/android/src/main/java/com/callstack/brownie/BrownieStoreBridge.kt b/packages/brownie/android/src/main/java/com/callstack/brownie/BrownieStoreBridge.kt new file mode 100644 index 00000000..e7d1b554 --- /dev/null +++ b/packages/brownie/android/src/main/java/com/callstack/brownie/BrownieStoreBridge.kt @@ -0,0 +1,85 @@ +package com.callstack.brownie + +import java.util.UUID +import java.util.concurrent.ConcurrentHashMap + +object BrownieStoreBridge { + private var legacyStoreDidChangeListener: ((String) -> Unit)? = null + private val storeDidChangeListeners = ConcurrentHashMap Unit>() + + init { + System.loadLibrary("brownie") + } + + @Synchronized + fun setStoreDidChangeListener(listener: ((String) -> Unit)?) { + legacyStoreDidChangeListener = listener + } + + fun addStoreDidChangeListener(listener: (String) -> Unit): String { + val listenerId = UUID.randomUUID().toString() + storeDidChangeListeners[listenerId] = listener + return listenerId + } + + fun removeStoreDidChangeListener(listenerId: String) { + storeDidChangeListeners.remove(listenerId) + } + + fun registerStore(storeKey: String) { + nativeRegisterStore(storeKey) + } + + fun removeStore(storeKey: String) { + nativeRemoveStore(storeKey) + } + + fun setValue(valueJson: String, propKey: String, storeKey: String) { + nativeSetValue(valueJson, propKey, storeKey) + } + + fun getValue(propKey: String, storeKey: String): String? { + return nativeGetValue(propKey, storeKey) + } + + fun getSnapshot(storeKey: String): String? { + return nativeGetSnapshot(storeKey) + } + + fun setState(stateJson: String, storeKey: String) { + nativeSetState(stateJson, storeKey) + } + + fun installJSIBindings(runtimePointer: Long) { + nativeInstallJSIBindings(runtimePointer) + } + + @JvmStatic + private fun onStoreDidChange(storeKey: String) { + legacyStoreDidChangeListener?.invoke(storeKey) + storeDidChangeListeners.values.forEach { listener -> + listener.invoke(storeKey) + } + } + + @JvmStatic + private external fun nativeInstallJSIBindings(runtimePointer: Long) + + @JvmStatic + private external fun nativeRegisterStore(storeKey: String) + + @JvmStatic + private external fun nativeRemoveStore(storeKey: String) + + @JvmStatic + private external fun nativeSetValue(valueJson: String, propKey: String, storeKey: String) + + @JvmStatic + private external fun nativeGetValue(propKey: String, storeKey: String): String? + + @JvmStatic + private external fun nativeGetSnapshot(storeKey: String): String? + + @JvmStatic + private external fun nativeSetState(stateJson: String, storeKey: String) +} From b8460553e31ba84b4d7d40a53788ab3d53a598f6 Mon Sep 17 00:00:00 2001 From: Hur Ali Date: Wed, 25 Feb 2026 18:02:35 +0500 Subject: [PATCH 03/23] feat: add compiler flags and target linkages --- packages/brownie/android/build.gradle | 2 ++ .../android/src/main/cpp/CMakeLists.txt | 35 ++++++++++++++++--- 2 files changed, 32 insertions(+), 5 deletions(-) diff --git a/packages/brownie/android/build.gradle b/packages/brownie/android/build.gradle index 162484f5..b15df4d5 100644 --- a/packages/brownie/android/build.gradle +++ b/packages/brownie/android/build.gradle @@ -44,6 +44,7 @@ android { externalNativeBuild { cmake { + arguments "-DANDROID_STL=c++_shared" cppFlags "-std=c++20 -fexceptions -frtti" } } @@ -51,6 +52,7 @@ android { buildFeatures { buildConfig true + prefab true } buildTypes { diff --git a/packages/brownie/android/src/main/cpp/CMakeLists.txt b/packages/brownie/android/src/main/cpp/CMakeLists.txt index df43446a..9772ae22 100644 --- a/packages/brownie/android/src/main/cpp/CMakeLists.txt +++ b/packages/brownie/android/src/main/cpp/CMakeLists.txt @@ -5,7 +5,22 @@ project(brownie) set(CMAKE_CXX_STANDARD 20) set(CMAKE_CXX_STANDARD_REQUIRED ON) -set(PACKAGE_ROOT "${CMAKE_CURRENT_SOURCE_DIR}/../../..") +set(PACKAGE_ROOT "${CMAKE_CURRENT_SOURCE_DIR}/../../../..") + +SET(folly_FLAGS + -DFOLLY_NO_CONFIG=1 + -DFOLLY_HAVE_CLOCK_GETTIME=1 + -DFOLLY_USE_LIBCPP=1 + -DFOLLY_CFG_NO_COROUTINES=1 + -DFOLLY_MOBILE=1 + -DFOLLY_HAVE_RECVMMSG=1 + -DFOLLY_HAVE_PTHREAD=1 + # Once we target android-23 above, we can comment + # the following line. NDK uses GNU style stderror_r() after API 23. + -DFOLLY_HAVE_XSI_STRERROR_R=1 + ) + +add_compile_options(${folly_FLAGS}) add_library( brownie @@ -25,7 +40,7 @@ target_include_directories( find_library(log-lib log) find_package(fbjni REQUIRED CONFIG) -find_package(ReactAndroid QUIET CONFIG) +find_package(ReactAndroid REQUIRED CONFIG) target_link_libraries( brownie @@ -34,10 +49,20 @@ target_link_libraries( fbjni::fbjni ) -if(TARGET ReactAndroid::folly_runtime) - target_link_libraries(brownie PRIVATE ReactAndroid::folly_runtime) -endif() if(TARGET ReactAndroid::jsi) target_link_libraries(brownie PRIVATE ReactAndroid::jsi) +elseif(TARGET jsi) + target_link_libraries(brownie PRIVATE jsi) +endif() + +if(TARGET ReactAndroid::reactnative) + target_link_libraries(brownie PRIVATE ReactAndroid::reactnative) +endif() + +if(TARGET ReactAndroid::folly_runtime) + target_link_libraries(brownie PRIVATE ReactAndroid::folly_runtime) +elseif(TARGET folly_runtime) + target_link_libraries(brownie PRIVATE folly_runtime) endif() + From 64480674da669b5e07b4ebc214d3572925b54102 Mon Sep 17 00:00:00 2001 From: Hur Ali Date: Wed, 25 Feb 2026 18:03:36 +0500 Subject: [PATCH 04/23] feat: use brownie on android --- apps/AndroidApp/app/build.gradle.kts | 8 +++ .../android/example/MainActivity.kt | 21 +++++-- .../android/example/BrownieStoreSetup.kt | 61 +++++++++++++++++++ apps/RNApp/package.json | 2 +- apps/RNApp/src/components/counter/index.tsx | 13 +++- 5 files changed, 98 insertions(+), 7 deletions(-) create mode 100644 apps/AndroidApp/app/src/vanilla/java/com/callstack/brownfield/android/example/BrownieStoreSetup.kt diff --git a/apps/AndroidApp/app/build.gradle.kts b/apps/AndroidApp/app/build.gradle.kts index 2b6cd3a6..15e6779f 100644 --- a/apps/AndroidApp/app/build.gradle.kts +++ b/apps/AndroidApp/app/build.gradle.kts @@ -58,6 +58,14 @@ android { buildFeatures { compose = true } + + // TODO: investigate why BGP does not handle it + packagingOptions { + pickFirst("lib/arm64-v8a/libc++_shared.so") + pickFirst("lib/armeabi-v7a/libc++_shared.so") + pickFirst("lib/x86/libc++_shared.so") + pickFirst("lib/x86_64/libc++_shared.so") + } } dependencies { diff --git a/apps/AndroidApp/app/src/main/java/com/callstack/brownfield/android/example/MainActivity.kt b/apps/AndroidApp/app/src/main/java/com/callstack/brownfield/android/example/MainActivity.kt index 4143acf2..a3fe9b8f 100644 --- a/apps/AndroidApp/app/src/main/java/com/callstack/brownfield/android/example/MainActivity.kt +++ b/apps/AndroidApp/app/src/main/java/com/callstack/brownfield/android/example/MainActivity.kt @@ -20,9 +20,10 @@ import androidx.compose.material3.MaterialTheme import androidx.compose.material3.Scaffold import androidx.compose.material3.Text import androidx.compose.runtime.Composable +import androidx.compose.runtime.DisposableEffect import androidx.compose.runtime.getValue -import androidx.compose.runtime.mutableStateOf -import androidx.compose.runtime.saveable.rememberSaveable +import androidx.compose.runtime.mutableIntStateOf +import androidx.compose.runtime.remember import androidx.compose.runtime.setValue import androidx.compose.ui.Alignment import androidx.compose.ui.Modifier @@ -54,6 +55,8 @@ class MainActivity : AppCompatActivity() { Toast.LENGTH_LONG ).show() } + + registerBrownieStoreIfNeeded() } setContent { @@ -99,7 +102,17 @@ fun GreetingCard( name: String, modifier: Modifier = Modifier ) { - var counter by rememberSaveable { mutableStateOf(0) } + var counter by remember { mutableIntStateOf(0) } + + DisposableEffect(Unit) { + val unsubscribe = subscribeToBrownieCounter { updatedCounter -> + counter = updatedCounter + } + + onDispose { + unsubscribe() + } + } Card( modifier = modifier, @@ -125,7 +138,7 @@ fun GreetingCard( style = MaterialTheme.typography.bodyMedium ) - Button(onClick = { counter++ }) { + Button(onClick = { incrementBrownieCounter() }) { Text("Increment counter") } } diff --git a/apps/AndroidApp/app/src/vanilla/java/com/callstack/brownfield/android/example/BrownieStoreSetup.kt b/apps/AndroidApp/app/src/vanilla/java/com/callstack/brownfield/android/example/BrownieStoreSetup.kt new file mode 100644 index 00000000..40327f78 --- /dev/null +++ b/apps/AndroidApp/app/src/vanilla/java/com/callstack/brownfield/android/example/BrownieStoreSetup.kt @@ -0,0 +1,61 @@ +package com.callstack.brownfield.android.example + +import com.callstack.brownie.BrownieStoreDefinition +import com.callstack.brownie.BrownieStoreSerializer +import com.callstack.brownie.Store +import com.callstack.brownie.register +import com.rnapp.brownfieldlib.BrownfieldStore +import com.rnapp.brownfieldlib.User +import java.util.concurrent.atomic.AtomicBoolean +import org.json.JSONObject + +private val didRegisterBrownieStore = AtomicBoolean(false) +private var brownieStore: Store? = null + +private object BrownfieldStoreDefinitionAndroid : BrownieStoreDefinition { + override val storeName: String = BrownfieldStore.STORE_NAME + override val serializer: BrownieStoreSerializer = object : BrownieStoreSerializer { + override fun encode(state: BrownfieldStore): String { + return JSONObject() + .put("counter", state.counter) + .put("user", JSONObject().put("name", state.user.name)) + .toString() + } + + override fun decode(snapshotJson: String): BrownfieldStore { + val stateJson = JSONObject(snapshotJson) + val userJson = stateJson.optJSONObject("user") + + return BrownfieldStore( + counter = stateJson.optDouble("counter", 0.0), + user = User(name = userJson?.optString("name").orEmpty()) + ) + } + } +} + +fun registerBrownieStoreIfNeeded() { + if (!didRegisterBrownieStore.compareAndSet(false, true)) { + return + } + + brownieStore = BrownfieldStoreDefinitionAndroid.register( + initialState = BrownfieldStore( + counter = 0.0, + user = User(name = "Username") + ) + ) +} + +fun subscribeToBrownieCounter(onCounterChanged: (Int) -> Unit): () -> Unit { + val store = brownieStore ?: return {} + return store.subscribe { state -> + onCounterChanged(state.counter.toInt()) + } +} + +fun incrementBrownieCounter() { + brownieStore?.set { state -> + state.copy(counter = state.counter + 1) + } +} diff --git a/apps/RNApp/package.json b/apps/RNApp/package.json index 07ea82c4..91b177ec 100644 --- a/apps/RNApp/package.json +++ b/apps/RNApp/package.json @@ -7,7 +7,7 @@ "ios": "react-native run-ios", "build:example:android-rn": "react-native build-android", "build:example:ios-rn": "react-native build-ios", - "brownfield:package:android": "brownfield package:android --module-name :BrownfieldLib --variant release", + "brownfield:package:android": "brownfield package:android --module-name :BrownfieldLib --variant debug", "brownfield:publish:android": "brownfield publish:android --module-name :BrownfieldLib", "brownfield:package:ios": "brownfield package:ios --scheme BrownfieldLib --configuration Release", "lint": "eslint .", diff --git a/apps/RNApp/src/components/counter/index.tsx b/apps/RNApp/src/components/counter/index.tsx index fa9f280c..20ca26e4 100644 --- a/apps/RNApp/src/components/counter/index.tsx +++ b/apps/RNApp/src/components/counter/index.tsx @@ -1,15 +1,24 @@ -import { StyleSheet, Text } from 'react-native'; +import { Button, StyleSheet, Text } from 'react-native'; +import { useStore } from '@callstack/brownie'; type CounterProps = { colors: { primary: string; secondary: string }; }; const Counter = ({ colors }: CounterProps) => { + const [counter, setState] = useStore('BrownfieldStore', (s) => s.counter); + return ( <> - Brownie: To be implemented + Count: {counter} + +