From 5d184601707dbcbe27afa7c0d32c10d3876a68be Mon Sep 17 00:00:00 2001 From: Alexandre Ferris Date: Wed, 5 Jun 2024 12:55:48 +0200 Subject: [PATCH] feat: add anonymous analytics (WPB-8933) (#3076) Signed-off-by: alexandreferris --- app/build.gradle.kts | 23 ++ .../com/wire/android/WireApplication.kt | 22 ++ .../com/wire/android/ui/WireActivity.kt | 23 ++ .../kotlin/AndroidLibraryConventionPlugin.kt | 2 +- .../kotlin/customization/FeatureConfigs.kt | 7 + core/analytics-disabled/.gitignore | 1 + core/analytics-disabled/build.gradle.kts | 17 ++ core/analytics-disabled/consumer-rules.pro | 0 core/analytics-disabled/lint-baseline.xml | 4 + core/analytics-disabled/proguard-rules.pro | 21 ++ .../src/main/AndroidManifest.xml | 20 ++ .../AnonymousAnalyticsManagerImpl.kt | 20 ++ .../AnonymousAnalyticsRecorderImpl.kt | 20 ++ .../src/main/res/values/strings.xml | 20 ++ core/analytics-enabled/.gitignore | 1 + core/analytics-enabled/build.gradle.kts | 21 ++ core/analytics-enabled/consumer-rules.pro | 0 core/analytics-enabled/lint-baseline.xml | 4 + core/analytics-enabled/proguard-rules.pro | 21 ++ .../src/main/AndroidManifest.xml | 20 ++ .../AnonymousAnalyticsManagerImpl.kt | 78 +++++++ .../AnonymousAnalyticsRecorderImpl.kt | 67 ++++++ .../src/main/res/values/strings.xml | 20 ++ .../AnonymousAnalyticsManagerTest.kt | 215 ++++++++++++++++++ core/analytics/.gitignore | 1 + core/analytics/build.gradle.kts | 15 ++ core/analytics/consumer-rules.pro | 0 core/analytics/lint-baseline.xml | 4 + core/analytics/proguard-rules.pro | 21 ++ core/analytics/src/main/AndroidManifest.xml | 20 ++ .../analytics/AnonymousAnalyticsManager.kt | 44 ++++ .../AnonymousAnalyticsManagerStub.kt | 42 ++++ .../analytics/AnonymousAnalyticsRecorder.kt | 36 +++ .../AnonymousAnalyticsRecorderStub.kt | 33 +++ .../feature/analytics/model/AnalyticsEvent.kt | 59 +++++ .../analytics/model/AnalyticsSettings.kt | 24 ++ .../analytics/src/main/res/values/strings.xml | 20 ++ default.json | 30 ++- features/template/lint-baseline.xml | 4 + gradle/libs.versions.toml | 6 + 40 files changed, 999 insertions(+), 7 deletions(-) create mode 100644 core/analytics-disabled/.gitignore create mode 100644 core/analytics-disabled/build.gradle.kts create mode 100644 core/analytics-disabled/consumer-rules.pro create mode 100644 core/analytics-disabled/lint-baseline.xml create mode 100644 core/analytics-disabled/proguard-rules.pro create mode 100644 core/analytics-disabled/src/main/AndroidManifest.xml create mode 100644 core/analytics-disabled/src/main/kotlin/com/wire/android/feature/analytics/AnonymousAnalyticsManagerImpl.kt create mode 100644 core/analytics-disabled/src/main/kotlin/com/wire/android/feature/analytics/AnonymousAnalyticsRecorderImpl.kt create mode 100644 core/analytics-disabled/src/main/res/values/strings.xml create mode 100644 core/analytics-enabled/.gitignore create mode 100644 core/analytics-enabled/build.gradle.kts create mode 100644 core/analytics-enabled/consumer-rules.pro create mode 100644 core/analytics-enabled/lint-baseline.xml create mode 100644 core/analytics-enabled/proguard-rules.pro create mode 100644 core/analytics-enabled/src/main/AndroidManifest.xml create mode 100644 core/analytics-enabled/src/main/kotlin/com/wire/android/feature/analytics/AnonymousAnalyticsManagerImpl.kt create mode 100644 core/analytics-enabled/src/main/kotlin/com/wire/android/feature/analytics/AnonymousAnalyticsRecorderImpl.kt create mode 100644 core/analytics-enabled/src/main/res/values/strings.xml create mode 100644 core/analytics-enabled/src/test/kotlin/com/wire/android/feature/analytics/AnonymousAnalyticsManagerTest.kt create mode 100644 core/analytics/.gitignore create mode 100644 core/analytics/build.gradle.kts create mode 100644 core/analytics/consumer-rules.pro create mode 100644 core/analytics/lint-baseline.xml create mode 100644 core/analytics/proguard-rules.pro create mode 100644 core/analytics/src/main/AndroidManifest.xml create mode 100644 core/analytics/src/main/kotlin/com/wire/android/feature/analytics/AnonymousAnalyticsManager.kt create mode 100644 core/analytics/src/main/kotlin/com/wire/android/feature/analytics/AnonymousAnalyticsManagerStub.kt create mode 100644 core/analytics/src/main/kotlin/com/wire/android/feature/analytics/AnonymousAnalyticsRecorder.kt create mode 100644 core/analytics/src/main/kotlin/com/wire/android/feature/analytics/AnonymousAnalyticsRecorderStub.kt create mode 100644 core/analytics/src/main/kotlin/com/wire/android/feature/analytics/model/AnalyticsEvent.kt create mode 100644 core/analytics/src/main/kotlin/com/wire/android/feature/analytics/model/AnalyticsSettings.kt create mode 100644 core/analytics/src/main/res/values/strings.xml create mode 100644 features/template/lint-baseline.xml diff --git a/app/build.gradle.kts b/app/build.gradle.kts index bae74e567ec..d5ea6e64513 100644 --- a/app/build.gradle.kts +++ b/app/build.gradle.kts @@ -1,3 +1,5 @@ +import customization.ConfigurationFileImporter +import customization.NormalizedFlavorSettings import scripts.Variants_gradle /* @@ -49,6 +51,16 @@ fun isFossSourceSet(): Boolean { .lowercase() .contains("fdroid") } + +private fun getFlavorsSettings(): NormalizedFlavorSettings = + try { + val file = file("${project.rootDir}/default.json") + val configurationFileImporter = ConfigurationFileImporter() + configurationFileImporter.loadConfigsFromFile(file) + } catch (e: Exception) { + error(">> Error reading current flavors, exception: ${e.localizedMessage}") + } + android { // Most of the configuration is done in the build-logic // through the Wire Application convention plugin @@ -193,6 +205,17 @@ dependencies { } implementation(libs.androidx.work) + // Anonymous Analytics + val flavors = getFlavorsSettings() + flavors.flavorMap.entries.forEach { (key, configs) -> + if (configs["analytics_enabled"] as? Boolean == true) { + println(">> Adding Anonymous Analytics dependency to [$key] flavor") + add("${key}Implementation",project(":core:analytics-enabled")) + } else { + add("${key}Implementation",project(":core:analytics-disabled")) + } + } + // commonMark implementation(libs.commonmark.core) implementation(libs.commonmark.strikethrough) diff --git a/app/src/main/kotlin/com/wire/android/WireApplication.kt b/app/src/main/kotlin/com/wire/android/WireApplication.kt index 1e4e54b44d4..a02556316fd 100644 --- a/app/src/main/kotlin/com/wire/android/WireApplication.kt +++ b/app/src/main/kotlin/com/wire/android/WireApplication.kt @@ -29,6 +29,9 @@ import com.wire.android.datastore.GlobalDataStore 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.model.AnalyticsSettings import com.wire.android.util.DataDogLogger import com.wire.android.util.LogFileWriter import com.wire.android.util.getGitBuildId @@ -147,6 +150,25 @@ class WireApplication : Application(), Configuration.Provider { // 4. Everything ready, now we can log device info appLogger.i("Logger enabled") logDeviceInformation() + // 5. Verify if we can initialize Anonymous Analytics + initializeAnonymousAnalytics() + } + + private fun initializeAnonymousAnalytics() { + val anonymousAnalyticsRecorder = AnonymousAnalyticsRecorderImpl() + val analyticsSettings = AnalyticsSettings( + countlyAppKey = BuildConfig.ANALYTICS_APP_KEY, + countlyServerUrl = BuildConfig.ANALYTICS_SERVER_URL, + enableDebugLogging = BuildConfig.DEBUG + ) + + AnonymousAnalyticsManagerImpl.init( + context = this, + analyticsSettings = analyticsSettings, + isEnabledFlowProvider = globalDataStore.get()::isAnonymousUsageDataEnabled, + anonymousAnalyticsRecorder = anonymousAnalyticsRecorder, + dispatcher = Dispatchers.IO + ) } private fun logDeviceInformation() { diff --git a/app/src/main/kotlin/com/wire/android/ui/WireActivity.kt b/app/src/main/kotlin/com/wire/android/ui/WireActivity.kt index e33bf01eb19..2a8574d6f82 100644 --- a/app/src/main/kotlin/com/wire/android/ui/WireActivity.kt +++ b/app/src/main/kotlin/com/wire/android/ui/WireActivity.kt @@ -41,10 +41,12 @@ 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 @@ -58,6 +60,8 @@ 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 @@ -277,6 +281,25 @@ 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 diff --git a/build-logic/plugins/src/main/kotlin/AndroidLibraryConventionPlugin.kt b/build-logic/plugins/src/main/kotlin/AndroidLibraryConventionPlugin.kt index a7af11e04cf..0c81086aaef 100644 --- a/build-logic/plugins/src/main/kotlin/AndroidLibraryConventionPlugin.kt +++ b/build-logic/plugins/src/main/kotlin/AndroidLibraryConventionPlugin.kt @@ -31,7 +31,7 @@ class AndroidLibraryConventionPlugin : Plugin { } extensions.configure { - namespace = "com.wire.android.feature.${target.name}" + namespace = "com.wire.android.feature.${target.name.replace("-", "_")}" // TODO: Handle flavors. Currently implemented in `variants.gradle.kts` script configureKotlinAndroid(this) diff --git a/buildSrc/src/main/kotlin/customization/FeatureConfigs.kt b/buildSrc/src/main/kotlin/customization/FeatureConfigs.kt index 34daf221405..513be66fe05 100644 --- a/buildSrc/src/main/kotlin/customization/FeatureConfigs.kt +++ b/buildSrc/src/main/kotlin/customization/FeatureConfigs.kt @@ -100,4 +100,11 @@ enum class FeatureConfigs(val value: String, val configType: ConfigType) { MAX_REMOTE_SEARCH_RESULT_COUNT("max_remote_search_result_count", ConfigType.INT), LIMIT_TEAM_MEMBERS_FETCH_DURING_SLOW_SYNC("limit_team_members_fetch_during_slow_sync", ConfigType.INT), + + /** + * Anonymous Analytics + */ + ANALYTICS_ENABLED("analytics_enabled", ConfigType.BOOLEAN), + ANALYTICS_APP_KEY("analytics_app_key", ConfigType.STRING), + ANALYTICS_SERVER_URL("analytics_server_url", ConfigType.STRING) } diff --git a/core/analytics-disabled/.gitignore b/core/analytics-disabled/.gitignore new file mode 100644 index 00000000000..42afabfd2ab --- /dev/null +++ b/core/analytics-disabled/.gitignore @@ -0,0 +1 @@ +/build \ No newline at end of file diff --git a/core/analytics-disabled/build.gradle.kts b/core/analytics-disabled/build.gradle.kts new file mode 100644 index 00000000000..ef9d07df4fb --- /dev/null +++ b/core/analytics-disabled/build.gradle.kts @@ -0,0 +1,17 @@ +plugins { + id(libs.plugins.wire.android.library.get().pluginId) + id(libs.plugins.wire.kover.get().pluginId) +} + +dependencies { + implementation(libs.androidx.core) + implementation(libs.androidx.appcompat) + + api(project(":core:analytics")) + + val composeBom = platform(libs.compose.bom) + implementation(composeBom) + implementation(libs.compose.ui) + + testImplementation(libs.junit4) +} diff --git a/core/analytics-disabled/consumer-rules.pro b/core/analytics-disabled/consumer-rules.pro new file mode 100644 index 00000000000..e69de29bb2d diff --git a/core/analytics-disabled/lint-baseline.xml b/core/analytics-disabled/lint-baseline.xml new file mode 100644 index 00000000000..ddc2d3f41cc --- /dev/null +++ b/core/analytics-disabled/lint-baseline.xml @@ -0,0 +1,4 @@ + + + + diff --git a/core/analytics-disabled/proguard-rules.pro b/core/analytics-disabled/proguard-rules.pro new file mode 100644 index 00000000000..481bb434814 --- /dev/null +++ b/core/analytics-disabled/proguard-rules.pro @@ -0,0 +1,21 @@ +# Add project specific ProGuard rules here. +# You can control the set of applied configuration files using the +# proguardFiles setting in build.gradle. +# +# For more details, see +# http://developer.android.com/guide/developing/tools/proguard.html + +# If your project uses WebView with JS, uncomment the following +# and specify the fully qualified class name to the JavaScript interface +# class: +#-keepclassmembers class fqcn.of.javascript.interface.for.webview { +# public *; +#} + +# Uncomment this to preserve the line number information for +# debugging stack traces. +#-keepattributes SourceFile,LineNumberTable + +# If you keep the line number information, uncomment this to +# hide the original source file name. +#-renamesourcefileattribute SourceFile \ No newline at end of file diff --git a/core/analytics-disabled/src/main/AndroidManifest.xml b/core/analytics-disabled/src/main/AndroidManifest.xml new file mode 100644 index 00000000000..44b6ed94db1 --- /dev/null +++ b/core/analytics-disabled/src/main/AndroidManifest.xml @@ -0,0 +1,20 @@ + + + + diff --git a/core/analytics-disabled/src/main/kotlin/com/wire/android/feature/analytics/AnonymousAnalyticsManagerImpl.kt b/core/analytics-disabled/src/main/kotlin/com/wire/android/feature/analytics/AnonymousAnalyticsManagerImpl.kt new file mode 100644 index 00000000000..b037f08b594 --- /dev/null +++ b/core/analytics-disabled/src/main/kotlin/com/wire/android/feature/analytics/AnonymousAnalyticsManagerImpl.kt @@ -0,0 +1,20 @@ +/* + * Wire + * Copyright (C) 2024 Wire Swiss GmbH + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see http://www.gnu.org/licenses/. + */ +package com.wire.android.feature.analytics + +object AnonymousAnalyticsManagerImpl : AnonymousAnalyticsManagerStub() diff --git a/core/analytics-disabled/src/main/kotlin/com/wire/android/feature/analytics/AnonymousAnalyticsRecorderImpl.kt b/core/analytics-disabled/src/main/kotlin/com/wire/android/feature/analytics/AnonymousAnalyticsRecorderImpl.kt new file mode 100644 index 00000000000..f84c579fa69 --- /dev/null +++ b/core/analytics-disabled/src/main/kotlin/com/wire/android/feature/analytics/AnonymousAnalyticsRecorderImpl.kt @@ -0,0 +1,20 @@ +/* + * Wire + * Copyright (C) 2024 Wire Swiss GmbH + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see http://www.gnu.org/licenses/. + */ +package com.wire.android.feature.analytics + +class AnonymousAnalyticsRecorderImpl : AnonymousAnalyticsRecorderStub() diff --git a/core/analytics-disabled/src/main/res/values/strings.xml b/core/analytics-disabled/src/main/res/values/strings.xml new file mode 100644 index 00000000000..dff63a72baa --- /dev/null +++ b/core/analytics-disabled/src/main/res/values/strings.xml @@ -0,0 +1,20 @@ + + + Disabled Analytics Module + diff --git a/core/analytics-enabled/.gitignore b/core/analytics-enabled/.gitignore new file mode 100644 index 00000000000..42afabfd2ab --- /dev/null +++ b/core/analytics-enabled/.gitignore @@ -0,0 +1 @@ +/build \ No newline at end of file diff --git a/core/analytics-enabled/build.gradle.kts b/core/analytics-enabled/build.gradle.kts new file mode 100644 index 00000000000..c416711f80c --- /dev/null +++ b/core/analytics-enabled/build.gradle.kts @@ -0,0 +1,21 @@ +plugins { + id(libs.plugins.wire.android.library.get().pluginId) + id(libs.plugins.wire.kover.get().pluginId) +} + +dependencies { + implementation(libs.androidx.core) + implementation(libs.androidx.appcompat) + + api(project(":core:analytics")) + + val composeBom = platform(libs.compose.bom) + implementation(composeBom) + implementation(libs.compose.ui) + + implementation(libs.countly.sdk) + + testImplementation(libs.junit4) + testImplementation(libs.mockk.core) + testImplementation(libs.coroutines.test) +} diff --git a/core/analytics-enabled/consumer-rules.pro b/core/analytics-enabled/consumer-rules.pro new file mode 100644 index 00000000000..e69de29bb2d diff --git a/core/analytics-enabled/lint-baseline.xml b/core/analytics-enabled/lint-baseline.xml new file mode 100644 index 00000000000..ddc2d3f41cc --- /dev/null +++ b/core/analytics-enabled/lint-baseline.xml @@ -0,0 +1,4 @@ + + + + diff --git a/core/analytics-enabled/proguard-rules.pro b/core/analytics-enabled/proguard-rules.pro new file mode 100644 index 00000000000..481bb434814 --- /dev/null +++ b/core/analytics-enabled/proguard-rules.pro @@ -0,0 +1,21 @@ +# Add project specific ProGuard rules here. +# You can control the set of applied configuration files using the +# proguardFiles setting in build.gradle. +# +# For more details, see +# http://developer.android.com/guide/developing/tools/proguard.html + +# If your project uses WebView with JS, uncomment the following +# and specify the fully qualified class name to the JavaScript interface +# class: +#-keepclassmembers class fqcn.of.javascript.interface.for.webview { +# public *; +#} + +# Uncomment this to preserve the line number information for +# debugging stack traces. +#-keepattributes SourceFile,LineNumberTable + +# If you keep the line number information, uncomment this to +# hide the original source file name. +#-renamesourcefileattribute SourceFile \ No newline at end of file diff --git a/core/analytics-enabled/src/main/AndroidManifest.xml b/core/analytics-enabled/src/main/AndroidManifest.xml new file mode 100644 index 00000000000..44b6ed94db1 --- /dev/null +++ b/core/analytics-enabled/src/main/AndroidManifest.xml @@ -0,0 +1,20 @@ + + + + diff --git a/core/analytics-enabled/src/main/kotlin/com/wire/android/feature/analytics/AnonymousAnalyticsManagerImpl.kt b/core/analytics-enabled/src/main/kotlin/com/wire/android/feature/analytics/AnonymousAnalyticsManagerImpl.kt new file mode 100644 index 00000000000..045de507bfa --- /dev/null +++ b/core/analytics-enabled/src/main/kotlin/com/wire/android/feature/analytics/AnonymousAnalyticsManagerImpl.kt @@ -0,0 +1,78 @@ +/* + * Wire + * Copyright (C) 2024 Wire Swiss GmbH + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see http://www.gnu.org/licenses/. + */ +package com.wire.android.feature.analytics + +import android.app.Activity +import android.content.Context +import android.util.Log +import com.wire.android.feature.analytics.model.AnalyticsEvent +import com.wire.android.feature.analytics.model.AnalyticsSettings +import kotlinx.coroutines.CoroutineDispatcher +import kotlinx.coroutines.CoroutineScope +import kotlinx.coroutines.flow.Flow +import kotlinx.coroutines.launch + +object AnonymousAnalyticsManagerImpl : AnonymousAnalyticsManager { + private const val TAG = "AnonymousAnalyticsManagerImpl" + private var isAnonymousUsageDataEnabled = false + private var anonymousAnalyticsRecorder: AnonymousAnalyticsRecorder? = null + + override fun init( + context: Context, + analyticsSettings: AnalyticsSettings, + isEnabledFlowProvider: suspend () -> Flow, + anonymousAnalyticsRecorder: AnonymousAnalyticsRecorder, + dispatcher: CoroutineDispatcher + ) { + this.anonymousAnalyticsRecorder = anonymousAnalyticsRecorder + + CoroutineScope(dispatcher).launch { + isEnabledFlowProvider().collect { enabled -> + isAnonymousUsageDataEnabled = enabled + if (enabled) { + anonymousAnalyticsRecorder.configure( + context = context, + analyticsSettings = analyticsSettings, + ) + } + } + } + globalAnalyticsManager = this + } + + override fun onStart(activity: Activity) { + if (!isAnonymousUsageDataEnabled) return + + anonymousAnalyticsRecorder?.onStart(activity = activity) + ?: Log.w(TAG, "Calling onStart with a null recorder.") + } + + override fun onStop() { + if (!isAnonymousUsageDataEnabled) return + + anonymousAnalyticsRecorder?.onStop() + ?: Log.w(TAG, "Calling onStop with a null recorder.") + } + + override fun sendEvent(event: AnalyticsEvent) { + if (!isAnonymousUsageDataEnabled) return + + anonymousAnalyticsRecorder?.sendEvent(event = event) + ?: Log.w(TAG, "Calling sendEvent with key : ${event.key} with a null recorder.") + } +} diff --git a/core/analytics-enabled/src/main/kotlin/com/wire/android/feature/analytics/AnonymousAnalyticsRecorderImpl.kt b/core/analytics-enabled/src/main/kotlin/com/wire/android/feature/analytics/AnonymousAnalyticsRecorderImpl.kt new file mode 100644 index 00000000000..15bf79af9fd --- /dev/null +++ b/core/analytics-enabled/src/main/kotlin/com/wire/android/feature/analytics/AnonymousAnalyticsRecorderImpl.kt @@ -0,0 +1,67 @@ +/* + * Wire + * Copyright (C) 2024 Wire Swiss GmbH + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see http://www.gnu.org/licenses/. + */ +package com.wire.android.feature.analytics + +import android.app.Activity +import android.content.Context +import com.wire.android.feature.analytics.model.AnalyticsEvent +import com.wire.android.feature.analytics.model.AnalyticsEventConstants +import com.wire.android.feature.analytics.model.AnalyticsSettings +import ly.count.android.sdk.Countly +import ly.count.android.sdk.CountlyConfig + +class AnonymousAnalyticsRecorderImpl : AnonymousAnalyticsRecorder { + + private var isConfigured: Boolean = false + + override fun configure( + context: Context, + analyticsSettings: AnalyticsSettings + ) { + if (isConfigured) return + + val countlyConfig = CountlyConfig( + context, + analyticsSettings.countlyAppKey, + analyticsSettings.countlyServerUrl + ) + countlyConfig.setLoggingEnabled(analyticsSettings.enableDebugLogging) + + Countly.sharedInstance().init(countlyConfig) + + val packageInfo = context.packageManager.getPackageInfo(context.packageName, 0) + val globalSegmentations = mapOf( + AnalyticsEventConstants.APP_NAME to AnalyticsEventConstants.APP_NAME_ANDROID, + AnalyticsEventConstants.APP_VERSION to packageInfo.versionName + ) + Countly.sharedInstance().views().setGlobalViewSegmentation(globalSegmentations) + isConfigured = true + } + + override fun onStart(activity: Activity) { + Countly.sharedInstance().onStart(activity) + } + + override fun onStop() { + Countly.sharedInstance().onStop() + } + + override fun sendEvent(event: AnalyticsEvent) { + Countly.sharedInstance().events().recordEvent(event.key, event.toSegmentation()) + } +} diff --git a/core/analytics-enabled/src/main/res/values/strings.xml b/core/analytics-enabled/src/main/res/values/strings.xml new file mode 100644 index 00000000000..ff7c6d4fd39 --- /dev/null +++ b/core/analytics-enabled/src/main/res/values/strings.xml @@ -0,0 +1,20 @@ + + + Enabled Analytics Module + diff --git a/core/analytics-enabled/src/test/kotlin/com/wire/android/feature/analytics/AnonymousAnalyticsManagerTest.kt b/core/analytics-enabled/src/test/kotlin/com/wire/android/feature/analytics/AnonymousAnalyticsManagerTest.kt new file mode 100644 index 00000000000..ed62ad88bda --- /dev/null +++ b/core/analytics-enabled/src/test/kotlin/com/wire/android/feature/analytics/AnonymousAnalyticsManagerTest.kt @@ -0,0 +1,215 @@ +/* + * Wire + * Copyright (C) 2024 Wire Swiss GmbH + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see http://www.gnu.org/licenses/. + */ +package com.wire.android.feature.analytics + +import android.app.Activity +import android.content.Context +import com.wire.android.feature.analytics.model.AnalyticsEvent +import com.wire.android.feature.analytics.model.AnalyticsSettings +import io.mockk.MockKAnnotations +import io.mockk.every +import io.mockk.impl.annotations.MockK +import io.mockk.mockk +import io.mockk.verify +import kotlinx.coroutines.channels.Channel +import kotlinx.coroutines.flow.consumeAsFlow +import kotlinx.coroutines.test.StandardTestDispatcher +import kotlinx.coroutines.test.advanceUntilIdle +import kotlinx.coroutines.test.runTest +import org.junit.Test + +class AnonymousAnalyticsManagerTest { + private val dispatcher = StandardTestDispatcher() + + @Test + fun givenAnonymousAnalyticsManager_whenInitializing_thenAnalyticsImplementationIsConfiguredCorrectly() = runTest(dispatcher) { + // given + val (arrangement, manager) = Arrangement() + .withAnonymousAnalyticsRecorderConfigure() + .arrange() + + arrangement.toggleIsEnabledFlow(true) + + // when + manager.init( + context = arrangement.context, + analyticsSettings = Arrangement.analyticsSettings, + isEnabledFlowProvider = arrangement.isEnabledFlowProvider::consumeAsFlow, + anonymousAnalyticsRecorder = arrangement.anonymousAnalyticsRecorder, + dispatcher = dispatcher + ) + advanceUntilIdle() + + // then + verify(exactly = 1) { + arrangement.anonymousAnalyticsRecorder.configure( + arrangement.context, + Arrangement.analyticsSettings + ) + } + } + + @Test + fun givenIsEnabledFlowIsTrue_whenSendingAnEvent_thenEventIsSent() = runTest(dispatcher) { + // given + val (arrangement, manager) = Arrangement() + .withAnonymousAnalyticsRecorderConfigure() + .arrange() + + val event = Arrangement.Companion.DummyEvent( + key = "key1", + attribute1 = "attr1" + ) + + arrangement.toggleIsEnabledFlow(true) + + // when + manager.init( + context = arrangement.context, + analyticsSettings = Arrangement.analyticsSettings, + isEnabledFlowProvider = arrangement.isEnabledFlowProvider::consumeAsFlow, + anonymousAnalyticsRecorder = arrangement.anonymousAnalyticsRecorder, + dispatcher = dispatcher + ) + advanceUntilIdle() + + manager.sendEvent(event) + + // then + verify(exactly = 1) { + arrangement.anonymousAnalyticsRecorder.sendEvent(event) + } + } + + @Test + fun givenIsEnabledFlowIsTrue_whenSettingToFalseAndSendingEvent_thenNoEventsAreSent() = runTest(dispatcher) { + // given + val (arrangement, manager) = Arrangement() + .withAnonymousAnalyticsRecorderConfigure() + .arrange() + + val event = Arrangement.Companion.DummyEvent( + key = "key1", + attribute1 = "attr1" + ) + + arrangement.toggleIsEnabledFlow(true) + + // when + manager.init( + context = arrangement.context, + analyticsSettings = Arrangement.analyticsSettings, + isEnabledFlowProvider = arrangement.isEnabledFlowProvider::consumeAsFlow, + anonymousAnalyticsRecorder = arrangement.anonymousAnalyticsRecorder, + dispatcher = dispatcher + ) + advanceUntilIdle() + + manager.sendEvent(event) + + arrangement.toggleIsEnabledFlow(false) + advanceUntilIdle() + + manager.sendEvent(event) + + // then + verify(exactly = 1) { + arrangement.anonymousAnalyticsRecorder.sendEvent(event) + } + } + + @Test + fun givenIsEnabledFlowIsFalse_whenCallingOnStart_thenRecorderOnStartIsNotCalled() = runTest(dispatcher) { + // given + val (arrangement, manager) = Arrangement() + .withAnonymousAnalyticsRecorderConfigure() + .arrange() + + arrangement.toggleIsEnabledFlow(false) + advanceUntilIdle() + + manager.onStart(activity = mockk()) + + // then + verify(exactly = 0) { + arrangement.anonymousAnalyticsRecorder.onStart(any()) + } + } + + @Test + fun givenIsEnabledFlowIsFalse_whenCallingOnStop_thenRecorderOnStopIsNotCalled() = runTest(dispatcher) { + // given + val (arrangement, manager) = Arrangement() + .withAnonymousAnalyticsRecorderConfigure() + .arrange() + + arrangement.toggleIsEnabledFlow(false) + advanceUntilIdle() + + manager.onStop() + + // then + verify(exactly = 0) { + arrangement.anonymousAnalyticsRecorder.onStop() + } + } + + private class Arrangement { + @MockK + lateinit var context: Context + + @MockK + lateinit var anonymousAnalyticsRecorder: AnonymousAnalyticsRecorder + + val isEnabledFlowProvider = Channel(capacity = Channel.UNLIMITED) + + init { + MockKAnnotations.init(this, relaxUnitFun = true) + } + + private val manager by lazy { + AnonymousAnalyticsManagerImpl + } + + fun arrange() = this to manager + + fun withAnonymousAnalyticsRecorderConfigure() = apply { + every { anonymousAnalyticsRecorder.configure(any(), any()) } returns Unit + } + + suspend fun toggleIsEnabledFlow(enabled: Boolean) = apply { + isEnabledFlowProvider.send(enabled) + } + + companion object { + val analyticsSettings = AnalyticsSettings( + countlyAppKey = "appKey", + countlyServerUrl = "serverUrl", + enableDebugLogging = true + ) + data class DummyEvent( + override val key: String, + val attribute1: String + ) : AnalyticsEvent { + override fun toSegmentation(): Map = mapOf( + "attribute1" to attribute1 + ) + } + } + } +} diff --git a/core/analytics/.gitignore b/core/analytics/.gitignore new file mode 100644 index 00000000000..42afabfd2ab --- /dev/null +++ b/core/analytics/.gitignore @@ -0,0 +1 @@ +/build \ No newline at end of file diff --git a/core/analytics/build.gradle.kts b/core/analytics/build.gradle.kts new file mode 100644 index 00000000000..33ccfcf8e31 --- /dev/null +++ b/core/analytics/build.gradle.kts @@ -0,0 +1,15 @@ +plugins { + id(libs.plugins.wire.android.library.get().pluginId) + id(libs.plugins.wire.kover.get().pluginId) +} + +dependencies { + implementation(libs.androidx.core) + implementation(libs.androidx.appcompat) + + val composeBom = platform(libs.compose.bom) + implementation(composeBom) + implementation(libs.compose.ui) + + testImplementation(libs.junit4) +} diff --git a/core/analytics/consumer-rules.pro b/core/analytics/consumer-rules.pro new file mode 100644 index 00000000000..e69de29bb2d diff --git a/core/analytics/lint-baseline.xml b/core/analytics/lint-baseline.xml new file mode 100644 index 00000000000..ddc2d3f41cc --- /dev/null +++ b/core/analytics/lint-baseline.xml @@ -0,0 +1,4 @@ + + + + diff --git a/core/analytics/proguard-rules.pro b/core/analytics/proguard-rules.pro new file mode 100644 index 00000000000..481bb434814 --- /dev/null +++ b/core/analytics/proguard-rules.pro @@ -0,0 +1,21 @@ +# Add project specific ProGuard rules here. +# You can control the set of applied configuration files using the +# proguardFiles setting in build.gradle. +# +# For more details, see +# http://developer.android.com/guide/developing/tools/proguard.html + +# If your project uses WebView with JS, uncomment the following +# and specify the fully qualified class name to the JavaScript interface +# class: +#-keepclassmembers class fqcn.of.javascript.interface.for.webview { +# public *; +#} + +# Uncomment this to preserve the line number information for +# debugging stack traces. +#-keepattributes SourceFile,LineNumberTable + +# If you keep the line number information, uncomment this to +# hide the original source file name. +#-renamesourcefileattribute SourceFile \ No newline at end of file diff --git a/core/analytics/src/main/AndroidManifest.xml b/core/analytics/src/main/AndroidManifest.xml new file mode 100644 index 00000000000..44b6ed94db1 --- /dev/null +++ b/core/analytics/src/main/AndroidManifest.xml @@ -0,0 +1,20 @@ + + + + diff --git a/core/analytics/src/main/kotlin/com/wire/android/feature/analytics/AnonymousAnalyticsManager.kt b/core/analytics/src/main/kotlin/com/wire/android/feature/analytics/AnonymousAnalyticsManager.kt new file mode 100644 index 00000000000..19fd1ba0087 --- /dev/null +++ b/core/analytics/src/main/kotlin/com/wire/android/feature/analytics/AnonymousAnalyticsManager.kt @@ -0,0 +1,44 @@ +/* + * Wire + * Copyright (C) 2024 Wire Swiss GmbH + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see http://www.gnu.org/licenses/. + */ +package com.wire.android.feature.analytics + +import android.app.Activity +import android.content.Context +import com.wire.android.feature.analytics.model.AnalyticsEvent +import com.wire.android.feature.analytics.model.AnalyticsSettings +import kotlinx.coroutines.CoroutineDispatcher +import kotlinx.coroutines.flow.Flow + +var globalAnalyticsManager: AnonymousAnalyticsManager = AnonymousAnalyticsManagerStub() + +interface AnonymousAnalyticsManager { + + fun init( + context: Context, + analyticsSettings: AnalyticsSettings, + isEnabledFlowProvider: suspend () -> Flow, + anonymousAnalyticsRecorder: AnonymousAnalyticsRecorder, + dispatcher: CoroutineDispatcher + ) + + fun sendEvent(event: AnalyticsEvent) + + fun onStart(activity: Activity) + + fun onStop() +} diff --git a/core/analytics/src/main/kotlin/com/wire/android/feature/analytics/AnonymousAnalyticsManagerStub.kt b/core/analytics/src/main/kotlin/com/wire/android/feature/analytics/AnonymousAnalyticsManagerStub.kt new file mode 100644 index 00000000000..8d68e23a29c --- /dev/null +++ b/core/analytics/src/main/kotlin/com/wire/android/feature/analytics/AnonymousAnalyticsManagerStub.kt @@ -0,0 +1,42 @@ +/* + * Wire + * Copyright (C) 2024 Wire Swiss GmbH + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see http://www.gnu.org/licenses/. + */ +package com.wire.android.feature.analytics + +import android.app.Activity +import android.content.Context +import com.wire.android.feature.analytics.model.AnalyticsEvent +import com.wire.android.feature.analytics.model.AnalyticsSettings +import kotlinx.coroutines.CoroutineDispatcher +import kotlinx.coroutines.flow.Flow + +open class AnonymousAnalyticsManagerStub : AnonymousAnalyticsManager { + + override fun init( + context: Context, + analyticsSettings: AnalyticsSettings, + isEnabledFlowProvider: suspend () -> Flow, + anonymousAnalyticsRecorder: AnonymousAnalyticsRecorder, + dispatcher: CoroutineDispatcher + ) = Unit + + override fun sendEvent(event: AnalyticsEvent) = Unit + + override fun onStart(activity: Activity) = Unit + + override fun onStop() = Unit +} diff --git a/core/analytics/src/main/kotlin/com/wire/android/feature/analytics/AnonymousAnalyticsRecorder.kt b/core/analytics/src/main/kotlin/com/wire/android/feature/analytics/AnonymousAnalyticsRecorder.kt new file mode 100644 index 00000000000..a9a99d5b9e5 --- /dev/null +++ b/core/analytics/src/main/kotlin/com/wire/android/feature/analytics/AnonymousAnalyticsRecorder.kt @@ -0,0 +1,36 @@ +/* + * Wire + * Copyright (C) 2024 Wire Swiss GmbH + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see http://www.gnu.org/licenses/. + */ +package com.wire.android.feature.analytics + +import android.app.Activity +import android.content.Context +import com.wire.android.feature.analytics.model.AnalyticsEvent +import com.wire.android.feature.analytics.model.AnalyticsSettings + +interface AnonymousAnalyticsRecorder { + fun configure( + context: Context, + analyticsSettings: AnalyticsSettings + ) + + fun onStart(activity: Activity) + + fun onStop() + + fun sendEvent(event: AnalyticsEvent) +} diff --git a/core/analytics/src/main/kotlin/com/wire/android/feature/analytics/AnonymousAnalyticsRecorderStub.kt b/core/analytics/src/main/kotlin/com/wire/android/feature/analytics/AnonymousAnalyticsRecorderStub.kt new file mode 100644 index 00000000000..de30e3ad93a --- /dev/null +++ b/core/analytics/src/main/kotlin/com/wire/android/feature/analytics/AnonymousAnalyticsRecorderStub.kt @@ -0,0 +1,33 @@ +/* + * Wire + * Copyright (C) 2024 Wire Swiss GmbH + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see http://www.gnu.org/licenses/. + */ +package com.wire.android.feature.analytics + +import android.app.Activity +import android.content.Context +import com.wire.android.feature.analytics.model.AnalyticsEvent +import com.wire.android.feature.analytics.model.AnalyticsSettings + +open class AnonymousAnalyticsRecorderStub : AnonymousAnalyticsRecorder { + override fun configure(context: Context, analyticsSettings: AnalyticsSettings) = Unit + + override fun onStart(activity: Activity) = Unit + + override fun onStop() = Unit + + override fun sendEvent(event: AnalyticsEvent) = Unit +} diff --git a/core/analytics/src/main/kotlin/com/wire/android/feature/analytics/model/AnalyticsEvent.kt b/core/analytics/src/main/kotlin/com/wire/android/feature/analytics/model/AnalyticsEvent.kt new file mode 100644 index 00000000000..f56c91ae2eb --- /dev/null +++ b/core/analytics/src/main/kotlin/com/wire/android/feature/analytics/model/AnalyticsEvent.kt @@ -0,0 +1,59 @@ +/* + * Wire + * Copyright (C) 2024 Wire Swiss GmbH + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see http://www.gnu.org/licenses/. + */ +package com.wire.android.feature.analytics.model + +interface AnalyticsEvent { + /** + * Key to be used to differentiate every event + */ + val key: String + + /** + * Each AnalyticsEvent must implement its own attributes. + * This method is to be implemented as a Map based + * on each attribute. + * + * Example: + * data class ExampleEvent( + * override val key: String, + * val attr1: String, + * val attr2: String + * ) : AnalyticsEvent() { + * override fun toSegmentation() { + * return mapOf( + * "attr1" to attr1, + * "attr2" to attr2 + * ) + * } + * } + */ + fun toSegmentation(): Map + + data class AppOpen( + override val key: String = AnalyticsEventConstants.APP_OPEN + ) : AnalyticsEvent { + override fun toSegmentation(): Map = mapOf() + } +} + +object AnalyticsEventConstants { + const val APP_NAME = "app_name" + const val APP_NAME_ANDROID = "android" + const val APP_VERSION = "app_version" + const val APP_OPEN = "app.open" +} diff --git a/core/analytics/src/main/kotlin/com/wire/android/feature/analytics/model/AnalyticsSettings.kt b/core/analytics/src/main/kotlin/com/wire/android/feature/analytics/model/AnalyticsSettings.kt new file mode 100644 index 00000000000..066386592d8 --- /dev/null +++ b/core/analytics/src/main/kotlin/com/wire/android/feature/analytics/model/AnalyticsSettings.kt @@ -0,0 +1,24 @@ +/* + * Wire + * Copyright (C) 2024 Wire Swiss GmbH + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see http://www.gnu.org/licenses/. + */ +package com.wire.android.feature.analytics.model + +data class AnalyticsSettings( + val countlyAppKey: String, + val countlyServerUrl: String, + val enableDebugLogging: Boolean +) diff --git a/core/analytics/src/main/res/values/strings.xml b/core/analytics/src/main/res/values/strings.xml new file mode 100644 index 00000000000..cc4c4d323b5 --- /dev/null +++ b/core/analytics/src/main/res/values/strings.xml @@ -0,0 +1,20 @@ + + + Core Analytics Module + diff --git a/default.json b/default.json index df26199bd3e..e35d1e46c41 100644 --- a/default.json +++ b/default.json @@ -6,7 +6,10 @@ "logging_enabled": false, "application_is_private_build": false, "development_api_enabled": false, - "mls_support_enabled": false + "mls_support_enabled": false, + "analytics_enabled": true, + "analytics_app_key": "4483f7a58ae3e70b3780319c4ccb5c88a037be49", + "analytics_server_url": "https://countly.wire.com/" }, "dev": { "application_id": "com.waz.zclient.dev", @@ -27,7 +30,10 @@ "default_backend_url_website": "https://wire.com", "default_backend_title": "wire-staging", "is_password_protected_guest_link_enabled": true, - "encrypt_proteus_storage": true + "encrypt_proteus_storage": true, + "analytics_enabled": true, + "analytics_app_key": "8ffae535f1836ed5f58fd5c8a11c00eca07c5438", + "analytics_server_url": "https://countly.wire.com/" }, "staging": { "application_id": "com.waz.zclient.dev", @@ -46,7 +52,10 @@ "default_backend_url_blacklist": "https://clientblacklist.wire.com/staging", "default_backend_url_website": "https://wire.com", "default_backend_title": "wire-staging", - "encrypt_proteus_storage": true + "encrypt_proteus_storage": true, + "analytics_enabled": true, + "analytics_app_key": "8ffae535f1836ed5f58fd5c8a11c00eca07c5438", + "analytics_server_url": "https://countly.wire.com/" }, "beta": { "application_id": "com.wire.android.internal", @@ -55,7 +64,10 @@ "application_is_private_build": true, "development_api_enabled": false, "mls_support_enabled": false, - "encrypt_proteus_storage": true + "encrypt_proteus_storage": true, + "analytics_enabled": true, + "analytics_app_key": "8ffae535f1836ed5f58fd5c8a11c00eca07c5438", + "analytics_server_url": "https://countly.wire.com/" }, "internal": { "application_id": "com.wire.internal", @@ -63,7 +75,10 @@ "logging_enabled": true, "application_is_private_build": true, "development_api_enabled": false, - "encrypt_proteus_storage": true + "encrypt_proteus_storage": true, + "analytics_enabled": true, + "analytics_app_key": "8ffae535f1836ed5f58fd5c8a11c00eca07c5438", + "analytics_server_url": "https://countly.wire.com/" }, "fdroid": { "application_id": "com.wire", @@ -71,7 +86,10 @@ "logging_enabled": false, "application_is_private_build": false, "development_api_enabled": false, - "mls_support_enabled": false + "mls_support_enabled": false, + "analytics_enabled": false, + "analytics_app_key": "", + "analytics_server_url": "" } }, "application_name": "Wire", diff --git a/features/template/lint-baseline.xml b/features/template/lint-baseline.xml new file mode 100644 index 00000000000..ddc2d3f41cc --- /dev/null +++ b/features/template/lint-baseline.xml @@ -0,0 +1,4 @@ + + + + diff --git a/gradle/libs.versions.toml b/gradle/libs.versions.toml index 263e44ac439..4800612d202 100644 --- a/gradle/libs.versions.toml +++ b/gradle/libs.versions.toml @@ -64,6 +64,9 @@ material = "1.11.0" coil = "2.6.0" commonmark = "0.22.0" +# Countly +countly = "24.4.0" + # RSS rss-parser = "6.0.7" @@ -238,6 +241,9 @@ commonmark-core = { module = "org.commonmark:commonmark", version.ref = "commonm commonmark-strikethrough = { module = "org.commonmark:commonmark-ext-gfm-strikethrough", version.ref = "commonmark" } commonmark-tables = { module = "org.commonmark:commonmark-ext-gfm-tables", version.ref = "commonmark" } +# Countly +countly-sdk = { module = "ly.count.android:sdk", version.ref = "countly" } + # Dev tools aboutLibraries-core = { module = "com.mikepenz:aboutlibraries-core", version.ref = "aboutLibraries" } aboutLibraries-ui = { module = "com.mikepenz:aboutlibraries-compose", version.ref = "aboutLibraries" }