You can not select more than 25 topics Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.
 
 
 
 
 
 

251 lines
9.1 KiB

/* This Source Code Form is subject to the terms of the Mozilla Public
* License, v. 2.0. If a copy of the MPL was not distributed with this
* file, You can obtain one at http://mozilla.org/MPL/2.0/. */
package org.mozilla.fenix.settings.logins.fragment
import android.content.DialogInterface
import android.os.Bundle
import android.text.InputType
import android.view.LayoutInflater
import android.view.Menu
import android.view.MenuInflater
import android.view.MenuItem
import android.view.View
import android.view.ViewGroup
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.ExperimentalCoroutinesApi
import kotlinx.coroutines.ObsoleteCoroutinesApi
import mozilla.components.lib.state.ext.consumeFrom
import org.mozilla.fenix.BrowserDirection
import org.mozilla.fenix.HomeActivity
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.increaseTapArea
import org.mozilla.fenix.ext.redirectToReAuth
import org.mozilla.fenix.ext.requireComponents
import org.mozilla.fenix.ext.settings
import org.mozilla.fenix.ext.showToolbar
import org.mozilla.fenix.ext.simplifiedUrl
import org.mozilla.fenix.settings.logins.LoginsFragmentStore
import org.mozilla.fenix.settings.logins.LoginsListState
import org.mozilla.fenix.settings.logins.SavedLogin
import org.mozilla.fenix.settings.logins.controller.SavedLoginsStorageController
import org.mozilla.fenix.settings.logins.interactor.LoginDetailInteractor
import org.mozilla.fenix.settings.logins.togglePasswordReveal
import org.mozilla.fenix.settings.logins.view.LoginDetailView
/**
* 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
private lateinit var interactor: LoginDetailInteractor
private lateinit var menu: Menu
private var deleteDialog: AlertDialog? = null
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,
duplicateLogins = listOf() // assume on load there are no dupes
)
)
}
loginDetailView = LoginDetailView(
view.findViewById(R.id.loginDetailLayout)
)
return view
}
@ObsoleteCoroutinesApi
@ExperimentalCoroutinesApi
override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
super.onViewCreated(view, savedInstanceState)
interactor = LoginDetailInteractor(
SavedLoginsStorageController(
passwordsStorage = requireContext().components.core.passwordsStorage,
viewLifecycleScope = viewLifecycleOwner.lifecycleScope,
navController = findNavController(),
loginsFragmentStore = savedLoginsStore
)
)
interactor.onFetchLoginList(args.savedLoginId)
consumeFrom(savedLoginsStore) {
loginDetailView.update(it)
login = savedLoginsStore.state.currentItem
setUpCopyButtons()
showToolbar(
savedLoginsStore.state.currentItem?.origin?.simplifiedUrl()
?: ""
)
setUpPasswordReveal()
}
togglePasswordReveal(passwordText, revealPasswordButton)
}
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
setHasOptionsMenu(true)
}
/**
* As described in #10727, the User should re-auth if the fragment is paused and the user is not
* navigating to SavedLoginsFragment or EditLoginFragment
*
*/
override fun onPause() {
deleteDialog?.isShowing.run { deleteDialog?.dismiss() }
menu.close()
redirectToReAuth(
listOf(R.id.editLoginFragment, R.id.savedLoginsFragment),
findNavController().currentDestination?.id
)
super.onPause()
}
private fun setUpPasswordReveal() {
passwordText.inputType =
InputType.TYPE_CLASS_TEXT or InputType.TYPE_TEXT_VARIATION_PASSWORD
revealPasswordButton.increaseTapArea(BUTTON_INCREASE_DPS)
revealPasswordButton.setOnClickListener {
togglePasswordReveal(passwordText, revealPasswordButton)
}
passwordText.setOnClickListener {
togglePasswordReveal(passwordText, revealPasswordButton)
}
}
private fun setUpCopyButtons() {
webAddressText.text = login?.origin
openWebAddress.increaseTapArea(BUTTON_INCREASE_DPS)
copyUsername.increaseTapArea(BUTTON_INCREASE_DPS)
copyPassword.increaseTapArea(BUTTON_INCREASE_DPS)
openWebAddress.setOnClickListener {
navigateToBrowser(requireNotNull(login?.origin))
}
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)
)
}
override fun onCreateOptionsMenu(menu: Menu, inflater: MenuInflater) {
inflater.inflate(R.menu.login_options_menu, menu)
this.menu = 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 navigateToBrowser(address: String) {
(activity as HomeActivity).openToBrowserAndLoad(
address,
newTab = true,
from = BrowserDirection.FromLoginDetailFragment
)
}
private fun editLogin() {
requireComponents.analytics.metrics.track(Event.EditLogin)
val directions =
LoginDetailFragmentDirections.actionLoginDetailFragmentToEditLoginFragment(
login!!
)
findNavController().navigate(directions)
}
private fun displayDeleteLoginDialog() {
activity?.let { activity ->
deleteDialog = 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, _ ->
requireComponents.analytics.metrics.track(Event.DeleteLogin)
interactor.onDeleteLogin(args.savedLoginId)
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()
}
}
}
private companion object {
private const val BUTTON_INCREASE_DPS = 24
}
}