Skip to content

Commit

Permalink
Fix #3821: Ensure Portuguese can be a default audio language (#4575)
Browse files Browse the repository at this point in the history
## Explanation
Fixes #3821

This PR introduces the following end-user visible changes:
- It adds Brazilian Portuguese as an option when selecting a default audio language.
- It removes the 'No Audio' option from the list since it's confusing and presently defaults to 'English' when playing audio voiceovers, anyway.
- It changes the display names of the default audio languages to be localized to their own locale to make the languages easier to read by native speakers.
- It fixes a bug in the tablet version of OptionsActivity wherein switching between audio & reading text would result in fragment stacking (i.e. you could see the bottom of the longer fragment when on the shorter one). This entailed changing fragment 'adds' to 'replaces' for the options fragment transactions.

Technical details:
- This PR migrates the audio language pattern from using a string over to using the AudioLanguage proto enum, similar to the recent change for ReadingTextSize. This was preferred since changing everywhere to support a localized name of each language seemed non-ideal.
- A few models & listeners needed to be split between app & audio languages due to the previous approaching having a lot of codesharing between the two that's no longer applicable with strong types.

Exemptions:
- AppLanguageResourceHandler is now allowed to use ``Locale`` directly so that it can produce localized display names for each audio language. In the future, this could be moved to MachineLocale maybe.
- AppLanguageResourceHandlerTest is now allowed to use parameterized tests. The new parameterized tests that were added are technically violating the principle of now parameterizing the expected output of the test, but these are medium-term tests that will go away when the new language selector is added.
- The test file exemptions aren't technically "new" in the sense that they're just preserving the existing exemptions except that the exemptions are now split in the same way as the new app/audio versions of the classes.

## Essential Checklist
- [x] The PR title and explanation each start with "Fix #bugnum: " (If this PR fixes part of an issue, prefix the title with "Fix part of #bugnum: ...".)
- [x] Any changes to [scripts/assets](https://github.com/oppia/oppia-android/tree/develop/scripts/assets) files have their rationale included in the PR explanation.
- [x] The PR follows the [style guide](https://github.com/oppia/oppia-android/wiki/Coding-style-guide).
- [x] The PR does not contain any unnecessary code changes from Android Studio ([reference](https://github.com/oppia/oppia-android/wiki/Guidance-on-submitting-a-PR#undo-unnecessary-changes)).
- [x] The PR is made from a branch that's **not** called "develop" and is up-to-date with "develop".
- [x] The PR is **assigned** to the appropriate reviewers ([reference](https://github.com/oppia/oppia-android/wiki/Guidance-on-submitting-a-PR#clarification-regarding-assignees-and-reviewers-section)).

## For UI-specific PRs only
This section will be filled in post-merge (#4567 is tracking ensuring that the description is finished later).

Commits:

* Add pt-Br as option for default voiceover audio.

This also makes some infrastructural improvements, a small UX
improvement (removing the 'No Audio' option), and fixed a tablet bug in
the options menu.

* Update app/src/main/java/org/oppia/android/app/translation/AppLanguageResourceHandler.kt
  • Loading branch information
BenHenning authored Sep 9, 2022
1 parent 52f7a2c commit 7ff4874
Show file tree
Hide file tree
Showing 43 changed files with 653 additions and 488 deletions.
9 changes: 6 additions & 3 deletions app/BUILD.bazel
Original file line number Diff line number Diff line change
Expand Up @@ -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",
Expand Down Expand Up @@ -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",
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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(
Expand All @@ -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()
}
Expand All @@ -37,16 +37,16 @@ class AppLanguageFragmentPresenter @Inject constructor(
}

fun getLanguageSelected(): String? {
return languageSelectionViewModel.selectedLanguage.value
return appLanguageSelectionViewModel.selectedLanguage.value
}

private fun createRecyclerViewAdapter(): BindableAdapter<LanguageItemViewModel> {
private fun createRecyclerViewAdapter(): BindableAdapter<AppLanguageItemViewModel> {
return BindableAdapter.SingleTypeBuilder
.newBuilder<LanguageItemViewModel>()
.newBuilder<AppLanguageItemViewModel>()
.setLifecycleOwner(fragment)
.registerViewDataBinderWithSameModelType(
inflateDataBinding = LanguageItemsBinding::inflate,
setViewModel = LanguageItemsBinding::setViewModel
inflateDataBinding = AppLanguageItemBinding::inflate,
setViewModel = AppLanguageItemBinding::setViewModel
).build()
}

Expand All @@ -61,7 +61,7 @@ class AppLanguageFragmentPresenter @Inject constructor(
}

fun onLanguageSelected(selectedLanguage: String) {
languageSelectionViewModel.selectedLanguage.value = selectedLanguage
appLanguageSelectionViewModel.selectedLanguage.value = selectedLanguage
updateAppLanguage(selectedLanguage)
}
}
Original file line number Diff line number Diff line change
@@ -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<String>,
val appLanguageRadioButtonListener: AppLanguageRadioButtonListener
) : ObservableViewModel() {
/**
* Indicates whether the language corresponding to this view model is _currently_ selected in the
* radio button list.
*/
val isLanguageSelected: LiveData<Boolean> by lazy {
Transformations.map(currentSelectedLanguage) { it == language }
}
}
Original file line number Diff line number Diff line change
@@ -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)
}
Original file line number Diff line number Diff line change
@@ -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<String>()
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<AppLanguageItemViewModel> by lazy { appLanguagesList }
}
Original file line number Diff line number Diff line change
Expand Up @@ -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()
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -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
}
}
Loading

0 comments on commit 7ff4874

Please sign in to comment.