From 137d66a5118cb625ffd90f6dbc56811449bcefe2 Mon Sep 17 00:00:00 2001 From: Elise Richards Date: Wed, 10 Jun 2020 16:52:47 -0500 Subject: [PATCH] For 10172: Set edit text listeners (#11196) * Set edit text listeners * Set clearable icons and change with error states * Clear text buttons show and hide * Move error checks to afterTextChanged. Refactor. Remove unused color. --- .../settings/logins/EditLoginFragment.kt | 151 ++++++++++++++---- .../settings/logins/LoginsFragmentStore.kt | 2 +- .../saved_login_clear_edit_text_tint.xml | 10 ++ .../main/res/layout/fragment_edit_login.xml | 20 +-- .../main/res/layout/fragment_login_detail.xml | 5 +- app/src/main/res/values/dimens.xml | 2 +- 6 files changed, 142 insertions(+), 48 deletions(-) create mode 100644 app/src/main/res/color/saved_login_clear_edit_text_tint.xml diff --git a/app/src/main/java/org/mozilla/fenix/settings/logins/EditLoginFragment.kt b/app/src/main/java/org/mozilla/fenix/settings/logins/EditLoginFragment.kt index b4d2a0beb..80f54d5b6 100644 --- a/app/src/main/java/org/mozilla/fenix/settings/logins/EditLoginFragment.kt +++ b/app/src/main/java/org/mozilla/fenix/settings/logins/EditLoginFragment.kt @@ -7,30 +7,24 @@ package org.mozilla.fenix.settings.logins import android.os.Bundle import android.text.Editable import android.text.InputType +import android.text.TextWatcher import android.util.Log import android.view.Menu import android.view.MenuInflater import android.view.MenuItem import android.view.View +import androidx.core.content.ContextCompat import androidx.fragment.app.Fragment import androidx.lifecycle.lifecycleScope import androidx.navigation.fragment.findNavController import androidx.navigation.fragment.navArgs -import com.google.android.material.snackbar.Snackbar -import kotlinx.android.synthetic.main.fragment_edit_login.inputLayoutPassword -import kotlinx.android.synthetic.main.fragment_edit_login.inputLayoutUsername -import kotlinx.android.synthetic.main.fragment_edit_login.hostnameText -import kotlinx.android.synthetic.main.fragment_edit_login.usernameText -import kotlinx.android.synthetic.main.fragment_edit_login.passwordText -import kotlinx.android.synthetic.main.fragment_edit_login.clearUsernameTextButton -import kotlinx.android.synthetic.main.fragment_edit_login.clearPasswordTextButton -import kotlinx.android.synthetic.main.fragment_edit_login.revealPasswordButton -import kotlinx.coroutines.Deferred -import kotlinx.coroutines.launch +import kotlinx.android.synthetic.main.fragment_edit_login.* import kotlinx.coroutines.CancellationException +import kotlinx.coroutines.Deferred import kotlinx.coroutines.Dispatchers.IO import kotlinx.coroutines.Dispatchers.Main import kotlinx.coroutines.async +import kotlinx.coroutines.launch import kotlinx.coroutines.withContext import mozilla.components.concept.storage.Login import mozilla.components.service.sync.logins.InvalidRecordException @@ -38,7 +32,6 @@ import mozilla.components.service.sync.logins.LoginsStorageException import mozilla.components.service.sync.logins.NoSuchRecordException import mozilla.components.support.ktx.android.view.hideKeyboard import org.mozilla.fenix.R -import org.mozilla.fenix.components.FenixSnackbar import org.mozilla.fenix.components.StoreProvider import org.mozilla.fenix.components.metrics.Event import org.mozilla.fenix.ext.components @@ -55,10 +48,12 @@ class EditLoginFragment : Fragment(R.layout.fragment_edit_login) { private lateinit var savedLoginsStore: LoginsFragmentStore fun String.toEditable(): Editable = Editable.Factory.getInstance().newEditable(this) + private lateinit var oldLogin: SavedLogin + override fun onCreate(savedInstanceState: Bundle?) { super.onCreate(savedInstanceState) setHasOptionsMenu(true) - + oldLogin = args.savedLoginItem savedLoginsStore = StoreProvider.get(this) { LoginsFragmentStore( LoginsListState( @@ -82,12 +77,16 @@ class EditLoginFragment : Fragment(R.layout.fragment_edit_login) { hostnameText.isFocusable = false usernameText.text = args.savedLoginItem.username.toEditable() - passwordText.text = args.savedLoginItem.password!!.toEditable() + passwordText.text = args.savedLoginItem.password.toEditable() // TODO: extend PasswordTransformationMethod() to change bullets to asterisks passwordText.inputType = InputType.TYPE_CLASS_TEXT or InputType.TYPE_TEXT_VARIATION_PASSWORD + passwordText.compoundDrawablePadding = + requireContext().resources + .getDimensionPixelOffset(R.dimen.saved_logins_end_icon_drawable_padding) setUpClickListeners() + setUpTextListeners() } private fun setUpClickListeners() { @@ -96,18 +95,100 @@ class EditLoginFragment : Fragment(R.layout.fragment_edit_login) { usernameText.isCursorVisible = true usernameText.hasFocus() inputLayoutUsername.hasFocus() + it.isEnabled = false } clearPasswordTextButton.setOnClickListener { passwordText.text?.clear() passwordText.isCursorVisible = true passwordText.hasFocus() inputLayoutPassword.hasFocus() + it.isEnabled = false } revealPasswordButton.setOnClickListener { togglePasswordReveal() } + + var firstClick = true passwordText.setOnClickListener { - togglePasswordReveal() + if (firstClick) { + togglePasswordReveal() + firstClick = false + } + } + } + + private fun setUpTextListeners() { + val frag = view?.findViewById(R.id.editLoginFragment) + frag?.onFocusChangeListener = View.OnFocusChangeListener { _, hasFocus -> + if (hasFocus) { + view?.hideKeyboard() + } + } + + editLoginLayout.onFocusChangeListener = View.OnFocusChangeListener { _, hasFocus -> + if (!hasFocus) { + view?.hideKeyboard() + } + } + + usernameText.addTextChangedListener(object : TextWatcher { + override fun afterTextChanged(u: Editable?) { + if (u.toString() == oldLogin.username) { + inputLayoutUsername.error = null + inputLayoutUsername.errorIconDrawable = null + } else { + clearUsernameTextButton.isEnabled = true + // setDupeError() TODO in #10173 + } + } + + override fun beforeTextChanged(u: CharSequence?, start: Int, count: Int, after: Int) { + // NOOP + } + + override fun onTextChanged(u: CharSequence?, start: Int, before: Int, count: Int) { + // NOOP + } + }) + + passwordText.addTextChangedListener(object : TextWatcher { + override fun afterTextChanged(p: Editable?) { + when { + p.toString().isEmpty() -> { + clearPasswordTextButton.isEnabled = false + setPasswordError() + } + p.toString() == oldLogin.password -> { + inputLayoutPassword.error = null + inputLayoutPassword.errorIconDrawable = null + clearPasswordTextButton.isEnabled = true + } + else -> { + inputLayoutPassword.error = null + inputLayoutPassword.errorIconDrawable = null + clearPasswordTextButton.isEnabled = true + } + } + } + + override fun beforeTextChanged(p: CharSequence?, start: Int, count: Int, after: Int) { + // NOOP + } + + override fun onTextChanged(p: CharSequence?, start: Int, before: Int, count: Int) { + // NOOP + } + }) + } + + private fun setPasswordError() { + inputLayoutPassword?.let { layout -> + layout.error = context?.getString(R.string.saved_login_password_required) + layout.setErrorIconDrawable(R.drawable.mozac_ic_warning) + + layout.errorIconDrawable?.setTint( + ContextCompat.getColor(requireContext(), R.color.design_default_color_error) + ) } } @@ -126,26 +207,26 @@ class EditLoginFragment : Fragment(R.layout.fragment_edit_login) { override fun onOptionsItemSelected(item: MenuItem): Boolean = when (item.itemId) { R.id.save_login_button -> { view?.hideKeyboard() - try { - if (!passwordText.text.isNullOrBlank()) { + if (!passwordText.text.isNullOrBlank()) { + try { attemptSaveAndExit() - } else { - view?.let { - FenixSnackbar.make( - view = it, - duration = Snackbar.LENGTH_SHORT, - isDisplayedWithBrowserToolbar = false - ).setText(getString(R.string.saved_login_password_required)).show() + } catch (loginException: LoginsStorageException) { + when (loginException) { + is NoSuchRecordException, + is InvalidRecordException -> { + Log.e( + "Edit login", + "Failed to save edited login.", + loginException + ) + } + else -> Log.e( + "Edit login", + "Failed to save edited login.", + loginException + ) } } - } catch (loginException: LoginsStorageException) { - when (loginException) { - is NoSuchRecordException, - is InvalidRecordException -> { - Log.e("Edit login", "Failed to save edited login.", loginException) - } - else -> Log.e("Edit login", "Failed to save edited login.", loginException) - } } true } @@ -158,9 +239,11 @@ class EditLoginFragment : Fragment(R.layout.fragment_edit_login) { var saveLoginJob: Deferred? = null viewLifecycleOwner.lifecycleScope.launch(IO) { saveLoginJob = async { - val oldLogin = requireContext().components.core.passwordsStorage.get(args.savedLoginItem.guid) + val oldLogin = + requireContext().components.core.passwordsStorage.get(args.savedLoginItem.guid) - // Update requires a Login type, which needs at least one of httpRealm or formActionOrigin + // Update requires a Login type, which needs at least one of + // httpRealm or formActionOrigin val loginToSave = Login( guid = oldLogin?.guid, origin = oldLogin?.origin!!, diff --git a/app/src/main/java/org/mozilla/fenix/settings/logins/LoginsFragmentStore.kt b/app/src/main/java/org/mozilla/fenix/settings/logins/LoginsFragmentStore.kt index 06b4f10dc..7cc6ab37c 100644 --- a/app/src/main/java/org/mozilla/fenix/settings/logins/LoginsFragmentStore.kt +++ b/app/src/main/java/org/mozilla/fenix/settings/logins/LoginsFragmentStore.kt @@ -24,7 +24,7 @@ data class SavedLogin( val guid: String, val origin: String, val username: String, - val password: String?, + val password: String, val timeLastUsed: Long ) : Parcelable diff --git a/app/src/main/res/color/saved_login_clear_edit_text_tint.xml b/app/src/main/res/color/saved_login_clear_edit_text_tint.xml new file mode 100644 index 000000000..acb4e5e7e --- /dev/null +++ b/app/src/main/res/color/saved_login_clear_edit_text_tint.xml @@ -0,0 +1,10 @@ + + + + + + \ No newline at end of file diff --git a/app/src/main/res/layout/fragment_edit_login.xml b/app/src/main/res/layout/fragment_edit_login.xml index 68e25da69..344aeb043 100644 --- a/app/src/main/res/layout/fragment_edit_login.xml +++ b/app/src/main/res/layout/fragment_edit_login.xml @@ -12,7 +12,9 @@ android:layout_height="wrap_content" android:layout_marginStart="72dp" android:layout_marginEnd="20dp" - android:layout_marginTop="12dp" > + android:layout_marginTop="12dp" + android:clickable="true" + android:focusable="true" > @@ -135,9 +137,9 @@ android:layout_width="48dp" android:layout_height="30dp" android:layout_marginBottom="10dp" - android:background="?android:attr/selectableItemBackgroundBorderless" + android:background="@null" android:contentDescription="@string/saved_login_copy_username" - app:tint="?android:colorAccent" + app:tint="@color/saved_login_clear_edit_text_tint" app:layout_constraintEnd_toEndOf="parent" app:layout_constraintBottom_toBottomOf="@id/inputLayoutUsername" app:srcCompat="@drawable/ic_clear" /> @@ -186,6 +188,7 @@ android:colorControlActivated="?primaryText" android:colorControlHighlight="?primaryText" android:cursorVisible="true" + android:textCursorDrawable="@null" android:ellipsize="end" android:focusable="true" android:fontFamily="sans-serif" @@ -195,7 +198,6 @@ android:maxLines="1" android:singleLine="true" android:textColor="?primaryText" - android:textCursorDrawable="?primaryText" android:textSize="16sp" android:textStyle="normal" app:backgroundTint="?primaryText" @@ -207,9 +209,9 @@ android:layout_width="48dp" android:layout_height="30dp" android:layout_marginTop="3dp" - android:background="?android:attr/selectableItemBackgroundBorderless" + android:background="@null" android:contentDescription="@string/saved_login_reveal_password" - app:tint="?android:colorAccent" + app:tint="?primaryText" app:layout_constraintEnd_toStartOf="@id/clearPasswordTextButton" app:layout_constraintTop_toTopOf="@id/inputLayoutPassword" app:srcCompat="@drawable/mozac_ic_password_reveal" /> @@ -218,9 +220,9 @@ android:id="@+id/clearPasswordTextButton" android:layout_width="48dp" android:layout_height="30dp" - android:background="?android:attr/selectableItemBackgroundBorderless" + android:background="@null" android:contentDescription="@string/saved_logins_copy_password" - app:tint="?android:colorAccent" + app:tint="@color/saved_login_clear_edit_text_tint" app:layout_constraintEnd_toEndOf="parent" app:layout_constraintTop_toTopOf="@id/revealPasswordButton" app:srcCompat="@drawable/ic_clear" /> diff --git a/app/src/main/res/layout/fragment_login_detail.xml b/app/src/main/res/layout/fragment_login_detail.xml index 1c8c9c2bf..e6cfa17a5 100644 --- a/app/src/main/res/layout/fragment_login_detail.xml +++ b/app/src/main/res/layout/fragment_login_detail.xml @@ -124,7 +124,6 @@ android:layout_width="0dp" android:layout_height="30dp" android:gravity="center_vertical" - android:inputType="textPassword|text" android:letterSpacing="0.01" android:lineSpacingExtra="8sp" android:layout_marginTop="2dp" @@ -142,7 +141,7 @@ android:layout_width="48dp" android:layout_height="30dp" android:layout_marginBottom="2dp" - android:background="?android:attr/selectableItemBackgroundBorderless" + android:background="@null" android:contentDescription="@string/saved_login_reveal_password" app:layout_constraintBottom_toBottomOf="@id/passwordText" app:layout_constraintEnd_toStartOf="@id/copyPassword" @@ -153,7 +152,7 @@ android:id="@+id/copyPassword" android:layout_width="48dp" android:layout_height="30dp" - android:background="?android:attr/selectableItemBackgroundBorderless" + android:background="@null" android:contentDescription="@string/saved_logins_copy_password" app:layout_constraintBottom_toBottomOf="@id/revealPasswordButton" app:layout_constraintEnd_toEndOf="parent" diff --git a/app/src/main/res/values/dimens.xml b/app/src/main/res/values/dimens.xml index eed8da787..3f38be6ad 100644 --- a/app/src/main/res/values/dimens.xml +++ b/app/src/main/res/values/dimens.xml @@ -154,6 +154,6 @@ 10dp 12dp 5dp - + 16dp