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

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

View file

@ -28,11 +28,6 @@
<uses-permission android:name="android.permission.WRITE_CONTACTS"/>
<uses-permission android:name="android.permission.ACCESS_NOTIFICATION_POLICY" />
<uses-permission android:name="android.permission.RECEIVE_SMS"/>
<uses-permission android:name="android.permission.RECEIVE_MMS"/>
<uses-permission android:name="android.permission.READ_SMS"/>
<uses-permission android:name="android.permission.SEND_SMS"/>
<uses-permission android:name="android.permission.WRITE_SMS"/>
<uses-permission android:name="android.permission.READ_PHONE_STATE" />
<uses-permission android:name="android.permission.READ_PHONE_NUMBERS" />
@ -1066,13 +1061,6 @@
android:launchMode="singleTask"
android:exported="false"/>
<activity android:name=".megaphone.SmsExportMegaphoneActivity"
android:theme="@style/Theme.Signal.DayNight.NoActionBar"
android:screenOrientation="portrait"
android:configChanges="touchscreen|keyboard|keyboardHidden|orientation|screenLayout|screenSize|uiMode"
android:launchMode="singleTask"
android:exported="false"/>
<activity android:name=".ratelimit.RecaptchaProofActivity"
android:theme="@style/Theme.Signal.DayNight.NoActionBar"
android:configChanges="touchscreen|keyboard|keyboardHidden|orientation|screenLayout|screenSize|uiMode"
@ -1094,24 +1082,11 @@
android:configChanges="touchscreen|keyboard|keyboardHidden|orientation|screenLayout|screenSize"
android:exported="false"/>
<activity android:name=".exporter.flow.SmsExportActivity"
android:theme="@style/Theme.Signal.DayNight.NoActionBar"
android:launchMode="singleTask"
android:screenOrientation="portrait"
android:configChanges="touchscreen|keyboard|keyboardHidden|orientation|screenLayout|screenSize"
android:exported="false"/>
<activity android:name=".components.settings.app.subscription.donate.DonateToSignalActivity"
android:theme="@style/Theme.Signal.DayNight.NoActionBar"
android:configChanges="touchscreen|keyboard|keyboardHidden|orientation|screenLayout|screenSize"
android:exported="false"/>
<service
android:enabled="true"
android:name=".exporter.SignalSmsExportService"
android:foregroundServiceType="dataSync"
android:exported="false"/>
<service
android:enabled="true"
android:name=".service.webrtc.WebRtcCallService"

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

View file

@ -1,260 +0,0 @@
<?xml version="1.0" encoding="utf-8"?>
<ScrollView xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:app="http://schemas.android.com/apk/res-auto"
android:layout_width="match_parent"
android:layout_height="match_parent"
android:fillViewport="true">
<androidx.constraintlayout.widget.ConstraintLayout
android:layout_width="match_parent"
android:layout_height="wrap_content">
<androidx.appcompat.widget.AppCompatImageView
android:id="@+id/hero"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:layout_marginTop="84dp"
android:importantForAccessibility="no"
app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintTop_toTopOf="parent"
app:srcCompat="@drawable/export_sms" />
<TextView
android:id="@+id/headline"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:layout_marginHorizontal="32dp"
android:layout_marginTop="40dp"
android:gravity="center"
android:text="@string/ChooseANewDefaultSmsAppFragment__choose_a_new"
android:textAppearance="@style/Signal.Text.HeadlineLarge"
app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintTop_toBottomOf="@id/hero" />
<View
android:layout_width="0dp"
android:layout_height="0dp"
android:background="@drawable/circle_tintable"
android:backgroundTint="@color/signal_colorSurface3"
app:layout_constraintBottom_toBottomOf="@+id/bullet_1"
app:layout_constraintEnd_toEndOf="@+id/bullet_1"
app:layout_constraintStart_toStartOf="@+id/bullet_1"
app:layout_constraintTop_toTopOf="@+id/bullet_1" />
<TextView
android:id="@+id/bullet_1"
android:layout_width="0dp"
android:layout_height="wrap_content"
android:layout_marginStart="32dp"
android:layout_marginTop="24dp"
android:gravity="center"
android:minWidth="28dp"
android:minHeight="28dp"
android:padding="4dp"
android:text="@string/ChooseANewDefaultSmsAppFragment__bullet_1"
android:textAppearance="@style/Signal.Text.BodyMedium"
android:textStyle="bold"
app:layout_constraintDimensionRatio="1:1"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintTop_toBottomOf="@id/headline" />
<TextView
android:id="@+id/bullet_1_text"
android:layout_width="0dp"
android:layout_height="wrap_content"
android:layout_marginStart="8dp"
android:layout_marginTop="24dp"
android:layout_marginEnd="32dp"
android:minWidth="28dp"
android:minHeight="28dp"
android:padding="4dp"
android:text="@string/ChooseANewDefaultSmsAppFragment__tap_continue_to_open_the_defaults_apps_screen_in_settings"
android:textAlignment="viewStart"
android:textAppearance="@style/Signal.Text.BodyLarge"
app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintStart_toEndOf="@+id/bullet_1"
app:layout_constraintTop_toBottomOf="@id/headline" />
<androidx.constraintlayout.widget.Barrier
android:id="@+id/bullet_1_barrier"
android:layout_width="match_parent"
android:layout_height="1dp"
android:orientation="horizontal"
app:barrierDirection="bottom"
app:constraint_referenced_ids="bullet_1,bullet_1_text" />
<View
android:layout_width="0dp"
android:layout_height="0dp"
android:background="@drawable/circle_tintable"
android:backgroundTint="@color/signal_colorSurface3"
app:layout_constraintBottom_toBottomOf="@+id/bullet_2"
app:layout_constraintEnd_toEndOf="@+id/bullet_2"
app:layout_constraintStart_toStartOf="@+id/bullet_2"
app:layout_constraintTop_toTopOf="@+id/bullet_2" />
<TextView
android:id="@+id/bullet_2"
android:layout_width="0dp"
android:layout_height="wrap_content"
android:layout_marginStart="32dp"
android:layout_marginTop="24dp"
android:gravity="center"
android:minWidth="28dp"
android:minHeight="28dp"
android:padding="4dp"
android:text="@string/ChooseANewDefaultSmsAppFragment__bullet_2"
android:textAppearance="@style/Signal.Text.BodyMedium"
android:textStyle="bold"
app:layout_constraintDimensionRatio="1:1"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintTop_toBottomOf="@id/bullet_1_barrier" />
<TextView
android:id="@+id/bullet_2_text"
android:layout_width="0dp"
android:layout_height="wrap_content"
android:layout_marginStart="8dp"
android:layout_marginTop="24dp"
android:layout_marginEnd="32dp"
android:minWidth="28dp"
android:minHeight="28dp"
android:padding="4dp"
android:text="@string/ChooseANewDefaultSmsAppFragment__select_sms_app_from_the_list"
android:textAlignment="viewStart"
android:textAppearance="@style/Signal.Text.BodyLarge"
app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintStart_toEndOf="@+id/bullet_2"
app:layout_constraintTop_toBottomOf="@id/bullet_1_barrier" />
<androidx.constraintlayout.widget.Barrier
android:id="@+id/bullet_2_barrier"
android:layout_width="match_parent"
android:layout_height="1dp"
android:orientation="horizontal"
app:barrierDirection="bottom"
app:constraint_referenced_ids="bullet_2,bullet_2_text" />
<View
android:layout_width="0dp"
android:layout_height="0dp"
android:background="@drawable/circle_tintable"
android:backgroundTint="@color/signal_colorSurface3"
app:layout_constraintBottom_toBottomOf="@+id/bullet_3"
app:layout_constraintEnd_toEndOf="@+id/bullet_3"
app:layout_constraintStart_toStartOf="@+id/bullet_3"
app:layout_constraintTop_toTopOf="@+id/bullet_3" />
<TextView
android:id="@+id/bullet_3"
android:layout_width="0dp"
android:layout_height="wrap_content"
android:layout_marginStart="32dp"
android:layout_marginTop="24dp"
android:gravity="center"
android:minWidth="28dp"
android:minHeight="28dp"
android:padding="4dp"
android:text="@string/ChooseANewDefaultSmsAppFragment__bullet_3"
android:textAppearance="@style/Signal.Text.BodyMedium"
android:textStyle="bold"
app:layout_constraintDimensionRatio="1:1"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintTop_toBottomOf="@id/bullet_2_barrier" />
<TextView
android:id="@+id/bullet_3_text"
android:layout_width="0dp"
android:layout_height="wrap_content"
android:layout_marginStart="8dp"
android:layout_marginTop="24dp"
android:layout_marginEnd="32dp"
android:minWidth="28dp"
android:minHeight="28dp"
android:padding="4dp"
android:text="@string/ChooseANewDefaultSmsAppFragment__choose_another_app_to_use_for_sms_messaging"
android:textAlignment="viewStart"
android:textAppearance="@style/Signal.Text.BodyLarge"
app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintStart_toEndOf="@+id/bullet_3"
app:layout_constraintTop_toBottomOf="@id/bullet_2_barrier" />
<androidx.constraintlayout.widget.Barrier
android:id="@+id/bullet_3_barrier"
android:layout_width="match_parent"
android:layout_height="1dp"
android:orientation="horizontal"
app:barrierDirection="bottom"
app:constraint_referenced_ids="bullet_3,bullet_3_text" />
<View
android:layout_width="0dp"
android:layout_height="0dp"
android:background="@drawable/circle_tintable"
android:backgroundTint="@color/signal_colorSurface3"
app:layout_constraintBottom_toBottomOf="@+id/bullet_4"
app:layout_constraintEnd_toEndOf="@+id/bullet_4"
app:layout_constraintStart_toStartOf="@+id/bullet_4"
app:layout_constraintTop_toTopOf="@+id/bullet_4" />
<TextView
android:id="@+id/bullet_4"
android:layout_width="0dp"
android:layout_height="wrap_content"
android:layout_marginStart="32dp"
android:layout_marginTop="24dp"
android:gravity="center"
android:minWidth="28dp"
android:minHeight="28dp"
android:padding="4dp"
android:text="@string/ChooseANewDefaultSmsAppFragment__bullet_4"
android:textAppearance="@style/Signal.Text.BodyMedium"
android:textStyle="bold"
app:layout_constraintDimensionRatio="1:1"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintTop_toBottomOf="@id/bullet_3_barrier" />
<TextView
android:id="@+id/bullet_4_text"
android:layout_width="0dp"
android:layout_height="wrap_content"
android:layout_marginStart="8dp"
android:layout_marginTop="24dp"
android:layout_marginEnd="32dp"
android:minWidth="28dp"
android:minHeight="28dp"
android:padding="4dp"
android:text="@string/ChooseANewDefaultSmsAppFragment__return_to_signal"
android:textAlignment="viewStart"
android:textAppearance="@style/Signal.Text.BodyLarge"
app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintStart_toEndOf="@+id/bullet_4"
app:layout_constraintTop_toBottomOf="@id/bullet_3_barrier" />
<androidx.constraintlayout.widget.Barrier
android:id="@+id/bullet_4_barrier"
android:layout_width="match_parent"
android:layout_height="1dp"
android:orientation="horizontal"
app:barrierDirection="bottom"
app:constraint_referenced_ids="bullet_4,bullet_4_text" />
<com.google.android.material.button.MaterialButton
android:id="@+id/continue_button"
style="@style/Signal.Widget.Button.Large.Tonal"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_marginTop="48dp"
android:layout_marginBottom="44dp"
android:minWidth="220dp"
android:text="@string/ChooseANewDefaultSmsAppFragment__continue"
app:layout_constraintBottom_toBottomOf="parent"
app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintTop_toBottomOf="@id/bullet_4_barrier"
app:layout_constraintVertical_bias="1" />
</androidx.constraintlayout.widget.ConstraintLayout>
</ScrollView>

