1
0
Fork 0

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.
master
Elise Richards 2020-06-10 16:52:47 -05:00 committed by GitHub
parent 31edbc924c
commit 137d66a511
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
6 changed files with 142 additions and 48 deletions

View File

@ -7,30 +7,24 @@ package org.mozilla.fenix.settings.logins
import android.os.Bundle import android.os.Bundle
import android.text.Editable import android.text.Editable
import android.text.InputType import android.text.InputType
import android.text.TextWatcher
import android.util.Log import android.util.Log
import android.view.Menu import android.view.Menu
import android.view.MenuInflater import android.view.MenuInflater
import android.view.MenuItem import android.view.MenuItem
import android.view.View import android.view.View
import androidx.core.content.ContextCompat
import androidx.fragment.app.Fragment import androidx.fragment.app.Fragment
import androidx.lifecycle.lifecycleScope import androidx.lifecycle.lifecycleScope
import androidx.navigation.fragment.findNavController import androidx.navigation.fragment.findNavController
import androidx.navigation.fragment.navArgs import androidx.navigation.fragment.navArgs
import com.google.android.material.snackbar.Snackbar import kotlinx.android.synthetic.main.fragment_edit_login.*
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.coroutines.CancellationException import kotlinx.coroutines.CancellationException
import kotlinx.coroutines.Deferred
import kotlinx.coroutines.Dispatchers.IO import kotlinx.coroutines.Dispatchers.IO
import kotlinx.coroutines.Dispatchers.Main import kotlinx.coroutines.Dispatchers.Main
import kotlinx.coroutines.async import kotlinx.coroutines.async
import kotlinx.coroutines.launch
import kotlinx.coroutines.withContext import kotlinx.coroutines.withContext
import mozilla.components.concept.storage.Login import mozilla.components.concept.storage.Login
import mozilla.components.service.sync.logins.InvalidRecordException 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.service.sync.logins.NoSuchRecordException
import mozilla.components.support.ktx.android.view.hideKeyboard import mozilla.components.support.ktx.android.view.hideKeyboard
import org.mozilla.fenix.R import org.mozilla.fenix.R
import org.mozilla.fenix.components.FenixSnackbar
import org.mozilla.fenix.components.StoreProvider import org.mozilla.fenix.components.StoreProvider
import org.mozilla.fenix.components.metrics.Event import org.mozilla.fenix.components.metrics.Event
import org.mozilla.fenix.ext.components import org.mozilla.fenix.ext.components
@ -55,10 +48,12 @@ class EditLoginFragment : Fragment(R.layout.fragment_edit_login) {
private lateinit var savedLoginsStore: LoginsFragmentStore private lateinit var savedLoginsStore: LoginsFragmentStore
fun String.toEditable(): Editable = Editable.Factory.getInstance().newEditable(this) fun String.toEditable(): Editable = Editable.Factory.getInstance().newEditable(this)
private lateinit var oldLogin: SavedLogin
override fun onCreate(savedInstanceState: Bundle?) { override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState) super.onCreate(savedInstanceState)
setHasOptionsMenu(true) setHasOptionsMenu(true)
oldLogin = args.savedLoginItem
savedLoginsStore = StoreProvider.get(this) { savedLoginsStore = StoreProvider.get(this) {
LoginsFragmentStore( LoginsFragmentStore(
LoginsListState( LoginsListState(
@ -82,12 +77,16 @@ class EditLoginFragment : Fragment(R.layout.fragment_edit_login) {
hostnameText.isFocusable = false hostnameText.isFocusable = false
usernameText.text = args.savedLoginItem.username.toEditable() 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 // TODO: extend PasswordTransformationMethod() to change bullets to asterisks
passwordText.inputType = InputType.TYPE_CLASS_TEXT or InputType.TYPE_TEXT_VARIATION_PASSWORD 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() setUpClickListeners()
setUpTextListeners()
} }
private fun setUpClickListeners() { private fun setUpClickListeners() {
@ -96,18 +95,100 @@ class EditLoginFragment : Fragment(R.layout.fragment_edit_login) {
usernameText.isCursorVisible = true usernameText.isCursorVisible = true
usernameText.hasFocus() usernameText.hasFocus()
inputLayoutUsername.hasFocus() inputLayoutUsername.hasFocus()
it.isEnabled = false
} }
clearPasswordTextButton.setOnClickListener { clearPasswordTextButton.setOnClickListener {
passwordText.text?.clear() passwordText.text?.clear()
passwordText.isCursorVisible = true passwordText.isCursorVisible = true
passwordText.hasFocus() passwordText.hasFocus()
inputLayoutPassword.hasFocus() inputLayoutPassword.hasFocus()
it.isEnabled = false
} }
revealPasswordButton.setOnClickListener { revealPasswordButton.setOnClickListener {
togglePasswordReveal() togglePasswordReveal()
} }
var firstClick = true
passwordText.setOnClickListener { passwordText.setOnClickListener {
togglePasswordReveal() if (firstClick) {
togglePasswordReveal()
firstClick = false
}
}
}
private fun setUpTextListeners() {
val frag = view?.findViewById<View>(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) { override fun onOptionsItemSelected(item: MenuItem): Boolean = when (item.itemId) {
R.id.save_login_button -> { R.id.save_login_button -> {
view?.hideKeyboard() view?.hideKeyboard()
try { if (!passwordText.text.isNullOrBlank()) {
if (!passwordText.text.isNullOrBlank()) { try {
attemptSaveAndExit() attemptSaveAndExit()
} else { } catch (loginException: LoginsStorageException) {
view?.let { when (loginException) {
FenixSnackbar.make( is NoSuchRecordException,
view = it, is InvalidRecordException -> {
duration = Snackbar.LENGTH_SHORT, Log.e(
isDisplayedWithBrowserToolbar = false "Edit login",
).setText(getString(R.string.saved_login_password_required)).show() "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 true
} }
@ -158,9 +239,11 @@ class EditLoginFragment : Fragment(R.layout.fragment_edit_login) {
var saveLoginJob: Deferred<Unit>? = null var saveLoginJob: Deferred<Unit>? = null
viewLifecycleOwner.lifecycleScope.launch(IO) { viewLifecycleOwner.lifecycleScope.launch(IO) {
saveLoginJob = async { 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( val loginToSave = Login(
guid = oldLogin?.guid, guid = oldLogin?.guid,
origin = oldLogin?.origin!!, origin = oldLogin?.origin!!,

View File

@ -24,7 +24,7 @@ data class SavedLogin(
val guid: String, val guid: String,
val origin: String, val origin: String,
val username: String, val username: String,
val password: String?, val password: String,
val timeLastUsed: Long val timeLastUsed: Long
) : Parcelable ) : Parcelable

View File

@ -0,0 +1,10 @@
<?xml version="1.0" encoding="utf-8"?>
<!-- This Source Code Form is subject to the terms of the Mozilla Public
- License, v. 2.0. If a copy of the MPL was not distributed with this
- file, You can obtain one at http://mozilla.org/MPL/2.0/. -->
<selector xmlns:android="http://schemas.android.com/apk/res/android">
<item android:state_enabled="true"
android:color="?primaryText" />
<item android:state_enabled="false"
android:color="@android:color/transparent" />
</selector>

View File

@ -12,7 +12,9 @@
android:layout_height="wrap_content" android:layout_height="wrap_content"
android:layout_marginStart="72dp" android:layout_marginStart="72dp"
android:layout_marginEnd="20dp" android:layout_marginEnd="20dp"
android:layout_marginTop="12dp" > android:layout_marginTop="12dp"
android:clickable="true"
android:focusable="true" >
<TextView <TextView
android:id="@+id/hostnameHeaderText" android:id="@+id/hostnameHeaderText"
@ -125,7 +127,7 @@
android:clickable="true" android:clickable="true"
android:focusable="true" android:focusable="true"
android:cursorVisible="true" android:cursorVisible="true"
android:textCursorDrawable="?primaryText" android:textCursorDrawable="@null"
app:backgroundTint="?primaryText" app:backgroundTint="?primaryText"
tools:ignore="Autofill"/> tools:ignore="Autofill"/>
</com.google.android.material.textfield.TextInputLayout> </com.google.android.material.textfield.TextInputLayout>
@ -135,9 +137,9 @@
android:layout_width="48dp" android:layout_width="48dp"
android:layout_height="30dp" android:layout_height="30dp"
android:layout_marginBottom="10dp" android:layout_marginBottom="10dp"
android:background="?android:attr/selectableItemBackgroundBorderless" android:background="@null"
android:contentDescription="@string/saved_login_copy_username" 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_constraintEnd_toEndOf="parent"
app:layout_constraintBottom_toBottomOf="@id/inputLayoutUsername" app:layout_constraintBottom_toBottomOf="@id/inputLayoutUsername"
app:srcCompat="@drawable/ic_clear" /> app:srcCompat="@drawable/ic_clear" />
@ -186,6 +188,7 @@
android:colorControlActivated="?primaryText" android:colorControlActivated="?primaryText"
android:colorControlHighlight="?primaryText" android:colorControlHighlight="?primaryText"
android:cursorVisible="true" android:cursorVisible="true"
android:textCursorDrawable="@null"
android:ellipsize="end" android:ellipsize="end"
android:focusable="true" android:focusable="true"
android:fontFamily="sans-serif" android:fontFamily="sans-serif"
@ -195,7 +198,6 @@
android:maxLines="1" android:maxLines="1"
android:singleLine="true" android:singleLine="true"
android:textColor="?primaryText" android:textColor="?primaryText"
android:textCursorDrawable="?primaryText"
android:textSize="16sp" android:textSize="16sp"
android:textStyle="normal" android:textStyle="normal"
app:backgroundTint="?primaryText" app:backgroundTint="?primaryText"
@ -207,9 +209,9 @@
android:layout_width="48dp" android:layout_width="48dp"
android:layout_height="30dp" android:layout_height="30dp"
android:layout_marginTop="3dp" android:layout_marginTop="3dp"
android:background="?android:attr/selectableItemBackgroundBorderless" android:background="@null"
android:contentDescription="@string/saved_login_reveal_password" android:contentDescription="@string/saved_login_reveal_password"
app:tint="?android:colorAccent" app:tint="?primaryText"
app:layout_constraintEnd_toStartOf="@id/clearPasswordTextButton" app:layout_constraintEnd_toStartOf="@id/clearPasswordTextButton"
app:layout_constraintTop_toTopOf="@id/inputLayoutPassword" app:layout_constraintTop_toTopOf="@id/inputLayoutPassword"
app:srcCompat="@drawable/mozac_ic_password_reveal" /> app:srcCompat="@drawable/mozac_ic_password_reveal" />
@ -218,9 +220,9 @@
android:id="@+id/clearPasswordTextButton" android:id="@+id/clearPasswordTextButton"
android:layout_width="48dp" android:layout_width="48dp"
android:layout_height="30dp" android:layout_height="30dp"
android:background="?android:attr/selectableItemBackgroundBorderless" android:background="@null"
android:contentDescription="@string/saved_logins_copy_password" 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_constraintEnd_toEndOf="parent"
app:layout_constraintTop_toTopOf="@id/revealPasswordButton" app:layout_constraintTop_toTopOf="@id/revealPasswordButton"
app:srcCompat="@drawable/ic_clear" /> app:srcCompat="@drawable/ic_clear" />

View File

@ -124,7 +124,6 @@
android:layout_width="0dp" android:layout_width="0dp"
android:layout_height="30dp" android:layout_height="30dp"
android:gravity="center_vertical" android:gravity="center_vertical"
android:inputType="textPassword|text"
android:letterSpacing="0.01" android:letterSpacing="0.01"
android:lineSpacingExtra="8sp" android:lineSpacingExtra="8sp"
android:layout_marginTop="2dp" android:layout_marginTop="2dp"
@ -142,7 +141,7 @@
android:layout_width="48dp" android:layout_width="48dp"
android:layout_height="30dp" android:layout_height="30dp"
android:layout_marginBottom="2dp" android:layout_marginBottom="2dp"
android:background="?android:attr/selectableItemBackgroundBorderless" android:background="@null"
android:contentDescription="@string/saved_login_reveal_password" android:contentDescription="@string/saved_login_reveal_password"
app:layout_constraintBottom_toBottomOf="@id/passwordText" app:layout_constraintBottom_toBottomOf="@id/passwordText"
app:layout_constraintEnd_toStartOf="@id/copyPassword" app:layout_constraintEnd_toStartOf="@id/copyPassword"
@ -153,7 +152,7 @@
android:id="@+id/copyPassword" android:id="@+id/copyPassword"
android:layout_width="48dp" android:layout_width="48dp"
android:layout_height="30dp" android:layout_height="30dp"
android:background="?android:attr/selectableItemBackgroundBorderless" android:background="@null"
android:contentDescription="@string/saved_logins_copy_password" android:contentDescription="@string/saved_logins_copy_password"
app:layout_constraintBottom_toBottomOf="@id/revealPasswordButton" app:layout_constraintBottom_toBottomOf="@id/revealPasswordButton"
app:layout_constraintEnd_toEndOf="parent" app:layout_constraintEnd_toEndOf="parent"

View File

@ -154,6 +154,6 @@
<dimen name="saved_logins_sort_menu_dropdown_chevron_icon_margin_start">10dp</dimen> <dimen name="saved_logins_sort_menu_dropdown_chevron_icon_margin_start">10dp</dimen>
<dimen name="saved_logins_sort_menu_dropdown_chevron_icon_size">12dp</dimen> <dimen name="saved_logins_sort_menu_dropdown_chevron_icon_size">12dp</dimen>
<dimen name="saved_logins_detail_menu_vertical_padding">5dp</dimen> <dimen name="saved_logins_detail_menu_vertical_padding">5dp</dimen>
<dimen name="saved_logins_end_icon_drawable_padding">16dp</dimen>
</resources> </resources>