From da425d439e0e372fbc3bcd3d1ced177602ef38ec Mon Sep 17 00:00:00 2001 From: AdamGrzybkowski Date: Wed, 20 Aug 2025 11:24:26 +0200 Subject: [PATCH 1/6] Bind privacy settings and userInfo with the TracksTracker --- analytics/build.gradle.kts | 1 + .../kotlin/com/gravatar/analytics/Tracker.kt | 1 - .../gravatar/analytics/TrackerSetupData.kt | 11 ++ .../analytics/TrackerSetupDataProvider.kt | 7 + .../analytics/tracks/TracksTracker.kt | 25 ++- app/build.gradle.kts | 3 + .../app/AppTrackerSetupDataProvider.kt | 31 ++++ .../com/gravatar/app/analytics/AppEvent.kt | 10 -- .../java/com/gravatar/app/di/AppModule.kt | 5 + .../app/AppTrackerSetupDataProviderTest.kt | 149 ++++++++++++++++++ 10 files changed, 230 insertions(+), 13 deletions(-) create mode 100644 analytics/src/main/kotlin/com/gravatar/analytics/TrackerSetupData.kt create mode 100644 analytics/src/main/kotlin/com/gravatar/analytics/TrackerSetupDataProvider.kt create mode 100644 app/src/main/java/com/gravatar/app/AppTrackerSetupDataProvider.kt delete mode 100644 app/src/main/java/com/gravatar/app/analytics/AppEvent.kt create mode 100644 app/src/test/kotlin/com/gravatar/app/AppTrackerSetupDataProviderTest.kt diff --git a/analytics/build.gradle.kts b/analytics/build.gradle.kts index 0950dec3..b4fabd44 100644 --- a/analytics/build.gradle.kts +++ b/analytics/build.gradle.kts @@ -10,6 +10,7 @@ dependencies { implementation(project.dependencies.platform(libs.koin.bom)) implementation(libs.koin.core) + implementation(libs.kotlinx.coroutines) implementation(libs.automattic.tracks) testImplementation(libs.junit) diff --git a/analytics/src/main/kotlin/com/gravatar/analytics/Tracker.kt b/analytics/src/main/kotlin/com/gravatar/analytics/Tracker.kt index fc9394cc..c83224ca 100644 --- a/analytics/src/main/kotlin/com/gravatar/analytics/Tracker.kt +++ b/analytics/src/main/kotlin/com/gravatar/analytics/Tracker.kt @@ -1,7 +1,6 @@ package com.gravatar.analytics abstract class Tracker { - abstract var userId: String? abstract fun trackEvent(event: Event) abstract fun flush() } diff --git a/analytics/src/main/kotlin/com/gravatar/analytics/TrackerSetupData.kt b/analytics/src/main/kotlin/com/gravatar/analytics/TrackerSetupData.kt new file mode 100644 index 00000000..7d828da2 --- /dev/null +++ b/analytics/src/main/kotlin/com/gravatar/analytics/TrackerSetupData.kt @@ -0,0 +1,11 @@ +package com.gravatar.analytics + +data class TrackerSetupData( + val trackingState: TrackingState = TrackingState.ENABLED, + val userId: String? = null, +) + +enum class TrackingState { + ENABLED, + DISABLED, +} diff --git a/analytics/src/main/kotlin/com/gravatar/analytics/TrackerSetupDataProvider.kt b/analytics/src/main/kotlin/com/gravatar/analytics/TrackerSetupDataProvider.kt new file mode 100644 index 00000000..5635fac5 --- /dev/null +++ b/analytics/src/main/kotlin/com/gravatar/analytics/TrackerSetupDataProvider.kt @@ -0,0 +1,7 @@ +package com.gravatar.analytics + +import kotlinx.coroutines.flow.Flow + +interface TrackerSetupDataProvider { + fun getTrackerSetupData(): Flow +} diff --git a/analytics/src/main/kotlin/com/gravatar/analytics/tracks/TracksTracker.kt b/analytics/src/main/kotlin/com/gravatar/analytics/tracks/TracksTracker.kt index d620da29..c2943085 100644 --- a/analytics/src/main/kotlin/com/gravatar/analytics/tracks/TracksTracker.kt +++ b/analytics/src/main/kotlin/com/gravatar/analytics/tracks/TracksTracker.kt @@ -3,19 +3,40 @@ package com.gravatar.analytics.tracks import com.automattic.android.tracks.TracksClient import com.gravatar.analytics.Event import com.gravatar.analytics.Tracker +import com.gravatar.analytics.TrackerSetupDataProvider +import com.gravatar.analytics.TrackingState import com.gravatar.analytics.asJson +import kotlinx.coroutines.CoroutineScope +import kotlinx.coroutines.flow.launchIn +import kotlinx.coroutines.flow.onEach import java.util.UUID -internal class TracksTracker(private val tracksClient: TracksClient) : Tracker() { +internal class TracksTracker( + trackerSetupDataProvider: TrackerSetupDataProvider, + applicationScope: CoroutineScope, + private val tracksClient: TracksClient, +) : Tracker() { internal companion object { const val TRACKS_EVENT_NAME_PREFIX = "gravatarandroid_" } - override var userId: String? = null + init { + trackerSetupDataProvider.getTrackerSetupData() + .onEach { + trackingState = it.trackingState + userId = it.userId + } + .launchIn(applicationScope) + } + + private var userId: String? = null private val anonId: String = generateNewAnonID() + private var trackingState: TrackingState = TrackingState.ENABLED override fun trackEvent(event: Event) { + if (trackingState == TrackingState.DISABLED) return + val userType = userId?.let { TracksClient.NosaraUserType.WPCOM } ?: TracksClient.NosaraUserType.ANON diff --git a/app/build.gradle.kts b/app/build.gradle.kts index 34bf5349..e5b17c97 100644 --- a/app/build.gradle.kts +++ b/app/build.gradle.kts @@ -33,5 +33,8 @@ dependencies { testImplementation(libs.junit) testImplementation(libs.koin.test.junit4) + testImplementation(libs.kotlinx.coroutines.test) + testImplementation(libs.mockk.android) + testImplementation(libs.turbine) testImplementation(project(":testUtils")) } diff --git a/app/src/main/java/com/gravatar/app/AppTrackerSetupDataProvider.kt b/app/src/main/java/com/gravatar/app/AppTrackerSetupDataProvider.kt new file mode 100644 index 00000000..1d86aa0d --- /dev/null +++ b/app/src/main/java/com/gravatar/app/AppTrackerSetupDataProvider.kt @@ -0,0 +1,31 @@ +package com.gravatar.app + +import com.gravatar.analytics.TrackerSetupData +import com.gravatar.analytics.TrackerSetupDataProvider +import com.gravatar.analytics.TrackingState +import com.gravatar.app.usercomponent.domain.repository.UserRepository +import com.gravatar.app.usercomponent.domain.usecase.GetPrivacySettings +import kotlinx.coroutines.flow.Flow +import kotlinx.coroutines.flow.combine +import kotlinx.coroutines.flow.distinctUntilChanged +import kotlinx.coroutines.flow.map + +class AppTrackerSetupDataProvider( + private val getPrivacySettings: GetPrivacySettings, + private val userRepository: UserRepository, +) : TrackerSetupDataProvider { + override fun getTrackerSetupData(): Flow { + val userIdFlow: Flow = userRepository.getProfile().map { it?.userId?.toString() }.distinctUntilChanged() + return getPrivacySettings() + .combine(userIdFlow) { privacySettings, userId -> + TrackerSetupData( + trackingState = if (privacySettings.analyticsEnabled) { + TrackingState.ENABLED + } else { + TrackingState.DISABLED + }, + userId = userId + ) + } + } +} diff --git a/app/src/main/java/com/gravatar/app/analytics/AppEvent.kt b/app/src/main/java/com/gravatar/app/analytics/AppEvent.kt deleted file mode 100644 index 6de994f7..00000000 --- a/app/src/main/java/com/gravatar/app/analytics/AppEvent.kt +++ /dev/null @@ -1,10 +0,0 @@ -package com.gravatar.app.analytics - -import com.gravatar.analytics.Event - -sealed class AppEvent : Event { - - data object Test : AppEvent() { - override val name: String = "gravatarandroid_test_test" - } -} diff --git a/app/src/main/java/com/gravatar/app/di/AppModule.kt b/app/src/main/java/com/gravatar/app/di/AppModule.kt index 2c872745..02b83e32 100644 --- a/app/src/main/java/com/gravatar/app/di/AppModule.kt +++ b/app/src/main/java/com/gravatar/app/di/AppModule.kt @@ -1,11 +1,15 @@ package com.gravatar.app.di +import com.gravatar.analytics.TrackerSetupDataProvider import com.gravatar.analytics.di.analyticsModule +import com.gravatar.app.AppTrackerSetupDataProvider import com.gravatar.app.clock.di.clockModule import com.gravatar.app.homeUi.di.homeUiModule import com.gravatar.app.loginUi.di.loginUiModule import com.gravatar.app.networkmonitor.di.networkMonitorModule import com.gravatar.crashlogging.di.crashLoggingModule +import org.koin.core.module.dsl.bind +import org.koin.core.module.dsl.singleOf import org.koin.dsl.module val appModule = module { @@ -19,4 +23,5 @@ val appModule = module { buildConfigModule, crashLoggingModule, ) + singleOf(::AppTrackerSetupDataProvider) { bind() } } diff --git a/app/src/test/kotlin/com/gravatar/app/AppTrackerSetupDataProviderTest.kt b/app/src/test/kotlin/com/gravatar/app/AppTrackerSetupDataProviderTest.kt new file mode 100644 index 00000000..771cbbb7 --- /dev/null +++ b/app/src/test/kotlin/com/gravatar/app/AppTrackerSetupDataProviderTest.kt @@ -0,0 +1,149 @@ +package com.gravatar.app + +import app.cash.turbine.test +import com.gravatar.analytics.TrackerSetupData +import com.gravatar.analytics.TrackingState +import com.gravatar.app.testUtils.CoroutineTestRule +import com.gravatar.app.usercomponent.domain.model.PrivacySettings +import com.gravatar.app.usercomponent.domain.repository.UserRepository +import com.gravatar.app.usercomponent.domain.usecase.GetPrivacySettings +import com.gravatar.restapi.models.Profile +import kotlinx.coroutines.ExperimentalCoroutinesApi +import kotlinx.coroutines.flow.Flow +import kotlinx.coroutines.flow.MutableSharedFlow +import kotlinx.coroutines.test.StandardTestDispatcher +import kotlinx.coroutines.test.runTest +import org.junit.Assert.assertEquals +import org.junit.Before +import org.junit.Rule +import org.junit.Test +import java.net.URI + +@OptIn(ExperimentalCoroutinesApi::class) +class AppTrackerSetupDataProviderTest { + private val testDispatcher = StandardTestDispatcher() + + @get:Rule + var coroutineTestRule = CoroutineTestRule(testDispatcher) + + private lateinit var provider: AppTrackerSetupDataProvider + + private lateinit var privacySettingsFlow: MutableSharedFlow + private lateinit var profileFlow: MutableSharedFlow + + private val getPrivacySettings: GetPrivacySettings = object : GetPrivacySettings { + override fun invoke() = privacySettingsFlow + } + + private val userRepository: UserRepository = object : UserRepository { + override suspend fun refreshProfile(): Result = throw NotImplementedError() + override suspend fun selectAvatar(avatarId: String): Result = throw NotImplementedError() + override suspend fun getAvatars() = throw NotImplementedError() + override fun getProfile(): Flow = profileFlow + override suspend fun updateProfile( + updateRequest: com.gravatar.restapi.models.UpdateProfileRequest + ): Result = + throw NotImplementedError() + + override suspend fun uploadAvatar(avatarFile: java.io.File) = throw NotImplementedError() + override suspend fun deleteAvatar(avatarId: String): Result = throw NotImplementedError() + } + + @Before + fun setup() { + privacySettingsFlow = MutableSharedFlow() + profileFlow = MutableSharedFlow() + provider = AppTrackerSetupDataProvider( + getPrivacySettings = getPrivacySettings, + userRepository = userRepository, + ) + } + + @Test + fun `emits ENABLED when analytics enabled with non-null user id`() = runTest { + // Given + val profile = createProfile(123) + + provider.getTrackerSetupData().test { + // When: emit both flows (combine requires both) + privacySettingsFlow.emit(PrivacySettings(analyticsEnabled = true, crashReportingEnabled = true)) + profileFlow.emit(profile) + + // Then + assertEquals(TrackerSetupData(TrackingState.ENABLED, "123"), awaitItem()) + cancelAndIgnoreRemainingEvents() + } + } + + @Test + fun `emits DISABLED when analytics disabled with null profile`() = runTest { + provider.getTrackerSetupData().test { + // When + privacySettingsFlow.emit(PrivacySettings(analyticsEnabled = false, crashReportingEnabled = true)) + profileFlow.emit(null) + + // Then + assertEquals(TrackerSetupData(TrackingState.DISABLED, null), awaitItem()) + cancelAndIgnoreRemainingEvents() + } + } + + @Test + fun `updates when privacy settings or user id changes and skips duplicate user id`() = runTest { + // Given initial emissions + val profile = createProfile(1) + + provider.getTrackerSetupData().test { + privacySettingsFlow.emit(PrivacySettings(analyticsEnabled = true, crashReportingEnabled = true)) + profileFlow.emit(profile) + + // First combined emission + assertEquals( + TrackerSetupData(trackingState = TrackingState.ENABLED, userId = "1"), + awaitItem() + ) + + // When: change privacy to disabled => new emission expected + privacySettingsFlow.emit(PrivacySettings(analyticsEnabled = false, crashReportingEnabled = true)) + assertEquals( + TrackerSetupData(trackingState = TrackingState.DISABLED, userId = "1"), + awaitItem() + ) + + // When: emit profile with same userId (1) => due to distinctUntilChanged on userIdFlow, no new emission from combine + profileFlow.emit(profile) + expectNoEvents() + + // When: emit profile with new userId (2) => new emission expected + val profile2 = createProfile(2) + profileFlow.emit(profile2) + assertEquals( + TrackerSetupData(trackingState = TrackingState.DISABLED, userId = "2"), + awaitItem() + ) + + cancelAndIgnoreRemainingEvents() + } + } + + private fun createProfile(id: Int): Profile { + return Profile { + firstName = "John" + lastName = "Doe" + displayName = "Johny" + hash = "1234567890abcdef1234567890abcdef" + location = "New York, USA" + jobTitle = "Software Engineer" + company = "Acme Inc." + description = "A passionate software engineer with a love for coding and technology." + verifiedAccounts = emptyList() + profileUrl = URI.create("https://johndoe.com") + avatarUrl = URI.create("https://www.gravatar.com/avatar/123") + avatarAltText = "John Doe's Gravatar" + pronouns = "he/him" + pronunciation = "John Doe" + verifiedAccounts = emptyList() + userId = id + } + } +} From b246c157867ccbfda30f1dcbaa1a4e835a281210 Mon Sep 17 00:00:00 2001 From: AdamGrzybkowski Date: Wed, 20 Aug 2025 12:53:53 +0200 Subject: [PATCH 2/6] Add tests for TracksTracker --- .../analytics/tracks/TrackTrackerTest.kt | 40 ++++++++++++++++++- 1 file changed, 38 insertions(+), 2 deletions(-) diff --git a/analytics/src/test/kotlin/com/gravatar/analytics/tracks/TrackTrackerTest.kt b/analytics/src/test/kotlin/com/gravatar/analytics/tracks/TrackTrackerTest.kt index 6f74001f..f1bb11bb 100644 --- a/analytics/src/test/kotlin/com/gravatar/analytics/tracks/TrackTrackerTest.kt +++ b/analytics/src/test/kotlin/com/gravatar/analytics/tracks/TrackTrackerTest.kt @@ -2,9 +2,15 @@ package com.gravatar.analytics.tracks import com.automattic.android.tracks.TracksClient import com.gravatar.analytics.Event +import com.gravatar.analytics.TrackerSetupData +import com.gravatar.analytics.TrackerSetupDataProvider import io.mockk.mockk import io.mockk.verify import io.mockk.verifySequence +import kotlinx.coroutines.CoroutineScope +import kotlinx.coroutines.Dispatchers +import kotlinx.coroutines.flow.Flow +import kotlinx.coroutines.flow.MutableStateFlow import org.junit.Before import org.junit.Test @@ -12,11 +18,18 @@ class TrackTrackerTest { private lateinit var tracker: TracksTracker private lateinit var mockClient: TracksClient + private lateinit var applicationScope: CoroutineScope + private lateinit var mutableSetupData: MutableStateFlow @Before fun setUp() { mockClient = mockk(relaxed = true) - tracker = TracksTracker(mockClient) + applicationScope = CoroutineScope(Dispatchers.Unconfined) + mutableSetupData = MutableStateFlow(TrackerSetupData()) + val provider: TrackerSetupDataProvider = object : TrackerSetupDataProvider { + override fun getTrackerSetupData(): Flow = mutableSetupData + } + tracker = TracksTracker(provider, applicationScope, mockClient) } @Test @@ -42,7 +55,8 @@ class TrackTrackerTest { val event = object : Event { override val name: String = "test_event_with_user" } - tracker.userId = "someUserId" + // Update the shared state to include a userId so the tracker switches to WPCOM + mutableSetupData.value = TrackerSetupData(userId = "someUserId") tracker.trackEvent(event) @@ -64,4 +78,26 @@ class TrackTrackerTest { mockClient.flush() } } + + @Test + fun `when TrackingState changes then trackEvent behavior updates accordingly`() { + val event = object : Event { + override val name: String = "test_event_tracking_state" + } + + // Initially ENABLED by default, should track + tracker.trackEvent(event) + verify(exactly = 1) { mockClient.track(any(), any(), any(), any()) } + + // Disable tracking: should not track further events + mutableSetupData.value = TrackerSetupData(trackingState = com.gravatar.analytics.TrackingState.DISABLED) + tracker.trackEvent(event) + // Still only one call so far + verify(exactly = 1) { mockClient.track(any(), any(), any(), any()) } + + // Re-enable tracking: should resume tracking + mutableSetupData.value = TrackerSetupData(trackingState = com.gravatar.analytics.TrackingState.ENABLED) + tracker.trackEvent(event) + verify(exactly = 2) { mockClient.track(any(), any(), any(), any()) } + } } From 8c309946987b7bf5c151f53db2b800a6c706b58b Mon Sep 17 00:00:00 2001 From: AdamGrzybkowski Date: Wed, 20 Aug 2025 12:55:34 +0200 Subject: [PATCH 3/6] PR review comments --- .../com/gravatar/app/AppTrackerSetupDataProviderTest.kt | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/app/src/test/kotlin/com/gravatar/app/AppTrackerSetupDataProviderTest.kt b/app/src/test/kotlin/com/gravatar/app/AppTrackerSetupDataProviderTest.kt index 771cbbb7..c4e01a8f 100644 --- a/app/src/test/kotlin/com/gravatar/app/AppTrackerSetupDataProviderTest.kt +++ b/app/src/test/kotlin/com/gravatar/app/AppTrackerSetupDataProviderTest.kt @@ -60,7 +60,7 @@ class AppTrackerSetupDataProviderTest { } @Test - fun `emits ENABLED when analytics enabled with non-null user id`() = runTest { + fun `emits ENABLED when analytics enabled and non-null user id`() = runTest { // Given val profile = createProfile(123) @@ -76,7 +76,7 @@ class AppTrackerSetupDataProviderTest { } @Test - fun `emits DISABLED when analytics disabled with null profile`() = runTest { + fun `emits DISABLED when analytics disabled and null user Id when profile is null`() = runTest { provider.getTrackerSetupData().test { // When privacySettingsFlow.emit(PrivacySettings(analyticsEnabled = false, crashReportingEnabled = true)) From b09ff9ec86d21734ff361af4419c7484e993b3a4 Mon Sep 17 00:00:00 2001 From: AdamGrzybkowski Date: Wed, 20 Aug 2025 14:35:14 +0200 Subject: [PATCH 4/6] Fix AnalyticsModuleTest --- analytics/build.gradle.kts | 3 +++ .../kotlin/com/gravatar/analytics/tracks/TracksTracker.kt | 5 +++-- 2 files changed, 6 insertions(+), 2 deletions(-) diff --git a/analytics/build.gradle.kts b/analytics/build.gradle.kts index b4fabd44..308e3d08 100644 --- a/analytics/build.gradle.kts +++ b/analytics/build.gradle.kts @@ -1,5 +1,6 @@ plugins { alias(libs.plugins.gravatar.android.library) + alias(libs.plugins.ksp) } android { @@ -10,6 +11,8 @@ dependencies { implementation(project.dependencies.platform(libs.koin.bom)) implementation(libs.koin.core) + implementation(libs.koin.annotations) + ksp(libs.koin.ksp.compiler) implementation(libs.kotlinx.coroutines) implementation(libs.automattic.tracks) diff --git a/analytics/src/main/kotlin/com/gravatar/analytics/tracks/TracksTracker.kt b/analytics/src/main/kotlin/com/gravatar/analytics/tracks/TracksTracker.kt index c2943085..cdce49d9 100644 --- a/analytics/src/main/kotlin/com/gravatar/analytics/tracks/TracksTracker.kt +++ b/analytics/src/main/kotlin/com/gravatar/analytics/tracks/TracksTracker.kt @@ -9,11 +9,12 @@ import com.gravatar.analytics.asJson import kotlinx.coroutines.CoroutineScope import kotlinx.coroutines.flow.launchIn import kotlinx.coroutines.flow.onEach +import org.koin.core.annotation.Provided import java.util.UUID internal class TracksTracker( - trackerSetupDataProvider: TrackerSetupDataProvider, - applicationScope: CoroutineScope, + @Provided trackerSetupDataProvider: TrackerSetupDataProvider, + @Provided applicationScope: CoroutineScope, private val tracksClient: TracksClient, ) : Tracker() { From 4e4291bd94b41de4b5041dd76323537716026068 Mon Sep 17 00:00:00 2001 From: AdamGrzybkowski Date: Wed, 20 Aug 2025 14:37:22 +0200 Subject: [PATCH 5/6] Use userLogin instead of userId --- .../app/AppTrackerSetupDataProvider.kt | 2 +- .../app/AppTrackerSetupDataProviderTest.kt | 20 ++++++++++--------- 2 files changed, 12 insertions(+), 10 deletions(-) diff --git a/app/src/main/java/com/gravatar/app/AppTrackerSetupDataProvider.kt b/app/src/main/java/com/gravatar/app/AppTrackerSetupDataProvider.kt index 1d86aa0d..7f682c28 100644 --- a/app/src/main/java/com/gravatar/app/AppTrackerSetupDataProvider.kt +++ b/app/src/main/java/com/gravatar/app/AppTrackerSetupDataProvider.kt @@ -15,7 +15,7 @@ class AppTrackerSetupDataProvider( private val userRepository: UserRepository, ) : TrackerSetupDataProvider { override fun getTrackerSetupData(): Flow { - val userIdFlow: Flow = userRepository.getProfile().map { it?.userId?.toString() }.distinctUntilChanged() + val userIdFlow: Flow = userRepository.getProfile().map { it?.userLogin }.distinctUntilChanged() return getPrivacySettings() .combine(userIdFlow) { privacySettings, userId -> TrackerSetupData( diff --git a/app/src/test/kotlin/com/gravatar/app/AppTrackerSetupDataProviderTest.kt b/app/src/test/kotlin/com/gravatar/app/AppTrackerSetupDataProviderTest.kt index c4e01a8f..fc14b29f 100644 --- a/app/src/test/kotlin/com/gravatar/app/AppTrackerSetupDataProviderTest.kt +++ b/app/src/test/kotlin/com/gravatar/app/AppTrackerSetupDataProviderTest.kt @@ -62,7 +62,7 @@ class AppTrackerSetupDataProviderTest { @Test fun `emits ENABLED when analytics enabled and non-null user id`() = runTest { // Given - val profile = createProfile(123) + val profile = createProfile("user") provider.getTrackerSetupData().test { // When: emit both flows (combine requires both) @@ -70,7 +70,7 @@ class AppTrackerSetupDataProviderTest { profileFlow.emit(profile) // Then - assertEquals(TrackerSetupData(TrackingState.ENABLED, "123"), awaitItem()) + assertEquals(TrackerSetupData(TrackingState.ENABLED, "user"), awaitItem()) cancelAndIgnoreRemainingEvents() } } @@ -91,7 +91,9 @@ class AppTrackerSetupDataProviderTest { @Test fun `updates when privacy settings or user id changes and skips duplicate user id`() = runTest { // Given initial emissions - val profile = createProfile(1) + val user1 = "user1" + val user2 = "user2" + val profile = createProfile(user1) provider.getTrackerSetupData().test { privacySettingsFlow.emit(PrivacySettings(analyticsEnabled = true, crashReportingEnabled = true)) @@ -99,14 +101,14 @@ class AppTrackerSetupDataProviderTest { // First combined emission assertEquals( - TrackerSetupData(trackingState = TrackingState.ENABLED, userId = "1"), + TrackerSetupData(trackingState = TrackingState.ENABLED, userId = user1), awaitItem() ) // When: change privacy to disabled => new emission expected privacySettingsFlow.emit(PrivacySettings(analyticsEnabled = false, crashReportingEnabled = true)) assertEquals( - TrackerSetupData(trackingState = TrackingState.DISABLED, userId = "1"), + TrackerSetupData(trackingState = TrackingState.DISABLED, userId = user1), awaitItem() ) @@ -115,10 +117,10 @@ class AppTrackerSetupDataProviderTest { expectNoEvents() // When: emit profile with new userId (2) => new emission expected - val profile2 = createProfile(2) + val profile2 = createProfile(user2) profileFlow.emit(profile2) assertEquals( - TrackerSetupData(trackingState = TrackingState.DISABLED, userId = "2"), + TrackerSetupData(trackingState = TrackingState.DISABLED, userId = user2), awaitItem() ) @@ -126,7 +128,7 @@ class AppTrackerSetupDataProviderTest { } } - private fun createProfile(id: Int): Profile { + private fun createProfile(user: String): Profile { return Profile { firstName = "John" lastName = "Doe" @@ -143,7 +145,7 @@ class AppTrackerSetupDataProviderTest { pronouns = "he/him" pronunciation = "John Doe" verifiedAccounts = emptyList() - userId = id + userLogin = user } } } From 389ef2d9d901380111731d88b596dbd56a9193d2 Mon Sep 17 00:00:00 2001 From: AdamGrzybkowski Date: Wed, 20 Aug 2025 15:17:48 +0200 Subject: [PATCH 6/6] Add userLogin to the ProfileEntity --- .../3.json | 200 ++++++++++++++++++ .../data/database/UserDatabase.kt | 7 +- .../data/database/model/ProfileEntity.kt | 4 + 3 files changed, 209 insertions(+), 2 deletions(-) create mode 100644 userComponent/schemas/com.gravatar.app.usercomponent.data.database.UserDatabase/3.json diff --git a/userComponent/schemas/com.gravatar.app.usercomponent.data.database.UserDatabase/3.json b/userComponent/schemas/com.gravatar.app.usercomponent.data.database.UserDatabase/3.json new file mode 100644 index 00000000..b8863592 --- /dev/null +++ b/userComponent/schemas/com.gravatar.app.usercomponent.data.database.UserDatabase/3.json @@ -0,0 +1,200 @@ +{ + "formatVersion": 1, + "database": { + "version": 3, + "identityHash": "27e992bd5dca715c985e3a00c579cec7", + "entities": [ + { + "tableName": "user_profiles", + "createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`user_id` INTEGER NOT NULL, `hash` TEXT NOT NULL, `display_name` TEXT NOT NULL, `profile_url` TEXT NOT NULL, `avatar_url` TEXT NOT NULL, `avatar_alt_text` TEXT NOT NULL, `description` TEXT NOT NULL, `pronouns` TEXT NOT NULL, `pronunciation` TEXT NOT NULL, `location` TEXT NOT NULL, `job_title` TEXT NOT NULL, `company` TEXT NOT NULL, `first_name` TEXT, `last_name` TEXT, `contact_cell_phone` TEXT, `contact_email` TEXT, `user_login` TEXT, PRIMARY KEY(`user_id`))", + "fields": [ + { + "fieldPath": "userId", + "columnName": "user_id", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "hash", + "columnName": "hash", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "displayName", + "columnName": "display_name", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "profileUrl", + "columnName": "profile_url", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "avatarUrl", + "columnName": "avatar_url", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "avatarAltText", + "columnName": "avatar_alt_text", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "description", + "columnName": "description", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "pronouns", + "columnName": "pronouns", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "pronunciation", + "columnName": "pronunciation", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "location", + "columnName": "location", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "jobTitle", + "columnName": "job_title", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "company", + "columnName": "company", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "firstName", + "columnName": "first_name", + "affinity": "TEXT" + }, + { + "fieldPath": "lastName", + "columnName": "last_name", + "affinity": "TEXT" + }, + { + "fieldPath": "contactCellPhone", + "columnName": "contact_cell_phone", + "affinity": "TEXT" + }, + { + "fieldPath": "contactEmail", + "columnName": "contact_email", + "affinity": "TEXT" + }, + { + "fieldPath": "userLogin", + "columnName": "user_login", + "affinity": "TEXT" + } + ], + "primaryKey": { + "autoGenerate": false, + "columnNames": [ + "user_id" + ] + } + }, + { + "tableName": "verified_accounts", + "createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`id` INTEGER PRIMARY KEY AUTOINCREMENT NOT NULL, `profile_user_id` INTEGER NOT NULL, `service_type` TEXT NOT NULL, `service_label` TEXT NOT NULL, `service_icon` TEXT NOT NULL, `url` TEXT NOT NULL, `is_hidden` INTEGER NOT NULL, FOREIGN KEY(`profile_user_id`) REFERENCES `user_profiles`(`user_id`) ON UPDATE NO ACTION ON DELETE CASCADE )", + "fields": [ + { + "fieldPath": "id", + "columnName": "id", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "profileUserId", + "columnName": "profile_user_id", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "serviceType", + "columnName": "service_type", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "serviceLabel", + "columnName": "service_label", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "serviceIcon", + "columnName": "service_icon", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "url", + "columnName": "url", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "isHidden", + "columnName": "is_hidden", + "affinity": "INTEGER", + "notNull": true + } + ], + "primaryKey": { + "autoGenerate": true, + "columnNames": [ + "id" + ] + }, + "indices": [ + { + "name": "index_verified_accounts_profile_user_id", + "unique": false, + "columnNames": [ + "profile_user_id" + ], + "orders": [], + "createSql": "CREATE INDEX IF NOT EXISTS `index_verified_accounts_profile_user_id` ON `${TABLE_NAME}` (`profile_user_id`)" + } + ], + "foreignKeys": [ + { + "table": "user_profiles", + "onDelete": "CASCADE", + "onUpdate": "NO ACTION", + "columns": [ + "profile_user_id" + ], + "referencedColumns": [ + "user_id" + ] + } + ] + } + ], + "setupQueries": [ + "CREATE TABLE IF NOT EXISTS room_master_table (id INTEGER PRIMARY KEY,identity_hash TEXT)", + "INSERT OR REPLACE INTO room_master_table (id,identity_hash) VALUES(42, '27e992bd5dca715c985e3a00c579cec7')" + ] + } +} \ No newline at end of file diff --git a/userComponent/src/main/kotlin/com/gravatar/app/usercomponent/data/database/UserDatabase.kt b/userComponent/src/main/kotlin/com/gravatar/app/usercomponent/data/database/UserDatabase.kt index 6c3f68a2..525cd070 100644 --- a/userComponent/src/main/kotlin/com/gravatar/app/usercomponent/data/database/UserDatabase.kt +++ b/userComponent/src/main/kotlin/com/gravatar/app/usercomponent/data/database/UserDatabase.kt @@ -10,9 +10,12 @@ import com.gravatar.app.usercomponent.data.database.model.VerifiedAccountEntity @Database( entities = [ProfileEntity::class, VerifiedAccountEntity::class], - version = 2, + version = 3, exportSchema = true, - autoMigrations = [AutoMigration(from = 1, to = 2)], + autoMigrations = [ + AutoMigration(from = 1, to = 2), + AutoMigration(from = 2, to = 3), + ], ) internal abstract class UserDatabase : RoomDatabase() { diff --git a/userComponent/src/main/kotlin/com/gravatar/app/usercomponent/data/database/model/ProfileEntity.kt b/userComponent/src/main/kotlin/com/gravatar/app/usercomponent/data/database/model/ProfileEntity.kt index f7b41add..b6dc4979 100644 --- a/userComponent/src/main/kotlin/com/gravatar/app/usercomponent/data/database/model/ProfileEntity.kt +++ b/userComponent/src/main/kotlin/com/gravatar/app/usercomponent/data/database/model/ProfileEntity.kt @@ -43,6 +43,8 @@ data class ProfileEntity( val contactCellPhone: String? = null, @ColumnInfo(name = "contact_email") val contactEmail: String? = null, + @ColumnInfo(name = "user_login") + val userLogin: String? = null, ) { /** * Converts this entity to a Profile model. @@ -63,6 +65,7 @@ data class ProfileEntity( company = this@ProfileEntity.company firstName = this@ProfileEntity.firstName lastName = this@ProfileEntity.lastName + userLogin = this@ProfileEntity.userLogin contactInfo = ProfileContactInfo { cellPhone = this@ProfileEntity.contactCellPhone email = this@ProfileEntity.contactEmail @@ -93,6 +96,7 @@ data class ProfileEntity( lastName = profile.lastName, contactCellPhone = profile.contactInfo?.cellPhone, contactEmail = profile.contactInfo?.email, + userLogin = profile.userLogin, ) } }