mirror of
https://github.com/signalapp/Signal-Android.git
synced 2026-05-03 15:11:42 +01:00
Add underpinnings to allow for local plaintext export.
Co-authored-by: Cody Henthorne <cody@signal.org>
This commit is contained in:
committed by
Cody Henthorne
parent
b605148ac4
commit
f2e4881026
@@ -21,6 +21,7 @@ import androidx.compose.foundation.layout.padding
|
||||
import androidx.compose.foundation.rememberScrollState
|
||||
import androidx.compose.foundation.text.KeyboardOptions
|
||||
import androidx.compose.foundation.verticalScroll
|
||||
import androidx.compose.material3.AlertDialog
|
||||
import androidx.compose.material3.ExperimentalMaterial3Api
|
||||
import androidx.compose.material3.Icon
|
||||
import androidx.compose.material3.IconButton
|
||||
@@ -71,9 +72,11 @@ import org.signal.core.util.Hex
|
||||
import org.signal.core.util.Util
|
||||
import org.signal.core.util.getLength
|
||||
import org.thoughtcrime.securesms.MainActivity
|
||||
import org.thoughtcrime.securesms.backup.isIdle
|
||||
import org.thoughtcrime.securesms.backup.v2.BackupRepository
|
||||
import org.thoughtcrime.securesms.backup.v2.ui.BackupAlert
|
||||
import org.thoughtcrime.securesms.backup.v2.ui.BackupAlertBottomSheet
|
||||
import org.thoughtcrime.securesms.backup.v2.ui.status.BackupCreationProgressRow
|
||||
import org.thoughtcrime.securesms.components.settings.app.internal.backup.InternalBackupPlaygroundViewModel.DialogState
|
||||
import org.thoughtcrime.securesms.components.settings.app.internal.backup.InternalBackupPlaygroundViewModel.ScreenState
|
||||
import org.thoughtcrime.securesms.dependencies.AppDependencies
|
||||
@@ -111,10 +114,7 @@ class InternalBackupPlaygroundFragment : ComposeFragment() {
|
||||
savePlaintextBackupToDiskLauncher = registerForActivityResult(ActivityResultContracts.StartActivityForResult()) { result ->
|
||||
if (result.resultCode == RESULT_OK) {
|
||||
result.data?.data?.let { uri ->
|
||||
viewModel.exportPlaintext(
|
||||
openStream = { requireContext().contentResolver.openOutputStream(uri)!! },
|
||||
appendStream = { requireContext().contentResolver.openOutputStream(uri, "wa")!! }
|
||||
)
|
||||
viewModel.exportPlaintextZip(uri)
|
||||
} ?: Toast.makeText(requireContext(), "No URI selected", Toast.LENGTH_SHORT).show()
|
||||
}
|
||||
}
|
||||
@@ -173,15 +173,21 @@ class InternalBackupPlaygroundFragment : ComposeFragment() {
|
||||
saveEncryptedBackupToDiskLauncher.launch(intent)
|
||||
},
|
||||
onSavePlaintextBackupToDiskClicked = {
|
||||
val intent = Intent().apply {
|
||||
action = Intent.ACTION_CREATE_DOCUMENT
|
||||
type = "application/octet-stream"
|
||||
addCategory(Intent.CATEGORY_OPENABLE)
|
||||
putExtra(Intent.EXTRA_TITLE, "backup-plaintext-${System.currentTimeMillis()}.bin")
|
||||
}
|
||||
|
||||
viewModel.showPlaintextExportDialog()
|
||||
},
|
||||
onPlaintextExportWithMedia = {
|
||||
viewModel.onPlaintextExportMediaChoiceSelected(includeMedia = true)
|
||||
val intent = Intent(Intent.ACTION_OPEN_DOCUMENT_TREE)
|
||||
savePlaintextBackupToDiskLauncher.launch(intent)
|
||||
},
|
||||
onPlaintextExportWithoutMedia = {
|
||||
viewModel.onPlaintextExportMediaChoiceSelected(includeMedia = false)
|
||||
val intent = Intent(Intent.ACTION_OPEN_DOCUMENT_TREE)
|
||||
savePlaintextBackupToDiskLauncher.launch(intent)
|
||||
},
|
||||
onPlaintextExportDismissed = {
|
||||
viewModel.dismissPlaintextExportDialog()
|
||||
},
|
||||
onSavePlaintextCopyOfRemoteBackupClicked = {
|
||||
val intent = Intent().apply {
|
||||
action = Intent.ACTION_CREATE_DOCUMENT
|
||||
@@ -352,6 +358,9 @@ fun Screen(
|
||||
onValidateBackupClicked: () -> Unit = {},
|
||||
onSaveEncryptedBackupToDiskClicked: () -> Unit = {},
|
||||
onSavePlaintextBackupToDiskClicked: () -> Unit = {},
|
||||
onPlaintextExportWithMedia: () -> Unit = {},
|
||||
onPlaintextExportWithoutMedia: () -> Unit = {},
|
||||
onPlaintextExportDismissed: () -> Unit = {},
|
||||
onImportEncryptedBackupFromDiskClicked: () -> Unit = {},
|
||||
onImportEncryptedBackupFromDiskDismissed: () -> Unit = {},
|
||||
onImportEncryptedBackupFromDiskConfirmed: (aci: String, backupKey: String) -> Unit = { _, _ -> },
|
||||
@@ -370,6 +379,19 @@ fun Screen(
|
||||
onDismissed = onImportEncryptedBackupFromDiskDismissed
|
||||
)
|
||||
}
|
||||
DialogState.PlaintextExportMediaChoice -> {
|
||||
AlertDialog(
|
||||
onDismissRequest = onPlaintextExportDismissed,
|
||||
title = { Text("Plaintext backup") },
|
||||
text = { Text("Include media in the backup?") },
|
||||
confirmButton = {
|
||||
TextButton(onClick = onPlaintextExportWithMedia) { Text("With media") }
|
||||
},
|
||||
dismissButton = {
|
||||
TextButton(onClick = onPlaintextExportWithoutMedia) { Text("Without media") }
|
||||
}
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
Surface {
|
||||
@@ -446,11 +468,18 @@ fun Screen(
|
||||
onClick = onSaveEncryptedBackupToDiskClicked
|
||||
)
|
||||
|
||||
Rows.TextRow(
|
||||
text = "Save plaintext backup to disk",
|
||||
label = "Generates a plaintext, uncompressed backup and saves it to your local disk.",
|
||||
onClick = onSavePlaintextBackupToDiskClicked
|
||||
)
|
||||
if (state.plaintextProgress.isIdle) {
|
||||
Rows.TextRow(
|
||||
text = "Save plaintext backup to disk",
|
||||
label = "Generates a plaintext backup as a zip file in the selected directory.",
|
||||
onClick = onSavePlaintextBackupToDiskClicked
|
||||
)
|
||||
} else {
|
||||
BackupCreationProgressRow(
|
||||
progress = state.plaintextProgress,
|
||||
isRemote = false
|
||||
)
|
||||
}
|
||||
|
||||
Rows.TextRow(
|
||||
text = "Save plaintext copy of remote backup",
|
||||
@@ -602,7 +631,7 @@ private fun ImportCredentialsDialog(onSubmit: (aci: String, backupKey: String) -
|
||||
keyboardType = KeyboardType.Ascii,
|
||||
imeAction = ImeAction.Next
|
||||
)
|
||||
androidx.compose.material3.AlertDialog(
|
||||
AlertDialog(
|
||||
onDismissRequest = onDismissed,
|
||||
title = { Text(text = "Are you sure?") },
|
||||
text = {
|
||||
|
||||
@@ -5,6 +5,7 @@
|
||||
|
||||
package org.thoughtcrime.securesms.components.settings.app.internal.backup
|
||||
|
||||
import android.net.Uri
|
||||
import androidx.compose.runtime.MutableState
|
||||
import androidx.compose.runtime.State
|
||||
import androidx.compose.runtime.mutableStateOf
|
||||
@@ -24,7 +25,7 @@ import kotlinx.coroutines.launch
|
||||
import kotlinx.coroutines.withContext
|
||||
import org.signal.archive.stream.EncryptedBackupReader
|
||||
import org.signal.archive.stream.EncryptedBackupReader.Companion.MAC_SIZE
|
||||
import org.signal.core.models.ServiceId.ACI
|
||||
import org.signal.core.models.ServiceId
|
||||
import org.signal.core.models.backup.MessageBackupKey
|
||||
import org.signal.core.util.Hex
|
||||
import org.signal.core.util.ThreadUtil
|
||||
@@ -49,7 +50,9 @@ import org.thoughtcrime.securesms.database.AttachmentTable.DebugAttachmentStats
|
||||
import org.thoughtcrime.securesms.database.SignalDatabase
|
||||
import org.thoughtcrime.securesms.dependencies.AppDependencies
|
||||
import org.thoughtcrime.securesms.jobs.BackupMessagesJob
|
||||
import org.thoughtcrime.securesms.jobs.LocalBackupJob
|
||||
import org.thoughtcrime.securesms.keyvalue.SignalStore
|
||||
import org.thoughtcrime.securesms.keyvalue.protos.LocalBackupCreationProgress
|
||||
import org.thoughtcrime.securesms.net.SignalNetwork
|
||||
import org.thoughtcrime.securesms.providers.BlobProvider
|
||||
import org.thoughtcrime.securesms.recipients.Recipient
|
||||
@@ -87,9 +90,12 @@ class InternalBackupPlaygroundViewModel : ViewModel() {
|
||||
|
||||
val statsState: MutableStateFlow<StatsState> = MutableStateFlow(StatsState())
|
||||
|
||||
enum class DialogState {
|
||||
None,
|
||||
ImportCredentials
|
||||
init {
|
||||
viewModelScope.launch {
|
||||
SignalStore.backup.newLocalPlaintextBackupProgressFlow.collect { progress ->
|
||||
_state.value = _state.value.copy(plaintextProgress = progress)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
fun exportEncrypted(openStream: () -> OutputStream, appendStream: () -> OutputStream) {
|
||||
@@ -108,21 +114,23 @@ class InternalBackupPlaygroundViewModel : ViewModel() {
|
||||
}
|
||||
}
|
||||
|
||||
fun exportPlaintext(openStream: () -> OutputStream, appendStream: () -> OutputStream) {
|
||||
_state.value = _state.value.copy(statusMessage = "Exporting plaintext backup to disk...")
|
||||
disposables += Single
|
||||
.fromCallable {
|
||||
BackupRepository.exportForDebugging(
|
||||
outputStream = openStream(),
|
||||
append = { bytes -> appendStream().use { it.write(bytes) } },
|
||||
plaintext = true
|
||||
)
|
||||
}
|
||||
.subscribeOn(Schedulers.io())
|
||||
.observeOn(AndroidSchedulers.mainThread())
|
||||
.subscribe { data ->
|
||||
_state.value = _state.value.copy(statusMessage = "Plaintext backup complete!")
|
||||
}
|
||||
private var plaintextIncludeMedia: Boolean = false
|
||||
|
||||
fun showPlaintextExportDialog() {
|
||||
_state.value = _state.value.copy(dialog = DialogState.PlaintextExportMediaChoice)
|
||||
}
|
||||
|
||||
fun dismissPlaintextExportDialog() {
|
||||
_state.value = _state.value.copy(dialog = DialogState.None)
|
||||
}
|
||||
|
||||
fun onPlaintextExportMediaChoiceSelected(includeMedia: Boolean) {
|
||||
plaintextIncludeMedia = includeMedia
|
||||
_state.value = _state.value.copy(dialog = DialogState.None)
|
||||
}
|
||||
|
||||
fun exportPlaintextZip(directoryUri: Uri) {
|
||||
LocalBackupJob.enqueuePlaintextArchive(directoryUri.toString(), plaintextIncludeMedia)
|
||||
}
|
||||
|
||||
fun validateBackup() {
|
||||
@@ -289,7 +297,7 @@ class InternalBackupPlaygroundViewModel : ViewModel() {
|
||||
|
||||
/** True if data is valid, else false */
|
||||
fun onImportConfirmed(aci: String, backupKey: String): Boolean {
|
||||
val parsedAci: ACI? = ACI.parseOrNull(aci)
|
||||
val parsedAci: ServiceId.ACI? = ServiceId.ACI.parseOrNull(aci)
|
||||
|
||||
if (aci.isNotBlank() && parsedAci == null) {
|
||||
_state.value = _state.value.copy(statusMessage = "Invalid ACI! Cannot import.")
|
||||
@@ -332,6 +340,7 @@ class InternalBackupPlaygroundViewModel : ViewModel() {
|
||||
_state.value = _state.value.copy(statusMessage = "Import complete!")
|
||||
ThreadUtil.runOnMain { afterDbRestoreCallback() }
|
||||
}
|
||||
|
||||
RemoteRestoreResult.Canceled,
|
||||
RemoteRestoreResult.Failure,
|
||||
RemoteRestoreResult.PermanentSvrBFailure,
|
||||
@@ -382,6 +391,7 @@ class InternalBackupPlaygroundViewModel : ViewModel() {
|
||||
SignalStore.backup.cachedMediaCdnPath = null
|
||||
return@withContext true
|
||||
}
|
||||
|
||||
else -> Log.w(TAG, "Unable to delete remote data", result.getCause())
|
||||
}
|
||||
|
||||
@@ -400,6 +410,7 @@ class InternalBackupPlaygroundViewModel : ViewModel() {
|
||||
val canReadWriteBackupDirectory: Boolean = false,
|
||||
val backupTier: MessageBackupTier? = null,
|
||||
val statusMessage: String? = null,
|
||||
val plaintextProgress: LocalBackupCreationProgress = LocalBackupCreationProgress(),
|
||||
val customBackupCredentials: ImportCredentials? = null,
|
||||
val dialog: DialogState = DialogState.None
|
||||
)
|
||||
@@ -470,7 +481,7 @@ class InternalBackupPlaygroundViewModel : ViewModel() {
|
||||
|
||||
data class ImportCredentials(
|
||||
val messageBackupKey: MessageBackupKey,
|
||||
val aci: ACI
|
||||
val aci: ServiceId.ACI
|
||||
)
|
||||
|
||||
data class StatsState(
|
||||
@@ -481,4 +492,10 @@ class InternalBackupPlaygroundViewModel : ViewModel() {
|
||||
) {
|
||||
val valid = attachmentStats != null
|
||||
}
|
||||
|
||||
enum class DialogState {
|
||||
None,
|
||||
ImportCredentials,
|
||||
PlaintextExportMediaChoice
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user