[fenix] Fixes https://github.com/mozilla-mobile/fenix/issues/9504: Edit logins (https://github.com/mozilla-mobile/fenix/pull/9693)
* 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 descriptionpull/600/head
parent
b676537519
commit
7d3693de38
@ -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
|
||||
}
|
||||
}
|
@ -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
|
||||
}
|
||||
}
|
||||
}
|
@ -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()
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
@ -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
|
||||
}
|
||||
}
|
@ -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)
|
||||
}
|
||||
}
|
@ -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>
|
@ -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,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>
|
Loading…
Reference in New Issue