1
0
Fork 0

For #6623 - Adds ability to delete a login

master
Emily Kager 2020-02-07 17:58:30 +01:00 committed by Jeff Boek
parent b231afb05f
commit 2264e6e1b1
5 changed files with 110 additions and 15 deletions

View File

@ -7,15 +7,26 @@ package org.mozilla.fenix.settings.logins
import android.content.Context import android.content.Context
import android.os.Bundle import android.os.Bundle
import android.text.InputType import android.text.InputType
import android.view.Menu
import android.view.MenuInflater
import android.view.MenuItem
import android.view.View import android.view.View
import android.view.WindowManager import android.view.WindowManager
import androidx.annotation.StringRes import androidx.annotation.StringRes
import androidx.appcompat.content.res.AppCompatResources.getDrawable import androidx.appcompat.content.res.AppCompatResources.getDrawable
import androidx.fragment.app.Fragment import androidx.fragment.app.Fragment
import androidx.lifecycle.lifecycleScope
import androidx.navigation.fragment.findNavController import androidx.navigation.fragment.findNavController
import androidx.navigation.fragment.navArgs import androidx.navigation.fragment.navArgs
import com.google.android.material.snackbar.Snackbar import com.google.android.material.snackbar.Snackbar
import kotlinx.android.synthetic.main.fragment_saved_login_site_info.* 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.R
import org.mozilla.fenix.components.FenixSnackbar import org.mozilla.fenix.components.FenixSnackbar
import org.mozilla.fenix.components.metrics.Event import org.mozilla.fenix.components.metrics.Event
@ -29,6 +40,11 @@ class SavedLoginSiteInfoFragment : Fragment(R.layout.fragment_saved_login_site_i
private val args by navArgs<SavedLoginSiteInfoFragmentArgs>() private val args by navArgs<SavedLoginSiteInfoFragmentArgs>()
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
setHasOptionsMenu(true)
}
override fun onPause() { override fun onPause() {
// If we pause this fragment, we want to pop users back to reauth // If we pause this fragment, we want to pop users back to reauth
if (findNavController().currentDestination?.id != R.id.savedLoginsFragment) { if (findNavController().currentDestination?.id != R.id.savedLoginsFragment) {
@ -62,16 +78,61 @@ class SavedLoginSiteInfoFragment : Fragment(R.layout.fragment_saved_login_site_i
) )
} }
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 -> {
deleteLogin()
true
}
else -> false
}
private fun deleteLogin() {
var deleteLoginJob: Deferred<Boolean>? = null
val deleteJob = lifecycleScope.launch(IO) {
deleteLoginJob = async {
requireContext().components.core.syncablePasswordsStorage.withUnlocked {
it.delete(args.savedLoginItem.id).await()
}
}
deleteLoginJob?.await()
withContext(Main) {
findNavController().popBackStack(R.id.savedLoginsFragment, false)
}
}
deleteJob.invokeOnCompletion {
if (it is CancellationException) {
deleteLoginJob?.cancel()
}
}
}
private fun togglePasswordReveal(context: Context) { private fun togglePasswordReveal(context: Context) {
if (passwordInfoText.inputType == InputType.TYPE_TEXT_VARIATION_PASSWORD or InputType.TYPE_CLASS_TEXT) { if (passwordInfoText.inputType == InputType.TYPE_TEXT_VARIATION_PASSWORD or InputType.TYPE_CLASS_TEXT) {
context.components.analytics.metrics.track(Event.ViewLoginPassword) context.components.analytics.metrics.track(Event.ViewLoginPassword)
passwordInfoText.inputType = InputType.TYPE_TEXT_VARIATION_VISIBLE_PASSWORD passwordInfoText.inputType = InputType.TYPE_TEXT_VARIATION_VISIBLE_PASSWORD
revealPasswordItem.setImageDrawable(getDrawable(context, R.drawable.mozac_ic_password_hide)) revealPasswordItem.setImageDrawable(
revealPasswordItem.contentDescription = context.getString(R.string.saved_login_hide_password) getDrawable(
context,
R.drawable.mozac_ic_password_hide
)
)
revealPasswordItem.contentDescription =
context.getString(R.string.saved_login_hide_password)
} else { } else {
passwordInfoText.inputType = InputType.TYPE_CLASS_TEXT or InputType.TYPE_TEXT_VARIATION_PASSWORD passwordInfoText.inputType =
revealPasswordItem.setImageDrawable(getDrawable(context, R.drawable.mozac_ic_password_reveal)) InputType.TYPE_CLASS_TEXT or InputType.TYPE_TEXT_VARIATION_PASSWORD
revealPasswordItem.contentDescription = context.getString(R.string.saved_login_reveal_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 // For the new type to take effect you need to reset the text
passwordInfoText.text = args.savedLoginItem.password passwordInfoText.text = args.savedLoginItem.password

View File

@ -13,12 +13,16 @@ import androidx.fragment.app.Fragment
import androidx.lifecycle.lifecycleScope import androidx.lifecycle.lifecycleScope
import androidx.navigation.fragment.findNavController import androidx.navigation.fragment.findNavController
import kotlinx.android.synthetic.main.fragment_saved_logins.view.* import kotlinx.android.synthetic.main.fragment_saved_logins.view.*
import kotlinx.coroutines.CancellationException
import kotlinx.coroutines.Deferred
import kotlinx.coroutines.Dispatchers.IO import kotlinx.coroutines.Dispatchers.IO
import kotlinx.coroutines.Dispatchers.Main import kotlinx.coroutines.Dispatchers.Main
import kotlinx.coroutines.ExperimentalCoroutinesApi import kotlinx.coroutines.ExperimentalCoroutinesApi
import kotlinx.coroutines.ObsoleteCoroutinesApi import kotlinx.coroutines.ObsoleteCoroutinesApi
import kotlinx.coroutines.async
import kotlinx.coroutines.launch import kotlinx.coroutines.launch
import kotlinx.coroutines.withContext import kotlinx.coroutines.withContext
import mozilla.appservices.logins.ServerPassword
import mozilla.components.lib.state.ext.consumeFrom import mozilla.components.lib.state.ext.consumeFrom
import org.mozilla.fenix.BrowserDirection import org.mozilla.fenix.BrowserDirection
import org.mozilla.fenix.HomeActivity import org.mozilla.fenix.HomeActivity
@ -58,7 +62,7 @@ class SavedLoginsFragment : Fragment() {
} }
savedLoginsInteractor = SavedLoginsInteractor(::itemClicked, ::openLearnMore) savedLoginsInteractor = SavedLoginsInteractor(::itemClicked, ::openLearnMore)
savedLoginsView = SavedLoginsView(view.savedLoginsLayout, savedLoginsInteractor) savedLoginsView = SavedLoginsView(view.savedLoginsLayout, savedLoginsInteractor)
lifecycleScope.launch(Main) { loadAndMapLogins() } loadAndMapLogins()
return view return view
} }
@ -98,16 +102,27 @@ class SavedLoginsFragment : Fragment() {
) )
} }
private suspend fun loadAndMapLogins() { private fun loadAndMapLogins() {
val syncedLogins = withContext(IO) { var deferredLogins: Deferred<List<ServerPassword>>? = null
requireContext().components.core.syncablePasswordsStorage.withUnlocked { val fetchLoginsJob = lifecycleScope.launch(IO) {
it.list().await().map { item -> deferredLogins = async {
SavedLoginsItem(item.hostname, item.username, item.password) requireContext().components.core.syncablePasswordsStorage.withUnlocked {
it.list().await()
}
}
val logins = deferredLogins?.await()
logins?.let {
withContext(Main) {
savedLoginsStore.dispatch(SavedLoginsFragmentAction.UpdateLogins(logins.map { item ->
SavedLoginsItem(item.hostname, item.username, item.password, item.id)
}))
} }
} }
} }
withContext(Main) { fetchLoginsJob.invokeOnCompletion {
savedLoginsStore.dispatch(SavedLoginsFragmentAction.UpdateLogins(syncedLogins)) if (it is CancellationException) {
deferredLogins?.cancel()
}
} }
} }
} }

View File

@ -17,7 +17,12 @@ import mozilla.components.lib.state.Store
* @property password Password that's saved for this site * @property password Password that's saved for this site
*/ */
@Parcelize @Parcelize
data class SavedLoginsItem(val url: String, val userName: String?, val password: String?) : data class SavedLoginsItem(
val url: String,
val userName: String?,
val password: String?,
val id: String
) :
Parcelable Parcelable
/** /**

View File

@ -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">
<item
android:id="@+id/delete_login_button"
android:contentDescription="@string/login_menu_delete_button"
android:icon="@drawable/ic_delete"
android:title="@string/login_menu_delete_button"
app:iconTint="?primaryText"
app:showAsAction="ifRoom" />
</menu>

View File

@ -19,7 +19,7 @@ class SavedLoginsInteractorTest {
learnMore learnMore
) )
val item = SavedLoginsItem("mozilla.org", "username", "password") val item = SavedLoginsItem("mozilla.org", "username", "password", "id")
interactor.itemClicked(item) interactor.itemClicked(item)
verify { verify {