You cannot select more than 25 topics
Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.
258 lines
9.5 KiB
Kotlin
258 lines
9.5 KiB
Kotlin
/* 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.controller
|
|
|
|
import android.util.Log
|
|
import androidx.navigation.NavController
|
|
import kotlinx.coroutines.CancellationException
|
|
import kotlinx.coroutines.CoroutineDispatcher
|
|
import kotlinx.coroutines.CoroutineScope
|
|
import kotlinx.coroutines.Deferred
|
|
import kotlinx.coroutines.Dispatchers
|
|
import kotlinx.coroutines.async
|
|
import kotlinx.coroutines.launch
|
|
import kotlinx.coroutines.withContext
|
|
import mozilla.components.concept.storage.Login
|
|
import mozilla.components.concept.storage.LoginEntry
|
|
import mozilla.components.service.sync.logins.InvalidRecordException
|
|
import mozilla.components.service.sync.logins.LoginsStorageException
|
|
import mozilla.components.service.sync.logins.NoSuchRecordException
|
|
import mozilla.components.service.sync.logins.SyncableLoginsStorage
|
|
import org.mozilla.fenix.R
|
|
import org.mozilla.fenix.settings.logins.LoginsAction
|
|
import org.mozilla.fenix.settings.logins.LoginsFragmentStore
|
|
import org.mozilla.fenix.settings.logins.fragment.EditLoginFragmentDirections
|
|
import org.mozilla.fenix.settings.logins.fragment.AddLoginFragmentDirections
|
|
import org.mozilla.fenix.settings.logins.mapToSavedLogin
|
|
|
|
/**
|
|
* Controller for all saved logins interactions with the password storage component
|
|
*/
|
|
@Suppress("TooManyFunctions", "LargeClass")
|
|
open class SavedLoginsStorageController(
|
|
private val passwordsStorage: SyncableLoginsStorage,
|
|
private val lifecycleScope: CoroutineScope,
|
|
private val navController: NavController,
|
|
private val loginsFragmentStore: LoginsFragmentStore,
|
|
private val ioDispatcher: CoroutineDispatcher = Dispatchers.IO
|
|
) {
|
|
|
|
private suspend fun getLogin(loginId: String): Login? = passwordsStorage.get(loginId)
|
|
|
|
fun delete(loginId: String) {
|
|
var deleteLoginJob: Deferred<Boolean>? = null
|
|
val deleteJob = lifecycleScope.launch(ioDispatcher) {
|
|
deleteLoginJob = async {
|
|
passwordsStorage.delete(loginId)
|
|
}
|
|
deleteLoginJob?.await()
|
|
withContext(Dispatchers.Main) {
|
|
navController.popBackStack(R.id.savedLoginsFragment, false)
|
|
}
|
|
}
|
|
deleteJob.invokeOnCompletion {
|
|
if (it is CancellationException) {
|
|
deleteLoginJob?.cancel()
|
|
}
|
|
}
|
|
}
|
|
|
|
// Create a [LoginEntry] for the add login dialog
|
|
private fun loginEntryForAdd(originText: String, usernameText: String, passwordText: String) = LoginEntry(
|
|
origin = originText,
|
|
username = usernameText,
|
|
password = passwordText,
|
|
// Implicitly fill in httpRealm with the origin
|
|
httpRealm = originText
|
|
)
|
|
|
|
fun add(originText: String, usernameText: String, passwordText: String) {
|
|
var saveLoginJob: Deferred<Unit>? = null
|
|
lifecycleScope.launch(ioDispatcher) {
|
|
saveLoginJob = async {
|
|
add(loginEntryForAdd(originText, usernameText, passwordText))
|
|
}
|
|
saveLoginJob?.await()
|
|
withContext(Dispatchers.Main) {
|
|
val directions =
|
|
AddLoginFragmentDirections.actionAddLoginFragmentToSavedLoginsFragment()
|
|
navController.navigate(directions)
|
|
}
|
|
}
|
|
saveLoginJob?.invokeOnCompletion {
|
|
if (it is CancellationException) {
|
|
saveLoginJob?.cancel()
|
|
}
|
|
}
|
|
}
|
|
|
|
private suspend fun add(loginEntryToSave: LoginEntry) {
|
|
try {
|
|
val encryptedLogin = passwordsStorage.add(loginEntryToSave)
|
|
syncAndUpdateList(passwordsStorage.decryptLogin(encryptedLogin))
|
|
} catch (loginException: LoginsStorageException) {
|
|
Log.e(
|
|
"Add new login",
|
|
"Failed to add new login.", loginException
|
|
)
|
|
}
|
|
}
|
|
|
|
// Create a [LoginEntry] for the edit login dialog
|
|
private suspend fun loginEntryForSave(loginId: String, usernameText: String, passwordText: String): LoginEntry {
|
|
// must retrieve from storage to get the httpsRealm and formActionOrigin
|
|
val oldLogin = passwordsStorage.get(loginId)!!
|
|
return LoginEntry(
|
|
// Copied from the existing login
|
|
origin = oldLogin.origin,
|
|
httpRealm = oldLogin.httpRealm,
|
|
formActionOrigin = oldLogin.formActionOrigin,
|
|
// New values
|
|
username = usernameText,
|
|
password = passwordText,
|
|
)
|
|
}
|
|
|
|
fun save(loginId: String, usernameText: String, passwordText: String) {
|
|
var saveLoginJob: Deferred<Unit>? = null
|
|
lifecycleScope.launch(ioDispatcher) {
|
|
saveLoginJob = async {
|
|
save(loginId, loginEntryForSave(loginId, usernameText, passwordText))
|
|
}
|
|
saveLoginJob?.await()
|
|
withContext(Dispatchers.Main) {
|
|
val directions =
|
|
EditLoginFragmentDirections.actionEditLoginFragmentToLoginDetailFragment(
|
|
loginId
|
|
)
|
|
navController.navigate(directions)
|
|
}
|
|
}
|
|
saveLoginJob?.invokeOnCompletion {
|
|
if (it is CancellationException) {
|
|
saveLoginJob?.cancel()
|
|
}
|
|
}
|
|
}
|
|
|
|
private suspend fun save(guid: String, loginEntryToSave: LoginEntry) {
|
|
try {
|
|
val encryptedLogin = passwordsStorage.update(guid, loginEntryToSave)
|
|
syncAndUpdateList(passwordsStorage.decryptLogin(encryptedLogin))
|
|
} catch (loginException: LoginsStorageException) {
|
|
when (loginException) {
|
|
is NoSuchRecordException,
|
|
is InvalidRecordException -> {
|
|
Log.e(
|
|
"Edit login",
|
|
"Failed to save edited login.", loginException
|
|
)
|
|
}
|
|
else -> Log.e(
|
|
"Edit login",
|
|
"Failed to save edited login.", loginException
|
|
)
|
|
}
|
|
}
|
|
}
|
|
|
|
private fun syncAndUpdateList(updatedLogin: Login) {
|
|
val login = updatedLogin.mapToSavedLogin()
|
|
loginsFragmentStore.dispatch(
|
|
LoginsAction.UpdateLoginsList(
|
|
listOf(login)
|
|
)
|
|
)
|
|
}
|
|
|
|
fun findDuplicateForAdd(originText: String, usernameText: String, passwordText: String) {
|
|
lifecycleScope.launch(ioDispatcher) {
|
|
findDuplicate(
|
|
loginEntryForAdd(originText, usernameText, passwordText)
|
|
)
|
|
}
|
|
}
|
|
|
|
fun findDuplicateForSave(loginId: String, usernameText: String, passwordText: String) {
|
|
lifecycleScope.launch(ioDispatcher) {
|
|
findDuplicate(
|
|
loginEntryForSave(loginId, usernameText, passwordText),
|
|
loginId
|
|
)
|
|
}
|
|
}
|
|
|
|
private fun findDuplicate(entry: LoginEntry, currentGuid: String? = null) {
|
|
// Ensure that we have a valid, non-blank password. The value doesn't
|
|
// matter for dupe-checking and we want to make sure that
|
|
// findLoginToUpdate() doesn't throw an error because the [LoginEntry]
|
|
// is invalid
|
|
val validEntry = if (entry.password.isNotEmpty()) entry else entry.copy(password = "password")
|
|
var dupe = try {
|
|
passwordsStorage.findLoginToUpdate(validEntry)?.mapToSavedLogin()
|
|
} catch (e: LoginsStorageException) {
|
|
// If the entry was invalid, then consider it not a dupe
|
|
null
|
|
}
|
|
if (dupe != null && dupe.guid == currentGuid) {
|
|
// If the found login matches the current login, don't consider it a dupe
|
|
dupe = null
|
|
}
|
|
loginsFragmentStore.dispatch(LoginsAction.DuplicateLogin(dupe))
|
|
}
|
|
|
|
fun fetchLoginDetails(loginId: String) {
|
|
var deferredLogin: Deferred<List<Login>>? = null
|
|
val fetchLoginJob = lifecycleScope.launch(ioDispatcher) {
|
|
deferredLogin = async {
|
|
passwordsStorage.list()
|
|
}
|
|
val fetchedLoginList = deferredLogin?.await()
|
|
|
|
fetchedLoginList?.let {
|
|
withContext(Dispatchers.Main) {
|
|
val login = fetchedLoginList.filter {
|
|
it.guid == loginId
|
|
}.first()
|
|
loginsFragmentStore.dispatch(
|
|
LoginsAction.UpdateCurrentLogin(
|
|
login.mapToSavedLogin()
|
|
)
|
|
)
|
|
}
|
|
}
|
|
}
|
|
fetchLoginJob.invokeOnCompletion {
|
|
if (it is CancellationException) {
|
|
deferredLogin?.cancel()
|
|
}
|
|
}
|
|
}
|
|
|
|
fun handleLoadAndMapLogins() {
|
|
var deferredLogins: Deferred<List<Login>>? = null
|
|
val fetchLoginsJob = lifecycleScope.launch(ioDispatcher) {
|
|
deferredLogins = async {
|
|
passwordsStorage.list()
|
|
}
|
|
val logins = deferredLogins?.await()
|
|
logins?.let {
|
|
withContext(Dispatchers.Main) {
|
|
loginsFragmentStore.dispatch(
|
|
LoginsAction.UpdateLoginsList(
|
|
logins.map { it.mapToSavedLogin() }
|
|
)
|
|
)
|
|
}
|
|
}
|
|
}
|
|
fetchLoginsJob.invokeOnCompletion {
|
|
if (it is CancellationException) {
|
|
deferredLogins?.cancel()
|
|
}
|
|
}
|
|
}
|
|
}
|