* For https://github.com/mozilla-mobile/fenix/issues/25588, hide password in android 13 clipboard preview

This patch hide passwords copied from "Saved logins" in android 13 clipboard preview

Closes https://github.com/mozilla-mobile/fenix/issues/25588

* fix: show a toast for android 12 and lower

* For mozilla-mobilehttps://github.com/mozilla-mobile/fenix/issues/25588, hide password in android 13 clipboard preview

* fix: move metrics to new listeners

* fix: actually move metrics to new listeners

---------

Co-authored-by: cschanaj <cschanaj@users.noreply.github.com>
Co-authored-by: mergify[bot] <37929162+mergify[bot]@users.noreply.github.com>
pull/600/head
Jeff Boek 2 years ago committed by GitHub
parent 30ee8f1f81
commit 71349f9688

@ -26,6 +26,7 @@ import org.mozilla.fenix.settings.logins.LoginsFragmentStore
import org.mozilla.fenix.settings.logins.fragment.AddLoginFragmentDirections
import org.mozilla.fenix.settings.logins.fragment.EditLoginFragmentDirections
import org.mozilla.fenix.settings.logins.mapToSavedLogin
import org.mozilla.fenix.utils.ClipboardHandler
/**
* Controller for all saved logins interactions with the password storage component
@ -37,6 +38,7 @@ open class SavedLoginsStorageController(
private val navController: NavController,
private val loginsFragmentStore: LoginsFragmentStore,
private val ioDispatcher: CoroutineDispatcher = Dispatchers.IO,
private val clipboardHandler: ClipboardHandler,
) {
fun delete(loginId: String) {
@ -252,4 +254,22 @@ open class SavedLoginsStorageController(
}
}
}
/**
* Copy login username to clipboard
* @param loginId id of the login entry to copy username from
*/
fun copyUsername(loginId: String) = lifecycleScope.launch {
val login = passwordsStorage.get(loginId)
clipboardHandler.text = login?.username
}
/**
* Copy login password to clipboard
* @param loginId id of the login entry to copy password from
*/
fun copyPassword(loginId: String) = lifecycleScope.launch {
val login = passwordsStorage.get(loginId)
clipboardHandler.sensitiveText = login?.password
}
}

@ -75,6 +75,7 @@ class AddLoginFragment : Fragment(R.layout.fragment_add_login), MenuProvider {
lifecycleScope = lifecycleScope,
navController = findNavController(),
loginsFragmentStore = loginsFragmentStore,
clipboardHandler = requireContext().components.clipboardHandler,
),
)

@ -82,6 +82,7 @@ class EditLoginFragment : Fragment(R.layout.fragment_edit_login), MenuProvider {
lifecycleScope = lifecycleScope,
navController = findNavController(),
loginsFragmentStore = loginsFragmentStore,
clipboardHandler = requireContext().components.clipboardHandler,
),
)

