mirror of
https://github.com/signalapp/Signal-Android.git
synced 2026-04-23 02:10:44 +01:00
Add ability to save remote backup key to device password manager.
Co-authored-by: Cody Henthorne <cody@signal.org>
This commit is contained in:
committed by
Cody Henthorne
parent
7616ec1fd2
commit
015fc9be2c
@@ -0,0 +1,83 @@
|
||||
/*
|
||||
* Copyright 2025 Signal Messenger, LLC
|
||||
* SPDX-License-Identifier: AGPL-3.0-only
|
||||
*/
|
||||
|
||||
package org.thoughtcrime.securesms.components.settings.app.backups.remote
|
||||
|
||||
import org.signal.core.util.logging.Log
|
||||
import org.signal.core.util.logging.logW
|
||||
|
||||
/**
|
||||
* Handles the process of storing a backup key to the device password manager.
|
||||
*/
|
||||
interface BackupKeyCredentialManagerHandler {
|
||||
companion object {
|
||||
private val TAG = Log.tag(BackupKeyCredentialManagerHandler::class)
|
||||
}
|
||||
|
||||
/** Updates the [BackupKeySaveState]. Implementers must update their associated state to match [newState]. */
|
||||
fun updateBackupKeySaveState(newState: BackupKeySaveState?)
|
||||
|
||||
/** Called when the user initiates the backup key save flow. */
|
||||
fun onBackupKeySaveRequested() = updateBackupKeySaveState(BackupKeySaveState.RequestingConfirmation)
|
||||
|
||||
/** Called when the user confirms they want to save the backup key to the password manager. */
|
||||
fun onBackupKeySaveConfirmed() = updateBackupKeySaveState(BackupKeySaveState.AwaitingCredentialManager(isRetry = false))
|
||||
|
||||
/** Handles the password manager save operation response. */
|
||||
fun onBackupKeySaveCompleted(result: CredentialManagerResult) {
|
||||
when (result) {
|
||||
is CredentialManagerResult.Success -> {
|
||||
Log.d(TAG, "Successfully saved backup key to credential manager.")
|
||||
updateBackupKeySaveState(newState = BackupKeySaveState.Success)
|
||||
}
|
||||
|
||||
is CredentialManagerResult.UserCanceled -> {
|
||||
Log.d(TAG, "User canceled saving backup key to credential manager.")
|
||||
updateBackupKeySaveState(newState = null)
|
||||
}
|
||||
|
||||
is CredentialManagerResult.Interrupted -> {
|
||||
Log.i(TAG, "Retry saving backup key to credential manager after interruption.", result.exception)
|
||||
updateBackupKeySaveState(newState = BackupKeySaveState.AwaitingCredentialManager(isRetry = true))
|
||||
}
|
||||
|
||||
is CredentialManagerError.MissingCredentialManager -> {
|
||||
Log.w(TAG, "Error saving backup key to credential manager: no credential manager is configured.", result.exception)
|
||||
updateBackupKeySaveState(newState = BackupKeySaveState.Error(result))
|
||||
}
|
||||
|
||||
is CredentialManagerError.Unexpected -> {
|
||||
throw result.exception.logW(TAG, "Unexpected error when saving backup key to credential manager.")
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/** Represents state related to saving a backup key to the device password manager. */
|
||||
sealed interface BackupKeySaveState {
|
||||
/** Awaiting the user to confirm they want to save the backup key. */
|
||||
data object RequestingConfirmation : BackupKeySaveState
|
||||
|
||||
/** Awaiting the password manager's response for the backup key save operation. */
|
||||
data class AwaitingCredentialManager(val isRetry: Boolean) : BackupKeySaveState
|
||||
data object Success : BackupKeySaveState
|
||||
data class Error(val errorType: CredentialManagerError) : BackupKeySaveState
|
||||
}
|
||||
|
||||
sealed interface CredentialManagerResult {
|
||||
data object Success : CredentialManagerResult
|
||||
data object UserCanceled : CredentialManagerResult
|
||||
|
||||
/** The backup key save operation was interrupted and should be retried. */
|
||||
data class Interrupted(val exception: Exception) : CredentialManagerResult
|
||||
}
|
||||
|
||||
sealed class CredentialManagerError : CredentialManagerResult {
|
||||
abstract val exception: Exception
|
||||
|
||||
/** No password manager is configured on the device. */
|
||||
data class MissingCredentialManager(override val exception: Exception) : CredentialManagerError()
|
||||
data class Unexpected(override val exception: Exception) : CredentialManagerError()
|
||||
}
|
||||
@@ -6,11 +6,14 @@
|
||||
package org.thoughtcrime.securesms.components.settings.app.backups.remote
|
||||
|
||||
import androidx.compose.runtime.Composable
|
||||
import androidx.compose.runtime.getValue
|
||||
import androidx.lifecycle.compose.collectAsStateWithLifecycle
|
||||
import androidx.navigation.fragment.findNavController
|
||||
import org.thoughtcrime.securesms.backup.v2.ui.subscription.MessageBackupsKeyRecordScreen
|
||||
import org.thoughtcrime.securesms.compose.ComposeFragment
|
||||
import org.thoughtcrime.securesms.keyvalue.SignalStore
|
||||
import org.thoughtcrime.securesms.util.Util
|
||||
import org.thoughtcrime.securesms.util.viewModel
|
||||
|
||||
/**
|
||||
* Fragment which only displays the backup key to the user.
|
||||
@@ -21,12 +24,20 @@ class BackupKeyDisplayFragment : ComposeFragment() {
|
||||
const val CLIPBOARD_TIMEOUT_SECONDS = 60
|
||||
}
|
||||
|
||||
private val viewModel: BackupKeyDisplayViewModel by viewModel { BackupKeyDisplayViewModel() }
|
||||
|
||||
@Composable
|
||||
override fun FragmentContent() {
|
||||
val state by viewModel.uiState.collectAsStateWithLifecycle()
|
||||
|
||||
MessageBackupsKeyRecordScreen(
|
||||
backupKey = SignalStore.account.accountEntropyPool.displayValue,
|
||||
keySaveState = state.keySaveState,
|
||||
onNavigationClick = { findNavController().popBackStack() },
|
||||
onCopyToClipboardClick = { Util.copyToClipboard(requireContext(), it, CLIPBOARD_TIMEOUT_SECONDS) },
|
||||
onRequestSaveToPasswordManager = viewModel::onBackupKeySaveRequested,
|
||||
onConfirmSaveToPasswordManager = viewModel::onBackupKeySaveConfirmed,
|
||||
onSaveToPasswordManagerComplete = viewModel::onBackupKeySaveCompleted,
|
||||
onNextClick = { findNavController().popBackStack() }
|
||||
)
|
||||
}
|
||||
|
||||
@@ -0,0 +1,25 @@
|
||||
/*
|
||||
* Copyright 2025 Signal Messenger, LLC
|
||||
* SPDX-License-Identifier: AGPL-3.0-only
|
||||
*/
|
||||
|
||||
package org.thoughtcrime.securesms.components.settings.app.backups.remote
|
||||
|
||||
import androidx.lifecycle.ViewModel
|
||||
import kotlinx.coroutines.flow.MutableStateFlow
|
||||
import kotlinx.coroutines.flow.StateFlow
|
||||
import kotlinx.coroutines.flow.asStateFlow
|
||||
import kotlinx.coroutines.flow.update
|
||||
|
||||
class BackupKeyDisplayViewModel : ViewModel(), BackupKeyCredentialManagerHandler {
|
||||
private val _uiState = MutableStateFlow(BackupKeyDisplayUiState())
|
||||
val uiState: StateFlow<BackupKeyDisplayUiState> = _uiState.asStateFlow()
|
||||
|
||||
override fun updateBackupKeySaveState(newState: BackupKeySaveState?) {
|
||||
_uiState.update { it.copy(keySaveState = newState) }
|
||||
}
|
||||
}
|
||||
|
||||
data class BackupKeyDisplayUiState(
|
||||
val keySaveState: BackupKeySaveState? = null
|
||||
)
|
||||
Reference in New Issue
Block a user