diff --git a/app/BUILD.bazel b/app/BUILD.bazel index 93a3c1dd4b6..1f8fc6eeacb 100644 --- a/app/BUILD.bazel +++ b/app/BUILD.bazel @@ -124,7 +124,8 @@ LISTENERS = [ "src/main/java/org/oppia/android/app/home/topiclist/TopicSummaryClickListener.kt", "src/main/java/org/oppia/android/app/onboarding/OnboardingNavigationListener.kt", "src/main/java/org/oppia/android/app/onboarding/RouteToProfileListListener.kt", - "src/main/java/org/oppia/android/app/options/LanguageRadioButtonListener.kt", + "src/main/java/org/oppia/android/app/options/AppLanguageRadioButtonListener.kt", + "src/main/java/org/oppia/android/app/options/AudioLanguageRadioButtonListener.kt", "src/main/java/org/oppia/android/app/options/LoadAppLanguageListListener.kt", "src/main/java/org/oppia/android/app/options/LoadAudioLanguageListListener.kt", "src/main/java/org/oppia/android/app/options/LoadReadingTextSizeListener.kt", @@ -293,8 +294,10 @@ VIEW_MODELS = [ "src/main/java/org/oppia/android/app/onboarding/OnboardingViewPagerViewModel.kt", "src/main/java/org/oppia/android/app/onboarding/ViewPagerSlide.kt", "src/main/java/org/oppia/android/app/ongoingtopiclist/OngoingTopicListViewModel.kt", - "src/main/java/org/oppia/android/app/options/LanguageItemViewModel.kt", - "src/main/java/org/oppia/android/app/options/LanguageSelectionViewModel.kt", + "src/main/java/org/oppia/android/app/options/AppLanguageItemViewModel.kt", + "src/main/java/org/oppia/android/app/options/AppLanguageSelectionViewModel.kt", + "src/main/java/org/oppia/android/app/options/AudioLanguageItemViewModel.kt", + "src/main/java/org/oppia/android/app/options/AudioLanguageSelectionViewModel.kt", "src/main/java/org/oppia/android/app/options/OptionControlsViewModel.kt", "src/main/java/org/oppia/android/app/options/OptionsAppLanguageViewModel.kt", "src/main/java/org/oppia/android/app/options/OptionsAudioLanguageViewModel.kt", diff --git a/app/src/main/java/org/oppia/android/app/options/AppLanguageFragment.kt b/app/src/main/java/org/oppia/android/app/options/AppLanguageFragment.kt index e5b3b5e5ab0..d83088db9af 100644 --- a/app/src/main/java/org/oppia/android/app/options/AppLanguageFragment.kt +++ b/app/src/main/java/org/oppia/android/app/options/AppLanguageFragment.kt @@ -17,9 +17,7 @@ private const val APP_LANGUAGE_PREFERENCE_SUMMARY_VALUE_ARGUMENT_KEY = private const val SELECTED_LANGUAGE_SAVED_KEY = "AppLanguageFragment.selected_language" /** The fragment to change the language of the app. */ -class AppLanguageFragment : - InjectableFragment(), - LanguageRadioButtonListener { +class AppLanguageFragment : InjectableFragment(), AppLanguageRadioButtonListener { @Inject lateinit var appLanguageFragmentPresenter: AppLanguageFragmentPresenter diff --git a/app/src/main/java/org/oppia/android/app/options/AppLanguageFragmentPresenter.kt b/app/src/main/java/org/oppia/android/app/options/AppLanguageFragmentPresenter.kt index 1b4b2e6a1f8..89810a9266f 100644 --- a/app/src/main/java/org/oppia/android/app/options/AppLanguageFragmentPresenter.kt +++ b/app/src/main/java/org/oppia/android/app/options/AppLanguageFragmentPresenter.kt @@ -6,13 +6,13 @@ import android.view.ViewGroup import androidx.fragment.app.Fragment import org.oppia.android.app.recyclerview.BindableAdapter import org.oppia.android.databinding.AppLanguageFragmentBinding -import org.oppia.android.databinding.LanguageItemsBinding +import org.oppia.android.databinding.AppLanguageItemBinding import javax.inject.Inject /** The presenter for [AppLanguageFragment]. */ class AppLanguageFragmentPresenter @Inject constructor( private val fragment: Fragment, - private val languageSelectionViewModel: LanguageSelectionViewModel + private val appLanguageSelectionViewModel: AppLanguageSelectionViewModel ) { private lateinit var prefSummaryValue: String fun handleOnCreateView( @@ -27,8 +27,8 @@ class AppLanguageFragmentPresenter @Inject constructor( /* attachToRoot= */ false ) this.prefSummaryValue = prefSummaryValue - binding.viewModel = languageSelectionViewModel - languageSelectionViewModel.selectedLanguage.value = prefSummaryValue + binding.viewModel = appLanguageSelectionViewModel + appLanguageSelectionViewModel.selectedLanguage.value = prefSummaryValue binding.languageRecyclerView.apply { adapter = createRecyclerViewAdapter() } @@ -37,16 +37,16 @@ class AppLanguageFragmentPresenter @Inject constructor( } fun getLanguageSelected(): String? { - return languageSelectionViewModel.selectedLanguage.value + return appLanguageSelectionViewModel.selectedLanguage.value } - private fun createRecyclerViewAdapter(): BindableAdapter { + private fun createRecyclerViewAdapter(): BindableAdapter { return BindableAdapter.SingleTypeBuilder - .newBuilder() + .newBuilder() .setLifecycleOwner(fragment) .registerViewDataBinderWithSameModelType( - inflateDataBinding = LanguageItemsBinding::inflate, - setViewModel = LanguageItemsBinding::setViewModel + inflateDataBinding = AppLanguageItemBinding::inflate, + setViewModel = AppLanguageItemBinding::setViewModel ).build() } @@ -61,7 +61,7 @@ class AppLanguageFragmentPresenter @Inject constructor( } fun onLanguageSelected(selectedLanguage: String) { - languageSelectionViewModel.selectedLanguage.value = selectedLanguage + appLanguageSelectionViewModel.selectedLanguage.value = selectedLanguage updateAppLanguage(selectedLanguage) } } diff --git a/app/src/main/java/org/oppia/android/app/options/AppLanguageItemViewModel.kt b/app/src/main/java/org/oppia/android/app/options/AppLanguageItemViewModel.kt new file mode 100644 index 00000000000..c328328936a --- /dev/null +++ b/app/src/main/java/org/oppia/android/app/options/AppLanguageItemViewModel.kt @@ -0,0 +1,27 @@ +package org.oppia.android.app.options + +import androidx.lifecycle.LiveData +import androidx.lifecycle.Transformations +import org.oppia.android.app.viewmodel.ObservableViewModel + +/** + * Language item view model for the recycler view in [AppLanguageFragment] and. + * + * @property language the app language corresponding to this language item to be displayed + * @property currentSelectedLanguage the [LiveData] tracking the currently selected language + * @property appLanguageRadioButtonListener the listener which will be called if this language is + * selected by the user + */ +class AppLanguageItemViewModel( + val language: String, + private val currentSelectedLanguage: LiveData, + val appLanguageRadioButtonListener: AppLanguageRadioButtonListener +) : ObservableViewModel() { + /** + * Indicates whether the language corresponding to this view model is _currently_ selected in the + * radio button list. + */ + val isLanguageSelected: LiveData by lazy { + Transformations.map(currentSelectedLanguage) { it == language } + } +} diff --git a/app/src/main/java/org/oppia/android/app/options/AppLanguageRadioButtonListener.kt b/app/src/main/java/org/oppia/android/app/options/AppLanguageRadioButtonListener.kt new file mode 100644 index 00000000000..5ae0f387241 --- /dev/null +++ b/app/src/main/java/org/oppia/android/app/options/AppLanguageRadioButtonListener.kt @@ -0,0 +1,7 @@ +package org.oppia.android.app.options + +/** Listener for when a language is selected for the [AppLanguageFragment]. */ +interface AppLanguageRadioButtonListener { + /** Called when the user selected a new app language to use as their default preference. */ + fun onLanguageSelected(appLanguage: String) +} diff --git a/app/src/main/java/org/oppia/android/app/options/AppLanguageSelectionViewModel.kt b/app/src/main/java/org/oppia/android/app/options/AppLanguageSelectionViewModel.kt new file mode 100644 index 00000000000..72dd156f64d --- /dev/null +++ b/app/src/main/java/org/oppia/android/app/options/AppLanguageSelectionViewModel.kt @@ -0,0 +1,27 @@ +package org.oppia.android.app.options + +import androidx.fragment.app.Fragment +import androidx.lifecycle.MutableLiveData +import org.oppia.android.app.fragment.FragmentScope +import org.oppia.android.app.viewmodel.ObservableViewModel +import javax.inject.Inject + +/** Language list view model for the recycler view in [AppLanguageFragment]. */ +@FragmentScope +class AppLanguageSelectionViewModel @Inject constructor( + val fragment: Fragment +) : ObservableViewModel() { + /** The name of the app language currently selected in the radio button list. */ + val selectedLanguage = MutableLiveData() + private val appLanguageRadioButtonListener = fragment as AppLanguageRadioButtonListener + + private val appLanguagesList = listOf( + AppLanguageItemViewModel("English", selectedLanguage, appLanguageRadioButtonListener), + AppLanguageItemViewModel("French", selectedLanguage, appLanguageRadioButtonListener), + AppLanguageItemViewModel("Hindi", selectedLanguage, appLanguageRadioButtonListener), + AppLanguageItemViewModel("Chinese", selectedLanguage, appLanguageRadioButtonListener) + ) + + /** The list of [AppLanguageItemViewModel]s which can be bound to a recycler view. */ + val recyclerViewAppLanguageList: List by lazy { appLanguagesList } +} diff --git a/app/src/main/java/org/oppia/android/app/options/AudioLanguageActivity.kt b/app/src/main/java/org/oppia/android/app/options/AudioLanguageActivity.kt index 860cef3b6d6..3487ce0231f 100644 --- a/app/src/main/java/org/oppia/android/app/options/AudioLanguageActivity.kt +++ b/app/src/main/java/org/oppia/android/app/options/AudioLanguageActivity.kt @@ -5,72 +5,64 @@ import android.content.Intent import android.os.Bundle import org.oppia.android.app.activity.ActivityComponentImpl import org.oppia.android.app.activity.InjectableAppCompatActivity +import org.oppia.android.app.model.AudioLanguage +import org.oppia.android.app.model.AudioLanguageActivityParams +import org.oppia.android.app.model.AudioLanguageActivityStateBundle +import org.oppia.android.util.extensions.getProto +import org.oppia.android.util.extensions.getProtoExtra +import org.oppia.android.util.extensions.putProto +import org.oppia.android.util.extensions.putProtoExtra import javax.inject.Inject /** The activity to change the Default Audio language of the app. */ class AudioLanguageActivity : InjectableAppCompatActivity() { - - @Inject - lateinit var audioLanguageActivityPresenter: AudioLanguageActivityPresenter - private lateinit var prefKey: String - private lateinit var prefSummaryValue: String + @Inject lateinit var audioLanguageActivityPresenter: AudioLanguageActivityPresenter override fun onCreate(savedInstanceState: Bundle?) { super.onCreate(savedInstanceState) (activityComponent as ActivityComponentImpl).inject(this) - prefKey = checkNotNull(intent.getStringExtra(AUDIO_LANGUAGE_PREFERENCE_TITLE_EXTRA_KEY)) { - "Expected $AUDIO_LANGUAGE_PREFERENCE_TITLE_EXTRA_KEY to be in intent extras." - } - prefSummaryValue = if (savedInstanceState != null) { - savedInstanceState.get(AUDIO_LANGUAGE_PREFERENCE_SUMMARY_VALUE_EXTRA_KEY) as String - } else { - checkNotNull(intent.getStringExtra(AUDIO_LANGUAGE_PREFERENCE_SUMMARY_VALUE_EXTRA_KEY)) { - "Expected $AUDIO_LANGUAGE_PREFERENCE_SUMMARY_VALUE_EXTRA_KEY to be in intent extras." - } - } - audioLanguageActivityPresenter.handleOnCreate(prefKey, prefSummaryValue) + audioLanguageActivityPresenter.handleOnCreate( + savedInstanceState?.retrieveLanguageFromSavedState() ?: intent.retrieveLanguageFromParams() + ) + } + + override fun onSaveInstanceState(outState: Bundle) { + super.onSaveInstanceState(outState) + val state = AudioLanguageActivityStateBundle.newBuilder().apply { + audioLanguage = audioLanguageActivityPresenter.getLanguageSelected() + }.build() + outState.putProto(ACTIVITY_SAVED_STATE_KEY, state) } + override fun onBackPressed() = audioLanguageActivityPresenter.finishWithResult() + companion object { - internal const val AUDIO_LANGUAGE_PREFERENCE_TITLE_EXTRA_KEY = - "AudioLanguageActivity.audio_language_preference_title" - const val AUDIO_LANGUAGE_PREFERENCE_SUMMARY_VALUE_EXTRA_KEY = - "AudioLanguageActivity.audio_language_preference_summary_value" + private const val ACTIVITY_PARAMS_KEY = "AudioLanguageActivity.params" + private const val ACTIVITY_SAVED_STATE_KEY = "AudioLanguageActivity.saved_state" /** Returns a new [Intent] to route to [AudioLanguageActivity]. */ fun createAudioLanguageActivityIntent( context: Context, - prefKey: String, - summaryValue: String? + audioLanguage: AudioLanguage ): Intent { - val intent = Intent(context, AudioLanguageActivity::class.java) - intent.putExtra(AUDIO_LANGUAGE_PREFERENCE_TITLE_EXTRA_KEY, prefKey) - intent.putExtra(AUDIO_LANGUAGE_PREFERENCE_SUMMARY_VALUE_EXTRA_KEY, summaryValue) - return intent + return Intent(context, AudioLanguageActivity::class.java).apply { + val arguments = AudioLanguageActivityParams.newBuilder().apply { + this.audioLanguage = audioLanguage + }.build() + putProtoExtra(ACTIVITY_PARAMS_KEY, arguments) + } } - fun getKeyAudioLanguagePreferenceTitle(): String { - return AUDIO_LANGUAGE_PREFERENCE_TITLE_EXTRA_KEY + private fun Intent.retrieveLanguageFromParams(): AudioLanguage { + return getProtoExtra( + ACTIVITY_PARAMS_KEY, AudioLanguageActivityParams.getDefaultInstance() + ).audioLanguage } - fun getKeyAudioLanguagePreferenceSummaryValue(): String { - return AUDIO_LANGUAGE_PREFERENCE_SUMMARY_VALUE_EXTRA_KEY + private fun Bundle.retrieveLanguageFromSavedState(): AudioLanguage { + return getProto( + ACTIVITY_SAVED_STATE_KEY, AudioLanguageActivityStateBundle.getDefaultInstance() + ).audioLanguage } } - - override fun onSaveInstanceState(outState: Bundle) { - super.onSaveInstanceState(outState) - outState.putString( - AUDIO_LANGUAGE_PREFERENCE_SUMMARY_VALUE_EXTRA_KEY, - audioLanguageActivityPresenter.getLanguageSelected() - ) - } - - override fun onBackPressed() { - val message = audioLanguageActivityPresenter.getLanguageSelected() - val intent = Intent() - intent.putExtra(MESSAGE_AUDIO_LANGUAGE_ARGUMENT_KEY, message) - setResult(REQUEST_CODE_AUDIO_LANGUAGE, intent) - finish() - } } diff --git a/app/src/main/java/org/oppia/android/app/options/AudioLanguageActivityPresenter.kt b/app/src/main/java/org/oppia/android/app/options/AudioLanguageActivityPresenter.kt index 0b303875ed9..0a842397a4b 100644 --- a/app/src/main/java/org/oppia/android/app/options/AudioLanguageActivityPresenter.kt +++ b/app/src/main/java/org/oppia/android/app/options/AudioLanguageActivityPresenter.kt @@ -5,46 +5,60 @@ import androidx.appcompat.app.AppCompatActivity import androidx.databinding.DataBindingUtil import org.oppia.android.R import org.oppia.android.app.activity.ActivityScope +import org.oppia.android.app.model.AudioLanguage +import org.oppia.android.app.model.AudioLanguageActivityResultBundle import org.oppia.android.databinding.AudioLanguageActivityBinding +import org.oppia.android.util.extensions.putProtoExtra import javax.inject.Inject /** The presenter for [AudioLanguageActivity]. */ @ActivityScope class AudioLanguageActivityPresenter @Inject constructor(private val activity: AppCompatActivity) { + private lateinit var audioLanguage: AudioLanguage - private lateinit var prefSummaryValue: String - - fun handleOnCreate(prefKey: String, prefValue: String) { - val binding: AudioLanguageActivityBinding = DataBindingUtil.setContentView( - activity, - R.layout.audio_language_activity, - ) - val toolbar = binding.audioLanguageToolbar - toolbar.setNavigationOnClickListener { - val intent = Intent().apply { - putExtra(MESSAGE_AUDIO_LANGUAGE_ARGUMENT_KEY, prefSummaryValue) - } - (activity as AudioLanguageActivity).setResult(REQUEST_CODE_AUDIO_LANGUAGE, intent) - activity.finish() + /** Handles when the activity is first created. */ + fun handleOnCreate(audioLanguage: AudioLanguage) { + this.audioLanguage = audioLanguage + + val binding: AudioLanguageActivityBinding = + DataBindingUtil.setContentView(activity, R.layout.audio_language_activity) + binding.audioLanguageToolbar.setNavigationOnClickListener { + finishWithResult() } - setLanguageSelected(prefValue) if (getAudioLanguageFragment() == null) { - val audioLanguageFragment = AudioLanguageFragment.newInstance(prefKey, prefValue) + val audioLanguageFragment = AudioLanguageFragment.newInstance(audioLanguage) activity.supportFragmentManager.beginTransaction() .add(R.id.audio_language_fragment_container, audioLanguageFragment).commitNow() } } - fun setLanguageSelected(audioLanguage: String) { - prefSummaryValue = audioLanguage + /** Updates the currently selected [AudioLanguage] to the specified [audioLanguage]. */ + fun setLanguageSelected(audioLanguage: AudioLanguage) { + this.audioLanguage = audioLanguage } - fun getLanguageSelected(): String { - return prefSummaryValue + /** Returns the current [AudioLanguage] selected in the activity. */ + fun getLanguageSelected(): AudioLanguage = audioLanguage + + /** + * Finishes the current activity with a result (specifically, an intent result with + * [AudioLanguageActivityResultBundle] populated with the [AudioLanguage] that was selected in the + * activity). + */ + fun finishWithResult() { + val intent = Intent().apply { + val result = AudioLanguageActivityResultBundle.newBuilder().apply { + this.audioLanguage = this@AudioLanguageActivityPresenter.audioLanguage + }.build() + putProtoExtra(MESSAGE_AUDIO_LANGUAGE_RESULTS_KEY, result) + } + + activity.setResult(REQUEST_CODE_AUDIO_LANGUAGE, intent) + activity.finish() } private fun getAudioLanguageFragment(): AudioLanguageFragment? { return activity.supportFragmentManager - .findFragmentById(R.id.audio_language_fragment_container) as AudioLanguageFragment? + .findFragmentById(R.id.audio_language_fragment_container) as? AudioLanguageFragment } } diff --git a/app/src/main/java/org/oppia/android/app/options/AudioLanguageFragment.kt b/app/src/main/java/org/oppia/android/app/options/AudioLanguageFragment.kt index 4ae93257ada..fd98e6259cd 100644 --- a/app/src/main/java/org/oppia/android/app/options/AudioLanguageFragment.kt +++ b/app/src/main/java/org/oppia/android/app/options/AudioLanguageFragment.kt @@ -7,34 +7,16 @@ import android.view.View import android.view.ViewGroup import org.oppia.android.app.fragment.FragmentComponentImpl import org.oppia.android.app.fragment.InjectableFragment -import org.oppia.android.util.extensions.getStringFromBundle +import org.oppia.android.app.model.AudioLanguage +import org.oppia.android.app.model.AudioLanguageFragmentArguments +import org.oppia.android.app.model.AudioLanguageFragmentStateBundle +import org.oppia.android.util.extensions.getProto +import org.oppia.android.util.extensions.putProto import javax.inject.Inject -private const val AUDIO_LANGUAGE_PREFERENCE_TITLE_ARGUMENT_KEY = - "AudioLanguageFragment.audio_language_preference_title" -private const val AUDIO_LANGUAGE_PREFERENCE_SUMMARY_VALUE_ARGUMENT_KEY = - "AudioLanguageFragment.audio_language_preference_summary_value" -private const val SELECTED_AUDIO_LANGUAGE_SAVED_KEY = - "AudioLanguageFragment.selected_audio_language" - /** The fragment to change the default audio language of the app. */ -class AudioLanguageFragment : - InjectableFragment(), - LanguageRadioButtonListener { - - @Inject - lateinit var audioLanguageFragmentPresenter: AudioLanguageFragmentPresenter - - companion object { - fun newInstance(prefsKey: String, prefsSummaryValue: String): AudioLanguageFragment { - val args = Bundle() - args.putString(AUDIO_LANGUAGE_PREFERENCE_TITLE_ARGUMENT_KEY, prefsKey) - args.putString(AUDIO_LANGUAGE_PREFERENCE_SUMMARY_VALUE_ARGUMENT_KEY, prefsSummaryValue) - val fragment = AudioLanguageFragment() - fragment.arguments = args - return fragment - } - } +class AudioLanguageFragment : InjectableFragment(), AudioLanguageRadioButtonListener { + @Inject lateinit var audioLanguageFragmentPresenter: AudioLanguageFragmentPresenter override fun onAttach(context: Context) { super.onAttach(context) @@ -45,36 +27,56 @@ class AudioLanguageFragment : inflater: LayoutInflater, container: ViewGroup?, savedInstanceState: Bundle? - ): View? { - val args = - checkNotNull(arguments) { "Expected arguments to be passed to AudioLanguageFragment" } - val prefsKey = args.getStringFromBundle(AUDIO_LANGUAGE_PREFERENCE_TITLE_ARGUMENT_KEY) - val audioLanguageDefaultSummary = checkNotNull( - args.getStringFromBundle(AUDIO_LANGUAGE_PREFERENCE_SUMMARY_VALUE_ARGUMENT_KEY) - ) - val prefsSummaryValue = if (savedInstanceState == null) { - audioLanguageDefaultSummary - } else { - savedInstanceState.get(SELECTED_AUDIO_LANGUAGE_SAVED_KEY) as? String - ?: audioLanguageDefaultSummary - } - return audioLanguageFragmentPresenter.handleOnCreateView( - inflater, - container, - prefsKey!!, - prefsSummaryValue - ) + ): View { + val audioLanguage = + checkNotNull( + savedInstanceState?.retrieveLanguageFromSavedState() + ?: arguments?.retrieveLanguageFromArguments() + ) { "Expected arguments to be passed to AudioLanguageFragment" } + return audioLanguageFragmentPresenter.handleOnCreateView(inflater, container, audioLanguage) } override fun onSaveInstanceState(outState: Bundle) { super.onSaveInstanceState(outState) - outState.putString( - SELECTED_AUDIO_LANGUAGE_SAVED_KEY, - audioLanguageFragmentPresenter.getLanguageSelected() - ) + val state = AudioLanguageFragmentStateBundle.newBuilder().apply { + audioLanguage = audioLanguageFragmentPresenter.getLanguageSelected() + }.build() + outState.putProto(FRAGMENT_SAVED_STATE_KEY, state) + } + + override fun onLanguageSelected(audioLanguage: AudioLanguage) { + audioLanguageFragmentPresenter.onLanguageSelected(audioLanguage) } - override fun onLanguageSelected(selectedLanguage: String) { - audioLanguageFragmentPresenter.onLanguageSelected(selectedLanguage) + companion object { + private const val FRAGMENT_ARGUMENTS_KEY = "AudioLanguageFragment.arguments" + private const val FRAGMENT_SAVED_STATE_KEY = "AudioLanguageFragment.saved_state" + + /** + * Returns a new [AudioLanguageFragment] corresponding to the specified [AudioLanguage] (as the + * initial selection). + */ + fun newInstance(audioLanguage: AudioLanguage): AudioLanguageFragment { + return AudioLanguageFragment().apply { + arguments = Bundle().apply { + val args = AudioLanguageFragmentArguments.newBuilder().apply { + this.audioLanguage = audioLanguage + }.build() + putProto(FRAGMENT_ARGUMENTS_KEY, args) + } + } + } + + private fun Bundle.retrieveLanguageFromArguments(): AudioLanguage { + return getProto( + FRAGMENT_ARGUMENTS_KEY, AudioLanguageFragmentArguments.getDefaultInstance() + ).audioLanguage + } + + private fun Bundle.retrieveLanguageFromSavedState(): AudioLanguage { + return getProto( + FRAGMENT_SAVED_STATE_KEY, AudioLanguageFragmentStateBundle.getDefaultInstance() + ).audioLanguage + } } } diff --git a/app/src/main/java/org/oppia/android/app/options/AudioLanguageFragmentPresenter.kt b/app/src/main/java/org/oppia/android/app/options/AudioLanguageFragmentPresenter.kt index 9e8cb65ee06..f235685035f 100644 --- a/app/src/main/java/org/oppia/android/app/options/AudioLanguageFragmentPresenter.kt +++ b/app/src/main/java/org/oppia/android/app/options/AudioLanguageFragmentPresenter.kt @@ -4,53 +4,56 @@ import android.view.LayoutInflater import android.view.View import android.view.ViewGroup import androidx.fragment.app.Fragment +import org.oppia.android.app.model.AudioLanguage import org.oppia.android.app.recyclerview.BindableAdapter import org.oppia.android.databinding.AudioLanguageFragmentBinding -import org.oppia.android.databinding.LanguageItemsBinding +import org.oppia.android.databinding.AudioLanguageItemBinding import javax.inject.Inject /** The presenter for [AudioLanguageFragment]. */ class AudioLanguageFragmentPresenter @Inject constructor( private val fragment: Fragment, - private val languageSelectionViewModel: LanguageSelectionViewModel + private val audioLanguageSelectionViewModel: AudioLanguageSelectionViewModel ) { - private lateinit var prefSummaryValue: String + /** + * Returns a newly inflated view to render the fragment with the specified [audioLanguage] as the + * initial selected language. + */ fun handleOnCreateView( inflater: LayoutInflater, container: ViewGroup?, - prefKey: String, - prefValue: String - ): View? { - val binding = AudioLanguageFragmentBinding.inflate( + audioLanguage: AudioLanguage + ): View { + return AudioLanguageFragmentBinding.inflate( inflater, container, /* attachToRoot= */ false - ) - binding.viewModel = languageSelectionViewModel - prefSummaryValue = prefValue - languageSelectionViewModel.selectedLanguage.value = prefSummaryValue - binding.audioLanguageRecyclerView.apply { - adapter = createRecyclerViewAdapter() - } - - return binding.root + ).apply { + audioLanguageSelectionViewModel.selectedLanguage.value = audioLanguage + audioLanguageRecyclerView.apply { + viewModel = audioLanguageSelectionViewModel + adapter = createRecyclerViewAdapter() + } + }.root } - fun getLanguageSelected(): String? { - return languageSelectionViewModel.selectedLanguage.value + /** Returns the language currently selected in the fragment. */ + fun getLanguageSelected(): AudioLanguage { + return audioLanguageSelectionViewModel.selectedLanguage.value + ?: AudioLanguage.AUDIO_LANGUAGE_UNSPECIFIED } - private fun createRecyclerViewAdapter(): BindableAdapter { + private fun createRecyclerViewAdapter(): BindableAdapter { return BindableAdapter.SingleTypeBuilder - .newBuilder() + .newBuilder() .setLifecycleOwner(fragment) .registerViewDataBinderWithSameModelType( - inflateDataBinding = LanguageItemsBinding::inflate, - setViewModel = LanguageItemsBinding::setViewModel + inflateDataBinding = AudioLanguageItemBinding::inflate, + setViewModel = AudioLanguageItemBinding::setViewModel ).build() } - private fun updateAudioLanguage(audioLanguage: String) { + private fun updateAudioLanguage(audioLanguage: AudioLanguage) { // The first branch of (when) will be used in the case of multipane when (val parentActivity = fragment.activity) { is OptionsActivity -> @@ -60,8 +63,9 @@ class AudioLanguageFragmentPresenter @Inject constructor( } } - fun onLanguageSelected(selectedLanguage: String) { - languageSelectionViewModel.selectedLanguage.value = selectedLanguage - updateAudioLanguage(selectedLanguage) + /** Handles when a new [AudioLanguage] has been selected by the user. */ + fun onLanguageSelected(audioLanguage: AudioLanguage) { + audioLanguageSelectionViewModel.selectedLanguage.value = audioLanguage + updateAudioLanguage(audioLanguage) } } diff --git a/app/src/main/java/org/oppia/android/app/options/AudioLanguageItemViewModel.kt b/app/src/main/java/org/oppia/android/app/options/AudioLanguageItemViewModel.kt new file mode 100644 index 00000000000..7ebe3fa7931 --- /dev/null +++ b/app/src/main/java/org/oppia/android/app/options/AudioLanguageItemViewModel.kt @@ -0,0 +1,32 @@ +package org.oppia.android.app.options + +import androidx.lifecycle.LiveData +import androidx.lifecycle.Transformations +import org.oppia.android.app.model.AudioLanguage +import org.oppia.android.app.viewmodel.ObservableViewModel + +/** + * Language item view model for the recycler view in [AppLanguageFragment] and + * [AudioLanguageFragment]. + * + * @property language the [AudioLanguage] corresponding to this language item to be displayed + * @property languageDisplayName the human-readable version of [language] to display to users to + * represent the language + * @property currentSelectedLanguage the [LiveData] tracking the currently selected [AudioLanguage] + * @property audioLanguageRadioButtonListener the listener which will be called if this language is + * selected by the user + */ +class AudioLanguageItemViewModel( + val language: AudioLanguage, + val languageDisplayName: String, + private val currentSelectedLanguage: LiveData, + val audioLanguageRadioButtonListener: AudioLanguageRadioButtonListener +) : ObservableViewModel() { + /** + * Indicates whether the language corresponding to this view model is _currently_ selected in the + * radio button list. + */ + val isLanguageSelected: LiveData by lazy { + Transformations.map(currentSelectedLanguage) { it == language } + } +} diff --git a/app/src/main/java/org/oppia/android/app/options/AudioLanguageRadioButtonListener.kt b/app/src/main/java/org/oppia/android/app/options/AudioLanguageRadioButtonListener.kt new file mode 100644 index 00000000000..a60f661cb48 --- /dev/null +++ b/app/src/main/java/org/oppia/android/app/options/AudioLanguageRadioButtonListener.kt @@ -0,0 +1,9 @@ +package org.oppia.android.app.options + +import org.oppia.android.app.model.AudioLanguage + +/** Listener for when the a language is selected for the [AudioLanguageFragment]. */ +interface AudioLanguageRadioButtonListener { + /** Called when the user selected a new [AudioLanguage] to use as their default preference. */ + fun onLanguageSelected(audioLanguage: AudioLanguage) +} diff --git a/app/src/main/java/org/oppia/android/app/options/AudioLanguageSelectionViewModel.kt b/app/src/main/java/org/oppia/android/app/options/AudioLanguageSelectionViewModel.kt new file mode 100644 index 00000000000..c9e0d998e1b --- /dev/null +++ b/app/src/main/java/org/oppia/android/app/options/AudioLanguageSelectionViewModel.kt @@ -0,0 +1,40 @@ +package org.oppia.android.app.options + +import androidx.fragment.app.Fragment +import androidx.lifecycle.MutableLiveData +import org.oppia.android.app.fragment.FragmentScope +import org.oppia.android.app.model.AudioLanguage +import org.oppia.android.app.translation.AppLanguageResourceHandler +import org.oppia.android.app.viewmodel.ObservableViewModel +import javax.inject.Inject + +/** Language list view model for the recycler view in [AudioLanguageFragment]. */ +@FragmentScope +class AudioLanguageSelectionViewModel @Inject constructor( + private val fragment: Fragment, + private val appLanguageResourceHandler: AppLanguageResourceHandler +) : ObservableViewModel() { + /** The [AudioLanguage] currently selected in the radio button list. */ + val selectedLanguage = MutableLiveData() + + /** The list of [AudioLanguageItemViewModel]s which can be bound to a recycler view. */ + val recyclerViewAudioLanguageList: List by lazy { + AudioLanguage.values().filter { it !in IGNORED_AUDIO_LANGUAGES }.map(::createItemViewModel) + } + + private fun createItemViewModel(language: AudioLanguage): AudioLanguageItemViewModel { + return AudioLanguageItemViewModel( + language, + appLanguageResourceHandler.computeLocalizedDisplayName(language), + selectedLanguage, + fragment as AudioLanguageRadioButtonListener + ) + } + + private companion object { + private val IGNORED_AUDIO_LANGUAGES = + listOf( + AudioLanguage.NO_AUDIO, AudioLanguage.AUDIO_LANGUAGE_UNSPECIFIED, AudioLanguage.UNRECOGNIZED + ) + } +} diff --git a/app/src/main/java/org/oppia/android/app/options/LanguageItemViewModel.kt b/app/src/main/java/org/oppia/android/app/options/LanguageItemViewModel.kt deleted file mode 100644 index 05fabe0cfe2..00000000000 --- a/app/src/main/java/org/oppia/android/app/options/LanguageItemViewModel.kt +++ /dev/null @@ -1,19 +0,0 @@ -package org.oppia.android.app.options - -import androidx.lifecycle.LiveData -import androidx.lifecycle.Transformations -import org.oppia.android.app.viewmodel.ObservableViewModel - -/** - * Language item view model for the recycler view in [AppLanguageFragment] and - * [AudioLanguageFragment]. - */ -class LanguageItemViewModel( - val language: String, - private val selectedLanguage: LiveData, - val languageRadioButtonListener: LanguageRadioButtonListener -) : ObservableViewModel() { - val isLanguageSelected: LiveData by lazy { - Transformations.map(selectedLanguage) { it == language } - } -} diff --git a/app/src/main/java/org/oppia/android/app/options/LanguageRadioButtonListener.kt b/app/src/main/java/org/oppia/android/app/options/LanguageRadioButtonListener.kt deleted file mode 100644 index 5eff5d5e21c..00000000000 --- a/app/src/main/java/org/oppia/android/app/options/LanguageRadioButtonListener.kt +++ /dev/null @@ -1,9 +0,0 @@ -package org.oppia.android.app.options - -/** - * Listener for when the language is selected from the [AppLanguageFragment] or - * [AudioLanguageFragment]. - */ -interface LanguageRadioButtonListener { - fun onLanguageSelected(selectedLanguage: String) -} diff --git a/app/src/main/java/org/oppia/android/app/options/LanguageSelectionViewModel.kt b/app/src/main/java/org/oppia/android/app/options/LanguageSelectionViewModel.kt deleted file mode 100644 index 83ced75d498..00000000000 --- a/app/src/main/java/org/oppia/android/app/options/LanguageSelectionViewModel.kt +++ /dev/null @@ -1,44 +0,0 @@ -package org.oppia.android.app.options - -import androidx.appcompat.app.AppCompatActivity -import androidx.fragment.app.Fragment -import androidx.lifecycle.MutableLiveData -import org.oppia.android.app.fragment.FragmentScope -import org.oppia.android.app.viewmodel.ObservableViewModel -import javax.inject.Inject - -/** - * Language list view model for the recycler view in [AppLanguageFragment] and - * [AudioLanguageFragment]. - */ -@FragmentScope -class LanguageSelectionViewModel @Inject constructor( - val activity: AppCompatActivity, - val fragment: Fragment -) : ObservableViewModel() { - - val selectedLanguage = MutableLiveData() - val languageRadioButtonListener = fragment as LanguageRadioButtonListener - - private val appLanguagesList = listOf( - LanguageItemViewModel("English", selectedLanguage, languageRadioButtonListener), - LanguageItemViewModel("French", selectedLanguage, languageRadioButtonListener), - LanguageItemViewModel("Hindi", selectedLanguage, languageRadioButtonListener), - LanguageItemViewModel("Chinese", selectedLanguage, languageRadioButtonListener) - ) - private val audioLanguagesList = listOf( - LanguageItemViewModel("No Audio", selectedLanguage, languageRadioButtonListener), - LanguageItemViewModel("English", selectedLanguage, languageRadioButtonListener), - LanguageItemViewModel("French", selectedLanguage, languageRadioButtonListener), - LanguageItemViewModel("Hindi", selectedLanguage, languageRadioButtonListener), - LanguageItemViewModel("Chinese", selectedLanguage, languageRadioButtonListener) - ) - - val recyclerViewAudioLanguageList: List by lazy { - audioLanguagesList - } - - val recyclerViewAppLanguageList: List by lazy { - appLanguagesList - } -} diff --git a/app/src/main/java/org/oppia/android/app/options/LoadAudioLanguageListListener.kt b/app/src/main/java/org/oppia/android/app/options/LoadAudioLanguageListListener.kt index 77f1aac8e2c..4d9f1e1f12f 100644 --- a/app/src/main/java/org/oppia/android/app/options/LoadAudioLanguageListListener.kt +++ b/app/src/main/java/org/oppia/android/app/options/LoadAudioLanguageListListener.kt @@ -1,6 +1,12 @@ package org.oppia.android.app.options +import org.oppia.android.app.model.AudioLanguage + /** Listener for when an activity should load a [AudioLanguageFragment]. */ interface LoadAudioLanguageListListener { - fun loadAudioLanguageFragment(audioLanguage: String) + /** + * Called when the user wishes to change their default audio language (where [audioLanguage] is + * the current default language), when the app is in tablet mode. + */ + fun loadAudioLanguageFragment(audioLanguage: AudioLanguage) } 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 9b1b96c564a..264080d7fd9 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 @@ -9,7 +9,6 @@ import androidx.lifecycle.Transformations import androidx.lifecycle.ViewModel import org.oppia.android.app.fragment.FragmentScope import org.oppia.android.app.model.AppLanguage -import org.oppia.android.app.model.AudioLanguage import org.oppia.android.app.model.Profile import org.oppia.android.app.model.ProfileId import org.oppia.android.app.translation.AppLanguageResourceHandler @@ -96,12 +95,13 @@ class OptionControlsViewModel @Inject constructor( val optionAudioViewViewModel = OptionsAudioLanguageViewModel( routeToAudioLanguageListListener, - loadAudioLanguageListListener + loadAudioLanguageListListener, + profile.audioLanguage, + resourceHandler.computeLocalizedDisplayName(profile.audioLanguage) ) optionsReadingTextSizeViewModel.readingTextSize.set(profile.readingTextSize) optionsAppLanguageViewModel.appLanguage.set(getAppLanguage(profile.appLanguage)) - optionAudioViewViewModel.audioLanguage.set(getAudioLanguage(profile.audioLanguage)) itemViewModelList.add(optionsReadingTextSizeViewModel as OptionsItemViewModel) @@ -137,15 +137,4 @@ class OptionControlsViewModel @Inject constructor( else -> "English" } } - - fun getAudioLanguage(audioLanguage: AudioLanguage): String { - return when (audioLanguage) { - AudioLanguage.NO_AUDIO -> "No Audio" - AudioLanguage.ENGLISH_AUDIO_LANGUAGE -> "English" - AudioLanguage.HINDI_AUDIO_LANGUAGE -> "Hindi" - AudioLanguage.FRENCH_AUDIO_LANGUAGE -> "French" - AudioLanguage.CHINESE_AUDIO_LANGUAGE -> "Chinese" - else -> "No Audio" - } - } } diff --git a/app/src/main/java/org/oppia/android/app/options/OptionsActivity.kt b/app/src/main/java/org/oppia/android/app/options/OptionsActivity.kt index 32898ba060d..1e44a74a288 100644 --- a/app/src/main/java/org/oppia/android/app/options/OptionsActivity.kt +++ b/app/src/main/java/org/oppia/android/app/options/OptionsActivity.kt @@ -8,6 +8,8 @@ import org.oppia.android.R import org.oppia.android.app.activity.ActivityComponentImpl import org.oppia.android.app.activity.InjectableAppCompatActivity import org.oppia.android.app.drawer.NAVIGATION_PROFILE_ID_ARGUMENT_KEY +import org.oppia.android.app.model.AudioLanguage +import org.oppia.android.app.model.AudioLanguageActivityResultBundle import org.oppia.android.app.model.ReadingTextSize import org.oppia.android.app.model.ReadingTextSizeActivityResultBundle import org.oppia.android.app.translation.AppLanguageResourceHandler @@ -100,8 +102,10 @@ class OptionsActivity : val appLanguage = data.getStringExtra(MESSAGE_APP_LANGUAGE_ARGUMENT_KEY) as String optionActivityPresenter.updateAppLanguage(appLanguage) } - else -> { - val audioLanguage = data.getStringExtra(MESSAGE_AUDIO_LANGUAGE_ARGUMENT_KEY) as String + REQUEST_CODE_AUDIO_LANGUAGE -> { + val audioLanguage = data.getProtoExtra( + MESSAGE_AUDIO_LANGUAGE_RESULTS_KEY, AudioLanguageActivityResultBundle.getDefaultInstance() + ).audioLanguage optionActivityPresenter.updateAudioLanguage(audioLanguage) } } @@ -118,13 +122,9 @@ class OptionsActivity : ) } - override fun routeAudioLanguageList(audioLanguage: String?) { + override fun routeAudioLanguageList(audioLanguage: AudioLanguage) { startActivityForResult( - AudioLanguageActivity.createAudioLanguageActivityIntent( - this, - AUDIO_LANGUAGE, - audioLanguage - ), + AudioLanguageActivity.createAudioLanguageActivityIntent(this, audioLanguage), REQUEST_CODE_AUDIO_LANGUAGE ) } @@ -152,7 +152,7 @@ class OptionsActivity : optionActivityPresenter.loadAppLanguageFragment(appLanguage) } - override fun loadAudioLanguageFragment(audioLanguage: String) { + override fun loadAudioLanguageFragment(audioLanguage: AudioLanguage) { selectedFragment = AUDIO_LANGUAGE_FRAGMENT optionActivityPresenter.setExtraOptionTitle( resourceHandler.getStringInLocale(R.string.audio_language) diff --git a/app/src/main/java/org/oppia/android/app/options/OptionsActivityPresenter.kt b/app/src/main/java/org/oppia/android/app/options/OptionsActivityPresenter.kt index 6654e2e935e..abe803f31af 100644 --- a/app/src/main/java/org/oppia/android/app/options/OptionsActivityPresenter.kt +++ b/app/src/main/java/org/oppia/android/app/options/OptionsActivityPresenter.kt @@ -9,6 +9,7 @@ import androidx.drawerlayout.widget.DrawerLayout import org.oppia.android.R import org.oppia.android.app.activity.ActivityScope import org.oppia.android.app.drawer.NavigationDrawerFragment +import org.oppia.android.app.model.AudioLanguage import org.oppia.android.app.model.ReadingTextSize import javax.inject.Inject @@ -88,7 +89,7 @@ class OptionsActivityPresenter @Inject constructor( getOptionFragment()?.updateAppLanguage(appLanguage) } - fun updateAudioLanguage(audioLanguage: String) { + fun updateAudioLanguage(audioLanguage: AudioLanguage) { getOptionFragment()?.updateAudioLanguage(audioLanguage) } @@ -96,7 +97,7 @@ class OptionsActivityPresenter @Inject constructor( val readingTextSizeFragment = ReadingTextSizeFragment.newInstance(textSize) activity.supportFragmentManager .beginTransaction() - .add(R.id.multipane_options_container, readingTextSizeFragment) + .replace(R.id.multipane_options_container, readingTextSizeFragment) .commitNow() getOptionFragment()?.setSelectedFragment(READING_TEXT_SIZE_FRAGMENT) } @@ -106,17 +107,16 @@ class OptionsActivityPresenter @Inject constructor( AppLanguageFragment.newInstance(APP_LANGUAGE, appLanguage) activity.supportFragmentManager .beginTransaction() - .add(R.id.multipane_options_container, appLanguageFragment) + .replace(R.id.multipane_options_container, appLanguageFragment) .commitNow() getOptionFragment()?.setSelectedFragment(APP_LANGUAGE_FRAGMENT) } - fun loadAudioLanguageFragment(audioLanguage: String) { - val audioLanguageFragment = - AudioLanguageFragment.newInstance(AUDIO_LANGUAGE, audioLanguage) + fun loadAudioLanguageFragment(audioLanguage: AudioLanguage) { + val audioLanguageFragment = AudioLanguageFragment.newInstance(audioLanguage) activity.supportFragmentManager .beginTransaction() - .add(R.id.multipane_options_container, audioLanguageFragment) + .replace(R.id.multipane_options_container, audioLanguageFragment) .commitNow() getOptionFragment()?.setSelectedFragment(AUDIO_LANGUAGE_FRAGMENT) } diff --git a/app/src/main/java/org/oppia/android/app/options/OptionsAudioLanguageViewModel.kt b/app/src/main/java/org/oppia/android/app/options/OptionsAudioLanguageViewModel.kt index 95ccd0e773d..4cde780e528 100644 --- a/app/src/main/java/org/oppia/android/app/options/OptionsAudioLanguageViewModel.kt +++ b/app/src/main/java/org/oppia/android/app/options/OptionsAudioLanguageViewModel.kt @@ -1,23 +1,23 @@ package org.oppia.android.app.options -import androidx.databinding.ObservableField +import org.oppia.android.app.model.AudioLanguage /** Audio language settings view model for the recycler view in [OptionsFragment]. */ class OptionsAudioLanguageViewModel( private val routeToAudioLanguageListListener: RouteToAudioLanguageListListener, - private val loadAudioLanguageListListener: LoadAudioLanguageListListener + private val loadAudioLanguageListListener: LoadAudioLanguageListListener, + private val audioLanguage: AudioLanguage, + val audioLanguageDisplayName: String ) : OptionsItemViewModel() { - val audioLanguage = ObservableField("") - - fun setAudioLanguage(audioLanguageValue: String) { - audioLanguage.set(audioLanguageValue) - } - + /** + * Handles when the user wishes to change their default audio language and clicks on the button to + * open that configuration screen/pane. + */ fun onAudioLanguageClicked() { if (isMultipane.get()!!) { - loadAudioLanguageListListener.loadAudioLanguageFragment(audioLanguage.get()!!) + loadAudioLanguageListListener.loadAudioLanguageFragment(audioLanguage) } else { - routeToAudioLanguageListListener.routeAudioLanguageList(audioLanguage.get()) + routeToAudioLanguageListListener.routeAudioLanguageList(audioLanguage) } } } diff --git a/app/src/main/java/org/oppia/android/app/options/OptionsFragment.kt b/app/src/main/java/org/oppia/android/app/options/OptionsFragment.kt index a573a79027c..ecf01ac8773 100644 --- a/app/src/main/java/org/oppia/android/app/options/OptionsFragment.kt +++ b/app/src/main/java/org/oppia/android/app/options/OptionsFragment.kt @@ -7,13 +7,14 @@ import android.view.View import android.view.ViewGroup import org.oppia.android.app.fragment.FragmentComponentImpl import org.oppia.android.app.fragment.InjectableFragment +import org.oppia.android.app.model.AudioLanguage import org.oppia.android.app.model.ReadingTextSize import org.oppia.android.util.extensions.getStringFromBundle import javax.inject.Inject const val MESSAGE_READING_TEXT_SIZE_RESULTS_KEY = "OptionsFragment.message_reading_text_size" const val MESSAGE_APP_LANGUAGE_ARGUMENT_KEY = "OptionsFragment.message_app_language" -const val MESSAGE_AUDIO_LANGUAGE_ARGUMENT_KEY = "OptionsFragment.message_audio_language" +const val MESSAGE_AUDIO_LANGUAGE_RESULTS_KEY = "OptionsFragment.message_audio_language" const val REQUEST_CODE_TEXT_SIZE = 1 const val REQUEST_CODE_APP_LANGUAGE = 2 const val REQUEST_CODE_AUDIO_LANGUAGE = 3 @@ -79,7 +80,7 @@ class OptionsFragment : InjectableFragment() { } } - fun updateAudioLanguage(audioLanguage: String) { + fun updateAudioLanguage(audioLanguage: AudioLanguage) { optionsFragmentPresenter.runAfterUIInitialization { optionsFragmentPresenter.updateAudioLanguage(audioLanguage) } diff --git a/app/src/main/java/org/oppia/android/app/options/OptionsFragmentPresenter.kt b/app/src/main/java/org/oppia/android/app/options/OptionsFragmentPresenter.kt index cf019653dd2..86978573d83 100644 --- a/app/src/main/java/org/oppia/android/app/options/OptionsFragmentPresenter.kt +++ b/app/src/main/java/org/oppia/android/app/options/OptionsFragmentPresenter.kt @@ -27,7 +27,6 @@ import java.security.InvalidParameterException import javax.inject.Inject const val APP_LANGUAGE = "APP_LANGUAGE" -const val AUDIO_LANGUAGE = "AUDIO_LANGUAGE" private const val READING_TEXT_SIZE_TAG = "ReadingTextSize" private const val APP_LANGUAGE_TAG = "AppLanguage" private const val AUDIO_LANGUAGE_TAG = "AudioLanguage" @@ -271,87 +270,14 @@ class OptionsFragmentPresenter @Inject constructor( recyclerViewAdapter.notifyItemChanged(1) } - fun updateAudioLanguage(language: String) { - when (language) { - getOptionControlsItemViewModel().getAudioLanguage(AudioLanguage.NO_AUDIO) -> { - profileManagementController.updateAudioLanguage( - profileId, - AudioLanguage.NO_AUDIO - ).toLiveData().observe( - fragment, - Observer { - when (it) { - is AsyncResult.Success -> audioLanguage = AudioLanguage.NO_AUDIO - is AsyncResult.Failure -> - oppiaLogger.e(AUDIO_LANGUAGE_TAG, "$AUDIO_LANGUAGE_ERROR: No Audio", it.error) - is AsyncResult.Pending -> {} // Wait for a result. - } - } - ) - } - getOptionControlsItemViewModel().getAudioLanguage(AudioLanguage.ENGLISH_AUDIO_LANGUAGE) -> { - profileManagementController.updateAudioLanguage( - profileId, - AudioLanguage.ENGLISH_AUDIO_LANGUAGE - ).toLiveData().observe( - fragment, - Observer { - when (it) { - is AsyncResult.Success -> audioLanguage = AudioLanguage.ENGLISH_AUDIO_LANGUAGE - is AsyncResult.Failure -> - oppiaLogger.e(AUDIO_LANGUAGE_TAG, "$AUDIO_LANGUAGE_ERROR: English", it.error) - is AsyncResult.Pending -> {} // Wait for a result. - } - } - ) - } - getOptionControlsItemViewModel().getAudioLanguage(AudioLanguage.HINDI_AUDIO_LANGUAGE) -> { - profileManagementController.updateAudioLanguage( - profileId, - AudioLanguage.HINDI_AUDIO_LANGUAGE - ).toLiveData().observe( - fragment, - Observer { - when (it) { - is AsyncResult.Success -> audioLanguage = AudioLanguage.HINDI_AUDIO_LANGUAGE - is AsyncResult.Failure -> - oppiaLogger.e(AUDIO_LANGUAGE_TAG, "$AUDIO_LANGUAGE_ERROR: Hindi", it.error) - is AsyncResult.Pending -> {} // Wait for a result. - } - } - ) - } - getOptionControlsItemViewModel().getAudioLanguage(AudioLanguage.CHINESE_AUDIO_LANGUAGE) -> { - profileManagementController.updateAudioLanguage( - profileId, - AudioLanguage.CHINESE_AUDIO_LANGUAGE - ).toLiveData().observe( - fragment, - Observer { - when (it) { - is AsyncResult.Success -> audioLanguage = AudioLanguage.CHINESE_AUDIO_LANGUAGE - is AsyncResult.Failure -> - oppiaLogger.e(AUDIO_LANGUAGE_TAG, "$AUDIO_LANGUAGE_ERROR: Chinese", it.error) - is AsyncResult.Pending -> {} // Wait for a result. - } - } - ) - } - getOptionControlsItemViewModel().getAudioLanguage(AudioLanguage.FRENCH_AUDIO_LANGUAGE) -> { - profileManagementController.updateAudioLanguage( - profileId, - AudioLanguage.FRENCH_AUDIO_LANGUAGE - ).toLiveData().observe( - fragment, - Observer { - when (it) { - is AsyncResult.Success -> audioLanguage = AudioLanguage.FRENCH_AUDIO_LANGUAGE - is AsyncResult.Failure -> - oppiaLogger.e(AUDIO_LANGUAGE_TAG, "$AUDIO_LANGUAGE_ERROR: French", it.error) - is AsyncResult.Pending -> {} // Wait for a result. - } - } - ) + fun updateAudioLanguage(language: AudioLanguage) { + val updateLanguageResult = profileManagementController.updateAudioLanguage(profileId, language) + updateLanguageResult.toLiveData().observe(fragment) { + when (it) { + is AsyncResult.Success -> audioLanguage = language + is AsyncResult.Failure -> + oppiaLogger.e(AUDIO_LANGUAGE_TAG, "$AUDIO_LANGUAGE_ERROR: $language", it.error) + is AsyncResult.Pending -> {} // Wait for a result. } } diff --git a/app/src/main/java/org/oppia/android/app/options/RouteToAudioLanguageListListener.kt b/app/src/main/java/org/oppia/android/app/options/RouteToAudioLanguageListListener.kt index 9ccf2ac035d..363fa9588d6 100644 --- a/app/src/main/java/org/oppia/android/app/options/RouteToAudioLanguageListListener.kt +++ b/app/src/main/java/org/oppia/android/app/options/RouteToAudioLanguageListListener.kt @@ -1,6 +1,12 @@ package org.oppia.android.app.options +import org.oppia.android.app.model.AudioLanguage + /** Listener for when an activity should route to a [AudioLanguageActivity]. */ interface RouteToAudioLanguageListListener { - fun routeAudioLanguageList(audioLanguage: String?) + /** + * Called when the user wishes to change their default audio language (where [audioLanguage] is + * the current default language). + */ + fun routeAudioLanguageList(audioLanguage: 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 2de7bc529ad..07c50966429 100755 --- 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 @@ -133,11 +133,12 @@ class AudioFragmentPresenter @Inject constructor( /** Gets language code by [AudioLanguage]. */ private fun getAudioLanguage(audioLanguage: AudioLanguage): String { return when (audioLanguage) { - AudioLanguage.ENGLISH_AUDIO_LANGUAGE -> "en" AudioLanguage.HINDI_AUDIO_LANGUAGE -> "hi" AudioLanguage.FRENCH_AUDIO_LANGUAGE -> "fr" AudioLanguage.CHINESE_AUDIO_LANGUAGE -> "zh" - else -> "en" + AudioLanguage.BRAZILIAN_PORTUGUESE_LANGUAGE -> "pt" + AudioLanguage.NO_AUDIO, AudioLanguage.UNRECOGNIZED, AudioLanguage.AUDIO_LANGUAGE_UNSPECIFIED, + AudioLanguage.ENGLISH_AUDIO_LANGUAGE -> "en" } } 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 dc31fe5ccd2..d659c6997a3 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 @@ -4,7 +4,9 @@ import androidx.annotation.ArrayRes import androidx.annotation.PluralsRes import androidx.annotation.StringRes import androidx.appcompat.app.AppCompatActivity +import org.oppia.android.app.model.AudioLanguage import org.oppia.android.util.locale.OppiaLocale +import java.util.Locale import javax.inject.Inject /** @@ -133,6 +135,30 @@ class AppLanguageResourceHandler @Inject constructor( fun getLayoutDirection(): Int = getDisplayLocale().getLayoutDirection() /** Returns the current [OppiaLocale.DisplayLocale] used for resource processing. */ - fun getDisplayLocale(): OppiaLocale.DisplayLocale = - appLanguageLocaleHandler.getDisplayLocale() + fun getDisplayLocale(): OppiaLocale.DisplayLocale = appLanguageLocaleHandler.getDisplayLocale() + + // TODO(#3793): Remove this once OppiaLanguage is used as the source of truth. + /** + * Returns a human-readable, localized representation of the specified [AudioLanguage]. + * + * Note that the returned string is not expected to be localized to the user's current locale. + * Instead, it will be localized for that specific language (i.e. each language will be + * represented within that language to make it easier to identify when choosing a language). + */ + 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.NO_AUDIO, AudioLanguage.AUDIO_LANGUAGE_UNSPECIFIED, AudioLanguage.UNRECOGNIZED, + AudioLanguage.ENGLISH_AUDIO_LANGUAGE -> getLocalizedDisplayName("en") + } + } + + private fun getLocalizedDisplayName(languageCode: String, regionCode: String = ""): String { + // TODO(#3791): Remove this dependency. + val locale = Locale(languageCode, regionCode) + return locale.getDisplayLanguage(locale).capitalize(locale) + } } diff --git a/app/src/main/res/layout-sw600dp/app_language_fragment.xml b/app/src/main/res/layout-sw600dp/app_language_fragment.xml index db84ed235b7..834fa5d33df 100644 --- a/app/src/main/res/layout-sw600dp/app_language_fragment.xml +++ b/app/src/main/res/layout-sw600dp/app_language_fragment.xml @@ -6,7 +6,7 @@ + type="org.oppia.android.app.options.AppLanguageSelectionViewModel" /> + type="org.oppia.android.app.options.AudioLanguageSelectionViewModel" /> + type="org.oppia.android.app.options.AppLanguageSelectionViewModel" /> + type="org.oppia.android.app.options.AppLanguageItemViewModel" /> diff --git a/app/src/main/res/layout/audio_language_fragment.xml b/app/src/main/res/layout/audio_language_fragment.xml index b6f60c44606..fad1aff4a48 100644 --- a/app/src/main/res/layout/audio_language_fragment.xml +++ b/app/src/main/res/layout/audio_language_fragment.xml @@ -6,7 +6,7 @@ + type="org.oppia.android.app.options.AudioLanguageSelectionViewModel" /> + + + + + + + + + + + + + diff --git a/app/src/main/res/layout/option_audio_language.xml b/app/src/main/res/layout/option_audio_language.xml index 6cba0d235fd..2c679265baa 100644 --- a/app/src/main/res/layout/option_audio_language.xml +++ b/app/src/main/res/layout/option_audio_language.xml @@ -44,7 +44,7 @@ android:layout_width="match_parent" android:layout_height="wrap_content" android:fontFamily="sans-serif" - android:text="@{viewModel.audioLanguage}" + android:text="@{viewModel.audioLanguageDisplayName}" android:textColor="@color/component_color_option_activity_sub_heading_text_color" android:textSize="14sp" app:layout_constraintEnd_toEndOf="parent" diff --git a/app/src/sharedTest/java/org/oppia/android/app/options/AudioLanguageActivityTest.kt b/app/src/sharedTest/java/org/oppia/android/app/options/AudioLanguageActivityTest.kt index f11cac82f24..99ff6404f7f 100644 --- a/app/src/sharedTest/java/org/oppia/android/app/options/AudioLanguageActivityTest.kt +++ b/app/src/sharedTest/java/org/oppia/android/app/options/AudioLanguageActivityTest.kt @@ -2,7 +2,6 @@ package org.oppia.android.app.options import android.app.Application import android.content.Context -import android.content.Intent import androidx.appcompat.app.AppCompatActivity import androidx.test.core.app.ApplicationProvider import androidx.test.ext.junit.runners.AndroidJUnit4 @@ -24,6 +23,8 @@ import org.oppia.android.app.application.ApplicationStartupListenerModule import org.oppia.android.app.application.testing.TestingBuildFlavorModule import org.oppia.android.app.devoptions.DeveloperOptionsModule import org.oppia.android.app.devoptions.DeveloperOptionsStarterModule +import org.oppia.android.app.model.AudioLanguage +import org.oppia.android.app.model.AudioLanguage.ENGLISH_AUDIO_LANGUAGE import org.oppia.android.app.player.state.itemviewmodel.SplitScreenInteractionModule import org.oppia.android.app.shim.ViewBindingShimModule import org.oppia.android.app.topic.PracticeTabModule @@ -91,21 +92,15 @@ import javax.inject.Singleton qualifiers = "port-xxhdpi" ) class AudioLanguageActivityTest { - @get:Rule - val initializeDefaultLocaleRule = InitializeDefaultLocaleRule() - - @get:Rule - val oppiaTestRule = OppiaTestRule() + @get:Rule val initializeDefaultLocaleRule = InitializeDefaultLocaleRule() + @get:Rule val oppiaTestRule = OppiaTestRule() @get:Rule val activityTestRule: ActivityTestRule = ActivityTestRule( AudioLanguageActivity::class.java, /* initialTouchMode= */ true, /* launchActivity= */ false ) - @Inject - lateinit var context: Context - - private val summaryValue = "English" + @Inject lateinit var context: Context @Before fun setUp() { @@ -118,24 +113,17 @@ class AudioLanguageActivityTest { @Test fun testAudioLanguageActivity_hasCorrectActivityLabel() { - activityTestRule.launchActivity( - createDefaultAudioActivityIntent( - summaryValue - ) - ) + activityTestRule.launchActivity(createDefaultAudioActivityIntent(ENGLISH_AUDIO_LANGUAGE)) + val title = activityTestRule.activity.title // Verify that the activity label is correct as a proxy to verify TalkBack will announce the // correct string when it's read out. assertThat(title).isEqualTo(context.getString(R.string.audio_language_activity_title)) } - private fun createDefaultAudioActivityIntent(summaryValue: String): Intent { - return AudioLanguageActivity.createAudioLanguageActivityIntent( - ApplicationProvider.getApplicationContext(), - AUDIO_LANGUAGE, - summaryValue - ) - } + @Suppress("SameParameterValue") + private fun createDefaultAudioActivityIntent(audioLanguage: AudioLanguage) = + AudioLanguageActivity.createAudioLanguageActivityIntent(context, audioLanguage) // TODO(#59): Figure out a way to reuse modules instead of needing to re-declare them. @Singleton 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 c888949ce4d..cb353d2e98b 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 @@ -2,8 +2,8 @@ package org.oppia.android.app.options import android.app.Application import android.content.Context -import android.content.Intent import androidx.appcompat.app.AppCompatActivity +import androidx.test.core.app.ActivityScenario import androidx.test.core.app.ActivityScenario.launch import androidx.test.core.app.ApplicationProvider import androidx.test.espresso.Espresso.onView @@ -11,6 +11,7 @@ import androidx.test.espresso.action.ViewActions.click import androidx.test.espresso.assertion.ViewAssertions.matches import androidx.test.espresso.matcher.ViewMatchers.isChecked import androidx.test.espresso.matcher.ViewMatchers.isRoot +import androidx.test.espresso.matcher.ViewMatchers.withText import androidx.test.ext.junit.runners.AndroidJUnit4 import dagger.Component import dagger.Module @@ -30,6 +31,9 @@ import org.oppia.android.app.application.ApplicationStartupListenerModule import org.oppia.android.app.application.testing.TestingBuildFlavorModule import org.oppia.android.app.devoptions.DeveloperOptionsModule import org.oppia.android.app.devoptions.DeveloperOptionsStarterModule +import org.oppia.android.app.model.AudioLanguage +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.player.state.itemviewmodel.SplitScreenInteractionModule import org.oppia.android.app.recyclerview.RecyclerViewMatcher.Companion.atPositionOnView import org.oppia.android.app.shim.ViewBindingShimModule @@ -94,28 +98,24 @@ import org.robolectric.annotation.LooperMode import javax.inject.Inject import javax.inject.Singleton -private const val ENGLISH = 1 -private const val FRENCH = 2 - /** Tests for [AudioLanguageFragment]. */ +// Function name: test names are conventionally named with underscores. +@Suppress("FunctionName") @RunWith(AndroidJUnit4::class) @LooperMode(LooperMode.Mode.PAUSED) @Config(application = AudioLanguageFragmentTest.TestApplication::class) class AudioLanguageFragmentTest { - @get:Rule - val initializeDefaultLocaleRule = InitializeDefaultLocaleRule() - - @get:Rule - val oppiaTestRule = OppiaTestRule() - - @Inject - lateinit var context: Context + private companion object { + private const val ENGLISH_BUTTON_INDEX = 0 + private const val PORTUGUESE_BUTTON_INDEX = 4 + } - @Inject - lateinit var profileTestHelper: ProfileTestHelper + @get:Rule val initializeDefaultLocaleRule = InitializeDefaultLocaleRule() + @get:Rule val oppiaTestRule = OppiaTestRule() - @Inject - lateinit var testCoroutineDispatchers: TestCoroutineDispatchers + @Inject lateinit var context: Context + @Inject lateinit var profileTestHelper: ProfileTestHelper + @Inject lateinit var testCoroutineDispatchers: TestCoroutineDispatchers @Before fun setUp() { @@ -124,65 +124,103 @@ class AudioLanguageFragmentTest { } @Test - fun testAudioLanguage_selectedLanguageIsEnglish() { - launch(createDefaultAudioActivityIntent("English")).use { - checkSelectedLanguage(ENGLISH) + fun testOpenFragment_withEnglish_selectedLanguageIsEnglish() { + launchActivityWithLanguage(ENGLISH_AUDIO_LANGUAGE).use { + verifyEnglishIsSelected() + } + } + + @Test + fun testOpenFragment_withPortuguese_selectedLanguageIsPortuguese() { + launchActivityWithLanguage(BRAZILIAN_PORTUGUESE_LANGUAGE).use { + verifyPortugueseIsSelected() } } @Test fun testAudioLanguage_configChange_selectedLanguageIsEnglish() { - launch(createDefaultAudioActivityIntent("English")).use { + launchActivityWithLanguage(ENGLISH_AUDIO_LANGUAGE).use { rotateToLandscape() - checkSelectedLanguage(ENGLISH) + + verifyEnglishIsSelected() } } @Test @Config(qualifiers = "sw600dp") fun testAudioLanguage_tabletConfig_selectedLanguageIsEnglish() { - launch(createDefaultAudioActivityIntent("English")).use { + launchActivityWithLanguage(ENGLISH_AUDIO_LANGUAGE).use { testCoroutineDispatchers.runCurrent() - checkSelectedLanguage(ENGLISH) + + verifyEnglishIsSelected() } } @Test - fun testAudioLanguage_changeLanguageToFrench_selectedLanguageIsFrench() { - launch(createDefaultAudioActivityIntent("English")).use { - checkSelectedLanguage(ENGLISH) - selectLanguage(FRENCH) - checkSelectedLanguage(FRENCH) + fun testAudioLanguage_changeLanguageToPortuguese_selectedLanguageIsPortuguese() { + launchActivityWithLanguage(ENGLISH_AUDIO_LANGUAGE).use { + selectPortuguese() + + verifyPortugueseIsSelected() } } @Test - fun testAudioLanguage_changeLanguageToFrench_configChange_selectedLanguageIsFrench() { - launch(createDefaultAudioActivityIntent("English")).use { - checkSelectedLanguage(ENGLISH) - selectLanguage(FRENCH) + fun testAudioLanguage_changeLanguageToPortuguese_configChange_selectedLanguageIsPortuguese() { + launchActivityWithLanguage(ENGLISH_AUDIO_LANGUAGE).use { + selectPortuguese() + rotateToLandscape() - checkSelectedLanguage(FRENCH) + + verifyPortugueseIsSelected() } } @Test @Config(qualifiers = "sw600dp") - fun testAudioLanguage_configChange_changeLanguageToFrench_selectedLanguageIsFrench() { - launch(createDefaultAudioActivityIntent("English")).use { + fun testAudioLanguage_configChange_changeLanguageToPortuguese_selectedLanguageIsPortuguese() { + launchActivityWithLanguage(ENGLISH_AUDIO_LANGUAGE).use { + rotateToLandscape() + + selectPortuguese() + + verifyPortugueseIsSelected() + } + } + + @Test + fun testAudioLanguage_selectPortuguese_thenEnglish_selectedLanguageIsPortuguese() { + launchActivityWithLanguage(ENGLISH_AUDIO_LANGUAGE).use { + selectPortuguese() + + selectEnglish() + + verifyEnglishIsSelected() + } + } + + private fun launchActivityWithLanguage( + audioLanguage: AudioLanguage + ): ActivityScenario { + return launch(createDefaultAudioActivityIntent(audioLanguage)).also { testCoroutineDispatchers.runCurrent() - checkSelectedLanguage(ENGLISH) - selectLanguage(FRENCH) - checkSelectedLanguage(FRENCH) } } - private fun createDefaultAudioActivityIntent(summaryValue: String): Intent { - return AudioLanguageActivity.createAudioLanguageActivityIntent( - ApplicationProvider.getApplicationContext(), - AUDIO_LANGUAGE, - summaryValue - ) + private fun createDefaultAudioActivityIntent(audioLanguage: AudioLanguage) = + AudioLanguageActivity.createAudioLanguageActivityIntent(context, audioLanguage) + + private fun rotateToLandscape() { + onView(isRoot()).perform(orientationLandscape()) + testCoroutineDispatchers.runCurrent() + } + + private fun selectEnglish() { + selectLanguage(ENGLISH_BUTTON_INDEX) + } + + private fun selectPortuguese() { + selectLanguage(PORTUGUESE_BUTTON_INDEX) } private fun selectLanguage(index: Int) { @@ -192,18 +230,19 @@ class AudioLanguageFragmentTest { position = index, targetViewId = R.id.language_radio_button ) - ).perform( - click() - ) + ).perform(click()) testCoroutineDispatchers.runCurrent() } - private fun rotateToLandscape() { - onView(isRoot()).perform(orientationLandscape()) - testCoroutineDispatchers.runCurrent() + private fun verifyEnglishIsSelected() { + verifyLanguageIsSelected(index = ENGLISH_BUTTON_INDEX, expectedLanguageName = "English") } - private fun checkSelectedLanguage(index: Int) { + private fun verifyPortugueseIsSelected() { + verifyLanguageIsSelected(index = PORTUGUESE_BUTTON_INDEX, expectedLanguageName = "Português") + } + + private fun verifyLanguageIsSelected(index: Int, expectedLanguageName: String) { onView( atPositionOnView( R.id.audio_language_recycler_view, @@ -211,7 +250,13 @@ class AudioLanguageFragmentTest { R.id.language_radio_button ) ).check(matches(isChecked())) - testCoroutineDispatchers.runCurrent() + onView( + atPositionOnView( + R.id.audio_language_recycler_view, + index, + R.id.language_text_view + ) + ).check(matches(withText(expectedLanguageName))) } private fun setUpTestApplicationComponent() { diff --git a/app/src/sharedTest/java/org/oppia/android/app/options/OptionsFragmentTest.kt b/app/src/sharedTest/java/org/oppia/android/app/options/OptionsFragmentTest.kt index 3496ad87adb..56815264df9 100644 --- a/app/src/sharedTest/java/org/oppia/android/app/options/OptionsFragmentTest.kt +++ b/app/src/sharedTest/java/org/oppia/android/app/options/OptionsFragmentTest.kt @@ -46,6 +46,8 @@ import org.oppia.android.app.application.ApplicationStartupListenerModule import org.oppia.android.app.application.testing.TestingBuildFlavorModule import org.oppia.android.app.devoptions.DeveloperOptionsModule import org.oppia.android.app.devoptions.DeveloperOptionsStarterModule +import org.oppia.android.app.model.AudioLanguage +import org.oppia.android.app.model.AudioLanguageActivityParams import org.oppia.android.app.model.ReadingTextSize import org.oppia.android.app.model.ReadingTextSizeActivityParams import org.oppia.android.app.player.state.itemviewmodel.SplitScreenInteractionModule @@ -554,16 +556,13 @@ class OptionsFragmentTest { targetViewId = R.id.audio_language_text_view ) ).perform(click()) + + val expectedParams = AudioLanguageActivityParams.newBuilder().apply { + audioLanguage = AudioLanguage.ENGLISH_AUDIO_LANGUAGE + }.build() intended( allOf( - hasExtra( - AudioLanguageActivity.getKeyAudioLanguagePreferenceTitle(), - AUDIO_LANGUAGE - ), - hasExtra( - AudioLanguageActivity.getKeyAudioLanguagePreferenceSummaryValue(), - "English" - ), + hasProtoExtra("AudioLanguageActivity.params", expectedParams), hasComponent(AudioLanguageActivity::class.java.name) ) ) @@ -586,16 +585,13 @@ class OptionsFragmentTest { targetViewId = R.id.audio_language_text_view ) ).perform(click()) + + val expectedParams = AudioLanguageActivityParams.newBuilder().apply { + audioLanguage = AudioLanguage.ENGLISH_AUDIO_LANGUAGE + }.build() intended( allOf( - hasExtra( - AudioLanguageActivity.getKeyAudioLanguagePreferenceSummaryValue(), - "English" - ), - hasExtra( - AudioLanguageActivity.getKeyAudioLanguagePreferenceTitle(), - AUDIO_LANGUAGE - ), + hasProtoExtra("AudioLanguageActivity.params", expectedParams), hasComponent(AudioLanguageActivity::class.java.name) ) ) 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 93a26276a3d..9a030dbf6a9 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 @@ -6,7 +6,6 @@ import android.content.res.Resources import androidx.appcompat.app.AppCompatActivity import androidx.test.core.app.ApplicationProvider import androidx.test.ext.junit.rules.ActivityScenarioRule -import androidx.test.ext.junit.runners.AndroidJUnit4 import com.google.common.truth.Truth.assertThat import dagger.BindsInstance import dagger.Component @@ -26,6 +25,7 @@ import org.oppia.android.app.application.testing.TestingBuildFlavorModule import org.oppia.android.app.devoptions.DeveloperOptionsModule import org.oppia.android.app.devoptions.DeveloperOptionsStarterModule import org.oppia.android.app.model.AppLanguageSelection +import org.oppia.android.app.model.AudioLanguage import org.oppia.android.app.model.OppiaLanguage import org.oppia.android.app.model.ProfileId import org.oppia.android.app.player.state.itemviewmodel.SplitScreenInteractionModule @@ -68,6 +68,12 @@ import org.oppia.android.testing.TestLogReportingModule import org.oppia.android.testing.assertThrows import org.oppia.android.testing.data.DataProviderTestMonitor import org.oppia.android.testing.junit.InitializeDefaultLocaleRule +import org.oppia.android.testing.junit.OppiaParameterizedTestRunner +import org.oppia.android.testing.junit.OppiaParameterizedTestRunner.Iteration +import org.oppia.android.testing.junit.OppiaParameterizedTestRunner.Parameter +import org.oppia.android.testing.junit.OppiaParameterizedTestRunner.RunParameterized +import org.oppia.android.testing.junit.OppiaParameterizedTestRunner.SelectRunnerPlatform +import org.oppia.android.testing.junit.ParameterizedRobolectricTestRunner import org.oppia.android.testing.robolectric.RobolectricModule import org.oppia.android.testing.threading.TestDispatcherModule import org.oppia.android.testing.time.FakeOppiaClockModule @@ -101,12 +107,12 @@ import javax.inject.Singleton // FunctionName: test names are conventionally named with underscores. // SameParameterValue: tests should have specific context included/excluded for readability. @Suppress("FunctionName", "SameParameterValue") -@RunWith(AndroidJUnit4::class) +@RunWith(OppiaParameterizedTestRunner::class) +@SelectRunnerPlatform(ParameterizedRobolectricTestRunner::class) @LooperMode(LooperMode.Mode.PAUSED) @Config(application = AppLanguageResourceHandlerTest.TestApplication::class) class AppLanguageResourceHandlerTest { - @get:Rule - val initializeDefaultLocaleRule = InitializeDefaultLocaleRule() + @get:Rule val initializeDefaultLocaleRule = InitializeDefaultLocaleRule() @get:Rule var activityRule = @@ -114,20 +120,16 @@ class AppLanguageResourceHandlerTest { TestActivity.createIntent(ApplicationProvider.getApplicationContext()) ) - @Inject - lateinit var context: Context - - @Inject - lateinit var wrapperChecker: TestOppiaBidiFormatter.Checker - - @Inject - lateinit var appLanguageLocaleHandler: AppLanguageLocaleHandler + @Inject lateinit var context: Context + @Inject lateinit var wrapperChecker: TestOppiaBidiFormatter.Checker + @Inject lateinit var appLanguageLocaleHandler: AppLanguageLocaleHandler + @Inject lateinit var translationController: TranslationController + @Inject lateinit var monitorFactory: DataProviderTestMonitor.Factory - @Inject - lateinit var translationController: TranslationController + @Parameter lateinit var lang: String + @Parameter lateinit var expectedDisplayText: String - @Inject - lateinit var monitorFactory: DataProviderTestMonitor.Factory + private val audioLanguage by lazy { AudioLanguage.valueOf(lang) } @Before fun setUp() { @@ -475,6 +477,30 @@ class AppLanguageResourceHandlerTest { assertThat(dateString).contains("Apr") } + // This test is breaking the "don't parameterize output" principle out of convenience since it's + // testing functionality that's expected to go away with later language selection work. + // TODO(#3793): Remove this once OppiaLanguage is used as the source of truth. + @Test + @RunParameterized( + 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("unsp", "lang=AUDIO_LANGUAGE_UNSPECIFIED", "expectedDisplayText=English"), + Iteration("none", "lang=NO_AUDIO", "expectedDisplayText=English"), + Iteration("unknown", "lang=UNRECOGNIZED", "expectedDisplayText=English"), + Iteration("en", "lang=ENGLISH_AUDIO_LANGUAGE", "expectedDisplayText=English") + ) + fun testComputeLocalizedDisplayName_englishLocale_forAllLanguages_hasTheExpectedOutput() { + updateAppLanguageTo(OppiaLanguage.ENGLISH) + val handler = retrieveAppLanguageResourceHandler() + + val displayText = handler.computeLocalizedDisplayName(audioLanguage) + + // The display name is localized to that language rather than the current locale (English). + assertThat(displayText).isEqualTo(expectedDisplayText) + } + @Test fun testComputeDateTimeString_forFixedTime_returnsMinHourMonthDayYearParts() { updateAppLanguageTo(OppiaLanguage.ENGLISH) diff --git a/app/src/test/java/org/oppia/android/app/translation/BUILD.bazel b/app/src/test/java/org/oppia/android/app/translation/BUILD.bazel index 46184129002..891a9399d13 100644 --- a/app/src/test/java/org/oppia/android/app/translation/BUILD.bazel +++ b/app/src/test/java/org/oppia/android/app/translation/BUILD.bazel @@ -64,10 +64,11 @@ oppia_android_test( "//testing", "//testing/src/main/java/org/oppia/android/testing/data:data_provider_test_monitor", "//testing/src/main/java/org/oppia/android/testing/junit:initialize_default_locale_rule", + "//testing/src/main/java/org/oppia/android/testing/junit:oppia_parameterized_test_runner", + "//testing/src/main/java/org/oppia/android/testing/junit:parameterized_robolectric_test_runner", "//testing/src/main/java/org/oppia/android/testing/robolectric:test_module", "//testing/src/main/java/org/oppia/android/testing/threading:test_module", "//testing/src/main/java/org/oppia/android/testing/time:test_module", - "//third_party:androidx_test_ext_junit", "//third_party:com_google_truth_truth", "//third_party:junit_junit", "//third_party:org_robolectric_robolectric", diff --git a/model/src/main/proto/arguments.proto b/model/src/main/proto/arguments.proto index 2e07098faf0..c7526995c81 100644 --- a/model/src/main/proto/arguments.proto +++ b/model/src/main/proto/arguments.proto @@ -189,6 +189,36 @@ message ReadingTextSizeFragmentStateBundle { ReadingTextSize selected_reading_text_size = 1; } +// Params required when creating a new AudioLanguageActivity. +message AudioLanguageActivityParams { + // The default audio language previously selected by the user (upon opening the activity). + AudioLanguage audio_language = 1; +} + +// The bundle of properties that are saved upon configuration changes in AudioLanguageActivity. +message AudioLanguageActivityStateBundle { + // The default audio language selected by the user. + AudioLanguage audio_language = 1; +} + +// The bundle of properties that are returned by AudioLanguageActivity after it's finished. +message AudioLanguageActivityResultBundle { + // The new default audio language selected by the user. + AudioLanguage audio_language = 1; +} + +// Arguments required when creating a new AudioLanguageFragment. +message AudioLanguageFragmentArguments { + // The default audio language previously selected by the user (upon opening the fragment). + AudioLanguage audio_language = 1; +} + +// The bundle of properties that are saved upon configuration changes in AudioLanguageFragment. +message AudioLanguageFragmentStateBundle { + // The default audio language selected by the user. + AudioLanguage audio_language = 1; +} + // Activity Parameters needed to open the policy page. message PoliciesActivityParams { // The specific policy page that should be displayed. diff --git a/model/src/main/proto/profile.proto b/model/src/main/proto/profile.proto index 535e1103fb1..35da9d0196d 100644 --- a/model/src/main/proto/profile.proto +++ b/model/src/main/proto/profile.proto @@ -131,4 +131,5 @@ enum AudioLanguage { HINDI_AUDIO_LANGUAGE = 3; FRENCH_AUDIO_LANGUAGE = 4; CHINESE_AUDIO_LANGUAGE = 5; + BRAZILIAN_PORTUGUESE_LANGUAGE = 6; } diff --git a/scripts/assets/file_content_validation_checks.textproto b/scripts/assets/file_content_validation_checks.textproto index 50c3589aa19..463108125ad 100644 --- a/scripts/assets/file_content_validation_checks.textproto +++ b/scripts/assets/file_content_validation_checks.textproto @@ -132,6 +132,7 @@ file_content_checks { file_path_regex: ".+?\\.kt" prohibited_content_regex: "(format|getString|getStringArray|getQuantityString|getQuantityText|toLowerCase|toUpperCase|capitalize|decapitalize|lowercase|uppercase)\\(" failure_message: "String formatting and resource retrieval should go through AppLanguageResourceHandler, OppiaLocale.DisplayLocale, or OppiaLocale.MachineLocale depending on the context (see each class's documentation for details on when each should be used)." + exempted_file_name: "app/src/main/java/org/oppia/android/app/translation/AppLanguageResourceHandler.kt" exempted_file_name: "domain/src/main/java/org/oppia/android/domain/util/JsonExtensions.kt" exempted_file_name: "domain/src/main/java/org/oppia/android/domain/util/WorkDataExtensions.kt" exempted_file_name: "domain/src/test/java/org/oppia/android/domain/onboarding/AppStartupStateControllerTest.kt" @@ -292,6 +293,7 @@ file_content_checks { failure_message: "To use OppiaParameterizedTestRunner, please add an exemption to file_content_validation_checks.textproto and add an explanation for your use case in your PR description. Note that parameterized tests should only be used in special circumstances where a single behavior can be tested across multiple inputs, or for especially large test suites that can be trivially reduced." exempted_file_name: "app/src/sharedTest/java/org/oppia/android/app/customview/interaction/MathExpressionInteractionsViewTest.kt" exempted_file_name: "app/src/sharedTest/java/org/oppia/android/app/splash/SplashActivityTest.kt" + exempted_file_name: "app/src/test/java/org/oppia/android/app/translation/AppLanguageResourceHandlerTest.kt" exempted_file_name: "app/src/test/java/org/oppia/android/app/utility/math/MathExpressionAccessibilityUtilTest.kt" exempted_file_name: "domain/src/test/java/org/oppia/android/domain/classify/rules/algebraicexpressioninput/AlgebraicExpressionInputIsEquivalentToRuleClassifierProviderTest.kt" exempted_file_name: "domain/src/test/java/org/oppia/android/domain/classify/rules/algebraicexpressioninput/AlgebraicExpressionInputMatchesExactlyWithRuleClassifierProviderTest.kt" diff --git a/scripts/assets/kdoc_validity_exemptions.textproto b/scripts/assets/kdoc_validity_exemptions.textproto index 189547e027f..b2200d5aa13 100644 --- a/scripts/assets/kdoc_validity_exemptions.textproto +++ b/scripts/assets/kdoc_validity_exemptions.textproto @@ -125,20 +125,11 @@ exempted_file_path: "app/src/main/java/org/oppia/android/app/options/AppLanguage exempted_file_path: "app/src/main/java/org/oppia/android/app/options/AppLanguageActivityPresenter.kt" exempted_file_path: "app/src/main/java/org/oppia/android/app/options/AppLanguageFragment.kt" exempted_file_path: "app/src/main/java/org/oppia/android/app/options/AppLanguageFragmentPresenter.kt" -exempted_file_path: "app/src/main/java/org/oppia/android/app/options/AudioLanguageActivity.kt" -exempted_file_path: "app/src/main/java/org/oppia/android/app/options/AudioLanguageActivityPresenter.kt" -exempted_file_path: "app/src/main/java/org/oppia/android/app/options/AudioLanguageFragment.kt" -exempted_file_path: "app/src/main/java/org/oppia/android/app/options/AudioLanguageFragmentPresenter.kt" -exempted_file_path: "app/src/main/java/org/oppia/android/app/options/LanguageItemViewModel.kt" -exempted_file_path: "app/src/main/java/org/oppia/android/app/options/LanguageRadioButtonListener.kt" -exempted_file_path: "app/src/main/java/org/oppia/android/app/options/LanguageSelectionViewModel.kt" exempted_file_path: "app/src/main/java/org/oppia/android/app/options/LoadAppLanguageListListener.kt" -exempted_file_path: "app/src/main/java/org/oppia/android/app/options/LoadAudioLanguageListListener.kt" exempted_file_path: "app/src/main/java/org/oppia/android/app/options/OptionControlsViewModel.kt" exempted_file_path: "app/src/main/java/org/oppia/android/app/options/OptionsActivity.kt" exempted_file_path: "app/src/main/java/org/oppia/android/app/options/OptionsActivityPresenter.kt" exempted_file_path: "app/src/main/java/org/oppia/android/app/options/OptionsAppLanguageViewModel.kt" -exempted_file_path: "app/src/main/java/org/oppia/android/app/options/OptionsAudioLanguageViewModel.kt" exempted_file_path: "app/src/main/java/org/oppia/android/app/options/OptionsFragment.kt" exempted_file_path: "app/src/main/java/org/oppia/android/app/options/OptionsFragmentPresenter.kt" exempted_file_path: "app/src/main/java/org/oppia/android/app/options/OptionsItemViewModel.kt" @@ -148,7 +139,6 @@ exempted_file_path: "app/src/main/java/org/oppia/android/app/options/ReadingText exempted_file_path: "app/src/main/java/org/oppia/android/app/options/ReadingTextSizeFragmentPresenter.kt" exempted_file_path: "app/src/main/java/org/oppia/android/app/options/ReadingTextSizeSelectionViewModel.kt" exempted_file_path: "app/src/main/java/org/oppia/android/app/options/RouteToAppLanguageListListener.kt" -exempted_file_path: "app/src/main/java/org/oppia/android/app/options/RouteToAudioLanguageListListener.kt" exempted_file_path: "app/src/main/java/org/oppia/android/app/options/TextSizeItemViewModel.kt" exempted_file_path: "app/src/main/java/org/oppia/android/app/parser/StringToNumberParser.kt" exempted_file_path: "app/src/main/java/org/oppia/android/app/parser/StringToRatioParser.kt" diff --git a/scripts/assets/test_file_exemptions.textproto b/scripts/assets/test_file_exemptions.textproto index 336a15a6ba0..615fd3d789f 100644 --- a/scripts/assets/test_file_exemptions.textproto +++ b/scripts/assets/test_file_exemptions.textproto @@ -241,11 +241,14 @@ exempted_file_path: "app/src/main/java/org/oppia/android/app/ongoingtopiclist/On exempted_file_path: "app/src/main/java/org/oppia/android/app/ongoingtopiclist/OngoingTopicListViewModel.kt" exempted_file_path: "app/src/main/java/org/oppia/android/app/options/AppLanguageActivityPresenter.kt" exempted_file_path: "app/src/main/java/org/oppia/android/app/options/AppLanguageFragmentPresenter.kt" +exempted_file_path: "app/src/main/java/org/oppia/android/app/options/AppLanguageRadioButtonListener.kt" +exempted_file_path: "app/src/main/java/org/oppia/android/app/options/AppLanguageItemViewModel.kt" +exempted_file_path: "app/src/main/java/org/oppia/android/app/options/AppLanguageSelectionViewModel.kt" exempted_file_path: "app/src/main/java/org/oppia/android/app/options/AudioLanguageActivityPresenter.kt" exempted_file_path: "app/src/main/java/org/oppia/android/app/options/AudioLanguageFragmentPresenter.kt" -exempted_file_path: "app/src/main/java/org/oppia/android/app/options/LanguageItemViewModel.kt" -exempted_file_path: "app/src/main/java/org/oppia/android/app/options/LanguageRadioButtonListener.kt" -exempted_file_path: "app/src/main/java/org/oppia/android/app/options/LanguageSelectionViewModel.kt" +exempted_file_path: "app/src/main/java/org/oppia/android/app/options/AudioLanguageItemViewModel.kt" +exempted_file_path: "app/src/main/java/org/oppia/android/app/options/AudioLanguageRadioButtonListener.kt" +exempted_file_path: "app/src/main/java/org/oppia/android/app/options/AudioLanguageSelectionViewModel.kt" exempted_file_path: "app/src/main/java/org/oppia/android/app/options/LoadAppLanguageListListener.kt" exempted_file_path: "app/src/main/java/org/oppia/android/app/options/LoadAudioLanguageListListener.kt" exempted_file_path: "app/src/main/java/org/oppia/android/app/options/LoadReadingTextSizeListener.kt"