Remove SMS export.

This commit is contained in:
Cody Henthorne
2024-01-24 16:04:08 -05:00
committed by Nicholas Tinsley
parent 98865d61dd
commit aa33fd44b8
93 changed files with 3 additions and 5407 deletions

View File

@@ -1,26 +1,18 @@
package org.thoughtcrime.securesms.components.settings.app.chats
import android.app.Activity
import android.content.Intent
import androidx.activity.result.ActivityResultLauncher
import androidx.activity.result.contract.ActivityResultContracts
import androidx.lifecycle.ViewModelProvider
import androidx.navigation.Navigation
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.app.chats.sms.SmsExportState
import org.thoughtcrime.securesms.components.settings.configure
import org.thoughtcrime.securesms.exporter.flow.SmsExportActivity
import org.thoughtcrime.securesms.exporter.flow.SmsExportDialogs
import org.thoughtcrime.securesms.util.adapter.mapping.MappingAdapter
import org.thoughtcrime.securesms.util.navigation.safeNavigate
class ChatsSettingsFragment : DSLSettingsFragment(R.string.preferences_chats__chats) {
private lateinit var viewModel: ChatsSettingsViewModel
private lateinit var smsExportLauncher: ActivityResultLauncher<Intent>
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),

View File

@@ -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
)

View File

@@ -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<ChatsSettingsState> = 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<ChatsSettingsState> = 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

View File

@@ -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
}

View File

@@ -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<Intent>
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())
}
}

View File

@@ -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<SmsExportState> {
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
}
}
}

View File

@@ -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
)

View File

@@ -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<SmsSettingsState> = 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())) }
}
}

View File

@@ -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);

View File

@@ -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<ExportableMessage>, 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<ExportableMessage> {
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<ExportableMessage> {
private var messageIterator: Iterator<MessageRecord>? = 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<String> = 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<ExportableMessage.Mms.Part> = 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()) }
}
}
}

View File

@@ -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<ExportableMessage> {
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()
}
}
}

View File

@@ -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()
}
}
}

View File

@@ -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)
}
}

View File

@@ -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())
}
}
}
}

View File

@@ -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)))
}
)
}
}

View File

@@ -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()
}
}

View File

@@ -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<String?>, 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()
}
}

View File

@@ -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<SmsExportSizeEstimations> {
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)
}

View File

@@ -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())
}
}

View File

@@ -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)
}
}
}
}

View File

@@ -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()
}
}

View File

@@ -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()
}
}

View File

@@ -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 <T : ViewModel> create(modelClass: Class<T>): T {
return requireNotNull(modelClass.cast(SmsExportViewModel(isFromMegaphone, isReExport)))
}
}
}

View File

@@ -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())
}
}
}
}

View File

@@ -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");

View File

@@ -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<Intent>
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)
}
}

View File

@@ -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
}
}
}