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