diff --git a/app/src/main/java/org/thoughtcrime/securesms/ApplicationContext.java b/app/src/main/java/org/thoughtcrime/securesms/ApplicationContext.java index 9d3d84597a..650d35c910 100644 --- a/app/src/main/java/org/thoughtcrime/securesms/ApplicationContext.java +++ b/app/src/main/java/org/thoughtcrime/securesms/ApplicationContext.java @@ -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() { diff --git a/app/src/main/java/org/thoughtcrime/securesms/components/settings/app/account/export/ExportAccountDataFragment.kt b/app/src/main/java/org/thoughtcrime/securesms/components/settings/app/account/export/ExportAccountDataFragment.kt index d5808b8d91..2835da464e 100644 --- a/app/src/main/java/org/thoughtcrime/securesms/components/settings/app/account/export/ExportAccountDataFragment.kt +++ b/app/src/main/java/org/thoughtcrime/securesms/components/settings/app/account/export/ExportAccountDataFragment.kt @@ -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) diff --git a/app/src/main/java/org/thoughtcrime/securesms/components/settings/app/account/export/ExportAccountDataRepository.kt b/app/src/main/java/org/thoughtcrime/securesms/components/settings/app/account/export/ExportAccountDataRepository.kt index 2801209ed4..4f12bded0d 100644 --- a/app/src/main/java/org/thoughtcrime/securesms/components/settings/app/account/export/ExportAccountDataRepository.kt +++ b/app/src/main/java/org/thoughtcrime/securesms/components/settings/app/account/export/ExportAccountDataRepository.kt @@ -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 { + 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) } diff --git a/app/src/main/java/org/thoughtcrime/securesms/components/settings/app/account/export/ExportAccountDataState.kt b/app/src/main/java/org/thoughtcrime/securesms/components/settings/app/account/export/ExportAccountDataState.kt index 4092e8b78b..cedb6084b4 100644 --- a/app/src/main/java/org/thoughtcrime/securesms/components/settings/app/account/export/ExportAccountDataState.kt +++ b/app/src/main/java/org/thoughtcrime/securesms/components/settings/app/account/export/ExportAccountDataState.kt @@ -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 ) diff --git a/app/src/main/java/org/thoughtcrime/securesms/components/settings/app/account/export/ExportAccountDataViewModel.kt b/app/src/main/java/org/thoughtcrime/securesms/components/settings/app/account/export/ExportAccountDataViewModel.kt index c03964b704..f92625abbf 100644 --- a/app/src/main/java/org/thoughtcrime/securesms/components/settings/app/account/export/ExportAccountDataViewModel.kt +++ b/app/src/main/java/org/thoughtcrime/securesms/components/settings/app/account/export/ExportAccountDataViewModel.kt @@ -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 = _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 { _state.value = _state.value.copy(downloadInProgress = true) - disposables += repository.downloadAccountDataReport() + val maybe = MaybeSubject.create() + 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() } diff --git a/app/src/main/java/org/thoughtcrime/securesms/keyvalue/AccountValues.kt b/app/src/main/java/org/thoughtcrime/securesms/keyvalue/AccountValues.kt index 4805bacc76..55db4a89c5 100644 --- a/app/src/main/java/org/thoughtcrime/securesms/keyvalue/AccountValues.kt +++ b/app/src/main/java/org/thoughtcrime/securesms/keyvalue/AccountValues.kt @@ -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) diff --git a/app/src/main/res/values/strings.xml b/app/src/main/res/values/strings.xml index 0e54b07dec..084da8d916 100644 --- a/app/src/main/res/values/strings.xml +++ b/app/src/main/res/values/strings.xml @@ -3980,21 +3980,17 @@ Require your Signal PIN to register your phone number with Signal again Change phone number - Request account data + Your account data Your account data - Download and export a report of your Signal account data. This report does not include any messages or media. %1$s + Export a report of your Signal account data. This report does not include any messages or media. %1$s Learn more - - Download report Export report - - Delete report Export as TXT @@ -4010,8 +4006,10 @@ OK - - Your report could not be downloaded due to a network error. Check your connection and try again. + + Couldn\'t generate report + + Check your connection and try again. Export data? @@ -4020,19 +4018,10 @@ Export - - Delete - - Delete report? - - This will not delete any data from your account. - - - Report deleted - Downloading report… - - Your report will be available for export for 30 days and will then be automatically deleted. + Generating report… + + Your report is generated only at the time of export and is not stored by Signal on your device. Use this to change your current phone number to a new phone number. You can’t undo this change.\n\nBefore continuing, make sure your new number can receive SMS or calls. diff --git a/app/src/test/java/org/thoughtcrime/securesms/components/settings/app/account/export/ExportAccountDataTest.kt b/app/src/test/java/org/thoughtcrime/securesms/components/settings/app/account/export/ExportAccountDataTest.kt index f04dbb9506..c7b9184a1d 100644 --- a/app/src/test/java/org/thoughtcrime/securesms/components/settings/app/account/export/ExportAccountDataTest.kt +++ b/app/src/test/java/org/thoughtcrime/securesms/components/settings/app/account/export/ExportAccountDataTest.kt @@ -6,7 +6,7 @@ import com.fasterxml.jackson.core.JsonParseException import io.mockk.every import io.mockk.mockk import io.reactivex.rxjava3.android.plugins.RxAndroidPlugins -import io.reactivex.rxjava3.core.Completable +import io.reactivex.rxjava3.core.Single import io.reactivex.rxjava3.schedulers.TestScheduler import org.junit.Assert.assertEquals import org.junit.Assert.assertFalse @@ -19,13 +19,9 @@ import org.robolectric.RobolectricTestRunner import org.robolectric.annotation.Config import org.thoughtcrime.securesms.dependencies.ApplicationDependencies import org.thoughtcrime.securesms.dependencies.MockApplicationDependencyProvider -import org.thoughtcrime.securesms.keyvalue.AccountValues -import org.thoughtcrime.securesms.keyvalue.KeyValueDataSet -import org.thoughtcrime.securesms.keyvalue.KeyValueStore -import org.thoughtcrime.securesms.keyvalue.MockKeyValuePersistentStorage -import org.thoughtcrime.securesms.keyvalue.SignalStore import org.thoughtcrime.securesms.providers.BlobProvider import org.thoughtcrime.securesms.util.JsonUtils +import org.whispersystems.signalservice.api.SignalServiceAccountManager import java.io.IOException @RunWith(RobolectricTestRunner::class) @@ -75,86 +71,57 @@ class ExportAccountDataTest { } } - @Test - fun `Successful download flow with progress states and delete flow`() { - val dataSet = KeyValueDataSet() - val scheduler = TestScheduler() - val mockRepository: ExportAccountDataRepository = mockk { - every { downloadAccountDataReport() } returns Completable.create { - dataSet.putString(AccountValues.KEY_ACCOUNT_DATA_REPORT, mockJson) - dataSet.putLong(AccountValues.KEY_ACCOUNT_DATA_REPORT_DOWNLOAD_TIME, 123456L) - it.onComplete() - }.subscribeOn(scheduler) - } - - SignalStore.inject(KeyValueStore(MockKeyValuePersistentStorage.withDataSet(dataSet))) - RxAndroidPlugins.setMainThreadSchedulerHandler { - scheduler - } - - val viewModel = ExportAccountDataViewModel(mockRepository) - assertFalse(viewModel.state.value.reportDownloaded) - viewModel.onDownloadReport() - assertTrue(viewModel.state.value.downloadInProgress) - scheduler.triggerActions() - assertTrue(viewModel.state.value.reportDownloaded) - assertFalse(viewModel.state.value.downloadInProgress) - - assertEquals(SignalStore.account().accountDataReport, mockJson) - viewModel.deleteReport() - assertEquals(SignalStore.account().accountDataReport, null) - assertFalse(viewModel.state.value.reportDownloaded) - } - @Test fun `Export json without text field`() { - val dataSet = KeyValueDataSet() - dataSet.putString(AccountValues.KEY_ACCOUNT_DATA_REPORT, mockJson) - SignalStore.inject(KeyValueStore(MockKeyValuePersistentStorage.withDataSet(dataSet))) - - val mockRepository: ExportAccountDataRepository = mockk { - every { generateAccountDataReport(any()) } answers { callOriginal() } + val scheduler = TestScheduler() + val accountManager: SignalServiceAccountManager = mockk { + every { accountDataReport } returns mockJson } + val mockRepository = ExportAccountDataRepository(accountManager) val viewModel = ExportAccountDataViewModel(mockRepository) viewModel.setExportAsTxt() - val txtReport = viewModel.onGenerateReport() - assertEquals(txtReport.mimeType, "text/plain") - assertThrows(JsonParseException::class.java) { - JsonUtils.getMapper().readTree(BlobProvider.getInstance().getMemoryBlob(txtReport.uri) as ByteArray) - } - + viewModel.onGenerateReport() + .observeOn(scheduler) + .subscribe { txtReport -> + assertEquals(txtReport.mimeType, "text/plain") + assertThrows(JsonParseException::class.java) { + JsonUtils.getMapper().readTree(BlobProvider.getInstance().getMemoryBlob(txtReport.uri) as ByteArray) + } + } + scheduler.triggerActions() viewModel.setExportAsJson() - val jsonReport = viewModel.onGenerateReport() - assertEquals(jsonReport.mimeType, "application/json") - val json = JsonUtils.getMapper().readTree(BlobProvider.getInstance().getMemoryBlob(jsonReport.uri) as ByteArray) - assertFalse(json.has("text")) - assertTrue(json.has("data")) - assertTrue(json.has("reportId")) - assertTrue(json.has("reportTimestamp")) + viewModel.onGenerateReport() + .observeOn(scheduler) + .subscribe { jsonReport -> + assertEquals(jsonReport.mimeType, "application/json") + val json = JsonUtils.getMapper().readTree(BlobProvider.getInstance().getMemoryBlob(jsonReport.uri) as ByteArray) + assertFalse(json.has("text")) + assertTrue(json.has("data")) + assertTrue(json.has("reportId")) + assertTrue(json.has("reportTimestamp")) + } + scheduler.triggerActions() } @Test fun `Failed download error flow`() { - val dataSet = KeyValueDataSet() val scheduler = TestScheduler() val mockRepository: ExportAccountDataRepository = mockk { - every { downloadAccountDataReport() } returns Completable.create { + every { downloadAccountDataReport(any()) } returns Single.create { it.onError(IOException()) }.subscribeOn(scheduler) } - SignalStore.inject(KeyValueStore(MockKeyValuePersistentStorage.withDataSet(dataSet))) RxAndroidPlugins.setMainThreadSchedulerHandler { scheduler } val viewModel = ExportAccountDataViewModel(mockRepository) - assertEquals(viewModel.state.value.reportDownloaded, false) - viewModel.onDownloadReport() + assertEquals(viewModel.state.value.downloadInProgress, false) + viewModel.onGenerateReport() assertEquals(viewModel.state.value.downloadInProgress, true) scheduler.triggerActions() - assertEquals(viewModel.state.value.reportDownloaded, false) assertEquals(viewModel.state.value.downloadInProgress, false) assertEquals(viewModel.state.value.showDownloadFailedDialog, true) diff --git a/core-ui/src/main/java/org/signal/core/ui/Dialogs.kt b/core-ui/src/main/java/org/signal/core/ui/Dialogs.kt index ad9b055258..fafd62925d 100644 --- a/core-ui/src/main/java/org/signal/core/ui/Dialogs.kt +++ b/core-ui/src/main/java/org/signal/core/ui/Dialogs.kt @@ -18,11 +18,13 @@ object Dialogs { dismiss: String, onDismiss: () -> Unit, modifier: Modifier = Modifier, + title: String? = null, dismissColor: Color = Color.Unspecified, properties: DialogProperties = DialogProperties() ) { androidx.compose.material3.AlertDialog( onDismissRequest = onDismiss, + title = if (title == null) null else { { Text(text = title) } }, text = { Text(text = message) }, confirmButton = { TextButton(onClick = {