diff --git a/app/src/main/AndroidManifest.xml b/app/src/main/AndroidManifest.xml
index f036a892fbf..7dfcc70bd01 100644
--- a/app/src/main/AndroidManifest.xml
+++ b/app/src/main/AndroidManifest.xml
@@ -348,6 +348,9 @@
android:name=".app.onboarding.IntroActivity"
android:label="@string/onboarding_learner_intro_activity_title"
android:theme="@style/OppiaThemeWithoutActionBar" />
+
/**
* Returns a newly inflated view to render the fragment with an evaluated audio language as the
@@ -29,9 +47,10 @@ class AudioLanguageFragmentPresenter @Inject constructor(
*/
fun handleCreateView(
inflater: LayoutInflater,
- container: ViewGroup?
+ container: ViewGroup?,
+ profileId: ProfileId,
+ outState: Bundle?
): View {
-
// Hide toolbar as it's not needed in this layout. The toolbar is created by a shared activity
// and is required in OptionsFragment.
activity.findViewById(R.id.reading_list_app_bar_layout).visibility = View.GONE
@@ -41,33 +60,110 @@ class AudioLanguageFragmentPresenter @Inject constructor(
container,
/* attachToRoot= */ false
)
- binding.lifecycleOwner = fragment
+
+ val savedSelectedLanguage = outState?.getProto(
+ FRAGMENT_SAVED_STATE_KEY,
+ AudioLanguageFragmentStateBundle.getDefaultInstance()
+ )?.selectedLanguage
+
+ binding.apply {
+ lifecycleOwner = fragment
+ viewModel = audioLanguageSelectionViewModel
+ }
+
+ audioLanguageSelectionViewModel.updateProfileId(profileId)
+
+ savedSelectedLanguage?.let {
+ if (it != OppiaLanguage.LANGUAGE_UNSPECIFIED) {
+ setSelectedLanguage(it)
+ } else {
+ observePreselectedLanguage()
+ }
+ } ?: observePreselectedLanguage()
binding.audioLanguageText.text = appLanguageResourceHandler.getStringInLocaleWithWrapping(
R.string.audio_language_fragment_text,
appLanguageResourceHandler.getStringInLocale(R.string.app_name)
)
- binding.onboardingNavigationBack.setOnClickListener {
- activity.finish()
- }
+ binding.onboardingNavigationBack.setOnClickListener { activity.finish() }
- val adapter = ArrayAdapter(
- fragment.requireContext(),
- R.layout.onboarding_language_dropdown_item,
- R.id.onboarding_language_text_view,
- audioLanguageSelectionViewModel.availableAudioLanguages
+ audioLanguageSelectionViewModel.supportedOppiaLanguagesLiveData.observe(
+ fragment,
+ { languages ->
+ supportedLanguages = languages
+ val adapter = ArrayAdapter(
+ fragment.requireContext(),
+ R.layout.onboarding_language_dropdown_item,
+ R.id.onboarding_language_text_view,
+ languages.map { appLanguageResourceHandler.computeLocalizedDisplayName(it) }
+ )
+ binding.audioLanguageDropdownList.setAdapter(adapter)
+ }
)
binding.audioLanguageDropdownList.apply {
- setAdapter(adapter)
- setText(
- audioLanguageSelectionViewModel.defaultLanguageSelection,
- false
- )
setRawInputType(EditorInfo.TYPE_NULL)
+
+ onItemClickListener =
+ AdapterView.OnItemClickListener { _, _, position, _ ->
+ val selectedItem = adapter.getItem(position) as? String
+ selectedItem?.let {
+ selectedLanguage = supportedLanguages.associateBy { oppiaLanguage ->
+ appLanguageResourceHandler.computeLocalizedDisplayName(oppiaLanguage)
+ }[it] ?: OppiaLanguage.ENGLISH
+ }
+ }
+ }
+
+ binding.onboardingNavigationContinue.setOnClickListener {
+ updateSelectedAudioLanguage(selectedLanguage, profileId).also {
+ val intent = HomeActivity.createHomeActivity(fragment.requireContext(), profileId)
+ fragment.startActivity(intent)
+ // Finish this activity as well as all activities immediately below it in the current
+ // task so that the user cannot navigate back to the onboarding flow by pressing the
+ // back button once onboarding is complete
+ fragment.activity?.finishAffinity()
+ }
}
return binding.root
}
+
+ private fun observePreselectedLanguage() {
+ audioLanguageSelectionViewModel.languagePreselectionLiveData.observe(
+ fragment,
+ { selectedLanguage -> setSelectedLanguage(selectedLanguage) }
+ )
+ }
+
+ private fun setSelectedLanguage(selectedLanguage: OppiaLanguage) {
+ this.selectedLanguage = selectedLanguage
+ audioLanguageSelectionViewModel.selectedAudioLanguage.set(selectedLanguage)
+ }
+
+ private fun updateSelectedAudioLanguage(selectedLanguage: OppiaLanguage, profileId: ProfileId) {
+ val audioLanguageSelection =
+ AudioTranslationLanguageSelection.newBuilder().setSelectedLanguage(selectedLanguage).build()
+ translationController.updateAudioTranslationContentLanguage(profileId, audioLanguageSelection)
+ .toLiveData().observe(fragment) {
+ when (it) {
+ is AsyncResult.Failure ->
+ oppiaLogger.e(
+ "AudioLanguageFragment",
+ "Failed to set the selected language.",
+ it.error
+ )
+ else -> {} // Do nothing.
+ }
+ }
+ }
+
+ /** Save the current dropdown selection to be retrieved on configuration change. */
+ fun handleSavedState(outState: Bundle) {
+ outState.putProto(
+ FRAGMENT_SAVED_STATE_KEY,
+ AudioLanguageFragmentStateBundle.newBuilder().setSelectedLanguage(selectedLanguage).build()
+ )
+ }
}
diff --git a/app/src/main/java/org/oppia/android/app/onboarding/CreateProfileActivity.kt b/app/src/main/java/org/oppia/android/app/onboarding/CreateProfileActivity.kt
index 7a0fcb956e1..44cb30b2c92 100644
--- a/app/src/main/java/org/oppia/android/app/onboarding/CreateProfileActivity.kt
+++ b/app/src/main/java/org/oppia/android/app/onboarding/CreateProfileActivity.kt
@@ -5,8 +5,11 @@ import android.content.Intent
import android.os.Bundle
import org.oppia.android.app.activity.ActivityComponentImpl
import org.oppia.android.app.activity.InjectableAutoLocalizedAppCompatActivity
+import org.oppia.android.app.model.CreateProfileActivityParams
import org.oppia.android.app.model.ScreenName.CREATE_PROFILE_ACTIVITY
+import org.oppia.android.util.extensions.getProtoExtra
import org.oppia.android.util.logging.CurrentAppScreenNameIntentDecorator.decorateWithScreenName
+import org.oppia.android.util.profile.CurrentUserProfileIdIntentDecorator.extractCurrentUserProfileId
import javax.inject.Inject
/** Activity for displaying a new learner profile creation flow. */
@@ -18,7 +21,13 @@ class CreateProfileActivity : InjectableAutoLocalizedAppCompatActivity() {
super.onCreate(savedInstanceState)
(activityComponent as ActivityComponentImpl).inject(this)
- learnerProfileActivityPresenter.handleOnCreate()
+ val profileId = intent.extractCurrentUserProfileId()
+ val profileType = intent.getProtoExtra(
+ CREATE_PROFILE_PARAMS_KEY,
+ CreateProfileActivityParams.getDefaultInstance()
+ ).profileType
+
+ learnerProfileActivityPresenter.handleOnCreate(profileId, profileType)
}
companion object {
diff --git a/app/src/main/java/org/oppia/android/app/onboarding/CreateProfileActivityPresenter.kt b/app/src/main/java/org/oppia/android/app/onboarding/CreateProfileActivityPresenter.kt
index 2fcba3da31e..86f4d548a49 100644
--- a/app/src/main/java/org/oppia/android/app/onboarding/CreateProfileActivityPresenter.kt
+++ b/app/src/main/java/org/oppia/android/app/onboarding/CreateProfileActivityPresenter.kt
@@ -1,11 +1,20 @@
package org.oppia.android.app.onboarding
+import android.os.Bundle
import androidx.appcompat.app.AppCompatActivity
import androidx.databinding.DataBindingUtil
import org.oppia.android.R
+import org.oppia.android.app.model.CreateProfileFragmentArguments
+import org.oppia.android.app.model.ProfileId
+import org.oppia.android.app.model.ProfileType
import org.oppia.android.databinding.CreateProfileActivityBinding
+import org.oppia.android.util.extensions.putProto
+import org.oppia.android.util.profile.CurrentUserProfileIdIntentDecorator.decorateWithUserProfileId
import javax.inject.Inject
+/** Argument key for [CreateProfileFragment] arguments. */
+const val CREATE_PROFILE_FRAGMENT_ARGS = "CreateProfileFragment.args"
+
private const val TAG_CREATE_PROFILE_ACTIVITY_FRAGMENT = "TAG_CREATE_PROFILE_ACTIVITY_FRAGMENT"
/** Presenter for [CreateProfileActivity]. */
@@ -15,7 +24,7 @@ class CreateProfileActivityPresenter @Inject constructor(
private lateinit var binding: CreateProfileActivityBinding
/** Handle creation and binding of the CreateProfileActivity layout. */
- fun handleOnCreate() {
+ fun handleOnCreate(profileId: ProfileId, profileType: ProfileType) {
binding = DataBindingUtil.setContentView(activity, R.layout.create_profile_activity)
binding.apply {
lifecycleOwner = activity
@@ -23,6 +32,16 @@ class CreateProfileActivityPresenter @Inject constructor(
if (getNewLearnerProfileFragment() == null) {
val createLearnerProfileFragment = CreateProfileFragment()
+
+ val args = Bundle().apply {
+ val fragmentArgs =
+ CreateProfileFragmentArguments.newBuilder().setProfileType(profileType).build()
+ putProto(CREATE_PROFILE_FRAGMENT_ARGS, fragmentArgs)
+ decorateWithUserProfileId(profileId)
+ }
+
+ createLearnerProfileFragment.arguments = args
+
activity.supportFragmentManager.beginTransaction().add(
R.id.profile_fragment_placeholder,
createLearnerProfileFragment,
diff --git a/app/src/main/java/org/oppia/android/app/onboarding/CreateProfileFragment.kt b/app/src/main/java/org/oppia/android/app/onboarding/CreateProfileFragment.kt
index ac09fc5fbd9..7e308004cf1 100644
--- a/app/src/main/java/org/oppia/android/app/onboarding/CreateProfileFragment.kt
+++ b/app/src/main/java/org/oppia/android/app/onboarding/CreateProfileFragment.kt
@@ -9,6 +9,9 @@ import android.view.ViewGroup
import androidx.activity.result.contract.ActivityResultContracts
import org.oppia.android.app.fragment.FragmentComponentImpl
import org.oppia.android.app.fragment.InjectableFragment
+import org.oppia.android.app.model.CreateProfileFragmentArguments
+import org.oppia.android.util.extensions.getProto
+import org.oppia.android.util.profile.CurrentUserProfileIdIntentDecorator.extractCurrentUserProfileId
import javax.inject.Inject
/** Fragment for displaying a new learner profile creation flow. */
@@ -33,6 +36,23 @@ class CreateProfileFragment : InjectableFragment() {
createProfileFragmentPresenter.handleOnActivityResult(result.data)
}
}
- return createProfileFragmentPresenter.handleCreateView(inflater, container)
+
+ val profileId = checkNotNull(arguments?.extractCurrentUserProfileId()) {
+ "Expected CreateProfileFragment to have a profileId argument."
+ }
+ val profileType = checkNotNull(
+ arguments?.getProto(
+ CREATE_PROFILE_FRAGMENT_ARGS, CreateProfileFragmentArguments.getDefaultInstance()
+ )?.profileType
+ ) {
+ "Expected CreateProfileFragment to have a profileType argument."
+ }
+
+ return createProfileFragmentPresenter.handleCreateView(
+ inflater,
+ container,
+ profileId,
+ profileType
+ )
}
}
diff --git a/app/src/main/java/org/oppia/android/app/onboarding/CreateProfileFragmentPresenter.kt b/app/src/main/java/org/oppia/android/app/onboarding/CreateProfileFragmentPresenter.kt
index 10193abe3ec..44c1aad1746 100644
--- a/app/src/main/java/org/oppia/android/app/onboarding/CreateProfileFragmentPresenter.kt
+++ b/app/src/main/java/org/oppia/android/app/onboarding/CreateProfileFragmentPresenter.kt
@@ -2,6 +2,7 @@ package org.oppia.android.app.onboarding
import android.content.Intent
import android.graphics.PorterDuff
+import android.net.Uri
import android.provider.MediaStore
import android.text.Editable
import android.text.TextWatcher
@@ -11,13 +12,24 @@ import android.view.ViewGroup
import android.widget.ImageView
import androidx.activity.result.ActivityResultLauncher
import androidx.appcompat.app.AppCompatActivity
+import androidx.core.content.ContextCompat
import androidx.core.content.res.ResourcesCompat
import androidx.fragment.app.Fragment
import org.oppia.android.R
import org.oppia.android.app.fragment.FragmentScope
+import org.oppia.android.app.model.IntroActivityParams
+import org.oppia.android.app.model.ProfileId
+import org.oppia.android.app.model.ProfileType
+import org.oppia.android.app.translation.AppLanguageResourceHandler
import org.oppia.android.databinding.CreateProfileFragmentBinding
+import org.oppia.android.domain.oppialogger.OppiaLogger
+import org.oppia.android.domain.profile.ProfileManagementController
+import org.oppia.android.util.data.AsyncResult
+import org.oppia.android.util.data.DataProviders.Companion.toLiveData
+import org.oppia.android.util.extensions.putProtoExtra
import org.oppia.android.util.parser.image.ImageLoader
import org.oppia.android.util.parser.image.ImageViewTarget
+import org.oppia.android.util.profile.CurrentUserProfileIdIntentDecorator.decorateWithUserProfileId
import javax.inject.Inject
/** Presenter for [CreateProfileFragment]. */
@@ -25,23 +37,37 @@ import javax.inject.Inject
class CreateProfileFragmentPresenter @Inject constructor(
private val fragment: Fragment,
private val activity: AppCompatActivity,
+ private val imageLoader: ImageLoader,
private val createProfileViewModel: CreateProfileViewModel,
- private val imageLoader: ImageLoader
+ private val profileManagementController: ProfileManagementController,
+ private val oppiaLogger: OppiaLogger,
+ private val appLanguageResourceHandler: AppLanguageResourceHandler
) {
private lateinit var binding: CreateProfileFragmentBinding
private lateinit var uploadImageView: ImageView
private lateinit var selectedImage: String
+ private lateinit var profileId: ProfileId
+ private lateinit var profileType: ProfileType
+ private var selectedImageUri: Uri? = null
/** Launcher for picking an image from device gallery. */
lateinit var activityResultLauncher: ActivityResultLauncher
/** Initialize layout bindings. */
- fun handleCreateView(inflater: LayoutInflater, container: ViewGroup?): View {
+ fun handleCreateView(
+ inflater: LayoutInflater,
+ container: ViewGroup?,
+ profileId: ProfileId,
+ profileType: ProfileType
+ ): View {
binding = CreateProfileFragmentBinding.inflate(
inflater,
container,
/* attachToRoot= */ false
)
+ this.profileId = profileId
+ this.profileType = profileType
+
binding.let {
it.lifecycleOwner = fragment
it.viewModel = createProfileViewModel
@@ -68,11 +94,8 @@ class CreateProfileFragmentPresenter @Inject constructor(
binding.onboardingNavigationContinue.setOnClickListener {
val nickname = binding.createProfileNicknameEdittext.text.toString().trim()
- createProfileViewModel.hasErrorMessage.set(nickname.isBlank())
-
- if (createProfileViewModel.hasErrorMessage.get() != true) {
- val intent = IntroActivity.createIntroActivity(activity, nickname)
- fragment.startActivity(intent)
+ if (!checkNicknameAndUpdateError(nickname)) {
+ updateProfileDetails(nickname)
}
}
@@ -89,10 +112,22 @@ class CreateProfileFragmentPresenter @Inject constructor(
return binding.root
}
+ private fun checkNicknameAndUpdateError(nickname: String): Boolean {
+ val hasError = nickname.isBlank()
+ createProfileViewModel.hasErrorMessage.set(hasError)
+ createProfileViewModel.errorMessage.set(
+ appLanguageResourceHandler.getStringInLocale(
+ R.string.create_profile_activity_nickname_error
+ )
+ )
+ return hasError
+ }
+
/** Receive the result of image upload and load it into the image view. */
fun handleOnActivityResult(intent: Intent?) {
intent?.let {
binding.createProfilePicturePrompt.visibility = View.GONE
+ selectedImageUri = intent.data
selectedImage =
checkNotNull(intent.data.toString()) { "Could not find the selected image." }
imageLoader.loadBitmap(
@@ -107,19 +142,108 @@ class CreateProfileFragmentPresenter @Inject constructor(
binding.onboardingNavigationBack.setOnClickListener { activity.finish() }
binding.createProfileEditPictureIcon.setOnClickListener {
- activityResultLauncher.launch(
- galleryIntent
- )
+ activityResultLauncher.launch(galleryIntent)
}
binding.createProfilePicturePrompt.setOnClickListener {
- activityResultLauncher.launch(
- galleryIntent
- )
+ activityResultLauncher.launch(galleryIntent)
}
binding.createProfileUserImageView.setOnClickListener {
- activityResultLauncher.launch(
- galleryIntent
- )
+ activityResultLauncher.launch(galleryIntent)
}
}
+
+ private fun updateProfileDetails(profileName: String) {
+ profileManagementController.updateNewProfileDetails(
+ profileId = profileId,
+ profileType = profileType,
+ avatarImagePath = selectedImageUri,
+ colorRgb = selectUniqueRandomColor(),
+ newName = profileName,
+ isAdmin = true
+ ).toLiveData().observe(
+ fragment,
+ { result ->
+ when (result) {
+ is AsyncResult.Success -> {
+ createProfileViewModel.hasErrorMessage.set(false)
+
+ val params = IntroActivityParams.newBuilder()
+ .setProfileNickname(profileName)
+ .build()
+
+ val intent =
+ IntroActivity.createIntroActivity(activity).apply {
+ putProtoExtra(IntroActivity.PARAMS_KEY, params)
+ decorateWithUserProfileId(profileId)
+ }
+
+ fragment.startActivity(intent)
+ }
+ is AsyncResult.Failure -> {
+ createProfileViewModel.hasErrorMessage.set(true)
+
+ val errorMessage = when (result.error) {
+ is ProfileManagementController.ProfileNameOnlyLettersException ->
+ appLanguageResourceHandler.getStringInLocale(
+ R.string.add_profile_error_name_only_letters
+ )
+ is ProfileManagementController.UnknownProfileTypeException ->
+ appLanguageResourceHandler.getStringInLocale(
+ R.string.add_profile_error_missing_profile_type
+ )
+ else -> {
+ appLanguageResourceHandler.getStringInLocale(
+ R.string.add_profile_default_error_message
+ )
+ }
+ }
+
+ createProfileViewModel.errorMessage.set(errorMessage)
+
+ oppiaLogger.e(
+ "CreateProfileFragment",
+ "Failed to update profile details.",
+ result.error
+ )
+ }
+ is AsyncResult.Pending -> {}
+ }
+ }
+ )
+ }
+
+ /** Randomly selects a color for the new profile that is not already in use. */
+ private fun selectUniqueRandomColor(): Int {
+ return ContextCompat.getColor(fragment.requireContext(), COLORS_LIST.random())
+ }
+
+ private companion object {
+ private val COLORS_LIST = listOf(
+ R.color.component_color_avatar_background_1_color,
+ R.color.component_color_avatar_background_2_color,
+ R.color.component_color_avatar_background_3_color,
+ R.color.component_color_avatar_background_4_color,
+ R.color.component_color_avatar_background_5_color,
+ R.color.component_color_avatar_background_6_color,
+ R.color.component_color_avatar_background_7_color,
+ R.color.component_color_avatar_background_8_color,
+ R.color.component_color_avatar_background_9_color,
+ R.color.component_color_avatar_background_10_color,
+ R.color.component_color_avatar_background_11_color,
+ R.color.component_color_avatar_background_12_color,
+ R.color.component_color_avatar_background_13_color,
+ R.color.component_color_avatar_background_14_color,
+ R.color.component_color_avatar_background_15_color,
+ R.color.component_color_avatar_background_16_color,
+ R.color.component_color_avatar_background_17_color,
+ R.color.component_color_avatar_background_18_color,
+ R.color.component_color_avatar_background_19_color,
+ R.color.component_color_avatar_background_20_color,
+ R.color.component_color_avatar_background_21_color,
+ R.color.component_color_avatar_background_22_color,
+ R.color.component_color_avatar_background_23_color,
+ R.color.component_color_avatar_background_24_color,
+ R.color.component_color_avatar_background_25_color
+ )
+ }
}
diff --git a/app/src/main/java/org/oppia/android/app/onboarding/CreateProfileViewModel.kt b/app/src/main/java/org/oppia/android/app/onboarding/CreateProfileViewModel.kt
index e6ef763f23c..fa5deceb2da 100644
--- a/app/src/main/java/org/oppia/android/app/onboarding/CreateProfileViewModel.kt
+++ b/app/src/main/java/org/oppia/android/app/onboarding/CreateProfileViewModel.kt
@@ -9,6 +9,9 @@ import javax.inject.Inject
@FragmentScope
class CreateProfileViewModel @Inject constructor() : ObservableViewModel() {
- /** ObservableField that tracks whether creating a nickname has triggered an error condition. */
+ /** [ObservableField] that tracks whether creating a profile has triggered an error condition. */
val hasErrorMessage = ObservableField(false)
+
+ /** [ObservableField] that tracks the error message to be displayed to the user. */
+ val errorMessage = ObservableField("")
}
diff --git a/app/src/main/java/org/oppia/android/app/onboarding/IntroActivity.kt b/app/src/main/java/org/oppia/android/app/onboarding/IntroActivity.kt
index 9ca2991707d..17daf8c3ec4 100644
--- a/app/src/main/java/org/oppia/android/app/onboarding/IntroActivity.kt
+++ b/app/src/main/java/org/oppia/android/app/onboarding/IntroActivity.kt
@@ -8,8 +8,8 @@ import org.oppia.android.app.activity.InjectableAutoLocalizedAppCompatActivity
import org.oppia.android.app.model.IntroActivityParams
import org.oppia.android.app.model.ScreenName.INTRO_ACTIVITY
import org.oppia.android.util.extensions.getProtoExtra
-import org.oppia.android.util.extensions.putProtoExtra
import org.oppia.android.util.logging.CurrentAppScreenNameIntentDecorator.decorateWithScreenName
+import org.oppia.android.util.profile.CurrentUserProfileIdIntentDecorator.extractCurrentUserProfileId
import javax.inject.Inject
/** The activity for showing the learner welcome screen. */
@@ -17,43 +17,30 @@ class IntroActivity : InjectableAutoLocalizedAppCompatActivity() {
@Inject
lateinit var onboardingLearnerIntroActivityPresenter: IntroActivityPresenter
- private lateinit var profileNickname: String
-
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
(activityComponent as ActivityComponentImpl).inject(this)
- val params = intent.extractParams()
- this.profileNickname = params.profileNickname
+ val profileNickname =
+ intent.getProtoExtra(PARAMS_KEY, IntroActivityParams.getDefaultInstance()).profileNickname
+
+ val profileId = intent.extractCurrentUserProfileId()
- onboardingLearnerIntroActivityPresenter.handleOnCreate(profileNickname)
+ onboardingLearnerIntroActivityPresenter.handleOnCreate(profileNickname, profileId)
}
companion object {
- private const val PARAMS_KEY = "OnboardingIntroActivity.params"
+ /** Argument key for [IntroActivity]'s intent parameters. */
+ const val PARAMS_KEY = "OnboardingIntroActivity.params"
/**
* A convenience function for creating a new [OnboardingLearnerIntroActivity] intent by prefilling
* common params needed by the activity.
*/
- fun createIntroActivity(context: Context, profileNickname: String): Intent {
- val params = IntroActivityParams.newBuilder()
- .setProfileNickname(profileNickname)
- .build()
- return createOnboardingLearnerIntroActivity(context, params)
- }
-
- private fun createOnboardingLearnerIntroActivity(
- context: Context,
- params: IntroActivityParams
- ): Intent {
+ fun createIntroActivity(context: Context): Intent {
return Intent(context, IntroActivity::class.java).apply {
- putProtoExtra(PARAMS_KEY, params)
decorateWithScreenName(INTRO_ACTIVITY)
}
}
-
- private fun Intent.extractParams() =
- getProtoExtra(PARAMS_KEY, IntroActivityParams.getDefaultInstance())
}
}
diff --git a/app/src/main/java/org/oppia/android/app/onboarding/IntroActivityPresenter.kt b/app/src/main/java/org/oppia/android/app/onboarding/IntroActivityPresenter.kt
index 7615fbc1c75..52bd6058eb3 100644
--- a/app/src/main/java/org/oppia/android/app/onboarding/IntroActivityPresenter.kt
+++ b/app/src/main/java/org/oppia/android/app/onboarding/IntroActivityPresenter.kt
@@ -5,13 +5,17 @@ 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.IntroFragmentArguments
+import org.oppia.android.app.model.ProfileId
import org.oppia.android.databinding.IntroActivityBinding
+import org.oppia.android.util.extensions.putProto
+import org.oppia.android.util.profile.CurrentUserProfileIdIntentDecorator.decorateWithUserProfileId
import javax.inject.Inject
private const val TAG_LEARNER_INTRO_FRAGMENT = "TAG_INTRO_FRAGMENT"
-/** Argument key for bundling the profileId. */
-const val PROFILE_NICKNAME_ARGUMENT_KEY = "profile_nickname"
+/** Argument key for bundling the profile nickname. */
+const val PROFILE_NICKNAME_ARGUMENT_KEY = "IntroFragment.Arguments"
/** The Presenter for [IntroActivity]. */
@ActivityScope
@@ -21,15 +25,21 @@ class IntroActivityPresenter @Inject constructor(
private lateinit var binding: IntroActivityBinding
/** Handle creation and binding of the [IntroActivity] layout. */
- fun handleOnCreate(profileNickname: String) {
+ fun handleOnCreate(profileNickname: String, profileId: ProfileId) {
binding = DataBindingUtil.setContentView(activity, R.layout.intro_activity)
binding.lifecycleOwner = activity
if (getIntroFragment() == null) {
val introFragment = IntroFragment()
- val args = Bundle()
- args.putString(PROFILE_NICKNAME_ARGUMENT_KEY, profileNickname)
+ val argumentsProto =
+ IntroFragmentArguments.newBuilder().setProfileNickname(profileNickname).build()
+
+ val args = Bundle().apply {
+ decorateWithUserProfileId(profileId)
+ putProto(PROFILE_NICKNAME_ARGUMENT_KEY, argumentsProto)
+ }
+
introFragment.arguments = args
activity.supportFragmentManager.beginTransaction().add(
diff --git a/app/src/main/java/org/oppia/android/app/onboarding/IntroFragment.kt b/app/src/main/java/org/oppia/android/app/onboarding/IntroFragment.kt
index 0c954d2df85..6c3e40bc529 100644
--- a/app/src/main/java/org/oppia/android/app/onboarding/IntroFragment.kt
+++ b/app/src/main/java/org/oppia/android/app/onboarding/IntroFragment.kt
@@ -7,7 +7,9 @@ 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.IntroFragmentArguments
+import org.oppia.android.util.extensions.getProto
+import org.oppia.android.util.profile.CurrentUserProfileIdIntentDecorator.extractCurrentUserProfileId
import javax.inject.Inject
/** Fragment that contains the introduction message for new learners. */
@@ -26,13 +28,25 @@ class IntroFragment : InjectableFragment() {
savedInstanceState: Bundle?
): View? {
val profileNickname =
- checkNotNull(arguments?.getStringFromBundle(PROFILE_NICKNAME_ARGUMENT_KEY)) {
+ checkNotNull(
+ arguments?.getProto(
+ PROFILE_NICKNAME_ARGUMENT_KEY,
+ IntroFragmentArguments.getDefaultInstance()
+ )
+ ) {
"Expected profileNickname to be included in the arguments for IntroFragment."
+ }.profileNickname
+
+ val profileId =
+ checkNotNull(arguments?.extractCurrentUserProfileId()) {
+ "Expected profileId to be included in the arguments for IntroFragment."
}
+
return introFragmentPresenter.handleCreateView(
inflater,
container,
- profileNickname
+ profileNickname,
+ profileId
)
}
}
diff --git a/app/src/main/java/org/oppia/android/app/onboarding/IntroFragmentPresenter.kt b/app/src/main/java/org/oppia/android/app/onboarding/IntroFragmentPresenter.kt
index 50fa51300c7..ac7739d5ad3 100644
--- a/app/src/main/java/org/oppia/android/app/onboarding/IntroFragmentPresenter.kt
+++ b/app/src/main/java/org/oppia/android/app/onboarding/IntroFragmentPresenter.kt
@@ -7,9 +7,11 @@ import androidx.appcompat.app.AppCompatActivity
import androidx.fragment.app.Fragment
import org.oppia.android.R
import org.oppia.android.app.model.AudioLanguage
+import org.oppia.android.app.model.ProfileId
import org.oppia.android.app.options.AudioLanguageActivity
import org.oppia.android.app.translation.AppLanguageResourceHandler
import org.oppia.android.databinding.LearnerIntroFragmentBinding
+import org.oppia.android.util.profile.CurrentUserProfileIdIntentDecorator.decorateWithUserProfileId
import javax.inject.Inject
/** The presenter for [IntroFragment]. */
@@ -25,6 +27,7 @@ class IntroFragmentPresenter @Inject constructor(
inflater: LayoutInflater,
container: ViewGroup?,
profileNickname: String,
+ profileId: ProfileId
): View {
binding = LearnerIntroFragmentBinding.inflate(
inflater,
@@ -51,6 +54,7 @@ class IntroFragmentPresenter @Inject constructor(
fragment.requireContext(),
AudioLanguage.ENGLISH_AUDIO_LANGUAGE
)
+ intent.decorateWithUserProfileId(profileId)
fragment.startActivity(intent)
}
diff --git a/app/src/main/java/org/oppia/android/app/onboarding/OnboardingAppLanguageViewModel.kt b/app/src/main/java/org/oppia/android/app/onboarding/OnboardingAppLanguageViewModel.kt
new file mode 100644
index 00000000000..d792861aab3
--- /dev/null
+++ b/app/src/main/java/org/oppia/android/app/onboarding/OnboardingAppLanguageViewModel.kt
@@ -0,0 +1,28 @@
+package org.oppia.android.app.onboarding
+
+import androidx.lifecycle.LiveData
+import androidx.lifecycle.MutableLiveData
+import org.oppia.android.app.model.OppiaLanguage
+import org.oppia.android.app.viewmodel.ObservableViewModel
+import javax.inject.Inject
+
+/** ViewModel for managing language selection in [OnboardingFragment]. */
+class OnboardingAppLanguageViewModel @Inject constructor() : ObservableViewModel() {
+ /** The selected app language displayed in the language dropdown. */
+ val languageSelectionLiveData: LiveData get() = _languageSelectionLiveData
+ private val _languageSelectionLiveData = MutableLiveData()
+
+ /** Get the list of app supported languages to be displayed in the language dropdown. */
+ val supportedAppLanguagesList: LiveData> get() = _supportedAppLanguagesList
+ private val _supportedAppLanguagesList = MutableLiveData>()
+
+ /** Sets the app language selection. */
+ fun setSelectedLanguageLivedata(language: OppiaLanguage) {
+ _languageSelectionLiveData.value = language
+ }
+
+ /** Sets the list of app supported languages to be displayed in the language dropdown. */
+ fun setSupportedAppLanguages(languageList: List) {
+ _supportedAppLanguagesList.value = languageList
+ }
+}
diff --git a/app/src/main/java/org/oppia/android/app/onboarding/OnboardingFragment.kt b/app/src/main/java/org/oppia/android/app/onboarding/OnboardingFragment.kt
index 677a4a08515..5c207579761 100644
--- a/app/src/main/java/org/oppia/android/app/onboarding/OnboardingFragment.kt
+++ b/app/src/main/java/org/oppia/android/app/onboarding/OnboardingFragment.kt
@@ -34,9 +34,16 @@ class OnboardingFragment : InjectableFragment() {
savedInstanceState: Bundle?
): View? {
return if (enableOnboardingFlowV2.value) {
- onboardingFragmentPresenter.handleCreateView(inflater, container)
+ onboardingFragmentPresenter.handleCreateView(inflater, container, savedInstanceState)
} else {
onboardingFragmentPresenterV1.handleCreateView(inflater, container)
}
}
+
+ override fun onSaveInstanceState(outState: Bundle) {
+ super.onSaveInstanceState(outState)
+ if (enableOnboardingFlowV2.value) {
+ onboardingFragmentPresenter.saveToSavedInstanceState(outState)
+ }
+ }
}
diff --git a/app/src/main/java/org/oppia/android/app/onboarding/OnboardingFragmentPresenter.kt b/app/src/main/java/org/oppia/android/app/onboarding/OnboardingFragmentPresenter.kt
index 79bd8dc270c..332fd930117 100644
--- a/app/src/main/java/org/oppia/android/app/onboarding/OnboardingFragmentPresenter.kt
+++ b/app/src/main/java/org/oppia/android/app/onboarding/OnboardingFragmentPresenter.kt
@@ -1,33 +1,78 @@
package org.oppia.android.app.onboarding
+import android.os.Bundle
import android.view.LayoutInflater
import android.view.View
import android.view.ViewGroup
+import android.view.inputmethod.EditorInfo
+import android.widget.AdapterView
+import android.widget.ArrayAdapter
import androidx.appcompat.app.AppCompatActivity
import androidx.fragment.app.Fragment
+import androidx.lifecycle.LiveData
+import androidx.lifecycle.Transformations
import org.oppia.android.R
import org.oppia.android.app.fragment.FragmentScope
+import org.oppia.android.app.model.AppLanguageSelection
+import org.oppia.android.app.model.OnboardingFragmentStateBundle
+import org.oppia.android.app.model.OppiaLanguage
+import org.oppia.android.app.model.Profile
+import org.oppia.android.app.model.ProfileId
import org.oppia.android.app.translation.AppLanguageResourceHandler
import org.oppia.android.databinding.OnboardingAppLanguageSelectionFragmentBinding
+import org.oppia.android.domain.oppialogger.OppiaLogger
+import org.oppia.android.domain.profile.ProfileManagementController
+import org.oppia.android.domain.translation.TranslationController
+import org.oppia.android.util.data.AsyncResult
+import org.oppia.android.util.data.DataProviders.Companion.toLiveData
+import org.oppia.android.util.extensions.getProto
+import org.oppia.android.util.extensions.putProto
+import org.oppia.android.util.locale.OppiaLocale
+import org.oppia.android.util.profile.CurrentUserProfileIdIntentDecorator.decorateWithUserProfileId
import javax.inject.Inject
+private const val ONBOARDING_FRAGMENT_SAVED_STATE_KEY = "OnboardingFragment.saved_state"
+
/** The presenter for [OnboardingFragment]. */
@FragmentScope
class OnboardingFragmentPresenter @Inject constructor(
private val activity: AppCompatActivity,
private val fragment: Fragment,
- private val appLanguageResourceHandler: AppLanguageResourceHandler
+ private val appLanguageResourceHandler: AppLanguageResourceHandler,
+ private val profileManagementController: ProfileManagementController,
+ private val oppiaLogger: OppiaLogger,
+ private val translationController: TranslationController,
+ private val onboardingAppLanguageViewModel: OnboardingAppLanguageViewModel
) {
private lateinit var binding: OnboardingAppLanguageSelectionFragmentBinding
+ private var profileId: ProfileId = ProfileId.getDefaultInstance()
+ private lateinit var selectedLanguage: OppiaLanguage
+ private lateinit var supportedLanguages: List
/** Handle creation and binding of the [OnboardingFragment] layout. */
- fun handleCreateView(inflater: LayoutInflater, container: ViewGroup?): View {
+ fun handleCreateView(inflater: LayoutInflater, container: ViewGroup?, outState: Bundle?): View {
binding = OnboardingAppLanguageSelectionFragmentBinding.inflate(
inflater,
container,
/* attachToRoot= */ false
)
+ val savedSelectedLanguage = outState?.getProto(
+ ONBOARDING_FRAGMENT_SAVED_STATE_KEY,
+ OnboardingFragmentStateBundle.getDefaultInstance()
+ )?.selectedLanguage
+
+ if (savedSelectedLanguage != null) {
+ selectedLanguage = savedSelectedLanguage
+ onboardingAppLanguageViewModel.setSelectedLanguageLivedata(savedSelectedLanguage)
+ } else {
+ initializeSelectedLanguageToSystemLanguage()
+ }
+
+ retrieveSupportedLanguages()
+
+ subscribeToGetProfileList()
+
binding.apply {
lifecycleOwner = fragment
@@ -36,13 +81,198 @@ class OnboardingFragmentPresenter @Inject constructor(
appLanguageResourceHandler.getStringInLocale(R.string.app_name)
)
+ onboardingAppLanguageViewModel.supportedAppLanguagesList.observe(
+ fragment,
+ { languagesList ->
+ supportedLanguages = languagesList
+ val adapter = ArrayAdapter(
+ fragment.requireContext(),
+ R.layout.onboarding_language_dropdown_item,
+ R.id.onboarding_language_text_view,
+ languagesList.map { appLanguageResourceHandler.computeLocalizedDisplayName(it) }
+ )
+ onboardingLanguageDropdown.setAdapter(adapter)
+ }
+ )
+
+ onboardingAppLanguageViewModel.languageSelectionLiveData.observe(
+ fragment,
+ { language ->
+ selectedLanguage = language
+ onboardingLanguageDropdown.setText(
+ appLanguageResourceHandler.computeLocalizedDisplayName(
+ language
+ ),
+ false
+ )
+ }
+ )
+
+ onboardingLanguageDropdown.apply {
+ setRawInputType(EditorInfo.TYPE_NULL)
+
+ onItemClickListener =
+ AdapterView.OnItemClickListener { _, _, position, _ ->
+ adapter.getItem(position).let { selectedItem ->
+ selectedItem?.let {
+ selectedLanguage = supportedLanguages.associateBy { oppiaLanguage ->
+ appLanguageResourceHandler.computeLocalizedDisplayName(oppiaLanguage)
+ }[it] ?: OppiaLanguage.ENGLISH
+ onboardingAppLanguageViewModel.setSelectedLanguageLivedata(selectedLanguage)
+ }
+ }
+ }
+ }
+
onboardingLanguageLetsGoButton.setOnClickListener {
- val intent =
- OnboardingProfileTypeActivity.createOnboardingProfileTypeActivityIntent(activity)
- fragment.startActivity(intent)
+ updateSelectedLanguage(selectedLanguage).also {
+ val intent =
+ OnboardingProfileTypeActivity.createOnboardingProfileTypeActivityIntent(activity)
+ intent.decorateWithUserProfileId(profileId)
+ fragment.startActivity(intent)
+ }
}
}
return binding.root
}
+
+ private val existingProfiles: LiveData> by lazy {
+ Transformations.map(
+ profileManagementController.getProfiles().toLiveData(),
+ ::processGetProfilesResult
+ )
+ }
+
+ /** Save the current dropdown selection to be retrieved on configuration change. */
+ fun saveToSavedInstanceState(outState: Bundle) {
+ outState.putProto(
+ ONBOARDING_FRAGMENT_SAVED_STATE_KEY,
+ OnboardingFragmentStateBundle.newBuilder().setSelectedLanguage(selectedLanguage).build()
+ )
+ }
+
+ private fun updateSelectedLanguage(selectedLanguage: OppiaLanguage) {
+ val selection = AppLanguageSelection.newBuilder().setSelectedLanguage(selectedLanguage).build()
+ translationController.updateAppLanguage(profileId, selection).toLiveData()
+ .observe(
+ fragment,
+ { result ->
+ when (result) {
+ is AsyncResult.Failure -> oppiaLogger.e(
+ "OnboardingFragment",
+ "Failed to set AppLanguageSelection",
+ result.error
+ )
+ else -> {} // Do nothing. The user should be able to progress regardless of the result.
+ }
+ }
+ )
+ }
+
+ private fun initializeSelectedLanguageToSystemLanguage() {
+ translationController.getSystemLanguageLocale().toLiveData().observe(
+ fragment,
+ { result ->
+ onboardingAppLanguageViewModel.setSelectedLanguageLivedata(
+ processSystemLanguageResult(result)
+ )
+ }
+ )
+ }
+
+ private fun processSystemLanguageResult(
+ result: AsyncResult
+ ): OppiaLanguage {
+ return when (result) {
+ is AsyncResult.Success -> {
+ result.value.getCurrentLanguage()
+ }
+ is AsyncResult.Failure -> {
+ oppiaLogger.e(
+ "OnboardingFragment",
+ "Failed to retrieve system language locale.",
+ result.error
+ )
+ OppiaLanguage.ENGLISH
+ }
+ is AsyncResult.Pending -> OppiaLanguage.ENGLISH
+ }
+ }
+
+ private fun retrieveSupportedLanguages() {
+ translationController.getSupportedAppLanguages().toLiveData().observe(
+ fragment,
+ { result ->
+ when (result) {
+ is AsyncResult.Success -> {
+ onboardingAppLanguageViewModel.setSupportedAppLanguages(result.value)
+ }
+ is AsyncResult.Failure -> {
+ oppiaLogger.e(
+ "OnboardingFragment",
+ "Failed to retrieve supported language list.",
+ result.error
+ )
+ }
+ is AsyncResult.Pending -> {}
+ }
+ }
+ )
+ }
+
+ private fun subscribeToGetProfileList() {
+ existingProfiles.observe(
+ fragment,
+ { profilesList ->
+ if (!profilesList.isNullOrEmpty()) {
+ profileId = profilesList.first().id
+ } else {
+ createDefaultProfile()
+ }
+ }
+ )
+ }
+
+ private fun processGetProfilesResult(profilesResult: AsyncResult>): List {
+ val profileList = when (profilesResult) {
+ is AsyncResult.Failure -> {
+ oppiaLogger.e(
+ "OnboardingFragment", "Failed to retrieve the list of profiles", profilesResult.error
+ )
+ emptyList()
+ }
+ is AsyncResult.Pending -> emptyList()
+ is AsyncResult.Success -> profilesResult.value
+ }
+
+ return profileList
+ }
+
+ private fun createDefaultProfile() {
+ profileManagementController.addProfile(
+ name = "Admin", // TODO(#4938): Refactor to empty name once proper admin profile creation flow
+ // is implemented.
+ pin = "",
+ avatarImagePath = null,
+ allowDownloadAccess = true,
+ colorRgb = -10710042,
+ isAdmin = true
+ ).toLiveData()
+ .observe(
+ fragment,
+ { result ->
+ when (result) {
+ is AsyncResult.Success -> subscribeToGetProfileList()
+ is AsyncResult.Failure -> {
+ oppiaLogger.e(
+ "OnboardingFragment", "Error creating the default profile", result.error
+ )
+ activity.finish()
+ }
+ is AsyncResult.Pending -> {}
+ }
+ }
+ )
+ }
}
diff --git a/app/src/main/java/org/oppia/android/app/onboarding/OnboardingProfileTypeActivity.kt b/app/src/main/java/org/oppia/android/app/onboarding/OnboardingProfileTypeActivity.kt
index 3be8b397e83..223ade63fb8 100644
--- a/app/src/main/java/org/oppia/android/app/onboarding/OnboardingProfileTypeActivity.kt
+++ b/app/src/main/java/org/oppia/android/app/onboarding/OnboardingProfileTypeActivity.kt
@@ -7,6 +7,7 @@ import org.oppia.android.app.activity.ActivityComponentImpl
import org.oppia.android.app.activity.InjectableAutoLocalizedAppCompatActivity
import org.oppia.android.app.model.ScreenName.ONBOARDING_PROFILE_TYPE_ACTIVITY
import org.oppia.android.util.logging.CurrentAppScreenNameIntentDecorator.decorateWithScreenName
+import org.oppia.android.util.profile.CurrentUserProfileIdIntentDecorator.extractCurrentUserProfileId
import javax.inject.Inject
/** The activity for showing the profile type selection screen. */
@@ -18,7 +19,9 @@ class OnboardingProfileTypeActivity : InjectableAutoLocalizedAppCompatActivity()
super.onCreate(savedInstanceState)
(activityComponent as ActivityComponentImpl).inject(this)
- onboardingProfileTypeActivityPresenter.handleOnCreate()
+ val profileId = intent.extractCurrentUserProfileId()
+
+ onboardingProfileTypeActivityPresenter.handleOnCreate(profileId)
}
companion object {
diff --git a/app/src/main/java/org/oppia/android/app/onboarding/OnboardingProfileTypeActivityPresenter.kt b/app/src/main/java/org/oppia/android/app/onboarding/OnboardingProfileTypeActivityPresenter.kt
index 48c0792a006..e251658bbae 100644
--- a/app/src/main/java/org/oppia/android/app/onboarding/OnboardingProfileTypeActivityPresenter.kt
+++ b/app/src/main/java/org/oppia/android/app/onboarding/OnboardingProfileTypeActivityPresenter.kt
@@ -1,10 +1,13 @@
package org.oppia.android.app.onboarding
+import android.os.Bundle
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.ProfileId
import org.oppia.android.databinding.OnboardingProfileTypeActivityBinding
+import org.oppia.android.util.profile.CurrentUserProfileIdIntentDecorator.decorateWithUserProfileId
import javax.inject.Inject
private const val TAG_PROFILE_TYPE_FRAGMENT = "TAG_PROFILE_TYPE_FRAGMENT"
@@ -17,7 +20,7 @@ class OnboardingProfileTypeActivityPresenter @Inject constructor(
private lateinit var binding: OnboardingProfileTypeActivityBinding
/** Handle creation and binding of the OnboardingProfileTypeActivity layout. */
- fun handleOnCreate() {
+ fun handleOnCreate(profileId: ProfileId) {
binding = DataBindingUtil.setContentView(activity, R.layout.onboarding_profile_type_activity)
binding.apply {
lifecycleOwner = activity
@@ -25,6 +28,11 @@ class OnboardingProfileTypeActivityPresenter @Inject constructor(
if (getOnboardingProfileTypeFragment() == null) {
val onboardingProfileTypeFragment = OnboardingProfileTypeFragment()
+ val args = Bundle().apply {
+ decorateWithUserProfileId(profileId)
+ }
+ onboardingProfileTypeFragment.arguments = args
+
activity.supportFragmentManager.beginTransaction().add(
R.id.profile_type_fragment_placeholder,
onboardingProfileTypeFragment,
diff --git a/app/src/main/java/org/oppia/android/app/onboarding/OnboardingProfileTypeFragment.kt b/app/src/main/java/org/oppia/android/app/onboarding/OnboardingProfileTypeFragment.kt
index 128788b3c4d..a4b594e9e15 100644
--- a/app/src/main/java/org/oppia/android/app/onboarding/OnboardingProfileTypeFragment.kt
+++ b/app/src/main/java/org/oppia/android/app/onboarding/OnboardingProfileTypeFragment.kt
@@ -7,6 +7,7 @@ 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.profile.CurrentUserProfileIdIntentDecorator.extractCurrentUserProfileId
import javax.inject.Inject
/** Fragment that contains the profile type selection flow of the app. */
@@ -24,6 +25,9 @@ class OnboardingProfileTypeFragment : InjectableFragment() {
container: ViewGroup?,
savedInstanceState: Bundle?
): View? {
- return onboardingProfileTypeFragmentPresenter.handleCreateView(inflater, container)
+ val profileId = checkNotNull(arguments?.extractCurrentUserProfileId()) {
+ "Expected OnboardingProfileTypeFragment to have a profileId argument."
+ }
+ return onboardingProfileTypeFragmentPresenter.handleCreateView(inflater, container, profileId)
}
}
diff --git a/app/src/main/java/org/oppia/android/app/onboarding/OnboardingProfileTypeFragmentPresenter.kt b/app/src/main/java/org/oppia/android/app/onboarding/OnboardingProfileTypeFragmentPresenter.kt
index 72ae543dd0c..5d8a7734007 100644
--- a/app/src/main/java/org/oppia/android/app/onboarding/OnboardingProfileTypeFragmentPresenter.kt
+++ b/app/src/main/java/org/oppia/android/app/onboarding/OnboardingProfileTypeFragmentPresenter.kt
@@ -5,10 +5,18 @@ import android.view.View
import android.view.ViewGroup
import androidx.appcompat.app.AppCompatActivity
import androidx.fragment.app.Fragment
+import org.oppia.android.app.model.CreateProfileActivityParams
+import org.oppia.android.app.model.ProfileId
+import org.oppia.android.app.model.ProfileType
import org.oppia.android.app.profile.ProfileChooserActivity
import org.oppia.android.databinding.OnboardingProfileTypeFragmentBinding
+import org.oppia.android.util.extensions.putProtoExtra
+import org.oppia.android.util.profile.CurrentUserProfileIdIntentDecorator.decorateWithUserProfileId
import javax.inject.Inject
+/** Argument key for [CreateProfileActivity] intent parameters. */
+const val CREATE_PROFILE_PARAMS_KEY = "CreateProfileActivity.params"
+
/** The presenter for [OnboardingProfileTypeFragment]. */
class OnboardingProfileTypeFragmentPresenter @Inject constructor(
private val fragment: Fragment,
@@ -17,7 +25,11 @@ class OnboardingProfileTypeFragmentPresenter @Inject constructor(
private lateinit var binding: OnboardingProfileTypeFragmentBinding
/** Handle creation and binding of the OnboardingProfileTypeFragment layout. */
- fun handleCreateView(inflater: LayoutInflater, container: ViewGroup?): View {
+ fun handleCreateView(
+ inflater: LayoutInflater,
+ container: ViewGroup?,
+ profileId: ProfileId
+ ): View {
binding = OnboardingProfileTypeFragmentBinding.inflate(
inflater,
container,
@@ -29,11 +41,21 @@ class OnboardingProfileTypeFragmentPresenter @Inject constructor(
profileTypeLearnerNavigationCard.setOnClickListener {
val intent = CreateProfileActivity.createProfileActivityIntent(activity)
+ intent.apply {
+ decorateWithUserProfileId(profileId)
+ putProtoExtra(
+ CREATE_PROFILE_PARAMS_KEY,
+ CreateProfileActivityParams.newBuilder()
+ .setProfileType(ProfileType.SOLE_LEARNER)
+ .build()
+ )
+ }
fragment.startActivity(intent)
}
profileTypeSupervisorNavigationCard.setOnClickListener {
val intent = ProfileChooserActivity.createProfileChooserActivity(activity)
+ // TODO(#4938): Add profileId and ProfileType to intent extras.
fragment.startActivity(intent)
}
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 7042393f3d4..48b3c1ef4b5 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
@@ -14,6 +14,7 @@ import org.oppia.android.util.extensions.getProtoExtra
import org.oppia.android.util.extensions.putProto
import org.oppia.android.util.extensions.putProtoExtra
import org.oppia.android.util.logging.CurrentAppScreenNameIntentDecorator.decorateWithScreenName
+import org.oppia.android.util.profile.CurrentUserProfileIdIntentDecorator.extractCurrentUserProfileId
import javax.inject.Inject
/** The activity to change the Default Audio language of the app. */
@@ -23,8 +24,10 @@ class AudioLanguageActivity : InjectableAutoLocalizedAppCompatActivity() {
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
(activityComponent as ActivityComponentImpl).inject(this)
+ val profileId = intent.extractCurrentUserProfileId()
audioLanguageActivityPresenter.handleOnCreate(
- savedInstanceState?.retrieveLanguageFromSavedState() ?: intent.retrieveLanguageFromParams()
+ savedInstanceState?.retrieveLanguageFromSavedState() ?: intent.retrieveLanguageFromParams(),
+ profileId
)
}
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 cb33ecf7c0e..fa4e149207d 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
@@ -8,6 +8,7 @@ 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.app.model.ProfileId
import org.oppia.android.databinding.AudioLanguageActivityBinding
import org.oppia.android.util.extensions.putProtoExtra
import javax.inject.Inject
@@ -18,7 +19,7 @@ class AudioLanguageActivityPresenter @Inject constructor(private val activity: A
private lateinit var audioLanguage: AudioLanguage
/** Handles when the activity is first created. */
- fun handleOnCreate(audioLanguage: AudioLanguage) {
+ fun handleOnCreate(audioLanguage: AudioLanguage, profileId: ProfileId) {
this.audioLanguage = audioLanguage
val binding: AudioLanguageActivityBinding =
@@ -27,7 +28,7 @@ class AudioLanguageActivityPresenter @Inject constructor(private val activity: A
finishWithResult()
}
if (getAudioLanguageFragment() == null) {
- val audioLanguageFragment = AudioLanguageFragment.newInstance(audioLanguage)
+ val audioLanguageFragment = AudioLanguageFragment.newInstance(audioLanguage, profileId)
activity.supportFragmentManager.beginTransaction()
.add(R.id.audio_language_fragment_container, audioLanguageFragment).commitNow()
}
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 4cb067f8cc7..06e0e2cac1c 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
@@ -10,11 +10,14 @@ import org.oppia.android.app.fragment.InjectableFragment
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.app.model.ProfileId
import org.oppia.android.app.onboarding.AudioLanguageFragmentPresenter
import org.oppia.android.util.extensions.getProto
import org.oppia.android.util.extensions.putProto
import org.oppia.android.util.platformparameter.EnableOnboardingFlowV2
import org.oppia.android.util.platformparameter.PlatformParameterValue
+import org.oppia.android.util.profile.CurrentUserProfileIdIntentDecorator.decorateWithUserProfileId
+import org.oppia.android.util.profile.CurrentUserProfileIdIntentDecorator.extractCurrentUserProfileId
import javax.inject.Inject
/** The fragment to change the default audio language of the app. */
@@ -41,9 +44,18 @@ class AudioLanguageFragment : InjectableFragment(), AudioLanguageRadioButtonList
checkNotNull(
savedInstanceState?.retrieveLanguageFromSavedState()
?: arguments?.retrieveLanguageFromArguments()
- ) { "Expected arguments to be passed to AudioLanguageFragment" }
+ ) { "Expected arguments to be passed to AudioLanguageFragment." }
+
return if (enableOnboardingFlowV2.value) {
- audioLanguageFragmentPresenter.handleCreateView(inflater, container)
+ val profileId = checkNotNull(arguments?.extractCurrentUserProfileId()) {
+ "Expected a profileId argument to be passed to AudioLanguageFragment."
+ }
+ audioLanguageFragmentPresenter.handleCreateView(
+ inflater,
+ container,
+ profileId,
+ savedInstanceState
+ )
} else {
audioLanguageFragmentPresenterV1.handleOnCreateView(inflater, container, audioLanguage)
}
@@ -51,7 +63,9 @@ class AudioLanguageFragment : InjectableFragment(), AudioLanguageRadioButtonList
override fun onSaveInstanceState(outState: Bundle) {
super.onSaveInstanceState(outState)
- if (!enableOnboardingFlowV2.value) {
+ if (enableOnboardingFlowV2.value) {
+ audioLanguageFragmentPresenter.handleSavedState(outState)
+ } else {
val state = AudioLanguageFragmentStateBundle.newBuilder().apply {
audioLanguage = audioLanguageFragmentPresenterV1.getLanguageSelected()
}.build()
@@ -67,19 +81,22 @@ class AudioLanguageFragment : InjectableFragment(), AudioLanguageRadioButtonList
companion object {
private const val FRAGMENT_ARGUMENTS_KEY = "AudioLanguageFragment.arguments"
- private const val FRAGMENT_SAVED_STATE_KEY = "AudioLanguageFragment.saved_state"
+
+ /** Argument key for the [AudioLanguageFragment] saved instance state bundle. */
+ 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 {
+ fun newInstance(audioLanguage: AudioLanguage, profileId: ProfileId): AudioLanguageFragment {
return AudioLanguageFragment().apply {
arguments = Bundle().apply {
val args = AudioLanguageFragmentArguments.newBuilder().apply {
this.audioLanguage = audioLanguage
}.build()
putProto(FRAGMENT_ARGUMENTS_KEY, args)
+ decorateWithUserProfileId(profileId)
}
}
}
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
index 5e25aaabf5d..27b40c88b34 100644
--- a/app/src/main/java/org/oppia/android/app/options/AudioLanguageSelectionViewModel.kt
+++ b/app/src/main/java/org/oppia/android/app/options/AudioLanguageSelectionViewModel.kt
@@ -1,19 +1,59 @@
package org.oppia.android.app.options
+import androidx.databinding.ObservableField
import androidx.fragment.app.Fragment
+import androidx.lifecycle.LiveData
import androidx.lifecycle.MutableLiveData
+import androidx.lifecycle.Transformations
import org.oppia.android.app.fragment.FragmentScope
+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.translation.AppLanguageResourceHandler
import org.oppia.android.app.viewmodel.ObservableViewModel
+import org.oppia.android.domain.oppialogger.OppiaLogger
+import org.oppia.android.domain.translation.TranslationController
+import org.oppia.android.util.data.AsyncResult
+import org.oppia.android.util.data.DataProvider
+import org.oppia.android.util.data.DataProviders.Companion.combineWith
+import org.oppia.android.util.data.DataProviders.Companion.toLiveData
+import org.oppia.android.util.locale.OppiaLocale
import javax.inject.Inject
-/** Language list view model for the recycler view in [AudioLanguageFragment]. */
+private const val PRE_SELECTED_LANGUAGE_PROVIDER_ID = "systemLanguage+appLanguageProvider"
+
+/** ViewModel for managing language selection in [AudioLanguageFragment]. */
@FragmentScope
class AudioLanguageSelectionViewModel @Inject constructor(
private val fragment: Fragment,
- private val appLanguageResourceHandler: AppLanguageResourceHandler
+ private val appLanguageResourceHandler: AppLanguageResourceHandler,
+ private val translationController: TranslationController,
+ private val oppiaLogger: OppiaLogger
) : ObservableViewModel() {
+ private lateinit var profileId: ProfileId
+
+ /** An [ObservableField] to bind the resolved audio language to the dropdown text. */
+ val selectedAudioLanguage = ObservableField(OppiaLanguage.LANGUAGE_UNSPECIFIED)
+
+ /** The [LiveData] representing the language to be displayed by default in the dropdown menu. */
+ val languagePreselectionLiveData: LiveData by lazy {
+ Transformations.map(languagePreselectionProvider.toLiveData()) { languageResult ->
+ return@map when (languageResult) {
+ is AsyncResult.Failure -> {
+ oppiaLogger.e(
+ "AudioLanguageFragment",
+ "Failed to retrieve language information.",
+ languageResult.error
+ )
+ OppiaLanguage.LANGUAGE_UNSPECIFIED
+ }
+ is AsyncResult.Pending -> OppiaLanguage.LANGUAGE_UNSPECIFIED
+ is AsyncResult.Success -> languageResult.value
+ }
+ }
+ }
+
/** The [AudioLanguage] currently selected in the radio button list. */
val selectedLanguage = MutableLiveData()
@@ -22,6 +62,67 @@ class AudioLanguageSelectionViewModel @Inject constructor(
AudioLanguage.values().filter { it !in IGNORED_AUDIO_LANGUAGES }.map(::createItemViewModel)
}
+ /** Get the list of app supported languages to be displayed in the language dropdown. */
+ val availableAudioLanguages: LiveData> get() = _availableAudioLanguages
+ private val _availableAudioLanguages = MutableLiveData>()
+
+ /** Sets the list of audio languages supported by the app based on [OppiaLanguage]. */
+ val supportedOppiaLanguagesLiveData: LiveData> by lazy {
+ Transformations.map(
+ translationController.getSupportedAppLanguages().toLiveData()
+ ) { supportedLanguagesResult ->
+ return@map when (supportedLanguagesResult) {
+ is AsyncResult.Failure -> {
+ oppiaLogger.e(
+ "AudioLanguageFragment",
+ "Failed to retrieve supported languages.",
+ supportedLanguagesResult.error
+ )
+ listOf()
+ }
+ is AsyncResult.Pending -> listOf()
+ is AsyncResult.Success -> supportedLanguagesResult.value
+ }
+ }
+ }
+
+ // TODO(#4938): Update the pre-selection logic to include the admin profile audio language for
+ // non-sole learners.
+ private val languagePreselectionProvider: DataProvider by lazy {
+ appLanguageSelectionProvider.combineWith(
+ systemLanguageProvider,
+ PRE_SELECTED_LANGUAGE_PROVIDER_ID
+ ) { appLanguageSelection: AppLanguageSelection, displayLocale: OppiaLocale.DisplayLocale ->
+ val appLanguage = appLanguageSelection.selectedLanguage
+ val systemLanguage = displayLocale.getCurrentLanguage()
+ computePreselection(appLanguage, systemLanguage)
+ }
+ }
+
+ private val appLanguageSelectionProvider: DataProvider by lazy {
+ translationController.getAppLanguageSelection(profileId)
+ }
+
+ private val systemLanguageProvider: DataProvider by lazy {
+ translationController.getSystemLanguageLocale()
+ }
+
+ /** Receives and sets the current profileId in this viewModel. */
+ fun updateProfileId(profileId: ProfileId) {
+ this.profileId = profileId
+ }
+
+ private fun computePreselection(
+ appLanguage: OppiaLanguage,
+ systemLanguage: OppiaLanguage
+ ): OppiaLanguage {
+ return when {
+ appLanguage != OppiaLanguage.LANGUAGE_UNSPECIFIED -> appLanguage
+ systemLanguage != OppiaLanguage.LANGUAGE_UNSPECIFIED -> systemLanguage
+ else -> OppiaLanguage.LANGUAGE_UNSPECIFIED
+ }
+ }
+
private fun createItemViewModel(language: AudioLanguage): AudioLanguageItemViewModel {
return AudioLanguageItemViewModel(
language,
@@ -31,19 +132,6 @@ class AudioLanguageSelectionViewModel @Inject constructor(
)
}
- // TODO(#4938): Update the pre-selection logic.
- /** The pre-selected [AudioLanguage] to be shown in the language selection dropdown. */
- val defaultLanguageSelection = getLanguageDisplayName(AudioLanguage.ENGLISH_AUDIO_LANGUAGE)
-
- /** The list of [AudioLanguage]s supported by the app. */
- val availableAudioLanguages: List by lazy {
- AudioLanguage.values().filter { it !in IGNORED_AUDIO_LANGUAGES }.map(::getLanguageDisplayName)
- }
-
- private fun getLanguageDisplayName(audioLanguage: AudioLanguage): String {
- return appLanguageResourceHandler.computeLocalizedDisplayName(audioLanguage)
- }
-
private companion object {
private val IGNORED_AUDIO_LANGUAGES =
listOf(
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 52a993a52f6..60220f2e02e 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
@@ -55,7 +55,8 @@ class OptionsActivity :
// used to initially load the suitable fragment in the case of multipane.
private var isFirstOpen = true
private lateinit var selectedFragment: String
- private var profileId: Int? = -1
+ private lateinit var profileId: ProfileId
+ private var internalProfileId: Int = -1
private lateinit var readingTextSizeLauncher: ActivityResultLauncher
private lateinit var audioLanguageLauncher: ActivityResultLauncher
@@ -94,7 +95,8 @@ class OptionsActivity :
OptionsActivityParams.getDefaultInstance()
)
val isFromNavigationDrawer = args?.isFromNavigationDrawer ?: false
- profileId = intent.extractCurrentUserProfileId().internalId
+ profileId = intent.extractCurrentUserProfileId()
+ internalProfileId = profileId.internalId
if (savedInstanceState != null) {
isFirstOpen = false
}
@@ -116,7 +118,7 @@ class OptionsActivity :
extraOptionsTitle,
isFirstOpen,
selectedFragment,
- profileId!!
+ internalProfileId
)
title = resourceHandler.getStringInLocale(R.string.menu_options)
@@ -153,15 +155,15 @@ class OptionsActivity :
AppLanguageActivity.createAppLanguageActivityIntent(
this,
oppiaLanguage,
- profileId!!
+ internalProfileId
)
)
}
override fun routeAudioLanguageList(audioLanguage: AudioLanguage) {
- audioLanguageLauncher.launch(
- AudioLanguageActivity.createAudioLanguageActivityIntent(this, audioLanguage)
- )
+ val intent = AudioLanguageActivity.createAudioLanguageActivityIntent(this, audioLanguage)
+ intent.decorateWithUserProfileId(profileId)
+ audioLanguageLauncher.launch(intent)
}
override fun routeReadingTextSize(readingTextSize: ReadingTextSize) {
@@ -191,7 +193,7 @@ class OptionsActivity :
optionActivityPresenter.setExtraOptionTitle(
resourceHandler.getStringInLocale(R.string.audio_language)
)
- optionActivityPresenter.loadAudioLanguageFragment(audioLanguage)
+ optionActivityPresenter.loadAudioLanguageFragment(audioLanguage, profileId)
}
override fun onSaveInstanceState(outState: Bundle) {
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 ccdff3ba113..e611795f4b8 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
@@ -11,6 +11,7 @@ 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.OppiaLanguage
+import org.oppia.android.app.model.ProfileId
import org.oppia.android.app.model.ReadingTextSize
import javax.inject.Inject
@@ -135,8 +136,8 @@ class OptionsActivityPresenter @Inject constructor(
*
* @param audioLanguage the initially selected audio language
*/
- fun loadAudioLanguageFragment(audioLanguage: AudioLanguage) {
- val audioLanguageFragment = AudioLanguageFragment.newInstance(audioLanguage)
+ fun loadAudioLanguageFragment(audioLanguage: AudioLanguage, profileId: ProfileId) {
+ val audioLanguageFragment = AudioLanguageFragment.newInstance(audioLanguage, profileId)
activity.supportFragmentManager
.beginTransaction()
.replace(R.id.multipane_options_container, audioLanguageFragment)
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 60f4214458e..db1b92f4f8c 100644
--- a/app/src/main/java/org/oppia/android/app/player/audio/AudioFragmentPresenter.kt
+++ b/app/src/main/java/org/oppia/android/app/player/audio/AudioFragmentPresenter.kt
@@ -11,12 +11,10 @@ import androidx.appcompat.app.AlertDialog
import androidx.appcompat.app.AppCompatActivity
import androidx.fragment.app.Fragment
import androidx.lifecycle.LiveData
-import androidx.lifecycle.Observer
import androidx.lifecycle.Transformations
import org.oppia.android.R
import org.oppia.android.app.fragment.FragmentScope
import org.oppia.android.app.model.AudioLanguage
-import org.oppia.android.app.model.CellularDataPreference
import org.oppia.android.app.model.ProfileId
import org.oppia.android.app.model.Spotlight
import org.oppia.android.app.model.State
@@ -38,7 +36,6 @@ import javax.inject.Inject
const val TAG_LANGUAGE_DIALOG = "LANGUAGE_DIALOG"
private const val TAG_CELLULAR_DATA_DIALOG = "CELLULAR_DATA_DIALOG"
-const val AUDIO_FRAGMENT_PROFILE_ID_ARGUMENT_KEY = "AUDIO_FRAGMENT_PROFILE_ID_ARGUMENT_KEY"
/** The presenter for [AudioFragment]. */
@FragmentScope
@@ -75,7 +72,7 @@ class AudioFragmentPresenter @Inject constructor(
cellularAudioDialogController.getCellularDataPreference().toLiveData()
.observe(
fragment,
- Observer> {
+ {
if (it is AsyncResult.Success) {
showCellularDataDialog = !it.value.hideDialog
useCellularData = it.value.useCellularData
@@ -103,7 +100,7 @@ class AudioFragmentPresenter @Inject constructor(
})
audioViewModel.playStatusLiveData.observe(
fragment,
- Observer {
+ {
prepared = it != UiAudioPlayStatus.LOADING && it != UiAudioPlayStatus.FAILED
binding.audioProgressSeekBar.isEnabled = prepared
@@ -156,7 +153,7 @@ class AudioFragmentPresenter @Inject constructor(
private fun subscribeToAudioLanguageLiveData() {
retrieveAudioLanguageCode().observe(
activity,
- Observer { result ->
+ { result ->
audioViewModel.selectedLanguageCode = result
audioViewModel.loadMainContentAudio(allowAutoPlay = false, reloadingContent = false)
}
diff --git a/app/src/main/java/org/oppia/android/app/testing/ColorBindingAdaptersTestActivity.kt b/app/src/main/java/org/oppia/android/app/testing/ColorBindingAdaptersTestActivity.kt
index 9d8878c520f..d9b99d434e1 100644
--- a/app/src/main/java/org/oppia/android/app/testing/ColorBindingAdaptersTestActivity.kt
+++ b/app/src/main/java/org/oppia/android/app/testing/ColorBindingAdaptersTestActivity.kt
@@ -6,7 +6,7 @@ import android.os.Bundle
import org.oppia.android.R
import org.oppia.android.app.activity.InjectableAutoLocalizedAppCompatActivity
-/** Test activity for ViewBindingAdapters. */
+/** Test activity for ColorBindingAdapters. */
class ColorBindingAdaptersTestActivity : InjectableAutoLocalizedAppCompatActivity() {
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
diff --git a/app/src/main/java/org/oppia/android/app/testing/TextInputLayoutBindingAdaptersTestActivity.kt b/app/src/main/java/org/oppia/android/app/testing/TextInputLayoutBindingAdaptersTestActivity.kt
new file mode 100644
index 00000000000..02fcec01b90
--- /dev/null
+++ b/app/src/main/java/org/oppia/android/app/testing/TextInputLayoutBindingAdaptersTestActivity.kt
@@ -0,0 +1,26 @@
+package org.oppia.android.app.testing
+
+import android.content.Context
+import android.content.Intent
+import android.os.Bundle
+import org.oppia.android.R
+import org.oppia.android.app.activity.InjectableAutoLocalizedAppCompatActivity
+
+/** Test activity for [TextInputLayoutBindingAdapters]. */
+class TextInputLayoutBindingAdaptersTestActivity : InjectableAutoLocalizedAppCompatActivity() {
+ override fun onCreate(savedInstanceState: Bundle?) {
+ super.onCreate(savedInstanceState)
+ setContentView(R.layout.text_input_layout_binding_adapters_test_activity)
+
+ supportFragmentManager.beginTransaction().add(
+ R.id.background,
+ TextInputLayoutBindingAdaptersTestFragment()
+ ).commitNow()
+ }
+
+ companion object {
+ /** Intent to open this activity. */
+ fun createIntent(context: Context): Intent =
+ Intent(context, TextInputLayoutBindingAdaptersTestActivity::class.java)
+ }
+}
diff --git a/app/src/main/java/org/oppia/android/app/testing/TextInputLayoutBindingAdaptersTestFragment.kt b/app/src/main/java/org/oppia/android/app/testing/TextInputLayoutBindingAdaptersTestFragment.kt
new file mode 100644
index 00000000000..ce0167f7b33
--- /dev/null
+++ b/app/src/main/java/org/oppia/android/app/testing/TextInputLayoutBindingAdaptersTestFragment.kt
@@ -0,0 +1,30 @@
+package org.oppia.android.app.testing
+
+import android.os.Bundle
+import android.view.LayoutInflater
+import android.view.View
+import android.view.ViewGroup
+import org.oppia.android.app.fragment.InjectableFragment
+import org.oppia.android.databinding.TextInputLayoutBindingAdaptersTestFragmentBinding
+
+/** Test-only fragment for verifying behaviors of [TextInputLayoutBindingAdapters]. */
+class TextInputLayoutBindingAdaptersTestFragment : InjectableFragment() {
+
+ private lateinit var binding: TextInputLayoutBindingAdaptersTestFragmentBinding
+
+ override fun onCreateView(
+ inflater: LayoutInflater,
+ container: ViewGroup?,
+ savedInstanceState: Bundle?
+ ): View {
+ binding = TextInputLayoutBindingAdaptersTestFragmentBinding.inflate(
+ inflater,
+ container,
+ false
+ )
+
+ binding.lifecycleOwner = this@TextInputLayoutBindingAdaptersTestFragment
+
+ return binding.root
+ }
+}
diff --git a/app/src/main/res/drawable/learner_otter.xml b/app/src/main/res/drawable/learner_otter.xml
index 2fba3df3a5f..69f037bdca9 100644
--- a/app/src/main/res/drawable/learner_otter.xml
+++ b/app/src/main/res/drawable/learner_otter.xml
@@ -2,7 +2,8 @@
android:width="180dp"
android:height="180dp"
android:viewportWidth="500"
- android:viewportHeight="500">
+ android:viewportHeight="500"
+ android:autoMirrored="true">
+ android:viewportHeight="167"
+ android:autoMirrored="true">
diff --git a/app/src/main/res/drawable/parent_teacher_otter.xml b/app/src/main/res/drawable/parent_teacher_otter.xml
index abeec4882c4..8671cb9dbbf 100644
--- a/app/src/main/res/drawable/parent_teacher_otter.xml
+++ b/app/src/main/res/drawable/parent_teacher_otter.xml
@@ -2,7 +2,8 @@
android:width="180dp"
android:height="180dp"
android:viewportWidth="500"
- android:viewportHeight="500">
+ android:viewportHeight="500"
+ android:autoMirrored="true">
+
+
+
+
+
+ android:padding="@dimen/onboarding_shared_padding_small"
+ app:filter="@{false}"
+ app:languageSelection="@{viewModel.selectedAudioLanguage}"/>
diff --git a/app/src/main/res/layout-land/create_profile_fragment.xml b/app/src/main/res/layout-land/create_profile_fragment.xml
index 93c01c2f32d..aa839f0e2e9 100644
--- a/app/src/main/res/layout-land/create_profile_fragment.xml
+++ b/app/src/main/res/layout-land/create_profile_fragment.xml
@@ -124,7 +124,7 @@
android:layout_marginStart="@dimen/phone_shared_margin_xl"
android:layout_marginTop="@dimen/phone_shared_margin_small"
android:layout_marginEnd="@dimen/phone_shared_margin_medium"
- android:text="@string/create_profile_activity_nickname_error"
+ android:text="@{viewModel.errorMessage}"
android:visibility="@{viewModel.hasErrorMessage ? View.VISIBLE : View.GONE}"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintTop_toBottomOf="@id/create_profile_nickname_edittext" />
diff --git a/app/src/main/res/layout-land/onboarding_app_language_selection_fragment.xml b/app/src/main/res/layout-land/onboarding_app_language_selection_fragment.xml
index 06652937c5a..cbe45fadc7a 100644
--- a/app/src/main/res/layout-land/onboarding_app_language_selection_fragment.xml
+++ b/app/src/main/res/layout-land/onboarding_app_language_selection_fragment.xml
@@ -96,6 +96,7 @@
app:startIconTint="@color/component_color_shared_black_background_color">
+
+
+
+
+
+ android:padding="@dimen/onboarding_shared_padding_small"
+ app:filter="@{false}"
+ app:languageSelection="@{viewModel.selectedAudioLanguage}" />
diff --git a/app/src/main/res/layout-sw600dp-land/create_profile_fragment.xml b/app/src/main/res/layout-sw600dp-land/create_profile_fragment.xml
index 5eba49ebb23..3e33448c69f 100644
--- a/app/src/main/res/layout-sw600dp-land/create_profile_fragment.xml
+++ b/app/src/main/res/layout-sw600dp-land/create_profile_fragment.xml
@@ -121,7 +121,7 @@
android:layout_height="wrap_content"
android:layout_marginTop="@dimen/tablet_shared_margin_x_small"
android:layout_marginEnd="@dimen/tablet_shared_margin_small"
- android:text="@string/create_profile_activity_nickname_error"
+ android:text="@{viewModel.errorMessage}"
android:visibility="@{viewModel.hasErrorMessage ? View.VISIBLE : View.GONE}"
app:layout_constraintStart_toStartOf="@id/create_profile_nickname_edittext"
app:layout_constraintTop_toBottomOf="@id/create_profile_nickname_edittext" />
diff --git a/app/src/main/res/layout-sw600dp-land/onboarding_app_language_selection_fragment.xml b/app/src/main/res/layout-sw600dp-land/onboarding_app_language_selection_fragment.xml
index a319e663457..7b27335c708 100644
--- a/app/src/main/res/layout-sw600dp-land/onboarding_app_language_selection_fragment.xml
+++ b/app/src/main/res/layout-sw600dp-land/onboarding_app_language_selection_fragment.xml
@@ -105,6 +105,7 @@
app:startIconTint="@color/component_color_shared_black_background_color">
+
+
+
+
+
+ android:padding="@dimen/onboarding_shared_padding_small"
+ app:filter="@{false}"
+ app:languageSelection="@{viewModel.selectedAudioLanguage}" />
diff --git a/app/src/main/res/layout-sw600dp-port/create_profile_fragment.xml b/app/src/main/res/layout-sw600dp-port/create_profile_fragment.xml
index 0edd5932959..689c67ae91f 100644
--- a/app/src/main/res/layout-sw600dp-port/create_profile_fragment.xml
+++ b/app/src/main/res/layout-sw600dp-port/create_profile_fragment.xml
@@ -120,7 +120,7 @@
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_marginTop="@dimen/tablet_shared_margin_small"
- android:text="@string/create_profile_activity_nickname_error"
+ android:text="@{viewModel.errorMessage}"
android:visibility="@{viewModel.hasErrorMessage ? View.VISIBLE : View.GONE}"
app:layout_constraintStart_toStartOf="@id/create_profile_nickname_edittext"
app:layout_constraintTop_toBottomOf="@id/create_profile_nickname_edittext" />
diff --git a/app/src/main/res/layout-sw600dp-port/onboarding_app_language_selection_fragment.xml b/app/src/main/res/layout-sw600dp-port/onboarding_app_language_selection_fragment.xml
index 9425ffc352d..e2fc66f56c0 100644
--- a/app/src/main/res/layout-sw600dp-port/onboarding_app_language_selection_fragment.xml
+++ b/app/src/main/res/layout-sw600dp-port/onboarding_app_language_selection_fragment.xml
@@ -104,6 +104,7 @@
app:startIconTint="@color/component_color_shared_black_background_color">
+
+
+
+
+
+ android:padding="@dimen/onboarding_shared_padding_small"
+ app:filter="@{false}"
+ app:languageSelection="@{viewModel.selectedAudioLanguage}" />
diff --git a/app/src/main/res/layout/create_profile_fragment.xml b/app/src/main/res/layout/create_profile_fragment.xml
index ab53fbfc69a..40f8c4116f3 100644
--- a/app/src/main/res/layout/create_profile_fragment.xml
+++ b/app/src/main/res/layout/create_profile_fragment.xml
@@ -123,7 +123,7 @@
android:layout_marginStart="@dimen/phone_shared_margin_xl"
android:layout_marginTop="@dimen/phone_shared_margin_small"
android:layout_marginEnd="@dimen/phone_shared_margin_medium"
- android:text="@string/create_profile_activity_nickname_error"
+ android:text="@{viewModel.errorMessage}"
android:visibility="@{viewModel.hasErrorMessage ? View.VISIBLE : View.GONE}"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintTop_toBottomOf="@id/create_profile_nickname_edittext" />
diff --git a/app/src/main/res/layout/onboarding_app_language_selection_fragment.xml b/app/src/main/res/layout/onboarding_app_language_selection_fragment.xml
index 2c711918350..9737d8f8a59 100644
--- a/app/src/main/res/layout/onboarding_app_language_selection_fragment.xml
+++ b/app/src/main/res/layout/onboarding_app_language_selection_fragment.xml
@@ -100,6 +100,7 @@
app:startIconTint="@color/component_color_shared_black_background_color">
+
diff --git a/app/src/main/res/layout/text_input_layout_binding_adapters_test_fragment.xml b/app/src/main/res/layout/text_input_layout_binding_adapters_test_fragment.xml
new file mode 100644
index 00000000000..1beae3f8b41
--- /dev/null
+++ b/app/src/main/res/layout/text_input_layout_binding_adapters_test_fragment.xml
@@ -0,0 +1,21 @@
+
+
+
+
+
+
+
+
+
+
+
diff --git a/app/src/main/res/values/strings.xml b/app/src/main/res/values/strings.xml
index 319d70ff93a..a1b7ad23201 100755
--- a/app/src/main/res/values/strings.xml
+++ b/app/src/main/res/values/strings.xml
@@ -261,6 +261,8 @@
This name is already in use by another profile.
Please enter a valid name for this profile.
Please choose a profile name that doesn\'t include numbers or symbols.
+ Profile type unknown.
+ An error occurred while creating a profile.
Your PIN should be 3 digits long.
Please make sure that both PINs match.
More information on 3-digit PINs.
diff --git a/app/src/sharedTest/java/org/oppia/android/app/databinding/ColorBindingAdaptersTest.kt b/app/src/sharedTest/java/org/oppia/android/app/databinding/ColorBindingAdaptersTest.kt
index 393311789b3..b0054de19ff 100644
--- a/app/src/sharedTest/java/org/oppia/android/app/databinding/ColorBindingAdaptersTest.kt
+++ b/app/src/sharedTest/java/org/oppia/android/app/databinding/ColorBindingAdaptersTest.kt
@@ -91,7 +91,7 @@ import org.robolectric.annotation.LooperMode
import javax.inject.Inject
import javax.inject.Singleton
-/** Tests for [MarginBindingAdapters]. */
+/** Tests for [ColorBindingAdapters]. */
@RunWith(AndroidJUnit4::class)
@LooperMode(LooperMode.Mode.PAUSED)
@Config(
diff --git a/app/src/sharedTest/java/org/oppia/android/app/databinding/TextInputLayoutBindingAdaptersTest.kt b/app/src/sharedTest/java/org/oppia/android/app/databinding/TextInputLayoutBindingAdaptersTest.kt
new file mode 100644
index 00000000000..844f2e70327
--- /dev/null
+++ b/app/src/sharedTest/java/org/oppia/android/app/databinding/TextInputLayoutBindingAdaptersTest.kt
@@ -0,0 +1,239 @@
+package org.oppia.android.app.databinding
+
+import android.app.Application
+import android.content.Context
+import android.widget.AutoCompleteTextView
+import androidx.appcompat.app.AppCompatActivity
+import androidx.test.core.app.ActivityScenario
+import androidx.test.core.app.ApplicationProvider
+import androidx.test.espresso.intent.Intents
+import androidx.test.ext.junit.runners.AndroidJUnit4
+import com.google.android.material.textfield.TextInputLayout
+import com.google.common.truth.Truth.assertThat
+import dagger.Component
+import org.junit.After
+import org.junit.Before
+import org.junit.Test
+import org.junit.runner.RunWith
+import org.oppia.android.R
+import org.oppia.android.app.activity.ActivityComponent
+import org.oppia.android.app.activity.ActivityComponentFactory
+import org.oppia.android.app.activity.route.ActivityRouterModule
+import org.oppia.android.app.application.ApplicationComponent
+import org.oppia.android.app.application.ApplicationInjector
+import org.oppia.android.app.application.ApplicationInjectorProvider
+import org.oppia.android.app.application.ApplicationModule
+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.OppiaLanguage
+import org.oppia.android.app.player.state.itemviewmodel.SplitScreenInteractionModule
+import org.oppia.android.app.shim.ViewBindingShimModule
+import org.oppia.android.app.testing.TextInputLayoutBindingAdaptersTestActivity
+import org.oppia.android.app.translation.testing.ActivityRecreatorTestModule
+import org.oppia.android.data.backends.gae.NetworkConfigProdModule
+import org.oppia.android.data.backends.gae.NetworkModule
+import org.oppia.android.domain.classify.InteractionsModule
+import org.oppia.android.domain.classify.rules.algebraicexpressioninput.AlgebraicExpressionInputModule
+import org.oppia.android.domain.classify.rules.continueinteraction.ContinueModule
+import org.oppia.android.domain.classify.rules.dragAndDropSortInput.DragDropSortInputModule
+import org.oppia.android.domain.classify.rules.fractioninput.FractionInputModule
+import org.oppia.android.domain.classify.rules.imageClickInput.ImageClickInputModule
+import org.oppia.android.domain.classify.rules.itemselectioninput.ItemSelectionInputModule
+import org.oppia.android.domain.classify.rules.mathequationinput.MathEquationInputModule
+import org.oppia.android.domain.classify.rules.multiplechoiceinput.MultipleChoiceInputModule
+import org.oppia.android.domain.classify.rules.numberwithunits.NumberWithUnitsRuleModule
+import org.oppia.android.domain.classify.rules.numericexpressioninput.NumericExpressionInputModule
+import org.oppia.android.domain.classify.rules.numericinput.NumericInputRuleModule
+import org.oppia.android.domain.classify.rules.ratioinput.RatioInputModule
+import org.oppia.android.domain.classify.rules.textinput.TextInputRuleModule
+import org.oppia.android.domain.exploration.ExplorationProgressModule
+import org.oppia.android.domain.exploration.ExplorationStorageModule
+import org.oppia.android.domain.hintsandsolution.HintsAndSolutionConfigModule
+import org.oppia.android.domain.hintsandsolution.HintsAndSolutionProdModule
+import org.oppia.android.domain.onboarding.ExpirationMetaDataRetrieverModule
+import org.oppia.android.domain.oppialogger.LogStorageModule
+import org.oppia.android.domain.oppialogger.LoggingIdentifierModule
+import org.oppia.android.domain.oppialogger.analytics.ApplicationLifecycleModule
+import org.oppia.android.domain.oppialogger.analytics.CpuPerformanceSnapshotterModule
+import org.oppia.android.domain.oppialogger.logscheduler.MetricLogSchedulerModule
+import org.oppia.android.domain.oppialogger.loguploader.LogReportWorkerModule
+import org.oppia.android.domain.platformparameter.PlatformParameterModule
+import org.oppia.android.domain.platformparameter.PlatformParameterSingletonModule
+import org.oppia.android.domain.question.QuestionModule
+import org.oppia.android.domain.workmanager.WorkManagerConfigurationModule
+import org.oppia.android.testing.TestImageLoaderModule
+import org.oppia.android.testing.TestLogReportingModule
+import org.oppia.android.testing.espresso.EditTextInputAction
+import org.oppia.android.testing.firebase.TestAuthenticationModule
+import org.oppia.android.testing.robolectric.RobolectricModule
+import org.oppia.android.testing.threading.TestCoroutineDispatchers
+import org.oppia.android.testing.threading.TestDispatcherModule
+import org.oppia.android.testing.time.FakeOppiaClockModule
+import org.oppia.android.util.accessibility.AccessibilityTestModule
+import org.oppia.android.util.caching.AssetModule
+import org.oppia.android.util.caching.testing.CachingTestModule
+import org.oppia.android.util.gcsresource.GcsResourceModule
+import org.oppia.android.util.locale.LocaleProdModule
+import org.oppia.android.util.logging.EventLoggingConfigurationModule
+import org.oppia.android.util.logging.LoggerModule
+import org.oppia.android.util.logging.SyncStatusModule
+import org.oppia.android.util.logging.firebase.FirebaseLogUploaderModule
+import org.oppia.android.util.networking.NetworkConnectionDebugUtilModule
+import org.oppia.android.util.networking.NetworkConnectionUtilDebugModule
+import org.oppia.android.util.parser.html.HtmlParserEntityTypeModule
+import org.oppia.android.util.parser.image.ImageParsingModule
+import org.robolectric.annotation.Config
+import org.robolectric.annotation.LooperMode
+import javax.inject.Inject
+import javax.inject.Singleton
+
+/** Tests for [TextInputLayoutBindingAdapters]. */
+@RunWith(AndroidJUnit4::class)
+@LooperMode(LooperMode.Mode.PAUSED)
+@Config(
+ application = TextInputLayoutBindingAdaptersTest.TestApplication::class,
+ qualifiers = "port-xxhdpi"
+)
+class TextInputLayoutBindingAdaptersTest {
+ @Inject
+ lateinit var context: Context
+
+ @Inject
+ lateinit var testCoroutineDispatchers: TestCoroutineDispatchers
+
+ @Inject
+ lateinit var editTextInputAction: EditTextInputAction
+
+ @Before
+ fun setUp() {
+ setUpTestApplicationComponent()
+ Intents.init()
+ testCoroutineDispatchers.registerIdlingResource()
+ }
+
+ @After
+ fun tearDown() {
+ testCoroutineDispatchers.registerIdlingResource()
+ Intents.release()
+ }
+
+ @Test
+ fun testBindingAdapters_setErrorMessage_setsMessageCorrectly() {
+ launchActivity().use { scenario ->
+ scenario?.onActivity { activity ->
+ val testView: TextInputLayout = activity.findViewById(R.id.test_text_input_view)
+ TextInputLayoutBindingAdapters.setErrorMessage(testView, "Some error message.")
+ assertThat(testView.error).isEqualTo("Some error message.")
+ }
+ }
+ }
+
+ @Test
+ fun testBindingAdapters_setSelection_filterDisabled_setsSelectionCorrectly() {
+ launchActivity().use { scenario ->
+ scenario?.onActivity { activity ->
+ val testView: AutoCompleteTextView = activity.findViewById(R.id.test_autocomplete_view)
+ TextInputLayoutBindingAdapters.setLanguageSelection(testView, OppiaLanguage.ENGLISH, false)
+ assertThat(testView.text.toString()).isEqualTo("English")
+ }
+ }
+ }
+
+ @Test
+ fun testBindingAdapters_setSelection_filterEnabled_setsSelectionCorrectly() {
+ launchActivity().use { scenario ->
+ scenario?.onActivity { activity ->
+ val testView: AutoCompleteTextView = activity.findViewById(R.id.test_autocomplete_view)
+ TextInputLayoutBindingAdapters.setLanguageSelection(testView, OppiaLanguage.ENGLISH, true)
+ assertThat(testView.text.toString()).isEqualTo("English")
+ }
+ }
+ }
+
+ @Test
+ fun testBindingAdapters_setSelection_arabicLanguage_setsSelectionCorrectly() {
+ launchActivity().use { scenario ->
+ scenario?.onActivity { activity ->
+ val testView: AutoCompleteTextView = activity.findViewById(R.id.test_autocomplete_view)
+ TextInputLayoutBindingAdapters.setLanguageSelection(testView, OppiaLanguage.ARABIC, true)
+ assertThat(testView.text.toString()).isEqualTo(
+ context.getString(R.string.arabic_localized_language_name)
+ )
+ }
+ }
+ }
+
+ private fun launchActivity():
+ ActivityScenario? {
+ val scenario = ActivityScenario.launch(
+ TextInputLayoutBindingAdaptersTestActivity.createIntent(context)
+ )
+ testCoroutineDispatchers.runCurrent()
+ return scenario
+ }
+
+ private fun setUpTestApplicationComponent() {
+ ApplicationProvider.getApplicationContext().inject(this)
+ }
+
+ // TODO(#59): Figure out a way to reuse modules instead of needing to re-declare them.
+ @Singleton
+ @Component(
+ modules = [
+ RobolectricModule::class,
+ PlatformParameterModule::class, PlatformParameterSingletonModule::class,
+ TestDispatcherModule::class, ApplicationModule::class,
+ LoggerModule::class, ContinueModule::class, FractionInputModule::class,
+ ItemSelectionInputModule::class, MultipleChoiceInputModule::class,
+ NumberWithUnitsRuleModule::class, NumericInputRuleModule::class, TextInputRuleModule::class,
+ DragDropSortInputModule::class, ImageClickInputModule::class, InteractionsModule::class,
+ GcsResourceModule::class, TestImageLoaderModule::class, ImageParsingModule::class,
+ HtmlParserEntityTypeModule::class, QuestionModule::class, TestLogReportingModule::class,
+ AccessibilityTestModule::class, LogStorageModule::class, CachingTestModule::class,
+ ExpirationMetaDataRetrieverModule::class,
+ ViewBindingShimModule::class, RatioInputModule::class, WorkManagerConfigurationModule::class,
+ ApplicationStartupListenerModule::class, LogReportWorkerModule::class,
+ HintsAndSolutionConfigModule::class, HintsAndSolutionProdModule::class,
+ FirebaseLogUploaderModule::class, FakeOppiaClockModule::class,
+ DeveloperOptionsStarterModule::class, DeveloperOptionsModule::class,
+ ExplorationStorageModule::class, NetworkModule::class, NetworkConfigProdModule::class,
+ NetworkConnectionUtilDebugModule::class, NetworkConnectionDebugUtilModule::class,
+ AssetModule::class, LocaleProdModule::class, ActivityRecreatorTestModule::class,
+ NumericExpressionInputModule::class, AlgebraicExpressionInputModule::class,
+ MathEquationInputModule::class, SplitScreenInteractionModule::class,
+ LoggingIdentifierModule::class, ApplicationLifecycleModule::class,
+ SyncStatusModule::class, MetricLogSchedulerModule::class, TestingBuildFlavorModule::class,
+ EventLoggingConfigurationModule::class, ActivityRouterModule::class,
+ CpuPerformanceSnapshotterModule::class, ExplorationProgressModule::class,
+ TestAuthenticationModule::class
+ ]
+ )
+ interface TestApplicationComponent : ApplicationComponent {
+ @Component.Builder
+ interface Builder : ApplicationComponent.Builder {
+ override fun build(): TestApplicationComponent
+ }
+
+ fun inject(textInputLayoutBindingAdaptersTest: TextInputLayoutBindingAdaptersTest)
+ }
+
+ class TestApplication : Application(), ActivityComponentFactory, ApplicationInjectorProvider {
+ private val component: TestApplicationComponent by lazy {
+ DaggerTextInputLayoutBindingAdaptersTest_TestApplicationComponent.builder()
+ .setApplication(this)
+ .build() as TestApplicationComponent
+ }
+
+ fun inject(textInputLayoutBindingAdaptersTest: TextInputLayoutBindingAdaptersTest) {
+ component.inject(textInputLayoutBindingAdaptersTest)
+ }
+
+ override fun createActivityComponent(activity: AppCompatActivity): ActivityComponent {
+ return component.getActivityComponentBuilderProvider().get().setActivity(activity).build()
+ }
+
+ override fun getApplicationInjector(): ApplicationInjector = component
+ }
+}
diff --git a/app/src/sharedTest/java/org/oppia/android/app/onboarding/CreateProfileFragmentTest.kt b/app/src/sharedTest/java/org/oppia/android/app/onboarding/CreateProfileFragmentTest.kt
index 5e9c8ada80e..c59489c20c9 100644
--- a/app/src/sharedTest/java/org/oppia/android/app/onboarding/CreateProfileFragmentTest.kt
+++ b/app/src/sharedTest/java/org/oppia/android/app/onboarding/CreateProfileFragmentTest.kt
@@ -20,6 +20,7 @@ import androidx.test.espresso.intent.Intents.intended
import androidx.test.espresso.intent.Intents.intending
import androidx.test.espresso.intent.matcher.IntentMatchers.hasAction
import androidx.test.espresso.intent.matcher.IntentMatchers.hasComponent
+import androidx.test.espresso.intent.matcher.IntentMatchers.hasExtraWithKey
import androidx.test.espresso.matcher.ViewMatchers.Visibility
import androidx.test.espresso.matcher.ViewMatchers.isDisplayed
import androidx.test.espresso.matcher.ViewMatchers.isRoot
@@ -28,13 +29,10 @@ import androidx.test.espresso.matcher.ViewMatchers.withId
import androidx.test.espresso.matcher.ViewMatchers.withText
import androidx.test.ext.junit.runners.AndroidJUnit4
import com.google.common.truth.Truth.assertThat
-import com.google.protobuf.MessageLite
import dagger.Component
-import org.hamcrest.Description
import org.hamcrest.Matcher
import org.hamcrest.Matchers.allOf
import org.hamcrest.Matchers.not
-import org.hamcrest.TypeSafeMatcher
import org.junit.After
import org.junit.Before
import org.junit.Rule
@@ -52,10 +50,14 @@ 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.CreateProfileActivityParams
import org.oppia.android.app.model.IntroActivityParams
+import org.oppia.android.app.model.ProfileId
+import org.oppia.android.app.model.ProfileType
import org.oppia.android.app.player.state.itemviewmodel.SplitScreenInteractionModule
import org.oppia.android.app.shim.ViewBindingShimModule
import org.oppia.android.app.translation.testing.ActivityRecreatorTestModule
+import org.oppia.android.app.utility.EspressoTestsMatchers.hasProtoExtra
import org.oppia.android.app.utility.OrientationChangeAction.Companion.orientationLandscape
import org.oppia.android.data.backends.gae.NetworkConfigProdModule
import org.oppia.android.data.backends.gae.NetworkModule
@@ -87,6 +89,7 @@ import org.oppia.android.domain.oppialogger.loguploader.LogReportWorkerModule
import org.oppia.android.domain.platformparameter.PlatformParameterSingletonModule
import org.oppia.android.domain.question.QuestionModule
import org.oppia.android.domain.workmanager.WorkManagerConfigurationModule
+import org.oppia.android.testing.DisableAccessibilityChecks
import org.oppia.android.testing.OppiaTestRule
import org.oppia.android.testing.TestImageLoaderModule
import org.oppia.android.testing.TestLogReportingModule
@@ -94,6 +97,7 @@ import org.oppia.android.testing.espresso.EditTextInputAction
import org.oppia.android.testing.firebase.TestAuthenticationModule
import org.oppia.android.testing.junit.InitializeDefaultLocaleRule
import org.oppia.android.testing.platformparameter.TestPlatformParameterModule
+import org.oppia.android.testing.profile.ProfileTestHelper
import org.oppia.android.testing.robolectric.RobolectricModule
import org.oppia.android.testing.threading.TestCoroutineDispatchers
import org.oppia.android.testing.threading.TestDispatcherModule
@@ -101,7 +105,7 @@ import org.oppia.android.testing.time.FakeOppiaClockModule
import org.oppia.android.util.accessibility.AccessibilityTestModule
import org.oppia.android.util.caching.AssetModule
import org.oppia.android.util.caching.testing.CachingTestModule
-import org.oppia.android.util.extensions.getProtoExtra
+import org.oppia.android.util.extensions.putProtoExtra
import org.oppia.android.util.gcsresource.GcsResourceModule
import org.oppia.android.util.locale.LocaleProdModule
import org.oppia.android.util.logging.EventLoggingConfigurationModule
@@ -113,6 +117,8 @@ import org.oppia.android.util.networking.NetworkConnectionUtilDebugModule
import org.oppia.android.util.parser.html.HtmlParserEntityTypeModule
import org.oppia.android.util.parser.image.ImageParsingModule
import org.oppia.android.util.parser.image.TestGlideImageLoader
+import org.oppia.android.util.profile.CurrentUserProfileIdIntentDecorator.decorateWithUserProfileId
+import org.oppia.android.util.profile.PROFILE_ID_INTENT_DECORATOR
import org.robolectric.annotation.Config
import org.robolectric.annotation.LooperMode
import javax.inject.Inject
@@ -128,18 +134,27 @@ import javax.inject.Singleton
qualifiers = "port-xxhdpi"
)
class CreateProfileFragmentTest {
- @get:Rule val initializeDefaultLocaleRule = InitializeDefaultLocaleRule()
- @get:Rule val oppiaTestRule = OppiaTestRule()
- @Inject lateinit var testCoroutineDispatchers: TestCoroutineDispatchers
- @Inject lateinit var context: Context
- @Inject lateinit var editTextInputAction: EditTextInputAction
- @Inject lateinit var testGlideImageLoader: TestGlideImageLoader
+ @get:Rule
+ val initializeDefaultLocaleRule = InitializeDefaultLocaleRule()
+ @get:Rule
+ val oppiaTestRule = OppiaTestRule()
+ @Inject
+ lateinit var testCoroutineDispatchers: TestCoroutineDispatchers
+ @Inject
+ lateinit var context: Context
+ @Inject
+ lateinit var editTextInputAction: EditTextInputAction
+ @Inject
+ lateinit var testGlideImageLoader: TestGlideImageLoader
+ @Inject
+ lateinit var profileTestHelper: ProfileTestHelper
@Before
fun setUp() {
Intents.init()
setUpTestApplicationComponent()
testCoroutineDispatchers.registerIdlingResource()
+ profileTestHelper.createDefaultAdminProfile()
}
@After
@@ -195,6 +210,7 @@ class CreateProfileFragmentTest {
closeSoftKeyboard()
)
testCoroutineDispatchers.runCurrent()
+
onView(withId(R.id.onboarding_navigation_continue))
.perform(click())
testCoroutineDispatchers.runCurrent()
@@ -203,13 +219,15 @@ class CreateProfileFragmentTest {
intended(
allOf(
hasComponent(IntroActivity::class.java.name),
- hasProtoExtra("OnboardingIntroActivity.params", expectedParams)
+ hasProtoExtra(IntroActivity.PARAMS_KEY, expectedParams),
+ hasExtraWithKey(PROFILE_ID_INTENT_DECORATOR)
)
)
}
}
@Test
+ @DisableAccessibilityChecks
fun testFragment_continueButtonClicked_filledNickname_doesNotShowErrorText() {
launchNewLearnerProfileActivity().use {
onView(withId(R.id.create_profile_nickname_edittext))
@@ -218,11 +236,12 @@ class CreateProfileFragmentTest {
closeSoftKeyboard()
)
testCoroutineDispatchers.runCurrent()
+
onView(withId(R.id.onboarding_navigation_continue))
.perform(click())
testCoroutineDispatchers.runCurrent()
- onView(withText(R.string.create_profile_activity_nickname_error))
+ onView(withId(R.id.create_profile_nickname_error))
.check(matches(withEffectiveVisibility(Visibility.GONE)))
}
}
@@ -244,6 +263,7 @@ class CreateProfileFragmentTest {
onView(withId(R.id.onboarding_navigation_continue))
.perform(click())
testCoroutineDispatchers.runCurrent()
+
onView(withText(R.string.create_profile_activity_nickname_error))
.check(matches(isDisplayed()))
@@ -253,6 +273,7 @@ class CreateProfileFragmentTest {
closeSoftKeyboard()
)
testCoroutineDispatchers.runCurrent()
+
onView(withId(R.id.onboarding_navigation_continue))
.perform(click())
testCoroutineDispatchers.runCurrent()
@@ -261,7 +282,8 @@ class CreateProfileFragmentTest {
intended(
allOf(
hasComponent(IntroActivity::class.java.name),
- hasProtoExtra("OnboardingIntroActivity.params", expectedParams)
+ hasProtoExtra(IntroActivity.PARAMS_KEY, expectedParams),
+ hasExtraWithKey(PROFILE_ID_INTENT_DECORATOR)
)
)
}
@@ -290,12 +312,15 @@ class CreateProfileFragmentTest {
@Test
fun testFragment_landscapeMode_filledNickname_continueButtonClicked_launchesLearnerIntroScreen() {
launchNewLearnerProfileActivity().use {
+ onView(isRoot()).perform(orientationLandscape())
+
onView(withId(R.id.create_profile_nickname_edittext))
.perform(
editTextInputAction.appendText("John"),
closeSoftKeyboard()
)
testCoroutineDispatchers.runCurrent()
+
onView(withId(R.id.onboarding_navigation_continue))
.perform(click())
testCoroutineDispatchers.runCurrent()
@@ -304,7 +329,8 @@ class CreateProfileFragmentTest {
intended(
allOf(
hasComponent(IntroActivity::class.java.name),
- hasProtoExtra("OnboardingIntroActivity.params", expectedParams)
+ hasProtoExtra(IntroActivity.PARAMS_KEY, expectedParams),
+ hasExtraWithKey(PROFILE_ID_INTENT_DECORATOR)
)
)
}
@@ -365,7 +391,8 @@ class CreateProfileFragmentTest {
intended(
allOf(
hasComponent(IntroActivity::class.java.name),
- hasProtoExtra("OnboardingIntroActivity.params", expectedParams)
+ hasProtoExtra(IntroActivity.PARAMS_KEY, expectedParams),
+ hasExtraWithKey(PROFILE_ID_INTENT_DECORATOR)
)
)
}
@@ -451,6 +478,155 @@ class CreateProfileFragmentTest {
}
}
+ @Test
+ fun testFragment_inputNameWithNumbers_showsNameOnlyLettersError() {
+ launchNewLearnerProfileActivity().use {
+ onView(withId(R.id.create_profile_nickname_edittext))
+ .perform(
+ editTextInputAction.appendText("John123"),
+ closeSoftKeyboard()
+ )
+
+ testCoroutineDispatchers.runCurrent()
+
+ onView(withId(R.id.onboarding_navigation_continue)).perform(click())
+ testCoroutineDispatchers.runCurrent()
+
+ onView(withId(R.id.create_profile_nickname_error))
+ .check(matches(withText(R.string.add_profile_error_name_only_letters)))
+ }
+ }
+
+ @Test
+ fun testFragment_landscape_inputNameWithNumbers_showsNameOnlyLettersError() {
+ launchNewLearnerProfileActivity().use {
+ onView(isRoot()).perform(orientationLandscape())
+
+ onView(withId(R.id.create_profile_nickname_edittext))
+ .perform(
+ editTextInputAction.appendText("John123"),
+ closeSoftKeyboard()
+ )
+
+ testCoroutineDispatchers.runCurrent()
+
+ onView(withId(R.id.onboarding_navigation_continue)).perform(click())
+ testCoroutineDispatchers.runCurrent()
+
+ onView(withId(R.id.create_profile_nickname_error))
+ .check(matches(withText(R.string.add_profile_error_name_only_letters)))
+ }
+ }
+
+ @Test
+ fun testFragment_inputNameWithNumbers_configChange_errorIsRetained() {
+ launchNewLearnerProfileActivity().use {
+ onView(withId(R.id.create_profile_nickname_edittext))
+ .perform(
+ editTextInputAction.appendText("John123"),
+ closeSoftKeyboard()
+ )
+ testCoroutineDispatchers.runCurrent()
+
+ onView(withId(R.id.onboarding_navigation_continue)).perform(click())
+ testCoroutineDispatchers.runCurrent()
+
+ onView(withId(R.id.create_profile_nickname_error))
+ .check(matches(withText(R.string.add_profile_error_name_only_letters)))
+
+ onView(isRoot()).perform(orientationLandscape())
+ testCoroutineDispatchers.runCurrent()
+
+ onView(withText(R.string.add_profile_error_name_only_letters))
+ .check(matches(withEffectiveVisibility(Visibility.VISIBLE)))
+ }
+ }
+
+ @Test
+ fun testFragment_inputNameWithNumbers_thenInputNameWithLetters_errorIsCleared() {
+ launchNewLearnerProfileActivity().use {
+ onView(withId(R.id.create_profile_nickname_edittext))
+ .perform(
+ editTextInputAction.appendText("John123"),
+ closeSoftKeyboard()
+ )
+ testCoroutineDispatchers.runCurrent()
+
+ onView(withId(R.id.onboarding_navigation_continue)).perform(click())
+ testCoroutineDispatchers.runCurrent()
+
+ onView(withId(R.id.create_profile_nickname_error))
+ .check(matches(withText(R.string.add_profile_error_name_only_letters)))
+
+ onView(withId(R.id.create_profile_nickname_edittext))
+ .perform(
+ editTextInputAction.appendText("John"),
+ closeSoftKeyboard()
+ )
+ testCoroutineDispatchers.runCurrent()
+
+ onView(withId(R.id.create_profile_nickname_error))
+ .check(matches(withEffectiveVisibility(Visibility.GONE)))
+ }
+ }
+
+ @Test
+ fun testFragment_inputNameWithNumbers_configChange_thenInputNameWithLetters_errorIsCleared() {
+ launchNewLearnerProfileActivity().use {
+ onView(withId(R.id.create_profile_nickname_edittext))
+ .perform(
+ editTextInputAction.appendText("John123"),
+ closeSoftKeyboard()
+ )
+ testCoroutineDispatchers.runCurrent()
+
+ onView(withId(R.id.onboarding_navigation_continue)).perform(click())
+ testCoroutineDispatchers.runCurrent()
+
+ onView(withId(R.id.create_profile_nickname_error))
+ .check(matches(withText(R.string.add_profile_error_name_only_letters)))
+
+ onView(isRoot()).perform(orientationLandscape())
+ testCoroutineDispatchers.runCurrent()
+
+ onView(withId(R.id.create_profile_nickname_edittext))
+ .perform(
+ editTextInputAction.appendText("John"),
+ closeSoftKeyboard()
+ )
+ testCoroutineDispatchers.runCurrent()
+
+ onView(withId(R.id.create_profile_nickname_error))
+ .check(matches(withEffectiveVisibility(Visibility.GONE)))
+ }
+ }
+
+ @Test
+ fun testFragment_profileTypeArgumentMissing_showsUnknownProfileTypeError() {
+ val intent = CreateProfileActivity.createProfileActivityIntent(context)
+ // Not adding the profile type intent parameter to trigger the exception.
+ intent.decorateWithUserProfileId(ProfileId.newBuilder().setInternalId(0).build())
+
+ val scenario = ActivityScenario.launch(intent)
+ testCoroutineDispatchers.runCurrent()
+
+ scenario.use {
+ onView(withId(R.id.create_profile_nickname_edittext))
+ .perform(
+ editTextInputAction.appendText("John"),
+ closeSoftKeyboard()
+ )
+
+ testCoroutineDispatchers.runCurrent()
+
+ onView(withId(R.id.onboarding_navigation_continue)).perform(click())
+ testCoroutineDispatchers.runCurrent()
+
+ onView(withId(R.id.create_profile_nickname_error))
+ .check(matches(withText(R.string.add_profile_error_missing_profile_type)))
+ }
+ }
+
private fun createGalleryPickActivityResultStub(): Instrumentation.ActivityResult {
val resources: Resources = context.resources
val imageUri = Uri.parse(
@@ -466,27 +642,19 @@ class CreateProfileFragmentTest {
private fun launchNewLearnerProfileActivity():
ActivityScenario? {
- val scenario = ActivityScenario.launch(
- CreateProfileActivity.createProfileActivityIntent(context)
+ val intent = CreateProfileActivity.createProfileActivityIntent(context)
+ intent.decorateWithUserProfileId(ProfileId.newBuilder().setInternalId(0).build())
+ intent.putProtoExtra(
+ CREATE_PROFILE_PARAMS_KEY,
+ CreateProfileActivityParams.newBuilder()
+ .setProfileType(ProfileType.SOLE_LEARNER)
+ .build()
)
+ val scenario = ActivityScenario.launch(intent)
testCoroutineDispatchers.runCurrent()
return scenario
}
- private fun hasProtoExtra(keyName: String, expectedProto: T): Matcher {
- val defaultProto = expectedProto.newBuilderForType().build()
- return object : TypeSafeMatcher() {
- override fun describeTo(description: Description) {
- description.appendText("Intent with extra: $keyName and proto value: $expectedProto")
- }
-
- override fun matchesSafely(intent: Intent): Boolean {
- return intent.hasExtra(keyName) &&
- intent.getProtoExtra(keyName, defaultProto) == expectedProto
- }
- }
- }
-
private fun setUpTestApplicationComponent() {
ApplicationProvider.getApplicationContext().inject(this)
}
diff --git a/app/src/sharedTest/java/org/oppia/android/app/onboarding/IntroActivityTest.kt b/app/src/sharedTest/java/org/oppia/android/app/onboarding/IntroActivityTest.kt
index 11ded15d116..73dbd70e492 100644
--- a/app/src/sharedTest/java/org/oppia/android/app/onboarding/IntroActivityTest.kt
+++ b/app/src/sharedTest/java/org/oppia/android/app/onboarding/IntroActivityTest.kt
@@ -110,8 +110,6 @@ class IntroActivityTest {
@Inject
lateinit var testCoroutineDispatchers: TestCoroutineDispatchers
- private val testProfileNickname = "John"
-
@Before
fun setUp() {
Intents.init()
@@ -126,10 +124,7 @@ class IntroActivityTest {
@Test
fun testActivity_createIntent_verifyScreenNameInIntent() {
val screenName =
- IntroActivity.createIntroActivity(
- context,
- testProfileNickname
- )
+ IntroActivity.createIntroActivity(context)
.extractCurrentAppScreenName()
assertThat(screenName).isEqualTo(ScreenName.INTRO_ACTIVITY)
@@ -151,10 +146,7 @@ class IntroActivityTest {
private fun launchOnboardingLearnerIntroActivity():
ActivityScenario? {
val scenario = ActivityScenario.launch(
- IntroActivity.createIntroActivity(
- context,
- testProfileNickname
- )
+ IntroActivity.createIntroActivity(context)
)
testCoroutineDispatchers.runCurrent()
return scenario
diff --git a/app/src/sharedTest/java/org/oppia/android/app/onboarding/IntroFragmentTest.kt b/app/src/sharedTest/java/org/oppia/android/app/onboarding/IntroFragmentTest.kt
index 72fea853fbc..c72f4e5721b 100644
--- a/app/src/sharedTest/java/org/oppia/android/app/onboarding/IntroFragmentTest.kt
+++ b/app/src/sharedTest/java/org/oppia/android/app/onboarding/IntroFragmentTest.kt
@@ -9,6 +9,9 @@ import androidx.test.espresso.Espresso.onView
import androidx.test.espresso.action.ViewActions.click
import androidx.test.espresso.assertion.ViewAssertions.matches
import androidx.test.espresso.intent.Intents
+import androidx.test.espresso.intent.Intents.intended
+import androidx.test.espresso.intent.matcher.IntentMatchers.hasComponent
+import androidx.test.espresso.intent.matcher.IntentMatchers.hasExtraWithKey
import androidx.test.espresso.matcher.ViewMatchers
import androidx.test.espresso.matcher.ViewMatchers.isDisplayed
import androidx.test.espresso.matcher.ViewMatchers.withId
@@ -33,6 +36,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.IntroActivityParams
+import org.oppia.android.app.options.AudioLanguageActivity
import org.oppia.android.app.player.state.itemviewmodel.SplitScreenInteractionModule
import org.oppia.android.app.shim.ViewBindingShimModule
import org.oppia.android.app.translation.testing.ActivityRecreatorTestModule
@@ -79,6 +84,7 @@ import org.oppia.android.testing.time.FakeOppiaClockModule
import org.oppia.android.util.accessibility.AccessibilityTestModule
import org.oppia.android.util.caching.AssetModule
import org.oppia.android.util.caching.testing.CachingTestModule
+import org.oppia.android.util.extensions.putProtoExtra
import org.oppia.android.util.gcsresource.GcsResourceModule
import org.oppia.android.util.locale.LocaleProdModule
import org.oppia.android.util.logging.EventLoggingConfigurationModule
@@ -90,6 +96,7 @@ import org.oppia.android.util.networking.NetworkConnectionUtilDebugModule
import org.oppia.android.util.parser.html.HtmlParserEntityTypeModule
import org.oppia.android.util.parser.image.GlideImageLoaderModule
import org.oppia.android.util.parser.image.ImageParsingModule
+import org.oppia.android.util.profile.PROFILE_ID_INTENT_DECORATOR
import org.robolectric.annotation.Config
import org.robolectric.annotation.LooperMode
import javax.inject.Inject
@@ -185,21 +192,8 @@ class IntroFragmentTest {
onView(withId(R.id.onboarding_navigation_continue)).perform(click())
testCoroutineDispatchers.runCurrent()
- // Do nothing for now, but will fail once navigation is implemented
- onView(withId(R.id.onboarding_learner_intro_title))
- .check(matches(withText("Welcome, John!")))
- onView(withText(R.string.onboarding_learner_intro_classroom_text))
- .check(matches(isDisplayed()))
- onView(withText(R.string.onboarding_learner_intro_practice_text))
- .check(matches(isDisplayed()))
- onView(
- withText(
- context.getString(
- R.string.onboarding_learner_intro_feedback_text,
- context.getString(R.string.app_name)
- )
- )
- ).check(matches(isDisplayed()))
+ intended(hasComponent(AudioLanguageActivity::class.java.name))
+ intended(hasExtraWithKey(PROFILE_ID_INTENT_DECORATOR))
}
}
@@ -211,31 +205,21 @@ class IntroFragmentTest {
onView(withId(R.id.onboarding_navigation_continue)).perform(click())
testCoroutineDispatchers.runCurrent()
- // Do nothing for now, but will fail once navigation is implemented
- onView(withId(R.id.onboarding_learner_intro_title))
- .check(matches(withText("Welcome, John!")))
- onView(withText(R.string.onboarding_learner_intro_classroom_text))
- .check(matches(isDisplayed()))
- onView(withText(R.string.onboarding_learner_intro_practice_text))
- .check(matches(isDisplayed()))
- onView(
- withText(
- context.getString(
- R.string.onboarding_learner_intro_feedback_text,
- context.getString(R.string.app_name)
- )
- )
- ).check(matches(isDisplayed()))
+ intended(hasComponent(AudioLanguageActivity::class.java.name))
+ intended(hasExtraWithKey(PROFILE_ID_INTENT_DECORATOR))
}
}
private fun launchOnboardingLearnerIntroActivity():
ActivityScenario? {
+ val params = IntroActivityParams.newBuilder()
+ .setProfileNickname(testProfileNickname)
+ .build()
+
val scenario = ActivityScenario.launch(
- IntroActivity.createIntroActivity(
- context,
- testProfileNickname
- )
+ IntroActivity.createIntroActivity(context).apply {
+ putProtoExtra(IntroActivity.PARAMS_KEY, params)
+ }
)
testCoroutineDispatchers.runCurrent()
return scenario
diff --git a/app/src/sharedTest/java/org/oppia/android/app/onboarding/OnboardingFragmentTest.kt b/app/src/sharedTest/java/org/oppia/android/app/onboarding/OnboardingFragmentTest.kt
index 46f1b08fe88..b7c3f0f5231 100644
--- a/app/src/sharedTest/java/org/oppia/android/app/onboarding/OnboardingFragmentTest.kt
+++ b/app/src/sharedTest/java/org/oppia/android/app/onboarding/OnboardingFragmentTest.kt
@@ -5,8 +5,10 @@ import android.content.Context
import android.content.res.Resources
import android.view.View
import androidx.appcompat.app.AppCompatActivity
+import androidx.core.view.ViewCompat
import androidx.test.core.app.ActivityScenario.launch
import androidx.test.core.app.ApplicationProvider
+import androidx.test.espresso.Espresso.onData
import androidx.test.espresso.Espresso.onView
import androidx.test.espresso.UiController
import androidx.test.espresso.ViewAction
@@ -19,6 +21,8 @@ import androidx.test.espresso.assertion.ViewAssertions.matches
import androidx.test.espresso.intent.Intents
import androidx.test.espresso.intent.Intents.intended
import androidx.test.espresso.intent.matcher.IntentMatchers.hasComponent
+import androidx.test.espresso.intent.matcher.IntentMatchers.hasExtraWithKey
+import androidx.test.espresso.matcher.RootMatchers.withDecorView
import androidx.test.espresso.matcher.ViewMatchers.isAssignableFrom
import androidx.test.espresso.matcher.ViewMatchers.isCompletelyDisplayed
import androidx.test.espresso.matcher.ViewMatchers.isDisplayed
@@ -29,10 +33,13 @@ import androidx.test.espresso.matcher.ViewMatchers.withId
import androidx.test.espresso.matcher.ViewMatchers.withText
import androidx.test.ext.junit.runners.AndroidJUnit4
import androidx.viewpager2.widget.ViewPager2
+import com.google.common.truth.Truth.assertThat
import dagger.Component
+import org.hamcrest.CoreMatchers.`is`
import org.hamcrest.CoreMatchers.allOf
import org.hamcrest.CoreMatchers.not
import org.hamcrest.Matcher
+import org.hamcrest.core.IsInstanceOf.instanceOf
import org.junit.After
import org.junit.Rule
import org.junit.Test
@@ -49,6 +56,7 @@ 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.OppiaLanguage
import org.oppia.android.app.player.state.itemviewmodel.SplitScreenInteractionModule
import org.oppia.android.app.profile.ProfileChooserActivity
import org.oppia.android.app.shim.ViewBindingShimModule
@@ -85,9 +93,13 @@ import org.oppia.android.domain.oppialogger.loguploader.LogReportWorkerModule
import org.oppia.android.domain.platformparameter.PlatformParameterSingletonModule
import org.oppia.android.domain.question.QuestionModule
import org.oppia.android.domain.workmanager.WorkManagerConfigurationModule
+import org.oppia.android.testing.BuildEnvironment
import org.oppia.android.testing.OppiaTestRule
+import org.oppia.android.testing.RunOn
import org.oppia.android.testing.TestLogReportingModule
+import org.oppia.android.testing.TestPlatform
import org.oppia.android.testing.firebase.TestAuthenticationModule
+import org.oppia.android.testing.junit.DefineAppLanguageLocaleContext
import org.oppia.android.testing.junit.InitializeDefaultLocaleRule
import org.oppia.android.testing.platformparameter.TestPlatformParameterModule
import org.oppia.android.testing.robolectric.RobolectricModule
@@ -97,7 +109,6 @@ import org.oppia.android.testing.time.FakeOppiaClockModule
import org.oppia.android.util.accessibility.AccessibilityTestModule
import org.oppia.android.util.caching.AssetModule
import org.oppia.android.util.caching.testing.CachingTestModule
-import org.oppia.android.util.gcsresource.DefaultResourceBucketName
import org.oppia.android.util.gcsresource.GcsResourceModule
import org.oppia.android.util.locale.LocaleProdModule
import org.oppia.android.util.logging.EventLoggingConfigurationModule
@@ -106,12 +117,13 @@ import org.oppia.android.util.logging.SyncStatusModule
import org.oppia.android.util.logging.firebase.FirebaseLogUploaderModule
import org.oppia.android.util.networking.NetworkConnectionDebugUtilModule
import org.oppia.android.util.networking.NetworkConnectionUtilDebugModule
-import org.oppia.android.util.parser.html.HtmlParser
import org.oppia.android.util.parser.html.HtmlParserEntityTypeModule
import org.oppia.android.util.parser.image.GlideImageLoaderModule
import org.oppia.android.util.parser.image.ImageParsingModule
+import org.oppia.android.util.profile.PROFILE_ID_INTENT_DECORATOR
import org.robolectric.annotation.Config
import org.robolectric.annotation.LooperMode
+import java.util.Locale
import javax.inject.Inject
import javax.inject.Singleton
@@ -132,19 +144,12 @@ class OnboardingFragmentTest {
@Inject
lateinit var testCoroutineDispatchers: TestCoroutineDispatchers
- @Inject
- lateinit var htmlParserFactory: HtmlParser.Factory
-
@Inject
lateinit var context: Context
@Inject
lateinit var appLanguageLocaleHandler: AppLanguageLocaleHandler
- @Inject
- @field:DefaultResourceBucketName
- lateinit var resourceBucketName: String
-
@After
fun tearDown() {
testCoroutineDispatchers.unregisterIdlingResource()
@@ -792,6 +797,299 @@ class OnboardingFragmentTest {
}
}
+ @Test
+ @RunOn(TestPlatform.ROBOLECTRIC, buildEnvironments = [BuildEnvironment.BAZEL])
+ fun testOnboardingFragment_onboardingV2Enabled_englishLocale_englishIsPreselected() {
+ setUpTestWithOnboardingV2Enabled()
+
+ launch(OnboardingActivity::class.java).use {
+ testCoroutineDispatchers.runCurrent()
+
+ // Verify that the display locale is set up correctly (for string formatting).
+ val displayLocale = appLanguageLocaleHandler.getDisplayLocale()
+ val localeContext = displayLocale.localeContext
+ assertThat(localeContext.languageDefinition.language)
+ .isEqualTo(OppiaLanguage.ENGLISH)
+
+ onView(withId(R.id.onboarding_language_dropdown)).check(
+ matches(withText(R.string.english_localized_language_name))
+ )
+ }
+ }
+
+ @Test
+ fun testOnboardingFragment_onboardingV2Enabled_englishLocale_layoutIsLtr() {
+ setUpTestWithOnboardingV2Enabled()
+ launch(OnboardingActivity::class.java).use {
+ testCoroutineDispatchers.runCurrent()
+
+ val displayLocale = appLanguageLocaleHandler.getDisplayLocale()
+ val layoutDirection = displayLocale.getLayoutDirection()
+ assertThat(layoutDirection).isEqualTo(ViewCompat.LAYOUT_DIRECTION_LTR)
+ }
+ }
+
+ @Test
+ @DefineAppLanguageLocaleContext(
+ oppiaLanguageEnumId = OppiaLanguage.ARABIC_VALUE,
+ appStringIetfTag = "ar",
+ appStringAndroidLanguageId = "ar"
+ )
+ @RunOn(TestPlatform.ROBOLECTRIC, buildEnvironments = [BuildEnvironment.BAZEL])
+ fun testOnboardingFragment_onboardingV2Enabled_arabicLocale_arabicIsPreselected() {
+ setUpTestWithOnboardingV2Enabled()
+ forceDefaultLocale(EGYPT_ARABIC_LOCALE)
+ launch(OnboardingActivity::class.java).use {
+ testCoroutineDispatchers.runCurrent()
+
+ // Verify that the display locale is set up correctly (for string formatting).
+ val displayLocale = appLanguageLocaleHandler.getDisplayLocale()
+ val localeContext = displayLocale.localeContext
+ assertThat(localeContext.languageDefinition.language)
+ .isEqualTo(OppiaLanguage.ARABIC)
+
+ onView(withId(R.id.onboarding_language_dropdown)).check(
+ matches(withText(R.string.arabic_localized_language_name))
+ )
+ }
+ }
+
+ @Test
+ @DefineAppLanguageLocaleContext(
+ oppiaLanguageEnumId = OppiaLanguage.ARABIC_VALUE,
+ appStringIetfTag = "ar",
+ appStringAndroidLanguageId = "ar"
+ )
+ @RunOn(TestPlatform.ROBOLECTRIC)
+ fun testOnboardingFragment_onboardingV2Enabled_arabicLocale_layoutIsRtl() {
+ setUpTestWithOnboardingV2Enabled()
+ forceDefaultLocale(EGYPT_ARABIC_LOCALE)
+ launch(OnboardingActivity::class.java).use {
+ testCoroutineDispatchers.runCurrent()
+
+ val displayLocale = appLanguageLocaleHandler.getDisplayLocale()
+ val layoutDirection = displayLocale.getLayoutDirection()
+ assertThat(layoutDirection).isEqualTo(ViewCompat.LAYOUT_DIRECTION_RTL)
+ }
+ }
+
+ @Test
+ @DefineAppLanguageLocaleContext(
+ oppiaLanguageEnumId = OppiaLanguage.BRAZILIAN_PORTUGUESE_VALUE,
+ appStringIetfTag = "pt-BR",
+ appStringAndroidLanguageId = "pt",
+ appStringAndroidRegionId = "BR"
+ )
+ @RunOn(TestPlatform.ROBOLECTRIC, buildEnvironments = [BuildEnvironment.BAZEL])
+ fun testOnboardingFragment_onboardingV2Enabled_portugueseLocale_portugueseIsPreselected() {
+ setUpTestWithOnboardingV2Enabled()
+ forceDefaultLocale(BRAZIL_PORTUGUESE_LOCALE)
+ launch(OnboardingActivity::class.java).use {
+ testCoroutineDispatchers.runCurrent()
+
+ // Verify that the display locale is set up correctly (for string formatting).
+ val displayLocale = appLanguageLocaleHandler.getDisplayLocale()
+ val localeContext = displayLocale.localeContext
+ assertThat(localeContext.languageDefinition.language)
+ .isEqualTo(OppiaLanguage.BRAZILIAN_PORTUGUESE)
+
+ onView(withId(R.id.onboarding_language_dropdown)).check(
+ matches(withText(R.string.portuguese_localized_language_name))
+ )
+ }
+ }
+
+ @Test
+ @DefineAppLanguageLocaleContext(
+ oppiaLanguageEnumId = OppiaLanguage.BRAZILIAN_PORTUGUESE_VALUE,
+ appStringIetfTag = "pt-BR",
+ appStringAndroidLanguageId = "pt",
+ appStringAndroidRegionId = "BR"
+ )
+ @RunOn(TestPlatform.ROBOLECTRIC)
+ fun testOnboardingFragment_onboardingV2Enabled_portugueseLocale_layoutIsLtr() {
+ setUpTestWithOnboardingV2Enabled()
+ forceDefaultLocale(BRAZIL_PORTUGUESE_LOCALE)
+ launch(OnboardingActivity::class.java).use {
+ testCoroutineDispatchers.runCurrent()
+
+ val displayLocale = appLanguageLocaleHandler.getDisplayLocale()
+ val layoutDirection = displayLocale.getLayoutDirection()
+ assertThat(layoutDirection).isEqualTo(ViewCompat.LAYOUT_DIRECTION_LTR)
+ }
+ }
+
+ @Test
+ @DefineAppLanguageLocaleContext(
+ oppiaLanguageEnumId = OppiaLanguage.NIGERIAN_PIDGIN_VALUE,
+ appStringIetfTag = "pcm",
+ appStringAndroidLanguageId = "pcm",
+ appStringAndroidRegionId = "NG"
+ )
+ @RunOn(TestPlatform.ROBOLECTRIC, buildEnvironments = [BuildEnvironment.BAZEL])
+ fun testOnboardingFragment_onboardingV2Enabled_nigeriaLocale_naijaIsPreselected() {
+ setUpTestWithOnboardingV2Enabled()
+ forceDefaultLocale(NIGERIA_NAIJA_LOCALE)
+ launch(OnboardingActivity::class.java).use {
+ testCoroutineDispatchers.runCurrent()
+
+ // Verify that the display locale is set up correctly (for string formatting).
+ val displayLocale = appLanguageLocaleHandler.getDisplayLocale()
+ val localeContext = displayLocale.localeContext
+ assertThat(localeContext.languageDefinition.language)
+ .isEqualTo(OppiaLanguage.NIGERIAN_PIDGIN)
+
+ onView(withId(R.id.onboarding_language_dropdown)).check(
+ matches(withText(R.string.nigerian_pidgin_localized_language_name))
+ )
+ }
+ }
+
+ @Test
+ @DefineAppLanguageLocaleContext(
+ oppiaLanguageEnumId = OppiaLanguage.NIGERIAN_PIDGIN_VALUE,
+ appStringIetfTag = "pcm",
+ appStringAndroidLanguageId = "pcm",
+ appStringAndroidRegionId = "NG"
+ )
+ @RunOn(TestPlatform.ROBOLECTRIC)
+ fun testOnboardingFragment_onboardingV2Enabled_nigeriaLocale_layoutIsLtr() {
+ setUpTestWithOnboardingV2Enabled()
+ forceDefaultLocale(NIGERIA_NAIJA_LOCALE)
+ launch(OnboardingActivity::class.java).use {
+ testCoroutineDispatchers.runCurrent()
+
+ val displayLocale = appLanguageLocaleHandler.getDisplayLocale()
+ val layoutDirection = displayLocale.getLayoutDirection()
+ assertThat(layoutDirection).isEqualTo(ViewCompat.LAYOUT_DIRECTION_LTR)
+ }
+ }
+
+ @Test
+ @DefineAppLanguageLocaleContext(
+ oppiaLanguageEnumId = OppiaLanguage.LANGUAGE_UNSPECIFIED_VALUE,
+ appStringIetfTag = "fr",
+ appStringAndroidLanguageId = "fr-CA",
+ appStringAndroidRegionId = "CA"
+ )
+ @RunOn(TestPlatform.ROBOLECTRIC, buildEnvironments = [BuildEnvironment.BAZEL])
+ fun testOnboardingFragment_onboardingV2Enabled_unsupportedLocale_englishIsPreselected() {
+ setUpTestWithOnboardingV2Enabled()
+ forceDefaultLocale(CANADA_FRENCH_LOCALE)
+ launch(OnboardingActivity::class.java).use {
+ testCoroutineDispatchers.runCurrent()
+
+ // Verify that the display locale is set up correctly (for string formatting).
+ val displayLocale = appLanguageLocaleHandler.getDisplayLocale()
+ val localeContext = displayLocale.localeContext
+ assertThat(localeContext.languageDefinition.language)
+ .isEqualTo(OppiaLanguage.LANGUAGE_UNSPECIFIED)
+
+ onView(withId(R.id.onboarding_language_dropdown)).check(
+ matches(withText(R.string.english_localized_language_name))
+ )
+ }
+ }
+
+ @Test
+ fun testFragment_onboardingV2Enabled_clickLetsGoButton_launchesProfileTypeScreen() {
+ setUpTestWithOnboardingV2Enabled()
+ launch(OnboardingActivity::class.java).use {
+ testCoroutineDispatchers.runCurrent()
+ // Verifies that the default language selection is set if the user does not make a selection.
+ onView(withId(R.id.onboarding_language_lets_go_button)).perform(click())
+ testCoroutineDispatchers.runCurrent()
+ intended(hasComponent(OnboardingProfileTypeActivity::class.java.name))
+ intended(hasExtraWithKey(PROFILE_ID_INTENT_DECORATOR))
+ }
+ }
+
+ @Test
+ @RunOn(TestPlatform.ROBOLECTRIC, buildEnvironments = [BuildEnvironment.BAZEL])
+ fun testFragment_onboardingV2_languageSelectionChanged_languageIsUpdated() {
+ setUpTestWithOnboardingV2Enabled()
+ launch(OnboardingActivity::class.java).use { scenario ->
+ testCoroutineDispatchers.runCurrent()
+
+ scenario.onActivity { activity ->
+ onView(withId(R.id.onboarding_language_dropdown)).perform(click())
+
+ onData(allOf(`is`(instanceOf(String::class.java)), `is`("Naijá")))
+ .inRoot(withDecorView(not(`is`(activity.window.decorView))))
+ .perform(click())
+
+ testCoroutineDispatchers.runCurrent()
+
+ onView(withId(R.id.onboarding_language_dropdown)).check(
+ matches(withText(R.string.nigerian_pidgin_localized_language_name))
+ )
+
+ onView(withId(R.id.onboarding_language_lets_go_button)).perform(click())
+ testCoroutineDispatchers.runCurrent()
+ intended(hasComponent(OnboardingProfileTypeActivity::class.java.name))
+ intended(hasExtraWithKey(PROFILE_ID_INTENT_DECORATOR))
+ }
+ }
+ }
+
+ @Test
+ @RunOn(TestPlatform.ROBOLECTRIC, buildEnvironments = [BuildEnvironment.BAZEL])
+ fun testFragment_onboardingV2_languageSelectionChanged_configChange_languageIsUpdated() {
+ setUpTestWithOnboardingV2Enabled()
+ launch(OnboardingActivity::class.java).use { scenario ->
+ testCoroutineDispatchers.runCurrent()
+
+ scenario.onActivity { activity ->
+ onView(withId(R.id.onboarding_language_dropdown)).perform(click())
+
+ onData(allOf(`is`(instanceOf(String::class.java)), `is`("Naijá")))
+ .inRoot(withDecorView(not(`is`(activity.window.decorView))))
+ .perform(click())
+
+ onView(isRoot()).perform(orientationLandscape())
+
+ testCoroutineDispatchers.runCurrent()
+
+ // Verifies that the selected language is still set successfully after configuration change.
+ onView(withId(R.id.onboarding_language_lets_go_button)).perform(click())
+ testCoroutineDispatchers.runCurrent()
+ intended(hasComponent(OnboardingProfileTypeActivity::class.java.name))
+ intended(hasExtraWithKey(PROFILE_ID_INTENT_DECORATOR))
+ }
+ }
+ }
+
+ @Test
+ @RunOn(TestPlatform.ROBOLECTRIC, buildEnvironments = [BuildEnvironment.BAZEL])
+ fun testFragment_onboardingV2_orientationChange_languageSelectionIsRestored() {
+ setUpTestWithOnboardingV2Enabled()
+ launch(OnboardingActivity::class.java).use { scenario ->
+ testCoroutineDispatchers.runCurrent()
+
+ scenario.onActivity { activity ->
+ onView(withId(R.id.onboarding_language_dropdown)).perform(click())
+
+ onData(allOf(`is`(instanceOf(String::class.java)), `is`("Naijá")))
+ .inRoot(withDecorView(not(`is`(activity.window.decorView))))
+ .perform(click())
+
+ testCoroutineDispatchers.runCurrent()
+
+ onView(isRoot()).perform(orientationLandscape())
+ testCoroutineDispatchers.runCurrent()
+
+ onView(withId(R.id.onboarding_language_dropdown)).check(
+ matches(withText(R.string.nigerian_pidgin_localized_language_name))
+ )
+ }
+ }
+ }
+
+ private fun forceDefaultLocale(locale: Locale) {
+ context.applicationContext.resources.configuration.setLocale(locale)
+ Locale.setDefault(locale)
+ }
+
private fun setUpTestWithOnboardingV2Disabled() {
TestPlatformParameterModule.forceEnableOnboardingFlowV2(false)
setUp()
@@ -890,4 +1188,11 @@ class OnboardingFragmentTest {
override fun getApplicationInjector(): ApplicationInjector = component
}
+
+ private companion object {
+ private val BRAZIL_PORTUGUESE_LOCALE = Locale("pt", "BR")
+ private val EGYPT_ARABIC_LOCALE = Locale("ar", "EG")
+ private val NIGERIA_NAIJA_LOCALE = Locale("pcm", "NG")
+ private val CANADA_FRENCH_LOCALE = Locale("fr", "CA")
+ }
}
diff --git a/app/src/sharedTest/java/org/oppia/android/app/onboarding/OnboardingProfileTypeFragmentTest.kt b/app/src/sharedTest/java/org/oppia/android/app/onboarding/OnboardingProfileTypeFragmentTest.kt
index 2649e11c610..fbeb04c4f11 100644
--- a/app/src/sharedTest/java/org/oppia/android/app/onboarding/OnboardingProfileTypeFragmentTest.kt
+++ b/app/src/sharedTest/java/org/oppia/android/app/onboarding/OnboardingProfileTypeFragmentTest.kt
@@ -11,6 +11,7 @@ import androidx.test.espresso.assertion.ViewAssertions.matches
import androidx.test.espresso.intent.Intents
import androidx.test.espresso.intent.Intents.intended
import androidx.test.espresso.intent.matcher.IntentMatchers.hasComponent
+import androidx.test.espresso.intent.matcher.IntentMatchers.hasExtraWithKey
import androidx.test.espresso.matcher.ViewMatchers.hasDescendant
import androidx.test.espresso.matcher.ViewMatchers.isDisplayed
import androidx.test.espresso.matcher.ViewMatchers.isRoot
@@ -37,10 +38,13 @@ 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.CreateProfileActivityParams
+import org.oppia.android.app.model.ProfileType
import org.oppia.android.app.player.state.itemviewmodel.SplitScreenInteractionModule
import org.oppia.android.app.profile.ProfileChooserActivity
import org.oppia.android.app.shim.ViewBindingShimModule
import org.oppia.android.app.translation.testing.ActivityRecreatorTestModule
+import org.oppia.android.app.utility.EspressoTestsMatchers.hasProtoExtra
import org.oppia.android.app.utility.OrientationChangeAction.Companion.orientationLandscape
import org.oppia.android.data.backends.gae.NetworkConfigProdModule
import org.oppia.android.data.backends.gae.NetworkModule
@@ -96,6 +100,7 @@ import org.oppia.android.util.networking.NetworkConnectionUtilDebugModule
import org.oppia.android.util.parser.html.HtmlParserEntityTypeModule
import org.oppia.android.util.parser.image.GlideImageLoaderModule
import org.oppia.android.util.parser.image.ImageParsingModule
+import org.oppia.android.util.profile.PROFILE_ID_INTENT_DECORATOR
import org.robolectric.annotation.Config
import org.robolectric.annotation.LooperMode
import javax.inject.Inject
@@ -257,12 +262,14 @@ class OnboardingProfileTypeFragmentTest {
launchOnboardingProfileTypeActivity().use {
onView(withId(R.id.profile_type_learner_navigation_card)).perform(click())
testCoroutineDispatchers.runCurrent()
- // Does nothing for now, but should fail once navigation is implemented in a future PR.
- onView(withId(R.id.profile_type_learner_navigation_card))
- .check(matches(isDisplayed()))
- onView(withId(R.id.profile_type_supervisor_navigation_card))
- .check(matches(isDisplayed()))
+ val params = CreateProfileActivityParams.newBuilder()
+ .setProfileType(ProfileType.SOLE_LEARNER)
+ .build()
+
+ intended(hasComponent(CreateProfileActivity::class.java.name))
+ intended(hasExtraWithKey(PROFILE_ID_INTENT_DECORATOR))
+ intended(hasProtoExtra(CREATE_PROFILE_PARAMS_KEY, params))
}
}
@@ -271,9 +278,17 @@ class OnboardingProfileTypeFragmentTest {
launchOnboardingProfileTypeActivity().use {
onView(isRoot()).perform(orientationLandscape())
testCoroutineDispatchers.runCurrent()
+
onView(withId(R.id.profile_type_learner_navigation_card)).perform(click())
testCoroutineDispatchers.runCurrent()
+
+ val params = CreateProfileActivityParams.newBuilder()
+ .setProfileType(ProfileType.SOLE_LEARNER)
+ .build()
+
intended(hasComponent(CreateProfileActivity::class.java.name))
+ intended(hasExtraWithKey(PROFILE_ID_INTENT_DECORATOR))
+ intended(hasProtoExtra(CREATE_PROFILE_PARAMS_KEY, params))
}
}
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 b8aed771aaa..8195ac0f683 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
@@ -6,10 +6,14 @@ 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.onData
import androidx.test.espresso.Espresso.onView
import androidx.test.espresso.action.ViewActions.click
import androidx.test.espresso.assertion.ViewAssertions.matches
import androidx.test.espresso.intent.Intents
+import androidx.test.espresso.intent.Intents.intended
+import androidx.test.espresso.intent.matcher.IntentMatchers.hasComponent
+import androidx.test.espresso.matcher.RootMatchers.withDecorView
import androidx.test.espresso.matcher.ViewMatchers.Visibility
import androidx.test.espresso.matcher.ViewMatchers.isChecked
import androidx.test.espresso.matcher.ViewMatchers.isRoot
@@ -19,6 +23,11 @@ import androidx.test.espresso.matcher.ViewMatchers.withText
import androidx.test.ext.junit.runners.AndroidJUnit4
import com.google.common.truth.Truth.assertThat
import dagger.Component
+import org.hamcrest.CoreMatchers
+import org.hamcrest.CoreMatchers.`is`
+import org.hamcrest.CoreMatchers.instanceOf
+import org.hamcrest.CoreMatchers.not
+import org.hamcrest.core.AllOf.allOf
import org.junit.After
import org.junit.Rule
import org.junit.Test
@@ -35,6 +44,7 @@ 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.home.HomeActivity
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
@@ -75,8 +85,11 @@ import org.oppia.android.domain.oppialogger.loguploader.LogReportWorkerModule
import org.oppia.android.domain.platformparameter.PlatformParameterSingletonModule
import org.oppia.android.domain.question.QuestionModule
import org.oppia.android.domain.workmanager.WorkManagerConfigurationModule
+import org.oppia.android.testing.BuildEnvironment
import org.oppia.android.testing.OppiaTestRule
+import org.oppia.android.testing.RunOn
import org.oppia.android.testing.TestLogReportingModule
+import org.oppia.android.testing.TestPlatform
import org.oppia.android.testing.firebase.TestAuthenticationModule
import org.oppia.android.testing.junit.InitializeDefaultLocaleRule
import org.oppia.android.testing.platformparameter.TestPlatformParameterModule
@@ -311,22 +324,13 @@ class AudioLanguageFragmentTest {
launch(
createDefaultAudioActivityIntent(ENGLISH_AUDIO_LANGUAGE)
).use {
+ testCoroutineDispatchers.runCurrent()
+
onView(withId(R.id.onboarding_navigation_continue)).perform(click())
testCoroutineDispatchers.runCurrent()
- // Do nothing for now, but will fail once navigation is implemented
- onView(withId(R.id.audio_language_text)).check(
- matches(withText("In Oppia, you can listen to lessons!"))
- )
- onView(withId(R.id.audio_language_subtitle)).check(
- matches(withText(context.getString(R.string.audio_language_fragment_subtitle)))
- )
- onView(withId(R.id.onboarding_navigation_back)).check(
- matches(withEffectiveVisibility(Visibility.VISIBLE))
- )
- onView(withId(R.id.onboarding_navigation_continue)).check(
- matches(withEffectiveVisibility(Visibility.VISIBLE))
- )
+ // Verifies that accepting the default language selection works correctly.
+ intended(hasComponent(HomeActivity::class.java.name))
}
}
@@ -341,19 +345,73 @@ class AudioLanguageFragmentTest {
onView(withId(R.id.onboarding_navigation_continue)).perform(click())
testCoroutineDispatchers.runCurrent()
- // Do nothing for now, but will fail once navigation is implemented
- onView(withId(R.id.audio_language_text)).check(
- matches(withText("In Oppia, you can listen to lessons!"))
- )
- onView(withId(R.id.audio_language_subtitle)).check(
- matches(withText(context.getString(R.string.audio_language_fragment_subtitle)))
- )
- onView(withId(R.id.onboarding_navigation_back)).check(
- matches(withEffectiveVisibility(Visibility.VISIBLE))
- )
- onView(withId(R.id.onboarding_navigation_continue)).check(
- matches(withEffectiveVisibility(Visibility.VISIBLE))
- )
+ // Verifies that accepting the default language selection works correctly.
+ intended(hasComponent(HomeActivity::class.java.name))
+ }
+ }
+
+ @Test
+ @RunOn(TestPlatform.ROBOLECTRIC, buildEnvironments = [BuildEnvironment.BAZEL])
+ fun testFragment_languageSelectionChanged_selectionIsUpdated() {
+ initializeTestApplicationComponent(enableOnboardingFlowV2 = true)
+ launch(
+ createDefaultAudioActivityIntent(ENGLISH_AUDIO_LANGUAGE)
+ ).use { scenario ->
+ testCoroutineDispatchers.runCurrent()
+
+ scenario.onActivity { activity ->
+ onView(withId(R.id.audio_language_dropdown_list)).perform(click())
+
+ onData(allOf(`is`(instanceOf(String::class.java)), `is`("Naijá")))
+ .inRoot(withDecorView(not(`is`(activity.window.decorView))))
+ .perform(click())
+
+ testCoroutineDispatchers.runCurrent()
+
+ onView(withId(R.id.audio_language_dropdown_list)).check(
+ matches(withText(R.string.nigerian_pidgin_localized_language_name))
+ )
+
+ onView(withId(R.id.onboarding_navigation_continue)).perform(click())
+ testCoroutineDispatchers.runCurrent()
+ intended(hasComponent(HomeActivity::class.java.name))
+ }
+ }
+ }
+
+ @Test
+ @RunOn(TestPlatform.ROBOLECTRIC, buildEnvironments = [BuildEnvironment.BAZEL])
+ fun testFragment_languageSelectionChanged_configChange_selectionIsUpdated() {
+ initializeTestApplicationComponent(enableOnboardingFlowV2 = true)
+ launch(
+ createDefaultAudioActivityIntent(ENGLISH_AUDIO_LANGUAGE)
+ ).use { scenario ->
+ testCoroutineDispatchers.runCurrent()
+
+ scenario.onActivity { activity ->
+ onView(withId(R.id.audio_language_dropdown_list)).perform(click())
+
+ onData(
+ CoreMatchers.allOf(
+ `is`(instanceOf(String::class.java)), `is`("Naijá")
+ )
+ )
+ .inRoot(withDecorView(not(`is`(activity.window.decorView))))
+ .perform(click())
+
+ onView(isRoot()).perform(orientationLandscape())
+ testCoroutineDispatchers.runCurrent()
+
+ // Verifies that the selected language is still set successfully after configuration change.
+ onView(withId(R.id.audio_language_dropdown_list)).check(
+ matches(withText(R.string.nigerian_pidgin_localized_language_name))
+ )
+
+ onView(withId(R.id.onboarding_navigation_continue)).perform(click())
+ testCoroutineDispatchers.runCurrent()
+
+ intended(hasComponent(HomeActivity::class.java.name))
+ }
}
}
@@ -512,9 +570,7 @@ class AudioLanguageFragmentTest {
)
interface TestApplicationComponent : ApplicationComponent {
@Component.Builder
- interface Builder : ApplicationComponent.Builder {
- override fun build(): TestApplicationComponent
- }
+ interface Builder : ApplicationComponent.Builder
fun inject(audioLanguageFragmentTest: AudioLanguageFragmentTest)
}
diff --git a/domain/src/main/java/org/oppia/android/domain/profile/ProfileManagementController.kt b/domain/src/main/java/org/oppia/android/domain/profile/ProfileManagementController.kt
index 983caebf6db..95438d0b9d0 100644
--- a/domain/src/main/java/org/oppia/android/domain/profile/ProfileManagementController.kt
+++ b/domain/src/main/java/org/oppia/android/domain/profile/ProfileManagementController.kt
@@ -16,6 +16,7 @@ import org.oppia.android.app.model.Profile
import org.oppia.android.app.model.ProfileAvatar
import org.oppia.android.app.model.ProfileDatabase
import org.oppia.android.app.model.ProfileId
+import org.oppia.android.app.model.ProfileType
import org.oppia.android.app.model.ReadingTextSize
import org.oppia.android.data.persistence.PersistentCacheStore
import org.oppia.android.data.persistence.PersistentCacheStore.PublishMode
@@ -78,6 +79,8 @@ private const val SET_LAST_SELECTED_CLASSROOM_ID_PROVIDER_ID =
"set_last_selected_classroom_id_provider_id"
private const val RETRIEVE_LAST_SELECTED_CLASSROOM_ID_PROVIDER_ID =
"retrieve_last_selected_classroom_id_provider_id"
+private const val UPDATE_PROFILE_DETAILS_PROVIDER_ID = "update_profile_details_data_provider_id"
+private const val UPDATE_PROFILE_TYPE_PROVIDER_ID = "update_profile_type_data_provider_id"
/** Controller for retrieving, adding, updating, and deleting profiles. */
@Singleton
@@ -112,7 +115,7 @@ class ProfileManagementController @Inject constructor(
/** Indicates that the selected image was not stored properly. */
class FailedToStoreImageException(msg: String) : Exception(msg)
- /** Indicates that the profile's directory was not delete properly. */
+ /** Indicates that the profile's directory was not deleted properly. */
class FailedToDeleteDirException(msg: String) : Exception(msg)
/** Indicates that the given profileId is not associated with an existing profile. */
@@ -124,6 +127,9 @@ class ProfileManagementController @Inject constructor(
/** Indicates that the Profile already has admin. */
class ProfileAlreadyHasAdminException(msg: String) : Exception(msg)
+ /** Indicates that the a ProfileType was not passed. */
+ class UnknownProfileTypeException(msg: String) : Exception(msg)
+
/** Indicates that the there is not device settings currently. */
class DeviceSettingsNotFoundException(msg: String) : Exception(msg)
@@ -169,7 +175,10 @@ class ProfileManagementController @Inject constructor(
* Indicates that the operation failed due to an attempt to re-elevate an administrator to
* administrator status (this should never happen in regular app operations).
*/
- PROFILE_ALREADY_HAS_ADMIN
+ PROFILE_ALREADY_HAS_ADMIN,
+
+ /** Indicates that the operation failed due to the profileType property not supplied. */
+ PROFILE_TYPE_UNKNOWN,
}
// TODO(#272): Remove init block when storeDataAsync is fixed
@@ -365,7 +374,7 @@ class ProfileManagementController @Inject constructor(
* Updates the name of an existing profile.
*
* @param profileId the ID corresponding to the profile being updated.
- * @param newName New name for the profile being updated.
+ * @param newName new name for the profile being updated.
* @return a [DataProvider] that indicates the success/failure of this update operation.
*/
fun updateName(profileId: ProfileId, newName: String): DataProvider {
@@ -395,6 +404,47 @@ class ProfileManagementController @Inject constructor(
}
}
+ /**
+ * Updates the profile type field of an existing profile.
+ *
+ * @param profileId the ID of the profile to update
+ * @return a [DataProvider] that represents the result of the update operation
+ */
+ fun updateProfileType(
+ profileId: ProfileId,
+ profileType: ProfileType
+ ): DataProvider {
+ val deferred = profileDataStore.storeDataWithCustomChannelAsync(
+ updateInMemoryCache = true
+ ) {
+ val profile =
+ it.profilesMap[profileId.internalId] ?: return@storeDataWithCustomChannelAsync Pair(
+ it,
+ ProfileActionStatus.PROFILE_NOT_FOUND
+ )
+
+ val updatedProfile = profile.toBuilder()
+
+ if (profileType == ProfileType.PROFILE_TYPE_UNSPECIFIED) {
+ return@storeDataWithCustomChannelAsync Pair(
+ it,
+ ProfileActionStatus.PROFILE_TYPE_UNKNOWN
+ )
+ } else {
+ updatedProfile.profileType = profileType
+ }
+
+ val profileDatabaseBuilder = it.toBuilder().putProfiles(
+ profileId.internalId,
+ updatedProfile.build()
+ )
+ Pair(profileDatabaseBuilder.build(), ProfileActionStatus.SUCCESS)
+ }
+ return dataProviders.createInMemoryDataProviderAsync(UPDATE_PROFILE_TYPE_PROVIDER_ID) {
+ return@createInMemoryDataProviderAsync getDeferredResult(profileId, null, deferred)
+ }
+ }
+
/**
* Updates the PIN of an existing profile.
*
@@ -679,6 +729,77 @@ class ProfileManagementController @Inject constructor(
).transform(UPDATE_AUDIO_LANGUAGE_PROVIDER_ID) { value -> value }
}
+ /**
+ * Updates the provided details of an newly created profile to migrate onboarding flow v2 support.
+ *
+ * @param profileId the ID of the profile to update
+ * @param avatarImagePath the path to the profile's avatar image, or null if unset
+ * @param colorRgb the randomly selected unique color to be used in place of a picture
+ * @param newName the nickname to identify the profile
+ * @param isAdmin whether the profile has administrator privileges
+ * @return [DataProvider] that represents the result of the update operation
+ */
+ fun updateNewProfileDetails(
+ profileId: ProfileId,
+ profileType: ProfileType,
+ avatarImagePath: Uri?,
+ colorRgb: Int,
+ newName: String,
+ isAdmin: Boolean
+ ): DataProvider {
+ val deferred = profileDataStore.storeDataWithCustomChannelAsync(
+ updateInMemoryCache = true
+ ) {
+ if (!enableLearnerStudyAnalytics.value && !profileNameValidator.isNameValid(newName)) {
+ return@storeDataWithCustomChannelAsync Pair(it, ProfileActionStatus.INVALID_PROFILE_NAME)
+ }
+ val profile =
+ it.profilesMap[profileId.internalId] ?: return@storeDataWithCustomChannelAsync Pair(
+ it,
+ ProfileActionStatus.PROFILE_NOT_FOUND
+ )
+ val profileDir = directoryManagementUtil.getOrCreateDir(profileId.toString())
+
+ val updatedProfile = profile.toBuilder()
+
+ if (avatarImagePath != null) {
+ val imageUri =
+ saveImageToInternalStorage(avatarImagePath, profileDir)
+ ?: return@storeDataWithCustomChannelAsync Pair(
+ it,
+ ProfileActionStatus.FAILED_TO_STORE_IMAGE
+ )
+ updatedProfile.avatar =
+ ProfileAvatar.newBuilder().setAvatarImageUri(imageUri).build()
+ } else {
+ updatedProfile.avatar =
+ ProfileAvatar.newBuilder().setAvatarColorRgb(colorRgb).build()
+ }
+
+ if (profileType == ProfileType.PROFILE_TYPE_UNSPECIFIED) {
+ return@storeDataWithCustomChannelAsync Pair(
+ it,
+ ProfileActionStatus.PROFILE_TYPE_UNKNOWN
+ )
+ } else {
+ updatedProfile.profileType = profileType
+ }
+
+ updatedProfile.name = newName
+
+ updatedProfile.isAdmin = isAdmin
+
+ val profileDatabaseBuilder = it.toBuilder().putProfiles(
+ profileId.internalId,
+ updatedProfile.build()
+ )
+ Pair(profileDatabaseBuilder.build(), ProfileActionStatus.SUCCESS)
+ }
+ return dataProviders.createInMemoryDataProviderAsync(UPDATE_PROFILE_DETAILS_PROVIDER_ID) {
+ return@createInMemoryDataProviderAsync getDeferredResult(profileId, newName, deferred)
+ }
+ }
+
/**
* Log in to the user's Profile by setting the current profile Id, updating profile's last logged
* in time and updating the total number of logins for the current profile Id.
@@ -962,6 +1083,8 @@ class ProfileManagementController @Inject constructor(
"Profile cannot be an admin"
)
)
+ ProfileActionStatus.PROFILE_TYPE_UNKNOWN ->
+ AsyncResult.Failure(UnknownProfileTypeException("ProfileType must be set."))
}
}
diff --git a/domain/src/test/java/org/oppia/android/domain/profile/ProfileManagementControllerTest.kt b/domain/src/test/java/org/oppia/android/domain/profile/ProfileManagementControllerTest.kt
index 91d58907646..287239d6e72 100644
--- a/domain/src/test/java/org/oppia/android/domain/profile/ProfileManagementControllerTest.kt
+++ b/domain/src/test/java/org/oppia/android/domain/profile/ProfileManagementControllerTest.kt
@@ -28,6 +28,7 @@ import org.oppia.android.app.model.AudioLanguage.NIGERIAN_PIDGIN_LANGUAGE
import org.oppia.android.app.model.Profile
import org.oppia.android.app.model.ProfileDatabase
import org.oppia.android.app.model.ProfileId
+import org.oppia.android.app.model.ProfileType
import org.oppia.android.app.model.ReadingTextSize.MEDIUM_TEXT_SIZE
import org.oppia.android.domain.classroom.TEST_CLASSROOM_ID_1
import org.oppia.android.domain.classroom.TEST_CLASSROOM_ID_2
@@ -141,7 +142,7 @@ class ProfileManagementControllerTest {
assertThat(profile.isContinueButtonAnimationSeen).isEqualTo(false)
assertThat(File(getAbsoluteDirPath("0")).isDirectory).isTrue()
assertThat(profile.surveyLastShownTimestampMs).isEqualTo(0L)
- assertThat(profile.lastSelectedClassroomId).isEqualTo("")
+ assertThat(profile.lastSelectedClassroomId).isEmpty()
}
@Test
@@ -1434,6 +1435,190 @@ class ProfileManagementControllerTest {
assertThat(lastSelectedClassroomId).isEmpty()
}
+ @Test
+ fun testUpdateProfile_updateMultipleFields_checkUpdateIsSuccessful() {
+ setUpTestApplicationComponent()
+ profileTestHelper.createDefaultAdminProfile()
+
+ val updateProvider = profileManagementController.updateNewProfileDetails(
+ PROFILE_ID_0,
+ ProfileType.SOLE_LEARNER,
+ null,
+ -1,
+ "John",
+ isAdmin = true
+ )
+ monitorFactory.waitForNextSuccessfulResult(updateProvider)
+
+ val profileProvider = profileManagementController.getProfile(PROFILE_ID_0)
+ val profile = monitorFactory.waitForNextSuccessfulResult(profileProvider)
+
+ assertThat(profile.name).isEqualTo("John")
+ assertThat(profile.profileType).isEqualTo(ProfileType.SOLE_LEARNER)
+ assertThat(profile.isAdmin).isEqualTo(true)
+ assertThat(profile.avatar.avatarImageUri).isEmpty()
+ assertThat(profile.avatar.avatarColorRgb).isEqualTo(-1)
+ }
+
+ @Test
+ fun testUpdateProfile_updateMultipleFields_invalidName_checkNameUpdateFailed() {
+ setUpTestApplicationComponent()
+ profileTestHelper.createDefaultAdminProfile()
+
+ val updateProvider = profileManagementController.updateNewProfileDetails(
+ PROFILE_ID_0,
+ ProfileType.SOLE_LEARNER,
+ null,
+ -1,
+ "John123",
+ isAdmin = true
+ )
+ val failure = monitorFactory.waitForNextFailureResult(updateProvider)
+
+ assertThat(failure).hasMessageThat().contains("John123 does not contain only letters")
+ }
+
+ @Test
+ fun testUpdateProfile_updateMultipleFields_nullAvatarUri_setsAvatarColorSuccessfully() {
+ setUpTestApplicationComponent()
+ profileTestHelper.createDefaultAdminProfile()
+
+ val updateProvider = profileManagementController.updateNewProfileDetails(
+ PROFILE_ID_0,
+ ProfileType.SOLE_LEARNER,
+ null,
+ -11235672,
+ "John",
+ isAdmin = true
+ )
+ monitorFactory.waitForNextSuccessfulResult(updateProvider)
+
+ val profileProvider = profileManagementController.getProfile(PROFILE_ID_0)
+ val profile = monitorFactory.waitForNextSuccessfulResult(profileProvider)
+
+ assertThat(profile.avatar.avatarImageUri).isEmpty()
+ assertThat(profile.avatar.avatarColorRgb).isEqualTo(-11235672)
+ assertThat(profile.name).isEqualTo("John")
+ assertThat(profile.profileType).isEqualTo(ProfileType.SOLE_LEARNER)
+ assertThat(profile.isAdmin).isEqualTo(true)
+ }
+
+ @Test
+ fun testUpdateProfile_updateMultipleFields_unspecifiedProfileType_returnsProfileTypeError() {
+ setUpTestApplicationComponent()
+ profileTestHelper.createDefaultAdminProfile()
+
+ val updateProvider = profileManagementController.updateNewProfileDetails(
+ PROFILE_ID_0,
+ ProfileType.PROFILE_TYPE_UNSPECIFIED,
+ null,
+ -11235672,
+ "John",
+ isAdmin = true
+ )
+
+ val failure = monitorFactory.waitForNextFailureResult(updateProvider)
+ assertThat(failure).hasMessageThat().isEqualTo("ProfileType must be set.")
+ }
+
+ @Test
+ fun testUpdateProfile_updateMultipleFields_invalidProfileId_checkUpdateFailed() {
+ setUpTestApplicationComponent()
+ profileTestHelper.createDefaultAdminProfile()
+
+ val updateProvider = profileManagementController.updateNewProfileDetails(
+ PROFILE_ID_3,
+ ProfileType.SOLE_LEARNER,
+ null,
+ -1,
+ "John",
+ isAdmin = true
+ )
+ val failure = monitorFactory.waitForNextFailureResult(updateProvider)
+
+ assertThat(failure).hasMessageThat()
+ .contains("ProfileId ${PROFILE_ID_3?.internalId} does not match an existing Profile")
+ }
+
+ @Test
+ fun testUpdateExistingAdminProfile_updateProfileTypeToSupervisor_checkProfileTypeSupervisor() {
+ setUpTestApplicationComponent()
+ profileTestHelper.addOnlyAdminProfile()
+
+ val updateProvider = profileManagementController.updateProfileType(
+ PROFILE_ID_0,
+ ProfileType.SUPERVISOR
+ )
+ monitorFactory.waitForNextSuccessfulResult(updateProvider)
+
+ val updatedProfileProvider = profileManagementController.getProfile(PROFILE_ID_0)
+ val updatedProfile = monitorFactory.waitForNextSuccessfulResult(updatedProfileProvider)
+ assertThat(updatedProfile.profileType).isEqualTo(ProfileType.SUPERVISOR)
+ }
+
+ @Test
+ fun testUpdateExistingPinlessAdmin_updateProfileTypeToSoleLearner_checkProfileTypeSoleLearner() {
+ setUpTestApplicationComponent()
+ addAdminProfile(name = "Admin", pin = "")
+
+ val updateProvider = profileManagementController.updateProfileType(
+ PROFILE_ID_0,
+ ProfileType.SOLE_LEARNER
+ )
+ monitorFactory.waitForNextSuccessfulResult(updateProvider)
+
+ val updatedProfileProvider = profileManagementController.getProfile(PROFILE_ID_0)
+ val updatedProfile = monitorFactory.waitForNextSuccessfulResult(updatedProfileProvider)
+ assertThat(updatedProfile.profileType).isEqualTo(ProfileType.SOLE_LEARNER)
+ }
+
+ @Test
+ fun testUpdateExistingNonAdminProfile_updateProfileTypeToLearner_checkProfileTypeAddLearner() {
+ setUpTestApplicationComponent()
+ addAdminProfile("Admin")
+ addNonAdminProfileAndWait(name = "Rajat", pin = "01234")
+
+ val updateProvider = profileManagementController.updateProfileType(
+ PROFILE_ID_1,
+ ProfileType.ADDITIONAL_LEARNER
+ )
+ monitorFactory.waitForNextSuccessfulResult(updateProvider)
+
+ val updatedProfileProvider = profileManagementController.getProfile(PROFILE_ID_1)
+ val updatedProfile = monitorFactory.waitForNextSuccessfulResult(updatedProfileProvider)
+ assertThat(updatedProfile.profileType).isEqualTo(ProfileType.ADDITIONAL_LEARNER)
+ }
+
+ @Test
+ fun testUpdateDefaultProfile_profileTypeToSoleLearner_checkProfileTypeSoleLearner() {
+ setUpTestApplicationComponent()
+ profileTestHelper.createDefaultAdminProfile()
+
+ val updateProvider = profileManagementController.updateProfileType(
+ PROFILE_ID_0,
+ ProfileType.SOLE_LEARNER
+ )
+ monitorFactory.waitForNextSuccessfulResult(updateProvider)
+
+ val updatedProfileProvider = profileManagementController.getProfile(PROFILE_ID_0)
+ val updatedProfile = monitorFactory.waitForNextSuccessfulResult(updatedProfileProvider)
+ assertThat(updatedProfile.profileType).isEqualTo(ProfileType.SOLE_LEARNER)
+ }
+
+ @Test
+ fun testUpdateDefaultProfile_profileTypeUnspecified_returnsProfileTypeError() {
+ setUpTestApplicationComponent()
+ profileTestHelper.createDefaultAdminProfile()
+
+ val updateProvider = profileManagementController.updateProfileType(
+ PROFILE_ID_0,
+ ProfileType.PROFILE_TYPE_UNSPECIFIED
+ )
+
+ val failure = monitorFactory.waitForNextFailureResult(updateProvider)
+ assertThat(failure).hasMessageThat().isEqualTo("ProfileType must be set.")
+ }
+
private fun addTestProfiles() {
val profileAdditionProviders = PROFILES_LIST.map {
addNonAdminProfile(it.name, pin = it.pin, allowDownloadAccess = it.allowDownloadAccess)
diff --git a/model/src/main/proto/arguments.proto b/model/src/main/proto/arguments.proto
index 5b9e2c24af6..ac21f121a5d 100644
--- a/model/src/main/proto/arguments.proto
+++ b/model/src/main/proto/arguments.proto
@@ -281,6 +281,9 @@ message AudioLanguageFragmentArguments {
message AudioLanguageFragmentStateBundle {
// The default audio language selected by the user.
AudioLanguage audio_language = 1;
+
+ // The selected language display name.
+ OppiaLanguage selected_language = 2;
}
// Activity Parameters needed to create the policy page.
@@ -886,3 +889,27 @@ message IntroActivityParams {
// The nickname associated with a newly created profile.
string profile_nickname = 1;
}
+
+// Arguments required when creating a new IntroFragment.
+message IntroFragmentArguments {
+ // The nickname associated with a newly created profile.
+ string profile_nickname = 1;
+}
+
+// Params required when creating a new CreateProfileActivity.
+message CreateProfileActivityParams {
+ // The ProfileType of the new profile as implied by the user's selection.
+ ProfileType profile_type = 1;
+}
+
+// Arguments required when creating a new CreateProfileFragment.
+message CreateProfileFragmentArguments {
+ // The ProfileType of the new profile as implied by the user's selection.
+ ProfileType profile_type = 1;
+}
+
+// The bundle of properties that are saved on configuration change in OnboardingFragment.
+message OnboardingFragmentStateBundle {
+ // The current selected language.
+ OppiaLanguage selected_language = 1;
+}
diff --git a/model/src/main/proto/profile.proto b/model/src/main/proto/profile.proto
index bffdb1ec194..bb55c8b2b47 100644
--- a/model/src/main/proto/profile.proto
+++ b/model/src/main/proto/profile.proto
@@ -90,6 +90,24 @@ message Profile {
// Represents the ID of the classroom that the user selected during their last login.
string last_selected_classroom_id = 19;
+
+ // Represents the type of user which informs the configuration options available to them.
+ ProfileType profile_type = 20;
+}
+
+// Represents the type of user using the app.
+enum ProfileType {
+ // The undefined ProfileType.
+ PROFILE_TYPE_UNSPECIFIED = 0;
+
+ // Represents a single learner profile without an admin pin set.
+ SOLE_LEARNER = 1;
+
+ // Represents an admin profile when there are more than one profiles.
+ SUPERVISOR = 2;
+
+ // Represents a non-admin profile in a multiple profile setup.
+ ADDITIONAL_LEARNER = 3;
}
// Represents a profile avatar image.
diff --git a/scripts/assets/accessibility_label_exemptions.textproto b/scripts/assets/accessibility_label_exemptions.textproto
index a1993f3b4dd..206b77a0466 100644
--- a/scripts/assets/accessibility_label_exemptions.textproto
+++ b/scripts/assets/accessibility_label_exemptions.textproto
@@ -36,6 +36,7 @@ exempted_activity: "app/src/main/java/org/oppia/android/app/testing/SplashTestAc
exempted_activity: "app/src/main/java/org/oppia/android/app/testing/StateAssemblerMarginBindingAdaptersTestActivity"
exempted_activity: "app/src/main/java/org/oppia/android/app/testing/StateAssemblerPaddingBindingAdaptersTestActivity"
exempted_activity: "app/src/main/java/org/oppia/android/app/testing/TestFontScaleConfigurationUtilActivity"
+exempted_activity: "app/src/main/java/org/oppia/android/app/testing/TextInputLayoutBindingAdaptersTestActivity"
exempted_activity: "app/src/main/java/org/oppia/android/app/testing/TextViewBindingAdaptersTestActivity"
exempted_activity: "app/src/main/java/org/oppia/android/app/testing/TopicRevisionTestActivity"
exempted_activity: "app/src/main/java/org/oppia/android/app/testing/TopicTestActivity"
diff --git a/scripts/assets/file_content_validation_checks.textproto b/scripts/assets/file_content_validation_checks.textproto
index d4e3597a664..fe0d7d9aba9 100644
--- a/scripts/assets/file_content_validation_checks.textproto
+++ b/scripts/assets/file_content_validation_checks.textproto
@@ -276,6 +276,7 @@ file_content_checks {
exempted_file_name: "app/src/main/java/org/oppia/android/app/player/audio/LanguageDialogFragment.kt"
exempted_file_name: "app/src/sharedTest/java/org/oppia/android/app/administratorcontrols/AppVersionActivityTest.kt"
exempted_file_name: "app/src/sharedTest/java/org/oppia/android/app/home/HomeActivityTest.kt"
+ exempted_file_name: "app/src/sharedTest/java/org/oppia/android/app/onboarding/OnboardingFragmentTest.kt"
exempted_file_name: "app/src/sharedTest/java/org/oppia/android/app/parser/HtmlParserTest.kt"
exempted_file_name: "app/src/sharedTest/java/org/oppia/android/app/parser/ListItemLeadingMarginSpanTest.kt"
exempted_file_name: "app/src/sharedTest/java/org/oppia/android/app/player/audio/AudioFragmentTest.kt"
diff --git a/scripts/assets/test_file_exemptions.textproto b/scripts/assets/test_file_exemptions.textproto
index bbc8a2fd872..9bf4d08e951 100644
--- a/scripts/assets/test_file_exemptions.textproto
+++ b/scripts/assets/test_file_exemptions.textproto
@@ -1342,6 +1342,10 @@ test_file_exemption {
exempted_file_path: "app/src/main/java/org/oppia/android/app/onboarding/OnboardingViewModel.kt"
test_file_not_required: true
}
+test_file_exemption {
+ exempted_file_path: "app/src/main/java/org/oppia/android/app/onboarding/OnboardingAppLanguageViewModel.kt"
+ test_file_not_required: true
+}
test_file_exemption {
exempted_file_path: "app/src/main/java/org/oppia/android/app/onboarding/OnboardingViewPagerViewModel.kt"
test_file_not_required: true
@@ -2378,6 +2382,10 @@ test_file_exemption {
exempted_file_path: "app/src/main/java/org/oppia/android/app/testing/BindableAdapterTestActivity.kt"
test_file_not_required: true
}
+test_file_exemption {
+ exempted_file_path: "app/src/main/java/org/oppia/android/app/testing/TextInputLayoutBindingAdaptersTestActivity.kt"
+ test_file_not_required: true
+}
test_file_exemption {
exempted_file_path: "app/src/main/java/org/oppia/android/app/testing/BindableAdapterTestDataModel.kt"
test_file_not_required: true
@@ -2410,6 +2418,10 @@ test_file_exemption {
exempted_file_path: "app/src/main/java/org/oppia/android/app/testing/ColorBindingAdaptersTestFragment.kt"
test_file_not_required: true
}
+test_file_exemption {
+ exempted_file_path: "app/src/main/java/org/oppia/android/app/testing/TextInputLayoutBindingAdaptersTestFragment.kt"
+ test_file_not_required: true
+}
test_file_exemption {
exempted_file_path: "app/src/main/java/org/oppia/android/app/testing/ConceptCardFragmentTestActivity.kt"
test_file_not_required: true
diff --git a/testing/src/main/java/org/oppia/android/testing/espresso/EditTextInputAction.kt b/testing/src/main/java/org/oppia/android/testing/espresso/EditTextInputAction.kt
index 0f038c038e3..b65dd4f976b 100644
--- a/testing/src/main/java/org/oppia/android/testing/espresso/EditTextInputAction.kt
+++ b/testing/src/main/java/org/oppia/android/testing/espresso/EditTextInputAction.kt
@@ -50,7 +50,7 @@ class EditTextInputAction @Inject constructor(
override fun perform(uiController: UiController?, view: View?) {
// Appending text only works on Robolectric, whereas Espresso needs to use typeText().
if (Build.FINGERPRINT.contains("robolectric", ignoreCase = true)) {
- (view as? EditText)?.append(text)
+ (view as? EditText)?.setText(text)
testCoroutineDispatchers.runCurrent()
} else baseAction.perform(uiController, view)
}
diff --git a/testing/src/main/java/org/oppia/android/testing/profile/ProfileTestHelper.kt b/testing/src/main/java/org/oppia/android/testing/profile/ProfileTestHelper.kt
index 3dc71a049a1..a5e877fa705 100644
--- a/testing/src/main/java/org/oppia/android/testing/profile/ProfileTestHelper.kt
+++ b/testing/src/main/java/org/oppia/android/testing/profile/ProfileTestHelper.kt
@@ -76,6 +76,16 @@ class ProfileTestHelper @Inject constructor(
}
}
+ /** Creates one admin profile with default values for all fields. */
+ fun createDefaultAdminProfile() {
+ addProfileAndWait(
+ name = "",
+ pin = "",
+ allowDownloadAccess = false,
+ isAdmin = true
+ )
+ }
+
/** Log in to admin profile. */
fun logIntoAdmin() = logIntoProfile(internalProfileId = 0)
diff --git a/testing/src/test/java/org/oppia/android/testing/profile/ProfileTestHelperTest.kt b/testing/src/test/java/org/oppia/android/testing/profile/ProfileTestHelperTest.kt
index dcddadc11ab..74c9ab3846c 100644
--- a/testing/src/test/java/org/oppia/android/testing/profile/ProfileTestHelperTest.kt
+++ b/testing/src/test/java/org/oppia/android/testing/profile/ProfileTestHelperTest.kt
@@ -102,6 +102,18 @@ class ProfileTestHelperTest {
assertThat(profiles).hasSize(10)
}
+ @Test
+ fun testAddDefaultProfile_createDefaultProfile_checkProfileIsAdded() {
+ profileTestHelper.createDefaultAdminProfile()
+ testCoroutineDispatchers.runCurrent()
+ val profilesProvider = profileManagementController.getProfiles()
+ testCoroutineDispatchers.runCurrent()
+
+ val profiles = monitorFactory.waitForNextSuccessfulResult(profilesProvider)
+ assertThat(profiles).hasSize(1)
+ assertThat(profiles.first().isAdmin).isTrue()
+ }
+
@Test
fun testLogIntoAdmin_initializeProfiles_logIntoAdmin_checkIsSuccessful() {
profileTestHelper.initializeProfiles()