/* 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() 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 } }