@ -5,6 +5,7 @@
package org.mozilla.fenix.settings.logins.fragment
import android.content.DialogInterface
import android.os.Build
import android.os.Bundle
import android.text.InputType
import android.view.LayoutInflater
@ -13,7 +14,6 @@ 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.core.view.MenuProvider
import androidx.lifecycle.Lifecycle
@ -89,8 +89,10 @@ class LoginDetailFragment : SecureFragment(R.layout.fragment_login_detail), Menu
lifecycleScope = lifecycleScope,
navController = findNavController(),
loginsFragmentStore = savedLoginsStore,
clipboardHandler = requireContext().components.clipboardHandler,
),
)
interactor.onFetchLoginList(args.savedLoginId)
consumeFrom(savedLoginsStore) {
@ -145,14 +147,18 @@ class LoginDetailFragment : SecureFragment(R.layout.fragment_login_detail), Menu
}
binding.usernameText.text = login?.username
binding.copyUsername.setOnClickListener(
CopyButtonListener(login?.username, R.string.logins_username_copied),
)
binding.copyUsername.setOnClickListener {
interactor.onCopyUsername(args.savedLoginId)
showCopiedSnackbar(view = it, copiedItem = it.context.getString(R.string.logins_username_copied))
Logins.copyLogin.record(NoExtras())
}
binding.passwordText.text = login?.password
binding.copyPassword.setOnClickListener(
CopyButtonListener(login?.password, R.string.logins_password_copied),
)
binding.copyPassword.setOnClickListener {
interactor.onCopyPassword(args.savedLoginId)
showCopiedSnackbar(view = it, copiedItem = it.context.getString(R.string.logins_password_copied))
Logins.copyLogin.record(NoExtras())
}
}
override fun onCreateMenu(menu: Menu, inflater: MenuInflater) {
@ -172,6 +178,17 @@ class LoginDetailFragment : SecureFragment(R.layout.fragment_login_detail), Menu
else -> false
}
private fun showCopiedSnackbar(view: View, copiedItem: String) {
// Only show a toast for Android 12 and lower.
if (Build.VERSION.SDK_INT <= Build.VERSION_CODES.S_V2) {
FenixSnackbar.make(
view,
duration = Snackbar.LENGTH_SHORT,
isDisplayedWithBrowserToolbar = false,
).setText(copiedItem).show()
}
}
private fun navigateToBrowser(address: String) {
(activity as HomeActivity).openToBrowserAndLoad(
address,
@ -206,33 +223,6 @@ class LoginDetailFragment : SecureFragment(R.layout.fragment_login_detail), Menu
}
}
/**
* 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))
Logins.copyLogin.record(NoExtras())
}
private fun showCopiedSnackbar(copiedItem: String) {
view?.let {
FenixSnackbar.make(
view = it,
duration = Snackbar.LENGTH_SHORT,
isDisplayedWithBrowserToolbar = false,
).setText(copiedItem).show()
}
}
}
override fun onDestroyView() {
super.onDestroyView()
_binding = null

@ -88,6 +88,7 @@ class SavedLoginsFragment : SecureFragment(), MenuProvider {
lifecycleScope = viewLifecycleOwner.lifecycleScope,
navController = findNavController(),
loginsFragmentStore = savedLoginsStore,
clipboardHandler = requireContext().components.clipboardHandler,
)
savedLoginsInteractor =

@ -21,4 +21,20 @@ class LoginDetailInteractor(
fun onDeleteLogin(loginId: String) {
savedLoginsController.delete(loginId)
}
/**
* for the copy username button
* @param loginId id of the login entry to copy username from
*/
fun onCopyUsername(loginId: String) {
savedLoginsController.copyUsername(loginId)
}
/**
* for the copy password button
* @param loginId id of the login entry to copy password from
*/
fun onCopyPassword(loginId: String) {
savedLoginsController.copyPassword(loginId)
}
}

@ -8,6 +8,7 @@ import android.content.ClipData
import android.content.ClipboardManager
import android.content.Context
import android.os.Build
import android.os.PersistableBundle
import android.view.textclassifier.TextClassifier
import androidx.annotation.VisibleForTesting
import androidx.core.content.getSystemService
@ -47,7 +48,39 @@ class ClipboardHandler(val context: Context) {
return null
}
set(value) {
clipboard.setPrimaryClip(ClipData.newPlainText("Text", value))
val clipData = ClipData.newPlainText("Text", value)
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.N) {
clipData.apply {
description.extras = PersistableBundle().apply {
putBoolean("android.content.extra.IS_SENSITIVE", false)
}
}
}
clipboard.setPrimaryClip(clipData)
}
/**
* Provides access to the sensitive content of the clipboard, be aware this is a sensitive
* API as from Android 12 and above, accessing it will trigger a notification letting the user
* know the app has accessed the clipboard, make sure when you call this API that users are
* completely aware that we are accessing the clipboard.
* See for more details https://github.com/mozilla-mobile/fenix/issues/22271.
*
*/
var sensitiveText: String?
get() {
return text
}
set(value) {
val clipData = ClipData.newPlainText("Text", value)
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.N) {
clipData.apply {
description.extras = PersistableBundle().apply {
putBoolean("android.content.extra.IS_SENSITIVE", true)
}
}
}
clipboard.setPrimaryClip(clipData)
}
/**

@ -26,6 +26,7 @@ import org.mozilla.fenix.ext.directionsEq
import org.mozilla.fenix.helpers.FenixRobolectricTestRunner
import org.mozilla.fenix.settings.logins.controller.SavedLoginsStorageController
import org.mozilla.fenix.settings.logins.fragment.EditLoginFragmentDirections
import org.mozilla.fenix.utils.ClipboardHandler
@RunWith(FenixRobolectricTestRunner::class)
class SavedLoginsStorageControllerTest {
@ -38,6 +39,7 @@ class SavedLoginsStorageControllerTest {
private lateinit var controller: SavedLoginsStorageController
private val navController: NavController = mockk(relaxed = true)
private val loginsFragmentStore: LoginsFragmentStore = mockk(relaxed = true)
private val clipboardHandler: ClipboardHandler = mockk(relaxed = true)
private val loginMock: Login = mockk(relaxed = true)
@Before
@ -54,6 +56,7 @@ class SavedLoginsStorageControllerTest {
navController = navController,
loginsFragmentStore = loginsFragmentStore,
ioDispatcher = ioDispatcher,
clipboardHandler = clipboardHandler,
)
}

Loading…
Cancel
Save