Update the backup playground to be more friendly.
This commit is contained in:
parent
d1bfa6ee9e
commit
3ea9dd5e1d
4 changed files with 298 additions and 455 deletions
|
@ -300,20 +300,6 @@ class ArchiveImportExportTests {
|
|||
)
|
||||
}
|
||||
|
||||
private fun assertPassesValidator(testName: String, generatedBackupData: ByteArray): TestResult.Failure? {
|
||||
try {
|
||||
BackupRepository.validate(
|
||||
length = generatedBackupData.size.toLong(),
|
||||
inputStreamFactory = { ByteArrayInputStream(generatedBackupData) },
|
||||
selfData = BackupRepository.SelfData(SELF_ACI, SELF_PNI, SELF_E164, ProfileKey(SELF_PROFILE_KEY))
|
||||
)
|
||||
} catch (e: Exception) {
|
||||
return TestResult.Failure(testName, "Generated backup failed validation: ${e.message}")
|
||||
}
|
||||
|
||||
return null
|
||||
}
|
||||
|
||||
private fun checkEquivalent(testName: String, import: ByteArray, export: ByteArray): TestResult.Failure? {
|
||||
val importComparable = try {
|
||||
ComparableBackup.readUnencrypted(MessageBackup.Purpose.REMOTE_BACKUP, import.inputStream(), import.size.toLong())
|
||||
|
|
|
@ -33,9 +33,6 @@ import org.signal.core.util.requireNonNullString
|
|||
import org.signal.core.util.stream.NonClosingOutputStream
|
||||
import org.signal.core.util.urlEncode
|
||||
import org.signal.core.util.withinTransaction
|
||||
import org.signal.libsignal.messagebackup.MessageBackup
|
||||
import org.signal.libsignal.messagebackup.MessageBackup.ValidationResult
|
||||
import org.signal.libsignal.protocol.ServiceId.Aci
|
||||
import org.signal.libsignal.zkgroup.backups.BackupLevel
|
||||
import org.signal.libsignal.zkgroup.profiles.ProfileKey
|
||||
import org.thoughtcrime.securesms.attachments.Attachment
|
||||
|
@ -109,7 +106,6 @@ import java.util.concurrent.atomic.AtomicLong
|
|||
import kotlin.time.Duration.Companion.days
|
||||
import kotlin.time.Duration.Companion.hours
|
||||
import kotlin.time.Duration.Companion.milliseconds
|
||||
import org.signal.libsignal.messagebackup.MessageBackupKey as LibSignalMessageBackupKey
|
||||
|
||||
object BackupRepository {
|
||||
|
||||
|
@ -894,13 +890,6 @@ object BackupRepository {
|
|||
return ImportResult.Success(backupTime = header.backupTimeMs)
|
||||
}
|
||||
|
||||
fun validate(length: Long, inputStreamFactory: () -> InputStream, selfData: SelfData): ValidationResult {
|
||||
val accountEntropyPool = SignalStore.account.accountEntropyPool.value
|
||||
val key = LibSignalMessageBackupKey(accountEntropyPool, Aci.parseFromBinary(selfData.aci.toByteArray()))
|
||||
|
||||
return MessageBackup.validate(key, MessageBackup.Purpose.REMOTE_BACKUP, inputStreamFactory, length)
|
||||
}
|
||||
|
||||
fun listRemoteMediaObjects(limit: Int, cursor: String? = null): NetworkResult<ArchiveGetMediaItemsResponse> {
|
||||
return initBackupAndFetchAuth()
|
||||
.then { credential ->
|
||||
|
|
|
@ -7,7 +7,6 @@ package org.thoughtcrime.securesms.components.settings.app.internal.backup
|
|||
|
||||
import android.app.Activity.RESULT_OK
|
||||
import android.content.Intent
|
||||
import android.content.res.Configuration
|
||||
import android.os.Bundle
|
||||
import android.widget.Toast
|
||||
import androidx.activity.result.ActivityResultLauncher
|
||||
|
@ -23,13 +22,11 @@ import androidx.compose.foundation.layout.Spacer
|
|||
import androidx.compose.foundation.layout.fillMaxSize
|
||||
import androidx.compose.foundation.layout.height
|
||||
import androidx.compose.foundation.layout.padding
|
||||
import androidx.compose.foundation.layout.width
|
||||
import androidx.compose.foundation.lazy.LazyColumn
|
||||
import androidx.compose.foundation.rememberScrollState
|
||||
import androidx.compose.foundation.shape.RoundedCornerShape
|
||||
import androidx.compose.foundation.verticalScroll
|
||||
import androidx.compose.material3.Button
|
||||
import androidx.compose.material3.ButtonDefaults
|
||||
import androidx.compose.material3.Checkbox
|
||||
import androidx.compose.material3.CircularProgressIndicator
|
||||
import androidx.compose.material3.DropdownMenu
|
||||
|
@ -42,7 +39,6 @@ import androidx.compose.material3.RadioButton
|
|||
import androidx.compose.material3.Scaffold
|
||||
import androidx.compose.material3.SnackbarHostState
|
||||
import androidx.compose.material3.Surface
|
||||
import androidx.compose.material3.Switch
|
||||
import androidx.compose.material3.Tab
|
||||
import androidx.compose.material3.TabRow
|
||||
import androidx.compose.material3.Text
|
||||
|
@ -67,18 +63,15 @@ import androidx.compose.ui.unit.dp
|
|||
import androidx.fragment.app.viewModels
|
||||
import androidx.navigation.fragment.findNavController
|
||||
import com.google.android.material.dialog.MaterialAlertDialogBuilder
|
||||
import org.signal.core.ui.Buttons
|
||||
import org.signal.core.ui.Dividers
|
||||
import org.signal.core.ui.Previews
|
||||
import org.signal.core.ui.Rows
|
||||
import org.signal.core.ui.SignalPreview
|
||||
import org.signal.core.ui.Snackbars
|
||||
import org.signal.core.ui.theme.SignalTheme
|
||||
import org.signal.core.util.bytes
|
||||
import org.signal.core.util.getLength
|
||||
import org.signal.core.util.roundedString
|
||||
import org.thoughtcrime.securesms.R
|
||||
import org.thoughtcrime.securesms.attachments.AttachmentId
|
||||
import org.thoughtcrime.securesms.backup.v2.MessageBackupTier
|
||||
import org.thoughtcrime.securesms.components.settings.app.internal.backup.InternalBackupPlaygroundViewModel.BackupState
|
||||
import org.thoughtcrime.securesms.components.settings.app.internal.backup.InternalBackupPlaygroundViewModel.BackupUploadState
|
||||
import org.thoughtcrime.securesms.components.settings.app.internal.backup.InternalBackupPlaygroundViewModel.ScreenState
|
||||
import org.thoughtcrime.securesms.compose.ComposeFragment
|
||||
import org.thoughtcrime.securesms.jobs.LocalBackupJob
|
||||
|
@ -87,46 +80,47 @@ import org.thoughtcrime.securesms.keyvalue.SignalStore
|
|||
class InternalBackupPlaygroundFragment : ComposeFragment() {
|
||||
|
||||
private val viewModel: InternalBackupPlaygroundViewModel by viewModels()
|
||||
private lateinit var exportFileLauncher: ActivityResultLauncher<Intent>
|
||||
private lateinit var importFileLauncher: ActivityResultLauncher<Intent>
|
||||
private lateinit var validateFileLauncher: ActivityResultLauncher<Intent>
|
||||
private lateinit var savePlaintextcopyLauncher: ActivityResultLauncher<Intent>
|
||||
private lateinit var saveEncryptedBackupToDiskLauncher: ActivityResultLauncher<Intent>
|
||||
private lateinit var savePlaintextBackupToDiskLauncher: ActivityResultLauncher<Intent>
|
||||
private lateinit var importEncryptedBackupFromDiskLauncher: ActivityResultLauncher<Intent>
|
||||
private lateinit var savePlaintextCopyLauncher: ActivityResultLauncher<Intent>
|
||||
|
||||
override fun onCreate(savedInstanceState: Bundle?) {
|
||||
super.onCreate(savedInstanceState)
|
||||
|
||||
exportFileLauncher = registerForActivityResult(ActivityResultContracts.StartActivityForResult()) { result ->
|
||||
saveEncryptedBackupToDiskLauncher = registerForActivityResult(ActivityResultContracts.StartActivityForResult()) { result ->
|
||||
if (result.resultCode == RESULT_OK) {
|
||||
result.data?.data?.let { uri ->
|
||||
requireContext().contentResolver.openOutputStream(uri)?.use { outputStream ->
|
||||
outputStream.write(viewModel.backupData!!)
|
||||
Toast.makeText(requireContext(), "Saved successfully", Toast.LENGTH_SHORT).show()
|
||||
} ?: Toast.makeText(requireContext(), "Failed to open output stream", Toast.LENGTH_SHORT).show()
|
||||
viewModel.exportEncrypted(
|
||||
openStream = { requireContext().contentResolver.openOutputStream(uri)!! },
|
||||
appendStream = { requireContext().contentResolver.openOutputStream(uri, "wa")!! }
|
||||
)
|
||||
} ?: Toast.makeText(requireContext(), "No URI selected", Toast.LENGTH_SHORT).show()
|
||||
}
|
||||
}
|
||||
|
||||
importFileLauncher = registerForActivityResult(ActivityResultContracts.StartActivityForResult()) { result ->
|
||||
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")!! }
|
||||
)
|
||||
} ?: Toast.makeText(requireContext(), "No URI selected", Toast.LENGTH_SHORT).show()
|
||||
}
|
||||
}
|
||||
|
||||
importEncryptedBackupFromDiskLauncher = registerForActivityResult(ActivityResultContracts.StartActivityForResult()) { result ->
|
||||
if (result.resultCode == RESULT_OK) {
|
||||
result.data?.data?.let { uri ->
|
||||
requireContext().contentResolver.getLength(uri)?.let { length ->
|
||||
viewModel.import(length) { requireContext().contentResolver.openInputStream(uri)!! }
|
||||
viewModel.importEncryptedBackup(length) { requireContext().contentResolver.openInputStream(uri)!! }
|
||||
}
|
||||
} ?: Toast.makeText(requireContext(), "No URI selected", Toast.LENGTH_SHORT).show()
|
||||
}
|
||||
}
|
||||
|
||||
validateFileLauncher = registerForActivityResult(ActivityResultContracts.StartActivityForResult()) { result ->
|
||||
if (result.resultCode == RESULT_OK) {
|
||||
result.data?.data?.let { uri ->
|
||||
requireContext().contentResolver.getLength(uri)?.let { length ->
|
||||
viewModel.validate(length) { requireContext().contentResolver.openInputStream(uri)!! }
|
||||
}
|
||||
} ?: Toast.makeText(requireContext(), "No URI selected", Toast.LENGTH_SHORT).show()
|
||||
}
|
||||
}
|
||||
|
||||
savePlaintextcopyLauncher = registerForActivityResult(ActivityResultContracts.StartActivityForResult()) { result ->
|
||||
savePlaintextCopyLauncher = registerForActivityResult(ActivityResultContracts.StartActivityForResult()) { result ->
|
||||
if (result.resultCode == RESULT_OK) {
|
||||
result.data?.data?.let { uri ->
|
||||
viewModel.fetchRemoteBackupAndWritePlaintext(requireContext().contentResolver.openOutputStream(uri))
|
||||
|
@ -152,54 +146,32 @@ class InternalBackupPlaygroundFragment : ComposeFragment() {
|
|||
mainContent = {
|
||||
Screen(
|
||||
state = state,
|
||||
onExportClicked = { viewModel.export() },
|
||||
onExportDirectoryClicked = { LocalBackupJob.enqueueArchive() },
|
||||
onImportMemoryClicked = { viewModel.import() },
|
||||
onImportFileClicked = {
|
||||
val intent = Intent().apply {
|
||||
action = Intent.ACTION_GET_CONTENT
|
||||
type = "application/octet-stream"
|
||||
addCategory(Intent.CATEGORY_OPENABLE)
|
||||
}
|
||||
|
||||
importFileLauncher.launch(intent)
|
||||
},
|
||||
onImportDirectoryClicked = {
|
||||
viewModel.import(SignalStore.settings.signalBackupDirectory!!)
|
||||
},
|
||||
onPlaintextClicked = { viewModel.onPlaintextToggled() },
|
||||
onSaveToDiskClicked = {
|
||||
onBackupTierSelected = { tier -> viewModel.onBackupTierSelected(tier) },
|
||||
onCheckRemoteBackupStateClicked = { viewModel.checkRemoteBackupState() },
|
||||
onEnqueueRemoteBackupClicked = { viewModel.triggerBackupJob() },
|
||||
onHaltAllBackupJobsClicked = { viewModel.haltAllJobs() },
|
||||
onValidateBackupClicked = { viewModel.validateBackup() },
|
||||
onSaveEncryptedBackupToDiskClicked = {
|
||||
val intent = Intent().apply {
|
||||
action = Intent.ACTION_CREATE_DOCUMENT
|
||||
type = "application/octet-stream"
|
||||
addCategory(Intent.CATEGORY_OPENABLE)
|
||||
putExtra(Intent.EXTRA_TITLE, "backup-${if (state.plaintext) "plaintext" else "encrypted"}-${System.currentTimeMillis()}.bin")
|
||||
putExtra(Intent.EXTRA_TITLE, "backup-encrypted-${System.currentTimeMillis()}.bin")
|
||||
}
|
||||
|
||||
exportFileLauncher.launch(intent)
|
||||
saveEncryptedBackupToDiskLauncher.launch(intent)
|
||||
},
|
||||
onUploadToRemoteClicked = { viewModel.uploadBackupToRemote() },
|
||||
onCheckRemoteBackupStateClicked = { viewModel.checkRemoteBackupState() },
|
||||
onValidateFileClicked = {
|
||||
onSavePlaintextBackupToDiskClicked = {
|
||||
val intent = Intent().apply {
|
||||
action = Intent.ACTION_GET_CONTENT
|
||||
action = Intent.ACTION_CREATE_DOCUMENT
|
||||
type = "application/octet-stream"
|
||||
addCategory(Intent.CATEGORY_OPENABLE)
|
||||
putExtra(Intent.EXTRA_TITLE, "backup-plaintext-${System.currentTimeMillis()}.bin")
|
||||
}
|
||||
|
||||
validateFileLauncher.launch(intent)
|
||||
savePlaintextBackupToDiskLauncher.launch(intent)
|
||||
},
|
||||
onTriggerBackupJobClicked = { viewModel.triggerBackupJob() },
|
||||
onWipeDataAndRestoreClicked = {
|
||||
MaterialAlertDialogBuilder(context)
|
||||
.setTitle("Are you sure?")
|
||||
.setMessage("This will delete all of your chats! Make sure you've finished a backup first, we don't check for you. Only do this on a test device!")
|
||||
.setPositiveButton("Wipe and restore") { _, _ -> viewModel.wipeAllDataAndRestoreFromRemote() }
|
||||
.show()
|
||||
},
|
||||
onBackupTierSelected = { tier -> viewModel.onBackupTierSelected(tier) },
|
||||
onHaltAllJobs = { viewModel.haltAllJobs() },
|
||||
onSavePlaintextCopy = {
|
||||
onSavePlaintextCopyOfRemoteBackupClicked = {
|
||||
val intent = Intent().apply {
|
||||
action = Intent.ACTION_CREATE_DOCUMENT
|
||||
type = "application/octet-stream"
|
||||
|
@ -207,7 +179,31 @@ class InternalBackupPlaygroundFragment : ComposeFragment() {
|
|||
putExtra(Intent.EXTRA_TITLE, "backup-plaintext-${System.currentTimeMillis()}.binproto")
|
||||
}
|
||||
|
||||
savePlaintextcopyLauncher.launch(intent)
|
||||
savePlaintextCopyLauncher.launch(intent)
|
||||
},
|
||||
onExportNewStyleLocalBackupClicked = { LocalBackupJob.enqueueArchive() },
|
||||
onWipeDataAndRestoreFromRemoteClicked = {
|
||||
MaterialAlertDialogBuilder(context)
|
||||
.setTitle("Are you sure?")
|
||||
.setMessage("This will delete all of your chats! Make sure you've finished a backup first, we don't check for you. Only do this on a test device!")
|
||||
.setPositiveButton("Wipe and restore") { _, _ -> viewModel.wipeAllDataAndRestoreFromRemote() }
|
||||
.show()
|
||||
},
|
||||
onImportEncryptedBackupFromDiskClicked = {
|
||||
val intent = Intent().apply {
|
||||
action = Intent.ACTION_GET_CONTENT
|
||||
type = "application/octet-stream"
|
||||
addCategory(Intent.CATEGORY_OPENABLE)
|
||||
}
|
||||
|
||||
importEncryptedBackupFromDiskLauncher.launch(intent)
|
||||
},
|
||||
onImportNewStyleLocalBackupClicked = {
|
||||
MaterialAlertDialogBuilder(context)
|
||||
.setTitle("Are you sure?")
|
||||
.setMessage("After you choose a file to import, this will delete all of your chats, then restore them from the file! Only do this on a test device!")
|
||||
.setPositiveButton("Wipe and restore") { _, _ -> viewModel.import(SignalStore.settings.signalBackupDirectory!!) }
|
||||
.show()
|
||||
}
|
||||
)
|
||||
},
|
||||
|
@ -290,21 +286,18 @@ fun Tabs(
|
|||
@Composable
|
||||
fun Screen(
|
||||
state: ScreenState,
|
||||
onExportClicked: () -> Unit = {},
|
||||
onExportDirectoryClicked: () -> Unit = {},
|
||||
onImportMemoryClicked: () -> Unit = {},
|
||||
onImportFileClicked: () -> Unit = {},
|
||||
onImportDirectoryClicked: () -> Unit = {},
|
||||
onPlaintextClicked: () -> Unit = {},
|
||||
onSaveToDiskClicked: () -> Unit = {},
|
||||
onValidateFileClicked: () -> Unit = {},
|
||||
onUploadToRemoteClicked: () -> Unit = {},
|
||||
onExportNewStyleLocalBackupClicked: () -> Unit = {},
|
||||
onImportNewStyleLocalBackupClicked: () -> Unit = {},
|
||||
onCheckRemoteBackupStateClicked: () -> Unit = {},
|
||||
onTriggerBackupJobClicked: () -> Unit = {},
|
||||
onWipeDataAndRestoreClicked: () -> Unit = {},
|
||||
onEnqueueRemoteBackupClicked: () -> Unit = {},
|
||||
onWipeDataAndRestoreFromRemoteClicked: () -> Unit = {},
|
||||
onBackupTierSelected: (MessageBackupTier?) -> Unit = {},
|
||||
onHaltAllJobs: () -> Unit = {},
|
||||
onSavePlaintextCopy: () -> Unit = {}
|
||||
onHaltAllBackupJobsClicked: () -> Unit = {},
|
||||
onSavePlaintextCopyOfRemoteBackupClicked: () -> Unit = {},
|
||||
onValidateBackupClicked: () -> Unit = {},
|
||||
onSaveEncryptedBackupToDiskClicked: () -> Unit = {},
|
||||
onSavePlaintextBackupToDiskClicked: () -> Unit = {},
|
||||
onImportEncryptedBackupFromDiskClicked: () -> Unit = {}
|
||||
) {
|
||||
val scrollState = rememberScrollState()
|
||||
val options = remember {
|
||||
|
@ -322,7 +315,6 @@ fun Screen(
|
|||
modifier = Modifier
|
||||
.fillMaxSize()
|
||||
.verticalScroll(scrollState)
|
||||
.padding(16.dp)
|
||||
) {
|
||||
Row(verticalAlignment = Alignment.CenterVertically) {
|
||||
Text("Tier", fontWeight = FontWeight.Bold)
|
||||
|
@ -339,191 +331,121 @@ fun Screen(
|
|||
|
||||
Dividers.Default()
|
||||
|
||||
Buttons.LargePrimary(
|
||||
onClick = onTriggerBackupJobClicked
|
||||
) {
|
||||
Text("Enqueue remote backup")
|
||||
}
|
||||
|
||||
Button(
|
||||
onClick = onWipeDataAndRestoreClicked,
|
||||
colors = ButtonDefaults.buttonColors(containerColor = Color(0xFFC33C00))
|
||||
) {
|
||||
Text("Wipe data and restore")
|
||||
}
|
||||
|
||||
Buttons.LargeTonal(
|
||||
onClick = onHaltAllJobs
|
||||
) {
|
||||
Text("Halt all backup jobs")
|
||||
}
|
||||
|
||||
Buttons.LargeTonal(
|
||||
onClick = onSavePlaintextCopy
|
||||
) {
|
||||
Text("Save plaintext copy of remote backup")
|
||||
}
|
||||
Rows.TextRow(
|
||||
text = {
|
||||
Text(
|
||||
text = state.statusMessage ?: "Status messages will appear here as you perform operations",
|
||||
style = MaterialTheme.typography.bodyMedium,
|
||||
color = MaterialTheme.colorScheme.onSurfaceVariant
|
||||
)
|
||||
}
|
||||
)
|
||||
|
||||
Dividers.Default()
|
||||
|
||||
Buttons.LargeTonal(
|
||||
Rows.TextRow(
|
||||
text = "Check remote backup state",
|
||||
label = "Get a summary of what your remote backup looks like (presence, size, etc).",
|
||||
onClick = onCheckRemoteBackupStateClicked
|
||||
)
|
||||
|
||||
Rows.TextRow(
|
||||
text = "Enqueue remote backup",
|
||||
label = "Schedules a job that will perform a routine remote backup.",
|
||||
onClick = onEnqueueRemoteBackupClicked
|
||||
)
|
||||
|
||||
Rows.TextRow(
|
||||
text = "Halt all backup jobs",
|
||||
label = "Stops all backup-related jobs to the best of our ability.",
|
||||
onClick = onHaltAllBackupJobsClicked
|
||||
)
|
||||
|
||||
Rows.TextRow(
|
||||
text = "Validate backup",
|
||||
label = "Generates a new backup and reports whether it passes validation. Does not save or upload anything.",
|
||||
onClick = onValidateBackupClicked
|
||||
)
|
||||
|
||||
Rows.TextRow(
|
||||
text = "Save encrypted backup to disk",
|
||||
label = "Generates an encrypted backup (the same thing you would upload) and saves it to your local disk.",
|
||||
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
|
||||
)
|
||||
|
||||
Rows.TextRow(
|
||||
text = "Save plaintext copy of remote backup",
|
||||
label = "Downloads your most recently uploaded backup and saves it to disk, plaintext and uncompressed.",
|
||||
onClick = onSavePlaintextCopyOfRemoteBackupClicked
|
||||
)
|
||||
|
||||
Rows.TextRow(
|
||||
text = "Perform a new-style local backup",
|
||||
label = "Creates a local backup (in your already-chosen backup directory) using the new on-disk backup format. This is the successor to the local backups existing users can build.",
|
||||
onClick = onExportNewStyleLocalBackupClicked
|
||||
)
|
||||
|
||||
Dividers.Default()
|
||||
|
||||
Text(
|
||||
text = "DANGER ZONE",
|
||||
color = Color.Red,
|
||||
fontWeight = FontWeight.Bold
|
||||
)
|
||||
Rows.TextRow(
|
||||
text = {
|
||||
Text(
|
||||
text = "The following operations are potentially destructive! Only use them if you know what you're doing.",
|
||||
style = MaterialTheme.typography.bodyMedium,
|
||||
color = MaterialTheme.colorScheme.onSurfaceVariant
|
||||
)
|
||||
}
|
||||
)
|
||||
|
||||
Rows.TextRow(
|
||||
text = "Wipe data and restore from remote",
|
||||
label = "Erases all content on your device, followed by a restore of your remote backup.",
|
||||
onClick = onWipeDataAndRestoreFromRemoteClicked
|
||||
)
|
||||
|
||||
Rows.TextRow(
|
||||
text = "Clear backup init flag",
|
||||
label = "Clears our local state around whether backups have been initialized or not. Will force us to make request to claim backupId and set public keys.",
|
||||
onClick = {
|
||||
SignalStore.backup.backupsInitialized = false
|
||||
}
|
||||
) {
|
||||
Text("Clear backup init flag")
|
||||
}
|
||||
)
|
||||
|
||||
Buttons.LargeTonal(
|
||||
Rows.TextRow(
|
||||
text = "Clear backup credentials",
|
||||
label = "Clears any cached backup credentials, for both our message and media backups.",
|
||||
onClick = {
|
||||
SignalStore.backup.messageCredentials.clearAll()
|
||||
SignalStore.backup.mediaCredentials.clearAll()
|
||||
}
|
||||
) {
|
||||
Text("Clear backup credentials")
|
||||
}
|
||||
)
|
||||
|
||||
Rows.TextRow(
|
||||
text = "Wipe all data and restore from file",
|
||||
label = "Erases all content on your device, followed by a restore of an encrypted backup selected from disk.",
|
||||
onClick = onImportEncryptedBackupFromDiskClicked
|
||||
)
|
||||
|
||||
Rows.TextRow(
|
||||
text = "Wipe all data and restore a new-style local backup",
|
||||
label = "Erases all content on your device, followed by a restore of a previously-generated new-style local backup.",
|
||||
onClick = onImportNewStyleLocalBackupClicked
|
||||
)
|
||||
|
||||
Dividers.Default()
|
||||
|
||||
Row(
|
||||
verticalAlignment = Alignment.CenterVertically
|
||||
) {
|
||||
StateLabel(text = "Plaintext?")
|
||||
Spacer(modifier = Modifier.width(8.dp))
|
||||
Switch(
|
||||
checked = state.plaintext,
|
||||
onCheckedChange = { onPlaintextClicked() }
|
||||
)
|
||||
}
|
||||
|
||||
Spacer(modifier = Modifier.height(8.dp))
|
||||
|
||||
Buttons.LargePrimary(
|
||||
onClick = onExportClicked,
|
||||
enabled = !state.backupState.inProgress
|
||||
) {
|
||||
Text("Export")
|
||||
}
|
||||
|
||||
Buttons.LargePrimary(
|
||||
onClick = onExportDirectoryClicked,
|
||||
enabled = !state.backupState.inProgress && state.canReadWriteBackupDirectory
|
||||
) {
|
||||
Text("Export to backup directory")
|
||||
}
|
||||
|
||||
Dividers.Default()
|
||||
|
||||
Buttons.LargeTonal(
|
||||
onClick = onImportMemoryClicked,
|
||||
enabled = state.backupState == BackupState.EXPORT_DONE
|
||||
) {
|
||||
Text("Import from memory")
|
||||
}
|
||||
Buttons.LargeTonal(
|
||||
onClick = onImportFileClicked
|
||||
) {
|
||||
Text("Import from file")
|
||||
}
|
||||
Buttons.LargeTonal(
|
||||
onClick = onImportDirectoryClicked,
|
||||
enabled = state.canReadWriteBackupDirectory
|
||||
) {
|
||||
Text("Import from backup directory")
|
||||
}
|
||||
|
||||
Buttons.LargeTonal(
|
||||
onClick = onValidateFileClicked
|
||||
) {
|
||||
Text("Validate file")
|
||||
}
|
||||
|
||||
Spacer(modifier = Modifier.height(16.dp))
|
||||
|
||||
when (state.backupState) {
|
||||
BackupState.NONE -> {
|
||||
StateLabel("")
|
||||
}
|
||||
|
||||
BackupState.EXPORT_IN_PROGRESS -> {
|
||||
StateLabel("Export in progress...")
|
||||
}
|
||||
|
||||
BackupState.EXPORT_DONE -> {
|
||||
StateLabel("Export complete. Sitting in memory. You can click 'Import' to import that data, save it to a file, or upload it to remote.")
|
||||
|
||||
Spacer(modifier = Modifier.height(8.dp))
|
||||
|
||||
Buttons.MediumTonal(onClick = onSaveToDiskClicked) {
|
||||
Text("Save to file")
|
||||
}
|
||||
}
|
||||
|
||||
BackupState.BACKUP_JOB_DONE -> {
|
||||
StateLabel("Backup complete and uploaded")
|
||||
}
|
||||
|
||||
BackupState.IMPORT_IN_PROGRESS -> {
|
||||
StateLabel("Import in progress...")
|
||||
}
|
||||
}
|
||||
|
||||
Dividers.Default()
|
||||
|
||||
Buttons.LargeTonal(
|
||||
onClick = onCheckRemoteBackupStateClicked
|
||||
) {
|
||||
Text("Check remote backup state")
|
||||
}
|
||||
|
||||
Spacer(modifier = Modifier.height(8.dp))
|
||||
|
||||
when (state.remoteBackupState) {
|
||||
is InternalBackupPlaygroundViewModel.RemoteBackupState.Available -> {
|
||||
StateLabel("Exists/allocated. ${state.remoteBackupState.response.mediaCount} media items, using ${state.remoteBackupState.response.usedSpace} bytes (${state.remoteBackupState.response.usedSpace.bytes.inMebiBytes.roundedString(3)} MiB)")
|
||||
}
|
||||
|
||||
InternalBackupPlaygroundViewModel.RemoteBackupState.GeneralError -> {
|
||||
StateLabel("Hit an unknown error. Check the logs.")
|
||||
}
|
||||
|
||||
InternalBackupPlaygroundViewModel.RemoteBackupState.NotFound -> {
|
||||
StateLabel("Not found.")
|
||||
}
|
||||
|
||||
InternalBackupPlaygroundViewModel.RemoteBackupState.Unknown -> {
|
||||
StateLabel("Hit the button above to check the state.")
|
||||
}
|
||||
}
|
||||
|
||||
Spacer(modifier = Modifier.height(8.dp))
|
||||
|
||||
Buttons.LargePrimary(
|
||||
onClick = onUploadToRemoteClicked,
|
||||
enabled = state.backupState == BackupState.EXPORT_DONE
|
||||
) {
|
||||
Text("Upload to remote")
|
||||
}
|
||||
|
||||
Spacer(modifier = Modifier.height(8.dp))
|
||||
|
||||
when (state.uploadState) {
|
||||
BackupUploadState.NONE -> {
|
||||
StateLabel("")
|
||||
}
|
||||
|
||||
BackupUploadState.UPLOAD_IN_PROGRESS -> {
|
||||
StateLabel("Upload in progress...")
|
||||
}
|
||||
|
||||
BackupUploadState.UPLOAD_DONE -> {
|
||||
StateLabel("Upload complete.")
|
||||
}
|
||||
|
||||
BackupUploadState.UPLOAD_FAILED -> {
|
||||
StateLabel("Upload failed.")
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@ -713,57 +635,18 @@ private data class MediaMultiSelectState(
|
|||
val expandedOption: AttachmentId? = null
|
||||
)
|
||||
|
||||
@Preview(name = "Light Theme", group = "screen", uiMode = Configuration.UI_MODE_NIGHT_NO)
|
||||
@Preview(name = "Dark Theme", group = "screen", uiMode = Configuration.UI_MODE_NIGHT_YES)
|
||||
@SignalPreview
|
||||
@Composable
|
||||
fun PreviewScreen() {
|
||||
SignalTheme {
|
||||
Surface {
|
||||
Screen(state = ScreenState(backupState = BackupState.NONE, plaintext = false))
|
||||
}
|
||||
Previews.Preview {
|
||||
Screen(state = ScreenState())
|
||||
}
|
||||
}
|
||||
|
||||
@Preview(name = "Light Theme", group = "screen", uiMode = Configuration.UI_MODE_NIGHT_NO)
|
||||
@Preview(name = "Dark Theme", group = "screen", uiMode = Configuration.UI_MODE_NIGHT_YES)
|
||||
@SignalPreview
|
||||
@Composable
|
||||
fun PreviewScreenExportInProgress() {
|
||||
SignalTheme {
|
||||
Surface {
|
||||
Screen(state = ScreenState(backupState = BackupState.EXPORT_IN_PROGRESS, plaintext = false))
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@Preview(name = "Light Theme", group = "screen", uiMode = Configuration.UI_MODE_NIGHT_NO)
|
||||
@Preview(name = "Dark Theme", group = "screen", uiMode = Configuration.UI_MODE_NIGHT_YES)
|
||||
@Composable
|
||||
fun PreviewScreenExportDone() {
|
||||
SignalTheme {
|
||||
Surface {
|
||||
Screen(state = ScreenState(backupState = BackupState.EXPORT_DONE, plaintext = false))
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@Preview(name = "Light Theme", group = "screen", uiMode = Configuration.UI_MODE_NIGHT_NO)
|
||||
@Preview(name = "Dark Theme", group = "screen", uiMode = Configuration.UI_MODE_NIGHT_YES)
|
||||
@Composable
|
||||
fun PreviewScreenImportInProgress() {
|
||||
SignalTheme {
|
||||
Surface {
|
||||
Screen(state = ScreenState(backupState = BackupState.IMPORT_IN_PROGRESS, plaintext = false))
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@Preview(name = "Light Theme", group = "screen", uiMode = Configuration.UI_MODE_NIGHT_NO)
|
||||
@Preview(name = "Dark Theme", group = "screen", uiMode = Configuration.UI_MODE_NIGHT_YES)
|
||||
@Composable
|
||||
fun PreviewScreenUploadInProgress() {
|
||||
SignalTheme {
|
||||
Surface {
|
||||
Screen(state = ScreenState(uploadState = BackupUploadState.UPLOAD_IN_PROGRESS, plaintext = false))
|
||||
}
|
||||
Previews.Preview {
|
||||
Screen(state = ScreenState(statusMessage = "Some random status message."))
|
||||
}
|
||||
}
|
||||
|
|
|
@ -18,14 +18,17 @@ import io.reactivex.rxjava3.disposables.CompositeDisposable
|
|||
import io.reactivex.rxjava3.kotlin.plusAssign
|
||||
import io.reactivex.rxjava3.kotlin.subscribeBy
|
||||
import io.reactivex.rxjava3.schedulers.Schedulers
|
||||
import org.signal.core.util.bytes
|
||||
import org.signal.core.util.concurrent.SignalExecutors
|
||||
import org.signal.core.util.copyTo
|
||||
import org.signal.core.util.logging.Log
|
||||
import org.signal.core.util.readNBytesOrThrow
|
||||
import org.signal.core.util.roundedString
|
||||
import org.signal.core.util.stream.LimitedInputStream
|
||||
import org.signal.libsignal.zkgroup.profiles.ProfileKey
|
||||
import org.thoughtcrime.securesms.attachments.AttachmentId
|
||||
import org.thoughtcrime.securesms.attachments.DatabaseAttachment
|
||||
import org.thoughtcrime.securesms.backup.v2.ArchiveValidator
|
||||
import org.thoughtcrime.securesms.backup.v2.BackupMetadata
|
||||
import org.thoughtcrime.securesms.backup.v2.BackupRepository
|
||||
import org.thoughtcrime.securesms.backup.v2.MessageBackupTier
|
||||
|
@ -54,7 +57,7 @@ import org.thoughtcrime.securesms.providers.BlobProvider
|
|||
import org.thoughtcrime.securesms.recipients.Recipient
|
||||
import org.whispersystems.signalservice.api.NetworkResult
|
||||
import org.whispersystems.signalservice.api.backup.MediaName
|
||||
import java.io.ByteArrayInputStream
|
||||
import java.io.FileOutputStream
|
||||
import java.io.IOException
|
||||
import java.io.InputStream
|
||||
import java.io.OutputStream
|
||||
|
@ -72,15 +75,10 @@ class InternalBackupPlaygroundViewModel : ViewModel() {
|
|||
private val TAG = Log.tag(InternalBackupPlaygroundViewModel::class)
|
||||
}
|
||||
|
||||
var backupData: ByteArray? = null
|
||||
|
||||
val disposables = CompositeDisposable()
|
||||
|
||||
private val _state: MutableState<ScreenState> = mutableStateOf(
|
||||
ScreenState(
|
||||
backupState = BackupState.NONE,
|
||||
uploadState = BackupUploadState.NONE,
|
||||
plaintext = false,
|
||||
canReadWriteBackupDirectory = SignalStore.settings.signalBackupDirectory?.let {
|
||||
val file = DocumentFile.fromTreeUri(AppDependencies.application, it)
|
||||
file != null && file.canWrite() && file.canRead()
|
||||
|
@ -93,66 +91,94 @@ class InternalBackupPlaygroundViewModel : ViewModel() {
|
|||
private val _mediaState: MutableState<MediaState> = mutableStateOf(MediaState())
|
||||
val mediaState: State<MediaState> = _mediaState
|
||||
|
||||
fun export() {
|
||||
_state.value = _state.value.copy(backupState = BackupState.EXPORT_IN_PROGRESS)
|
||||
val plaintext = _state.value.plaintext
|
||||
|
||||
disposables += Single.fromCallable { BackupRepository.debugExport(plaintext = plaintext) }
|
||||
fun exportEncrypted(openStream: () -> OutputStream, appendStream: () -> OutputStream) {
|
||||
_state.value = _state.value.copy(statusMessage = "Exporting encrypted backup to disk...")
|
||||
disposables += Single
|
||||
.fromCallable {
|
||||
BackupRepository.export(
|
||||
outputStream = openStream(),
|
||||
append = { bytes -> appendStream().use { it.write(bytes) } }
|
||||
)
|
||||
}
|
||||
.subscribeOn(Schedulers.io())
|
||||
.observeOn(AndroidSchedulers.mainThread())
|
||||
.subscribe { data ->
|
||||
backupData = data
|
||||
_state.value = _state.value.copy(backupState = BackupState.EXPORT_DONE)
|
||||
_state.value = _state.value.copy(statusMessage = "Encrypted backup complete!")
|
||||
}
|
||||
}
|
||||
|
||||
fun exportPlaintext(openStream: () -> OutputStream, appendStream: () -> OutputStream) {
|
||||
_state.value = _state.value.copy(statusMessage = "Exporting plaintext backup to disk...")
|
||||
disposables += Single
|
||||
.fromCallable {
|
||||
BackupRepository.export(
|
||||
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!")
|
||||
}
|
||||
}
|
||||
|
||||
fun validateBackup() {
|
||||
_state.value = _state.value.copy(statusMessage = "Exporting to a temporary file...")
|
||||
val tempFile = BlobProvider.getInstance().forNonAutoEncryptingSingleSessionOnDisk(AppDependencies.application)
|
||||
|
||||
disposables += Single
|
||||
.fromCallable {
|
||||
BackupRepository.export(
|
||||
outputStream = FileOutputStream(tempFile),
|
||||
append = { bytes -> tempFile.appendBytes(bytes) }
|
||||
)
|
||||
_state.value = _state.value.copy(statusMessage = "Export complete! Validating...")
|
||||
ArchiveValidator.validate(tempFile, SignalStore.backup.messageBackupKey)
|
||||
}
|
||||
.subscribeOn(Schedulers.io())
|
||||
.observeOn(AndroidSchedulers.mainThread())
|
||||
.subscribe { result ->
|
||||
val message = when (result) {
|
||||
is ArchiveValidator.ValidationResult.ReadError -> "Failed to read backup file!"
|
||||
ArchiveValidator.ValidationResult.Success -> "Validation passed!"
|
||||
is ArchiveValidator.ValidationResult.ValidationError -> {
|
||||
Log.w(TAG, "Validation failed!", result.exception)
|
||||
"Validation failed :( Check the logs for details."
|
||||
}
|
||||
}
|
||||
_state.value = _state.value.copy(statusMessage = message)
|
||||
}
|
||||
}
|
||||
|
||||
fun triggerBackupJob() {
|
||||
_state.value = _state.value.copy(backupState = BackupState.EXPORT_IN_PROGRESS)
|
||||
_state.value = _state.value.copy(statusMessage = "Upload job in progress...")
|
||||
|
||||
disposables += Single.fromCallable { AppDependencies.jobManager.runSynchronously(BackupMessagesJob(), 120_000) }
|
||||
.subscribeOn(Schedulers.io())
|
||||
.observeOn(AndroidSchedulers.mainThread())
|
||||
.subscribeBy {
|
||||
_state.value = _state.value.copy(backupState = BackupState.BACKUP_JOB_DONE)
|
||||
.subscribeBy { result ->
|
||||
_state.value = _state.value.copy(statusMessage = "Upload job complete! Result: ${result.takeIf { it.isPresent }?.get() ?: "N/A"}")
|
||||
}
|
||||
}
|
||||
|
||||
fun import() {
|
||||
backupData?.let {
|
||||
_state.value = _state.value.copy(backupState = BackupState.IMPORT_IN_PROGRESS)
|
||||
val plaintext = _state.value.plaintext
|
||||
|
||||
val self = Recipient.self()
|
||||
val selfData = BackupRepository.SelfData(self.aci.get(), self.pni.get(), self.e164.get(), ProfileKey(self.profileKey))
|
||||
|
||||
disposables += Single.fromCallable { BackupRepository.import(it.size.toLong(), { ByteArrayInputStream(it) }, selfData, plaintext = plaintext) }
|
||||
.subscribeOn(Schedulers.io())
|
||||
.observeOn(AndroidSchedulers.mainThread())
|
||||
.subscribeBy {
|
||||
backupData = null
|
||||
_state.value = _state.value.copy(backupState = BackupState.NONE)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
fun import(length: Long, inputStreamFactory: () -> InputStream) {
|
||||
_state.value = _state.value.copy(backupState = BackupState.IMPORT_IN_PROGRESS)
|
||||
val plaintext = _state.value.plaintext
|
||||
fun importEncryptedBackup(length: Long, inputStreamFactory: () -> InputStream) {
|
||||
_state.value = _state.value.copy(statusMessage = "Importing encrypted backup...")
|
||||
|
||||
val self = Recipient.self()
|
||||
val selfData = BackupRepository.SelfData(self.aci.get(), self.pni.get(), self.e164.get(), ProfileKey(self.profileKey))
|
||||
|
||||
disposables += Single.fromCallable { BackupRepository.import(length, inputStreamFactory, selfData, plaintext = plaintext) }
|
||||
disposables += Single.fromCallable { BackupRepository.import(length, inputStreamFactory, selfData, plaintext = false) }
|
||||
.subscribeOn(Schedulers.io())
|
||||
.observeOn(AndroidSchedulers.mainThread())
|
||||
.subscribeBy {
|
||||
backupData = null
|
||||
_state.value = _state.value.copy(backupState = BackupState.NONE)
|
||||
_state.value = _state.value.copy(statusMessage = "Encrypted backup import complete!")
|
||||
}
|
||||
}
|
||||
|
||||
fun import(uri: Uri) {
|
||||
_state.value = _state.value.copy(backupState = BackupState.IMPORT_IN_PROGRESS)
|
||||
_state.value = _state.value.copy(statusMessage = "Importing new-style local backup...")
|
||||
|
||||
val self = Recipient.self()
|
||||
val selfData = BackupRepository.SelfData(self.aci.get(), self.pni.get(), self.e164.get(), ProfileKey(self.profileKey))
|
||||
|
@ -170,42 +196,59 @@ class InternalBackupPlaygroundViewModel : ViewModel() {
|
|||
.subscribeOn(Schedulers.io())
|
||||
.observeOn(AndroidSchedulers.mainThread())
|
||||
.subscribeBy {
|
||||
backupData = null
|
||||
_state.value = _state.value.copy(backupState = BackupState.NONE)
|
||||
_state.value = _state.value.copy(statusMessage = "New-style local backup import complete!")
|
||||
}
|
||||
}
|
||||
|
||||
fun validate(length: Long, inputStreamFactory: () -> InputStream) {
|
||||
val self = Recipient.self()
|
||||
val selfData = BackupRepository.SelfData(self.aci.get(), self.pni.get(), self.e164.get(), ProfileKey(self.profileKey))
|
||||
|
||||
disposables += Single.fromCallable { BackupRepository.validate(length, inputStreamFactory, selfData) }
|
||||
.subscribeOn(Schedulers.io())
|
||||
.observeOn(AndroidSchedulers.mainThread())
|
||||
.subscribeBy {
|
||||
backupData = null
|
||||
_state.value = _state.value.copy(backupState = BackupState.NONE)
|
||||
}
|
||||
fun haltAllJobs() {
|
||||
AppDependencies.jobManager.cancelAllInQueue(BackfillDigestJob.QUEUE)
|
||||
AppDependencies.jobManager.cancelAllInQueue("ArchiveAttachmentJobs_0")
|
||||
AppDependencies.jobManager.cancelAllInQueue("ArchiveAttachmentJobs_1")
|
||||
AppDependencies.jobManager.cancelAllInQueue("ArchiveThumbnailUploadJob")
|
||||
AppDependencies.jobManager.cancelAllInQueue("BackupRestoreJob")
|
||||
AppDependencies.jobManager.cancelAllInQueue("__LOCAL_BACKUP__")
|
||||
}
|
||||
|
||||
fun onPlaintextToggled() {
|
||||
_state.value = _state.value.copy(plaintext = !_state.value.plaintext)
|
||||
}
|
||||
fun fetchRemoteBackupAndWritePlaintext(outputStream: OutputStream?) {
|
||||
check(outputStream != null)
|
||||
|
||||
fun uploadBackupToRemote() {
|
||||
_state.value = _state.value.copy(uploadState = BackupUploadState.UPLOAD_IN_PROGRESS)
|
||||
SignalExecutors.BOUNDED_IO.execute {
|
||||
Log.d(TAG, "Downloading file...")
|
||||
val tempBackupFile = BlobProvider.getInstance().forNonAutoEncryptingSingleSessionOnDisk(AppDependencies.application)
|
||||
|
||||
disposables += Single
|
||||
.fromCallable { BackupRepository.debugUploadBackupFile(backupData!!.inputStream(), backupData!!.size.toLong()) is NetworkResult.Success }
|
||||
.subscribeOn(Schedulers.io())
|
||||
.subscribe { success ->
|
||||
_state.value = _state.value.copy(uploadState = if (success) BackupUploadState.UPLOAD_DONE else BackupUploadState.UPLOAD_FAILED)
|
||||
when (val result = BackupRepository.downloadBackupFile(tempBackupFile)) {
|
||||
is NetworkResult.Success -> Log.i(TAG, "Download successful")
|
||||
else -> {
|
||||
Log.w(TAG, "Failed to download backup file", result.getCause())
|
||||
throw IOException(result.getCause())
|
||||
}
|
||||
}
|
||||
|
||||
val encryptedStream = tempBackupFile.inputStream()
|
||||
val iv = encryptedStream.readNBytesOrThrow(16)
|
||||
val backupKey = SignalStore.backup.messageBackupKey
|
||||
val keyMaterial = backupKey.deriveBackupSecrets(Recipient.self().aci.get())
|
||||
val cipher = Cipher.getInstance("AES/CBC/PKCS5Padding").apply {
|
||||
init(Cipher.DECRYPT_MODE, SecretKeySpec(keyMaterial.aesKey, "AES"), IvParameterSpec(iv))
|
||||
}
|
||||
|
||||
val plaintextStream = GZIPInputStream(
|
||||
CipherInputStream(
|
||||
LimitedInputStream(
|
||||
wrapped = encryptedStream,
|
||||
maxBytes = tempBackupFile.length() - MAC_SIZE
|
||||
),
|
||||
cipher
|
||||
)
|
||||
)
|
||||
|
||||
Log.d(TAG, "Copying...")
|
||||
plaintextStream.copyTo(outputStream)
|
||||
Log.d(TAG, "Done!")
|
||||
}
|
||||
}
|
||||
|
||||
fun checkRemoteBackupState() {
|
||||
_state.value = _state.value.copy(remoteBackupState = RemoteBackupState.Unknown)
|
||||
|
||||
disposables += Single
|
||||
.fromCallable {
|
||||
BackupRepository.restoreBackupTier(SignalStore.account.requireAci())
|
||||
|
@ -215,16 +258,18 @@ class InternalBackupPlaygroundViewModel : ViewModel() {
|
|||
.subscribe { result ->
|
||||
when {
|
||||
result is NetworkResult.Success -> {
|
||||
_state.value = _state.value.copy(remoteBackupState = RemoteBackupState.Available(result.result))
|
||||
_state.value = _state.value.copy(
|
||||
statusMessage = "Remote backup exists. ${result.result.mediaCount} media items, using ${result.result.usedSpace} bytes (${result.result.usedSpace.bytes.inMebiBytes.roundedString(3)} MiB)"
|
||||
)
|
||||
}
|
||||
|
||||
result is NetworkResult.StatusCodeError && result.code == 404 -> {
|
||||
_state.value = _state.value.copy(remoteBackupState = RemoteBackupState.NotFound)
|
||||
_state.value = _state.value.copy(statusMessage = "Remote backup does not exists.")
|
||||
}
|
||||
|
||||
else -> {
|
||||
Log.w(TAG, "Error checking remote backup state", result.getCause())
|
||||
_state.value = _state.value.copy(remoteBackupState = RemoteBackupState.GeneralError)
|
||||
_state.value = _state.value.copy(statusMessage = "Failed to fetch remote backup state.")
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@ -242,7 +287,7 @@ class InternalBackupPlaygroundViewModel : ViewModel() {
|
|||
}
|
||||
|
||||
private fun restoreFromRemote() {
|
||||
_state.value = _state.value.copy(backupState = BackupState.IMPORT_IN_PROGRESS)
|
||||
_state.value = _state.value.copy(statusMessage = "Importing from remote...")
|
||||
|
||||
disposables += Single.fromCallable {
|
||||
AppDependencies
|
||||
|
@ -255,7 +300,7 @@ class InternalBackupPlaygroundViewModel : ViewModel() {
|
|||
.subscribeOn(Schedulers.io())
|
||||
.observeOn(AndroidSchedulers.mainThread())
|
||||
.subscribeBy {
|
||||
_state.value = _state.value.copy(backupState = BackupState.NONE)
|
||||
_state.value = _state.value.copy(statusMessage = "Import complete!")
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -437,33 +482,21 @@ class InternalBackupPlaygroundViewModel : ViewModel() {
|
|||
}
|
||||
|
||||
data class ScreenState(
|
||||
val backupState: BackupState = BackupState.NONE,
|
||||
val uploadState: BackupUploadState = BackupUploadState.NONE,
|
||||
val remoteBackupState: RemoteBackupState = RemoteBackupState.Unknown,
|
||||
val plaintext: Boolean,
|
||||
val canReadWriteBackupDirectory: Boolean = false,
|
||||
val backupTier: MessageBackupTier? = null
|
||||
val backupTier: MessageBackupTier? = null,
|
||||
val statusMessage: String? = null
|
||||
)
|
||||
|
||||
enum class BackupState(val inProgress: Boolean = false) {
|
||||
NONE,
|
||||
EXPORT_IN_PROGRESS(true),
|
||||
EXPORT_DONE,
|
||||
BACKUP_JOB_DONE,
|
||||
IMPORT_IN_PROGRESS(true)
|
||||
}
|
||||
|
||||
enum class BackupUploadState(val inProgress: Boolean = false) {
|
||||
NONE,
|
||||
UPLOAD_IN_PROGRESS(true),
|
||||
UPLOAD_DONE,
|
||||
UPLOAD_FAILED
|
||||
}
|
||||
|
||||
sealed class RemoteBackupState {
|
||||
object Unknown : RemoteBackupState()
|
||||
object NotFound : RemoteBackupState()
|
||||
object GeneralError : RemoteBackupState()
|
||||
data class Available(val response: BackupMetadata) : RemoteBackupState()
|
||||
}
|
||||
|
||||
|
@ -532,52 +565,4 @@ class InternalBackupPlaygroundViewModel : ViewModel() {
|
|||
fun <T> MutableState<T>.set(update: T.() -> T) {
|
||||
this.value = this.value.update()
|
||||
}
|
||||
|
||||
fun haltAllJobs() {
|
||||
AppDependencies.jobManager.cancelAllInQueue(BackfillDigestJob.QUEUE)
|
||||
AppDependencies.jobManager.cancelAllInQueue("ArchiveAttachmentJobs_0")
|
||||
AppDependencies.jobManager.cancelAllInQueue("ArchiveAttachmentJobs_1")
|
||||
AppDependencies.jobManager.cancelAllInQueue("ArchiveThumbnailUploadJob")
|
||||
AppDependencies.jobManager.cancelAllInQueue("BackupRestoreJob")
|
||||
AppDependencies.jobManager.cancelAllInQueue("__LOCAL_BACKUP__")
|
||||
}
|
||||
|
||||
fun fetchRemoteBackupAndWritePlaintext(outputStream: OutputStream?) {
|
||||
check(outputStream != null)
|
||||
|
||||
SignalExecutors.BOUNDED_IO.execute {
|
||||
Log.d(TAG, "Downloading file...")
|
||||
val tempBackupFile = BlobProvider.getInstance().forNonAutoEncryptingSingleSessionOnDisk(AppDependencies.application)
|
||||
|
||||
when (val result = BackupRepository.downloadBackupFile(tempBackupFile)) {
|
||||
is NetworkResult.Success -> Log.i(TAG, "Download successful")
|
||||
else -> {
|
||||
Log.w(TAG, "Failed to download backup file", result.getCause())
|
||||
throw IOException(result.getCause())
|
||||
}
|
||||
}
|
||||
|
||||
val encryptedStream = tempBackupFile.inputStream()
|
||||
val iv = encryptedStream.readNBytesOrThrow(16)
|
||||
val backupKey = SignalStore.backup.messageBackupKey
|
||||
val keyMaterial = backupKey.deriveBackupSecrets(Recipient.self().aci.get())
|
||||
val cipher = Cipher.getInstance("AES/CBC/PKCS5Padding").apply {
|
||||
init(Cipher.DECRYPT_MODE, SecretKeySpec(keyMaterial.aesKey, "AES"), IvParameterSpec(iv))
|
||||
}
|
||||
|
||||
val plaintextStream = GZIPInputStream(
|
||||
CipherInputStream(
|
||||
LimitedInputStream(
|
||||
wrapped = encryptedStream,
|
||||
maxBytes = tempBackupFile.length() - MAC_SIZE
|
||||
),
|
||||
cipher
|
||||
)
|
||||
)
|
||||
|
||||
Log.d(TAG, "Copying...")
|
||||
plaintextStream.copyTo(outputStream)
|
||||
Log.d(TAG, "Done!")
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
Loading…
Add table
Reference in a new issue