Skip to content

Commit

Permalink
Streamline export account data to not save to disk.
Browse files Browse the repository at this point in the history
  • Loading branch information
clark-signal authored and alex-signal committed Apr 4, 2023
1 parent 5e94c35 commit ad93370
Show file tree
Hide file tree
Showing 9 changed files with 80 additions and 221 deletions.
Original file line number Diff line number Diff line change
Expand Up @@ -492,9 +492,6 @@ private void checkIsGooglePayReady() {
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() {
Expand Down
Original file line number Diff line number Diff line change
@@ -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
Expand All @@ -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
Expand All @@ -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
Expand All @@ -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() {
Expand All @@ -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()
}
Expand Down Expand Up @@ -128,11 +132,7 @@ class ExportAccountDataFragment : ComposeFragment() {
}

item {
if (state.reportDownloaded) {
ExportReportOptions(exportAsJson = state.exportAsJson)
} else {
DownloadReportOptions()
}
ExportReportOptions(exportAsJson = state.exportAsJson)
}
}
if (state.downloadInProgress) {
Expand All @@ -141,8 +141,6 @@ class ExportAccountDataFragment : ComposeFragment() {
DownloadFailedDialog()
} else if (state.showExportDialog) {
ExportReportConfirmationDialog()
} else if (state.showDeleteDialog) {
DeleteReportConfirmationDialog()
}
}
}
Expand Down Expand Up @@ -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(
Expand All @@ -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(
Expand Down Expand Up @@ -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)
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand All @@ -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) {
Expand All @@ -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()
Expand All @@ -50,7 +48,7 @@ class ExportAccountDataRepository(
.forData(dataStr.encodeToByteArray())
.withMimeType(mimeType)
.withFileName(fileName)
.createForSingleSessionInMemory()
.createForSingleUseInMemory()

return ExportedReport(mimeType = mimeType, uri = uri)
}
Expand Down
Original file line number Diff line number Diff line change
@@ -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
)
Original file line number Diff line number Diff line change
Expand Up @@ -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()
Expand All @@ -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() {
Expand All @@ -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)
}
Expand All @@ -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()
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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) {

Expand Down Expand Up @@ -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"

Expand Down Expand Up @@ -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)

Expand Down

0 comments on commit ad93370

Please sign in to comment.