diff --git a/domain/src/main/java/org/oppia/android/domain/oppialogger/LoggingIdentifierController.kt b/domain/src/main/java/org/oppia/android/domain/oppialogger/LoggingIdentifierController.kt index f90e36ac141..878b9c621af 100644 --- a/domain/src/main/java/org/oppia/android/domain/oppialogger/LoggingIdentifierController.kt +++ b/domain/src/main/java/org/oppia/android/domain/oppialogger/LoggingIdentifierController.kt @@ -42,7 +42,7 @@ class LoggingIdentifierController @Inject constructor( private val appSessionIdRandom by lazy { Random(appSessionRandomSeed) } private val sessionId by lazy { MutableStateFlow(computeSessionId()) } - private val appSessionId by lazy { MutableStateFlow(computeAppSessionId()) } + private val appSessionId by lazy { MutableStateFlow(generateCustomUUID().toString()) } private val sessionIdDataProvider by lazy { dataProviders.run { sessionId.convertToAutomaticDataProvider(SESSION_ID_DATA_PROVIDER_ID) } } @@ -166,4 +166,30 @@ class LoggingIdentifierController @Inject constructor( */ private fun Random.randomUuid(): UUID = UUID.nameUUIDFromBytes(ByteArray(16).also { this@randomUuid.nextBytes(it) }) + + /** + * Returns a new [UUID] as [ULong] using a custom algorithm derived from Twitter's snowflake algorithm. + * + * The [UUID] is made up of; + * - 1 bit - This will cater for the Year 2038 problem. + * - 41 bits - Timestamp representation of the UTC time of generation of the UUID. + * - 22 bits - Random number to increase the uniqueness of the UUID generated. + * + * Return type is [ULong] to take full advantage of all the bits and avoid generating + * negative [UUID]s + */ + fun generateCustomUUID(): ULong { + val SIGNED_BIT = 1L + val TIMESTAMP_BITS = 41 + val RANDOM_BITS = 22 + val MAX_RANDOM_VALUE: Long = (1L shl RANDOM_BITS) - 1 + + val currentTimestamp = System.currentTimeMillis() + val randomNumber = Random().nextLong() and MAX_RANDOM_VALUE + + return ( + (SIGNED_BIT shl (TIMESTAMP_BITS + RANDOM_BITS)) + or (currentTimestamp shl RANDOM_BITS) or randomNumber + ).toULong() + } } diff --git a/domain/src/main/java/org/oppia/android/domain/oppialogger/analytics/ApplicationLifecycleObserver.kt b/domain/src/main/java/org/oppia/android/domain/oppialogger/analytics/ApplicationLifecycleObserver.kt index 24eaf81f248..4eb8a77f909 100644 --- a/domain/src/main/java/org/oppia/android/domain/oppialogger/analytics/ApplicationLifecycleObserver.kt +++ b/domain/src/main/java/org/oppia/android/domain/oppialogger/analytics/ApplicationLifecycleObserver.kt @@ -178,7 +178,9 @@ class ApplicationLifecycleObserver @Inject constructor( CoroutineScope(backgroundDispatcher).launch { // TODO(#5341): Replace appSessionId generation to the modified Twitter snowflake algorithm. val appSessionId = loggingIdentifierController.getAppSessionIdFlow().value - featureFlagsLogger.logAllFeatureFlags(appSessionId) + val profileId = profileManagementController.fetchCurrentProfileUuid() + + featureFlagsLogger.logAllFeatureFlags(appSessionId, profileId) }.invokeOnCompletion { failure -> if (failure != null) { oppiaLogger.e( diff --git a/domain/src/main/java/org/oppia/android/domain/oppialogger/analytics/FeatureFlagsLogger.kt b/domain/src/main/java/org/oppia/android/domain/oppialogger/analytics/FeatureFlagsLogger.kt index f6983f25aee..17205644f85 100644 --- a/domain/src/main/java/org/oppia/android/domain/oppialogger/analytics/FeatureFlagsLogger.kt +++ b/domain/src/main/java/org/oppia/android/domain/oppialogger/analytics/FeatureFlagsLogger.kt @@ -106,7 +106,7 @@ class FeatureFlagsLogger @Inject constructor( * * @param appSessionId denotes the id of the current appInForeground session */ - fun logAllFeatureFlags(appSessionId: String) { + fun logAllFeatureFlags(appSessionId: String, currentProfileUuid: String?) { val featureFlagItemList = mutableListOf() for (flag in featureFlagItemMap) { featureFlagItemList.add( @@ -117,6 +117,7 @@ class FeatureFlagsLogger @Inject constructor( // TODO(#5341): Set the UUID value for this context val featureFlagContext = FeatureFlagListContext.newBuilder() .setAppSessionId(appSessionId) + .setUniqueUserUuid(currentProfileUuid) .addAllFeatureFlags(featureFlagItemList) .build() diff --git a/domain/src/main/java/org/oppia/android/domain/profile/ProfileManagementController.kt b/domain/src/main/java/org/oppia/android/domain/profile/ProfileManagementController.kt index 95438d0b9d0..fb86c252b3b 100644 --- a/domain/src/main/java/org/oppia/android/domain/profile/ProfileManagementController.kt +++ b/domain/src/main/java/org/oppia/android/domain/profile/ProfileManagementController.kt @@ -285,6 +285,7 @@ class ProfileManagementController @Inject constructor( this.allowDownloadAccess = allowDownloadAccess this.allowInLessonQuickLanguageSwitching = allowInLessonQuickLanguageSwitching this.id = ProfileId.newBuilder().setInternalId(nextProfileId).build() + this.uuid = loggingIdentifierController.generateCustomUUID().toString() dateCreatedTimestampMs = oppiaClock.getCurrentTimeMs() this.isAdmin = isAdmin readingTextSize = ReadingTextSize.MEDIUM_TEXT_SIZE @@ -918,6 +919,49 @@ class ProfileManagementController @Inject constructor( */ suspend fun fetchCurrentLearnerId(): String? = getCurrentProfileId()?.let { fetchLearnerId(it) } + suspend fun fetchCurrentProfileUuid(): String { + val profileId = getCurrentProfileId() + val profileDatabase = profileDataStore.readDataAsync().await() + + return if (profileDatabase.profilesMap[profileId?.internalId]?.uuid != null) { + profileDatabase.profilesMap[profileId!!.internalId]!!.uuid!! + } else { + if (profileId == null) return "" + + val randomUuid = loggingIdentifierController.generateCustomUUID().toString() + updateCurrentProfileUuid(profileId, randomUuid) + randomUuid + } + } + + /** + * Updates the UUID of an existing profile. + * + * @param profileId the ID corresponding to the profile being updated. + * @param newUuid New UUID for the profile being updated. + * @return a [DataProvider] that indicates the success/failure of this update operation. + */ + private fun updateCurrentProfileUuid(profileId: ProfileId, newUuid: String): DataProvider { + val deferred = profileDataStore.storeDataWithCustomChannelAsync( + updateInMemoryCache = true + ) { + val profile = + it.profilesMap[profileId.internalId] ?: return@storeDataWithCustomChannelAsync Pair( + it, + ProfileActionStatus.PROFILE_NOT_FOUND + ) + val updatedProfile = profile.toBuilder().setUuid(newUuid).build() + val profileDatabaseBuilder = it.toBuilder().putProfiles( + profileId.internalId, + updatedProfile + ) + Pair(profileDatabaseBuilder.build(), ProfileActionStatus.SUCCESS) + } + return dataProviders.createInMemoryDataProviderAsync(UPDATE_NAME_PROVIDER_ID) { + return@createInMemoryDataProviderAsync getDeferredResult(profileId, newUuid, deferred) + } + } + /** * Returns the learner ID corresponding to the specified [profileId], or null if the specified * profile doesn't exist. diff --git a/model/src/main/proto/profile.proto b/model/src/main/proto/profile.proto index bb55c8b2b47..30d86db4c6f 100644 --- a/model/src/main/proto/profile.proto +++ b/model/src/main/proto/profile.proto @@ -34,6 +34,9 @@ message Profile { // Unique ID given to each profile. ProfileId id = 1; + // Unique 64-bit UUID assigned to each profile + string uuid = 20; + // Name of the user. string name = 2;