From 31f31534ce1ae55868839bfd6bca87cf97671774 Mon Sep 17 00:00:00 2001 From: Cody Henthorne Date: Wed, 28 Sep 2022 10:47:37 -0400 Subject: [PATCH] Round out sms/mms export process. --- app/src/main/AndroidManifest.xml | 1 + .../app/chats/sms/SmsSettingsFragment.kt | 8 +- .../app/chats/sms/SmsSettingsRepository.kt | 14 +- .../securesms/database/MessageDatabase.java | 21 +- .../securesms/database/MmsDatabase.java | 5 +- .../securesms/database/SmsDatabase.java | 4 +- .../exporter/SignalSmsExportReader.kt | 250 ++++++++++----- .../exporter/SignalSmsExportService.kt | 21 +- .../flow/ChooseANewDefaultSmsAppFragment.kt | 7 + .../flow/ExportingSmsMessagesFragment.kt | 32 +- .../exporter/flow/SmsExportActivity.kt | 7 + .../main/res/drawable-night/choose_signal.xml | 50 +++ .../main/res/drawable-night/export_sms.xml | 37 +++ .../main/res/drawable-night/sms_message.xml | 25 ++ app/src/main/res/drawable/choose_signal.xml | 46 +++ app/src/main/res/drawable/export_sms.xml | 37 +++ app/src/main/res/drawable/sms_message.xml | 25 ++ .../choose_a_new_default_sms_app_fragment.xml | 298 +++++++++++++++--- .../export_your_sms_messages_fragment.xml | 6 +- .../exporting_sms_messages_fragment.xml | 2 +- ...set_signal_as_default_sms_app_fragment.xml | 13 +- app/src/main/res/values/strings.xml | 34 +- .../smsexporter/app/TestSmsExportService.kt | 4 +- .../signal/smsexporter/ExportableMessage.kt | 8 +- .../signal/smsexporter/SmsExportService.kt | 14 +- .../internal/mms/ExportMmsMessagesUseCase.kt | 4 +- .../mms/GetOrCreateMmsThreadIdsUseCase.kt | 6 +- .../internal/sms/ExportSmsMessagesUseCase.kt | 2 +- .../java/org/signal/smsexporter/TestUtils.kt | 4 +- .../mms/ExportMmsMessagesUseCaseTest.kt | 2 +- .../sms/ExportSmsMessagesUseCaseTest.kt | 2 +- 31 files changed, 792 insertions(+), 197 deletions(-) create mode 100644 app/src/main/res/drawable-night/choose_signal.xml create mode 100644 app/src/main/res/drawable-night/export_sms.xml create mode 100644 app/src/main/res/drawable-night/sms_message.xml create mode 100644 app/src/main/res/drawable/choose_signal.xml create mode 100644 app/src/main/res/drawable/export_sms.xml create mode 100644 app/src/main/res/drawable/sms_message.xml diff --git a/app/src/main/AndroidManifest.xml b/app/src/main/AndroidManifest.xml index cf888449ec..eb7611d2cb 100644 --- a/app/src/main/AndroidManifest.xml +++ b/app/src/main/AndroidManifest.xml @@ -683,6 +683,7 @@ diff --git a/app/src/main/java/org/thoughtcrime/securesms/components/settings/app/chats/sms/SmsSettingsFragment.kt b/app/src/main/java/org/thoughtcrime/securesms/components/settings/app/chats/sms/SmsSettingsFragment.kt index ccbc051dc9..0d4c24f5af 100644 --- a/app/src/main/java/org/thoughtcrime/securesms/components/settings/app/chats/sms/SmsSettingsFragment.kt +++ b/app/src/main/java/org/thoughtcrime/securesms/components/settings/app/chats/sms/SmsSettingsFragment.kt @@ -141,14 +141,16 @@ class SmsSettingsFragment : DSLSettingsFragment(R.string.preferences__sms_mms) { private fun showSmsRemovalDialog() { MaterialAlertDialogBuilder(requireContext()) .setTitle(R.string.RemoveSmsMessagesDialogFragment__remove_sms_messages) - .setMessage(R.string.RemoveSmsMessagesDialogFragment__you_have_changed) - .setPositiveButton(R.string.RemoveSmsMessagesDialogFragment__keep_messages) { _, _ -> } + .setMessage(R.string.RemoveSmsMessagesDialogFragment__you_can_now_remove_sms_messages_from_signal) + .setPositiveButton(R.string.RemoveSmsMessagesDialogFragment__keep_messages) { _, _ -> + Snackbar.make(requireView(), R.string.SmsSettingsFragment__you_can_remove_sms_messages_from_signal_in_settings, Snackbar.LENGTH_SHORT).show() + } .setNegativeButton(R.string.RemoveSmsMessagesDialogFragment__remove_messages) { _, _ -> SignalExecutors.BOUNDED.execute { SignalDatabase.sms.deleteExportedMessages() SignalDatabase.mms.deleteExportedMessages() } - Snackbar.make(requireView(), R.string.SmsSettingsFragment__sms_messages_removed, Snackbar.LENGTH_SHORT).show() + Snackbar.make(requireView(), R.string.SmsSettingsFragment__removing_sms_messages_from_signal, Snackbar.LENGTH_SHORT).show() } .show() } diff --git a/app/src/main/java/org/thoughtcrime/securesms/components/settings/app/chats/sms/SmsSettingsRepository.kt b/app/src/main/java/org/thoughtcrime/securesms/components/settings/app/chats/sms/SmsSettingsRepository.kt index a500e5fa89..e1a1bf20aa 100644 --- a/app/src/main/java/org/thoughtcrime/securesms/components/settings/app/chats/sms/SmsSettingsRepository.kt +++ b/app/src/main/java/org/thoughtcrime/securesms/components/settings/app/chats/sms/SmsSettingsRepository.kt @@ -3,10 +3,14 @@ package org.thoughtcrime.securesms.components.settings.app.chats.sms import androidx.annotation.WorkerThread import io.reactivex.rxjava3.core.Single import io.reactivex.rxjava3.schedulers.Schedulers +import org.thoughtcrime.securesms.database.MessageDatabase import org.thoughtcrime.securesms.database.SignalDatabase import org.thoughtcrime.securesms.util.FeatureFlags -class SmsSettingsRepository { +class SmsSettingsRepository( + private val smsDatabase: MessageDatabase = SignalDatabase.sms, + private val mmsDatabase: MessageDatabase = SignalDatabase.mms +) { fun getSmsExportState(): Single { if (!FeatureFlags.smsExporter()) { return Single.just(SmsSettingsState.SmsExportState.NOT_AVAILABLE) @@ -19,9 +23,7 @@ class SmsSettingsRepository { @WorkerThread private fun checkInsecureMessageCount(): SmsSettingsState.SmsExportState? { - val smsCount = SignalDatabase.sms.insecureMessageCount - val mmsCount = SignalDatabase.mms.insecureMessageCount - val totalSmsMmsCount = smsCount + mmsCount + val totalSmsMmsCount = smsDatabase.insecureMessageCount + mmsDatabase.insecureMessageCount return if (totalSmsMmsCount == 0) { SmsSettingsState.SmsExportState.NO_SMS_MESSAGES_IN_DATABASE @@ -32,9 +34,7 @@ class SmsSettingsRepository { @WorkerThread private fun checkUnexportedInsecureMessageCount(): SmsSettingsState.SmsExportState { - val unexportedSmsCount = SignalDatabase.sms.unexportedInsecureMessages.use { it.count } - val unexportedMmsCount = SignalDatabase.mms.unexportedInsecureMessages.use { it.count } - val totalUnexportedCount = unexportedSmsCount + unexportedMmsCount + val totalUnexportedCount = smsDatabase.unexportedInsecureMessagesCount + mmsDatabase.unexportedInsecureMessagesCount return if (totalUnexportedCount > 0) { SmsSettingsState.SmsExportState.HAS_UNEXPORTED_MESSAGES 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 3462898cdc..ace2a1f06c 100644 --- a/app/src/main/java/org/thoughtcrime/securesms/database/MessageDatabase.java +++ b/app/src/main/java/org/thoughtcrime/securesms/database/MessageDatabase.java @@ -96,7 +96,7 @@ public abstract class MessageDatabase extends Database implements MmsSmsColumns, public abstract boolean isSent(long messageId); public abstract List getProfileChangeDetailsRecords(long threadId, long afterTimestamp); public abstract Set getAllRateLimitedMessageIds(); - public abstract Cursor getUnexportedInsecureMessages(); + public abstract Cursor getUnexportedInsecureMessages(int limit); public abstract int getInsecureMessageCount(); public abstract void deleteExportedMessages(); @@ -360,11 +360,22 @@ public abstract class MessageDatabase extends Database implements MmsSmsColumns, } protected String getInsecureMessageClause() { - 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 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) + ")"; - return String.format(Locale.ENGLISH, "(%s OR %s) AND NOT %s", isSent, isReceived, isSecure); + return String.format(Locale.ENGLISH, "(%s OR %s) AND NOT %s AND %s", isSent, isReceived, isSecure, isNotSecure); + } + + public int getUnexportedInsecureMessagesCount() { + try (Cursor cursor = getWritableDatabase().query(getTableName(), SqlUtil.COUNT, getInsecureMessageClause() + " AND NOT " + EXPORTED, null, null, null, null)) { + if (cursor.moveToFirst()) { + return cursor.getInt(0); + } + } + + return 0; } public void setReactionsSeen(long threadId, long sinceTimestamp) { 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 2a3ac8fc74..9a9dcd4514 100644 --- a/app/src/main/java/org/thoughtcrime/securesms/database/MmsDatabase.java +++ b/app/src/main/java/org/thoughtcrime/securesms/database/MmsDatabase.java @@ -54,7 +54,6 @@ import org.thoughtcrime.securesms.database.model.MediaMmsMessageRecord; import org.thoughtcrime.securesms.database.model.Mention; import org.thoughtcrime.securesms.database.model.MessageId; import org.thoughtcrime.securesms.database.model.MessageRecord; -import org.thoughtcrime.securesms.database.model.MmsMessageRecord; import org.thoughtcrime.securesms.database.model.NotificationMmsMessageRecord; import org.thoughtcrime.securesms.database.model.ParentStoryId; import org.thoughtcrime.securesms.database.model.Quote; @@ -2422,13 +2421,13 @@ public class MmsDatabase extends MessageDatabase { } @Override - public Cursor getUnexportedInsecureMessages() { + public Cursor getUnexportedInsecureMessages(int limit) { return rawQuery( SqlUtil.appendArg(MMS_PROJECTION, EXPORT_STATE), getInsecureMessageClause() + " AND NOT " + EXPORTED, null, false, - 0 + limit ); } 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 9db05fc62d..cb49530648 100644 --- a/app/src/main/java/org/thoughtcrime/securesms/database/SmsDatabase.java +++ b/app/src/main/java/org/thoughtcrime/securesms/database/SmsDatabase.java @@ -908,13 +908,13 @@ public class SmsDatabase extends MessageDatabase { } @Override - public Cursor getUnexportedInsecureMessages() { + public Cursor getUnexportedInsecureMessages(int limit) { return queryMessages( SqlUtil.appendArg(MESSAGE_PROJECTION, EXPORT_STATE), getInsecureMessageClause() + " AND NOT " + EXPORTED, null, false, - -1 + limit ); } diff --git a/app/src/main/java/org/thoughtcrime/securesms/exporter/SignalSmsExportReader.kt b/app/src/main/java/org/thoughtcrime/securesms/exporter/SignalSmsExportReader.kt index 30af01eb6e..aa530ae0d0 100644 --- a/app/src/main/java/org/thoughtcrime/securesms/exporter/SignalSmsExportReader.kt +++ b/app/src/main/java/org/thoughtcrime/securesms/exporter/SignalSmsExportReader.kt @@ -1,136 +1,222 @@ package org.thoughtcrime.securesms.exporter -import android.database.Cursor +import org.signal.core.util.logging.Log import org.signal.smsexporter.ExportableMessage import org.signal.smsexporter.SmsExportState import org.thoughtcrime.securesms.attachments.DatabaseAttachment +import org.thoughtcrime.securesms.database.MessageDatabase import org.thoughtcrime.securesms.database.MmsDatabase import org.thoughtcrime.securesms.database.SignalDatabase import org.thoughtcrime.securesms.database.SmsDatabase +import org.thoughtcrime.securesms.database.model.MessageId import org.thoughtcrime.securesms.database.model.MessageRecord import org.thoughtcrime.securesms.database.model.MmsMessageRecord import org.thoughtcrime.securesms.database.model.databaseprotos.MessageExportState +import org.thoughtcrime.securesms.dependencies.ApplicationDependencies import org.thoughtcrime.securesms.recipients.Recipient import org.thoughtcrime.securesms.util.JsonUtils import java.io.Closeable import kotlin.time.Duration.Companion.milliseconds +/** + * Reads through the SMS and MMS databases for insecure messages that haven't been exported. Due to cursor size limitations + * we "page" through the unexported messages to reduce chances of exceeding that limit. + */ class SignalSmsExportReader( - smsCursor: Cursor, - mmsCursor: Cursor + private val smsDatabase: MessageDatabase = SignalDatabase.sms, + private val mmsDatabase: MessageDatabase = SignalDatabase.mms ) : Iterable, Closeable { - private val smsReader = SmsDatabase.readerFor(smsCursor) - private val mmsReader = MmsDatabase.readerFor(mmsCursor) + companion object { + private val TAG = Log.tag(SignalSmsExportReader::class.java) + private const val CURSOR_LIMIT = 1000 + } + + private var smsReader: SmsDatabase.Reader? = null + private var smsDone: Boolean = false + private var mmsReader: MmsDatabase.Reader? = null + private var mmsDone: Boolean = false override fun iterator(): Iterator { return ExportableMessageIterator() } fun getCount(): Int { - return smsReader.count + mmsReader.count + return smsDatabase.unexportedInsecureMessagesCount + mmsDatabase.unexportedInsecureMessagesCount } override fun close() { - smsReader.close() - mmsReader.close() + smsReader?.close() + mmsReader?.close() + } + + private fun refreshReaders() { + if (!smsDone) { + smsReader?.close() + smsReader = null + + val refreshedSmsReader = SmsDatabase.readerFor(smsDatabase.getUnexportedInsecureMessages(CURSOR_LIMIT)) + if (refreshedSmsReader.count > 0) { + smsReader = refreshedSmsReader + return + } else { + refreshedSmsReader.close() + smsDone = true + } + } + + if (!mmsDone) { + mmsReader?.close() + mmsReader = null + + val refreshedMmsReader = MmsDatabase.readerFor(mmsDatabase.getUnexportedInsecureMessages(CURSOR_LIMIT)) + if (refreshedMmsReader.count > 0) { + mmsReader = refreshedMmsReader + return + } else { + refreshedMmsReader.close() + mmsDone = true + } + } } private inner class ExportableMessageIterator : Iterator { - private val smsIterator = smsReader.iterator() - private val mmsIterator = mmsReader.iterator() + private var smsIterator: Iterator? = null + private var mmsIterator: Iterator? = null + + private fun refreshIterators() { + refreshReaders() + smsIterator = smsReader?.iterator() + mmsIterator = mmsReader?.iterator() + } override fun hasNext(): Boolean { - return smsIterator.hasNext() || mmsIterator.hasNext() + if (smsIterator?.hasNext() == true) { + return true + } else if (!smsDone) { + refreshIterators() + if (smsIterator?.hasNext() == true) { + return true + } + } + + if (mmsIterator?.hasNext() == true) { + return true + } else if (!mmsDone) { + refreshIterators() + if (mmsIterator?.hasNext() == true) { + return true + } + } + + return false } override fun next(): ExportableMessage { - return if (smsIterator.hasNext()) { - readExportableSmsMessageFromRecord(smsIterator.next()) - } else if (mmsIterator.hasNext()) { - readExportableMmsMessageFromRecord(mmsIterator.next()) + var record: MessageRecord? = null + try { + return if (smsIterator?.hasNext() == true) { + record = smsIterator!!.next() + readExportableSmsMessageFromRecord(record, smsReader!!.messageExportStateForCurrentRecord) + } else if (mmsIterator?.hasNext() == true) { + record = mmsIterator!!.next() + readExportableMmsMessageFromRecord(record, mmsReader!!.messageExportStateForCurrentRecord) + } else { + throw NoSuchElementException() + } + } catch (e: Throwable) { + Log.w(TAG, "Error processing message: isMms: ${record?.isMms} type: ${record?.type}") + throw e + } + } + + private fun readExportableMmsMessageFromRecord(record: MessageRecord, exportState: MessageExportState): ExportableMessage { + val threadRecipient: Recipient = SignalDatabase.threads.getRecipientForThreadId(record.threadId)!! + val addresses: Set = if (threadRecipient.isMmsGroup) { + Recipient + .resolvedList(threadRecipient.participantIds) + .map { r -> r.smsExportAddress() } + .toSet() } else { - throw NoSuchElementException() + setOf(threadRecipient.smsExportAddress()) } - } - } - private fun readExportableMmsMessageFromRecord(record: MessageRecord): ExportableMessage { - val threadRecipient = SignalDatabase.threads.getRecipientForThreadId(record.threadId)!! - val addresses = if (threadRecipient.isMmsGroup) { - Recipient.resolvedList(threadRecipient.participantIds).map { it.requireSmsAddress() }.toSet() - } else { - setOf(threadRecipient.requireSmsAddress()) - } - - val parts: MutableList = mutableListOf() - if (record.body.isNotBlank()) { - parts.add(ExportableMessage.Mms.Part.Text(record.body)) - } - - if (record is MmsMessageRecord) { - val slideDeck = record.slideDeck - slideDeck.slides.forEach { - parts.add( - ExportableMessage.Mms.Part.Stream( - id = JsonUtils.toJson((it.asAttachment() as DatabaseAttachment).attachmentId), - contentType = it.contentType - ) - ) + val parts: MutableList = mutableListOf() + if (record.body.isNotBlank()) { + parts.add(ExportableMessage.Mms.Part.Text(record.body)) } - } - val sender = if (record.isOutgoing) Recipient.self().requireSmsAddress() else record.individualRecipient.requireSmsAddress() + if (record is MmsMessageRecord) { + val slideDeck = record.slideDeck + slideDeck + .slides + .filter { it.asAttachment() is DatabaseAttachment } + .forEach { + parts.add( + ExportableMessage.Mms.Part.Stream( + id = JsonUtils.toJson((it.asAttachment() as DatabaseAttachment).attachmentId), + contentType = it.contentType + ) + ) + } + } - return ExportableMessage.Mms( - id = record.id.toString(), - exportState = mapExportState(mmsReader.messageExportStateForCurrentRecord), - addresses = addresses, - dateReceived = record.dateReceived.milliseconds, - dateSent = record.dateSent.milliseconds, - isRead = true, - isOutgoing = record.isOutgoing, - parts = parts, - sender = sender - ) - } + val sender: String = if (record.isOutgoing) Recipient.self().smsExportAddress() else record.individualRecipient.smsExportAddress() - private fun readExportableSmsMessageFromRecord(record: MessageRecord): ExportableMessage { - val threadRecipient = SignalDatabase.threads.getRecipientForThreadId(record.threadId)!! - - return if (threadRecipient.isMmsGroup) { - readExportableMmsMessageFromRecord(record) - } else { - ExportableMessage.Sms( - id = record.id.toString(), - exportState = mapExportState(smsReader.messageExportStateForCurrentRecord), - address = record.recipient.requireSmsAddress(), + return ExportableMessage.Mms( + id = MessageId(record.id, record.isMms), + exportState = mapExportState(exportState), + addresses = addresses, dateReceived = record.dateReceived.milliseconds, dateSent = record.dateSent.milliseconds, isRead = true, isOutgoing = record.isOutgoing, - body = record.body + parts = parts, + sender = sender ) } - } - private fun mapExportState(messageExportState: MessageExportState): SmsExportState { - return SmsExportState( - messageId = messageExportState.messageId, - startedRecipients = messageExportState.startedRecipientsList.toSet(), - completedRecipients = messageExportState.completedRecipientsList.toSet(), - startedAttachments = messageExportState.startedAttachmentsList.toSet(), - completedAttachments = messageExportState.completedAttachmentsList.toSet(), - progress = messageExportState.progress.let { - when (it) { - MessageExportState.Progress.INIT -> SmsExportState.Progress.INIT - MessageExportState.Progress.STARTED -> SmsExportState.Progress.STARTED - MessageExportState.Progress.COMPLETED -> SmsExportState.Progress.COMPLETED - MessageExportState.Progress.UNRECOGNIZED -> SmsExportState.Progress.INIT - null -> SmsExportState.Progress.INIT - } + private fun readExportableSmsMessageFromRecord(record: MessageRecord, exportState: MessageExportState): ExportableMessage { + val threadRecipient = SignalDatabase.threads.getRecipientForThreadId(record.threadId)!! + + return if (threadRecipient.isMmsGroup) { + readExportableMmsMessageFromRecord(record, exportState) + } else { + ExportableMessage.Sms( + id = MessageId(record.id, record.isMms), + exportState = mapExportState(exportState), + address = record.recipient.smsExportAddress(), + dateReceived = record.dateReceived.milliseconds, + dateSent = record.dateSent.milliseconds, + isRead = true, + isOutgoing = record.isOutgoing, + body = record.body + ) } - ) + } + + private fun mapExportState(messageExportState: MessageExportState): SmsExportState { + return SmsExportState( + messageId = messageExportState.messageId, + startedRecipients = messageExportState.startedRecipientsList.toSet(), + completedRecipients = messageExportState.completedRecipientsList.toSet(), + startedAttachments = messageExportState.startedAttachmentsList.toSet(), + completedAttachments = messageExportState.completedAttachmentsList.toSet(), + progress = messageExportState.progress.let { + when (it) { + MessageExportState.Progress.INIT -> SmsExportState.Progress.INIT + MessageExportState.Progress.STARTED -> SmsExportState.Progress.STARTED + MessageExportState.Progress.COMPLETED -> SmsExportState.Progress.COMPLETED + MessageExportState.Progress.UNRECOGNIZED -> SmsExportState.Progress.INIT + null -> SmsExportState.Progress.INIT + } + } + ) + } + + private fun Recipient.smsExportAddress(): String { + return smsAddress.orElseGet { getDisplayName(ApplicationDependencies.getApplication()) } + } } } diff --git a/app/src/main/java/org/thoughtcrime/securesms/exporter/SignalSmsExportService.kt b/app/src/main/java/org/thoughtcrime/securesms/exporter/SignalSmsExportService.kt index 4cb6f87975..9e76bbf870 100644 --- a/app/src/main/java/org/thoughtcrime/securesms/exporter/SignalSmsExportService.kt +++ b/app/src/main/java/org/thoughtcrime/securesms/exporter/SignalSmsExportService.kt @@ -4,6 +4,7 @@ import android.content.Context import android.content.Intent import androidx.core.app.NotificationCompat import androidx.core.content.ContextCompat +import app.cash.exhaustive.Exhaustive import org.signal.smsexporter.ExportableMessage import org.signal.smsexporter.SmsExportService import org.thoughtcrime.securesms.R @@ -36,7 +37,7 @@ class SignalSmsExportService : SmsExportService() { return ExportNotification( NotificationIds.SMS_EXPORT_SERVICE, NotificationCompat.Builder(this, NotificationChannels.BACKUPS) - .setSmallIcon(R.drawable.ic_launcher_foreground) + .setSmallIcon(R.drawable.ic_signal_backup) .setContentTitle(getString(R.string.SignalSmsExportService__exporting_messages)) .setProgress(total, progress, false) .build() @@ -126,18 +127,22 @@ class SignalSmsExportService : SmsExportService() { } private fun ExportableMessage.getMessageId(): MessageId { - return when (this) { - is ExportableMessage.Mms -> MessageId(id.toLong(), true) - is ExportableMessage.Sms -> MessageId(id.toLong(), false) + @Exhaustive + val messageId: Any = when (this) { + is ExportableMessage.Mms<*> -> id + is ExportableMessage.Sms<*> -> id + } + + if (messageId is MessageId) { + return messageId + } else { + throw AssertionError("Exportable message id must be type MessageId. Type: ${messageId.javaClass}") } } private fun ensureReader() { if (reader == null) { - reader = SignalSmsExportReader( - smsCursor = SignalDatabase.sms.unexportedInsecureMessages, - mmsCursor = SignalDatabase.mms.unexportedInsecureMessages - ) + reader = SignalSmsExportReader() } } } diff --git a/app/src/main/java/org/thoughtcrime/securesms/exporter/flow/ChooseANewDefaultSmsAppFragment.kt b/app/src/main/java/org/thoughtcrime/securesms/exporter/flow/ChooseANewDefaultSmsAppFragment.kt index 2a1144fb06..23c7714f0b 100644 --- a/app/src/main/java/org/thoughtcrime/securesms/exporter/flow/ChooseANewDefaultSmsAppFragment.kt +++ b/app/src/main/java/org/thoughtcrime/securesms/exporter/flow/ChooseANewDefaultSmsAppFragment.kt @@ -1,6 +1,7 @@ package org.thoughtcrime.securesms.exporter.flow import android.app.Activity +import android.os.Build import android.os.Bundle import android.view.View import androidx.fragment.app.Fragment @@ -23,6 +24,12 @@ class ChooseANewDefaultSmsAppFragment : Fragment(R.layout.choose_a_new_default_s override fun onViewCreated(view: View, savedInstanceState: Bundle?) { val binding = ChooseANewDefaultSmsAppFragmentBinding.bind(view) + if (Build.VERSION.SDK_INT < 24) { + binding.bullet1Text.setText(R.string.ChooseANewDefaultSmsAppFragment__open_your_phones_settings_app) + binding.bullet2Text.setText(R.string.ChooseANewDefaultSmsAppFragment__navigate_to_apps_default_apps_sms_app) + binding.continueButton.setText(R.string.ChooseANewDefaultSmsAppFragment__done) + } + DefaultSmsHelper.releaseDefaultSms(requireContext()).either( onSuccess = { binding.continueButton.setOnClickListener { _ -> startActivity(it) } 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 25b5d23736..a8ea19ebf9 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 @@ -1,10 +1,14 @@ 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.fragment.app.Fragment import androidx.navigation.fragment.findNavController import io.reactivex.rxjava3.android.schedulers.AndroidSchedulers +import io.reactivex.rxjava3.disposables.Disposable import org.signal.smsexporter.SmsExportProgress import org.signal.smsexporter.SmsExportService import org.thoughtcrime.securesms.R @@ -20,6 +24,30 @@ import org.thoughtcrime.securesms.util.navigation.safeNavigate class ExportingSmsMessagesFragment : Fragment(R.layout.exporting_sms_messages_fragment) { private val lifecycleDisposable = LifecycleDisposable() + private var navigationDisposable = Disposable.disposed() + + override fun onGetLayoutInflater(savedInstanceState: Bundle?): LayoutInflater { + val inflater = super.onGetLayoutInflater(savedInstanceState) + val contextThemeWrapper: Context = ContextThemeWrapper(requireContext(), R.style.Signal_DayNight) + return inflater.cloneInContext(contextThemeWrapper) + } + + override fun onResume() { + super.onResume() + navigationDisposable = SmsExportService + .progressState + .observeOn(AndroidSchedulers.mainThread()) + .subscribe { + if (it is SmsExportProgress.Done) { + findNavController().safeNavigate(ExportingSmsMessagesFragmentDirections.actionExportingSmsMessagesFragmentToChooseANewDefaultSmsAppFragment()) + } + } + } + + override fun onPause() { + super.onPause() + navigationDisposable.dispose() + } override fun onViewCreated(view: View, savedInstanceState: Bundle?) { val binding = ExportingSmsMessagesFragmentBinding.bind(view) @@ -27,9 +55,7 @@ class ExportingSmsMessagesFragment : Fragment(R.layout.exporting_sms_messages_fr lifecycleDisposable.bindTo(viewLifecycleOwner) lifecycleDisposable += SmsExportService.progressState.observeOn(AndroidSchedulers.mainThread()).subscribe { when (it) { - SmsExportProgress.Done -> { - findNavController().safeNavigate(ExportingSmsMessagesFragmentDirections.actionExportingSmsMessagesFragmentToChooseANewDefaultSmsAppFragment()) - } + SmsExportProgress.Done -> Unit is SmsExportProgress.InProgress -> { binding.progress.isIndeterminate = false binding.progress.max = it.total 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 7be09db061..ef55771be2 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 @@ -6,8 +6,15 @@ import androidx.fragment.app.Fragment import androidx.navigation.fragment.NavHostFragment import org.thoughtcrime.securesms.R import org.thoughtcrime.securesms.components.FragmentWrapperActivity +import org.thoughtcrime.securesms.util.WindowUtil class SmsExportActivity : FragmentWrapperActivity() { + + override fun onResume() { + super.onResume() + WindowUtil.setLightStatusBarFromTheme(this) + } + override fun getFragment(): Fragment { return NavHostFragment.create(R.navigation.sms_export) } diff --git a/app/src/main/res/drawable-night/choose_signal.xml b/app/src/main/res/drawable-night/choose_signal.xml new file mode 100644 index 0000000000..04bcbd8b68 --- /dev/null +++ b/app/src/main/res/drawable-night/choose_signal.xml @@ -0,0 +1,50 @@ + + + + + + + + + + + + + + + diff --git a/app/src/main/res/drawable-night/export_sms.xml b/app/src/main/res/drawable-night/export_sms.xml new file mode 100644 index 0000000000..2c08dfe180 --- /dev/null +++ b/app/src/main/res/drawable-night/export_sms.xml @@ -0,0 +1,37 @@ + + + + + + + + + + + + + diff --git a/app/src/main/res/drawable-night/sms_message.xml b/app/src/main/res/drawable-night/sms_message.xml new file mode 100644 index 0000000000..d58670ed9b --- /dev/null +++ b/app/src/main/res/drawable-night/sms_message.xml @@ -0,0 +1,25 @@ + + + + + + + diff --git a/app/src/main/res/drawable/choose_signal.xml b/app/src/main/res/drawable/choose_signal.xml new file mode 100644 index 0000000000..8def5262f0 --- /dev/null +++ b/app/src/main/res/drawable/choose_signal.xml @@ -0,0 +1,46 @@ + + + + + + + + + + + + + + diff --git a/app/src/main/res/drawable/export_sms.xml b/app/src/main/res/drawable/export_sms.xml new file mode 100644 index 0000000000..3588ae7280 --- /dev/null +++ b/app/src/main/res/drawable/export_sms.xml @@ -0,0 +1,37 @@ + + + + + + + + + + + + + diff --git a/app/src/main/res/drawable/sms_message.xml b/app/src/main/res/drawable/sms_message.xml new file mode 100644 index 0000000000..116445c2f3 --- /dev/null +++ b/app/src/main/res/drawable/sms_message.xml @@ -0,0 +1,25 @@ + + + + + + + diff --git a/app/src/main/res/layout/choose_a_new_default_sms_app_fragment.xml b/app/src/main/res/layout/choose_a_new_default_sms_app_fragment.xml index c128a6b2dc..145f3a9c8b 100644 --- a/app/src/main/res/layout/choose_a_new_default_sms_app_fragment.xml +++ b/app/src/main/res/layout/choose_a_new_default_sms_app_fragment.xml @@ -1,57 +1,259 @@ - + android:layout_height="match_parent" + android:fillViewport="true"> - + android:layout_height="wrap_content"> - + - + - + - \ No newline at end of file + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/app/src/main/res/layout/export_your_sms_messages_fragment.xml b/app/src/main/res/layout/export_your_sms_messages_fragment.xml index 9b54526fe7..2bb21296c9 100644 --- a/app/src/main/res/layout/export_your_sms_messages_fragment.xml +++ b/app/src/main/res/layout/export_your_sms_messages_fragment.xml @@ -13,14 +13,14 @@ app:layout_constraintTop_toTopOf="parent" app:navigationIcon="@drawable/ic_arrow_left_24" /> - - diff --git a/app/src/main/res/layout/exporting_sms_messages_fragment.xml b/app/src/main/res/layout/exporting_sms_messages_fragment.xml index f833c64e01..6763892379 100644 --- a/app/src/main/res/layout/exporting_sms_messages_fragment.xml +++ b/app/src/main/res/layout/exporting_sms_messages_fragment.xml @@ -26,7 +26,7 @@ android:layout_marginHorizontal="32dp" android:layout_marginTop="24dp" android:gravity="center" - tools:text="Please don't close the app..." + tools:text="[WIP]" android:textAppearance="@style/Signal.Text.BodyLarge" android:textColor="@color/signal_colorOnSurfaceVariant" app:layout_constraintEnd_toEndOf="parent" diff --git a/app/src/main/res/layout/set_signal_as_default_sms_app_fragment.xml b/app/src/main/res/layout/set_signal_as_default_sms_app_fragment.xml index 20a479fed2..5dabaf147b 100644 --- a/app/src/main/res/layout/set_signal_as_default_sms_app_fragment.xml +++ b/app/src/main/res/layout/set_signal_as_default_sms_app_fragment.xml @@ -1,7 +1,6 @@ @@ -13,8 +12,7 @@ app:layout_constraintTop_toTopOf="parent" app:navigationIcon="@drawable/ic_arrow_left_24" /> - - + app:layout_constraintVertical_bias="0" + app:srcCompat="@drawable/choose_signal" /> @@ -42,10 +41,10 @@ android:layout_marginHorizontal="32dp" android:layout_marginTop="24dp" android:gravity="center" + android:text="@string/SetSignalAsDefaultSmsAppFragment__to_export_your_sms_messages" android:textAppearance="@style/Signal.Text.BodyLarge" android:textColor="@color/signal_colorOnSurfaceVariant" - app:layout_constraintTop_toBottomOf="@id/headline" - tools:text="WIP Placeholder text" /> + app:layout_constraintTop_toBottomOf="@id/headline" /> Remove SMS messages - SMS messages removed + Removing SMS messages from Signal… + + You can remove SMS messages from Signal in Settings at any time. Messages @@ -5231,6 +5233,8 @@ Export your SMS messages + + You can export your SMS messages to your phone\'s SMS database. This allows other SMS apps on your phone to access and import them. This does not create a shareable file of your SMS message history. Continue @@ -5245,6 +5249,28 @@ Choose a new default SMS app Continue + + Done + + 1 + + 2 + + 3 + + 4 + + Tap \"Continue\" to open the \"Default apps\" screen in Settings + + Select \"SMS app\" from the list + + Choose another app to use for SMS messaging + + Return to Signal + + Open your phone\'s Settings app + + Navigate to \"Apps\" > \"Default apps\" > \"SMS app\" @@ -5254,11 +5280,13 @@ Remove SMS messages from Signal? - You have changed the default SMS app, do you want to remove SMS messages from Signal? + You can now remove SMS messages from Signal to clear up storage space. They will still be available to other SMS apps on your phone even if you remove them. - First, set Signal as the default SMS app + Set Signal as the default SMS app + + To export your SMS messages, you need to set Signal as the default SMS app. Next diff --git a/sms-exporter/app/src/main/java/org/signal/smsexporter/app/TestSmsExportService.kt b/sms-exporter/app/src/main/java/org/signal/smsexporter/app/TestSmsExportService.kt index 8685989164..6b8aceaee7 100644 --- a/sms-exporter/app/src/main/java/org/signal/smsexporter/app/TestSmsExportService.kt +++ b/sms-exporter/app/src/main/java/org/signal/smsexporter/app/TestSmsExportService.kt @@ -124,7 +124,7 @@ class TestSmsExportService : SmsExportService() { return message } - private fun getMmsMessage(it: Int): ExportableMessage.Mms { + private fun getMmsMessage(it: Int): ExportableMessage.Mms<*> { val me = "+15065550101" val addresses = setOf(me, "+15065550102", "+15065550121") val address = addresses.random() @@ -144,7 +144,7 @@ class TestSmsExportService : SmsExportService() { ) } - private fun getSmsMessage(it: Int): ExportableMessage.Sms { + private fun getSmsMessage(it: Int): ExportableMessage.Sms<*> { return ExportableMessage.Sms( id = it.toString(), exportState = SmsExportState(), diff --git a/sms-exporter/lib/src/main/java/org/signal/smsexporter/ExportableMessage.kt b/sms-exporter/lib/src/main/java/org/signal/smsexporter/ExportableMessage.kt index 3ff59d1800..43609d9c60 100644 --- a/sms-exporter/lib/src/main/java/org/signal/smsexporter/ExportableMessage.kt +++ b/sms-exporter/lib/src/main/java/org/signal/smsexporter/ExportableMessage.kt @@ -16,8 +16,8 @@ sealed interface ExportableMessage { /** * An exportable SMS message */ - data class Sms( - val id: String, + data class Sms( + val id: ID, override val exportState: SmsExportState, val address: String, val dateReceived: Duration, @@ -30,8 +30,8 @@ sealed interface ExportableMessage { /** * An exportable MMS message */ - data class Mms( - val id: String, + data class Mms( + val id: ID, override val exportState: SmsExportState, val addresses: Set, val dateReceived: Duration, 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 c496250008..0135a5e4c3 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 @@ -65,12 +65,14 @@ abstract class SmsExportService : Service() { val exportState = message.exportState if (exportState.progress != SmsExportState.Progress.COMPLETED) { when (message) { - is ExportableMessage.Sms -> exportSms(exportState, message) - is ExportableMessage.Mms -> exportMms(exportState, message) + is ExportableMessage.Sms<*> -> exportSms(exportState, message) + is ExportableMessage.Mms<*> -> exportMms(exportState, message) } progress++ - updateNotification(progress, totalCount) + if (progress == 1 || progress.mod(100) == 0) { + updateNotification(progress, totalCount) + } progressState.onNext(SmsExportProgress.InProgress(progress, totalCount)) } } @@ -177,7 +179,7 @@ abstract class SmsExportService : Service() { startForeground(exportNotification.id, exportNotification.notification) } - private fun exportSms(smsExportState: SmsExportState, sms: ExportableMessage.Sms) { + private fun exportSms(smsExportState: SmsExportState, sms: ExportableMessage.Sms<*>) { onMessageExportStarted(sms) val mayAlreadyExist = smsExportState.progress == SmsExportState.Progress.STARTED ExportSmsMessagesUseCase.execute(this, sms, mayAlreadyExist).either(onSuccess = { @@ -187,7 +189,7 @@ abstract class SmsExportService : Service() { }) } - private fun exportMms(smsExportState: SmsExportState, mms: ExportableMessage.Mms) { + private fun exportMms(smsExportState: SmsExportState, mms: ExportableMessage.Mms<*>) { onMessageExportStarted(mms) val threadIdOutput: GetOrCreateMmsThreadIdsUseCase.Output? = getThreadId(mms) val exportMmsOutput: ExportMmsMessagesUseCase.Output? = threadIdOutput?.let { exportMms(smsExportState, it) } @@ -207,7 +209,7 @@ abstract class SmsExportService : Service() { } } - private fun getThreadId(mms: ExportableMessage.Mms): GetOrCreateMmsThreadIdsUseCase.Output? { + private fun getThreadId(mms: ExportableMessage.Mms<*>): GetOrCreateMmsThreadIdsUseCase.Output? { return GetOrCreateMmsThreadIdsUseCase.execute(this, mms, threadCache).either( onSuccess = { output -> output diff --git a/sms-exporter/lib/src/main/java/org/signal/smsexporter/internal/mms/ExportMmsMessagesUseCase.kt b/sms-exporter/lib/src/main/java/org/signal/smsexporter/internal/mms/ExportMmsMessagesUseCase.kt index 1a1d9d5e99..37795e4346 100644 --- a/sms-exporter/lib/src/main/java/org/signal/smsexporter/internal/mms/ExportMmsMessagesUseCase.kt +++ b/sms-exporter/lib/src/main/java/org/signal/smsexporter/internal/mms/ExportMmsMessagesUseCase.kt @@ -17,7 +17,7 @@ internal object ExportMmsMessagesUseCase { private val TAG = Log.tag(ExportMmsMessagesUseCase::class.java) - internal fun getTransactionId(mms: ExportableMessage.Mms): String { + internal fun getTransactionId(mms: ExportableMessage.Mms<*>): String { return "signal:T${mms.id}" } @@ -82,7 +82,7 @@ internal object ExportMmsMessagesUseCase { } data class Output( - val mms: ExportableMessage.Mms, + val mms: ExportableMessage.Mms<*>, val messageId: Long ) } diff --git a/sms-exporter/lib/src/main/java/org/signal/smsexporter/internal/mms/GetOrCreateMmsThreadIdsUseCase.kt b/sms-exporter/lib/src/main/java/org/signal/smsexporter/internal/mms/GetOrCreateMmsThreadIdsUseCase.kt index 56d7f22efc..ea042c241e 100644 --- a/sms-exporter/lib/src/main/java/org/signal/smsexporter/internal/mms/GetOrCreateMmsThreadIdsUseCase.kt +++ b/sms-exporter/lib/src/main/java/org/signal/smsexporter/internal/mms/GetOrCreateMmsThreadIdsUseCase.kt @@ -14,7 +14,7 @@ import org.signal.smsexporter.ExportableMessage internal object GetOrCreateMmsThreadIdsUseCase { fun execute( context: Context, - mms: ExportableMessage.Mms, + mms: ExportableMessage.Mms<*>, threadCache: MutableMap, Long> ): Try { return try { @@ -37,7 +37,7 @@ internal object GetOrCreateMmsThreadIdsUseCase { } } - private fun getRecipientSet(mms: ExportableMessage.Mms): Set { + private fun getRecipientSet(mms: ExportableMessage.Mms<*>): Set { val recipients = mms.addresses if (recipients.isEmpty()) { error("Expected non-empty recipient count.") @@ -46,5 +46,5 @@ internal object GetOrCreateMmsThreadIdsUseCase { return HashSet(recipients.map { it }) } - data class Output(val mms: ExportableMessage.Mms, val threadId: Long) + data class Output(val mms: ExportableMessage.Mms<*>, val threadId: Long) } diff --git a/sms-exporter/lib/src/main/java/org/signal/smsexporter/internal/sms/ExportSmsMessagesUseCase.kt b/sms-exporter/lib/src/main/java/org/signal/smsexporter/internal/sms/ExportSmsMessagesUseCase.kt index c095cdc55f..24538ccf55 100644 --- a/sms-exporter/lib/src/main/java/org/signal/smsexporter/internal/sms/ExportSmsMessagesUseCase.kt +++ b/sms-exporter/lib/src/main/java/org/signal/smsexporter/internal/sms/ExportSmsMessagesUseCase.kt @@ -12,7 +12,7 @@ import java.lang.Exception * Returns nothing. */ internal object ExportSmsMessagesUseCase { - fun execute(context: Context, sms: ExportableMessage.Sms, checkForExistence: Boolean): Try { + fun execute(context: Context, sms: ExportableMessage.Sms<*>, checkForExistence: Boolean): Try { try { if (checkForExistence) { val exists = context.contentResolver.query( diff --git a/sms-exporter/lib/src/test/java/org/signal/smsexporter/TestUtils.kt b/sms-exporter/lib/src/test/java/org/signal/smsexporter/TestUtils.kt index a17e629690..b8aee722c0 100644 --- a/sms-exporter/lib/src/test/java/org/signal/smsexporter/TestUtils.kt +++ b/sms-exporter/lib/src/test/java/org/signal/smsexporter/TestUtils.kt @@ -15,7 +15,7 @@ object TestUtils { isRead: Boolean = false, isOutgoing: Boolean = false, body: String = "Hello, $id" - ): ExportableMessage.Sms { + ): ExportableMessage.Sms<*> { return ExportableMessage.Sms(id, SmsExportState(), address, dateReceived, dateSent, isRead, isOutgoing, body) } @@ -28,7 +28,7 @@ object TestUtils { isOutgoing: Boolean = false, parts: List = listOf(ExportableMessage.Mms.Part.Text("Hello, $id")), sender: CharSequence = "+15555060177" - ): ExportableMessage.Mms { + ): ExportableMessage.Mms<*> { return ExportableMessage.Mms(id, SmsExportState(), addresses, dateReceived, dateSent, isRead, isOutgoing, parts, sender) } diff --git a/sms-exporter/lib/src/test/java/org/signal/smsexporter/internal/mms/ExportMmsMessagesUseCaseTest.kt b/sms-exporter/lib/src/test/java/org/signal/smsexporter/internal/mms/ExportMmsMessagesUseCaseTest.kt index eef3e98df5..a896654787 100644 --- a/sms-exporter/lib/src/test/java/org/signal/smsexporter/internal/mms/ExportMmsMessagesUseCaseTest.kt +++ b/sms-exporter/lib/src/test/java/org/signal/smsexporter/internal/mms/ExportMmsMessagesUseCaseTest.kt @@ -104,7 +104,7 @@ class ExportMmsMessagesUseCaseTest { } private fun validateExportedMessage( - mms: ExportableMessage.Mms, + mms: ExportableMessage.Mms<*>, expectedRowCount: Int = 1, threadId: Long = 1L ) { diff --git a/sms-exporter/lib/src/test/java/org/signal/smsexporter/internal/sms/ExportSmsMessagesUseCaseTest.kt b/sms-exporter/lib/src/test/java/org/signal/smsexporter/internal/sms/ExportSmsMessagesUseCaseTest.kt index 901d7c1a1c..674030ed50 100644 --- a/sms-exporter/lib/src/test/java/org/signal/smsexporter/internal/sms/ExportSmsMessagesUseCaseTest.kt +++ b/sms-exporter/lib/src/test/java/org/signal/smsexporter/internal/sms/ExportSmsMessagesUseCaseTest.kt @@ -101,7 +101,7 @@ class ExportSmsMessagesUseCaseTest { ) } - private fun validateExportedMessage(sms: ExportableMessage.Sms, expectedRowCount: Int = 1) { + private fun validateExportedMessage(sms: ExportableMessage.Sms<*>, expectedRowCount: Int = 1) { // 1. Grab the SMS record from the content resolver val context: Context = ApplicationProvider.getApplicationContext() val baseUri: Uri = Telephony.Sms.CONTENT_URI