* Create editable view and fragment. Update login info page to display options menu with edit and delete. * Create feature flag for edit. Check flag in the login detail fragment and default to just delete. * Add three-dot kebab options menu in login detail fragment. Add title to the login item. * Nav to and from edit view on save and back pressed. * Save login through AC login manager. Clear text in editable field on button click. * Match colors, fonts, dimens to UX specs for edit logins. Enable password reveal/hide and clearing text fields. * Refactoring logins fragments. Using component Login object for consistency. Fetch login list when saved logins are opened. Fetch login details when detail view is opened. Revert "Fetch login list when saved logins are opened. Fetch login details when detail view is opened." This reverts commit 44fe17166c3332b330229258b2e8982832672e3b. * Using parcelable login and Login component class to pass ids and items between fragments * Retrieve login from storage when viewing login details. Rename login logic for consistency. Ktlint cleanup Fix nits and naming consistency. * UX consistency for login detail and edit login pages * Rebasing with logins sort - updating logins store. * Rebasing with logins sort - merging fragments and controllers. * Lint and removing unused files. * UX cleanup. * Update string descriptionmaster
parent
f7b4f1c959
commit
edc75c3ad0
|
@ -39,6 +39,11 @@ object FeatureFlags {
|
|||
*/
|
||||
val tips = Config.channel.isDebug
|
||||
|
||||
/**
|
||||
* Allows edit of saved logins.
|
||||
*/
|
||||
val loginsEdit = Config.channel.isNightlyOrDebug
|
||||
|
||||
/**
|
||||
* Enables new tab tray pref
|
||||
*/
|
||||
|
|
|
@ -69,10 +69,10 @@ import org.mozilla.fenix.perf.Performance
|
|||
import org.mozilla.fenix.perf.StartupTimeline
|
||||
import org.mozilla.fenix.search.SearchFragmentDirections
|
||||
import org.mozilla.fenix.settings.DefaultBrowserSettingsFragmentDirections
|
||||
import org.mozilla.fenix.settings.logins.SavedLoginsAuthFragmentDirections
|
||||
import org.mozilla.fenix.settings.SettingsFragmentDirections
|
||||
import org.mozilla.fenix.settings.TrackingProtectionFragmentDirections
|
||||
import org.mozilla.fenix.settings.about.AboutFragmentDirections
|
||||
import org.mozilla.fenix.settings.logins.SavedLoginsFragmentDirections
|
||||
import org.mozilla.fenix.theme.DefaultThemeManager
|
||||
import org.mozilla.fenix.theme.ThemeManager
|
||||
import org.mozilla.fenix.utils.BrowsersCache
|
||||
|
@ -387,7 +387,7 @@ open class HomeActivity : LocaleAwareAppCompatActivity() {
|
|||
BrowserDirection.FromDefaultBrowserSettingsFragment ->
|
||||
DefaultBrowserSettingsFragmentDirections.actionGlobalBrowser(customTabSessionId)
|
||||
BrowserDirection.FromSavedLoginsFragment ->
|
||||
SavedLoginsFragmentDirections.actionGlobalBrowser(customTabSessionId)
|
||||
SavedLoginsAuthFragmentDirections.actionGlobalBrowser(customTabSessionId)
|
||||
}
|
||||
|
||||
private fun load(
|
||||
|
|
|
@ -22,7 +22,7 @@ class ExceptionsListItemViewHolder(
|
|||
) : RecyclerView.ViewHolder(view) {
|
||||
|
||||
private val favicon = view.favicon_image
|
||||
private val url = view.domainView
|
||||
private val url = view.webAddressView
|
||||
private val deleteButton = view.delete_exception
|
||||
|
||||
private var item: TrackingProtectionException? = null
|
||||
|
|
|
@ -240,7 +240,7 @@ class SettingsFragment : PreferenceFragmentCompat() {
|
|||
null
|
||||
}
|
||||
resources.getString(R.string.pref_key_passwords) -> {
|
||||
SettingsFragmentDirections.actionSettingsFragmentToLoginsFragment()
|
||||
SettingsFragmentDirections.actionSettingsFragmentToSavedLoginsAuthFragment()
|
||||
}
|
||||
resources.getString(R.string.pref_key_about) -> {
|
||||
SettingsFragmentDirections.actionSettingsFragmentToAboutFragment()
|
||||
|
|
|
@ -0,0 +1,215 @@
|
|||
/* 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 android.os.Bundle
|
||||
import android.text.Editable
|
||||
import android.text.InputType
|
||||
import android.util.Log
|
||||
import android.view.Menu
|
||||
import android.view.MenuInflater
|
||||
import android.view.MenuItem
|
||||
import android.view.View
|
||||
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.coroutines.CancellationException
|
||||
import kotlinx.coroutines.Dispatchers.IO
|
||||
import kotlinx.coroutines.Dispatchers.Main
|
||||
import kotlinx.coroutines.async
|
||||
import kotlinx.coroutines.withContext
|
||||
import mozilla.components.concept.storage.Login
|
||||
import mozilla.components.service.sync.logins.InvalidRecordException
|
||||
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
|
||||
import org.mozilla.fenix.ext.settings
|
||||
|
||||
/**
|
||||
* Displays the editable saved login information for a single website.
|
||||
*/
|
||||
@Suppress("TooManyFunctions", "NestedBlockDepth", "ForbiddenComment")
|
||||
class EditLoginFragment : Fragment(R.layout.fragment_edit_login) {
|
||||
|
||||
private val args by navArgs<EditLoginFragmentArgs>()
|
||||
private lateinit var savedLoginsStore: LoginsFragmentStore
|
||||
fun String.toEditable(): Editable = Editable.Factory.getInstance().newEditable(this)
|
||||
|
||||
override fun onCreate(savedInstanceState: Bundle?) {
|
||||
super.onCreate(savedInstanceState)
|
||||
setHasOptionsMenu(true)
|
||||
|
||||
savedLoginsStore = StoreProvider.get(this) {
|
||||
LoginsFragmentStore(
|
||||
LoginsListState(
|
||||
isLoading = true,
|
||||
loginList = listOf(),
|
||||
filteredItems = listOf(),
|
||||
searchedForText = null,
|
||||
sortingStrategy = requireContext().settings().savedLoginsSortingStrategy,
|
||||
highlightedItem = requireContext().settings().savedLoginsMenuHighlightedItem
|
||||
)
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
|
||||
super.onViewCreated(view, savedInstanceState)
|
||||
|
||||
// ensure hostname isn't editable
|
||||
hostnameText.text = args.savedLoginItem.origin.toEditable()
|
||||
hostnameText.isClickable = false
|
||||
hostnameText.isFocusable = false
|
||||
|
||||
usernameText.text = args.savedLoginItem.username.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
|
||||
|
||||
setUpClickListeners()
|
||||
}
|
||||
|
||||
private fun setUpClickListeners() {
|
||||
clearUsernameTextButton.setOnClickListener {
|
||||
usernameText.text?.clear()
|
||||
usernameText.isCursorVisible = true
|
||||
usernameText.hasFocus()
|
||||
inputLayoutUsername.hasFocus()
|
||||
}
|
||||
clearPasswordTextButton.setOnClickListener {
|
||||
passwordText.text?.clear()
|
||||
passwordText.isCursorVisible = true
|
||||
passwordText.hasFocus()
|
||||
inputLayoutPassword.hasFocus()
|
||||
}
|
||||
revealPasswordButton.setOnClickListener {
|
||||
togglePasswordReveal()
|
||||
}
|
||||
passwordText.setOnClickListener {
|
||||
togglePasswordReveal()
|
||||
}
|
||||
}
|
||||
|
||||
override fun onCreateOptionsMenu(menu: Menu, inflater: MenuInflater) {
|
||||
inflater.inflate(R.menu.login_save, menu)
|
||||
}
|
||||
|
||||
override fun onOptionsItemSelected(item: MenuItem): Boolean = when (item.itemId) {
|
||||
R.id.save_login_button -> {
|
||||
view?.hideKeyboard()
|
||||
try {
|
||||
if (!passwordText.text.isNullOrBlank()) {
|
||||
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)
|
||||
}
|
||||
}
|
||||
true
|
||||
}
|
||||
else -> false
|
||||
}
|
||||
|
||||
// TODO: Move interactions with the component's password storage into a separate datastore
|
||||
// This includes Delete, Update/Edit, Create
|
||||
private fun attemptSaveAndExit() {
|
||||
var saveLoginJob: Deferred<Unit>? = null
|
||||
viewLifecycleOwner.lifecycleScope.launch(IO) {
|
||||
saveLoginJob = async {
|
||||
val oldLogin = requireContext().components.core.passwordsStorage.get(args.savedLoginItem.guid)
|
||||
|
||||
// Update requires a Login type, which needs at least one of httpRealm or formActionOrigin
|
||||
val loginToSave = Login(
|
||||
guid = oldLogin?.guid,
|
||||
origin = oldLogin?.origin!!,
|
||||
username = usernameText.text.toString(), // new value
|
||||
password = passwordText.text.toString(), // new value
|
||||
httpRealm = oldLogin.httpRealm,
|
||||
formActionOrigin = oldLogin.formActionOrigin
|
||||
)
|
||||
|
||||
save(loginToSave)
|
||||
syncAndUpdateList(loginToSave)
|
||||
}
|
||||
saveLoginJob?.await()
|
||||
withContext(Main) {
|
||||
val directions =
|
||||
EditLoginFragmentDirections
|
||||
.actionEditLoginFragmentToLoginDetailFragment(args.savedLoginItem.guid)
|
||||
findNavController().navigate(directions)
|
||||
}
|
||||
}
|
||||
saveLoginJob?.invokeOnCompletion {
|
||||
if (it is CancellationException) {
|
||||
saveLoginJob?.cancel()
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private suspend fun save(loginToSave: Login) =
|
||||
requireContext().components.core.passwordsStorage.update(loginToSave)
|
||||
|
||||
private fun syncAndUpdateList(updatedLogin: Login) {
|
||||
val login = updatedLogin.mapToSavedLogin()
|
||||
savedLoginsStore.dispatch(LoginsAction.UpdateLoginsList(listOf(login)))
|
||||
}
|
||||
|
||||
// TODO: create helper class for toggling passwords. Used in login info and edit fragments.
|
||||
private fun togglePasswordReveal() {
|
||||
val currText = passwordText.text
|
||||
if (passwordText.inputType == InputType.TYPE_TEXT_VARIATION_PASSWORD
|
||||
or InputType.TYPE_CLASS_TEXT
|
||||
) {
|
||||
context?.components?.analytics?.metrics?.track(Event.ViewLoginPassword)
|
||||
passwordText.inputType = InputType.TYPE_TEXT_VARIATION_VISIBLE_PASSWORD
|
||||
revealPasswordButton.setImageDrawable(
|
||||
resources.getDrawable(R.drawable.mozac_ic_password_hide, null)
|
||||
)
|
||||
revealPasswordButton.contentDescription =
|
||||
resources.getString(R.string.saved_login_hide_password)
|
||||
} else {
|
||||
passwordText.inputType =
|
||||
InputType.TYPE_CLASS_TEXT or InputType.TYPE_TEXT_VARIATION_PASSWORD
|
||||
revealPasswordButton.setImageDrawable(
|
||||
resources.getDrawable(R.drawable.mozac_ic_password_reveal, null)
|
||||
)
|
||||
revealPasswordButton.contentDescription =
|
||||
context?.getString(R.string.saved_login_reveal_password)
|
||||
}
|
||||
// For the new type to take effect you need to reset the text to it's current edited version
|
||||
passwordText?.text = currText
|
||||
}
|
||||
}
|
|
@ -0,0 +1,265 @@
|
|||
/* 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 android.content.DialogInterface
|
||||
import android.os.Bundle
|
||||
import android.text.InputType
|
||||
import android.view.MenuItem
|
||||
import android.view.MenuInflater
|
||||
import android.view.Menu
|
||||
import android.view.View
|
||||
import android.view.ViewGroup
|
||||
import android.view.LayoutInflater
|
||||
import androidx.annotation.StringRes
|
||||
import androidx.appcompat.app.AlertDialog
|
||||
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_login_detail.*
|
||||
import kotlinx.coroutines.Dispatchers.IO
|
||||
import kotlinx.coroutines.Dispatchers.Main
|
||||
import kotlinx.coroutines.ExperimentalCoroutinesApi
|
||||
import kotlinx.coroutines.ObsoleteCoroutinesApi
|
||||
import kotlinx.coroutines.Deferred
|
||||
import kotlinx.coroutines.CancellationException
|
||||
import kotlinx.coroutines.launch
|
||||
import kotlinx.coroutines.async
|
||||
import kotlinx.coroutines.withContext
|
||||
import mozilla.components.concept.storage.Login
|
||||
import mozilla.components.lib.state.ext.consumeFrom
|
||||
import org.mozilla.fenix.FeatureFlags
|
||||
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
|
||||
import org.mozilla.fenix.ext.settings
|
||||
import org.mozilla.fenix.ext.showToolbar
|
||||
import org.mozilla.fenix.ext.urlToTrimmedHost
|
||||
|
||||
/**
|
||||
* Displays saved login information for a single website.
|
||||
*/
|
||||
@Suppress("TooManyFunctions", "ForbiddenComment")
|
||||
@ExperimentalCoroutinesApi
|
||||
class LoginDetailFragment : Fragment(R.layout.fragment_login_detail) {
|
||||
|
||||
private val args by navArgs<LoginDetailFragmentArgs>()
|
||||
private var login: SavedLogin? = null
|
||||
private lateinit var savedLoginsStore: LoginsFragmentStore
|
||||
private lateinit var loginDetailView: LoginDetailView
|
||||
|
||||
override fun onCreateView(
|
||||
inflater: LayoutInflater,
|
||||
container: ViewGroup?,
|
||||
savedInstanceState: Bundle?
|
||||
): View? {
|
||||
val view = inflater.inflate(R.layout.fragment_login_detail, container, false)
|
||||
savedLoginsStore = StoreProvider.get(this) {
|
||||
LoginsFragmentStore(
|
||||
LoginsListState(
|
||||
isLoading = true,
|
||||
loginList = listOf(),
|
||||
filteredItems = listOf(),
|
||||
searchedForText = null,
|
||||
sortingStrategy = requireContext().settings().savedLoginsSortingStrategy,
|
||||
highlightedItem = requireContext().settings().savedLoginsMenuHighlightedItem
|
||||
)
|
||||
)
|
||||
}
|
||||
loginDetailView = LoginDetailView(view?.findViewById(R.id.loginDetailLayout))
|
||||
fetchLoginDetails()
|
||||
|
||||
return view
|
||||
}
|
||||
|
||||
@ObsoleteCoroutinesApi
|
||||
@ExperimentalCoroutinesApi
|
||||
override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
|
||||
consumeFrom(savedLoginsStore) {
|
||||
loginDetailView.update(it)
|
||||
login = savedLoginsStore.state.currentItem
|
||||
setUpCopyButtons()
|
||||
showToolbar(
|
||||
savedLoginsStore.state.currentItem?.origin?.urlToTrimmedHost(requireContext())
|
||||
?: ""
|
||||
)
|
||||
setUpPasswordReveal()
|
||||
}
|
||||
}
|
||||
|
||||
override fun onCreate(savedInstanceState: Bundle?) {
|
||||
super.onCreate(savedInstanceState)
|
||||
setHasOptionsMenu(true)
|
||||
}
|
||||
|
||||
private fun setUpPasswordReveal() {
|
||||
passwordText.inputType =
|
||||
InputType.TYPE_CLASS_TEXT or InputType.TYPE_TEXT_VARIATION_PASSWORD
|
||||
revealPasswordButton.setOnClickListener {
|
||||
togglePasswordReveal()
|
||||
}
|
||||
}
|
||||
|
||||
private fun setUpCopyButtons() {
|
||||
webAddressText.text = login?.origin
|
||||
copyWebAddress.setOnClickListener(
|
||||
CopyButtonListener(login?.origin, R.string.logins_site_copied)
|
||||
)
|
||||
|
||||
usernameText.text = login?.username
|
||||
copyUsername.setOnClickListener(
|
||||
CopyButtonListener(login?.username, R.string.logins_username_copied)
|
||||
)
|
||||
|
||||
passwordText.text = login?.password
|
||||
copyPassword.setOnClickListener(
|
||||
CopyButtonListener(login?.password, R.string.logins_password_copied)
|
||||
)
|
||||
}
|
||||
|
||||
// TODO: Move interactions with the component's password storage into a separate datastore
|
||||
private fun fetchLoginDetails() {
|
||||
var deferredLogin: Deferred<List<Login>>? = null
|
||||
val fetchLoginJob = viewLifecycleOwner.lifecycleScope.launch(IO) {
|
||||
deferredLogin = async {
|
||||
requireContext().components.core.passwordsStorage.list()
|
||||
}
|
||||
val fetchedLoginList = deferredLogin?.await()
|
||||
|
||||
fetchedLoginList?.let {
|
||||
withContext(Main) {
|
||||
val login = fetchedLoginList.filter {
|
||||
it.guid == args.savedLoginId
|
||||
}.first()
|
||||
savedLoginsStore.dispatch(
|
||||
LoginsAction.UpdateCurrentLogin(login.mapToSavedLogin())
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
fetchLoginJob.invokeOnCompletion {
|
||||
if (it is CancellationException) {
|
||||
deferredLogin?.cancel()
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
override fun onCreateOptionsMenu(menu: Menu, inflater: MenuInflater) {
|
||||
if (FeatureFlags.loginsEdit) {
|
||||
inflater.inflate(R.menu.login_options_menu, menu)
|
||||
} else {
|
||||
inflater.inflate(R.menu.login_delete, menu)
|
||||
}
|
||||
}
|
||||
|
||||
override fun onOptionsItemSelected(item: MenuItem): Boolean = when (item.itemId) {
|
||||
R.id.delete_login_button -> {
|
||||
displayDeleteLoginDialog()
|
||||
true
|
||||
}
|
||||
R.id.edit_login_button -> {
|
||||
editLogin()
|
||||
true
|
||||
}
|
||||
else -> false
|
||||
}
|
||||
|
||||
private fun editLogin() {
|
||||
val directions =
|
||||
LoginDetailFragmentDirections
|
||||
.actionLoginDetailFragmentToEditLoginFragment(login!!)
|
||||
findNavController().navigate(directions)
|
||||
}
|
||||
|
||||
private fun displayDeleteLoginDialog() {
|
||||
activity?.let { activity ->
|
||||
AlertDialog.Builder(activity).apply {
|
||||
setMessage(R.string.login_deletion_confirmation)
|
||||
setNegativeButton(android.R.string.cancel) { dialog: DialogInterface, _ ->
|
||||
dialog.cancel()
|
||||
}
|
||||
setPositiveButton(R.string.dialog_delete_positive) { dialog: DialogInterface, _ ->
|
||||
deleteLogin()
|
||||
dialog.dismiss()
|
||||
}
|
||||
create()
|
||||
}.show()
|
||||
}
|
||||
}
|
||||
|
||||
// TODO: Move interactions with the component's password storage into a separate datastore
|
||||
// This includes Delete, Update/Edit, Create
|
||||
private fun deleteLogin() {
|
||||
var deleteLoginJob: Deferred<Boolean>? = null
|
||||
val deleteJob = viewLifecycleOwner.lifecycleScope.launch(IO) {
|
||||
deleteLoginJob = async {
|
||||
requireContext().components.core.passwordsStorage.delete(args.savedLoginId)
|
||||
}
|
||||
deleteLoginJob?.await()
|
||||
withContext(Main) {
|
||||
findNavController().popBackStack(R.id.savedLoginsFragment, false)
|
||||
}
|
||||
}
|
||||
deleteJob.invokeOnCompletion {
|
||||
if (it is CancellationException) {
|
||||
deleteLoginJob?.cancel()
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// TODO: create helper class for toggling passwords. Used in login info and edit fragments.
|
||||
private fun togglePasswordReveal() {
|
||||
if (passwordText.inputType == InputType.TYPE_TEXT_VARIATION_PASSWORD or InputType.TYPE_CLASS_TEXT) {
|
||||
context?.components?.analytics?.metrics?.track(Event.ViewLoginPassword)
|
||||
passwordText.inputType = InputType.TYPE_TEXT_VARIATION_VISIBLE_PASSWORD
|
||||
revealPasswordButton.setImageDrawable(
|
||||
resources.getDrawable(R.drawable.mozac_ic_password_hide, null)
|
||||
)
|
||||
revealPasswordButton.contentDescription =
|
||||
resources.getString(R.string.saved_login_hide_password)
|
||||
} else {
|
||||
passwordText.inputType =
|
||||
InputType.TYPE_CLASS_TEXT or InputType.TYPE_TEXT_VARIATION_PASSWORD
|
||||
revealPasswordButton.setImageDrawable(
|
||||
resources.getDrawable(R.drawable.mozac_ic_password_reveal, null)
|
||||
)
|
||||
revealPasswordButton.contentDescription =
|
||||
context?.getString(R.string.saved_login_reveal_password)
|
||||
}
|
||||
// For the new type to take effect you need to reset the text
|
||||
passwordText.text = login?.password
|
||||
}
|
||||
|
||||
/**
|
||||
* Click listener for a textview's copy button.
|
||||
* @param value Value to be copied
|
||||
* @param snackbarText Text to display in snackbar after copying.
|
||||
*/
|
||||
private inner class CopyButtonListener(
|
||||
private val value: String?,
|
||||
@StringRes private val snackbarText: Int
|
||||
) : View.OnClickListener {
|
||||
override fun onClick(view: View) {
|
||||
val clipboard = view.context.components.clipboardHandler
|
||||
clipboard.text = value
|
||||
showCopiedSnackbar(view.context.getString(snackbarText))
|
||||
view.context.components.analytics.metrics.track(Event.CopyLogin)
|
||||
}
|
||||
|
||||
private fun showCopiedSnackbar(copiedItem: String) {
|
||||
view?.let {
|
||||
FenixSnackbar.make(
|
||||
view = it,
|
||||
duration = Snackbar.LENGTH_SHORT,
|
||||
isDisplayedWithBrowserToolbar = false
|
||||
).setText(copiedItem).show()
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
|
@ -0,0 +1,20 @@
|
|||
/* 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 android.view.ViewGroup
|
||||
import kotlinx.android.extensions.LayoutContainer
|
||||
import kotlinx.android.synthetic.main.fragment_login_detail.*
|
||||
|
||||
/**
|
||||
* View that contains and configures the Login Details
|
||||
*/
|
||||
class LoginDetailView(override val containerView: ViewGroup?) : LayoutContainer {
|
||||
fun update(login: LoginsListState) {
|
||||
webAddressText.text = login.currentItem?.origin
|
||||
usernameText.text = login.currentItem?.username
|
||||
passwordText.text = login.currentItem?.password
|
||||
}
|
||||
}
|
|
@ -8,33 +8,30 @@ import android.view.LayoutInflater
|
|||
import android.view.ViewGroup
|
||||
import androidx.recyclerview.widget.DiffUtil
|
||||
import androidx.recyclerview.widget.ListAdapter
|
||||
import org.mozilla.fenix.R
|
||||
|
||||
private sealed class AdapterItem {
|
||||
data class Item(val item: SavedLoginsItem) : AdapterItem()
|
||||
}
|
||||
|
||||
class SavedLoginsAdapter(
|
||||
class LoginsAdapter(
|
||||
private val interactor: SavedLoginsInteractor
|
||||
) : ListAdapter<SavedLoginsItem, SavedLoginsListItemViewHolder>(DiffCallback) {
|
||||
) : ListAdapter<SavedLogin, LoginsListViewHolder>(DiffCallback) {
|
||||
|
||||
override fun onCreateViewHolder(
|
||||
parent: ViewGroup,
|
||||
viewType: Int
|
||||
): SavedLoginsListItemViewHolder {
|
||||
): LoginsListViewHolder {
|
||||
val view = LayoutInflater.from(parent.context)
|
||||
.inflate(SavedLoginsListItemViewHolder.LAYOUT_ID, parent, false)
|
||||
return SavedLoginsListItemViewHolder(view, interactor)
|
||||
.inflate(R.layout.logins_item, parent, false)
|
||||
return LoginsListViewHolder(view, interactor)
|
||||
}
|
||||
|
||||
override fun onBindViewHolder(holder: SavedLoginsListItemViewHolder, position: Int) {
|
||||
override fun onBindViewHolder(holder: LoginsListViewHolder, position: Int) {
|
||||
holder.bind(getItem(position))
|
||||
}
|
||||
|
||||
private object DiffCallback : DiffUtil.ItemCallback<SavedLoginsItem>() {
|
||||
override fun areItemsTheSame(oldItem: SavedLoginsItem, newItem: SavedLoginsItem) =
|
||||
oldItem.url == newItem.url
|
||||
private object DiffCallback : DiffUtil.ItemCallback<SavedLogin>() {
|
||||
override fun areItemsTheSame(oldItem: SavedLogin, newItem: SavedLogin) =
|
||||
oldItem.origin == newItem.origin
|
||||
|
||||
override fun areContentsTheSame(oldItem: SavedLoginsItem, newItem: SavedLoginsItem) =
|
||||
override fun areContentsTheSame(oldItem: SavedLogin, newItem: SavedLogin) =
|
||||
oldItem == newItem
|
||||
}
|
||||
}
|
|
@ -0,0 +1,161 @@
|
|||
/* 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 android.os.Parcelable
|
||||
import kotlinx.android.parcel.Parcelize
|
||||
import mozilla.components.concept.storage.Login
|
||||
import mozilla.components.lib.state.Action
|
||||
import mozilla.components.lib.state.State
|
||||
import mozilla.components.lib.state.Store
|
||||
|
||||
/**
|
||||
* Class representing a parcelable saved logins item
|
||||
* @property guid The id of the saved login
|
||||
* @property origin Site of the saved login
|
||||
* @property username Username that's saved for this site
|
||||
* @property password Password that's saved for this site
|
||||
* @property timeLastUsed Time of last use in milliseconds from the unix epoch.
|
||||
*/
|
||||
@Parcelize
|
||||
data class SavedLogin(
|
||||
val guid: String,
|
||||
val origin: String,
|
||||
val username: String,
|
||||
val password: String?,
|
||||
val timeLastUsed: Long
|
||||
) : Parcelable
|
||||
|
||||
fun Login.mapToSavedLogin(): SavedLogin =
|
||||
SavedLogin(
|
||||
guid = this.guid!!,
|
||||
origin = this.origin,
|
||||
username = this.username,
|
||||
password = this.password,
|
||||
timeLastUsed = this.timeLastUsed
|
||||
)
|
||||
|
||||
/**
|
||||
* The [Store] for holding the [LoginsListState] and applying [LoginsAction]s.
|
||||
*/
|
||||
class LoginsFragmentStore(initialState: LoginsListState) :
|
||||
Store<LoginsListState, LoginsAction>(
|
||||
initialState,
|
||||
::savedLoginsStateReducer
|
||||
)
|
||||
|
||||
/**
|
||||
* Actions to dispatch through the `LoginsFragmentStore` to modify `LoginsListState` through the reducer.
|
||||
*/
|
||||
sealed class LoginsAction : Action {
|
||||
data class FilterLogins(val newText: String?) : LoginsAction()
|
||||
data class UpdateLoginsList(val list: List<SavedLogin>) : LoginsAction()
|
||||
data class UpdateCurrentLogin(val item: SavedLogin) : LoginsAction()
|
||||
data class SortLogins(val sortingStrategy: SortingStrategy) : LoginsAction()
|
||||
}
|
||||
|
||||
/**
|
||||
* The state for the Saved Logins Screen
|
||||
* @property loginList Source of truth for local list of logins
|
||||
* @property loginList Filterable list of logins to display
|
||||
* @property currentItem The last item that was opened into the detail view
|
||||
* @property searchedForText String used by the user to filter logins
|
||||
* @property sortingStrategy sorting strategy selected by the user (Currently we support
|
||||
* sorting alphabetically and by last used)
|
||||
* @property highlightedItem The current selected sorting strategy from the sort menu
|
||||
*/
|
||||
data class LoginsListState(
|
||||
val isLoading: Boolean = false,
|
||||
val loginList: List<SavedLogin>,
|
||||
val filteredItems: List<SavedLogin>,
|
||||
val currentItem: SavedLogin? = null,
|
||||
val searchedForText: String?,
|
||||
val sortingStrategy: SortingStrategy,
|
||||
val highlightedItem: SavedLoginsSortingStrategyMenu.Item
|
||||
) : State
|
||||
|
||||
/**
|
||||
* Handles changes in the saved logins list, including updates and filtering.
|
||||
*/
|
||||
private fun savedLoginsStateReducer(
|
||||
state: LoginsListState,
|
||||
action: LoginsAction
|
||||
): LoginsListState {
|
||||
return when (action) {
|
||||
is LoginsAction.UpdateLoginsList -> state.copy(
|
||||
isLoading = false,
|
||||
loginList = action.list,
|
||||
filteredItems = action.list
|
||||
)
|
||||
is LoginsAction.FilterLogins -> {
|
||||
filterItems(
|
||||
action.newText,
|
||||
state.sortingStrategy,
|
||||
state
|
||||
)
|
||||
}
|
||||
is LoginsAction.UpdateCurrentLogin -> {
|
||||
state.copy(
|
||||
currentItem = action.item
|
||||
)
|
||||
}
|
||||
is LoginsAction.SortLogins -> {
|
||||
filterItems(
|
||||
state.searchedForText,
|
||||
action.sortingStrategy,
|
||||
state
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* @return [LoginsListState] containing a new [LoginsListState.filteredItems]
|
||||
* with filtered [LoginsListState.items]
|
||||
*
|
||||
* @param searchedForText based on which [LoginsListState.items] will be filtered.
|
||||
* @param sortingStrategy based on which [LoginsListState.items] will be sorted.
|
||||
* @param state previous [LoginsListState] containing all the other properties
|
||||
* with which a new state will be created
|
||||
*/
|
||||
private fun filterItems(
|
||||
searchedForText: String?,
|
||||
sortingStrategy: SortingStrategy,
|
||||
state: LoginsListState
|
||||
): LoginsListState {
|
||||
return if (searchedForText.isNullOrBlank()) {
|
||||
state.copy(
|
||||
isLoading = false,
|
||||
sortingStrategy = sortingStrategy,
|
||||
highlightedItem = sortingStrategyToMenuItem(sortingStrategy),
|
||||
searchedForText = searchedForText,
|
||||
filteredItems = sortingStrategy(state.loginList)
|
||||
)
|
||||
} else {
|
||||
state.copy(
|
||||
isLoading = false,
|
||||
sortingStrategy = sortingStrategy,
|
||||
highlightedItem = sortingStrategyToMenuItem(sortingStrategy),
|
||||
searchedForText = searchedForText,
|
||||
filteredItems = sortingStrategy(state.loginList).filter {
|
||||
it.origin.contains(
|
||||
searchedForText
|
||||
)
|
||||
}
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
private fun sortingStrategyToMenuItem(sortingStrategy: SortingStrategy): SavedLoginsSortingStrategyMenu.Item {
|
||||
return when (sortingStrategy) {
|
||||
is SortingStrategy.Alphabetically -> {
|
||||
SavedLoginsSortingStrategyMenu.Item.AlphabeticallySort
|
||||
}
|
||||
|
||||
is SortingStrategy.LastUsed -> {
|
||||
SavedLoginsSortingStrategyMenu.Item.LastUsedSort
|
||||
}
|
||||
}
|
||||
}
|
|
@ -7,26 +7,32 @@ package org.mozilla.fenix.settings.logins
|
|||
import android.view.View
|
||||
import androidx.recyclerview.widget.RecyclerView
|
||||
import kotlinx.android.synthetic.main.logins_item.view.*
|
||||
import org.mozilla.fenix.R
|
||||
import org.mozilla.fenix.ext.components
|
||||
import org.mozilla.fenix.ext.loadIntoView
|
||||
|
||||
class SavedLoginsListItemViewHolder(
|
||||
class LoginsListViewHolder(
|
||||
private val view: View,
|
||||
private val interactor: SavedLoginsInteractor
|
||||
) : RecyclerView.ViewHolder(view) {
|
||||
|
||||
private val favicon = view.favicon_image
|
||||
private val url = view.domainView
|
||||
private val userName = view.userView
|
||||
private val url = view.webAddressView
|
||||
private val username = view.usernameView
|
||||
private var loginItem: SavedLogin? = null
|
||||
|
||||
private var item: SavedLoginsItem? = null
|
||||
fun bind(item: SavedLogin) {
|
||||
this.loginItem = SavedLogin(
|
||||
guid = item.guid,
|
||||
origin = item.origin,
|
||||
password = item.password,
|
||||
username = item.username,
|
||||
timeLastUsed = item.timeLastUsed
|
||||
)
|
||||
url.text = item.origin
|
||||
username.text = item.username
|
||||
|
||||
updateFavIcon(item.origin)
|
||||
|
||||
fun bind(item: SavedLoginsItem) {
|
||||
this.item = item
|
||||
url.text = item.url
|
||||
userName.text = item.userName
|
||||
updateFavIcon(item.url)
|
||||
view.setOnClickListener {
|
||||
interactor.itemClicked(item)
|
||||
}
|
||||
|
@ -35,8 +41,4 @@ class SavedLoginsListItemViewHolder(
|
|||
private fun updateFavIcon(url: String) {
|
||||
favicon.context.components.core.icons.loadIntoView(favicon, url)
|
||||
}
|
||||
|
||||
companion object {
|
||||
const val LAYOUT_ID = R.layout.logins_item
|
||||
}
|
||||
}
|
|
@ -1,194 +0,0 @@
|
|||
/* 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 android.content.Context
|
||||
import android.content.DialogInterface
|
||||
import android.os.Bundle
|
||||
import android.text.InputType
|
||||
import android.view.Menu
|
||||
import android.view.MenuInflater
|
||||
import android.view.MenuItem
|
||||
import android.view.View
|
||||
import android.view.WindowManager
|
||||
import androidx.annotation.StringRes
|
||||
import androidx.appcompat.app.AlertDialog
|
||||
import androidx.appcompat.content.res.AppCompatResources.getDrawable
|
||||
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_saved_login_site_info.*
|
||||
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 org.mozilla.fenix.R
|
||||
import org.mozilla.fenix.components.FenixSnackbar
|
||||
import org.mozilla.fenix.components.metrics.Event
|
||||
import org.mozilla.fenix.ext.checkAndUpdateScreenshotPermission
|
||||
import org.mozilla.fenix.ext.components
|
||||
import org.mozilla.fenix.ext.settings
|
||||
import org.mozilla.fenix.ext.showToolbar
|
||||
|
||||
/**
|
||||
* Displays saved login information for a single website.
|
||||
*/
|
||||
class SavedLoginSiteInfoFragment : Fragment(R.layout.fragment_saved_login_site_info) {
|
||||
|
||||
private val args by navArgs<SavedLoginSiteInfoFragmentArgs>()
|
||||
|
||||
override fun onCreate(savedInstanceState: Bundle?) {
|
||||
super.onCreate(savedInstanceState)
|
||||
setHasOptionsMenu(true)
|
||||
}
|
||||
|
||||
override fun onPause() {
|
||||
// If we pause this fragment, we want to pop users back to reauth
|
||||
if (findNavController().currentDestination?.id != R.id.savedLoginsFragment) {
|
||||
activity?.let { it.checkAndUpdateScreenshotPermission(it.settings()) }
|
||||
findNavController().popBackStack(R.id.loginsFragment, false)
|
||||
}
|
||||
super.onPause()
|
||||
}
|
||||
|
||||
override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
|
||||
super.onViewCreated(view, savedInstanceState)
|
||||
|
||||
siteInfoText.text = args.savedLoginItem.url
|
||||
copySiteItem.setOnClickListener(
|
||||
CopyButtonListener(args.savedLoginItem.url, R.string.logins_site_copied)
|
||||
)
|
||||
|
||||
usernameInfoText.text = args.savedLoginItem.userName
|
||||
copyUsernameItem.setOnClickListener(
|
||||
CopyButtonListener(args.savedLoginItem.userName, R.string.logins_username_copied)
|
||||
)
|
||||
|
||||
passwordInfoText.inputType =
|
||||
InputType.TYPE_CLASS_TEXT or InputType.TYPE_TEXT_VARIATION_PASSWORD
|
||||
passwordInfoText.text = args.savedLoginItem.password
|
||||
revealPasswordItem.setOnClickListener {
|
||||
togglePasswordReveal(it.context)
|
||||
}
|
||||
copyPasswordItem.setOnClickListener(
|
||||
CopyButtonListener(args.savedLoginItem.password, R.string.logins_password_copied)
|
||||
)
|
||||
}
|
||||
|
||||
override fun onCreateOptionsMenu(menu: Menu, inflater: MenuInflater) {
|
||||
inflater.inflate(R.menu.login_edit, menu)
|
||||
}
|
||||
|
||||
override fun onOptionsItemSelected(item: MenuItem): Boolean = when (item.itemId) {
|
||||
R.id.delete_login_button -> {
|
||||
displayDeleteLoginDialog()
|
||||
true
|
||||
}
|
||||
else -> false
|
||||
}
|
||||
|
||||
private fun deleteLogin() {
|
||||
var deleteLoginJob: Deferred<Boolean>? = null
|
||||
val deleteJob = viewLifecycleOwner.lifecycleScope.launch(IO) {
|
||||
deleteLoginJob = async {
|
||||
requireContext().components.core.passwordsStorage.delete(args.savedLoginItem.id)
|
||||
}
|
||||
deleteLoginJob?.await()
|
||||
withContext(Main) {
|
||||
findNavController().popBackStack(R.id.savedLoginsFragment, false)
|
||||
}
|
||||
}
|
||||
deleteJob.invokeOnCompletion {
|
||||
if (it is CancellationException) {
|
||||
deleteLoginJob?.cancel()
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private fun togglePasswordReveal(context: Context) {
|
||||
if (passwordInfoText.inputType == InputType.TYPE_TEXT_VARIATION_PASSWORD or InputType.TYPE_CLASS_TEXT) {
|
||||
context.components.analytics.metrics.track(Event.ViewLoginPassword)
|
||||
passwordInfoText.inputType = InputType.TYPE_TEXT_VARIATION_VISIBLE_PASSWORD
|
||||
revealPasswordItem.setImageDrawable(
|
||||
getDrawable(
|
||||
context,
|
||||
R.drawable.mozac_ic_password_hide
|
||||
)
|
||||
)
|
||||
revealPasswordItem.contentDescription =
|
||||
context.getString(R.string.saved_login_hide_password)
|
||||
} else {
|
||||
passwordInfoText.inputType =
|
||||
InputType.TYPE_CLASS_TEXT or InputType.TYPE_TEXT_VARIATION_PASSWORD
|
||||
revealPasswordItem.setImageDrawable(
|
||||
getDrawable(
|
||||
context,
|
||||
R.drawable.mozac_ic_password_reveal
|
||||
)
|
||||
)
|
||||
revealPasswordItem.contentDescription =
|
||||
context.getString(R.string.saved_login_reveal_password)
|
||||
}
|
||||
// For the new type to take effect you need to reset the text
|
||||
passwordInfoText.text = args.savedLoginItem.password
|
||||
}
|
||||
|
||||
override fun onResume() {
|
||||
super.onResume()
|
||||
activity?.window?.setFlags(
|
||||
WindowManager.LayoutParams.FLAG_SECURE,
|
||||
WindowManager.LayoutParams.FLAG_SECURE
|
||||
)
|
||||
showToolbar(args.savedLoginItem.url)
|
||||
}
|
||||
|
||||
private fun displayDeleteLoginDialog() {
|
||||
activity?.let { activity ->
|
||||
AlertDialog.Builder(activity).apply {
|
||||
setMessage(R.string.login_deletion_confirmation)
|
||||
setNegativeButton(android.R.string.cancel) { dialog: DialogInterface, _ ->
|
||||
dialog.cancel()
|
||||
}
|
||||
setPositiveButton(R.string.dialog_delete_positive) { dialog: DialogInterface, _ ->
|
||||
deleteLogin()
|
||||
dialog.dismiss()
|
||||
}
|
||||
create()
|
||||
}.show()
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Click listener for a textview's copy button.
|
||||
* @param value Value to be copied
|
||||
* @param snackbarText Text to display in snackbar after copying.
|
||||
*/
|
||||
private inner class CopyButtonListener(
|
||||
private val value: String?,
|
||||
@StringRes private val snackbarText: Int
|
||||
) : View.OnClickListener {
|
||||
override fun onClick(view: View) {
|
||||
val clipboard = view.context.components.clipboardHandler
|
||||
clipboard.text = value
|
||||
showCopiedSnackbar(view.context.getString(snackbarText))
|
||||
view.context.components.analytics.metrics.track(Event.CopyLogin)
|
||||
}
|
||||
|
||||
private fun showCopiedSnackbar(copiedItem: String) {
|
||||
view?.let {
|
||||
FenixSnackbar.make(
|
||||
view = it,
|
||||
duration = Snackbar.LENGTH_SHORT,
|
||||
isDisplayedWithBrowserToolbar = false
|
||||
).setText(copiedItem).show()
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
|
@ -2,7 +2,7 @@
|
|||
* 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
|
||||
package org.mozilla.fenix.settings.logins
|
||||
|
||||
import android.annotation.TargetApi
|
||||
import android.app.Activity.RESULT_OK
|
||||
|
@ -38,10 +38,11 @@ import org.mozilla.fenix.ext.requireComponents
|
|||
import org.mozilla.fenix.ext.secure
|
||||
import org.mozilla.fenix.ext.settings
|
||||
import org.mozilla.fenix.ext.showToolbar
|
||||
import org.mozilla.fenix.settings.SharedPreferenceUpdater
|
||||
import java.util.concurrent.Executors
|
||||
|
||||
@Suppress("TooManyFunctions", "LargeClass")
|
||||
class LoginsFragment : PreferenceFragmentCompat(), AccountObserver {
|
||||
class SavedLoginsAuthFragment : PreferenceFragmentCompat(), AccountObserver {
|
||||
|
||||
@TargetApi(M)
|
||||
private lateinit var biometricPromptCallback: BiometricPrompt.AuthenticationCallback
|
||||
|
@ -114,7 +115,8 @@ class LoginsFragment : PreferenceFragmentCompat(), AccountObserver {
|
|||
isEnabled = context.settings().shouldPromptToSaveLogins
|
||||
isChecked =
|
||||
context.settings().shouldAutofillLogins && context.settings().shouldPromptToSaveLogins
|
||||
onPreferenceChangeListener = SharedPreferenceUpdater()
|
||||
onPreferenceChangeListener =
|
||||
SharedPreferenceUpdater()
|
||||
}
|
||||
|
||||
val savedLoginsKey = getPreferenceKey(R.string.pref_key_saved_logins)
|
||||
|
@ -253,7 +255,9 @@ class LoginsFragment : PreferenceFragmentCompat(), AccountObserver {
|
|||
getString(R.string.logins_biometric_prompt_message_pin),
|
||||
getString(R.string.logins_biometric_prompt_message)
|
||||
)
|
||||
startActivityForResult(intent, PIN_REQUEST)
|
||||
startActivityForResult(intent,
|
||||
PIN_REQUEST
|
||||
)
|
||||
}
|
||||
|
||||
override fun onActivityResult(requestCode: Int, resultCode: Int, data: Intent?) {
|
||||
|
@ -266,29 +270,29 @@ class LoginsFragment : PreferenceFragmentCompat(), AccountObserver {
|
|||
|
||||
private fun navigateToSavedLoginsFragment() {
|
||||
context?.components?.analytics?.metrics?.track(Event.OpenLogins)
|
||||
val directions = LoginsFragmentDirections.actionLoginsFragmentToSavedLoginsFragment()
|
||||
val directions = SavedLoginsAuthFragmentDirections.actionSavedLoginsAuthFragmentToLoginsListFragment()
|
||||
findNavController().navigate(directions)
|
||||
}
|
||||
|
||||
private fun navigateToAccountSettingsFragment() {
|
||||
val directions =
|
||||
LoginsFragmentDirections.actionGlobalAccountSettingsFragment()
|
||||
SavedLoginsAuthFragmentDirections.actionGlobalAccountSettingsFragment()
|
||||
findNavController().navigate(directions)
|
||||
}
|
||||
|
||||
private fun navigateToAccountProblemFragment() {
|
||||
val directions = LoginsFragmentDirections.actionGlobalAccountProblemFragment()
|
||||
val directions = SavedLoginsAuthFragmentDirections.actionGlobalAccountProblemFragment()
|
||||
findNavController().navigate(directions)
|
||||
}
|
||||
|
||||
private fun navigateToTurnOnSyncFragment() {
|
||||
val directions = LoginsFragmentDirections.actionLoginsFragmentToTurnOnSyncFragment()
|
||||
val directions = SavedLoginsAuthFragmentDirections.actionSavedLoginsAuthFragmentToTurnOnSyncFragment()
|
||||
findNavController().navigate(directions)
|
||||
}
|
||||
|
||||
private fun navigateToSaveLoginSettingFragment() {
|
||||
val directions =
|
||||
LoginsFragmentDirections.actionLoginsFragmentToSaveLoginSettingFragment()
|
||||
SavedLoginsAuthFragmentDirections.actionSavedLoginsAuthFragmentToSavedLoginsSettingFragment()
|
||||
findNavController().navigate(directions)
|
||||
}
|
||||
|
|
@ -1,22 +0,0 @@
|
|||
/* 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 org.mozilla.fenix.utils.Settings
|
||||
|
||||
interface SavedLoginsController {
|
||||
fun handleSort(sortingStrategy: SortingStrategy)
|
||||
}
|
||||
|
||||
class DefaultSavedLoginsController(
|
||||
val store: SavedLoginsFragmentStore,
|
||||
val settings: Settings
|
||||
) : SavedLoginsController {
|
||||
|
||||
override fun handleSort(sortingStrategy: SortingStrategy) {
|
||||
store.dispatch(SavedLoginsFragmentAction.SortLogins(sortingStrategy))
|
||||
settings.savedLoginsSortingStrategy = sortingStrategy
|
||||
}
|
||||
}
|
|
@ -46,7 +46,7 @@ import org.mozilla.fenix.settings.SupportUtils
|
|||
|
||||
@SuppressWarnings("TooManyFunctions")
|
||||
class SavedLoginsFragment : Fragment() {
|
||||
private lateinit var savedLoginsStore: SavedLoginsFragmentStore
|
||||
private lateinit var savedLoginsStore: LoginsFragmentStore
|
||||
private lateinit var savedLoginsView: SavedLoginsView
|
||||
private lateinit var savedLoginsInteractor: SavedLoginsInteractor
|
||||
private lateinit var dropDownMenuAnchorView: View
|
||||
|
@ -76,10 +76,10 @@ class SavedLoginsFragment : Fragment() {
|
|||
): View? {
|
||||
val view = inflater.inflate(R.layout.fragment_saved_logins, container, false)
|
||||
savedLoginsStore = StoreProvider.get(this) {
|
||||
SavedLoginsFragmentStore(
|
||||
SavedLoginsFragmentState(
|
||||
LoginsFragmentStore(
|
||||
LoginsListState(
|
||||
isLoading = true,
|
||||
items = listOf(),
|
||||
loginList = listOf(),
|
||||
filteredItems = listOf(),
|
||||
searchedForText = null,
|
||||
sortingStrategy = requireContext().settings().savedLoginsSortingStrategy,
|
||||
|
@ -88,7 +88,7 @@ class SavedLoginsFragment : Fragment() {
|
|||
)
|
||||
}
|
||||
val savedLoginsController: SavedLoginsController =
|
||||
DefaultSavedLoginsController(savedLoginsStore, requireContext().settings())
|
||||
SavedLoginsController(savedLoginsStore, requireContext().settings())
|
||||
savedLoginsInteractor =
|
||||
SavedLoginsInteractor(savedLoginsController, ::itemClicked, ::openLearnMore)
|
||||
savedLoginsView = SavedLoginsView(view.savedLoginsLayout, savedLoginsInteractor)
|
||||
|
@ -119,7 +119,7 @@ class SavedLoginsFragment : Fragment() {
|
|||
}
|
||||
|
||||
override fun onQueryTextChange(newText: String?): Boolean {
|
||||
savedLoginsStore.dispatch(SavedLoginsFragmentAction.FilterLogins(newText))
|
||||
savedLoginsStore.dispatch(LoginsAction.FilterLogins(newText))
|
||||
return false
|
||||
}
|
||||
})
|
||||
|
@ -134,17 +134,17 @@ class SavedLoginsFragment : Fragment() {
|
|||
(activity as HomeActivity).getSupportActionBarAndInflateIfNecessary().setDisplayShowTitleEnabled(true)
|
||||
sortingStrategyPopupMenu.dismiss()
|
||||
|
||||
if (findNavController().currentDestination?.id != R.id.savedLoginSiteInfoFragment) {
|
||||
if (findNavController().currentDestination?.id != R.id.loginDetailFragment) {
|
||||
activity?.let { it.checkAndUpdateScreenshotPermission(it.settings()) }
|
||||
findNavController().popBackStack(R.id.loginsFragment, false)
|
||||
findNavController().popBackStack(R.id.savedLoginsAuthFragment, false)
|
||||
}
|
||||
super.onPause()
|
||||
}
|
||||
|
||||
private fun itemClicked(item: SavedLoginsItem) {
|
||||
private fun itemClicked(item: SavedLogin) {
|
||||
context?.components?.analytics?.metrics?.track(Event.OpenOneLogin)
|
||||
val directions =
|
||||
SavedLoginsFragmentDirections.actionSavedLoginsFragmentToSavedLoginSiteInfoFragment(item)
|
||||
SavedLoginsFragmentDirections.actionSavedLoginsFragmentToLoginDetailFragment(item.guid)
|
||||
findNavController().navigate(directions)
|
||||
}
|
||||
|
||||
|
@ -166,9 +166,9 @@ class SavedLoginsFragment : Fragment() {
|
|||
val logins = deferredLogins?.await()
|
||||
logins?.let {
|
||||
withContext(Main) {
|
||||
savedLoginsStore.dispatch(SavedLoginsFragmentAction.UpdateLogins(logins.map { item ->
|
||||
SavedLoginsItem(item.origin, item.username, item.password, item.guid!!, item.timeLastUsed)
|
||||
}))
|
||||
savedLoginsStore.dispatch(
|
||||
LoginsAction.UpdateLoginsList(logins.map { it.mapToSavedLogin() })
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
@ -1,146 +0,0 @@
|
|||
/* 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 android.os.Parcelable
|
||||
import kotlinx.android.parcel.Parcelize
|
||||
import mozilla.components.lib.state.Action
|
||||
import mozilla.components.lib.state.State
|
||||
import mozilla.components.lib.state.Store
|
||||
|
||||
/**
|
||||
* Class representing an saved logins item
|
||||
* @property url Site of the saved login
|
||||
* @property userName Username that's saved for this site
|
||||
* @property password Password that's saved for this site
|
||||
* @property id The unique identifier for this login entry
|
||||
* @property timeLastUsed Time of last use in milliseconds from the unix epoch.
|
||||
*/
|
||||
@Parcelize
|
||||
data class SavedLoginsItem(
|
||||
val url: String,
|
||||
val userName: String?,
|
||||
val password: String?,
|
||||
val id: String,
|
||||
val timeLastUsed: Long
|
||||
) :
|
||||
Parcelable
|
||||
|
||||
/**
|
||||
* The [Store] for holding the [SavedLoginsFragmentState] and applying [SavedLoginsFragmentAction]s.
|
||||
*/
|
||||
class SavedLoginsFragmentStore(initialState: SavedLoginsFragmentState) :
|
||||
Store<SavedLoginsFragmentState, SavedLoginsFragmentAction>(
|
||||
initialState,
|
||||
::savedLoginsStateReducer
|
||||
)
|
||||
|
||||
/**
|
||||
* Actions to dispatch through the `SavedLoginsStore` to modify `SavedLoginsFragmentState` through the reducer.
|
||||
*/
|
||||
sealed class SavedLoginsFragmentAction : Action {
|
||||
data class FilterLogins(val newText: String?) : SavedLoginsFragmentAction()
|
||||
data class UpdateLogins(val list: List<SavedLoginsItem>) : SavedLoginsFragmentAction()
|
||||
data class SortLogins(val sortingStrategy: SortingStrategy) : SavedLoginsFragmentAction()
|
||||
}
|
||||
|
||||
/**
|
||||
* The state for the Saved Logins Screen
|
||||
* @property isLoading State to know when to show loading
|
||||
* @property items Source of truth of list of logins
|
||||
* @property filteredItems Filtered (or not) list of logins to display
|
||||
* @property searchedForText String used by the user to filter logins
|
||||
* @property sortingStrategy sorting strategy selected by the user (Currently we support
|
||||
* sorting alphabetically and by last used)
|
||||
*/
|
||||
data class SavedLoginsFragmentState(
|
||||
val isLoading: Boolean = false,
|
||||
val items: List<SavedLoginsItem>,
|
||||
val filteredItems: List<SavedLoginsItem>,
|
||||
val searchedForText: String?,
|
||||
val sortingStrategy: SortingStrategy,
|
||||
val highlightedItem: SavedLoginsSortingStrategyMenu.Item
|
||||
) : State
|
||||
|
||||
/**
|
||||
* The SavedLoginsState Reducer.
|
||||
*/
|
||||
private fun savedLoginsStateReducer(
|
||||
state: SavedLoginsFragmentState,
|
||||
action: SavedLoginsFragmentAction
|
||||
): SavedLoginsFragmentState {
|
||||
return when (action) {
|
||||
is SavedLoginsFragmentAction.UpdateLogins -> {
|
||||
filterItems(
|
||||
state.searchedForText, state.sortingStrategy, state.copy(
|
||||
isLoading = false,
|
||||
items = action.list,
|
||||
filteredItems = emptyList()
|
||||
)
|
||||
)
|
||||
}
|
||||
is SavedLoginsFragmentAction.FilterLogins ->
|
||||
filterItems(
|
||||
action.newText,
|
||||
state.sortingStrategy,
|
||||
state
|
||||
)
|
||||
is SavedLoginsFragmentAction.SortLogins ->
|
||||
filterItems(
|
||||
state.searchedForText,
|
||||
action.sortingStrategy,
|
||||
state
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* @return [SavedLoginsFragmentState] containing a new [SavedLoginsFragmentState.filteredItems]
|
||||
* with filtered [SavedLoginsFragmentState.items]
|
||||
*
|
||||
* @param searchedForText based on which [SavedLoginsFragmentState.items] will be filtered.
|
||||
* @param sortingStrategy based on which [SavedLoginsFragmentState.items] will be sorted.
|
||||
* @param state previous [SavedLoginsFragmentState] containing all the other properties
|
||||
* with which a new state will be created
|
||||
*/
|
||||
private fun filterItems(
|
||||
searchedForText: String?,
|
||||
sortingStrategy: SortingStrategy,
|
||||
state: SavedLoginsFragmentState
|
||||
): SavedLoginsFragmentState {
|
||||
return if (searchedForText.isNullOrBlank()) {
|
||||
state.copy(
|
||||
isLoading = false,
|
||||
sortingStrategy = sortingStrategy,
|
||||
highlightedItem = sortingStrategyToMenuItem(sortingStrategy),
|
||||
searchedForText = searchedForText,
|
||||
filteredItems = sortingStrategy(state.items)
|
||||
)
|
||||
} else {
|
||||
state.copy(
|
||||
isLoading = false,
|
||||
sortingStrategy = sortingStrategy,
|
||||
highlightedItem = sortingStrategyToMenuItem(sortingStrategy),
|
||||
searchedForText = searchedForText,
|
||||
filteredItems = sortingStrategy(state.items).filter {
|
||||
it.url.contains(
|
||||
searchedForText
|
||||
)
|
||||
}
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
private fun sortingStrategyToMenuItem(sortingStrategy: SortingStrategy): SavedLoginsSortingStrategyMenu.Item {
|
||||
return when (sortingStrategy) {
|
||||
is SortingStrategy.Alphabetically -> {
|
||||
SavedLoginsSortingStrategyMenu.Item.AlphabeticallySort
|
||||
}
|
||||
|
||||
is SortingStrategy.LastUsed -> {
|
||||
SavedLoginsSortingStrategyMenu.Item.LastUsedSort
|
||||
}
|
||||
}
|
||||
}
|
|
@ -1,26 +0,0 @@
|
|||
/* 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 for the saved logins screen
|
||||
* Provides implementations for the SavedLoginsViewInteractor
|
||||
*/
|
||||
class SavedLoginsInteractor(
|
||||
private val savedLoginsController: SavedLoginsController,
|
||||
private val itemClicked: (SavedLoginsItem) -> Unit,
|
||||
private val learnMore: () -> Unit
|
||||
) : SavedLoginsViewInteractor {
|
||||
override fun itemClicked(item: SavedLoginsItem) {
|
||||
itemClicked.invoke(item)
|
||||
}
|
||||
override fun onLearnMore() {
|
||||
learnMore.invoke()
|
||||
}
|
||||
|
||||
override fun sort(sortingStrategy: SortingStrategy) {
|
||||
savedLoginsController.handleSort(sortingStrategy)
|
||||
}
|
||||
}
|
|
@ -15,7 +15,7 @@ import org.mozilla.fenix.ext.showToolbar
|
|||
import org.mozilla.fenix.settings.RadioButtonPreference
|
||||
import org.mozilla.fenix.settings.SharedPreferenceUpdater
|
||||
|
||||
class SaveLoginSettingFragment : PreferenceFragmentCompat() {
|
||||
class SavedLoginsSettingFragment : PreferenceFragmentCompat() {
|
||||
override fun onCreatePreferences(savedInstanceState: Bundle?, rootKey: String?) {
|
||||
setPreferencesFromResource(R.xml.save_logins_preferences, rootKey)
|
||||
}
|
|
@ -16,21 +16,7 @@ import kotlinx.android.extensions.LayoutContainer
|
|||
import kotlinx.android.synthetic.main.component_saved_logins.view.*
|
||||
import kotlinx.android.synthetic.main.component_saved_logins.view.progress_bar
|
||||
import org.mozilla.fenix.R
|
||||
|
||||
/**
|
||||
* Interface for the SavedLoginsViewInteractor. This interface is implemented by objects that want
|
||||
* to respond to user interaction on the SavedLoginsView
|
||||
*/
|
||||
interface SavedLoginsViewInteractor {
|
||||
/**
|
||||
* Called whenever one item is clicked
|
||||
*/
|
||||
fun itemClicked(item: SavedLoginsItem)
|
||||
|
||||
fun onLearnMore()
|
||||
|
||||
fun sort(sortingStrategy: SortingStrategy)
|
||||
}
|
||||
import org.mozilla.fenix.utils.Settings
|
||||
|
||||
/**
|
||||
* View that contains and configures the Saved Logins List
|
||||
|
@ -44,7 +30,7 @@ class SavedLoginsView(
|
|||
.inflate(R.layout.component_saved_logins, containerView, true)
|
||||
.findViewById(R.id.saved_logins_wrapper)
|
||||
|
||||
private val loginsAdapter = SavedLoginsAdapter(interactor)
|
||||
private val loginsAdapter = LoginsAdapter(interactor)
|
||||
|
||||
init {
|
||||
view.saved_logins_list.apply {
|
||||
|
@ -65,21 +51,51 @@ class SavedLoginsView(
|
|||
|
||||
with(view.saved_passwords_empty_message) {
|
||||
val appName = context.getString(R.string.app_name)
|
||||
text = context.getString(
|
||||
R.string.preferences_passwords_saved_logins_description_empty_text,
|
||||
appName
|
||||
text = String.format(
|
||||
context.getString(
|
||||
R.string.preferences_passwords_saved_logins_description_empty_text
|
||||
), appName
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
fun update(state: SavedLoginsFragmentState) {
|
||||
fun update(state: LoginsListState) {
|
||||
if (state.isLoading) {
|
||||
view.progress_bar.isVisible = true
|
||||
} else {
|
||||
view.progress_bar.isVisible = false
|
||||
view.saved_logins_list.isVisible = state.items.isNotEmpty()
|
||||
view.saved_passwords_empty_view.isVisible = state.items.isEmpty()
|
||||
view.saved_logins_list.isVisible = state.loginList.isNotEmpty()
|
||||
view.saved_passwords_empty_view.isVisible = state.loginList.isEmpty()
|
||||
}
|
||||
loginsAdapter.submitList(state.filteredItems)
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Interactor for the saved logins screen
|
||||
*/
|
||||
class SavedLoginsInteractor(
|
||||
private val savedLoginsController: SavedLoginsController,
|
||||
private val itemClicked: (SavedLogin) -> Unit,
|
||||
private val learnMore: () -> Unit
|
||||
) {
|
||||
fun itemClicked(item: SavedLogin) {
|
||||
itemClicked.invoke(item)
|
||||
}
|
||||
fun onLearnMore() {
|
||||
learnMore.invoke()
|
||||
}
|
||||
fun sort(sortingStrategy: SortingStrategy) {
|
||||
savedLoginsController.handleSort(sortingStrategy)
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Controller for the saved logins screen
|
||||
*/
|
||||
class SavedLoginsController(val store: LoginsFragmentStore, val settings: Settings) {
|
||||
fun handleSort(sortingStrategy: SortingStrategy) {
|
||||
store.dispatch(LoginsAction.SortLogins(sortingStrategy))
|
||||
settings.savedLoginsSortingStrategy = sortingStrategy
|
||||
}
|
||||
}
|
||||
|
|
|
@ -8,17 +8,17 @@ import android.content.Context
|
|||
import org.mozilla.fenix.ext.urlToTrimmedHost
|
||||
|
||||
sealed class SortingStrategy {
|
||||
abstract operator fun invoke(logins: List<SavedLoginsItem>): List<SavedLoginsItem>
|
||||
abstract operator fun invoke(logins: List<SavedLogin>): List<SavedLogin>
|
||||
abstract val appContext: Context
|
||||
|
||||
data class Alphabetically(override val appContext: Context) : SortingStrategy() {
|
||||
override fun invoke(logins: List<SavedLoginsItem>): List<SavedLoginsItem> {
|
||||
return logins.sortedBy { it.url.urlToTrimmedHost(appContext) }
|
||||
override fun invoke(logins: List<SavedLogin>): List<SavedLogin> {
|
||||
return logins.sortedBy { it.origin.urlToTrimmedHost(appContext) }
|
||||
}
|
||||
}
|
||||
|
||||
data class LastUsed(override val appContext: Context) : SortingStrategy() {
|
||||
override fun invoke(logins: List<SavedLoginsItem>): List<SavedLoginsItem> {
|
||||
override fun invoke(logins: List<SavedLogin>): List<SavedLogin> {
|
||||
return logins.sortedByDescending { it.timeLastUsed }
|
||||
}
|
||||
}
|
||||
|
|
|
@ -0,0 +1,16 @@
|
|||
<!--
|
||||
~ 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/.
|
||||
-->
|
||||
|
||||
<vector xmlns:android="http://schemas.android.com/apk/res/android"
|
||||
android:width="24dp"
|
||||
android:height="24dp"
|
||||
android:viewportWidth="24"
|
||||
android:viewportHeight="24"
|
||||
android:tint="#FFFFFF">
|
||||
<path
|
||||
android:fillColor="#FF000000"
|
||||
android:pathData="M12,8c1.1,0 2,-0.9 2,-2s-0.9,-2 -2,-2 -2,0.9 -2,2 0.9,2 2,2zM12,10c-1.1,0 -2,0.9 -2,2s0.9,2 2,2 2,-0.9 2,-2 -0.9,-2 -2,-2zM12,16c-1.1,0 -2,0.9 -2,2s0.9,2 2,2 2,-0.9 2,-2 -0.9,-2 -2,-2z"/>
|
||||
</vector>
|
|
@ -26,7 +26,7 @@
|
|||
app:layout_constraintTop_toTopOf="parent" />
|
||||
|
||||
<TextView
|
||||
android:id="@+id/domainView"
|
||||
android:id="@+id/webAddressView"
|
||||
android:layout_width="0dp"
|
||||
android:layout_height="wrap_content"
|
||||
android:layout_marginEnd="12dp"
|
||||
|
|
|
@ -0,0 +1,227 @@
|
|||
<?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/editLoginLayout"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="wrap_content"
|
||||
android:layout_marginStart="72dp"
|
||||
android:layout_marginEnd="20dp"
|
||||
android:layout_marginTop="12dp" >
|
||||
|
||||
<TextView
|
||||
android:id="@+id/hostnameHeaderText"
|
||||
android:layout_width="0dp"
|
||||
android:layout_height="wrap_content"
|
||||
android:paddingStart="3dp"
|
||||
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"
|
||||
tools:ignore="RtlSymmetry" />
|
||||
|
||||
<com.google.android.material.textfield.TextInputLayout
|
||||
android:id="@+id/inputLayoutHostname"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="wrap_content"
|
||||
android:clickable="false"
|
||||
android:focusable="false"
|
||||
android:background="@android:color/transparent"
|
||||
android:textColor="?primaryText"
|
||||
android:layout_marginTop="12dp"
|
||||
android:layout_marginStart="3dp"
|
||||
android:contentDescription="@string/saved_login_hostname_description"
|
||||
app:backgroundTint="@android:color/transparent"
|
||||
app:hintEnabled="false"
|
||||
app:layout_constraintEnd_toEndOf="@id/hostnameHeaderText"
|
||||
app:layout_constraintStart_toStartOf="@id/hostnameHeaderText"
|
||||
app:layout_constraintTop_toBottomOf="@id/hostnameHeaderText"
|
||||
tools:ignore="RtlSymmetry">
|
||||
|
||||
<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:textColor="?disabled"
|
||||
android:letterSpacing="0.01"
|
||||
android:lineSpacingExtra="8sp"
|
||||
android:hint="@string/saved_login_hostname_description"
|
||||
android:inputType="textNoSuggestions"
|
||||
android:ellipsize="end"
|
||||
android:maxLines="1"
|
||||
android:singleLine="true"
|
||||
android:clickable="false"
|
||||
android:focusable="false"
|
||||
android:textCursorDrawable="@android:color/transparent"
|
||||
android:background="@android:color/transparent"
|
||||
app:backgroundTint="@android:color/transparent"
|
||||
tools:ignore="Autofill"/>
|
||||
</com.google.android.material.textfield.TextInputLayout>
|
||||
|
||||
<TextView
|
||||
android:id="@+id/usernameHeader"
|
||||
android:layout_width="0dp"
|
||||
android:layout_height="16dp"
|
||||
android:gravity="center_vertical"
|
||||
android:paddingStart="3dp"
|
||||
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"
|
||||
tools:ignore="RtlSymmetry" />
|
||||
|
||||
<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"
|
||||
tools:ignore="RtlSymmetry">
|
||||
|
||||
<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="?primaryText"
|
||||
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_marginBottom="10dp"
|
||||
android:background="?android:attr/selectableItemBackgroundBorderless"
|
||||
android:contentDescription="@string/saved_login_copy_username"
|
||||
app:tint="?android:colorAccent"
|
||||
app:layout_constraintEnd_toEndOf="parent"
|
||||
app:layout_constraintBottom_toBottomOf="@id/inputLayoutUsername"
|
||||
app:srcCompat="@drawable/ic_clear" />
|
||||
|
||||
<TextView
|
||||
android:id="@+id/passwordHeader"
|
||||
android:layout_width="0dp"
|
||||
android:layout_height="wrap_content"
|
||||
android:gravity="center_vertical"
|
||||
android:paddingStart="3dp"
|
||||
android:text="@string/preferences_passwords_saved_logins_password"
|
||||
android:textColor="?primaryText"
|
||||
android:textSize="12sp"
|
||||
android:layout_marginTop="10dp"
|
||||
android:letterSpacing="0.05"
|
||||
app:fontFamily="@font/metropolis_semibold"
|
||||
app:layout_constraintBottom_toTopOf="@id/inputLayoutPassword"
|
||||
app:layout_constraintEnd_toStartOf="@id/revealPasswordButton"
|
||||
app:layout_constraintStart_toStartOf="parent"
|
||||
app:layout_constraintTop_toBottomOf="@id/inputLayoutUsername"
|
||||
app:layout_constraintVertical_chainStyle="packed"
|
||||
tools:ignore="RtlSymmetry" />
|
||||
|
||||
<com.google.android.material.textfield.TextInputLayout
|
||||
android:id="@+id/inputLayoutPassword"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="wrap_content"
|
||||
android:paddingBottom="11dp"
|
||||
android:colorControlHighlight="?primaryText"
|
||||
android:colorControlActivated="?primaryText"
|
||||
android:textColor="?primaryText"
|
||||
android:contentDescription="@string/saved_login_password_description"
|
||||
app:hintEnabled="false"
|
||||
app:layout_constraintBottom_toBottomOf="parent"
|
||||
app:layout_constraintEnd_toEndOf="@id/passwordHeader"
|
||||
app:layout_constraintStart_toStartOf="@id/passwordHeader"
|
||||
app:layout_constraintTop_toBottomOf="@id/passwordHeader"
|
||||
app:layout_constraintVertical_chainStyle="packed"
|
||||
tools:ignore="RtlSymmetry">
|
||||
|
||||
<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="?primaryText"
|
||||
android:textSize="16sp"
|
||||
android:textStyle="normal"
|
||||
app:backgroundTint="?primaryText"
|
||||
tools:ignore="Autofill" />
|
||||
</com.google.android.material.textfield.TextInputLayout>
|
||||
|
||||
<ImageButton
|
||||
android:id="@+id/revealPasswordButton"
|
||||
android:layout_width="48dp"
|
||||
android:layout_height="30dp"
|
||||
android:layout_marginTop="3dp"
|
||||
android:background="?android:attr/selectableItemBackgroundBorderless"
|
||||
android:contentDescription="@string/saved_login_reveal_password"
|
||||
app:tint="?android:colorAccent"
|
||||
app:layout_constraintEnd_toStartOf="@id/clearPasswordTextButton"
|
||||
app:layout_constraintTop_toTopOf="@id/inputLayoutPassword"
|
||||
app:srcCompat="@drawable/mozac_ic_password_reveal" />
|
||||
|
||||
<ImageButton
|
||||
android:id="@+id/clearPasswordTextButton"
|
||||
android:layout_width="48dp"
|
||||
android:layout_height="30dp"
|
||||
android:background="?android:attr/selectableItemBackgroundBorderless"
|
||||
android:contentDescription="@string/saved_logins_copy_password"
|
||||
app:tint="?android:colorAccent"
|
||||
app:layout_constraintEnd_toEndOf="parent"
|
||||
app:layout_constraintTop_toTopOf="@id/revealPasswordButton"
|
||||
app:srcCompat="@drawable/ic_clear" />
|
||||
</androidx.constraintlayout.widget.ConstraintLayout>
|
|
@ -0,0 +1,159 @@
|
|||
<?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/loginDetailLayout"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="wrap_content"
|
||||
android:layout_marginStart="73dp"
|
||||
android:layout_marginTop="12dp"
|
||||
android:layout_marginEnd="20dp">
|
||||
|
||||
<TextView
|
||||
android:id="@+id/webAddressHeader"
|
||||
android:layout_width="0dp"
|
||||
android:layout_height="24dp"
|
||||
android:gravity="center_vertical"
|
||||
android:letterSpacing="0.05"
|
||||
android:paddingBottom="5dp"
|
||||
android:text="@string/preferences_passwords_saved_logins_site"
|
||||
android:textColor="?primaryText"
|
||||
android:textSize="12sp"
|
||||
app:fontFamily="@font/metropolis_semibold"
|
||||
app:layout_constraintBottom_toTopOf="@id/webAddressText"
|
||||
app:layout_constraintEnd_toStartOf="@id/copyWebAddress"
|
||||
app:layout_constraintStart_toStartOf="parent"
|
||||
app:layout_constraintTop_toTopOf="parent"
|
||||
app:layout_constraintVertical_chainStyle="packed" />
|
||||
|
||||
<TextView
|
||||
android:id="@+id/webAddressText"
|
||||
android:layout_width="0dp"
|
||||
android:layout_height="30dp"
|
||||
android:gravity="center_vertical"
|
||||
android:textColor="?primaryText"
|
||||
android:textSize="16sp"
|
||||
app:layout_constraintEnd_toEndOf="@id/webAddressHeader"
|
||||
app:layout_constraintStart_toStartOf="@id/webAddressHeader"
|
||||
app:layout_constraintTop_toBottomOf="@id/webAddressHeader"
|
||||
app:layout_constraintVertical_chainStyle="packed"
|
||||
tools:text="Info" />
|
||||
|
||||
<ImageButton
|
||||
android:id="@+id/copyWebAddress"
|
||||
android:layout_width="48dp"
|
||||
android:layout_height="48dp"
|
||||
android:background="?android:attr/selectableItemBackgroundBorderless"
|
||||
android:contentDescription="@string/saved_login_copy_site"
|
||||
app:layout_constraintBottom_toBottomOf="@id/webAddressText"
|
||||
app:layout_constraintEnd_toEndOf="parent"
|
||||
app:layout_constraintTop_toTopOf="@id/webAddressText"
|
||||
app:srcCompat="@drawable/ic_copy"
|
||||
app:tint="?android:colorAccent" />
|
||||
|
||||
<TextView
|
||||
android:id="@+id/usernameHeader"
|
||||
android:layout_width="0dp"
|
||||
android:layout_height="24dp"
|
||||
android:layout_marginTop="16dp"
|
||||
android:gravity="center_vertical"
|
||||
android:letterSpacing="0.05"
|
||||
android:text="@string/preferences_passwords_saved_logins_username"
|
||||
android:textColor="?primaryText"
|
||||
android:textSize="12sp"
|
||||
app:fontFamily="@font/metropolis_semibold"
|
||||
app:layout_constraintBottom_toTopOf="@id/usernameText"
|
||||
app:layout_constraintEnd_toStartOf="@id/copyUsername"
|
||||
app:layout_constraintStart_toStartOf="parent"
|
||||
app:layout_constraintTop_toBottomOf="@id/webAddressText"
|
||||
app:layout_constraintVertical_chainStyle="packed" />
|
||||
|
||||
<TextView
|
||||
android:id="@+id/usernameText"
|
||||
android:layout_width="0dp"
|
||||
android:layout_height="30dp"
|
||||
android:gravity="center_vertical"
|
||||
android:textColor="?primaryText"
|
||||
android:textSize="16sp"
|
||||
android:layout_marginTop="1dp"
|
||||
app:layout_constraintEnd_toEndOf="@id/usernameHeader"
|
||||
app:layout_constraintStart_toStartOf="@id/usernameHeader"
|
||||
app:layout_constraintTop_toBottomOf="@id/usernameHeader"
|
||||
app:layout_constraintVertical_chainStyle="packed"
|
||||
tools:text="Info" />
|
||||
|
||||
<ImageButton
|
||||
android:id="@+id/copyUsername"
|
||||
android:layout_width="48dp"
|
||||
android:layout_height="48dp"
|
||||
android:background="?android:attr/selectableItemBackgroundBorderless"
|
||||
android:contentDescription="@string/saved_login_copy_username"
|
||||
app:layout_constraintBottom_toBottomOf="@id/usernameText"
|
||||
app:layout_constraintEnd_toEndOf="parent"
|
||||
app:layout_constraintTop_toTopOf="@id/usernameText"
|
||||
app:srcCompat="@drawable/ic_copy"
|
||||
app:tint="?android:colorAccent" />
|
||||
|
||||
<TextView
|
||||
android:id="@+id/passwordHeader"
|
||||
android:layout_width="0dp"
|
||||
android:layout_height="24dp"
|
||||
android:gravity="center_vertical"
|
||||
android:letterSpacing="0.05"
|
||||
android:text="@string/preferences_passwords_saved_logins_password"
|
||||
android:textColor="?primaryText"
|
||||
android:textSize="12sp"
|
||||
android:layout_marginTop="12dp"
|
||||
app:fontFamily="@font/metropolis_semibold"
|
||||
app:layout_constraintBottom_toTopOf="@id/passwordText"
|
||||
app:layout_constraintEnd_toStartOf="@id/revealPasswordButton"
|
||||
app:layout_constraintStart_toStartOf="parent"
|
||||
app:layout_constraintTop_toBottomOf="@id/usernameText"
|
||||
app:layout_constraintVertical_chainStyle="packed" />
|
||||
|
||||
<TextView
|
||||
android:id="@+id/passwordText"
|
||||
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"
|
||||
android:textColor="?primaryText"
|
||||
android:textSize="16sp"
|
||||
app:layout_constraintBottom_toBottomOf="parent"
|
||||
app:layout_constraintEnd_toEndOf="@id/passwordHeader"
|
||||
app:layout_constraintStart_toStartOf="@id/passwordHeader"
|
||||
app:layout_constraintTop_toBottomOf="@id/passwordHeader"
|
||||
app:layout_constraintVertical_chainStyle="packed"
|
||||
tools:text="Info" />
|
||||
|
||||
<ImageButton
|
||||
android:id="@+id/revealPasswordButton"
|
||||
android:layout_width="48dp"
|
||||
android:layout_height="30dp"
|
||||
android:layout_marginBottom="2dp"
|
||||
android:background="?android:attr/selectableItemBackgroundBorderless"
|
||||
android:contentDescription="@string/saved_login_reveal_password"
|
||||
app:layout_constraintBottom_toBottomOf="@id/passwordText"
|
||||
app:layout_constraintEnd_toStartOf="@id/copyPassword"
|
||||
app:srcCompat="@drawable/mozac_ic_password_reveal"
|
||||
app:tint="?android:colorAccent" />
|
||||
|
||||
<ImageButton
|
||||
android:id="@+id/copyPassword"
|
||||
android:layout_width="48dp"
|
||||
android:layout_height="30dp"
|
||||
android:background="?android:attr/selectableItemBackgroundBorderless"
|
||||
android:contentDescription="@string/saved_logins_copy_password"
|
||||
app:layout_constraintBottom_toBottomOf="@id/revealPasswordButton"
|
||||
app:layout_constraintEnd_toEndOf="parent"
|
||||
app:layout_constraintTop_toTopOf="@id/revealPasswordButton"
|
||||
app:srcCompat="@drawable/ic_copy"
|
||||
app:tint="?android:colorAccent" />
|
||||
</androidx.constraintlayout.widget.ConstraintLayout>
|
|
@ -1,134 +0,0 @@
|
|||
<?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/siteLayout"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="wrap_content"
|
||||
android:layout_marginStart="66dp"
|
||||
android:layout_marginTop="12dp">
|
||||
|
||||
<TextView
|
||||
android:id="@+id/siteHeaderText"
|
||||
android:layout_width="0dp"
|
||||
android:layout_height="wrap_content"
|
||||
android:text="@string/preferences_passwords_saved_logins_site"
|
||||
android:textColor="?primaryText"
|
||||
android:textSize="12sp"
|
||||
app:layout_constraintBottom_toTopOf="@id/siteInfoText"
|
||||
app:layout_constraintEnd_toStartOf="@id/copySiteItem"
|
||||
app:layout_constraintStart_toStartOf="parent"
|
||||
app:layout_constraintTop_toTopOf="parent"
|
||||
app:layout_constraintVertical_chainStyle="packed" />
|
||||
|
||||
<TextView
|
||||
android:id="@+id/siteInfoText"
|
||||
android:layout_width="0dp"
|
||||
android:layout_height="wrap_content"
|
||||
android:textSize="16sp"
|
||||
app:layout_constraintEnd_toEndOf="@id/siteHeaderText"
|
||||
app:layout_constraintStart_toStartOf="@id/siteHeaderText"
|
||||
app:layout_constraintTop_toBottomOf="@id/siteHeaderText"
|
||||
app:layout_constraintVertical_chainStyle="packed"
|
||||
tools:text="Info" />
|
||||
|
||||
<ImageButton
|
||||
android:id="@+id/copySiteItem"
|
||||
android:layout_width="48dp"
|
||||
android:layout_height="48dp"
|
||||
android:background="?android:attr/selectableItemBackgroundBorderless"
|
||||
android:contentDescription="@string/saved_login_copy_site"
|
||||
app:layout_constraintBottom_toBottomOf="@id/siteInfoText"
|
||||
app:layout_constraintEnd_toEndOf="parent"
|
||||
app:layout_constraintTop_toTopOf="@id/siteHeaderText"
|
||||
app:srcCompat="@drawable/ic_copy" />
|
||||
|
||||
<TextView
|
||||
android:id="@+id/usernameHeaderText"
|
||||
android:layout_width="0dp"
|
||||
android:layout_height="wrap_content"
|
||||
android:layout_marginTop="12dp"
|
||||
android:text="@string/preferences_passwords_saved_logins_username"
|
||||
android:textColor="?primaryText"
|
||||
android:textSize="12sp"
|
||||
app:layout_constraintBottom_toTopOf="@id/usernameInfoText"
|
||||
app:layout_constraintEnd_toStartOf="@id/copyUsernameItem"
|
||||
app:layout_constraintStart_toStartOf="parent"
|
||||
app:layout_constraintTop_toBottomOf="@id/siteInfoText"
|
||||
app:layout_constraintVertical_chainStyle="packed" />
|
||||
|
||||
<TextView
|
||||
android:id="@+id/usernameInfoText"
|
||||
android:layout_width="0dp"
|
||||
android:layout_height="wrap_content"
|
||||
android:textSize="16sp"
|
||||
app:layout_constraintEnd_toEndOf="@id/usernameHeaderText"
|
||||
app:layout_constraintStart_toStartOf="@id/usernameHeaderText"
|
||||
app:layout_constraintTop_toBottomOf="@id/usernameHeaderText"
|
||||
app:layout_constraintVertical_chainStyle="packed"
|
||||
tools:text="Info" />
|
||||
|
||||
<ImageButton
|
||||
android:id="@+id/copyUsernameItem"
|
||||
android:layout_width="48dp"
|
||||
android:layout_height="48dp"
|
||||
android:background="?android:attr/selectableItemBackgroundBorderless"
|
||||
android:contentDescription="@string/saved_login_copy_username"
|
||||
app:layout_constraintBottom_toBottomOf="@id/usernameInfoText"
|
||||
app:layout_constraintEnd_toEndOf="parent"
|
||||
app:layout_constraintTop_toTopOf="@id/usernameHeaderText"
|
||||
app:srcCompat="@drawable/ic_copy" />
|
||||
|
||||
<TextView
|
||||
android:id="@+id/passwordHeaderText"
|
||||
android:layout_width="0dp"
|
||||
android:layout_height="wrap_content"
|
||||
android:layout_marginTop="12dp"
|
||||
android:text="@string/preferences_passwords_saved_logins_password"
|
||||
android:textColor="?primaryText"
|
||||
android:textSize="12sp"
|
||||
app:layout_constraintBottom_toTopOf="@id/passwordInfoText"
|
||||
app:layout_constraintEnd_toStartOf="@id/revealPasswordItem"
|
||||
app:layout_constraintStart_toStartOf="parent"
|
||||
app:layout_constraintTop_toBottomOf="@id/usernameInfoText"
|
||||
app:layout_constraintVertical_chainStyle="packed" />
|
||||
|
||||
<TextView
|
||||
android:id="@+id/passwordInfoText"
|
||||
android:layout_width="0dp"
|
||||
android:layout_height="wrap_content"
|
||||
android:inputType="textPassword|text"
|
||||
android:textSize="16sp"
|
||||
app:layout_constraintBottom_toBottomOf="parent"
|
||||
app:layout_constraintEnd_toEndOf="@id/passwordHeaderText"
|
||||
app:layout_constraintStart_toStartOf="@id/passwordHeaderText"
|
||||
app:layout_constraintTop_toBottomOf="@id/passwordHeaderText"
|
||||
app:layout_constraintVertical_chainStyle="packed"
|
||||
tools:text="Info" />
|
||||
|
||||
<ImageButton
|
||||
android:id="@+id/revealPasswordItem"
|
||||
android:layout_width="48dp"
|
||||
android:layout_height="48dp"
|
||||
android:layout_marginEnd="16dp"
|
||||
android:background="?android:attr/selectableItemBackgroundBorderless"
|
||||
android:contentDescription="@string/saved_login_reveal_password"
|
||||
android:tint="?android:colorAccent"
|
||||
app:layout_constraintBottom_toBottomOf="@id/passwordInfoText"
|
||||
app:layout_constraintEnd_toStartOf="@id/copyPasswordItem"
|
||||
app:layout_constraintTop_toTopOf="@id/passwordHeaderText"
|
||||
app:srcCompat="@drawable/mozac_ic_password_reveal" />
|
||||
|
||||
<ImageButton
|
||||
android:id="@+id/copyPasswordItem"
|
||||
android:layout_width="48dp"
|
||||
android:layout_height="48dp"
|
||||
android:background="?android:attr/selectableItemBackgroundBorderless"
|
||||
android:contentDescription="@string/saved_logins_copy_password"
|
||||
app:layout_constraintBottom_toBottomOf="@id/revealPasswordItem"
|
||||
app:layout_constraintEnd_toEndOf="parent"
|
||||
app:layout_constraintTop_toTopOf="@id/revealPasswordItem"
|
||||
app:srcCompat="@drawable/ic_copy" />
|
||||
</androidx.constraintlayout.widget.ConstraintLayout>
|
|
@ -1,7 +1,11 @@
|
|||
<?xml version="1.0" encoding="utf-8"?><!-- This Source Code Form is subject to the terms of the Mozilla Public
|
||||
<?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"
|
||||
|
||||
<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:layout_width="match_parent"
|
||||
|
@ -25,7 +29,7 @@
|
|||
app:layout_constraintTop_toTopOf="parent" />
|
||||
|
||||
<TextView
|
||||
android:id="@+id/domainView"
|
||||
android:id="@+id/webAddressView"
|
||||
android:layout_width="0dp"
|
||||
android:layout_height="wrap_content"
|
||||
android:layout_marginEnd="12dp"
|
||||
|
@ -36,7 +40,7 @@
|
|||
android:singleLine="true"
|
||||
android:textAppearance="?android:attr/textAppearanceListItem"
|
||||
android:textColor="?primaryText"
|
||||
app:layout_constraintBottom_toTopOf="@id/userView"
|
||||
app:layout_constraintBottom_toTopOf="@id/usernameView"
|
||||
app:layout_constraintEnd_toEndOf="parent"
|
||||
app:layout_constraintStart_toEndOf="@+id/favicon_image"
|
||||
app:layout_constraintTop_toTopOf="parent"
|
||||
|
@ -44,7 +48,7 @@
|
|||
tools:text="mozilla.org" />
|
||||
|
||||
<TextView
|
||||
android:id="@+id/userView"
|
||||
android:id="@+id/usernameView"
|
||||
android:layout_width="0dp"
|
||||
android:layout_height="wrap_content"
|
||||
android:layout_marginEnd="12dp"
|
||||
|
@ -57,7 +61,7 @@
|
|||
app:layout_constraintBottom_toBottomOf="parent"
|
||||
app:layout_constraintEnd_toEndOf="parent"
|
||||
app:layout_constraintStart_toEndOf="@+id/favicon_image"
|
||||
app:layout_constraintTop_toBottomOf="@id/domainView"
|
||||
app:layout_constraintTop_toBottomOf="@id/webAddressView"
|
||||
app:layout_constraintVertical_chainStyle="packed"
|
||||
tools:text="mozilla.org" />
|
||||
</androidx.constraintlayout.widget.ConstraintLayout>
|
||||
|
|
|
@ -1,9 +1,12 @@
|
|||
<?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/. -->
|
||||
|
||||
<menu xmlns:android="http://schemas.android.com/apk/res/android"
|
||||
xmlns:app="http://schemas.android.com/apk/res-auto">
|
||||
xmlns:app="http://schemas.android.com/apk/res-auto"
|
||||
android:id="@+id/login_delete" >
|
||||
<item
|
||||
android:id="@+id/delete_login_button"
|
||||
android:contentDescription="@string/login_menu_delete_button"
|
|
@ -0,0 +1,35 @@
|
|||
<?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/. -->
|
||||
|
||||
<menu xmlns:android="http://schemas.android.com/apk/res/android"
|
||||
xmlns:app="http://schemas.android.com/apk/res-auto"
|
||||
android:id="@+id/loginOptionsEditDelete"
|
||||
android:contentDescription="@string/login_options_menu"
|
||||
android:title="@string/login_options_menu"
|
||||
app:showAsAction="never" >
|
||||
|
||||
<item
|
||||
android:id="@+id/edit_login_button"
|
||||
android:contentDescription="@string/login_menu_delete_button"
|
||||
android:icon="@drawable/ic_edit"
|
||||
android:minWidth="82dp"
|
||||
android:padding="20dp"
|
||||
android:paddingStart="20dp"
|
||||
android:title="@string/login_menu_edit_button"
|
||||
app:iconTint="?primaryText"
|
||||
app:showAsAction="never" />
|
||||
|
||||
<item
|
||||
android:id="@+id/delete_login_button"
|
||||
android:contentDescription="@string/login_menu_delete_button"
|
||||
android:icon="@drawable/ic_delete"
|
||||
android:minWidth="82dp"
|
||||
android:padding="20dp"
|
||||
android:paddingStart="20dp"
|
||||
android:title="@string/login_menu_delete_button"
|
||||
app:iconTint="?primaryText"
|
||||
app:showAsAction="never" />
|
||||
</menu>
|
|
@ -0,0 +1,14 @@
|
|||
<?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/. -->
|
||||
<menu xmlns:android="http://schemas.android.com/apk/res/android"
|
||||
xmlns:app="http://schemas.android.com/apk/res-auto"
|
||||
android:id="@+id/login_save">
|
||||
<item
|
||||
android:id="@+id/save_login_button"
|
||||
android:icon="@drawable/mozac_ic_check"
|
||||
app:iconTint="?primaryText"
|
||||
android:title="@string/save_changes_to_login"
|
||||
app:showAsAction="always" />
|
||||
</menu>
|
|
@ -263,25 +263,25 @@
|
|||
</fragment>
|
||||
|
||||
<fragment
|
||||
android:id="@+id/loginsFragment"
|
||||
android:name="org.mozilla.fenix.settings.LoginsFragment"
|
||||
android:id="@+id/savedLoginsAuthFragment"
|
||||
android:name="org.mozilla.fenix.settings.logins.SavedLoginsAuthFragment"
|
||||
android:label="@string/preferences_passwords_logins_and_passwords">
|
||||
<action
|
||||
android:id="@+id/action_loginsFragment_to_savedLoginsFragment"
|
||||
android:id="@+id/action_savedLoginsAuthFragment_to_loginsListFragment"
|
||||
app:enterAnim="@anim/slide_in_right"
|
||||
app:exitAnim="@anim/slide_out_left"
|
||||
app:popEnterAnim="@anim/slide_in_left"
|
||||
app:popExitAnim="@anim/slide_out_right"
|
||||
app:destination="@id/savedLoginsFragment" />
|
||||
<action
|
||||
android:id="@+id/action_loginsFragment_to_turnOnSyncFragment"
|
||||
android:id="@+id/action_savedLoginsAuthFragment_to_turnOnSyncFragment"
|
||||
app:enterAnim="@anim/slide_in_right"
|
||||
app:exitAnim="@anim/slide_out_left"
|
||||
app:popEnterAnim="@anim/slide_in_left"
|
||||
app:popExitAnim="@anim/slide_out_right"
|
||||
app:destination="@id/turnOnSyncFragment" />
|
||||
<action
|
||||
android:id="@+id/action_loginsFragment_to_saveLoginSettingFragment"
|
||||
android:id="@+id/action_savedLoginsAuthFragment_to_savedLoginsSettingFragment"
|
||||
app:enterAnim="@anim/slide_in_right"
|
||||
app:exitAnim="@anim/slide_out_left"
|
||||
app:popEnterAnim="@anim/slide_in_left"
|
||||
|
@ -289,6 +289,50 @@
|
|||
app:destination="@id/saveLoginSettingFragment" />
|
||||
</fragment>
|
||||
|
||||
<fragment
|
||||
android:id="@+id/savedLoginsFragment"
|
||||
android:name="org.mozilla.fenix.settings.logins.SavedLoginsFragment"
|
||||
tools:layout="@layout/fragment_saved_logins">
|
||||
<action
|
||||
android:id="@+id/action_savedLoginsFragment_to_loginDetailFragment"
|
||||
app:destination="@id/loginDetailFragment" />
|
||||
<action
|
||||
android:id="@+id/action_savedLoginsFragment_to_browserFragment"
|
||||
app:destination="@id/browserFragment"
|
||||
app:popUpTo="@id/settingsFragment"
|
||||
app:popUpToInclusive="true" />
|
||||
</fragment>
|
||||
|
||||
<fragment
|
||||
android:id="@+id/loginDetailFragment"
|
||||
android:name="org.mozilla.fenix.settings.logins.LoginDetailFragment"
|
||||
tools:layout="@layout/fragment_login_detail">
|
||||
<argument
|
||||
android:name="savedLoginId"
|
||||
app:argType="string"
|
||||
app:nullable="false"/>
|
||||
<action
|
||||
android:id="@+id/action_loginDetailFragment_to_editLoginFragment"
|
||||
app:destination="@id/editLoginFragment"
|
||||
app:popUpTo="@id/editLoginFragment"
|
||||
app:popUpToInclusive="true"/>
|
||||
</fragment>
|
||||
|
||||
<fragment
|
||||
android:id="@+id/editLoginFragment"
|
||||
android:name="org.mozilla.fenix.settings.logins.EditLoginFragment"
|
||||
android:label="@string/edit">
|
||||
<argument
|
||||
android:name="savedLoginItem"
|
||||
app:argType="org.mozilla.fenix.settings.logins.SavedLogin"
|
||||
app:nullable="false"/>
|
||||
<action
|
||||
android:id="@+id/action_editLoginFragment_to_loginDetailFragment"
|
||||
app:destination="@id/loginDetailFragment"
|
||||
app:popUpTo="@id/loginDetailFragment"
|
||||
app:popUpToInclusive="true"/>
|
||||
</fragment>
|
||||
|
||||
<fragment
|
||||
android:id="@+id/settingsFragment"
|
||||
android:name="org.mozilla.fenix.settings.SettingsFragment"
|
||||
|
@ -309,12 +353,12 @@
|
|||
app:popExitAnim="@anim/slide_out_right"
|
||||
app:destination="@id/sitePermissionsFragment" />
|
||||
<action
|
||||
android:id="@+id/action_settingsFragment_to_loginsFragment"
|
||||
android:id="@+id/action_settingsFragment_to_savedLoginsAuthFragment"
|
||||
app:enterAnim="@anim/slide_in_right"
|
||||
app:exitAnim="@anim/slide_out_left"
|
||||
app:popEnterAnim="@anim/slide_in_left"
|
||||
app:popExitAnim="@anim/slide_out_right"
|
||||
app:destination="@id/loginsFragment" />
|
||||
app:destination="@id/savedLoginsAuthFragment" />
|
||||
<action
|
||||
android:id="@+id/action_settingsFragment_to_accessibilityFragment"
|
||||
app:enterAnim="@anim/slide_in_right"
|
||||
|
@ -668,22 +712,6 @@
|
|||
<fragment
|
||||
android:id="@+id/defaultBrowserSettingsFragment"
|
||||
android:name="org.mozilla.fenix.settings.DefaultBrowserSettingsFragment"/>
|
||||
<fragment
|
||||
android:id="@+id/savedLoginsFragment"
|
||||
android:name="org.mozilla.fenix.settings.logins.SavedLoginsFragment"
|
||||
tools:layout="@layout/fragment_saved_logins">
|
||||
<action
|
||||
android:id="@+id/action_savedLoginsFragment_to_savedLoginSiteInfoFragment"
|
||||
app:destination="@id/savedLoginSiteInfoFragment" />
|
||||
</fragment>
|
||||
<fragment
|
||||
android:id="@+id/savedLoginSiteInfoFragment"
|
||||
android:name="org.mozilla.fenix.settings.logins.SavedLoginSiteInfoFragment"
|
||||
tools:layout="@layout/fragment_saved_login_site_info">
|
||||
<argument
|
||||
android:name="savedLoginItem"
|
||||
app:argType="org.mozilla.fenix.settings.logins.SavedLoginsItem" />
|
||||
</fragment>
|
||||
<fragment
|
||||
android:id="@+id/addSearchEngineFragment"
|
||||
android:name="org.mozilla.fenix.settings.search.AddSearchEngineFragment" />
|
||||
|
@ -699,7 +727,7 @@
|
|||
android:name="org.mozilla.fenix.settings.advanced.LocaleSettingsFragment" />
|
||||
<fragment
|
||||
android:id="@+id/saveLoginSettingFragment"
|
||||
android:name="org.mozilla.fenix.settings.logins.SaveLoginSettingFragment"
|
||||
android:name="org.mozilla.fenix.settings.logins.SavedLoginsSettingFragment"
|
||||
android:label="SaveLoginSettingFragment" />
|
||||
<fragment
|
||||
android:id="@+id/addonsManagementFragment"
|
||||
|
|
|
@ -1336,8 +1336,26 @@
|
|||
<string name="certificate_info_verified_by">Verified By: %1$s </string>
|
||||
<!-- Login overflow menu delete button -->
|
||||
<string name="login_menu_delete_button">Delete</string>
|
||||
<!-- Login overflow menu edit button -->
|
||||
<string name="login_menu_edit_button">Edit</string>
|
||||
<!-- Message in delete confirmation dialog for logins -->
|
||||
<string name="login_deletion_confirmation">Are you sure you want to delete this login?</string>
|
||||
<!-- Positive action of a dialog asking to delete -->
|
||||
<string name="dialog_delete_positive">Delete</string>
|
||||
<!-- The saved login options menu description. -->
|
||||
<string name="login_options_menu">Login options</string>
|
||||
<!-- The editable text field for a login's web address. -->
|
||||
<string name="saved_login_hostname_description">The editable text field for the web address of the login.</string>
|
||||
<!-- The editable text field for a login's username. -->
|
||||
<string name="saved_login_username_description">The editable text field for the username of the login.</string>
|
||||
<!-- The editable text field for a login's password. -->
|
||||
<string name="saved_login_password_description">The editable text field for the password of the login.</string>
|
||||
<!-- The button description to save changes to an edited login. -->
|
||||
<string name="save_changes_to_login">Save changes to login.</string>
|
||||
<!-- The button description to discard changes to an edited login. -->
|
||||
<string name="discard_changes">Discard changes</string>
|
||||
<!-- The page title for editing a saved login. -->
|
||||
<string name="edit">Edit</string>
|
||||
<!-- The error message in edit login view when password field is blank. -->
|
||||
<string name="saved_login_password_required">Password required</string>
|
||||
</resources>
|
||||
|
|
|
@ -14,10 +14,10 @@ import org.mozilla.fenix.utils.Settings
|
|||
|
||||
@RunWith(FenixRobolectricTestRunner::class)
|
||||
class SavedLoginsControllerTest {
|
||||
private val store: SavedLoginsFragmentStore = mockk(relaxed = true)
|
||||
private val store: LoginsFragmentStore = mockk(relaxed = true)
|
||||
private val settings: Settings = mockk(relaxed = true)
|
||||
private val sortingStrategy: SortingStrategy = SortingStrategy.Alphabetically(testContext)
|
||||
private val controller = DefaultSavedLoginsController(store, settings)
|
||||
private val controller = SavedLoginsController(store, settings)
|
||||
|
||||
@Test
|
||||
fun `GIVEN a sorting strategy, WHEN handleSort is called on the controller, THEN the correct action should be dispatched and the strategy saved in sharedPref`() {
|
||||
|
@ -25,7 +25,7 @@ class SavedLoginsControllerTest {
|
|||
|
||||
verify {
|
||||
store.dispatch(
|
||||
SavedLoginsFragmentAction.SortLogins(
|
||||
LoginsAction.SortLogins(
|
||||
SortingStrategy.Alphabetically(
|
||||
testContext
|
||||
)
|
||||
|
|
|
@ -15,7 +15,7 @@ import kotlin.random.Random
|
|||
@RunWith(FenixRobolectricTestRunner::class)
|
||||
class SavedLoginsInteractorTest {
|
||||
private val controller: SavedLoginsController = mockk(relaxed = true)
|
||||
private val savedLoginClicked: (SavedLoginsItem) -> Unit = mockk(relaxed = true)
|
||||
private val savedLoginClicked: (SavedLogin) -> Unit = mockk(relaxed = true)
|
||||
private val learnMore: () -> Unit = mockk(relaxed = true)
|
||||
private val interactor = SavedLoginsInteractor(
|
||||
controller,
|
||||
|
@ -25,7 +25,7 @@ class SavedLoginsInteractorTest {
|
|||
|
||||
@Test
|
||||
fun itemClicked() {
|
||||
val item = SavedLoginsItem("mozilla.org", "username", "password", "id", Random.nextLong())
|
||||
val item = SavedLogin("mozilla.org", "username", "password", "id", Random.nextLong())
|
||||
interactor.itemClicked(item)
|
||||
|
||||
verify {
|
||||
|
|
Loading…
Reference in New Issue