View file

@ -1,55 +0,0 @@
<?xml version="1.0" encoding="utf-8"?>
<androidx.constraintlayout.widget.ConstraintLayout xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:app="http://schemas.android.com/apk/res-auto"
xmlns:tools="http://schemas.android.com/tools"
android:layout_width="match_parent"
android:layout_height="match_parent">
<TextView
android:id="@+id/export_complete_title"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:text="@string/ExportSmsCompleteFragment__export_complete"
android:textAppearance="@style/Signal.Text.TitleLarge"
app:layout_constraintBottom_toBottomOf="parent"
app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintTop_toTopOf="parent" />
<androidx.appcompat.widget.AppCompatImageView
android:id="@+id/export_complete_done_icon"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_marginBottom="16dp"
android:scaleType="centerInside"
app:layout_constraintBottom_toTopOf="@+id/export_complete_title"
app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintStart_toStartOf="parent"
app:srcCompat="@drawable/complete"
tools:ignore="UnusedAttribute,ImageContrastCheck" />
<TextView
android:id="@+id/export_complete_status"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_marginTop="8dp"
android:textAppearance="@style/Signal.Text.BodySmall"
android:textColor="@color/signal_colorOnSurfaceVariant"
app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintTop_toBottomOf="@+id/export_complete_title"
tools:text="248 of 248 messages exported" />
<com.google.android.material.button.MaterialButton
android:id="@+id/export_complete_next"
style="@style/Signal.Widget.Button.Large.Tonal"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:minWidth="200dp"
android:layout_marginBottom="24dp"
android:text="@string/ExportSmsCompleteFragment__next"
app:layout_constraintBottom_toBottomOf="parent"
app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintStart_toStartOf="parent" />
</androidx.constraintlayout.widget.ConstraintLayout>

View file

@ -1,94 +0,0 @@
<?xml version="1.0" encoding="utf-8"?>
<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:app="http://schemas.android.com/apk/res-auto"
xmlns:tools="http://schemas.android.com/tools"
android:layout_width="match_parent"
android:layout_height="match_parent"
android:orientation="vertical">
<androidx.core.widget.NestedScrollView
android:id="@+id/scroll_view"
android:layout_width="match_parent"
android:layout_height="match_parent"
android:fillViewport="true">
<androidx.constraintlayout.widget.ConstraintLayout
android:layout_width="match_parent"
android:layout_height="wrap_content">
<androidx.appcompat.widget.AppCompatImageView
android:id="@+id/hero"
android:layout_width="112dp"
android:layout_height="112dp"
android:layout_marginTop="84dp"
android:layout_marginBottom="16dp"
app:layout_constraintBottom_toTopOf="@+id/export_complete_title"
app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintTop_toTopOf="parent"
app:srcCompat="@drawable/sms_export_error"
tools:ignore="UnusedAttribute,ImageContrastCheck" />
<TextView
android:id="@+id/headline"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:layout_marginHorizontal="32dp"
android:layout_marginTop="40dp"
android:gravity="center"
android:text="@string/ExportSmsFullError__error_exporting_sms_messages"
android:textAppearance="@style/Signal.Text.TitleLarge"
app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintTop_toBottomOf="@id/hero" />
<TextView
android:id="@+id/export_complete_status"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_marginTop="8dp"
android:textAppearance="@style/Signal.Text.BodySmall"
android:textColor="@color/signal_colorOnSurfaceVariant"
app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintTop_toBottomOf="@+id/headline"
tools:text="248 of 248 messages exported" />
<org.thoughtcrime.securesms.util.views.LearnMoreTextView
android:id="@+id/please_try_again"
android:layout_width="0dp"
android:layout_height="wrap_content"
android:layout_marginStart="32dp"
android:layout_marginTop="60dp"
android:layout_marginEnd="32dp"
android:text="@string/ExportSmsFullError__please_try_again_if_the_problem_persists"
android:textAlignment="center"
android:textAppearance="@style/Signal.Text.BodyLarge"
android:textColor="@color/signal_colorOnSurfaceVariant"
app:layout_constraintBottom_toTopOf="@+id/retry_button"
app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintTop_toBottomOf="@id/export_complete_status"
app:layout_constraintVertical_chainStyle="spread_inside" />
<com.google.android.material.button.MaterialButton
android:id="@+id/retry_button"
style="@style/Signal.Widget.Button.Large.Tonal"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_marginHorizontal="32dp"
android:layout_marginTop="48dp"
android:layout_marginBottom="44dp"
android:minWidth="220dp"
android:text="@string/ExportSmsPartiallyComplete__retry"
app:layout_constraintBottom_toBottomOf="parent"
app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintTop_toBottomOf="@id/please_try_again"
app:layout_constraintVertical_bias="1" />
</androidx.constraintlayout.widget.ConstraintLayout>
</androidx.core.widget.NestedScrollView>
</LinearLayout>

View file

