From 95699f922321f49a3503783187a14ad1cef0d5d3 Mon Sep 17 00:00:00 2001
From: Adhiambo Peres <59600948+adhiamboperes@users.noreply.github.com>
Date: Sat, 19 Oct 2024 00:23:02 +0300
Subject: [PATCH] Fix Part of #4938: Language Selection Config and New Profile
 Creation Flow (#5457)

<!-- READ ME FIRST: Please fill in the explanation section below and
check off every point from the Essential Checklist! -->
## Explanation
Fixes Part of #4938: Modifies the profile creation flow and sets the
app/audio language selection during onboarding.

### Default Profile Creation
A default empty profile is created when the Onboarding screen is opened
for the first time. This is necessary to provide a ProfileId to use when
saving the selected app language.

Because this will be the first profile on the app in onboarding v2, it
is an admin.

Provisions have been made so that, should the user exit the app before
completing the onboarding flow, this profile will be fetched, to prevent
multiple profile creation.




### App Language Selection
The language selector will be shown to the user on initial app launch,
or if profile onboarding is not complete.

- There will be a pre-filled language option based on the locale of the
device when the app is installed. If the locale is unsupported, English
will be the default selection.
- A user can select any preferred supported app language from the
dropdown list.
- The existing language functionality/behavior will be retained.

Tests have been added to verify these requirements, and efforts have
been made to ensure language selection persists on configuration change.
I noticed during testing that failure to do this resulted in an
unpleasant UX.

### Profile Nickname and Picture
The "Create profile Screen" has been repurposed to update the default
profile instead, providing the remaining profile properties(ProfileType,
name, avatar)

Checks have been added to check for profile creation errors, consistent
with the legacy flow.

A new function has been added to the ProfileManagementController to
allow for batch update of these fields, and corresponding tests have
been added.

### Audio Language Selection
The final step of onboarding is the audio language selection, and there
will be a pre-filled language selectionas follows:

- Selected app language(from first onboarding screen) if available as
audio language. (Audio language need not be completed)
- Otherwise (if available as audio language), audio language of the
administrator account -- this will be added downstream in M3, as it only
impacts additional learners.
- Otherwise (if available as audio language), device language.
- Else, English.

There have been some incidental changes in AudioLanguageFragment and
OptionsFragment, and their related tests due to sharing of the classes
between the existing screens and the new screens.

### ProfileTestHelper.kt
A new function, and related tests, have been added to create a default
profile to be used in tests.

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

## For UI-specific PRs only
The screen recordings are in [this drive
location](https://drive.google.com/drive/folders/1CXTAALPgpCKfekOQKjRFTb5G2fPgWkRt?usp=sharing),
since github does not support webm.

---------

Co-authored-by: Ben Henning <ben@oppia.org>
---
 app/src/main/AndroidManifest.xml              |   3 +
 .../TextInputLayoutBindingAdapters.java       |  42 +++
 .../AudioLanguageFragmentPresenter.kt         | 130 ++++++-
 .../app/onboarding/CreateProfileActivity.kt   |  11 +-
 .../CreateProfileActivityPresenter.kt         |  21 +-
 .../app/onboarding/CreateProfileFragment.kt   |  22 +-
 .../CreateProfileFragmentPresenter.kt         | 156 ++++++++-
 .../app/onboarding/CreateProfileViewModel.kt  |   5 +-
 .../android/app/onboarding/IntroActivity.kt   |  31 +-
 .../app/onboarding/IntroActivityPresenter.kt  |  20 +-
 .../android/app/onboarding/IntroFragment.kt   |  20 +-
 .../app/onboarding/IntroFragmentPresenter.kt  |   4 +
 .../OnboardingAppLanguageViewModel.kt         |  28 ++
 .../app/onboarding/OnboardingFragment.kt      |   9 +-
 .../onboarding/OnboardingFragmentPresenter.kt | 240 ++++++++++++-
 .../OnboardingProfileTypeActivity.kt          |   5 +-
 .../OnboardingProfileTypeActivityPresenter.kt |  10 +-
 .../OnboardingProfileTypeFragment.kt          |   6 +-
 .../OnboardingProfileTypeFragmentPresenter.kt |  24 +-
 .../app/options/AudioLanguageActivity.kt      |   5 +-
 .../options/AudioLanguageActivityPresenter.kt |   5 +-
 .../app/options/AudioLanguageFragment.kt      |  27 +-
 .../AudioLanguageSelectionViewModel.kt        | 118 ++++++-
 .../android/app/options/OptionsActivity.kt    |  18 +-
 .../app/options/OptionsActivityPresenter.kt   |   5 +-
 .../player/audio/AudioFragmentPresenter.kt    |   9 +-
 .../ColorBindingAdaptersTestActivity.kt       |   2 +-
 ...tInputLayoutBindingAdaptersTestActivity.kt |  26 ++
 ...tInputLayoutBindingAdaptersTestFragment.kt |  30 ++
 app/src/main/res/drawable/learner_otter.xml   |   3 +-
 app/src/main/res/drawable/otter.xml           |   3 +-
 .../res/drawable/parent_teacher_otter.xml     |   3 +-
 .../audio_language_selection_fragment.xml     |  11 +-
 .../layout-land/create_profile_fragment.xml   |   2 +-
 ...arding_app_language_selection_fragment.xml |   1 +
 .../audio_language_selection_fragment.xml     |  11 +-
 .../create_profile_fragment.xml               |   2 +-
 ...arding_app_language_selection_fragment.xml |   1 +
 .../audio_language_selection_fragment.xml     |  11 +-
 .../create_profile_fragment.xml               |   2 +-
 ...arding_app_language_selection_fragment.xml |   1 +
 .../audio_language_selection_fragment.xml     |  11 +-
 .../res/layout/create_profile_fragment.xml    |   2 +-
 ...arding_app_language_selection_fragment.xml |   1 +
 ..._layout_binding_adapters_test_activity.xml |   6 +
 ..._layout_binding_adapters_test_fragment.xml |  21 ++
 app/src/main/res/values/strings.xml           |   2 +
 .../databinding/ColorBindingAdaptersTest.kt   |   2 +-
 .../TextInputLayoutBindingAdaptersTest.kt     | 239 +++++++++++++
 .../onboarding/CreateProfileFragmentTest.kt   | 230 +++++++++++--
 .../app/onboarding/IntroActivityTest.kt       |  12 +-
 .../app/onboarding/IntroFragmentTest.kt       |  52 +--
 .../app/onboarding/OnboardingFragmentTest.kt  | 323 +++++++++++++++++-
 .../OnboardingProfileTypeFragmentTest.kt      |  25 +-
 .../app/options/AudioLanguageFragmentTest.kt  | 114 +++++--
 .../profile/ProfileManagementController.kt    | 129 ++++++-
 .../ProfileManagementControllerTest.kt        | 187 +++++++++-
 model/src/main/proto/arguments.proto          |  27 ++
 model/src/main/proto/profile.proto            |  18 +
 .../accessibility_label_exemptions.textproto  |   1 +
 .../file_content_validation_checks.textproto  |   1 +
 scripts/assets/test_file_exemptions.textproto |  12 +
 .../testing/espresso/EditTextInputAction.kt   |   2 +-
 .../testing/profile/ProfileTestHelper.kt      |  10 +
 .../testing/profile/ProfileTestHelperTest.kt  |  12 +
 65 files changed, 2270 insertions(+), 252 deletions(-)
 create mode 100644 app/src/main/java/org/oppia/android/app/onboarding/OnboardingAppLanguageViewModel.kt
 create mode 100644 app/src/main/java/org/oppia/android/app/testing/TextInputLayoutBindingAdaptersTestActivity.kt
 create mode 100644 app/src/main/java/org/oppia/android/app/testing/TextInputLayoutBindingAdaptersTestFragment.kt
 create mode 100644 app/src/main/res/layout/text_input_layout_binding_adapters_test_activity.xml
 create mode 100644 app/src/main/res/layout/text_input_layout_binding_adapters_test_fragment.xml
 create mode 100644 app/src/sharedTest/java/org/oppia/android/app/databinding/TextInputLayoutBindingAdaptersTest.kt

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" />
+    <activity
+      android:name=".app.testing.TextInputLayoutBindingAdaptersTestActivity"
+      android:theme="@style/OppiaThemeWithoutActionBar" />
     <provider
       android:name="androidx.work.impl.WorkManagerInitializer"
       android:authorities="${applicationId}.workmanager-init"
diff --git a/app/src/main/java/org/oppia/android/app/databinding/TextInputLayoutBindingAdapters.java b/app/src/main/java/org/oppia/android/app/databinding/TextInputLayoutBindingAdapters.java
index d0dd35c2a77..dfac960ef8f 100644
--- a/app/src/main/java/org/oppia/android/app/databinding/TextInputLayoutBindingAdapters.java
+++ b/app/src/main/java/org/oppia/android/app/databinding/TextInputLayoutBindingAdapters.java
@@ -1,8 +1,17 @@
 package org.oppia.android.app.databinding;
 
+import android.app.Activity;
+import android.content.Context;
+import android.content.ContextWrapper;
+import android.view.View;
+import android.widget.AutoCompleteTextView;
 import androidx.annotation.NonNull;
+import androidx.annotation.Nullable;
 import androidx.databinding.BindingAdapter;
 import com.google.android.material.textfield.TextInputLayout;
+import org.oppia.android.app.model.OppiaLanguage;
+import org.oppia.android.app.translation.AppLanguageActivityInjectorProvider;
+import org.oppia.android.app.translation.AppLanguageResourceHandler;
 
 /** Holds all custom binding adapters that bind to [TextInputLayout]. */
 public final class TextInputLayoutBindingAdapters {
@@ -15,4 +24,37 @@ public static void setErrorMessage(
   ) {
     textInputLayout.setError(errorMessage);
   }
+
+  /** Binding adapter for setting the text of an [AutoCompleteTextView]. */
+  @BindingAdapter({"languageSelection", "filter"})
+  public static void setLanguageSelection(
+      @NonNull AutoCompleteTextView textView,
+      @Nullable OppiaLanguage selectedItem,
+      Boolean filter) {
+    textView.setText(getAppLanguageResourceHandler(textView)
+        .computeLocalizedDisplayName(selectedItem), filter);
+  }
+
+  private static AppLanguageResourceHandler getAppLanguageResourceHandler(View view) {
+    AppLanguageActivityInjectorProvider provider =
+        (AppLanguageActivityInjectorProvider) getAttachedActivity(view);
+    return provider.getAppLanguageActivityInjector().getAppLanguageResourceHandler();
+  }
+
+  private static Activity getAttachedActivity(View view) {
+    Context context = view.getContext();
+    while (context != null && !(context instanceof Activity)) {
+      if (!(context instanceof ContextWrapper)) {
+        throw new IllegalStateException(
+            "Encountered context in view (" + view + ") that doesn't wrap a parent context: "
+                + context
+        );
+      }
+      context = ((ContextWrapper) context).getBaseContext();
+    }
+    if (context == null) {
+      throw new IllegalStateException("Failed to find base Activity for view: " + view);
+    }
+    return (Activity) context;
+  }
 }
diff --git a/app/src/main/java/org/oppia/android/app/onboarding/AudioLanguageFragmentPresenter.kt b/app/src/main/java/org/oppia/android/app/onboarding/AudioLanguageFragmentPresenter.kt
index 3a238d4b010..43ac0698801 100644
--- a/app/src/main/java/org/oppia/android/app/onboarding/AudioLanguageFragmentPresenter.kt
+++ b/app/src/main/java/org/oppia/android/app/onboarding/AudioLanguageFragmentPresenter.kt
@@ -1,17 +1,31 @@
 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 com.google.android.material.appbar.AppBarLayout
 import org.oppia.android.R
+import org.oppia.android.app.home.HomeActivity
+import org.oppia.android.app.model.AudioLanguageFragmentStateBundle
+import org.oppia.android.app.model.AudioTranslationLanguageSelection
+import org.oppia.android.app.model.OppiaLanguage
+import org.oppia.android.app.model.ProfileId
+import org.oppia.android.app.options.AudioLanguageFragment.Companion.FRAGMENT_SAVED_STATE_KEY
 import org.oppia.android.app.options.AudioLanguageSelectionViewModel
 import org.oppia.android.app.translation.AppLanguageResourceHandler
 import org.oppia.android.databinding.AudioLanguageSelectionFragmentBinding
+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.DataProviders.Companion.toLiveData
+import org.oppia.android.util.extensions.getProto
+import org.oppia.android.util.extensions.putProto
 import javax.inject.Inject
 
 /** The presenter for [AudioLanguageFragment]. */
@@ -19,9 +33,13 @@ class AudioLanguageFragmentPresenter @Inject constructor(
   private val fragment: Fragment,
   private val activity: AppCompatActivity,
   private val appLanguageResourceHandler: AppLanguageResourceHandler,
-  private val audioLanguageSelectionViewModel: AudioLanguageSelectionViewModel
+  private val audioLanguageSelectionViewModel: AudioLanguageSelectionViewModel,
+  private val translationController: TranslationController,
+  private val oppiaLogger: OppiaLogger
 ) {
   private lateinit var binding: AudioLanguageSelectionFragmentBinding
+  private lateinit var selectedLanguage: OppiaLanguage
+  private lateinit var supportedLanguages: List<OppiaLanguage>
 
   /**
    * 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<AppBarLayout>(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<Intent>
 
   /** 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<OppiaLanguage> get() = _languageSelectionLiveData
+  private val _languageSelectionLiveData = MutableLiveData<OppiaLanguage>()
+
+  /** Get the list of app supported languages to be displayed in the language dropdown. */
+  val supportedAppLanguagesList: LiveData<List<OppiaLanguage>> get() = _supportedAppLanguagesList
+  private val _supportedAppLanguagesList = MutableLiveData<List<OppiaLanguage>>()
+
+  /** 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<OppiaLanguage>) {
+    _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<OppiaLanguage>
 
   /** 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<List<Profile>> 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<OppiaLocale.DisplayLocale>
+  ): 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<Profile>>): List<Profile> {
+    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<OppiaLanguage> 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<AudioLanguage>()
 
@@ -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<List<String>> get() = _availableAudioLanguages
+  private val _availableAudioLanguages = MutableLiveData<List<String>>()
+
+  /** Sets the list of audio languages supported by the app based on [OppiaLanguage]. */
+  val supportedOppiaLanguagesLiveData: LiveData<List<OppiaLanguage>> 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<OppiaLanguage> 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<AppLanguageSelection> by lazy {
+    translationController.getAppLanguageSelection(profileId)
+  }
+
+  private val systemLanguageProvider: DataProvider<OppiaLocale.DisplayLocale> 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<String> 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<Intent>
   private lateinit var audioLanguageLauncher: ActivityResultLauncher<Intent>
 
@@ -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<AsyncResult<CellularDataPreference>> {
+        {
           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<String> { 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">
   <path
     android:fillColor="#ffebc2"
     android:pathData="m303.21,263.99c-6.05,5.51 -11.75,11.37 -19.45,14.81 -6.81,2.78 -13.92,4.79 -21.18,5.99 -6.57,2.41 -13.28,1.37 -19.51,-0.47 -7.58,-2.21 -13.55,-2.43 -20.37,3 -8.44,6.77 -19,7.45 -29.58,5.04 -5.67,-1.73 -10.94,-4.56 -15.51,-8.33 -1.84,-1.7 -4.49,-2.22 -6.84,-1.35 -16.79,5.53 -35.12,3.88 -50.65,-4.57 -1.17,-1.46 -2.02,-3.29 -4.23,-3.47 -0.61,-1.08 0.11,-1.8 0.63,-2.66 4.82,-8.26 12.31,-13.66 19.9,-18.95 -8.06,4.43 -14.87,10.83 -19.79,18.61 -0.68,0.56 -0.83,1.8 -2.16,1.51 -4,-3.3 -7.69,-6.95 -11.03,-10.9 -0.29,-2.65 1.8,-3.94 3.31,-5.4 6.42,-5.84 14.02,-10.25 22.28,-12.92 0.94,-0.22 1.84,-0.56 2.68,-1.03 0.25,-0.16 0.38,-0.38 0.45,-0.9 -1.31,-0.9 -2.5,0 -3.6,0.47 -8.56,3.14 -16.27,8.27 -22.47,14.95 -1.12,1.12 -1.96,2.75 -3.92,2.68 -1.66,-3.38 -3.69,-6.6 -4.32,-10.38 -0.13,-0.83 -0.82,-1.45 -1.66,-1.51 -0.22,-1.91 1.35,-2.59 2.5,-3.45 7.97,-5.99 17.41,-9.72 27.31,-10.8 1.98,0 3.9,-0.71 5.4,-2.02 -5.52,-0.12 -11.02,0.81 -16.19,2.75 -5.16,1.85 -10.01,4.47 -14.39,7.76 -1.33,0.94 -2.54,2.21 -4.43,1.8 0.14,-1.72 -0.18,-3.44 -0.92,-5 0.25,-16.05 8.17,-27.66 21.11,-36.33 1.14,-0.79 2.52,-1.18 3.9,-1.08 5.83,2 11.39,4.84 17.71,5.27 8.08,0.87 16.25,0.47 24.2,-1.19 13.01,-3.18 20.73,-12.29 26.86,-23.39 1.93,-3.44 2.43,-7.68 5.78,-10.33 4.67,-1.79 9.82,-1.87 14.54,-0.23 1.8,0.88 2.14,2.74 2.86,4.35 4.17,9.54 9.32,18.37 18.21,24.44 6.23,4.12 13.48,6.42 20.94,6.64 11.35,0.59 22.56,0 32.68,-6.19 0.61,-0.33 1.29,-0.52 1.98,-0.54 9.39,3.6 15.76,10.54 20.76,18.97 2.87,4.97 4.67,10.49 5.27,16.19 -1.49,3.17 -0.11,6.78 -1.58,9.97 -1.8,0.49 -2.75,-0.94 -3.9,-1.8 -7.84,-6.18 -17.13,-10.27 -26.99,-11.86 -1.83,-0.42 -3.74,-0.38 -5.56,0.11 1.03,1.16 2.55,1.78 4.1,1.66 10.19,1.07 19.93,4.79 28.23,10.8 1.51,1.08 3.4,1.98 3.27,4.35 -0.68,4.84 -3.8,8.74 -5.09,13.37 -0.31,0.24 -0.73,0.28 -1.08,0.11 -6.66,-7.65 -13.87,-14.57 -23.39,-18.7 -2.4,-1.28 -5.06,-2.02 -7.77,-2.18 1.26,2.07 2.91,2.03 4.26,2.5 8.25,2.82 15.82,7.36 22.19,13.32 1.74,1.53 3.21,3.36 4.32,5.4 0.18,0.36 0.15,0.78 -0.07,1.12Z"
diff --git a/app/src/main/res/drawable/otter.xml b/app/src/main/res/drawable/otter.xml
index bc0891510c1..4847563679a 100644
--- a/app/src/main/res/drawable/otter.xml
+++ b/app/src/main/res/drawable/otter.xml
@@ -2,7 +2,8 @@
     android:width="159dp"
     android:height="167dp"
     android:viewportWidth="159"
-    android:viewportHeight="167">
+    android:viewportHeight="167"
+    android:autoMirrored="true">
   <group>
     <clip-path
         android:pathData="M0,0h159v167h-159z"/>
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">
   <path
     android:fillColor="#ffebc2"
     android:pathData="m475.96,237.39c-6.34,5.78 -12.33,11.93 -20.41,15.54 -7.15,2.92 -14.6,5.03 -22.22,6.29 -6.89,2.53 -13.93,1.43 -20.47,-0.49 -7.95,-2.32 -14.22,-2.55 -21.37,3.15 -8.85,7.1 -19.94,7.82 -31.04,5.29 -5.95,-1.82 -11.48,-4.79 -16.27,-8.74 -1.94,-1.78 -4.71,-2.33 -7.17,-1.42 -17.62,5.8 -36.85,4.07 -53.15,-4.8 -1.23,-1.53 -2.11,-3.46 -4.44,-3.64 -0.64,-1.13 0.11,-1.89 0.66,-2.79 5.06,-8.67 12.91,-14.33 20.88,-19.88 -8.46,4.65 -15.6,11.37 -20.77,19.52 -0.72,0.59 -0.87,1.89 -2.27,1.59 -4.19,-3.46 -8.07,-7.29 -11.57,-11.44 -0.3,-2.78 1.89,-4.13 3.47,-5.66 6.74,-6.13 14.71,-10.75 23.37,-13.56 0.98,-0.23 1.93,-0.59 2.81,-1.08 0.26,-0.17 0.4,-0.4 0.47,-0.94 -1.38,-0.94 -2.62,0 -3.78,0.49 -8.99,3.3 -17.07,8.67 -23.58,15.69 -1.17,1.17 -2.06,2.89 -4.12,2.81 -1.74,-3.55 -3.87,-6.93 -4.53,-10.89 -0.14,-0.87 -0.86,-1.53 -1.74,-1.59 -0.23,-2 1.42,-2.72 2.62,-3.62 8.36,-6.28 18.26,-10.19 28.66,-11.33 2.08,0 4.09,-0.75 5.66,-2.11 -5.8,-0.13 -11.56,0.85 -16.99,2.89 -5.41,1.95 -10.5,4.69 -15.1,8.14 -1.4,0.98 -2.66,2.32 -4.64,1.89 0.15,-1.8 -0.19,-3.61 -0.96,-5.25 0.26,-16.84 8.57,-29.02 22.15,-38.12 1.2,-0.83 2.64,-1.23 4.1,-1.13 6.12,2.1 11.95,5.08 18.58,5.53 8.48,0.91 17.05,0.49 25.39,-1.25 13.65,-3.34 21.75,-12.89 28.19,-24.54 2.02,-3.61 2.55,-8.06 6.06,-10.84 4.9,-1.87 10.3,-1.96 15.25,-0.25 1.89,0.93 2.25,2.87 3,4.57 4.38,10.01 9.78,19.28 19.11,25.64 6.53,4.32 14.15,6.73 21.98,6.97 11.91,0.62 23.68,0 34.29,-6.49 0.64,-0.35 1.35,-0.54 2.08,-0.57 9.86,3.78 16.54,11.06 21.79,19.9 3.01,5.21 4.9,11 5.53,16.99 -1.57,3.32 -0.11,7.12 -1.66,10.46 -1.89,0.51 -2.89,-0.98 -4.1,-1.89 -8.23,-6.49 -17.98,-10.77 -28.32,-12.44 -1.92,-0.44 -3.93,-0.4 -5.83,0.11 1.08,1.22 2.68,1.86 4.3,1.74 10.7,1.12 20.91,5.03 29.62,11.33 1.59,1.13 3.57,2.08 3.44,4.57 -0.72,5.08 -3.98,9.18 -5.34,14.03 -0.33,0.25 -0.76,0.29 -1.13,0.11 -6.99,-8.02 -14.56,-15.29 -24.54,-19.62 -2.52,-1.34 -5.3,-2.12 -8.16,-2.28 1.32,2.17 3.06,2.13 4.47,2.62 8.66,2.96 16.6,7.72 23.28,13.97 1.83,1.61 3.36,3.53 4.53,5.66 0.19,0.37 0.16,0.82 -0.08,1.17Z"
diff --git a/app/src/main/res/layout-land/audio_language_selection_fragment.xml b/app/src/main/res/layout-land/audio_language_selection_fragment.xml
index ed683db064e..28cc53ec8f1 100644
--- a/app/src/main/res/layout-land/audio_language_selection_fragment.xml
+++ b/app/src/main/res/layout-land/audio_language_selection_fragment.xml
@@ -3,6 +3,13 @@
   xmlns:app="http://schemas.android.com/apk/res-auto"
   xmlns:card_view="http://schemas.android.com/apk/res-auto">
 
+  <data>
+
+    <variable
+      name="viewModel"
+      type="org.oppia.android.app.options.AudioLanguageSelectionViewModel" />
+  </data>
+
   <androidx.constraintlayout.widget.ConstraintLayout
     android:layout_width="match_parent"
     android:layout_height="match_parent"
@@ -69,7 +76,9 @@
           android:layout_width="match_parent"
           android:layout_height="wrap_content"
           android:inputType="none"
-          android:padding="@dimen/onboarding_shared_padding_small" />
+          android:padding="@dimen/onboarding_shared_padding_small"
+          app:filter="@{false}"
+          app:languageSelection="@{viewModel.selectedAudioLanguage}"/>
       </com.google.android.material.textfield.TextInputLayout>
     </com.google.android.material.card.MaterialCardView>
 
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">
 
         <AutoCompleteTextView
+          android:id="@+id/onboarding_language_dropdown"
           android:layout_width="match_parent"
           android:layout_height="wrap_content"
           android:inputType="none"
diff --git a/app/src/main/res/layout-sw600dp-land/audio_language_selection_fragment.xml b/app/src/main/res/layout-sw600dp-land/audio_language_selection_fragment.xml
index 157f25a8040..fd98348fcc5 100644
--- a/app/src/main/res/layout-sw600dp-land/audio_language_selection_fragment.xml
+++ b/app/src/main/res/layout-sw600dp-land/audio_language_selection_fragment.xml
@@ -3,6 +3,13 @@
   xmlns:app="http://schemas.android.com/apk/res-auto"
   xmlns:card_view="http://schemas.android.com/apk/res-auto">
 
+  <data>
+
+    <variable
+      name="viewModel"
+      type="org.oppia.android.app.options.AudioLanguageSelectionViewModel" />
+  </data>
+
   <androidx.constraintlayout.widget.ConstraintLayout
     android:layout_width="match_parent"
     android:layout_height="match_parent"
@@ -85,7 +92,9 @@
           android:layout_width="match_parent"
           android:layout_height="wrap_content"
           android:inputType="none"
-          android:padding="@dimen/onboarding_shared_padding_small" />
+          android:padding="@dimen/onboarding_shared_padding_small"
+          app:filter="@{false}"
+          app:languageSelection="@{viewModel.selectedAudioLanguage}" />
       </com.google.android.material.textfield.TextInputLayout>
     </com.google.android.material.card.MaterialCardView>
 
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">
 
         <AutoCompleteTextView
+          android:id="@+id/onboarding_language_dropdown"
           android:layout_width="match_parent"
           android:layout_height="wrap_content"
           android:inputType="none"
diff --git a/app/src/main/res/layout-sw600dp-port/audio_language_selection_fragment.xml b/app/src/main/res/layout-sw600dp-port/audio_language_selection_fragment.xml
index 08adbf3496e..c60971aba30 100644
--- a/app/src/main/res/layout-sw600dp-port/audio_language_selection_fragment.xml
+++ b/app/src/main/res/layout-sw600dp-port/audio_language_selection_fragment.xml
@@ -3,6 +3,13 @@
   xmlns:app="http://schemas.android.com/apk/res-auto"
   xmlns:card_view="http://schemas.android.com/apk/res-auto">
 
+  <data>
+
+    <variable
+      name="viewModel"
+      type="org.oppia.android.app.options.AudioLanguageSelectionViewModel" />
+  </data>
+
   <androidx.constraintlayout.widget.ConstraintLayout
     android:layout_width="match_parent"
     android:layout_height="match_parent"
@@ -85,7 +92,9 @@
           android:layout_width="match_parent"
           android:layout_height="wrap_content"
           android:inputType="none"
-          android:padding="@dimen/onboarding_shared_padding_small" />
+          android:padding="@dimen/onboarding_shared_padding_small"
+          app:filter="@{false}"
+          app:languageSelection="@{viewModel.selectedAudioLanguage}" />
       </com.google.android.material.textfield.TextInputLayout>
     </com.google.android.material.card.MaterialCardView>
 
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">
 
         <AutoCompleteTextView
+          android:id="@+id/onboarding_language_dropdown"
           android:layout_width="match_parent"
           android:layout_height="wrap_content"
           android:inputType="none"
diff --git a/app/src/main/res/layout/audio_language_selection_fragment.xml b/app/src/main/res/layout/audio_language_selection_fragment.xml
index 77eb7eca1de..a134e039772 100644
--- a/app/src/main/res/layout/audio_language_selection_fragment.xml
+++ b/app/src/main/res/layout/audio_language_selection_fragment.xml
@@ -3,6 +3,13 @@
   xmlns:app="http://schemas.android.com/apk/res-auto"
   xmlns:card_view="http://schemas.android.com/apk/res-auto">
 
+  <data>
+
+    <variable
+      name="viewModel"
+      type="org.oppia.android.app.options.AudioLanguageSelectionViewModel" />
+  </data>
+
   <androidx.constraintlayout.widget.ConstraintLayout
     android:layout_width="match_parent"
     android:layout_height="match_parent"
@@ -88,7 +95,9 @@
           android:layout_width="match_parent"
           android:layout_height="wrap_content"
           android:inputType="none"
-          android:padding="@dimen/onboarding_shared_padding_small" />
+          android:padding="@dimen/onboarding_shared_padding_small"
+          app:filter="@{false}"
+          app:languageSelection="@{viewModel.selectedAudioLanguage}" />
       </com.google.android.material.textfield.TextInputLayout>
     </com.google.android.material.card.MaterialCardView>
 
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">
 
         <AutoCompleteTextView
+          android:id="@+id/onboarding_language_dropdown"
           android:layout_width="match_parent"
           android:layout_height="wrap_content"
           android:inputType="none"
diff --git a/app/src/main/res/layout/text_input_layout_binding_adapters_test_activity.xml b/app/src/main/res/layout/text_input_layout_binding_adapters_test_activity.xml
new file mode 100644
index 00000000000..b52cc1e44b2
--- /dev/null
+++ b/app/src/main/res/layout/text_input_layout_binding_adapters_test_activity.xml
@@ -0,0 +1,6 @@
+<?xml version="1.0" encoding="utf-8"?>
+<FrameLayout xmlns:android="http://schemas.android.com/apk/res/android"
+  android:id="@+id/background"
+  android:layout_width="match_parent"
+  android:layout_height="match_parent"
+  android:orientation="vertical" />
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 @@
+<?xml version="1.0" encoding="utf-8"?>
+<layout xmlns:android="http://schemas.android.com/apk/res/android">
+
+  <FrameLayout
+    android:id="@+id/fragment_container"
+    android:layout_width="match_parent"
+    android:layout_height="match_parent">
+
+    <com.google.android.material.textfield.TextInputLayout
+      android:id="@+id/test_text_input_view"
+      android:layout_width="match_parent"
+      android:layout_height="match_parent">
+
+      <AutoCompleteTextView
+        android:id="@+id/test_autocomplete_view"
+        android:layout_width="match_parent"
+        android:layout_height="wrap_content"
+        android:inputType="none" />
+    </com.google.android.material.textfield.TextInputLayout>
+  </FrameLayout>
+</layout>
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 @@
   <string name="add_profile_error_name_not_unique">This name is already in use by another profile.</string>
   <string name="add_profile_error_name_empty">Please enter a valid name for this profile.</string>
   <string name="add_profile_error_name_only_letters">Please choose a profile name that doesn\'t include numbers or symbols.</string>
+  <string name="add_profile_error_missing_profile_type">Profile type unknown.</string>
+  <string name="add_profile_default_error_message">An error occurred while creating a profile.</string>
   <string name="add_profile_error_pin_length">Your PIN should be 3 digits long.</string>
   <string name="add_profile_error_pin_confirm_wrong">Please make sure that both PINs match.</string>
   <string name="add_profile_info_content_description">More information on 3-digit PINs.</string>
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<TextInputLayoutBindingAdaptersTestActivity>? {
+      val scenario = ActivityScenario.launch<TextInputLayoutBindingAdaptersTestActivity>(
+        TextInputLayoutBindingAdaptersTestActivity.createIntent(context)
+      )
+      testCoroutineDispatchers.runCurrent()
+      return scenario
+    }
+
+  private fun setUpTestApplicationComponent() {
+    ApplicationProvider.getApplicationContext<TestApplication>().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<CreateProfileActivity>(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<CreateProfileActivity>? {
-      val scenario = ActivityScenario.launch<CreateProfileActivity>(
-        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<CreateProfileActivity>(intent)
       testCoroutineDispatchers.runCurrent()
       return scenario
     }
 
-  private fun <T : MessageLite> hasProtoExtra(keyName: String, expectedProto: T): Matcher<Intent> {
-    val defaultProto = expectedProto.newBuilderForType().build()
-    return object : TypeSafeMatcher<Intent>() {
-      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<TestApplication>().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<IntroActivity>? {
       val scenario = ActivityScenario.launch<IntroActivity>(
-        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<IntroActivity>? {
+      val params = IntroActivityParams.newBuilder()
+        .setProfileNickname(testProfileNickname)
+        .build()
+
       val scenario = ActivityScenario.launch<IntroActivity>(
-        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<AudioLanguageActivity>(
       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<AudioLanguageActivity>(
+      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<AudioLanguageActivity>(
+      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<Any?> {
@@ -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<Any?> {
+    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<Any?> {
+    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()