diff --git a/.google/BUILDME b/.google/BUILDME
new file mode 100644
index 000000000..5295ed188
--- /dev/null
+++ b/.google/BUILDME
@@ -0,0 +1,2 @@
+# This file can be used to trigger an internal build by changing the number below
+3
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 4163090ce..b7cb7d1d7 100644
--- a/app/build.gradle.kts
+++ b/app/build.gradle.kts
@@ -19,17 +19,18 @@ 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("jacoco")
- id("nowinandroid.firebase-perf")
+ id("nowinandroid.android.application.firebase")
}
android {
defaultConfig {
applicationId = "com.google.samples.apps.nowinandroid"
- versionCode = 4
- versionName = "0.0.4" // X.Y.Z; X = Major, Y = minor, Z = Patch level
+ versionCode = 5
+ versionName = "0.0.5" // X.Y.Z; X = Major, Y = minor, Z = Patch level
// Custom test runner to set up Hilt dependency graph
testInstrumentationRunner = "com.google.samples.apps.nowinandroid.core.testing.NiaTestRunner"
@@ -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..99c233910 100644
--- a/app/src/main/AndroidManifest.xml
+++ b/app/src/main/AndroidManifest.xml
@@ -19,6 +19,13 @@
+
+
+
+
+
+
+
+
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 0152a902b..0b929d4f7 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)
compileOnly(libs.ksp.gradlePlugin)
}
@@ -73,9 +75,13 @@ gradlePlugin {
id = "nowinandroid.android.room"
implementationClass = "AndroidRoomConventionPlugin"
}
- register("firebase-perf") {
- id = "nowinandroid.firebase-perf"
- implementationClass = "FirebasePerfConventionPlugin"
+ 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 6bc5f9f9e..26b6951d3 100644
--- a/build-logic/convention/src/main/kotlin/AndroidApplicationConventionPlugin.kt
+++ b/build-logic/convention/src/main/kotlin/AndroidApplicationConventionPlugin.kt
@@ -14,10 +14,9 @@
* 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.google.samples.apps.nowinandroid.configureGradleManagedDevices
+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
@@ -35,7 +34,6 @@ class AndroidApplicationConventionPlugin : Plugin {
extensions.configure {
configureKotlinAndroid(this)
defaultConfig.targetSdk = 33
- configureFlavors(this)
configureGradleManagedDevices(this)
}
extensions.configure {
diff --git a/build-logic/convention/src/main/kotlin/AndroidApplicationFirebaseConventionPlugin.kt b/build-logic/convention/src/main/kotlin/AndroidApplicationFirebaseConventionPlugin.kt
new file mode 100644
index 000000000..598da727d
--- /dev/null
+++ b/build-logic/convention/src/main/kotlin/AndroidApplicationFirebaseConventionPlugin.kt
@@ -0,0 +1,58 @@
+/*
+ * Copyright 2022 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.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 AndroidApplicationFirebaseConventionPlugin : Plugin {
+ override fun apply(target: Project) {
+ with(target) {
+ with(pluginManager) {
+ apply("com.google.gms.google-services")
+ apply("com.google.firebase.firebase-perf")
+ apply("com.google.firebase.crashlytics")
+ }
+
+ val libs = extensions.getByType().named("libs")
+ dependencies {
+ val bom = libs.findLibrary("firebase-bom").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/FirebasePerfConventionPlugin.kt b/build-logic/convention/src/main/kotlin/AndroidApplicationFlavorsConventionPlugin.kt
similarity index 66%
rename from build-logic/convention/src/main/kotlin/FirebasePerfConventionPlugin.kt
rename to build-logic/convention/src/main/kotlin/AndroidApplicationFlavorsConventionPlugin.kt
index 48f750678..46b019d7a 100644
--- a/build-logic/convention/src/main/kotlin/FirebasePerfConventionPlugin.kt
+++ b/build-logic/convention/src/main/kotlin/AndroidApplicationFlavorsConventionPlugin.kt
@@ -1,5 +1,5 @@
/*
- * Copyright 2022 The Android Open Source Project
+ * 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.
@@ -14,16 +14,18 @@
* 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 FirebasePerfConventionPlugin : Plugin {
+class AndroidApplicationFlavorsConventionPlugin : Plugin {
override fun apply(target: Project) {
with(target) {
- pluginManager.findPlugin("com.google.firebase.firebase-perf").apply {
- version = "1.4.1"
+ 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 0c71d9d4c..1b567ae2d 100644
--- a/build-logic/convention/src/main/kotlin/AndroidFeatureConventionPlugin.kt
+++ b/build-logic/convention/src/main/kotlin/AndroidFeatureConventionPlugin.kt
@@ -49,6 +49,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/AndroidHiltConventionPlugin.kt b/build-logic/convention/src/main/kotlin/AndroidHiltConventionPlugin.kt
index 772064942..29cb748c2 100644
--- a/build-logic/convention/src/main/kotlin/AndroidHiltConventionPlugin.kt
+++ b/build-logic/convention/src/main/kotlin/AndroidHiltConventionPlugin.kt
@@ -14,14 +14,11 @@
* limitations under the License.
*/
-import com.android.build.api.variant.LibraryAndroidComponentsExtension
-import com.google.samples.apps.nowinandroid.configureJacoco
import org.gradle.api.Plugin
import org.gradle.api.Project
import org.gradle.api.artifacts.VersionCatalogsExtension
import org.gradle.kotlin.dsl.dependencies
import org.gradle.kotlin.dsl.getByType
-import org.gradle.kotlin.dsl.kotlin
class AndroidHiltConventionPlugin : Plugin {
override fun apply(target: Project) {
diff --git a/build-logic/convention/src/main/kotlin/AndroidLibraryConventionPlugin.kt b/build-logic/convention/src/main/kotlin/AndroidLibraryConventionPlugin.kt
index 842873bae..5b2f76edb 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-logic/convention/src/main/kotlin/com/google/samples/apps/nowinandroid/NiaFlavor.kt b/build-logic/convention/src/main/kotlin/com/google/samples/apps/nowinandroid/NiaFlavor.kt
index 5cafdf7ce..dec592542 100644
--- a/build-logic/convention/src/main/kotlin/com/google/samples/apps/nowinandroid/NiaFlavor.kt
+++ b/build-logic/convention/src/main/kotlin/com/google/samples/apps/nowinandroid/NiaFlavor.kt
@@ -16,8 +16,8 @@ enum class FlavorDimension {
// These two product flavors reflect this behaviour.
@Suppress("EnumEntryName")
enum class NiaFlavor(val dimension: FlavorDimension, val applicationIdSuffix: String? = null) {
- demo(FlavorDimension.contentType),
- prod(FlavorDimension.contentType, ".prod")
+ demo(FlavorDimension.contentType, applicationIdSuffix = ".demo"),
+ prod(FlavorDimension.contentType, )
}
fun Project.configureFlavors(
diff --git a/build.gradle.kts b/build.gradle.kts
index 30640d41c..17690f83e 100644
--- a/build.gradle.kts
+++ b/build.gradle.kts
@@ -22,13 +22,16 @@ buildscript {
// Android Build Server
maven { url = uri("../nowinandroid-prebuilts/m2repository") }
}
-
}
+// Lists all plugins used throughout the project without applying them.
plugins {
alias(libs.plugins.android.application) apply false
alias(libs.plugins.kotlin.jvm) apply false
alias(libs.plugins.kotlin.serialization) apply false
+ alias(libs.plugins.firebase.crashlytics) apply false
+ alias(libs.plugins.firebase.perf) apply false
+ alias(libs.plugins.gms) apply false
alias(libs.plugins.hilt) apply false
alias(libs.plugins.ksp) apply false
alias(libs.plugins.secrets) apply false
diff --git a/build_android_release.sh b/build_android_release.sh
index dfdf37500..c7e5fc835 100755
--- a/build_android_release.sh
+++ b/build_android_release.sh
@@ -25,44 +25,26 @@ export JAVA_HOME="$(cd $DIR/../../../prebuilts/studio/jdk/jdk11/linux && pwd )"
echo "JAVA_HOME=$JAVA_HOME"
export ANDROID_HOME="$(cd $DIR/../../../prebuilts/fullsdk/linux && pwd )"
-
echo "ANDROID_HOME=$ANDROID_HOME"
-cd $DIR
-# Build
-GRADLE_PARAMS=" --stacktrace"
-$DIR/gradlew :app:clean :app:assemble ${GRADLE_PARAMS}
-BUILD_RESULT=$?
+echo "Copying google-services.json"
+cp $DIR/../nowinandroid-prebuilts/google-services.json $DIR/app
-# Demo debug
-cp $APP_OUT/apk/demo/debug/app-demo-debug.apk $DIST_DIR
+echo "Copying local.properties"
+cp $DIR/../nowinandroid-prebuilts/local.properties $DIR
-# Demo release
-cp $APP_OUT/apk/demo/release/app-demo-release.apk $DIST_DIR
+cd $DIR
-# Prod debug
-cp $APP_OUT/apk/prod/debug/app-prod-debug.apk $DIST_DIR/app-prod-debug.apk
+# Build the prodRelease variant
+GRADLE_PARAMS=" --stacktrace -Puse-google-services"
+$DIR/gradlew :app:clean :app:assembleProdRelease :app:bundleProdRelease ${GRADLE_PARAMS}
+BUILD_RESULT=$?
-# Prod release
+# Prod release apk
cp $APP_OUT/apk/prod/release/app-prod-release.apk $DIST_DIR/app-prod-release.apk
-#cp $APP_OUT/mapping/release/mapping.txt $DIST_DIR/mobile-release-apk-mapping.txt
-
-# Build App Bundles
-# Don't clean here, otherwise all apks are gone.
-$DIR/gradlew :app:bundle ${GRADLE_PARAMS}
-
-# Demo debug
-cp $APP_OUT/bundle/demoDebug/app-demo-debug.aab $DIST_DIR/app-demo-debug.aab
-
-# Demo release
-cp $APP_OUT/bundle/demoRelease/app-demo-release.aab $DIST_DIR/app-demo-release.aab
-
-# Prod debug
-cp $APP_OUT/bundle/prodDebug/app-prod-debug.aab $DIST_DIR/app-prod-debug.aab
-
-# Prod release
+# Prod release bundle
cp $APP_OUT/bundle/prodRelease/app-prod-release.aab $DIST_DIR/app-prod-release.aab
-#cp $APP_OUT/mapping/prodRelease/mapping.txt $DIST_DIR/mobile-release-aab-mapping.txt
-BUILD_RESULT=$?
+# Prod release bundle mapping
+cp $APP_OUT/mapping/prodRelease/mapping.txt $DIST_DIR/mobile-release-aab-mapping.txt
-exit $BUILD_RESULT
\ No newline at end of file
+exit $BUILD_RESULT
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 2ad38a26e..3b0015bab 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
@@ -57,12 +58,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 a2c02f84f..0f6861fbc 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
/**
@@ -46,12 +47,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 5f43fd235..2db5a7280 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
@@ -98,6 +99,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 cb0b0ecd6..fce7f8fed 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
@@ -211,6 +212,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 ec0179139..cde929917 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 3d9fd27d3..2cec43f7c 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 9d2c3e817..08bacd01d 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
@@ -72,6 +73,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 38c1f7d8e..a6636c98e 100644
--- a/gradle/libs.versions.toml
+++ b/gradle/libs.versions.toml
@@ -28,6 +28,10 @@ androidxUiAutomator = "2.2.0"
androidxWindowManager = "1.0.0"
androidxWork = "2.7.1"
coil = "2.2.2"
+firebaseBom = "31.2.0"
+firebaseCrashlyticsPlugin = "2.9.2"
+firebasePerfPlugin = "1.4.2"
+gmsPlugin = "4.3.14"
hilt = "2.44.2"
hiltExt = "1.0.0"
jacoco = "0.8.7"
@@ -96,6 +100,12 @@ androidx-work-testing = { group = "androidx.work", name = "work-testing", versio
coil-kt = { group = "io.coil-kt", name = "coil", version.ref = "coil" }
coil-kt-compose = { group = "io.coil-kt", name = "coil-compose", version.ref = "coil" }
coil-kt-svg = { group = "io.coil-kt", name = "coil-svg", version.ref = "coil" }
+firebase-bom = { group = "com.google.firebase", name = "firebase-bom", version.ref="firebaseBom"}
+firebase-analytics = { group = "com.google.firebase", name = "firebase-analytics-ktx"}
+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" }
@@ -127,6 +137,9 @@ ksp-gradlePlugin = { group = "com.google.devtools.ksp", name = "com.google.devto
android-application = { id = "com.android.application", version.ref = "androidGradlePlugin" }
android-library = { id = "com.android.library", version.ref = "androidGradlePlugin" }
android-test = { id = "com.android.test", version.ref = "androidGradlePlugin" }
+firebase-crashlytics = { id = "com.google.firebase.crashlytics", version.ref = "firebaseCrashlyticsPlugin"}
+firebase-perf = { id = "com.google.firebase.firebase-perf", version.ref = "firebasePerfPlugin"}
+gms = { id = "com.google.gms.google-services", version.ref = "gmsPlugin"}
hilt = { id = "com.google.dagger.hilt.android", version.ref = "hilt" }
kotlin-jvm = { id = "org.jetbrains.kotlin.jvm", version.ref = "kotlin" }
kotlin-serialization = { id = "org.jetbrains.kotlin.plugin.serialization", version.ref = "kotlin" }
diff --git a/kokoro/build.sh b/kokoro/build.sh
index c283382d7..5c50a814f 100755
--- a/kokoro/build.sh
+++ b/kokoro/build.sh
@@ -20,7 +20,7 @@ set -e
set -x
deviceIds=${1:-'Nexus5,Pixel2,Pixel3,Nexus9'}
-osVersionIds=${2:-'23,27,30'}
+osVersionIds=${2:-'27,30'}
GRADLE_FLAGS=()
if [[ -n "$GRADLE_DEBUG" ]]; then
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 {