@ -1,250 +0,0 @@
<?xml version="1.0" encoding="utf-8"?>
<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:app="http://schemas.android.com/apk/res-auto"
xmlns:tools="http://schemas.android.com/tools"
android:layout_width="match_parent"
android:layout_height="match_parent"
android:orientation="vertical">
<androidx.core.widget.NestedScrollView
android:id="@+id/scroll_view"
android:layout_width="match_parent"
android:layout_height="match_parent"
android:fillViewport="true">
<androidx.constraintlayout.widget.ConstraintLayout
android:layout_width="match_parent"
android:layout_height="wrap_content">
<androidx.appcompat.widget.AppCompatImageView
android:id="@+id/hero"
android:layout_width="112dp"
android:layout_height="112dp"
android:layout_marginTop="84dp"
android:layout_marginBottom="16dp"
android:scaleType="centerInside"
app:layout_constraintBottom_toTopOf="@+id/export_complete_title"
app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintTop_toTopOf="parent"
app:srcCompat="@drawable/sms_export_partial_complete"
tools:ignore="UnusedAttribute,ImageContrastCheck" />
<TextView
android:id="@+id/headline"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:layout_marginHorizontal="32dp"
android:layout_marginTop="40dp"
android:gravity="center"
android:text="@string/ExportSmsPartiallyComplete__export_partially_complete"
android:textAppearance="@style/Signal.Text.TitleLarge"
app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintTop_toBottomOf="@id/hero" />
<TextView
android:id="@+id/export_complete_status"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_marginTop="8dp"
android:textAppearance="@style/Signal.Text.BodySmall"
android:textColor="@color/signal_colorOnSurfaceVariant"
app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintTop_toBottomOf="@+id/headline"
tools:text="248 of 248 messages exported" />
<View
android:layout_width="0dp"
android:layout_height="0dp"
android:background="@drawable/circle_tintable"
android:backgroundTint="@color/signal_colorSurface3"
app:layout_constraintBottom_toBottomOf="@+id/bullet_1"
app:layout_constraintEnd_toEndOf="@+id/bullet_1"
app:layout_constraintStart_toStartOf="@+id/bullet_1"
app:layout_constraintTop_toTopOf="@+id/bullet_1" />
<TextView
android:id="@+id/bullet_1"
android:layout_width="0dp"
android:layout_height="wrap_content"
android:layout_marginStart="32dp"
android:layout_marginTop="60dp"
android:gravity="center"
android:minWidth="28dp"
android:minHeight="28dp"
android:padding="4dp"
android:text="@string/ChooseANewDefaultSmsAppFragment__bullet_1"
android:textAppearance="@style/Signal.Text.BodyMedium"
android:textStyle="bold"
app:layout_constraintDimensionRatio="1:1"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintTop_toBottomOf="@id/export_complete_status" />
<TextView
android:id="@+id/bullet_1_text"
android:layout_width="0dp"
android:layout_height="wrap_content"
android:layout_marginStart="8dp"
android:layout_marginTop="59dp"
android:layout_marginEnd="32dp"
android:minWidth="28dp"
android:minHeight="28dp"
android:padding="4dp"
android:text="@string/ExportSmsPartiallyComplete__ensure_you_have_an_additional_s_free_on_your_phone_to_export_your_messages"
android:textAlignment="viewStart"
android:textAppearance="@style/Signal.Text.BodyLarge"
app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintStart_toEndOf="@+id/bullet_1"
app:layout_constraintTop_toBottomOf="@id/export_complete_status" />
<androidx.constraintlayout.widget.Barrier
android:id="@+id/bullet_1_barrier"
android:layout_width="match_parent"
android:layout_height="1dp"
android:orientation="horizontal"
app:barrierDirection="bottom"
app:constraint_referenced_ids="bullet_1,bullet_1_text" />
<View
android:layout_width="0dp"
android:layout_height="0dp"
android:background="@drawable/circle_tintable"
android:backgroundTint="@color/signal_colorSurface3"
app:layout_constraintBottom_toBottomOf="@+id/bullet_2"
app:layout_constraintEnd_toEndOf="@+id/bullet_2"
app:layout_constraintStart_toStartOf="@+id/bullet_2"
app:layout_constraintTop_toTopOf="@+id/bullet_2" />
<TextView
android:id="@+id/bullet_2"
android:layout_width="0dp"
android:layout_height="wrap_content"
android:layout_marginStart="32dp"
android:layout_marginTop="24dp"
android:gravity="center"
android:minWidth="28dp"
android:minHeight="28dp"
android:padding="4dp"
android:text="@string/ChooseANewDefaultSmsAppFragment__bullet_2"
android:textAppearance="@style/Signal.Text.BodyMedium"
android:textStyle="bold"
app:layout_constraintDimensionRatio="1:1"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintTop_toBottomOf="@id/bullet_1_barrier" />
<TextView
android:id="@+id/bullet_2_text"
android:layout_width="0dp"
android:layout_height="wrap_content"
android:layout_marginStart="8dp"
android:layout_marginTop="23dp"
android:layout_marginEnd="32dp"
android:minWidth="28dp"
android:minHeight="28dp"
android:padding="4dp"
android:text="@string/ExportSmsPartiallyComplete__retry_export_which_will_only_retry_messages_that_have_not_yet_been_exported"
android:textAlignment="viewStart"
android:textAppearance="@style/Signal.Text.BodyLarge"
app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintStart_toEndOf="@+id/bullet_2"
app:layout_constraintTop_toBottomOf="@id/bullet_1_barrier" />
<androidx.constraintlayout.widget.Barrier
android:id="@+id/bullet_2_barrier"
android:layout_width="match_parent"
android:layout_height="1dp"
android:orientation="horizontal"
app:barrierDirection="bottom"
app:constraint_referenced_ids="bullet_2,bullet_2_text" />
<View
android:layout_width="0dp"
android:layout_height="0dp"
android:background="@drawable/circle_tintable"
android:backgroundTint="@color/signal_colorSurface3"
app:layout_constraintBottom_toBottomOf="@+id/bullet_3"
app:layout_constraintEnd_toEndOf="@+id/bullet_3"
app:layout_constraintStart_toStartOf="@+id/bullet_3"
app:layout_constraintTop_toTopOf="@+id/bullet_3" />
<TextView
android:id="@+id/bullet_3"
android:layout_width="0dp"
android:layout_height="wrap_content"
android:layout_marginStart="32dp"
android:layout_marginTop="24dp"
android:gravity="center"
android:minWidth="28dp"
android:minHeight="28dp"
android:padding="4dp"
android:text="@string/ChooseANewDefaultSmsAppFragment__bullet_3"
android:textAppearance="@style/Signal.Text.BodyMedium"
android:textStyle="bold"
app:layout_constraintDimensionRatio="1:1"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintTop_toBottomOf="@id/bullet_2_barrier" />
<org.thoughtcrime.securesms.util.views.LearnMoreTextView
android:id="@+id/bullet_3_text"
android:layout_width="0dp"
android:layout_height="wrap_content"
android:layout_marginStart="8dp"
android:layout_marginTop="23dp"
android:layout_marginEnd="32dp"
android:minWidth="28dp"
android:minHeight="28dp"
android:padding="4dp"
android:text="@string/ExportSmsPartiallyComplete__if_the_problem_persists"
android:textAlignment="viewStart"
android:textAppearance="@style/Signal.Text.BodyLarge"
app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintStart_toEndOf="@+id/bullet_3"
app:layout_constraintTop_toBottomOf="@id/bullet_2_barrier" />
<androidx.constraintlayout.widget.Barrier
android:id="@+id/bullet_3_barrier"
android:layout_width="match_parent"
android:layout_height="1dp"
android:orientation="horizontal"
app:barrierDirection="bottom"
app:constraint_referenced_ids="bullet_3,bullet_3_text" />
<com.google.android.material.button.MaterialButton
android:id="@+id/retry_button"
style="@style/Signal.Widget.Button.Large.Tonal"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_marginHorizontal="32dp"
android:layout_marginTop="48dp"
android:layout_marginBottom="16dp"
android:minWidth="220dp"
android:text="@string/ExportSmsPartiallyComplete__retry"
app:layout_constraintBottom_toTopOf="@+id/continue_button"
app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintTop_toBottomOf="@id/bullet_3_barrier"
app:layout_constraintVertical_bias="1" />
<com.google.android.material.button.MaterialButton
android:id="@+id/continue_button"
style="@style/Signal.Widget.Button.Large.Secondary"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_marginHorizontal="32dp"
android:layout_marginTop="48dp"
android:layout_marginBottom="44dp"
android:minWidth="220dp"
android:text="@string/ExportSmsPartiallyComplete__continue_anyway"
app:layout_constraintBottom_toBottomOf="parent"
app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintTop_toBottomOf="@id/bullet_3_barrier"
app:layout_constraintVertical_bias="1" />
</androidx.constraintlayout.widget.ConstraintLayout>
</androidx.core.widget.NestedScrollView>
</LinearLayout>

View file

@ -1,77 +0,0 @@
<?xml version="1.0" encoding="utf-8"?>
<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:app="http://schemas.android.com/apk/res-auto"
android:layout_width="match_parent"
android:layout_height="match_parent"
android:orientation="vertical">
<androidx.appcompat.widget.Toolbar
android:id="@+id/toolbar"
android:layout_width="match_parent"
android:layout_height="64dp"
android:minHeight="64dp"
app:navigationIcon="@drawable/ic_arrow_left_24" />
<androidx.core.widget.NestedScrollView
android:id="@+id/scroll_view"
android:layout_width="match_parent"
android:layout_height="match_parent"
android:fillViewport="true">
<androidx.constraintlayout.widget.ConstraintLayout
android:layout_width="match_parent"
android:layout_height="wrap_content">
<androidx.appcompat.widget.AppCompatImageView
android:id="@+id/image"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:layout_marginTop="20dp"
android:importantForAccessibility="no"
android:scaleType="centerInside"
app:layout_constraintTop_toTopOf="parent"
app:srcCompat="@drawable/sms_message" />
<TextView
android:id="@+id/headline"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:layout_marginHorizontal="32dp"
android:layout_marginTop="40dp"
android:gravity="center"
android:text="@string/ExportYourSmsMessagesFragment__export_your_sms_messages"
android:textAppearance="@style/Signal.Text.HeadlineLarge"
app:layout_constraintTop_toBottomOf="@id/image" />
<TextView
android:id="@+id/description"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:layout_marginHorizontal="32dp"
android:layout_marginTop="24dp"
android:gravity="center"
android:text="@string/ExportYourSmsMessagesFragment__you_can_export_your_sms_messages_to_your_phones_sms_database_and_youll_have_the_option_to_keep_or_remove_them_from_signal"
android:textAppearance="@style/Signal.Text.BodyLarge"
android:textColor="@color/signal_colorOnSurfaceVariant"
app:layout_constraintTop_toBottomOf="@id/headline" />
<com.google.android.material.button.MaterialButton
android:id="@+id/continue_button"
style="@style/Signal.Widget.Button.Large.Tonal"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_marginTop="48dp"
android:layout_marginBottom="44dp"
android:minWidth="220dp"
android:text="@string/ExportYourSmsMessagesFragment__continue"
app:layout_constraintBottom_toBottomOf="parent"
app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintTop_toBottomOf="@id/description"
app:layout_constraintVertical_bias="1" />
</androidx.constraintlayout.widget.ConstraintLayout>
</androidx.core.widget.NestedScrollView>
</LinearLayout>

View file

