From aa33fd44b83263cda70fa4e9f3ce307c604e8719 Mon Sep 17 00:00:00 2001 From: Cody Henthorne Date: Wed, 24 Jan 2024 16:04:08 -0500 Subject: [PATCH] Remove SMS export. --- app/build.gradle.kts | 1 - app/src/main/AndroidManifest.xml | 25 -- .../app/chats/ChatsSettingsFragment.kt | 63 --- .../settings/app/chats/ChatsSettingsState.kt | 6 +- .../app/chats/ChatsSettingsViewModel.kt | 21 +- .../settings/app/chats/sms/SmsExportState.kt | 9 - .../app/chats/sms/SmsSettingsFragment.kt | 153 -------- .../app/chats/sms/SmsSettingsRepository.kt | 40 -- .../app/chats/sms/SmsSettingsState.kt | 8 - .../app/chats/sms/SmsSettingsViewModel.kt | 50 --- .../ConversationListFragment.java | 15 - .../exporter/SignalSmsExportReader.kt | 177 --------- .../exporter/SignalSmsExportService.kt | 208 ---------- .../flow/ChooseANewDefaultSmsAppFragment.kt | 59 --- .../flow/ExportSmsCompleteFragment.kt | 27 -- .../flow/ExportSmsFullErrorFragment.kt | 44 --- .../ExportSmsPartiallyCompleteFragment.kt | 57 --- .../flow/ExportYourSmsMessagesFragment.kt | 58 --- .../flow/ExportingSmsMessagesFragment.kt | 120 ------ .../exporter/flow/ExportingSmsRepository.kt | 38 -- .../flow/SetSignalAsDefaultSmsAppFragment.kt | 43 -- .../exporter/flow/SmsExportActivity.kt | 61 --- .../exporter/flow/SmsExportDialogs.kt | 39 -- .../exporter/flow/SmsExportHelpFragment.kt | 30 -- .../exporter/flow/SmsExportViewModel.kt | 17 - .../flow/SmsRemovalInformationFragment.kt | 56 --- .../securesms/megaphone/Megaphones.java | 23 -- .../megaphone/SmsExportMegaphoneActivity.kt | 79 ---- .../megaphone/SmsExportReminderSchedule.kt | 22 -- .../choose_a_new_default_sms_app_fragment.xml | 260 ------------- .../layout/export_sms_complete_fragment.xml | 55 --- .../layout/export_sms_full_error_fragment.xml | 94 ----- ...export_sms_partially_complete_fragment.xml | 250 ------------ .../export_your_sms_messages_fragment.xml | 77 ---- .../exporting_sms_messages_fragment.xml | 57 --- ...set_signal_as_default_sms_app_fragment.xml | 63 --- .../sms_removal_information_fragment.xml | 176 --------- app/src/main/res/navigation/sms_export.xml | 186 --------- app/src/main/res/values/strings.xml | 156 -------- settings.gradle.kts | 5 - sms-exporter/app/build.gradle | 15 - sms-exporter/app/proguard-rules.pro | 21 - sms-exporter/app/src/main/AndroidManifest.xml | 65 ---- .../signal/smsexporter/app/BitmapGenerator.kt | 35 -- .../smsexporter/app/BroadcastSmsReceiver.kt | 10 - .../app/BroadcastWapPushReceiver.kt | 10 - .../signal/smsexporter/app/MainActivity.kt | 140 ------- .../app/SendResponseViaMessageService.kt | 11 - .../smsexporter/app/TestSmsExportService.kt | 164 -------- .../drawable-v24/ic_launcher_foreground.xml | 30 -- .../res/drawable/ic_launcher_background.xml | 170 -------- .../app/src/main/res/layout/main_activity.xml | 59 --- .../res/mipmap-anydpi-v26/ic_launcher.xml | 5 - .../mipmap-anydpi-v26/ic_launcher_round.xml | 5 - .../src/main/res/mipmap-hdpi/ic_launcher.webp | Bin 1404 -> 0 bytes .../res/mipmap-hdpi/ic_launcher_round.webp | Bin 2898 -> 0 bytes .../src/main/res/mipmap-mdpi/ic_launcher.webp | Bin 982 -> 0 bytes .../res/mipmap-mdpi/ic_launcher_round.webp | Bin 1772 -> 0 bytes .../main/res/mipmap-xhdpi/ic_launcher.webp | Bin 1900 -> 0 bytes .../res/mipmap-xhdpi/ic_launcher_round.webp | Bin 3918 -> 0 bytes .../main/res/mipmap-xxhdpi/ic_launcher.webp | Bin 2884 -> 0 bytes .../res/mipmap-xxhdpi/ic_launcher_round.webp | Bin 5914 -> 0 bytes .../main/res/mipmap-xxxhdpi/ic_launcher.webp | Bin 3844 -> 0 bytes .../res/mipmap-xxxhdpi/ic_launcher_round.webp | Bin 7778 -> 0 bytes .../app/src/main/res/values/colors.xml | 6 - .../app/src/main/res/values/strings.xml | 6 - .../app/src/main/res/values/themes.xml | 9 - sms-exporter/lib/build.gradle | 14 - sms-exporter/lib/consumer-rules.pro | 0 sms-exporter/lib/proguard-rules.pro | 21 - sms-exporter/lib/src/main/AndroidManifest.xml | 4 - .../signal/smsexporter/BecomeSmsAppFailure.kt | 13 - .../signal/smsexporter/DefaultSmsHelper.kt | 26 -- .../signal/smsexporter/ExportableMessage.kt | 69 ---- .../smsexporter/ReleaseSmsAppFailure.kt | 13 - .../signal/smsexporter/SmsExportProgress.kt | 30 -- .../signal/smsexporter/SmsExportService.kt | 367 ------------------ .../org/signal/smsexporter/SmsExportState.kt | 20 - .../internal/BecomeDefaultSmsUseCase.kt | 36 -- .../smsexporter/internal/IsDefaultSms.kt | 20 - .../internal/ReleaseDefaultSmsUseCase.kt | 31 -- .../internal/mms/ExportMmsMessagesUseCase.kt | 88 ----- .../internal/mms/ExportMmsPartsUseCase.kt | 67 ---- .../mms/ExportMmsRecipientsUseCase.kt | 48 --- .../mms/GetOrCreateMmsThreadIdsUseCase.kt | 66 ---- .../internal/sms/ExportSmsMessagesUseCase.kt | 49 --- .../smsexporter/InMemoryContentProvider.kt | 115 ------ .../java/org/signal/smsexporter/TestUtils.kt | 48 --- .../mms/ExportMmsMessagesUseCaseTest.kt | 133 ------- .../internal/mms/ExportMmsPartsUseCaseTest.kt | 136 ------- .../mms/ExportMmsRecipientsUseCaseTest.kt | 138 ------- .../mms/GetOrCreateMmsThreadIdsUseCaseTest.kt | 42 -- .../sms/ExportSmsMessagesUseCaseTest.kt | 127 ------ 93 files changed, 3 insertions(+), 5407 deletions(-) delete mode 100644 app/src/main/java/org/thoughtcrime/securesms/components/settings/app/chats/sms/SmsExportState.kt delete mode 100644 app/src/main/java/org/thoughtcrime/securesms/components/settings/app/chats/sms/SmsSettingsFragment.kt delete mode 100644 app/src/main/java/org/thoughtcrime/securesms/components/settings/app/chats/sms/SmsSettingsRepository.kt delete mode 100644 app/src/main/java/org/thoughtcrime/securesms/components/settings/app/chats/sms/SmsSettingsState.kt delete mode 100644 app/src/main/java/org/thoughtcrime/securesms/components/settings/app/chats/sms/SmsSettingsViewModel.kt delete mode 100644 app/src/main/java/org/thoughtcrime/securesms/exporter/SignalSmsExportReader.kt delete mode 100644 app/src/main/java/org/thoughtcrime/securesms/exporter/SignalSmsExportService.kt delete mode 100644 app/src/main/java/org/thoughtcrime/securesms/exporter/flow/ChooseANewDefaultSmsAppFragment.kt delete mode 100644 app/src/main/java/org/thoughtcrime/securesms/exporter/flow/ExportSmsCompleteFragment.kt delete mode 100644 app/src/main/java/org/thoughtcrime/securesms/exporter/flow/ExportSmsFullErrorFragment.kt delete mode 100644 app/src/main/java/org/thoughtcrime/securesms/exporter/flow/ExportSmsPartiallyCompleteFragment.kt delete mode 100644 app/src/main/java/org/thoughtcrime/securesms/exporter/flow/ExportYourSmsMessagesFragment.kt delete mode 100644 app/src/main/java/org/thoughtcrime/securesms/exporter/flow/ExportingSmsMessagesFragment.kt delete mode 100644 app/src/main/java/org/thoughtcrime/securesms/exporter/flow/ExportingSmsRepository.kt delete mode 100644 app/src/main/java/org/thoughtcrime/securesms/exporter/flow/SetSignalAsDefaultSmsAppFragment.kt delete mode 100644 app/src/main/java/org/thoughtcrime/securesms/exporter/flow/SmsExportActivity.kt delete mode 100644 app/src/main/java/org/thoughtcrime/securesms/exporter/flow/SmsExportDialogs.kt delete mode 100644 app/src/main/java/org/thoughtcrime/securesms/exporter/flow/SmsExportHelpFragment.kt delete mode 100644 app/src/main/java/org/thoughtcrime/securesms/exporter/flow/SmsExportViewModel.kt delete mode 100644 app/src/main/java/org/thoughtcrime/securesms/exporter/flow/SmsRemovalInformationFragment.kt delete mode 100644 app/src/main/java/org/thoughtcrime/securesms/megaphone/SmsExportMegaphoneActivity.kt delete mode 100644 app/src/main/java/org/thoughtcrime/securesms/megaphone/SmsExportReminderSchedule.kt delete mode 100644 app/src/main/res/layout/choose_a_new_default_sms_app_fragment.xml delete mode 100644 app/src/main/res/layout/export_sms_complete_fragment.xml delete mode 100644 app/src/main/res/layout/export_sms_full_error_fragment.xml delete mode 100644 app/src/main/res/layout/export_sms_partially_complete_fragment.xml delete mode 100644 app/src/main/res/layout/export_your_sms_messages_fragment.xml delete mode 100644 app/src/main/res/layout/exporting_sms_messages_fragment.xml delete mode 100644 app/src/main/res/layout/set_signal_as_default_sms_app_fragment.xml delete mode 100644 app/src/main/res/layout/sms_removal_information_fragment.xml delete mode 100644 app/src/main/res/navigation/sms_export.xml delete mode 100644 sms-exporter/app/build.gradle delete mode 100644 sms-exporter/app/proguard-rules.pro delete mode 100644 sms-exporter/app/src/main/AndroidManifest.xml delete mode 100644 sms-exporter/app/src/main/java/org/signal/smsexporter/app/BitmapGenerator.kt delete mode 100644 sms-exporter/app/src/main/java/org/signal/smsexporter/app/BroadcastSmsReceiver.kt delete mode 100644 sms-exporter/app/src/main/java/org/signal/smsexporter/app/BroadcastWapPushReceiver.kt delete mode 100644 sms-exporter/app/src/main/java/org/signal/smsexporter/app/MainActivity.kt delete mode 100644 sms-exporter/app/src/main/java/org/signal/smsexporter/app/SendResponseViaMessageService.kt delete mode 100644 sms-exporter/app/src/main/java/org/signal/smsexporter/app/TestSmsExportService.kt delete mode 100644 sms-exporter/app/src/main/res/drawable-v24/ic_launcher_foreground.xml delete mode 100644 sms-exporter/app/src/main/res/drawable/ic_launcher_background.xml delete mode 100644 sms-exporter/app/src/main/res/layout/main_activity.xml delete mode 100644 sms-exporter/app/src/main/res/mipmap-anydpi-v26/ic_launcher.xml delete mode 100644 sms-exporter/app/src/main/res/mipmap-anydpi-v26/ic_launcher_round.xml delete mode 100644 sms-exporter/app/src/main/res/mipmap-hdpi/ic_launcher.webp delete mode 100644 sms-exporter/app/src/main/res/mipmap-hdpi/ic_launcher_round.webp delete mode 100644 sms-exporter/app/src/main/res/mipmap-mdpi/ic_launcher.webp delete mode 100644 sms-exporter/app/src/main/res/mipmap-mdpi/ic_launcher_round.webp delete mode 100644 sms-exporter/app/src/main/res/mipmap-xhdpi/ic_launcher.webp delete mode 100644 sms-exporter/app/src/main/res/mipmap-xhdpi/ic_launcher_round.webp delete mode 100644 sms-exporter/app/src/main/res/mipmap-xxhdpi/ic_launcher.webp delete mode 100644 sms-exporter/app/src/main/res/mipmap-xxhdpi/ic_launcher_round.webp delete mode 100644 sms-exporter/app/src/main/res/mipmap-xxxhdpi/ic_launcher.webp delete mode 100644 sms-exporter/app/src/main/res/mipmap-xxxhdpi/ic_launcher_round.webp delete mode 100644 sms-exporter/app/src/main/res/values/colors.xml delete mode 100644 sms-exporter/app/src/main/res/values/strings.xml delete mode 100644 sms-exporter/app/src/main/res/values/themes.xml delete mode 100644 sms-exporter/lib/build.gradle delete mode 100644 sms-exporter/lib/consumer-rules.pro delete mode 100644 sms-exporter/lib/proguard-rules.pro delete mode 100644 sms-exporter/lib/src/main/AndroidManifest.xml delete mode 100644 sms-exporter/lib/src/main/java/org/signal/smsexporter/BecomeSmsAppFailure.kt delete mode 100644 sms-exporter/lib/src/main/java/org/signal/smsexporter/DefaultSmsHelper.kt delete mode 100644 sms-exporter/lib/src/main/java/org/signal/smsexporter/ExportableMessage.kt delete mode 100644 sms-exporter/lib/src/main/java/org/signal/smsexporter/ReleaseSmsAppFailure.kt delete mode 100644 sms-exporter/lib/src/main/java/org/signal/smsexporter/SmsExportProgress.kt delete mode 100644 sms-exporter/lib/src/main/java/org/signal/smsexporter/SmsExportService.kt delete mode 100644 sms-exporter/lib/src/main/java/org/signal/smsexporter/SmsExportState.kt delete mode 100644 sms-exporter/lib/src/main/java/org/signal/smsexporter/internal/BecomeDefaultSmsUseCase.kt delete mode 100644 sms-exporter/lib/src/main/java/org/signal/smsexporter/internal/IsDefaultSms.kt delete mode 100644 sms-exporter/lib/src/main/java/org/signal/smsexporter/internal/ReleaseDefaultSmsUseCase.kt delete mode 100644 sms-exporter/lib/src/main/java/org/signal/smsexporter/internal/mms/ExportMmsMessagesUseCase.kt delete mode 100644 sms-exporter/lib/src/main/java/org/signal/smsexporter/internal/mms/ExportMmsPartsUseCase.kt delete mode 100644 sms-exporter/lib/src/main/java/org/signal/smsexporter/internal/mms/ExportMmsRecipientsUseCase.kt delete mode 100644 sms-exporter/lib/src/main/java/org/signal/smsexporter/internal/mms/GetOrCreateMmsThreadIdsUseCase.kt delete mode 100644 sms-exporter/lib/src/main/java/org/signal/smsexporter/internal/sms/ExportSmsMessagesUseCase.kt delete mode 100644 sms-exporter/lib/src/test/java/org/signal/smsexporter/InMemoryContentProvider.kt delete mode 100644 sms-exporter/lib/src/test/java/org/signal/smsexporter/TestUtils.kt delete mode 100644 sms-exporter/lib/src/test/java/org/signal/smsexporter/internal/mms/ExportMmsMessagesUseCaseTest.kt delete mode 100644 sms-exporter/lib/src/test/java/org/signal/smsexporter/internal/mms/ExportMmsPartsUseCaseTest.kt delete mode 100644 sms-exporter/lib/src/test/java/org/signal/smsexporter/internal/mms/ExportMmsRecipientsUseCaseTest.kt delete mode 100644 sms-exporter/lib/src/test/java/org/signal/smsexporter/internal/mms/GetOrCreateMmsThreadIdsUseCaseTest.kt delete mode 100644 sms-exporter/lib/src/test/java/org/signal/smsexporter/internal/sms/ExportSmsMessagesUseCaseTest.kt diff --git a/app/build.gradle.kts b/app/build.gradle.kts index aa750e3e28..38edc5d520 100644 --- a/app/build.gradle.kts +++ b/app/build.gradle.kts @@ -476,7 +476,6 @@ dependencies { implementation(project(":donations")) implementation(project(":contacts")) implementation(project(":qr")) - implementation(project(":sms-exporter")) implementation(project(":sticky-header-grid")) implementation(project(":photoview")) implementation(project(":core-ui")) diff --git a/app/src/main/AndroidManifest.xml b/app/src/main/AndroidManifest.xml index a5c8590177..678d1be864 100644 --- a/app/src/main/AndroidManifest.xml +++ b/app/src/main/AndroidManifest.xml @@ -28,11 +28,6 @@ - - - - - @@ -1066,13 +1061,6 @@ android:launchMode="singleTask" android:exported="false"/> - - - - - - override fun onResume() { super.onResume() @@ -29,12 +21,6 @@ class ChatsSettingsFragment : DSLSettingsFragment(R.string.preferences_chats__ch @Suppress("ReplaceGetOrSet") override fun bindAdapter(adapter: MappingAdapter) { - smsExportLauncher = registerForActivityResult(ActivityResultContracts.StartActivityForResult()) { - if (it.resultCode == Activity.RESULT_OK) { - SmsExportDialogs.showSmsRemovalDialog(requireContext(), requireView()) - } - } - viewModel = ViewModelProvider(this).get(ChatsSettingsViewModel::class.java) viewModel.state.observe(viewLifecycleOwner) { @@ -44,55 +30,6 @@ class ChatsSettingsFragment : DSLSettingsFragment(R.string.preferences_chats__ch private fun getConfiguration(state: ChatsSettingsState): DSLConfiguration { return configure { - if (!state.useAsDefaultSmsApp) { - when (state.smsExportState) { - SmsExportState.FETCHING -> Unit - SmsExportState.HAS_UNEXPORTED_MESSAGES -> { - clickPref( - title = DSLSettingsText.from(R.string.SmsSettingsFragment__export_sms_messages), - summary = DSLSettingsText.from(R.string.SmsSettingsFragment__you_can_export_your_sms_messages_to_your_phones_sms_database), - onClick = { - smsExportLauncher.launch(SmsExportActivity.createIntent(requireContext())) - } - ) - - dividerPref() - } - SmsExportState.ALL_MESSAGES_EXPORTED -> { - clickPref( - title = DSLSettingsText.from(R.string.SmsSettingsFragment__remove_sms_messages), - summary = DSLSettingsText.from(R.string.SmsSettingsFragment__remove_sms_messages_from_signal_to_clear_up_storage_space), - onClick = { - SmsExportDialogs.showSmsRemovalDialog(requireContext(), requireView()) - } - ) - - 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 - SmsExportState.NOT_AVAILABLE -> Unit - } - } else { - clickPref( - title = DSLSettingsText.from(R.string.preferences__sms_mms), - onClick = { - Navigation.findNavController(requireView()).safeNavigate(R.id.action_chatsSettingsFragment_to_smsSettingsFragment) - } - ) - - dividerPref() - } - switchPref( title = DSLSettingsText.from(R.string.preferences__generate_link_previews), summary = DSLSettingsText.from(R.string.preferences__retrieve_link_previews_from_websites_for_messages), diff --git a/app/src/main/java/org/thoughtcrime/securesms/components/settings/app/chats/ChatsSettingsState.kt b/app/src/main/java/org/thoughtcrime/securesms/components/settings/app/chats/ChatsSettingsState.kt index 7c4bfe7c75..f47f48d11a 100644 --- a/app/src/main/java/org/thoughtcrime/securesms/components/settings/app/chats/ChatsSettingsState.kt +++ b/app/src/main/java/org/thoughtcrime/securesms/components/settings/app/chats/ChatsSettingsState.kt @@ -1,14 +1,10 @@ package org.thoughtcrime.securesms.components.settings.app.chats -import org.thoughtcrime.securesms.components.settings.app.chats.sms.SmsExportState - data class ChatsSettingsState( val generateLinkPreviews: Boolean, val useAddressBook: Boolean, val keepMutedChatsArchived: Boolean, val useSystemEmoji: Boolean, val enterKeySends: Boolean, - val chatBackupsEnabled: Boolean, - val useAsDefaultSmsApp: Boolean, - val smsExportState: SmsExportState = SmsExportState.FETCHING + val chatBackupsEnabled: Boolean ) diff --git a/app/src/main/java/org/thoughtcrime/securesms/components/settings/app/chats/ChatsSettingsViewModel.kt b/app/src/main/java/org/thoughtcrime/securesms/components/settings/app/chats/ChatsSettingsViewModel.kt index f54d158746..2062181367 100644 --- a/app/src/main/java/org/thoughtcrime/securesms/components/settings/app/chats/ChatsSettingsViewModel.kt +++ b/app/src/main/java/org/thoughtcrime/securesms/components/settings/app/chats/ChatsSettingsViewModel.kt @@ -2,24 +2,18 @@ package org.thoughtcrime.securesms.components.settings.app.chats import androidx.lifecycle.LiveData import androidx.lifecycle.ViewModel -import io.reactivex.rxjava3.disposables.CompositeDisposable -import io.reactivex.rxjava3.kotlin.plusAssign -import org.thoughtcrime.securesms.components.settings.app.chats.sms.SmsSettingsRepository import org.thoughtcrime.securesms.dependencies.ApplicationDependencies import org.thoughtcrime.securesms.keyvalue.SignalStore import org.thoughtcrime.securesms.util.BackupUtil import org.thoughtcrime.securesms.util.ConversationUtil import org.thoughtcrime.securesms.util.ThrottledDebouncer -import org.thoughtcrime.securesms.util.Util import org.thoughtcrime.securesms.util.livedata.Store class ChatsSettingsViewModel @JvmOverloads constructor( - private val repository: ChatsSettingsRepository = ChatsSettingsRepository(), - smsSettingsRepository: SmsSettingsRepository = SmsSettingsRepository() + private val repository: ChatsSettingsRepository = ChatsSettingsRepository() ) : ViewModel() { private val refreshDebouncer = ThrottledDebouncer(500L) - private val disposables = CompositeDisposable() private val store: Store = Store( ChatsSettingsState( @@ -28,23 +22,12 @@ class ChatsSettingsViewModel @JvmOverloads constructor( keepMutedChatsArchived = SignalStore.settings().shouldKeepMutedChatsArchived(), useSystemEmoji = SignalStore.settings().isPreferSystemEmoji, enterKeySends = SignalStore.settings().isEnterKeySends, - chatBackupsEnabled = SignalStore.settings().isBackupEnabled && BackupUtil.canUserAccessBackupDirectory(ApplicationDependencies.getApplication()), - useAsDefaultSmsApp = Util.isDefaultSmsProvider(ApplicationDependencies.getApplication()) + chatBackupsEnabled = SignalStore.settings().isBackupEnabled && BackupUtil.canUserAccessBackupDirectory(ApplicationDependencies.getApplication()) ) ) val state: LiveData = store.stateLiveData - init { - disposables += smsSettingsRepository.getSmsExportState().subscribe { state -> - store.update { it.copy(smsExportState = state) } - } - } - - override fun onCleared() { - disposables.clear() - } - fun setGenerateLinkPreviewsEnabled(enabled: Boolean) { store.update { it.copy(generateLinkPreviews = enabled) } SignalStore.settings().isLinkPreviewsEnabled = enabled diff --git a/app/src/main/java/org/thoughtcrime/securesms/components/settings/app/chats/sms/SmsExportState.kt b/app/src/main/java/org/thoughtcrime/securesms/components/settings/app/chats/sms/SmsExportState.kt deleted file mode 100644 index 0cdbf8ed26..0000000000 --- a/app/src/main/java/org/thoughtcrime/securesms/components/settings/app/chats/sms/SmsExportState.kt +++ /dev/null @@ -1,9 +0,0 @@ -package org.thoughtcrime.securesms.components.settings.app.chats.sms - -enum class SmsExportState { - FETCHING, - HAS_UNEXPORTED_MESSAGES, - ALL_MESSAGES_EXPORTED, - NO_SMS_MESSAGES_IN_DATABASE, - NOT_AVAILABLE -} 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 deleted file mode 100644 index 87523bb6b8..0000000000 --- a/app/src/main/java/org/thoughtcrime/securesms/components/settings/app/chats/sms/SmsSettingsFragment.kt +++ /dev/null @@ -1,153 +0,0 @@ -package org.thoughtcrime.securesms.components.settings.app.chats.sms - -import android.annotation.SuppressLint -import android.app.Activity -import android.content.Intent -import android.os.Build -import android.provider.Settings -import androidx.activity.result.ActivityResultLauncher -import androidx.activity.result.contract.ActivityResultContracts -import androidx.lifecycle.ViewModelProvider -import androidx.navigation.fragment.findNavController -import org.thoughtcrime.securesms.R -import org.thoughtcrime.securesms.components.settings.DSLConfiguration -import org.thoughtcrime.securesms.components.settings.DSLSettingsFragment -import org.thoughtcrime.securesms.components.settings.DSLSettingsText -import org.thoughtcrime.securesms.components.settings.configure -import org.thoughtcrime.securesms.components.settings.models.OutlinedLearnMore -import org.thoughtcrime.securesms.exporter.flow.SmsExportActivity -import org.thoughtcrime.securesms.exporter.flow.SmsExportDialogs -import org.thoughtcrime.securesms.keyvalue.SignalStore -import org.thoughtcrime.securesms.util.Util -import org.thoughtcrime.securesms.util.adapter.mapping.MappingAdapter - -private const val SMS_REQUEST_CODE: Short = 1234 - -class SmsSettingsFragment : DSLSettingsFragment(R.string.preferences__sms_mms) { - - private lateinit var viewModel: SmsSettingsViewModel - private lateinit var smsExportLauncher: ActivityResultLauncher - - override fun onResume() { - super.onResume() - viewModel.checkSmsEnabled() - } - - override fun bindAdapter(adapter: MappingAdapter) { - OutlinedLearnMore.register(adapter) - - smsExportLauncher = registerForActivityResult(ActivityResultContracts.StartActivityForResult()) { - if (it.resultCode == Activity.RESULT_OK) { - SmsExportDialogs.showSmsRemovalDialog(requireContext(), requireView()) - } - } - - viewModel = ViewModelProvider(this)[SmsSettingsViewModel::class.java] - - viewModel.state.observe(viewLifecycleOwner) { - adapter.submitList(getConfiguration(it).toMappingModelList()) - } - } - - override fun onActivityResult(requestCode: Int, resultCode: Int, data: Intent?) { - if (Util.isDefaultSmsProvider(requireContext())) { - SignalStore.settings().setDefaultSms(true) - } else { - SignalStore.settings().setDefaultSms(false) - findNavController().navigateUp() - } - } - - private fun getConfiguration(state: SmsSettingsState): DSLConfiguration { - return configure { - if (state.useAsDefaultSmsApp) { - customPref( - OutlinedLearnMore.Model( - summary = DSLSettingsText.from(R.string.SmsSettingsFragment__sms_support_will_be_removed_soon_to_focus_on_encrypted_messaging), - learnMoreUrl = getString(R.string.sms_export_url) - ) - ) - } - - when (state.smsExportState) { - SmsExportState.FETCHING -> Unit - SmsExportState.HAS_UNEXPORTED_MESSAGES -> { - clickPref( - title = DSLSettingsText.from(R.string.SmsSettingsFragment__export_sms_messages), - summary = DSLSettingsText.from(R.string.SmsSettingsFragment__you_can_export_your_sms_messages_to_your_phones_sms_database), - onClick = { - smsExportLauncher.launch(SmsExportActivity.createIntent(requireContext())) - } - ) - - dividerPref() - } - SmsExportState.ALL_MESSAGES_EXPORTED -> { - clickPref( - title = DSLSettingsText.from(R.string.SmsSettingsFragment__remove_sms_messages), - summary = DSLSettingsText.from(R.string.SmsSettingsFragment__remove_sms_messages_from_signal_to_clear_up_storage_space), - onClick = { - SmsExportDialogs.showSmsRemovalDialog(requireContext(), requireView()) - } - ) - - 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 - SmsExportState.NOT_AVAILABLE -> Unit - } - - if (state.useAsDefaultSmsApp) { - @Suppress("DEPRECATION") - clickPref( - title = DSLSettingsText.from(R.string.SmsSettingsFragment__use_as_default_sms_app), - summary = DSLSettingsText.from(R.string.arrays__enabled), - onClick = { - startDefaultAppSelectionIntent() - } - ) - } - - switchPref( - title = DSLSettingsText.from(R.string.preferences__sms_delivery_reports), - summary = DSLSettingsText.from(R.string.preferences__request_a_delivery_report_for_each_sms_message_you_send), - isChecked = state.smsDeliveryReportsEnabled, - onClick = { - viewModel.setSmsDeliveryReportsEnabled(!state.smsDeliveryReportsEnabled) - } - ) - - switchPref( - title = DSLSettingsText.from(R.string.preferences__support_wifi_calling), - summary = DSLSettingsText.from(R.string.preferences__enable_if_your_device_supports_sms_mms_delivery_over_wifi), - isChecked = state.wifiCallingCompatibilityEnabled, - onClick = { - viewModel.setWifiCallingCompatibilityEnabled(!state.wifiCallingCompatibilityEnabled) - } - ) - } - } - - // Linter isn't smart enough to figure out the else only happens if API >= 24 - @SuppressLint("InlinedApi") - @Suppress("DEPRECATION") - private fun startDefaultAppSelectionIntent() { - val intent: Intent = when { - Build.VERSION.SDK_INT < 23 -> Intent(Settings.ACTION_WIRELESS_SETTINGS) - Build.VERSION.SDK_INT < 24 -> Intent(Settings.ACTION_SETTINGS) - else -> Intent(Settings.ACTION_MANAGE_DEFAULT_APPS_SETTINGS) - } - - startActivityForResult(intent, SMS_REQUEST_CODE.toInt()) - } -} 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 deleted file mode 100644 index 4a0e71b5fb..0000000000 --- a/app/src/main/java/org/thoughtcrime/securesms/components/settings/app/chats/sms/SmsSettingsRepository.kt +++ /dev/null @@ -1,40 +0,0 @@ -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.MessageTable -import org.thoughtcrime.securesms.database.SignalDatabase - -class SmsSettingsRepository( - private val smsDatabase: MessageTable = SignalDatabase.messages, - private val mmsDatabase: MessageTable = SignalDatabase.messages -) { - fun getSmsExportState(): Single { - return Single.fromCallable { - checkInsecureMessageCount() ?: checkUnexportedInsecureMessageCount() - }.subscribeOn(Schedulers.io()) - } - - @WorkerThread - private fun checkInsecureMessageCount(): SmsExportState? { - val totalSmsMmsCount = smsDatabase.getInsecureMessageCount() + mmsDatabase.getInsecureMessageCount() - - return if (totalSmsMmsCount == 0) { - SmsExportState.NO_SMS_MESSAGES_IN_DATABASE - } else { - null - } - } - - @WorkerThread - private fun checkUnexportedInsecureMessageCount(): SmsExportState { - val totalUnexportedCount = smsDatabase.getUnexportedInsecureMessagesCount() + mmsDatabase.getUnexportedInsecureMessagesCount() - - return if (totalUnexportedCount > 0) { - SmsExportState.HAS_UNEXPORTED_MESSAGES - } else { - SmsExportState.ALL_MESSAGES_EXPORTED - } - } -} diff --git a/app/src/main/java/org/thoughtcrime/securesms/components/settings/app/chats/sms/SmsSettingsState.kt b/app/src/main/java/org/thoughtcrime/securesms/components/settings/app/chats/sms/SmsSettingsState.kt deleted file mode 100644 index e3ec8342a5..0000000000 --- a/app/src/main/java/org/thoughtcrime/securesms/components/settings/app/chats/sms/SmsSettingsState.kt +++ /dev/null @@ -1,8 +0,0 @@ -package org.thoughtcrime.securesms.components.settings.app.chats.sms - -data class SmsSettingsState( - val useAsDefaultSmsApp: Boolean, - val smsDeliveryReportsEnabled: Boolean, - val wifiCallingCompatibilityEnabled: Boolean, - val smsExportState: SmsExportState = SmsExportState.FETCHING -) diff --git a/app/src/main/java/org/thoughtcrime/securesms/components/settings/app/chats/sms/SmsSettingsViewModel.kt b/app/src/main/java/org/thoughtcrime/securesms/components/settings/app/chats/sms/SmsSettingsViewModel.kt deleted file mode 100644 index 96faadd34e..0000000000 --- a/app/src/main/java/org/thoughtcrime/securesms/components/settings/app/chats/sms/SmsSettingsViewModel.kt +++ /dev/null @@ -1,50 +0,0 @@ -package org.thoughtcrime.securesms.components.settings.app.chats.sms - -import androidx.lifecycle.LiveData -import androidx.lifecycle.ViewModel -import io.reactivex.rxjava3.disposables.CompositeDisposable -import io.reactivex.rxjava3.kotlin.plusAssign -import org.thoughtcrime.securesms.dependencies.ApplicationDependencies -import org.thoughtcrime.securesms.keyvalue.SignalStore -import org.thoughtcrime.securesms.util.Util -import org.thoughtcrime.securesms.util.livedata.Store - -class SmsSettingsViewModel : ViewModel() { - - private val repository = SmsSettingsRepository() - - private val disposables = CompositeDisposable() - private val store = Store( - SmsSettingsState( - useAsDefaultSmsApp = Util.isDefaultSmsProvider(ApplicationDependencies.getApplication()), - smsDeliveryReportsEnabled = SignalStore.settings().isSmsDeliveryReportsEnabled, - wifiCallingCompatibilityEnabled = SignalStore.settings().isWifiCallingCompatibilityModeEnabled - ) - ) - - val state: LiveData = store.stateLiveData - - init { - disposables += repository.getSmsExportState().subscribe { state -> - store.update { it.copy(smsExportState = state) } - } - } - - override fun onCleared() { - disposables.clear() - } - - fun setSmsDeliveryReportsEnabled(enabled: Boolean) { - store.update { it.copy(smsDeliveryReportsEnabled = enabled) } - SignalStore.settings().isSmsDeliveryReportsEnabled = enabled - } - - fun setWifiCallingCompatibilityEnabled(enabled: Boolean) { - store.update { it.copy(wifiCallingCompatibilityEnabled = enabled) } - SignalStore.settings().isWifiCallingCompatibilityModeEnabled = enabled - } - - fun checkSmsEnabled() { - store.update { it.copy(useAsDefaultSmsApp = Util.isDefaultSmsProvider(ApplicationDependencies.getApplication())) } - } -} diff --git a/app/src/main/java/org/thoughtcrime/securesms/conversationlist/ConversationListFragment.java b/app/src/main/java/org/thoughtcrime/securesms/conversationlist/ConversationListFragment.java index 0e44e0abb7..0e75e44650 100644 --- a/app/src/main/java/org/thoughtcrime/securesms/conversationlist/ConversationListFragment.java +++ b/app/src/main/java/org/thoughtcrime/securesms/conversationlist/ConversationListFragment.java @@ -44,9 +44,6 @@ import android.widget.FrameLayout; import android.widget.Toast; import androidx.activity.OnBackPressedCallback; -import androidx.activity.result.ActivityResultCallback; -import androidx.activity.result.ActivityResultLauncher; -import androidx.activity.result.contract.ActivityResultContracts; import androidx.annotation.ColorInt; import androidx.annotation.DrawableRes; import androidx.annotation.IdRes; @@ -140,7 +137,6 @@ import org.thoughtcrime.securesms.database.ThreadTable; import org.thoughtcrime.securesms.database.model.ThreadRecord; import org.thoughtcrime.securesms.dependencies.ApplicationDependencies; import org.thoughtcrime.securesms.events.ReminderUpdateEvent; -import org.thoughtcrime.securesms.exporter.flow.SmsExportDialogs; import org.thoughtcrime.securesms.groups.SelectionLimits; import org.thoughtcrime.securesms.jobs.ServiceOutageDetectionJob; import org.thoughtcrime.securesms.keyvalue.SignalStore; @@ -152,12 +148,10 @@ import org.thoughtcrime.securesms.megaphone.Megaphone; import org.thoughtcrime.securesms.megaphone.MegaphoneActionController; import org.thoughtcrime.securesms.megaphone.MegaphoneViewBuilder; import org.thoughtcrime.securesms.megaphone.Megaphones; -import org.thoughtcrime.securesms.megaphone.SmsExportMegaphoneActivity; import org.thoughtcrime.securesms.mms.GlideApp; import org.thoughtcrime.securesms.notifications.MarkReadReceiver; import org.thoughtcrime.securesms.notifications.profiles.NotificationProfile; import org.thoughtcrime.securesms.permissions.Permissions; -import org.thoughtcrime.securesms.profiles.manage.EditProfileActivity; import org.thoughtcrime.securesms.profiles.manage.UsernameEditFragment; import org.thoughtcrime.securesms.ratelimit.RecaptchaProofBottomSheetFragment; import org.thoughtcrime.securesms.recipients.Recipient; @@ -691,15 +685,6 @@ public class ConversationListFragment extends MainFragment implements ActionMode @Override public void onActivityResult(int requestCode, int resultCode, @Nullable Intent data) { - if (requestCode == SmsExportMegaphoneActivity.REQUEST_CODE) { - ApplicationDependencies.getMegaphoneRepository().markSeen(Megaphones.Event.SMS_EXPORT); - if (resultCode == RESULT_CANCELED) { - Snackbar.make(fab, R.string.ConversationActivity__you_will_be_reminded_again_soon, Snackbar.LENGTH_LONG).show(); - } else { - SmsExportDialogs.showSmsRemovalDialog(requireContext(), fab); - } - } - if (resultCode == RESULT_OK && requestCode == CreateSvrPinActivity.REQUEST_NEW_PIN) { Snackbar.make(fab, R.string.ConfirmKbsPinFragment__pin_created, Snackbar.LENGTH_LONG).show(); viewModel.onMegaphoneCompleted(Megaphones.Event.PINS_FOR_ALL); diff --git a/app/src/main/java/org/thoughtcrime/securesms/exporter/SignalSmsExportReader.kt b/app/src/main/java/org/thoughtcrime/securesms/exporter/SignalSmsExportReader.kt deleted file mode 100644 index f4e6063edf..0000000000 --- a/app/src/main/java/org/thoughtcrime/securesms/exporter/SignalSmsExportReader.kt +++ /dev/null @@ -1,177 +0,0 @@ -package org.thoughtcrime.securesms.exporter - -import org.json.JSONException -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.MessageTable -import org.thoughtcrime.securesms.database.SignalDatabase -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( - private val messageTable: MessageTable = SignalDatabase.messages -) : Iterable, Closeable { - - companion object { - private val TAG = Log.tag(SignalSmsExportReader::class.java) - private const val CURSOR_LIMIT = 1000 - } - - private var messageReader: MessageTable.MmsReader? = null - private var done: Boolean = false - - override fun iterator(): Iterator { - return ExportableMessageIterator() - } - - fun getCount(): Int { - return messageTable.getUnexportedInsecureMessagesCount() - } - - override fun close() { - messageReader?.close() - } - - private fun refreshReaders() { - if (!done) { - messageReader?.close() - messageReader = null - - val refreshedMmsReader = MessageTable.mmsReaderFor(messageTable.getUnexportedInsecureMessages(CURSOR_LIMIT)) - if (refreshedMmsReader.getCount() > 0) { - messageReader = refreshedMmsReader - return - } else { - refreshedMmsReader.close() - done = true - } - } - } - - private inner class ExportableMessageIterator : Iterator { - - private var messageIterator: Iterator? = null - - private fun refreshIterators() { - refreshReaders() - messageIterator = messageReader?.iterator() - } - - override fun hasNext(): Boolean { - if (messageIterator?.hasNext() == true) { - return true - } else if (!done) { - refreshIterators() - if (messageIterator?.hasNext() == true) { - return true - } - } - - return false - } - - override fun next(): ExportableMessage { - var record: MessageRecord? = null - try { - return if (messageIterator?.hasNext() == true) { - record = messageIterator!!.next() - readExportableMmsMessageFromRecord(record, messageReader!!.getMessageExportStateForCurrentRecord()) - } else { - throw NoSuchElementException() - } - } catch (e: Throwable) { - if (e.cause is JSONException) { - Log.w(TAG, "Error processing attachment json, skipping message.", e) - return ExportableMessage.Skip(messageReader!!.getCurrentId()) - } - - Log.w(TAG, "Error processing message: isMms: ${record?.isMms} type: ${record?.type}") - throw e - } - } - - private fun readExportableMmsMessageFromRecord(record: MessageRecord, exportState: MessageExportState): ExportableMessage { - val self = Recipient.self() - val threadRecipient: Recipient? = SignalDatabase.threads.getRecipientForThreadId(record.threadId) - val addresses: Set = if (threadRecipient?.isMmsGroup == true) { - Recipient - .resolvedList(threadRecipient.participantIds) - .filter { it != self } - .map { r -> r.smsExportAddress() } - .toSet() - } else if (threadRecipient != null) { - setOf(threadRecipient.smsExportAddress()) - } else { - setOf(record.toRecipient.smsExportAddress()) - } - - 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 - .filter { it.asAttachment() is DatabaseAttachment } - .forEach { - parts.add( - ExportableMessage.Mms.Part.Stream( - id = JsonUtils.toJson((it.asAttachment() as DatabaseAttachment).attachmentId), - contentType = it.contentType - ) - ) - } - } - - val sender: String = if (record.isOutgoing) Recipient.self().smsExportAddress() else record.fromRecipient.smsExportAddress() - - return ExportableMessage.Mms( - id = MessageId(record.id), - exportState = mapExportState(exportState), - addresses = addresses, - dateReceived = record.dateReceived.milliseconds, - dateSent = record.dateSent.milliseconds, - isRead = true, - isOutgoing = record.isOutgoing, - parts = parts, - sender = sender - ) - } - - private fun mapExportState(messageExportState: MessageExportState): SmsExportState { - return SmsExportState( - messageId = messageExportState.messageId, - startedRecipients = messageExportState.startedRecipients.toSet(), - completedRecipients = messageExportState.completedRecipients.toSet(), - startedAttachments = messageExportState.startedAttachments.toSet(), - completedAttachments = messageExportState.completedAttachments.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 - } - } - ) - } - - 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 deleted file mode 100644 index 1c8443a442..0000000000 --- a/app/src/main/java/org/thoughtcrime/securesms/exporter/SignalSmsExportService.kt +++ /dev/null @@ -1,208 +0,0 @@ -package org.thoughtcrime.securesms.exporter - -import android.content.Context -import android.content.Intent -import androidx.core.app.NotificationCompat -import app.cash.exhaustive.Exhaustive -import org.signal.core.util.PendingIntentFlags -import org.signal.smsexporter.ExportableMessage -import org.signal.smsexporter.SmsExportService -import org.thoughtcrime.securesms.R -import org.thoughtcrime.securesms.attachments.AttachmentId -import org.thoughtcrime.securesms.crypto.ModernDecryptingPartInputStream -import org.thoughtcrime.securesms.database.SignalDatabase -import org.thoughtcrime.securesms.database.model.MessageId -import org.thoughtcrime.securesms.database.model.databaseprotos.MessageExportState -import org.thoughtcrime.securesms.dependencies.ApplicationDependencies -import org.thoughtcrime.securesms.exporter.flow.SmsExportActivity -import org.thoughtcrime.securesms.jobs.ForegroundServiceUtil -import org.thoughtcrime.securesms.notifications.NotificationChannels -import org.thoughtcrime.securesms.notifications.NotificationIds -import org.thoughtcrime.securesms.notifications.v2.NotificationPendingIntentHelper -import org.thoughtcrime.securesms.util.JsonUtils -import java.io.EOFException -import java.io.IOException -import java.io.InputStream - -/** - * Service which integrates the SMS exporter functionality. - */ -class SignalSmsExportService : SmsExportService() { - - companion object { - /** - * Launches the export service and immediately begins exporting messages. - */ - fun start(context: Context, clearPreviousExportState: Boolean) { - val intent = Intent(context, SignalSmsExportService::class.java) - .apply { putExtra(CLEAR_PREVIOUS_EXPORT_STATE_EXTRA, clearPreviousExportState) } - ForegroundServiceUtil.startOrThrow(context, intent) - } - } - - private var reader: SignalSmsExportReader? = null - - override fun getNotification(progress: Int, total: Int): ExportNotification { - val pendingIntent = NotificationPendingIntentHelper.getActivity( - this, - 0, - SmsExportActivity.createIntent(this), - PendingIntentFlags.mutable() - ) - - return ExportNotification( - NotificationIds.SMS_EXPORT_SERVICE, - NotificationCompat.Builder(this, NotificationChannels.getInstance().BACKUPS) - .setSmallIcon(R.drawable.ic_signal_backup) - .setContentTitle(getString(R.string.SignalSmsExportService__exporting_messages)) - .setContentIntent(pendingIntent) - .setProgress(total, progress, false) - .build() - ) - } - - override fun getExportCompleteNotification(): ExportNotification? { - if (ApplicationDependencies.getAppForegroundObserver().isForegrounded) { - return null - } - - val pendingIntent = NotificationPendingIntentHelper.getActivity( - this, - 0, - SmsExportActivity.createIntent(this), - PendingIntentFlags.mutable() - ) - - return ExportNotification( - NotificationIds.SMS_EXPORT_COMPLETE, - NotificationCompat.Builder(this, NotificationChannels.getInstance().APP_ALERTS) - .setSmallIcon(R.drawable.ic_notification) - .setContentTitle(getString(R.string.SignalSmsExportService__signal_sms_export_complete)) - .setContentText(getString(R.string.SignalSmsExportService__tap_to_return_to_signal)) - .setContentIntent(pendingIntent) - .build() - ) - } - - override fun clearPreviousExportState() { - SignalDatabase.messages.clearExportState() - } - - override fun prepareForExport() { - SignalDatabase.messages.clearInsecureMessageExportedErrorStatus() - } - - override fun getUnexportedMessageCount(): Int { - ensureReader() - return reader!!.getCount() - } - - override fun getUnexportedMessages(): Iterable { - ensureReader() - return reader!! - } - - override fun onMessageExportStarted(exportableMessage: ExportableMessage) { - SignalDatabase.messages.updateMessageExportState(exportableMessage.getMessageId()) { - it.newBuilder().progress(MessageExportState.Progress.STARTED).build() - } - } - - override fun onMessageExportSucceeded(exportableMessage: ExportableMessage) { - SignalDatabase.messages.updateMessageExportState(exportableMessage.getMessageId()) { - it.newBuilder().progress(MessageExportState.Progress.COMPLETED).build() - } - - SignalDatabase.messages.markMessageExported(exportableMessage.getMessageId()) - } - - override fun onMessageExportFailed(exportableMessage: ExportableMessage) { - SignalDatabase.messages.updateMessageExportState(exportableMessage.getMessageId()) { - it.newBuilder().progress(MessageExportState.Progress.INIT).build() - } - - SignalDatabase.messages.markMessageExportFailed(exportableMessage.getMessageId()) - } - - override fun onMessageIdCreated(exportableMessage: ExportableMessage, messageId: Long) { - SignalDatabase.messages.updateMessageExportState(exportableMessage.getMessageId()) { - it.newBuilder().messageId(messageId).build() - } - } - - override fun onAttachmentPartExportStarted(exportableMessage: ExportableMessage, part: ExportableMessage.Mms.Part) { - SignalDatabase.messages.updateMessageExportState(exportableMessage.getMessageId()) { - it.newBuilder().apply { startedAttachments += part.contentId }.build() - } - } - - override fun onAttachmentPartExportSucceeded(exportableMessage: ExportableMessage, part: ExportableMessage.Mms.Part) { - SignalDatabase.messages.updateMessageExportState(exportableMessage.getMessageId()) { - it.newBuilder().apply { completedAttachments += part.contentId }.build() - } - } - - override fun onAttachmentPartExportFailed(exportableMessage: ExportableMessage, part: ExportableMessage.Mms.Part) { - SignalDatabase.messages.updateMessageExportState(exportableMessage.getMessageId()) { - val startedAttachments = it.startedAttachments - part.contentId - it.newBuilder().startedAttachments(startedAttachments).build() - } - } - - override fun onRecipientExportStarted(exportableMessage: ExportableMessage, recipient: String) { - SignalDatabase.messages.updateMessageExportState(exportableMessage.getMessageId()) { - it.newBuilder().apply { startedRecipients += recipient }.build() - } - } - - override fun onRecipientExportSucceeded(exportableMessage: ExportableMessage, recipient: String) { - SignalDatabase.messages.updateMessageExportState(exportableMessage.getMessageId()) { - it.newBuilder().apply { completedRecipients += recipient }.build() - } - } - - override fun onRecipientExportFailed(exportableMessage: ExportableMessage, recipient: String) { - SignalDatabase.messages.updateMessageExportState(exportableMessage.getMessageId()) { - val startedAttachments = it.startedRecipients - recipient - it.newBuilder().startedRecipients(startedAttachments).build() - } - } - - @Throws(IOException::class) - override fun getInputStream(part: ExportableMessage.Mms.Part): InputStream { - try { - return SignalDatabase.attachments.getAttachmentStream(JsonUtils.fromJson(part.contentId, AttachmentId::class.java), 0) - } catch (e: IOException) { - if (e.message == ModernDecryptingPartInputStream.PREMATURE_END_ERROR_MESSAGE) { - throw EOFException(e.message) - } else { - throw e - } - } - } - - override fun onExportPassCompleted() { - reader?.close() - } - - private fun ExportableMessage.getMessageId(): MessageId { - @Exhaustive - val messageId: Any = when (this) { - is ExportableMessage.Mms<*> -> id - is ExportableMessage.Sms<*> -> id - is ExportableMessage.Skip<*> -> 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() - } - } -} 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 deleted file mode 100644 index 23c7714f0b..0000000000 --- a/app/src/main/java/org/thoughtcrime/securesms/exporter/flow/ChooseANewDefaultSmsAppFragment.kt +++ /dev/null @@ -1,59 +0,0 @@ -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 -import org.signal.core.util.logging.Log -import org.signal.smsexporter.DefaultSmsHelper -import org.signal.smsexporter.ReleaseSmsAppFailure -import org.thoughtcrime.securesms.R -import org.thoughtcrime.securesms.databinding.ChooseANewDefaultSmsAppFragmentBinding - -/** - * Fragment which can launch the user into picking an alternative - * SMS app, or give them instructions on how to do so manually. - */ -class ChooseANewDefaultSmsAppFragment : Fragment(R.layout.choose_a_new_default_sms_app_fragment) { - - companion object { - private val TAG = Log.tag(ChooseANewDefaultSmsAppFragment::class.java) - } - - 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) } - }, - onFailure = { - when (it) { - ReleaseSmsAppFailure.APP_IS_INELIGIBLE_TO_RELEASE_SMS_SELECTION -> { - Log.w(TAG, "App is ineligible to release sms selection") - binding.continueButton.setOnClickListener { requireActivity().finish() } - } - ReleaseSmsAppFailure.NO_METHOD_TO_RELEASE_SMS_AVIALABLE -> { - Log.w(TAG, "We can't navigate the user to a specific spot so we should display instructions instead.") - binding.continueButton.setOnClickListener { requireActivity().finish() } - } - } - } - ) - } - - override fun onResume() { - super.onResume() - if (!DefaultSmsHelper.isDefaultSms(requireContext())) { - requireActivity().setResult(Activity.RESULT_OK) - requireActivity().finish() - } - } -} diff --git a/app/src/main/java/org/thoughtcrime/securesms/exporter/flow/ExportSmsCompleteFragment.kt b/app/src/main/java/org/thoughtcrime/securesms/exporter/flow/ExportSmsCompleteFragment.kt deleted file mode 100644 index 2fa902d30a..0000000000 --- a/app/src/main/java/org/thoughtcrime/securesms/exporter/flow/ExportSmsCompleteFragment.kt +++ /dev/null @@ -1,27 +0,0 @@ -package org.thoughtcrime.securesms.exporter.flow - -import android.os.Bundle -import android.view.View -import androidx.fragment.app.Fragment -import androidx.navigation.fragment.findNavController -import androidx.navigation.fragment.navArgs -import org.thoughtcrime.securesms.R -import org.thoughtcrime.securesms.SmsExportDirections -import org.thoughtcrime.securesms.databinding.ExportSmsCompleteFragmentBinding -import org.thoughtcrime.securesms.util.navigation.safeNavigate - -/** - * Shown when export sms completes. - */ -class ExportSmsCompleteFragment : Fragment(R.layout.export_sms_complete_fragment) { - - private val args: ExportSmsCompleteFragmentArgs by navArgs() - - override fun onViewCreated(view: View, savedInstanceState: Bundle?) { - val exportSuccessCount = args.exportMessageCount - args.exportMessageFailureCount - - val binding = ExportSmsCompleteFragmentBinding.bind(view) - binding.exportCompleteNext.setOnClickListener { findNavController().safeNavigate(SmsExportDirections.actionDirectToChooseANewDefaultSmsAppFragment()) } - binding.exportCompleteStatus.text = resources.getQuantityString(R.plurals.ExportSmsCompleteFragment__d_of_d_messages_exported, args.exportMessageCount, exportSuccessCount, args.exportMessageCount) - } -} diff --git a/app/src/main/java/org/thoughtcrime/securesms/exporter/flow/ExportSmsFullErrorFragment.kt b/app/src/main/java/org/thoughtcrime/securesms/exporter/flow/ExportSmsFullErrorFragment.kt deleted file mode 100644 index 26fa06190e..0000000000 --- a/app/src/main/java/org/thoughtcrime/securesms/exporter/flow/ExportSmsFullErrorFragment.kt +++ /dev/null @@ -1,44 +0,0 @@ -package org.thoughtcrime.securesms.exporter.flow - -import android.content.Context -import android.os.Bundle -import android.view.ContextThemeWrapper -import android.view.LayoutInflater -import android.view.View -import androidx.core.content.ContextCompat -import androidx.navigation.fragment.findNavController -import androidx.navigation.fragment.navArgs -import org.thoughtcrime.securesms.LoggingFragment -import org.thoughtcrime.securesms.R -import org.thoughtcrime.securesms.SmsExportDirections -import org.thoughtcrime.securesms.databinding.ExportSmsFullErrorFragmentBinding -import org.thoughtcrime.securesms.util.navigation.safeNavigate - -/** - * Fragment shown when all export messages failed. - */ -class ExportSmsFullErrorFragment : LoggingFragment(R.layout.export_sms_full_error_fragment) { - private val args: ExportSmsFullErrorFragmentArgs by navArgs() - - override fun onGetLayoutInflater(savedInstanceState: Bundle?): LayoutInflater { - val inflater = super.onGetLayoutInflater(savedInstanceState) - val contextThemeWrapper: Context = ContextThemeWrapper(requireContext(), R.style.Signal_DayNight) - return inflater.cloneInContext(contextThemeWrapper) - } - - @Suppress("UsePropertyAccessSyntax") - override fun onViewCreated(view: View, savedInstanceState: Bundle?) { - val binding = ExportSmsFullErrorFragmentBinding.bind(view) - - val exportSuccessCount = args.exportMessageCount - args.exportMessageFailureCount - binding.exportCompleteStatus.text = resources.getQuantityString(R.plurals.ExportSmsCompleteFragment__d_of_d_messages_exported, args.exportMessageCount, exportSuccessCount, args.exportMessageCount) - binding.retryButton.setOnClickListener { findNavController().safeNavigate(SmsExportDirections.actionDirectToExportYourSmsMessagesFragment()) } - binding.pleaseTryAgain.apply { - setLinkColor(ContextCompat.getColor(requireContext(), R.color.signal_colorPrimary)) - setLearnMoreVisible(true, R.string.ExportSmsPartiallyComplete__contact_us) - setOnLinkClickListener { - findNavController().safeNavigate(SmsExportDirections.actionDirectToHelpFragment()) - } - } - } -} diff --git a/app/src/main/java/org/thoughtcrime/securesms/exporter/flow/ExportSmsPartiallyCompleteFragment.kt b/app/src/main/java/org/thoughtcrime/securesms/exporter/flow/ExportSmsPartiallyCompleteFragment.kt deleted file mode 100644 index d1473396e4..0000000000 --- a/app/src/main/java/org/thoughtcrime/securesms/exporter/flow/ExportSmsPartiallyCompleteFragment.kt +++ /dev/null @@ -1,57 +0,0 @@ -package org.thoughtcrime.securesms.exporter.flow - -import android.content.Context -import android.os.Bundle -import android.text.format.Formatter -import android.view.ContextThemeWrapper -import android.view.LayoutInflater -import android.view.View -import androidx.core.content.ContextCompat -import androidx.navigation.fragment.findNavController -import androidx.navigation.fragment.navArgs -import org.signal.core.util.concurrent.SimpleTask -import org.thoughtcrime.securesms.LoggingFragment -import org.thoughtcrime.securesms.R -import org.thoughtcrime.securesms.SmsExportDirections -import org.thoughtcrime.securesms.database.SignalDatabase -import org.thoughtcrime.securesms.databinding.ExportSmsPartiallyCompleteFragmentBinding -import org.thoughtcrime.securesms.util.navigation.safeNavigate - -/** - * Fragment shown when some messages exported and some failed. - */ -class ExportSmsPartiallyCompleteFragment : LoggingFragment(R.layout.export_sms_partially_complete_fragment) { - - private val args: ExportSmsPartiallyCompleteFragmentArgs by navArgs() - - override fun onGetLayoutInflater(savedInstanceState: Bundle?): LayoutInflater { - val inflater = super.onGetLayoutInflater(savedInstanceState) - val contextThemeWrapper: Context = ContextThemeWrapper(requireContext(), R.style.Signal_DayNight) - return inflater.cloneInContext(contextThemeWrapper) - } - - @Suppress("UsePropertyAccessSyntax") - override fun onViewCreated(view: View, savedInstanceState: Bundle?) { - val binding = ExportSmsPartiallyCompleteFragmentBinding.bind(view) - - val exportSuccessCount = args.exportMessageCount - args.exportMessageFailureCount - binding.exportCompleteStatus.text = resources.getQuantityString(R.plurals.ExportSmsCompleteFragment__d_of_d_messages_exported, args.exportMessageCount, exportSuccessCount, args.exportMessageCount) - binding.retryButton.setOnClickListener { findNavController().safeNavigate(SmsExportDirections.actionDirectToExportYourSmsMessagesFragment()) } - binding.continueButton.setOnClickListener { findNavController().safeNavigate(SmsExportDirections.actionDirectToChooseANewDefaultSmsAppFragment()) } - binding.bullet3Text.apply { - setLinkColor(ContextCompat.getColor(requireContext(), R.color.signal_colorPrimary)) - setLearnMoreVisible(true, R.string.ExportSmsPartiallyComplete__contact_us) - setOnLinkClickListener { - findNavController().safeNavigate(SmsExportDirections.actionDirectToHelpFragment()) - } - } - - SimpleTask.runWhenValid( - viewLifecycleOwner.lifecycle, - { SignalDatabase.messages.getUnexportedInsecureMessagesEstimatedSize() + SignalDatabase.messages.getUnexportedInsecureMessagesEstimatedSize() }, - { totalSize -> - binding.bullet1Text.setText(getString(R.string.ExportSmsPartiallyComplete__ensure_you_have_an_additional_s_free_on_your_phone_to_export_your_messages, Formatter.formatFileSize(requireContext(), totalSize))) - } - ) - } -} diff --git a/app/src/main/java/org/thoughtcrime/securesms/exporter/flow/ExportYourSmsMessagesFragment.kt b/app/src/main/java/org/thoughtcrime/securesms/exporter/flow/ExportYourSmsMessagesFragment.kt deleted file mode 100644 index 5458758602..0000000000 --- a/app/src/main/java/org/thoughtcrime/securesms/exporter/flow/ExportYourSmsMessagesFragment.kt +++ /dev/null @@ -1,58 +0,0 @@ -package org.thoughtcrime.securesms.exporter.flow - -import android.os.Bundle -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.DefaultSmsHelper -import org.signal.smsexporter.SmsExportProgress -import org.signal.smsexporter.SmsExportService -import org.thoughtcrime.securesms.R -import org.thoughtcrime.securesms.databinding.ExportYourSmsMessagesFragmentBinding -import org.thoughtcrime.securesms.util.Material3OnScrollHelper -import org.thoughtcrime.securesms.util.navigation.safeNavigate - -/** - * "Welcome" screen for exporting sms - */ -class ExportYourSmsMessagesFragment : Fragment(R.layout.export_your_sms_messages_fragment) { - - private var navigationDisposable = Disposable.disposed() - - override fun onViewCreated(view: View, savedInstanceState: Bundle?) { - val binding = ExportYourSmsMessagesFragmentBinding.bind(view) - - binding.toolbar.setOnClickListener { - requireActivity().finish() - } - - binding.continueButton.setOnClickListener { - if (DefaultSmsHelper.isDefaultSms(requireContext())) { - findNavController().safeNavigate(ExportYourSmsMessagesFragmentDirections.actionExportYourSmsMessagesFragmentToExportingSmsMessagesFragment()) - } else { - findNavController().safeNavigate(ExportYourSmsMessagesFragmentDirections.actionExportYourSmsMessagesFragmentToSetSignalAsDefaultSmsAppFragment()) - } - } - - Material3OnScrollHelper(requireActivity(), binding.toolbar, viewLifecycleOwner).attach(binding.scrollView) - } - - override fun onResume() { - super.onResume() - navigationDisposable = SmsExportService - .progressState - .observeOn(AndroidSchedulers.mainThread()) - .subscribe { - if (it !is SmsExportProgress.Init) { - findNavController().safeNavigate(ExportYourSmsMessagesFragmentDirections.actionExportYourSmsMessagesFragmentToExportingSmsMessagesFragment()) - } - } - } - - override fun onPause() { - super.onPause() - navigationDisposable.dispose() - } -} 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 deleted file mode 100644 index c80dc6ecce..0000000000 --- a/app/src/main/java/org/thoughtcrime/securesms/exporter/flow/ExportingSmsMessagesFragment.kt +++ /dev/null @@ -1,120 +0,0 @@ -package org.thoughtcrime.securesms.exporter.flow - -import android.Manifest -import android.content.Context -import android.os.Bundle -import android.text.format.Formatter -import android.view.ContextThemeWrapper -import android.view.LayoutInflater -import android.view.View -import androidx.fragment.app.Fragment -import androidx.fragment.app.activityViewModels -import androidx.navigation.fragment.findNavController -import com.google.android.material.dialog.MaterialAlertDialogBuilder -import io.reactivex.rxjava3.android.schedulers.AndroidSchedulers -import io.reactivex.rxjava3.disposables.Disposable -import org.signal.core.util.concurrent.LifecycleDisposable -import org.signal.smsexporter.SmsExportProgress -import org.signal.smsexporter.SmsExportService -import org.thoughtcrime.securesms.R -import org.thoughtcrime.securesms.databinding.ExportingSmsMessagesFragmentBinding -import org.thoughtcrime.securesms.exporter.SignalSmsExportService -import org.thoughtcrime.securesms.permissions.Permissions -import org.thoughtcrime.securesms.util.mb -import org.thoughtcrime.securesms.util.navigation.safeNavigate - -/** - * "Export in progress" fragment which should be displayed - * when we start exporting messages. - */ -class ExportingSmsMessagesFragment : Fragment(R.layout.exporting_sms_messages_fragment) { - - private val viewModel: SmsExportViewModel by activityViewModels() - - 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) - } - - @Suppress("KotlinConstantConditions") - override fun onResume() { - super.onResume() - navigationDisposable = SmsExportService - .progressState - .observeOn(AndroidSchedulers.mainThread()) - .subscribe { smsExportProgress -> - if (smsExportProgress is SmsExportProgress.Done) { - SmsExportService.clearProgressState() - if (smsExportProgress.errorCount == 0) { - findNavController().safeNavigate(ExportingSmsMessagesFragmentDirections.actionExportingSmsMessagesFragmentToExportSmsCompleteFragment(smsExportProgress.total, smsExportProgress.errorCount)) - } else if (smsExportProgress.errorCount == smsExportProgress.total) { - findNavController().safeNavigate(ExportingSmsMessagesFragmentDirections.actionExportingSmsMessagesFragmentToExportSmsFullErrorFragment(smsExportProgress.total, smsExportProgress.errorCount)) - } else { - findNavController().safeNavigate(ExportingSmsMessagesFragmentDirections.actionExportingSmsMessagesFragmentToExportSmsPartiallyCompleteFragment(smsExportProgress.total, smsExportProgress.errorCount)) - } - } - } - } - - override fun onPause() { - super.onPause() - navigationDisposable.dispose() - } - - override fun onViewCreated(view: View, savedInstanceState: Bundle?) { - val binding = ExportingSmsMessagesFragmentBinding.bind(view) - - lifecycleDisposable.bindTo(viewLifecycleOwner) - lifecycleDisposable += SmsExportService.progressState.observeOn(AndroidSchedulers.mainThread()).subscribe { - when (it) { - SmsExportProgress.Init -> binding.progress.isIndeterminate = true - SmsExportProgress.Starting -> binding.progress.isIndeterminate = true - is SmsExportProgress.InProgress -> { - binding.progress.isIndeterminate = false - binding.progress.max = it.total - binding.progress.progress = it.progress - binding.progressLabel.text = resources.getQuantityString(R.plurals.ExportingSmsMessagesFragment__exporting_d_of_d, it.total, it.progress, it.total) - } - is SmsExportProgress.Done -> Unit - } - } - - lifecycleDisposable += ExportingSmsRepository() - .getSmsExportSizeEstimations() - .observeOn(AndroidSchedulers.mainThread()) - .subscribe { (internalFreeSpace, estimatedRequiredSpace) -> - val adjustedFreeSpace = internalFreeSpace - estimatedRequiredSpace - 100.mb - if (estimatedRequiredSpace > adjustedFreeSpace) { - MaterialAlertDialogBuilder(requireContext()) - .setTitle(R.string.ExportingSmsMessagesFragment__you_may_not_have_enough_disk_space) - .setMessage(getString(R.string.ExportingSmsMessagesFragment__you_need_approximately_s_to_export_your_messages_ensure_you_have_enough_space_before_continuing, Formatter.formatFileSize(requireContext(), estimatedRequiredSpace))) - .setPositiveButton(R.string.ExportingSmsMessagesFragment__continue_anyway) { _, _ -> checkPermissionsAndStartExport() } - .setNegativeButton(android.R.string.cancel) { _, _ -> findNavController().safeNavigate(ExportingSmsMessagesFragmentDirections.actionDirectToExportYourSmsMessagesFragment()) } - .setCancelable(false) - .show() - } else { - checkPermissionsAndStartExport() - } - } - } - - @Suppress("OVERRIDE_DEPRECATION") - override fun onRequestPermissionsResult(requestCode: Int, permissions: Array, grantResults: IntArray) { - Permissions.onRequestPermissionsResult(this, requestCode, permissions, grantResults) - } - - private fun checkPermissionsAndStartExport() { - Permissions.with(this) - .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(), 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/ExportingSmsRepository.kt b/app/src/main/java/org/thoughtcrime/securesms/exporter/flow/ExportingSmsRepository.kt deleted file mode 100644 index 76cb65e343..0000000000 --- a/app/src/main/java/org/thoughtcrime/securesms/exporter/flow/ExportingSmsRepository.kt +++ /dev/null @@ -1,38 +0,0 @@ -package org.thoughtcrime.securesms.exporter.flow - -import android.app.Application -import android.os.Build -import android.os.storage.StorageManager -import androidx.core.content.ContextCompat -import io.reactivex.rxjava3.core.Single -import io.reactivex.rxjava3.schedulers.Schedulers -import org.thoughtcrime.securesms.database.SignalDatabase -import org.thoughtcrime.securesms.dependencies.ApplicationDependencies -import java.io.File - -class ExportingSmsRepository(private val context: Application = ApplicationDependencies.getApplication()) { - - @Suppress("UsePropertyAccessSyntax") - fun getSmsExportSizeEstimations(): Single { - return Single.fromCallable { - val internalStorageFile = if (Build.VERSION.SDK_INT < 24) { - File(context.applicationInfo.dataDir) - } else { - context.dataDir - } - - val internalFreeSpace: Long = if (Build.VERSION.SDK_INT < 26) { - internalStorageFile.usableSpace - } else { - val storageManagerFreeSpace = ContextCompat.getSystemService(context, StorageManager::class.java)?.let { storageManager -> - storageManager.getAllocatableBytes(storageManager.getUuidForPath(internalStorageFile)) - } - storageManagerFreeSpace ?: internalStorageFile.usableSpace - } - - SmsExportSizeEstimations(internalFreeSpace, SignalDatabase.messages.getUnexportedInsecureMessagesEstimatedSize() + SignalDatabase.messages.getUnexportedInsecureMessagesEstimatedSize()) - }.subscribeOn(Schedulers.io()) - } - - data class SmsExportSizeEstimations(val estimatedInternalFreeSpace: Long, val estimatedRequiredSpace: Long) -} diff --git a/app/src/main/java/org/thoughtcrime/securesms/exporter/flow/SetSignalAsDefaultSmsAppFragment.kt b/app/src/main/java/org/thoughtcrime/securesms/exporter/flow/SetSignalAsDefaultSmsAppFragment.kt deleted file mode 100644 index 11d6584d8d..0000000000 --- a/app/src/main/java/org/thoughtcrime/securesms/exporter/flow/SetSignalAsDefaultSmsAppFragment.kt +++ /dev/null @@ -1,43 +0,0 @@ -package org.thoughtcrime.securesms.exporter.flow - -import android.os.Bundle -import android.view.View -import androidx.activity.result.contract.ActivityResultContracts -import androidx.fragment.app.Fragment -import androidx.navigation.fragment.findNavController -import org.signal.smsexporter.BecomeSmsAppFailure -import org.signal.smsexporter.DefaultSmsHelper -import org.thoughtcrime.securesms.R -import org.thoughtcrime.securesms.databinding.SetSignalAsDefaultSmsAppFragmentBinding -import org.thoughtcrime.securesms.util.navigation.safeNavigate - -class SetSignalAsDefaultSmsAppFragment : Fragment(R.layout.set_signal_as_default_sms_app_fragment) { - - override fun onViewCreated(view: View, savedInstanceState: Bundle?) { - val binding = SetSignalAsDefaultSmsAppFragmentBinding.bind(view) - - val smsDefaultLauncher = registerForActivityResult(ActivityResultContracts.StartActivityForResult()) { - if (DefaultSmsHelper.isDefaultSms(requireContext())) { - navigateToExporter() - } - } - - binding.continueButton.setOnClickListener { - DefaultSmsHelper.becomeDefaultSms(requireContext()).either( - onSuccess = { - smsDefaultLauncher.launch(it) - }, - onFailure = { - when (it) { - BecomeSmsAppFailure.ALREADY_DEFAULT_SMS -> navigateToExporter() - BecomeSmsAppFailure.ROLE_IS_NOT_AVAILABLE -> error("Should never happen") - } - } - ) - } - } - - private fun navigateToExporter() { - findNavController().safeNavigate(SetSignalAsDefaultSmsAppFragmentDirections.actionSetSignalAsDefaultSmsAppFragmentToExportingSmsMessagesFragment()) - } -} 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 deleted file mode 100644 index 3b422ad598..0000000000 --- a/app/src/main/java/org/thoughtcrime/securesms/exporter/flow/SmsExportActivity.kt +++ /dev/null @@ -1,61 +0,0 @@ -package org.thoughtcrime.securesms.exporter.flow - -import android.content.Context -import android.content.Intent -import android.os.Bundle -import androidx.activity.OnBackPressedCallback -import androidx.core.app.NotificationManagerCompat -import androidx.fragment.app.Fragment -import androidx.lifecycle.ViewModelProvider -import androidx.navigation.findNavController -import androidx.navigation.fragment.NavHostFragment -import org.thoughtcrime.securesms.R -import org.thoughtcrime.securesms.components.FragmentWrapperActivity -import org.thoughtcrime.securesms.notifications.NotificationIds -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_FROM_MEGAPHONE, false), intent.getBooleanExtra(IS_RE_EXPORT, false)) - viewModel = ViewModelProvider(this, factory).get(SmsExportViewModel::class.java) - } - - override fun getFragment(): Fragment { - return NavHostFragment.create(R.navigation.sms_export) - } - - private inner class OnBackPressed : OnBackPressedCallback(true) { - override fun handleOnBackPressed() { - if (!findNavController(R.id.fragment_container).popBackStack()) { - finish() - } - } - } - - companion object { - private const val IS_RE_EXPORT = "is_re_export" - private const val IS_FROM_MEGAPHONE = "is_from_megaphone" - - @JvmOverloads - @JvmStatic - fun createIntent(context: Context, isFromMegaphone: Boolean = false, isReExport: Boolean = false): Intent { - return Intent(context, SmsExportActivity::class.java).apply { - putExtra(IS_RE_EXPORT, isReExport) - putExtra(IS_FROM_MEGAPHONE, isFromMegaphone) - } - } - } -} 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 deleted file mode 100644 index 86cbb43bca..0000000000 --- a/app/src/main/java/org/thoughtcrime/securesms/exporter/flow/SmsExportDialogs.kt +++ /dev/null @@ -1,39 +0,0 @@ -package org.thoughtcrime.securesms.exporter.flow - -import android.content.Context -import android.view.View -import com.google.android.material.dialog.MaterialAlertDialogBuilder -import com.google.android.material.snackbar.Snackbar -import org.signal.core.util.concurrent.SignalExecutors -import org.thoughtcrime.securesms.R -import org.thoughtcrime.securesms.database.SignalDatabase - -object SmsExportDialogs { - @JvmStatic - fun showSmsRemovalDialog(context: Context, view: View) { - MaterialAlertDialogBuilder(context) - .setTitle(R.string.RemoveSmsMessagesDialogFragment__remove_sms_messages) - .setMessage(R.string.RemoveSmsMessagesDialogFragment__you_can_now_remove_sms_messages_from_signal) - .setPositiveButton(R.string.RemoveSmsMessagesDialogFragment__keep_messages) { _, _ -> - Snackbar.make(view, 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.messages.deleteExportedMessages() - SignalDatabase.messages.deleteExportedMessages() - } - Snackbar.make(view, R.string.SmsSettingsFragment__removing_sms_messages_from_signal, Snackbar.LENGTH_SHORT).show() - } - .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/SmsExportHelpFragment.kt b/app/src/main/java/org/thoughtcrime/securesms/exporter/flow/SmsExportHelpFragment.kt deleted file mode 100644 index 02e3cb47fe..0000000000 --- a/app/src/main/java/org/thoughtcrime/securesms/exporter/flow/SmsExportHelpFragment.kt +++ /dev/null @@ -1,30 +0,0 @@ -package org.thoughtcrime.securesms.exporter.flow - -import android.os.Bundle -import android.view.View -import androidx.core.os.bundleOf -import androidx.navigation.fragment.findNavController -import org.thoughtcrime.securesms.LoggingFragment -import org.thoughtcrime.securesms.R -import org.thoughtcrime.securesms.databinding.SmsExportHelpFragmentBinding -import org.thoughtcrime.securesms.help.HelpFragment - -/** - * Fragment wrapper around the app settings help fragment to provide a toolbar and set default category for sms export. - */ -class SmsExportHelpFragment : LoggingFragment(R.layout.sms_export_help_fragment) { - override fun onViewCreated(view: View, savedInstanceState: Bundle?) { - val binding = SmsExportHelpFragmentBinding.bind(view) - - binding.toolbar.setOnClickListener { - if (!findNavController().popBackStack()) { - requireActivity().finish() - } - } - - childFragmentManager - .beginTransaction() - .replace(binding.smsExportHelpFragmentFragment.id, HelpFragment().apply { arguments = bundleOf(HelpFragment.START_CATEGORY_INDEX to HelpFragment.SMS_EXPORT_INDEX) }) - .commitNow() - } -} diff --git a/app/src/main/java/org/thoughtcrime/securesms/exporter/flow/SmsExportViewModel.kt b/app/src/main/java/org/thoughtcrime/securesms/exporter/flow/SmsExportViewModel.kt deleted file mode 100644 index fb2ef6d3ed..0000000000 --- a/app/src/main/java/org/thoughtcrime/securesms/exporter/flow/SmsExportViewModel.kt +++ /dev/null @@ -1,17 +0,0 @@ -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 isFromMegaphone: Boolean, val isReExport: Boolean) : ViewModel() { - class Factory(private val isFromMegaphone: Boolean, private val isReExport: Boolean) : ViewModelProvider.Factory { - override fun create(modelClass: Class): T { - return requireNotNull(modelClass.cast(SmsExportViewModel(isFromMegaphone, isReExport))) - } - } -} diff --git a/app/src/main/java/org/thoughtcrime/securesms/exporter/flow/SmsRemovalInformationFragment.kt b/app/src/main/java/org/thoughtcrime/securesms/exporter/flow/SmsRemovalInformationFragment.kt deleted file mode 100644 index b36a45d2c3..0000000000 --- a/app/src/main/java/org/thoughtcrime/securesms/exporter/flow/SmsRemovalInformationFragment.kt +++ /dev/null @@ -1,56 +0,0 @@ -package org.thoughtcrime.securesms.exporter.flow - -import android.os.Bundle -import android.view.LayoutInflater -import android.view.View -import android.view.ViewGroup -import androidx.fragment.app.activityViewModels -import androidx.navigation.fragment.findNavController -import org.thoughtcrime.securesms.LoggingFragment -import org.thoughtcrime.securesms.R -import org.thoughtcrime.securesms.databinding.SmsRemovalInformationFragmentBinding -import org.thoughtcrime.securesms.util.CommunicationActions -import org.thoughtcrime.securesms.util.navigation.safeNavigate - -/** - * Fragment shown when entering the sms export flow from the basic megaphone. - * - * Layout shared with full screen megaphones for Phase 2/3. - */ -class SmsRemovalInformationFragment : LoggingFragment() { - private val viewModel: SmsExportViewModel by activityViewModels() - - private lateinit var binding: SmsRemovalInformationFragmentBinding - - override fun onCreateView(inflater: LayoutInflater, container: ViewGroup?, savedInstanceState: Bundle?): View { - binding = SmsRemovalInformationFragmentBinding.inflate(inflater, container, false) - return binding.root - } - - override fun onViewCreated(view: View, savedInstanceState: Bundle?) { - super.onViewCreated(view, savedInstanceState) - - if (!viewModel.isFromMegaphone) { - findNavController().safeNavigate(SmsRemovalInformationFragmentDirections.actionSmsRemovalInformationFragmentToExportYourSmsMessagesFragment()) - } else { - val goBackClickListener = { _: View -> - if (!findNavController().popBackStack()) { - requireActivity().finish() - } - } - - binding.bullet1Text.text = getString(R.string.SmsRemoval_info_bullet_1) - - binding.toolbar.setNavigationOnClickListener(goBackClickListener) - binding.laterButton.setOnClickListener(goBackClickListener) - - binding.learnMoreButton.setOnClickListener { - CommunicationActions.openBrowserLink(requireContext(), getString(R.string.sms_export_url)) - } - - binding.exportSmsButton.setOnClickListener { - findNavController().safeNavigate(SmsRemovalInformationFragmentDirections.actionSmsRemovalInformationFragmentToExportYourSmsMessagesFragment()) - } - } - } -} diff --git a/app/src/main/java/org/thoughtcrime/securesms/megaphone/Megaphones.java b/app/src/main/java/org/thoughtcrime/securesms/megaphone/Megaphones.java index 472b7e159f..27a2764be5 100644 --- a/app/src/main/java/org/thoughtcrime/securesms/megaphone/Megaphones.java +++ b/app/src/main/java/org/thoughtcrime/securesms/megaphone/Megaphones.java @@ -24,7 +24,6 @@ import org.thoughtcrime.securesms.dependencies.ApplicationDependencies; import org.thoughtcrime.securesms.jobmanager.impl.NetworkConstraint; import org.thoughtcrime.securesms.keyvalue.PhoneNumberPrivacyValues; import org.thoughtcrime.securesms.keyvalue.SignalStore; -import org.thoughtcrime.securesms.keyvalue.SmsExportPhase; import org.thoughtcrime.securesms.lock.SignalPinReminderDialog; import org.thoughtcrime.securesms.lock.SignalPinReminders; import org.thoughtcrime.securesms.lock.v2.CreateSvrPinActivity; @@ -108,7 +107,6 @@ public final class Megaphones { put(Event.CLIENT_DEPRECATED, SignalStore.misc().isClientDeprecated() ? ALWAYS : NEVER); put(Event.NOTIFICATIONS, shouldShowNotificationsMegaphone(context) ? RecurringSchedule.every(TimeUnit.DAYS.toMillis(30)) : NEVER); put(Event.GRANT_FULL_SCREEN_INTENT, shouldShowGrantFullScreenIntentPermission(context) ? RecurringSchedule.every(TimeUnit.DAYS.toMillis(3)) : NEVER); - put(Event.SMS_EXPORT, new SmsExportReminderSchedule(context)); put(Event.BACKUP_SCHEDULE_PERMISSION, shouldShowBackupSchedulePermissionMegaphone(context) ? RecurringSchedule.every(TimeUnit.DAYS.toMillis(3)) : NEVER); put(Event.ONBOARDING, shouldShowOnboardingMegaphone(context) ? ALWAYS : NEVER); put(Event.TURN_OFF_CENSORSHIP_CIRCUMVENTION, shouldShowTurnOffCircumventionMegaphone() ? RecurringSchedule.every(TimeUnit.DAYS.toMillis(7)) : NEVER); @@ -141,8 +139,6 @@ public final class Megaphones { return buildRemoteMegaphone(context); case BACKUP_SCHEDULE_PERMISSION: return buildBackupPermissionMegaphone(context); - case SMS_EXPORT: - return buildSmsExportMegaphone(context); case SET_UP_YOUR_USERNAME: return buildSetUpYourUsernameMegaphone(context); case GRANT_FULL_SCREEN_INTENT: @@ -321,24 +317,6 @@ public final class Megaphones { .build(); } - private static @NonNull Megaphone buildSmsExportMegaphone(@NonNull Context context) { - SmsExportPhase phase = SignalStore.misc().getSmsExportPhase(); - - Megaphone.Builder builder = new Megaphone.Builder(Event.SMS_EXPORT, Megaphone.Style.FULLSCREEN) - .setOnVisibleListener((megaphone, controller) -> { - if (phase.isBlockingUi()) { - SmsExportReminderSchedule.setShowPhase3Megaphone(false); - } - controller.onMegaphoneNavigationRequested(new Intent(context, SmsExportMegaphoneActivity.class), SmsExportMegaphoneActivity.REQUEST_CODE); - }); - - if (phase.isBlockingUi()) { - builder.disableSnooze(); - } - - return builder.build(); - } - public static @NonNull Megaphone buildSetUpYourUsernameMegaphone(@NonNull Context context) { return new Megaphone.Builder(Event.SET_UP_YOUR_USERNAME, Megaphone.Style.BASIC) .setTitle(R.string.NewWaysToConnectDialogFragment__new_ways_to_connect) @@ -471,7 +449,6 @@ public final class Megaphones { TURN_OFF_CENSORSHIP_CIRCUMVENTION("turn_off_censorship_circumvention"), REMOTE_MEGAPHONE("remote_megaphone"), BACKUP_SCHEDULE_PERMISSION("backup_schedule_permission"), - SMS_EXPORT("sms_export"), SET_UP_YOUR_USERNAME("set_up_your_username"), GRANT_FULL_SCREEN_INTENT("grant_full_screen_intent"); diff --git a/app/src/main/java/org/thoughtcrime/securesms/megaphone/SmsExportMegaphoneActivity.kt b/app/src/main/java/org/thoughtcrime/securesms/megaphone/SmsExportMegaphoneActivity.kt deleted file mode 100644 index 10c418ae19..0000000000 --- a/app/src/main/java/org/thoughtcrime/securesms/megaphone/SmsExportMegaphoneActivity.kt +++ /dev/null @@ -1,79 +0,0 @@ -package org.thoughtcrime.securesms.megaphone - -import android.app.Activity -import android.content.Intent -import android.os.Bundle -import androidx.activity.result.ActivityResultLauncher -import androidx.activity.result.contract.ActivityResultContracts -import org.thoughtcrime.securesms.PassphraseRequiredActivity -import org.thoughtcrime.securesms.R -import org.thoughtcrime.securesms.databinding.SmsRemovalInformationFragmentBinding -import org.thoughtcrime.securesms.dependencies.ApplicationDependencies -import org.thoughtcrime.securesms.exporter.flow.SmsExportActivity -import org.thoughtcrime.securesms.keyvalue.SignalStore -import org.thoughtcrime.securesms.util.CommunicationActions -import org.thoughtcrime.securesms.util.DynamicNoActionBarTheme -import org.thoughtcrime.securesms.util.DynamicTheme -import org.thoughtcrime.securesms.util.visible - -class SmsExportMegaphoneActivity : PassphraseRequiredActivity() { - - companion object { - const val REQUEST_CODE: Short = 5343 - } - - private val theme: DynamicTheme = DynamicNoActionBarTheme() - private lateinit var binding: SmsRemovalInformationFragmentBinding - private lateinit var smsExportLauncher: ActivityResultLauncher - - override fun onPreCreate() { - theme.onCreate(this) - } - - override fun onCreate(savedInstanceState: Bundle?, ready: Boolean) { - binding = SmsRemovalInformationFragmentBinding.inflate(layoutInflater) - setContentView(binding.root) - - smsExportLauncher = registerForActivityResult(ActivityResultContracts.StartActivityForResult()) { - if (it.resultCode == Activity.RESULT_OK) { - ApplicationDependencies.getMegaphoneRepository().markSeen(Megaphones.Event.SMS_EXPORT) - setResult(Activity.RESULT_OK) - finish() - } - } - - binding.toolbar.setNavigationOnClickListener { onBackPressed() } - - binding.learnMoreButton.setOnClickListener { - CommunicationActions.openBrowserLink(this, getString(R.string.sms_export_url)) - } - - if (SignalStore.misc().smsExportPhase.isBlockingUi()) { - binding.headline.setText(R.string.SmsExportMegaphoneActivity__signal_no_longer_supports_sms) - binding.laterButton.visible = false - binding.bullet1Text.setText(R.string.SmsRemoval_info_bullet_1_phase_3) - } else { - binding.bullet1Text.text = getString(R.string.SmsRemoval_info_bullet_1) - - binding.headline.setText(R.string.SmsExportMegaphoneActivity__signal_will_no_longer_support_sms) - binding.laterButton.setOnClickListener { - onBackPressed() - } - } - - binding.exportSmsButton.setOnClickListener { - smsExportLauncher.launch(SmsExportActivity.createIntent(this)) - } - } - - override fun onBackPressed() { - ApplicationDependencies.getMegaphoneRepository().markSeen(Megaphones.Event.SMS_EXPORT) - setResult(Activity.RESULT_CANCELED) - super.onBackPressed() - } - - override fun onResume() { - super.onResume() - theme.onResume(this) - } -} diff --git a/app/src/main/java/org/thoughtcrime/securesms/megaphone/SmsExportReminderSchedule.kt b/app/src/main/java/org/thoughtcrime/securesms/megaphone/SmsExportReminderSchedule.kt deleted file mode 100644 index 0c9168e072..0000000000 --- a/app/src/main/java/org/thoughtcrime/securesms/megaphone/SmsExportReminderSchedule.kt +++ /dev/null @@ -1,22 +0,0 @@ -package org.thoughtcrime.securesms.megaphone - -import android.content.Context -import androidx.annotation.WorkerThread -import org.thoughtcrime.securesms.util.Util - -class SmsExportReminderSchedule(private val context: Context) : MegaphoneSchedule { - - companion object { - @JvmStatic - var showPhase3Megaphone = true - } - - @WorkerThread - override fun shouldDisplay(seenCount: Int, lastSeen: Long, firstVisible: Long, currentTime: Long): Boolean { - return if (Util.isDefaultSmsProvider(context)) { - showPhase3Megaphone - } else { - false - } - } -} 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 deleted file mode 100644 index 306cb47128..0000000000 --- a/app/src/main/res/layout/choose_a_new_default_sms_app_fragment.xml +++ /dev/null @@ -1,260 +0,0 @@ - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - \ No newline at end of file diff --git a/app/src/main/res/layout/export_sms_complete_fragment.xml b/app/src/main/res/layout/export_sms_complete_fragment.xml deleted file mode 100644 index c998755a39..0000000000 --- a/app/src/main/res/layout/export_sms_complete_fragment.xml +++ /dev/null @@ -1,55 +0,0 @@ - - - - - - - - - - - - \ No newline at end of file diff --git a/app/src/main/res/layout/export_sms_full_error_fragment.xml b/app/src/main/res/layout/export_sms_full_error_fragment.xml deleted file mode 100644 index d6af75a1ea..0000000000 --- a/app/src/main/res/layout/export_sms_full_error_fragment.xml +++ /dev/null @@ -1,94 +0,0 @@ - - - - - - - - - - - - - - - - - - - - - - diff --git a/app/src/main/res/layout/export_sms_partially_complete_fragment.xml b/app/src/main/res/layout/export_sms_partially_complete_fragment.xml deleted file mode 100644 index d8149bd83b..0000000000 --- a/app/src/main/res/layout/export_sms_partially_complete_fragment.xml +++ /dev/null @@ -1,250 +0,0 @@ - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - 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 deleted file mode 100644 index 6ed011f3a1..0000000000 --- a/app/src/main/res/layout/export_your_sms_messages_fragment.xml +++ /dev/null @@ -1,77 +0,0 @@ - - - - - - - - - - - - - - - - - - - - - - diff --git a/app/src/main/res/layout/exporting_sms_messages_fragment.xml b/app/src/main/res/layout/exporting_sms_messages_fragment.xml deleted file mode 100644 index 7963b1d1ff..0000000000 --- a/app/src/main/res/layout/exporting_sms_messages_fragment.xml +++ /dev/null @@ -1,57 +0,0 @@ - - - - - - - - - - - - \ No newline at end of file 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 deleted file mode 100644 index 5dabaf147b..0000000000 --- a/app/src/main/res/layout/set_signal_as_default_sms_app_fragment.xml +++ /dev/null @@ -1,63 +0,0 @@ - - - - - - - - - - - - - - \ No newline at end of file diff --git a/app/src/main/res/layout/sms_removal_information_fragment.xml b/app/src/main/res/layout/sms_removal_information_fragment.xml deleted file mode 100644 index dbc59e9b67..0000000000 --- a/app/src/main/res/layout/sms_removal_information_fragment.xml +++ /dev/null @@ -1,176 +0,0 @@ - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - \ No newline at end of file diff --git a/app/src/main/res/navigation/sms_export.xml b/app/src/main/res/navigation/sms_export.xml deleted file mode 100644 index ba3633a216..0000000000 --- a/app/src/main/res/navigation/sms_export.xml +++ /dev/null @@ -1,186 +0,0 @@ - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - \ No newline at end of file diff --git a/app/src/main/res/values/strings.xml b/app/src/main/res/values/strings.xml index fee44b233e..ea2dda4d6b 100644 --- a/app/src/main/res/values/strings.xml +++ b/app/src/main/res/values/strings.xml @@ -4417,27 +4417,6 @@ Keyboard Enter key sends - - Use as default SMS app - - Export SMS messages - - Export SMS messages again - - Remove SMS messages - - Removing SMS messages from Signal… - - 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. - - SMS support will be removed soon to focus on encrypted messaging. - Messages Calls @@ -5828,48 +5807,6 @@ Overflow menu - - - Exporting messages… - - Signal SMS Export Complete - - Tap to return to Signal - - - - Export your SMS messages - - You can export your SMS messages to your phone\'s SMS database and you\'ll have the option to keep or remove them from Signal. This allows other SMS apps on your phone to import them. This does not create a shareable file of your SMS history. - - Continue - - - - Exporting SMS messages - - This may take a while - - - Exporting %1$d of %2$d… - Exporting %1$d of %2$d… - - - You may not have enough disk space - - You need approximately %1$s to export your messages, ensure you have enough space before continuing. - - Continue anyway - - Signal needs the SMS permission to be able to export your SMS messages. - - - - Choose a new default SMS app - - Continue - - Done 1 @@ -5878,47 +5815,6 @@ 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\" - - - - Keep messages - - Remove messages - - 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. - - - - 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 - - To export your SMS messages, you need to set Signal as the default SMS app. - - Next - Can\'t back up chats @@ -5937,45 +5833,6 @@ Go to settings - - - Signal will no longer support SMS - - Signal no longer supports SMS - - Remind me later - - - - Export Complete - - Next - - - %1$d of %2$d message exported - %1$d of %2$d messages exported - - - - Export partially complete - - Ensure you have an additional %1$s free on your phone to export your messages - - Retry export, which will only retry messages that have not yet been exported - - If the problem persists, - - contact us - - Retry - - Continue anyway - - Error exporting SMS messages - - Please try again. If the problem persists, - - Privacy over profit @@ -6220,19 +6077,6 @@ Tip: Pull down on the chat list to filter - - SMS support is going away - - SMS messaging in the Signal app will soon no longer be supported. - - SMS messages are different than Signal messages. This does not affect encrypted Signal messaging which will continue to work. - - You can export your SMS messages and choose a new SMS app. - - Signal has removed support for sending SMS messages. - - Export SMS - Set up your Signal username diff --git a/settings.gradle.kts b/settings.gradle.kts index 78ed729dcb..a4d05b2059 100644 --- a/settings.gradle.kts +++ b/settings.gradle.kts @@ -41,8 +41,6 @@ include(":device-transfer") include(":device-transfer-app") include(":image-editor") include(":image-editor-app") -include(":sms-exporter") -include(":sms-exporter-app") include(":donations") include(":donations-app") include(":spinner") @@ -69,9 +67,6 @@ project(":device-transfer-app").projectDir = file("device-transfer/app") project(":image-editor").projectDir = file("image-editor/lib") project(":image-editor-app").projectDir = file("image-editor/app") -project(":sms-exporter").projectDir = file("sms-exporter/lib") -project(":sms-exporter-app").projectDir = file("sms-exporter/app") - project(":donations").projectDir = file("donations/lib") project(":donations-app").projectDir = file("donations/app") diff --git a/sms-exporter/app/build.gradle b/sms-exporter/app/build.gradle deleted file mode 100644 index 1f38a5392d..0000000000 --- a/sms-exporter/app/build.gradle +++ /dev/null @@ -1,15 +0,0 @@ -plugins { - id("signal-sample-app") -} - -android { - namespace = "org.signal.smsexporter.app" - - defaultConfig { - applicationId = "org.signal.smsexporter.app" - } -} - -dependencies { - implementation(project(":sms-exporter")) -} diff --git a/sms-exporter/app/proguard-rules.pro b/sms-exporter/app/proguard-rules.pro deleted file mode 100644 index 481bb43481..0000000000 --- a/sms-exporter/app/proguard-rules.pro +++ /dev/null @@ -1,21 +0,0 @@ -# Add project specific ProGuard rules here. -# You can control the set of applied configuration files using the -# proguardFiles setting in build.gradle. -# -# For more details, see -# http://developer.android.com/guide/developing/tools/proguard.html - -# If your project uses WebView with JS, uncomment the following -# and specify the fully qualified class name to the JavaScript interface -# class: -#-keepclassmembers class fqcn.of.javascript.interface.for.webview { -# public *; -#} - -# Uncomment this to preserve the line number information for -# debugging stack traces. -#-keepattributes SourceFile,LineNumberTable - -# If you keep the line number information, uncomment this to -# hide the original source file name. -#-renamesourcefileattribute SourceFile \ No newline at end of file diff --git a/sms-exporter/app/src/main/AndroidManifest.xml b/sms-exporter/app/src/main/AndroidManifest.xml deleted file mode 100644 index 131f6d034f..0000000000 --- a/sms-exporter/app/src/main/AndroidManifest.xml +++ /dev/null @@ -1,65 +0,0 @@ - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - diff --git a/sms-exporter/app/src/main/java/org/signal/smsexporter/app/BitmapGenerator.kt b/sms-exporter/app/src/main/java/org/signal/smsexporter/app/BitmapGenerator.kt deleted file mode 100644 index 51a60835f5..0000000000 --- a/sms-exporter/app/src/main/java/org/signal/smsexporter/app/BitmapGenerator.kt +++ /dev/null @@ -1,35 +0,0 @@ -package org.signal.smsexporter.app - -import android.graphics.Bitmap -import android.graphics.Color -import androidx.core.graphics.applyCanvas -import java.io.ByteArrayInputStream -import java.io.ByteArrayOutputStream -import java.io.InputStream -import java.util.Random - -object BitmapGenerator { - - private val colors = listOf( - Color.BLACK, - Color.BLUE, - Color.GRAY, - Color.GREEN, - Color.RED, - Color.CYAN - ) - - fun getStream(): InputStream { - val bitmap = Bitmap.createBitmap(100, 100, Bitmap.Config.ARGB_8888) - bitmap.applyCanvas { - val random = Random() - drawColor(colors[random.nextInt(colors.size - 1)]) - } - - val out = ByteArrayOutputStream() - bitmap.compress(Bitmap.CompressFormat.JPEG, 80, out) - val data = out.toByteArray() - - return ByteArrayInputStream(data) - } -} diff --git a/sms-exporter/app/src/main/java/org/signal/smsexporter/app/BroadcastSmsReceiver.kt b/sms-exporter/app/src/main/java/org/signal/smsexporter/app/BroadcastSmsReceiver.kt deleted file mode 100644 index 79cadd9826..0000000000 --- a/sms-exporter/app/src/main/java/org/signal/smsexporter/app/BroadcastSmsReceiver.kt +++ /dev/null @@ -1,10 +0,0 @@ -package org.signal.smsexporter.app - -import android.content.BroadcastReceiver -import android.content.Context -import android.content.Intent - -class BroadcastSmsReceiver : BroadcastReceiver() { - override fun onReceive(context: Context?, intent: Intent?) { - } -} diff --git a/sms-exporter/app/src/main/java/org/signal/smsexporter/app/BroadcastWapPushReceiver.kt b/sms-exporter/app/src/main/java/org/signal/smsexporter/app/BroadcastWapPushReceiver.kt deleted file mode 100644 index 04625a965f..0000000000 --- a/sms-exporter/app/src/main/java/org/signal/smsexporter/app/BroadcastWapPushReceiver.kt +++ /dev/null @@ -1,10 +0,0 @@ -package org.signal.smsexporter.app - -import android.content.BroadcastReceiver -import android.content.Context -import android.content.Intent - -class BroadcastWapPushReceiver : BroadcastReceiver() { - override fun onReceive(context: Context?, intent: Intent?) { - } -} diff --git a/sms-exporter/app/src/main/java/org/signal/smsexporter/app/MainActivity.kt b/sms-exporter/app/src/main/java/org/signal/smsexporter/app/MainActivity.kt deleted file mode 100644 index cba4ea379a..0000000000 --- a/sms-exporter/app/src/main/java/org/signal/smsexporter/app/MainActivity.kt +++ /dev/null @@ -1,140 +0,0 @@ -package org.signal.smsexporter.app - -import android.content.Intent -import android.os.Bundle -import android.widget.TextView -import android.widget.Toast -import androidx.appcompat.app.AppCompatActivity -import androidx.core.content.ContextCompat -import androidx.core.view.isVisible -import com.google.android.material.button.MaterialButton -import com.google.android.material.progressindicator.LinearProgressIndicator -import io.reactivex.rxjava3.android.schedulers.AndroidSchedulers -import io.reactivex.rxjava3.disposables.CompositeDisposable -import io.reactivex.rxjava3.kotlin.plusAssign -import io.reactivex.rxjava3.schedulers.Schedulers -import org.signal.smsexporter.DefaultSmsHelper -import org.signal.smsexporter.ReleaseSmsAppFailure -import org.signal.smsexporter.SmsExportProgress -import org.signal.smsexporter.SmsExportService - -class MainActivity : AppCompatActivity(R.layout.main_activity) { - - private lateinit var exportSmsButton: MaterialButton - private lateinit var setAsDefaultSmsButton: MaterialButton - private lateinit var clearDefaultSmsButton: MaterialButton - private lateinit var exportStatus: TextView - private lateinit var exportProgress: LinearProgressIndicator - private val disposables = CompositeDisposable() - - override fun onCreate(savedInstanceState: Bundle?) { - super.onCreate(savedInstanceState) - - exportSmsButton = findViewById(R.id.export_sms) - setAsDefaultSmsButton = findViewById(R.id.set_as_default_sms) - clearDefaultSmsButton = findViewById(R.id.clear_default_sms) - exportStatus = findViewById(R.id.export_status) - exportProgress = findViewById(R.id.export_progress) - - disposables += SmsExportService.progressState.onBackpressureLatest().subscribeOn(Schedulers.computation()).observeOn(AndroidSchedulers.mainThread()).subscribe { - when (it) { - is SmsExportProgress.Done -> { - exportStatus.text = "Done" - exportProgress.isVisible = true - } - is SmsExportProgress.InProgress -> { - exportStatus.text = "$it" - exportProgress.isVisible = true - exportProgress.progress = it.progress - exportProgress.max = it.total - } - SmsExportProgress.Init -> { - exportStatus.text = "Init" - exportProgress.isVisible = false - } - SmsExportProgress.Starting -> { - exportStatus.text = "Starting" - exportProgress.isVisible = true - } - } - } - - setAsDefaultSmsButton.setOnClickListener { - DefaultSmsHelper.becomeDefaultSms(this).either( - onFailure = { onAppIsIneligableForDefaultSmsSelection() }, - onSuccess = this::onStartActivityForDefaultSmsSelection - ) - } - - clearDefaultSmsButton.setOnClickListener { - DefaultSmsHelper.releaseDefaultSms(this).either( - onFailure = { - when (it) { - ReleaseSmsAppFailure.APP_IS_INELIGIBLE_TO_RELEASE_SMS_SELECTION -> onAppIsIneligibleForReleaseSmsSelection() - ReleaseSmsAppFailure.NO_METHOD_TO_RELEASE_SMS_AVIALABLE -> onNoMethodToReleaseSmsAvailable() - } - }, - onSuccess = this::onStartActivityForReleaseSmsSelection - ) - } - - exportSmsButton.setOnClickListener { - exportSmsButton.isEnabled = false - ContextCompat.startForegroundService(this, Intent(this, TestSmsExportService::class.java)) - } - - presentButtonState() - } - - override fun onResume() { - super.onResume() - presentButtonState() - } - - override fun onDestroy() { - super.onDestroy() - disposables.clear() - } - - private fun presentButtonState() { - setAsDefaultSmsButton.isVisible = !DefaultSmsHelper.isDefaultSms(this) - clearDefaultSmsButton.isVisible = DefaultSmsHelper.isDefaultSms(this) - exportSmsButton.isVisible = DefaultSmsHelper.isDefaultSms(this) - } - - override fun onActivityResult(requestCode: Int, resultCode: Int, data: Intent?) { - when (requestCode) { - 1 -> presentButtonState() - 2 -> presentButtonState() - else -> super.onActivityResult(requestCode, resultCode, data) - } - } - - private fun onStartActivityForDefaultSmsSelection(intent: Intent) { - startActivityForResult(intent, 1) - } - - private fun onAppIsIneligableForDefaultSmsSelection() { - if (DefaultSmsHelper.isDefaultSms(this)) { - Toast.makeText(this, "Already the SMS manager.", Toast.LENGTH_SHORT).show() - } else { - Toast.makeText(this, "Cannot be SMS manager.", Toast.LENGTH_SHORT).show() - } - } - - private fun onStartActivityForReleaseSmsSelection(intent: Intent) { - startActivityForResult(intent, 2) - } - - private fun onAppIsIneligibleForReleaseSmsSelection() { - if (!DefaultSmsHelper.isDefaultSms(this)) { - Toast.makeText(this, "Already not the SMS manager.", Toast.LENGTH_SHORT).show() - } else { - Toast.makeText(this, "Cannot be SMS manager.", Toast.LENGTH_SHORT).show() - } - } - - private fun onNoMethodToReleaseSmsAvailable() { - Toast.makeText(this, "Cannot automatically release sms. Display manual instructions.", Toast.LENGTH_SHORT).show() - } -} diff --git a/sms-exporter/app/src/main/java/org/signal/smsexporter/app/SendResponseViaMessageService.kt b/sms-exporter/app/src/main/java/org/signal/smsexporter/app/SendResponseViaMessageService.kt deleted file mode 100644 index 5857bab86c..0000000000 --- a/sms-exporter/app/src/main/java/org/signal/smsexporter/app/SendResponseViaMessageService.kt +++ /dev/null @@ -1,11 +0,0 @@ -package org.signal.smsexporter.app - -import android.app.Service -import android.content.Intent -import android.os.IBinder - -class SendResponseViaMessageService : Service() { - override fun onBind(intent: Intent?): IBinder? { - return null - } -} 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 deleted file mode 100644 index 94efeba52d..0000000000 --- a/sms-exporter/app/src/main/java/org/signal/smsexporter/app/TestSmsExportService.kt +++ /dev/null @@ -1,164 +0,0 @@ -package org.signal.smsexporter.app - -import androidx.core.app.NotificationChannelCompat -import androidx.core.app.NotificationCompat -import androidx.core.app.NotificationManagerCompat -import org.signal.core.util.logging.Log -import org.signal.smsexporter.ExportableMessage -import org.signal.smsexporter.SmsExportService -import org.signal.smsexporter.SmsExportState -import java.io.InputStream -import kotlin.time.Duration.Companion.seconds - -class TestSmsExportService : SmsExportService() { - - companion object { - private val TAG = Log.tag(TestSmsExportService::class.java) - - private const val NOTIFICATION_ID = 1234 - private const val NOTIFICATION_CHANNEL_ID = "sms_export" - - private const val startTime = 1659377120L - } - - override fun getNotification(progress: Int, total: Int): ExportNotification { - ensureNotificationChannel() - return ExportNotification( - id = NOTIFICATION_ID, - NotificationCompat.Builder(this, NOTIFICATION_CHANNEL_ID) - .setSmallIcon(R.drawable.ic_launcher_foreground) - .setContentTitle("Test Exporter") - .setProgress(total, progress, false) - .build() - ) - } - - override fun getExportCompleteNotification(): ExportNotification? { - return null - } - - override fun getUnexportedMessageCount(): Int { - return 50 - } - - override fun getUnexportedMessages(): Iterable { - return object : Iterable { - override fun iterator(): Iterator { - return ExportableMessageIterator(getUnexportedMessageCount()) - } - } - } - - override fun onMessageExportStarted(exportableMessage: ExportableMessage) { - Log.d(TAG, "onMessageExportStarted() called with: exportableMessage = $exportableMessage") - } - - override fun onMessageExportSucceeded(exportableMessage: ExportableMessage) { - Log.d(TAG, "onMessageExportSucceeded() called with: exportableMessage = $exportableMessage") - } - - override fun onMessageExportFailed(exportableMessage: ExportableMessage) { - Log.d(TAG, "onMessageExportFailed() called with: exportableMessage = $exportableMessage") - } - - override fun onMessageIdCreated(exportableMessage: ExportableMessage, messageId: Long) { - Log.d(TAG, "onMessageIdCreated() called with: exportableMessage = $exportableMessage, messageId = $messageId") - } - - override fun onAttachmentPartExportStarted(exportableMessage: ExportableMessage, part: ExportableMessage.Mms.Part) { - Log.d(TAG, "onAttachmentPartExportStarted() called with: exportableMessage = $exportableMessage, attachment = $part") - } - - override fun onAttachmentPartExportSucceeded(exportableMessage: ExportableMessage, part: ExportableMessage.Mms.Part) { - Log.d(TAG, "onAttachmentPartExportSucceeded() called with: exportableMessage = $exportableMessage, attachment = $part") - } - - override fun onAttachmentPartExportFailed(exportableMessage: ExportableMessage, part: ExportableMessage.Mms.Part) { - Log.d(TAG, "onAttachmentPartExportFailed() called with: exportableMessage = $exportableMessage, attachment = $part") - } - - override fun onRecipientExportStarted(exportableMessage: ExportableMessage, recipient: String) { - Log.d(TAG, "onRecipientExportStarted() called with: exportableMessage = $exportableMessage, recipient = $recipient") - } - - override fun onRecipientExportSucceeded(exportableMessage: ExportableMessage, recipient: String) { - Log.d(TAG, "onRecipientExportSucceeded() called with: exportableMessage = $exportableMessage, recipient = $recipient") - } - - override fun onRecipientExportFailed(exportableMessage: ExportableMessage, recipient: String) { - Log.d(TAG, "onRecipientExportFailed() called with: exportableMessage = $exportableMessage, recipient = $recipient") - } - - override fun getInputStream(part: ExportableMessage.Mms.Part): InputStream { - return BitmapGenerator.getStream() - } - - override fun onExportPassCompleted() { - Log.d(TAG, "onExportPassCompleted() called") - } - - private fun ensureNotificationChannel() { - val notificationManager = NotificationManagerCompat.from(this) - val channel = notificationManager.getNotificationChannel(NOTIFICATION_CHANNEL_ID) - if (channel == null) { - val newChannel = NotificationChannelCompat - .Builder(NOTIFICATION_CHANNEL_ID, NotificationManagerCompat.IMPORTANCE_DEFAULT) - .setName("misc") - .build() - - notificationManager.createNotificationChannel(newChannel) - } - } - - private class ExportableMessageIterator(private val size: Int) : Iterator { - private var emitted: Int = 0 - - override fun hasNext(): Boolean { - return emitted < size - } - - override fun next(): ExportableMessage { - val message = if (emitted % 2 == 0) { - getSmsMessage(emitted) - } else { - getMmsMessage(emitted) - } - - emitted++ - return message - } - - private fun getMmsMessage(it: Int): ExportableMessage.Mms<*> { - val me = "+15065550101" - val addresses = setOf(me, "+15065550102", "+15065550121") - val address = addresses.random() - return ExportableMessage.Mms( - id = "$it", - exportState = SmsExportState(), - addresses = addresses, - dateSent = (startTime + it - 1).seconds, - dateReceived = (startTime + it).seconds, - isRead = true, - isOutgoing = address == me, - sender = address, - parts = listOf( - ExportableMessage.Mms.Part.Text("Hello, $it from $address"), - ExportableMessage.Mms.Part.Stream("$it", "image/jpeg") - ) - ) - } - - private fun getSmsMessage(it: Int): ExportableMessage.Sms<*> { - return ExportableMessage.Sms( - id = it.toString(), - exportState = SmsExportState(), - address = "+15065550102", - body = "Hello, World! $it", - dateSent = (startTime + it - 1).seconds, - dateReceived = (startTime + it).seconds, - isRead = true, - isOutgoing = it % 4 == 0 - ) - } - } -} diff --git a/sms-exporter/app/src/main/res/drawable-v24/ic_launcher_foreground.xml b/sms-exporter/app/src/main/res/drawable-v24/ic_launcher_foreground.xml deleted file mode 100644 index 2b068d1146..0000000000 --- a/sms-exporter/app/src/main/res/drawable-v24/ic_launcher_foreground.xml +++ /dev/null @@ -1,30 +0,0 @@ - - - - - - - - - - - \ No newline at end of file diff --git a/sms-exporter/app/src/main/res/drawable/ic_launcher_background.xml b/sms-exporter/app/src/main/res/drawable/ic_launcher_background.xml deleted file mode 100644 index 07d5da9cbf..0000000000 --- a/sms-exporter/app/src/main/res/drawable/ic_launcher_background.xml +++ /dev/null @@ -1,170 +0,0 @@ - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - diff --git a/sms-exporter/app/src/main/res/layout/main_activity.xml b/sms-exporter/app/src/main/res/layout/main_activity.xml deleted file mode 100644 index 888c01c7d0..0000000000 --- a/sms-exporter/app/src/main/res/layout/main_activity.xml +++ /dev/null @@ -1,59 +0,0 @@ - - - - - - - - - - - - - - \ No newline at end of file diff --git a/sms-exporter/app/src/main/res/mipmap-anydpi-v26/ic_launcher.xml b/sms-exporter/app/src/main/res/mipmap-anydpi-v26/ic_launcher.xml deleted file mode 100644 index eca70cfe52..0000000000 --- a/sms-exporter/app/src/main/res/mipmap-anydpi-v26/ic_launcher.xml +++ /dev/null @@ -1,5 +0,0 @@ - - - - - \ No newline at end of file diff --git a/sms-exporter/app/src/main/res/mipmap-anydpi-v26/ic_launcher_round.xml b/sms-exporter/app/src/main/res/mipmap-anydpi-v26/ic_launcher_round.xml deleted file mode 100644 index eca70cfe52..0000000000 --- a/sms-exporter/app/src/main/res/mipmap-anydpi-v26/ic_launcher_round.xml +++ /dev/null @@ -1,5 +0,0 @@ - - - - - \ No newline at end of file diff --git a/sms-exporter/app/src/main/res/mipmap-hdpi/ic_launcher.webp b/sms-exporter/app/src/main/res/mipmap-hdpi/ic_launcher.webp deleted file mode 100644 index c209e78ecd372343283f4157dcfd918ec5165bb3..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 1404 zcmV-?1%vuhNk&F=1pok7MM6+kP&il$0000G0000-002h-06|PpNX!5L00Dqw+t%{r zzW2vH!KF=w&cMnnN@{whkTw+#mAh0SV?YL=)3MimFYCWp#fpdtz~8$hD5VPuQgtcN zXl<@<#Cme5f5yr2h%@8TWh?)bSK`O z^Z@d={gn7J{iyxL_y_%J|L>ep{dUxUP8a{byupH&!UNR*OutO~0{*T4q5R6@ApLF! z5{w?Z150gC7#>(VHFJZ-^6O@PYp{t!jH(_Z*nzTK4 zkc{fLE4Q3|mA2`CWQ3{8;gxGizgM!zccbdQoOLZc8hThi-IhN90RFT|zlxh3Ty&VG z?Fe{#9RrRnxzsu|Lg2ddugg7k%>0JeD+{XZ7>Z~{=|M+sh1MF7~ zz>To~`~LVQe1nNoR-gEzkpe{Ak^7{{ZBk2i_<+`Bq<^GB!RYG+z)h;Y3+<{zlMUYd zrd*W4w&jZ0%kBuDZ1EW&KLpyR7r2=}fF2%0VwHM4pUs}ZI2egi#DRMYZPek*^H9YK zay4Iy3WXFG(F14xYsoDA|KXgGc5%2DhmQ1gFCkrgHBm!lXG8I5h*uf{rn48Z!_@ z4Bk6TJAB2CKYqPjiX&mWoW>OPFGd$wqroa($ne7EUK;#3VYkXaew%Kh^3OrMhtjYN?XEoY`tRPQsAkH-DSL^QqyN0>^ zmC>{#F14jz4GeW{pJoRpLFa_*GI{?T93^rX7SPQgT@LbLqpNA}<@2wH;q493)G=1Y z#-sCiRNX~qf3KgiFzB3I>4Z%AfS(3$`-aMIBU+6?gbgDb!)L~A)je+;fR0jWLL-Fu z4)P{c7{B4Hp91&%??2$v9iRSFnuckHUm}or9seH6 z>%NbT+5*@L5(I9j@06@(!{ZI?U0=pKn8uwIg&L{JV14+8s2hnvbRrU|hZCd}IJu7*;;ECgO%8_*W Kmw_-CKmY()leWbG diff --git a/sms-exporter/app/src/main/res/mipmap-hdpi/ic_launcher_round.webp b/sms-exporter/app/src/main/res/mipmap-hdpi/ic_launcher_round.webp deleted file mode 100644 index b2dfe3d1ba5cf3ee31b3ecc1ced89044a1f3b7a9..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 2898 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 diff --git a/sms-exporter/app/src/main/res/mipmap-mdpi/ic_launcher.webp b/sms-exporter/app/src/main/res/mipmap-mdpi/ic_launcher.webp deleted file mode 100644 index 4f0f1d64e58ba64d180ce43ee13bf9a17835fbca..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 982 zcmV;{11bDcNk&G_0{{S5MM6+kP&il$0000G0000l001ul06|PpNU8t;00Dqo+t#w^ z^1csucXz7-Qrhzl9HuHB%l>&>1tG2^vb*E&k^T3$FG1eQZ51g$uv4V+kI`0<^1Z@N zk?Jjh$olyC%l>)Xq;7!>{iBj&BjJ`P&$fsCfpve_epJOBkTF?nu-B7D!hO=2ZR}

C%4 zc_9eOXvPbC4kzU8YowIA8cW~Uv|eB&yYwAObSwL2vY~UYI7NXPvf3b+c^?wcs~_t{ ze_m66-0)^{JdOMKPwjpQ@Sna!*?$wTZ~su*tNv7o!gXT!GRgivP}ec?5>l1!7<(rT zds|8x(qGc673zrvYIz;J23FG{9nHMnAuP}NpAED^laz3mAN1sy+NXK)!6v1FxQ;lh zOBLA>$~P3r4b*NcqR;y6pwyhZ3_PiDb|%n1gGjl3ZU}ujInlP{eks-#oA6>rh&g+!f`hv#_%JrgYPu z(U^&XLW^QX7F9Z*SRPpQl{B%x)_AMp^}_v~?j7 zapvHMKxSf*Mtyx8I}-<*UGn3)oHd(nn=)BZ`d$lDBwq_GL($_TPaS{UeevT(AJ`p0 z9%+hQb6z)U9qjbuXjg|dExCLjpS8$VKQ55VsIC%@{N5t{NsW)=hNGI`J=x97_kbz@ E0Of=7!TQj4N+cqN`nQhxvX7dAV-`K|Ub$-q+H-5I?Tx0g9jWxd@A|?POE8`3b8fO$T))xP* z(X?&brZw({`)WU&rdAs1iTa0x6F@PIxJ&&L|dpySV!ID|iUhjCcKz(@mE z!x@~W#3H<)4Ae(4eQJRk`Iz3<1)6^m)0b_4_TRZ+cz#eD3f8V;2r-1fE!F}W zEi0MEkTTx}8i1{`l_6vo0(Vuh0HD$I4SjZ=?^?k82R51bC)2D_{y8mi_?X^=U?2|F{Vr7s!k(AZC$O#ZMyavHhlQ7 zUR~QXuH~#o#>(b$u4?s~HLF*3IcF7023AlwAYudn0FV~|odGH^05AYPEfR)8p`i{n zwg3zPVp{+wOsxKc>)(pMupKF!Y2HoUqQ3|Yu|8lwR=?5zZuhG6J?H`bSNk_wPoM{u zSL{c@pY7+c2kck>`^q1^^gR0QB7Y?KUD{vz-uVX~;V-rW)PDcI)$_UjgVV?S?=oLR zf4}zz{#*R_{LkiJ#0RdQLNC^2Vp%JPEUvG9ra2BVZ92(p9h7Ka@!yf9(lj#}>+|u* z;^_?KWdzkM`6gqPo9;;r6&JEa)}R3X{(CWv?NvgLeOTq$cZXqf7|sPImi-7cS8DCN zGf;DVt3Am`>hH3{4-WzH43Ftx)SofNe^-#|0HdCo<+8Qs!}TZP{HH8~z5n`ExcHuT zDL1m&|DVpIy=xsLO>8k92HcmfSKhflQ0H~9=^-{#!I1g(;+44xw~=* zxvNz35vfsQE)@)Zsp*6_GjYD};Squ83<_?^SbALb{a`j<0Gn%6JY!zhp=Fg}Ga2|8 z52e1WU%^L1}15Ex0fF$e@eCT(()_P zvV?CA%#Sy08_U6VPt4EtmVQraWJX` zh=N|WQ>LgrvF~R&qOfB$!%D3cGv?;Xh_z$z7k&s4N)$WYf*k=|*jCEkO19{h_(%W4 zPuOqbCw`SeAX*R}UUsbVsgtuG?xs(#Ikx9`JZoQFz0n*7ZG@Fv@kZk`gzO$HoA9kN z8U5{-yY zvV{`&WKU2$mZeoBmiJrEdzUZAv1sRxpePdg1)F*X^Y)zp^Y*R;;z~vOv-z&)&G)JQ{m!C9cmziu1^nHA z`#`0c>@PnQ9CJKgC5NjJD8HM3|KC(g5nnCq$n0Gsu_DXk36@ql%npEye|?%RmG)

FJ$wK}0tWNB{uH;AM~i diff --git a/sms-exporter/app/src/main/res/mipmap-xhdpi/ic_launcher.webp b/sms-exporter/app/src/main/res/mipmap-xhdpi/ic_launcher.webp deleted file mode 100644 index 948a3070fe34c611c42c0d3ad3013a0dce358be0..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 1900 zcmV-y2b1_xNk&Fw2LJ$9MM6+kP&il$0000G0001A003VA06|PpNH75a00DqwTbm-~ zullQTcXxO9ki!OCRx^i?oR|n!<8G0=kI^!JSjFi-LL*`V;ET0H2IXfU0*i>o6o6Gy zRq6Ap5(_{XLdXcL-MzlN`ugSdZY_`jXhcENAu)N_0?GhF))9R;E`!bo9p?g?SRgw_ zEXHhFG$0{qYOqhdX<(wE4N@es3VIo$%il%6xP9gjiBri+2pI6aY4 zJbgh-Ud|V%3O!IcHKQx1FQH(_*TK;1>FQWbt^$K1zNn^cczkBs=QHCYZ8b&l!UV{K z{L0$KCf_&KR^}&2Fe|L&?1I7~pBENnCtCuH3sjcx6$c zwqkNkru);ie``q+_QI;IYLD9OV0ZxkuyBz|5<$1BH|vtey$> z5oto4=l-R-Aaq`Dk0}o9N0VrkqW_#;!u{!bJLDq%0092{Ghe=F;(kn} z+sQ@1=UlX30+2nWjkL$B^b!H2^QYO@iFc0{(-~yXj2TWz?VG{v`Jg zg}WyYnwGgn>{HFaG7E~pt=)sOO}*yd(UU-D(E&x{xKEl6OcU?pl)K%#U$dn1mDF19 zSw@l8G!GNFB3c3VVK0?uyqN&utT-D5%NM4g-3@Sii9tSXKtwce~uF zS&Jn746EW^wV~8zdQ1XC28~kXu8+Yo9p!<8h&(Q({J*4DBglPdpe4M_mD8AguZFn~ ztiuO~{6Bx?SfO~_ZV(GIboeR9~hAym{{fV|VM=77MxDrbW6`ujX z<3HF(>Zr;#*uCvC*bpoSr~C$h?_%nXps@A)=l_;({Fo#6Y1+Zv`!T5HB+)#^-Ud_; zBwftPN=d8Vx)*O1Mj+0oO=mZ+NVH*ptNDC-&zZ7Hwho6UQ#l-yNvc0Cm+2$$6YUk2D2t#vdZX-u3>-Be1u9gtTBiMB^xwWQ_rgvGpZ6(C@e23c!^K=>ai-Rqu zhqT`ZQof;9Bu!AD(i^PCbYV%yha9zuoKMp`U^z;3!+&d@Hud&_iy!O-$b9ZLcSRh? z)R|826w}TU!J#X6P%@Zh=La$I6zXa#h!B;{qfug}O%z@K{EZECu6zl)7CiNi%xti0 zB{OKfAj83~iJvmpTU|&q1^?^cIMn2RQ?jeSB95l}{DrEPTW{_gmU_pqTc)h@4T>~& zluq3)GM=xa(#^VU5}@FNqpc$?#SbVsX!~RH*5p0p@w z;~v{QMX0^bFT1!cXGM8K9FP+=9~-d~#TK#ZE{4umGT=;dfvWi?rYj;^l_Zxywze`W z^Cr{55U@*BalS}K%Czii_80e0#0#Zkhlij4-~I@}`-JFJ7$5{>LnoJSs??J8kWVl6|8A}RCGAu9^rAsfCE=2}tHwl93t0C?#+jMpvr7O3`2=tr{Hg$=HlnjVG^ewm|Js0J*kfPa6*GhtB>`fN!m#9J(sU!?(OSfzY*zS(FJ<-Vb zfAIg+`U)YaXv#sY(c--|X zEB+TVyZ%Ie4L$gi#Fc++`h6%vzsS$pjz9aLt+ZL(g;n$Dzy5=m=_TV(3H8^C{r0xd zp#a%}ht55dOq?yhwYPrtp-m1xXp;4X;)NhxxUpgP%XTLmO zcjaFva^}dP3$&sfFTIR_jC=2pHh9kpI@2(6V*GQo7Ws)`j)hd+tr@P~gR*2gO@+1? zG<`_tB+LJuF|SZ9tIec;h%}}6WClT`L>HSW?E{Hp1h^+mlbf_$9zA>!ug>NALJsO{ mU%z=YwVD?}XMya)Bp;vlyE5&E_6!fzx9pwrdz474!~g(M6R?N? diff --git a/sms-exporter/app/src/main/res/mipmap-xhdpi/ic_launcher_round.webp b/sms-exporter/app/src/main/res/mipmap-xhdpi/ic_launcher_round.webp deleted file mode 100644 index 1b9a6956b3acdc11f40ce2bb3f6efbd845cc243f..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 3918 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�(p-{x$&=IACIfoAR!za)F-^da-t3#0Dycnp zwO~NVXwXCl;jE<}>%@xz|=8fIJAB?>+E{7)|4l${4ngA3G|=r z2Dyv;VVWSgZx9Wj>qUjleGl3Ei9K4>h!(lPS%8VOG>Xu0%6VDz^O=bjJmuP7>DeUv zrbI}MlHB^^d?{zv6d=@_ZD2lg1&G7UjnVN{1}9WkaM3H~btX0GtSzB+tZ^qRgWo4m z!GmimlG$=wgXCnr6j@m<1gAL46#T~5Bnm=2{^@>|t&`9mkEPddj zAvG~@Tv~TAm2i%VW}R-g(Z0)z-Y|szHr@rk>4MAyG*Ma*7Yh#H7(!-5>DZ@8r;_dx z{prSe<>~099F8vsYd2xff7uAS%7{S)f(|@me3t2$iy&NEc7OUEchp@9A|X;;IA>8!oX+y(BKJ$EzV* znR$z;!L$s7uy@{OT~nG#B!NRraT8(X##Ho!0r_o@gg0CA-9H^;-uE&?$2$nHv_00o z%cbuUc-tCx$Uh&EZ4Nf4Zgqv)Y6>usG3>GeQnxx_Z6+PcbX-+ysbt1hQ`K1LDpOE? zrAhIZhSN9yVIAOa22gn577tbc&i3|3V8NWy&!tw##`}9*x}gtI^h1DzZRA>UuaJG) zaZ7j)dq!O}{?#8Y7~7i6fHh4{`pL?>-18|p!S75Y#^DM>-S3)vuZG+Q7l@ek zQP~#cBpWgg#mApc_sPYjpw8odQuRokmTkzcNl`^CcKB7e&;zViV;{Y{o^Y$%7i0m# z62%#1Lq!RC?}lK>%mp}T!3Xv;L*0v*>USLm``N%>w>@fwC+#T&Tx2bN4w(20JB}oU zuSa6v^kXi0xPs?pbaOHnyiqq6By1EZY9OZ^^QA>{q-Hsd&m`pbQ%8121aWG-F5xf zlZ%;B{;C>X19|`^_?dVyCq>n+41w7|!tUS!{9rHlbhX=SZO5CQ^;!Du_E7*`GiR^Q w)2!4MKjfSAeNo!9>IaV6aUZ*?W>} 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

UmY2RIYtNPVDpE$%vda+HD#3m z&VuXJ{BK&Qe+rBa7eq}Q(bq|tn(RrJAk|ztj2(i{d>nmQnM?;HF2k&9sA6up5tmjl z7lySlzMbifH17-m-Lwa_F&e7nOH?ESi3#ckR3tsM+jsck3`oG!uMS}|eAwVXv>}qxwq?QY%QJ0}r@^;fhuUA9W z*BVl>TGo&N004@xSiwDUXUvp51sVmqO3m)=B55aPwf@0=e}cN+$-BdKxY`YrT_4)0 z_d10#i44Q*rFr8MC>*)v$EJvz``(pb{e&*6k+b zsMz%($|1+8hn8c2?P(l@;Rb&CsZeYoCI3?2!LqjbwPXW3z4G$Qfj=cT5Yb%vY0(AX oeb?AaKtwrnc|$|zzw9vfvn^aJJ!zd)XFXqqy0000001=f@-~a#s diff --git a/sms-exporter/app/src/main/res/values/colors.xml b/sms-exporter/app/src/main/res/values/colors.xml deleted file mode 100644 index d2cd14a9ff..0000000000 --- a/sms-exporter/app/src/main/res/values/colors.xml +++ /dev/null @@ -1,6 +0,0 @@ - - - #008577 - #00574B - #D81B60 - \ No newline at end of file diff --git a/sms-exporter/app/src/main/res/values/strings.xml b/sms-exporter/app/src/main/res/values/strings.xml deleted file mode 100644 index 8e85f3485c..0000000000 --- a/sms-exporter/app/src/main/res/values/strings.xml +++ /dev/null @@ -1,6 +0,0 @@ - - Sms Exporter Test App - Set as default SMS app - Clear default SMS app - Export SMS - diff --git a/sms-exporter/app/src/main/res/values/themes.xml b/sms-exporter/app/src/main/res/values/themes.xml deleted file mode 100644 index ab7e2b3f8f..0000000000 --- a/sms-exporter/app/src/main/res/values/themes.xml +++ /dev/null @@ -1,9 +0,0 @@ - - - - \ No newline at end of file diff --git a/sms-exporter/lib/build.gradle b/sms-exporter/lib/build.gradle deleted file mode 100644 index 98bd33b35b..0000000000 --- a/sms-exporter/lib/build.gradle +++ /dev/null @@ -1,14 +0,0 @@ -plugins { - id("signal-library") -} - -android { - namespace = "org.signal.smsexporter" -} - -dependencies { - implementation(project(":core-util")) - - implementation(libs.androidx.core.role) - implementation(libs.android.smsmms) -} \ No newline at end of file diff --git a/sms-exporter/lib/consumer-rules.pro b/sms-exporter/lib/consumer-rules.pro deleted file mode 100644 index e69de29bb2..0000000000 diff --git a/sms-exporter/lib/proguard-rules.pro b/sms-exporter/lib/proguard-rules.pro deleted file mode 100644 index 481bb43481..0000000000 --- a/sms-exporter/lib/proguard-rules.pro +++ /dev/null @@ -1,21 +0,0 @@ -# Add project specific ProGuard rules here. -# You can control the set of applied configuration files using the -# proguardFiles setting in build.gradle. -# -# For more details, see -# http://developer.android.com/guide/developing/tools/proguard.html - -# If your project uses WebView with JS, uncomment the following -# and specify the fully qualified class name to the JavaScript interface -# class: -#-keepclassmembers class fqcn.of.javascript.interface.for.webview { -# public *; -#} - -# Uncomment this to preserve the line number information for -# debugging stack traces. -#-keepattributes SourceFile,LineNumberTable - -# If you keep the line number information, uncomment this to -# hide the original source file name. -#-renamesourcefileattribute SourceFile \ No newline at end of file diff --git a/sms-exporter/lib/src/main/AndroidManifest.xml b/sms-exporter/lib/src/main/AndroidManifest.xml deleted file mode 100644 index 8bdb7e14b3..0000000000 --- a/sms-exporter/lib/src/main/AndroidManifest.xml +++ /dev/null @@ -1,4 +0,0 @@ - - - - diff --git a/sms-exporter/lib/src/main/java/org/signal/smsexporter/BecomeSmsAppFailure.kt b/sms-exporter/lib/src/main/java/org/signal/smsexporter/BecomeSmsAppFailure.kt deleted file mode 100644 index 9d6ecd0a64..0000000000 --- a/sms-exporter/lib/src/main/java/org/signal/smsexporter/BecomeSmsAppFailure.kt +++ /dev/null @@ -1,13 +0,0 @@ -package org.signal.smsexporter - -enum class BecomeSmsAppFailure { - /** - * Already the default sms app - */ - ALREADY_DEFAULT_SMS, - - /** - * The system doesn't think we are allowed to become the sms app - */ - ROLE_IS_NOT_AVAILABLE -} diff --git a/sms-exporter/lib/src/main/java/org/signal/smsexporter/DefaultSmsHelper.kt b/sms-exporter/lib/src/main/java/org/signal/smsexporter/DefaultSmsHelper.kt deleted file mode 100644 index d6413aae0d..0000000000 --- a/sms-exporter/lib/src/main/java/org/signal/smsexporter/DefaultSmsHelper.kt +++ /dev/null @@ -1,26 +0,0 @@ -package org.signal.smsexporter - -import android.content.Context -import org.signal.smsexporter.internal.BecomeDefaultSmsUseCase -import org.signal.smsexporter.internal.IsDefaultSms -import org.signal.smsexporter.internal.ReleaseDefaultSmsUseCase - -/** - * Basic API for checking / becoming / releasing default SMS - */ -object DefaultSmsHelper { - /** - * Checks whether this app is currently the default SMS app - */ - fun isDefaultSms(context: Context) = IsDefaultSms.checkIsDefaultSms(context) - - /** - * Attempts to get an Intent which can be launched to become the default SMS app - */ - fun becomeDefaultSms(context: Context) = BecomeDefaultSmsUseCase.execute(context) - - /** - * Attempts to get an Intent which can be launched to relinquish the role of default SMS app - */ - fun releaseDefaultSms(context: Context) = ReleaseDefaultSmsUseCase.execute(context) -} 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 deleted file mode 100644 index ca432f8868..0000000000 --- a/sms-exporter/lib/src/main/java/org/signal/smsexporter/ExportableMessage.kt +++ /dev/null @@ -1,69 +0,0 @@ -package org.signal.smsexporter - -import kotlin.time.Duration - -/** - * Represents an exportable MMS or SMS message - */ -sealed interface ExportableMessage { - - /** - * This represents the initial exportState of the message, and it is *not* updated as - * the message moves through processing. - */ - val exportState: SmsExportState - - /** - * An exportable SMS message - */ - data class Sms( - val id: ID, - override val exportState: SmsExportState, - val address: String, - val dateReceived: Duration, - val dateSent: Duration, - val isRead: Boolean, - val isOutgoing: Boolean, - val body: String - ) : ExportableMessage - - /** - * An exportable MMS message - */ - data class Mms( - val id: ID, - override val exportState: SmsExportState, - val addresses: Set, - val dateReceived: Duration, - val dateSent: Duration, - val isRead: Boolean, - val isOutgoing: Boolean, - val parts: List, - val sender: CharSequence - ) : ExportableMessage { - /** - * An attachment, attached to an MMS message - */ - sealed interface Part { - - val contentType: String - val contentId: String - - data class Text(val text: String) : Part { - override val contentType: String = "text/plain" - override val contentId: String = "text" - } - data class Stream( - val id: String, - override val contentType: String - ) : Part { - override val contentId: String = id - } - } - } - - data class Skip( - val id: ID, - override val exportState: SmsExportState = SmsExportState() - ) : ExportableMessage -} diff --git a/sms-exporter/lib/src/main/java/org/signal/smsexporter/ReleaseSmsAppFailure.kt b/sms-exporter/lib/src/main/java/org/signal/smsexporter/ReleaseSmsAppFailure.kt deleted file mode 100644 index 80809ed150..0000000000 --- a/sms-exporter/lib/src/main/java/org/signal/smsexporter/ReleaseSmsAppFailure.kt +++ /dev/null @@ -1,13 +0,0 @@ -package org.signal.smsexporter - -enum class ReleaseSmsAppFailure { - /** - * Occurs when we are not the default sms app - */ - APP_IS_INELIGIBLE_TO_RELEASE_SMS_SELECTION, - - /** - * No good way to release sms. Have to instruct user manually. - */ - NO_METHOD_TO_RELEASE_SMS_AVIALABLE -} diff --git a/sms-exporter/lib/src/main/java/org/signal/smsexporter/SmsExportProgress.kt b/sms-exporter/lib/src/main/java/org/signal/smsexporter/SmsExportProgress.kt deleted file mode 100644 index c23050a766..0000000000 --- a/sms-exporter/lib/src/main/java/org/signal/smsexporter/SmsExportProgress.kt +++ /dev/null @@ -1,30 +0,0 @@ -package org.signal.smsexporter - -/** - * Expresses the current progress of SMS exporting. - */ -sealed class SmsExportProgress { - /** - * Have not started yet. - */ - object Init : SmsExportProgress() - - /** - * Starting up and about to start processing messages - */ - object Starting : SmsExportProgress() - - /** - * Processing messages - */ - data class InProgress( - val progress: Int, - val errorCount: Int, - val total: Int - ) : SmsExportProgress() - - /** - * All done. - */ - data class Done(val errorCount: Int, val total: Int) : SmsExportProgress() -} 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 deleted file mode 100644 index f2b0ebe2fd..0000000000 --- a/sms-exporter/lib/src/main/java/org/signal/smsexporter/SmsExportService.kt +++ /dev/null @@ -1,367 +0,0 @@ -package org.signal.smsexporter - -import android.annotation.SuppressLint -import android.app.Notification -import android.app.Service -import android.content.Intent -import android.os.IBinder -import androidx.core.app.NotificationManagerCompat -import io.reactivex.rxjava3.processors.BehaviorProcessor -import org.signal.core.util.Result -import org.signal.core.util.Try -import org.signal.core.util.logging.Log -import org.signal.smsexporter.internal.mms.ExportMmsMessagesUseCase -import org.signal.smsexporter.internal.mms.ExportMmsPartsUseCase -import org.signal.smsexporter.internal.mms.ExportMmsRecipientsUseCase -import org.signal.smsexporter.internal.mms.GetOrCreateMmsThreadIdsUseCase -import org.signal.smsexporter.internal.sms.ExportSmsMessagesUseCase -import java.io.EOFException -import java.io.FileNotFoundException -import java.io.InputStream -import java.util.concurrent.Executor -import java.util.concurrent.Executors - -/** - * Exports SMS and MMS messages to the system database. - */ -abstract class SmsExportService : Service() { - - companion object { - 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? { - return null - } - - private val threadCache: MutableMap, Long> = mutableMapOf() - private var isStarted = false - - override fun onStartCommand(intent: Intent?, flags: Int, startId: Int): Int { - Log.d(TAG, "Got start command in SMS Export Service") - - startExport(intent?.getBooleanExtra(CLEAR_PREVIOUS_EXPORT_STATE_EXTRA, false) ?: false) - - return START_NOT_STICKY - } - - @SuppressLint("MissingPermission") - private fun startExport(clearExportState: Boolean) { - if (isStarted) { - Log.d(TAG, "Already running exporter.") - return - } - - Log.d(TAG, "Running export clearExportState: $clearExportState") - - isStarted = true - updateNotification(-1, -1) - progressState.onNext(SmsExportProgress.Starting) - - var progress = 0 - var errorCount = 0 - executor.execute { - if (clearExportState) { - clearPreviousExportState() - } - - prepareForExport() - val totalCount = getUnexportedMessageCount() - getUnexportedMessages().forEach { message -> - val exportState = message.exportState - if (exportState.progress != SmsExportState.Progress.COMPLETED) { - val successful = when (message) { - is ExportableMessage.Sms<*> -> exportSms(exportState, message) - is ExportableMessage.Mms<*> -> exportMms(exportState, message) - is ExportableMessage.Skip<*> -> { - onMessageExportSucceeded(message) - true - } - } - - if (!successful) { - errorCount++ - } - - progress++ - if (progress == 1 || progress.mod(100) == 0) { - updateNotification(progress, totalCount) - } - progressState.onNext(SmsExportProgress.InProgress(progress, errorCount, totalCount)) - } - } - - onExportPassCompleted() - progressState.onNext(SmsExportProgress.Done(errorCount, progress)) - - getExportCompleteNotification()?.let { notification -> - NotificationManagerCompat.from(this).notify(notification.id, notification.notification) - } - - Log.d(TAG, "Export complete") - - stopForeground(true) - stopSelf() - isStarted = false - } - } - - /** - * The executor that this service should do its work on. - */ - protected open val executor: Executor = Executors.newSingleThreadExecutor() - - /** - * Produces the notification and notification id to display for this foreground service. - * The progress and total represent how many messages we've processed, and how many total - * we have to process. Failures and successes are both aggregated in this progress. You can - * query for "failure" state *after* we signal completion of a run. - */ - protected abstract fun getNotification(progress: Int, total: Int): ExportNotification - - /** - * Produces the notification and notification id to display when the export is complete. - * - * Can be null if no notification is needed (e.g., the user is still in the app) - */ - protected abstract fun getExportCompleteNotification(): ExportNotification? - - /** - * 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 - - /** - * Gets the total number of messages to process. This is only used for the notification and - * progress events. - */ - protected abstract fun getUnexportedMessageCount(): Int - - /** - * Gets an iterable of exportable messages. - */ - protected abstract fun getUnexportedMessages(): Iterable - - /** - * We've started the export process for a given MMS / SMS message - */ - protected abstract fun onMessageExportStarted(exportableMessage: ExportableMessage) - - /** - * We've completely succeeded exporting a given MMS / SMS message. This is only - * called when all parts of the message (including recipients and attachments) have - * been completely exported. - */ - protected abstract fun onMessageExportSucceeded(exportableMessage: ExportableMessage) - - /** - * We've failed to completely export a given MMS / SMS message - */ - protected abstract fun onMessageExportFailed(exportableMessage: ExportableMessage) - - /** - * We've written the message contents to the system database and were handed back an id. - */ - protected abstract fun onMessageIdCreated(exportableMessage: ExportableMessage, messageId: Long) - - /** - * We've begun trying to export a part row for an attachment for the given message - */ - protected abstract fun onAttachmentPartExportStarted(exportableMessage: ExportableMessage, part: ExportableMessage.Mms.Part) - - /** - * We've successfully exported the attachment part for a given message and written the - * attachment file to the local filesystem. - */ - protected abstract fun onAttachmentPartExportSucceeded(exportableMessage: ExportableMessage, part: ExportableMessage.Mms.Part) - - /** - * We failed to export the attachment part for a given message. - */ - protected abstract fun onAttachmentPartExportFailed(exportableMessage: ExportableMessage, part: ExportableMessage.Mms.Part) - - /** - * We've begun trying to export a recipient addr for a given message - */ - protected abstract fun onRecipientExportStarted(exportableMessage: ExportableMessage, recipient: String) - - /** - * We've successfully exported a recipient addr for a given message - */ - protected abstract fun onRecipientExportSucceeded(exportableMessage: ExportableMessage, recipient: String) - - /** - * We've failed to export a recipient addr for a given message - */ - protected abstract fun onRecipientExportFailed(exportableMessage: ExportableMessage, recipient: String) - - /** - * Gets the input stream for the given attachment, so that it might be written out to disk. - */ - protected abstract fun getInputStream(part: ExportableMessage.Mms.Part): InputStream - - /** - * Called when an export pass completes. It is up to the implementation to determine whether - * there are still messages to export. This is where the system could initiate a multiple-pass - * system to ensure all messages are exported, though an approach like this can have data races - * and other pitfalls. - */ - protected abstract fun onExportPassCompleted() - - private fun updateNotification(progress: Int, total: Int) { - val exportNotification = getNotification(progress, total) - startForeground(exportNotification.id, exportNotification.notification) - } - - private fun exportSms(smsExportState: SmsExportState, sms: ExportableMessage.Sms<*>): Boolean { - onMessageExportStarted(sms) - val mayAlreadyExist = smsExportState.progress == SmsExportState.Progress.STARTED - return ExportSmsMessagesUseCase.execute(this, sms, mayAlreadyExist).either(onSuccess = { - onMessageExportSucceeded(sms) - true - }, onFailure = { - onMessageExportFailed(sms) - false - }) - } - - private fun exportMms(smsExportState: SmsExportState, mms: ExportableMessage.Mms<*>): Boolean { - onMessageExportStarted(mms) - val threadIdOutput: GetOrCreateMmsThreadIdsUseCase.Output? = getThreadId(mms) - val exportMmsOutput: ExportMmsMessagesUseCase.Output? = threadIdOutput?.let { exportMms(smsExportState, it) } - val exportMmsPartsOutput: List? = exportMmsOutput?.let { exportMmsParts(smsExportState, it) } - val writeMmsPartsOutput: List>? = exportMmsPartsOutput?.filterNotNull()?.map { writeAttachmentToDisk(smsExportState, it) } - val exportMmsRecipients: List? = exportMmsOutput?.let { exportMmsRecipients(smsExportState, it) } - - return if (threadIdOutput != null && - exportMmsOutput != null && - exportMmsPartsOutput != null && !exportMmsPartsOutput.contains(null) && - writeMmsPartsOutput != null && writeMmsPartsOutput.all { it is Result.Success || (it is Result.Failure && (it.failure.cause ?: it.failure) is FileNotFoundException) } && - exportMmsRecipients != null && !exportMmsRecipients.contains(null) - ) { - onMessageExportSucceeded(mms) - true - } else { - onMessageExportFailed(mms) - false - } - } - - private fun getThreadId(mms: ExportableMessage.Mms<*>): GetOrCreateMmsThreadIdsUseCase.Output? { - return GetOrCreateMmsThreadIdsUseCase.execute(this, mms, threadCache).either( - onSuccess = { output -> - output - }, - onFailure = { - Log.w(TAG, "Failed to get thread id for export", it) - null - } - ) - } - - private fun exportMms(smsExportState: SmsExportState, threadIdOutput: GetOrCreateMmsThreadIdsUseCase.Output): ExportMmsMessagesUseCase.Output? { - return ExportMmsMessagesUseCase.execute(this, threadIdOutput, smsExportState.progress == SmsExportState.Progress.STARTED).either( - onSuccess = { - onMessageIdCreated(it.mms, it.messageId) - it - }, - onFailure = { - Log.w(TAG, "Failed to export MMS into system database", it) - null - } - ) - } - - private fun exportMmsParts(smsExportState: SmsExportState, exportMmsOutput: ExportMmsMessagesUseCase.Output): List { - val attachments = exportMmsOutput.mms.parts - return if (attachments.isEmpty()) { - emptyList() - } else { - attachments.filterNot { it.contentId in smsExportState.completedAttachments }.map { attachment -> - onAttachmentPartExportStarted(exportMmsOutput.mms, attachment) - ExportMmsPartsUseCase.execute(this, attachment, exportMmsOutput, smsExportState.startedAttachments.contains(attachment.contentId)).either( - onSuccess = { - it - }, - onFailure = { - onAttachmentPartExportFailed(exportMmsOutput.mms, attachment) - Log.d(TAG, "Could not export MMS Part", it) - null - } - ) - } - } - } - - private fun exportMmsRecipients(smsExportState: SmsExportState, exportMmsOutput: ExportMmsMessagesUseCase.Output): List { - val recipients = exportMmsOutput.mms.addresses.map { it }.toSet() - return if (recipients.isEmpty()) { - emptyList() - } else { - recipients.filterNot { it in smsExportState.completedRecipients }.map { recipient -> - onRecipientExportStarted(exportMmsOutput.mms, recipient) - ExportMmsRecipientsUseCase.execute(this, exportMmsOutput.messageId, recipient, exportMmsOutput.mms.sender.toString(), smsExportState.startedRecipients.contains(recipient)).either( - onSuccess = { - onRecipientExportSucceeded(exportMmsOutput.mms, recipient) - }, - onFailure = { - onRecipientExportFailed(exportMmsOutput.mms, recipient) - Log.w(TAG, "Failed to export MMS Recipient", it) - null - } - ) - } - } - } - - private fun writeAttachmentToDisk(smsExportState: SmsExportState, output: ExportMmsPartsUseCase.Output): Try { - if (output.part.contentId in smsExportState.completedAttachments) { - return Try.success(Unit) - } - - if (output.part is ExportableMessage.Mms.Part.Text) { - onAttachmentPartExportSucceeded(output.message, output.part) - return Try.success(Unit) - } - - return try { - contentResolver.openOutputStream(output.uri)!!.use { out -> - getInputStream(output.part).use { - it.copyTo(out) - } - } - - onAttachmentPartExportSucceeded(output.message, output.part) - Try.success(Unit) - } catch (e: Exception) { - if (e is EOFException) { - Log.d(TAG, "Unrecoverable failure to write attachment to disk, marking as successful and moving on", e) - onAttachmentPartExportSucceeded(output.message, output.part) - Try.success(Unit) - } else { - Log.d(TAG, "Failed to write attachment to disk.", e) - Try.failure(e) - } - } - } - - data class ExportNotification( - val id: Int, - val notification: Notification - ) -} diff --git a/sms-exporter/lib/src/main/java/org/signal/smsexporter/SmsExportState.kt b/sms-exporter/lib/src/main/java/org/signal/smsexporter/SmsExportState.kt deleted file mode 100644 index 853657eac2..0000000000 --- a/sms-exporter/lib/src/main/java/org/signal/smsexporter/SmsExportState.kt +++ /dev/null @@ -1,20 +0,0 @@ -package org.signal.smsexporter - -/** - * Describes the current "Export State" of a given message. This should be updated - * by and persisted by the application whenever a state change occurs. - */ -data class SmsExportState( - val messageId: Long = -1L, - val startedRecipients: Set = emptySet(), - val completedRecipients: Set = emptySet(), - val startedAttachments: Set = emptySet(), - val completedAttachments: Set = emptySet(), - val progress: Progress = Progress.INIT -) { - enum class Progress { - INIT, - STARTED, - COMPLETED - } -} diff --git a/sms-exporter/lib/src/main/java/org/signal/smsexporter/internal/BecomeDefaultSmsUseCase.kt b/sms-exporter/lib/src/main/java/org/signal/smsexporter/internal/BecomeDefaultSmsUseCase.kt deleted file mode 100644 index b6a55968f1..0000000000 --- a/sms-exporter/lib/src/main/java/org/signal/smsexporter/internal/BecomeDefaultSmsUseCase.kt +++ /dev/null @@ -1,36 +0,0 @@ -package org.signal.smsexporter.internal - -import android.app.role.RoleManager -import android.content.Context -import android.content.Intent -import android.os.Build -import android.provider.Telephony -import androidx.core.role.RoleManagerCompat -import org.signal.core.util.Result -import org.signal.smsexporter.BecomeSmsAppFailure - -/** - * Requests that this app becomes the default SMS app. The exact UX here is - * API dependant. - * - * Returns an intent to fire for a result, or a Failure. - */ -internal object BecomeDefaultSmsUseCase { - fun execute(context: Context): Result { - return if (IsDefaultSms.checkIsDefaultSms(context)) { - Result.failure(BecomeSmsAppFailure.ALREADY_DEFAULT_SMS) - } else if (Build.VERSION.SDK_INT >= 29) { - val roleManager = context.getSystemService(RoleManager::class.java) - if (roleManager.isRoleAvailable(RoleManagerCompat.ROLE_SMS)) { - Result.success(roleManager.createRequestRoleIntent(RoleManagerCompat.ROLE_SMS)) - } else { - Result.failure(BecomeSmsAppFailure.ROLE_IS_NOT_AVAILABLE) - } - } else { - Result.success( - Intent(Telephony.Sms.Intents.ACTION_CHANGE_DEFAULT) - .putExtra(Telephony.Sms.Intents.EXTRA_PACKAGE_NAME, context.packageName) - ) - } - } -} diff --git a/sms-exporter/lib/src/main/java/org/signal/smsexporter/internal/IsDefaultSms.kt b/sms-exporter/lib/src/main/java/org/signal/smsexporter/internal/IsDefaultSms.kt deleted file mode 100644 index 1622f36c8e..0000000000 --- a/sms-exporter/lib/src/main/java/org/signal/smsexporter/internal/IsDefaultSms.kt +++ /dev/null @@ -1,20 +0,0 @@ -package org.signal.smsexporter.internal - -import android.app.role.RoleManager -import android.content.Context -import android.os.Build -import android.provider.Telephony -import androidx.core.role.RoleManagerCompat - -/** - * Uses the appropriate service to check if we are the default sms - */ -internal object IsDefaultSms { - fun checkIsDefaultSms(context: Context): Boolean { - return if (Build.VERSION.SDK_INT >= 29) { - context.getSystemService(RoleManager::class.java).isRoleHeld(RoleManagerCompat.ROLE_SMS) - } else { - context.packageName == Telephony.Sms.getDefaultSmsPackage(context) - } - } -} diff --git a/sms-exporter/lib/src/main/java/org/signal/smsexporter/internal/ReleaseDefaultSmsUseCase.kt b/sms-exporter/lib/src/main/java/org/signal/smsexporter/internal/ReleaseDefaultSmsUseCase.kt deleted file mode 100644 index 2d5406ef54..0000000000 --- a/sms-exporter/lib/src/main/java/org/signal/smsexporter/internal/ReleaseDefaultSmsUseCase.kt +++ /dev/null @@ -1,31 +0,0 @@ -package org.signal.smsexporter.internal - -import android.content.Context -import android.content.Intent -import android.os.Build -import android.provider.Settings -import org.signal.core.util.Result -import org.signal.smsexporter.ReleaseSmsAppFailure - -/** - * Request to no longer be the default SMS app. This has a pretty bad UX, we need - * to get the user to manually do it in settings. On API 24+ we can launch the default - * app settings screen, whereas on 19 to 23, we can't. In this situation, we should - * display some UX (perhaps based off API level) explaining to the user exactly what to - * do. - * - * Returns the Intent to fire off, or a Failure. - */ -internal object ReleaseDefaultSmsUseCase { - fun execute(context: Context): Result { - return if (!IsDefaultSms.checkIsDefaultSms(context)) { - Result.failure(ReleaseSmsAppFailure.APP_IS_INELIGIBLE_TO_RELEASE_SMS_SELECTION) - } else if (Build.VERSION.SDK_INT >= 24) { - Result.success( - Intent(Settings.ACTION_MANAGE_DEFAULT_APPS_SETTINGS) - ) - } else { - Result.failure(ReleaseSmsAppFailure.NO_METHOD_TO_RELEASE_SMS_AVIALABLE) - } - } -} 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 deleted file mode 100644 index 37795e4346..0000000000 --- a/sms-exporter/lib/src/main/java/org/signal/smsexporter/internal/mms/ExportMmsMessagesUseCase.kt +++ /dev/null @@ -1,88 +0,0 @@ -package org.signal.smsexporter.internal.mms - -import android.content.ContentUris -import android.content.Context -import android.provider.Telephony -import androidx.core.content.contentValuesOf -import com.google.android.mms.pdu_alt.PduHeaders -import org.signal.core.util.Try -import org.signal.core.util.logging.Log -import org.signal.smsexporter.ExportableMessage - -/** - * Takes a list of messages and inserts them as a single batch. This includes - * thread id get/create if necessary. The output is a list of (mms, message_id) - */ -internal object ExportMmsMessagesUseCase { - - private val TAG = Log.tag(ExportMmsMessagesUseCase::class.java) - - internal fun getTransactionId(mms: ExportableMessage.Mms<*>): String { - return "signal:T${mms.id}" - } - - fun execute( - context: Context, - getOrCreateThreadOutput: GetOrCreateMmsThreadIdsUseCase.Output, - checkForExistence: Boolean - ): Try { - try { - val (mms, threadId) = getOrCreateThreadOutput - val transactionId = getTransactionId(mms) - - if (checkForExistence) { - Log.d(TAG, "Checking if the message is already in the database.") - val messageId = isMessageAlreadyInDatabase(context, transactionId) - if (messageId != -1L) { - Log.d(TAG, "Message exists in database. Returning its id.") - return Try.success(Output(mms, messageId)) - } - } - - val mmsContentValues = contentValuesOf( - Telephony.Mms.THREAD_ID to threadId, - Telephony.Mms.DATE to mms.dateReceived.inWholeSeconds, - Telephony.Mms.DATE_SENT to mms.dateSent.inWholeSeconds, - Telephony.Mms.MESSAGE_BOX to if (mms.isOutgoing) Telephony.Mms.MESSAGE_BOX_SENT else Telephony.Mms.MESSAGE_BOX_INBOX, - Telephony.Mms.READ to if (mms.isRead) 1 else 0, - Telephony.Mms.CONTENT_TYPE to "application/vnd.wap.multipart.related", - Telephony.Mms.MESSAGE_TYPE to PduHeaders.MESSAGE_TYPE_SEND_REQ, - Telephony.Mms.MMS_VERSION to PduHeaders.MMS_VERSION_1_3, - Telephony.Mms.MESSAGE_CLASS to "personal", - Telephony.Mms.PRIORITY to PduHeaders.PRIORITY_NORMAL, - Telephony.Mms.TRANSACTION_ID to transactionId, - Telephony.Mms.RESPONSE_STATUS to PduHeaders.RESPONSE_STATUS_OK, - Telephony.Mms.SEEN to 1, - Telephony.Mms.TEXT_ONLY to if (mms.parts.all { it is ExportableMessage.Mms.Part.Text }) 1 else 0 - ) - - val uri = context.contentResolver.insert(Telephony.Mms.CONTENT_URI, mmsContentValues) - val newMessageId = ContentUris.parseId(uri!!) - - return Try.success(Output(getOrCreateThreadOutput.mms, newMessageId)) - } catch (e: Exception) { - return Try.failure(e) - } - } - - private fun isMessageAlreadyInDatabase(context: Context, transactionId: String): Long { - return context.contentResolver.query( - Telephony.Mms.CONTENT_URI, - arrayOf("_id"), - "${Telephony.Mms.TRANSACTION_ID} == ?", - arrayOf(transactionId), - null - )?.use { - if (it.moveToFirst()) { - it.getLong(0) - } else { - -1L - } - } ?: -1L - } - - data class Output( - val mms: ExportableMessage.Mms<*>, - val messageId: Long - ) -} diff --git a/sms-exporter/lib/src/main/java/org/signal/smsexporter/internal/mms/ExportMmsPartsUseCase.kt b/sms-exporter/lib/src/main/java/org/signal/smsexporter/internal/mms/ExportMmsPartsUseCase.kt deleted file mode 100644 index 999cfc4abf..0000000000 --- a/sms-exporter/lib/src/main/java/org/signal/smsexporter/internal/mms/ExportMmsPartsUseCase.kt +++ /dev/null @@ -1,67 +0,0 @@ -package org.signal.smsexporter.internal.mms - -import android.content.ContentUris -import android.content.Context -import android.net.Uri -import android.provider.Telephony -import androidx.core.content.contentValuesOf -import org.signal.core.util.Try -import org.signal.core.util.logging.Log -import org.signal.smsexporter.ExportableMessage - -/** - * Inserts the part objects for the given list of mms message insertion outputs. Returns a list - * of attachments that can be enqueued for a disk write. - */ -internal object ExportMmsPartsUseCase { - - private val TAG = Log.tag(ExportMmsPartsUseCase::class.java) - - internal fun getContentId(part: ExportableMessage.Mms.Part): String { - return "" - } - - fun execute(context: Context, part: ExportableMessage.Mms.Part, output: ExportMmsMessagesUseCase.Output, checkForExistence: Boolean): Try { - try { - val (message, messageId) = output - val contentId = getContentId(part) - val mmsPartUri = Telephony.Mms.CONTENT_URI.buildUpon().appendPath(messageId.toString()).appendPath("part").build() - - if (checkForExistence) { - Log.d(TAG, "Checking attachment that may already be present...") - val partId: Long? = context.contentResolver.query(mmsPartUri, arrayOf(Telephony.Mms.Part._ID), "${Telephony.Mms.Part.CONTENT_ID} = ?", arrayOf(contentId), null)?.use { - if (it.moveToFirst()) { - it.getLong(0) - } else { - null - } - } - - if (partId != null) { - Log.d(TAG, "Found attachment part that already exists.") - return Try.success( - Output( - uri = ContentUris.withAppendedId(mmsPartUri, partId), - part = part, - message = message - ) - ) - } - } - - val mmsPartContentValues = contentValuesOf( - Telephony.Mms.Part.MSG_ID to messageId, - Telephony.Mms.Part.CONTENT_TYPE to part.contentType, - Telephony.Mms.Part.CONTENT_ID to contentId, - Telephony.Mms.Part.TEXT to if (part is ExportableMessage.Mms.Part.Text) part.text else null - ) - - val attachmentUri = context.contentResolver.insert(mmsPartUri, mmsPartContentValues)!! - return Try.success(Output(attachmentUri, part, message)) - } catch (e: Exception) { - return Try.failure(e) - } - } - - data class Output(val uri: Uri, val part: ExportableMessage.Mms.Part, val message: ExportableMessage) -} diff --git a/sms-exporter/lib/src/main/java/org/signal/smsexporter/internal/mms/ExportMmsRecipientsUseCase.kt b/sms-exporter/lib/src/main/java/org/signal/smsexporter/internal/mms/ExportMmsRecipientsUseCase.kt deleted file mode 100644 index ff52cfb54c..0000000000 --- a/sms-exporter/lib/src/main/java/org/signal/smsexporter/internal/mms/ExportMmsRecipientsUseCase.kt +++ /dev/null @@ -1,48 +0,0 @@ -package org.signal.smsexporter.internal.mms - -import android.content.Context -import android.provider.Telephony -import androidx.core.content.contentValuesOf -import com.google.android.mms.pdu_alt.CharacterSets -import com.google.android.mms.pdu_alt.PduHeaders -import org.signal.core.util.Try -import org.signal.core.util.logging.Log -import org.signal.smsexporter.internal.sms.ExportSmsMessagesUseCase - -/** - * Inserts the recipients for each individual message in the insert mms output. Returns nothing. - */ -object ExportMmsRecipientsUseCase { - - private val TAG = Log.tag(ExportSmsMessagesUseCase::class.java) - - fun execute(context: Context, messageId: Long, recipient: String, sender: String, checkForExistence: Boolean): Try { - try { - val addrUri = Telephony.Mms.CONTENT_URI.buildUpon().appendPath(messageId.toString()).appendPath("addr").build() - - if (checkForExistence) { - Log.d(TAG, "Checking for recipient that may have already been inserted...") - val exists = context.contentResolver.query(addrUri, arrayOf("_id"), "${Telephony.Mms.Addr.ADDRESS} == ?", arrayOf(recipient), null)?.use { - it.moveToFirst() - } ?: false - - if (exists) { - Log.d(TAG, "Recipient was already inserted. Skipping.") - return Try.success(Unit) - } - } - - val addrValues = contentValuesOf( - Telephony.Mms.Addr.ADDRESS to recipient, - Telephony.Mms.Addr.CHARSET to CharacterSets.DEFAULT_CHARSET, - Telephony.Mms.Addr.TYPE to if (recipient == sender) PduHeaders.FROM else PduHeaders.TO - ) - - context.contentResolver.insert(addrUri, addrValues) - - return Try.success(Unit) - } catch (e: Exception) { - return Try.failure(e) - } - } -} 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 deleted file mode 100644 index 85793506e4..0000000000 --- a/sms-exporter/lib/src/main/java/org/signal/smsexporter/internal/mms/GetOrCreateMmsThreadIdsUseCase.kt +++ /dev/null @@ -1,66 +0,0 @@ -package org.signal.smsexporter.internal.mms - -import android.content.Context -import android.database.sqlite.SQLiteException -import android.os.Build -import android.provider.Telephony -import com.klinker.android.send_message.Utils -import org.signal.core.util.Try -import org.signal.core.util.logging.Log -import org.signal.smsexporter.ExportableMessage - -/** - * Given a list of messages, gets or creates the threadIds for each different recipient set. - * Returns a list of outputs that tie a given message to a thread id. - * - * This method will also filter out messages that do not have addresses. - */ -internal object GetOrCreateMmsThreadIdsUseCase { - - private val TAG = Log.tag(GetOrCreateMmsThreadIdsUseCase::class.java) - - fun execute( - context: Context, - mms: ExportableMessage.Mms<*>, - threadCache: MutableMap, Long> - ): Try { - return try { - val recipients = getRecipientSet(mms) - val threadId = getOrCreateThreadId(context, recipients, threadCache) - - Try.success(Output(mms, threadId)) - } catch (e: Exception) { - Try.failure(e) - } - } - - private fun getOrCreateThreadId(context: Context, recipients: Set, cache: MutableMap, Long>): Long { - return if (cache.containsKey(recipients)) { - cache[recipients]!! - } else { - val threadId = try { - Utils.getOrCreateThreadId(context, recipients) - } catch (e: SQLiteException) { - Log.w(TAG, "Unable to create thread using Klinker, falling back to system if possible") - if (Build.VERSION.SDK_INT >= 23) { - Telephony.Threads.getOrCreateThreadId(context, recipients) - } else { - throw e - } - } - cache[recipients] = threadId - threadId - } - } - - private fun getRecipientSet(mms: ExportableMessage.Mms<*>): Set { - val recipients = mms.addresses - if (recipients.isEmpty()) { - error("Expected non-empty recipient count.") - } - - return HashSet(recipients.map { it }) - } - - 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 deleted file mode 100644 index 24538ccf55..0000000000 --- a/sms-exporter/lib/src/main/java/org/signal/smsexporter/internal/sms/ExportSmsMessagesUseCase.kt +++ /dev/null @@ -1,49 +0,0 @@ -package org.signal.smsexporter.internal.sms - -import android.content.Context -import android.provider.Telephony -import androidx.core.content.contentValuesOf -import org.signal.core.util.Try -import org.signal.smsexporter.ExportableMessage -import java.lang.Exception - -/** - * Given a list of Sms messages, export each one to the system SMS database - * Returns nothing. - */ -internal object ExportSmsMessagesUseCase { - fun execute(context: Context, sms: ExportableMessage.Sms<*>, checkForExistence: Boolean): Try { - try { - if (checkForExistence) { - val exists = context.contentResolver.query( - Telephony.Sms.CONTENT_URI, - arrayOf("_id"), - "${Telephony.Sms.ADDRESS} = ? AND ${Telephony.Sms.DATE_SENT} = ?", - arrayOf(sms.address, sms.dateSent.inWholeMilliseconds.toString()), - null - )?.use { - it.count > 0 - } ?: false - - if (exists) { - return Try.success(Unit) - } - } - - val contentValues = contentValuesOf( - Telephony.Sms.ADDRESS to sms.address, - Telephony.Sms.BODY to sms.body, - Telephony.Sms.DATE to sms.dateReceived.inWholeMilliseconds, - Telephony.Sms.DATE_SENT to sms.dateSent.inWholeMilliseconds, - Telephony.Sms.READ to if (sms.isRead) 1 else 0, - Telephony.Sms.TYPE to if (sms.isOutgoing) Telephony.Sms.MESSAGE_TYPE_SENT else Telephony.Sms.MESSAGE_TYPE_INBOX - ) - - context.contentResolver.insert(Telephony.Sms.CONTENT_URI, contentValues) - - return Try.success(Unit) - } catch (e: Exception) { - return Try.failure(e) - } - } -} diff --git a/sms-exporter/lib/src/test/java/org/signal/smsexporter/InMemoryContentProvider.kt b/sms-exporter/lib/src/test/java/org/signal/smsexporter/InMemoryContentProvider.kt deleted file mode 100644 index ed2bb2c392..0000000000 --- a/sms-exporter/lib/src/test/java/org/signal/smsexporter/InMemoryContentProvider.kt +++ /dev/null @@ -1,115 +0,0 @@ -package org.signal.smsexporter - -import android.content.ContentProvider -import android.content.ContentValues -import android.database.Cursor -import android.database.sqlite.SQLiteDatabase -import android.database.sqlite.SQLiteOpenHelper -import android.net.Uri -import android.provider.Telephony -import androidx.test.core.app.ApplicationProvider - -/** - * Provides a content provider which reads and writes to an in-memory database. - */ -class InMemoryContentProvider : ContentProvider() { - - private val database: InMemoryDatabase = InMemoryDatabase() - - override fun onCreate(): Boolean { - return false - } - - override fun query(p0: Uri, p1: Array?, p2: String?, p3: Array?, p4: String?): Cursor? { - val tableName = if (p0.pathSegments.isNotEmpty()) p0.lastPathSegment else p0.authority - return database.readableDatabase.query(tableName, p1, p2, p3, p4, null, null) - } - - override fun getType(p0: Uri): String? { - return null - } - - override fun insert(p0: Uri, p1: ContentValues?): Uri? { - val tableName = if (p0.pathSegments.isNotEmpty()) p0.lastPathSegment else p0.authority - val id = database.writableDatabase.insert(tableName, null, p1) - return if (id == -1L) { - null - } else { - p0.buildUpon().appendPath("$id").build() - } - } - - override fun delete(p0: Uri, p1: String?, p2: Array?): Int { - return -1 - } - - override fun update(p0: Uri, p1: ContentValues?, p2: String?, p3: Array?): Int { - return -1 - } - - private class InMemoryDatabase : SQLiteOpenHelper(ApplicationProvider.getApplicationContext(), null, null, 1) { - override fun onCreate(db: SQLiteDatabase) { - db.execSQL( - """ - CREATE TABLE sms ( - ${Telephony.Sms._ID} INTEGER PRIMARY KEY AUTOINCREMENT, - ${Telephony.Sms.ADDRESS} TEXT, - ${Telephony.Sms.DATE_SENT} INTEGER, - ${Telephony.Sms.DATE} INTEGER, - ${Telephony.Sms.BODY} TEXT, - ${Telephony.Sms.READ} INTEGER, - ${Telephony.Sms.TYPE} INTEGER - ); - - """ - ) - - db.execSQL( - """ - CREATE TABLE mms ( - ${Telephony.Mms._ID} INTEGER PRIMARY KEY AUTOINCREMENT, - ${Telephony.Mms.THREAD_ID} INTEGER, - ${Telephony.Mms.DATE} INTEGER, - ${Telephony.Mms.DATE_SENT} INTEGER, - ${Telephony.Mms.MESSAGE_BOX} INTEGER, - ${Telephony.Mms.READ} INTEGER, - ${Telephony.Mms.CONTENT_TYPE} TEXT, - ${Telephony.Mms.MESSAGE_TYPE} INTEGER, - ${Telephony.Mms.MMS_VERSION} INTEGER, - ${Telephony.Mms.MESSAGE_CLASS} TEXT, - ${Telephony.Mms.PRIORITY} INTEGER, - ${Telephony.Mms.TRANSACTION_ID} TEXT, - ${Telephony.Mms.RESPONSE_STATUS} INTEGER, - ${Telephony.Mms.SEEN} INTEGER, - ${Telephony.Mms.TEXT_ONLY} INTEGER - ); - """ - ) - - db.execSQL( - """ - CREATE TABLE part ( - ${Telephony.Mms.Part._ID} INTEGER PRIMARY KEY AUTOINCREMENT, - ${Telephony.Mms.Part.MSG_ID} INTEGER, - ${Telephony.Mms.Part.CONTENT_TYPE} TEXT, - ${Telephony.Mms.Part.CONTENT_ID} INTEGER, - ${Telephony.Mms.Part.TEXT} TEXT - ) - """ - ) - - db.execSQL( - """ - CREATE TABLE addr ( - ${Telephony.Mms.Addr._ID} INTEGER PRIMARY KEY AUTOINCREMENT, - ${Telephony.Mms.Addr.ADDRESS} TEXT, - ${Telephony.Mms.Addr.CHARSET} INTEGER, - ${Telephony.Mms.Addr.TYPE} INTEGER - ) - """ - ) - } - - override fun onUpgrade(db: SQLiteDatabase, p1: Int, p2: Int) = Unit - } -} 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 deleted file mode 100644 index b8aee722c0..0000000000 --- a/sms-exporter/lib/src/test/java/org/signal/smsexporter/TestUtils.kt +++ /dev/null @@ -1,48 +0,0 @@ -package org.signal.smsexporter - -import android.provider.Telephony -import org.robolectric.shadows.ShadowContentResolver -import java.util.UUID -import kotlin.time.Duration -import kotlin.time.Duration.Companion.seconds - -object TestUtils { - fun generateSmsMessage( - id: String = UUID.randomUUID().toString(), - address: String = "+15555060177", - dateReceived: Duration = 2.seconds, - dateSent: Duration = 1.seconds, - isRead: Boolean = false, - isOutgoing: Boolean = false, - body: String = "Hello, $id" - ): ExportableMessage.Sms<*> { - return ExportableMessage.Sms(id, SmsExportState(), address, dateReceived, dateSent, isRead, isOutgoing, body) - } - - fun generateMmsMessage( - id: String = UUID.randomUUID().toString(), - addresses: Set = setOf("+15555060177"), - dateReceived: Duration = 2.seconds, - dateSent: Duration = 1.seconds, - isRead: Boolean = false, - isOutgoing: Boolean = false, - parts: List = listOf(ExportableMessage.Mms.Part.Text("Hello, $id")), - sender: CharSequence = "+15555060177" - ): ExportableMessage.Mms<*> { - return ExportableMessage.Mms(id, SmsExportState(), addresses, dateReceived, dateSent, isRead, isOutgoing, parts, sender) - } - - fun setUpSmsContentProviderAndResolver() { - ShadowContentResolver.registerProviderInternal( - Telephony.Sms.CONTENT_URI.authority, - InMemoryContentProvider() - ) - } - - fun setUpMmsContentProviderAndResolver() { - ShadowContentResolver.registerProviderInternal( - Telephony.Mms.CONTENT_URI.authority, - InMemoryContentProvider() - ) - } -} 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 deleted file mode 100644 index a896654787..0000000000 --- a/sms-exporter/lib/src/test/java/org/signal/smsexporter/internal/mms/ExportMmsMessagesUseCaseTest.kt +++ /dev/null @@ -1,133 +0,0 @@ -package org.signal.smsexporter.internal.mms - -import android.content.Context -import android.net.Uri -import android.provider.Telephony -import androidx.test.core.app.ApplicationProvider -import org.junit.Assert.assertEquals -import org.junit.Before -import org.junit.Test -import org.junit.runner.RunWith -import org.robolectric.RobolectricTestRunner -import org.signal.core.util.CursorUtil -import org.signal.smsexporter.ExportableMessage -import org.signal.smsexporter.TestUtils - -@RunWith(RobolectricTestRunner::class) -class ExportMmsMessagesUseCaseTest { - - @Before - fun setUp() { - TestUtils.setUpMmsContentProviderAndResolver() - } - - @Test - fun `Given an MMS message, when I execute, then I expect an MMS record to be created`() { - // GIVEN - val mmsMessage = TestUtils.generateMmsMessage() - val threadUseCaseOutput = GetOrCreateMmsThreadIdsUseCase.Output(mmsMessage, 1) - - // WHEN - val result = ExportMmsMessagesUseCase.execute( - ApplicationProvider.getApplicationContext(), - threadUseCaseOutput, - false - ) - - // THEN - result.either( - onSuccess = { - validateExportedMessage(mmsMessage) - }, - onFailure = { - throw it - } - ) - } - - @Test - fun `Given an MMS message that already exists, when I execute and check for existence, then I expect no new MMS record to be created`() { - // GIVEN - val mmsMessage = TestUtils.generateMmsMessage() - val threadUseCaseOutput = GetOrCreateMmsThreadIdsUseCase.Output(mmsMessage, 1) - ExportMmsMessagesUseCase.execute( - ApplicationProvider.getApplicationContext(), - threadUseCaseOutput, - false - ) - - // WHEN - val result = ExportMmsMessagesUseCase.execute( - ApplicationProvider.getApplicationContext(), - threadUseCaseOutput, - true - ) - - // THEN - result.either( - onSuccess = { - validateExportedMessage(mmsMessage) - }, - onFailure = { - throw it - } - ) - } - - @Test - fun `Given an MMS message that already exists, when I execute and do not check for existence, then I expect a duplicate MMS record to be created`() { - // GIVEN - val mmsMessage = TestUtils.generateMmsMessage() - val threadUseCaseOutput = GetOrCreateMmsThreadIdsUseCase.Output(mmsMessage, 1) - ExportMmsMessagesUseCase.execute( - ApplicationProvider.getApplicationContext(), - threadUseCaseOutput, - false - ) - - // WHEN - val result = ExportMmsMessagesUseCase.execute( - ApplicationProvider.getApplicationContext(), - threadUseCaseOutput, - false - ) - - // THEN - result.either( - onSuccess = { - validateExportedMessage(mmsMessage, expectedRowCount = 2) - }, - onFailure = { - throw it - } - ) - } - - private fun validateExportedMessage( - mms: ExportableMessage.Mms<*>, - expectedRowCount: Int = 1, - threadId: Long = 1L - ) { - val context: Context = ApplicationProvider.getApplicationContext() - val baseUri: Uri = Telephony.Mms.CONTENT_URI - val transactionId = ExportMmsMessagesUseCase.getTransactionId(mms) - - context.contentResolver.query( - baseUri, - null, - "${Telephony.Mms.TRANSACTION_ID} = ?", - arrayOf(transactionId), - null, - null - )?.use { - it.moveToFirst() - assertEquals(expectedRowCount, it.count) - assertEquals(threadId, CursorUtil.requireLong(it, Telephony.Mms.THREAD_ID)) - assertEquals(mms.dateReceived.inWholeSeconds, CursorUtil.requireLong(it, Telephony.Mms.DATE)) - assertEquals(mms.dateSent.inWholeSeconds, CursorUtil.requireLong(it, Telephony.Mms.DATE_SENT)) - assertEquals(if (mms.isOutgoing) Telephony.Mms.MESSAGE_BOX_SENT else Telephony.Mms.MESSAGE_BOX_INBOX, CursorUtil.requireInt(it, Telephony.Mms.MESSAGE_BOX)) - assertEquals(mms.isRead, CursorUtil.requireBoolean(it, Telephony.Mms.READ)) - assertEquals(transactionId, CursorUtil.requireString(it, Telephony.Mms.TRANSACTION_ID)) - } ?: org.junit.Assert.fail("Content Resolver returned a null cursor") - } -} diff --git a/sms-exporter/lib/src/test/java/org/signal/smsexporter/internal/mms/ExportMmsPartsUseCaseTest.kt b/sms-exporter/lib/src/test/java/org/signal/smsexporter/internal/mms/ExportMmsPartsUseCaseTest.kt deleted file mode 100644 index b6b30833bf..0000000000 --- a/sms-exporter/lib/src/test/java/org/signal/smsexporter/internal/mms/ExportMmsPartsUseCaseTest.kt +++ /dev/null @@ -1,136 +0,0 @@ -package org.signal.smsexporter.internal.mms - -import android.content.Context -import android.net.Uri -import android.provider.Telephony -import androidx.test.core.app.ApplicationProvider -import org.junit.Assert.assertEquals -import org.junit.Before -import org.junit.Test -import org.junit.runner.RunWith -import org.robolectric.RobolectricTestRunner -import org.signal.core.util.CursorUtil -import org.signal.smsexporter.ExportableMessage -import org.signal.smsexporter.TestUtils - -@RunWith(RobolectricTestRunner::class) -class ExportMmsPartsUseCaseTest { - - @Before - fun setUp() { - TestUtils.setUpMmsContentProviderAndResolver() - } - - @Test - fun `Given a message with a part, when I export part, then I expect a valid part row`() { - // GIVEN - val message = TestUtils.generateMmsMessage() - val output = ExportMmsMessagesUseCase.Output(message, 1) - - // WHEN - val result = ExportMmsPartsUseCase.execute( - ApplicationProvider.getApplicationContext(), - message.parts.first(), - output, - false - ) - - // THEN - result.either( - onSuccess = { - validateExportedPart(message.parts.first(), output.messageId) - }, - onFailure = { - throw it - } - ) - } - - @Test - fun `Given an already exported part, when I export part with check, then I expect a single part row`() { - // GIVEN - val message = TestUtils.generateMmsMessage() - val output = ExportMmsMessagesUseCase.Output(message, 1) - ExportMmsPartsUseCase.execute( - ApplicationProvider.getApplicationContext(), - message.parts.first(), - output, - false - ) - - // WHEN - val result = ExportMmsPartsUseCase.execute( - ApplicationProvider.getApplicationContext(), - message.parts.first(), - output, - true - ) - - // THEN - result.either( - onSuccess = { - validateExportedPart(message.parts.first(), output.messageId) - }, - onFailure = { - throw it - } - ) - } - - @Test - fun `Given an already exported part, when I export part without check, then I expect a duplicated part row`() { - // GIVEN - val message = TestUtils.generateMmsMessage() - val output = ExportMmsMessagesUseCase.Output(message, 1) - ExportMmsPartsUseCase.execute( - ApplicationProvider.getApplicationContext(), - message.parts.first(), - output, - false - ) - - // WHEN - val result = ExportMmsPartsUseCase.execute( - ApplicationProvider.getApplicationContext(), - message.parts.first(), - output, - false - ) - - // THEN - result.either( - onSuccess = { - validateExportedPart(message.parts.first(), output.messageId, expectedRowCount = 2) - }, - onFailure = { - throw it - } - ) - } - - private fun validateExportedPart( - part: ExportableMessage.Mms.Part, - messageId: Long, - expectedRowCount: Int = 1 - ) { - val context: Context = ApplicationProvider.getApplicationContext() - val baseUri: Uri = Telephony.Mms.CONTENT_URI.buildUpon().appendPath("part").build() - val contentId = ExportMmsPartsUseCase.getContentId(part) - - context.contentResolver.query( - baseUri, - null, - "${Telephony.Mms.Part.CONTENT_ID} = ?", - arrayOf(contentId), - null, - null - )?.use { - it.moveToFirst() - assertEquals(expectedRowCount, it.count) - assertEquals(part.contentType, CursorUtil.requireString(it, Telephony.Mms.Part.CONTENT_TYPE)) - assertEquals(contentId, CursorUtil.requireString(it, Telephony.Mms.Part.CONTENT_ID)) - assertEquals(messageId, CursorUtil.requireLong(it, Telephony.Mms.Part.MSG_ID)) - assertEquals(if (part is ExportableMessage.Mms.Part.Text) part.text else null, CursorUtil.requireString(it, Telephony.Mms.Part.TEXT)) - } ?: org.junit.Assert.fail("Content Resolver returned a null cursor") - } -} diff --git a/sms-exporter/lib/src/test/java/org/signal/smsexporter/internal/mms/ExportMmsRecipientsUseCaseTest.kt b/sms-exporter/lib/src/test/java/org/signal/smsexporter/internal/mms/ExportMmsRecipientsUseCaseTest.kt deleted file mode 100644 index 277d14c23d..0000000000 --- a/sms-exporter/lib/src/test/java/org/signal/smsexporter/internal/mms/ExportMmsRecipientsUseCaseTest.kt +++ /dev/null @@ -1,138 +0,0 @@ -package org.signal.smsexporter.internal.mms - -import android.content.Context -import android.net.Uri -import android.provider.Telephony -import androidx.test.core.app.ApplicationProvider -import com.google.android.mms.pdu_alt.PduHeaders -import org.junit.Assert.assertEquals -import org.junit.Assert.fail -import org.junit.Before -import org.junit.Test -import org.junit.runner.RunWith -import org.robolectric.RobolectricTestRunner -import org.signal.core.util.CursorUtil -import org.signal.smsexporter.TestUtils - -@RunWith(RobolectricTestRunner::class) -class ExportMmsRecipientsUseCaseTest { - - @Before - fun setUp() { - TestUtils.setUpMmsContentProviderAndResolver() - } - - @Test - fun `When I export recipient, then I expect a valid exported recipient`() { - // GIVEN - val address = "+15065550177" - val sender = "+15065550123" - val messageId = 1L - - // WHEN - val result = ExportMmsRecipientsUseCase.execute( - ApplicationProvider.getApplicationContext(), - messageId, - address, - sender, - false - ) - - // THEN - result.either( - onSuccess = { - validateExportedRecipient(address, sender, messageId) - }, - onFailure = { - throw it - } - ) - } - - @Test - fun `Given recipient already exported, When I export recipient with check, then I expect a single exported recipient`() { - // GIVEN - val address = "+15065550177" - val sender = "+15065550123" - val messageId = 1L - ExportMmsRecipientsUseCase.execute( - ApplicationProvider.getApplicationContext(), - messageId, - address, - sender, - false - ) - - // WHEN - val result = ExportMmsRecipientsUseCase.execute( - ApplicationProvider.getApplicationContext(), - messageId, - address, - sender, - true - ) - - // THEN - result.either( - onSuccess = { - validateExportedRecipient(address, sender, messageId) - }, - onFailure = { - throw it - } - ) - } - - @Test - fun `Given recipient already exported, When I export recipient with check, then I expect a duplicate exported recipient`() { - // GIVEN - val address = "+15065550177" - val sender = "+15065550123" - val messageId = 1L - ExportMmsRecipientsUseCase.execute( - ApplicationProvider.getApplicationContext(), - messageId, - address, - sender, - false - ) - - // WHEN - val result = ExportMmsRecipientsUseCase.execute( - ApplicationProvider.getApplicationContext(), - messageId, - address, - sender, - false - ) - - // THEN - result.either( - onSuccess = { - validateExportedRecipient(address, sender, messageId, expectedRowCount = 2) - }, - onFailure = { - throw it - } - ) - } - - private fun validateExportedRecipient(address: String, sender: String, messageId: Long, expectedRowCount: Int = 1) { - val context: Context = ApplicationProvider.getApplicationContext() - val baseUri: Uri = Telephony.Mms.CONTENT_URI.buildUpon().appendPath(messageId.toString()).appendPath("addr").build() - - context.contentResolver.query( - baseUri, - null, - "${Telephony.Mms.Addr.ADDRESS} = ?", - arrayOf(address), - null, - null - )?.use { - it.moveToFirst() - assertEquals(expectedRowCount, it.count) - assertEquals(address, CursorUtil.requireString(it, Telephony.Mms.Addr.ADDRESS)) - assertEquals(if (address == sender) PduHeaders.FROM else PduHeaders.TO, CursorUtil.requireInt(it, Telephony.Mms.Addr.TYPE)) - } ?: fail("Content Resolver returned a null cursor") - } -} diff --git a/sms-exporter/lib/src/test/java/org/signal/smsexporter/internal/mms/GetOrCreateMmsThreadIdsUseCaseTest.kt b/sms-exporter/lib/src/test/java/org/signal/smsexporter/internal/mms/GetOrCreateMmsThreadIdsUseCaseTest.kt deleted file mode 100644 index abaaec740e..0000000000 --- a/sms-exporter/lib/src/test/java/org/signal/smsexporter/internal/mms/GetOrCreateMmsThreadIdsUseCaseTest.kt +++ /dev/null @@ -1,42 +0,0 @@ -package org.signal.smsexporter.internal.mms - -import androidx.test.core.app.ApplicationProvider -import org.junit.Assert.assertEquals -import org.junit.Before -import org.junit.Test -import org.junit.runner.RunWith -import org.robolectric.RobolectricTestRunner -import org.signal.smsexporter.TestUtils - -@RunWith(RobolectricTestRunner::class) -class GetOrCreateMmsThreadIdsUseCaseTest { - - @Before - fun setUp() { - TestUtils.setUpMmsContentProviderAndResolver() - } - - @Test - fun `Given a message, when I execute, then I update the cache with the thread id`() { - // GIVEN - val mms = TestUtils.generateMmsMessage() - val threadCache = mutableMapOf, Long>() - - // WHEN - val result = GetOrCreateMmsThreadIdsUseCase.execute( - ApplicationProvider.getApplicationContext(), - mms, - threadCache - ) - - // THEN - result.either( - onSuccess = { - assertEquals(threadCache[mms.addresses], it.threadId) - }, - onFailure = { - throw it - } - ) - } -} 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 deleted file mode 100644 index 674030ed50..0000000000 --- a/sms-exporter/lib/src/test/java/org/signal/smsexporter/internal/sms/ExportSmsMessagesUseCaseTest.kt +++ /dev/null @@ -1,127 +0,0 @@ -package org.signal.smsexporter.internal.sms - -import android.content.Context -import android.net.Uri -import android.provider.Telephony -import androidx.test.core.app.ApplicationProvider -import org.junit.Assert -import org.junit.Assert.assertEquals -import org.junit.Before -import org.junit.Test -import org.junit.runner.RunWith -import org.robolectric.RobolectricTestRunner -import org.signal.core.util.CursorUtil -import org.signal.smsexporter.ExportableMessage -import org.signal.smsexporter.TestUtils - -@RunWith(RobolectricTestRunner::class) -class ExportSmsMessagesUseCaseTest { - - @Before - fun setUp() { - TestUtils.setUpSmsContentProviderAndResolver() - } - - @Test - fun `Given an SMS message, when I execute, then I expect a record to be inserted into the SMS database`() { - // GIVEN - val exportableSmsMessage = TestUtils.generateSmsMessage() - - // WHEN - val result = ExportSmsMessagesUseCase.execute( - context = ApplicationProvider.getApplicationContext(), - sms = exportableSmsMessage, - checkForExistence = false - ) - - // THEN - result.either( - onSuccess = { - validateExportedMessage(exportableSmsMessage) - }, - onFailure = { - throw it - } - ) - } - - @Test - fun `Given an SMS message that already exists, when I execute and check for existence, then I expect only a single record to be inserted into the SMS database`() { - // GIVEN - val exportableSmsMessage = TestUtils.generateSmsMessage() - ExportSmsMessagesUseCase.execute( - context = ApplicationProvider.getApplicationContext(), - sms = exportableSmsMessage, - checkForExistence = false - ) - - // WHEN - val result = ExportSmsMessagesUseCase.execute( - context = ApplicationProvider.getApplicationContext(), - sms = exportableSmsMessage, - checkForExistence = true - ) - - // THEN - result.either( - onSuccess = { - validateExportedMessage(exportableSmsMessage) - }, - onFailure = { - throw it - } - ) - } - - @Test - fun `Given an SMS message that already exists, when I execute and do not check for existence, then I expect only a duplicate record to be inserted into the SMS database`() { - // GIVEN - val exportableSmsMessage = TestUtils.generateSmsMessage() - ExportSmsMessagesUseCase.execute( - context = ApplicationProvider.getApplicationContext(), - sms = exportableSmsMessage, - checkForExistence = false - ) - - // WHEN - val result = ExportSmsMessagesUseCase.execute( - context = ApplicationProvider.getApplicationContext(), - sms = exportableSmsMessage, - checkForExistence = false - ) - - // THEN - result.either( - onSuccess = { - validateExportedMessage(exportableSmsMessage, expectedRowCount = 2) - }, - onFailure = { - throw it - } - ) - } - - 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 - - context.contentResolver.query( - baseUri, - null, - "${Telephony.Sms.ADDRESS} = ? AND ${Telephony.Sms.DATE_SENT} = ?", - arrayOf(sms.address, sms.dateSent.inWholeMilliseconds.toString()), - null, - null - )?.use { - it.moveToFirst() - assertEquals(expectedRowCount, it.count) - assertEquals(sms.address, CursorUtil.requireString(it, Telephony.Sms.ADDRESS)) - assertEquals(sms.dateSent.inWholeMilliseconds, CursorUtil.requireLong(it, Telephony.Sms.DATE_SENT)) - assertEquals(sms.dateReceived.inWholeMilliseconds, CursorUtil.requireLong(it, Telephony.Sms.DATE)) - assertEquals(sms.isRead, CursorUtil.requireBoolean(it, Telephony.Sms.READ)) - assertEquals(sms.body, CursorUtil.requireString(it, Telephony.Sms.BODY)) - assertEquals(if (sms.isOutgoing) Telephony.Sms.MESSAGE_TYPE_SENT else Telephony.Sms.MESSAGE_TYPE_INBOX, CursorUtil.requireInt(it, Telephony.Sms.TYPE)) - } ?: Assert.fail("Content Resolver returned a null cursor") - } -}