diff --git a/app/src/main/java/org/mozilla/fenix/settings/logins/SavedLoginSiteInfoFragment.kt b/app/src/main/java/org/mozilla/fenix/settings/logins/SavedLoginSiteInfoFragment.kt index 5af5af594..7a37cbd64 100644 --- a/app/src/main/java/org/mozilla/fenix/settings/logins/SavedLoginSiteInfoFragment.kt +++ b/app/src/main/java/org/mozilla/fenix/settings/logins/SavedLoginSiteInfoFragment.kt @@ -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() + 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? = 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 diff --git a/app/src/main/java/org/mozilla/fenix/settings/logins/SavedLoginsFragment.kt b/app/src/main/java/org/mozilla/fenix/settings/logins/SavedLoginsFragment.kt index 498b49d6f..e99015400 100644 --- a/app/src/main/java/org/mozilla/fenix/settings/logins/SavedLoginsFragment.kt +++ b/app/src/main/java/org/mozilla/fenix/settings/logins/SavedLoginsFragment.kt @@ -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>? = 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() + } } } } diff --git a/app/src/main/java/org/mozilla/fenix/settings/logins/SavedLoginsFragmentStore.kt b/app/src/main/java/org/mozilla/fenix/settings/logins/SavedLoginsFragmentStore.kt index 051920098..8c7d6566d 100644 --- a/app/src/main/java/org/mozilla/fenix/settings/logins/SavedLoginsFragmentStore.kt +++ b/app/src/main/java/org/mozilla/fenix/settings/logins/SavedLoginsFragmentStore.kt @@ -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 /** diff --git a/app/src/main/res/menu/login_edit.xml b/app/src/main/res/menu/login_edit.xml new file mode 100644 index 000000000..f4b80b31c --- /dev/null +++ b/app/src/main/res/menu/login_edit.xml @@ -0,0 +1,14 @@ + + + + + diff --git a/app/src/test/java/org/mozilla/fenix/settings/logins/SavedLoginsInteractorTest.kt b/app/src/test/java/org/mozilla/fenix/settings/logins/SavedLoginsInteractorTest.kt index 9434f6720..e566208d4 100644 --- a/app/src/test/java/org/mozilla/fenix/settings/logins/SavedLoginsInteractorTest.kt +++ b/app/src/test/java/org/mozilla/fenix/settings/logins/SavedLoginsInteractorTest.kt @@ -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 {