@ -1,57 +0,0 @@
<?xml version="1.0" encoding="utf-8"?>
<androidx.constraintlayout.widget.ConstraintLayout xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:app="http://schemas.android.com/apk/res-auto"
xmlns:tools="http://schemas.android.com/tools"
android:layout_width="match_parent"
android:layout_height="match_parent">
<TextView
android:id="@+id/headline"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:layout_marginHorizontal="32dp"
android:layout_marginTop="64dp"
android:gravity="center"
android:text="@string/ExportingSmsMessagesFragment__exporting_sms_messages"
android:textAppearance="@style/Signal.Text.HeadlineLarge"
app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintTop_toTopOf="parent" />
<TextView
android:id="@+id/description"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:layout_marginHorizontal="32dp"
android:layout_marginTop="24dp"
android:gravity="center"
android:text="@string/ExportingSmsMessagesFragment__this_may_take_awhile"
android:textAppearance="@style/Signal.Text.BodyLarge"
android:textColor="@color/signal_colorOnSurfaceVariant"
app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintTop_toBottomOf="@id/headline" />
<com.google.android.material.progressindicator.LinearProgressIndicator
android:id="@+id/progress"
android:layout_width="0dp"
android:layout_height="wrap_content"
app:layout_constraintBottom_toBottomOf="parent"
app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintTop_toTopOf="parent"
app:layout_constraintWidth_max="260dp" />
<TextView
android:id="@+id/progress_label"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:layout_marginHorizontal="32dp"
android:layout_marginTop="11dp"
android:gravity="center"
android:textAppearance="@style/Signal.Text.BodySmall"
android:textColor="@color/signal_colorOnSurfaceVariant"
app:layout_constraintTop_toBottomOf="@id/progress"
tools:text="Exporting 5 of 264..." />
</androidx.constraintlayout.widget.ConstraintLayout>

View file

@ -1,63 +0,0 @@
<?xml version="1.0" encoding="utf-8"?>
<androidx.constraintlayout.widget.ConstraintLayout xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:app="http://schemas.android.com/apk/res-auto"
android:layout_width="match_parent"
android:layout_height="match_parent">
<androidx.appcompat.widget.Toolbar
android:id="@+id/toolbar"
android:layout_width="match_parent"
android:layout_height="64dp"
android:minHeight="64dp"
app:layout_constraintTop_toTopOf="parent"
app:navigationIcon="@drawable/ic_arrow_left_24" />
<androidx.appcompat.widget.AppCompatImageView
android:id="@+id/image"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:layout_marginTop="70dp"
android:importantForAccessibility="no"
android:scaleType="centerInside"
app:layout_constraintBottom_toTopOf="@id/continue_button"
app:layout_constraintTop_toBottomOf="@id/description"
app:layout_constraintVertical_bias="0"
app:srcCompat="@drawable/choose_signal" />
<TextView
android:id="@+id/headline"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:layout_marginHorizontal="32dp"
android:gravity="center"
android:text="@string/SetSignalAsDefaultSmsAppFragment__set_signal_as_the_default_sms_app"
android:textAppearance="@style/Signal.Text.HeadlineLarge"
app:layout_constraintTop_toBottomOf="@id/toolbar" />
<TextView
android:id="@+id/description"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:layout_marginHorizontal="32dp"
android:layout_marginTop="24dp"
android:gravity="center"
android:text="@string/SetSignalAsDefaultSmsAppFragment__to_export_your_sms_messages"
android:textAppearance="@style/Signal.Text.BodyLarge"
android:textColor="@color/signal_colorOnSurfaceVariant"
app:layout_constraintTop_toBottomOf="@id/headline" />
<com.google.android.material.button.MaterialButton
android:id="@+id/continue_button"
style="@style/Signal.Widget.Button.Large.Tonal"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_marginBottom="44dp"
android:minWidth="221dp"
android:text="@string/SetSignalAsDefaultSmsAppFragment__next"
app:layout_constraintBottom_toBottomOf="parent"
app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintTop_toBottomOf="@id/description"
app:layout_constraintVertical_bias="1" />
</androidx.constraintlayout.widget.ConstraintLayout>

View file

@ -1,176 +0,0 @@
<?xml version="1.0" encoding="utf-8"?>
<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:app="http://schemas.android.com/apk/res-auto"
xmlns:tools="http://schemas.android.com/tools"
android:layout_width="match_parent"
android:layout_height="match_parent"
android:orientation="vertical">
<androidx.appcompat.widget.Toolbar
android:id="@+id/toolbar"
android:layout_width="match_parent"
android:layout_height="64dp"
android:minHeight="64dp"
app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintTop_toTopOf="parent"
app:navigationIcon="@drawable/ic_arrow_left_24" />
<ScrollView
android:layout_width="match_parent"
android:layout_height="match_parent"
android:fillViewport="true">
<androidx.constraintlayout.widget.ConstraintLayout
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:paddingHorizontal="32dp">
<TextView
android:id="@+id/headline"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:gravity="center"
android:text="@string/SmsRemoval_title_going_away"
android:textAppearance="@style/Signal.Text.HeadlineLarge"
app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintTop_toTopOf="parent" />
<androidx.appcompat.widget.AppCompatImageView
android:id="@+id/bullet_1"
android:layout_width="56dp"
android:layout_height="56dp"
android:layout_marginTop="32dp"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintTop_toBottomOf="@id/headline"
app:srcCompat="@drawable/sms_small_insecure" />
<TextView
android:id="@+id/bullet_1_text"
android:layout_width="0dp"
android:layout_height="wrap_content"
android:layout_marginStart="16dp"
android:layout_marginTop="32dp"
android:textAlignment="viewStart"
android:textAppearance="@style/Signal.Text.BodyLarge"
android:textColor="@color/signal_colorOnSurfaceVariant"
app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintStart_toEndOf="@+id/bullet_1"
app:layout_constraintTop_toBottomOf="@id/headline"
tools:text="@string/SmsRemoval_info_bullet_1_s" />
<androidx.constraintlayout.widget.Barrier
android:id="@+id/bullet_1_barrier"
android:layout_width="match_parent"
android:layout_height="1dp"
android:orientation="horizontal"
app:barrierDirection="bottom"
app:constraint_referenced_ids="bullet_1,bullet_1_text" />
<androidx.appcompat.widget.AppCompatImageView
android:id="@+id/bullet_2"
android:layout_width="56dp"
android:layout_height="56dp"
android:layout_marginTop="32dp"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintTop_toBottomOf="@id/bullet_1_barrier"
app:srcCompat="@drawable/sms_small_encrypted" />
<TextView
android:id="@+id/bullet_2_text"
android:layout_width="0dp"
android:layout_height="wrap_content"
android:layout_marginStart="16dp"
android:layout_marginTop="32dp"
android:text="@string/SmsRemoval_info_bullet_2"
android:textAlignment="viewStart"
android:textAppearance="@style/Signal.Text.BodyLarge"
android:textColor="@color/signal_colorOnSurfaceVariant"
app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintStart_toEndOf="@+id/bullet_2"
app:layout_constraintTop_toBottomOf="@id/bullet_1_barrier" />
<androidx.constraintlayout.widget.Barrier
android:id="@+id/bullet_2_barrier"
android:layout_width="match_parent"
android:layout_height="1dp"
android:orientation="horizontal"
app:barrierDirection="bottom"
app:constraint_referenced_ids="bullet_2,bullet_2_text" />
<androidx.appcompat.widget.AppCompatImageView
android:id="@+id/bullet_3"
android:layout_width="56dp"
android:layout_height="56dp"
android:layout_marginTop="32dp"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintTop_toBottomOf="@id/bullet_2_barrier"
app:srcCompat="@drawable/sms_small_export" />
<TextView
android:id="@+id/bullet_3_text"
android:layout_width="0dp"
android:layout_height="wrap_content"
android:layout_marginStart="16dp"
android:layout_marginTop="32dp"
android:text="@string/SmsRemoval_info_bullet_3"
android:textAlignment="viewStart"
android:textAppearance="@style/Signal.Text.BodyLarge"
android:textColor="@color/signal_colorOnSurfaceVariant"
app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintStart_toEndOf="@+id/bullet_3"
app:layout_constraintTop_toBottomOf="@id/bullet_2_barrier" />
<androidx.constraintlayout.widget.Barrier
android:id="@+id/bullet_3_barrier"
android:layout_width="match_parent"
android:layout_height="1dp"
android:orientation="horizontal"
app:barrierDirection="bottom"
app:constraint_referenced_ids="bullet_3,bullet_3_text" />
<com.google.android.material.button.MaterialButton
android:id="@+id/learn_more_button"
style="@style/Signal.Widget.Button.Medium.Secondary"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_marginTop="32dp"
android:text="@string/LearnMoreTextView_learn_more"
android:textColor="@color/signal_colorPrimary"
app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintTop_toBottomOf="@+id/bullet_3_barrier" />
<com.google.android.material.button.MaterialButton
android:id="@+id/export_sms_button"
style="@style/Signal.Widget.Button.Large.Tonal"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_marginTop="48dp"
android:layout_marginBottom="16dp"
android:minWidth="220dp"
android:text="@string/SmsRemoval_export_sms"
app:layout_constraintBottom_toTopOf="@+id/later_button"
app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintTop_toBottomOf="@id/learn_more_button"
app:layout_constraintVertical_bias="1" />
<com.google.android.material.button.MaterialButton
android:id="@+id/later_button"
style="@style/Signal.Widget.Button.Large.Secondary"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_marginBottom="24dp"
android:minWidth="221dp"
android:text="@string/SmsExportMegaphoneActivity__remind_me_later"
android:textColor="@color/signal_colorOnSurfaceVariant"
app:layout_constraintBottom_toBottomOf="parent"
app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintVertical_bias="1" />
</androidx.constraintlayout.widget.ConstraintLayout>
</ScrollView>
</LinearLayout>

View file

