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.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.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
@ -29,6 +40,11 @@ class SavedLoginSiteInfoFragment : Fragment(R.layout.fragment_saved_login_site_i
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) {
@ -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) {
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)
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)
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

View File

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

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
)
val item = SavedLoginsItem("mozilla.org", "username", "password")
val item = SavedLoginsItem("mozilla.org", "username", "password", "id")
interactor.itemClicked(item)
verify {