diff --git a/app/src/main/java/org/oppia/app/profile/AddProfileActivityPresenter.kt b/app/src/main/java/org/oppia/app/profile/AddProfileActivityPresenter.kt index cfb4307155b..447821e07aa 100644 --- a/app/src/main/java/org/oppia/app/profile/AddProfileActivityPresenter.kt +++ b/app/src/main/java/org/oppia/app/profile/AddProfileActivityPresenter.kt @@ -41,6 +41,9 @@ class AddProfileActivityPresenter @Inject constructor( private var allowDownloadAccess = false private var inputtedPin = false private var inputtedConfirmPin = false + private var storyTextSize = 14f + private var appLanguage = "English" + private var audioLanguage = "No Audio" @ExperimentalCoroutinesApi fun handleOnCreate() { @@ -111,7 +114,10 @@ class AddProfileActivityPresenter @Inject constructor( avatarImagePath = selectedImage, allowDownloadAccess = allowDownloadAccess, colorRgb = activity.intent.getIntExtra(KEY_ADD_PROFILE_COLOR_RGB, -10710042), - isAdmin = false + isAdmin = false, + storyTextSize = storyTextSize, + appLanguage = appLanguage, + audioLanguage = audioLanguage ) .observe(activity, Observer { handleAddProfileResult(it, binding) diff --git a/app/src/main/java/org/oppia/app/profile/ProfileActivityPresenter.kt b/app/src/main/java/org/oppia/app/profile/ProfileActivityPresenter.kt index 421a2c63147..dbf2dd10bef 100644 --- a/app/src/main/java/org/oppia/app/profile/ProfileActivityPresenter.kt +++ b/app/src/main/java/org/oppia/app/profile/ProfileActivityPresenter.kt @@ -21,7 +21,10 @@ class ProfileActivityPresenter @Inject constructor( avatarImagePath = null, allowDownloadAccess = true, colorRgb = -10710042, - isAdmin = true + isAdmin = true, + storyTextSize = 14f, + appLanguage = "English", + audioLanguage = "No Audio" ) activity.setContentView(R.layout.profile_activity) if (getProfileChooserFragment() == null) { diff --git a/app/src/sharedTest/java/org/oppia/app/profile/ProfileChooserFragmentTest.kt b/app/src/sharedTest/java/org/oppia/app/profile/ProfileChooserFragmentTest.kt index 43dd1bfb7f3..5a056e01669 100644 --- a/app/src/sharedTest/java/org/oppia/app/profile/ProfileChooserFragmentTest.kt +++ b/app/src/sharedTest/java/org/oppia/app/profile/ProfileChooserFragmentTest.kt @@ -133,7 +133,7 @@ class ProfileChooserFragmentTest { @Test fun testProfileChooserFragment_clickAdminProfileWithNoPin_checkOpensAdminPinActivity() { - profileManagementController.addProfile("Sean", "", null, true, -10710042, true) + profileManagementController.addProfile("Sean", "", null, true, -10710042, true, 16f, "English", "English") ActivityScenario.launch(ProfileActivity::class.java).use { onView(atPosition(R.id.profile_recycler_view, 1)).perform(click()) intended(hasComponent(AdminPinActivity::class.java.name)) diff --git a/app/src/sharedTest/java/org/oppia/app/settings/profile/ProfileEditActivityTest.kt b/app/src/sharedTest/java/org/oppia/app/settings/profile/ProfileEditActivityTest.kt index 2b1cc6cd6d0..20936395a80 100644 --- a/app/src/sharedTest/java/org/oppia/app/settings/profile/ProfileEditActivityTest.kt +++ b/app/src/sharedTest/java/org/oppia/app/settings/profile/ProfileEditActivityTest.kt @@ -129,7 +129,10 @@ class ProfileEditActivityTest { avatarImagePath = null, allowDownloadAccess = true, colorRgb = -10710042, - isAdmin = false + isAdmin = false, + storyTextSize = 16f, + appLanguage = "English", + audioLanguage = "No Audio" ) ActivityScenario.launch(ProfileEditActivity.createProfileEditActivity(context, 2)).use { onView(withId(R.id.profile_edit_allow_download_switch)).check(matches(isChecked())) diff --git a/domain/src/main/java/org/oppia/domain/profile/ProfileManagementController.kt b/domain/src/main/java/org/oppia/domain/profile/ProfileManagementController.kt index 70d55a86517..176e3b1d42b 100644 --- a/domain/src/main/java/org/oppia/domain/profile/ProfileManagementController.kt +++ b/domain/src/main/java/org/oppia/domain/profile/ProfileManagementController.kt @@ -22,9 +22,7 @@ import org.oppia.util.logging.Logger import org.oppia.util.profile.DirectoryManagementUtil import java.io.File import java.io.FileOutputStream -import java.lang.Exception -import java.util.Date -import java.util.Locale +import java.util.* import javax.inject.Inject import javax.inject.Singleton @@ -37,6 +35,9 @@ private const val UPDATE_DOWNLOAD_ACCESS_TRANSFORMED_PROVIDER_ID = "update_downl private const val LOGIN_PROFILE_TRANSFORMED_PROVIDER_ID = "login_profile_transformed_id" private const val DELETE_PROFILE_TRANSFORMED_PROVIDER_ID = "delete_profile_transformed_id" private const val SET_PROFILE_TRANSFORMED_PROVIDER_ID = "set_profile_transformed_id" +private const val UPDATE_STORY_TEXT_SIZE_TRANSFORMED_ID = "update_story_text_size_transformed_id" +private const val UPDATE_APP_LANGUAGE_TRANSFORMED_PROVIDER_ID = "update_app_language_transformed_id" +private const val UPDATE_AUDIO_LANGUAGE_TRANSFORMED_PROVIDER_ID = "update_audio_language_transformed_id" const val PROFILE_AVATAR_FILE_NAME = "profile_avatar.png" @@ -132,8 +133,12 @@ class ProfileManagementController @Inject constructor( avatarImagePath: Uri?, allowDownloadAccess: Boolean, colorRgb: Int, - isAdmin: Boolean + isAdmin: Boolean, + storyTextSize: Float?, + appLanguage: String?, + audioLanguage: String? ): LiveData> { + if (!onlyLetters(name)) { return MutableLiveData(AsyncResult.failed(ProfileNameOnlyLettersException("$name does not contain only letters"))) } @@ -151,6 +156,9 @@ class ProfileManagementController @Inject constructor( .setAllowDownloadAccess(allowDownloadAccess) .setId(ProfileId.newBuilder().setInternalId(nextProfileId)) .setDateCreatedTimestampMs(Date().time).setIsAdmin(isAdmin) + .setStoryTextSize(storyTextSize!!) + .setAppLanguage(appLanguage) + .setAudioLanguage(audioLanguage) if (avatarImagePath != null) { val imageUri = @@ -250,6 +258,81 @@ class ProfileManagementController @Inject constructor( }) } + /** + * Updates the story text size of the profile. + * + * @param profileId the ID corresponding to the profile being updated. + * @param storyTextSize New text size for the profile being updated. + * @return a [LiveData] that indicates the success/failure of this update operation. + */ + fun updateStoryTextSize( + profileId: ProfileId, storyTextSize: Float + ): LiveData> { + val deferred = profileDataStore.storeDataWithCustomChannelAsync(updateInMemoryCache = true) { + val profile = it.profilesMap[profileId.internalId] ?: return@storeDataWithCustomChannelAsync Pair( + it, + ProfileActionStatus.PROFILE_NOT_FOUND + ) + val updatedProfile = profile.toBuilder().setStoryTextSize(storyTextSize).build() + val profileDatabaseBuilder = it.toBuilder().putProfiles(profileId.internalId, updatedProfile) + Pair(profileDatabaseBuilder.build(), ProfileActionStatus.SUCCESS) + } + return dataProviders.convertToLiveData( + dataProviders.createInMemoryDataProviderAsync(UPDATE_STORY_TEXT_SIZE_TRANSFORMED_ID) { + return@createInMemoryDataProviderAsync getDeferredResult(profileId, null, deferred) + }) + } + + /** + * Updates the app language of the profile. + * + * @param profileId the ID corresponding to the profile being updated. + * @param appLanguage New app language for the profile being updated. + * @return a [LiveData] that indicates the success/failure of this update operation. + */ + fun updateAppLanguage( + profileId: ProfileId, appLanguage: String + ): LiveData> { + val deferred = profileDataStore.storeDataWithCustomChannelAsync(updateInMemoryCache = true) { + val profile = it.profilesMap[profileId.internalId] ?: return@storeDataWithCustomChannelAsync Pair( + it, + ProfileActionStatus.PROFILE_NOT_FOUND + ) + val updatedProfile = profile.toBuilder().setAppLanguage(appLanguage).build() + val profileDatabaseBuilder = it.toBuilder().putProfiles(profileId.internalId, updatedProfile) + Pair(profileDatabaseBuilder.build(), ProfileActionStatus.SUCCESS) + } + return dataProviders.convertToLiveData( + dataProviders.createInMemoryDataProviderAsync(UPDATE_APP_LANGUAGE_TRANSFORMED_PROVIDER_ID) { + return@createInMemoryDataProviderAsync getDeferredResult(profileId, null, deferred) + }) + } + + /** + * 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 [LiveData] that indicates the success/failure of this update operation. + */ + fun updateAudioLanguage( + profileId: ProfileId, audioLanguage: String + ): LiveData> { + 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.convertToLiveData( + dataProviders.createInMemoryDataProviderAsync(UPDATE_AUDIO_LANGUAGE_TRANSFORMED_PROVIDER_ID) { + return@createInMemoryDataProviderAsync getDeferredResult(profileId, null, deferred) + }) + } + /** * Log in to the user's Profile by setting the current profile Id and updating profile's last logged in time. * diff --git a/domain/src/main/java/org/oppia/domain/profile/ProfileTestHelper.kt b/domain/src/main/java/org/oppia/domain/profile/ProfileTestHelper.kt index 4ad980f0ebe..3ae6a4fefb7 100644 --- a/domain/src/main/java/org/oppia/domain/profile/ProfileTestHelper.kt +++ b/domain/src/main/java/org/oppia/domain/profile/ProfileTestHelper.kt @@ -17,7 +17,10 @@ class ProfileTestHelper @Inject constructor( avatarImagePath = null, allowDownloadAccess = true, colorRgb = -10710042, - isAdmin = true + isAdmin = true, + storyTextSize = 16f, + appLanguage = "English", + audioLanguage = "Hindi" ) profileManagementController.addProfile( name = "Ben", @@ -25,7 +28,10 @@ class ProfileTestHelper @Inject constructor( avatarImagePath = null, allowDownloadAccess = false, colorRgb = -10710042, - isAdmin = false + isAdmin = false, + storyTextSize = 16f, + appLanguage = "Hindi", + audioLanguage = "English" ) return profileManagementController.loginToProfile(ProfileId.newBuilder().setInternalId(0).build()) } @@ -39,7 +45,10 @@ class ProfileTestHelper @Inject constructor( avatarImagePath = null, allowDownloadAccess = false, colorRgb = -10710042, - isAdmin = false + isAdmin = false, + storyTextSize = 18f, + appLanguage = "Chinese", + audioLanguage = "French" ) } } @@ -51,4 +60,7 @@ class ProfileTestHelper @Inject constructor( /** Login to user profile. */ fun loginToUser() = profileManagementController.loginToProfile(ProfileId.newBuilder().setInternalId(1).build()) + /** Login to user profile. */ + fun loginToUser2() = + profileManagementController.loginToProfile(ProfileId.newBuilder().setInternalId(2).build()) } diff --git a/domain/src/test/java/org/oppia/domain/profile/ProfileManagementControllerTest.kt b/domain/src/test/java/org/oppia/domain/profile/ProfileManagementControllerTest.kt index c389d8a9e7d..c81b82ffc91 100644 --- a/domain/src/test/java/org/oppia/domain/profile/ProfileManagementControllerTest.kt +++ b/domain/src/test/java/org/oppia/domain/profile/ProfileManagementControllerTest.kt @@ -128,7 +128,10 @@ class ProfileManagementControllerTest { avatarImagePath = null, allowDownloadAccess = true, colorRgb = -10710042, - isAdmin = true + isAdmin = true, + storyTextSize = 14f, + appLanguage = "English", + audioLanguage = "No Audio" ).observeForever(mockUpdateResultObserver) advanceUntilIdle() @@ -157,7 +160,10 @@ class ProfileManagementControllerTest { avatarImagePath = null, allowDownloadAccess = false, colorRgb = -10710042, - isAdmin = true + isAdmin = true, + storyTextSize = 14f, + appLanguage = "English", + audioLanguage = "No Audio" ).observeForever(mockUpdateResultObserver) verify(mockUpdateResultObserver, atLeastOnce()).onChanged(updateResultCaptor.capture()) @@ -179,7 +185,10 @@ class ProfileManagementControllerTest { avatarImagePath = null, allowDownloadAccess = false, colorRgb = -10710042, - isAdmin = true + isAdmin = true, + storyTextSize = 14f, + appLanguage = "English", + audioLanguage = "No Audio" ).observeForever(mockUpdateResultObserver) verify(mockUpdateResultObserver, atLeastOnce()).onChanged(updateResultCaptor.capture()) @@ -237,7 +246,10 @@ class ProfileManagementControllerTest { avatarImagePath = null, allowDownloadAccess = false, colorRgb = -10710042, - isAdmin = false + isAdmin = false, + storyTextSize = 14f, + appLanguage = "English", + audioLanguage = "No Audio" ) advanceUntilIdle() profileManagementController.getProfiles().observeForever(mockProfilesObserver) @@ -381,6 +393,67 @@ class ProfileManagementControllerTest { .contains("ProfileId 6 does not match an existing Profile") } + + @Test + @ExperimentalCoroutinesApi + fun testUpdateStoryTextSize_addProfiles_updateWithFontSize18_checkUpdateIsSuccessful() = + runBlockingTest(coroutineContext) { + addTestProfiles() + advanceUntilIdle() + + val profileId = ProfileId.newBuilder().setInternalId(2).build() + profileManagementController.updateStoryTextSize(profileId, 18f) + .observeForever(mockUpdateResultObserver) + advanceUntilIdle() + profileManagementController.getProfile(profileId).observeForever(mockProfileObserver) + + verify(mockUpdateResultObserver, atLeastOnce()).onChanged(updateResultCaptor.capture()) + verify(mockProfileObserver, atLeastOnce()).onChanged(profileResultCaptor.capture()) + assertThat(updateResultCaptor.value.isSuccess()).isTrue() + assertThat(profileResultCaptor.value.isSuccess()).isTrue() + assertThat(profileResultCaptor.value.getOrThrow().storyTextSize).isEqualTo(18f) + } + + @Test + @ExperimentalCoroutinesApi + fun testUpdateAppLanguage_addProfiles_updateWithChineseLanguage_checkUpdateIsSuccessful() = + runBlockingTest(coroutineContext) { + addTestProfiles() + advanceUntilIdle() + + val profileId = ProfileId.newBuilder().setInternalId(2).build() + profileManagementController.updateAppLanguage(profileId, "Chinese") + .observeForever(mockUpdateResultObserver) + advanceUntilIdle() + profileManagementController.getProfile(profileId).observeForever(mockProfileObserver) + + verify(mockUpdateResultObserver, atLeastOnce()).onChanged(updateResultCaptor.capture()) + verify(mockProfileObserver, atLeastOnce()).onChanged(profileResultCaptor.capture()) + assertThat(updateResultCaptor.value.isSuccess()).isTrue() + assertThat(profileResultCaptor.value.isSuccess()).isTrue() + assertThat(profileResultCaptor.value.getOrThrow().appLanguage).isEqualTo("Chinese") + } + + @Test + @ExperimentalCoroutinesApi + fun testUpdateAudioLanguage_addProfiles_updateWithFrenchLanguage_checkUpdateIsSuccessful() = + runBlockingTest(coroutineContext) { + addTestProfiles() + advanceUntilIdle() + + val profileId = ProfileId.newBuilder().setInternalId(2).build() + profileManagementController.updateAudioLanguage(profileId, "French") + .observeForever(mockUpdateResultObserver) + advanceUntilIdle() + profileManagementController.getProfile(profileId).observeForever(mockProfileObserver) + + verify(mockUpdateResultObserver, atLeastOnce()).onChanged(updateResultCaptor.capture()) + verify(mockProfileObserver, atLeastOnce()).onChanged(profileResultCaptor.capture()) + assertThat(updateResultCaptor.value.isSuccess()).isTrue() + assertThat(profileResultCaptor.value.isSuccess()).isTrue() + assertThat(profileResultCaptor.value.getOrThrow().audioLanguage).isEqualTo("French") + } + @Test @ExperimentalCoroutinesApi fun testDeleteProfile_addProfiles_deleteProfile_checkDeletionIsSuccessful() = @@ -417,7 +490,10 @@ class ProfileManagementControllerTest { avatarImagePath = null, allowDownloadAccess = false, colorRgb = -10710042, - isAdmin = true + isAdmin = true, + storyTextSize = 14f, + appLanguage = "English", + audioLanguage = "No Audio" ) advanceUntilIdle() profileManagementController.getProfiles().observeForever(mockProfilesObserver) @@ -510,7 +586,10 @@ class ProfileManagementControllerTest { avatarImagePath = null, allowDownloadAccess = it.allowDownloadAccess, colorRgb = -10710042, - isAdmin = false + isAdmin = false, + storyTextSize = it.storyTextSize, + appLanguage = it.appLanguage, + audioLanguage = it.audioLanguage ) } } diff --git a/model/src/main/proto/profile.proto b/model/src/main/proto/profile.proto index b18a669398e..5e7e9776e1c 100644 --- a/model/src/main/proto/profile.proto +++ b/model/src/main/proto/profile.proto @@ -41,6 +41,15 @@ message Profile { // Represents the time the user's profile was created. int64 date_created_timestamp_ms = 9; + + // Represents user selected story-text-size. + float story_text_size = 10; + + // Represents user selected audio-language. + string audio_language = 11; + + // Represents user selected app-language. + string app_language = 12; } // Represents a profile avatar image. @@ -72,3 +81,4 @@ message ProfileChooserUiModel { bool add_profile = 2; } } +