@ -1,186 +0,0 @@
<?xml version="1.0" encoding="utf-8"?>
<navigation xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:app="http://schemas.android.com/apk/res-auto"
xmlns:tools="http://schemas.android.com/tools"
android:id="@+id/sms_export"
app:startDestination="@id/smsRemovalInformationFragment">
<fragment
android:id="@+id/smsRemovalInformationFragment"
android:name="org.thoughtcrime.securesms.exporter.flow.SmsRemovalInformationFragment"
tools:layout="@layout/export_your_sms_messages_fragment">
<action
android:id="@+id/action_smsRemovalInformationFragment_to_exportYourSmsMessagesFragment"
app:destination="@id/exportYourSmsMessagesFragment"
app:enterAnim="@anim/fragment_open_enter"
app:exitAnim="@anim/fragment_open_exit"
app:popEnterAnim="@anim/fragment_close_enter"
app:popExitAnim="@anim/fragment_close_exit"
app:popUpTo="@id/sms_export"
app:popUpToInclusive="true" />
</fragment>
<fragment
android:id="@+id/exportYourSmsMessagesFragment"
android:name="org.thoughtcrime.securesms.exporter.flow.ExportYourSmsMessagesFragment"
android:label="fragment_export_your_sms_messages"
tools:layout="@layout/export_your_sms_messages_fragment">
<action
android:id="@+id/action_exportYourSmsMessagesFragment_to_exportingSmsMessagesFragment"
app:destination="@id/exportingSmsMessagesFragment"
app:enterAnim="@anim/fragment_open_enter"
app:exitAnim="@anim/fragment_open_exit"
app:popEnterAnim="@anim/fragment_close_enter"
app:popExitAnim="@anim/fragment_close_exit"
app:popUpTo="@id/sms_export"
app:popUpToInclusive="true" />
<action
android:id="@+id/action_exportYourSmsMessagesFragment_to_setSignalAsDefaultSmsAppFragment"
app:destination="@id/setSignalAsDefaultSmsAppFragment"
app:enterAnim="@anim/fragment_open_enter"
app:exitAnim="@anim/fragment_open_exit"
app:popEnterAnim="@anim/fragment_close_enter"
app:popExitAnim="@anim/fragment_close_exit" />
</fragment>
<fragment
android:id="@+id/setSignalAsDefaultSmsAppFragment"
android:name="org.thoughtcrime.securesms.exporter.flow.SetSignalAsDefaultSmsAppFragment"
android:label="fragment_set_signal_as_default_sms_app"
tools:layout="@layout/choose_a_new_default_sms_app_fragment">
<action
android:id="@+id/action_setSignalAsDefaultSmsAppFragment_to_exportingSmsMessagesFragment"
app:destination="@id/exportingSmsMessagesFragment"
app:enterAnim="@anim/fragment_open_enter"
app:exitAnim="@anim/fragment_open_exit"
app:popEnterAnim="@anim/fragment_close_enter"
app:popExitAnim="@anim/fragment_close_exit"
app:popUpTo="@+id/exportYourSmsMessagesFragment"
app:popUpToInclusive="true" />
</fragment>
<fragment
android:id="@+id/exportingSmsMessagesFragment"
android:name="org.thoughtcrime.securesms.exporter.flow.ExportingSmsMessagesFragment"
android:label="fragment_exporting_sms_messages"
tools:layout="@layout/exporting_sms_messages_fragment">
<action
android:id="@+id/action_exportingSmsMessagesFragment_to_exportSmsCompleteFragment"
app:destination="@id/exportSmsCompleteFragment"
app:enterAnim="@anim/fragment_open_enter"
app:exitAnim="@anim/fragment_open_exit"
app:popEnterAnim="@anim/fragment_close_enter"
app:popExitAnim="@anim/fragment_close_exit"
app:popUpTo="@+id/sms_export"
app:popUpToInclusive="true" />
<action
android:id="@+id/action_exportingSmsMessagesFragment_to_exportSmsPartiallyCompleteFragment"
app:destination="@id/exportSmsPartiallyCompleteFragment"
app:enterAnim="@anim/fragment_open_enter"
app:exitAnim="@anim/fragment_open_exit"
app:popEnterAnim="@anim/fragment_close_enter"
app:popExitAnim="@anim/fragment_close_exit"
app:popUpTo="@id/sms_export"
app:popUpToInclusive="true" />
<action
android:id="@+id/action_exportingSmsMessagesFragment_to_exportSmsFullErrorFragment"
app:destination="@id/exportSmsFullErrorFragment"
app:enterAnim="@anim/fragment_open_enter"
app:exitAnim="@anim/fragment_open_exit"
app:popEnterAnim="@anim/fragment_close_enter"
app:popExitAnim="@anim/fragment_close_exit"
app:popUpTo="@id/sms_export"
app:popUpToInclusive="true" />
</fragment>
<fragment
android:id="@+id/exportSmsCompleteFragment"
android:name="org.thoughtcrime.securesms.exporter.flow.ExportSmsCompleteFragment"
tools:layout="@layout/export_sms_complete_fragment">
<argument
android:name="export_message_count"
app:argType="integer" />
<argument
android:name="export_message_failure_count"
app:argType="integer" />
</fragment>
<fragment
android:id="@+id/chooseANewDefaultSmsAppFragment"
android:name="org.thoughtcrime.securesms.exporter.flow.ChooseANewDefaultSmsAppFragment"
android:label="fragment_choose_a_new_default_sms_app"
tools:layout="@layout/choose_a_new_default_sms_app_fragment" />
<fragment
android:id="@+id/exportSmsPartiallyCompleteFragment"
android:name="org.thoughtcrime.securesms.exporter.flow.ExportSmsPartiallyCompleteFragment"
tools:layout="@layout/export_sms_partially_complete_fragment">
<argument
android:name="export_message_count"
app:argType="integer" />
<argument
android:name="export_message_failure_count"
app:argType="integer" />
</fragment>
<fragment
android:id="@+id/exportSmsFullErrorFragment"
android:name="org.thoughtcrime.securesms.exporter.flow.ExportSmsFullErrorFragment"
tools:layout="@layout/export_sms_full_error_fragment">
<argument
android:name="export_message_count"
app:argType="integer" />
<argument
android:name="export_message_failure_count"
app:argType="integer" />
</fragment>
<fragment
android:id="@+id/smsExportHelpFragment"
android:name="org.thoughtcrime.securesms.exporter.flow.SmsExportHelpFragment"
tools:layout="@layout/sms_export_help_fragment" />
<action
android:id="@+id/action_direct_to_exportYourSmsMessagesFragment"
app:destination="@id/exportYourSmsMessagesFragment"
app:enterAnim="@anim/fragment_open_enter"
app:exitAnim="@anim/fragment_open_exit"
app:popEnterAnim="@anim/fragment_close_enter"
app:popExitAnim="@anim/fragment_close_exit"
app:popUpTo="@id/sms_export"
app:popUpToInclusive="true" />
<action
android:id="@+id/action_direct_to_chooseANewDefaultSmsAppFragment"
app:destination="@id/chooseANewDefaultSmsAppFragment"
app:enterAnim="@anim/fragment_open_enter"
app:exitAnim="@anim/fragment_open_exit"
app:popEnterAnim="@anim/fragment_close_enter"
app:popExitAnim="@anim/fragment_close_exit"
app:popUpTo="@+id/sms_export"
app:popUpToInclusive="true" />
<action
android:id="@+id/action_direct_to_helpFragment"
app:destination="@id/smsExportHelpFragment"
app:enterAnim="@anim/fragment_open_enter"
app:exitAnim="@anim/fragment_open_exit"
app:popEnterAnim="@anim/fragment_close_enter"
app:popExitAnim="@anim/fragment_close_exit" />
</navigation>

View file

