[fenix] For https://github.com/mozilla-mobile/fenix/issues/19947: manually add login (https://github.com/mozilla-mobile/fenix/pull/21199)
* [WIP] New Layout for adding login and 'add login' button in 'SavedLoginsListView' to launch it. Fixed bindings. * [WIP] Removed "reveal password" button * [WIP] Added interactor for the add login screen * [WIP] Trying to check for duplicates * [WIP] Renaming "addNew..." with "add..." * [WIP] Check for duplicates * [WIP] Fixes after merge * Cleaning up the layout and making edit text for hostname selectable * Error handling on add login screen. Tests for interactors and controllers Co-authored-by: Vitaly V. Pinchuk <vetal.978@gmail.com> Co-authored-by: mergify[bot] <37929162+mergify[bot]@users.noreply.github.com>pull/600/head
parent
0ea0ce4532
commit
d0757d386a
@ -0,0 +1,354 @@
|
||||
/* 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/. */
|
||||
|
||||
package org.mozilla.fenix.settings.logins.fragment
|
||||
|
||||
import android.content.Context
|
||||
import android.content.res.ColorStateList
|
||||
import android.os.Bundle
|
||||
import android.text.Editable
|
||||
import android.text.InputType
|
||||
import android.text.TextWatcher
|
||||
import android.view.Menu
|
||||
import android.view.MenuInflater
|
||||
import android.view.MenuItem
|
||||
import android.view.View
|
||||
import android.view.inputmethod.InputMethodManager
|
||||
import android.webkit.URLUtil
|
||||
import androidx.core.content.ContextCompat
|
||||
import androidx.core.view.isVisible
|
||||
import androidx.fragment.app.Fragment
|
||||
import androidx.lifecycle.lifecycleScope
|
||||
import androidx.navigation.fragment.findNavController
|
||||
import kotlinx.coroutines.ExperimentalCoroutinesApi
|
||||
import mozilla.components.lib.state.ext.consumeFrom
|
||||
import mozilla.components.support.ktx.android.view.hideKeyboard
|
||||
import org.mozilla.fenix.R
|
||||
import org.mozilla.fenix.components.StoreProvider
|
||||
import org.mozilla.fenix.databinding.FragmentAddLoginBinding
|
||||
import org.mozilla.fenix.ext.components
|
||||
import org.mozilla.fenix.ext.redirectToReAuth
|
||||
import org.mozilla.fenix.ext.showToolbar
|
||||
import org.mozilla.fenix.ext.toEditable
|
||||
import org.mozilla.fenix.ext.settings
|
||||
import org.mozilla.fenix.settings.logins.controller.SavedLoginsStorageController
|
||||
import org.mozilla.fenix.settings.logins.interactor.AddLoginInteractor
|
||||
import org.mozilla.fenix.settings.logins.LoginsFragmentStore
|
||||
import org.mozilla.fenix.settings.logins.SavedLogin
|
||||
import org.mozilla.fenix.settings.logins.createInitialLoginsListState
|
||||
|
||||
/**
|
||||
* Displays the editable new login information for a single website
|
||||
*/
|
||||
@ExperimentalCoroutinesApi
|
||||
@Suppress("TooManyFunctions", "NestedBlockDepth", "ForbiddenComment")
|
||||
class AddLoginFragment : Fragment(R.layout.fragment_add_login) {
|
||||
|
||||
private lateinit var loginsFragmentStore: LoginsFragmentStore
|
||||
private lateinit var interactor: AddLoginInteractor
|
||||
|
||||
private var listOfPossibleDupes: List<SavedLogin>? = null
|
||||
|
||||
private var validPassword = true
|
||||
private var validUsername = true
|
||||
private var validHostname = false
|
||||
|
||||
private var _binding: FragmentAddLoginBinding? = null
|
||||
private val binding get() = _binding!!
|
||||
|
||||
override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
|
||||
super.onViewCreated(view, savedInstanceState)
|
||||
setHasOptionsMenu(true)
|
||||
|
||||
_binding = FragmentAddLoginBinding.bind(view)
|
||||
|
||||
loginsFragmentStore = StoreProvider.get(this) {
|
||||
LoginsFragmentStore(
|
||||
createInitialLoginsListState(requireContext().settings())
|
||||
)
|
||||
}
|
||||
|
||||
interactor = AddLoginInteractor(
|
||||
SavedLoginsStorageController(
|
||||
passwordsStorage = requireContext().components.core.passwordsStorage,
|
||||
lifecycleScope = lifecycleScope,
|
||||
navController = findNavController(),
|
||||
loginsFragmentStore = loginsFragmentStore
|
||||
)
|
||||
)
|
||||
|
||||
initEditableValues()
|
||||
|
||||
setUpClickListeners()
|
||||
setUpTextListeners()
|
||||
|
||||
consumeFrom(loginsFragmentStore) {
|
||||
listOfPossibleDupes = loginsFragmentStore.state.duplicateLogins
|
||||
}
|
||||
}
|
||||
|
||||
private fun initEditableValues() {
|
||||
binding.hostnameText.text = "".toEditable()
|
||||
binding.usernameText.text = "".toEditable()
|
||||
binding.passwordText.text = "".toEditable()
|
||||
|
||||
binding.hostnameText.inputType = InputType.TYPE_TEXT_FLAG_NO_SUGGESTIONS
|
||||
binding.usernameText.inputType = InputType.TYPE_TEXT_FLAG_NO_SUGGESTIONS
|
||||
|
||||
// TODO: extend PasswordTransformationMethod() to change bullets to asterisks
|
||||
binding.passwordText.inputType =
|
||||
InputType.TYPE_CLASS_TEXT or InputType.TYPE_TEXT_VARIATION_PASSWORD
|
||||
|
||||
binding.passwordText.compoundDrawablePadding =
|
||||
requireContext().resources
|
||||
.getDimensionPixelOffset(R.dimen.saved_logins_end_icon_drawable_padding)
|
||||
}
|
||||
|
||||
private fun setUpClickListeners() {
|
||||
binding.hostnameText.requestFocus()
|
||||
val imm =
|
||||
requireContext().getSystemService(Context.INPUT_METHOD_SERVICE) as InputMethodManager
|
||||
imm.toggleSoftInput(InputMethodManager.SHOW_IMPLICIT, 0)
|
||||
|
||||
binding.clearHostnameTextButton.setOnClickListener {
|
||||
binding.hostnameText.text?.clear()
|
||||
binding.hostnameText.isCursorVisible = true
|
||||
binding.hostnameText.hasFocus()
|
||||
binding.inputLayoutHostname.hasFocus()
|
||||
it.isEnabled = false
|
||||
}
|
||||
|
||||
binding.clearUsernameTextButton.setOnClickListener {
|
||||
binding.usernameText.text?.clear()
|
||||
binding.usernameText.isCursorVisible = true
|
||||
binding.usernameText.hasFocus()
|
||||
binding.inputLayoutUsername.hasFocus()
|
||||
it.isEnabled = false
|
||||
}
|
||||
|
||||
binding.clearPasswordTextButton.setOnClickListener {
|
||||
binding.passwordText.text?.clear()
|
||||
binding.passwordText.isCursorVisible = true
|
||||
binding.passwordText.hasFocus()
|
||||
binding.inputLayoutPassword.hasFocus()
|
||||
it.isEnabled = false
|
||||
}
|
||||
}
|
||||
|
||||
private fun setUpTextListeners() {
|
||||
val frag = view?.findViewById<View>(R.id.addLoginFragment)
|
||||
|
||||
frag?.onFocusChangeListener = View.OnFocusChangeListener { _, hasFocus ->
|
||||
if (hasFocus) {
|
||||
view?.hideKeyboard()
|
||||
}
|
||||
}
|
||||
|
||||
binding.addLoginLayout.onFocusChangeListener = View.OnFocusChangeListener { _, hasFocus ->
|
||||
if (!hasFocus) {
|
||||
view?.hideKeyboard()
|
||||
}
|
||||
}
|
||||
|
||||
binding.hostnameText.addTextChangedListener(object : TextWatcher {
|
||||
override fun afterTextChanged(h: Editable?) {
|
||||
val hostnameText = h.toString()
|
||||
|
||||
when {
|
||||
hostnameText.isEmpty() -> {
|
||||
setHostnameError()
|
||||
binding.clearHostnameTextButton.isEnabled = false
|
||||
}
|
||||
!URLUtil.isHttpUrl(hostnameText) && !URLUtil.isHttpsUrl(hostnameText) -> {
|
||||
setHostnameError()
|
||||
binding.clearHostnameTextButton.isEnabled = true
|
||||
}
|
||||
else -> {
|
||||
validHostname = true
|
||||
|
||||
binding.clearHostnameTextButton.isEnabled = true
|
||||
binding.inputLayoutHostname.error = null
|
||||
binding.inputLayoutHostname.errorIconDrawable = null
|
||||
|
||||
interactor.findPotentialDuplicates(
|
||||
hostnameText = h.toString(),
|
||||
binding.usernameText.text.toString(),
|
||||
binding.passwordText.text.toString()
|
||||
)
|
||||
}
|
||||
}
|
||||
setSaveButtonState()
|
||||
}
|
||||
|
||||
override fun beforeTextChanged(u: CharSequence?, start: Int, count: Int, after: Int) {
|
||||
// NOOP
|
||||
}
|
||||
|
||||
override fun onTextChanged(u: CharSequence?, start: Int, before: Int, count: Int) {
|
||||
// NOOP
|
||||
}
|
||||
})
|
||||
|
||||
binding.usernameText.addTextChangedListener(object : TextWatcher {
|
||||
override fun afterTextChanged(u: Editable?) {
|
||||
when {
|
||||
u.toString().isEmpty() -> {
|
||||
binding.clearUsernameTextButton.isVisible = false
|
||||
setUsernameError()
|
||||
}
|
||||
else -> {
|
||||
setDupeError()
|
||||
binding.inputLayoutUsername.error = null
|
||||
binding.inputLayoutUsername.errorIconDrawable = null
|
||||
}
|
||||
}
|
||||
binding.clearUsernameTextButton.isEnabled = u.toString().isNotEmpty()
|
||||
setSaveButtonState()
|
||||
}
|
||||
|
||||
override fun beforeTextChanged(u: CharSequence?, start: Int, count: Int, after: Int) {
|
||||
// NOOP
|
||||
}
|
||||
|
||||
override fun onTextChanged(u: CharSequence?, start: Int, before: Int, count: Int) {
|
||||
// NOOP
|
||||
}
|
||||
})
|
||||
|
||||
binding.passwordText.addTextChangedListener(object : TextWatcher {
|
||||
override fun afterTextChanged(p: Editable?) {
|
||||
when {
|
||||
p.toString().isEmpty() -> {
|
||||
binding.clearPasswordTextButton.isVisible = false
|
||||
setPasswordError()
|
||||
}
|
||||
else -> {
|
||||
validPassword = true
|
||||
binding.inputLayoutPassword.error = null
|
||||
binding.inputLayoutPassword.errorIconDrawable = null
|
||||
binding.clearPasswordTextButton.isVisible = true
|
||||
}
|
||||
}
|
||||
setSaveButtonState()
|
||||
}
|
||||
|
||||
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 isDupe(username: String): Boolean =
|
||||
loginsFragmentStore.state.duplicateLogins.filter { it.username == username }.any()
|
||||
|
||||
private fun setDupeError() {
|
||||
if (isDupe(binding.usernameText.text.toString())) {
|
||||
binding.inputLayoutUsername.let {
|
||||
validUsername = false
|
||||
it.error = context?.getString(R.string.saved_login_duplicate)
|
||||
it.setErrorIconDrawable(R.drawable.mozac_ic_warning_with_bottom_padding)
|
||||
it.setErrorIconTintList(
|
||||
ColorStateList.valueOf(
|
||||
ContextCompat.getColor(requireContext(), R.color.design_error)
|
||||
)
|
||||
)
|
||||
binding.clearUsernameTextButton.isVisible = false
|
||||
}
|
||||
} else {
|
||||
validUsername = true
|
||||
binding.inputLayoutUsername.error = null
|
||||
binding.inputLayoutUsername.errorIconDrawable = null
|
||||
binding.clearUsernameTextButton.isVisible = true
|
||||
}
|
||||
}
|
||||
|
||||
private fun setPasswordError() {
|
||||
binding.inputLayoutPassword.let { layout ->
|
||||
validPassword = false
|
||||
layout.error = context?.getString(R.string.saved_login_password_required)
|
||||
layout.setErrorIconDrawable(R.drawable.mozac_ic_warning_with_bottom_padding)
|
||||
layout.setErrorIconTintList(
|
||||
ColorStateList.valueOf(
|
||||
ContextCompat.getColor(requireContext(), R.color.design_error)
|
||||
)
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
private fun setUsernameError() {
|
||||
binding.inputLayoutUsername.let { layout ->
|
||||
validUsername = false
|
||||
layout.error = context?.getString(R.string.saved_login_username_required)
|
||||
layout.setErrorIconDrawable(R.drawable.mozac_ic_warning_with_bottom_padding)
|
||||
layout.setErrorIconTintList(
|
||||
ColorStateList.valueOf(
|
||||
ContextCompat.getColor(requireContext(), R.color.design_error)
|
||||
)
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
private fun setHostnameError() {
|
||||
binding.inputLayoutHostname.let { layout ->
|
||||
validHostname = false
|
||||
layout.error = context?.getString(R.string.add_login_hostname_invalid_text_2)
|
||||
layout.setErrorIconDrawable(R.drawable.mozac_ic_warning_with_bottom_padding)
|
||||
layout.setErrorIconTintList(
|
||||
ColorStateList.valueOf(
|
||||
ContextCompat.getColor(requireContext(), R.color.design_error)
|
||||
)
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
private fun setSaveButtonState() {
|
||||
activity?.invalidateOptionsMenu()
|
||||
}
|
||||
|
||||
override fun onCreateOptionsMenu(menu: Menu, inflater: MenuInflater) {
|
||||
inflater.inflate(R.menu.login_save, menu)
|
||||
}
|
||||
|
||||
override fun onPrepareOptionsMenu(menu: Menu) {
|
||||
val saveButton = menu.findItem(R.id.save_login_button)
|
||||
val changesMadeWithNoErrors = validHostname && validUsername && validPassword
|
||||
saveButton.isEnabled = changesMadeWithNoErrors
|
||||
}
|
||||
|
||||
override fun onPause() {
|
||||
redirectToReAuth(
|
||||
listOf(R.id.loginDetailFragment, R.id.savedLoginsFragment),
|
||||
findNavController().currentDestination?.id,
|
||||
R.id.editLoginFragment
|
||||
)
|
||||
super.onPause()
|
||||
}
|
||||
|
||||
override fun onResume() {
|
||||
super.onResume()
|
||||
showToolbar(getString(R.string.add_login))
|
||||
}
|
||||
|
||||
override fun onOptionsItemSelected(item: MenuItem): Boolean = when (item.itemId) {
|
||||
R.id.save_login_button -> {
|
||||
view?.hideKeyboard()
|
||||
interactor.onAddLogin(
|
||||
binding.hostnameText.text.toString(),
|
||||
binding.usernameText.text.toString(),
|
||||
binding.passwordText.text.toString()
|
||||
)
|
||||
true
|
||||
}
|
||||
else -> false
|
||||
}
|
||||
|
||||
override fun onDestroyView() {
|
||||
super.onDestroyView()
|
||||
_binding = null
|
||||
}
|
||||
}
|
@ -0,0 +1,24 @@
|
||||
/* 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/. */
|
||||
|
||||
package org.mozilla.fenix.settings.logins.interactor
|
||||
|
||||
import org.mozilla.fenix.settings.logins.controller.SavedLoginsStorageController
|
||||
|
||||
/**
|
||||
* Interactor for the add login screen
|
||||
*
|
||||
* @property savedLoginsController controller for the saved logins storage
|
||||
*/
|
||||
class AddLoginInteractor(
|
||||
private val savedLoginsController: SavedLoginsStorageController
|
||||
) {
|
||||
fun findPotentialDuplicates(hostnameText: String, usernameText: String, passwordText: String) {
|
||||
savedLoginsController.findPotentialDuplicates(hostnameText, usernameText, passwordText)
|
||||
}
|
||||
|
||||
fun onAddLogin(hostnameText: String, usernameText: String, passwordText: String) {
|
||||
savedLoginsController.add(hostnameText, usernameText, passwordText)
|
||||
}
|
||||
}
|
@ -0,0 +1,232 @@
|
||||
<?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/. -->
|
||||
|
||||
<androidx.constraintlayout.widget.ConstraintLayout
|
||||
xmlns:android="http://schemas.android.com/apk/res/android"
|
||||
xmlns:app="http://schemas.android.com/apk/res-auto"
|
||||
xmlns:tools="http://schemas.android.com/tools"
|
||||
android:id="@+id/addLoginLayout"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="wrap_content"
|
||||
android:layout_marginStart="20dp"
|
||||
android:layout_marginEnd="20dp"
|
||||
android:layout_marginTop="16dp"
|
||||
android:clickable="true"
|
||||
android:focusable="true" >
|
||||
|
||||
<TextView
|
||||
android:id="@+id/hostnameHeaderText"
|
||||
android:layout_width="0dp"
|
||||
android:layout_height="wrap_content"
|
||||
android:paddingStart="3dp"
|
||||
android:paddingEnd="0dp"
|
||||
android:gravity="center_vertical"
|
||||
android:text="@string/preferences_passwords_saved_logins_site"
|
||||
android:textColor="?primaryText"
|
||||
android:textSize="12sp"
|
||||
android:letterSpacing="0.05"
|
||||
app:fontFamily="@font/metropolis_semibold"
|
||||
app:layout_constraintEnd_toEndOf="parent"
|
||||
app:layout_constraintStart_toStartOf="parent"
|
||||
app:layout_constraintTop_toTopOf="parent"
|
||||
app:layout_constraintVertical_chainStyle="packed" />
|
||||
|
||||
<com.google.android.material.textfield.TextInputLayout
|
||||
android:id="@+id/inputLayoutHostname"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="wrap_content"
|
||||
android:colorControlHighlight="?primaryText"
|
||||
android:colorControlActivated="?primaryText"
|
||||
android:textColor="?primaryText"
|
||||
app:layout_constraintEnd_toEndOf="@id/hostnameHeaderText"
|
||||
app:layout_constraintStart_toStartOf="@id/hostnameHeaderText"
|
||||
app:layout_constraintTop_toBottomOf="@id/hostnameHeaderText"
|
||||
app:helperTextEnabled="true"
|
||||
app:helperText="@string/add_login_hostname_invalid_text_1"
|
||||
app:hintEnabled="false" >
|
||||
|
||||
<com.google.android.material.textfield.TextInputEditText
|
||||
android:id="@+id/hostnameText"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="wrap_content"
|
||||
android:textSize="16sp"
|
||||
android:fontFamily="sans-serif"
|
||||
android:textStyle="normal"
|
||||
android:colorControlHighlight="?primaryText"
|
||||
android:colorControlActivated="?primaryText"
|
||||
android:textColor="?primaryText"
|
||||
android:letterSpacing="0.01"
|
||||
android:lineSpacingExtra="8sp"
|
||||
android:hint="@string/add_login_hostname_hint_text"
|
||||
android:inputType="textNoSuggestions"
|
||||
android:ellipsize="end"
|
||||
android:maxLines="1"
|
||||
android:singleLine="true"
|
||||
android:clickable="true"
|
||||
android:focusable="true"
|
||||
android:cursorVisible="true"
|
||||
android:textCursorDrawable="@null"
|
||||
app:backgroundTint="?primaryText"
|
||||
tools:ignore="Autofill"/>
|
||||
</com.google.android.material.textfield.TextInputLayout>
|
||||
|
||||
<ImageButton
|
||||
android:id="@+id/clearHostnameTextButton"
|
||||
android:layout_width="48dp"
|
||||
android:layout_height="30dp"
|
||||
android:layout_marginTop="3dp"
|
||||
android:layout_marginBottom="10dp"
|
||||
android:background="@null"
|
||||
android:contentDescription="@string/saved_login_clear_hostname"
|
||||
android:visibility="invisible"
|
||||
app:tint="@color/saved_login_clear_edit_text_tint"
|
||||
app:layout_constraintEnd_toEndOf="parent"
|
||||
app:layout_constraintTop_toTopOf="@id/inputLayoutHostname"
|
||||
app:srcCompat="@drawable/mozac_ic_clear" />
|
||||
|
||||
<TextView
|
||||
android:id="@+id/usernameHeader"
|
||||
android:layout_width="0dp"
|
||||
android:layout_height="16dp"
|
||||
android:gravity="center_vertical"
|
||||
android:paddingStart="3dp"
|
||||
android:paddingEnd="0dp"
|
||||
android:layout_marginTop="20dp"
|
||||
android:text="@string/preferences_passwords_saved_logins_username"
|
||||
android:textColor="?primaryText"
|
||||
android:textSize="12sp"
|
||||
android:letterSpacing="0.05"
|
||||
app:fontFamily="@font/metropolis_semibold"
|
||||
app:layout_constraintBottom_toTopOf="@id/inputLayoutUsername"
|
||||
app:layout_constraintEnd_toStartOf="@id/clearUsernameTextButton"
|
||||
app:layout_constraintStart_toStartOf="parent"
|
||||
app:layout_constraintTop_toBottomOf="@id/inputLayoutHostname"
|
||||
app:layout_constraintVertical_chainStyle="packed" />
|
||||
|
||||
<com.google.android.material.textfield.TextInputLayout
|
||||
android:id="@+id/inputLayoutUsername"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="wrap_content"
|
||||
android:colorControlHighlight="?primaryText"
|
||||
android:colorControlActivated="?primaryText"
|
||||
android:textColor="?primaryText"
|
||||
android:contentDescription="@string/saved_login_username_description"
|
||||
app:layout_constraintEnd_toEndOf="@id/usernameHeader"
|
||||
app:layout_constraintStart_toStartOf="@id/usernameHeader"
|
||||
app:layout_constraintTop_toBottomOf="@id/usernameHeader"
|
||||
app:layout_constraintVertical_chainStyle="packed"
|
||||
app:hintEnabled="false">
|
||||
|
||||
<com.google.android.material.textfield.TextInputEditText
|
||||
android:id="@+id/usernameText"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="wrap_content"
|
||||
android:textSize="16sp"
|
||||
android:fontFamily="sans-serif"
|
||||
android:textStyle="normal"
|
||||
android:colorControlHighlight="?primaryText"
|
||||
android:colorControlActivated="?primaryText"
|
||||
android:textColor="?primaryText"
|
||||
android:letterSpacing="0.01"
|
||||
android:lineSpacingExtra="8sp"
|
||||
android:inputType="textNoSuggestions"
|
||||
android:ellipsize="end"
|
||||
android:maxLines="1"
|
||||
android:singleLine="true"
|
||||
android:clickable="true"
|
||||
android:focusable="true"
|
||||
android:cursorVisible="true"
|
||||
android:textCursorDrawable="@null"
|
||||
app:backgroundTint="?primaryText"
|
||||
tools:ignore="Autofill"/>
|
||||
</com.google.android.material.textfield.TextInputLayout>
|
||||
|
||||
<ImageButton
|
||||
android:id="@+id/clearUsernameTextButton"
|
||||
android:layout_width="48dp"
|
||||
android:layout_height="30dp"
|
||||
android:layout_marginTop="3dp"
|
||||
android:layout_marginBottom="10dp"
|
||||
android:background="@null"
|
||||
android:contentDescription="@string/saved_login_clear_username"
|
||||
android:visibility="invisible"
|
||||
app:tint="@color/saved_login_clear_edit_text_tint"
|
||||
app:layout_constraintEnd_toEndOf="parent"
|
||||
app:layout_constraintTop_toTopOf="@id/inputLayoutUsername"
|
||||
app:srcCompat="@drawable/mozac_ic_clear" />
|
||||
|
||||
<TextView
|
||||
android:id="@+id/passwordHeader"
|
||||
android:layout_width="0dp"
|
||||
android:layout_height="wrap_content"
|
||||
android:layout_marginTop="10dp"
|
||||
android:gravity="center_vertical"
|
||||
android:letterSpacing="0.05"
|
||||
android:paddingStart="3dp"
|
||||
android:paddingEnd="0dp"
|
||||
android:text="@string/preferences_passwords_saved_logins_password"
|
||||
android:textColor="?primaryText"
|
||||
android:textSize="12sp"
|
||||
app:fontFamily="@font/metropolis_semibold"
|
||||
app:layout_constraintBottom_toTopOf="@id/inputLayoutPassword"
|
||||
app:layout_constraintEnd_toStartOf="@+id/clearPasswordTextButton"
|
||||
app:layout_constraintStart_toStartOf="parent"
|
||||
app:layout_constraintTop_toBottomOf="@id/inputLayoutUsername"
|
||||
app:layout_constraintVertical_chainStyle="packed" />
|
||||
|
||||
<com.google.android.material.textfield.TextInputLayout
|
||||
android:id="@+id/inputLayoutPassword"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="wrap_content"
|
||||
android:colorControlActivated="?primaryText"
|
||||
android:colorControlHighlight="?primaryText"
|
||||
android:contentDescription="@string/saved_login_password_description"
|
||||
android:paddingBottom="11dp"
|
||||
android:textColor="?primaryText"
|
||||
app:hintEnabled="false"
|
||||
app:layout_constraintEnd_toEndOf="@id/passwordHeader"
|
||||
app:layout_constraintStart_toStartOf="@id/passwordHeader"
|
||||
app:layout_constraintTop_toBottomOf="@id/passwordHeader"
|
||||
app:layout_constraintVertical_chainStyle="packed">
|
||||
|
||||
<com.google.android.material.textfield.TextInputEditText
|
||||
android:id="@+id/passwordText"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="wrap_content"
|
||||
android:clickable="true"
|
||||
android:colorControlActivated="?primaryText"
|
||||
android:colorControlHighlight="?primaryText"
|
||||
android:cursorVisible="true"
|
||||
android:ellipsize="end"
|
||||
android:focusable="true"
|
||||
android:fontFamily="sans-serif"
|
||||
android:inputType="textNoSuggestions"
|
||||
android:letterSpacing="0.01"
|
||||
android:lineSpacingExtra="8sp"
|
||||
android:maxLines="1"
|
||||
android:singleLine="true"
|
||||
android:textColor="?primaryText"
|
||||
android:textCursorDrawable="@null"
|
||||
android:textSize="16sp"
|
||||
android:textStyle="normal"
|
||||
app:backgroundTint="?primaryText"
|
||||
tools:ignore="Autofill" />
|
||||
</com.google.android.material.textfield.TextInputLayout>
|
||||
|
||||
<ImageButton
|
||||
android:id="@+id/clearPasswordTextButton"
|
||||
android:layout_width="48dp"
|
||||
android:layout_height="30dp"
|
||||
android:layout_marginTop="3dp"
|
||||
android:layout_marginBottom="10dp"
|
||||
android:background="@null"
|
||||
android:contentDescription="@string/saved_logins_clear_password"
|
||||
android:visibility="invisible"
|
||||
app:layout_constraintEnd_toEndOf="parent"
|
||||
app:layout_constraintTop_toTopOf="@id/inputLayoutPassword"
|
||||
app:srcCompat="@drawable/mozac_ic_clear"
|
||||
app:tint="@color/saved_login_clear_edit_text_tint" />
|
||||
|
||||
</androidx.constraintlayout.widget.ConstraintLayout>
|
@ -0,0 +1,39 @@
|
||||
<?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/. -->
|
||||
<androidx.constraintlayout.widget.ConstraintLayout
|
||||
android:id="@+id/add_login_layout"
|
||||
xmlns:android="http://schemas.android.com/apk/res/android"
|
||||
xmlns:app="http://schemas.android.com/apk/res-auto"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="wrap_content"
|
||||
android:background="?attr/selectableItemBackground"
|
||||
android:clickable="true"
|
||||
android:focusable="true">
|
||||
|
||||
<androidx.appcompat.widget.AppCompatImageView
|
||||
android:id="@+id/add_login_icon"
|
||||
android:layout_width="wrap_content"
|
||||
android:layout_height="wrap_content"
|
||||
android:layout_marginStart="@dimen/credit_cards_saved_cards_item_margin_start"
|
||||
app:srcCompat="@drawable/ic_new"
|
||||
android:importantForAccessibility="no"
|
||||
app:layout_constraintBottom_toBottomOf="@id/add_login_text"
|
||||
app:layout_constraintStart_toStartOf="parent"
|
||||
app:layout_constraintTop_toTopOf="@id/add_login_text" />
|
||||
|
||||
<com.google.android.material.textview.MaterialTextView
|
||||
android:id="@+id/add_login_text"
|
||||
android:layout_width="0dp"
|
||||
android:layout_height="wrap_content"
|
||||
android:layout_margin="@dimen/credit_cards_saved_cards_item_margin_start"
|
||||
android:text="@string/preferences_logins_add_login"
|
||||
style="@style/Body16TextStyle"
|
||||
app:layout_constraintBottom_toBottomOf="parent"
|
||||
app:layout_constraintEnd_toEndOf="parent"
|
||||
app:layout_constraintStart_toEndOf="@id/add_login_icon"
|
||||
app:layout_constraintTop_toTopOf="parent"
|
||||
android:textAlignment="viewStart" />
|
||||
|
||||
</androidx.constraintlayout.widget.ConstraintLayout>
|
@ -0,0 +1,51 @@
|
||||
/* 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/. */
|
||||
|
||||
package org.mozilla.fenix.settings.logins
|
||||
|
||||
import io.mockk.mockk
|
||||
import io.mockk.verify
|
||||
import org.junit.Test
|
||||
import org.mozilla.fenix.settings.logins.controller.SavedLoginsStorageController
|
||||
import org.mozilla.fenix.settings.logins.interactor.AddLoginInteractor
|
||||
|
||||
class AddLoginInteractorTest {
|
||||
|
||||
private val loginsController: SavedLoginsStorageController = mockk(relaxed = true)
|
||||
private val interactor = AddLoginInteractor(loginsController)
|
||||
|
||||
private val hostname = "https://www.cats.com"
|
||||
private val username = "myFunUsername111"
|
||||
private val password = "superDuperSecure123!"
|
||||
|
||||
@Test
|
||||
fun findPotentialDupesTest() {
|
||||
interactor.findPotentialDuplicates(
|
||||
hostname,
|
||||
username,
|
||||
password
|
||||
)
|
||||
|
||||
verify {
|
||||
loginsController.findPotentialDuplicates(
|
||||
hostname,
|
||||
username,
|
||||
password
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
@Test
|
||||
fun addNewLoginTest() {
|
||||
interactor.onAddLogin(hostname, username, password)
|
||||
|
||||
verify {
|
||||
loginsController.add(
|
||||
hostname,
|
||||
username,
|
||||
password
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
Loading…
Reference in New Issue