diff --git a/app/build.gradle b/app/build.gradle index 13fae2de07a..7c94678f75c 100644 --- a/app/build.gradle +++ b/app/build.gradle @@ -107,7 +107,8 @@ def filesToExclude = [ '**/*AppLanguageLocaleHandlerTest*.kt', '**/*AppLanguageResourceHandlerTest*.kt', '**/*AppLanguageWatcherMixinTest*.kt', - '**/*ActivityLanguageLocaleHandlerTest*.kt' + '**/*ActivityLanguageLocaleHandlerTest*.kt', + '**/*OptionsFragmentTest*.kt', // Excludes 2 tests. ] _excludeSourceFiles(filesToExclude) 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/sharedTest/java/org/oppia/android/app/options/ReadingTextSizeFragmentTest.kt b/app/src/sharedTest/java/org/oppia/android/app/options/ReadingTextSizeFragmentTest.kt index 75e9b20db35..5e4b14be4aa 100644 --- a/app/src/sharedTest/java/org/oppia/android/app/options/ReadingTextSizeFragmentTest.kt +++ b/app/src/sharedTest/java/org/oppia/android/app/options/ReadingTextSizeFragmentTest.kt @@ -74,7 +74,9 @@ import org.oppia.android.domain.platformparameter.PlatformParameterModule import org.oppia.android.domain.platformparameter.PlatformParameterSingletonModule import org.oppia.android.domain.question.QuestionModule import org.oppia.android.domain.workmanager.WorkManagerConfigurationModule +import org.oppia.android.testing.BuildEnvironment import org.oppia.android.testing.OppiaTestRule +import org.oppia.android.testing.RunOn import org.oppia.android.testing.TestLogReportingModule import org.oppia.android.testing.firebase.TestAuthenticationModule import org.oppia.android.testing.junit.InitializeDefaultLocaleRule @@ -180,8 +182,10 @@ class ReadingTextSizeFragmentTest { } } + // Requires language configurations. @Test @Config(qualifiers = "sw600dp") + @RunOn(buildEnvironments = [BuildEnvironment.BAZEL]) fun testTextSize_changeTextSizeToMedium_mediumItemIsSelected() { launch(createOptionActivityIntent(0, true)).use { testCoroutineDispatchers.runCurrent() 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..91d58907646 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 @@ -17,10 +17,14 @@ import kotlinx.coroutines.async import kotlinx.coroutines.flow.MutableStateFlow import kotlinx.coroutines.flow.StateFlow import org.junit.After +import org.junit.Rule 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 @@ -31,7 +35,10 @@ import org.oppia.android.domain.oppialogger.ApplicationIdSeed import org.oppia.android.domain.oppialogger.LogStorageModule import org.oppia.android.domain.oppialogger.LoggingIdentifierController import org.oppia.android.domain.oppialogger.analytics.ApplicationLifecycleModule +import org.oppia.android.testing.BuildEnvironment import org.oppia.android.testing.FakeAnalyticsEventLogger +import org.oppia.android.testing.OppiaTestRule +import org.oppia.android.testing.RunOn import org.oppia.android.testing.TestLogReportingModule import org.oppia.android.testing.data.DataProviderTestMonitor import org.oppia.android.testing.logging.EventLogSubject.Companion.assertThat @@ -74,6 +81,7 @@ import javax.inject.Singleton @LooperMode(LooperMode.Mode.PAUSED) @Config(application = ProfileManagementControllerTest.TestApplication::class) class ProfileManagementControllerTest { + @get:Rule val oppiaTestRule = OppiaTestRule() @Inject lateinit var context: Context @Inject lateinit var profileTestHelper: ProfileTestHelper @Inject lateinit var profileManagementController: ProfileManagementController @@ -129,7 +137,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 +204,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 @@ -715,18 +721,168 @@ class ProfileManagementControllerTest { assertThat(profile.readingTextSize).isEqualTo(MEDIUM_TEXT_SIZE) } + // Requires language configurations. @Test - fun testUpdateAudioLanguage_addProfiles_updateWithFrenchLanguage_checkUpdateIsSuccessful() { + @RunOn(buildEnvironments = [BuildEnvironment.BAZEL]) + 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() + } + + // Requires language configurations. + @Test + @RunOn(buildEnvironments = [BuildEnvironment.BAZEL]) + 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) + } + + // Requires language configurations. + @Test + @RunOn(buildEnvironments = [BuildEnvironment.BAZEL]) + 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) + } + + // Requires language configurations. + @Test + @RunOn(buildEnvironments = [BuildEnvironment.BAZEL]) + 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) + } + + // Requires language configurations. + @Test + @RunOn(buildEnvironments = [BuildEnvironment.BAZEL]) + 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) + } + + // Requires language configurations. + @Test + @RunOn(buildEnvironments = [BuildEnvironment.BAZEL]) + 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) + } + + // Requires language configurations. + @Test + @RunOn(buildEnvironments = [BuildEnvironment.BAZEL]) + 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;