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 @@ + + + + + + + + + + + + + + + + + + + +