diff --git a/app/src/main/AndroidManifest.xml b/app/src/main/AndroidManifest.xml
index 88d592793fb..c69ebee0a1f 100644
--- a/app/src/main/AndroidManifest.xml
+++ b/app/src/main/AndroidManifest.xml
@@ -27,6 +27,12 @@
android:screenOrientation="portrait"
android:windowSoftInputMode="adjustResize"/>
+
+
+ fun inject(addProfileActivity: AddProfileActivity)
+ fun inject(adminAuthActivity: AdminAuthActivity)
fun inject(audioFragmentTestActivity: AudioFragmentTestActivity)
fun inject(bindableAdapterTestActivity: BindableAdapterTestActivity)
fun inject(conceptCardFragmentTestActivity: ConceptCardFragmentTestActivity)
diff --git a/app/src/main/java/org/oppia/app/fragment/FragmentComponent.kt b/app/src/main/java/org/oppia/app/fragment/FragmentComponent.kt
index ace5a6bb839..2c1f81024d4 100644
--- a/app/src/main/java/org/oppia/app/fragment/FragmentComponent.kt
+++ b/app/src/main/java/org/oppia/app/fragment/FragmentComponent.kt
@@ -11,8 +11,6 @@ import org.oppia.app.player.state.StateFragment
import org.oppia.app.settings.profile.ProfileEditFragment
import org.oppia.app.settings.profile.ProfileListFragment
import org.oppia.app.player.state.itemviewmodel.InteractionViewModelModule
-import org.oppia.app.profile.AddProfileFragment
-import org.oppia.app.profile.AdminAuthFragment
import org.oppia.app.profile.ProfileChooserFragment
import org.oppia.app.story.StoryFragment
import org.oppia.app.testing.BindableAdapterTestFragment
@@ -57,6 +55,4 @@ interface FragmentComponent {
fun inject(profileListFragment: ProfileListFragment)
fun inject(profileEditFragment: ProfileEditFragment)
fun inject(profileChooserFragment: ProfileChooserFragment)
- fun inject(adminAuthFragment: AdminAuthFragment)
- fun inject(addProfileFragment: AddProfileFragment)
}
diff --git a/app/src/main/java/org/oppia/app/profile/AddProfileActivity.kt b/app/src/main/java/org/oppia/app/profile/AddProfileActivity.kt
new file mode 100644
index 00000000000..b55aea24781
--- /dev/null
+++ b/app/src/main/java/org/oppia/app/profile/AddProfileActivity.kt
@@ -0,0 +1,30 @@
+package org.oppia.app.profile
+
+import android.content.Intent
+import android.os.Bundle
+import kotlinx.coroutines.ExperimentalCoroutinesApi
+import org.oppia.app.activity.InjectableAppCompatActivity
+import javax.inject.Inject
+
+/** Activity that allows users to create new profiles. */
+class AddProfileActivity : InjectableAppCompatActivity() {
+ @Inject
+ lateinit var addProfileFragmentPresenter: AddProfileActivityPresenter
+
+ @ExperimentalCoroutinesApi
+ override fun onCreate(savedInstanceState: Bundle?) {
+ super.onCreate(savedInstanceState)
+ activityComponent.inject(this)
+ addProfileFragmentPresenter.handleOnCreate()
+ }
+
+ override fun onSupportNavigateUp(): Boolean {
+ finish()
+ return false
+ }
+
+ override fun onActivityResult(requestCode: Int, resultCode: Int, data: Intent?) {
+ super.onActivityResult(requestCode, resultCode, data)
+ addProfileFragmentPresenter.handleOnActivityResult(requestCode, resultCode, data)
+ }
+}
diff --git a/app/src/main/java/org/oppia/app/profile/AddProfileActivityPresenter.kt b/app/src/main/java/org/oppia/app/profile/AddProfileActivityPresenter.kt
new file mode 100644
index 00000000000..10ef574e8de
--- /dev/null
+++ b/app/src/main/java/org/oppia/app/profile/AddProfileActivityPresenter.kt
@@ -0,0 +1,207 @@
+package org.oppia.app.profile
+
+import android.app.Activity
+import android.content.Context
+import android.content.Intent
+import android.net.Uri
+import android.provider.MediaStore
+import android.text.Editable
+import android.text.TextWatcher
+import android.view.inputmethod.InputMethodManager
+import android.widget.EditText
+import android.widget.ImageView
+import androidx.appcompat.app.AlertDialog
+import androidx.appcompat.app.AppCompatActivity
+import androidx.databinding.DataBindingUtil
+import androidx.lifecycle.Observer
+import com.bumptech.glide.Glide
+import com.bumptech.glide.request.RequestOptions
+import kotlinx.coroutines.ExperimentalCoroutinesApi
+import org.oppia.app.R
+import org.oppia.app.activity.ActivityScope
+import org.oppia.app.databinding.AddProfileActivityBinding
+import org.oppia.app.viewmodel.ViewModelProvider
+import org.oppia.domain.profile.ProfileManagementController
+import org.oppia.util.data.AsyncResult
+import javax.inject.Inject
+
+const val GALLERY_INTENT_RESULT_CODE = 1
+
+/** The presenter for [AddProfileActivity]. */
+@ActivityScope
+class AddProfileActivityPresenter @Inject constructor(
+ private val activity: AppCompatActivity,
+ private val profileManagementController: ProfileManagementController,
+ private val viewModelProvider: ViewModelProvider
+) {
+ private lateinit var uploadImageView: ImageView
+ private val profileViewModel by lazy {
+ getAddProfileViewModel()
+ }
+ private var selectedImage: Uri? = null
+ private var allowDownloadAccess = false
+ private var inputtedPin = false
+ private var inputtedConfirmPin = false
+
+ @ExperimentalCoroutinesApi
+ fun handleOnCreate() {
+ activity.title = activity.getString(R.string.add_profile_title)
+ activity.supportActionBar?.setDisplayHomeAsUpEnabled(true)
+ activity.supportActionBar?.setHomeAsUpIndicator(R.drawable.ic_close_white_24dp)
+
+ val binding = DataBindingUtil.setContentView(activity, R.layout.add_profile_activity)
+
+ binding.apply {
+ viewModel = profileViewModel
+ }
+
+ binding.allowDownloadSwitch.setOnCheckedChangeListener { _, isChecked ->
+ allowDownloadAccess = isChecked
+ }
+
+ binding.infoIcon.setOnClickListener {
+ showInfoDialog()
+ }
+
+ uploadImageView = binding.uploadImageButton
+
+ addTextChangedListeners(binding)
+ addButtonListeners(binding)
+ }
+
+ fun handleOnActivityResult(requestCode: Int, resultCode: Int, data: Intent?) {
+ if (requestCode == GALLERY_INTENT_RESULT_CODE && resultCode == Activity.RESULT_OK) {
+ data?.let {
+ selectedImage = data.data
+ Glide.with(activity)
+ .load(selectedImage)
+ .centerCrop()
+ .apply(RequestOptions.circleCropTransform())
+ .into(uploadImageView)
+ }
+ }
+ }
+
+ private fun addButtonListeners(binding: AddProfileActivityBinding) {
+ binding.uploadImageButton.setOnClickListener {
+ val galleryIntent = Intent(Intent.ACTION_PICK, MediaStore.Images.Media.EXTERNAL_CONTENT_URI)
+ activity.startActivityForResult(galleryIntent, GALLERY_INTENT_RESULT_CODE)
+ }
+
+ binding.createButton.setOnClickListener {
+ profileViewModel.clearAllErrorMessages()
+
+ val imm = activity.getSystemService(Context.INPUT_METHOD_SERVICE) as? InputMethodManager
+ imm?.hideSoftInputFromWindow(activity.currentFocus?.windowToken, 0)
+
+ val name = binding.inputName.getInput()
+ val pin = binding.inputPin.getInput()
+ val confirmPin = binding.inputConfirmPin.getInput()
+
+ if (checkInputsAreValid(name, pin, confirmPin)) {
+ binding.scroll.smoothScrollTo(0, 0)
+ return@setOnClickListener
+ }
+
+ profileManagementController.addProfile(name, pin, selectedImage, allowDownloadAccess, isAdmin = false)
+ .observe(activity, Observer {
+ handleAddProfileResult(it, binding)
+ })
+ }
+ }
+
+ private fun checkInputsAreValid(name: String, pin: String, confirmPin: String): Boolean {
+ var failed = false
+ if (name.isEmpty()) {
+ profileViewModel.nameErrorMsg.set(activity.resources.getString(R.string.add_profile_error_name_empty))
+ failed = true
+ }
+ if (pin.isNotEmpty() && pin.length < 3) {
+ profileViewModel.pinErrorMsg.set(activity.resources.getString(R.string.add_profile_error_pin_length))
+ failed = true
+ }
+ if (pin != confirmPin) {
+ profileViewModel.confirmPinErrorMsg.set(activity.resources.getString(R.string.add_profile_error_pin_confirm_wrong))
+ failed = true
+ }
+ return failed
+ }
+
+ private fun handleAddProfileResult(result: AsyncResult, binding: AddProfileActivityBinding) {
+ if (result.isSuccess()) {
+ val intent = Intent(activity, ProfileActivity::class.java)
+ intent.addFlags(Intent.FLAG_ACTIVITY_CLEAR_TOP)
+ activity.startActivity(intent)
+ } else if (result.isFailure()) {
+ when (result.getErrorOrNull()) {
+ is ProfileManagementController.ProfileNameNotUniqueException -> profileViewModel.nameErrorMsg.set(
+ activity.resources.getString(
+ R.string.add_profile_error_name_not_unique
+ )
+ )
+ is ProfileManagementController.ProfileNameOnlyLettersException -> profileViewModel.nameErrorMsg.set(
+ activity.resources.getString(
+ R.string.add_profile_error_name_only_letters
+ )
+ )
+ }
+ binding.scroll.smoothScrollTo(0, 0)
+ }
+ }
+
+ private fun addTextChangedListeners(binding: AddProfileActivityBinding) {
+ fun setValidPin() {
+ if (inputtedPin && inputtedConfirmPin) {
+ profileViewModel.validPin.set(true)
+ } else {
+ binding.allowDownloadSwitch.isChecked = false
+ profileViewModel.validPin.set(false)
+ }
+ }
+
+ addTextChangedListener(binding.inputPin) { pin ->
+ pin?.let {
+ profileViewModel.pinErrorMsg.set("")
+ inputtedPin = pin.isNotEmpty()
+ setValidPin()
+ }
+ }
+
+ addTextChangedListener(binding.inputConfirmPin) { confirmPin ->
+ confirmPin?.let {
+ profileViewModel.confirmPinErrorMsg.set("")
+ inputtedConfirmPin = confirmPin.isNotEmpty()
+ setValidPin()
+ }
+ }
+
+ addTextChangedListener(binding.inputName) { name ->
+ name?.let {
+ profileViewModel.nameErrorMsg.set("")
+ }
+ }
+ }
+
+ private fun addTextChangedListener(profileInputView: ProfileInputView, onTextChanged: (CharSequence?) -> Unit) {
+ profileInputView.addTextChangedListener(object : TextWatcher {
+ override fun onTextChanged(p0: CharSequence?, p1: Int, p2: Int, p3: Int) {
+ onTextChanged(p0)
+ }
+
+ override fun afterTextChanged(p0: Editable?) {}
+ override fun beforeTextChanged(p0: CharSequence?, p1: Int, p2: Int, p3: Int) {}
+ })
+ }
+
+ private fun showInfoDialog() {
+ AlertDialog.Builder(activity as Context, R.style.AlertDialogTheme)
+ .setMessage(R.string.add_profile_pin_info)
+ .setPositiveButton(R.string.add_profile_close) { dialog, _ ->
+ dialog.dismiss()
+ }.create().show()
+ }
+
+ private fun getAddProfileViewModel(): AddProfileViewModel {
+ return viewModelProvider.getForActivity(activity, AddProfileViewModel::class.java)
+ }
+}
diff --git a/app/src/main/java/org/oppia/app/profile/AddProfileFragment.kt b/app/src/main/java/org/oppia/app/profile/AddProfileFragment.kt
deleted file mode 100644
index 118ced0d6d1..00000000000
--- a/app/src/main/java/org/oppia/app/profile/AddProfileFragment.kt
+++ /dev/null
@@ -1,23 +0,0 @@
-package org.oppia.app.profile
-
-import android.content.Context
-import android.os.Bundle
-import android.view.LayoutInflater
-import android.view.View
-import android.view.ViewGroup
-import org.oppia.app.fragment.InjectableFragment
-import javax.inject.Inject
-
-/** Fragment that allows users to create new profiles. */
-class AddProfileFragment : InjectableFragment() {
- @Inject lateinit var addProfileFragmentPresenter: AddProfileFragmentPresenter
-
- override fun onAttach(context: Context) {
- super.onAttach(context)
- fragmentComponent.inject(this)
- }
-
- override fun onCreateView(inflater: LayoutInflater, container: ViewGroup?, savedInstanceState: Bundle?): View? {
- return addProfileFragmentPresenter.handleCreateView(inflater, container)
- }
-}
diff --git a/app/src/main/java/org/oppia/app/profile/AddProfileFragmentPresenter.kt b/app/src/main/java/org/oppia/app/profile/AddProfileFragmentPresenter.kt
deleted file mode 100644
index 262f347f9fc..00000000000
--- a/app/src/main/java/org/oppia/app/profile/AddProfileFragmentPresenter.kt
+++ /dev/null
@@ -1,20 +0,0 @@
-package org.oppia.app.profile
-
-import android.view.LayoutInflater
-import android.view.View
-import android.view.ViewGroup
-import androidx.fragment.app.Fragment
-import org.oppia.app.databinding.AddProfileFragmentBinding
-import org.oppia.app.fragment.FragmentScope
-import javax.inject.Inject
-
-/** The presenter for [AddProfileFragment]. */
-@FragmentScope
-class AddProfileFragmentPresenter @Inject constructor(
- private val fragment: Fragment
-) {
- fun handleCreateView(inflater: LayoutInflater, container: ViewGroup?): View? {
- val binding = AddProfileFragmentBinding.inflate(inflater, container, /* attachToRoot= */ false)
- return binding.root
- }
-}
diff --git a/app/src/main/java/org/oppia/app/profile/AddProfileViewModel.kt b/app/src/main/java/org/oppia/app/profile/AddProfileViewModel.kt
new file mode 100644
index 00000000000..e3f19d02b19
--- /dev/null
+++ b/app/src/main/java/org/oppia/app/profile/AddProfileViewModel.kt
@@ -0,0 +1,21 @@
+package org.oppia.app.profile
+
+import androidx.databinding.ObservableField
+import org.oppia.app.activity.ActivityScope
+import org.oppia.app.viewmodel.ObservableViewModel
+import javax.inject.Inject
+
+/** The ViewModel for [AddProfileActivity]. */
+@ActivityScope
+class AddProfileViewModel @Inject constructor() : ObservableViewModel() {
+ val validPin = ObservableField(false)
+ val pinErrorMsg = ObservableField("")
+ val confirmPinErrorMsg = ObservableField("")
+ val nameErrorMsg = ObservableField("")
+
+ fun clearAllErrorMessages() {
+ pinErrorMsg.set("")
+ confirmPinErrorMsg.set("")
+ nameErrorMsg.set("")
+ }
+}
diff --git a/app/src/main/java/org/oppia/app/profile/AdminAuthActivity.kt b/app/src/main/java/org/oppia/app/profile/AdminAuthActivity.kt
new file mode 100644
index 00000000000..3c65704f362
--- /dev/null
+++ b/app/src/main/java/org/oppia/app/profile/AdminAuthActivity.kt
@@ -0,0 +1,35 @@
+package org.oppia.app.profile
+
+import android.content.Context
+import android.content.Intent
+import android.os.Bundle
+import org.oppia.app.activity.InjectableAppCompatActivity
+import javax.inject.Inject
+
+const val KEY_ADMIN_AUTH_ADMIN_PIN = "ADMIN_AUTH_ADMIN_PIN"
+
+/** Activity that authenticates by checking for admin's PIN. */
+class AdminAuthActivity : InjectableAppCompatActivity() {
+ @Inject
+ lateinit var adminAuthFragmentPresenter: AdminAuthActivityPresenter
+
+ companion object {
+ fun createAdminAuthActivityIntent(context: Context, adminPin: String): Intent {
+ val intent = Intent(context, AdminAuthActivity::class.java)
+ intent.addFlags(Intent.FLAG_ACTIVITY_NO_HISTORY)
+ intent.putExtra(KEY_ADMIN_AUTH_ADMIN_PIN, adminPin)
+ return intent
+ }
+ }
+
+ override fun onCreate(savedInstanceState: Bundle?) {
+ super.onCreate(savedInstanceState)
+ activityComponent.inject(this)
+ adminAuthFragmentPresenter.handleOnCreate()
+ }
+
+ override fun onSupportNavigateUp(): Boolean {
+ finish()
+ return false
+ }
+}
diff --git a/app/src/main/java/org/oppia/app/profile/AdminAuthActivityPresenter.kt b/app/src/main/java/org/oppia/app/profile/AdminAuthActivityPresenter.kt
new file mode 100644
index 00000000000..75c67a9493c
--- /dev/null
+++ b/app/src/main/java/org/oppia/app/profile/AdminAuthActivityPresenter.kt
@@ -0,0 +1,64 @@
+package org.oppia.app.profile
+
+import android.content.Intent
+import android.text.Editable
+import android.text.TextWatcher
+import androidx.appcompat.app.AppCompatActivity
+import androidx.databinding.DataBindingUtil
+import org.oppia.app.R
+import org.oppia.app.activity.ActivityScope
+import org.oppia.app.databinding.AdminAuthActivityBinding
+import org.oppia.app.viewmodel.ViewModelProvider
+import javax.inject.Inject
+
+/** The presenter for [AdminAuthActivity]. */
+@ActivityScope
+class AdminAuthActivityPresenter @Inject constructor(
+ private val activity: AppCompatActivity,
+ private val viewModelProvider: ViewModelProvider
+) {
+ private val authViewModel by lazy {
+ getAdminAuthViewModel()
+ }
+
+ /** Binds ViewModel and sets up text and button listeners. */
+ fun handleOnCreate() {
+ activity.title = activity.getString(R.string.add_profile_title)
+ activity.supportActionBar?.setDisplayHomeAsUpEnabled(true)
+ activity.supportActionBar?.setHomeAsUpIndicator(R.drawable.ic_close_white_24dp)
+
+ val binding = DataBindingUtil.setContentView(activity, R.layout.admin_auth_activity)
+ val adminPin = activity.intent.getStringExtra(KEY_ADMIN_AUTH_ADMIN_PIN)
+ binding.apply {
+ lifecycleOwner = activity
+ viewModel = authViewModel
+ }
+
+ binding.inputPin.addTextChangedListener(object : TextWatcher {
+ override fun onTextChanged(confirmPin: CharSequence?, start: Int, before: Int, count: Int) {
+ confirmPin?.let {
+ authViewModel.errorMessage.set("")
+ }
+ }
+
+ override fun afterTextChanged(confirmPin: Editable?) {}
+ override fun beforeTextChanged(p0: CharSequence?, start: Int, count: Int, after: Int) {}
+ })
+
+ binding.submitButton.setOnClickListener {
+ val inputPin = binding.inputPin.getInput()
+ if (inputPin.isEmpty()) {
+ return@setOnClickListener
+ }
+ if (inputPin == adminPin) {
+ activity.startActivity(Intent(activity, AddProfileActivity::class.java))
+ } else {
+ authViewModel.errorMessage.set(activity.resources.getString(R.string.admin_auth_incorrect))
+ }
+ }
+ }
+
+ private fun getAdminAuthViewModel(): AdminAuthViewModel {
+ return viewModelProvider.getForActivity(activity, AdminAuthViewModel::class.java)
+ }
+}
diff --git a/app/src/main/java/org/oppia/app/profile/AdminAuthFragment.kt b/app/src/main/java/org/oppia/app/profile/AdminAuthFragment.kt
deleted file mode 100644
index 8fabb431deb..00000000000
--- a/app/src/main/java/org/oppia/app/profile/AdminAuthFragment.kt
+++ /dev/null
@@ -1,23 +0,0 @@
-package org.oppia.app.profile
-
-import android.content.Context
-import android.os.Bundle
-import android.view.LayoutInflater
-import android.view.View
-import android.view.ViewGroup
-import org.oppia.app.fragment.InjectableFragment
-import javax.inject.Inject
-
-/** Fragment that authenticates by checking for admin's PIN. */
-class AdminAuthFragment : InjectableFragment() {
- @Inject lateinit var adminAuthFragmentPresenter: AdminAuthFragmentPresenter
-
- override fun onAttach(context: Context) {
- super.onAttach(context)
- fragmentComponent.inject(this)
- }
-
- override fun onCreateView(inflater: LayoutInflater, container: ViewGroup?, savedInstanceState: Bundle?): View? {
- return adminAuthFragmentPresenter.handleCreateView(inflater, container)
- }
-}
diff --git a/app/src/main/java/org/oppia/app/profile/AdminAuthFragmentPresenter.kt b/app/src/main/java/org/oppia/app/profile/AdminAuthFragmentPresenter.kt
deleted file mode 100644
index d34e2c3fd1d..00000000000
--- a/app/src/main/java/org/oppia/app/profile/AdminAuthFragmentPresenter.kt
+++ /dev/null
@@ -1,20 +0,0 @@
-package org.oppia.app.profile
-
-import android.view.LayoutInflater
-import android.view.View
-import android.view.ViewGroup
-import androidx.fragment.app.Fragment
-import org.oppia.app.databinding.AdminAuthFragmentBinding
-import org.oppia.app.fragment.FragmentScope
-import javax.inject.Inject
-
-/** The presenter for [AdminAuthFragment]. */
-@FragmentScope
-class AdminAuthFragmentPresenter @Inject constructor(
- private val fragment: Fragment
-) {
- fun handleCreateView(inflater: LayoutInflater, container: ViewGroup?): View? {
- val binding = AdminAuthFragmentBinding.inflate(inflater, container, /* attachToRoot= */ false)
- return binding.root
- }
-}
diff --git a/app/src/main/java/org/oppia/app/profile/AdminAuthViewModel.kt b/app/src/main/java/org/oppia/app/profile/AdminAuthViewModel.kt
new file mode 100644
index 00000000000..003966112b7
--- /dev/null
+++ b/app/src/main/java/org/oppia/app/profile/AdminAuthViewModel.kt
@@ -0,0 +1,12 @@
+package org.oppia.app.profile
+
+import androidx.databinding.ObservableField
+import org.oppia.app.activity.ActivityScope
+import org.oppia.app.viewmodel.ObservableViewModel
+import javax.inject.Inject
+
+/** The ViewModel for [AdminAuthActivity]. */
+@ActivityScope
+class AdminAuthViewModel @Inject constructor() : ObservableViewModel() {
+ val errorMessage = ObservableField("")
+}
diff --git a/app/src/main/java/org/oppia/app/profile/ProfileActivity.kt b/app/src/main/java/org/oppia/app/profile/ProfileActivity.kt
index 6094df970f3..d9b9e71cf41 100644
--- a/app/src/main/java/org/oppia/app/profile/ProfileActivity.kt
+++ b/app/src/main/java/org/oppia/app/profile/ProfileActivity.kt
@@ -1,6 +1,7 @@
package org.oppia.app.profile
import android.os.Bundle
+import kotlinx.coroutines.ExperimentalCoroutinesApi
import org.oppia.app.activity.InjectableAppCompatActivity
import javax.inject.Inject
@@ -9,6 +10,7 @@ class ProfileActivity : InjectableAppCompatActivity() {
@Inject
lateinit var profileActivityPresenter: ProfileActivityPresenter
+ @ExperimentalCoroutinesApi
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
activityComponent.inject(this)
diff --git a/app/src/main/java/org/oppia/app/profile/ProfileActivityPresenter.kt b/app/src/main/java/org/oppia/app/profile/ProfileActivityPresenter.kt
index a736150a344..2e06a3d1671 100644
--- a/app/src/main/java/org/oppia/app/profile/ProfileActivityPresenter.kt
+++ b/app/src/main/java/org/oppia/app/profile/ProfileActivityPresenter.kt
@@ -1,14 +1,22 @@
package org.oppia.app.profile
import androidx.appcompat.app.AppCompatActivity
+import kotlinx.coroutines.ExperimentalCoroutinesApi
import org.oppia.app.R
import org.oppia.app.activity.ActivityScope
+import org.oppia.domain.profile.ProfileManagementController
import javax.inject.Inject
/** The presenter for [ProfileActivity]. */
@ActivityScope
-class ProfileActivityPresenter @Inject constructor(private val activity: AppCompatActivity) {
+class ProfileActivityPresenter @Inject constructor(
+ private val activity: AppCompatActivity,
+ private val profileManagementController: ProfileManagementController
+) {
+ /** Adds [ProfileChooserFragment] to view. */
fun handleOnCreate() {
+ // TODO(#482): Ensures that an admin profile is present. Remove when there is proper admin account creation.
+ profileManagementController.addProfile("Sean", "12345", null, true, true)
activity.setContentView(R.layout.profile_activity)
if (getProfileChooserFragment() == null) {
activity.supportFragmentManager.beginTransaction().add(
diff --git a/app/src/main/java/org/oppia/app/profile/ProfileChooserFragmentPresenter.kt b/app/src/main/java/org/oppia/app/profile/ProfileChooserFragmentPresenter.kt
index a6e082db1e6..cee975162ab 100644
--- a/app/src/main/java/org/oppia/app/profile/ProfileChooserFragmentPresenter.kt
+++ b/app/src/main/java/org/oppia/app/profile/ProfileChooserFragmentPresenter.kt
@@ -6,7 +6,6 @@ import android.view.View
import android.view.ViewGroup
import androidx.fragment.app.Fragment
import androidx.lifecycle.Observer
-import org.oppia.app.R
import org.oppia.app.databinding.ProfileChooserAddViewBinding
import org.oppia.app.databinding.ProfileChooserFragmentBinding
import org.oppia.app.databinding.ProfileChooserProfileViewBinding
@@ -25,12 +24,16 @@ class ProfileChooserFragmentPresenter @Inject constructor(
private val viewModelProvider: ViewModelProvider,
private val profileManagementController: ProfileManagementController
) {
+ private val chooserViewModel: ProfileChooserViewModel by lazy {
+ getProfileChooserViewModel()
+ }
+
/** Binds ViewModel and sets up RecyclerView Adapter. */
fun handleCreateView(inflater: LayoutInflater, container: ViewGroup?): View? {
val binding =
ProfileChooserFragmentBinding.inflate(inflater, container, /* attachToRoot= */ false)
binding.apply {
- viewModel = getProfileChooserViewModel()
+ viewModel = chooserViewModel
lifecycleOwner = fragment
}
binding.profileRecyclerView.apply {
@@ -74,24 +77,9 @@ class ProfileChooserFragmentPresenter @Inject constructor(
}
}
- private fun bindAddView(binding: ProfileChooserAddViewBinding, data: ProfileChooserModel) {
+ private fun bindAddView(binding: ProfileChooserAddViewBinding, @Suppress("UNUSED_PARAMETER") data: ProfileChooserModel) {
binding.root.setOnClickListener {
- if (getAdminAuthFragment() == null) {
- fragment.requireActivity().supportFragmentManager.beginTransaction()
- .setCustomAnimations(
- R.anim.slide_up,
- R.anim.slide_down,
- R.anim.slide_up,
- R.anim.slide_down
- ).add(
- R.id.profile_chooser_fragment_placeholder,
- AdminAuthFragment()
- ).addToBackStack(null).commit()
- }
+ fragment.requireActivity().startActivity(AdminAuthActivity.createAdminAuthActivityIntent(fragment.requireContext(), chooserViewModel.adminPin))
}
}
-
- private fun getAdminAuthFragment(): AdminAuthFragment? {
- return fragment.requireActivity().supportFragmentManager.findFragmentById(R.id.profile_chooser_fragment_placeholder) as? AdminAuthFragment?
- }
}
diff --git a/app/src/main/java/org/oppia/app/profile/ProfileInputView.kt b/app/src/main/java/org/oppia/app/profile/ProfileInputView.kt
new file mode 100644
index 00000000000..29ed931ea62
--- /dev/null
+++ b/app/src/main/java/org/oppia/app/profile/ProfileInputView.kt
@@ -0,0 +1,76 @@
+package org.oppia.app.profile
+
+import android.content.Context
+import android.text.InputFilter
+import android.text.InputType
+import android.text.TextWatcher
+import android.util.AttributeSet
+import android.view.LayoutInflater
+import android.widget.EditText
+import android.widget.LinearLayout
+import android.widget.TextView
+import androidx.databinding.BindingAdapter
+import androidx.databinding.DataBindingUtil
+import org.oppia.app.R
+import org.oppia.app.databinding.ProfileInputViewBinding
+
+/** Custom view that is used for name or pin input with error messages */
+class ProfileInputView @JvmOverloads constructor(
+ context: Context,
+ attrs: AttributeSet? = null,
+ defStyle: Int = 0
+) : LinearLayout(context, attrs, defStyle) {
+ companion object {
+ @JvmStatic
+ @BindingAdapter("profile:error")
+ fun setProfileImage(profileInputView: ProfileInputView, errorMessage: String) {
+ if (errorMessage.isEmpty()) {
+ profileInputView.clearErrorText()
+ } else {
+ profileInputView.setErrorText(errorMessage)
+ }
+ }
+ }
+
+ private var errorText: TextView
+ private var input: EditText
+
+ init {
+ val binding = DataBindingUtil.inflate(
+ LayoutInflater.from(context),
+ R.layout.profile_input_view, this,
+ /* attachToRoot= */ true
+ )
+ val attributes = context.obtainStyledAttributes(attrs, R.styleable.ProfileInputView)
+ binding.labelText.text = attributes.getString(R.styleable.ProfileInputView_label)
+ input = binding.input
+ errorText = binding.errorText
+ orientation = VERTICAL
+ if (attributes.getBoolean(R.styleable.ProfileInputView_isPasswordInput, /** defVal= */ false)) {
+ input.inputType = InputType.TYPE_CLASS_NUMBER or InputType.TYPE_NUMBER_VARIATION_PASSWORD
+ }
+ val inputLength = attributes.getInt(R.styleable.ProfileInputView_inputLength, -1)
+ if (inputLength > 0) {
+ input.filters = arrayOf(InputFilter.LengthFilter(inputLength))
+ }
+ attributes.recycle()
+ }
+
+ /** Gets input of editText. */
+ fun getInput() = input.text.toString()
+
+ /** Allows editText to be watched. */
+ fun addTextChangedListener(textWatcher: TextWatcher) = input.addTextChangedListener(textWatcher)
+
+ /** Clears red border and error text. */
+ fun clearErrorText() {
+ input.background = context.resources.getDrawable(R.drawable.edit_text_black_border)
+ errorText.text = ""
+ }
+
+ /** Sets red border and error text. */
+ fun setErrorText(errorMessage: String) {
+ input.background = context.resources.getDrawable(R.drawable.edit_text_red_border)
+ errorText.text = errorMessage
+ }
+}
diff --git a/app/src/main/java/org/oppia/app/viewmodel/ViewModelProvider.kt b/app/src/main/java/org/oppia/app/viewmodel/ViewModelProvider.kt
index d8dbc644bfa..ae386f4da38 100644
--- a/app/src/main/java/org/oppia/app/viewmodel/ViewModelProvider.kt
+++ b/app/src/main/java/org/oppia/app/viewmodel/ViewModelProvider.kt
@@ -1,5 +1,6 @@
package org.oppia.app.viewmodel
+import androidx.appcompat.app.AppCompatActivity
import androidx.fragment.app.Fragment
import androidx.lifecycle.ViewModel
import androidx.lifecycle.ViewModelProviders
@@ -14,4 +15,9 @@ class ViewModelProvider @Inject constructor(private val bridgeFac
fun getForFragment(fragment: Fragment, clazz: Class): V {
return ViewModelProviders.of(fragment, bridgeFactory).get(clazz)
}
+
+ /** Retrieves a new instance of the [ViewModel] of type [V] scoped to the specified activity. */
+ fun getForActivity(activity: AppCompatActivity, clazz: Class): V {
+ return ViewModelProviders.of(activity, bridgeFactory).get(clazz)
+ }
}
diff --git a/app/src/main/res/drawable/edit_text_black_border.xml b/app/src/main/res/drawable/edit_text_black_border.xml
new file mode 100644
index 00000000000..0a0a90fb291
--- /dev/null
+++ b/app/src/main/res/drawable/edit_text_black_border.xml
@@ -0,0 +1,13 @@
+
+
+
+
+
+
+
diff --git a/app/src/main/res/drawable/edit_text_red_border.xml b/app/src/main/res/drawable/edit_text_red_border.xml
new file mode 100644
index 00000000000..eb23869b8ab
--- /dev/null
+++ b/app/src/main/res/drawable/edit_text_red_border.xml
@@ -0,0 +1,13 @@
+
+
+
+
+
+
+
diff --git a/app/src/main/res/drawable/ic_info_icon.xml b/app/src/main/res/drawable/ic_info_icon.xml
new file mode 100644
index 00000000000..715b90c9bd8
--- /dev/null
+++ b/app/src/main/res/drawable/ic_info_icon.xml
@@ -0,0 +1,9 @@
+
+
+
diff --git a/app/src/main/res/drawable/ic_upload_photo.xml b/app/src/main/res/drawable/ic_upload_photo.xml
new file mode 100644
index 00000000000..5d53be664a1
--- /dev/null
+++ b/app/src/main/res/drawable/ic_upload_photo.xml
@@ -0,0 +1,12 @@
+
+
+
+
+
+
+
diff --git a/app/src/main/res/layout/add_profile_activity.xml b/app/src/main/res/layout/add_profile_activity.xml
new file mode 100644
index 00000000000..ec205a11a96
--- /dev/null
+++ b/app/src/main/res/layout/add_profile_activity.xml
@@ -0,0 +1,118 @@
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
diff --git a/app/src/main/res/layout/add_profile_fragment.xml b/app/src/main/res/layout/add_profile_fragment.xml
deleted file mode 100644
index 7e42279a97a..00000000000
--- a/app/src/main/res/layout/add_profile_fragment.xml
+++ /dev/null
@@ -1,7 +0,0 @@
-
-
-
-
-
diff --git a/app/src/main/res/layout/admin_auth_activity.xml b/app/src/main/res/layout/admin_auth_activity.xml
new file mode 100644
index 00000000000..b32102680b6
--- /dev/null
+++ b/app/src/main/res/layout/admin_auth_activity.xml
@@ -0,0 +1,65 @@
+
+
+
+
+
+
+
+
+
+
+
+
diff --git a/app/src/main/res/layout/admin_auth_fragment.xml b/app/src/main/res/layout/admin_auth_fragment.xml
deleted file mode 100644
index a4ffe4965ec..00000000000
--- a/app/src/main/res/layout/admin_auth_fragment.xml
+++ /dev/null
@@ -1,13 +0,0 @@
-
-
-
-
-
-
diff --git a/app/src/main/res/layout/profile_chooser_profile_view.xml b/app/src/main/res/layout/profile_chooser_profile_view.xml
index 6acd7131542..8a5556b3618 100644
--- a/app/src/main/res/layout/profile_chooser_profile_view.xml
+++ b/app/src/main/res/layout/profile_chooser_profile_view.xml
@@ -1,42 +1,50 @@
+ xmlns:app="http://schemas.android.com/apk/res-auto"
+ xmlns:profile="http://schemas.android.com/tools">
+
+
+
+
+ android:gravity="center"
+ android:orientation="vertical">
+
+ profile:src="@{viewModel.profile.avatarImageUri}" />
+
+ android:textColor="@color/white"
+ android:textSize="16sp" />
+
+ android:visibility="@{viewModel.profile.isAdmin ? View.VISIBLE : View.GONE}" />
-
+
diff --git a/app/src/main/res/layout/profile_input_view.xml b/app/src/main/res/layout/profile_input_view.xml
new file mode 100644
index 00000000000..79df37f043b
--- /dev/null
+++ b/app/src/main/res/layout/profile_input_view.xml
@@ -0,0 +1,28 @@
+
+
+
+
+
+
+
+
diff --git a/app/src/main/res/values/attrs.xml b/app/src/main/res/values/attrs.xml
new file mode 100644
index 00000000000..97d2ed089d1
--- /dev/null
+++ b/app/src/main/res/values/attrs.xml
@@ -0,0 +1,8 @@
+
+
+
+
+
+
+
+
diff --git a/app/src/main/res/values/colors.xml b/app/src/main/res/values/colors.xml
index c7d54437ffe..4410c664b8a 100644
--- a/app/src/main/res/values/colors.xml
+++ b/app/src/main/res/values/colors.xml
@@ -36,10 +36,12 @@
#33000000
#8A000000
#F9F9F9
+ #FF0000
@color/oppiaDarkBlue
#4E4E50
+ #FFFFF0
#212121
#26A69A
#C55F45
diff --git a/app/src/main/res/values/strings.xml b/app/src/main/res/values/strings.xml
index 2146a71a534..cef185b1719 100755
--- a/app/src/main/res/values/strings.xml
+++ b/app/src/main/res/values/strings.xml
@@ -106,4 +106,25 @@
Administrator
Select your profile
Add Profile
+
+ Administrator Authorization Required
+ Enter the Administrator PIN in order to create a new account.
+ Administrator\'s PIN (5 digits)
+ Incorrect Administrator PIN. Please try again.
+
+ Add Profile
+ Name (required)
+ 3-Digit PIN (optional)
+ Confirm 3-Digit PIN
+ Allow Download Access
+ User is able to download and delete content without Administrator password. Only available with PIN.
+ CREATE
+ CLOSE
+ With a PIN, nobody else can access a profile besides this assigned user.
+ We failed to store your avatar image. Please try again.
+ This name is already in use by another profile.
+ Please enter a name for this profile.
+ Names can only have letters. Try another name?
+ Your PIN should be 3 digits long.
+ Please make sure that both PINs match.
diff --git a/app/src/main/res/values/styles.xml b/app/src/main/res/values/styles.xml
index 5f4ed235634..ca2bacc34f7 100644
--- a/app/src/main/res/values/styles.xml
+++ b/app/src/main/res/values/styles.xml
@@ -91,6 +91,13 @@
- 14sp
- false
+
+
+