diff --git a/build.gradle b/build.gradle
index 55faa13a..1159082e 100644
--- a/build.gradle
+++ b/build.gradle
@@ -22,19 +22,6 @@ allprojects {
}
}
-apply plugin: 'org.jetbrains.kotlinx.kover'
-
-dependencies {
- kover(project(':sdk'))
- kover(project(':mindbox-firebase'))
- kover(project(':mindbox-huawei'))
- kover(project(':mindbox-rustore'))
- kover(project(':mindbox-firebase-starter'))
- kover(project(':mindbox-huawei-starter'))
- kover(project(':mindbox-rustore-starter'))
- kover(project(':mindbox-sdk-starter-core'))
-}
-
tasks.register('clean', Delete) {
delete rootProject.getLayout().getBuildDirectory()
}
\ No newline at end of file
diff --git a/gradle/libs.versions.toml b/gradle/libs.versions.toml
index 926a903d..ae3ad7fe 100644
--- a/gradle/libs.versions.toml
+++ b/gradle/libs.versions.toml
@@ -19,6 +19,7 @@ volley = "1.2.1"
gson = "2.8.9"
work_manager = "2.8.1"
androidx_lifecycle = "2.8.7"
+androidx_startup = "1.2.0"
androidx_core_ktx = "1.13.0"
androidx_annotations = "1.3.0"
constraint_layout = "2.1.4"
@@ -39,7 +40,6 @@ agcp = "1.9.1.300"
ktlint-plugin = "12.1.1"
ksp = "1.9.22-1.0.17"
maven_publish = "0.32.0"
-kover = "0.8.3"
pushclient = "7.2.0"
@@ -52,7 +52,6 @@ buildscript-plugins = [
"ktlint_gradle_plugin",
"ksp_gradle_plugin",
"maven_publish_plugin",
- "kover_gradle_plugin",
]
test = [
@@ -88,6 +87,7 @@ room_ktx = { group = "androidx.room", name = "room-ktx", version.ref = "room" }
room_compiler = { group = "androidx.room", name = "room-compiler", version.ref = "room" }
work_manager = { group = "androidx.work", name = "work-runtime-ktx", version.ref = "work_manager" }
androidx_lifecycle = { group = "androidx.lifecycle", name = "lifecycle-process", version.ref = "androidx_lifecycle" }
+androidx_startup = { group = "androidx.startup", name = "startup-runtime", version.ref = "androidx_startup" }
hms_push = { group = "com.huawei.hms", name = "push", version.ref = "hms_push" }
hms_ads_identifier = { group = "com.huawei.hms", name = "ads-identifier", version.ref = "hms_ads_identifier" }
constraint_layout = { group = "androidx.constraintlayout", name = "constraintlayout", version.ref = "constraint_layout" }
@@ -118,5 +118,4 @@ google_services = { module = "com.google.gms:google-services", version.ref = "go
agcp = { module = "com.huawei.agconnect:agcp", version.ref = "agcp" }
ktlint_gradle_plugin = { module = "org.jlleitschuh.gradle:ktlint-gradle", version.ref = "ktlint-plugin" }
ksp_gradle_plugin = { module = "com.google.devtools.ksp:symbol-processing-gradle-plugin", version.ref = "ksp" }
-maven_publish_plugin = { module = "com.vanniktech:gradle-maven-publish-plugin", version.ref = "maven_publish" }
-kover_gradle_plugin = { module = "org.jetbrains.kotlinx:kover-gradle-plugin", version.ref = "kover" }
\ No newline at end of file
+maven_publish_plugin = { module = "com.vanniktech:gradle-maven-publish-plugin", version.ref = "maven_publish" }
\ No newline at end of file
diff --git a/modulesCommon.gradle b/modulesCommon.gradle
index e1b436a4..3d607614 100644
--- a/modulesCommon.gradle
+++ b/modulesCommon.gradle
@@ -3,7 +3,6 @@ apply plugin: 'kotlin-android'
apply plugin: 'signing'
apply plugin: 'org.jlleitschuh.gradle.ktlint'
apply plugin: 'com.vanniktech.maven.publish'
-apply plugin: 'org.jetbrains.kotlinx.kover'
group = 'com.github.mindbox-cloud'
diff --git a/sdk/build.gradle b/sdk/build.gradle
index 9d7d78b7..79789515 100644
--- a/sdk/build.gradle
+++ b/sdk/build.gradle
@@ -87,6 +87,7 @@ dependencies {
// Handle app lifecycle
implementation libs.androidx.lifecycle
+ implementation libs.androidx.startup
implementation libs.threetenabp
// Glide
diff --git a/sdk/src/main/AndroidManifest.xml b/sdk/src/main/AndroidManifest.xml
index e50bafe2..92ba798a 100644
--- a/sdk/src/main/AndroidManifest.xml
+++ b/sdk/src/main/AndroidManifest.xml
@@ -9,6 +9,16 @@
+
+
+
+
Unit>()
private val deviceUuidCallbacks = ConcurrentHashMap Unit>()
- private lateinit var lifecycleManager: LifecycleManager
+ private val lifecycleManager: LifecycleManager? get() = LifecycleManager.instance
private val userVisitManager: UserVisitManager by mindboxInject { userVisitManager }
private val timeProvider by mindboxInject { timeProvider }
@@ -554,152 +552,139 @@ public object Mindbox : MindboxLog {
context: Context,
configuration: MindboxConfiguration,
pushServices: List,
- ) {
- LoggingExceptionHandler.runCatching {
- verifyThreadExecution(methodName = "init")
- val currentProcessName = context.getCurrentProcessName()
- if (!context.isMainProcess(currentProcessName)) {
- logW("Skip Mindbox init not in main process! Current process $currentProcessName")
- return@runCatching
- }
- Stopwatch.start(Stopwatch.INIT_SDK)
+ ): Unit = loggingRunCatching {
+ verifyThreadExecution(methodName = "init")
+ val currentProcessName = context.getCurrentProcessName()
+ if (!context.isMainProcess(currentProcessName)) {
+ logW("Skip Mindbox init not in main process! Current process $currentProcessName")
+ return@loggingRunCatching
+ }
+ Stopwatch.start(Stopwatch.INIT_SDK)
- initComponents(context.applicationContext)
- pushConverters = selectPushServiceHandler(pushServices)
- logI("init in $currentProcessName. firstInitCall: ${firstInitCall.get()}, " +
+ initComponents(context.applicationContext)
+ pushConverters = selectPushServiceHandler(pushServices)
+ logI(
+ "init in $currentProcessName. firstInitCall: ${firstInitCall.get()}, " +
"configuration: $configuration, pushServices: " +
pushServices.joinToString(", ") { it.javaClass.simpleName } +
- ", SdkVersion:${getSdkVersion()}, CommonSdkVersion:${MindboxCommon.VERSION_NAME}")
+ ", SdkVersion:${getSdkVersion()}, CommonSdkVersion:${MindboxCommon.VERSION_NAME}",
+ )
- if (!firstInitCall.get()) {
- InitializeLock.reset(InitializeLock.State.SAVE_MINDBOX_CONFIG)
- } else {
- userVisitManager.saveUserVisit()
- }
+ if (!firstInitCall.get()) {
+ InitializeLock.reset(InitializeLock.State.SAVE_MINDBOX_CONFIG)
+ } else {
+ userVisitManager.saveUserVisit()
+ }
- initScope.launch {
- InitializeLock.await(InitializeLock.State.MIGRATION)
- val checkResult = checkConfig(configuration)
- val validatedConfiguration = validateConfiguration(configuration)
- DbManager.saveConfigurations(Configuration(configuration))
- logI("init. checkResult: $checkResult")
- if (checkResult != ConfigUpdate.NOT_UPDATED && !MindboxPreferences.isFirstInitialize) {
- logI("init. softReinitialization")
- softReinitialization(context.applicationContext)
- }
+ launchInitJob(context, configuration, pushServices)
+ setupLifecycleManager(context)
+ attachLifecycleCallbacks()
+ }
- if (checkResult == ConfigUpdate.UPDATED) {
- setPushServiceHandler(context, pushServices)
- firstInitialization(context.applicationContext, validatedConfiguration)
+ private fun launchInitJob(
+ context: Context,
+ configuration: MindboxConfiguration,
+ pushServices: List,
+ ) {
+ initScope.launch {
+ InitializeLock.await(InitializeLock.State.MIGRATION)
+ val checkResult = checkConfig(configuration)
+ val validatedConfiguration = validateConfiguration(configuration)
+ DbManager.saveConfigurations(Configuration(configuration))
+ logI("init. checkResult: $checkResult")
+ if (checkResult != ConfigUpdate.NOT_UPDATED && !MindboxPreferences.isFirstInitialize) {
+ logI("init. softReinitialization")
+ softReinitialization(context.applicationContext)
+ }
- val isTrackVisitNotSent = Mindbox::lifecycleManager.isInitialized &&
- !lifecycleManager.isTrackVisitSent()
- if (isTrackVisitNotSent) {
- MindboxLoggerImpl.d(this, "Track visit event with source $DIRECT")
- sendTrackVisitEvent(context.applicationContext, DIRECT)
- }
- } else {
- mindboxScope.launch {
- setPushServiceHandler(context, pushServices)
- }
- MindboxEventManager.sendEventsIfExist(context.applicationContext)
+ if (checkResult == ConfigUpdate.UPDATED) {
+ setPushServiceHandler(context, pushServices)
+ firstInitialization(context.applicationContext, validatedConfiguration)
+ } else {
+ mindboxScope.launch {
+ setPushServiceHandler(context, pushServices)
}
- MindboxPreferences.uuidDebugEnabled = configuration.uuidDebugEnabled
- }.initState(InitializeLock.State.SAVE_MINDBOX_CONFIG)
- .invokeOnCompletion { throwable ->
- if (throwable == null) {
- if (firstInitCall.get()) {
- val activity = context as? Activity
- if (activity != null && lifecycleManager.isCurrentActivityResumed) {
- inAppMessageManager.registerCurrentActivity(activity)
- mindboxScope.launch {
- inAppMutex.withLock {
- logI("Start inapp manager after init. firstInitCall: ${firstInitCall.get()}")
- if (!firstInitCall.getAndSet(false)) return@launch
- inAppMessageManager.listenEventAndInApp()
- inAppMessageManager.initLogs()
- MindboxEventManager.eventFlow.emit(MindboxEventManager.appStarted())
- inAppMessageManager.requestConfig().join()
- }
- }
+ MindboxEventManager.sendEventsIfExist(context.applicationContext)
+ }
+ MindboxPreferences.uuidDebugEnabled = configuration.uuidDebugEnabled
+ }.initState(InitializeLock.State.SAVE_MINDBOX_CONFIG)
+ .invokeOnCompletion { throwable ->
+ if (throwable == null && firstInitCall.get()) {
+ val activity = context as? Activity
+ if (activity != null && lifecycleManager?.isCurrentActivityResumed == true) {
+ inAppMessageManager.registerCurrentActivity(activity)
+ mindboxScope.launch {
+ inAppMutex.withLock {
+ logI("Start inapp manager after init. firstInitCall: ${firstInitCall.get()}")
+ if (!firstInitCall.getAndSet(false)) return@launch
+ inAppMessageManager.listenEventAndInApp()
+ inAppMessageManager.initLogs()
+ MindboxEventManager.eventFlow.emit(MindboxEventManager.appStarted())
+ inAppMessageManager.requestConfig().join()
}
}
}
}
- // Handle back app in foreground
- (context.applicationContext as? Application)?.apply {
- val applicationLifecycle = ProcessLifecycleOwner.get().lifecycle
+ }
+ }
- if (!Mindbox::lifecycleManager.isInitialized) {
- val activity = context as? Activity
- val isApplicationResumed = applicationLifecycle.currentState == RESUMED
- if (isApplicationResumed && activity == null) {
- logE("Incorrect context type for calling init in this place")
- }
- if (isApplicationResumed || context !is Application) {
- logW(
- "We recommend to call Mindbox.init() synchronously from " +
- "Application.onCreate. If you can't do so, don't forget to " +
- "call Mindbox.initPushServices from Application.onCreate",
- )
+ private fun setupLifecycleManager(context: Context) {
+ if (LifecycleManager.isRegister) {
+ if (!firstInitCall.get()) {
+ lifecycleManager?.scheduleReinitTrackVisit()
+ }
+ return
+ }
+
+ logW("Register LifecycleManager (startup initializer not found)")
+ LifecycleManager.register(context)
+ }
+
+ private fun attachLifecycleCallbacks() {
+ lifecycleManager?.callbacks = object : LifecycleManager.Callbacks {
+ override fun onActivityStarted(activity: Activity) {
+ UuidCopyManager.onAppMovedToForeground(activity)
+ mindboxScope.launch {
+ if (!MindboxPreferences.isFirstInitialize) {
+ updateAppInfo(activity.applicationContext)
}
+ }
+ }
- logI("init. init lifecycleManager")
- lifecycleManager = LifecycleManager(
- currentActivityName = activity?.javaClass?.name,
- currentIntent = activity?.intent,
- isAppInBackground = !isApplicationResumed,
- onActivityStarted = { startedActivity ->
- UuidCopyManager.onAppMovedToForeground(startedActivity)
- mindboxScope.launch {
- if (!MindboxPreferences.isFirstInitialize) {
- updateAppInfo(startedActivity.applicationContext)
- }
- }
- },
- onActivityPaused = { pausedActivity ->
- inAppMessageManager.onPauseCurrentActivity(pausedActivity)
- },
- onActivityResumed = { resumedActivity ->
- inAppMessageManager.onResumeCurrentActivity(
- resumedActivity
- )
- if (firstInitCall.get()) {
- mindboxScope.launch {
- InitializeLock.await(InitializeLock.State.SAVE_MINDBOX_CONFIG)
- inAppMutex.withLock {
- logI("Start inapp manager after resume activity. firstInitCall: ${firstInitCall.get()}")
- if (!firstInitCall.getAndSet(false)) return@launch
- inAppMessageManager.listenEventAndInApp()
- inAppMessageManager.initLogs()
- MindboxEventManager.eventFlow.emit(MindboxEventManager.appStarted())
- inAppMessageManager.requestConfig().join()
- }
- }
- }
- },
- onActivityStopped = { resumedActivity ->
- inAppMessageManager.onStopCurrentActivity(resumedActivity)
- },
- onTrackVisitReady = { source, requestUrl ->
- sessionStorageManager.hasSessionExpired()
- eventScope.launch {
- sendTrackVisitEvent(
- MindboxDI.appModule.appContext,
- source,
- requestUrl
- )
- }
+ override fun onActivityPaused(activity: Activity) {
+ inAppMessageManager.onPauseCurrentActivity(activity)
+ }
+
+ override fun onActivityResumed(activity: Activity) {
+ inAppMessageManager.onResumeCurrentActivity(activity)
+ if (firstInitCall.get()) {
+ mindboxScope.launch {
+ InitializeLock.await(InitializeLock.State.SAVE_MINDBOX_CONFIG)
+ inAppMutex.withLock {
+ logI("Start in-app manager after resume activity. firstInitCall: ${firstInitCall.get()}")
+ if (!firstInitCall.getAndSet(false)) return@launch
+ inAppMessageManager.listenEventAndInApp()
+ inAppMessageManager.initLogs()
+ MindboxEventManager.eventFlow.emit(MindboxEventManager.appStarted())
+ inAppMessageManager.requestConfig().join()
}
- )
- } else {
- unregisterActivityLifecycleCallbacks(lifecycleManager)
- applicationLifecycle.removeObserver(lifecycleManager)
- lifecycleManager.wasReinitialized()
+ }
}
+ }
- registerActivityLifecycleCallbacks(lifecycleManager)
- applicationLifecycle.addObserver(lifecycleManager)
+ override fun onActivityStopped(activity: Activity) {
+ inAppMessageManager.onStopCurrentActivity(activity)
+ }
+
+ override fun onTrackVisitReady(source: String?, requestUrl: String?) {
+ sessionStorageManager.hasSessionExpired()
+ eventScope.launch {
+ sendTrackVisitEvent(
+ MindboxDI.appModule.appContext,
+ source,
+ requestUrl,
+ )
+ }
}
}
}
@@ -889,8 +874,8 @@ public object Mindbox : MindboxLog {
*/
public fun onNewIntent(intent: Intent?): Unit = LoggingExceptionHandler.runCatching {
MindboxLoggerImpl.d(this, "onNewIntent. intent: $intent")
- if (Mindbox::lifecycleManager.isInitialized) {
- lifecycleManager.onNewIntent(intent)
+ if (lifecycleManager != null) {
+ lifecycleManager?.onNewIntent(intent)
} else {
MindboxLoggerImpl.d(this, "onNewIntent. LifecycleManager is not initialized. Skipping.")
}
diff --git a/sdk/src/main/java/cloud/mindbox/mobile_sdk/inapp/presentation/view/WebViewInappViewHolder.kt b/sdk/src/main/java/cloud/mindbox/mobile_sdk/inapp/presentation/view/WebViewInappViewHolder.kt
index 76c5ccca..e86c6770 100644
--- a/sdk/src/main/java/cloud/mindbox/mobile_sdk/inapp/presentation/view/WebViewInappViewHolder.kt
+++ b/sdk/src/main/java/cloud/mindbox/mobile_sdk/inapp/presentation/view/WebViewInappViewHolder.kt
@@ -648,7 +648,7 @@ internal class WebViewInAppViewHolder(
errorDescription = "Failed to fetch HTML content for In-App",
throwable = e
)
- inAppController.close()
+ controller.executeOnViewThread { inAppController.close() }
}
} ?: run {
inAppFailureTracker.sendFailureWithContext(
@@ -656,7 +656,7 @@ internal class WebViewInAppViewHolder(
failureReason = FailureReason.WEBVIEW_LOAD_FAILED,
errorDescription = "WebView content URL is null"
)
- inAppController.close()
+ controller.executeOnViewThread { inAppController.close() }
}
}
}
@@ -782,12 +782,10 @@ internal class WebViewInAppViewHolder(
}
private data class NavigationInterceptedPayload(
- @SerializedName("url")
val url: String
)
private data class ErrorPayload(
- @SerializedName("error")
val error: String
)
diff --git a/sdk/src/main/java/cloud/mindbox/mobile_sdk/managers/LifecycleManager.kt b/sdk/src/main/java/cloud/mindbox/mobile_sdk/managers/LifecycleManager.kt
index 858cb1e4..df8dc7b1 100644
--- a/sdk/src/main/java/cloud/mindbox/mobile_sdk/managers/LifecycleManager.kt
+++ b/sdk/src/main/java/cloud/mindbox/mobile_sdk/managers/LifecycleManager.kt
@@ -2,11 +2,16 @@ package cloud.mindbox.mobile_sdk.managers
import android.app.Activity
import android.app.Application
+import android.content.Context
import android.content.Intent
import android.os.Bundle
import androidx.lifecycle.Lifecycle
import androidx.lifecycle.LifecycleEventObserver
import androidx.lifecycle.LifecycleOwner
+import androidx.lifecycle.ProcessLifecycleOwner
+import cloud.mindbox.mobile_sdk.Mindbox.logE
+import cloud.mindbox.mobile_sdk.Mindbox.logW
+import cloud.mindbox.mobile_sdk.logger.MindboxLog
import cloud.mindbox.mobile_sdk.logger.mindboxLogI
import cloud.mindbox.mobile_sdk.models.DIRECT
import cloud.mindbox.mobile_sdk.models.LINK
@@ -16,186 +21,283 @@ import cloud.mindbox.mobile_sdk.utils.loggingRunCatching
import java.util.Timer
import kotlin.concurrent.timer
-internal class LifecycleManager(
+internal class LifecycleManager internal constructor(
private var currentActivityName: String?,
private var currentIntent: Intent?,
private var isAppInBackground: Boolean,
- private var onActivityResumed: (resumedActivity: Activity) -> Unit,
- private var onActivityPaused: (pausedActivity: Activity) -> Unit,
- private var onActivityStarted: (activity: Activity) -> Unit,
- private var onActivityStopped: (activity: Activity) -> Unit,
- private var onTrackVisitReady: (source: String?, requestUrl: String?) -> Unit,
-) : Application.ActivityLifecycleCallbacks, LifecycleEventObserver {
+) : Application.ActivityLifecycleCallbacks, LifecycleEventObserver, MindboxLog {
+
+ internal interface Callbacks {
+ fun onActivityStarted(activity: Activity) {}
+
+ fun onActivityPaused(activity: Activity) {}
+
+ fun onActivityResumed(activity: Activity) {}
+
+ fun onActivityStopped(activity: Activity) {}
+
+ fun onTrackVisitReady(source: String?, requestUrl: String?) {}
+ }
companion object {
- private const val SCHEMA_HTTP = "http"
- private const val SCHEMA_HTTPS = "https"
+ private const val TIMER_PERIOD = 1_200_000L
+ private const val MAX_INTENT_HASHES = 50
+
+ @Volatile
+ internal var instance: LifecycleManager? = null
- private const val TIMER_PERIOD = 1200000L
- private const val MAX_INTENT_HASHES_SIZE = 50
+ internal val isRegister: Boolean get() = instance != null
+
+ internal fun register(context: Context) {
+ val lifecycle = ProcessLifecycleOwner.get().lifecycle
+ val activity = context as? Activity
+ val application = context.applicationContext as? Application
+ val isForegrounded = lifecycle.currentState.isAtLeast(Lifecycle.State.STARTED)
+
+ if (isForegrounded && activity == null) {
+ logE("Incorrect context type for calling init in this place")
+ }
+ if (isForegrounded || context !is Application) {
+ logW(
+ "We recommend to call Mindbox.init() synchronously from " +
+ "Application.onCreate. If you can't do so, don't forget to " +
+ "call Mindbox.initPushServices from Application.onCreate",
+ )
+ }
+
+ LifecycleManager(
+ currentActivityName = activity?.javaClass?.name,
+ currentIntent = activity?.intent,
+ isAppInBackground = !isForegrounded,
+ ).also { manager ->
+ application?.registerActivityLifecycleCallbacks(manager)
+ lifecycle.addObserver(manager)
+ instance = manager
+ }
+ }
}
- private var isIntentChanged = true
- private var timer: Timer? = null
+ /**
+ * True when a foreground transition happened before [callbacks] was set —
+ * i.e. before [cloud.mindbox.mobile_sdk.Mindbox.init] was called.
+ */
+ @Volatile
+ private var pendingVisit: Boolean = false
+
+ @Volatile
+ var callbacks: Callbacks? = null
+ set(value) {
+ field = value
+ if (value != null && pendingVisit) {
+ pendingVisit = false
+ dispatchCurrentVisit(value)
+ }
+ }
+
+ /**
+ * True by default — Activity.onResume() fires before the manager is registered
+ * when Mindbox.init() is called from Activity.onCreate().
+ */
+ var isCurrentActivityResumed: Boolean = true
+ private set
+
+ private var intentChanged = true
+ private var keepaliveTimer: Timer? = null
private val intentHashes = mutableListOf()
+ private var skipNextTrackVisit = false
/**
- * True by default.
- * Has to be true because Activity.onResume() triggers before Lifecycle Manager is registered
- * when Mindbox.init() was called in Activity.onCreate()
- **/
- var isCurrentActivityResumed = true
- private var skipSendingTrackVisit = false
+ * True when [onMovedToForeground] was called while [currentIntent] was still null —
+ * i.e. the app foregrounded before the first [onActivityStarted] callback arrived.
+ *
+ * This happens in Case 3: no [MindboxLifecycleInitializer], [Mindbox.init] called from
+ * [Application.onCreate]. [ProcessLifecycleOwnerInitializer] registers [LifecycleDispatcher]
+ * first, so the process-level ON_START fires *before* [LifecycleManager.onActivityStarted]
+ * updates [currentIntent]. The flag is cleared and the visit is dispatched inside
+ * [onActivityStarted] once the intent becomes available.
+ */
+ private var foregroundedWithoutIntent = false
- override fun onActivityCreated(activity: Activity, p1: Bundle?) {
- }
+ override fun onActivityCreated(activity: Activity, p1: Bundle?) = Unit
+
+ override fun onActivitySaveInstanceState(activity: Activity, p1: Bundle) = Unit
+
+ override fun onActivityDestroyed(activity: Activity) = Unit
override fun onActivityStarted(activity: Activity): Unit = loggingRunCatching {
mindboxLogI("onActivityStarted. activity: ${activity.javaClass.simpleName}")
- onActivityStarted.invoke(activity)
- val areActivitiesEqual = currentActivityName == activity.javaClass.name
+ callbacks?.onActivityStarted(activity)
+
+ val sameActivity = currentActivityName == activity.javaClass.name
val intent = activity.intent
- isIntentChanged = if (currentIntent != intent) {
- updateActivityParameters(activity)
- intent?.hashCode()?.let(::updateHashesList) ?: true
+ intentChanged = if (currentIntent != intent) {
+ updateActivityState(activity)
+ intent?.hashCode()?.let(::isNewHash) ?: true
} else {
false
}
- if (isAppInBackground || !isIntentChanged) {
+ if (isAppInBackground || !intentChanged) {
isAppInBackground = false
+ if (foregroundedWithoutIntent && intentChanged) {
+ foregroundedWithoutIntent = false
+ sendTrackVisit(intent ?: return@loggingRunCatching)
+ }
return@loggingRunCatching
}
- sendTrackVisit(activity.intent, areActivitiesEqual)
+ sendTrackVisit(intent ?: return@loggingRunCatching, sameActivity)
}
override fun onActivityResumed(activity: Activity) {
mindboxLogI("onActivityResumed. activity: ${activity.javaClass.simpleName}")
isCurrentActivityResumed = true
- onActivityResumed.invoke(activity)
- isCurrentActivityResumed = true
+ callbacks?.onActivityResumed(activity)
}
override fun onActivityPaused(activity: Activity) {
mindboxLogI("onActivityPaused. activity: ${activity.javaClass.simpleName}")
isCurrentActivityResumed = false
- onActivityPaused.invoke(activity)
- isCurrentActivityResumed = false
+ callbacks?.onActivityPaused(activity)
}
override fun onActivityStopped(activity: Activity) {
mindboxLogI("onActivityStopped. activity: ${activity.javaClass.simpleName}")
if (currentIntent == null || currentActivityName == null) {
- updateActivityParameters(activity)
+ updateActivityState(activity)
}
- onActivityStopped.invoke(activity)
+ callbacks?.onActivityStopped(activity)
}
- override fun onActivitySaveInstanceState(activity: Activity, p1: Bundle) {
- }
-
- override fun onActivityDestroyed(activity: Activity) {
+ override fun onStateChanged(source: LifecycleOwner, event: Lifecycle.Event) {
+ when (event) {
+ Lifecycle.Event.ON_STOP -> onMovedToBackground()
+ Lifecycle.Event.ON_START -> onMovedToForeground()
+ else -> Unit
+ }
}
fun isTrackVisitSent(): Boolean {
currentIntent?.let { intent ->
- if (updateHashesList(intent.hashCode())) {
+ if (isNewHash(intent.hashCode())) {
sendTrackVisit(intent)
}
}
return currentIntent != null
}
- fun wasReinitialized() {
- skipSendingTrackVisit = true
+ /**
+ * Schedules a track-visit to be dispatched the next time [callbacks] is assigned.
+ *
+ * Call this before replacing [callbacks] via [cloud.mindbox.mobile_sdk.Mindbox.init]
+ * so the new endpoint receives a track-visit immediately upon reinitialisation.
+ * The backend uses this signal to learn the device is now active in the new environment.
+ */
+ fun scheduleReinitTrackVisit() {
+ pendingVisit = true
+ mindboxLogI("Track visit scheduled for reinit")
}
- fun onNewIntent(newIntent: Intent?): Unit? = newIntent?.let { intent ->
- if (intent.data != null || intent.extras?.getBoolean(IS_OPENED_FROM_PUSH_BUNDLE_KEY) == true) {
- isIntentChanged = updateHashesList(intent.hashCode())
- sendTrackVisit(intent)
- skipSendingTrackVisit = isAppInBackground
- }
+ fun onNewIntent(newIntent: Intent?) {
+ val intent = newIntent ?: return
+ val hasDeepLink = intent.data != null
+ val isFromPush = intent.extras?.getBoolean(IS_OPENED_FROM_PUSH_BUNDLE_KEY) == true
+ if (!hasDeepLink && !isFromPush) return
+
+ intentChanged = isNewHash(intent.hashCode())
+ sendTrackVisit(intent)
+ skipNextTrackVisit = isAppInBackground
}
- private fun onAppMovedToBackground(): Unit = loggingRunCatching {
+ private fun onMovedToBackground(): Unit = loggingRunCatching {
mindboxLogI("onAppMovedToBackground")
isAppInBackground = true
+ pendingVisit = false
+ foregroundedWithoutIntent = false
cancelKeepaliveTimer()
}
- private fun onAppMovedToForeground(): Unit = loggingRunCatching {
+ private fun onMovedToForeground(): Unit = loggingRunCatching {
mindboxLogI("onAppMovedToForeground")
- if (!skipSendingTrackVisit) {
- currentIntent?.let(::sendTrackVisit)
+ if (skipNextTrackVisit) {
+ skipNextTrackVisit = false
+ return@loggingRunCatching
+ }
+ val intent = currentIntent
+ if (intent != null) {
+ sendTrackVisit(intent)
} else {
- skipSendingTrackVisit = false
+ foregroundedWithoutIntent = true
+ mindboxLogI("Track visit deferred — foregrounded before first activity")
}
}
- private fun updateActivityParameters(activity: Activity): Unit = loggingRunCatching {
+ private fun updateActivityState(activity: Activity): Unit = loggingRunCatching {
currentActivityName = activity.javaClass.name
currentIntent = activity.intent
}
private fun sendTrackVisit(
intent: Intent,
- areActivitiesEqual: Boolean = true,
+ sameActivity: Boolean = true,
): Unit = loggingRunCatching {
- val source = if (isIntentChanged) source(intent) else DIRECT
-
- if (areActivitiesEqual || source != DIRECT) {
- val requestUrl = if (source == LINK) intent.data?.toString() else null
- onTrackVisitReady.invoke(source, requestUrl)
- startKeepaliveTimer()
+ val source = if (intentChanged) intentSource(intent) else DIRECT
+ if (!sameActivity && source == DIRECT) return@loggingRunCatching
- mindboxLogI("Track visit event with source $source and url $requestUrl")
+ val cb = callbacks
+ if (cb == null) {
+ pendingVisit = true
+ mindboxLogI("Track visit pending (no callbacks yet)")
+ return@loggingRunCatching
}
+ pendingVisit = false
+ val requestUrl = if (source == LINK) intent.data?.toString() else null
+ cb.onTrackVisitReady(source, requestUrl)
+ startKeepaliveTimer()
+ mindboxLogI("Track visit event with source $source and url $requestUrl")
}
- private fun source(intent: Intent?): String? = loggingRunCatching(defaultValue = null) {
- when {
- intent?.scheme == SCHEMA_HTTP || intent?.scheme == SCHEMA_HTTPS -> LINK
- intent?.extras?.getBoolean(IS_OPENED_FROM_PUSH_BUNDLE_KEY) == true -> PUSH
- else -> DIRECT
- }
+ /**
+ * Derives source and URL from the already-stored [currentIntent]/[intentChanged] and
+ * dispatches the track-visit through [cb].
+ *
+ * Called from the [callbacks] setter when [pendingVisit] is raised — the same pattern
+ * iOS uses in `MBSessionManager` when `initializationCompleted` fires while `isActive` is true.
+ */
+ private fun dispatchCurrentVisit(cb: Callbacks): Unit = loggingRunCatching {
+ val intent = currentIntent ?: return@loggingRunCatching
+ val source = if (intentChanged) intentSource(intent) else DIRECT
+ val requestUrl = if (source == LINK) intent.data?.toString() else null
+ cb.onTrackVisitReady(source, requestUrl)
+ startKeepaliveTimer()
+ mindboxLogI("Track visit dispatched from pending state: source=$source url=$requestUrl")
}
- private fun updateHashesList(code: Int): Boolean = loggingRunCatching(defaultValue = true) {
- if (!intentHashes.contains(code)) {
- if (intentHashes.size >= MAX_INTENT_HASHES_SIZE) {
- intentHashes.removeAt(0)
- }
- intentHashes.add(code)
- true
- } else {
- false
- }
+ private fun intentSource(intent: Intent): String = when {
+ intent.scheme == "http" || intent.scheme == "https" -> LINK
+ intent.extras?.getBoolean(IS_OPENED_FROM_PUSH_BUNDLE_KEY) == true -> PUSH
+ else -> DIRECT
+ }
+
+ private fun isNewHash(hash: Int): Boolean = loggingRunCatching(defaultValue = true) {
+ if (intentHashes.contains(hash)) return@loggingRunCatching false
+ if (intentHashes.size >= MAX_INTENT_HASHES) intentHashes.removeAt(0)
+ intentHashes.add(hash)
+ true
}
private fun startKeepaliveTimer(): Unit = loggingRunCatching {
cancelKeepaliveTimer()
- timer = timer(
+ keepaliveTimer = timer(
initialDelay = TIMER_PERIOD,
period = TIMER_PERIOD,
- action = { onTrackVisitReady.invoke(null, null) },
+ action = { callbacks?.onTrackVisitReady(null, null) },
)
}
private fun cancelKeepaliveTimer(): Unit = loggingRunCatching {
- timer?.cancel()
- timer = null
- }
-
- override fun onStateChanged(source: LifecycleOwner, event: Lifecycle.Event) {
- when (event) {
- Lifecycle.Event.ON_STOP -> onAppMovedToBackground()
- Lifecycle.Event.ON_START -> onAppMovedToForeground()
- else -> {
- // do nothing
- }
- }
+ keepaliveTimer?.cancel()
+ keepaliveTimer = null
}
}
diff --git a/sdk/src/main/java/cloud/mindbox/mobile_sdk/managers/MindboxLifecycleInitializer.kt b/sdk/src/main/java/cloud/mindbox/mobile_sdk/managers/MindboxLifecycleInitializer.kt
new file mode 100644
index 00000000..379f44ab
--- /dev/null
+++ b/sdk/src/main/java/cloud/mindbox/mobile_sdk/managers/MindboxLifecycleInitializer.kt
@@ -0,0 +1,31 @@
+package cloud.mindbox.mobile_sdk.managers
+
+import android.content.Context
+import android.util.Log
+import androidx.annotation.RestrictTo
+import androidx.startup.Initializer
+import cloud.mindbox.mobile_sdk.getCurrentProcessName
+import cloud.mindbox.mobile_sdk.isMainProcess
+import cloud.mindbox.mobile_sdk.logger.MindboxLoggerImpl
+
+/**
+ * Registers [LifecycleManager] at application startup via androidx.startup so that lifecycle
+ * tracking begins before [cloud.mindbox.mobile_sdk.Mindbox.init] is called.
+ *
+ * Track-visit events are only dispatched after [cloud.mindbox.mobile_sdk.Mindbox.init] wires
+ * the [LifecycleManager.onTrackVisitReady] callback.
+ */
+@RestrictTo(RestrictTo.Scope.LIBRARY)
+public class MindboxLifecycleInitializer : Initializer {
+
+ override fun create(context: Context) {
+ val currentProcessName = context.getCurrentProcessName()
+ if (!context.isMainProcess(currentProcessName)) return
+
+ // Log before init mindbox
+ Log.i(MindboxLoggerImpl.TAG, "LifecycleInitializer: Register LifecycleManager in startup initializer")
+ LifecycleManager.register(context)
+ }
+
+ override fun dependencies(): List>> = emptyList()
+}