You cannot select more than 25 topics
Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.
299 lines
11 KiB
Kotlin
299 lines
11 KiB
Kotlin
4 years ago
|
/* 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/. */
|
||
|
|
||
4 years ago
|
package org.mozilla.fenix.settings.logins.fragment
|
||
4 years ago
|
|
||
|
import android.os.Bundle
|
||
|
import android.text.Editable
|
||
|
import android.text.InputType
|
||
4 years ago
|
import android.text.TextWatcher
|
||
4 years ago
|
import android.view.Menu
|
||
|
import android.view.MenuInflater
|
||
|
import android.view.MenuItem
|
||
|
import android.view.View
|
||
4 years ago
|
import androidx.appcompat.view.menu.ActionMenuItemView
|
||
4 years ago
|
import androidx.fragment.app.Fragment
|
||
|
import androidx.lifecycle.lifecycleScope
|
||
|
import androidx.navigation.fragment.findNavController
|
||
|
import androidx.navigation.fragment.navArgs
|
||
4 years ago
|
import kotlinx.android.synthetic.main.fragment_edit_login.*
|
||
4 years ago
|
import kotlinx.android.synthetic.main.fragment_edit_login.view.*
|
||
|
import kotlinx.coroutines.ExperimentalCoroutinesApi
|
||
|
import mozilla.components.lib.state.ext.consumeFrom
|
||
4 years ago
|
import mozilla.components.support.ktx.android.view.hideKeyboard
|
||
|
import org.mozilla.fenix.R
|
||
|
import org.mozilla.fenix.components.StoreProvider
|
||
|
import org.mozilla.fenix.components.metrics.Event
|
||
4 years ago
|
import org.mozilla.fenix.ext.redirectToReAuth
|
||
4 years ago
|
import org.mozilla.fenix.ext.requireComponents
|
||
4 years ago
|
import org.mozilla.fenix.ext.settings
|
||
4 years ago
|
import org.mozilla.fenix.settings.logins.LoginsAction
|
||
|
import org.mozilla.fenix.settings.logins.LoginsFragmentStore
|
||
|
import org.mozilla.fenix.settings.logins.LoginsListState
|
||
|
import org.mozilla.fenix.settings.logins.SavedLogin
|
||
|
import org.mozilla.fenix.settings.logins.controller.SavedLoginsStorageController
|
||
|
import org.mozilla.fenix.settings.logins.interactor.EditLoginInteractor
|
||
|
import org.mozilla.fenix.settings.logins.view.EditLoginView
|
||
4 years ago
|
|
||
|
/**
|
||
4 years ago
|
* Displays the editable saved login information for a single website
|
||
4 years ago
|
*/
|
||
4 years ago
|
@ExperimentalCoroutinesApi
|
||
4 years ago
|
@Suppress("TooManyFunctions", "NestedBlockDepth", "ForbiddenComment")
|
||
|
class EditLoginFragment : Fragment(R.layout.fragment_edit_login) {
|
||
|
|
||
|
fun String.toEditable(): Editable = Editable.Factory.getInstance().newEditable(this)
|
||
|
|
||
4 years ago
|
private val args by navArgs<EditLoginFragmentArgs>()
|
||
|
private lateinit var loginsFragmentStore: LoginsFragmentStore
|
||
|
private lateinit var interactor: EditLoginInteractor
|
||
|
private lateinit var editLoginView: EditLoginView
|
||
4 years ago
|
private lateinit var oldLogin: SavedLogin
|
||
|
|
||
4 years ago
|
private var listOfPossibleDupes: List<SavedLogin>? = null
|
||
|
|
||
|
private var usernameChanged = false
|
||
|
private var passwordChanged = false
|
||
|
private var saveEnabled = false
|
||
|
private var showPassword = true
|
||
|
|
||
|
private var validPassword = true
|
||
|
private var validUsername = true
|
||
|
|
||
|
override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
|
||
|
super.onViewCreated(view, savedInstanceState)
|
||
4 years ago
|
setHasOptionsMenu(true)
|
||
4 years ago
|
oldLogin = args.savedLoginItem
|
||
4 years ago
|
editLoginView = EditLoginView(view.editLoginLayout)
|
||
|
|
||
|
loginsFragmentStore = StoreProvider.get(this) {
|
||
4 years ago
|
LoginsFragmentStore(
|
||
|
LoginsListState(
|
||
|
isLoading = true,
|
||
|
loginList = listOf(),
|
||
|
filteredItems = listOf(),
|
||
|
searchedForText = null,
|
||
|
sortingStrategy = requireContext().settings().savedLoginsSortingStrategy,
|
||
4 years ago
|
highlightedItem = requireContext().settings().savedLoginsMenuHighlightedItem,
|
||
|
duplicateLogins = listOf()
|
||
4 years ago
|
)
|
||
|
)
|
||
|
}
|
||
|
|
||
4 years ago
|
interactor = EditLoginInteractor(
|
||
|
SavedLoginsStorageController(
|
||
|
context = requireContext(),
|
||
|
viewLifecycleScope = viewLifecycleOwner.lifecycleScope,
|
||
|
navController = findNavController(),
|
||
|
loginsFragmentStore = loginsFragmentStore
|
||
|
)
|
||
|
)
|
||
4 years ago
|
|
||
4 years ago
|
loginsFragmentStore.dispatch(LoginsAction.UpdateCurrentLogin(args.savedLoginItem))
|
||
|
interactor.findPotentialDuplicates(args.savedLoginItem.guid)
|
||
4 years ago
|
|
||
4 years ago
|
// initialize editable values
|
||
|
hostnameText.text = args.savedLoginItem.origin.toEditable()
|
||
4 years ago
|
usernameText.text = args.savedLoginItem.username.toEditable()
|
||
4 years ago
|
passwordText.text = args.savedLoginItem.password.toEditable()
|
||
4 years ago
|
|
||
4 years ago
|
formatEditableValues()
|
||
|
initSaveState()
|
||
|
setUpClickListeners()
|
||
|
setUpTextListeners()
|
||
|
editLoginView.showPassword()
|
||
|
|
||
|
consumeFrom(loginsFragmentStore) {
|
||
|
listOfPossibleDupes = loginsFragmentStore.state.duplicateLogins
|
||
|
}
|
||
|
}
|
||
|
|
||
|
private fun initSaveState() {
|
||
|
saveEnabled = false // don't enable saving until something has been changed
|
||
|
val saveButton =
|
||
|
activity?.findViewById<ActionMenuItemView>(R.id.save_login_button)
|
||
|
saveButton?.isEnabled = saveEnabled
|
||
|
|
||
|
usernameChanged = false
|
||
|
passwordChanged = false
|
||
|
}
|
||
|
|
||
|
private fun formatEditableValues() {
|
||
|
hostnameText.isClickable = false
|
||
|
hostnameText.isFocusable = false
|
||
|
usernameText.inputType = InputType.TYPE_TEXT_FLAG_NO_SUGGESTIONS
|
||
4 years ago
|
// TODO: extend PasswordTransformationMethod() to change bullets to asterisks
|
||
|
passwordText.inputType = InputType.TYPE_CLASS_TEXT or InputType.TYPE_TEXT_VARIATION_PASSWORD
|
||
4 years ago
|
passwordText.compoundDrawablePadding =
|
||
|
requireContext().resources
|
||
|
.getDimensionPixelOffset(R.dimen.saved_logins_end_icon_drawable_padding)
|
||
4 years ago
|
}
|
||
|
|
||
|
private fun setUpClickListeners() {
|
||
|
clearUsernameTextButton.setOnClickListener {
|
||
|
usernameText.text?.clear()
|
||
|
usernameText.isCursorVisible = true
|
||
|
usernameText.hasFocus()
|
||
|
inputLayoutUsername.hasFocus()
|
||
4 years ago
|
it.isEnabled = false
|
||
4 years ago
|
}
|
||
|
clearPasswordTextButton.setOnClickListener {
|
||
|
passwordText.text?.clear()
|
||
|
passwordText.isCursorVisible = true
|
||
|
passwordText.hasFocus()
|
||
|
inputLayoutPassword.hasFocus()
|
||
4 years ago
|
it.isEnabled = false
|
||
4 years ago
|
}
|
||
|
revealPasswordButton.setOnClickListener {
|
||
4 years ago
|
showPassword = !showPassword
|
||
|
if (showPassword) {
|
||
|
editLoginView.showPassword()
|
||
|
} else {
|
||
|
editLoginView.hidePassword()
|
||
4 years ago
|
}
|
||
|
}
|
||
|
}
|
||
|
|
||
|
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?) {
|
||
4 years ago
|
when {
|
||
|
u.toString() == oldLogin.username -> {
|
||
|
usernameChanged = false
|
||
|
validUsername = true
|
||
|
inputLayoutUsername.error = null
|
||
|
inputLayoutUsername.errorIconDrawable = null
|
||
|
}
|
||
|
else -> {
|
||
|
usernameChanged = true
|
||
|
clearUsernameTextButton.isEnabled = true
|
||
|
setDupeError()
|
||
|
}
|
||
4 years ago
|
}
|
||
4 years ago
|
setSaveButtonState()
|
||
4 years ago
|
}
|
||
|
|
||
|
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() -> {
|
||
4 years ago
|
passwordChanged = true
|
||
4 years ago
|
clearPasswordTextButton.isEnabled = false
|
||
|
setPasswordError()
|
||
|
}
|
||
|
p.toString() == oldLogin.password -> {
|
||
4 years ago
|
passwordChanged = false
|
||
|
validPassword = true
|
||
4 years ago
|
inputLayoutPassword.error = null
|
||
|
inputLayoutPassword.errorIconDrawable = null
|
||
|
clearPasswordTextButton.isEnabled = true
|
||
|
}
|
||
|
else -> {
|
||
4 years ago
|
passwordChanged = true
|
||
|
validPassword = true
|
||
4 years ago
|
inputLayoutPassword.error = null
|
||
|
inputLayoutPassword.errorIconDrawable = null
|
||
|
clearPasswordTextButton.isEnabled = true
|
||
|
}
|
||
|
}
|
||
4 years ago
|
setSaveButtonState()
|
||
4 years ago
|
}
|
||
|
|
||
|
override fun beforeTextChanged(p: CharSequence?, start: Int, count: Int, after: Int) {
|
||
|
// NOOP
|
||
|
}
|
||
|
|
||
|
override fun onTextChanged(p: CharSequence?, start: Int, before: Int, count: Int) {
|
||
|
// NOOP
|
||
|
}
|
||
|
})
|
||
|
}
|
||
|
|
||
4 years ago
|
private fun isDupe(username: String): Boolean =
|
||
|
loginsFragmentStore.state.duplicateLogins.filter { it.username == username }.any()
|
||
|
|
||
|
private fun setDupeError() {
|
||
|
if (isDupe(usernameText.text.toString())) {
|
||
|
inputLayoutUsername?.let {
|
||
|
usernameChanged = true
|
||
|
validUsername = false
|
||
|
it.setErrorIconDrawable(R.drawable.mozac_ic_warning)
|
||
|
it.error = context?.getString(R.string.saved_login_duplicate)
|
||
|
}
|
||
|
} else {
|
||
|
usernameChanged = true
|
||
|
validUsername = true
|
||
|
inputLayoutUsername.error = null
|
||
|
}
|
||
|
}
|
||
|
|
||
4 years ago
|
private fun setPasswordError() {
|
||
|
inputLayoutPassword?.let { layout ->
|
||
4 years ago
|
validPassword = false
|
||
4 years ago
|
layout.error = context?.getString(R.string.saved_login_password_required)
|
||
|
layout.setErrorIconDrawable(R.drawable.mozac_ic_warning)
|
||
4 years ago
|
}
|
||
|
}
|
||
4 years ago
|
|
||
4 years ago
|
private fun setSaveButtonState() {
|
||
|
val saveButton = activity?.findViewById<ActionMenuItemView>(R.id.save_login_button)
|
||
|
val changesMadeWithNoErrors =
|
||
|
validUsername && validPassword && (usernameChanged || passwordChanged)
|
||
|
|
||
|
changesMadeWithNoErrors.let {
|
||
|
saveButton?.isEnabled = it
|
||
|
saveEnabled = it
|
||
4 years ago
|
}
|
||
|
}
|
||
|
|
||
|
override fun onCreateOptionsMenu(menu: Menu, inflater: MenuInflater) {
|
||
|
inflater.inflate(R.menu.login_save, menu)
|
||
|
}
|
||
|
|
||
4 years ago
|
override fun onPause() {
|
||
|
redirectToReAuth(
|
||
|
listOf(R.id.loginDetailFragment),
|
||
|
findNavController().currentDestination?.id
|
||
|
)
|
||
|
super.onPause()
|
||
|
}
|
||
|
|
||
4 years ago
|
override fun onOptionsItemSelected(item: MenuItem): Boolean = when (item.itemId) {
|
||
|
R.id.save_login_button -> {
|
||
|
view?.hideKeyboard()
|
||
4 years ago
|
if (saveEnabled) {
|
||
|
interactor.onSaveLogin(
|
||
|
args.savedLoginItem.guid,
|
||
|
usernameText.text.toString(),
|
||
|
passwordText.text.toString()
|
||
|
)
|
||
|
requireComponents.analytics.metrics.track(Event.EditLoginSave)
|
||
4 years ago
|
}
|
||
|
true
|
||
|
}
|
||
|
else -> false
|
||
|
}
|
||
|
}
|