Coverage Summary for Class: Mindbox (cloud.mindbox.mobile_sdk)

Class Method, % Branch, % Line, % Instruction, %
Mindbox 35.9% (28/78) 12.5% (8/64) 21.8% (45/206) 29.5% (359/1215)
Mindbox$asyncOperation$1 0% (0/1) 0% (0/4) 0% (0/10)
Mindbox$asyncOperation$2 0% (0/1) 0% (0/2) 0% (0/20)
Mindbox$attachLifecycleCallbacks$1 16.7% (1/6) 0% (0/2) 10% (1/10) 3.1% (2/65)
Mindbox$attachLifecycleCallbacks$1$onActivityResumed$1 0% (0/1) 0% (0/2) 0% (0/9) 0% (0/63)
Mindbox$attachLifecycleCallbacks$1$onActivityStarted$1 0% (0/1) 0% (0/2) 0% (0/2) 0% (0/22)
Mindbox$attachLifecycleCallbacks$1$onTrackVisitReady$1 0% (0/1) 0% (0/5) 0% (0/22)
Mindbox$checkConfig$1 0% (0/1) 0% (0/18) 0% (0/22) 0% (0/83)
Mindbox$executeSyncOperation$1 0% (0/1) 0% (0/7) 0% (0/24)
Mindbox$executeSyncOperation$2 0% (0/1) 0% (0/6) 0% (0/22)
Mindbox$firstInitialization$2 100% (1/1) 75% (3/4) 100% (27/27) 98.5% (129/131)
Mindbox$getDeviceId$1
Mindbox$getDeviceId$2$1$adidResult$1 0% (0/1) 0% (0/4) 0% (0/2) 0% (0/28)
Mindbox$getPushTokensSaveDate$1 100% (1/1) 100% (4/4) 100% (24/24)
Mindbox$getSdkVersion$1 0% (0/1) 0% (0/2) 0% (0/6)
Mindbox$handleRemoteMessage$1 0% (0/1) 0% (0/14) 0% (0/22) 0% (0/148)
Mindbox$handleRemoteMessage$1$2 0% (0/1) 0% (0/9) 0% (0/26)
Mindbox$inAppMessageManager$2 0% (0/1) 0% (0/1) 0% (0/2)
Mindbox$initComponents$1 0% (0/1) 0% (0/1) 0% (0/11)
Mindbox$initialize$1 0% (0/1) 0% (0/4) 0% (0/18) 0% (0/87)
Mindbox$initialize$1$1 0% (0/1) 0% (0/1) 0% (0/7)
Mindbox$initPushServices$1 0% (0/1) 0% (0/2) 0% (0/26)
Mindbox$launchInitJob$1 0% (0/1) 0% (0/6) 0% (0/14) 0% (0/123)
Mindbox$launchInitJob$1$1 0% (0/1) 0% (0/1) 0% (0/14)
Mindbox$launchInitJob$2 0% (0/1) 0% (0/14) 0% (0/5) 0% (0/49)
Mindbox$launchInitJob$2$1 0% (0/1) 0% (0/2) 0% (0/8) 0% (0/51)
Mindbox$migrationManager$2 0% (0/1) 0% (0/1) 0% (0/2)
Mindbox$mindboxWorkerFactory$2 0% (0/1) 0% (0/1) 0% (0/1)
Mindbox$onNewIntent$1 0% (0/1) 0% (0/4) 0% (0/3) 0% (0/22)
Mindbox$onPushClicked$1 0% (0/1) 0% (0/2) 0% (0/5) 0% (0/42)
Mindbox$onPushClicked$1$1 0% (0/1) 0% (0/1) 0% (0/15)
Mindbox$onPushClicked$2 0% (0/1) 0% (0/2) 0% (0/6) 0% (0/35)
Mindbox$onPushReceived$1 0% (0/1) 0% (0/2) 0% (0/4) 0% (0/29)
Mindbox$onPushReceived$1$1 0% (0/1) 0% (0/1) 0% (0/15)
Mindbox$sendTrackVisitEvent$1 0% (0/1) 0% (0/8) 0% (0/12) 0% (0/56)
Mindbox$sessionStorageManager$2 0% (0/1) 0% (0/1) 0% (0/2)
Mindbox$setPushServiceHandler$2 0% (0/1) 0% (0/6) 0% (0/17) 0% (0/130)
Mindbox$setPushServiceHandler$2$1$1 0% (0/1) 0% (0/1) 0% (0/3)
Mindbox$setPushServiceHandler$2$1$3$1 0% (0/1) 0% (0/5) 0% (0/52)
Mindbox$setPushServiceHandler$2$1$4 0% (0/1) 0% (0/2) 0% (0/2) 0% (0/18)
Mindbox$special$$inlined$CoroutineExceptionHandler$1 0% (0/2)
Mindbox$subscribePushTokens$getPushTokens$1 0% (0/1) 0% (0/5) 0% (0/19)
Mindbox$timeProvider$2 100% (1/1) 100% (1/1) 100% (2/2)
Mindbox$updateAppInfo$2 100% (1/1) 81.2% (13/16) 96.6% (28/29) 97.6% (165/169)
Mindbox$updateNotificationPermissionStatus$1 0% (0/1) 0% (0/2) 0% (0/17)
Mindbox$updateNotificationPermissionStatus$1$1 0% (0/1) 0% (0/2) 0% (0/27)
Mindbox$updatePushToken$2 0% (0/1) 0% (0/6) 0% (0/11) 0% (0/81)
Mindbox$updatePushToken$2$1 0% (0/1) 0% (0/1) 0% (0/20)
Mindbox$userVisitManager$2 0% (0/1) 0% (0/1) 0% (0/2)
Mindbox$validateOperation$1 0% (0/1) 0% (0/2) 0% (0/6) 0% (0/20)
Mindbox$WhenMappings
Total 25% (33/132) 12.9% (24/186) 20.9% (106/508) 22.3% (681/3058)


 @file:Suppress("DEPRECATION")
 
 package cloud.mindbox.mobile_sdk
 
 import android.app.Activity
 import android.app.Application
 import android.content.Context
 import android.content.Intent
 import androidx.annotation.DrawableRes
 import androidx.annotation.MainThread
 import androidx.annotation.VisibleForTesting
 import androidx.annotation.WorkerThread
 import androidx.work.WorkerFactory
 import cloud.mindbox.common.MindboxCommon
 import cloud.mindbox.mobile_sdk.di.MindboxDI
 import cloud.mindbox.mobile_sdk.di.mindboxInject
 import cloud.mindbox.mobile_sdk.inapp.data.managers.SessionStorageManager
 import cloud.mindbox.mobile_sdk.inapp.presentation.InAppCallback
 import cloud.mindbox.mobile_sdk.inapp.presentation.InAppMessageManager
 import cloud.mindbox.mobile_sdk.logger.*
 import cloud.mindbox.mobile_sdk.managers.*
 import cloud.mindbox.mobile_sdk.models.*
 import cloud.mindbox.mobile_sdk.models.operation.OperationBody
 import cloud.mindbox.mobile_sdk.models.operation.request.OperationBodyRequestBase
 import cloud.mindbox.mobile_sdk.models.operation.response.OperationResponse
 import cloud.mindbox.mobile_sdk.models.operation.response.OperationResponseBase
 import cloud.mindbox.mobile_sdk.pushes.*
 import cloud.mindbox.mobile_sdk.pushes.handler.MindboxMessageHandler
 import cloud.mindbox.mobile_sdk.pushes.handler.image.MindboxImageFailureHandler
 import cloud.mindbox.mobile_sdk.pushes.handler.image.MindboxImageLoader
 import cloud.mindbox.mobile_sdk.repository.MindboxPreferences
 import cloud.mindbox.mobile_sdk.services.BackgroundWorkManager
 import cloud.mindbox.mobile_sdk.utils.*
 import cloud.mindbox.mobile_sdk.utils.MindboxUtils.Stopwatch
 import com.jakewharton.threetenabp.AndroidThreeTen
 import kotlinx.coroutines.*
 import kotlinx.coroutines.Dispatchers.Default
 import kotlinx.coroutines.sync.Mutex
 import kotlinx.coroutines.sync.withLock
 import java.util.Date
 import java.util.TimeZone
 import java.util.UUID
 import java.util.concurrent.ConcurrentHashMap
 import java.util.concurrent.Executors
 import java.util.concurrent.TimeUnit
 import java.util.concurrent.atomic.AtomicBoolean
 
 @SuppressWarnings("deprecated")
 public object Mindbox : MindboxLog {
 
     /**
      * Factory for custom initialisation of WorkManager
      *
      * You don't need this if you are using default WorkManager initialisation
      *
      * If you disabled automatic initialisation, add this factory to your DelegatingWorkerFactory
      * in place, where you register your factories
      *
      * Example:
      *
      * override fun getWorkManagerConfiguration() = Configuration.Builder()
      *     .setWorkerFactory(
      *         DelegatingWorkerFactory().apply {
      *             // your factories
      *             addFactory(Mindbox.mindboxWorkerFactory) // Mindbox factory
      *         }
      *      )
      *     .build()
      */
     public val mindboxWorkerFactory: WorkerFactory by lazy { MindboxWorkerFactory }
 
     private const val OPERATION_NAME_REGEX = "^[A-Za-z0-9-\\.]{1,249}\$"
     private const val DELIVER_TOKEN_DELAY = 1L
     private const val INIT_PUSH_SERVICES_TIMEOUT = 5000L
 
     public val coroutineExceptionHandler: CoroutineExceptionHandler = CoroutineExceptionHandler { _, throwable ->
         MindboxLoggerImpl.e(Mindbox, "Mindbox caught unhandled error", throwable)
     }
     private val initScope = createMindboxScope()
     internal var mindboxScope = createMindboxScope()
         private set
 
     internal var eventScope = createMindboxScope(Dispatchers.IO)
         private set
 
     private val tokenCallbacks = ConcurrentHashMap<String, (String?) -> Unit>()
     private val deviceUuidCallbacks = ConcurrentHashMap<String, (String) -> Unit>()
 
     private val lifecycleManager: LifecycleManager? get() = LifecycleManager.instance
 
     private val userVisitManager: UserVisitManager by mindboxInject { userVisitManager }
     private val timeProvider by mindboxInject { timeProvider }
 
     internal var pushServiceHandlers: List<PushServiceHandler> = listOf()
 
     private var pushConverters: List<PushConverter> by SingleInitDelegate()
 
     private val inAppMessageManager: InAppMessageManager by mindboxInject { inAppMessageManager }
 
     private val getDeviceIdMutex = Mutex()
     private val inAppMutex = Mutex()
     private val mutexUpdateAppInfo: Mutex = Mutex()
     private val pushServiceMutex = Mutex()
 
     private val firstInitCall: AtomicBoolean = AtomicBoolean(true)
 
     private val migrationManager: MigrationManager by mindboxInject { migrationManager }
 
     private val sessionStorageManager: SessionStorageManager by mindboxInject { sessionStorageManager }
 
     /**
      * Allows you to specify additional components for message handling
      * when calling the [handleRemoteMessage] function.
      *
      * Standard image failure handling strategies:
      *  - applyDefaultStrategy (Used by default)
      *  - applyDefaultAndRetryStrategy
      *  - retryOrCancelStrategy
      *  - retryOrDefaultStrategy
      *  - cancellationStrategy
      * See [MindboxImageFailureHandler] for more information.
      *
      *
      * Example:
      *
      *  class App : Application {
      *
      *      override fun onCreate() {
      *          ...
      *          val defaultImage = ContextCompat.getDrawable(this, R.drawable.ic_placeholder)?.toBitmap()
      *          Mindbox.setMessageHandling(
      *              imageLoader = MindboxImageLoader.default(),
      *              imageFailureHandler = MindboxImageFailureHandler.applyDefaultAndRetryStrategy(
      *                  maxAttempts = 5,
      *                  delay = 3_000,
      *                  defaultImage = defaultImage,
      *              )
      *          )
      *          ...
      *      }
      *  }
      *
      * @see MindboxImageLoader
      * @see MindboxImageFailureHandler
      * @see handleRemoteMessage
      */
     public fun setMessageHandling(
         imageFailureHandler: MindboxImageFailureHandler = PushNotificationManager.messageHandler.imageFailureHandler,
         imageLoader: MindboxImageLoader = PushNotificationManager.messageHandler.imageLoader,
     ) {
         MindboxLoggerImpl.d(
             this, "setMessageHandling " +
                 "imageFailureHandler: ${imageFailureHandler.javaClass.simpleName}, " +
                 "imageLoader: ${imageLoader.javaClass.simpleName}"
         )
         PushNotificationManager.messageHandler = MindboxMessageHandler(
             imageFailureHandler = imageFailureHandler,
             imageLoader = imageLoader,
         )
     }
 
     /**
      * Subscribe to gets token from push service used by SDK
      *
      * @param subscription - invocation function with push token
      * @return String identifier of subscription
      * @see disposePushTokenSubscription
      */
     @Deprecated(
         message = "Use subscribePushTokens instead",
         level = DeprecationLevel.ERROR,
         replaceWith = ReplaceWith("subscribePushTokens"),
     )
     public fun subscribeFmsToken(subscription: (String?) -> Unit): String {
         logW("Called subscribeFmsToken")
         return subscribePushToken(subscription)
     }
 
     /**
      * Subscribe to gets token from push service used by SDK
      *
      * @param subscription - invocation function with push token
      * @return String identifier of subscription
      * @see disposePushTokenSubscription
      */
     @Deprecated(
         message = "Use subscribePushTokens instead",
         level = DeprecationLevel.WARNING,
         replaceWith = ReplaceWith("subscribePushTokens"),
     )
     public fun subscribePushToken(subscription: (String?) -> Unit): String {
         logW("Called subscribePushToken")
         val subscriptionId = "Subscription-${UUID.randomUUID()} " +
             "(USE THIS ONLY TO UNSUBSCRIBE FROM 'PushToken' " +
             "IN Mindbox.disposePushTokenSubscription(...))"
 
         if (SharedPreferencesManager.isInitialized() && !MindboxPreferences.isFirstInitialize) {
             subscription.invoke(
                 MindboxPreferences.pushTokens
                     .entries
                     .takeIf { it.isNotEmpty() }
                     ?.maxBy { it.value.updateDate }
                     ?.value
                     ?.token
             )
         } else {
             tokenCallbacks[subscriptionId] = subscription
         }
 
         return subscriptionId
     }
 
     /**
      * Subscribe to gets tokens from push services used by SDK
      *
      * @param subscription - invocation function with json object with push tokens
      * example: {"FCM":"token1","HMS":"token2"}
      * @return String identifier of subscription
      * @see disposePushTokenSubscription
      */
     public fun subscribePushTokens(subscription: (String?) -> Unit): String {
         logI("Called subscribePushTokens")
         val subscriptionId = "Subscription-${UUID.randomUUID()} " +
             "(USE THIS ONLY TO UNSUBSCRIBE FROM 'PushToken' " +
             "IN Mindbox.disposePushTokenSubscription(...))"
 
         val getPushTokens: (String?) -> Unit = {
             subscription(
                 MindboxPreferences
                     .pushTokens
                     .mapValues { it.value.token }
                     .toPreferences()
             )
         }
         if (SharedPreferencesManager.isInitialized() && !MindboxPreferences.isFirstInitialize) {
             getPushTokens.invoke(subscriptionId)
         } else {
             tokenCallbacks[subscriptionId] = getPushTokens
         }
 
         return subscriptionId
     }
 
     /**
      * Removes push token subscription if it is no longer necessary
      *
      * @param subscriptionId - identifier of the subscription to remove
      */
     @Deprecated(
         message = "Use disposePushTokenSubscription",
         level = DeprecationLevel.ERROR,
         replaceWith = ReplaceWith("disposePushTokenSubscription"),
     )
     public fun disposeFmsTokenSubscription(
         subscriptionId: String,
     ) {
         MindboxLoggerImpl.d(this, "disposeFmsTokenSubscription")
         disposePushTokenSubscription(subscriptionId)
     }
 
     /**
      * Removes push token subscription if it is no longer necessary
      *
      * @param subscriptionId - identifier of the subscription to remove
      */
     public fun disposePushTokenSubscription(subscriptionId: String) {
         MindboxLoggerImpl.d(this, "disposePushTokenSubscription")
         tokenCallbacks.remove(subscriptionId)
     }
 
     /**
      * Returns date of push token saving
      */
     @Deprecated(
         message = "Use getPushTokensSaveDate instead",
         level = DeprecationLevel.ERROR,
         replaceWith = ReplaceWith("getPushTokenSaveDate"),
     )
     public fun getFmsTokenSaveDate(): String {
         logW("Used deprecated getFmsTokenSaveDate")
 
         @Suppress("DEPRECATION_ERROR")
         return getPushTokenSaveDate()
     }
 
     /**
      * Returns date of push token saving
      */
     @Deprecated(
         message = "Use getPushTokensSaveDate instead",
         level = DeprecationLevel.ERROR,
         replaceWith = ReplaceWith("getPushTokenSaveDate"),
     )
     public fun getPushTokenSaveDate(): String = loggingRunCatching(defaultValue = "") {
         logW("Called getPushTokenSaveDate")
         MindboxPreferences.pushTokens
             .entries
             .maxOf { it.value.updateDate }
             .let { Date(it).toString() }
     }
 
     /**
      * Returns map of push tokens with providers and save dates
      */
     public fun getPushTokensSaveDate(): Map<String, Long> = loggingRunCatching(defaultValue = emptyMap()) {
         logI("Called getPushTokensSaveDate")
         MindboxPreferences.pushTokens.map { (provider, token) ->
             provider to token.updateDate
         }.toMap()
     }
 
     /**
      * Returns SDK version
      */
     public fun getSdkVersion(): String = LoggingExceptionHandler.runCatching(defaultValue = "") {
         MindboxLoggerImpl.d(this, "getSdkVersion")
         BuildConfig.VERSION_NAME
     }
 
     /**
      * Subscribe to gets deviceUUID used by SDK
      *
      * @param subscription - invocation function with deviceUUID
      * @return String identifier of subscription
      * @see disposeDeviceUuidSubscription
      */
     public fun subscribeDeviceUuid(subscription: (String) -> Unit): String {
         MindboxLoggerImpl.d(this, "subscribeDeviceUuid")
         val subscriptionId = "Subscription-${UUID.randomUUID()} " +
             "(USE THIS ONLY TO UNSUBSCRIBE FROM DeviceUuid " +
             "IN Mindbox.disposeDeviceUuidSubscription(...))"
 
         if (SharedPreferencesManager.isInitialized() && !MindboxPreferences.isFirstInitialize) {
             subscription.invoke(MindboxPreferences.deviceUuid)
         } else {
             deviceUuidCallbacks[subscriptionId] = subscription
         }
 
         return subscriptionId
     }
 
     /**
      * Removes deviceUuid subscription if it is no longer necessary
      *
      * @param subscriptionId - identifier of the subscription to remove
      */
     public fun disposeDeviceUuidSubscription(subscriptionId: String) {
         MindboxLoggerImpl.d(this, "disposeDeviceUuidSubscription")
         deviceUuidCallbacks.remove(subscriptionId)
     }
 
     /**
      * Updates push token for SDK
      * Call it from onNewToken in messaging service
      *
      * @param context used to initialize the main tools
      * @param token - token of push service
      */
     @Deprecated(
         message = "Use updatePushToken(context: Context, token: String, services: MindboxPushService)",
         level = DeprecationLevel.ERROR,
         replaceWith = ReplaceWith("this.updatePushToken(context, token, pushService)")
     )
     public fun updatePushToken(context: Context, token: String): Unit = LoggingExceptionHandler.runCatching {
         initComponents(context)
         mindboxLogW("Used deprecated updatePushToken. token: $token")
         if (token.trim().isNotEmpty()) {
             if (!MindboxPreferences.isFirstInitialize) {
                 mindboxScope.launch {
                     pushServiceHandlers.firstOrNull()?.let { handler ->
                         if (pushServiceHandlers.size == 1) {
                             updateAppInfo(context, PushToken(handler.notificationProvider, token))
                         } else {
                             updateAppInfo(context)
                         }
                     }
                 }
             } else {
                 mindboxLogI("updatePushToken. MindboxPreferences.isFirstInitialize == true. Skipping update.")
             }
         }
     }
 
     /**
      * Updates push token for SDK
      * Call it from onNewToken in messaging service
      *
      * @param context used to initialize the main tools
      * @param token - token of push service
      * @param pushService - the instance of [MindboxPushService], which handles push notifications.
      */
     public fun updatePushToken(context: Context, token: String, pushService: MindboxPushService): Unit =
         loggingRunCatching {
             initComponents(context)
             mindboxLogI("updatePushToken token: $token with provider $pushService")
 
             if (token.trim().isEmpty()) {
                 mindboxLogW("Token is empty! Skipping update token.")
                 return@loggingRunCatching
             }
 
             if (MindboxPreferences.isFirstInitialize) {
                 mindboxLogW("Mindbox init was never called. Skipping update token.")
                 return@loggingRunCatching
             }
 
             val notificationProvider = pushServiceHandlers.firstOrNull { handler ->
                 pushService.tag == handler.notificationProvider
             }
 
             if (notificationProvider == null) {
                 mindboxLogW("Unknown token provider ${pushService.tag}. Skipping token update.")
                 return@loggingRunCatching
             }
 
             mindboxScope.launch {
                 updateAppInfo(context, PushToken(pushService.tag, token))
             }
         }
 
     /**
      * This method is used to inform when the notification permission status changed to "allowed"
      * @param context current context is used
      **/
     public fun updateNotificationPermissionStatus(context: Context): Unit = LoggingExceptionHandler.runCatching {
         mindboxLogI("updateNotificationPermissionStatus was called")
         mindboxScope.launch {
             InitializeLock.await(InitializeLock.State.SAVE_MINDBOX_CONFIG)
             updateAppInfo(context)
         }
     }
 
     /**
      * Creates and deliveries event of "Push delivered". Recommended call this method from
      * background thread.
      *
      * Use this method only if you have custom push handling you don't use [Mindbox.handleRemoteMessage].
      * You must not call it otherwise.
      *
      * @param context used to initialize the main tools
      * @param uniqKey - unique identifier of push notification
      */
     public fun onPushReceived(context: Context, uniqKey: String): Unit = LoggingExceptionHandler.runCatching {
         initComponents(context)
         MindboxLoggerImpl.d(this, "onPushReceived. uniqKey: $uniqKey")
 
         if (!MindboxPreferences.isFirstInitialize) {
             mindboxScope.launch {
                 updateAppInfo(context)
             }
         }
     }
 
     /**
      * Creates and deliveries event of "Push clicked". Recommended call this method from background
      * thread.
      *
      * @param context used to initialize the main tools
      * @param uniqKey - unique identifier of push notification
      * @param buttonUniqKey - unique identifier of push notification button
      */
     public fun onPushClicked(
         context: Context,
         uniqKey: String,
         buttonUniqKey: String?,
     ): Unit = LoggingExceptionHandler.runCatching {
         initComponents(context)
         MindboxLoggerImpl.d(this, "onPushClicked. uniqKey: $uniqKey, buttonUniqKey: $buttonUniqKey")
         MindboxEventManager.pushClicked(context, TrackClickData(uniqKey, buttonUniqKey))
 
         if (!MindboxPreferences.isFirstInitialize) {
             mindboxScope.launch {
                 updateAppInfo(context)
             }
         }
     }
 
     /**
      * Creates and deliveries event of "Push clicked".
      * Recommended to be used with Mindbox SDK pushes with [handleRemoteMessage] method.
      * Intent should contain "uniq_push_key" and "uniq_push_button_key" (optionally) in order to work correctly
      * Recommended call this method from background thread.
      *
      * @param context used to initialize the main tools
      * @param intent - intent received in app component
      *
      * @return true if Mindbox SDK recognises push intent as Mindbox SDK push intent
      *         false if Mindbox SDK cannot find critical information in intent
      */
     public fun onPushClicked(
         context: Context,
         intent: Intent,
     ): Boolean = LoggingExceptionHandler.runCatching(defaultValue = false) {
         MindboxLoggerImpl.d(this, "onPushClicked with intent")
         intent.getMindboxUniqKeyFromPushIntent()?.let { uniqKey ->
             val pushButtonUniqKey = intent.getMindboxUniqPushButtonKeyFromPushIntent()
             onPushClicked(context, uniqKey, pushButtonUniqKey)
             true
         } ?: false
     }
 
     /**
      * Initializes the SDK for further work.
      *
      * This method must be called synchronously in onCreate on an application class
      *
      * If you must call it the other way, invoke [Mindbox.setPushServiceHandler] in [Application.onCreate] or else pushes won't be shown when application is inactive
      *
      * @param application used to initialize the main tools
      * @param configuration contains the data that is needed to connect to the Mindbox
      * @param pushServices list, containing [MindboxPushService]s, i.e.
      * ```
      *     listOf(MindboxFirebase, MindboxHuawei, MindboxRuStore)
      * ```
      */
     @MainThread
     public fun init(
         application: Application,
         configuration: MindboxConfiguration,
         pushServices: List<MindboxPushService>,
     ) {
         logI("Initialization with application started")
         initialize(application, configuration, pushServices)
     }
 
     /**
      * Initializes the SDK for further work.
      *
      * This method should be called in
      * [Activity.onCreate] and should be used if you're unable to call [Mindbox.init] in [Application.onCreate] on an application class
      *
      * If you use this method, invoke [Mindbox.setPushServiceHandler] in [Application.onCreate] or else pushes won't be shown when application is inactive
      *
      * @param activity used to initialize the main tools
      * @param configuration contains the data that is needed to connect to the Mindbox
      * @param pushServices list, containing [MindboxPushService]s, i.e.
      * ```
      *     listOf(MindboxFirebase, MindboxHuawei, MindboxRuStore)
      * ```
      */
     @MainThread
     public fun init(
         activity: Activity,
         configuration: MindboxConfiguration,
         pushServices: List<MindboxPushService>,
     ) {
         logI("Initialization with activity started")
         initialize(activity, configuration, pushServices)
     }
 
     private fun initialize(
         context: Context,
         configuration: MindboxConfiguration,
         pushServices: List<MindboxPushService>,
     ): 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()}, " +
                 "configuration: $configuration, pushServices: " +
                 pushServices.joinToString(", ") { it.javaClass.simpleName } +
                 ", SdkVersion:${getSdkVersion()}, CommonSdkVersion:${MindboxCommon.VERSION_NAME}",
         )
 
         if (!firstInitCall.get()) {
             InitializeLock.reset(InitializeLock.State.SAVE_MINDBOX_CONFIG)
         } else {
             userVisitManager.saveUserVisit()
         }
 
         launchInitJob(context, configuration, pushServices)
         setupLifecycleManager(context)
         attachLifecycleCallbacks()
     }
 
     private fun launchInitJob(
         context: Context,
         configuration: MindboxConfiguration,
         pushServices: List<MindboxPushService>,
     ) {
         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)
             }
 
             if (checkResult == ConfigUpdate.UPDATED) {
                 setPushServiceHandler(context, pushServices)
                 firstInitialization(context.applicationContext, validatedConfiguration)
             } else {
                 mindboxScope.launch {
                     setPushServiceHandler(context, pushServices)
                 }
                 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()
                             }
                         }
                     }
                 }
             }
     }
 
     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)
                     }
                 }
             }
 
             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()
                         }
                     }
                 }
             }
 
             override fun onActivityStopped(activity: Activity) {
                 inAppMessageManager.onStopCurrentActivity(activity)
             }
 
             override fun onTrackVisitReady(source: String?, requestUrl: String?) {
                 sessionStorageManager.hasSessionExpired()
                 eventScope.launch {
                     InitializeLock.await(InitializeLock.State.SAVE_MINDBOX_CONFIG)
                     sendTrackVisitEvent(
                         MindboxDI.appModule.appContext,
                         source,
                         requestUrl,
                     )
                 }
             }
         }
     }
 
     /**
      * Initializes the SDK for further work.
      *
      * We recommend calling it synchronously in onCreate on an application class
      *
      * If you must call it the other way, invoke [Mindbox.setPushServiceHandler] in [Application.onCreate] or else pushes won't be shown when application is inactive
      *
      * @param context used to initialize the main tools
      * @param configuration contains the data that is needed to connect to the Mindbox
      * @param pushServices list, containing [MindboxPushService]s, i.e.
      * ```
      *     listOf(MindboxFirebase, MindboxHuawei, MindboxRuStore)
      * ```
      * @Deprecated Use either [Mindbox.init] with application parameter or [Mindbox.init] with activity parameter
      */
     @Deprecated(
         "Use either Mindbox.init with application parameter or Mindbox.init with activity parameter"
     )
     public fun init(
         context: Context,
         configuration: MindboxConfiguration,
         pushServices: List<MindboxPushService>,
     ) {
         logW("Use either Mindbox.init with application parameter or Mindbox.init with activity parameter")
         initialize(context = context, configuration = configuration, pushServices = pushServices)
     }
 
     /**
      * Registers a callback for InApp messages.
      *
      * Call this method after [Mindbox.init]. The SDK holds a **strong reference** to
      * [inAppCallback], so the callback persists until explicitly replaced or removed via
      * [unregisterInAppCallback].
      *
      * Calling this method again replaces the previously registered callback.
      *
      * **Application-level callback (recommended):**
      * Register once in `Application.onCreate` with a callback that does not reference any
      * Activity. No cleanup needed.
      * ```kotlin
      * class MyApp : Application() {
      *     override fun onCreate() {
      *         super.onCreate()
      *         Mindbox.init(...)
      *         Mindbox.registerInAppCallback(MyGlobalInAppCallback())
      *     }
      * }
      * ```
      *
      * **Per-screen callback:**
      * If different screens require different callback behavior and the callback captures an
      * Activity reference, use `onResume`/`onPause` — **not** `onCreate`/`onDestroy`.
      * Android guarantees that `onPause` of the current Activity is called before `onResume`
      * of the next, so callbacks never overlap and the Activity reference is always cleared
      * before the Activity can be garbage-collected.
      * ```kotlin
      * override fun onResume() {
      *     super.onResume()
      *     Mindbox.registerInAppCallback(myScreenCallback)
      * }
      * override fun onPause() {
      *     super.onPause()
      *     Mindbox.unregisterInAppCallback()
      * }
      * ```
      *
      * @param inAppCallback the callback implementation to register
      **/
     public fun registerInAppCallback(inAppCallback: InAppCallback) {
         mindboxLogI("InApp callback registered: ${inAppCallback::class.simpleName}")
         inAppMessageManager.registerInAppCallback(inAppCallback)
     }
 
     /**
      * Unregisters the current InApp message callback and restores the default SDK behavior.
      *
      * The default behavior handles URL redirects, deep links, payload copying, and logging
      * automatically — the same actions performed when no custom callback is registered.
      *
      * **When to call:**
      * Only needed for per-screen callbacks registered in `onResume`. Call in the corresponding
      * `onPause` to release the Activity reference and restore default behavior while another
      * screen is in the foreground.
      *
      * Not needed if the callback was registered at the Application level and does not
      * reference any Activity.
      *
      * @see registerInAppCallback
      **/
     public fun unregisterInAppCallback() {
         mindboxLogI("InApp callback unregistered, default behavior restored")
         inAppMessageManager.unregisterInAppCallback()
     }
 
     /**
      * Method to initialise push services
      *
      * You must call this method in onCreate in your Application class if you call [Mindbox.init] not there
      *
      * @param context used to initialize the main tools
      * @param pushServices list, containing [MindboxPushService]s, i.e.
      * ```
      *     listOf(MindboxFirebase, MindboxHuawei, MindboxRuStore)
      * ```
      */
     @MainThread
     public fun initPushServices(
         context: Context,
         pushServices: List<MindboxPushService>,
     ) {
         verifyThreadExecution(methodName = "initPushServices")
         initComponents(context)
         pushConverters = selectPushServiceHandler(pushServices)
         mindboxScope.launch {
             InitializeLock.await(InitializeLock.State.MIGRATION)
             setPushServiceHandler(context, pushServices)
         }
     }
 
     private suspend fun setPushServiceHandler(
         context: Context,
         pushServices: List<MindboxPushService>,
     ): Unit = loggingRunCatchingSuspending {
         if (pushServices.isEmpty()) {
             mindboxLogW("initPushServices: Push services list is empty")
             return@loggingRunCatchingSuspending
         }
 
         if (pushServiceHandlers.isNotEmpty()) {
             mindboxLogI("initPushServices: Push services already initialized")
             return@loggingRunCatchingSuspending
         }
 
         pushServiceMutex.withLock {
             if (pushServiceHandlers.isNotEmpty()) {
                 mindboxLogI("initPushServices: Push services already initialized")
                 return@withLock
             }
 
             mindboxLogI("initPushServices: " + pushServices.joinToString { it.tag })
             Stopwatch.start(Stopwatch.INIT_PUSH_SERVICES)
 
             pushServiceHandlers = selectPushServiceHandler(pushServices)
                 .filter { it.isServiceAvailable(context) }
 
             pushServiceHandlers.map { handler ->
                 mindboxScope.async {
                     runCatching {
                         handler.initService(context)
                     }.getOrNull {
                         mindboxLogE("initPushServices: ${handler.notificationProvider} failed to initialize", it)
                     }
                 }
             }.awaitAllWithTimeout(INIT_PUSH_SERVICES_TIMEOUT)
 
             mindboxLogI("initPushServices completed in " + Stopwatch.stop(Stopwatch.INIT_PUSH_SERVICES))
             mindboxScope.launch {
                 if (!MindboxPreferences.isFirstInitialize) {
                     updateAppInfo(context)
                 }
             }
         }
     }
 
     private fun createMindboxScope(dispatcher: CoroutineDispatcher = Default) =
         CoroutineScope(
             dispatcher + SupervisorJob() + coroutineExceptionHandler
         )
 
     private fun selectPushServiceHandler(
         pushServices: List<MindboxPushService>,
     ): List<PushServiceHandler> =
         pushServices
             .map { it.getServiceHandler(MindboxLoggerImpl, LoggingExceptionHandler) }
 
     /**
      * Send track visit event after link or push was clicked for [Activity] with launchMode equals
      * "singleTop" or "singleTask" or if a client used the [Intent.FLAG_ACTIVITY_SINGLE_TOP] or
      * [Intent.FLAG_ACTIVITY_NEW_TASK]
      * flag when calling {@link #startActivity}.
      *
      * @param intent new intent for activity, which was received in [Activity.onNewIntent] method
      */
     public fun onNewIntent(intent: Intent?): Unit = LoggingExceptionHandler.runCatching {
         mindboxLogI("onNewIntent. intent: $intent")
         lifecycleManager?.onNewIntent(intent)
             ?: mindboxLogI("onNewIntent. LifecycleManager is not initialized. Skipping.")
     }
 
     /**
      * Specifies log level for Mindbox
      *
      * @param level - is used for showing Mindbox logs starts from [Level]. Default
      * is [Level.WARN]. [Level.NONE] turns off all logs.
      */
     public fun setLogLevel(level: Level) {
         MindboxLoggerImpl.level = level
     }
 
     /**
      * Creates and deliveries event with specified name and body. Recommended call this method from
      * background thread.
      *
      * @param context current context is used
      * @param operationSystemName the name of asynchronous operation
      * @param operationBody [T] which extends [OperationBody] and will be send as event json body of operation.
      */
     @Deprecated("Used Mindbox.executeAsyncOperation with OperationBodyRequestBase")
     public fun <T : OperationBody> executeAsyncOperation(
         context: Context,
         operationSystemName: String,
         operationBody: T,
     ) {
         initComponents(context)
         MindboxLoggerImpl.d(
             this,
             "executeAsyncOperation (deprecated). operationSystemName: $operationSystemName"
         )
         asyncOperation(context, operationSystemName, operationBody)
     }
 
     /**
      * Creates and deliveries event with specified name and body. Recommended call this method from
      * background thread.
      *
      * @param context current context is used
      * @param operationSystemName the name of asynchronous operation
      * @param operationBody [T] which extends [OperationBodyRequestBase] and will be send as event json body of operation.
      */
     public fun <T : OperationBodyRequestBase> executeAsyncOperation(
         context: Context,
         operationSystemName: String,
         operationBody: T,
     ) {
         initComponents(context)
         MindboxLoggerImpl.d(
             this,
             "executeAsyncOperation. operationSystemName: $operationSystemName"
         )
         asyncOperation(context, operationSystemName, operationBody)
     }
 
     /**
      * Creates and deliveries event with specified name and body. Recommended call this method from
      * background thread.
      *
      * @param context current context is used
      * @param operationSystemName the name of asynchronous operation
      * @param operationBodyJson event json body of operation.
      */
     public fun executeAsyncOperation(
         context: Context,
         operationSystemName: String,
         operationBodyJson: String,
     ) {
         initComponents(context)
         MindboxLoggerImpl.d(
             this, "executeAsyncOperation (with operationBodyJson). " +
                 "operationSystemName: $operationSystemName"
         )
         asyncOperation(context, operationSystemName, operationBodyJson)
     }
 
     /**
      * Creates and deliveries event synchronously with specified name and body.
      *
      * @param context current context is used
      * @param operationSystemName the name of synchronous operation
      * @param operationBody [T] which extends [OperationBodyRequestBase] and will be send as event json body of operation.
      * @param onSuccess Callback for response typed [OperationResponse] that will be invoked for success response to a given request.
      * @param onError Callback for response typed [MindboxError] and will be invoked for error response to a given request.
      */
     public fun <T : OperationBodyRequestBase> executeSyncOperation(
         context: Context,
         operationSystemName: String,
         operationBody: T,
         onSuccess: (OperationResponse) -> Unit,
         onError: (MindboxError) -> Unit,
     ): Unit = executeSyncOperation(
         context = context,
         operationSystemName = operationSystemName,
         operationBody = operationBody,
         classOfV = OperationResponse::class.java,
         onSuccess = onSuccess,
         onError = onError,
     )
 
     /**
      * Creates and deliveries event synchronously with specified name and body.
      *
      * @param context current context is used
      * @param operationSystemName the name of synchronous operation
      * @param operationBody [T] which extends [OperationBodyRequestBase] and will be send as event json body of operation.
      * @param classOfV Class type for response object.
      * @param onSuccess Callback for response typed [V] which extends [OperationResponseBase] that will be invoked for success response to a given request.
      * @param onError Callback for response typed [MindboxError] and will be invoked for error response to a given request.
      */
     public fun <T : OperationBodyRequestBase, V : OperationResponseBase> executeSyncOperation(
         context: Context,
         operationSystemName: String,
         operationBody: T,
         classOfV: Class<V>,
         onSuccess: (V) -> Unit,
         onError: (MindboxError) -> Unit,
     ) {
         initComponents(context)
         MindboxLoggerImpl.d(
             this, "executeSyncOperation. " +
                 "operationSystemName: $operationSystemName, classOfV: ${classOfV.simpleName}"
         )
         if (validateOperation(operationSystemName)) {
             mindboxScope.launch {
                 InitializeLock.await(InitializeLock.State.APP_STARTED)
 
                 MindboxEventManager.syncOperation(
                     name = operationSystemName,
                     body = operationBody,
                     classOfV = classOfV,
                     onSuccess = onSuccess,
                     onError = onError,
                 )
             }
         }
     }
 
     /**
      * Creates and deliveries event synchronously with specified name and body.
      *
      * @param context current context is used
      * @param operationSystemName the name of synchronous operation
      * @param operationBodyJson event json body of operation.
      * @param onSuccess Callback that will be invoked for success response to a given request.
      * @param onError Callback for response typed [MindboxError] and will be invoked for error response to a given request.
      */
     public fun executeSyncOperation(
         context: Context,
         operationSystemName: String,
         operationBodyJson: String,
         onSuccess: (String) -> Unit,
         onError: (MindboxError) -> Unit,
     ) {
         initComponents(context)
         MindboxLoggerImpl.d(
             this, "executeSyncOperation (with operationBodyJson). " +
                 "operationSystemName: $operationSystemName, operationBodyJson: $operationBodyJson"
         )
         if (validateOperation(operationSystemName)) {
             mindboxScope.launch {
                 InitializeLock.await(InitializeLock.State.APP_STARTED)
                 MindboxEventManager.syncOperation(
                     name = operationSystemName,
                     bodyJson = operationBodyJson,
                     onSuccess = onSuccess,
                     onError = onError,
                 )
             }
         }
     }
 
     /**
      * Handles only Mindbox notification message from [HmsMessageService], [FirebaseMessageService], [RuStoreMessagingService].
      *
      * @param context context used for Mindbox initializing and push notification showing
      * @param message the [MindboxRemoteMessage] received from Firebase, HMS, RuStore
      * @param channelId the id of channel for Mindbox pushes
      * @param channelName the name of channel for Mindbox pushes
      * @param pushSmallIcon icon for push notification as drawable resource
      * @param channelDescription the description of channel for Mindbox pushes. Default is null
      * @param activities map (url mask) -> (Activity class). When clicked on push or button with url, corresponding activity will be opened
      *        Currently supports '*' character - indicator of zero or more numerical, alphabetic and punctuation characters
      *        e.g. mask "https://sample.com/" will match only "https://sample.com/" link
      *        whereas mask "https://sample.com/\u002A" will match
      *        "https://sample.com/", "https://sample.com/foo", "https://sample.com/foo/bar", "https://sample.com/foo?bar=baz" and other masks
      * @param defaultActivity default activity to be opened if url was not found in [activities]
      *
      * @return true if notification is Mindbox push and it's successfully handled, false otherwise.
      */
     @WorkerThread
     public fun handleRemoteMessage(
         context: Context,
         message: Any?,
         channelId: String,
         channelName: String,
         @DrawableRes pushSmallIcon: Int,
         defaultActivity: Class<out Activity>,
         channelDescription: String? = null,
         activities: Map<String, Class<out Activity>>? = null,
     ): Boolean = loggingRunCatching(defaultValue = false) {
         verifyThreadExecution(methodName = "handleRemoteMessage", shouldBeMainThread = false)
         mindboxLogI(
             "handleRemoteMessage. channelId: $channelId, " +
                 "channelName: $channelName, channelDescription: $channelDescription, " +
                 "defaultActivity: ${defaultActivity.simpleName}, " +
                 "activities: ${
                     activities?.map {
                         "${it.key}: ${it.value.simpleName}"
                     }?.joinToString(", ")
                 }"
         )
         if (message == null) {
             logI("Cannot handle null message")
             return@loggingRunCatching false
         }
         if (pushConverters.isEmpty()) {
             logW("No push converters found")
         }
         val convertedMessage = when (message) {
             is MindboxRemoteMessage -> message
             else -> pushConverters.firstNotNullOfOrNull { handler ->
                 handler.convertToRemoteMessage(message)
             }
         }
         if (convertedMessage == null) {
             logW("Cannot convert message: $message")
             return@loggingRunCatching false
         } else {
             logI("Try to handle converted message: $convertedMessage")
         }
 
         runBlocking(mindboxScope.coroutineContext) {
             PushNotificationManager.handleRemoteMessage(
                 context = context,
                 remoteMessage = convertedMessage,
                 channelId = channelId,
                 channelName = channelName,
                 pushSmallIcon = pushSmallIcon,
                 channelDescription = channelDescription,
                 activities = activities,
                 defaultActivity = defaultActivity,
             )
         }
     }
 
     /**
      * Retrieves url from intent generated by notification manager
      *
      * @param intent an intent sent by SDK and received in activity
      * @return url associated with the push intent or null if there is none
      */
     public fun getUrlFromPushIntent(
         intent: Intent?,
     ): String? {
         MindboxLoggerImpl.d(this, "getUrlFromPushIntent. intent: $intent")
         return intent?.let(PushNotificationManager::getUrlFromPushIntent)
     }
 
     /**
      * Retrieves payload from intent generated by notification manager
      *
      * @param intent an intent sent by SDK and received in activity
      * @return payload delivered in push or null if there is none
      */
     public fun getPayloadFromPushIntent(
         intent: Intent?,
     ): String? {
         MindboxLoggerImpl.d(this, "getPayloadFromPushIntent. intent: $intent")
         return intent?.let(PushNotificationManager::getPayloadFromPushIntent)
     }
 
     /**
      * Writes a log message to the Mindbox logging system with the specified log level.
      * This method should be used only after Mindbox has been initialized by calling `Mindbox.init`.
      * Otherwise, the logs will not be recorded.
      * This method takes a log message and a log level, and routes the message to the appropriate
      * @param message The message to be logged.
      * @param logLevel The severity level of the log message. See [Level].
      */
     public fun writeLog(message: String, logLevel: Level) {
         when (logLevel) {
             Level.VERBOSE -> mindboxLogD(message = message)
             Level.DEBUG -> mindboxLogD(message = message)
             Level.INFO -> mindboxLogI(message = message)
             Level.WARN -> mindboxLogW(message = message)
             Level.ERROR -> mindboxLogE(message = message)
             Level.NONE -> mindboxLogD(message = message)
         }
     }
 
     private fun deliverToken(tokens: PushTokenMap) {
         Executors.newSingleThreadScheduledExecutor().schedule({
             tokenCallbacks.keys.asIterable().forEach { key ->
                 tokenCallbacks[key]?.invoke(tokens.entries.firstOrNull()?.value)
                 tokenCallbacks.remove(key)
             }
         }, DELIVER_TOKEN_DELAY, TimeUnit.SECONDS)
     }
 
     internal fun initComponents(context: Context) {
         MindboxDI.init(context.applicationContext)
         AndroidThreeTen.init(context)
         SharedPreferencesManager.with(context)
         DbManager.init(context)
 
         mindboxScope.launch {
             migrationManager.migrateAll()
         }
     }
 
     private fun <T> asyncOperation(
         context: Context,
         operationSystemName: String,
         operationBody: T,
     ) = LoggingExceptionHandler.runCatching {
         asyncOperation(
             context,
             operationSystemName,
             MindboxEventManager.operationBodyJson(operationBody),
         )
     }
 
     private fun asyncOperation(
         context: Context,
         operationSystemName: String,
         operationBodyJson: String,
     ) {
         MindboxLoggerImpl.d(this, "asyncOperation. operationBodyJson: $operationBodyJson")
         if (validateOperation(operationSystemName)) {
             initScope.launch {
                 InitializeLock.await(InitializeLock.State.APP_STARTED)
                 MindboxEventManager.asyncOperation(context, operationSystemName, operationBodyJson)
             }
         }
     }
 
     private fun validateOperation(
         operationSystemName: String,
     ) = LoggingExceptionHandler.runCatching(defaultValue = false) {
         if (operationSystemName.matches(OPERATION_NAME_REGEX.toRegex())) {
             return@runCatching true
         } else {
             MindboxLoggerImpl.w(
                 this,
                 "Operation name is incorrect. It should contain only latin letters, number, '-' or '.' and length from 1 to 250.",
             )
             return@runCatching false
         }
     }
 
     private suspend fun getDeviceId(
         context: Context,
     ): String {
         getDeviceIdMutex.withLock {
             return MindboxPreferences.deviceUuid.ifEmpty {
                 val adidResult = mindboxScope.async {
                     pushServiceHandlers.firstNotNullOfOrNull { handler ->
                         handler.getAdsIdentification(context)
                     }
                 }.await() ?: generateRandomUuid()
 
                 MindboxPreferences.deviceUuid = adidResult
                 adidResult
             }
         }
     }
 
     @VisibleForTesting(otherwise = VisibleForTesting.PRIVATE)
     internal suspend fun firstInitialization(
         context: Context,
         configuration: MindboxConfiguration,
     ) = loggingRunCatchingSuspending {
         val pushTokens: PushTokenMap = getPushTokens(context, emptyMap())
 
         val isNotificationEnabled = PushNotificationManager.isNotificationsEnabled(context)
         val deviceUuid = getDeviceId(context)
         val instanceId = generateRandomUuid()
 
         logI(
             "First SDK initialization with deviceUuid: $deviceUuid, " +
                 "pushTokens: $pushTokens, " +
                 "isNotificationEnabled: $isNotificationEnabled"
         )
 
         val timezone = TimeZone.getDefault().id.takeIf { configuration.shouldCreateCustomer }
         val initData = InitData(
             installationId = configuration.previousInstallationId,
             externalDeviceUUID = configuration.previousDeviceUUID,
             isNotificationsEnabled = isNotificationEnabled,
             subscribe = configuration.subscribeCustomerIfCreated,
             instanceId = instanceId,
             ianaTimeZone = timezone,
             tokens = pushTokens.toTokenData(),
         )
 
         MindboxPreferences.pushTokens = pushTokens.mapValues {
             PrefPushToken(it.value, Date().time)
         }
         MindboxPreferences.isNotificationEnabled = isNotificationEnabled
         MindboxPreferences.instanceId = instanceId
 
         if (MindboxPreferences.firstInitializationTime == null) {
             MindboxPreferences.firstInitializationTime = timeProvider.currentTimestamp()
                 .convertToIso8601String()
         }
 
         MindboxEventManager.appInstalled(context, initData, configuration.shouldCreateCustomer)
 
         deliverDeviceUuid(deviceUuid)
         deliverToken(pushTokens)
     }
 
     internal suspend fun updateAppInfo(
         context: Context,
         pushToken: PushToken? = null,
     ): Unit = loggingRunCatchingSuspending {
         mutexUpdateAppInfo.withLock {
             val prefsPushTokens = MindboxPreferences.pushTokens
             val savedPushTokens = prefsPushTokens.mapValues { it.value.token }
             val savedIsNotificationEnabled = MindboxPreferences.isNotificationEnabled
 
             val pushTokens: PushTokenMap =
                 if (pushToken != null && pushToken.token == savedPushTokens[pushToken.provider]) {
                     savedPushTokens
                 } else {
                     savedPushTokens + getPushTokens(context, savedPushTokens)
                 }
 
             val isNotificationEnabled = PushNotificationManager.isNotificationsEnabled(context)
 
             if (pushTokens == savedPushTokens && isNotificationEnabled == savedIsNotificationEnabled) {
                 return@loggingRunCatchingSuspending
             }
 
             mindboxLogI(
                 "updateAppInfo. pushToken: $pushTokens, isNotificationEnabled: $isNotificationEnabled, " +
                     "old isNotificationEnabled: $savedIsNotificationEnabled"
             )
             val initData = UpdateData(
                 isNotificationsEnabled = isNotificationEnabled,
                 instanceId = MindboxPreferences.instanceId,
                 version = MindboxPreferences.infoUpdatedVersion,
                 tokens = pushTokens.toTokenData(),
             )
 
             MindboxEventManager.appInfoUpdate(context, initData)
 
             if (isNotificationEnabled != savedIsNotificationEnabled) {
                 MindboxPreferences.isNotificationEnabled = isNotificationEnabled
             }
             if (pushTokens != savedPushTokens) {
                 MindboxPreferences.pushTokens = pushTokens.mapValues { (provider, token) ->
                     val prefsToken = prefsPushTokens[provider]
                     PrefPushToken(
                         token,
                         if (token != prefsToken?.token) Date().time else prefsToken.updateDate
                     )
                 }
             }
         }
     }
 
     private fun checkConfig(
         newConfiguration: MindboxConfiguration,
     ): ConfigUpdate = LoggingExceptionHandler.runCatching(ConfigUpdate.UPDATED) {
         MindboxLoggerImpl.d(
             this, "checkConfig. " +
                 "isFirstInitialize: ${MindboxPreferences.isFirstInitialize}"
         )
         if (MindboxPreferences.isFirstInitialize) {
             ConfigUpdate.UPDATED
         } else {
             DbManager.getConfigurations()?.let { currentConfiguration ->
                 val isUrlChanged = newConfiguration.domain != currentConfiguration.domain
                 val isEndpointChanged =
                     newConfiguration.endpointId != currentConfiguration.endpointId
                 val isShouldCreateCustomerChanged =
                     newConfiguration.shouldCreateCustomer != currentConfiguration.shouldCreateCustomer
 
                 MindboxLoggerImpl.d(
                     this, "checkConfig. isUrlChanged: $isUrlChanged, " +
                         "isEndpointChanged: $isEndpointChanged, " +
                         "isShouldCreateCustomerChanged: $isShouldCreateCustomerChanged"
                 )
                 when {
                     isUrlChanged || isEndpointChanged -> ConfigUpdate.UPDATED
                     !isShouldCreateCustomerChanged -> ConfigUpdate.NOT_UPDATED
                     currentConfiguration.shouldCreateCustomer &&
                         !newConfiguration.shouldCreateCustomer -> ConfigUpdate.UPDATED_SCC
 
                     else -> ConfigUpdate.UPDATED
                 }
             } ?: ConfigUpdate.UPDATED
         }
     }
 
     private fun softReinitialization(
         context: Context,
     ) {
         mindboxScope.cancel()
         DbManager.removeAllEventsFromQueue()
         BackgroundWorkManager.cancelAllWork(context)
         MindboxPreferences.resetAppInfoUpdated()
         mindboxScope = createMindboxScope()
     }
 
     private fun sendTrackVisitEvent(
         context: Context,
         @TrackVisitSource source: String? = null,
         requestUrl: String? = null,
     ) = LoggingExceptionHandler.runCatching {
         DbManager.getConfigurations()?.endpointId?.let { endpointId ->
             val applicationContext = context.applicationContext
             val trackVisitData = TrackVisitData(
                 ianaTimeZone = TimeZone.getDefault().id,
                 endpointId = endpointId,
                 source = source,
                 requestUrl = requestUrl,
                 sdkVersionNumeric = Constants.SDK_VERSION_NUMERIC
             )
             if (source != null || requestUrl != null) {
                 sessionStorageManager.lastTrackVisitData = trackVisitData
             }
             MindboxEventManager.appStarted(applicationContext, trackVisitData)
         }
     }
 
     private fun deliverDeviceUuid(deviceUuid: String) {
         Executors.newSingleThreadScheduledExecutor().schedule({
             deviceUuidCallbacks.keys.asIterable().forEach { key ->
                 deviceUuidCallbacks[key]?.invoke(deviceUuid)
                 deviceUuidCallbacks.remove(key)
             }
         }, 1, TimeUnit.SECONDS)
     }
 
     private fun validateConfiguration(configuration: MindboxConfiguration): MindboxConfiguration {
         val validationErrors = SdkValidation.validateConfiguration(
             domain = configuration.domain,
             endpointId = configuration.endpointId,
             previousDeviceUUID = configuration.previousDeviceUUID,
             previousInstallationId = configuration.previousInstallationId,
             operationsDomain = configuration.operationsDomain,
         )
 
         return if (validationErrors.isEmpty()) {
             configuration
         } else {
             if (validationErrors.any(SdkValidation.Error::critical)) {
                 throw InitializeMindboxException(validationErrors.toString())
             }
             MindboxLoggerImpl.e(this, "Invalid configuration parameters found: $validationErrors")
             val isDeviceIdError = validationErrors.contains(SdkValidation.Error.INVALID_DEVICE_ID)
             val isInstallationIdError = validationErrors.contains(SdkValidation.Error.INVALID_INSTALLATION_ID)
             val isOperationsDomainError = validationErrors.contains(SdkValidation.Error.INVALID_OPERATIONS_DOMAIN)
 
             configuration.copy(
                 previousDeviceUUID = if (isDeviceIdError) "" else configuration.previousDeviceUUID,
                 previousInstallationId = if (isInstallationIdError) "" else configuration.previousInstallationId,
                 operationsDomain = if (isOperationsDomainError) null else configuration.operationsDomain,
             )
         }
     }
 
     internal fun generateRandomUuid(): String = UUID.randomUUID().toString()
 }