@ -4417,27 +4417,6 @@
<string name="ChatsSettingsFragment__keyboard">Keyboard</string>
<string name="ChatsSettingsFragment__enter_key_sends">Enter key sends</string>
<!--SmsSettingsFragment -->
<string name="SmsSettingsFragment__use_as_default_sms_app">Use as default SMS app</string>
<!-- Preference title to export sms -->
<string name="SmsSettingsFragment__export_sms_messages">Export SMS messages</string>
<!-- Preference title to re-export sms -->
<string name="SmsSettingsFragment__export_sms_messages_again">Export SMS messages again</string>
<!-- Preference title to delete sms -->
<string name="SmsSettingsFragment__remove_sms_messages">Remove SMS messages</string>
<!-- Snackbar text to confirm deletion -->
<string name="SmsSettingsFragment__removing_sms_messages_from_signal">Removing SMS messages from Signal…</string>
<!-- Snackbar text to indicate can delete later -->
<string name="SmsSettingsFragment__you_can_remove_sms_messages_from_signal_in_settings">You can remove SMS messages from Signal in Settings at any time.</string>
<!-- Description for export sms preference -->
<string name="SmsSettingsFragment__you_can_export_your_sms_messages_to_your_phones_sms_database">You can export your SMS messages to your phone\'s SMS database</string>
<!-- Description for re-export sms preference -->
<string name="SmsSettingsFragment__exporting_again_can_result_in_duplicate_messages">Exporting again can result in duplicate messages.</string>
<!-- Description for remove sms preference -->
<string name="SmsSettingsFragment__remove_sms_messages_from_signal_to_clear_up_storage_space">Remove SMS messages from Signal to clear up storage space.</string>
<!-- Information message shown at the top of sms settings to indicate it is being removed soon. -->
<string name="SmsSettingsFragment__sms_support_will_be_removed_soon_to_focus_on_encrypted_messaging">SMS support will be removed soon to focus on encrypted messaging.</string>
<!-- NotificationsSettingsFragment -->
<string name="NotificationsSettingsFragment__messages">Messages</string>
<string name="NotificationsSettingsFragment__calls">Calls</string>
@ -5828,48 +5807,6 @@
<!-- Generic title for overflow menus -->
<string name="OverflowMenu__overflow_menu">Overflow menu</string>
<!-- SMS Export Service -->
<!-- Displayed in the notification while export is running -->
<string name="SignalSmsExportService__exporting_messages">Exporting messages…</string>
<!-- Displayed in the notification title when export completes -->
<string name="SignalSmsExportService__signal_sms_export_complete">Signal SMS Export Complete</string>
<!-- Displayed in the notification message when export completes -->
<string name="SignalSmsExportService__tap_to_return_to_signal">Tap to return to Signal</string>
<!-- ExportYourSmsMessagesFragment -->
<!-- Title of the screen -->
<string name="ExportYourSmsMessagesFragment__export_your_sms_messages">Export your SMS messages</string>
<!-- Message of the screen -->
<string name="ExportYourSmsMessagesFragment__you_can_export_your_sms_messages_to_your_phones_sms_database_and_youll_have_the_option_to_keep_or_remove_them_from_signal">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.</string>
<!-- Button label to begin export -->
<string name="ExportYourSmsMessagesFragment__continue">Continue</string>
<!-- ExportingSmsMessagesFragment -->
<!-- Title of the screen -->
<string name="ExportingSmsMessagesFragment__exporting_sms_messages">Exporting SMS messages</string>
<!-- Message of the screen when exporting sms messages -->
<string name="ExportingSmsMessagesFragment__this_may_take_awhile">This may take a while</string>
<!-- Progress indicator for export -->
<plurals name="ExportingSmsMessagesFragment__exporting_d_of_d">
<item quantity="one">Exporting %1$d of %2$d…</item>
<item quantity="other">Exporting %1$d of %2$d…</item>
</plurals>
<!-- Alert dialog title shown when we think a user may not have enough local storage available to export sms messages -->
<string name="ExportingSmsMessagesFragment__you_may_not_have_enough_disk_space">You may not have enough disk space</string>
<!-- Alert dialog message shown when we think a user may not have enough local storage available to export sms messages, placeholder is the file size, e.g., 128kB -->
<string name="ExportingSmsMessagesFragment__you_need_approximately_s_to_export_your_messages_ensure_you_have_enough_space_before_continuing">You need approximately %1$s to export your messages, ensure you have enough space before continuing.</string>
<!-- Alert dialog button to continue with exporting sms after seeing the lack of storage warning -->
<string name="ExportingSmsMessagesFragment__continue_anyway">Continue anyway</string>
<!-- Dialog text shown when Signal isn\'t granted the sms permission needed to export messages, different than being selected as the sms app -->
<string name="ExportingSmsMessagesFragment__signal_needs_the_sms_permission_to_be_able_to_export_your_sms_messages">Signal needs the SMS permission to be able to export your SMS messages.</string>
<!-- ChooseANewDefaultSmsAppFragment -->
<!-- Title of the screen -->
<string name="ChooseANewDefaultSmsAppFragment__choose_a_new">Choose a new default SMS app</string>
<!-- Button label to launch picker -->
<string name="ChooseANewDefaultSmsAppFragment__continue">Continue</string>
<!-- Button label for when done with changing default SMS app -->
<string name="ChooseANewDefaultSmsAppFragment__done">Done</string>
<!-- First step number/bullet for choose new default sms app instructions -->
<string name="ChooseANewDefaultSmsAppFragment__bullet_1">1</string>
<!-- Second step number/bullet for choose new default sms app instructions -->
@ -5878,47 +5815,6 @@
<string name="ChooseANewDefaultSmsAppFragment__bullet_3">3</string>
<!-- Fourth step number/bullet for choose new default sms app instructions -->
<string name="ChooseANewDefaultSmsAppFragment__bullet_4">4</string>
<!-- Instruction step for choosing a new default sms app -->
<string name="ChooseANewDefaultSmsAppFragment__tap_continue_to_open_the_defaults_apps_screen_in_settings">Tap \"Continue\" to open the \"Default apps\" screen in Settings</string>
<!-- Instruction step for choosing a new default sms app -->
<string name="ChooseANewDefaultSmsAppFragment__select_sms_app_from_the_list">Select \"SMS app\" from the list</string>
<!-- Instruction step for choosing a new default sms app -->
<string name="ChooseANewDefaultSmsAppFragment__choose_another_app_to_use_for_sms_messaging">Choose another app to use for SMS messaging</string>
<!-- Instruction step for choosing a new default sms app -->
<string name="ChooseANewDefaultSmsAppFragment__return_to_signal">Return to Signal</string>
<!-- Instruction step for choosing a new default sms app -->
<string name="ChooseANewDefaultSmsAppFragment__open_your_phones_settings_app">Open your phone\'s Settings app</string>
<!-- Instruction step for choosing a new default sms app -->
<string name="ChooseANewDefaultSmsAppFragment__navigate_to_apps_default_apps_sms_app">Navigate to \"Apps\" > \"Default apps\" > \"SMS app\"</string>
<!-- RemoveSmsMessagesDialogFragment -->
<!-- Action button to keep messages -->
<string name="RemoveSmsMessagesDialogFragment__keep_messages">Keep messages</string>
<!-- Action button to remove messages -->
<string name="RemoveSmsMessagesDialogFragment__remove_messages">Remove messages</string>
<!-- Title of dialog -->
<string name="RemoveSmsMessagesDialogFragment__remove_sms_messages">Remove SMS messages from Signal?</string>
<!-- Message of dialog -->
<string name="RemoveSmsMessagesDialogFragment__you_can_now_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.</string>
<!-- ReExportSmsMessagesDialogFragment -->
<!-- Action button to re-export messages -->
<string name="ReExportSmsMessagesDialogFragment__continue">Continue</string>
<!-- Action button to cancel re-export process -->
<string name="ReExportSmsMessagesDialogFragment__cancel">Cancel</string>
<!-- Title of dialog -->
<string name="ReExportSmsMessagesDialogFragment__export_sms_again">Export SMS again?</string>
<!-- Message of dialog -->
<string name="ReExportSmsMessagesDialogFragment__you_already_exported_your_sms_messages">You already exported your SMS messages.\nWARNING: If you continue, you may end up with duplicate messages.</string>
<!-- SetSignalAsDefaultSmsAppFragment -->
<!-- Title of the screen -->
<string name="SetSignalAsDefaultSmsAppFragment__set_signal_as_the_default_sms_app">Set Signal as the default SMS app</string>
<!-- Message of the screen -->
<string name="SetSignalAsDefaultSmsAppFragment__to_export_your_sms_messages">To export your SMS messages, you need to set Signal as the default SMS app.</string>
<!-- Button label to start export -->
<string name="SetSignalAsDefaultSmsAppFragment__next">Next</string>
<!-- BackupSchedulePermission Megaphone -->
<!-- The title on an alert window that explains to the user that we are unable to backup their messages -->
<string name="BackupSchedulePermissionMegaphone__cant_back_up_chats">Can\'t back up chats</string>
@ -5937,45 +5833,6 @@
<!-- Re-enable backups permission bottom sheet call to action button to open settings -->
<string name="BackupSchedulePermissionMegaphone__go_to_settings">Go to settings</string>
<!-- SmsExportMegaphoneActivity -->
<!-- Phase 2 title of full screen megaphone indicating sms will no longer be supported in the near future -->
<string name="SmsExportMegaphoneActivity__signal_will_no_longer_support_sms">Signal will no longer support SMS</string>
<!-- Phase 3 title of full screen megaphone indicating sms is longer supported -->
<string name="SmsExportMegaphoneActivity__signal_no_longer_supports_sms">Signal no longer supports SMS</string>
<!-- The text on a button in a popup that, when clicked, will dismiss the popup and schedule the prompt to occur at a later time. -->
<string name="SmsExportMegaphoneActivity__remind_me_later">Remind me later</string>
<!-- The text on a button in a popup that, when clicked, will navigate the user to a web article on SMS removal -->
<!-- Title for screen shown after sms export has completed -->
<string name="ExportSmsCompleteFragment__export_complete">Export Complete</string>
<!-- Button to continue to next screen -->
<string name="ExportSmsCompleteFragment__next">Next</string>
<!-- Message showing summary of sms export counts -->
<plurals name="ExportSmsCompleteFragment__d_of_d_messages_exported">
<item quantity="one">%1$d of %2$d message exported</item>
<item quantity="other">%1$d of %2$d messages exported</item>
</plurals>
<!-- Title of screen shown when some sms messages did not export -->
<string name="ExportSmsPartiallyComplete__export_partially_complete">Export partially complete</string>
<!-- Debug step 1 on screen shown when some sms messages did not export -->
<string name="ExportSmsPartiallyComplete__ensure_you_have_an_additional_s_free_on_your_phone_to_export_your_messages">Ensure you have an additional %1$s free on your phone to export your messages</string>
<!-- Debug step 2 on screen shown when some sms messages dit not export -->
<string name="ExportSmsPartiallyComplete__retry_export_which_will_only_retry_messages_that_have_not_yet_been_exported">Retry export, which will only retry messages that have not yet been exported</string>
<!-- Partial sentence for Debug step 3 on screen shown when some sms messages did not export, is combined with \'contact us\' -->
<string name="ExportSmsPartiallyComplete__if_the_problem_persists">If the problem persists, </string>
<!-- Partial sentence for deubg step 3 on screen shown when some sms messages did not export, combined with \'If the problem persists\', link text to open contact support view -->
<string name="ExportSmsPartiallyComplete__contact_us">contact us</string>
<!-- Button text to retry sms export -->
<string name="ExportSmsPartiallyComplete__retry">Retry</string>
<!-- Button text to continue sms export flow and not retry failed message exports -->
<string name="ExportSmsPartiallyComplete__continue_anyway">Continue anyway</string>
<!-- Title of screen shown when all sms messages failed to export -->
<string name="ExportSmsFullError__error_exporting_sms_messages">Error exporting SMS messages</string>
<!-- Helper text shown when all sms messages failed to export -->
<string name="ExportSmsFullError__please_try_again_if_the_problem_persists">Please try again. If the problem persists, </string>
<!-- DonateToSignalFragment -->
<!-- Title below avatar -->
<string name="DonateToSignalFragment__privacy_over_profit">Privacy over profit</string>
@ -6220,19 +6077,6 @@
<!-- Displayed in the "clear filter" item in the chat feed if the user opened the filter from the overflow menu -->
<string name="ChatFilter__tip_pull_down">Tip: Pull down on the chat list to filter</string>
<!-- Title for screen describing that sms support is going to be removed soon -->
<string name="SmsRemoval_title_going_away">SMS support is going away</string>
<!-- Bullet point message shown on describing screen as first bullet why sms is being removed, placeholder with be date of removal (e.g., March 21st) -->
<string name="SmsRemoval_info_bullet_1">SMS messaging in the Signal app will soon no longer be supported.</string>
<!-- Bullet point message shown on describing screen as second bullet why sms is being removed -->
<string name="SmsRemoval_info_bullet_2">SMS messages are different than Signal messages. <b>This does not affect encrypted Signal messaging which will continue to work.</b></string>
<!-- Bullet point message shown on describing screen as third bullet why sms is being removed -->
<string name="SmsRemoval_info_bullet_3">You can export your SMS messages and choose a new SMS app.</string>
<!-- Bullet point message shown on describing screen as first bullet variant why sms is being removed when user is locked out of sms -->
<string name="SmsRemoval_info_bullet_1_phase_3">Signal has removed support for sending SMS messages.</string>
<!-- Button label on sms removal info/megaphone to start the export SMS flow -->
<string name="SmsRemoval_export_sms">Export SMS</string>
<!-- Set up your username megaphone -->
<!-- Displayed as a title on a megaphone which prompts user to set up a username -->
<string name="SetUpYourUsername__set_up_your_signal_username">Set up your Signal username</string>

