diff --git a/app/src/main/java/com/darshan/notificity/AboutActivity.kt b/app/src/main/java/com/darshan/notificity/AboutActivity.kt index fd2ca0f..716ba85 100644 --- a/app/src/main/java/com/darshan/notificity/AboutActivity.kt +++ b/app/src/main/java/com/darshan/notificity/AboutActivity.kt @@ -43,12 +43,11 @@ import androidx.lifecycle.compose.collectAsStateWithLifecycle import com.bumptech.glide.integration.compose.ExperimentalGlideComposeApi import com.bumptech.glide.integration.compose.GlideImage import com.bumptech.glide.integration.compose.placeholder -import com.darshan.notificity.analytics.AnalyticsConstants -import com.darshan.notificity.analytics.AnalyticsLogger import com.darshan.notificity.components.BuyMeACoffee import com.darshan.notificity.components.ClickableSection import com.darshan.notificity.components.NotificityAppBar import com.darshan.notificity.extensions.openUrl +import com.darshan.notificity.analytics.enums.Screen import com.darshan.notificity.ui.BaseActivity import com.darshan.notificity.ui.settings.SettingsViewModel import com.darshan.notificity.ui.theme.NotificityTheme @@ -59,8 +58,8 @@ class AboutActivity : BaseActivity() { private val settingsViewModel: SettingsViewModel by viewModels() - override val screenName: String - get() = AnalyticsConstants.Screens.ABOUT + override val screen: Screen + get() = Screen.ABOUT override fun onCreate(savedInstanceState: Bundle?) { super.onCreate(savedInstanceState) @@ -69,14 +68,30 @@ class AboutActivity : BaseActivity() { val themeMode by remember { settingsViewModel.themeMode }.collectAsStateWithLifecycle() NotificityTheme(themeMode = themeMode) { - AboutScreen(onBack = { finish() }) + AboutScreen( + onBack = { finish() }, + onPrivacyPolicyClicked = { + analyticsManager.app.onPrivacyPolicyClicked() + }, + onBuyMeCoffeeClicked = { + analyticsManager.app.onBuyMeCoffeeClicked() + }, + onContributorProfileClicked = { name -> + analyticsManager.app.onContributorProfileClicked(name) + } + ) } } } } @Composable -fun AboutScreen(onBack: () -> Unit) { +fun AboutScreen( + onBack: () -> Unit, + onPrivacyPolicyClicked: () -> Unit, + onBuyMeCoffeeClicked: () -> Unit, + onContributorProfileClicked: (String) -> Unit +) { val context = LocalContext.current val scrollState = rememberScrollState() val showButton by remember { @@ -119,7 +134,7 @@ fun AboutScreen(onBack: () -> Unit) { onClick = { context.openUrl("https://github.com/darshanpania/Notificity/blob/main/PRIVACY.md") - AnalyticsLogger.onPrivacyPolicyClicked() + onPrivacyPolicyClicked() }) HorizontalDivider() @@ -129,13 +144,24 @@ fun AboutScreen(onBack: () -> Unit) { style = MaterialTheme.typography.titleMedium, color = MaterialTheme.colorScheme.primary ) - Contributor("Darshan Pania", "i_m_Pania") - Contributor("Shivam Sharma", "ShivamS707") - Contributor("Shrinath Gupta", "gupta_shrinath") - Contributor("William John", "goonerdroid11") - Contributor("Jay Rathod", "zzjjaayy") - Contributor("Avadhut", "mr_whoknows55") - Contributor("Md Anas Shikoh", "ansiili_billi") + + val contributors = listOf( + ContributorData("Darshan Pania", "i_m_Pania"), + ContributorData("Shivam Sharma", "ShivamS707"), + ContributorData("Shrinath Gupta", "gupta_shrinath"), + ContributorData("William John", "goonerdroid11"), + ContributorData("Jay Rathod", "zzjjaayy"), + ContributorData("Avadhut", "mr_whoknows55"), + ContributorData("Md Anas Shikoh", "ansiili_billi") + ) + + contributors.forEach { + Contributor( + it.name, + it.twitterUsername, + onContributorProfileClicked + ) + } } AnimatedVisibility( visible = showButton, enter = fadeIn(), exit = fadeOut(), @@ -144,16 +170,25 @@ fun AboutScreen(onBack: () -> Unit) { BuyMeACoffee(onClick = { context.openUrl(Constants.BUY_ME_A_COFFEE_LINK) - AnalyticsLogger.onBuyMeCoffeeClicked() + onBuyMeCoffeeClicked() }) } } } } +data class ContributorData( + val name: String, + val twitterUsername: String +) + @OptIn(ExperimentalGlideComposeApi::class) @Composable -fun Contributor(name: String, twitterUsername: String) { +fun Contributor( + name: String, + twitterUsername: String, + onContributorProfileClicked: (String) -> Unit +) { val context = LocalContext.current val twitterProfileUrl = "https://twitter.com/$twitterUsername" val profilePicUrl = "https://unavatar.io/twitter/$twitterUsername" @@ -164,7 +199,7 @@ fun Contributor(name: String, twitterUsername: String) { .clickable { context.openUrl(twitterProfileUrl) - AnalyticsLogger.onContributorProfileClicked(name) + onContributorProfileClicked(name) } .padding(vertical = 6.dp), verticalAlignment = Alignment.Companion.CenterVertically, @@ -199,5 +234,5 @@ fun Contributor(name: String, twitterUsername: String) { @Composable @Preview(showSystemUi = true, showBackground = true) fun ShowAboutScreen() { - AboutScreen {} + AboutScreen(onBack = {}, onPrivacyPolicyClicked = {}, onBuyMeCoffeeClicked = {}, onContributorProfileClicked = {}) } \ No newline at end of file diff --git a/app/src/main/java/com/darshan/notificity/MyApplication.kt b/app/src/main/java/com/darshan/notificity/MyApplication.kt index bd17d69..9841e7e 100644 --- a/app/src/main/java/com/darshan/notificity/MyApplication.kt +++ b/app/src/main/java/com/darshan/notificity/MyApplication.kt @@ -3,8 +3,6 @@ package com.darshan.notificity import android.app.Application import android.app.NotificationChannel import android.app.NotificationManager -import com.darshan.notificity.analytics.AnalyticsService -import com.darshan.notificity.analytics.FirebaseAnalyticsTracker import dagger.hilt.android.HiltAndroidApp @HiltAndroidApp @@ -13,7 +11,6 @@ class MyApplication : Application() { override fun onCreate() { super.onCreate() - AnalyticsService.init(FirebaseAnalyticsTracker()) createNotificationChannels() } diff --git a/app/src/main/java/com/darshan/notificity/NotificationsActivity.kt b/app/src/main/java/com/darshan/notificity/NotificationsActivity.kt index a05ad86..0d9dc50 100644 --- a/app/src/main/java/com/darshan/notificity/NotificationsActivity.kt +++ b/app/src/main/java/com/darshan/notificity/NotificationsActivity.kt @@ -51,11 +51,10 @@ import androidx.compose.ui.text.style.TextOverflow import androidx.compose.ui.unit.dp import androidx.compose.ui.unit.sp import androidx.lifecycle.compose.collectAsStateWithLifecycle -import com.darshan.notificity.analytics.AnalyticsConstants -import com.darshan.notificity.analytics.AnalyticsLogger import com.darshan.notificity.components.EmptyContentState import com.darshan.notificity.components.SwipeToDelete import com.darshan.notificity.main.viewmodel.MainViewModel +import com.darshan.notificity.analytics.enums.Screen import com.darshan.notificity.ui.BaseActivity import com.darshan.notificity.ui.settings.SettingsViewModel import com.darshan.notificity.ui.theme.NotificityTheme @@ -68,8 +67,8 @@ class NotificationsActivity : BaseActivity() { private val mainViewModel: MainViewModel by viewModels() private val settingsViewModel: SettingsViewModel by viewModels() - override val screenName: String - get() = AnalyticsConstants.Screens.NOTIFICATION_LIST + override val screen: Screen + get() = Screen.NOTIFICATION_LIST override fun onCreate(savedInstanceState: Bundle?) { super.onCreate(savedInstanceState) @@ -85,7 +84,10 @@ class NotificationsActivity : BaseActivity() { NotificationSearchScreen( appName = appName, notificationsMap = notificationsMap, - deleteNotification = mainViewModel::deleteNotification + deleteNotification = mainViewModel::deleteNotification, + onNotificationListOpened = { appName, total -> + analyticsManager.app.onNotificationListOpened(appName, total) + } ) } } @@ -98,6 +100,7 @@ fun NotificationSearchScreen( appName: String?, notificationsMap: Map>, deleteNotification: (NotificationEntity) -> Unit, + onNotificationListOpened: (String, Int) -> Unit ) { val dateRangePickerState = rememberDateRangePickerState() var notificationSearchQuery by remember { mutableStateOf("") } @@ -119,7 +122,8 @@ fun NotificationSearchScreen( notificationsMap = notificationsMap, searchQuery = notificationSearchQuery, selectedDateRange = selectedDateRange, - deleteNotification = deleteNotification + deleteNotification = deleteNotification, + onNotificationListOpened = onNotificationListOpened ) } @@ -181,6 +185,7 @@ fun NotificationList( searchQuery: String, selectedDateRange: Pair, deleteNotification: (NotificationEntity) -> Unit, + onNotificationListOpened: (String, Int) -> Unit ) { // Safely handle the case where appName is null if (appName == null) { @@ -220,7 +225,7 @@ fun NotificationList( LaunchedEffect(key1 = Unit) { // LaunchedEffect ensures the logging doesn't rerun on every recomposition but initial composition. if (filteredNotifications.isNotEmpty()) { - AnalyticsLogger.onNotificationListOpened(appName, filteredNotifications.size) + onNotificationListOpened(appName, filteredNotifications.size) } } diff --git a/app/src/main/java/com/darshan/notificity/analytics/AnalyticsConstants.kt b/app/src/main/java/com/darshan/notificity/analytics/AnalyticsConstants.kt deleted file mode 100644 index 486ec09..0000000 --- a/app/src/main/java/com/darshan/notificity/analytics/AnalyticsConstants.kt +++ /dev/null @@ -1,43 +0,0 @@ -package com.darshan.notificity.analytics - -/** - * Defines constants for analytics events, parameters, and screen names. - */ -object AnalyticsConstants { - - // Event names used for logging analytics. - object Events { - const val APP_LAUNCH = "app_launched" - const val NOTIFICATION_LIST_VIEWED = "notification_list_viewed" - const val THEME_TOGGLE = "theme_toggle" - const val RECOMMEND_APP_CLICKED = "recommend_app_clicked" - const val PRIVACY_POLICY_CLICKED = "privacy_policy_clicked" - const val CONTRIBUTOR_CLICKED = "contributor_clicked" - const val BUY_ME_COFFEE_CLICKED = "buy_me_coffee_clicked" - const val LOGIN = "login" - const val NOTIFICATION_PERMISSION_REQUESTED = "notification_permission_requested" - const val NOTIFICATION_PERMISSION_CHANGED = "notification_permission_changed" - } - - // Parameter keys used with analytics events. - object Params { - const val SOURCE = "source" - const val SCREEN_CLASS = "screen_class" - const val APP_NAME = "app_name" - const val TOTAL_NOTIFICATIONS = "total_notifications" - const val THEME = "theme" - const val CONTRIBUTOR_NAME = "contributor_name" - const val USER_TYPE = "user_type" - const val USER_ID = "user_id" - const val PERMISSION_STATUS = "permission_status" // e.g. granted, denied - } - - // Names of screens for screen view tracking. - object Screens { - const val MAIN = "Main" - const val SIGNIN = "Signin" - const val SETTINGS = "Settings" - const val NOTIFICATION_LIST = "Notification List" - const val ABOUT = "About" - } -} \ No newline at end of file diff --git a/app/src/main/java/com/darshan/notificity/analytics/AnalyticsEvent.kt b/app/src/main/java/com/darshan/notificity/analytics/AnalyticsEvent.kt deleted file mode 100644 index 3db7c3b..0000000 --- a/app/src/main/java/com/darshan/notificity/analytics/AnalyticsEvent.kt +++ /dev/null @@ -1,13 +0,0 @@ -package com.darshan.notificity.analytics - -/** - * Represents a generic analytics event. - */ -interface AnalyticsEvent { - - /** Name of the event to be logged. */ - val name: String - - /** Optional key-value data associated with the event. */ - val properties: Map? -} \ No newline at end of file diff --git a/app/src/main/java/com/darshan/notificity/analytics/AnalyticsLogger.kt b/app/src/main/java/com/darshan/notificity/analytics/AnalyticsLogger.kt deleted file mode 100644 index 4fcfe91..0000000 --- a/app/src/main/java/com/darshan/notificity/analytics/AnalyticsLogger.kt +++ /dev/null @@ -1,134 +0,0 @@ -package com.darshan.notificity.analytics - -import com.darshan.notificity.enums.NotificationPermissionStatus - -/** - * Wrapper around [AnalyticsService] to log analytics events. - * Simplifies event logging by providing easy-to-use functions for typical app actions. - */ -object AnalyticsLogger { - - /** Logs app launch event with the source of launch. */ - fun onAppLaunch(source: String) { - AnalyticsService.logEvent(object : AnalyticsEvent { - override val name = AnalyticsConstants.Events.APP_LAUNCH - override val properties = mapOf( - AnalyticsConstants.Params.SOURCE to source - ) - }) - } - - /** Logs screen view event with screen name and class. */ - fun onScreenViewed(screenName: String, screenClass: String) { - AnalyticsService.logScreen( - screenName = screenName, - properties = mapOf( - AnalyticsConstants.Params.SCREEN_CLASS to screenClass - ) - ) - } - - /** - * Logs the event when the app requests notification permission from the user. - */ - fun onNotificationPermissionRequested() { - AnalyticsService.logEvent(object : AnalyticsEvent { - override val name = AnalyticsConstants.Events.NOTIFICATION_PERMISSION_REQUESTED - override val properties = null - }) - } - - /** - * Logs a change in the user's notification permission status (granted or denied). - */ - fun onNotificationPermissionChanged(status: NotificationPermissionStatus) { - AnalyticsService.logEvent(object : AnalyticsEvent { - override val name = AnalyticsConstants.Events.NOTIFICATION_PERMISSION_CHANGED - override val properties = mapOf( - AnalyticsConstants.Params.PERMISSION_STATUS to status.code - ) - }) - } - - /** - * Sets the user's current notification permission status as a user property. - * Enables filtering or segmentation based on permission status in analytics. - */ - fun setNotificationPermissionProperty(status: NotificationPermissionStatus) { - AnalyticsService.setUserProperty( - AnalyticsConstants.Params.PERMISSION_STATUS, - status.code.toString() - ) - } - - /** Logs notification list viewed event with app name and total notifications. */ - fun onNotificationListOpened(appName: String, total: Int) { - AnalyticsService.logEvent(object : AnalyticsEvent { - override val name = AnalyticsConstants.Events.NOTIFICATION_LIST_VIEWED - override val properties = mapOf( - AnalyticsConstants.Params.APP_NAME to appName, - AnalyticsConstants.Params.TOTAL_NOTIFICATIONS to total - ) - }) - } - - /** Logs theme toggle click event with the selected theme. */ - fun onThemeToggleClicked(theme: String) { - AnalyticsService.logEvent(object : AnalyticsEvent { - override val name = AnalyticsConstants.Events.THEME_TOGGLE - override val properties = mapOf( - AnalyticsConstants.Params.THEME to theme - ) - }) - } - - /** Logs recommend app clicked event. */ - fun onRecommendAppClicked() { - AnalyticsService.logEvent(object : AnalyticsEvent { - override val name = AnalyticsConstants.Events.RECOMMEND_APP_CLICKED - override val properties = null - }) - } - - /** Logs privacy policy clicked event. */ - fun onPrivacyPolicyClicked() { - AnalyticsService.logEvent(object : AnalyticsEvent { - override val name = AnalyticsConstants.Events.PRIVACY_POLICY_CLICKED - override val properties = null - }) - } - - /** Logs contributor profile clicked event with contributor name. */ - fun onContributorProfileClicked(name: String) { - AnalyticsService.logEvent(object : AnalyticsEvent { - override val name = AnalyticsConstants.Events.CONTRIBUTOR_CLICKED - override val properties = mapOf( - AnalyticsConstants.Params.CONTRIBUTOR_NAME to name - ) - }) - } - - /** Logs buy me coffee clicked event. */ - fun onBuyMeCoffeeClicked() { - AnalyticsService.logEvent(object : AnalyticsEvent { - override val name = AnalyticsConstants.Events.BUY_ME_COFFEE_CLICKED - override val properties = null - }) - } - - /** - * Logs user login event. - * Also sets user ID and user type property on the analytics service. - */ - fun onLogin(type: String, userId: String) { - AnalyticsService.setUserId(userId) - AnalyticsService.setUserProperty(AnalyticsConstants.Params.USER_TYPE, type) - AnalyticsService.logEvent(object : AnalyticsEvent { - override val name = AnalyticsConstants.Events.LOGIN - override val properties = mapOf( - AnalyticsConstants.Params.USER_TYPE to type, - AnalyticsConstants.Params.USER_ID to userId - ) - }) - } -} \ No newline at end of file diff --git a/app/src/main/java/com/darshan/notificity/analytics/AnalyticsService.kt b/app/src/main/java/com/darshan/notificity/analytics/AnalyticsService.kt deleted file mode 100644 index 805c797..0000000 --- a/app/src/main/java/com/darshan/notificity/analytics/AnalyticsService.kt +++ /dev/null @@ -1,23 +0,0 @@ -package com.darshan.notificity.analytics - -/** - * Wrapper for [AnalyticsTracker] implementation. - * Provides static access to log events, screens, - * and set user properties through a configured tracker. - */ -object AnalyticsService { - private lateinit var tracker: AnalyticsTracker - - fun init(tracker: AnalyticsTracker) { - this.tracker = tracker - } - - fun logEvent(event: AnalyticsEvent) = tracker.logEvent(event) - - fun logScreen(screenName: String, properties: Map? = null) = - tracker.logScreen(screenName, properties) - - fun setUserId(userId: String?) = tracker.setUserId(userId) - - fun setUserProperty(key: String, value: String) = tracker.setUserProperty(key, value) -} \ No newline at end of file diff --git a/app/src/main/java/com/darshan/notificity/analytics/AnalyticsTracker.kt b/app/src/main/java/com/darshan/notificity/analytics/AnalyticsTracker.kt deleted file mode 100644 index b003b35..0000000 --- a/app/src/main/java/com/darshan/notificity/analytics/AnalyticsTracker.kt +++ /dev/null @@ -1,37 +0,0 @@ -package com.darshan.notificity.analytics - -/** - * Interface for tracking analytics events and user properties. - */ -interface AnalyticsTracker { - - /** - * Logs a custom analytics event. - * - * @param event The event to log. - */ - fun logEvent(event: AnalyticsEvent) - - /** - * Logs a screen view with optional properties. - * - * @param screenName Name of the screen. - * @param properties Optional additional properties. - */ - fun logScreen(screenName: String, properties: Map? = null) - - /** - * Sets the user ID for tracking. - * - * @param userId Unique identifier for the user. - */ - fun setUserId(userId: String?) - - /** - * Sets a custom user property. - * - * @param key Property name. - * @param value Property value. - */ - fun setUserProperty(key: String, value: String) -} diff --git a/app/src/main/java/com/darshan/notificity/analytics/Extensions.kt b/app/src/main/java/com/darshan/notificity/analytics/Extensions.kt new file mode 100644 index 0000000..499c1ae --- /dev/null +++ b/app/src/main/java/com/darshan/notificity/analytics/Extensions.kt @@ -0,0 +1,20 @@ +package com.darshan.notificity.analytics + +import com.darshan.notificity.analytics.constants.AnalyticsConstants +import com.darshan.notificity.auth.models.User + +/** + * Extension function for Map to merge common analytics parameters. + * + * @param user Optional user object to extract user ID from. If null, no user ID is added. + * @param timestamp Timestamp parameter (currently unused, timestamp is generated via getEpoch()) + * @return A new map containing the original parameters plus common analytics parameters + */ +fun Map.mergeCommonParams(user: User?, timestamp: Long): Map { + val enhancedParams = toMutableMap() + enhancedParams[AnalyticsConstants.TIMESTAMP] = timestamp + user?.id?.let { + enhancedParams[AnalyticsConstants.PARAM_USER_ID] = it + } + return enhancedParams +} \ No newline at end of file diff --git a/app/src/main/java/com/darshan/notificity/analytics/FirebaseAnalyticsTracker.kt b/app/src/main/java/com/darshan/notificity/analytics/FirebaseAnalyticsTracker.kt deleted file mode 100644 index 2233a1a..0000000 --- a/app/src/main/java/com/darshan/notificity/analytics/FirebaseAnalyticsTracker.kt +++ /dev/null @@ -1,56 +0,0 @@ -package com.darshan.notificity.analytics - -import android.os.Bundle -import com.google.firebase.analytics.FirebaseAnalytics -import com.google.firebase.analytics.ktx.analytics -import com.google.firebase.ktx.Firebase - -/** - * Implementation of [AnalyticsTracker] using Firebase Analytics. - * - * Logs events, screen views, user IDs, and user properties to Firebase Analytics. - **/ -class FirebaseAnalyticsTracker() : AnalyticsTracker { - - private val firebaseAnalytics = Firebase.analytics - - override fun logEvent(event: AnalyticsEvent) { - val bundle = Bundle().apply { - event.properties?.forEach { (key, value) -> - when (value) { - is String -> putString(key, value) - is Int -> putInt(key, value) - is Long -> putLong(key, value) - is Double -> putDouble(key, value) - is Boolean -> putInt(key, if (value) 1 else 0) - } - } - } - firebaseAnalytics.logEvent(event.name, bundle) - } - - override fun logScreen(screenName: String, properties: Map?) { - val bundle = Bundle().apply { - putString(FirebaseAnalytics.Param.SCREEN_NAME, screenName) - putString(FirebaseAnalytics.Param.SCREEN_CLASS, screenName) - properties?.forEach { (key, value) -> - when (value) { - is String -> putString(key, value) - is Int -> putInt(key, value) - is Long -> putLong(key, value) - is Double -> putDouble(key, value) - is Boolean -> putInt(key, if (value) 1 else 0) - } - } - } - firebaseAnalytics.logEvent(FirebaseAnalytics.Event.SCREEN_VIEW, bundle) - } - - override fun setUserId(userId: String?) { - firebaseAnalytics.setUserId(userId) - } - - override fun setUserProperty(key: String, value: String) { - firebaseAnalytics.setUserProperty(key, value) - } -} \ No newline at end of file diff --git a/app/src/main/java/com/darshan/notificity/analytics/constants/AnalyticsConstants.kt b/app/src/main/java/com/darshan/notificity/analytics/constants/AnalyticsConstants.kt new file mode 100644 index 0000000..f04f04e --- /dev/null +++ b/app/src/main/java/com/darshan/notificity/analytics/constants/AnalyticsConstants.kt @@ -0,0 +1,10 @@ +package com.darshan.notificity.analytics.constants + +/** + * Constants used across the analytics module for consistent parameter naming. + */ +object AnalyticsConstants { + const val PARAM_USER_ID = "user_id" + const val SOURCE = "source" + const val TIMESTAMP = "timestamp" +} \ No newline at end of file diff --git a/app/src/main/java/com/darshan/notificity/analytics/core/AnalyticsManager.kt b/app/src/main/java/com/darshan/notificity/analytics/core/AnalyticsManager.kt new file mode 100644 index 0000000..5580977 --- /dev/null +++ b/app/src/main/java/com/darshan/notificity/analytics/core/AnalyticsManager.kt @@ -0,0 +1,22 @@ +package com.darshan.notificity.analytics.core + +import com.darshan.notificity.analytics.domain.AppAnalytics +import com.darshan.notificity.analytics.domain.AuthAnalytics +import com.darshan.notificity.analytics.domain.PermissionAnalytics +import javax.inject.Inject +import javax.inject.Singleton + +/** + * Central manager providing single point of access to all analytics services. + * Uses dependency injection to provide analytics functionality for different app domains. + * + * @param auth Authentication-related analytics service + * @param app General app analytics service + * @param permission Permission-related analytics service + */ +@Singleton +class AnalyticsManager @Inject constructor( + val auth: AuthAnalytics, + val app: AppAnalytics, + val permission: PermissionAnalytics, +) \ No newline at end of file diff --git a/app/src/main/java/com/darshan/notificity/analytics/domain/AppAnalytics.kt b/app/src/main/java/com/darshan/notificity/analytics/domain/AppAnalytics.kt new file mode 100644 index 0000000..90d544a --- /dev/null +++ b/app/src/main/java/com/darshan/notificity/analytics/domain/AppAnalytics.kt @@ -0,0 +1,87 @@ +package com.darshan.notificity.analytics.domain + +import com.darshan.notificity.analytics.enums.Screen + +/** + * Interface for tracking general application analytics events. + * Handles screen views, user interactions, and error reporting. + */ +interface AppAnalytics { + /** + * Tracks when the application is launched. + * + * @param source The source of the app launch (default: SOURCE constant) + */ + fun onAppLaunch(source: String = SOURCE) + + /** + * Tracks when a screen is viewed by the user. + * + * @param screenName The screen that was viewed + */ + fun onScreenViewed(screenName: Screen) + + /** + * Tracks when the notification list is opened. + * + * @param appName The name of the app whose notifications are being viewed + * @param total The total number of notifications displayed + */ + fun onNotificationListOpened(appName: String, total: Int) + + /** + * Tracks when the user toggles the app theme. + * + * @param theme The theme that was selected ("DARK", "LIGHT" or "SYSTEM) + */ + fun onThemeToggleClicked(theme: String) + + /** + * Tracks when the user clicks the recommend app button. + */ + fun onRecommendAppClicked() + + /** + * Tracks when the user clicks the privacy policy link. + */ + fun onPrivacyPolicyClicked() + + /** + * Tracks when a contributor profile is clicked. + * + * @param name The name of the contributor whose profile was clicked + */ + fun onContributorProfileClicked(name: String) + + /** + * Tracks when the "Buy Me Coffee" button is clicked. + */ + fun onBuyMeCoffeeClicked() + + /** + * Reports when an error occurs in the application. + * + * @param screen The screen where the error occurred + * @param error Description of the error that occurred + */ + fun onErrorOccurred(screen: Screen, error: String) + + companion object { + // Event name constants + const val NOTIFICATION_LIST_VIEWED = "notification_list_viewed" + const val THEME_TOGGLE = "theme_toggle" + const val RECOMMEND_APP_CLICKED = "recommend_app_clicked" + const val PRIVACY_POLICY_CLICKED = "privacy_policy_clicked" + const val CONTRIBUTOR_CLICKED = "contributor_clicked" + const val BUY_ME_COFFEE_CLICKED = "buy_me_coffee_clicked" + const val ERROR_OCCURRED = "error_occurred" + + // Parameter key constants + const val SOURCE = "source" + const val ERROR = "error" + const val APP_NAME = "app_name" + const val TOTAL_NOTIFICATIONS = "total_notifications" + const val THEME = "theme" + const val CONTRIBUTOR_NAME = "contributor_name" + } +} \ No newline at end of file diff --git a/app/src/main/java/com/darshan/notificity/analytics/domain/AuthAnalytics.kt b/app/src/main/java/com/darshan/notificity/analytics/domain/AuthAnalytics.kt new file mode 100644 index 0000000..f9ea7f9 --- /dev/null +++ b/app/src/main/java/com/darshan/notificity/analytics/domain/AuthAnalytics.kt @@ -0,0 +1,51 @@ +package com.darshan.notificity.analytics.domain + +import com.darshan.notificity.auth.AuthType +import com.darshan.notificity.auth.models.User + +/** + * Interface for tracking authentication-related analytics events. + * Handles sign-in attempts, successes, failures, and sign-out events. + */ +interface AuthAnalytics { + /** + * Tracks when a user attempts to sign in. + * + * @param authProviderType The type of authentication provider used + */ + fun onSigninAttempt(authProviderType: AuthType) + + /** + * Tracks successful user sign-in events. + * + * @param user The user object containing user details after successful sign-in + */ + fun onSigninSuccess(user: User) + + /** + * Tracks failed sign-in attempts. + * + * @param provider The authentication provider that failed + * @param error Description of the sign-in error + */ + fun onSigninFailure(provider: AuthType, error: String) + + /** + * Tracks when a user signs out of the application. + */ + fun onSignout() + + companion object { + // Event name constants + const val EVENT_SIGNIN_ATTEMPT = "signin_attempt" + const val EVENT_SIGNIN_SUCCESS = "signin_success" + const val EVENT_SIGNIN_FAILURE = "signin_failure" + const val EVENT_SIGNOUT = "signout" + + // Parameter key constants + const val PARAM_PROVIDER = "provider" + const val PARAM_EMAIL = "email" + const val PARAM_NAME = "name" + const val PARAM_ERROR = "error" + } +} \ No newline at end of file diff --git a/app/src/main/java/com/darshan/notificity/analytics/domain/PermissionAnalytics.kt b/app/src/main/java/com/darshan/notificity/analytics/domain/PermissionAnalytics.kt new file mode 100644 index 0000000..8762186 --- /dev/null +++ b/app/src/main/java/com/darshan/notificity/analytics/domain/PermissionAnalytics.kt @@ -0,0 +1,38 @@ +package com.darshan.notificity.analytics.domain + +import com.darshan.notificity.enums.AppPermissions +import com.darshan.notificity.enums.NotificationPermissionStatus + +/** + * Interface for tracking permission-related analytics events. + * Handles permission requests and permission status changes. + */ +interface PermissionAnalytics { + /** + * Tracks when a permission is requested from the user. + * + * @param permission The specific permission that was requested + */ + fun onPermissionRequested(permission: AppPermissions) + + /** + * Tracks when a permission status changes. + * + * @param permission The permission whose status changed + * @param status The new status of the permission + */ + + fun onPermissionChanged( + permission: AppPermissions, + status: NotificationPermissionStatus + ) + + companion object { + // Event name constants + const val PERMISSION_REQUESTED = "permission_requested" + const val PERMISSION_CHANGED = "permission_changed" + + // Parameter key constants + const val PERMISSION_STATUS = "permission_status" + } +} \ No newline at end of file diff --git a/app/src/main/java/com/darshan/notificity/analytics/enums/Screen.kt b/app/src/main/java/com/darshan/notificity/analytics/enums/Screen.kt new file mode 100644 index 0000000..d5c18fd --- /dev/null +++ b/app/src/main/java/com/darshan/notificity/analytics/enums/Screen.kt @@ -0,0 +1,31 @@ +package com.darshan.notificity.analytics.enums + +import androidx.activity.ComponentActivity +import com.darshan.notificity.AboutActivity +import com.darshan.notificity.NotificationsActivity +import com.darshan.notificity.main.ui.MainActivity +import com.darshan.notificity.ui.settings.SettingsActivity +import com.darshan.notificity.ui.signin.SignInActivity +import kotlin.reflect.KClass + +/** + * Enumeration of application screens for consistent screen view tracking. + * Each screen is associated with its display name and corresponding Activity class. + * + * @param screenName The display name of the screen for analytics + * @param clazz The Activity class associated with this screen + */ +enum class Screen(val screenName: String, val clazz: KClass) { + SIGNIN("Signin", SignInActivity::class), + MAIN("Main", MainActivity::class), + NOTIFICATION_LIST("Notification List", NotificationsActivity::class), + SETTINGS("Settings", SettingsActivity::class), + ABOUT("About", AboutActivity::class); + + /** + * Gets the simple class name of the associated screen(i.e. Activity). + * @return The simple class name as a string + */ + val screenClass: String + get() = clazz.java.simpleName +} \ No newline at end of file diff --git a/app/src/main/java/com/darshan/notificity/analytics/providers/firebase/FirebaseAppAnalytics.kt b/app/src/main/java/com/darshan/notificity/analytics/providers/firebase/FirebaseAppAnalytics.kt new file mode 100644 index 0000000..2f2435f --- /dev/null +++ b/app/src/main/java/com/darshan/notificity/analytics/providers/firebase/FirebaseAppAnalytics.kt @@ -0,0 +1,135 @@ +package com.darshan.notificity.analytics.providers.firebase + +import com.darshan.notificity.analytics.constants.AnalyticsConstants +import com.darshan.notificity.analytics.domain.AppAnalytics +import com.darshan.notificity.analytics.enums.Screen +import com.darshan.notificity.analytics.mergeCommonParams +import com.darshan.notificity.auth.repository.AuthRepository +import com.darshan.notificity.extensions.toBundle +import com.darshan.notificity.utils.Util.Companion.getEpoch +import com.google.firebase.analytics.FirebaseAnalytics +import kotlinx.coroutines.CoroutineScope +import kotlinx.coroutines.Dispatchers +import kotlinx.coroutines.SupervisorJob +import kotlinx.coroutines.launch +import javax.inject.Inject + +/** + * Firebase implementation of [AppAnalytics] interface. + * Provides Firebase Analytics integration for general app events. + * + * @param firebaseAnalytics Firebase Analytics instance for logging events + * @param authRepository Repository for accessing current user data + */ +class FirebaseAppAnalytics @Inject constructor( + private val firebaseAnalytics: FirebaseAnalytics, + private val authRepository: AuthRepository +) : AppAnalytics { + + private val analyticsScope = CoroutineScope(SupervisorJob() + Dispatchers.IO) + + /** + * Logs an event with enhanced information. + * Automatically adds current user ID, timestamp etc. to event parameters. + */ + private suspend fun logEvent( + eventName: String, + parameters: Map = emptyMap() + ) { + val user = authRepository.getCurrentUserData() + + val enhancedParams = parameters.mergeCommonParams(user, getEpoch()) + firebaseAnalytics.logEvent(eventName, enhancedParams.toBundle()) + } + + override fun onAppLaunch(source: String) { + analyticsScope.launch { + logEvent(FirebaseAnalytics.Event.APP_OPEN, mapOf( + AnalyticsConstants.SOURCE to source + )) + } + } + + override fun onScreenViewed(screen: Screen) { + analyticsScope.launch { + logEvent( + eventName = FirebaseAnalytics.Event.SCREEN_VIEW, + parameters = mapOf( + FirebaseAnalytics.Param.SCREEN_NAME to screen.screenName, + FirebaseAnalytics.Param.SCREEN_CLASS to screen.screenClass + ) + ) + } + } + + override fun onThemeToggleClicked(theme: String) { + analyticsScope.launch { + logEvent( + eventName = AppAnalytics.THEME_TOGGLE, + parameters = mapOf( + AppAnalytics.THEME to theme + ) + ) + } + } + + override fun onNotificationListOpened(appName: String, total: Int) { + analyticsScope.launch { + logEvent( + eventName = AppAnalytics.NOTIFICATION_LIST_VIEWED, + parameters = mapOf( + AppAnalytics.APP_NAME to appName, + AppAnalytics.TOTAL_NOTIFICATIONS to total + ) + ) + } + } + + override fun onRecommendAppClicked() { + analyticsScope.launch { + logEvent( + eventName = AppAnalytics.RECOMMEND_APP_CLICKED + ) + } + } + + override fun onPrivacyPolicyClicked() { + analyticsScope.launch { + logEvent( + eventName = AppAnalytics.PRIVACY_POLICY_CLICKED + ) + } + } + + override fun onContributorProfileClicked(name: String) { + analyticsScope.launch { + logEvent( + eventName = AppAnalytics.CONTRIBUTOR_CLICKED, + parameters = mapOf( + AppAnalytics.CONTRIBUTOR_NAME to name + ) + ) + } + } + + override fun onBuyMeCoffeeClicked() { + analyticsScope.launch { + logEvent( + eventName = AppAnalytics.BUY_ME_COFFEE_CLICKED + ) + } + } + + override fun onErrorOccurred(screen: Screen, error: String) { + analyticsScope.launch { + logEvent( + eventName = AppAnalytics.ERROR_OCCURRED, + parameters = mapOf( + FirebaseAnalytics.Param.SCREEN_NAME to screen.screenName, + FirebaseAnalytics.Param.SCREEN_CLASS to screen.screenClass, + AppAnalytics.ERROR to error, + ) + ) + } + } +} \ No newline at end of file diff --git a/app/src/main/java/com/darshan/notificity/analytics/providers/firebase/FirebaseAuthAnalytics.kt b/app/src/main/java/com/darshan/notificity/analytics/providers/firebase/FirebaseAuthAnalytics.kt new file mode 100644 index 0000000..c3bb167 --- /dev/null +++ b/app/src/main/java/com/darshan/notificity/analytics/providers/firebase/FirebaseAuthAnalytics.kt @@ -0,0 +1,93 @@ +package com.darshan.notificity.analytics.providers.firebase + +import com.darshan.notificity.analytics.domain.AuthAnalytics +import com.darshan.notificity.analytics.mergeCommonParams +import com.darshan.notificity.auth.AuthType +import com.darshan.notificity.auth.models.User +import com.darshan.notificity.auth.repository.AuthRepository +import com.darshan.notificity.extensions.toBundle +import com.darshan.notificity.utils.Util.Companion.getEpoch +import com.google.firebase.analytics.FirebaseAnalytics +import kotlinx.coroutines.CoroutineScope +import kotlinx.coroutines.Dispatchers +import kotlinx.coroutines.SupervisorJob +import kotlinx.coroutines.launch +import javax.inject.Inject + +/** + * Firebase implementation of [AuthAnalytics] interface. + * Provides Firebase Analytics integration for authentication events. + * + * @param firebaseAnalytics Firebase Analytics instance for logging events + * @param authRepository Repository for accessing current user data + */ +class FirebaseAuthAnalytics @Inject constructor( + private val firebaseAnalytics: FirebaseAnalytics, + private val authRepository: AuthRepository +) : AuthAnalytics { + + private val analyticsScope = CoroutineScope(SupervisorJob() + Dispatchers.IO) + + /** + * Logs an event with enhanced information. + * Automatically adds current user ID, timestamp etc. to event parameters. + */ + private suspend fun logEvent( + user: User? = null, + eventName: String, + parameters: Map = emptyMap() + ) { + val user = user ?: authRepository.getCurrentUserData() + + val enhancedParams = parameters.mergeCommonParams(user, getEpoch()) + firebaseAnalytics.logEvent(eventName, enhancedParams.toBundle()) + } + + override fun onSigninAttempt(provider: AuthType) { + analyticsScope.launch { + logEvent( + eventName = AuthAnalytics.Companion.EVENT_SIGNIN_ATTEMPT, + parameters = mapOf( + AuthAnalytics.Companion.PARAM_PROVIDER to provider.name, + ) + ) + } + } + + override fun onSigninSuccess(user: User) { + firebaseAnalytics.setUserId(user.id) + firebaseAnalytics.setUserProperty(AuthAnalytics.PARAM_PROVIDER, user.authType.name) + firebaseAnalytics.setUserProperty(AuthAnalytics.PARAM_EMAIL, user.email) + firebaseAnalytics.setUserProperty(AuthAnalytics.PARAM_NAME, user.name) + + analyticsScope.launch { + logEvent( + user = user, + eventName = AuthAnalytics.EVENT_SIGNIN_SUCCESS, + parameters = mapOf( + AuthAnalytics.PARAM_PROVIDER to user.authType.name, + ) + ) + } + } + + override fun onSigninFailure(provider: AuthType, error: String) { + analyticsScope.launch { + logEvent( + eventName = AuthAnalytics.EVENT_SIGNIN_FAILURE, + parameters = mapOf( + AuthAnalytics.PARAM_PROVIDER to provider.name, + AuthAnalytics.PARAM_ERROR to error + ) + ) + } + } + + override fun onSignout() { + analyticsScope.launch { + logEvent( + eventName = AuthAnalytics.EVENT_SIGNOUT + ) + } + } +} \ No newline at end of file diff --git a/app/src/main/java/com/darshan/notificity/analytics/providers/firebase/FirebasePermissionAnalytics.kt b/app/src/main/java/com/darshan/notificity/analytics/providers/firebase/FirebasePermissionAnalytics.kt new file mode 100644 index 0000000..2797e45 --- /dev/null +++ b/app/src/main/java/com/darshan/notificity/analytics/providers/firebase/FirebasePermissionAnalytics.kt @@ -0,0 +1,69 @@ +package com.darshan.notificity.analytics.providers.firebase + +import com.darshan.notificity.analytics.domain.PermissionAnalytics +import com.darshan.notificity.analytics.mergeCommonParams +import com.darshan.notificity.auth.repository.AuthRepository +import com.darshan.notificity.enums.AppPermissions +import com.darshan.notificity.enums.NotificationPermissionStatus +import com.darshan.notificity.extensions.toBundle +import com.darshan.notificity.utils.Util.Companion.getEpoch +import com.google.firebase.analytics.FirebaseAnalytics +import kotlinx.coroutines.CoroutineScope +import kotlinx.coroutines.Dispatchers +import kotlinx.coroutines.SupervisorJob +import kotlinx.coroutines.launch +import javax.inject.Inject + +/** + * Firebase implementation of PermissionAnalytics interface. + * Provides Firebase Analytics integration for permission-related events. + * + * @param firebaseAnalytics Firebase Analytics instance for logging events + * @param authRepository Repository for accessing current user data + */ +class FirebasePermissionAnalytics @Inject constructor( + private val firebaseAnalytics: FirebaseAnalytics, + private val authRepository: AuthRepository +) : PermissionAnalytics { + + private val analyticsScope = CoroutineScope(SupervisorJob() + Dispatchers.IO) + + /** + * Logs an event with enhanced information. + * Automatically adds current user ID, timestamp etc. to event parameters. + */ + private suspend fun logEvent( + eventName: String, + parameters: Map = emptyMap() + ) { + val user = authRepository.getCurrentUserData() + + val enhancedParams = parameters.mergeCommonParams(user, getEpoch()) + firebaseAnalytics.logEvent(eventName, enhancedParams.toBundle()) + } + + override fun onPermissionRequested(permission: AppPermissions) { + analyticsScope.launch { + logEvent( + eventName = "${permission.name}_${PermissionAnalytics.PERMISSION_REQUESTED}" + ) + } + } + + override fun onPermissionChanged( + permission: AppPermissions, + status: NotificationPermissionStatus + ) { + // Set as user property for segmentation + firebaseAnalytics.setUserProperty("${permission.name}_permission", status.code.toString()) + + analyticsScope.launch { + logEvent( + eventName = PermissionAnalytics.PERMISSION_CHANGED, + parameters = mapOf( + PermissionAnalytics.PERMISSION_STATUS to status.code, + ) + ) + } + } +} \ No newline at end of file diff --git a/app/src/main/java/com/darshan/notificity/di/AnalyticsModule.kt b/app/src/main/java/com/darshan/notificity/di/AnalyticsModule.kt new file mode 100644 index 0000000..84392a2 --- /dev/null +++ b/app/src/main/java/com/darshan/notificity/di/AnalyticsModule.kt @@ -0,0 +1,45 @@ +package com.darshan.notificity.di + +import android.content.Context +import com.darshan.notificity.analytics.domain.AppAnalytics +import com.darshan.notificity.analytics.domain.AuthAnalytics +import com.darshan.notificity.analytics.domain.PermissionAnalytics +import com.darshan.notificity.analytics.providers.firebase.FirebaseAppAnalytics +import com.darshan.notificity.analytics.providers.firebase.FirebaseAuthAnalytics +import com.darshan.notificity.analytics.providers.firebase.FirebasePermissionAnalytics +import com.google.firebase.analytics.FirebaseAnalytics +import dagger.Binds +import dagger.Module +import dagger.Provides +import dagger.hilt.InstallIn +import dagger.hilt.android.qualifiers.ApplicationContext +import dagger.hilt.components.SingletonComponent +import javax.inject.Singleton + +@Module +@InstallIn(SingletonComponent::class) +abstract class AnalyticsModule { + + @Binds + abstract fun bindAuthAnalytics( + firebaseAuthAnalytics: FirebaseAuthAnalytics + ): AuthAnalytics + + @Binds + abstract fun bindAppAnalytics( + firebaseAppAnalytics: FirebaseAppAnalytics + ): AppAnalytics + + @Binds + abstract fun bindPermissionAnalytics( + firebasePermissionAnalytics: FirebasePermissionAnalytics + ): PermissionAnalytics + + companion object { + @Provides + @Singleton + fun provideFirebaseAnalytics(@ApplicationContext context: Context): FirebaseAnalytics { + return FirebaseAnalytics.getInstance(context) + } + } +} \ No newline at end of file diff --git a/app/src/main/java/com/darshan/notificity/enums/AppPermissions.kt b/app/src/main/java/com/darshan/notificity/enums/AppPermissions.kt new file mode 100644 index 0000000..c4ef2f0 --- /dev/null +++ b/app/src/main/java/com/darshan/notificity/enums/AppPermissions.kt @@ -0,0 +1,5 @@ +package com.darshan.notificity.enums + +enum class AppPermissions(val key: String) { + NOTIFICATION("notification") +} \ No newline at end of file diff --git a/app/src/main/java/com/darshan/notificity/extensions/Extensions.kt b/app/src/main/java/com/darshan/notificity/extensions/Extensions.kt index 0807a6b..2c10ff9 100644 --- a/app/src/main/java/com/darshan/notificity/extensions/Extensions.kt +++ b/app/src/main/java/com/darshan/notificity/extensions/Extensions.kt @@ -8,6 +8,7 @@ import android.content.Intent import android.content.pm.PackageManager import android.net.Uri import android.os.Build +import android.os.Bundle import android.provider.Settings import androidx.activity.result.ActivityResultLauncher import androidx.core.app.NotificationManagerCompat @@ -15,6 +16,8 @@ import androidx.core.content.ContextCompat import androidx.core.net.toUri import com.darshan.notificity.auth.AuthType import com.darshan.notificity.enums.NotificationPermissionStatus +import kotlin.collections.component1 +import kotlin.collections.component2 /** * Launches the app settings screen @@ -94,3 +97,18 @@ inline fun Map.getProviderOrError( ): T { return this[type] as? T ?: throw IllegalStateException(errorMessage) } + +fun Map.toBundle(): Bundle { + return Bundle().apply { + forEach { (key, value) -> + when (value) { + is String -> putString(key, value) + is Int -> putInt(key, value) + is Long -> putLong(key, value) + is Double -> putDouble(key, value) + is Boolean -> putBoolean(key, value) + else -> putString(key, value.toString()) + } + } + } +} \ No newline at end of file diff --git a/app/src/main/java/com/darshan/notificity/main/ui/MainActivity.kt b/app/src/main/java/com/darshan/notificity/main/ui/MainActivity.kt index 4a23203..8cf8f60 100644 --- a/app/src/main/java/com/darshan/notificity/main/ui/MainActivity.kt +++ b/app/src/main/java/com/darshan/notificity/main/ui/MainActivity.kt @@ -60,11 +60,10 @@ import androidx.lifecycle.lifecycleScope import com.darshan.notificity.AppInfo import com.darshan.notificity.NotificationEntity import com.darshan.notificity.NotificationsActivity -import com.darshan.notificity.analytics.AnalyticsConstants -import com.darshan.notificity.analytics.AnalyticsLogger import com.darshan.notificity.components.EmptyContentState import com.darshan.notificity.components.LoadingScreen import com.darshan.notificity.components.NotificityAppBar +import com.darshan.notificity.enums.AppPermissions import com.darshan.notificity.enums.NotificationPermissionStatus import com.darshan.notificity.extensions.getNotificationPermissionStatus import com.darshan.notificity.extensions.isLaunchedFromLauncher @@ -72,6 +71,7 @@ import com.darshan.notificity.extensions.launchActivity import com.darshan.notificity.extensions.openAppSettings import com.darshan.notificity.extensions.toTitleCase import com.darshan.notificity.main.viewmodel.MainViewModel +import com.darshan.notificity.analytics.enums.Screen import com.darshan.notificity.ui.BaseActivity import com.darshan.notificity.ui.settings.SettingsActivity import com.darshan.notificity.ui.settings.SettingsViewModel @@ -108,8 +108,8 @@ class MainActivity : BaseActivity() { logNotificationPermissionStatus(updatedStatus) } - override val screenName: String - get() = AnalyticsConstants.Screens.MAIN + override val screen: Screen + get() = Screen.MAIN override fun onCreate(savedInstanceState: Bundle?) { super.onCreate(savedInstanceState) @@ -163,7 +163,10 @@ class MainActivity : BaseActivity() { appSettingsLauncher = appSettingsLauncher, openNotificationAccessSettings = { openNotificationAccessSettings() }, requestPermissionLauncher = { requestPermissionLauncher.launch(it) }, - toggleNotificationPermissionDialog = mainViewModel::showNotificationPermissionBlockedDialog + toggleNotificationPermissionDialog = mainViewModel::showNotificationPermissionBlockedDialog, + onPermissionRequested = { + analyticsManager.permission.onPermissionRequested(AppPermissions.NOTIFICATION) + } ) } } @@ -175,7 +178,7 @@ class MainActivity : BaseActivity() { // It will ONLY run during a fresh launch (cold start) val source = if (intent.isLaunchedFromLauncher()) "launcher" else "external_or_notification" - AnalyticsLogger.onAppLaunch(source) + analyticsManager.app.onAppLaunch(source) // Log notification permission status at app launch val status = getNotificationPermissionStatus() @@ -194,8 +197,7 @@ class MainActivity : BaseActivity() { } fun logNotificationPermissionStatus(status: NotificationPermissionStatus) { - AnalyticsLogger.onNotificationPermissionChanged(status) - AnalyticsLogger.setNotificationPermissionProperty(status) + analyticsManager.permission.onPermissionChanged(AppPermissions.NOTIFICATION, status) } } @@ -210,6 +212,7 @@ fun MainScreen( toggleNotificationPermissionDialog: (Boolean) -> Unit, openNotificationAccessSettings: () -> Unit, requestPermissionLauncher: (String) -> Unit, + onPermissionRequested: () -> Unit ) { val context = LocalContext.current Scaffold( @@ -235,7 +238,8 @@ fun MainScreen( AppSearchScreen(notifications = notifications, allApps = apps) AskNotificationPermission( requestPermissionLauncher = requestPermissionLauncher, - toggleNotificationPermissionDialog = toggleNotificationPermissionDialog + toggleNotificationPermissionDialog = toggleNotificationPermissionDialog, + onPermissionRequested = onPermissionRequested ) } else { RequestAccessScreen(openNotificationAccessSettings = openNotificationAccessSettings) @@ -394,7 +398,8 @@ fun RequestAccessScreen(openNotificationAccessSettings: () -> Unit) { @Composable private fun AskNotificationPermission( requestPermissionLauncher: (String) -> Unit, - toggleNotificationPermissionDialog: (Boolean) -> Unit + toggleNotificationPermissionDialog: (Boolean) -> Unit, + onPermissionRequested: () -> Unit ) { var alreadyAsked by rememberSaveable { mutableStateOf(false) } val context = LocalContext.current @@ -409,7 +414,7 @@ private fun AskNotificationPermission( if (activity?.shouldShowRequestPermissionRationale(Manifest.permission.POST_NOTIFICATIONS) == true) { toggleNotificationPermissionDialog(true) } else { - AnalyticsLogger.onNotificationPermissionRequested() + onPermissionRequested() requestPermissionLauncher(Manifest.permission.POST_NOTIFICATIONS) } } diff --git a/app/src/main/java/com/darshan/notificity/ui/BaseActivity.kt b/app/src/main/java/com/darshan/notificity/ui/BaseActivity.kt index 73b303f..920ae64 100644 --- a/app/src/main/java/com/darshan/notificity/ui/BaseActivity.kt +++ b/app/src/main/java/com/darshan/notificity/ui/BaseActivity.kt @@ -1,11 +1,16 @@ package com.darshan.notificity.ui import androidx.activity.ComponentActivity -import com.darshan.notificity.analytics.AnalyticsLogger +import com.darshan.notificity.analytics.core.AnalyticsManager +import com.darshan.notificity.analytics.enums.Screen +import javax.inject.Inject abstract class BaseActivity : ComponentActivity() { - abstract val screenName: String + @Inject + lateinit var analyticsManager: AnalyticsManager + + abstract val screen: Screen companion object { private var lastScreenName: String? = null @@ -14,9 +19,9 @@ abstract class BaseActivity : ComponentActivity() { override fun onResume() { super.onResume() - if (lastScreenName != screenName) { - AnalyticsLogger.onScreenViewed(screenName, this::class.java.simpleName) - lastScreenName = screenName + if (lastScreenName != screen.screenName) { + analyticsManager.app.onScreenViewed(screen) + lastScreenName = screen.screenName } } } diff --git a/app/src/main/java/com/darshan/notificity/ui/settings/SettingsActivity.kt b/app/src/main/java/com/darshan/notificity/ui/settings/SettingsActivity.kt index 8b726e5..b1e779b 100644 --- a/app/src/main/java/com/darshan/notificity/ui/settings/SettingsActivity.kt +++ b/app/src/main/java/com/darshan/notificity/ui/settings/SettingsActivity.kt @@ -52,12 +52,11 @@ import androidx.lifecycle.compose.collectAsStateWithLifecycle import com.darshan.notificity.AboutActivity import com.darshan.notificity.CardColor import com.darshan.notificity.R -import com.darshan.notificity.analytics.AnalyticsConstants -import com.darshan.notificity.analytics.AnalyticsLogger import com.darshan.notificity.components.NotificityAppBar import com.darshan.notificity.components.dialogs.ConfirmationDialog import com.darshan.notificity.extensions.launchActivity import com.darshan.notificity.extensions.recommendApp +import com.darshan.notificity.analytics.enums.Screen import com.darshan.notificity.ui.BaseActivity import com.darshan.notificity.ui.signin.AuthViewModel import com.darshan.notificity.ui.signin.SignInActivity @@ -72,8 +71,8 @@ class SettingsActivity : BaseActivity() { private val settingsViewModel: SettingsViewModel by viewModels() private val authViewModel: AuthViewModel by viewModels() - override val screenName: String - get() = AnalyticsConstants.Screens.SETTINGS + override val screen: Screen + get() = Screen.SETTINGS override fun onCreate(savedInstanceState: Bundle?) { super.onCreate(savedInstanceState) @@ -91,6 +90,9 @@ class SettingsActivity : BaseActivity() { launchActivity() finish() + }, + onRecommendAppClicked = { + analyticsManager.app.onRecommendAppClicked() } ) } @@ -104,7 +106,8 @@ fun SettingsScreen( currentTheme: ThemeMode, themeChange: (ThemeMode) -> Unit, onBack: () -> Unit, - onLogout: () -> Unit = {} + onLogout: () -> Unit = {}, + onRecommendAppClicked: () -> Unit ) { val sheetState = rememberModalBottomSheetState() @@ -144,9 +147,8 @@ fun SettingsScreen( icon = rememberVectorPainter(image = Icons.Outlined.Share), text = "Recommend this app", onClick = { + onRecommendAppClicked() context.recommendApp() - - AnalyticsLogger.onRecommendAppClicked() } ) @@ -376,5 +378,5 @@ fun LogoutCard( @Composable @Preview(showSystemUi = true, showBackground = true) fun ShowSettingsScreen() { - SettingsScreen(currentTheme = ThemeMode.LIGHT, themeChange = {}, onBack = {}) + SettingsScreen(currentTheme = ThemeMode.LIGHT, themeChange = {}, onBack = {}, onRecommendAppClicked = {}) } \ No newline at end of file diff --git a/app/src/main/java/com/darshan/notificity/ui/settings/SettingsViewModel.kt b/app/src/main/java/com/darshan/notificity/ui/settings/SettingsViewModel.kt index 017a4d6..7ef9caa 100644 --- a/app/src/main/java/com/darshan/notificity/ui/settings/SettingsViewModel.kt +++ b/app/src/main/java/com/darshan/notificity/ui/settings/SettingsViewModel.kt @@ -2,7 +2,7 @@ package com.darshan.notificity.ui.settings import androidx.lifecycle.ViewModel import androidx.lifecycle.viewModelScope -import com.darshan.notificity.analytics.AnalyticsLogger +import com.darshan.notificity.analytics.domain.AppAnalytics import com.darshan.notificity.ui.theme.ThemeMode import com.darshan.notificity.ui.theme.ThemePreferenceManager import dagger.hilt.android.lifecycle.HiltViewModel @@ -13,7 +13,8 @@ import javax.inject.Inject @HiltViewModel class SettingsViewModel @Inject constructor( - private val themePreferenceManager: ThemePreferenceManager + private val themePreferenceManager: ThemePreferenceManager, + private val appAnalytics: AppAnalytics ) : ViewModel() { private val _themeMode = MutableStateFlow(ThemeMode.SYSTEM) @@ -32,7 +33,7 @@ class SettingsViewModel @Inject constructor( themePreferenceManager.saveTheme(theme) _themeMode.value = theme - AnalyticsLogger.onThemeToggleClicked(theme.name) + appAnalytics.onThemeToggleClicked(theme.name) } } } \ No newline at end of file diff --git a/app/src/main/java/com/darshan/notificity/ui/signin/AuthViewModel.kt b/app/src/main/java/com/darshan/notificity/ui/signin/AuthViewModel.kt index a60a689..9c555be 100644 --- a/app/src/main/java/com/darshan/notificity/ui/signin/AuthViewModel.kt +++ b/app/src/main/java/com/darshan/notificity/ui/signin/AuthViewModel.kt @@ -4,9 +4,11 @@ import android.content.Context import androidx.activity.ComponentActivity import androidx.lifecycle.ViewModel import androidx.lifecycle.viewModelScope +import com.darshan.notificity.auth.AuthType import com.darshan.notificity.auth.models.AuthResult import com.darshan.notificity.auth.models.AuthUiState import com.darshan.notificity.auth.repository.AuthRepository +import com.darshan.notificity.analytics.domain.AuthAnalytics import dagger.hilt.android.lifecycle.HiltViewModel import kotlinx.coroutines.flow.MutableStateFlow import kotlinx.coroutines.flow.StateFlow @@ -21,7 +23,8 @@ import javax.inject.Inject */ @HiltViewModel class AuthViewModel @Inject constructor( - private val authRepository: AuthRepository + private val authRepository: AuthRepository, + private val authAnalytics: AuthAnalytics ) : ViewModel() { private val _uiState = MutableStateFlow(AuthUiState()) @@ -65,11 +68,15 @@ class AuthViewModel @Inject constructor( it.copy(isGoogleLoading = true, error = null) } + authAnalytics.onSigninAttempt(AuthType.GOOGLE) + when (val result = authRepository.signInWithGoogle(activityContext)) { is AuthResult.Success -> { + authRepository.getCurrentUserData()?.let { authAnalytics.onSigninSuccess(it) } handleAuthSuccess() } is AuthResult.Error -> { + authAnalytics.onSigninFailure(AuthType.GOOGLE, result.exception.message ?: "") handleAuthError(result.exception.message ?: "Google sign-in failed") } } @@ -86,11 +93,15 @@ class AuthViewModel @Inject constructor( it.copy(isAnonymousLoading = true, error = null) } + authAnalytics.onSigninAttempt(AuthType.ANONYMOUS) + when (val result = authRepository.signInAnonymously()) { is AuthResult.Success -> { + authRepository.getCurrentUserData()?.let { authAnalytics.onSigninSuccess(it) } handleAuthSuccess() } is AuthResult.Error -> { + authAnalytics.onSigninFailure(AuthType.ANONYMOUS, result.exception.message ?: "") handleAuthError(result.exception.message ?: "Anonymous sign-in failed") } } @@ -103,6 +114,8 @@ class AuthViewModel @Inject constructor( fun signOut(context: Context) { viewModelScope.launch { + authAnalytics.onSignout() + when (val result = authRepository.signOut(context = context)) { is AuthResult.Success -> { _uiState.update { diff --git a/app/src/main/java/com/darshan/notificity/ui/signin/SigninActivity.kt b/app/src/main/java/com/darshan/notificity/ui/signin/SigninActivity.kt index 72baf9b..a7ebf0d 100644 --- a/app/src/main/java/com/darshan/notificity/ui/signin/SigninActivity.kt +++ b/app/src/main/java/com/darshan/notificity/ui/signin/SigninActivity.kt @@ -48,13 +48,13 @@ import androidx.compose.ui.res.painterResource import androidx.compose.ui.unit.dp import androidx.lifecycle.compose.collectAsStateWithLifecycle import com.darshan.notificity.R -import com.darshan.notificity.analytics.AnalyticsConstants import com.darshan.notificity.components.AppTitle import com.darshan.notificity.components.HeadlineWithDescription import com.darshan.notificity.components.LottieCenteredAnimation import com.darshan.notificity.components.buttons.PrimaryActionButton import com.darshan.notificity.extensions.launchActivity import com.darshan.notificity.main.ui.MainActivity +import com.darshan.notificity.analytics.enums.Screen import com.darshan.notificity.ui.BaseActivity import com.darshan.notificity.ui.settings.SettingsViewModel import com.darshan.notificity.ui.theme.NotificityTheme @@ -66,8 +66,8 @@ class SignInActivity : BaseActivity() { private val authViewModel: AuthViewModel by viewModels() private val settingsViewModel: SettingsViewModel by viewModels() - override val screenName: String - get() = AnalyticsConstants.Screens.SIGNIN + override val screen: Screen + get() = Screen.SIGNIN override fun onCreate(savedInstanceState: Bundle?) { super.onCreate(savedInstanceState) @@ -103,6 +103,7 @@ class SignInActivity : BaseActivity() { LaunchedEffect(uiState.error) { uiState.error?.let { error -> + analyticsManager.app.onErrorOccurred(screen, error) snackbarHostState.showSnackbar(message = error) viewModel.clearError() } diff --git a/app/src/main/java/com/darshan/notificity/utils/Util.kt b/app/src/main/java/com/darshan/notificity/utils/Util.kt index 5c715ac..705b4bd 100644 --- a/app/src/main/java/com/darshan/notificity/utils/Util.kt +++ b/app/src/main/java/com/darshan/notificity/utils/Util.kt @@ -13,5 +13,9 @@ open class Util { val formatter = DateTimeFormatter.ofPattern("dd-MMM-yyyy HH:mm") return localDate.format(formatter) } + + fun getEpoch(): Long { + return System.currentTimeMillis() / 1000 + } } } \ No newline at end of file