From 690e1e60ba2718652235070e5887d9512c71ebbe Mon Sep 17 00:00:00 2001 From: Cody Henthorne Date: Wed, 19 Oct 2022 22:11:31 -0400 Subject: [PATCH] Improve error reporting for SMS export. --- .../securesms/database/MessageDatabase.java | 13 +- .../securesms/database/MmsDatabase.java | 19 ++ .../securesms/database/SmsDatabase.java | 12 + .../flow/ExportSmsCompleteFragment.kt | 3 +- .../flow/ExportSmsFullErrorFragment.kt | 44 +++ .../ExportSmsPartiallyCompleteFragment.kt | 57 ++++ .../flow/ExportYourSmsMessagesFragment.kt | 5 +- .../flow/ExportingSmsMessagesFragment.kt | 41 ++- .../exporter/flow/ExportingSmsRepository.kt | 38 +++ .../exporter/flow/SmsExportActivity.kt | 16 ++ .../exporter/flow/SmsExportHelpFragment.kt | 30 +++ .../securesms/help/HelpFragment.java | 5 +- .../main/res/drawable-night/choose_signal.xml | 26 +- app/src/main/res/drawable-night/complete.xml | 17 ++ .../main/res/drawable-night/export_sms.xml | 14 +- .../res/drawable-night/sms_export_error.xml | 12 + .../sms_export_partial_complete.xml | 23 ++ .../main/res/drawable-night/sms_message.xml | 6 +- app/src/main/res/drawable/choose_signal.xml | 26 +- app/src/main/res/drawable/complete.xml | 17 ++ app/src/main/res/drawable/export_sms.xml | 18 +- .../main/res/drawable/sms_export_error.xml | 12 + .../drawable/sms_export_partial_complete.xml | 23 ++ app/src/main/res/drawable/sms_message.xml | 8 +- .../layout/export_sms_full_error_fragment.xml | 94 +++++++ ...export_sms_partially_complete_fragment.xml | 250 ++++++++++++++++++ .../res/layout/sms_export_help_fragment.xml | 22 ++ app/src/main/res/navigation/sms_export.xml | 98 ++++++- app/src/main/res/values/strings.xml | 29 +- core-util/build.gradle | 1 + .../org/signal/core/util/CursorExtensions.kt | 11 + .../core/util/concurrent/SimpleTask.java | 33 +++ .../signal/smsexporter/SmsExportService.kt | 5 + 33 files changed, 933 insertions(+), 95 deletions(-) create mode 100644 app/src/main/java/org/thoughtcrime/securesms/exporter/flow/ExportSmsFullErrorFragment.kt create mode 100644 app/src/main/java/org/thoughtcrime/securesms/exporter/flow/ExportSmsPartiallyCompleteFragment.kt create mode 100644 app/src/main/java/org/thoughtcrime/securesms/exporter/flow/ExportingSmsRepository.kt create mode 100644 app/src/main/java/org/thoughtcrime/securesms/exporter/flow/SmsExportHelpFragment.kt create mode 100644 app/src/main/res/drawable-night/complete.xml create mode 100644 app/src/main/res/drawable-night/sms_export_error.xml create mode 100644 app/src/main/res/drawable-night/sms_export_partial_complete.xml create mode 100644 app/src/main/res/drawable/complete.xml create mode 100644 app/src/main/res/drawable/sms_export_error.xml create mode 100644 app/src/main/res/drawable/sms_export_partial_complete.xml create mode 100644 app/src/main/res/layout/export_sms_full_error_fragment.xml create mode 100644 app/src/main/res/layout/export_sms_partially_complete_fragment.xml create mode 100644 app/src/main/res/layout/sms_export_help_fragment.xml diff --git a/app/src/main/java/org/thoughtcrime/securesms/database/MessageDatabase.java b/app/src/main/java/org/thoughtcrime/securesms/database/MessageDatabase.java index 90c6be1c3..1f7e22f19 100644 --- a/app/src/main/java/org/thoughtcrime/securesms/database/MessageDatabase.java +++ b/app/src/main/java/org/thoughtcrime/securesms/database/MessageDatabase.java @@ -99,6 +99,7 @@ public abstract class MessageDatabase extends Database implements MmsSmsColumns, public abstract List getProfileChangeDetailsRecords(long threadId, long afterTimestamp); public abstract Set getAllRateLimitedMessageIds(); public abstract Cursor getUnexportedInsecureMessages(int limit); + public abstract long getUnexportedInsecureMessagesEstimatedSize(); public abstract void deleteExportedMessages(); public abstract void markExpireStarted(long messageId); @@ -380,15 +381,15 @@ public abstract class MessageDatabase extends Database implements MmsSmsColumns, } protected String getInsecureMessageClause(long threadId) { - String isSent = "(" + getTypeField() + " & " + Types.BASE_TYPE_MASK + ") = " + Types.BASE_SENT_TYPE; - String isReceived = "(" + getTypeField() + " & " + Types.BASE_TYPE_MASK + ") = " + Types.BASE_INBOX_TYPE; - String isSecure = "(" + getTypeField() + " & " + (Types.SECURE_MESSAGE_BIT | Types.PUSH_MESSAGE_BIT) + ")"; - String isNotSecure = "(" + getTypeField() + " <= " + (Types.BASE_TYPE_MASK | Types.MESSAGE_ATTRIBUTE_MASK) + ")"; + String isSent = "(" + getTableName() + "." + getTypeField() + " & " + Types.BASE_TYPE_MASK + ") = " + Types.BASE_SENT_TYPE; + String isReceived = "(" + getTableName() + "." + getTypeField() + " & " + Types.BASE_TYPE_MASK + ") = " + Types.BASE_INBOX_TYPE; + String isSecure = "(" + getTableName() + "." + getTypeField() + " & " + (Types.SECURE_MESSAGE_BIT | Types.PUSH_MESSAGE_BIT) + ")"; + String isNotSecure = "(" + getTableName() + "." + getTypeField() + " <= " + (Types.BASE_TYPE_MASK | Types.MESSAGE_ATTRIBUTE_MASK) + ")"; String whereClause = String.format(Locale.ENGLISH, "(%s OR %s) AND NOT %s AND %s", isSent, isReceived, isSecure, isNotSecure); if (threadId != -1) { - whereClause += " AND " + THREAD_ID + " = " + threadId; + whereClause += " AND " + getTableName() + "." + THREAD_ID + " = " + threadId; } return whereClause; @@ -417,7 +418,7 @@ public abstract class MessageDatabase extends Database implements MmsSmsColumns, SQLiteDatabaseExtensionsKt.update(getWritableDatabase(), getTableName()) .values(values) - .where(EXPORTED + " < ?", MessageExportStatus.UNEXPORTED.getCode()) + .where(EXPORTED + " < ?", MessageExportStatus.UNEXPORTED) .run(); } diff --git a/app/src/main/java/org/thoughtcrime/securesms/database/MmsDatabase.java b/app/src/main/java/org/thoughtcrime/securesms/database/MmsDatabase.java index 8efc0726c..f5e69642f 100644 --- a/app/src/main/java/org/thoughtcrime/securesms/database/MmsDatabase.java +++ b/app/src/main/java/org/thoughtcrime/securesms/database/MmsDatabase.java @@ -36,6 +36,7 @@ import net.zetetic.database.sqlcipher.SQLiteStatement; import org.json.JSONArray; import org.json.JSONException; import org.json.JSONObject; +import org.signal.core.util.CursorExtensionsKt; import org.signal.core.util.CursorUtil; import org.signal.core.util.SQLiteDatabaseExtensionsKt; import org.signal.core.util.SqlUtil; @@ -2463,6 +2464,24 @@ public class MmsDatabase extends MessageDatabase { ); } + @Override + public long getUnexportedInsecureMessagesEstimatedSize() { + Cursor messageTextSize = SQLiteDatabaseExtensionsKt.select(getReadableDatabase(), "SUM(LENGTH(" + BODY + "))") + .from(TABLE_NAME) + .where(getInsecureMessageClause() + " AND " + EXPORTED + " < ?", MessageExportStatus.EXPORTED) + .run(); + + long bodyTextSize = CursorExtensionsKt.readToSingleLong(messageTextSize); + + String select = "SUM(" + AttachmentDatabase.TABLE_NAME + "." + AttachmentDatabase.SIZE + ") AS s"; + String fromJoin = TABLE_NAME + " INNER JOIN " + AttachmentDatabase.TABLE_NAME + " ON " + TABLE_NAME + "." + ID + " = " + AttachmentDatabase.TABLE_NAME + "." + AttachmentDatabase.MMS_ID; + String where = getInsecureMessageClause() + " AND " + EXPORTED + " < " + MessageExportStatus.EXPORTED.serialize(); + + long fileSize = CursorExtensionsKt.readToSingleLong(getReadableDatabase().rawQuery("SELECT " + select + " FROM " + fromJoin + " WHERE " + where, null)); + + return bodyTextSize + fileSize; + } + @Override public void deleteExportedMessages() { beginTransaction(); diff --git a/app/src/main/java/org/thoughtcrime/securesms/database/SmsDatabase.java b/app/src/main/java/org/thoughtcrime/securesms/database/SmsDatabase.java index e5aaa2ab5..0dabf3dc1 100644 --- a/app/src/main/java/org/thoughtcrime/securesms/database/SmsDatabase.java +++ b/app/src/main/java/org/thoughtcrime/securesms/database/SmsDatabase.java @@ -33,11 +33,13 @@ import com.google.protobuf.InvalidProtocolBufferException; import net.zetetic.database.sqlcipher.SQLiteStatement; +import org.signal.core.util.CursorExtensionsKt; import org.signal.core.util.CursorUtil; import org.signal.core.util.SQLiteDatabaseExtensionsKt; import org.signal.core.util.SqlUtil; import org.signal.core.util.logging.Log; import org.signal.libsignal.protocol.util.Pair; +import org.thoughtcrime.securesms.components.settings.app.chats.sms.SmsExportState; import org.thoughtcrime.securesms.database.documents.IdentityKeyMismatch; import org.thoughtcrime.securesms.database.documents.IdentityKeyMismatchSet; import org.thoughtcrime.securesms.database.documents.NetworkFailure; @@ -921,6 +923,16 @@ public class SmsDatabase extends MessageDatabase { ); } + @Override + public long getUnexportedInsecureMessagesEstimatedSize() { + Cursor cursor = SQLiteDatabaseExtensionsKt.select(getReadableDatabase(), "SUM(LENGTH(" + BODY + "))") + .from(TABLE_NAME) + .where(getInsecureMessageClause() + " AND " + EXPORTED + " < ?", MessageExportStatus.EXPORTED) + .run(); + + return CursorExtensionsKt.readToSingleLong(cursor); + } + @Override public void deleteExportedMessages() { beginTransaction(); diff --git a/app/src/main/java/org/thoughtcrime/securesms/exporter/flow/ExportSmsCompleteFragment.kt b/app/src/main/java/org/thoughtcrime/securesms/exporter/flow/ExportSmsCompleteFragment.kt index cd4e96dcb..2fa902d30 100644 --- a/app/src/main/java/org/thoughtcrime/securesms/exporter/flow/ExportSmsCompleteFragment.kt +++ b/app/src/main/java/org/thoughtcrime/securesms/exporter/flow/ExportSmsCompleteFragment.kt @@ -6,6 +6,7 @@ import androidx.fragment.app.Fragment import androidx.navigation.fragment.findNavController import androidx.navigation.fragment.navArgs import org.thoughtcrime.securesms.R +import org.thoughtcrime.securesms.SmsExportDirections import org.thoughtcrime.securesms.databinding.ExportSmsCompleteFragmentBinding import org.thoughtcrime.securesms.util.navigation.safeNavigate @@ -20,7 +21,7 @@ class ExportSmsCompleteFragment : Fragment(R.layout.export_sms_complete_fragment val exportSuccessCount = args.exportMessageCount - args.exportMessageFailureCount val binding = ExportSmsCompleteFragmentBinding.bind(view) - binding.exportCompleteNext.setOnClickListener { findNavController().safeNavigate(ExportSmsCompleteFragmentDirections.actionExportingSmsMessagesFragmentToChooseANewDefaultSmsAppFragment()) } + binding.exportCompleteNext.setOnClickListener { findNavController().safeNavigate(SmsExportDirections.actionDirectToChooseANewDefaultSmsAppFragment()) } binding.exportCompleteStatus.text = resources.getQuantityString(R.plurals.ExportSmsCompleteFragment__d_of_d_messages_exported, args.exportMessageCount, exportSuccessCount, args.exportMessageCount) } } diff --git a/app/src/main/java/org/thoughtcrime/securesms/exporter/flow/ExportSmsFullErrorFragment.kt b/app/src/main/java/org/thoughtcrime/securesms/exporter/flow/ExportSmsFullErrorFragment.kt new file mode 100644 index 000000000..26fa06190 --- /dev/null +++ b/app/src/main/java/org/thoughtcrime/securesms/exporter/flow/ExportSmsFullErrorFragment.kt @@ -0,0 +1,44 @@ +package org.thoughtcrime.securesms.exporter.flow + +import android.content.Context +import android.os.Bundle +import android.view.ContextThemeWrapper +import android.view.LayoutInflater +import android.view.View +import androidx.core.content.ContextCompat +import androidx.navigation.fragment.findNavController +import androidx.navigation.fragment.navArgs +import org.thoughtcrime.securesms.LoggingFragment +import org.thoughtcrime.securesms.R +import org.thoughtcrime.securesms.SmsExportDirections +import org.thoughtcrime.securesms.databinding.ExportSmsFullErrorFragmentBinding +import org.thoughtcrime.securesms.util.navigation.safeNavigate + +/** + * Fragment shown when all export messages failed. + */ +class ExportSmsFullErrorFragment : LoggingFragment(R.layout.export_sms_full_error_fragment) { + private val args: ExportSmsFullErrorFragmentArgs by navArgs() + + override fun onGetLayoutInflater(savedInstanceState: Bundle?): LayoutInflater { + val inflater = super.onGetLayoutInflater(savedInstanceState) + val contextThemeWrapper: Context = ContextThemeWrapper(requireContext(), R.style.Signal_DayNight) + return inflater.cloneInContext(contextThemeWrapper) + } + + @Suppress("UsePropertyAccessSyntax") + override fun onViewCreated(view: View, savedInstanceState: Bundle?) { + val binding = ExportSmsFullErrorFragmentBinding.bind(view) + + val exportSuccessCount = args.exportMessageCount - args.exportMessageFailureCount + binding.exportCompleteStatus.text = resources.getQuantityString(R.plurals.ExportSmsCompleteFragment__d_of_d_messages_exported, args.exportMessageCount, exportSuccessCount, args.exportMessageCount) + binding.retryButton.setOnClickListener { findNavController().safeNavigate(SmsExportDirections.actionDirectToExportYourSmsMessagesFragment()) } + binding.pleaseTryAgain.apply { + setLinkColor(ContextCompat.getColor(requireContext(), R.color.signal_colorPrimary)) + setLearnMoreVisible(true, R.string.ExportSmsPartiallyComplete__contact_us) + setOnLinkClickListener { + findNavController().safeNavigate(SmsExportDirections.actionDirectToHelpFragment()) + } + } + } +} diff --git a/app/src/main/java/org/thoughtcrime/securesms/exporter/flow/ExportSmsPartiallyCompleteFragment.kt b/app/src/main/java/org/thoughtcrime/securesms/exporter/flow/ExportSmsPartiallyCompleteFragment.kt new file mode 100644 index 000000000..b6c6b34e7 --- /dev/null +++ b/app/src/main/java/org/thoughtcrime/securesms/exporter/flow/ExportSmsPartiallyCompleteFragment.kt @@ -0,0 +1,57 @@ +package org.thoughtcrime.securesms.exporter.flow + +import android.content.Context +import android.os.Bundle +import android.text.format.Formatter +import android.view.ContextThemeWrapper +import android.view.LayoutInflater +import android.view.View +import androidx.core.content.ContextCompat +import androidx.navigation.fragment.findNavController +import androidx.navigation.fragment.navArgs +import org.signal.core.util.concurrent.SimpleTask +import org.thoughtcrime.securesms.LoggingFragment +import org.thoughtcrime.securesms.R +import org.thoughtcrime.securesms.SmsExportDirections +import org.thoughtcrime.securesms.database.SignalDatabase +import org.thoughtcrime.securesms.databinding.ExportSmsPartiallyCompleteFragmentBinding +import org.thoughtcrime.securesms.util.navigation.safeNavigate + +/** + * Fragment shown when some messages exported and some failed. + */ +class ExportSmsPartiallyCompleteFragment : LoggingFragment(R.layout.export_sms_partially_complete_fragment) { + + private val args: ExportSmsPartiallyCompleteFragmentArgs by navArgs() + + override fun onGetLayoutInflater(savedInstanceState: Bundle?): LayoutInflater { + val inflater = super.onGetLayoutInflater(savedInstanceState) + val contextThemeWrapper: Context = ContextThemeWrapper(requireContext(), R.style.Signal_DayNight) + return inflater.cloneInContext(contextThemeWrapper) + } + + @Suppress("UsePropertyAccessSyntax") + override fun onViewCreated(view: View, savedInstanceState: Bundle?) { + val binding = ExportSmsPartiallyCompleteFragmentBinding.bind(view) + + val exportSuccessCount = args.exportMessageCount - args.exportMessageFailureCount + binding.exportCompleteStatus.text = resources.getQuantityString(R.plurals.ExportSmsCompleteFragment__d_of_d_messages_exported, args.exportMessageCount, exportSuccessCount, args.exportMessageCount) + binding.retryButton.setOnClickListener { findNavController().safeNavigate(SmsExportDirections.actionDirectToExportYourSmsMessagesFragment()) } + binding.continueButton.setOnClickListener { findNavController().safeNavigate(SmsExportDirections.actionDirectToChooseANewDefaultSmsAppFragment()) } + binding.bullet3Text.apply { + setLinkColor(ContextCompat.getColor(requireContext(), R.color.signal_colorPrimary)) + setLearnMoreVisible(true, R.string.ExportSmsPartiallyComplete__contact_us) + setOnLinkClickListener { + findNavController().safeNavigate(SmsExportDirections.actionDirectToHelpFragment()) + } + } + + SimpleTask.runWhenValid( + viewLifecycleOwner.lifecycle, + { SignalDatabase.sms.getUnexportedInsecureMessagesEstimatedSize() + SignalDatabase.mms.getUnexportedInsecureMessagesEstimatedSize() }, + { totalSize -> + binding.bullet1Text.setText(getString(R.string.ExportSmsPartiallyComplete__ensure_you_have_an_additional_s_free_on_your_phone_to_export_your_messages, Formatter.formatFileSize(requireContext(), totalSize))) + } + ) + } +} diff --git a/app/src/main/java/org/thoughtcrime/securesms/exporter/flow/ExportYourSmsMessagesFragment.kt b/app/src/main/java/org/thoughtcrime/securesms/exporter/flow/ExportYourSmsMessagesFragment.kt index 11a195938..76de188cc 100644 --- a/app/src/main/java/org/thoughtcrime/securesms/exporter/flow/ExportYourSmsMessagesFragment.kt +++ b/app/src/main/java/org/thoughtcrime/securesms/exporter/flow/ExportYourSmsMessagesFragment.kt @@ -10,7 +10,6 @@ import org.signal.smsexporter.DefaultSmsHelper import org.signal.smsexporter.SmsExportProgress import org.signal.smsexporter.SmsExportService import org.thoughtcrime.securesms.R -import org.thoughtcrime.securesms.SmsExportDirections import org.thoughtcrime.securesms.databinding.ExportYourSmsMessagesFragmentBinding import org.thoughtcrime.securesms.util.Material3OnScrollHelper import org.thoughtcrime.securesms.util.navigation.safeNavigate @@ -46,9 +45,7 @@ class ExportYourSmsMessagesFragment : Fragment(R.layout.export_your_sms_messages .progressState .observeOn(AndroidSchedulers.mainThread()) .subscribe { - if (it is SmsExportProgress.Done) { - findNavController().safeNavigate(SmsExportDirections.actionDirectToExportSmsCompleteFragment(it.errorCount, it.total)) - } else if (it is SmsExportProgress.InProgress) { + if (it !is SmsExportProgress.Init) { findNavController().safeNavigate(ExportYourSmsMessagesFragmentDirections.actionExportYourSmsMessagesFragmentToExportingSmsMessagesFragment()) } } diff --git a/app/src/main/java/org/thoughtcrime/securesms/exporter/flow/ExportingSmsMessagesFragment.kt b/app/src/main/java/org/thoughtcrime/securesms/exporter/flow/ExportingSmsMessagesFragment.kt index 6ea9e050d..0a4983171 100644 --- a/app/src/main/java/org/thoughtcrime/securesms/exporter/flow/ExportingSmsMessagesFragment.kt +++ b/app/src/main/java/org/thoughtcrime/securesms/exporter/flow/ExportingSmsMessagesFragment.kt @@ -2,11 +2,13 @@ package org.thoughtcrime.securesms.exporter.flow import android.content.Context import android.os.Bundle +import android.text.format.Formatter import android.view.ContextThemeWrapper import android.view.LayoutInflater import android.view.View import androidx.fragment.app.Fragment import androidx.navigation.fragment.findNavController +import com.google.android.material.dialog.MaterialAlertDialogBuilder import io.reactivex.rxjava3.android.schedulers.AndroidSchedulers import io.reactivex.rxjava3.disposables.Disposable import org.signal.smsexporter.SmsExportProgress @@ -15,6 +17,7 @@ import org.thoughtcrime.securesms.R import org.thoughtcrime.securesms.databinding.ExportingSmsMessagesFragmentBinding import org.thoughtcrime.securesms.exporter.SignalSmsExportService import org.thoughtcrime.securesms.util.LifecycleDisposable +import org.thoughtcrime.securesms.util.mb import org.thoughtcrime.securesms.util.navigation.safeNavigate /** @@ -32,14 +35,22 @@ class ExportingSmsMessagesFragment : Fragment(R.layout.exporting_sms_messages_fr return inflater.cloneInContext(contextThemeWrapper) } + @Suppress("KotlinConstantConditions") override fun onResume() { super.onResume() navigationDisposable = SmsExportService .progressState .observeOn(AndroidSchedulers.mainThread()) - .subscribe { - if (it is SmsExportProgress.Done) { - findNavController().safeNavigate(ExportingSmsMessagesFragmentDirections.actionExportingSmsMessagesFragmentToExportSmsCompleteFragment(it.total, it.errorCount)) + .subscribe { smsExportProgress -> + if (smsExportProgress is SmsExportProgress.Done) { + SmsExportService.clearProgressState() + if (smsExportProgress.errorCount == 0) { + findNavController().safeNavigate(ExportingSmsMessagesFragmentDirections.actionExportingSmsMessagesFragmentToExportSmsCompleteFragment(smsExportProgress.total, smsExportProgress.errorCount)) + } else if (smsExportProgress.errorCount == smsExportProgress.total) { + findNavController().safeNavigate(ExportingSmsMessagesFragmentDirections.actionExportingSmsMessagesFragmentToExportSmsFullErrorFragment(smsExportProgress.total, smsExportProgress.errorCount)) + } else { + findNavController().safeNavigate(ExportingSmsMessagesFragmentDirections.actionExportingSmsMessagesFragmentToExportSmsPartiallyCompleteFragment(smsExportProgress.total, smsExportProgress.errorCount)) + } } } } @@ -55,18 +66,34 @@ class ExportingSmsMessagesFragment : Fragment(R.layout.exporting_sms_messages_fr lifecycleDisposable.bindTo(viewLifecycleOwner) lifecycleDisposable += SmsExportService.progressState.observeOn(AndroidSchedulers.mainThread()).subscribe { when (it) { - is SmsExportProgress.Done -> Unit + SmsExportProgress.Init -> binding.progress.isIndeterminate = true + SmsExportProgress.Starting -> binding.progress.isIndeterminate = true is SmsExportProgress.InProgress -> { binding.progress.isIndeterminate = false binding.progress.max = it.total binding.progress.progress = it.progress binding.progressLabel.text = resources.getQuantityString(R.plurals.ExportingSmsMessagesFragment__exporting_d_of_d, it.total, it.progress, it.total) } - SmsExportProgress.Init -> binding.progress.isIndeterminate = true - SmsExportProgress.Starting -> binding.progress.isIndeterminate = true + is SmsExportProgress.Done -> Unit } } - SignalSmsExportService.start(requireContext()) + lifecycleDisposable += ExportingSmsRepository() + .getSmsExportSizeEstimations() + .observeOn(AndroidSchedulers.mainThread()) + .subscribe { (internalFreeSpace, estimatedRequiredSpace) -> + val adjustedFreeSpace = internalFreeSpace - estimatedRequiredSpace - 100.mb + if (estimatedRequiredSpace > adjustedFreeSpace) { + MaterialAlertDialogBuilder(requireContext()) + .setTitle(R.string.ExportingSmsMessagesFragment__you_may_not_have_enough_disk_space) + .setMessage(getString(R.string.ExportingSmsMessagesFragment__you_need_approximately_s_to_export_your_messages_ensure_you_have_enough_space_before_continuing, Formatter.formatFileSize(requireContext(), estimatedRequiredSpace))) + .setPositiveButton(R.string.ExportingSmsMessagesFragment__continue_anyway) { _, _ -> SignalSmsExportService.start(requireContext()) } + .setNegativeButton(android.R.string.cancel) { _, _ -> findNavController().safeNavigate(ExportingSmsMessagesFragmentDirections.actionDirectToExportYourSmsMessagesFragment()) } + .setCancelable(false) + .show() + } else { + SignalSmsExportService.start(requireContext()) + } + } } } diff --git a/app/src/main/java/org/thoughtcrime/securesms/exporter/flow/ExportingSmsRepository.kt b/app/src/main/java/org/thoughtcrime/securesms/exporter/flow/ExportingSmsRepository.kt new file mode 100644 index 000000000..f0c31cc0c --- /dev/null +++ b/app/src/main/java/org/thoughtcrime/securesms/exporter/flow/ExportingSmsRepository.kt @@ -0,0 +1,38 @@ +package org.thoughtcrime.securesms.exporter.flow + +import android.app.Application +import android.os.Build +import android.os.storage.StorageManager +import androidx.core.content.ContextCompat +import io.reactivex.rxjava3.core.Single +import io.reactivex.rxjava3.schedulers.Schedulers +import org.thoughtcrime.securesms.database.SignalDatabase +import org.thoughtcrime.securesms.dependencies.ApplicationDependencies +import java.io.File + +class ExportingSmsRepository(private val context: Application = ApplicationDependencies.getApplication()) { + + @Suppress("UsePropertyAccessSyntax") + fun getSmsExportSizeEstimations(): Single { + return Single.fromCallable { + val internalStorageFile = if (Build.VERSION.SDK_INT < 24) { + File(context.applicationInfo.dataDir) + } else { + context.dataDir + } + + val internalFreeSpace: Long = if (Build.VERSION.SDK_INT < 26) { + internalStorageFile.usableSpace + } else { + val storageManagerFreeSpace = ContextCompat.getSystemService(context, StorageManager::class.java)?.let { storageManager -> + storageManager.getAllocatableBytes(storageManager.getUuidForPath(internalStorageFile)) + } + storageManagerFreeSpace ?: internalStorageFile.usableSpace + } + + SmsExportSizeEstimations(internalFreeSpace, SignalDatabase.sms.getUnexportedInsecureMessagesEstimatedSize() + SignalDatabase.mms.getUnexportedInsecureMessagesEstimatedSize()) + }.subscribeOn(Schedulers.io()) + } + + data class SmsExportSizeEstimations(val estimatedInternalFreeSpace: Long, val estimatedRequiredSpace: Long) +} diff --git a/app/src/main/java/org/thoughtcrime/securesms/exporter/flow/SmsExportActivity.kt b/app/src/main/java/org/thoughtcrime/securesms/exporter/flow/SmsExportActivity.kt index d4e9b4641..3b818fe5c 100644 --- a/app/src/main/java/org/thoughtcrime/securesms/exporter/flow/SmsExportActivity.kt +++ b/app/src/main/java/org/thoughtcrime/securesms/exporter/flow/SmsExportActivity.kt @@ -2,8 +2,11 @@ package org.thoughtcrime.securesms.exporter.flow import android.content.Context import android.content.Intent +import android.os.Bundle +import androidx.activity.OnBackPressedCallback import androidx.core.app.NotificationManagerCompat import androidx.fragment.app.Fragment +import androidx.navigation.findNavController import androidx.navigation.fragment.NavHostFragment import org.thoughtcrime.securesms.R import org.thoughtcrime.securesms.components.FragmentWrapperActivity @@ -18,10 +21,23 @@ class SmsExportActivity : FragmentWrapperActivity() { NotificationManagerCompat.from(this).cancel(NotificationIds.SMS_EXPORT_COMPLETE) } + override fun onCreate(savedInstanceState: Bundle?, ready: Boolean) { + super.onCreate(savedInstanceState, ready) + onBackPressedDispatcher.addCallback(this, OnBackPressed()) + } + override fun getFragment(): Fragment { return NavHostFragment.create(R.navigation.sms_export) } + private inner class OnBackPressed : OnBackPressedCallback(true) { + override fun handleOnBackPressed() { + if (!findNavController(R.id.fragment_container).popBackStack()) { + finish() + } + } + } + companion object { @JvmStatic fun createIntent(context: Context): Intent = Intent(context, SmsExportActivity::class.java) diff --git a/app/src/main/java/org/thoughtcrime/securesms/exporter/flow/SmsExportHelpFragment.kt b/app/src/main/java/org/thoughtcrime/securesms/exporter/flow/SmsExportHelpFragment.kt new file mode 100644 index 000000000..02e3cb47f --- /dev/null +++ b/app/src/main/java/org/thoughtcrime/securesms/exporter/flow/SmsExportHelpFragment.kt @@ -0,0 +1,30 @@ +package org.thoughtcrime.securesms.exporter.flow + +import android.os.Bundle +import android.view.View +import androidx.core.os.bundleOf +import androidx.navigation.fragment.findNavController +import org.thoughtcrime.securesms.LoggingFragment +import org.thoughtcrime.securesms.R +import org.thoughtcrime.securesms.databinding.SmsExportHelpFragmentBinding +import org.thoughtcrime.securesms.help.HelpFragment + +/** + * Fragment wrapper around the app settings help fragment to provide a toolbar and set default category for sms export. + */ +class SmsExportHelpFragment : LoggingFragment(R.layout.sms_export_help_fragment) { + override fun onViewCreated(view: View, savedInstanceState: Bundle?) { + val binding = SmsExportHelpFragmentBinding.bind(view) + + binding.toolbar.setOnClickListener { + if (!findNavController().popBackStack()) { + requireActivity().finish() + } + } + + childFragmentManager + .beginTransaction() + .replace(binding.smsExportHelpFragmentFragment.id, HelpFragment().apply { arguments = bundleOf(HelpFragment.START_CATEGORY_INDEX to HelpFragment.SMS_EXPORT_INDEX) }) + .commitNow() + } +} diff --git a/app/src/main/java/org/thoughtcrime/securesms/help/HelpFragment.java b/app/src/main/java/org/thoughtcrime/securesms/help/HelpFragment.java index d4ad6b263..f979dd954 100644 --- a/app/src/main/java/org/thoughtcrime/securesms/help/HelpFragment.java +++ b/app/src/main/java/org/thoughtcrime/securesms/help/HelpFragment.java @@ -40,6 +40,7 @@ public class HelpFragment extends LoggingFragment { public static final String START_CATEGORY_INDEX = "start_category_index"; public static final int PAYMENT_INDEX = 6; public static final int DONATION_INDEX = 7; + public static final int SMS_EXPORT_INDEX = 8; private EditText problem; private CheckBox includeDebugLogs; @@ -93,7 +94,7 @@ public class HelpFragment extends LoggingFragment { emoji.add(view.findViewById(feeling.getViewId())); } - categoryAdapter = ArrayAdapter.createFromResource(requireContext(), R.array.HelpFragment__categories_4, android.R.layout.simple_spinner_item); + categoryAdapter = ArrayAdapter.createFromResource(requireContext(), R.array.HelpFragment__categories_5, android.R.layout.simple_spinner_item); categoryAdapter.setDropDownViewResource(android.R.layout.simple_spinner_dropdown_item); categorySpinner.setAdapter(categoryAdapter); @@ -209,7 +210,7 @@ public class HelpFragment extends LoggingFragment { suffix.append(getString(feeling.getStringId())); } - String[] englishCategories = ResourceUtil.getEnglishResources(requireContext()).getStringArray(R.array.HelpFragment__categories_4); + String[] englishCategories = ResourceUtil.getEnglishResources(requireContext()).getStringArray(R.array.HelpFragment__categories_5); String category = (helpViewModel.getCategoryIndex() >= 0 && helpViewModel.getCategoryIndex() < englishCategories.length) ? englishCategories[helpViewModel.getCategoryIndex()] : categoryAdapter.getItem(helpViewModel.getCategoryIndex()).toString(); diff --git a/app/src/main/res/drawable-night/choose_signal.xml b/app/src/main/res/drawable-night/choose_signal.xml index 04bcbd8b6..6ccd99d93 100644 --- a/app/src/main/res/drawable-night/choose_signal.xml +++ b/app/src/main/res/drawable-night/choose_signal.xml @@ -6,22 +6,22 @@ android:viewportHeight="240" tools:ignore="VectorRaster"> + android:pathData="m50.18,92.6 l0.26,1.07c-1.04,0.26 -2.03,0.67 -2.95,1.22l-0.56,-0.94a11.69,11.69 0,0 1,3.25 -1.34ZM55.82,92.6 L55.55,93.67c1.04,0.26 2.03,0.67 2.95,1.22l0.57,-0.94a11.69,11.69 0,0 0,-3.26 -1.34ZM42.95,97.93a11.7,11.7 0,0 0,-1.34 3.25l1.07,0.26a10.61,10.61 0,0 1,1.22 -2.95l-0.94,-0.56ZM42.36,104c0,-0.53 0.04,-1.07 0.12,-1.59l-1.09,-0.17a11.79,11.79 0,0 0,0 3.52l1.09,-0.17c-0.08,-0.53 -0.12,-1.06 -0.12,-1.59ZM59.07,114.05 L58.5,113.11c-0.92,0.55 -1.91,0.96 -2.95,1.22l0.26,1.07a11.69,11.69 0,0 0,3.25 -1.34ZM63.64,104a10.63,10.63 0,0 1,-0.12 1.59l1.09,0.17a11.78,11.78 0,0 0,0 -3.52l-1.09,0.17c0.08,0.53 0.12,1.06 0.12,1.59ZM64.4,106.82 L63.33,106.55a10.61,10.61 0,0 1,-1.22 2.95l0.94,0.57a11.68,11.68 0,0 0,1.34 -3.25ZM54.59,114.52a10.71,10.71 0,0 1,-3.19 0l-0.17,1.09c1.17,0.18 2.35,0.18 3.52,0l-0.17,-1.09ZM61.56,110.31a10.69,10.69 0,0 1,-2.25 2.25l0.65,0.89a11.73,11.73 0,0 0,2.49 -2.48l-0.89,-0.66ZM59.31,95.44c0.86,0.63 1.62,1.39 2.25,2.25l0.89,-0.66a11.76,11.76 0,0 0,-2.48 -2.48l-0.66,0.89ZM44.44,97.69c0.63,-0.86 1.39,-1.62 2.25,-2.25l-0.66,-0.89a11.76,11.76 0,0 0,-2.48 2.48l0.89,0.66ZM63.05,97.93 L62.11,98.5c0.55,0.92 0.96,1.91 1.22,2.95l1.07,-0.26a11.69,11.69 0,0 0,-1.34 -3.25ZM51.41,93.48a10.71,10.71 0,0 1,3.19 0l0.17,-1.09a11.78,11.78 0,0 0,-3.52 0l0.17,1.09ZM45,113.74 L42.73,114.27 43.26,112 42.19,111.75 41.66,114.02a1.1,1.1 0,0 0,1.32 1.32l2.27,-0.52 -0.25,-1.08ZM42.42,110.77 L43.49,111.02 43.86,109.44a10.58,10.58 0,0 1,-1.18 -2.89l-1.07,0.26c0.24,0.97 0.6,1.91 1.08,2.79l-0.27,1.16ZM47.55,113.15 L45.98,113.52 46.23,114.59 47.39,114.32c0.88,0.48 1.82,0.84 2.79,1.08l0.26,-1.07a10.62,10.62 0,0 1,-2.88 -1.19l-0.01,0.01ZM53,94.46a9.54,9.54 0,0 0,-8.07 14.61l-0.92,3.91 3.91,-0.92a9.54,9.54 0,0 0,12.1 -1.61,9.53 9.53,0 0,0 2.36,-8.19A9.54,9.54 0,0 0,53 94.46Z" + android:fillColor="#fff"/> @@ -29,22 +29,10 @@ android:pathData="M53,164m-16,0a16,16 0,1 1,32 0a16,16 0,1 1,-32 0" android:fillColor="#5C5E65"/> - - - - diff --git a/app/src/main/res/drawable-night/complete.xml b/app/src/main/res/drawable-night/complete.xml new file mode 100644 index 000000000..e01e10552 --- /dev/null +++ b/app/src/main/res/drawable-night/complete.xml @@ -0,0 +1,17 @@ + + + + + diff --git a/app/src/main/res/drawable-night/export_sms.xml b/app/src/main/res/drawable-night/export_sms.xml index 2c08dfe18..def769f3b 100644 --- a/app/src/main/res/drawable-night/export_sms.xml +++ b/app/src/main/res/drawable-night/export_sms.xml @@ -5,7 +5,7 @@ android:viewportHeight="112"> + android:pathData="M0,0h134v112H0z"/> @@ -14,23 +14,23 @@ android:fillColor="#B6C5FA" android:fillAlpha="0.08"/> diff --git a/app/src/main/res/drawable-night/sms_export_error.xml b/app/src/main/res/drawable-night/sms_export_error.xml new file mode 100644 index 000000000..86f246a7e --- /dev/null +++ b/app/src/main/res/drawable-night/sms_export_error.xml @@ -0,0 +1,12 @@ + + + + diff --git a/app/src/main/res/drawable-night/sms_export_partial_complete.xml b/app/src/main/res/drawable-night/sms_export_partial_complete.xml new file mode 100644 index 000000000..d92519584 --- /dev/null +++ b/app/src/main/res/drawable-night/sms_export_partial_complete.xml @@ -0,0 +1,23 @@ + + + + + + + diff --git a/app/src/main/res/drawable-night/sms_message.xml b/app/src/main/res/drawable-night/sms_message.xml index d58670ed9..30823c296 100644 --- a/app/src/main/res/drawable-night/sms_message.xml +++ b/app/src/main/res/drawable-night/sms_message.xml @@ -11,15 +11,15 @@ android:fillColor="#B6C5FA" android:fillAlpha="0.08"/> diff --git a/app/src/main/res/drawable/choose_signal.xml b/app/src/main/res/drawable/choose_signal.xml index 8def5262f..4cc8fb1e7 100644 --- a/app/src/main/res/drawable/choose_signal.xml +++ b/app/src/main/res/drawable/choose_signal.xml @@ -6,18 +6,18 @@ android:viewportHeight="240" tools:ignore="VectorRaster"> + android:pathData="M9,40C9,17.91 26.91,0 49,0h160c22.09,0 40,17.91 40,40v160c0,22.09 -17.91,40 -40,40H49c-22.09,0 -40,-17.91 -40,-40V40Z" + android:fillColor="#fff"/> + android:pathData="m50.18,92.6 l0.26,1.07a10.61,10.61 0,0 0,-2.95 1.22l-0.56,-0.94a11.69,11.69 0,0 1,3.25 -1.34ZM55.82,92.6 L55.55,93.67c1.04,0.26 2.03,0.67 2.95,1.22l0.57,-0.94a11.69,11.69 0,0 0,-3.25 -1.34ZM42.95,97.93a11.7,11.7 0,0 0,-1.34 3.25l1.07,0.26a10.61,10.61 0,0 1,1.22 -2.95l-0.94,-0.56ZM42.36,104c0,-0.53 0.04,-1.07 0.12,-1.59l-1.09,-0.17a11.78,11.78 0,0 0,0 3.52l1.09,-0.17c-0.08,-0.53 -0.12,-1.06 -0.12,-1.59ZM59.07,114.05 L58.5,113.11c-0.92,0.55 -1.91,0.96 -2.95,1.22l0.26,1.07a11.7,11.7 0,0 0,3.25 -1.34ZM63.64,104c0,0.53 -0.04,1.07 -0.12,1.59l1.09,0.17a11.79,11.79 0,0 0,0 -3.52l-1.09,0.17c0.08,0.53 0.12,1.06 0.12,1.59ZM64.4,106.82 L63.33,106.55a10.61,10.61 0,0 1,-1.22 2.95l0.94,0.57a11.68,11.68 0,0 0,1.34 -3.25ZM54.59,114.52a10.71,10.71 0,0 1,-3.19 0l-0.17,1.09c1.17,0.18 2.35,0.18 3.52,0l-0.17,-1.09ZM61.56,110.31a10.69,10.69 0,0 1,-2.25 2.25l0.65,0.89a11.74,11.74 0,0 0,2.49 -2.48l-0.89,-0.66ZM59.31,95.44c0.86,0.63 1.62,1.39 2.25,2.25l0.89,-0.66a11.76,11.76 0,0 0,-2.48 -2.48l-0.66,0.89ZM44.44,97.69c0.63,-0.86 1.39,-1.62 2.25,-2.25l-0.66,-0.89a11.76,11.76 0,0 0,-2.48 2.48l0.89,0.66ZM63.05,97.93 L62.11,98.5c0.55,0.92 0.96,1.91 1.22,2.95l1.07,-0.26a11.69,11.69 0,0 0,-1.34 -3.25ZM51.41,93.48a10.71,10.71 0,0 1,3.19 0l0.17,-1.09a11.78,11.78 0,0 0,-3.52 0l0.17,1.09ZM45,113.74 L42.73,114.27 43.26,112 42.19,111.75 41.66,114.02a1.1,1.1 0,0 0,1.32 1.32l2.27,-0.52 -0.25,-1.08ZM42.42,110.77 L43.49,111.02 43.86,109.44a10.58,10.58 0,0 1,-1.18 -2.89l-1.07,0.26c0.24,0.97 0.6,1.91 1.08,2.79l-0.27,1.16ZM47.55,113.15 L45.98,113.52 46.23,114.59 47.39,114.32c0.88,0.48 1.82,0.84 2.79,1.08l0.26,-1.07a10.62,10.62 0,0 1,-2.88 -1.19l-0.01,0.01ZM53,94.46a9.54,9.54 0,0 0,-8.07 14.61l-0.92,3.91 3.91,-0.92a9.54,9.54 0,0 0,12.1 -1.61,9.53 9.53,0 0,0 0.6,-12.19 9.54,9.54 0,0 0,-7.63 -3.8Z" + android:fillColor="#fff"/> @@ -25,22 +25,10 @@ android:pathData="M53,164m-16,0a16,16 0,1 1,32 0a16,16 0,1 1,-32 0" android:fillColor="#D6D9DF"/> - - - - diff --git a/app/src/main/res/drawable/complete.xml b/app/src/main/res/drawable/complete.xml new file mode 100644 index 000000000..1a00c1140 --- /dev/null +++ b/app/src/main/res/drawable/complete.xml @@ -0,0 +1,17 @@ + + + + + diff --git a/app/src/main/res/drawable/export_sms.xml b/app/src/main/res/drawable/export_sms.xml index 3588ae728..feb33f5ad 100644 --- a/app/src/main/res/drawable/export_sms.xml +++ b/app/src/main/res/drawable/export_sms.xml @@ -5,7 +5,7 @@ android:viewportHeight="112"> + android:pathData="M0,0h134v112H0z"/> @@ -14,23 +14,23 @@ android:fillColor="#50679F" android:fillAlpha="0.08"/> + android:pathData="M36.8,89.1c0,2 -1.7,3.7 -3.7,3.7l-26.4,-0.1C4.7,92.7 3,91 3,89l0.1,-66.1c0,-2 1.7,-3.7 3.7,-3.7l26.4,0.1c2,0 3.7,1.7 3.7,3.7l-0.1,66.1Z" + android:fillColor="#fff"/> + android:pathData="M130.8,89.1c0,2 -1.7,3.7 -3.7,3.7l-26.4,-0.1c-2,0 -3.7,-1.7 -3.7,-3.7l0.1,-66.1c0,-2 1.7,-3.7 3.7,-3.7l26.4,0.1c2,0 3.7,1.7 3.7,3.7l-0.1,66.1Z" + android:fillColor="#fff"/> diff --git a/app/src/main/res/drawable/sms_export_error.xml b/app/src/main/res/drawable/sms_export_error.xml new file mode 100644 index 000000000..1b915df4a --- /dev/null +++ b/app/src/main/res/drawable/sms_export_error.xml @@ -0,0 +1,12 @@ + + + + diff --git a/app/src/main/res/drawable/sms_export_partial_complete.xml b/app/src/main/res/drawable/sms_export_partial_complete.xml new file mode 100644 index 000000000..e833122b0 --- /dev/null +++ b/app/src/main/res/drawable/sms_export_partial_complete.xml @@ -0,0 +1,23 @@ + + + + + + + diff --git a/app/src/main/res/drawable/sms_message.xml b/app/src/main/res/drawable/sms_message.xml index 116445c2f..aede48912 100644 --- a/app/src/main/res/drawable/sms_message.xml +++ b/app/src/main/res/drawable/sms_message.xml @@ -11,15 +11,15 @@ android:fillColor="#50679F" android:fillAlpha="0.08"/> diff --git a/app/src/main/res/layout/export_sms_full_error_fragment.xml b/app/src/main/res/layout/export_sms_full_error_fragment.xml new file mode 100644 index 000000000..d6af75a1e --- /dev/null +++ b/app/src/main/res/layout/export_sms_full_error_fragment.xml @@ -0,0 +1,94 @@ + + + + + + + + + + + + + + + + + + + + + + diff --git a/app/src/main/res/layout/export_sms_partially_complete_fragment.xml b/app/src/main/res/layout/export_sms_partially_complete_fragment.xml new file mode 100644 index 000000000..d8149bd83 --- /dev/null +++ b/app/src/main/res/layout/export_sms_partially_complete_fragment.xml @@ -0,0 +1,250 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/app/src/main/res/layout/sms_export_help_fragment.xml b/app/src/main/res/layout/sms_export_help_fragment.xml new file mode 100644 index 000000000..18b2958d1 --- /dev/null +++ b/app/src/main/res/layout/sms_export_help_fragment.xml @@ -0,0 +1,22 @@ + + + + + + + + diff --git a/app/src/main/res/navigation/sms_export.xml b/app/src/main/res/navigation/sms_export.xml index c6cfa78cb..2c870d5b7 100644 --- a/app/src/main/res/navigation/sms_export.xml +++ b/app/src/main/res/navigation/sms_export.xml @@ -16,7 +16,10 @@ app:enterAnim="@anim/fragment_open_enter" app:exitAnim="@anim/fragment_open_exit" app:popEnterAnim="@anim/fragment_close_enter" - app:popExitAnim="@anim/fragment_close_exit" /> + app:popExitAnim="@anim/fragment_close_exit" + app:popUpTo="@id/sms_export" + app:popUpToInclusive="true" /> + + + app:popExitAnim="@anim/fragment_close_exit" + app:popUpTo="@+id/sms_export" + app:popUpToInclusive="true" /> + + + + + - - + + + + + + + + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/app/src/main/res/values/strings.xml b/app/src/main/res/values/strings.xml index c431604ce..1863d47b5 100644 --- a/app/src/main/res/values/strings.xml +++ b/app/src/main/res/values/strings.xml @@ -2516,7 +2516,7 @@ Debug Log: Could not upload logs Please be as descriptive as possible to help us understand the issue. - + \-\- Please select an option \-\- Something\'s Not Working Feature Request @@ -2525,6 +2525,7 @@ Other Payments (MobileCoin) Donations & Badges + SMS Export @@ -5347,6 +5348,12 @@ Exporting %1$d of %2$d… Exporting %1$d of %2$d… + + You may not have enough disk space + + You need approximately %1$s to export your messages, ensure you have enough space before continuing. + + Continue anyway @@ -5444,6 +5451,26 @@ %1$d of %2$d messages exported + + Export partially complete + + Ensure you have an additional %1$s free on your phone to export your messages + + Retry export, which will only retry messages that have not yet been exported + + If the problem persists, + + contact us + + Retry + + Continue anyway + + Error exporting SMS messages + + Please try again. If the problem persists, + + diff --git a/core-util/build.gradle b/core-util/build.gradle index a6232e51c..a3672b04c 100644 --- a/core-util/build.gradle +++ b/core-util/build.gradle @@ -48,6 +48,7 @@ dependencies { api libs.androidx.annotation implementation libs.androidx.core.ktx + implementation libs.androidx.lifecycle.common.java8 implementation libs.google.protobuf.javalite implementation libs.androidx.sqlite implementation libs.rxjava3.rxjava diff --git a/core-util/src/main/java/org/signal/core/util/CursorExtensions.kt b/core-util/src/main/java/org/signal/core/util/CursorExtensions.kt index 46d61ad23..3ef30751a 100644 --- a/core-util/src/main/java/org/signal/core/util/CursorExtensions.kt +++ b/core-util/src/main/java/org/signal/core/util/CursorExtensions.kt @@ -67,6 +67,17 @@ fun Cursor.requireObject(column: String, serializer: StringSerializer): T return serializer.deserialize(CursorUtil.requireString(this, column)) } +@JvmOverloads +fun Cursor.readToSingleLong(defaultValue: Long = 0): Long { + return use { + if (it.moveToFirst()) { + it.getLong(0) + } else { + defaultValue + } + } +} + inline fun Cursor.readToList(predicate: (T) -> Boolean = { true }, mapper: (Cursor) -> T): List { val list = mutableListOf() use { diff --git a/core-util/src/main/java/org/signal/core/util/concurrent/SimpleTask.java b/core-util/src/main/java/org/signal/core/util/concurrent/SimpleTask.java index b4004bdf6..b876a333b 100644 --- a/core-util/src/main/java/org/signal/core/util/concurrent/SimpleTask.java +++ b/core-util/src/main/java/org/signal/core/util/concurrent/SimpleTask.java @@ -4,12 +4,16 @@ import android.os.AsyncTask; import androidx.annotation.NonNull; import androidx.lifecycle.Lifecycle; +import androidx.lifecycle.LifecycleEventObserver; +import androidx.lifecycle.LifecycleOwner; import org.signal.core.util.ThreadUtil; import org.signal.core.util.concurrent.SignalExecutors; import java.util.concurrent.Executor; +import io.reactivex.rxjava3.observers.DefaultObserver; + public class SimpleTask { /** @@ -37,6 +41,35 @@ public class SimpleTask { }); } + /** + * Runs a task in the background and passes the result of the computation to a task that is run + * on the main thread. Will only invoke the {@code foregroundTask} if the provided {@link Lifecycle} + * is or enters in the future a valid (i.e. visible) state. In this way, it is very similar to + * {@link AsyncTask}, but is safe in that you can guarantee your task won't be called when your + * view is in an invalid state. + */ + public static void runWhenValid(@NonNull Lifecycle lifecycle, @NonNull BackgroundTask backgroundTask, @NonNull ForegroundTask foregroundTask) { + lifecycle.addObserver(new LifecycleEventObserver() { + @Override public void onStateChanged(@NonNull LifecycleOwner lifecycleOwner, @NonNull Lifecycle.Event event) { + if (isValid(lifecycle)) { + lifecycle.removeObserver(this); + + SignalExecutors.BOUNDED.execute(() -> { + final E result = backgroundTask.run(); + + if (isValid(lifecycle)) { + ThreadUtil.runOnMain(() -> { + if (isValid(lifecycle)) { + foregroundTask.run(result); + } + }); + } + }); + } + } + }); + } + /** * Runs a task in the background and passes the result of the computation to a task that is run on * the main thread. Essentially {@link AsyncTask}, but lambda-compatible. diff --git a/sms-exporter/lib/src/main/java/org/signal/smsexporter/SmsExportService.kt b/sms-exporter/lib/src/main/java/org/signal/smsexporter/SmsExportService.kt index 093b73bfa..892dc43e8 100644 --- a/sms-exporter/lib/src/main/java/org/signal/smsexporter/SmsExportService.kt +++ b/sms-exporter/lib/src/main/java/org/signal/smsexporter/SmsExportService.kt @@ -25,6 +25,10 @@ import java.util.concurrent.Executors abstract class SmsExportService : Service() { companion object { + fun clearProgressState() { + progressState.onNext(SmsExportProgress.Init) + } + private val TAG = Log.tag(SmsExportService::class.java) /** @@ -95,6 +99,7 @@ abstract class SmsExportService : Service() { Log.d(TAG, "Export complete") stopForeground(true) + stopSelf() isStarted = false } }