Skip to content

Commit

Permalink
fix: anonymous analytics manager initialisation [WPB-10063] (#3176)
Browse files Browse the repository at this point in the history
  • Loading branch information
saleniuk authored Jul 9, 2024
1 parent 7e91bc8 commit 25a5eb4
Show file tree
Hide file tree
Showing 9 changed files with 167 additions and 69 deletions.
78 changes: 58 additions & 20 deletions app/src/main/kotlin/com/wire/android/WireApplication.kt
Original file line number Diff line number Diff line change
Expand Up @@ -18,8 +18,10 @@

package com.wire.android

import android.app.Activity
import android.content.ComponentCallbacks2
import android.os.Build
import android.os.Bundle
import android.os.StrictMode
import androidx.lifecycle.ProcessLifecycleOwner
import androidx.work.Configuration
Expand All @@ -28,10 +30,12 @@ import com.wire.android.datastore.GlobalDataStore
import com.wire.android.datastore.UserDataStoreProvider
import com.wire.android.di.ApplicationScope
import com.wire.android.di.KaliumCoreLogic
import com.wire.android.util.CurrentScreenManager
import com.wire.android.feature.analytics.AnonymousAnalyticsManagerImpl
import com.wire.android.feature.analytics.AnonymousAnalyticsRecorderImpl
import com.wire.android.feature.analytics.globalAnalyticsManager
import com.wire.android.feature.analytics.model.AnalyticsEvent
import com.wire.android.feature.analytics.model.AnalyticsSettings
import com.wire.android.util.CurrentScreenManager
import com.wire.android.util.DataDogLogger
import com.wire.android.util.LogFileWriter
import com.wire.android.util.getGitBuildId
Expand All @@ -46,8 +50,11 @@ import dagger.Lazy
import dagger.hilt.android.HiltAndroidApp
import kotlinx.coroutines.CoroutineScope
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.flow.collectLatest
import kotlinx.coroutines.flow.distinctUntilChanged
import kotlinx.coroutines.flow.filter
import kotlinx.coroutines.flow.first
import kotlinx.coroutines.flow.flatMapLatest
import kotlinx.coroutines.flow.flowOf
import kotlinx.coroutines.launch
import kotlinx.coroutines.withContext
import javax.inject.Inject
Expand Down Expand Up @@ -95,6 +102,8 @@ class WireApplication : BaseApp() {

enableStrictMode()

startActivityLifecycleCallback()

globalAppScope.launch {
initializeApplicationLoggingFrameworks()

Expand Down Expand Up @@ -134,6 +143,23 @@ class WireApplication : BaseApp() {
}
}

@Suppress("EmptyFunctionBlock")
private fun startActivityLifecycleCallback() {
registerActivityLifecycleCallbacks(object : ActivityLifecycleCallbacks {
override fun onActivityCreated(activity: Activity, savedInstanceState: Bundle?) {}
override fun onActivityStarted(activity: Activity) {
globalAnalyticsManager.onStart(activity)
}
override fun onActivityResumed(activity: Activity) {}
override fun onActivityPaused(activity: Activity) {}
override fun onActivityStopped(activity: Activity) {
globalAnalyticsManager.onStop(activity)
}
override fun onActivitySaveInstanceState(activity: Activity, outState: Bundle) {}
override fun onActivityDestroyed(activity: Activity) {}
})
}

private suspend fun initializeApplicationLoggingFrameworks() {
// 1. Datadog should be initialized first
ExternalLoggerManager.initDatadogLogger(applicationContext, globalDataStore.get())
Expand All @@ -159,30 +185,42 @@ class WireApplication : BaseApp() {
initializeAnonymousAnalytics()
}

private suspend fun initializeAnonymousAnalytics() {
private fun initializeAnonymousAnalytics() {
if (!BuildConfig.ANALYTICS_ENABLED) return

globalAppScope.launch {
val anonymousAnalyticsRecorder = AnonymousAnalyticsRecorderImpl()
val analyticsSettings = AnalyticsSettings(
countlyAppKey = BuildConfig.ANALYTICS_APP_KEY,
countlyServerUrl = BuildConfig.ANALYTICS_SERVER_URL,
enableDebugLogging = BuildConfig.DEBUG
)
val anonymousAnalyticsRecorder = AnonymousAnalyticsRecorderImpl()
val analyticsSettings = AnalyticsSettings(
countlyAppKey = BuildConfig.ANALYTICS_APP_KEY,
countlyServerUrl = BuildConfig.ANALYTICS_SERVER_URL,
enableDebugLogging = BuildConfig.DEBUG
)

coreLogic.get().getGlobalScope().session.currentSessionFlow().collectLatest { sessionResult ->
val isAnonymousUsageDataEnabledFlow = coreLogic.get().getGlobalScope().session.currentSessionFlow()
.flatMapLatest { sessionResult ->
if (sessionResult is CurrentSessionResult.Success && sessionResult.accountInfo.isValid()) {
val userDataStore = userDataStoreProvider.get().getOrCreate(sessionResult.accountInfo.userId)

AnonymousAnalyticsManagerImpl.init(
context = this@WireApplication,
analyticsSettings = analyticsSettings,
isEnabledFlowProvider = userDataStore::isAnonymousUsageDataEnabled,
anonymousAnalyticsRecorder = anonymousAnalyticsRecorder,
dispatcher = Dispatchers.IO
)
userDataStoreProvider.get().getOrCreate(sessionResult.accountInfo.userId).isAnonymousUsageDataEnabled()
} else {
flowOf(false)
}
}
.distinctUntilChanged()

AnonymousAnalyticsManagerImpl.init(
context = this,
analyticsSettings = analyticsSettings,
isEnabledFlow = isAnonymousUsageDataEnabledFlow,
anonymousAnalyticsRecorder = anonymousAnalyticsRecorder,
dispatcher = Dispatchers.IO
)

// observe the app visibility state and send AppOpen event if the app goes from the background to the foreground
globalAppScope.launch {
currentScreenManager
.isAppVisibleFlow()
.filter { isVisible -> isVisible }
.collect {
globalAnalyticsManager.sendEvent(AnalyticsEvent.AppOpen())
}
}
}

Expand Down
23 changes: 0 additions & 23 deletions app/src/main/kotlin/com/wire/android/ui/WireActivity.kt
Original file line number Diff line number Diff line change
Expand Up @@ -41,12 +41,10 @@ import androidx.compose.runtime.rememberUpdatedState
import androidx.compose.runtime.staticCompositionLocalOf
import androidx.compose.ui.Modifier
import androidx.compose.ui.platform.LocalContext
import androidx.compose.ui.platform.LocalLifecycleOwner
import androidx.compose.ui.platform.LocalSoftwareKeyboardController
import androidx.core.splashscreen.SplashScreen.Companion.installSplashScreen
import androidx.core.view.WindowCompat
import androidx.lifecycle.Lifecycle
import androidx.lifecycle.LifecycleEventObserver
import androidx.lifecycle.flowWithLifecycle
import androidx.lifecycle.lifecycleScope
import androidx.lifecycle.repeatOnLifecycle
Expand All @@ -60,8 +58,6 @@ import com.wire.android.config.CustomUiConfigurationProvider
import com.wire.android.config.LocalCustomUiConfigurationProvider
import com.wire.android.datastore.UserDataStore
import com.wire.android.feature.NavigationSwitchAccountActions
import com.wire.android.feature.analytics.globalAnalyticsManager
import com.wire.android.feature.analytics.model.AnalyticsEvent
import com.wire.android.navigation.BackStackMode
import com.wire.android.navigation.LocalNavigator
import com.wire.android.navigation.NavigationCommand
Expand Down Expand Up @@ -298,25 +294,6 @@ class WireActivity : AppCompatActivity() {
navController.removeOnDestinationChangedListener(currentScreenManager)
}
}

val lifecycleOwner = LocalLifecycleOwner.current
val activity = LocalContext.current as Activity
DisposableEffect(lifecycleOwner) {
val observer = LifecycleEventObserver { _, event ->
if (event == Lifecycle.Event.ON_START) {
globalAnalyticsManager.onStart(activity = activity)
globalAnalyticsManager.sendEvent(AnalyticsEvent.AppOpen())
}
if (event == Lifecycle.Event.ON_STOP) {
globalAnalyticsManager.onStop()
}
}
lifecycleOwner.lifecycle.addObserver(observer)

onDispose {
lifecycleOwner.lifecycle.removeObserver(observer)
}
}
}

@Composable
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -25,52 +25,71 @@ import com.wire.android.feature.analytics.model.AnalyticsSettings
import kotlinx.coroutines.CoroutineDispatcher
import kotlinx.coroutines.CoroutineScope
import kotlinx.coroutines.flow.Flow
import kotlinx.coroutines.flow.collectLatest
import kotlinx.coroutines.launch

object AnonymousAnalyticsManagerImpl : AnonymousAnalyticsManager {
private const val TAG = "AnonymousAnalyticsManagerImpl"
private var isAnonymousUsageDataEnabled = false
private var anonymousAnalyticsRecorder: AnonymousAnalyticsRecorder? = null
private val startedActivities = mutableSetOf<Activity>()

init {
globalAnalyticsManager = this
}

override fun init(
context: Context,
analyticsSettings: AnalyticsSettings,
isEnabledFlowProvider: suspend () -> Flow<Boolean>,
isEnabledFlow: Flow<Boolean>,
anonymousAnalyticsRecorder: AnonymousAnalyticsRecorder,
dispatcher: CoroutineDispatcher
) {
this.anonymousAnalyticsRecorder = anonymousAnalyticsRecorder

CoroutineScope(dispatcher).launch {
isEnabledFlowProvider().collect { enabled ->
isAnonymousUsageDataEnabled = enabled
if (enabled) {
anonymousAnalyticsRecorder.configure(
context = context,
analyticsSettings = analyticsSettings,
)
isEnabledFlow
.collectLatest { enabled ->
synchronized(this@AnonymousAnalyticsManagerImpl) {
if (enabled) {
anonymousAnalyticsRecorder.configure(
context = context,
analyticsSettings = analyticsSettings,
)
// start recording for all Activities started before the feature was enabled
startedActivities.forEach { activity ->
anonymousAnalyticsRecorder.onStart(activity = activity)
}
} else {
// immediately disable event tracking
anonymousAnalyticsRecorder.halt()
}
isAnonymousUsageDataEnabled = enabled
}
}
}
}
globalAnalyticsManager = this
}

override fun onStart(activity: Activity) {
if (!isAnonymousUsageDataEnabled) return
override fun onStart(activity: Activity) = synchronized(this@AnonymousAnalyticsManagerImpl) {
startedActivities.add(activity)

if (!isAnonymousUsageDataEnabled) return@synchronized

anonymousAnalyticsRecorder?.onStart(activity = activity)
?: Log.w(TAG, "Calling onStart with a null recorder.")
}

override fun onStop() {
if (!isAnonymousUsageDataEnabled) return
override fun onStop(activity: Activity) = synchronized(this@AnonymousAnalyticsManagerImpl) {
startedActivities.remove(activity)

if (!isAnonymousUsageDataEnabled) return@synchronized

anonymousAnalyticsRecorder?.onStop()
?: Log.w(TAG, "Calling onStop with a null recorder.")
}

override fun sendEvent(event: AnalyticsEvent) {
if (!isAnonymousUsageDataEnabled) return
override fun sendEvent(event: AnalyticsEvent) = synchronized(this@AnonymousAnalyticsManagerImpl) {
if (!isAnonymousUsageDataEnabled) return@synchronized

anonymousAnalyticsRecorder?.sendEvent(event = event)
?: Log.w(TAG, "Calling sendEvent with key : ${event.key} with a null recorder.")
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -65,4 +65,8 @@ class AnonymousAnalyticsRecorderImpl : AnonymousAnalyticsRecorder {
override fun sendEvent(event: AnalyticsEvent) {
Countly.sharedInstance().events().recordEvent(event.key, event.toSegmentation())
}

override fun halt() {
Countly.sharedInstance().halt()
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -49,7 +49,7 @@ class AnonymousAnalyticsManagerTest {
manager.init(
context = arrangement.context,
analyticsSettings = Arrangement.analyticsSettings,
isEnabledFlowProvider = arrangement.isEnabledFlowProvider::consumeAsFlow,
isEnabledFlow = arrangement.isEnabledFlow.consumeAsFlow(),
anonymousAnalyticsRecorder = arrangement.anonymousAnalyticsRecorder,
dispatcher = dispatcher
)
Expand Down Expand Up @@ -82,7 +82,7 @@ class AnonymousAnalyticsManagerTest {
manager.init(
context = arrangement.context,
analyticsSettings = Arrangement.analyticsSettings,
isEnabledFlowProvider = arrangement.isEnabledFlowProvider::consumeAsFlow,
isEnabledFlow = arrangement.isEnabledFlow.consumeAsFlow(),
anonymousAnalyticsRecorder = arrangement.anonymousAnalyticsRecorder,
dispatcher = dispatcher
)
Expand Down Expand Up @@ -114,7 +114,7 @@ class AnonymousAnalyticsManagerTest {
manager.init(
context = arrangement.context,
analyticsSettings = Arrangement.analyticsSettings,
isEnabledFlowProvider = arrangement.isEnabledFlowProvider::consumeAsFlow,
isEnabledFlow = arrangement.isEnabledFlow.consumeAsFlow(),
anonymousAnalyticsRecorder = arrangement.anonymousAnalyticsRecorder,
dispatcher = dispatcher
)
Expand Down Expand Up @@ -161,22 +161,78 @@ class AnonymousAnalyticsManagerTest {
arrangement.toggleIsEnabledFlow(false)
advanceUntilIdle()

manager.onStop()
manager.onStop(activity = mockk<Activity>())

// then
verify(exactly = 0) {
arrangement.anonymousAnalyticsRecorder.onStop()
}
}

@Test
fun givenIsEnabledFlowIsFalseAndOneActivityStarted_whenTogglingEnabledToTrue_thenCallStartAfterToggled() = runTest(dispatcher) {
// given
val (arrangement, manager) = Arrangement()
.withAnonymousAnalyticsRecorderConfigure()
.toggleIsEnabledFlow(false)
.arrange()
val activity: Activity = mockk()
manager.onStart(activity)
manager.init(
context = arrangement.context,
analyticsSettings = Arrangement.analyticsSettings,
isEnabledFlow = arrangement.isEnabledFlow.consumeAsFlow(),
anonymousAnalyticsRecorder = arrangement.anonymousAnalyticsRecorder,
dispatcher = dispatcher
)
advanceUntilIdle()
verify(exactly = 0) {
arrangement.anonymousAnalyticsRecorder.onStart(activity)
}

// when
arrangement.toggleIsEnabledFlow(true)
advanceUntilIdle()

// then
verify(exactly = 1) {
arrangement.anonymousAnalyticsRecorder.onStart(activity)
}
}

@Test
fun givenManagerInitialized_whenTogglingEnabledToFalse_thenHaltIsCalled() = runTest(dispatcher) {
// given
val (arrangement, manager) = Arrangement()
.withAnonymousAnalyticsRecorderConfigure()
.arrange()
manager.init(
context = arrangement.context,
analyticsSettings = Arrangement.analyticsSettings,
isEnabledFlow = arrangement.isEnabledFlow.consumeAsFlow(),
anonymousAnalyticsRecorder = arrangement.anonymousAnalyticsRecorder,
dispatcher = dispatcher
)
advanceUntilIdle()

// when
arrangement.toggleIsEnabledFlow(false)
advanceUntilIdle()

// then
verify(exactly = 1) {
arrangement.anonymousAnalyticsRecorder.halt()
}
}

private class Arrangement {
@MockK
lateinit var context: Context

@MockK
lateinit var anonymousAnalyticsRecorder: AnonymousAnalyticsRecorder

val isEnabledFlowProvider = Channel<Boolean>(capacity = Channel.UNLIMITED)
val isEnabledFlow = Channel<Boolean>(capacity = Channel.UNLIMITED)

init {
MockKAnnotations.init(this, relaxUnitFun = true)
Expand All @@ -193,7 +249,7 @@ class AnonymousAnalyticsManagerTest {
}

suspend fun toggleIsEnabledFlow(enabled: Boolean) = apply {
isEnabledFlowProvider.send(enabled)
isEnabledFlow.send(enabled)
}

companion object {
Expand Down
Loading

0 comments on commit 25a5eb4

Please sign in to comment.