View file

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

View file

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

View file

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

View file

@ -1,65 +0,0 @@
<?xml version="1.0" encoding="utf-8"?>
<manifest xmlns:android="http://schemas.android.com/apk/res/android">
<uses-permission android:name="android.permission.READ_SMS" />
<uses-permission android:name="android.permission.WRITE_SMS" />
<uses-permission android:name="android.permission.FOREGROUND_SERVICE" />
<application
android:allowBackup="true"
android:icon="@mipmap/ic_launcher"
android:label="@string/app_name"
android:roundIcon="@mipmap/ic_launcher_round"
android:supportsRtl="true"
android:theme="@style/Theme.Signal">
<service android:name=".TestSmsExportService" android:foregroundServiceType="dataSync" />
<service
android:name=".SendResponseViaMessageService"
android:exported="true"
android:permission="android.permission.SEND_RESPOND_VIA_MESSAGE">
<intent-filter>
<action android:name="android.intent.action.RESPOND_VIA_MESSAGE" />
<category android:name="android.intent.category.DEFAULT" />
<data android:scheme="smsto" />
</intent-filter>
</service>
<receiver
android:name=".BroadcastSmsReceiver"
android:exported="true"
android:permission="android.permission.BROADCAST_SMS">
<intent-filter>
<action android:name="android.provider.Telephony.SMS_DELIVER" />
</intent-filter>
</receiver>
<receiver
android:name=".BroadcastWapPushReceiver"
android:exported="true"
android:permission="android.permission.BROADCAST_WAP_PUSH">
<intent-filter>
<action android:name="android.provider.Telephony.WAP_PUSH_DELIVER" />
<data android:mimeType="application/vnd.wap.mms-message" />
</intent-filter>
</receiver>
<activity
android:name="org.signal.smsexporter.app.MainActivity"
android:exported="true">
<intent-filter>
<action android:name="android.intent.action.MAIN" />
<category android:name="android.intent.category.LAUNCHER" />
</intent-filter>
<intent-filter>
<action android:name="android.intent.action.SENDTO" />
<category android:name="android.intent.category.DEFAULT" />
<data android:scheme="smsto" />
</intent-filter>
</activity>
</application>
</manifest>

View file

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

View file

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

View file

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

View file

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

View file

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

View file

@ -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<ExportableMessage> {
return object : Iterable<ExportableMessage> {
override fun iterator(): Iterator<ExportableMessage> {
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<ExportableMessage> {
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
)
}
}
}

View file

@ -1,30 +0,0 @@
<vector xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:aapt="http://schemas.android.com/aapt"
android:width="108dp"
android:height="108dp"
android:viewportWidth="108"
android:viewportHeight="108">
<path android:pathData="M31,63.928c0,0 6.4,-11 12.1,-13.1c7.2,-2.6 26,-1.4 26,-1.4l38.1,38.1L107,108.928l-32,-1L31,63.928z">
<aapt:attr name="android:fillColor">
<gradient
android:endX="85.84757"
android:endY="92.4963"
android:startX="42.9492"
android:startY="49.59793"
android:type="linear">
<item
android:color="#44000000"
android:offset="0.0" />
<item
android:color="#00000000"
android:offset="1.0" />
</gradient>
</aapt:attr>
</path>
<path
android:fillColor="#FFFFFF"
android:fillType="nonZero"
android:pathData="M65.3,45.828l3.8,-6.6c0.2,-0.4 0.1,-0.9 -0.3,-1.1c-0.4,-0.2 -0.9,-0.1 -1.1,0.3l-3.9,6.7c-6.3,-2.8 -13.4,-2.8 -19.7,0l-3.9,-6.7c-0.2,-0.4 -0.7,-0.5 -1.1,-0.3C38.8,38.328 38.7,38.828 38.9,39.228l3.8,6.6C36.2,49.428 31.7,56.028 31,63.928h46C76.3,56.028 71.8,49.428 65.3,45.828zM43.4,57.328c-0.8,0 -1.5,-0.5 -1.8,-1.2c-0.3,-0.7 -0.1,-1.5 0.4,-2.1c0.5,-0.5 1.4,-0.7 2.1,-0.4c0.7,0.3 1.2,1 1.2,1.8C45.3,56.528 44.5,57.328 43.4,57.328L43.4,57.328zM64.6,57.328c-0.8,0 -1.5,-0.5 -1.8,-1.2s-0.1,-1.5 0.4,-2.1c0.5,-0.5 1.4,-0.7 2.1,-0.4c0.7,0.3 1.2,1 1.2,1.8C66.5,56.528 65.6,57.328 64.6,57.328L64.6,57.328z"
android:strokeWidth="1"
android:strokeColor="#00000000" />
</vector>

View file

@ -1,170 +0,0 @@
<?xml version="1.0" encoding="utf-8"?>
<vector xmlns:android="http://schemas.android.com/apk/res/android"
android:width="108dp"
android:height="108dp"
android:viewportWidth="108"
android:viewportHeight="108">
<path
android:fillColor="#3DDC84"
android:pathData="M0,0h108v108h-108z" />
<path
android:fillColor="#00000000"
android:pathData="M9,0L9,108"
android:strokeWidth="0.8"
android:strokeColor="#33FFFFFF" />
<path
android:fillColor="#00000000"
android:pathData="M19,0L19,108"
android:strokeWidth="0.8"
android:strokeColor="#33FFFFFF" />
<path
android:fillColor="#00000000"
android:pathData="M29,0L29,108"
android:strokeWidth="0.8"
android:strokeColor="#33FFFFFF" />
<path
android:fillColor="#00000000"
android:pathData="M39,0L39,108"
android:strokeWidth="0.8"
android:strokeColor="#33FFFFFF" />
<path
android:fillColor="#00000000"
android:pathData="M49,0L49,108"
android:strokeWidth="0.8"
android:strokeColor="#33FFFFFF" />
<path
android:fillColor="#00000000"
android:pathData="M59,0L59,108"
android:strokeWidth="0.8"
android:strokeColor="#33FFFFFF" />
<path
android:fillColor="#00000000"
android:pathData="M69,0L69,108"
android:strokeWidth="0.8"
android:strokeColor="#33FFFFFF" />
<path
android:fillColor="#00000000"
android:pathData="M79,0L79,108"
android:strokeWidth="0.8"
android:strokeColor="#33FFFFFF" />
<path
android:fillColor="#00000000"
android:pathData="M89,0L89,108"
android:strokeWidth="0.8"
android:strokeColor="#33FFFFFF" />
<path
android:fillColor="#00000000"
android:pathData="M99,0L99,108"
android:strokeWidth="0.8"
android:strokeColor="#33FFFFFF" />
<path
android:fillColor="#00000000"
android:pathData="M0,9L108,9"
android:strokeWidth="0.8"
android:strokeColor="#33FFFFFF" />
<path
android:fillColor="#00000000"
android:pathData="M0,19L108,19"
android:strokeWidth="0.8"
android:strokeColor="#33FFFFFF" />
<path
android:fillColor="#00000000"
android:pathData="M0,29L108,29"
android:strokeWidth="0.8"
android:strokeColor="#33FFFFFF" />
<path
android:fillColor="#00000000"
android:pathData="M0,39L108,39"
android:strokeWidth="0.8"
android:strokeColor="#33FFFFFF" />
<path
android:fillColor="#00000000"
android:pathData="M0,49L108,49"
android:strokeWidth="0.8"
android:strokeColor="#33FFFFFF" />
<path
android:fillColor="#00000000"
android:pathData="M0,59L108,59"
android:strokeWidth="0.8"
android:strokeColor="#33FFFFFF" />
<path
android:fillColor="#00000000"
android:pathData="M0,69L108,69"
android:strokeWidth="0.8"
android:strokeColor="#33FFFFFF" />
<path
android:fillColor="#00000000"
android:pathData="M0,79L108,79"
android:strokeWidth="0.8"
android:strokeColor="#33FFFFFF" />
<path
android:fillColor="#00000000"
android:pathData="M0,89L108,89"
android:strokeWidth="0.8"
android:strokeColor="#33FFFFFF" />
<path
android:fillColor="#00000000"
android:pathData="M0,99L108,99"
android:strokeWidth="0.8"
android:strokeColor="#33FFFFFF" />
<path
android:fillColor="#00000000"
android:pathData="M19,29L89,29"
android:strokeWidth="0.8"
android:strokeColor="#33FFFFFF" />
<path
android:fillColor="#00000000"
android:pathData="M19,39L89,39"
android:strokeWidth="0.8"
android:strokeColor="#33FFFFFF" />
<path
android:fillColor="#00000000"
android:pathData="M19,49L89,49"
android:strokeWidth="0.8"
android:strokeColor="#33FFFFFF" />
<path
android:fillColor="#00000000"
android:pathData="M19,59L89,59"
android:strokeWidth="0.8"
android:strokeColor="#33FFFFFF" />
<path
android:fillColor="#00000000"
android:pathData="M19,69L89,69"
android:strokeWidth="0.8"
android:strokeColor="#33FFFFFF" />
<path
android:fillColor="#00000000"
android:pathData="M19,79L89,79"
android:strokeWidth="0.8"
android:strokeColor="#33FFFFFF" />
<path
android:fillColor="#00000000"
android:pathData="M29,19L29,89"
android:strokeWidth="0.8"
android:strokeColor="#33FFFFFF" />
<path
android:fillColor="#00000000"
android:pathData="M39,19L39,89"
android:strokeWidth="0.8"
android:strokeColor="#33FFFFFF" />
<path
android:fillColor="#00000000"
android:pathData="M49,19L49,89"
android:strokeWidth="0.8"
android:strokeColor="#33FFFFFF" />
<path
android:fillColor="#00000000"
android:pathData="M59,19L59,89"
android:strokeWidth="0.8"
android:strokeColor="#33FFFFFF" />
<path
android:fillColor="#00000000"
android:pathData="M69,19L69,89"
android:strokeWidth="0.8"
android:strokeColor="#33FFFFFF" />
<path
android:fillColor="#00000000"
android:pathData="M79,19L79,89"
android:strokeWidth="0.8"
android:strokeColor="#33FFFFFF" />
</vector>

