diff --git a/app/src/main/java/org/thoughtcrime/securesms/components/settings/app/chats/ChatsSettingsFragment.kt b/app/src/main/java/org/thoughtcrime/securesms/components/settings/app/chats/ChatsSettingsFragment.kt index 4fcd2b620..a6b54e5df 100644 --- a/app/src/main/java/org/thoughtcrime/securesms/components/settings/app/chats/ChatsSettingsFragment.kt +++ b/app/src/main/java/org/thoughtcrime/securesms/components/settings/app/chats/ChatsSettingsFragment.kt @@ -70,6 +70,16 @@ class ChatsSettingsFragment : DSLSettingsFragment(R.string.preferences_chats__ch } ) + clickPref( + title = DSLSettingsText.from(R.string.SmsSettingsFragment__export_sms_messages_again), + summary = DSLSettingsText.from(R.string.SmsSettingsFragment__exporting_again_can_result_in_duplicate_messages), + onClick = { + SmsExportDialogs.showSmsReExportDialog(requireContext()) { + smsExportLauncher.launch(SmsExportActivity.createIntent(requireContext(), isReExport = true)) + } + } + ) + dividerPref() } SmsExportState.NO_SMS_MESSAGES_IN_DATABASE -> Unit 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 78132c1f6..d877b202c 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 @@ -98,6 +98,16 @@ class SmsSettingsFragment : DSLSettingsFragment(R.string.preferences__sms_mms) { } ) + clickPref( + title = DSLSettingsText.from(R.string.SmsSettingsFragment__export_sms_messages_again), + summary = DSLSettingsText.from(R.string.SmsSettingsFragment__exporting_again_can_result_in_duplicate_messages), + onClick = { + SmsExportDialogs.showSmsReExportDialog(requireContext()) { + smsExportLauncher.launch(SmsExportActivity.createIntent(requireContext(), isReExport = true)) + } + } + ) + dividerPref() } SmsExportState.NO_SMS_MESSAGES_IN_DATABASE -> Unit 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 5d4354775..37acb5c9c 100644 --- a/app/src/main/java/org/thoughtcrime/securesms/database/MessageDatabase.java +++ b/app/src/main/java/org/thoughtcrime/securesms/database/MessageDatabase.java @@ -417,6 +417,20 @@ public abstract class MessageDatabase extends Database implements MmsSmsColumns, return 0; } + /** + * Resets the exported state and exported flag so messages can be re-exported. + */ + public void clearExportState() { + ContentValues values = new ContentValues(2); + values.putNull(EXPORT_STATE); + values.put(EXPORTED, MessageExportStatus.UNEXPORTED.serialize()); + + SQLiteDatabaseExtensionsKt.update(getWritableDatabase(), getTableName()) + .values(values) + .where(EXPORT_STATE + " IS NOT NULL OR " + EXPORTED + " != ?", MessageExportStatus.UNEXPORTED) + .run(); + } + /** * Reset the exported status (not state) to the default for clearing errors. */ 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 57e484352..81c4bea9e 100644 --- a/app/src/main/java/org/thoughtcrime/securesms/exporter/SignalSmsExportService.kt +++ b/app/src/main/java/org/thoughtcrime/securesms/exporter/SignalSmsExportService.kt @@ -31,8 +31,10 @@ class SignalSmsExportService : SmsExportService() { /** * Launches the export service and immediately begins exporting messages. */ - fun start(context: Context) { - ContextCompat.startForegroundService(context, Intent(context, SignalSmsExportService::class.java)) + fun start(context: Context, clearPreviousExportState: Boolean) { + val intent = Intent(context, SignalSmsExportService::class.java) + .apply { putExtra(CLEAR_PREVIOUS_EXPORT_STATE_EXTRA, clearPreviousExportState) } + ContextCompat.startForegroundService(context, intent) } } @@ -80,6 +82,11 @@ class SignalSmsExportService : SmsExportService() { ) } + override fun clearPreviousExportState() { + SignalDatabase.sms.clearExportState() + SignalDatabase.mms.clearExportState() + } + override fun prepareForExport() { SignalDatabase.sms.clearInsecureMessageExportedErrorStatus() SignalDatabase.mms.clearInsecureMessageExportedErrorStatus() 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 338e7c594..5f3c82c1c 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 @@ -8,6 +8,7 @@ import android.view.ContextThemeWrapper import android.view.LayoutInflater import android.view.View import androidx.fragment.app.Fragment +import androidx.fragment.app.activityViewModels import androidx.navigation.fragment.findNavController import com.google.android.material.dialog.MaterialAlertDialogBuilder import io.reactivex.rxjava3.android.schedulers.AndroidSchedulers @@ -28,6 +29,8 @@ import org.thoughtcrime.securesms.util.navigation.safeNavigate */ class ExportingSmsMessagesFragment : Fragment(R.layout.exporting_sms_messages_fragment) { + private val viewModel: SmsExportViewModel by activityViewModels() + private val lifecycleDisposable = LifecycleDisposable() private var navigationDisposable = Disposable.disposed() @@ -109,7 +112,7 @@ class ExportingSmsMessagesFragment : Fragment(R.layout.exporting_sms_messages_fr .request(Manifest.permission.READ_SMS) .ifNecessary() .withRationaleDialog(getString(R.string.ExportingSmsMessagesFragment__signal_needs_the_sms_permission_to_be_able_to_export_your_sms_messages), R.drawable.ic_messages_solid_24) - .onAllGranted { SignalSmsExportService.start(requireContext()) } + .onAllGranted { SignalSmsExportService.start(requireContext(), viewModel.isReExport) } .withPermanentDenialDialog(getString(R.string.ExportingSmsMessagesFragment__signal_needs_the_sms_permission_to_be_able_to_export_your_sms_messages)) { requireActivity().finish() } .onAnyDenied { checkPermissionsAndStartExport() } .execute() 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 3b818fe5c..080c03bec 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,6 +6,7 @@ import android.os.Bundle import androidx.activity.OnBackPressedCallback import androidx.core.app.NotificationManagerCompat import androidx.fragment.app.Fragment +import androidx.lifecycle.ViewModelProvider import androidx.navigation.findNavController import androidx.navigation.fragment.NavHostFragment import org.thoughtcrime.securesms.R @@ -15,15 +16,21 @@ import org.thoughtcrime.securesms.util.WindowUtil class SmsExportActivity : FragmentWrapperActivity() { + private lateinit var viewModel: SmsExportViewModel + override fun onResume() { super.onResume() WindowUtil.setLightStatusBarFromTheme(this) NotificationManagerCompat.from(this).cancel(NotificationIds.SMS_EXPORT_COMPLETE) } + @Suppress("ReplaceGetOrSet") override fun onCreate(savedInstanceState: Bundle?, ready: Boolean) { super.onCreate(savedInstanceState, ready) onBackPressedDispatcher.addCallback(this, OnBackPressed()) + + val factory = SmsExportViewModel.Factory(intent.getBooleanExtra(IS_RE_EXPORT, false)) + viewModel = ViewModelProvider(this, factory).get(SmsExportViewModel::class.java) } override fun getFragment(): Fragment { @@ -39,7 +46,14 @@ class SmsExportActivity : FragmentWrapperActivity() { } companion object { + const val IS_RE_EXPORT = "is_re_export" + + @JvmOverloads @JvmStatic - fun createIntent(context: Context): Intent = Intent(context, SmsExportActivity::class.java) + fun createIntent(context: Context, isReExport: Boolean = false): Intent { + return Intent(context, SmsExportActivity::class.java).apply { + putExtra(IS_RE_EXPORT, isReExport) + } + } } } diff --git a/app/src/main/java/org/thoughtcrime/securesms/exporter/flow/SmsExportDialogs.kt b/app/src/main/java/org/thoughtcrime/securesms/exporter/flow/SmsExportDialogs.kt index ee13ddab9..bc78c0d73 100644 --- a/app/src/main/java/org/thoughtcrime/securesms/exporter/flow/SmsExportDialogs.kt +++ b/app/src/main/java/org/thoughtcrime/securesms/exporter/flow/SmsExportDialogs.kt @@ -26,4 +26,14 @@ object SmsExportDialogs { } .show() } + + @JvmStatic + fun showSmsReExportDialog(context: Context, continueCallback: Runnable) { + MaterialAlertDialogBuilder(context) + .setTitle(R.string.ReExportSmsMessagesDialogFragment__export_sms_again) + .setMessage(R.string.ReExportSmsMessagesDialogFragment__you_already_exported_your_sms_messages) + .setPositiveButton(R.string.ReExportSmsMessagesDialogFragment__continue) { _, _ -> continueCallback.run() } + .setNegativeButton(R.string.ReExportSmsMessagesDialogFragment__cancel, null) + .show() + } } diff --git a/app/src/main/java/org/thoughtcrime/securesms/exporter/flow/SmsExportViewModel.kt b/app/src/main/java/org/thoughtcrime/securesms/exporter/flow/SmsExportViewModel.kt new file mode 100644 index 000000000..31625e284 --- /dev/null +++ b/app/src/main/java/org/thoughtcrime/securesms/exporter/flow/SmsExportViewModel.kt @@ -0,0 +1,17 @@ +package org.thoughtcrime.securesms.exporter.flow + +import androidx.lifecycle.ViewModel +import androidx.lifecycle.ViewModelProvider + +/** + * Hold shared state for the SMS export flow. + * + * Note: Will be expanded on eventually to support different behavior when entering via megaphone. + */ +class SmsExportViewModel(val isReExport: Boolean) : ViewModel() { + class Factory(private val isReExport: Boolean) : ViewModelProvider.Factory { + override fun create(modelClass: Class): T { + return requireNotNull(modelClass.cast(SmsExportViewModel(isReExport))) + } + } +} diff --git a/app/src/main/java/org/thoughtcrime/securesms/keyvalue/MiscellaneousValues.java b/app/src/main/java/org/thoughtcrime/securesms/keyvalue/MiscellaneousValues.java index f14ac274b..69f3ecf7c 100644 --- a/app/src/main/java/org/thoughtcrime/securesms/keyvalue/MiscellaneousValues.java +++ b/app/src/main/java/org/thoughtcrime/securesms/keyvalue/MiscellaneousValues.java @@ -6,6 +6,7 @@ import androidx.annotation.Nullable; import org.thoughtcrime.securesms.database.model.databaseprotos.PendingChangeNumberMetadata; import java.util.Arrays; +import java.util.Collections; import java.util.List; import java.util.concurrent.TimeUnit; @@ -28,8 +29,7 @@ public final class MiscellaneousValues extends SignalStoreValues { private static final String LAST_FCM_FOREGROUND_TIME = "misc.last_fcm_foreground_time"; private static final String LAST_FOREGROUND_TIME = "misc.last_foreground_time"; private static final String PNI_INITIALIZED_DEVICES = "misc.pni_initialized_devices"; - private static final String SMS_PHASE_1_START_MS = "misc.sms_export.phase_1_start.2"; - private static final String STORIES_FEATURE_AVAILABLE_MS = "misc.stories_feature_available_ms"; + private static final String SMS_PHASE_1_START_MS = "misc.sms_export.phase_1_start.3"; MiscellaneousValues(@NonNull KeyValueStore store) { super(store); @@ -42,10 +42,7 @@ public final class MiscellaneousValues extends SignalStoreValues { @Override @NonNull List getKeysToIncludeInBackup() { - return Arrays.asList( - SMS_PHASE_1_START_MS, - STORIES_FEATURE_AVAILABLE_MS - ); + return Collections.singletonList(SMS_PHASE_1_START_MS); } public long getLastPrekeyRefreshTime() { @@ -234,20 +231,7 @@ public final class MiscellaneousValues extends SignalStoreValues { } } - public long getStoriesFeatureAvailableTimestamp() { - return getLong(STORIES_FEATURE_AVAILABLE_MS, 0); - } - - public void setStoriesFeatureAvailableTimestamp(long timestamp) { - putLong(STORIES_FEATURE_AVAILABLE_MS, timestamp); - } - public @NonNull SmsExportPhase getSmsExportPhase() { - if (getLong(SMS_PHASE_1_START_MS, 0) == 0) { - return SmsExportPhase.PHASE_0; - } - - long now = System.currentTimeMillis(); - return SmsExportPhase.getCurrentPhase(now - getLong(SMS_PHASE_1_START_MS, now)); + return SmsExportPhase.PHASE_0; } } diff --git a/app/src/main/java/org/thoughtcrime/securesms/megaphone/SmsExportReminderSchedule.kt b/app/src/main/java/org/thoughtcrime/securesms/megaphone/SmsExportReminderSchedule.kt index bab0febec..f4e1d26a6 100644 --- a/app/src/main/java/org/thoughtcrime/securesms/megaphone/SmsExportReminderSchedule.kt +++ b/app/src/main/java/org/thoughtcrime/securesms/megaphone/SmsExportReminderSchedule.kt @@ -4,8 +4,6 @@ import android.content.Context import androidx.annotation.WorkerThread import org.thoughtcrime.securesms.keyvalue.SignalStore import org.thoughtcrime.securesms.keyvalue.SmsExportPhase -import org.thoughtcrime.securesms.util.FeatureFlags -import org.thoughtcrime.securesms.util.Util import kotlin.time.Duration.Companion.days class SmsExportReminderSchedule(private val context: Context) : MegaphoneSchedule { @@ -32,17 +30,8 @@ class SmsExportReminderSchedule(private val context: Context) : MegaphoneSchedul } } - @Suppress("UsePropertyAccessSyntax") @WorkerThread - fun shouldShowMegaphone(): Boolean { - return if (SignalStore.misc().storiesFeatureAvailableTimestamp == 0L) { - SignalStore.misc().storiesFeatureAvailableTimestamp = System.currentTimeMillis() - false - } else if (System.currentTimeMillis() > (SignalStore.misc().storiesFeatureAvailableTimestamp + FeatureFlags.smsExportMegaphoneDelayDays().days.inWholeMilliseconds)) { - SignalStore.misc().startSmsPhase1() - FeatureFlags.smsExporter() && Util.isDefaultSmsProvider(context) - } else { - false - } + private fun shouldShowMegaphone(): Boolean { + return false } } diff --git a/app/src/main/java/org/thoughtcrime/securesms/util/FeatureFlags.java b/app/src/main/java/org/thoughtcrime/securesms/util/FeatureFlags.java index 23666c9eb..405ce9cd0 100644 --- a/app/src/main/java/org/thoughtcrime/securesms/util/FeatureFlags.java +++ b/app/src/main/java/org/thoughtcrime/securesms/util/FeatureFlags.java @@ -99,7 +99,6 @@ public final class FeatureFlags { private static final String RECIPIENT_MERGE_V2 = "android.recipientMergeV2"; private static final String SMS_EXPORTER = "android.sms.exporter.2"; private static final String HIDE_CONTACTS = "android.hide.contacts"; - private static final String SMS_EXPORT_MEGAPHONE_DELAY_DAYS = "android.smsExport.megaphoneDelayDays.2"; public static final String CREDIT_CARD_PAYMENTS = "android.credit.card.payments.3"; private static final String PAYMENTS_REQUEST_ACTIVATE_FLOW = "android.payments.requestActivateFlow"; private static final String KEEP_MUTED_CHATS_ARCHIVED = "android.keepMutedChatsArchived"; @@ -160,7 +159,6 @@ public final class FeatureFlags { RECIPIENT_MERGE_V2, SMS_EXPORTER, HIDE_CONTACTS, - SMS_EXPORT_MEGAPHONE_DELAY_DAYS, CREDIT_CARD_PAYMENTS, PAYMENTS_REQUEST_ACTIVATE_FLOW, KEEP_MUTED_CHATS_ARCHIVED, @@ -231,7 +229,6 @@ public final class FeatureFlags { TELECOM_MODEL_BLOCKLIST, CAMERAX_MODEL_BLOCKLIST, RECIPIENT_MERGE_V2, - SMS_EXPORT_MEGAPHONE_DELAY_DAYS, CREDIT_CARD_PAYMENTS, PAYMENTS_REQUEST_ACTIVATE_FLOW, KEEP_MUTED_CHATS_ARCHIVED, @@ -549,13 +546,6 @@ public final class FeatureFlags { return getBoolean(HIDE_CONTACTS, false); } - /** - * Number of days to postpone the sms export megaphone and Phase 1 start. - */ - public static int smsExportMegaphoneDelayDays() { - return getInteger(SMS_EXPORT_MEGAPHONE_DELAY_DAYS, 14); - } - /** * Whether or not we should allow credit card payments for donations * diff --git a/app/src/main/res/values/strings.xml b/app/src/main/res/values/strings.xml index f563fa7b2..e1edd0d7f 100644 --- a/app/src/main/res/values/strings.xml +++ b/app/src/main/res/values/strings.xml @@ -4029,6 +4029,8 @@ Use as default SMS app Export SMS messages + + Export SMS messages again Remove SMS messages @@ -4037,6 +4039,8 @@ You can remove SMS messages from Signal in Settings at any time. You can export your SMS messages to your phone\'s SMS database + + Exporting again can result in duplicate messages. Remove SMS messages from Signal to clear up storage space. @@ -5442,6 +5446,16 @@ 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. + + + Continue + + Cancel + + Export SMS again? + + You already exported your SMS messages.\nWARNING: If you continue, you may end up with duplicate messages. + Set Signal as the default SMS app 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 892dc43e8..4a4478fa6 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,16 +25,17 @@ 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) + const val CLEAR_PREVIOUS_EXPORT_STATE_EXTRA = "clear_previous_export_state" /** * Progress state which can be listened to by interested components, such as fragments. */ val progressState: BehaviorProcessor = BehaviorProcessor.createDefault(SmsExportProgress.Init) + + fun clearProgressState() { + progressState.onNext(SmsExportProgress.Init) + } } override fun onBind(intent: Intent?): IBinder? { @@ -47,18 +48,18 @@ abstract class SmsExportService : Service() { override fun onStartCommand(intent: Intent?, flags: Int, startId: Int): Int { Log.d(TAG, "Got start command in SMS Export Service") - startExport() + startExport(intent?.getBooleanExtra(CLEAR_PREVIOUS_EXPORT_STATE_EXTRA, false) ?: false) return START_NOT_STICKY } - private fun startExport() { + private fun startExport(clearExportState: Boolean) { if (isStarted) { Log.d(TAG, "Already running exporter.") return } - Log.d(TAG, "Running export...") + Log.d(TAG, "Running export clearExportState: $clearExportState") isStarted = true updateNotification(-1, -1) @@ -67,6 +68,10 @@ abstract class SmsExportService : Service() { var progress = 0 var errorCount = 0 executor.execute { + if (clearExportState) { + clearPreviousExportState() + } + prepareForExport() val totalCount = getUnexportedMessageCount() getUnexportedMessages().forEach { message -> @@ -124,7 +129,14 @@ abstract class SmsExportService : Service() { */ protected abstract fun getExportCompleteNotification(): ExportNotification? - /** Called prior to starting export for any task setup that may need to occur. */ + /** + * Called prior to starting export if the user has requested previous export state to be cleared. + */ + protected open fun clearPreviousExportState() = Unit + + /** + * Called prior to starting export for any task setup that may need to occur. + */ protected open fun prepareForExport() = Unit /**