Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
3 changes: 3 additions & 0 deletions sdk/build.gradle
Original file line number Diff line number Diff line change
Expand Up @@ -142,4 +142,7 @@ dependencies {
// dex
implementation "androidx.multidex:multidex:2.0.1"

// Handle app lifecycle
implementation "androidx.lifecycle:lifecycle-process:2.3.1"

}
92 changes: 77 additions & 15 deletions sdk/src/main/java/cloud/mindbox/mobile_sdk/Mindbox.kt
Original file line number Diff line number Diff line change
@@ -1,7 +1,11 @@
package cloud.mindbox.mobile_sdk

import android.app.Activity
import android.app.Application
import android.content.Context
import android.content.Intent
import androidx.lifecycle.Lifecycle.State.RESUMED
import androidx.lifecycle.ProcessLifecycleOwner
import androidx.annotation.DrawableRes
import cloud.mindbox.mobile_sdk.logger.Level
import cloud.mindbox.mobile_sdk.logger.MindboxLogger
Expand All @@ -20,12 +24,18 @@ import java.util.concurrent.TimeUnit

object Mindbox {

/**
* Used for determination app open from push
*/
const val IS_OPENED_FROM_PUSH_BUNDLE_KEY = "isOpenedFromPush"

private const val OPERATION_NAME_REGEX = "^[A-Za-z0-9-\\.]{1,249}\$"

private val mindboxJob = Job()
private val mindboxScope = CoroutineScope(Default + mindboxJob)
private val deviceUuidCallbacks = ConcurrentHashMap<String, (String) -> Unit>()
private val fmsTokenCallbacks = ConcurrentHashMap<String, (String?) -> Unit>()
private lateinit var lifecycleManager: LifecycleManager

/**
* Subscribe to gets token of Firebase Messaging Service used by SDK
Expand Down Expand Up @@ -58,9 +68,9 @@ object Mindbox {
/**
* Returns date of FMS token saving
*/
fun getFmsTokenSaveDate(): String =
runCatching { return MindboxPreferences.firebaseTokenSaveDate }
.returnOnException { "" }
fun getFmsTokenSaveDate(): String = runCatching {
return MindboxPreferences.firebaseTokenSaveDate
}.returnOnException { "" }

/**
* Returns SDK version
Expand Down Expand Up @@ -189,24 +199,67 @@ object Mindbox {
mindboxScope.launch {
if (MindboxPreferences.isFirstInitialize) {
firstInitialization(context, configuration)
val isTrackVisitNotSent = Mindbox::lifecycleManager.isInitialized
&& !lifecycleManager.isTrackVisitSent()
if (isTrackVisitNotSent) {
sendTrackVisitEvent(context, DIRECT)
}
} else {
updateAppInfo(context)
MindboxEventManager.sendEventsIfExist(context)
}
sendTrackVisitEvent(context, configuration.endpointId)
}

// Handle back app in foreground
val lifecycleManager = LifecycleManager {
runBlocking(Dispatchers.IO) {
sendTrackVisitEvent(context, configuration.endpointId)
// 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) {
MindboxLogger.e(
this@Mindbox,
"Incorrect context type for calling init in this place"
)
}

lifecycleManager = LifecycleManager(
currentActivityName = activity?.javaClass?.name,
currentIntent = activity?.intent,
isAppInBackground = !isApplicationResumed,
onTrackVisitReady = { source, requestUrl ->
runBlocking(Dispatchers.IO) {
sendTrackVisitEvent(context, source, requestUrl)
}
}
)
} else {
unregisterActivityLifecycleCallbacks(lifecycleManager)
applicationLifecycle.removeObserver(lifecycleManager)
lifecycleManager.wasReinitialized()
}
(context.applicationContext as? Application)
?.registerActivityLifecycleCallbacks(lifecycleManager)

registerActivityLifecycleCallbacks(lifecycleManager)
applicationLifecycle.addObserver(lifecycleManager)
}
}.returnOnException { }
}

/**
* 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
*/
fun onNewIntent(intent: Intent?) = runCatching {
if (Mindbox::lifecycleManager.isInitialized) {
lifecycleManager.onNewIntent(intent)
}
}.logOnException()

/**
* Specifies log level for Mindbox
*
Expand Down Expand Up @@ -367,14 +420,22 @@ object Mindbox {
}.logOnException()
}

private fun sendTrackVisitEvent(context: Context, endpointId: String) {
private fun sendTrackVisitEvent(
context: Context,
@TrackVisitSource source: String? = null,
requestUrl: String? = null
) = runCatching {
val applicationContext = context.applicationContext
val endpointId = DbManager.getConfigurations()?.endpointId ?: return
val trackVisitData = TrackVisitData(
ianaTimeZone = TimeZone.getDefault().id,
endpointId = endpointId
endpointId = endpointId,
source = source,
requestUrl = requestUrl
)

MindboxEventManager.appStarted(context, trackVisitData)
}
MindboxEventManager.appStarted(applicationContext, trackVisitData)
}.logOnException()

private fun deliverDeviceUuid(deviceUuid: String) {
Executors.newSingleThreadScheduledExecutor().schedule({
Expand All @@ -393,4 +454,5 @@ object Mindbox {
}
}, 1, TimeUnit.SECONDS)
}
}

}
143 changes: 133 additions & 10 deletions sdk/src/main/java/cloud/mindbox/mobile_sdk/managers/LifecycleManager.kt
Original file line number Diff line number Diff line change
Expand Up @@ -2,24 +2,66 @@ package cloud.mindbox.mobile_sdk.managers

import android.app.Activity
import android.app.Application
import android.content.Intent
import android.os.Bundle
import androidx.lifecycle.Lifecycle
import androidx.lifecycle.LifecycleObserver
import androidx.lifecycle.OnLifecycleEvent
import cloud.mindbox.mobile_sdk.Mindbox.IS_OPENED_FROM_PUSH_BUNDLE_KEY
import cloud.mindbox.mobile_sdk.logOnException
import cloud.mindbox.mobile_sdk.logger.MindboxLogger
import cloud.mindbox.mobile_sdk.models.DIRECT
import cloud.mindbox.mobile_sdk.models.LINK
import cloud.mindbox.mobile_sdk.models.PUSH
import cloud.mindbox.mobile_sdk.returnOnException
import java.util.*
import kotlin.concurrent.timer

internal class LifecycleManager(
private val onAppStarted: () -> Unit
) : Application.ActivityLifecycleCallbacks {
private var currentActivityName: String?,
private var currentIntent: Intent?,
private var onTrackVisitReady: (source: String?, requestUrl: String?) -> Unit,
private var isAppInBackground: Boolean
) : Application.ActivityLifecycleCallbacks, LifecycleObserver {

private var currentActivity: Activity? = null
companion object {

private const val SCHEMA_HTTP = "http"
private const val SCHEMA_HTTPS = "https"

private const val TIMER_PERIOD = 1200000L
private const val MAX_INTENT_HASHES_SIZE = 50

}

private var isIntentChanged = true
private var timer: Timer? = null
private val intentHashes = mutableListOf<Int>()

private var skipSendingTrackVisit = false

override fun onActivityCreated(activity: Activity, p1: Bundle?) {

}

override fun onActivityStarted(activity: Activity) {
if (currentActivity?.javaClass?.name == activity.javaClass.name) {
onAppStarted()
} else {
currentActivity = activity
}
runCatching {
val areActivitiesEqual = currentActivityName == activity.javaClass.name
val intent = activity.intent
isIntentChanged = if (currentIntent != intent) {
updateActivityParameters(activity)
intent?.hashCode()?.let(::updateHashesList) ?: true
} else {
false
}

if (isAppInBackground || !isIntentChanged) {
isAppInBackground = false
return
}

sendTrackVisit(activity.intent, areActivitiesEqual)
}.logOnException()
}

override fun onActivityResumed(activity: Activity) {
Expand All @@ -31,8 +73,8 @@ internal class LifecycleManager(
}

override fun onActivityStopped(activity: Activity) {
if (currentActivity == null) {
currentActivity = activity
if (currentIntent == null || currentActivityName == null) {
updateActivityParameters(activity)
}
}

Expand All @@ -44,4 +86,85 @@ internal class LifecycleManager(

}

fun isTrackVisitSent(): Boolean {
currentIntent?.let(::sendTrackVisit)
return currentIntent != null
}

fun wasReinitialized() {
skipSendingTrackVisit = true
}

fun onNewIntent(newIntent: Intent?) = newIntent?.let { intent ->
if (intent.data != null || intent.extras?.get(IS_OPENED_FROM_PUSH_BUNDLE_KEY) == true) {
isIntentChanged = updateHashesList(intent.hashCode())
sendTrackVisit(intent)
skipSendingTrackVisit = isAppInBackground
}
}

@OnLifecycleEvent(Lifecycle.Event.ON_STOP)
private fun onAppMovedToBackground() {
isAppInBackground = true
cancelKeepAliveTimer()
}

@OnLifecycleEvent(Lifecycle.Event.ON_START)
private fun onAppMovedToForeground() = if (!skipSendingTrackVisit) {
currentIntent?.let(::sendTrackVisit)
} else {
skipSendingTrackVisit = false
}

private fun updateActivityParameters(activity: Activity) = runCatching {
currentActivityName = activity.javaClass.name
currentIntent = activity.intent
}.logOnException()

private fun sendTrackVisit(intent: Intent, areActivitiesEqual: Boolean = true) = runCatching {
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()

MindboxLogger.d(this, "Track visit event with source $source and url $requestUrl")
}
}.logOnException()

private fun source(intent: Intent?) = runCatching {
when {
intent?.scheme == SCHEMA_HTTP || intent?.scheme == SCHEMA_HTTPS -> LINK
intent?.extras?.getBoolean(IS_OPENED_FROM_PUSH_BUNDLE_KEY) == true -> PUSH
else -> DIRECT
}
}.returnOnException { null }

private fun updateHashesList(code: Int) = runCatching {
if (!intentHashes.contains(code)) {
if (intentHashes.size >= MAX_INTENT_HASHES_SIZE) {
intentHashes.removeAt(0)
}
intentHashes.add(code)
true
} else {
false
}
}.returnOnException { true }

private fun startKeepAliveTimer() = runCatching {
cancelKeepAliveTimer()
timer = timer(
initialDelay = TIMER_PERIOD,
period = TIMER_PERIOD,
action = { onTrackVisitReady.invoke(null, null) }
)
}.logOnException()

private fun cancelKeepAliveTimer() = runCatching {
timer?.cancel()
timer = null
}.logOnException()

}
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
package cloud.mindbox.mobile_sdk.managers

import android.app.Notification.DEFAULT_ALL
import android.app.Notification.VISIBILITY_PRIVATE
import android.app.NotificationChannel
import android.app.NotificationManager
import android.app.PendingIntent
Expand Down Expand Up @@ -59,6 +60,7 @@ internal object PushNotificationManager {
.setPriority(NotificationCompat.PRIORITY_HIGH)
.setDefaults(DEFAULT_ALL)
.setAutoCancel(true)
.setVisibility(NotificationCompat.VISIBILITY_PRIVATE)
.handlePushClick(context, notificationId, uniqueKey)
.handleActions(context, notificationId, uniqueKey, pushActions)
.handleImageByUrl(data[DATA_IMAGE_URL], title, description)
Expand All @@ -82,6 +84,7 @@ internal object PushNotificationManager {
val importance = NotificationManager.IMPORTANCE_HIGH
val channel = NotificationChannel(channelId, channelName, importance).apply {
channelDescription.let { description = it }
lockscreenVisibility = VISIBILITY_PRIVATE
}

notificationManager.createNotificationChannel(channel)
Expand Down
Loading