diff --git a/app/benchmark-rules.pro b/app/benchmark-rules.pro
index ddecd591b..96b67f2d1 100644
--- a/app/benchmark-rules.pro
+++ b/app/benchmark-rules.pro
@@ -3,4 +3,16 @@
# Obsfuscation must be disabled for the build variant that generates Baseline Profile, otherwise
# wrong symbols would be generated. The generated Baseline Profile will be properly applied when generated
# without obfuscation and your app is being obfuscated.
--dontobfuscate
\ No newline at end of file
+-dontobfuscate
+
+# Please add these rules to your existing keep rules in order to suppress warnings.
+# This is generated automatically by the Android Gradle plugin.
+-dontwarn org.bouncycastle.jsse.BCSSLParameters
+-dontwarn org.bouncycastle.jsse.BCSSLSocket
+-dontwarn org.bouncycastle.jsse.provider.BouncyCastleJsseProvider
+-dontwarn org.conscrypt.Conscrypt$Version
+-dontwarn org.conscrypt.Conscrypt
+-dontwarn org.conscrypt.ConscryptHostnameVerifier
+-dontwarn org.openjsse.javax.net.ssl.SSLParameters
+-dontwarn org.openjsse.javax.net.ssl.SSLSocket
+-dontwarn org.openjsse.net.ssl.OpenJSSE
\ No newline at end of file
diff --git a/app/build.gradle.kts b/app/build.gradle.kts
index ab9d35284..1f34810ed 100644
--- a/app/build.gradle.kts
+++ b/app/build.gradle.kts
@@ -19,10 +19,11 @@ import com.android.build.api.dsl.ManagedVirtualDevice
plugins {
id("nowinandroid.android.application")
id("nowinandroid.android.application.compose")
+ id("nowinandroid.android.application.flavors")
id("nowinandroid.android.application.jacoco")
id("nowinandroid.android.hilt")
- id("nowinandroid.firebase")
id("jacoco")
+ id("nowinandroid.android.application.firebase")
}
android {
@@ -101,6 +102,7 @@ dependencies {
implementation(project(":core:designsystem"))
implementation(project(":core:data"))
implementation(project(":core:model"))
+ implementation(project(":core:analytics"))
implementation(project(":sync:work"))
diff --git a/app/google-services.json b/app/google-services.json
new file mode 100644
index 000000000..69f0b5da3
--- /dev/null
+++ b/app/google-services.json
@@ -0,0 +1,125 @@
+{
+ "project_info": {
+ "project_number": "YourProjectId",
+ "project_id": "abc",
+ "storage_bucket": "abc"
+ },
+ "client": [
+ {
+ "client_info": {
+ "mobilesdk_app_id": "Your:App:Id",
+ "android_client_info": {
+ "package_name": "com.google.samples.apps.nowinandroid"
+ }
+ },
+ "oauth_client": [],
+ "api_key": [
+ {
+ "current_key": "APlaceholderAPIKeyWith-ThirtyNineCharsX"
+ }
+ ],
+ "services": {
+ "appinvite_service": {
+ "other_platform_oauth_client": []
+ }
+ }
+ },
+ {
+ "client_info": {
+ "mobilesdk_app_id": "Your:App:Id",
+ "android_client_info": {
+ "package_name": "com.google.samples.apps.nowinandroid.demo.debug"
+ }
+ },
+ "oauth_client": [],
+ "api_key": [
+ {
+ "current_key": "APlaceholderAPIKeyWith-ThirtyNineCharsX"
+ }
+ ],
+ "services": {
+ "appinvite_service": {
+ "other_platform_oauth_client": []
+ }
+ }
+ },
+ {
+ "client_info": {
+ "mobilesdk_app_id": "Your:App:Id",
+ "android_client_info": {
+ "package_name": "com.google.samples.apps.nowinandroid.demo.benchmark"
+ }
+ },
+ "oauth_client": [],
+ "api_key": [
+ {
+ "current_key": "APlaceholderAPIKeyWith-ThirtyNineCharsX"
+ }
+ ],
+ "services": {
+ "appinvite_service": {
+ "other_platform_oauth_client": []
+ }
+ }
+ },
+ {
+ "client_info": {
+ "mobilesdk_app_id": "Your:App:Id",
+ "android_client_info": {
+ "package_name": "com.google.samples.apps.nowinandroid.benchmark"
+ }
+ },
+ "oauth_client": [],
+ "api_key": [
+ {
+ "current_key": "APlaceholderAPIKeyWith-ThirtyNineCharsX"
+ }
+ ],
+ "services": {
+ "appinvite_service": {
+ "other_platform_oauth_client": []
+ }
+ }
+ },
+ {
+ "client_info": {
+ "mobilesdk_app_id": "Your:App:Id",
+ "android_client_info": {
+ "package_name": "com.google.samples.apps.nowinandroid.debug"
+ }
+ },
+ "oauth_client": [],
+ "api_key": [
+ {
+ "current_key": "APlaceholderAPIKeyWith-ThirtyNineCharsX"
+ }
+ ],
+ "services": {
+ "appinvite_service": {
+ "other_platform_oauth_client": []
+ }
+ }
+ },
+ {
+ "client_info": {
+ "mobilesdk_app_id": "Your:App:Id",
+ "android_client_info": {
+ "package_name": "com.google.samples.apps.nowinandroid.demo"
+ }
+ },
+ "oauth_client": [],
+ "api_key": [
+ {
+ "current_key": "APlaceholderAPIKeyWith-ThirtyNineCharsX"
+ }
+ ],
+ "services": {
+ "appinvite_service": {
+ "other_platform_oauth_client": []
+ }
+ }
+ }
+ ],
+
+ "configuration_version": "1"
+}
\ No newline at end of file
diff --git a/app/src/main/AndroidManifest.xml b/app/src/main/AndroidManifest.xml
index 5c3b889d2..23b2e7862 100644
--- a/app/src/main/AndroidManifest.xml
+++ b/app/src/main/AndroidManifest.xml
@@ -39,6 +39,10 @@
+
+
+
diff --git a/app/src/main/java/com/google/samples/apps/nowinandroid/MainActivity.kt b/app/src/main/java/com/google/samples/apps/nowinandroid/MainActivity.kt
index e46d2156a..5fc9d0525 100644
--- a/app/src/main/java/com/google/samples/apps/nowinandroid/MainActivity.kt
+++ b/app/src/main/java/com/google/samples/apps/nowinandroid/MainActivity.kt
@@ -24,6 +24,7 @@ import androidx.compose.foundation.isSystemInDarkTheme
import androidx.compose.material3.windowsizeclass.ExperimentalMaterial3WindowSizeClassApi
import androidx.compose.material3.windowsizeclass.calculateWindowSizeClass
import androidx.compose.runtime.Composable
+import androidx.compose.runtime.CompositionLocalProvider
import androidx.compose.runtime.DisposableEffect
import androidx.compose.runtime.getValue
import androidx.compose.runtime.mutableStateOf
@@ -37,6 +38,8 @@ import androidx.metrics.performance.JankStats
import com.google.accompanist.systemuicontroller.rememberSystemUiController
import com.google.samples.apps.nowinandroid.MainActivityUiState.Loading
import com.google.samples.apps.nowinandroid.MainActivityUiState.Success
+import com.google.samples.apps.nowinandroid.core.analytics.AnalyticsHelper
+import com.google.samples.apps.nowinandroid.core.analytics.LocalAnalyticsHelper
import com.google.samples.apps.nowinandroid.core.data.util.NetworkMonitor
import com.google.samples.apps.nowinandroid.core.designsystem.theme.NiaTheme
import com.google.samples.apps.nowinandroid.core.model.data.DarkThemeConfig
@@ -61,6 +64,9 @@ class MainActivity : ComponentActivity() {
@Inject
lateinit var networkMonitor: NetworkMonitor
+ @Inject
+ lateinit var analyticsHelper: AnalyticsHelper
+
val viewModel: MainActivityViewModel by viewModels()
override fun onCreate(savedInstanceState: Bundle?) {
@@ -104,15 +110,17 @@ class MainActivity : ComponentActivity() {
onDispose {}
}
- NiaTheme(
- darkTheme = darkTheme,
- androidTheme = shouldUseAndroidTheme(uiState),
- disableDynamicTheming = shouldDisableDynamicTheming(uiState),
- ) {
- NiaApp(
- networkMonitor = networkMonitor,
- windowSizeClass = calculateWindowSizeClass(this),
- )
+ CompositionLocalProvider(LocalAnalyticsHelper provides analyticsHelper) {
+ NiaTheme(
+ darkTheme = darkTheme,
+ androidTheme = shouldUseAndroidTheme(uiState),
+ disableDynamicTheming = shouldDisableDynamicTheming(uiState),
+ ) {
+ NiaApp(
+ networkMonitor = networkMonitor,
+ windowSizeClass = calculateWindowSizeClass(this),
+ )
+ }
}
}
}
diff --git a/app/src/prod/AndroidManifest.xml b/app/src/prod/AndroidManifest.xml
new file mode 100644
index 000000000..2f8a8592a
--- /dev/null
+++ b/app/src/prod/AndroidManifest.xml
@@ -0,0 +1,26 @@
+
+
+
+
+
+
+
+
\ No newline at end of file
diff --git a/build-logic/convention/build.gradle.kts b/build-logic/convention/build.gradle.kts
index 11b8a5de1..9d7af2372 100644
--- a/build-logic/convention/build.gradle.kts
+++ b/build-logic/convention/build.gradle.kts
@@ -28,6 +28,8 @@ java {
dependencies {
compileOnly(libs.android.gradlePlugin)
compileOnly(libs.kotlin.gradlePlugin)
+ compileOnly(libs.firebase.performance.gradle)
+ compileOnly(libs.firebase.crashlytics.gradle)
}
gradlePlugin {
@@ -68,9 +70,14 @@ gradlePlugin {
id = "nowinandroid.android.hilt"
implementationClass = "AndroidHiltConventionPlugin"
}
- register("firebase") {
- id = "nowinandroid.firebase"
- implementationClass = "FirebaseConventionPlugin"
+ register("androidFirebase") {
+ id = "nowinandroid.android.application.firebase"
+ implementationClass = "AndroidApplicationFirebaseConventionPlugin"
}
+ register("androidFlavors") {
+ id = "nowinandroid.android.application.flavors"
+ implementationClass = "AndroidApplicationFlavorsConventionPlugin"
+ }
+
}
}
diff --git a/build-logic/convention/src/main/kotlin/AndroidApplicationConventionPlugin.kt b/build-logic/convention/src/main/kotlin/AndroidApplicationConventionPlugin.kt
index 612eb6ad4..dfbea8394 100644
--- a/build-logic/convention/src/main/kotlin/AndroidApplicationConventionPlugin.kt
+++ b/build-logic/convention/src/main/kotlin/AndroidApplicationConventionPlugin.kt
@@ -14,9 +14,8 @@
* limitations under the License.
*/
-import com.android.build.api.variant.ApplicationAndroidComponentsExtension
import com.android.build.api.dsl.ApplicationExtension
-import com.google.samples.apps.nowinandroid.configureFlavors
+import com.android.build.api.variant.ApplicationAndroidComponentsExtension
import com.google.samples.apps.nowinandroid.configureKotlinAndroid
import com.google.samples.apps.nowinandroid.configurePrintApksTask
import org.gradle.api.Plugin
@@ -34,7 +33,6 @@ class AndroidApplicationConventionPlugin : Plugin {
extensions.configure {
configureKotlinAndroid(this)
defaultConfig.targetSdk = 33
- configureFlavors(this)
}
extensions.configure {
configurePrintApksTask(this)
diff --git a/build-logic/convention/src/main/kotlin/FirebaseConventionPlugin.kt b/build-logic/convention/src/main/kotlin/AndroidApplicationFirebaseConventionPlugin.kt
similarity index 53%
rename from build-logic/convention/src/main/kotlin/FirebaseConventionPlugin.kt
rename to build-logic/convention/src/main/kotlin/AndroidApplicationFirebaseConventionPlugin.kt
index 70054408d..598da727d 100644
--- a/build-logic/convention/src/main/kotlin/FirebaseConventionPlugin.kt
+++ b/build-logic/convention/src/main/kotlin/AndroidApplicationFirebaseConventionPlugin.kt
@@ -14,17 +14,17 @@
* limitations under the License.
*/
+import com.android.build.api.variant.ApplicationAndroidComponentsExtension
+import com.google.firebase.crashlytics.buildtools.gradle.CrashlyticsExtension
import org.gradle.api.Plugin
import org.gradle.api.Project
import org.gradle.api.artifacts.VersionCatalogsExtension
+import org.gradle.kotlin.dsl.configure
import org.gradle.kotlin.dsl.dependencies
import org.gradle.kotlin.dsl.getByType
-class FirebaseConventionPlugin : Plugin {
+class AndroidApplicationFirebaseConventionPlugin : Plugin {
override fun apply(target: Project) {
- // Only apply this to Google Play releases.
- if (!target.hasProperty("use-google-services"))
- return
with(target) {
with(pluginManager) {
apply("com.google.gms.google-services")
@@ -35,10 +35,23 @@ class FirebaseConventionPlugin : Plugin {
val libs = extensions.getByType().named("libs")
dependencies {
val bom = libs.findLibrary("firebase-bom").get()
- add("releaseImplementation", platform(bom))
- "releaseImplementation"(libs.findLibrary("firebase.analytics").get())
- "releaseImplementation"(libs.findLibrary("firebase.crashlytics").get())
- "releaseImplementation"(libs.findLibrary("firebase.performance").get())
+ add("implementation", platform(bom))
+ "implementation"(libs.findLibrary("firebase.analytics").get())
+ "implementation"(libs.findLibrary("firebase.performance").get())
+ "implementation"(libs.findLibrary("firebase.crashlytics").get())
+ }
+
+ extensions.configure {
+ finalizeDsl {
+ it.buildTypes.forEach { buildType ->
+ // Disable the Crashlytics mapping file upload. This feature should only be
+ // enabled if a Firebase backend is available and configured in
+ // google-services.json.
+ buildType.configure {
+ mappingFileUploadEnabled = false
+ }
+ }
+ }
}
}
}
diff --git a/build-logic/convention/src/main/kotlin/AndroidApplicationFlavorsConventionPlugin.kt b/build-logic/convention/src/main/kotlin/AndroidApplicationFlavorsConventionPlugin.kt
new file mode 100644
index 000000000..46b019d7a
--- /dev/null
+++ b/build-logic/convention/src/main/kotlin/AndroidApplicationFlavorsConventionPlugin.kt
@@ -0,0 +1,31 @@
+/*
+ * Copyright 2023 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * https://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+import com.android.build.api.dsl.ApplicationExtension
+import com.google.samples.apps.nowinandroid.configureFlavors
+import org.gradle.api.Plugin
+import org.gradle.api.Project
+import org.gradle.kotlin.dsl.configure
+
+class AndroidApplicationFlavorsConventionPlugin : Plugin {
+ override fun apply(target: Project) {
+ with(target) {
+ extensions.configure {
+ configureFlavors(this)
+ }
+ }
+ }
+}
\ No newline at end of file
diff --git a/build-logic/convention/src/main/kotlin/AndroidFeatureConventionPlugin.kt b/build-logic/convention/src/main/kotlin/AndroidFeatureConventionPlugin.kt
index a637b78c0..9c5ab9d44 100644
--- a/build-logic/convention/src/main/kotlin/AndroidFeatureConventionPlugin.kt
+++ b/build-logic/convention/src/main/kotlin/AndroidFeatureConventionPlugin.kt
@@ -46,6 +46,7 @@ class AndroidFeatureConventionPlugin : Plugin {
add("implementation", project(":core:data"))
add("implementation", project(":core:common"))
add("implementation", project(":core:domain"))
+ add("implementation", project(":core:analytics"))
add("testImplementation", kotlin("test"))
add("testImplementation", project(":core:testing"))
diff --git a/build-logic/convention/src/main/kotlin/AndroidLibraryConventionPlugin.kt b/build-logic/convention/src/main/kotlin/AndroidLibraryConventionPlugin.kt
index f7d6d184a..6132e07b5 100644
--- a/build-logic/convention/src/main/kotlin/AndroidLibraryConventionPlugin.kt
+++ b/build-logic/convention/src/main/kotlin/AndroidLibraryConventionPlugin.kt
@@ -14,7 +14,6 @@
* limitations under the License.
*/
-import com.android.build.api.variant.AndroidComponentsExtension
import com.android.build.api.variant.LibraryAndroidComponentsExtension
import com.android.build.gradle.LibraryExtension
import com.google.samples.apps.nowinandroid.configureFlavors
diff --git a/build.gradle.kts b/build.gradle.kts
index bd395ceac..ca7937fb5 100644
--- a/build.gradle.kts
+++ b/build.gradle.kts
@@ -22,12 +22,6 @@ buildscript {
// Android Build Server
maven { url = uri("../nowinandroid-prebuilts/m2repository") }
}
-
- dependencies {
- if (project.hasProperty("use-google-services")) {
- classpath(libs.firebase.crashlytics.gradle)
- }
- }
}
// Lists all plugins used throughout the project without applying them.
diff --git a/core/analytics/.gitignore b/core/analytics/.gitignore
new file mode 100644
index 000000000..42afabfd2
--- /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 000000000..e42499769
--- /dev/null
+++ b/core/analytics/build.gradle.kts
@@ -0,0 +1,33 @@
+/*
+ * Copyright 2023 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * https://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+plugins {
+ id("nowinandroid.android.library")
+ id("nowinandroid.android.library.compose")
+ id("nowinandroid.android.hilt")
+}
+
+android {
+ namespace = "com.google.samples.apps.nowinandroid.core.analytics"
+}
+
+dependencies {
+ implementation(libs.kotlinx.coroutines.android)
+ implementation(libs.androidx.compose.runtime)
+ implementation(libs.androidx.core.ktx)
+
+ implementation(platform(libs.firebase.bom))
+ implementation(libs.firebase.analytics)
+}
diff --git a/core/analytics/src/demo/java/com/google/samples/apps/nowinandroid/core/analytics/AnalyticsModule.kt b/core/analytics/src/demo/java/com/google/samples/apps/nowinandroid/core/analytics/AnalyticsModule.kt
new file mode 100644
index 000000000..78ebec9e5
--- /dev/null
+++ b/core/analytics/src/demo/java/com/google/samples/apps/nowinandroid/core/analytics/AnalyticsModule.kt
@@ -0,0 +1,29 @@
+/*
+ * Copyright 2023 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * https://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.google.samples.apps.nowinandroid.core.analytics
+
+import dagger.Binds
+import dagger.Module
+import dagger.hilt.InstallIn
+import dagger.hilt.components.SingletonComponent
+
+@Module
+@InstallIn(SingletonComponent::class)
+abstract class AnalyticsModule {
+ @Binds
+ abstract fun bindsAnalyticsHelper(analyticsHelperImpl: StubAnalyticsHelper): AnalyticsHelper
+}
diff --git a/core/analytics/src/main/AndroidManifest.xml b/core/analytics/src/main/AndroidManifest.xml
new file mode 100644
index 000000000..a522a4c23
--- /dev/null
+++ b/core/analytics/src/main/AndroidManifest.xml
@@ -0,0 +1,17 @@
+
+
+
\ No newline at end of file
diff --git a/core/analytics/src/main/java/com/google/samples/apps/nowinandroid/core/analytics/AnalyticsEvent.kt b/core/analytics/src/main/java/com/google/samples/apps/nowinandroid/core/analytics/AnalyticsEvent.kt
new file mode 100644
index 000000000..97ae76b56
--- /dev/null
+++ b/core/analytics/src/main/java/com/google/samples/apps/nowinandroid/core/analytics/AnalyticsEvent.kt
@@ -0,0 +1,58 @@
+/*
+ * Copyright 2023 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * https://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.google.samples.apps.nowinandroid.core.analytics
+
+/**
+ * Represents an analytics event.
+ *
+ * @param type - the event type. Wherever possible use one of the standard
+ * event `Types`, however, if there is no suitable event type already defined, a custom event can be
+ * defined as long as it is configured in your backend analytics system (for example, by creating a
+ * Firebase Analytics custom event).
+ *
+ * @param extras - list of parameters which supply additional context to the event. See `Param`.
+ */
+data class AnalyticsEvent(
+ val type: String,
+ val extras: List = emptyList(),
+) {
+ // Standard analytics types.
+ class Types {
+ companion object {
+ const val SCREEN_VIEW = "screen_view" // (extras: SCREEN_NAME)
+ }
+ }
+
+ /**
+ * A key-value pair used to supply extra context to an analytics event.
+ *
+ * @param key - the parameter key. Wherever possible use one of the standard `ParamKeys`,
+ * however, if no suitable key is available you can define your own as long as it is configured
+ * in your backend analytics system (for example, by creating a Firebase Analytics custom
+ * parameter).
+ *
+ * @param value - the parameter value.
+ */
+ data class Param(val key: String, val value: String)
+
+ // Standard parameter keys.
+ class ParamKeys {
+ companion object {
+ const val SCREEN_NAME = "screen_name"
+ }
+ }
+}
diff --git a/core/analytics/src/main/java/com/google/samples/apps/nowinandroid/core/analytics/AnalyticsHelper.kt b/core/analytics/src/main/java/com/google/samples/apps/nowinandroid/core/analytics/AnalyticsHelper.kt
new file mode 100644
index 000000000..f9e6dad44
--- /dev/null
+++ b/core/analytics/src/main/java/com/google/samples/apps/nowinandroid/core/analytics/AnalyticsHelper.kt
@@ -0,0 +1,25 @@
+/*
+ * Copyright 2023 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * https://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.google.samples.apps.nowinandroid.core.analytics
+
+/**
+ * Interface for logging analytics events. See `FirebaseAnalyticsHelper` and
+ * `StubAnalyticsHelper` for implementations.
+ */
+interface AnalyticsHelper {
+ fun logEvent(event: AnalyticsEvent)
+}
diff --git a/core/analytics/src/main/java/com/google/samples/apps/nowinandroid/core/analytics/NoOpAnalyticsHelper.kt b/core/analytics/src/main/java/com/google/samples/apps/nowinandroid/core/analytics/NoOpAnalyticsHelper.kt
new file mode 100644
index 000000000..16a193439
--- /dev/null
+++ b/core/analytics/src/main/java/com/google/samples/apps/nowinandroid/core/analytics/NoOpAnalyticsHelper.kt
@@ -0,0 +1,24 @@
+/*
+ * Copyright 2023 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * https://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.google.samples.apps.nowinandroid.core.analytics
+
+/**
+ * Implementation of AnalyticsHelper which does nothing. Useful for tests and previews.
+ */
+class NoOpAnalyticsHelper : AnalyticsHelper {
+ override fun logEvent(event: AnalyticsEvent) = Unit
+}
diff --git a/core/analytics/src/main/java/com/google/samples/apps/nowinandroid/core/analytics/StubAnalyticsHelper.kt b/core/analytics/src/main/java/com/google/samples/apps/nowinandroid/core/analytics/StubAnalyticsHelper.kt
new file mode 100644
index 000000000..2ff022287
--- /dev/null
+++ b/core/analytics/src/main/java/com/google/samples/apps/nowinandroid/core/analytics/StubAnalyticsHelper.kt
@@ -0,0 +1,34 @@
+/*
+ * Copyright 2023 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * https://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.google.samples.apps.nowinandroid.core.analytics
+
+import android.util.Log
+import javax.inject.Inject
+import javax.inject.Singleton
+
+private const val TAG = "StubAnalyticsHelper"
+
+/**
+ * An implementation of AnalyticsHelper just writes the events to logcat. Used in builds where no
+ * analytics events should be sent to a backend.
+ */
+@Singleton
+class StubAnalyticsHelper @Inject constructor() : AnalyticsHelper {
+ override fun logEvent(event: AnalyticsEvent) {
+ Log.d(TAG, "Received analytics event: $event")
+ }
+}
diff --git a/core/analytics/src/main/java/com/google/samples/apps/nowinandroid/core/analytics/UiHelpers.kt b/core/analytics/src/main/java/com/google/samples/apps/nowinandroid/core/analytics/UiHelpers.kt
new file mode 100644
index 000000000..b0e5d29d8
--- /dev/null
+++ b/core/analytics/src/main/java/com/google/samples/apps/nowinandroid/core/analytics/UiHelpers.kt
@@ -0,0 +1,28 @@
+/*
+ * Copyright 2023 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * https://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.google.samples.apps.nowinandroid.core.analytics
+
+import androidx.compose.runtime.staticCompositionLocalOf
+
+/**
+ * Global key used to obtain access to the AnalyticsHelper through a CompositionLocal.
+ */
+val LocalAnalyticsHelper = staticCompositionLocalOf {
+ // Provide a default AnalyticsHelper which does nothing. This is so that tests and previews
+ // do not have to provide one. For real app builds provide a different implementation.
+ NoOpAnalyticsHelper()
+}
diff --git a/core/analytics/src/prod/java/com/google/samples/apps/nowinandroid/core/analytics/AnalyticsModule.kt b/core/analytics/src/prod/java/com/google/samples/apps/nowinandroid/core/analytics/AnalyticsModule.kt
new file mode 100644
index 000000000..e947d036b
--- /dev/null
+++ b/core/analytics/src/prod/java/com/google/samples/apps/nowinandroid/core/analytics/AnalyticsModule.kt
@@ -0,0 +1,29 @@
+/*
+ * Copyright 2023 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * https://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.google.samples.apps.nowinandroid.core.analytics
+
+import dagger.Binds
+import dagger.Module
+import dagger.hilt.InstallIn
+import dagger.hilt.components.SingletonComponent
+
+@Module
+@InstallIn(SingletonComponent::class)
+abstract class AnalyticsModule {
+ @Binds
+ abstract fun bindsAnalyticsHelper(analyticsHelperImpl: FirebaseAnalyticsHelper): AnalyticsHelper
+}
diff --git a/core/analytics/src/prod/java/com/google/samples/apps/nowinandroid/core/analytics/FirebaseAnalyticsHelper.kt b/core/analytics/src/prod/java/com/google/samples/apps/nowinandroid/core/analytics/FirebaseAnalyticsHelper.kt
new file mode 100644
index 000000000..96b9ce67d
--- /dev/null
+++ b/core/analytics/src/prod/java/com/google/samples/apps/nowinandroid/core/analytics/FirebaseAnalyticsHelper.kt
@@ -0,0 +1,44 @@
+/*
+ * Copyright 2023 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * https://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.google.samples.apps.nowinandroid.core.analytics
+
+import com.google.firebase.analytics.ktx.analytics
+import com.google.firebase.analytics.ktx.logEvent
+import com.google.firebase.ktx.Firebase
+import javax.inject.Inject
+import javax.inject.Singleton
+
+/**
+ * Implementation of `AnalyticsHelper` which logs events to a Firebase backend.
+ */
+@Singleton
+class FirebaseAnalyticsHelper @Inject constructor() : AnalyticsHelper {
+
+ private val firebaseAnalytics = Firebase.analytics
+
+ override fun logEvent(event: AnalyticsEvent) {
+ firebaseAnalytics.logEvent(event.type) {
+ for (extra in event.extras) {
+ // Truncate parameter keys and values according to firebase maximum length values.
+ param(
+ key = extra.key.take(40),
+ value = extra.value.take(100),
+ )
+ }
+ }
+ }
+}
diff --git a/core/data/build.gradle.kts b/core/data/build.gradle.kts
index 717082bfe..5b468c43e 100644
--- a/core/data/build.gradle.kts
+++ b/core/data/build.gradle.kts
@@ -35,6 +35,7 @@ dependencies {
implementation(project(":core:database"))
implementation(project(":core:datastore"))
implementation(project(":core:network"))
+ implementation(project(":core:analytics"))
testImplementation(project(":core:testing"))
testImplementation(project(":core:datastore-test"))
diff --git a/core/data/src/main/java/com/google/samples/apps/nowinandroid/core/data/repository/AnalyticsExtensions.kt b/core/data/src/main/java/com/google/samples/apps/nowinandroid/core/data/repository/AnalyticsExtensions.kt
new file mode 100644
index 000000000..d36f509d9
--- /dev/null
+++ b/core/data/src/main/java/com/google/samples/apps/nowinandroid/core/data/repository/AnalyticsExtensions.kt
@@ -0,0 +1,84 @@
+/*
+ * Copyright 2023 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * https://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.google.samples.apps.nowinandroid.core.data.repository
+
+import com.google.samples.apps.nowinandroid.core.analytics.AnalyticsEvent
+import com.google.samples.apps.nowinandroid.core.analytics.AnalyticsEvent.Param
+import com.google.samples.apps.nowinandroid.core.analytics.AnalyticsHelper
+
+fun AnalyticsHelper.logNewsResourceBookmarkToggled(newsResourceId: String, isBookmarked: Boolean) {
+ val eventType = if (isBookmarked) "news_resource_saved" else "news_resource_unsaved"
+ val paramKey = if (isBookmarked) "saved_news_resource_id" else "unsaved_news_resource_id"
+ logEvent(
+ AnalyticsEvent(
+ type = eventType,
+ extras = listOf(
+ Param(key = paramKey, value = newsResourceId),
+ ),
+ ),
+ )
+}
+
+fun AnalyticsHelper.logTopicFollowToggled(followedTopicId: String, isFollowed: Boolean) {
+ val eventType = if (isFollowed) "topic_followed" else "topic_unfollowed"
+ val paramKey = if (isFollowed) "followed_topic_id" else "unfollowed_topic_id"
+ logEvent(
+ AnalyticsEvent(
+ type = eventType,
+ extras = listOf(
+ Param(key = paramKey, value = followedTopicId),
+ ),
+ ),
+ )
+}
+
+fun AnalyticsHelper.logThemeChanged(themeName: String) =
+ logEvent(
+ AnalyticsEvent(
+ type = "theme_changed",
+ extras = listOf(
+ Param(key = "theme_name", value = themeName),
+ ),
+ ),
+ )
+
+fun AnalyticsHelper.logDarkThemeConfigChanged(darkThemeConfigName: String) =
+ logEvent(
+ AnalyticsEvent(
+ type = "dark_theme_config_changed",
+ extras = listOf(
+ Param(key = "dark_theme_config", value = darkThemeConfigName),
+ ),
+ ),
+ )
+
+fun AnalyticsHelper.logDynamicColorPreferenceChanged(useDynamicColor: Boolean) =
+ logEvent(
+ AnalyticsEvent(
+ type = "dynamic_color_preference_changed",
+ extras = listOf(
+ Param(key = "dynamic_color_preference", value = useDynamicColor.toString()),
+ ),
+ ),
+ )
+
+fun AnalyticsHelper.logOnboardingStateChanged(shouldHideOnboarding: Boolean) {
+ val eventType = if (shouldHideOnboarding) "onboarding_complete" else "onboarding_reset"
+ logEvent(
+ AnalyticsEvent(type = eventType),
+ )
+}
diff --git a/core/data/src/main/java/com/google/samples/apps/nowinandroid/core/data/repository/OfflineFirstUserDataRepository.kt b/core/data/src/main/java/com/google/samples/apps/nowinandroid/core/data/repository/OfflineFirstUserDataRepository.kt
index 200ca4a3d..334209538 100644
--- a/core/data/src/main/java/com/google/samples/apps/nowinandroid/core/data/repository/OfflineFirstUserDataRepository.kt
+++ b/core/data/src/main/java/com/google/samples/apps/nowinandroid/core/data/repository/OfflineFirstUserDataRepository.kt
@@ -16,6 +16,8 @@
package com.google.samples.apps.nowinandroid.core.data.repository
+import androidx.annotation.VisibleForTesting
+import com.google.samples.apps.nowinandroid.core.analytics.AnalyticsHelper
import com.google.samples.apps.nowinandroid.core.datastore.NiaPreferencesDataSource
import com.google.samples.apps.nowinandroid.core.model.data.DarkThemeConfig
import com.google.samples.apps.nowinandroid.core.model.data.ThemeBrand
@@ -25,29 +27,46 @@ import javax.inject.Inject
class OfflineFirstUserDataRepository @Inject constructor(
private val niaPreferencesDataSource: NiaPreferencesDataSource,
+ private val analyticsHelper: AnalyticsHelper,
) : UserDataRepository {
override val userData: Flow =
niaPreferencesDataSource.userData
+ @VisibleForTesting
override suspend fun setFollowedTopicIds(followedTopicIds: Set) =
niaPreferencesDataSource.setFollowedTopicIds(followedTopicIds)
- override suspend fun toggleFollowedTopicId(followedTopicId: String, followed: Boolean) =
+ override suspend fun toggleFollowedTopicId(followedTopicId: String, followed: Boolean) {
niaPreferencesDataSource.toggleFollowedTopicId(followedTopicId, followed)
+ analyticsHelper.logTopicFollowToggled(followedTopicId, followed)
+ }
- override suspend fun updateNewsResourceBookmark(newsResourceId: String, bookmarked: Boolean) =
+ override suspend fun updateNewsResourceBookmark(newsResourceId: String, bookmarked: Boolean) {
niaPreferencesDataSource.toggleNewsResourceBookmark(newsResourceId, bookmarked)
+ analyticsHelper.logNewsResourceBookmarkToggled(
+ newsResourceId = newsResourceId,
+ isBookmarked = bookmarked,
+ )
+ }
- override suspend fun setThemeBrand(themeBrand: ThemeBrand) =
+ override suspend fun setThemeBrand(themeBrand: ThemeBrand) {
niaPreferencesDataSource.setThemeBrand(themeBrand)
+ analyticsHelper.logThemeChanged(themeBrand.name)
+ }
- override suspend fun setDarkThemeConfig(darkThemeConfig: DarkThemeConfig) =
+ override suspend fun setDarkThemeConfig(darkThemeConfig: DarkThemeConfig) {
niaPreferencesDataSource.setDarkThemeConfig(darkThemeConfig)
+ analyticsHelper.logDarkThemeConfigChanged(darkThemeConfig.name)
+ }
- override suspend fun setDynamicColorPreference(useDynamicColor: Boolean) =
+ override suspend fun setDynamicColorPreference(useDynamicColor: Boolean) {
niaPreferencesDataSource.setDynamicColorPreference(useDynamicColor)
+ analyticsHelper.logDynamicColorPreferenceChanged(useDynamicColor)
+ }
- override suspend fun setShouldHideOnboarding(shouldHideOnboarding: Boolean) =
+ override suspend fun setShouldHideOnboarding(shouldHideOnboarding: Boolean) {
niaPreferencesDataSource.setShouldHideOnboarding(shouldHideOnboarding)
+ analyticsHelper.logOnboardingStateChanged(shouldHideOnboarding)
+ }
}
diff --git a/core/data/src/test/java/com/google/samples/apps/nowinandroid/core/data/repository/OfflineFirstUserDataRepositoryTest.kt b/core/data/src/test/java/com/google/samples/apps/nowinandroid/core/data/repository/OfflineFirstUserDataRepositoryTest.kt
index 055d8e074..61569d650 100644
--- a/core/data/src/test/java/com/google/samples/apps/nowinandroid/core/data/repository/OfflineFirstUserDataRepositoryTest.kt
+++ b/core/data/src/test/java/com/google/samples/apps/nowinandroid/core/data/repository/OfflineFirstUserDataRepositoryTest.kt
@@ -16,6 +16,7 @@
package com.google.samples.apps.nowinandroid.core.data.repository
+import com.google.samples.apps.nowinandroid.core.analytics.NoOpAnalyticsHelper
import com.google.samples.apps.nowinandroid.core.datastore.NiaPreferencesDataSource
import com.google.samples.apps.nowinandroid.core.datastore.test.testUserPreferencesDataStore
import com.google.samples.apps.nowinandroid.core.model.data.DarkThemeConfig
@@ -37,6 +38,8 @@ class OfflineFirstUserDataRepositoryTest {
private lateinit var niaPreferencesDataSource: NiaPreferencesDataSource
+ private val analyticsHelper = NoOpAnalyticsHelper()
+
@get:Rule
val tmpFolder: TemporaryFolder = TemporaryFolder.builder().assureDeletion().build()
@@ -48,6 +51,7 @@ class OfflineFirstUserDataRepositoryTest {
subject = OfflineFirstUserDataRepository(
niaPreferencesDataSource = niaPreferencesDataSource,
+ analyticsHelper,
)
}
diff --git a/core/ui/build.gradle.kts b/core/ui/build.gradle.kts
index 39f9bcff1..0438b8f36 100644
--- a/core/ui/build.gradle.kts
+++ b/core/ui/build.gradle.kts
@@ -30,6 +30,7 @@ dependencies {
implementation(project(":core:designsystem"))
implementation(project(":core:model"))
implementation(project(":core:domain"))
+ implementation(project(":core:analytics"))
implementation(libs.androidx.browser)
implementation(libs.androidx.core.ktx)
diff --git a/core/ui/src/main/java/com/google/samples/apps/nowinandroid/core/ui/AnalyticsExtensions.kt b/core/ui/src/main/java/com/google/samples/apps/nowinandroid/core/ui/AnalyticsExtensions.kt
new file mode 100644
index 000000000..38bce838a
--- /dev/null
+++ b/core/ui/src/main/java/com/google/samples/apps/nowinandroid/core/ui/AnalyticsExtensions.kt
@@ -0,0 +1,63 @@
+/*
+ * Copyright 2023 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * https://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.google.samples.apps.nowinandroid.core.ui
+
+import androidx.compose.runtime.Composable
+import androidx.compose.runtime.DisposableEffect
+import com.google.samples.apps.nowinandroid.core.analytics.AnalyticsEvent
+import com.google.samples.apps.nowinandroid.core.analytics.AnalyticsEvent.Param
+import com.google.samples.apps.nowinandroid.core.analytics.AnalyticsEvent.ParamKeys
+import com.google.samples.apps.nowinandroid.core.analytics.AnalyticsEvent.Types
+import com.google.samples.apps.nowinandroid.core.analytics.AnalyticsHelper
+import com.google.samples.apps.nowinandroid.core.analytics.LocalAnalyticsHelper
+
+/**
+ * Classes and functions associated with analytics events for the UI.
+ */
+fun AnalyticsHelper.logScreenView(screenName: String) {
+ logEvent(
+ AnalyticsEvent(
+ type = Types.SCREEN_VIEW,
+ extras = listOf(
+ Param(ParamKeys.SCREEN_NAME, screenName),
+ ),
+ ),
+ )
+}
+
+fun AnalyticsHelper.logNewsResourceOpened(newsResourceId: String, newsResourceTitle: String) {
+ logEvent(
+ event = AnalyticsEvent(
+ type = "news_resource_opened",
+ extras = listOf(
+ Param("opened_news_resource", newsResourceId),
+ ),
+ ),
+ )
+}
+
+/**
+ * A side-effect which records a screen view event.
+ */
+@Composable
+fun TrackScreenViewEvent(
+ screenName: String,
+ analyticsHelper: AnalyticsHelper = LocalAnalyticsHelper.current,
+) = DisposableEffect(Unit) {
+ analyticsHelper.logScreenView(screenName)
+ onDispose {}
+}
diff --git a/core/ui/src/main/java/com/google/samples/apps/nowinandroid/core/ui/NewsFeed.kt b/core/ui/src/main/java/com/google/samples/apps/nowinandroid/core/ui/NewsFeed.kt
index 73c1a21e4..bad60961b 100644
--- a/core/ui/src/main/java/com/google/samples/apps/nowinandroid/core/ui/NewsFeed.kt
+++ b/core/ui/src/main/java/com/google/samples/apps/nowinandroid/core/ui/NewsFeed.kt
@@ -37,6 +37,7 @@ import androidx.compose.ui.tooling.preview.Devices
import androidx.compose.ui.tooling.preview.Preview
import androidx.compose.ui.tooling.preview.PreviewParameter
import androidx.compose.ui.unit.dp
+import com.google.samples.apps.nowinandroid.core.analytics.LocalAnalyticsHelper
import com.google.samples.apps.nowinandroid.core.designsystem.theme.NiaTheme
import com.google.samples.apps.nowinandroid.core.domain.model.UserNewsResource
@@ -56,12 +57,19 @@ fun LazyGridScope.newsFeed(
mutableStateOf(Uri.parse(userNewsResource.url))
}
val context = LocalContext.current
+ val analyticsHelper = LocalAnalyticsHelper.current
val backgroundColor = MaterialTheme.colorScheme.background.toArgb()
NewsResourceCardExpanded(
userNewsResource = userNewsResource,
isBookmarked = userNewsResource.isSaved,
- onClick = { launchCustomChromeTab(context, resourceUrl, backgroundColor) },
+ onClick = {
+ analyticsHelper.logNewsResourceOpened(
+ newsResourceId = userNewsResource.id,
+ newsResourceTitle = userNewsResource.title,
+ )
+ launchCustomChromeTab(context, resourceUrl, backgroundColor)
+ },
onToggleBookmark = {
onNewsResourcesCheckedChanged(
userNewsResource.id,
diff --git a/core/ui/src/main/java/com/google/samples/apps/nowinandroid/core/ui/NewsResourceCardList.kt b/core/ui/src/main/java/com/google/samples/apps/nowinandroid/core/ui/NewsResourceCardList.kt
index a63bae657..87f110b36 100644
--- a/core/ui/src/main/java/com/google/samples/apps/nowinandroid/core/ui/NewsResourceCardList.kt
+++ b/core/ui/src/main/java/com/google/samples/apps/nowinandroid/core/ui/NewsResourceCardList.kt
@@ -23,6 +23,7 @@ import androidx.compose.material3.MaterialTheme
import androidx.compose.ui.Modifier
import androidx.compose.ui.graphics.toArgb
import androidx.compose.ui.platform.LocalContext
+import com.google.samples.apps.nowinandroid.core.analytics.LocalAnalyticsHelper
import com.google.samples.apps.nowinandroid.core.domain.model.UserNewsResource
/**
@@ -45,12 +46,17 @@ fun LazyListScope.userNewsResourceCardItems(
val resourceUrl = Uri.parse(userNewsResource.url)
val backgroundColor = MaterialTheme.colorScheme.background.toArgb()
val context = LocalContext.current
+ val analyticsHelper = LocalAnalyticsHelper.current
NewsResourceCardExpanded(
userNewsResource = userNewsResource,
isBookmarked = userNewsResource.isSaved,
onToggleBookmark = { onToggleBookmark(userNewsResource) },
onClick = {
+ analyticsHelper.logNewsResourceOpened(
+ newsResourceId = userNewsResource.id,
+ newsResourceTitle = userNewsResource.title,
+ )
when (onItemClick) {
null -> launchCustomChromeTab(context, resourceUrl, backgroundColor)
else -> onItemClick(userNewsResource)
diff --git a/feature/bookmarks/src/main/java/com/google/samples/apps/nowinandroid/feature/bookmarks/BookmarksScreen.kt b/feature/bookmarks/src/main/java/com/google/samples/apps/nowinandroid/feature/bookmarks/BookmarksScreen.kt
index 1169f5777..a5d290897 100644
--- a/feature/bookmarks/src/main/java/com/google/samples/apps/nowinandroid/feature/bookmarks/BookmarksScreen.kt
+++ b/feature/bookmarks/src/main/java/com/google/samples/apps/nowinandroid/feature/bookmarks/BookmarksScreen.kt
@@ -59,6 +59,7 @@ import com.google.samples.apps.nowinandroid.core.domain.model.UserNewsResource
import com.google.samples.apps.nowinandroid.core.ui.NewsFeedUiState
import com.google.samples.apps.nowinandroid.core.ui.NewsFeedUiState.Loading
import com.google.samples.apps.nowinandroid.core.ui.NewsFeedUiState.Success
+import com.google.samples.apps.nowinandroid.core.ui.TrackScreenViewEvent
import com.google.samples.apps.nowinandroid.core.ui.TrackScrollJank
import com.google.samples.apps.nowinandroid.core.ui.UserNewsResourcePreviewParameterProvider
import com.google.samples.apps.nowinandroid.core.ui.newsFeed
@@ -95,6 +96,7 @@ internal fun BookmarksScreen(
EmptyState(modifier)
}
}
+ TrackScreenViewEvent(screenName = "Saved")
}
@Composable
diff --git a/feature/foryou/src/main/java/com/google/samples/apps/nowinandroid/feature/foryou/ForYouScreen.kt b/feature/foryou/src/main/java/com/google/samples/apps/nowinandroid/feature/foryou/ForYouScreen.kt
index 492660142..52de0204b 100644
--- a/feature/foryou/src/main/java/com/google/samples/apps/nowinandroid/feature/foryou/ForYouScreen.kt
+++ b/feature/foryou/src/main/java/com/google/samples/apps/nowinandroid/feature/foryou/ForYouScreen.kt
@@ -85,6 +85,7 @@ import com.google.samples.apps.nowinandroid.core.designsystem.theme.NiaTheme
import com.google.samples.apps.nowinandroid.core.domain.model.UserNewsResource
import com.google.samples.apps.nowinandroid.core.ui.DevicePreviews
import com.google.samples.apps.nowinandroid.core.ui.NewsFeedUiState
+import com.google.samples.apps.nowinandroid.core.ui.TrackScreenViewEvent
import com.google.samples.apps.nowinandroid.core.ui.TrackScrollJank
import com.google.samples.apps.nowinandroid.core.ui.UserNewsResourcePreviewParameterProvider
import com.google.samples.apps.nowinandroid.core.ui.newsFeed
@@ -207,6 +208,7 @@ internal fun ForYouScreen(
)
}
}
+ TrackScreenViewEvent(screenName = "ForYou")
}
/**
diff --git a/feature/interests/src/main/java/com/google/samples/apps/nowinandroid/feature/interests/InterestsScreen.kt b/feature/interests/src/main/java/com/google/samples/apps/nowinandroid/feature/interests/InterestsScreen.kt
index fd45c7608..c8558cb2b 100644
--- a/feature/interests/src/main/java/com/google/samples/apps/nowinandroid/feature/interests/InterestsScreen.kt
+++ b/feature/interests/src/main/java/com/google/samples/apps/nowinandroid/feature/interests/InterestsScreen.kt
@@ -33,6 +33,7 @@ import com.google.samples.apps.nowinandroid.core.designsystem.theme.NiaTheme
import com.google.samples.apps.nowinandroid.core.domain.model.FollowableTopic
import com.google.samples.apps.nowinandroid.core.ui.DevicePreviews
import com.google.samples.apps.nowinandroid.core.ui.FollowableTopicPreviewParameterProvider
+import com.google.samples.apps.nowinandroid.core.ui.TrackScreenViewEvent
@OptIn(ExperimentalLifecycleComposeApi::class)
@Composable
@@ -78,6 +79,7 @@ internal fun InterestsScreen(
is InterestsUiState.Empty -> InterestsEmptyScreen()
}
}
+ TrackScreenViewEvent(screenName = "Interests")
}
@Composable
diff --git a/feature/settings/src/main/java/com/google/samples/apps/nowinandroid/feature/settings/SettingsDialog.kt b/feature/settings/src/main/java/com/google/samples/apps/nowinandroid/feature/settings/SettingsDialog.kt
index bed230d0d..f6620d00e 100644
--- a/feature/settings/src/main/java/com/google/samples/apps/nowinandroid/feature/settings/SettingsDialog.kt
+++ b/feature/settings/src/main/java/com/google/samples/apps/nowinandroid/feature/settings/SettingsDialog.kt
@@ -61,6 +61,7 @@ import com.google.samples.apps.nowinandroid.core.model.data.DarkThemeConfig.LIGH
import com.google.samples.apps.nowinandroid.core.model.data.ThemeBrand
import com.google.samples.apps.nowinandroid.core.model.data.ThemeBrand.ANDROID
import com.google.samples.apps.nowinandroid.core.model.data.ThemeBrand.DEFAULT
+import com.google.samples.apps.nowinandroid.core.ui.TrackScreenViewEvent
import com.google.samples.apps.nowinandroid.feature.settings.R.string
import com.google.samples.apps.nowinandroid.feature.settings.SettingsUiState.Loading
import com.google.samples.apps.nowinandroid.feature.settings.SettingsUiState.Success
@@ -134,6 +135,7 @@ fun SettingsDialog(
Divider(Modifier.padding(top = 8.dp))
LinksPanel()
}
+ TrackScreenViewEvent(screenName = "Settings")
},
confirmButton = {
Text(
diff --git a/feature/topic/src/main/java/com/google/samples/apps/nowinandroid/feature/topic/TopicScreen.kt b/feature/topic/src/main/java/com/google/samples/apps/nowinandroid/feature/topic/TopicScreen.kt
index e7b218072..37e76daec 100644
--- a/feature/topic/src/main/java/com/google/samples/apps/nowinandroid/feature/topic/TopicScreen.kt
+++ b/feature/topic/src/main/java/com/google/samples/apps/nowinandroid/feature/topic/TopicScreen.kt
@@ -55,6 +55,7 @@ import com.google.samples.apps.nowinandroid.core.designsystem.theme.NiaTheme
import com.google.samples.apps.nowinandroid.core.domain.model.FollowableTopic
import com.google.samples.apps.nowinandroid.core.domain.model.UserNewsResource
import com.google.samples.apps.nowinandroid.core.ui.DevicePreviews
+import com.google.samples.apps.nowinandroid.core.ui.TrackScreenViewEvent
import com.google.samples.apps.nowinandroid.core.ui.TrackScrollJank
import com.google.samples.apps.nowinandroid.core.ui.UserNewsResourcePreviewParameterProvider
import com.google.samples.apps.nowinandroid.core.ui.userNewsResourceCardItems
@@ -71,6 +72,7 @@ internal fun TopicRoute(
val topicUiState: TopicUiState by viewModel.topicUiState.collectAsStateWithLifecycle()
val newsUiState: NewsUiState by viewModel.newUiState.collectAsStateWithLifecycle()
+ TrackScreenViewEvent(screenName = "Topic: ${viewModel.topicId}")
TopicScreen(
topicUiState = topicUiState,
newsUiState = newsUiState,
diff --git a/feature/topic/src/main/java/com/google/samples/apps/nowinandroid/feature/topic/TopicViewModel.kt b/feature/topic/src/main/java/com/google/samples/apps/nowinandroid/feature/topic/TopicViewModel.kt
index c0c6bbafd..c58d40b5e 100644
--- a/feature/topic/src/main/java/com/google/samples/apps/nowinandroid/feature/topic/TopicViewModel.kt
+++ b/feature/topic/src/main/java/com/google/samples/apps/nowinandroid/feature/topic/TopicViewModel.kt
@@ -50,6 +50,8 @@ class TopicViewModel @Inject constructor(
private val topicArgs: TopicArgs = TopicArgs(savedStateHandle, stringDecoder)
+ val topicId = topicArgs.topicId
+
val topicUiState: StateFlow = topicUiState(
topicId = topicArgs.topicId,
userDataRepository = userDataRepository,
diff --git a/feature/topic/src/test/java/com/google/samples/apps/nowinandroid/feature/topic/TopicViewModelTest.kt b/feature/topic/src/test/java/com/google/samples/apps/nowinandroid/feature/topic/TopicViewModelTest.kt
index a8f1b0a88..dfed60385 100644
--- a/feature/topic/src/test/java/com/google/samples/apps/nowinandroid/feature/topic/TopicViewModelTest.kt
+++ b/feature/topic/src/test/java/com/google/samples/apps/nowinandroid/feature/topic/TopicViewModelTest.kt
@@ -70,6 +70,10 @@ class TopicViewModelTest {
)
}
+ @Test
+ fun topicId_matchesTopicIdFromSavedStateHandle() =
+ assertEquals(testInputTopics[0].topic.id, viewModel.topicId)
+
@Test
fun uiStateTopic_whenSuccess_matchesTopicFromRepository() = runTest {
val collectJob = launch(UnconfinedTestDispatcher()) { viewModel.topicUiState.collect() }
diff --git a/gradle/libs.versions.toml b/gradle/libs.versions.toml
index 00fdd422f..2716a1f77 100644
--- a/gradle/libs.versions.toml
+++ b/gradle/libs.versions.toml
@@ -28,7 +28,7 @@ androidxUiAutomator = "2.2.0"
androidxWindowManager = "1.0.0"
androidxWork = "2.7.1"
coil = "2.2.2"
-firebaseBom = "31.0.3"
+firebaseBom = "31.2.0"
firebaseCrashlyticsPlugin = "2.9.2"
firebasePerfPlugin = "1.4.2"
gmsPlugin = "4.3.14"
@@ -105,6 +105,7 @@ firebase-analytics = { group = "com.google.firebase", name = "firebase-analytics
firebase-crashlytics = { group = "com.google.firebase", name = "firebase-crashlytics-ktx"}
firebase-crashlytics-gradle = { group = "com.google.firebase", name="firebase-crashlytics-gradle", version.ref = "firebaseCrashlyticsPlugin"}
firebase-performance = { group = "com.google.firebase", name = "firebase-perf-ktx"}
+firebase-performance-gradle = { group = "com.google.firebase", name = "perf-plugin", version.ref = "firebasePerfPlugin"}
hilt-android = { group = "com.google.dagger", name = "hilt-android", version.ref = "hilt" }
hilt-android-testing = { group = "com.google.dagger", name = "hilt-android-testing", version.ref = "hilt" }
hilt-compiler = { group = "com.google.dagger", name = "hilt-android-compiler", version.ref = "hilt" }
diff --git a/settings.gradle.kts b/settings.gradle.kts
index 809efd27d..857f9d56c 100644
--- a/settings.gradle.kts
+++ b/settings.gradle.kts
@@ -46,6 +46,8 @@ include(":core:model")
include(":core:network")
include(":core:ui")
include(":core:testing")
+include(":core:analytics")
+
include(":feature:foryou")
include(":feature:interests")
include(":feature:bookmarks")
diff --git a/sync/work/build.gradle.kts b/sync/work/build.gradle.kts
index 70f6b2e89..a3b589db3 100644
--- a/sync/work/build.gradle.kts
+++ b/sync/work/build.gradle.kts
@@ -31,6 +31,7 @@ dependencies {
implementation(project(":core:model"))
implementation(project(":core:data"))
implementation(project(":core:datastore"))
+ implementation(project(":core:analytics"))
implementation(libs.kotlinx.coroutines.android)
diff --git a/sync/work/src/main/java/com/google/samples/apps/nowinandroid/sync/workers/AnalyticsExtensions.kt b/sync/work/src/main/java/com/google/samples/apps/nowinandroid/sync/workers/AnalyticsExtensions.kt
new file mode 100644
index 000000000..d5250b330
--- /dev/null
+++ b/sync/work/src/main/java/com/google/samples/apps/nowinandroid/sync/workers/AnalyticsExtensions.kt
@@ -0,0 +1,32 @@
+/*
+ * Copyright 2023 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * https://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.google.samples.apps.nowinandroid.sync.workers
+
+import com.google.samples.apps.nowinandroid.core.analytics.AnalyticsEvent
+import com.google.samples.apps.nowinandroid.core.analytics.AnalyticsHelper
+
+fun AnalyticsHelper.logSyncStarted() =
+ logEvent(
+ AnalyticsEvent(type = "network_sync_started"),
+ )
+
+fun AnalyticsHelper.logSyncFinished(syncedSuccessfully: Boolean) {
+ val eventType = if (syncedSuccessfully) "network_sync_successful" else "network_sync_failed"
+ logEvent(
+ AnalyticsEvent(type = eventType),
+ )
+}
diff --git a/sync/work/src/main/java/com/google/samples/apps/nowinandroid/sync/workers/SyncWorker.kt b/sync/work/src/main/java/com/google/samples/apps/nowinandroid/sync/workers/SyncWorker.kt
index c6ac6fb65..211940ddb 100644
--- a/sync/work/src/main/java/com/google/samples/apps/nowinandroid/sync/workers/SyncWorker.kt
+++ b/sync/work/src/main/java/com/google/samples/apps/nowinandroid/sync/workers/SyncWorker.kt
@@ -24,6 +24,7 @@ import androidx.work.ForegroundInfo
import androidx.work.OneTimeWorkRequestBuilder
import androidx.work.OutOfQuotaPolicy
import androidx.work.WorkerParameters
+import com.google.samples.apps.nowinandroid.core.analytics.AnalyticsHelper
import com.google.samples.apps.nowinandroid.core.data.Synchronizer
import com.google.samples.apps.nowinandroid.core.data.repository.NewsRepository
import com.google.samples.apps.nowinandroid.core.data.repository.TopicsRepository
@@ -52,6 +53,7 @@ class SyncWorker @AssistedInject constructor(
private val topicRepository: TopicsRepository,
private val newsRepository: NewsRepository,
@Dispatcher(IO) private val ioDispatcher: CoroutineDispatcher,
+ private val analyticsHelper: AnalyticsHelper,
) : CoroutineWorker(appContext, workerParams), Synchronizer {
override suspend fun getForegroundInfo(): ForegroundInfo =
@@ -59,12 +61,16 @@ class SyncWorker @AssistedInject constructor(
override suspend fun doWork(): Result = withContext(ioDispatcher) {
traceAsync("Sync", 0) {
+ analyticsHelper.logSyncStarted()
+
// First sync the repositories in parallel
val syncedSuccessfully = awaitAll(
async { topicRepository.sync() },
async { newsRepository.sync() },
).all { it }
+ analyticsHelper.logSyncFinished(syncedSuccessfully)
+
if (syncedSuccessfully) {
Result.success()
} else {