Streamline export account data to not save to disk.

This commit is contained in:
Clark
2023-03-31 12:29:15 -04:00
committed by Alex Hart
parent 5e94c350ed
commit ad9337021c
9 changed files with 80 additions and 221 deletions

View File

@@ -492,9 +492,6 @@ public class ApplicationContext extends MultiDexApplication implements AppForegr
private void initializeCleanup() {
int deleted = SignalDatabase.attachments().deleteAbandonedPreuploadedAttachments();
Log.i(TAG, "Deleted " + deleted + " abandoned attachments.");
if (SignalStore.account().clearOldAccountDataReport()) {
Log.i(TAG, "Deleted " + deleted + " expired account data report.");
}
}
private void initializeGlideCodecs() {

View File

@@ -1,5 +1,7 @@
package org.thoughtcrime.securesms.components.settings.app.account.export
import android.os.Bundle
import android.view.View
import androidx.compose.foundation.Image
import androidx.compose.foundation.clickable
import androidx.compose.foundation.layout.Arrangement.Center
@@ -10,7 +12,6 @@ import androidx.compose.foundation.layout.padding
import androidx.compose.foundation.layout.size
import androidx.compose.foundation.layout.wrapContentSize
import androidx.compose.foundation.lazy.LazyColumn
import androidx.compose.material3.ButtonDefaults
import androidx.compose.material3.Card
import androidx.compose.material3.CircularProgressIndicator
import androidx.compose.material3.LocalTextStyle
@@ -32,7 +33,7 @@ import androidx.compose.ui.window.DialogProperties
import androidx.core.app.ShareCompat
import androidx.fragment.app.viewModels
import androidx.navigation.fragment.findNavController
import com.google.android.material.snackbar.Snackbar
import io.reactivex.rxjava3.android.schedulers.AndroidSchedulers
import org.signal.core.ui.Buttons
import org.signal.core.ui.Dialogs
import org.signal.core.ui.Rows
@@ -42,6 +43,7 @@ import org.signal.core.util.logging.Log
import org.thoughtcrime.securesms.R
import org.thoughtcrime.securesms.compose.ComposeFragment
import org.thoughtcrime.securesms.util.CommunicationActions
import org.thoughtcrime.securesms.util.LifecycleDisposable
import org.thoughtcrime.securesms.util.SpanUtil
class ExportAccountDataFragment : ComposeFragment() {
@@ -52,27 +54,29 @@ class ExportAccountDataFragment : ComposeFragment() {
private val viewModel: ExportAccountDataViewModel by viewModels()
private fun deleteReport() {
viewModel.deleteReport()
Snackbar.make(requireView(), R.string.ExportAccountDataFragment__delete_report_snackbar, Snackbar.LENGTH_SHORT).show()
private val disposables = LifecycleDisposable()
override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
super.onViewCreated(view, savedInstanceState)
disposables.bindTo(viewLifecycleOwner)
}
private fun exportReport() {
val report = viewModel.onGenerateReport()
ShareCompat.IntentBuilder(requireContext())
.setStream(report.uri)
.setType(report.mimeType)
.startChooser()
disposables += viewModel.onGenerateReport()
.observeOn(AndroidSchedulers.mainThread())
.subscribe { report ->
ShareCompat.IntentBuilder(requireContext())
.setStream(report.uri)
.setType(report.mimeType)
.startChooser()
}
}
private fun dismissExportDialog() {
viewModel.dismissExportConfirmationDialog()
}
private fun dismissDeleteDialog() {
viewModel.dismissDeleteConfirmationDialog()
}
private fun dismissDownloadErrorDialog() {
viewModel.dismissDownloadErrorDialog()
}
@@ -128,11 +132,7 @@ class ExportAccountDataFragment : ComposeFragment() {
}
item {
if (state.reportDownloaded) {
ExportReportOptions(exportAsJson = state.exportAsJson)
} else {
DownloadReportOptions()
}
ExportReportOptions(exportAsJson = state.exportAsJson)
}
}
if (state.downloadInProgress) {
@@ -141,8 +141,6 @@ class ExportAccountDataFragment : ComposeFragment() {
DownloadFailedDialog()
} else if (state.showExportDialog) {
ExportReportConfirmationDialog()
} else if (state.showDeleteDialog) {
DeleteReportConfirmationDialog()
}
}
}
@@ -175,25 +173,13 @@ class ExportAccountDataFragment : ComposeFragment() {
@Composable
private fun DownloadFailedDialog() {
Dialogs.SimpleMessageDialog(
message = stringResource(id = R.string.ExportAccountDataFragment__report_download_failed),
message = stringResource(id = R.string.ExportAccountDataFragment__check_network),
dismiss = stringResource(id = R.string.ExportAccountDataFragment__ok_action),
title = stringResource(id = R.string.ExportAccountDataFragment__report_generation_failed),
onDismiss = this::dismissDownloadErrorDialog
)
}
@Composable
private fun DeleteReportConfirmationDialog() {
Dialogs.SimpleAlertDialog(
title = stringResource(R.string.ExportAccountDataFragment__delete_report_confirmation),
body = stringResource(R.string.ExportAccountDataFragment__delete_report_confirmation_message),
confirm = stringResource(R.string.ExportAccountDataFragment__delete_report_action),
dismiss = stringResource(R.string.ExportAccountDataFragment__cancel_action),
onConfirm = this::deleteReport,
onDismiss = this::dismissDeleteDialog,
confirmColor = MaterialTheme.colorScheme.error
)
}
@Composable
private fun ExportReportConfirmationDialog() {
Dialogs.SimpleAlertDialog(
@@ -206,22 +192,6 @@ class ExportAccountDataFragment : ComposeFragment() {
)
}
@Composable
private fun DownloadReportOptions() {
Buttons.LargeTonal(
onClick = viewModel::onDownloadReport,
modifier = Modifier
.fillMaxWidth()
.padding(top = 52.dp, start = 32.dp, end = 32.dp)
) {
Text(
text = stringResource(R.string.ExportAccountDataFragment__download_report),
style = MaterialTheme.typography.labelLarge,
color = MaterialTheme.colorScheme.onPrimaryContainer
)
}
}
@Composable
private fun ExportReportOptions(exportAsJson: Boolean) {
Rows.RadioRow(
@@ -255,22 +225,8 @@ class ExportAccountDataFragment : ComposeFragment() {
)
}
Buttons.LargeTonal(
onClick = viewModel::showDeleteConfirmationDialog,
colors = ButtonDefaults.textButtonColors(contentColor = MaterialTheme.colorScheme.error),
modifier = Modifier
.fillMaxWidth()
.padding(top = 14.dp, start = 32.dp, end = 32.dp)
) {
Text(
text = stringResource(R.string.ExportAccountDataFragment__delete_report),
style = MaterialTheme.typography.labelLarge,
color = MaterialTheme.colorScheme.error
)
}
Text(
text = stringResource(id = R.string.ExportAccountDataFragment__report_deletion_disclaimer),
text = stringResource(id = R.string.ExportAccountDataFragment__report_not_stored_disclaimer),
style = MaterialTheme.typography.bodySmall,
textAlign = TextAlign.Start,
modifier = Modifier.padding(top = 16.dp, start = 24.dp, end = 28.dp, bottom = 20.dp)

View File

@@ -3,10 +3,9 @@ package org.thoughtcrime.securesms.components.settings.app.account.export
import android.net.Uri
import com.fasterxml.jackson.databind.JsonNode
import com.fasterxml.jackson.databind.node.ObjectNode
import io.reactivex.rxjava3.core.Completable
import io.reactivex.rxjava3.core.Single
import io.reactivex.rxjava3.schedulers.Schedulers
import org.thoughtcrime.securesms.dependencies.ApplicationDependencies
import org.thoughtcrime.securesms.keyvalue.SignalStore
import org.thoughtcrime.securesms.providers.BlobProvider
import org.thoughtcrime.securesms.util.JsonUtils
import org.whispersystems.signalservice.api.SignalServiceAccountManager
@@ -16,18 +15,17 @@ class ExportAccountDataRepository(
private val accountManager: SignalServiceAccountManager = ApplicationDependencies.getSignalServiceAccountManager()
) {
fun downloadAccountDataReport(): Completable {
return Completable.create {
fun downloadAccountDataReport(exportAsJson: Boolean): Single<ExportedReport> {
return Single.create {
try {
SignalStore.account().setAccountDataReport(accountManager.accountDataReport, System.currentTimeMillis())
it.onComplete()
it.onSuccess(generateAccountDataReport(accountManager.accountDataReport, exportAsJson))
} catch (e: IOException) {
it.onError(e)
}
}.subscribeOn(Schedulers.io())
}
fun generateAccountDataReport(exportAsJson: Boolean): ExportedReport {
private fun generateAccountDataReport(report: String, exportAsJson: Boolean): ExportedReport {
val mimeType: String
val fileName: String
if (exportAsJson) {
@@ -38,7 +36,7 @@ class ExportAccountDataRepository(
fileName = "account-data.txt"
}
val tree: JsonNode = JsonUtils.getMapper().readTree(SignalStore.account().accountDataReport)
val tree: JsonNode = JsonUtils.getMapper().readTree(report)
val dataStr = if (exportAsJson) {
(tree as ObjectNode).remove("text")
tree.toString()
@@ -50,7 +48,7 @@ class ExportAccountDataRepository(
.forData(dataStr.encodeToByteArray())
.withMimeType(mimeType)
.withFileName(fileName)
.createForSingleSessionInMemory()
.createForSingleUseInMemory()
return ExportedReport(mimeType = mimeType, uri = uri)
}

View File

@@ -1,10 +1,8 @@
package org.thoughtcrime.securesms.components.settings.app.account.export
data class ExportAccountDataState(
val reportDownloaded: Boolean,
val downloadInProgress: Boolean,
val exportAsJson: Boolean,
val showDownloadFailedDialog: Boolean = false,
val showDeleteDialog: Boolean = false,
val showExportDialog: Boolean = false
)

View File

@@ -4,10 +4,11 @@ import androidx.compose.runtime.State
import androidx.compose.runtime.mutableStateOf
import androidx.lifecycle.ViewModel
import io.reactivex.rxjava3.android.schedulers.AndroidSchedulers
import io.reactivex.rxjava3.core.Maybe
import io.reactivex.rxjava3.disposables.CompositeDisposable
import io.reactivex.rxjava3.kotlin.plusAssign
import io.reactivex.rxjava3.subjects.MaybeSubject
import org.signal.core.util.logging.Log
import org.thoughtcrime.securesms.keyvalue.SignalStore
class ExportAccountDataViewModel(
private val repository: ExportAccountDataRepository = ExportAccountDataRepository()
@@ -20,26 +21,25 @@ class ExportAccountDataViewModel(
private val disposables = CompositeDisposable()
private val _state = mutableStateOf(
ExportAccountDataState(reportDownloaded = false, downloadInProgress = false, exportAsJson = false)
ExportAccountDataState(downloadInProgress = false, exportAsJson = false)
)
val state: State<ExportAccountDataState> = _state
init {
_state.value = _state.value.copy(reportDownloaded = SignalStore.account().hasAccountDataReport())
}
fun onGenerateReport(): ExportAccountDataRepository.ExportedReport = repository.generateAccountDataReport(state.value.exportAsJson)
fun onDownloadReport() {
fun onGenerateReport(): Maybe<ExportAccountDataRepository.ExportedReport> {
_state.value = _state.value.copy(downloadInProgress = true)
disposables += repository.downloadAccountDataReport()
val maybe = MaybeSubject.create<ExportAccountDataRepository.ExportedReport>()
disposables += repository.downloadAccountDataReport(state.value.exportAsJson)
.observeOn(AndroidSchedulers.mainThread())
.subscribe({
_state.value = _state.value.copy(downloadInProgress = false, reportDownloaded = true)
.subscribe({ report ->
_state.value = _state.value.copy(downloadInProgress = false)
maybe.onSuccess(report)
}, { throwable ->
Log.e(TAG, throwable)
_state.value = _state.value.copy(downloadInProgress = false, showDownloadFailedDialog = true)
maybe.onComplete()
})
return maybe
}
fun setExportAsJson() {
@@ -50,14 +50,6 @@ class ExportAccountDataViewModel(
_state.value = _state.value.copy(exportAsJson = false)
}
fun showDeleteConfirmationDialog() {
_state.value = _state.value.copy(showDeleteDialog = true)
}
fun dismissDeleteConfirmationDialog() {
_state.value = _state.value.copy(showDeleteDialog = false)
}
fun dismissDownloadErrorDialog() {
_state.value = _state.value.copy(showDownloadFailedDialog = false)
}
@@ -70,11 +62,6 @@ class ExportAccountDataViewModel(
_state.value = _state.value.copy(showExportDialog = false)
}
fun deleteReport() {
SignalStore.account().deleteAccountDataReport()
_state.value = _state.value.copy(reportDownloaded = false)
}
override fun onCleared() {
disposables.dispose()
}

View File

@@ -26,7 +26,6 @@ import org.whispersystems.signalservice.api.push.PNI
import org.whispersystems.signalservice.api.push.ServiceIds
import org.whispersystems.signalservice.api.push.SignalServiceAddress
import java.security.SecureRandom
import kotlin.time.Duration.Companion.days
internal class AccountValues internal constructor(store: KeyValueStore) : SignalStoreValues(store) {
@@ -58,12 +57,6 @@ internal class AccountValues internal constructor(store: KeyValueStore) : Signal
private const val KEY_PNI_SIGNED_PREKEY_FAILURE_COUNT = "account.pni_signed_prekey_failure_count"
private const val KEY_PNI_NEXT_ONE_TIME_PREKEY_ID = "account.pni_next_one_time_prekey_id"
@VisibleForTesting
const val KEY_ACCOUNT_DATA_REPORT = "account.data_report"
@VisibleForTesting
const val KEY_ACCOUNT_DATA_REPORT_DOWNLOAD_TIME = "account.data_report_download_time"
@VisibleForTesting
const val KEY_E164 = "account.e164"
@@ -324,34 +317,6 @@ internal class AccountValues internal constructor(store: KeyValueStore) : Signal
}
}
val accountDataReport: String?
get() = getString(KEY_ACCOUNT_DATA_REPORT, null)
fun setAccountDataReport(report: String, downloadTime: Long) {
store.beginWrite()
.putString(KEY_ACCOUNT_DATA_REPORT, report)
.putLong(KEY_ACCOUNT_DATA_REPORT_DOWNLOAD_TIME, downloadTime)
.apply()
}
fun hasAccountDataReport(): Boolean = store.containsKey(KEY_ACCOUNT_DATA_REPORT)
fun clearOldAccountDataReport(): Boolean {
return if (hasAccountDataReport() && (getLong(KEY_ACCOUNT_DATA_REPORT_DOWNLOAD_TIME, 0) + 30.days.inWholeMilliseconds) < System.currentTimeMillis()) {
deleteAccountDataReport()
true
} else {
false
}
}
fun deleteAccountDataReport() {
store.beginWrite()
.remove(KEY_ACCOUNT_DATA_REPORT)
.remove(KEY_ACCOUNT_DATA_REPORT_DOWNLOAD_TIME)
.apply()
}
val deviceName: String?
get() = getString(KEY_DEVICE_NAME, null)