View file

@ -1,59 +0,0 @@
<?xml version="1.0" encoding="utf-8"?>
<androidx.constraintlayout.widget.ConstraintLayout xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:app="http://schemas.android.com/apk/res-auto"
xmlns:tools="http://schemas.android.com/tools"
android:layout_width="match_parent"
android:layout_height="match_parent">
<com.google.android.material.button.MaterialButton
android:id="@+id/export_sms"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:text="@string/export_sms"
android:visibility="gone"
app:layout_constraintBottom_toTopOf="@id/clear_default_sms"
app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintTop_toTopOf="parent" />
<com.google.android.material.button.MaterialButton
android:id="@+id/set_as_default_sms"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:text="@string/set_as_default_sms_app"
android:visibility="gone"
app:layout_constraintBottom_toBottomOf="parent"
app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintTop_toTopOf="parent" />
<com.google.android.material.button.MaterialButton
android:id="@+id/clear_default_sms"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:text="@string/clear_default_sms_app"
android:visibility="gone"
app:layout_constraintBottom_toBottomOf="parent"
app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintTop_toBottomOf="@id/export_sms" />
<TextView
android:id="@+id/export_status"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:layout_marginHorizontal="16dp"
android:layout_marginBottom="16dp"
app:layout_constraintBottom_toTopOf="@id/export_progress" />
<com.google.android.material.progressindicator.LinearProgressIndicator
android:id="@+id/export_progress"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:layout_marginHorizontal="16dp"
android:layout_marginBottom="16dp"
app:layout_constraintBottom_toBottomOf="parent"
tools:max="100"
tools:progress="50" />
</androidx.constraintlayout.widget.ConstraintLayout>

View file

@ -1,5 +0,0 @@
<?xml version="1.0" encoding="utf-8"?>
<adaptive-icon xmlns:android="http://schemas.android.com/apk/res/android">
<background android:drawable="@drawable/ic_launcher_background" />
<foreground android:drawable="@drawable/ic_launcher_foreground" />
</adaptive-icon>

View file

@ -1,5 +0,0 @@
<?xml version="1.0" encoding="utf-8"?>
<adaptive-icon xmlns:android="http://schemas.android.com/apk/res/android">
<background android:drawable="@drawable/ic_launcher_background" />
<foreground android:drawable="@drawable/ic_launcher_foreground" />
</adaptive-icon>

Binary file not shown.

Before

Width:  |  Height:  |  Size: 1.4 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 2.8 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 982 B

Binary file not shown.

Before

Width:  |  Height:  |  Size: 1.7 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 1.9 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 3.8 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 2.8 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 5.8 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 3.8 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 7.6 KiB

View file

@ -1,6 +0,0 @@
<?xml version="1.0" encoding="utf-8"?>
<resources>
<color name="colorPrimary">#008577</color>
<color name="colorPrimaryDark">#00574B</color>
<color name="colorAccent">#D81B60</color>
</resources>

View file

@ -1,6 +0,0 @@
<resources>
<string name="app_name">Sms Exporter Test App</string>
<string name="set_as_default_sms_app">Set as default SMS app</string>
<string name="clear_default_sms_app">Clear default SMS app</string>
<string name="export_sms">Export SMS</string>
</resources>

View file

@ -1,9 +0,0 @@
<resources xmlns:tools="http://schemas.android.com/tools">
<!-- Base application theme. -->
<style name="Theme.Signal" parent="Theme.MaterialComponents.NoActionBar">
<!-- Customize your theme here. -->
<item name="colorPrimary">@color/colorPrimary</item>
<item name="colorPrimaryDark">@color/colorPrimaryDark</item>
<item name="colorAccent">@color/colorAccent</item>
</style>
</resources>

View file

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

View file

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

View file

@ -1,4 +0,0 @@
<?xml version="1.0" encoding="utf-8"?>
<manifest xmlns:android="http://schemas.android.com/apk/res/android">
</manifest>

View file

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

View file

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

View file

@ -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<out ID : Any>(
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<out ID : Any>(
val id: ID,
override val exportState: SmsExportState,
val addresses: Set<String>,
val dateReceived: Duration,
val dateSent: Duration,
val isRead: Boolean,
val isOutgoing: Boolean,
val parts: List<Part>,
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<out ID : Any>(
val id: ID,
override val exportState: SmsExportState = SmsExportState()
) : ExportableMessage
}

View file

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

View file

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

View file

@ -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<SmsExportProgress> = BehaviorProcessor.createDefault(SmsExportProgress.Init)
fun clearProgressState() {
progressState.onNext(SmsExportProgress.Init)
}
}
override fun onBind(intent: Intent?): IBinder? {
return null
}
private val threadCache: MutableMap<Set<String>, 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<ExportableMessage>
/**
* 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<ExportMmsPartsUseCase.Output?>? = exportMmsOutput?.let { exportMmsParts(smsExportState, it) }
val writeMmsPartsOutput: List<Result<Unit, Throwable>>? = exportMmsPartsOutput?.filterNotNull()?.map { writeAttachmentToDisk(smsExportState, it) }
val exportMmsRecipients: List<Unit?>? = 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<ExportMmsPartsUseCase.Output?> {
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<Unit?> {
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<Unit> {
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
)
}

View file

@ -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<String> = emptySet(),
val completedRecipients: Set<String> = emptySet(),
val startedAttachments: Set<String> = emptySet(),
val completedAttachments: Set<String> = emptySet(),
val progress: Progress = Progress.INIT
) {
enum class Progress {
INIT,
STARTED,
COMPLETED
}
}

View file

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

View file

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

View file

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

View file

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

View file

@ -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 "<signal:${part.contentId}>"
}
fun execute(context: Context, part: ExportableMessage.Mms.Part, output: ExportMmsMessagesUseCase.Output, checkForExistence: Boolean): Try<Output> {
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)
}

View file

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

View file

@ -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<Set<String>, Long>
): Try<Output> {
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<String>, cache: MutableMap<Set<String>, 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<String> {
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)
}

View file

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

View file

@ -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<out String>?, p2: String?, p3: Array<out String>?, 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<out String>?): Int {
return -1
}
override fun update(p0: Uri, p1: ContentValues?, p2: String?, p3: Array<out String>?): 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
}
}

View file

@ -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<String> = setOf("+15555060177"),
dateReceived: Duration = 2.seconds,
dateSent: Duration = 1.seconds,
isRead: Boolean = false,
isOutgoing: Boolean = false,
parts: List<ExportableMessage.Mms.Part> = 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()
)
}
}

View file

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

View file

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

View file

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

View file

@ -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<Set<String>, Long>()
// WHEN
val result = GetOrCreateMmsThreadIdsUseCase.execute(
ApplicationProvider.getApplicationContext(),
mms,
threadCache
)
// THEN
result.either(
onSuccess = {
assertEquals(threadCache[mms.addresses], it.threadId)
},
onFailure = {
throw it
}
)
}
}

View file

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