From 10a72b4e56fb53d1e74a70ebc8658c40780744f3 Mon Sep 17 00:00:00 2001 From: Ben Henning Date: Thu, 15 Aug 2024 00:32:01 +0000 Subject: [PATCH] Use TranslationController to back audio language. This updates ProfileManagementController to use TranslationController as the actual source of truth for a profile's audio language setting. It doesn't move any code away from AudioLanguage, but it does make OppiaLanguage the true proto backing this setting now (just in a way hidden from UI code). Long-term, AudioLanguage should be removed. This updates calling code that needs the audio language property to use a new getter in ProfileManagementController. The old audioLanguage property in Profile has been removed (which will result in a setting regression for users, but this is considered fine since we're in beta and it's a small regression most users are unlikely to even notice). French and Chinese have been removed entirely from AudioLanguage since they aren't valid languages for the app at the moment (per the OppiaLanguage enum and supported languages configurations). TranslationController was updated to persist written translation and audio language settings since, before, only the app language selection was persisted. --- .../app/options/OptionControlsViewModel.kt | 31 ++-- .../player/audio/AudioFragmentPresenter.kt | 27 ++- .../player/audio/LanguageDialogFragment.kt | 2 - .../translation/AppLanguageResourceHandler.kt | 2 - .../app/options/AudioLanguageFragmentTest.kt | 6 +- .../AppLanguageResourceHandlerTest.kt | 2 - .../profile/ProfileManagementController.kt | 75 ++++++--- .../translation/TranslationController.kt | 157 +++++++++--------- .../ProfileManagementControllerTest.kt | 157 ++++++++++++++++-- .../translation/TranslationControllerTest.kt | 14 +- model/src/main/proto/profile.proto | 10 +- 11 files changed, 316 insertions(+), 167 deletions(-) diff --git a/app/src/main/java/org/oppia/android/app/options/OptionControlsViewModel.kt b/app/src/main/java/org/oppia/android/app/options/OptionControlsViewModel.kt index cd03b74450a..8bc4515efd1 100644 --- a/app/src/main/java/org/oppia/android/app/options/OptionControlsViewModel.kt +++ b/app/src/main/java/org/oppia/android/app/options/OptionControlsViewModel.kt @@ -5,8 +5,8 @@ import androidx.databinding.ObservableField import androidx.lifecycle.LiveData import androidx.lifecycle.MutableLiveData import androidx.lifecycle.Transformations -import androidx.lifecycle.ViewModel import org.oppia.android.app.fragment.FragmentScope +import org.oppia.android.app.model.AudioLanguage import org.oppia.android.app.model.OppiaLanguage import org.oppia.android.app.model.Profile import org.oppia.android.app.model.ProfileId @@ -20,7 +20,8 @@ import org.oppia.android.util.data.DataProviders.Companion.combineWith import org.oppia.android.util.data.DataProviders.Companion.toLiveData import javax.inject.Inject -/** [ViewModel] for [OptionsFragment]. */ +private const val OPTIONS_ITEM_VIEW_MODEL_APP_AUDIO_LANGUAGE_PROVIDER_ID = + "OPTIONS_ITEM_VIEW_MODEL_APP_AUDIO_LANGUAGE_PROVIDER_ID" private const val OPTIONS_ITEM_VIEW_MODEL_LIST_PROVIDER_ID = "OPTIONS_ITEM_VIEW_MODEL_LIST_PROVIDER_ID" @@ -67,11 +68,14 @@ class OptionControlsViewModel @Inject constructor( } private fun createOptionsItemViewModelProvider(): DataProvider> { + val appAudioLangProvider = + translationController.getAppLanguage(profileId).combineWith( + profileManagementController.getAudioLanguage(profileId), + OPTIONS_ITEM_VIEW_MODEL_APP_AUDIO_LANGUAGE_PROVIDER_ID + ) { appLanguage, audioLanguage -> appLanguage to audioLanguage } return profileManagementController.getProfile(profileId).combineWith( - translationController.getAppLanguage(profileId), - OPTIONS_ITEM_VIEW_MODEL_LIST_PROVIDER_ID, - ::processViewModelList - ) + appAudioLangProvider, OPTIONS_ITEM_VIEW_MODEL_LIST_PROVIDER_ID + ) { profile, (appLang, audioLang) -> processViewModelList(profile, appLang, audioLang) } } private fun processViewModelListsResult( @@ -93,12 +97,13 @@ class OptionControlsViewModel @Inject constructor( private fun processViewModelList( profile: Profile, - oppiaLanguage: OppiaLanguage + appLanguage: OppiaLanguage, + audioLanguage: AudioLanguage ): List { return listOfNotNull( createReadingTextSizeViewModel(profile), - createAppLanguageViewModel(oppiaLanguage), - createAudioLanguageViewModel(profile) + createAppLanguageViewModel(appLanguage), + createAudioLanguageViewModel(audioLanguage) ) } @@ -117,12 +122,14 @@ class OptionControlsViewModel @Inject constructor( ) } - private fun createAudioLanguageViewModel(profile: Profile): OptionsAudioLanguageViewModel { + private fun createAudioLanguageViewModel( + audioLanguage: AudioLanguage + ): OptionsAudioLanguageViewModel { return OptionsAudioLanguageViewModel( routeToAudioLanguageListListener, loadAudioLanguageListListener, - profile.audioLanguage, - resourceHandler.computeLocalizedDisplayName(profile.audioLanguage) + audioLanguage, + resourceHandler.computeLocalizedDisplayName(audioLanguage) ) } } diff --git a/app/src/main/java/org/oppia/android/app/player/audio/AudioFragmentPresenter.kt b/app/src/main/java/org/oppia/android/app/player/audio/AudioFragmentPresenter.kt index 69b433d8aa9..60f4214458e 100644 --- a/app/src/main/java/org/oppia/android/app/player/audio/AudioFragmentPresenter.kt +++ b/app/src/main/java/org/oppia/android/app/player/audio/AudioFragmentPresenter.kt @@ -17,7 +17,6 @@ import org.oppia.android.R import org.oppia.android.app.fragment.FragmentScope import org.oppia.android.app.model.AudioLanguage import org.oppia.android.app.model.CellularDataPreference -import org.oppia.android.app.model.Profile import org.oppia.android.app.model.ProfileId import org.oppia.android.app.model.Spotlight import org.oppia.android.app.model.State @@ -147,15 +146,15 @@ class AudioFragmentPresenter @Inject constructor( ) as? SpotlightManager } - private fun getProfileData(): LiveData { + private fun retrieveAudioLanguageCode(): LiveData { return Transformations.map( - profileManagementController.getProfile(profileId).toLiveData(), - ::processGetProfileResult + profileManagementController.getAudioLanguage(profileId).toLiveData(), + ::processAudioLanguageResult ) } private fun subscribeToAudioLanguageLiveData() { - getProfileData().observe( + retrieveAudioLanguageCode().observe( activity, Observer { result -> audioViewModel.selectedLanguageCode = result @@ -165,11 +164,9 @@ class AudioFragmentPresenter @Inject constructor( } /** Gets language code by [AudioLanguage]. */ - private fun getAudioLanguage(audioLanguage: AudioLanguage): String { + private fun computeLanguageCode(audioLanguage: AudioLanguage): String { return when (audioLanguage) { AudioLanguage.HINDI_AUDIO_LANGUAGE -> "hi" - AudioLanguage.FRENCH_AUDIO_LANGUAGE -> "fr" - AudioLanguage.CHINESE_AUDIO_LANGUAGE -> "zh" AudioLanguage.BRAZILIAN_PORTUGUESE_LANGUAGE -> "pt" AudioLanguage.ARABIC_LANGUAGE -> "ar" AudioLanguage.NIGERIAN_PIDGIN_LANGUAGE -> "pcm" @@ -178,16 +175,16 @@ class AudioFragmentPresenter @Inject constructor( } } - private fun processGetProfileResult(profileResult: AsyncResult): String { - val profile = when (profileResult) { + private fun processAudioLanguageResult(languageResult: AsyncResult): String { + val audioLanguage = when (languageResult) { is AsyncResult.Failure -> { - oppiaLogger.e("AudioFragment", "Failed to retrieve profile", profileResult.error) - Profile.getDefaultInstance() + oppiaLogger.e("AudioFragment", "Failed to retrieve audio language", languageResult.error) + AudioLanguage.AUDIO_LANGUAGE_UNSPECIFIED } - is AsyncResult.Pending -> Profile.getDefaultInstance() - is AsyncResult.Success -> profileResult.value + is AsyncResult.Pending -> AudioLanguage.AUDIO_LANGUAGE_UNSPECIFIED + is AsyncResult.Success -> languageResult.value } - return getAudioLanguage(profile.audioLanguage) + return computeLanguageCode(audioLanguage) } /** Sets selected language code in presenter and ViewModel. */ diff --git a/app/src/main/java/org/oppia/android/app/player/audio/LanguageDialogFragment.kt b/app/src/main/java/org/oppia/android/app/player/audio/LanguageDialogFragment.kt index 178c93c57cb..158d59290c7 100644 --- a/app/src/main/java/org/oppia/android/app/player/audio/LanguageDialogFragment.kt +++ b/app/src/main/java/org/oppia/android/app/player/audio/LanguageDialogFragment.kt @@ -80,8 +80,6 @@ class LanguageDialogFragment : InjectableDialogFragment() { for (languageCode in languageCodeArrayList) { val audioLanguage = when (machineLocale.run { languageCode.toMachineLowerCase() }) { "hi" -> AudioLanguage.HINDI_AUDIO_LANGUAGE - "fr" -> AudioLanguage.FRENCH_AUDIO_LANGUAGE - "zh" -> AudioLanguage.CHINESE_AUDIO_LANGUAGE "pt", "pt-br" -> AudioLanguage.BRAZILIAN_PORTUGUESE_LANGUAGE "ar" -> AudioLanguage.ARABIC_LANGUAGE "pcm" -> AudioLanguage.NIGERIAN_PIDGIN_LANGUAGE diff --git a/app/src/main/java/org/oppia/android/app/translation/AppLanguageResourceHandler.kt b/app/src/main/java/org/oppia/android/app/translation/AppLanguageResourceHandler.kt index 05ad59fac13..be2fa522409 100644 --- a/app/src/main/java/org/oppia/android/app/translation/AppLanguageResourceHandler.kt +++ b/app/src/main/java/org/oppia/android/app/translation/AppLanguageResourceHandler.kt @@ -154,8 +154,6 @@ class AppLanguageResourceHandler @Inject constructor( fun computeLocalizedDisplayName(audioLanguage: AudioLanguage): String { return when (audioLanguage) { AudioLanguage.HINDI_AUDIO_LANGUAGE -> getLocalizedDisplayName("hi") - AudioLanguage.FRENCH_AUDIO_LANGUAGE -> getLocalizedDisplayName("fr") - AudioLanguage.CHINESE_AUDIO_LANGUAGE -> getLocalizedDisplayName("zh") AudioLanguage.BRAZILIAN_PORTUGUESE_LANGUAGE -> getLocalizedDisplayName("pt", "BR") AudioLanguage.ARABIC_LANGUAGE -> getLocalizedDisplayName("ar", "EG") AudioLanguage.NIGERIAN_PIDGIN_LANGUAGE -> diff --git a/app/src/sharedTest/java/org/oppia/android/app/options/AudioLanguageFragmentTest.kt b/app/src/sharedTest/java/org/oppia/android/app/options/AudioLanguageFragmentTest.kt index b25607c9158..7ad7462af04 100644 --- a/app/src/sharedTest/java/org/oppia/android/app/options/AudioLanguageFragmentTest.kt +++ b/app/src/sharedTest/java/org/oppia/android/app/options/AudioLanguageFragmentTest.kt @@ -112,9 +112,9 @@ import javax.inject.Singleton class AudioLanguageFragmentTest { private companion object { private const val ENGLISH_BUTTON_INDEX = 0 - private const val PORTUGUESE_BUTTON_INDEX = 4 - private const val ARABIC_BUTTON_INDEX = 5 - private const val NIGERIAN_PIDGIN_BUTTON_INDEX = 6 + private const val PORTUGUESE_BUTTON_INDEX = 2 + private const val ARABIC_BUTTON_INDEX = 3 + private const val NIGERIAN_PIDGIN_BUTTON_INDEX = 4 } @get:Rule val initializeDefaultLocaleRule = InitializeDefaultLocaleRule() diff --git a/app/src/test/java/org/oppia/android/app/translation/AppLanguageResourceHandlerTest.kt b/app/src/test/java/org/oppia/android/app/translation/AppLanguageResourceHandlerTest.kt index 76811f7f5b3..9c6e56f953a 100644 --- a/app/src/test/java/org/oppia/android/app/translation/AppLanguageResourceHandlerTest.kt +++ b/app/src/test/java/org/oppia/android/app/translation/AppLanguageResourceHandlerTest.kt @@ -494,8 +494,6 @@ class AppLanguageResourceHandlerTest { // TODO(#3793): Remove this once OppiaLanguage is used as the source of truth. @Test @Iteration("hi", "lang=HINDI_AUDIO_LANGUAGE", "expectedDisplayText=हिन्दी") - @Iteration("fr", "lang=FRENCH_AUDIO_LANGUAGE", "expectedDisplayText=Français") - @Iteration("zh", "lang=CHINESE_AUDIO_LANGUAGE", "expectedDisplayText=中文") @Iteration("pr-pt", "lang=BRAZILIAN_PORTUGUESE_LANGUAGE", "expectedDisplayText=Português") @Iteration("ar", "lang=ARABIC_LANGUAGE", "expectedDisplayText=العربية") @Iteration("pcm", "lang=NIGERIAN_PIDGIN_LANGUAGE", "expectedDisplayText=Naijá") 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 51db3f941bb..983caebf6db 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 @@ -9,7 +9,9 @@ import android.provider.MediaStore import androidx.exifinterface.media.ExifInterface import kotlinx.coroutines.Deferred import org.oppia.android.app.model.AudioLanguage +import org.oppia.android.app.model.AudioTranslationLanguageSelection import org.oppia.android.app.model.DeviceSettings +import org.oppia.android.app.model.OppiaLanguage import org.oppia.android.app.model.Profile import org.oppia.android.app.model.ProfileAvatar import org.oppia.android.app.model.ProfileDatabase @@ -22,6 +24,7 @@ import org.oppia.android.domain.oppialogger.LoggingIdentifierController import org.oppia.android.domain.oppialogger.OppiaLogger import org.oppia.android.domain.oppialogger.analytics.LearnerAnalyticsLogger import org.oppia.android.domain.oppialogger.exceptions.ExceptionsController +import org.oppia.android.domain.translation.TranslationController import org.oppia.android.util.data.AsyncResult import org.oppia.android.util.data.DataProvider import org.oppia.android.util.data.DataProviders @@ -64,8 +67,8 @@ private const val SET_CURRENT_PROFILE_ID_PROVIDER_ID = "set_current_profile_id_p private const val UPDATE_READING_TEXT_SIZE_PROVIDER_ID = "update_reading_text_size_provider_id" private const val UPDATE_APP_LANGUAGE_PROVIDER_ID = "update_app_language_provider_id" -private const val UPDATE_AUDIO_LANGUAGE_PROVIDER_ID = - "update_audio_language_provider_id" +private const val GET_AUDIO_LANGUAGE_PROVIDER_ID = "get_audio_language_provider_id" +private const val UPDATE_AUDIO_LANGUAGE_PROVIDER_ID = "update_audio_language_provider_id" private const val UPDATE_LEARNER_ID_PROVIDER_ID = "update_learner_id_provider_id" private const val SET_SURVEY_LAST_SHOWN_TIMESTAMP_PROVIDER_ID = "record_survey_last_shown_timestamp_provider_id" @@ -93,7 +96,8 @@ class ProfileManagementController @Inject constructor( private val enableLearnerStudyAnalytics: PlatformParameterValue, @EnableLoggingLearnerStudyIds private val enableLoggingLearnerStudyIds: PlatformParameterValue, - private val profileNameValidator: ProfileNameValidator + private val profileNameValidator: ProfileNameValidator, + private val translationController: TranslationController ) { private var currentProfileId: Int = DEFAULT_LOGGED_OUT_INTERNAL_PROFILE_ID private val profileDataStore = @@ -275,7 +279,6 @@ class ProfileManagementController @Inject constructor( dateCreatedTimestampMs = oppiaClock.getCurrentTimeMs() this.isAdmin = isAdmin readingTextSize = ReadingTextSize.MEDIUM_TEXT_SIZE - audioLanguage = AudioLanguage.ENGLISH_AUDIO_LANGUAGE numberOfLogins = 0 if (enableLoggingLearnerStudyIds.value) { @@ -628,34 +631,52 @@ class ProfileManagementController @Inject constructor( } } + /** + * Returns the current audio language configured for the specified profile ID, as possibly set by + * [updateAudioLanguage]. + * + * The return [DataProvider] will automatically update for subsequent calls to + * [updateAudioLanguage] for this [profileId]. + */ + fun getAudioLanguage(profileId: ProfileId): DataProvider { + return translationController.getAudioTranslationContentLanguage( + profileId + ).transform(GET_AUDIO_LANGUAGE_PROVIDER_ID) { oppiaLanguage -> + when (oppiaLanguage) { + OppiaLanguage.UNRECOGNIZED, OppiaLanguage.LANGUAGE_UNSPECIFIED, OppiaLanguage.HINGLISH, + OppiaLanguage.PORTUGUESE, OppiaLanguage.SWAHILI -> AudioLanguage.AUDIO_LANGUAGE_UNSPECIFIED + OppiaLanguage.ARABIC -> AudioLanguage.ARABIC_LANGUAGE + OppiaLanguage.ENGLISH -> AudioLanguage.ENGLISH_AUDIO_LANGUAGE + OppiaLanguage.HINDI -> AudioLanguage.HINDI_AUDIO_LANGUAGE + OppiaLanguage.BRAZILIAN_PORTUGUESE -> AudioLanguage.BRAZILIAN_PORTUGUESE_LANGUAGE + OppiaLanguage.NIGERIAN_PIDGIN -> AudioLanguage.NIGERIAN_PIDGIN_LANGUAGE + } + } + } + /** * Updates the audio language of the profile. * - * @param profileId the ID corresponding to the profile being updated. - * @param audioLanguage New audio language for the profile being updated. - * @return a [DataProvider] that indicates the success/failure of this update operation. + * @param profileId the ID corresponding to the profile being updated + * @param audioLanguage New audio language for the profile being updated + * @return a [DataProvider] that indicates the success/failure of this update operation */ fun updateAudioLanguage(profileId: ProfileId, audioLanguage: AudioLanguage): 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().setAudioLanguage(audioLanguage).build() - val profileDatabaseBuilder = it.toBuilder().putProfiles( - profileId.internalId, - updatedProfile - ) - Pair(profileDatabaseBuilder.build(), ProfileActionStatus.SUCCESS) - } - return dataProviders.createInMemoryDataProviderAsync( - UPDATE_AUDIO_LANGUAGE_PROVIDER_ID - ) { - return@createInMemoryDataProviderAsync getDeferredResult(profileId, null, deferred) - } + val audioSelection = AudioTranslationLanguageSelection.newBuilder().apply { + this.selectedLanguage = when (audioLanguage) { + AudioLanguage.UNRECOGNIZED, AudioLanguage.AUDIO_LANGUAGE_UNSPECIFIED, + AudioLanguage.NO_AUDIO -> OppiaLanguage.LANGUAGE_UNSPECIFIED + AudioLanguage.ENGLISH_AUDIO_LANGUAGE -> OppiaLanguage.ENGLISH + AudioLanguage.HINDI_AUDIO_LANGUAGE -> OppiaLanguage.HINDI + AudioLanguage.BRAZILIAN_PORTUGUESE_LANGUAGE -> OppiaLanguage.BRAZILIAN_PORTUGUESE + AudioLanguage.ARABIC_LANGUAGE -> OppiaLanguage.ARABIC + AudioLanguage.NIGERIAN_PIDGIN_LANGUAGE -> OppiaLanguage.NIGERIAN_PIDGIN + } + }.build() + // The transformation is needed to reinterpreted the result of the update to 'Any?'. + return translationController.updateAudioTranslationContentLanguage( + profileId, audioSelection + ).transform(UPDATE_AUDIO_LANGUAGE_PROVIDER_ID) { value -> value } } /** diff --git a/domain/src/main/java/org/oppia/android/domain/translation/TranslationController.kt b/domain/src/main/java/org/oppia/android/domain/translation/TranslationController.kt index a916d935ac4..11f54e9cfba 100644 --- a/domain/src/main/java/org/oppia/android/domain/translation/TranslationController.kt +++ b/domain/src/main/java/org/oppia/android/domain/translation/TranslationController.kt @@ -1,5 +1,6 @@ package org.oppia.android.domain.translation +import com.google.protobuf.MessageLite import org.oppia.android.app.model.AppLanguageSelection import org.oppia.android.app.model.AudioTranslationLanguageSelection import org.oppia.android.app.model.LanguageSupportDefinition @@ -28,10 +29,8 @@ import org.oppia.android.util.data.DataProviders.Companion.combineWithAsync import org.oppia.android.util.data.DataProviders.Companion.transform import org.oppia.android.util.data.DataProviders.Companion.transformAsync import org.oppia.android.util.locale.OppiaLocale -import java.util.concurrent.locks.ReentrantLock import javax.inject.Inject import javax.inject.Singleton -import kotlin.concurrent.withLock private const val SYSTEM_LANGUAGE_LOCALE_DATA_PROVIDER_ID = "system_language_locale" private const val APP_LANGUAGE_DATA_PROVIDER_ID = "app_language" @@ -56,6 +55,10 @@ private const val AUDIO_TRANSLATION_CONTENT_SELECTION_DATA_PROVIDER_ID = private const val UPDATE_AUDIO_TRANSLATION_CONTENT_DATA_PROVIDER_ID = "update_audio_translation_content" private const val APP_LANGUAGE_CONTENT_DATABASE = "app_language_content_database" +private const val WRITTEN_TRANSLATION_LANGUAGE_CONTENT_DATABASE = + "written_language_content_database" +private const val AUDIO_TRANSLATION_LANGUAGE_CONTENT_DATABASE = + "audio_translation_language_content_database" private const val RETRIEVED_CONTENT_LANGUAGE_DATA_PROVIDER_ID = "retrieved_content_language_data_provider_id" @@ -76,17 +79,12 @@ class TranslationController @Inject constructor( private val cacheStoreFactory: PersistentCacheStore.Factory, private val oppiaLogger: OppiaLogger, ) { - // TODO(#4938): Finish this implementation. The implementation below saves/restores per-profile app - // language, but not audio language. - - private val dataLock = ReentrantLock() - private val writtenTranslationLanguageSettings = - mutableMapOf() - private val audioVoiceoverLanguageSettings = - mutableMapOf() - - private val cacheStoreMap = + private val appLanguageCacheStoreMap = mutableMapOf>() + private val writtenTranslationLanguageCacheStoreMap = + mutableMapOf>() + private val audioTranslationLanguageCacheStoreMap = + mutableMapOf>() /** * Returns a data provider for an app string [OppiaLocale.DisplayLocale] corresponding to the @@ -144,7 +142,7 @@ class TranslationController @Inject constructor( * the underlying configured selection. */ fun getAppLanguageSelection(profileId: ProfileId): DataProvider = - retrieveLanguageContentCacheStore(profileId) + retrieveAppLanguageContentCacheStore(profileId) /** * Updates the language to be used by the specified user for app string translations. Note that @@ -156,18 +154,17 @@ class TranslationController @Inject constructor( * language matches a supported language, otherwise the app defaults to English). * * @return a [DataProvider] which succeeds only if the update succeeds, otherwise fails. The - * payload of the data provider is the *current* selection state. + * payload of the data provider is the *previous* selection state. */ fun updateAppLanguage( profileId: ProfileId, selection: AppLanguageSelection ): DataProvider { - val cacheStore = retrieveLanguageContentCacheStore(profileId) - val deferred = cacheStore.storeDataAsync(updateInMemoryCache = true) { selection } - + val cacheStore = retrieveAppLanguageContentCacheStore(profileId) return dataProviders.createInMemoryDataProviderAsync(UPDATE_APP_LANGUAGE_DATA_PROVIDER_ID) { - deferred.await() - AsyncResult.Success(cacheStore.readDataAsync().await()) + AsyncResult.Success(cacheStore.readDataAsync().await()).also { + cacheStore.storeDataAsync(updateInMemoryCache = true) { selection }.await() + } } } @@ -216,12 +213,8 @@ class TranslationController @Inject constructor( */ fun getWrittenTranslationContentLanguageSelection( profileId: ProfileId - ): DataProvider { - val providerId = WRITTEN_TRANSLATION_CONTENT_SELECTION_DATA_PROVIDER_ID - return dataProviders.createInMemoryDataProvider(providerId) { - retrieveWrittenTranslationContentLanguageSelection(profileId) - } - } + ): DataProvider = + retrieveWrittenTranslationLanguageContentCacheStore(profileId) /** * Updates the language to be used by the specified user for written content string translations. @@ -240,9 +233,13 @@ class TranslationController @Inject constructor( profileId: ProfileId, selection: WrittenTranslationLanguageSelection ): DataProvider { - val providerId = UPDATE_WRITTEN_TRANSLATION_CONTENT_DATA_PROVIDER_ID - return dataProviders.createInMemoryDataProviderAsync(providerId) { - AsyncResult.Success(updateWrittenTranslationContentLanguageSelection(profileId, selection)) + val cacheStore = retrieveWrittenTranslationLanguageContentCacheStore(profileId) + return dataProviders.createInMemoryDataProviderAsync( + UPDATE_WRITTEN_TRANSLATION_CONTENT_DATA_PROVIDER_ID + ) { + AsyncResult.Success(cacheStore.readDataAsync().await()).also { + cacheStore.storeDataAsync(updateInMemoryCache = true) { selection }.await() + } } } @@ -290,12 +287,8 @@ class TranslationController @Inject constructor( */ fun getAudioTranslationContentLanguageSelection( profileId: ProfileId - ): DataProvider { - val providerId = AUDIO_TRANSLATION_CONTENT_SELECTION_DATA_PROVIDER_ID - return dataProviders.createInMemoryDataProvider(providerId) { - retrieveAudioTranslationContentLanguageSelection(profileId) - } - } + ): DataProvider = + retrieveAudioTranslationLanguageContentCacheStore(profileId) /** * Updates the language to be used by the specified user for audio voiceover selection. Note that @@ -314,9 +307,13 @@ class TranslationController @Inject constructor( profileId: ProfileId, selection: AudioTranslationLanguageSelection ): DataProvider { - val providerId = UPDATE_AUDIO_TRANSLATION_CONTENT_DATA_PROVIDER_ID - return dataProviders.createInMemoryDataProviderAsync(providerId) { - AsyncResult.Success(updateAudioTranslationContentLanguageSelection(profileId, selection)) + val cacheStore = retrieveAudioTranslationLanguageContentCacheStore(profileId) + return dataProviders.createInMemoryDataProviderAsync( + UPDATE_AUDIO_TRANSLATION_CONTENT_DATA_PROVIDER_ID + ) { + AsyncResult.Success(cacheStore.readDataAsync().await()).also { + cacheStore.storeDataAsync(updateInMemoryCache = true) { selection }.await() + } } } @@ -414,15 +411,49 @@ class TranslationController @Inject constructor( } } - private fun retrieveLanguageContentCacheStore( + private fun retrieveAppLanguageContentCacheStore( profileId: ProfileId ): PersistentCacheStore { - return cacheStoreMap.getOrPut(profileId) { + return retrieveContentCacheStore( + profileId, + APP_LANGUAGE_CONTENT_DATABASE, + AppLanguageSelection.getDefaultInstance(), + appLanguageCacheStoreMap + ) + } + + private fun retrieveWrittenTranslationLanguageContentCacheStore( + profileId: ProfileId + ): PersistentCacheStore { + return retrieveContentCacheStore( + profileId, + WRITTEN_TRANSLATION_LANGUAGE_CONTENT_DATABASE, + WrittenTranslationLanguageSelection.getDefaultInstance(), + writtenTranslationLanguageCacheStoreMap + ) + } + + private fun retrieveAudioTranslationLanguageContentCacheStore( + profileId: ProfileId + ): PersistentCacheStore { + return retrieveContentCacheStore( + profileId, + AUDIO_TRANSLATION_LANGUAGE_CONTENT_DATABASE, + AudioTranslationLanguageSelection.getDefaultInstance(), + audioTranslationLanguageCacheStoreMap + ) + } + + private fun retrieveContentCacheStore( + profileId: ProfileId, + databaseName: String, + defaultCacheValue: T, + cacheMap: MutableMap> + ): PersistentCacheStore { + return cacheMap.getOrPut(profileId) { cacheStoreFactory.createPerProfile( - APP_LANGUAGE_CONTENT_DATABASE, - AppLanguageSelection.getDefaultInstance(), - profileId - ).also> { + databaseName, defaultCacheValue, profileId + ).also> { it.primeInMemoryAndDiskCacheAsync( updateMode = PersistentCacheStore.UpdateMode.UPDATE_IF_NEW_CACHE, publishMode = PersistentCacheStore.PublishMode.PUBLISH_TO_IN_MEMORY_CACHE @@ -439,46 +470,6 @@ class TranslationController @Inject constructor( } } - private fun retrieveWrittenTranslationContentLanguageSelection( - profileId: ProfileId - ): WrittenTranslationLanguageSelection { - return dataLock.withLock { - writtenTranslationLanguageSettings[profileId] - ?: WrittenTranslationLanguageSelection.getDefaultInstance() - } - } - - private suspend fun updateWrittenTranslationContentLanguageSelection( - profileId: ProfileId, - selection: WrittenTranslationLanguageSelection - ): WrittenTranslationLanguageSelection { - return dataLock.withLock { - writtenTranslationLanguageSettings.put(profileId, selection) - }.also { - asyncDataSubscriptionManager.notifyChange(WRITTEN_TRANSLATION_CONTENT_LOCALE_DATA_PROVIDER_ID) - } ?: WrittenTranslationLanguageSelection.getDefaultInstance() - } - - private fun retrieveAudioTranslationContentLanguageSelection( - profileId: ProfileId - ): AudioTranslationLanguageSelection { - return dataLock.withLock { - audioVoiceoverLanguageSettings[profileId] - ?: AudioTranslationLanguageSelection.getDefaultInstance() - } - } - - private suspend fun updateAudioTranslationContentLanguageSelection( - profileId: ProfileId, - selection: AudioTranslationLanguageSelection - ): AudioTranslationLanguageSelection { - return dataLock.withLock { - audioVoiceoverLanguageSettings.put(profileId, selection) - }.also { - asyncDataSubscriptionManager.notifyChange(AUDIO_TRANSLATION_CONTENT_LOCALE_DATA_PROVIDER_ID) - } ?: AudioTranslationLanguageSelection.getDefaultInstance() - } - private fun getSystemLanguage(): DataProvider = localeController.retrieveSystemLanguage() diff --git a/domain/src/test/java/org/oppia/android/domain/profile/ProfileManagementControllerTest.kt b/domain/src/test/java/org/oppia/android/domain/profile/ProfileManagementControllerTest.kt index ba6082fa1c5..ab3e4011330 100644 --- a/domain/src/test/java/org/oppia/android/domain/profile/ProfileManagementControllerTest.kt +++ b/domain/src/test/java/org/oppia/android/domain/profile/ProfileManagementControllerTest.kt @@ -19,8 +19,11 @@ import kotlinx.coroutines.flow.StateFlow import org.junit.After import org.junit.Test import org.junit.runner.RunWith -import org.oppia.android.app.model.AudioLanguage -import org.oppia.android.app.model.AudioLanguage.FRENCH_AUDIO_LANGUAGE +import org.oppia.android.app.model.AudioLanguage.ARABIC_LANGUAGE +import org.oppia.android.app.model.AudioLanguage.BRAZILIAN_PORTUGUESE_LANGUAGE +import org.oppia.android.app.model.AudioLanguage.ENGLISH_AUDIO_LANGUAGE +import org.oppia.android.app.model.AudioLanguage.HINDI_AUDIO_LANGUAGE +import org.oppia.android.app.model.AudioLanguage.NIGERIAN_PIDGIN_LANGUAGE import org.oppia.android.app.model.Profile import org.oppia.android.app.model.ProfileDatabase import org.oppia.android.app.model.ProfileId @@ -129,7 +132,6 @@ class ProfileManagementControllerTest { assertThat(profile.allowDownloadAccess).isEqualTo(true) assertThat(profile.id.internalId).isEqualTo(0) assertThat(profile.readingTextSize).isEqualTo(MEDIUM_TEXT_SIZE) - assertThat(profile.audioLanguage).isEqualTo(AudioLanguage.ENGLISH_AUDIO_LANGUAGE) assertThat(profile.numberOfLogins).isEqualTo(0) assertThat(profile.isContinueButtonAnimationSeen).isEqualTo(false) assertThat(File(getAbsoluteDirPath("0")).isDirectory).isTrue() @@ -197,7 +199,6 @@ class ProfileManagementControllerTest { assertThat(profile.allowDownloadAccess).isEqualTo(false) assertThat(profile.id.internalId).isEqualTo(3) assertThat(profile.readingTextSize).isEqualTo(MEDIUM_TEXT_SIZE) - assertThat(profile.audioLanguage).isEqualTo(AudioLanguage.ENGLISH_AUDIO_LANGUAGE) } @Test @@ -716,17 +717,153 @@ class ProfileManagementControllerTest { } @Test - fun testUpdateAudioLanguage_addProfiles_updateWithFrenchLanguage_checkUpdateIsSuccessful() { + fun testGetAudioLanguage_initialProfileCreation_defaultsToEnglish() { + setUpTestApplicationComponent() + + addTestProfiles() + + val audioLanguageProvider = profileManagementController.getAudioLanguage(PROFILE_ID_2) + val audioLanguage = monitorFactory.waitForNextSuccessfulResult(audioLanguageProvider) + assertThat(audioLanguage).isEqualTo(ENGLISH_AUDIO_LANGUAGE) + } + + @Test + fun testUpdateAudioLanguage_updateToHindi_updateIsSuccessful() { setUpTestApplicationComponent() addTestProfiles() val updateProvider = - profileManagementController.updateAudioLanguage(PROFILE_ID_2, FRENCH_AUDIO_LANGUAGE) - val profileProvider = profileManagementController.getProfile(PROFILE_ID_2) + profileManagementController.updateAudioLanguage(PROFILE_ID_2, HINDI_AUDIO_LANGUAGE) + val monitor = monitorFactory.createMonitor(updateProvider) + testCoroutineDispatchers.runCurrent() - monitorFactory.waitForNextSuccessfulResult(updateProvider) - val profile = monitorFactory.waitForNextSuccessfulResult(profileProvider) - assertThat(profile.audioLanguage).isEqualTo(FRENCH_AUDIO_LANGUAGE) + monitor.ensureNextResultIsSuccess() + } + + @Test + fun testUpdateAudioLanguage_updateToBrazilianPortuguese_updateIsSuccessful() { + setUpTestApplicationComponent() + addTestProfiles() + + val updateProvider = + profileManagementController.updateAudioLanguage(PROFILE_ID_2, BRAZILIAN_PORTUGUESE_LANGUAGE) + val monitor = monitorFactory.createMonitor(updateProvider) + testCoroutineDispatchers.runCurrent() + + monitor.ensureNextResultIsSuccess() + } + + @Test + fun testUpdateAudioLanguage_updateToArabic_updateIsSuccessful() { + setUpTestApplicationComponent() + addTestProfiles() + + val updateProvider = + profileManagementController.updateAudioLanguage(PROFILE_ID_2, ARABIC_LANGUAGE) + val monitor = monitorFactory.createMonitor(updateProvider) + testCoroutineDispatchers.runCurrent() + + monitor.ensureNextResultIsSuccess() + } + + @Test + fun testUpdateAudioLanguage_updateToNigerianPidgin_updateIsSuccessful() { + setUpTestApplicationComponent() + addTestProfiles() + + val updateProvider = + profileManagementController.updateAudioLanguage(PROFILE_ID_2, NIGERIAN_PIDGIN_LANGUAGE) + val monitor = monitorFactory.createMonitor(updateProvider) + testCoroutineDispatchers.runCurrent() + + monitor.ensureNextResultIsSuccess() + } + + @Test + fun testUpdateAudioLanguage_updateToHindi_updateChangesAudioLanguage() { + setUpTestApplicationComponent() + addTestProfiles() + + val updateProvider = + profileManagementController.updateAudioLanguage(PROFILE_ID_2, HINDI_AUDIO_LANGUAGE) + monitorFactory.ensureDataProviderExecutes(updateProvider) + + val audioLanguageProvider = profileManagementController.getAudioLanguage(PROFILE_ID_2) + val audioLanguage = monitorFactory.waitForNextSuccessfulResult(audioLanguageProvider) + assertThat(audioLanguage).isEqualTo(HINDI_AUDIO_LANGUAGE) + } + + @Test + fun testUpdateAudioLanguage_updateToBrazilianPortuguese_updateChangesAudioLanguage() { + setUpTestApplicationComponent() + addTestProfiles() + + val updateProvider = + profileManagementController.updateAudioLanguage(PROFILE_ID_2, BRAZILIAN_PORTUGUESE_LANGUAGE) + monitorFactory.ensureDataProviderExecutes(updateProvider) + + val audioLanguageProvider = profileManagementController.getAudioLanguage(PROFILE_ID_2) + val audioLanguage = monitorFactory.waitForNextSuccessfulResult(audioLanguageProvider) + assertThat(audioLanguage).isEqualTo(BRAZILIAN_PORTUGUESE_LANGUAGE) + } + + @Test + fun testUpdateAudioLanguage_updateToArabic_updateChangesAudioLanguage() { + setUpTestApplicationComponent() + addTestProfiles() + + val updateProvider = + profileManagementController.updateAudioLanguage(PROFILE_ID_2, ARABIC_LANGUAGE) + monitorFactory.ensureDataProviderExecutes(updateProvider) + + val audioLanguageProvider = profileManagementController.getAudioLanguage(PROFILE_ID_2) + val audioLanguage = monitorFactory.waitForNextSuccessfulResult(audioLanguageProvider) + assertThat(audioLanguage).isEqualTo(ARABIC_LANGUAGE) + } + + @Test + fun testUpdateAudioLanguage_updateToNigerianPidgin_updateChangesAudioLanguage() { + setUpTestApplicationComponent() + addTestProfiles() + + val updateProvider = + profileManagementController.updateAudioLanguage(PROFILE_ID_2, NIGERIAN_PIDGIN_LANGUAGE) + monitorFactory.ensureDataProviderExecutes(updateProvider) + + val audioLanguageProvider = profileManagementController.getAudioLanguage(PROFILE_ID_2) + val audioLanguage = monitorFactory.waitForNextSuccessfulResult(audioLanguageProvider) + assertThat(audioLanguage).isEqualTo(NIGERIAN_PIDGIN_LANGUAGE) + } + + @Test + fun testUpdateAudioLanguage_updateToArabicThenEnglish_updateChangesAudioLanguageToEnglish() { + setUpTestApplicationComponent() + addTestProfiles() + val updateProvider1 = + profileManagementController.updateAudioLanguage(PROFILE_ID_2, NIGERIAN_PIDGIN_LANGUAGE) + monitorFactory.ensureDataProviderExecutes(updateProvider1) + + val updateProvider2 = + profileManagementController.updateAudioLanguage(PROFILE_ID_2, ENGLISH_AUDIO_LANGUAGE) + monitorFactory.ensureDataProviderExecutes(updateProvider2) + + val audioLanguageProvider = profileManagementController.getAudioLanguage(PROFILE_ID_2) + val audioLanguage = monitorFactory.waitForNextSuccessfulResult(audioLanguageProvider) + assertThat(audioLanguage).isEqualTo(ENGLISH_AUDIO_LANGUAGE) + } + + @Test + fun testUpdateAudioLanguage_updateProfile1ToArabic_profile2IsUnchanged() { + setUpTestApplicationComponent() + addTestProfiles() + + val updateProvider = + profileManagementController.updateAudioLanguage(PROFILE_ID_1, ARABIC_LANGUAGE) + monitorFactory.ensureDataProviderExecutes(updateProvider) + + val audioLanguageProvider = profileManagementController.getAudioLanguage(PROFILE_ID_2) + val audioLanguage = monitorFactory.waitForNextSuccessfulResult(audioLanguageProvider) + assertThat(audioLanguage).isEqualTo(ENGLISH_AUDIO_LANGUAGE) } @Test diff --git a/domain/src/test/java/org/oppia/android/domain/translation/TranslationControllerTest.kt b/domain/src/test/java/org/oppia/android/domain/translation/TranslationControllerTest.kt index b3b501ff94c..c483bf494dd 100644 --- a/domain/src/test/java/org/oppia/android/domain/translation/TranslationControllerTest.kt +++ b/domain/src/test/java/org/oppia/android/domain/translation/TranslationControllerTest.kt @@ -490,7 +490,7 @@ class TranslationControllerTest { } @Test - fun testUpdateAppLanguage_uninitializedToSystem_returnsDefaultSelection() { + fun testUpdateAppLanguage_uninitializedToSystem_returnsUninitialized() { forceDefaultLocale(Locale.ROOT) val languageSelection = AppLanguageSelection.newBuilder().apply { @@ -503,11 +503,11 @@ class TranslationControllerTest { // The previous selection was uninitialized. val selection = monitorFactory.waitForNextSuccessfulResult(updateProvider) - assertThat(selection).isEqualTo(languageSelection) + assertThat(selection).isEqualToDefaultInstance() } @Test - fun testUpdateAppLanguage_uninitializedToEnglish_returnsEnglishSelection() { + fun testUpdateAppLanguage_uninitializedToEnglish_returnsUninitialized() { forceDefaultLocale(Locale.ROOT) val expectedLanguageSelection = AppLanguageSelection.newBuilder().apply { @@ -520,7 +520,7 @@ class TranslationControllerTest { // The previous selection was uninitialized. val selection = monitorFactory.waitForNextSuccessfulResult(updateProvider) - assertThat(selection).isEqualTo(expectedLanguageSelection) + assertThat(selection).isEqualToDefaultInstance() } @Test @@ -535,11 +535,11 @@ class TranslationControllerTest { // The previous selection was system language. val selection = monitorFactory.waitForNextSuccessfulResult(updateProvider) - assertThat(selection.selectionTypeCase).isEqualTo(SELECTED_APP_LANGUAGE) + assertThat(selection.selectionTypeCase).isEqualTo(USE_SYSTEM_LANGUAGE_OR_APP_DEFAULT) } @Test - fun testUpdateAppLanguage_englishToPortuguese_returnsPortugueseSelection() { + fun testUpdateAppLanguage_englishToPortuguese_returnsEnglishSelection() { forceDefaultLocale(Locale.ROOT) ensureAppLanguageIsUpdatedTo(PROFILE_ID_0, ENGLISH) @@ -551,7 +551,7 @@ class TranslationControllerTest { // The previous selection was English. val selection = monitorFactory.waitForNextSuccessfulResult(updateProvider) assertThat(selection.selectionTypeCase).isEqualTo(SELECTED_APP_LANGUAGE) - assertThat(selection.selectedLanguage).isEqualTo(BRAZILIAN_PORTUGUESE) + assertThat(selection.selectedLanguage).isEqualTo(ENGLISH) } /* Tests for written translation content functions */ diff --git a/model/src/main/proto/profile.proto b/model/src/main/proto/profile.proto index cc395949209..bffdb1ec194 100644 --- a/model/src/main/proto/profile.proto +++ b/model/src/main/proto/profile.proto @@ -64,8 +64,8 @@ message Profile { // Represents user selected reading-text-size. ReadingTextSize reading_text_size = 10; - // Represents user selected audio-language. - AudioLanguage audio_language = 11; + // Represented user selected audio-language (now deprecated). + reserved 11; // Reserve 12 which was used before as using it might cause import issues for older profiles. reserved 12; @@ -137,8 +137,10 @@ enum AudioLanguage { NO_AUDIO = 1; ENGLISH_AUDIO_LANGUAGE = 2; HINDI_AUDIO_LANGUAGE = 3; - FRENCH_AUDIO_LANGUAGE = 4; - CHINESE_AUDIO_LANGUAGE = 5; + // Previously corresponded to French. + reserved 4; + // Previously corresponded to Chinese. + reserved 5; BRAZILIAN_PORTUGUESE_LANGUAGE = 6; ARABIC_LANGUAGE = 7; NIGERIAN_PIDGIN_LANGUAGE = 8;