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 {