diff --git a/android/build.gradle b/android/build.gradle index 6930a00..25d6e32 100644 --- a/android/build.gradle +++ b/android/build.gradle @@ -33,6 +33,7 @@ android { targetSdkVersion getExtOrIntegerDefault('targetSdkVersion') versionCode 1 versionName "1.0" + consumerProguardFiles 'consumer-rules.pro' } buildTypes { diff --git a/android/consumer-rules.pro b/android/consumer-rules.pro new file mode 100644 index 0000000..70d3596 --- /dev/null +++ b/android/consumer-rules.pro @@ -0,0 +1,5 @@ +-keep class com.facebook.react.ReactHost { *; } +-keep interface com.facebook.react.ReactInstanceEventListener { *; } +-keep interface com.facebook.react.ReactApplication { + public com.facebook.react.ReactHost getReactHost(); +} diff --git a/android/src/main/AndroidManifest.xml b/android/src/main/AndroidManifest.xml index dc01239..9c8a289 100644 --- a/android/src/main/AndroidManifest.xml +++ b/android/src/main/AndroidManifest.xml @@ -1,4 +1,9 @@ - + + + diff --git a/android/src/main/java/com/mindboxsdk/MindboxEventEmitter.kt b/android/src/main/java/com/mindboxsdk/MindboxEventEmitter.kt new file mode 100644 index 0000000..5a6de8e --- /dev/null +++ b/android/src/main/java/com/mindboxsdk/MindboxEventEmitter.kt @@ -0,0 +1,55 @@ +package com.mindboxsdk + +import android.app.Activity +import android.app.Application +import android.content.Context +import android.content.Intent +import android.util.Log +import com.facebook.react.ReactApplication +import com.facebook.react.ReactInstanceManager +import com.facebook.react.bridge.ReactContext +import com.facebook.react.ReactActivity +import cloud.mindbox.mobile_sdk.Mindbox +import cloud.mindbox.mobile_sdk.logger.Level + +internal class MindboxEventEmitter ( + private val application: Application +) : MindboxEventSubscriber { + + private var jsDelivery: MindboxJsDelivery? = null + + override fun onEvent(event: MindboxSdkLifecycleEvent) { + when (event) { + is MindboxSdkLifecycleEvent.NewIntent -> handleNewIntent(event.reactContext, event.intent) + is MindboxSdkLifecycleEvent.ActivityCreated -> handleActivityCreated(event.reactContext, event.activity) + is MindboxSdkLifecycleEvent.ActivityDestroyed -> handleActivityDestroyed() + } + } + + private fun handleNewIntent(context: ReactContext, intent: Intent) { + Mindbox.writeLog("[RN] Handle new intent in event emitter. ", Level.INFO) + Mindbox.onNewIntent(intent) + Mindbox.onPushClicked(context, intent) + jsDelivery?.sendPushClicked(intent) + } + + private fun handleActivityCreated(reactContext:ReactContext, activity: Activity) { + Mindbox.writeLog("[RN] Handle activity created", Level.INFO) + runCatching { + reactContext.let { reactContext -> + initializeAndSendIntent(reactContext, activity) + } + } + } + + private fun initializeAndSendIntent(context: ReactContext, activity: Activity) { + Mindbox.writeLog("[RN] Initialize MindboxJsDelivery", Level.INFO) + jsDelivery = MindboxJsDelivery.Shared.getInstance(context) + val currentActivity = context.currentActivity ?: activity + currentActivity.intent?.let { handleNewIntent(context, it) } + } + + private fun handleActivityDestroyed() { + jsDelivery = null + } +} diff --git a/android/src/main/java/com/mindboxsdk/MindboxEventSubscriber.kt b/android/src/main/java/com/mindboxsdk/MindboxEventSubscriber.kt new file mode 100644 index 0000000..ad7105f --- /dev/null +++ b/android/src/main/java/com/mindboxsdk/MindboxEventSubscriber.kt @@ -0,0 +1,5 @@ +package com.mindboxsdk + +internal interface MindboxEventSubscriber { + fun onEvent(event: MindboxSdkLifecycleEvent) +} diff --git a/android/src/main/java/com/mindboxsdk/MindboxSdkInitProvider.kt b/android/src/main/java/com/mindboxsdk/MindboxSdkInitProvider.kt new file mode 100644 index 0000000..23f8633 --- /dev/null +++ b/android/src/main/java/com/mindboxsdk/MindboxSdkInitProvider.kt @@ -0,0 +1,64 @@ +package com.mindboxsdk + +import android.app.Application +import android.content.ContentProvider +import android.content.ContentValues +import android.content.pm.PackageManager +import android.database.Cursor +import android.net.Uri +import android.util.Log + +internal class MindboxSdkInitProvider : ContentProvider() { + + companion object { + private const val AUTO_INIT_ENABLED_KEY = "com.mindbox.sdk.AUTO_INIT_ENABLED" + private const val TAG = "MindboxSdkInitProvider" + } + + override fun onCreate(): Boolean { + runCatching { + Log.i(TAG, "onCreate initProvider.") + (context?.applicationContext as? Application)?.let { application -> + if (isAutoInitEnabled(application)) { + Log.i(TAG, "Automatic initialization is enabled.") + MindboxSdkLifecycleListener.register(application) + } else { + Log.i(TAG, "Automatic initialization is disabled.") + } + } + }.onFailure { error -> + Log.e(TAG, "Automatic initialization failed", error) + } + return true + } + + private fun isAutoInitEnabled(application: Application): Boolean = + runCatching { + val appInfo = application.packageManager.getApplicationInfo( + application.packageName, + PackageManager.GET_META_DATA + ) + appInfo.metaData + ?.getBoolean(AUTO_INIT_ENABLED_KEY, false) + ?.also { Log.i(TAG, "Result of reading mindbox metadata is $it") } + ?: false + }.getOrElse { false } + + override fun query( + uri: Uri, + projection: Array?, + selection: String?, + selectionArgs: Array?, + sortOrder: String? + ): Cursor? = null + + override fun getType(uri: Uri): String? = null + override fun insert(uri: Uri, values: ContentValues?): Uri? = null + override fun delete(uri: Uri, selection: String?, selectionArgs: Array?): Int = 0 + override fun update( + uri: Uri, + values: ContentValues?, + selection: String?, + selectionArgs: Array? + ): Int = 0 +} diff --git a/android/src/main/java/com/mindboxsdk/MindboxSdkLifecycleEvent.kt b/android/src/main/java/com/mindboxsdk/MindboxSdkLifecycleEvent.kt new file mode 100644 index 0000000..2cce9cf --- /dev/null +++ b/android/src/main/java/com/mindboxsdk/MindboxSdkLifecycleEvent.kt @@ -0,0 +1,11 @@ +package com.mindboxsdk + +import android.app.Activity +import android.content.Intent +import com.facebook.react.bridge.ReactContext + +internal sealed class MindboxSdkLifecycleEvent { + data class NewIntent(val reactContext: ReactContext, val intent: Intent) : MindboxSdkLifecycleEvent() + data class ActivityCreated(val reactContext: ReactContext, val activity: Activity) : MindboxSdkLifecycleEvent() + data class ActivityDestroyed(val activity: Activity) : MindboxSdkLifecycleEvent() +} diff --git a/android/src/main/java/com/mindboxsdk/MindboxSdkLifecycleListener.kt b/android/src/main/java/com/mindboxsdk/MindboxSdkLifecycleListener.kt new file mode 100644 index 0000000..89f2db5 --- /dev/null +++ b/android/src/main/java/com/mindboxsdk/MindboxSdkLifecycleListener.kt @@ -0,0 +1,170 @@ +package com.mindboxsdk + +import android.app.Activity +import android.app.Application +import android.content.Intent +import android.os.Bundle +import android.util.Log +import com.facebook.react.ReactApplication +import com.facebook.react.ReactInstanceManager +import com.facebook.react.bridge.ActivityEventListener +import com.facebook.react.bridge.ReactContext +import java.util.concurrent.atomic.AtomicBoolean +import cloud.mindbox.mobile_sdk.Mindbox +import cloud.mindbox.mobile_sdk.logger.Level + + +internal class MindboxSdkLifecycleListener private constructor( + private val application: Application, + private val subscriber: MindboxEventSubscriber +) : Application.ActivityLifecycleCallbacks { + + companion object { + fun register( + application: Application, + subscriber: MindboxEventSubscriber = MindboxEventEmitter(application) + ) { + val listener = MindboxSdkLifecycleListener(application, subscriber) + application.registerActivityLifecycleCallbacks(listener) + } + } + + private val mainActivityClassName: String? by lazy { getLauncherActivityClassName() } + + private fun getLauncherActivityClassName(): String? { + val pm = application.packageManager + val intent = pm.getLaunchIntentForPackage(application.packageName) + return intent?.component?.className + } + + private fun isMainActivity(activity: Activity): Boolean { + return activity.javaClass.name == mainActivityClassName + } + + private var activityEventListener: ActivityEventListener? = null + + private fun onReactContextAvailable(reactContext: ReactContext, activity: Activity) { + Mindbox.writeLog("[RN] ReactContext ready", Level.INFO) + addActivityEventListener(reactContext) + subscriber.onEvent(MindboxSdkLifecycleEvent.ActivityCreated(reactContext, activity)) + } + + private fun registerReactContextListener( + application: Application, + onReady: (ReactContext) -> Unit + ) { + val reactApplication = application.getReactApplication() ?: return + val reactInstanceManager = getReactInstanceManager() + + val wrapperListener = object : ReactInstanceManager.ReactInstanceEventListener { + private val called = AtomicBoolean(false) + override fun onReactContextInitialized(context: ReactContext) { + if (called.compareAndSet(false, true)) { + onReady(context) + } + } + } + + reactInstanceManager?.addReactInstanceEventListener(wrapperListener) + // RN 0.78+ introduced ReactHost.addReactInstanceEventListener(...). + // Older RN versions (<= 0.74) expose only ReactInstanceManager.addReactInstanceEventListener(...). + // In New Architecture the ReactInstanceManager listener might not fire + // To support RN 0.78+ reliably while keeping backward compatibility, + // we try to register via ReactHost using reflection (no compile-time dependency). + // If ReactHost API is unavailable (older RN), this call is silently ignored and we rely on + // the ReactInstanceManager path. + addReactHostListener(application, wrapperListener) + } + + override fun onActivityCreated(activity: Activity, savedInstanceState: Bundle?) { + if (!isMainActivity(activity)) return + + getReactInstanceManager()?.currentReactContext?.let { + onReactContextAvailable(it, activity) + Mindbox.writeLog("[RN] ReactContext already available; skipping listener registration ", Level.INFO) + return + } + + registerReactContextListener(application) { reactContext -> + onReactContextAvailable(reactContext, activity) + } + } + + private fun addActivityEventListener(reactContext: ReactContext) { + activityEventListener?.let { reactContext.removeActivityEventListener(it) } + + activityEventListener = object : ActivityEventListener { + override fun onNewIntent(intent: Intent?) { + intent ?: return + reactContext.currentActivity + ?.takeIf { isMainActivity(it) } + ?.let { + subscriber.onEvent( + MindboxSdkLifecycleEvent.NewIntent( + reactContext, + intent + ) + ) + } + } + + override fun onActivityResult( + activity: Activity?, requestCode: Int, resultCode: Int, data: Intent? + ) { + } + } + reactContext.addActivityEventListener(activityEventListener) + } + + override fun onActivityDestroyed(activity: Activity) { + if (!isMainActivity(activity)) return + subscriber.onEvent(MindboxSdkLifecycleEvent.ActivityDestroyed(activity)) + getReactInstanceManager() + ?.currentReactContext + ?.removeActivityEventListener(activityEventListener) + activityEventListener = null + } + + override fun onActivityStarted(activity: Activity) {} + override fun onActivityResumed(activity: Activity) {} + override fun onActivityPaused(activity: Activity) {} + override fun onActivityStopped(activity: Activity) {} + override fun onActivitySaveInstanceState(activity: Activity, outState: Bundle) {} + + + private fun getReactInstanceManager(): ReactInstanceManager? = + application.getReactApplication()?.reactNativeHost?.reactInstanceManager + + private fun Application.getReactApplication() = this as? ReactApplication + + private fun addReactHostListener( + application: Application, + wrapperListener: ReactInstanceManager.ReactInstanceEventListener + ) { + runCatching { + val reactApplication = application as ReactApplication + + val hostClass = Class.forName("com.facebook.react.ReactHost") + val listenerClass = Class.forName("com.facebook.react.ReactInstanceEventListener") + + val addMethod = hostClass.getMethod("addReactInstanceEventListener", listenerClass) + val getHostMethod = reactApplication.javaClass.getMethod("getReactHost") + val reactHost = getHostMethod.invoke(reactApplication) + + val proxy = java.lang.reflect.Proxy.newProxyInstance( + listenerClass.classLoader, + arrayOf(listenerClass) + ) { _, method, args -> + if (method.name == "onReactContextInitialized" && args?.size == 1 && args[0] is ReactContext) { + wrapperListener.onReactContextInitialized(args[0] as ReactContext) + } + null + } + + addMethod.invoke(reactHost, proxy) + Mindbox.writeLog("[RN] success added react context listener for reactHost", Level.INFO) + }.onFailure { + Mindbox.writeLog("[RN] failed added react context listener for reactHost ", Level.ERROR) + } + } +} diff --git a/example/exampleApp/android/app/src/main/AndroidManifest.xml b/example/exampleApp/android/app/src/main/AndroidManifest.xml index 3152724..5d112d5 100644 --- a/example/exampleApp/android/app/src/main/AndroidManifest.xml +++ b/example/exampleApp/android/app/src/main/AndroidManifest.xml @@ -45,5 +45,9 @@ + + diff --git a/example/exampleApp/android/app/src/main/java/com/exampleapp/MainActivity.kt b/example/exampleApp/android/app/src/main/java/com/exampleapp/MainActivity.kt index 5ab883f..790922f 100644 --- a/example/exampleApp/android/app/src/main/java/com/exampleapp/MainActivity.kt +++ b/example/exampleApp/android/app/src/main/java/com/exampleapp/MainActivity.kt @@ -13,7 +13,7 @@ import com.facebook.react.defaults.DefaultReactActivityDelegate import com.mindboxsdk.MindboxJsDelivery class MainActivity : ReactActivity() { - private var mJsDelivery: MindboxJsDelivery? = null + private var jsDelivery: MindboxJsDelivery? = null override fun getMainComponentName(): String = "exampleApp" override fun createReactActivityDelegate(): ReactActivityDelegate = @@ -22,7 +22,7 @@ class MainActivity : ReactActivity() { // Initializes MindboxJsDelivery and sends the current intent to React Native // https://developers.mindbox.ru/docs/flutter-push-navigation-react-native private fun initializeAndSentIntent(context: ReactContext) { - mJsDelivery = MindboxJsDelivery.Shared.getInstance(context) + jsDelivery = MindboxJsDelivery.Shared.getInstance(context) if (context.hasCurrentActivity()) { sendIntent(context, context.getCurrentActivity()!!.getIntent()) } else { @@ -32,8 +32,8 @@ class MainActivity : ReactActivity() { override fun onCreate(savedInstanceState: Bundle?) { super.onCreate(savedInstanceState) - val mReactInstanceManager = getReactNativeHost().getReactInstanceManager(); - val reactContext = mReactInstanceManager.getCurrentReactContext(); + val reactInstanceManager = getReactNativeHost().getReactInstanceManager(); + val reactContext = reactInstanceManager.getCurrentReactContext(); // Initialize and send intent if React context is already available // https://developers.mindbox.ru/docs/flutter-push-navigation-react-native @@ -41,11 +41,11 @@ class MainActivity : ReactActivity() { initializeAndSentIntent(reactContext); } else { // Add listener to initialize and send intent once React context is initialized - mReactInstanceManager.addReactInstanceEventListener(object : + reactInstanceManager.addReactInstanceEventListener(object : ReactInstanceManager.ReactInstanceEventListener { override fun onReactContextInitialized(context: ReactContext) { initializeAndSentIntent(context) - mReactInstanceManager.removeReactInstanceEventListener(this) + reactInstanceManager.removeReactInstanceEventListener(this) } }) } @@ -63,6 +63,6 @@ class MainActivity : ReactActivity() { Mindbox.onNewIntent(intent) //send click action Mindbox.onPushClicked(context, intent) - mJsDelivery?.sendPushClicked(intent); + jsDelivery?.sendPushClicked(intent); } } diff --git a/example/exampleApp/android/app/src/main/java/com/exampleapp/MainApplication.kt b/example/exampleApp/android/app/src/main/java/com/exampleapp/MainApplication.kt index d156ddb..7436ac3 100644 --- a/example/exampleApp/android/app/src/main/java/com/exampleapp/MainApplication.kt +++ b/example/exampleApp/android/app/src/main/java/com/exampleapp/MainApplication.kt @@ -21,6 +21,9 @@ import com.facebook.react.modules.core.DeviceEventManagerModule import com.google.gson.Gson import com.google.gson.reflect.TypeToken import cloud.mindbox.mindbox_rustore.MindboxRuStore +import com.facebook.react.defaults.DefaultNewArchitectureEntryPoint.load +import com.facebook.react.defaults.DefaultReactHost.getDefaultReactHost +import com.facebook.react.defaults.DefaultReactNativeHost class MainApplication : Application(), ReactApplication { @@ -39,6 +42,10 @@ class MainApplication : Application(), ReactApplication { //The fifth step of https://developers.mindbox.ru/docs/firebase-send-push-notifications-react-native Mindbox.initPushServices(this, listOf(MindboxFirebase, MindboxHuawei, MindboxRuStore)) SoLoader.init(this, false) + if (BuildConfig.IS_NEW_ARCHITECTURE_ENABLED) { + // If you opted-in for the New Architecture, we load the native entry point for this app. + load() + } } private val gson = Gson() diff --git a/example/exampleApp/android/build.gradle b/example/exampleApp/android/build.gradle index 52f2df7..d2f118c 100644 --- a/example/exampleApp/android/build.gradle +++ b/example/exampleApp/android/build.gradle @@ -2,10 +2,11 @@ buildscript { ext { buildToolsVersion = "34.0.0" minSdkVersion = 23 - compileSdkVersion = 34 - targetSdkVersion = 34 - ndkVersion = "25.1.8937393" + compileSdkVersion = 35 + targetSdkVersion = 35 + ndkVersion = "26.1.10909125" kotlinVersion = "1.8.0" + cmakeVersion = "3.22.1" } repositories { google() diff --git a/example/exampleApp/package.json b/example/exampleApp/package.json index 8d6e6b4..fcedece 100644 --- a/example/exampleApp/package.json +++ b/example/exampleApp/package.json @@ -7,6 +7,8 @@ "ios": "react-native run-ios", "lint": "eslint .", "start": "react-native start", + "wipe": "rm -rf ./node_modules && rm -f yarn.lock && rm -f yarn-error.log && cd ./ios && rm -rf ./Pods && rm -f Podfile.lock && cd ../android && rm -rf ./build && rm -rf ./.gradle && cd ./app && rm -rf ./build && cd ../../", + "assemble-debug": "mkdir -p android/app/src/main/assets && npx react-native bundle --platform android --dev false --entry-file index.js --bundle-output android/app/src/main/assets/index.android.bundle --assets-dest android/app/src/main/res/ && cd android && ./gradlew assembleDebug", "test": "jest" }, "dependencies": { @@ -16,10 +18,10 @@ "mindbox-sdk": "^2.13.1", "react": "18.2.0", "react-native": "0.74.0", - "react-native-gesture-handler": "^2.21.2", - "react-native-permissions": "^5.0.0", + "react-native-gesture-handler": "2.21.2", + "react-native-permissions": "^5.4.0", "react-native-safe-area-context": "^4.9.0", - "react-native-screens": "^3.29.0", + "react-native-screens": "^4.0.0", "react-native-snackbar": "^2.8.0" }, "devDependencies": {