diff --git a/app/src/main/AndroidManifest.xml b/app/src/main/AndroidManifest.xml
index 32630b92ca..dc1c7ff7a9 100644
--- a/app/src/main/AndroidManifest.xml
+++ b/app/src/main/AndroidManifest.xml
@@ -666,6 +666,12 @@
android:configChanges="touchscreen|keyboard|keyboardHidden|orientation|screenLayout|screenSize|uiMode"
android:launchMode="singleTask" />
+
+
@@ -685,6 +691,7 @@
diff --git a/app/src/main/java/org/thoughtcrime/securesms/BindableConversationItem.java b/app/src/main/java/org/thoughtcrime/securesms/BindableConversationItem.java
index 348c914dee..6ff05366b0 100644
--- a/app/src/main/java/org/thoughtcrime/securesms/BindableConversationItem.java
+++ b/app/src/main/java/org/thoughtcrime/securesms/BindableConversationItem.java
@@ -103,6 +103,7 @@ public interface BindableConversationItem extends Unbindable, GiphyMp4Playable,
void onDonateClicked();
void onBlockJoinRequest(@NonNull Recipient recipient);
void onRecipientNameClicked(@NonNull RecipientId target);
+ void onInviteToSignalClicked();
/** @return true if handled, false if you want to let the normal url handling continue */
boolean onUrlClicked(@NonNull String url);
diff --git a/app/src/main/java/org/thoughtcrime/securesms/ContactSelectionActivity.java b/app/src/main/java/org/thoughtcrime/securesms/ContactSelectionActivity.java
index 9692e4e813..cf3ca932e4 100644
--- a/app/src/main/java/org/thoughtcrime/securesms/ContactSelectionActivity.java
+++ b/app/src/main/java/org/thoughtcrime/securesms/ContactSelectionActivity.java
@@ -28,6 +28,7 @@ import org.signal.core.util.logging.Log;
import org.thoughtcrime.securesms.components.ContactFilterView;
import org.thoughtcrime.securesms.contacts.ContactsCursorLoader.DisplayMode;
import org.thoughtcrime.securesms.contacts.sync.ContactDiscovery;
+import org.thoughtcrime.securesms.keyvalue.SignalStore;
import org.thoughtcrime.securesms.recipients.RecipientId;
import org.thoughtcrime.securesms.util.DynamicNoActionBarTheme;
import org.thoughtcrime.securesms.util.DynamicTheme;
@@ -69,8 +70,8 @@ public abstract class ContactSelectionActivity extends PassphraseRequiredActivit
@Override
protected void onCreate(Bundle icicle, boolean ready) {
if (!getIntent().hasExtra(ContactSelectionListFragment.DISPLAY_MODE)) {
- int displayMode = Util.isDefaultSmsProvider(this) ? DisplayMode.FLAG_ALL
- : DisplayMode.FLAG_PUSH | DisplayMode.FLAG_ACTIVE_GROUPS | DisplayMode.FLAG_INACTIVE_GROUPS | DisplayMode.FLAG_SELF;
+ boolean includeSms = Util.isDefaultSmsProvider(this) && SignalStore.misc().getSmsExportPhase().isSmsSupported();
+ int displayMode = includeSms ? DisplayMode.FLAG_ALL : DisplayMode.FLAG_PUSH | DisplayMode.FLAG_ACTIVE_GROUPS | DisplayMode.FLAG_INACTIVE_GROUPS | DisplayMode.FLAG_SELF;
getIntent().putExtra(ContactSelectionListFragment.DISPLAY_MODE, displayMode);
}
diff --git a/app/src/main/java/org/thoughtcrime/securesms/InviteActivity.java b/app/src/main/java/org/thoughtcrime/securesms/InviteActivity.java
index aeffa5761b..850431c458 100644
--- a/app/src/main/java/org/thoughtcrime/securesms/InviteActivity.java
+++ b/app/src/main/java/org/thoughtcrime/securesms/InviteActivity.java
@@ -27,6 +27,7 @@ import org.thoughtcrime.securesms.contacts.ContactsCursorLoader.DisplayMode;
import org.thoughtcrime.securesms.contacts.SelectedContact;
import org.thoughtcrime.securesms.database.SignalDatabase;
import org.thoughtcrime.securesms.groups.SelectionLimits;
+import org.thoughtcrime.securesms.keyvalue.SignalStore;
import org.thoughtcrime.securesms.recipients.Recipient;
import org.thoughtcrime.securesms.recipients.RecipientId;
import org.thoughtcrime.securesms.sms.MessageSender;
@@ -118,7 +119,7 @@ public class InviteActivity extends PassphraseRequiredActivity implements Contac
smsSendButton.setOnClickListener(new SmsSendClickListener());
contactFilter.setOnFilterChangedListener(new ContactFilterChangedListener());
- if (Util.isDefaultSmsProvider(this)) {
+ if (Util.isDefaultSmsProvider(this) && SignalStore.misc().getSmsExportPhase().isSmsSupported()) {
shareButton.setOnClickListener(new ShareClickListener());
smsButton.setOnClickListener(new SmsClickListener());
} else {
diff --git a/app/src/main/java/org/thoughtcrime/securesms/NewConversationActivity.java b/app/src/main/java/org/thoughtcrime/securesms/NewConversationActivity.java
index f6ab00285d..45391f2955 100644
--- a/app/src/main/java/org/thoughtcrime/securesms/NewConversationActivity.java
+++ b/app/src/main/java/org/thoughtcrime/securesms/NewConversationActivity.java
@@ -52,6 +52,7 @@ import org.thoughtcrime.securesms.recipients.RecipientId;
import org.thoughtcrime.securesms.util.CommunicationActions;
import org.thoughtcrime.securesms.util.FeatureFlags;
import org.thoughtcrime.securesms.util.LifecycleDisposable;
+import org.thoughtcrime.securesms.util.Util;
import org.thoughtcrime.securesms.util.views.SimpleProgressDialog;
import java.io.IOException;
@@ -104,12 +105,14 @@ public class NewConversationActivity extends ContactSelectionActivity
@Override
public void onBeforeContactSelected(@NonNull Optional recipientId, String number, @NonNull Consumer callback) {
+ boolean smsSupported = Util.isDefaultSmsProvider(this) && SignalStore.misc().getSmsExportPhase().isSmsSupported();
+
if (recipientId.isPresent()) {
launch(Recipient.resolved(recipientId.get()));
} else {
Log.i(TAG, "[onContactSelected] Maybe creating a new recipient.");
- if (SignalStore.account().isRegistered() && NetworkConstraint.isMet(getApplication())) {
+ if (SignalStore.account().isRegistered()) {
Log.i(TAG, "[onContactSelected] Doing contact refresh.");
AlertDialog progress = SimpleProgressDialog.show(this);
@@ -124,15 +127,31 @@ public class NewConversationActivity extends ContactSelectionActivity
resolved = Recipient.resolved(resolved.getId());
} catch (IOException e) {
Log.w(TAG, "[onContactSelected] Failed to refresh directory for new contact.");
+ return null;
}
}
return resolved;
}, resolved -> {
progress.dismiss();
- launch(resolved);
+
+ if (resolved != null) {
+ if (smsSupported || resolved.isRegistered() && resolved.hasServiceId()) {
+ launch(resolved);
+ } else {
+ new MaterialAlertDialogBuilder(this)
+ .setMessage(getString(R.string.NewConversationActivity__s_is_not_a_signal_user, resolved.getDisplayName(this)))
+ .setPositiveButton(android.R.string.ok, null)
+ .show();
+ }
+ } else {
+ new MaterialAlertDialogBuilder(this)
+ .setMessage(R.string.NetworkFailure__network_error_check_your_connection_and_try_again)
+ .setPositiveButton(android.R.string.ok, null)
+ .show();
+ }
});
- } else {
+ } else if (smsSupported) {
launch(Recipient.external(this, number));
}
}
@@ -260,16 +279,20 @@ public class NewConversationActivity extends ContactSelectionActivity
return null;
}
- return new ActionItem(
- R.drawable.ic_phone_right_24,
- getString(R.string.NewConversationActivity__audio_call),
- R.color.signal_colorOnSurface,
- () -> CommunicationActions.startVoiceCall(this, recipient)
- );
+ if (recipient.isRegistered() || (Util.isDefaultSmsProvider(this) && SignalStore.misc().getSmsExportPhase().isSmsSupported())) {
+ return new ActionItem(
+ R.drawable.ic_phone_right_24,
+ getString(R.string.NewConversationActivity__audio_call),
+ R.color.signal_colorOnSurface,
+ () -> CommunicationActions.startVoiceCall(this, recipient)
+ );
+ } else {
+ return null;
+ }
}
private @Nullable ActionItem createVideoCallActionItem(@NonNull Recipient recipient) {
- if (recipient.isSelf() || recipient.isMmsGroup()) {
+ if (recipient.isSelf() || recipient.isMmsGroup() || !recipient.isRegistered()) {
return null;
}
diff --git a/app/src/main/java/org/thoughtcrime/securesms/components/SendButton.kt b/app/src/main/java/org/thoughtcrime/securesms/components/SendButton.kt
index dcfb8818bc..1bc79feaf6 100644
--- a/app/src/main/java/org/thoughtcrime/securesms/components/SendButton.kt
+++ b/app/src/main/java/org/thoughtcrime/securesms/components/SendButton.kt
@@ -6,10 +6,14 @@ import android.view.View
import android.view.View.OnLongClickListener
import android.view.ViewGroup
import androidx.appcompat.widget.AppCompatImageButton
+import com.google.android.material.snackbar.Snackbar
import org.signal.core.util.logging.Log
+import org.thoughtcrime.securesms.R
import org.thoughtcrime.securesms.components.menu.ActionItem
import org.thoughtcrime.securesms.components.menu.SignalContextMenu
import org.thoughtcrime.securesms.conversation.MessageSendType
+import org.thoughtcrime.securesms.keyvalue.SignalStore
+import org.thoughtcrime.securesms.util.Util
import org.thoughtcrime.securesms.util.ViewUtil
import java.lang.AssertionError
import java.util.concurrent.CopyOnWriteArrayList
@@ -30,6 +34,8 @@ class SendButton(context: Context, attributeSet: AttributeSet?) : AppCompatImage
private var activeMessageSendType: MessageSendType? = null
private var defaultTransportType: MessageSendType.TransportType = MessageSendType.TransportType.SMS
private var defaultSubscriptionId: Int? = null
+
+ lateinit var snackbarContainer: View
private var popupContainer: ViewGroup? = null
init {
@@ -146,10 +152,19 @@ class SendButton(context: Context, attributeSet: AttributeSet?) : AppCompatImage
}
override fun onLongClick(v: View): Boolean {
- if (!isEnabled || availableSendTypes.size == 1) {
+ if (!isEnabled) {
return false
}
+ if (availableSendTypes.size == 1) {
+ return if (!Util.isDefaultSmsProvider(context) || !SignalStore.misc().smsExportPhase.isSmsSupported()) {
+ Snackbar.make(snackbarContainer, R.string.InputPanel__sms_messaging_is_no_longer_supported_in_signal, Snackbar.LENGTH_SHORT).show()
+ true
+ } else {
+ false
+ }
+ }
+
val currentlySelected: MessageSendType = selectedSendType
val items = availableSendTypes
diff --git a/app/src/main/java/org/thoughtcrime/securesms/components/settings/app/chats/ChatsSettingsFragment.kt b/app/src/main/java/org/thoughtcrime/securesms/components/settings/app/chats/ChatsSettingsFragment.kt
index baf9d51ea3..2f4369217d 100644
--- a/app/src/main/java/org/thoughtcrime/securesms/components/settings/app/chats/ChatsSettingsFragment.kt
+++ b/app/src/main/java/org/thoughtcrime/securesms/components/settings/app/chats/ChatsSettingsFragment.kt
@@ -1,28 +1,41 @@
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
override fun onResume() {
super.onResume()
viewModel.refresh()
}
+ @Suppress("ReplaceGetOrSet")
override fun bindAdapter(adapter: MappingAdapter) {
- val repository = ChatsSettingsRepository()
- val factory = ChatsSettingsViewModel.Factory(repository)
- viewModel = ViewModelProvider(this, factory)[ChatsSettingsViewModel::class.java]
+ 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) {
adapter.submitList(getConfiguration(it).toMappingModelList())
@@ -32,14 +45,46 @@ class ChatsSettingsFragment : DSLSettingsFragment(R.string.preferences_chats__ch
private fun getConfiguration(state: ChatsSettingsState): DSLConfiguration {
return configure {
- clickPref(
- title = DSLSettingsText.from(R.string.preferences__sms_mms),
- onClick = {
- Navigation.findNavController(requireView()).safeNavigate(R.id.action_chatsSettingsFragment_to_smsSettingsFragment)
- }
- )
+ 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()
+ 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())
+ }
+ )
+
+ dividerPref()
+ }
+ SmsExportState.NO_SMS_MESSAGES_IN_DATABASE -> Unit
+ SmsExportState.NOT_AVAILABLE -> Unit
+ }
+ }
+
+ if (state.useAsDefaultSmsApp) {
+ 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),
diff --git a/app/src/main/java/org/thoughtcrime/securesms/components/settings/app/chats/ChatsSettingsState.kt b/app/src/main/java/org/thoughtcrime/securesms/components/settings/app/chats/ChatsSettingsState.kt
index 7f80d1dd07..bcb1de8429 100644
--- a/app/src/main/java/org/thoughtcrime/securesms/components/settings/app/chats/ChatsSettingsState.kt
+++ b/app/src/main/java/org/thoughtcrime/securesms/components/settings/app/chats/ChatsSettingsState.kt
@@ -1,9 +1,13 @@
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 useSystemEmoji: Boolean,
val enterKeySends: Boolean,
- val chatBackupsEnabled: Boolean
+ val chatBackupsEnabled: Boolean,
+ val useAsDefaultSmsApp: Boolean,
+ val smsExportState: SmsExportState = SmsExportState.FETCHING
)
diff --git a/app/src/main/java/org/thoughtcrime/securesms/components/settings/app/chats/ChatsSettingsViewModel.kt b/app/src/main/java/org/thoughtcrime/securesms/components/settings/app/chats/ChatsSettingsViewModel.kt
index 0d03716763..90f84256c7 100644
--- a/app/src/main/java/org/thoughtcrime/securesms/components/settings/app/chats/ChatsSettingsViewModel.kt
+++ b/app/src/main/java/org/thoughtcrime/securesms/components/settings/app/chats/ChatsSettingsViewModel.kt
@@ -2,17 +2,24 @@ package org.thoughtcrime.securesms.components.settings.app.chats
import androidx.lifecycle.LiveData
import androidx.lifecycle.ViewModel
-import androidx.lifecycle.ViewModelProvider
+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(private val repository: ChatsSettingsRepository) : ViewModel() {
+class ChatsSettingsViewModel @JvmOverloads constructor(
+ private val repository: ChatsSettingsRepository = ChatsSettingsRepository(),
+ smsSettingsRepository: SmsSettingsRepository = SmsSettingsRepository()
+) : ViewModel() {
private val refreshDebouncer = ThrottledDebouncer(500L)
+ private val disposables = CompositeDisposable()
private val store: Store = Store(
ChatsSettingsState(
@@ -20,12 +27,23 @@ class ChatsSettingsViewModel(private val repository: ChatsSettingsRepository) :
useAddressBook = SignalStore.settings().isPreferSystemContactPhotos,
useSystemEmoji = SignalStore.settings().isPreferSystemEmoji,
enterKeySends = SignalStore.settings().isEnterKeySends,
- chatBackupsEnabled = SignalStore.settings().isBackupEnabled && BackupUtil.canUserAccessBackupDirectory(ApplicationDependencies.getApplication())
+ chatBackupsEnabled = SignalStore.settings().isBackupEnabled && BackupUtil.canUserAccessBackupDirectory(ApplicationDependencies.getApplication()),
+ useAsDefaultSmsApp = Util.isDefaultSmsProvider(ApplicationDependencies.getApplication())
)
)
val state: LiveData = store.stateLiveData
+ init {
+ disposables += smsSettingsRepository.getSmsExportState().subscribe { state ->
+ store.update { it.copy(smsExportState = state) }
+ }
+ }
+
+ override fun onCleared() {
+ disposables.clear()
+ }
+
fun setGenerateLinkPreviewsEnabled(enabled: Boolean) {
store.update { it.copy(generateLinkPreviews = enabled) }
SignalStore.settings().isLinkPreviewsEnabled = enabled
@@ -55,10 +73,4 @@ class ChatsSettingsViewModel(private val repository: ChatsSettingsRepository) :
store.update { it.copy(chatBackupsEnabled = backupsEnabled) }
}
}
-
- class Factory(private val repository: ChatsSettingsRepository) : ViewModelProvider.Factory {
- override fun create(modelClass: Class): T {
- return requireNotNull(modelClass.cast(ChatsSettingsViewModel(repository)))
- }
- }
}
diff --git a/app/src/main/java/org/thoughtcrime/securesms/components/settings/app/chats/sms/SmsExportState.kt b/app/src/main/java/org/thoughtcrime/securesms/components/settings/app/chats/sms/SmsExportState.kt
new file mode 100644
index 0000000000..0cdbf8ed26
--- /dev/null
+++ b/app/src/main/java/org/thoughtcrime/securesms/components/settings/app/chats/sms/SmsExportState.kt
@@ -0,0 +1,9 @@
+package org.thoughtcrime.securesms.components.settings.app.chats.sms
+
+enum class SmsExportState {
+ FETCHING,
+ HAS_UNEXPORTED_MESSAGES,
+ ALL_MESSAGES_EXPORTED,
+ NO_SMS_MESSAGES_IN_DATABASE,
+ NOT_AVAILABLE
+}
diff --git a/app/src/main/java/org/thoughtcrime/securesms/components/settings/app/chats/sms/SmsSettingsFragment.kt b/app/src/main/java/org/thoughtcrime/securesms/components/settings/app/chats/sms/SmsSettingsFragment.kt
index 0d4c24f5af..6965013316 100644
--- a/app/src/main/java/org/thoughtcrime/securesms/components/settings/app/chats/sms/SmsSettingsFragment.kt
+++ b/app/src/main/java/org/thoughtcrime/securesms/components/settings/app/chats/sms/SmsSettingsFragment.kt
@@ -9,18 +9,16 @@ import androidx.activity.result.ActivityResultLauncher
import androidx.activity.result.contract.ActivityResultContracts
import androidx.lifecycle.ViewModelProvider
import androidx.navigation.Navigation
-import com.google.android.material.dialog.MaterialAlertDialogBuilder
-import com.google.android.material.snackbar.Snackbar
-import org.signal.core.util.concurrent.SignalExecutors
+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.database.SignalDatabase
+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.SmsUtil
import org.thoughtcrime.securesms.util.Util
import org.thoughtcrime.securesms.util.adapter.mapping.MappingAdapter
import org.thoughtcrime.securesms.util.navigation.safeNavigate
@@ -38,9 +36,11 @@ class SmsSettingsFragment : DSLSettingsFragment(R.string.preferences__sms_mms) {
}
override fun bindAdapter(adapter: MappingAdapter) {
+ OutlinedLearnMore.register(adapter)
+
smsExportLauncher = registerForActivityResult(ActivityResultContracts.StartActivityForResult()) {
if (it.resultCode == Activity.RESULT_OK) {
- showSmsRemovalDialog()
+ SmsExportDialogs.showSmsRemovalDialog(requireContext(), requireView())
}
}
@@ -52,16 +52,32 @@ class SmsSettingsFragment : DSLSettingsFragment(R.string.preferences__sms_mms) {
}
override fun onActivityResult(requestCode: Int, resultCode: Int, data: Intent?) {
- SignalStore.settings().setDefaultSms(Util.isDefaultSmsProvider(requireContext()))
+ 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) {
- SmsSettingsState.SmsExportState.FETCHING -> Unit
- SmsSettingsState.SmsExportState.HAS_UNEXPORTED_MESSAGES -> {
+ 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()))
}
@@ -69,32 +85,31 @@ class SmsSettingsFragment : DSLSettingsFragment(R.string.preferences__sms_mms) {
dividerPref()
}
- SmsSettingsState.SmsExportState.ALL_MESSAGES_EXPORTED -> {
+ 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 = {
- showSmsRemovalDialog()
+ SmsExportDialogs.showSmsRemovalDialog(requireContext(), requireView())
}
)
dividerPref()
}
- SmsSettingsState.SmsExportState.NO_SMS_MESSAGES_IN_DATABASE -> Unit
- SmsSettingsState.SmsExportState.NOT_AVAILABLE -> Unit
+ SmsExportState.NO_SMS_MESSAGES_IN_DATABASE -> Unit
+ SmsExportState.NOT_AVAILABLE -> Unit
}
- @Suppress("DEPRECATION")
- clickPref(
- title = DSLSettingsText.from(R.string.SmsSettingsFragment__use_as_default_sms_app),
- summary = DSLSettingsText.from(if (state.useAsDefaultSmsApp) R.string.arrays__enabled else R.string.arrays__disabled),
- onClick = {
- if (state.useAsDefaultSmsApp) {
+ 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()
- } else {
- startActivityForResult(SmsUtil.getSmsRoleIntent(requireContext()), SMS_REQUEST_CODE.toInt())
}
- }
- )
+ )
+ }
switchPref(
title = DSLSettingsText.from(R.string.preferences__sms_delivery_reports),
@@ -137,21 +152,4 @@ class SmsSettingsFragment : DSLSettingsFragment(R.string.preferences__sms_mms) {
startActivityForResult(intent, SMS_REQUEST_CODE.toInt())
}
-
- private fun showSmsRemovalDialog() {
- MaterialAlertDialogBuilder(requireContext())
- .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(requireView(), R.string.SmsSettingsFragment__you_can_remove_sms_messages_from_signal_in_settings, Snackbar.LENGTH_SHORT).show()
- }
- .setNegativeButton(R.string.RemoveSmsMessagesDialogFragment__remove_messages) { _, _ ->
- SignalExecutors.BOUNDED.execute {
- SignalDatabase.sms.deleteExportedMessages()
- SignalDatabase.mms.deleteExportedMessages()
- }
- Snackbar.make(requireView(), R.string.SmsSettingsFragment__removing_sms_messages_from_signal, Snackbar.LENGTH_SHORT).show()
- }
- .show()
- }
}
diff --git a/app/src/main/java/org/thoughtcrime/securesms/components/settings/app/chats/sms/SmsSettingsRepository.kt b/app/src/main/java/org/thoughtcrime/securesms/components/settings/app/chats/sms/SmsSettingsRepository.kt
index e1a1bf20aa..ed84c54e2c 100644
--- a/app/src/main/java/org/thoughtcrime/securesms/components/settings/app/chats/sms/SmsSettingsRepository.kt
+++ b/app/src/main/java/org/thoughtcrime/securesms/components/settings/app/chats/sms/SmsSettingsRepository.kt
@@ -11,9 +11,9 @@ class SmsSettingsRepository(
private val smsDatabase: MessageDatabase = SignalDatabase.sms,
private val mmsDatabase: MessageDatabase = SignalDatabase.mms
) {
- fun getSmsExportState(): Single {
+ fun getSmsExportState(): Single {
if (!FeatureFlags.smsExporter()) {
- return Single.just(SmsSettingsState.SmsExportState.NOT_AVAILABLE)
+ return Single.just(SmsExportState.NOT_AVAILABLE)
}
return Single.fromCallable {
@@ -22,24 +22,24 @@ class SmsSettingsRepository(
}
@WorkerThread
- private fun checkInsecureMessageCount(): SmsSettingsState.SmsExportState? {
+ private fun checkInsecureMessageCount(): SmsExportState? {
val totalSmsMmsCount = smsDatabase.insecureMessageCount + mmsDatabase.insecureMessageCount
return if (totalSmsMmsCount == 0) {
- SmsSettingsState.SmsExportState.NO_SMS_MESSAGES_IN_DATABASE
+ SmsExportState.NO_SMS_MESSAGES_IN_DATABASE
} else {
null
}
}
@WorkerThread
- private fun checkUnexportedInsecureMessageCount(): SmsSettingsState.SmsExportState {
+ private fun checkUnexportedInsecureMessageCount(): SmsExportState {
val totalUnexportedCount = smsDatabase.unexportedInsecureMessagesCount + mmsDatabase.unexportedInsecureMessagesCount
return if (totalUnexportedCount > 0) {
- SmsSettingsState.SmsExportState.HAS_UNEXPORTED_MESSAGES
+ SmsExportState.HAS_UNEXPORTED_MESSAGES
} else {
- SmsSettingsState.SmsExportState.ALL_MESSAGES_EXPORTED
+ SmsExportState.ALL_MESSAGES_EXPORTED
}
}
}
diff --git a/app/src/main/java/org/thoughtcrime/securesms/components/settings/app/chats/sms/SmsSettingsState.kt b/app/src/main/java/org/thoughtcrime/securesms/components/settings/app/chats/sms/SmsSettingsState.kt
index cdc7ac3431..e3ec8342a5 100644
--- a/app/src/main/java/org/thoughtcrime/securesms/components/settings/app/chats/sms/SmsSettingsState.kt
+++ b/app/src/main/java/org/thoughtcrime/securesms/components/settings/app/chats/sms/SmsSettingsState.kt
@@ -5,12 +5,4 @@ data class SmsSettingsState(
val smsDeliveryReportsEnabled: Boolean,
val wifiCallingCompatibilityEnabled: Boolean,
val smsExportState: SmsExportState = SmsExportState.FETCHING
-) {
- enum class SmsExportState {
- FETCHING,
- HAS_UNEXPORTED_MESSAGES,
- ALL_MESSAGES_EXPORTED,
- NO_SMS_MESSAGES_IN_DATABASE,
- NOT_AVAILABLE
- }
-}
+)
diff --git a/app/src/main/java/org/thoughtcrime/securesms/components/settings/app/notifications/profiles/SelectRecipientsFragment.kt b/app/src/main/java/org/thoughtcrime/securesms/components/settings/app/notifications/profiles/SelectRecipientsFragment.kt
index 8e96345bc5..01f3eb137b 100644
--- a/app/src/main/java/org/thoughtcrime/securesms/components/settings/app/notifications/profiles/SelectRecipientsFragment.kt
+++ b/app/src/main/java/org/thoughtcrime/securesms/components/settings/app/notifications/profiles/SelectRecipientsFragment.kt
@@ -15,6 +15,7 @@ import org.thoughtcrime.securesms.R
import org.thoughtcrime.securesms.components.ContactFilterView
import org.thoughtcrime.securesms.contacts.ContactsCursorLoader
import org.thoughtcrime.securesms.groups.SelectionLimits
+import org.thoughtcrime.securesms.keyvalue.SignalStore
import org.thoughtcrime.securesms.recipients.RecipientId
import org.thoughtcrime.securesms.util.LifecycleDisposable
import org.thoughtcrime.securesms.util.Util
@@ -105,7 +106,7 @@ class SelectRecipientsFragment : LoggingFragment(), ContactSelectionListFragment
ContactsCursorLoader.DisplayMode.FLAG_HIDE_RECENT_HEADER or
ContactsCursorLoader.DisplayMode.FLAG_GROUPS_AFTER_CONTACTS
- if (Util.isDefaultSmsProvider(requireContext())) {
+ if (Util.isDefaultSmsProvider(requireContext()) && SignalStore.misc().smsExportPhase.isSmsSupported()) {
mode = mode or ContactsCursorLoader.DisplayMode.FLAG_SMS
}
diff --git a/app/src/main/java/org/thoughtcrime/securesms/components/settings/conversation/ConversationSettingsViewModel.kt b/app/src/main/java/org/thoughtcrime/securesms/components/settings/conversation/ConversationSettingsViewModel.kt
index 2a160b1375..116cdfac9c 100644
--- a/app/src/main/java/org/thoughtcrime/securesms/components/settings/conversation/ConversationSettingsViewModel.kt
+++ b/app/src/main/java/org/thoughtcrime/securesms/components/settings/conversation/ConversationSettingsViewModel.kt
@@ -16,14 +16,17 @@ import org.thoughtcrime.securesms.components.settings.conversation.preferences.L
import org.thoughtcrime.securesms.database.AttachmentDatabase
import org.thoughtcrime.securesms.database.RecipientDatabase
import org.thoughtcrime.securesms.database.model.StoryViewState
+import org.thoughtcrime.securesms.dependencies.ApplicationDependencies
import org.thoughtcrime.securesms.groups.GroupId
import org.thoughtcrime.securesms.groups.LiveGroup
import org.thoughtcrime.securesms.groups.v2.GroupAddMembersResult
+import org.thoughtcrime.securesms.keyvalue.SignalStore
import org.thoughtcrime.securesms.recipients.Recipient
import org.thoughtcrime.securesms.recipients.RecipientId
import org.thoughtcrime.securesms.recipients.RecipientUtil
import org.thoughtcrime.securesms.util.FeatureFlags
import org.thoughtcrime.securesms.util.SingleLiveEvent
+import org.thoughtcrime.securesms.util.Util
import org.thoughtcrime.securesms.util.livedata.LiveDataUtil
import org.thoughtcrime.securesms.util.livedata.Store
import java.util.Optional
@@ -138,11 +141,17 @@ sealed class ConversationSettingsViewModel(
}
store.update(liveRecipient.liveData) { recipient, state ->
+ val isAudioAvailable = (recipient.isRegistered || (Util.isDefaultSmsProvider(ApplicationDependencies.getApplication()) && SignalStore.misc().smsExportPhase.isSmsSupported())) &&
+ !recipient.isGroup &&
+ !recipient.isBlocked &&
+ !recipient.isSelf &&
+ !recipient.isReleaseNotes
+
state.copy(
recipient = recipient,
buttonStripState = ButtonStripPreference.State(
isVideoAvailable = recipient.registered == RecipientDatabase.RegisteredState.REGISTERED && !recipient.isSelf && !recipient.isBlocked && !recipient.isReleaseNotes,
- isAudioAvailable = !recipient.isGroup && !recipient.isSelf && !recipient.isBlocked && !recipient.isReleaseNotes,
+ isAudioAvailable = isAudioAvailable,
isAudioSecure = recipient.registered == RecipientDatabase.RegisteredState.REGISTERED,
isMuted = recipient.isMuted,
isMuteAvailable = !recipient.isSelf,
diff --git a/app/src/main/java/org/thoughtcrime/securesms/components/settings/models/OutlinedLearnMore.kt b/app/src/main/java/org/thoughtcrime/securesms/components/settings/models/OutlinedLearnMore.kt
new file mode 100644
index 0000000000..62bc0fc843
--- /dev/null
+++ b/app/src/main/java/org/thoughtcrime/securesms/components/settings/models/OutlinedLearnMore.kt
@@ -0,0 +1,35 @@
+package org.thoughtcrime.securesms.components.settings.models
+
+import org.thoughtcrime.securesms.components.settings.DSLSettingsText
+import org.thoughtcrime.securesms.components.settings.PreferenceModel
+import org.thoughtcrime.securesms.databinding.DslOutlinedLearnMoreBinding
+import org.thoughtcrime.securesms.util.adapter.mapping.BindingFactory
+import org.thoughtcrime.securesms.util.adapter.mapping.BindingViewHolder
+import org.thoughtcrime.securesms.util.adapter.mapping.MappingAdapter
+
+/**
+ * Show a informational text message in an outlined bubble.
+ */
+object OutlinedLearnMore {
+
+ fun register(mappingAdapter: MappingAdapter) {
+ mappingAdapter.registerFactory(Model::class.java, BindingFactory(::ViewHolder, DslOutlinedLearnMoreBinding::inflate))
+ }
+
+ class Model(
+ summary: DSLSettingsText,
+ val learnMoreUrl: String
+ ) : PreferenceModel(summary = summary) {
+ override fun areContentsTheSame(newItem: Model): Boolean {
+ return super.areContentsTheSame(newItem) && learnMoreUrl == newItem.learnMoreUrl
+ }
+ }
+
+ private class ViewHolder(binding: DslOutlinedLearnMoreBinding) : BindingViewHolder(binding) {
+ override fun bind(model: Model) {
+ binding.root.text = model.summary!!.resolve(context)
+ binding.root.setLearnMoreVisible(true)
+ binding.root.setLink(model.learnMoreUrl)
+ }
+ }
+}
diff --git a/app/src/main/java/org/thoughtcrime/securesms/conversation/ConversationFragment.java b/app/src/main/java/org/thoughtcrime/securesms/conversation/ConversationFragment.java
index 99cc31f24d..d07df08612 100644
--- a/app/src/main/java/org/thoughtcrime/securesms/conversation/ConversationFragment.java
+++ b/app/src/main/java/org/thoughtcrime/securesms/conversation/ConversationFragment.java
@@ -1486,6 +1486,7 @@ public class ConversationFragment extends LoggingFragment implements Multiselect
void onVoiceNotePlaybackSpeedChanged(@NonNull Uri uri, float speed);
void onRegisterVoiceNoteCallbacks(@NonNull Observer onPlaybackStartObserver);
void onUnregisterVoiceNoteCallbacks(@NonNull Observer onPlaybackStartObserver);
+ void onInviteToSignal();
}
private class ConversationScrollListener extends OnScrollListener {
@@ -2080,6 +2081,11 @@ public class ConversationFragment extends LoggingFragment implements Multiselect
RecipientBottomSheetDialogFragment.create(target, recipient.get().getGroupId().orElse(null)).show(getParentFragmentManager(), "BOTTOM");
}
+ @Override
+ public void onInviteToSignalClicked() {
+ listener.onInviteToSignal();
+ }
+
@Override
public void onViewGiftBadgeClicked(@NonNull MessageRecord messageRecord) {
if (!MessageRecordUtil.hasGiftBadge(messageRecord)) {
diff --git a/app/src/main/java/org/thoughtcrime/securesms/conversation/ConversationParentFragment.java b/app/src/main/java/org/thoughtcrime/securesms/conversation/ConversationParentFragment.java
index 6bcc653e81..8f8bcd3fdc 100644
--- a/app/src/main/java/org/thoughtcrime/securesms/conversation/ConversationParentFragment.java
+++ b/app/src/main/java/org/thoughtcrime/securesms/conversation/ConversationParentFragment.java
@@ -189,6 +189,7 @@ import org.thoughtcrime.securesms.database.model.StoryType;
import org.thoughtcrime.securesms.dependencies.ApplicationDependencies;
import org.thoughtcrime.securesms.events.GroupCallPeekEvent;
import org.thoughtcrime.securesms.events.ReminderUpdateEvent;
+import org.thoughtcrime.securesms.exporter.flow.SmsExportActivity;
import org.thoughtcrime.securesms.groups.GroupId;
import org.thoughtcrime.securesms.groups.ui.GroupChangeFailureReason;
import org.thoughtcrime.securesms.groups.ui.GroupErrors;
@@ -401,7 +402,7 @@ public class ConversationParentFragment extends Fragment
private TextView charactersLeft;
private ConversationFragment fragment;
private Button unblockButton;
- private Button makeDefaultSmsButton;
+ private Stub smsExportStub;
private Button registerButton;
private InputAwareLayout container;
protected Stub reminderView;
@@ -925,8 +926,11 @@ public class ConversationParentFragment extends Fragment
}
if (isSingleConversation()) {
- if (viewModel.isPushAvailable()) inflater.inflate(R.menu.conversation_callable_secure, menu);
- else if (!recipient.get().isReleaseNotes()) inflater.inflate(R.menu.conversation_callable_insecure, menu);
+ if (viewModel.isPushAvailable()) {
+ inflater.inflate(R.menu.conversation_callable_secure, menu);
+ } else if (!recipient.get().isReleaseNotes() && Util.isDefaultSmsProvider(requireContext()) && SignalStore.misc().getSmsExportPhase().isSmsSupported()) {
+ inflater.inflate(R.menu.conversation_callable_insecure, menu);
+ }
} else if (isGroupConversation()) {
if (isActiveV2Group && Build.VERSION.SDK_INT > 19) {
inflater.inflate(R.menu.conversation_callable_groupv2, menu);
@@ -1303,14 +1307,24 @@ public class ConversationParentFragment extends Fragment
private void handleInviteLink() {
String inviteText = getString(R.string.ConversationActivity_lets_switch_to_signal, getString(R.string.install_url));
- if (viewModel.isDefaultSmsApplication()) {
+ if (viewModel.isDefaultSmsApplication() && SignalStore.misc().getSmsExportPhase().isSmsSupported()) {
composeText.appendInvite(inviteText);
- } else {
+ } else if (recipient.get().hasSmsAddress()) {
Intent intent = new Intent(Intent.ACTION_SENDTO);
intent.setData(Uri.parse("smsto:" + recipient.get().requireSmsAddress()));
intent.putExtra("sms_body", inviteText);
intent.putExtra(Intent.EXTRA_TEXT, inviteText);
startActivity(intent);
+ } else {
+ Intent sendIntent = new Intent();
+ sendIntent.setAction(Intent.ACTION_SEND);
+ sendIntent.putExtra(Intent.EXTRA_TEXT, inviteText);
+ sendIntent.setType("text/plain");
+ if (sendIntent.resolveActivity(requireContext().getPackageManager()) != null) {
+ startActivity(Intent.createChooser(sendIntent, getString(R.string.InviteActivity_invite_to_signal)));
+ } else {
+ Toast.makeText(requireContext(), R.string.InviteActivity_no_app_to_share_to, Toast.LENGTH_LONG).show();
+ }
}
}
@@ -1590,11 +1604,13 @@ public class ConversationParentFragment extends Fragment
if (!recipient.get().isPushGroup() && recipient.get().isForceSmsSelection() && smsEnabled) {
sendButton.setDefaultTransport(MessageSendType.TransportType.SMS);
+ viewModel.insertSmsExportUpdateEvent(recipient.get());
} else {
if (isPushAvailable || isPushGroupConversation() || recipient.get().isServiceIdOnly() || recipient.get().isReleaseNotes() || !smsEnabled) {
sendButton.setDefaultTransport(MessageSendType.TransportType.SIGNAL);
} else {
sendButton.setDefaultTransport(MessageSendType.TransportType.SMS);
+ viewModel.insertSmsExportUpdateEvent(recipient.get());
}
}
@@ -1997,7 +2013,7 @@ public class ConversationParentFragment extends Fragment
emojiDrawerStub = ViewUtil.findStubById(view, R.id.emoji_drawer_stub);
attachmentKeyboardStub = ViewUtil.findStubById(view, R.id.attachment_keyboard_stub);
unblockButton = view.findViewById(R.id.unblock_button);
- makeDefaultSmsButton = view.findViewById(R.id.make_default_sms_button);
+ smsExportStub = ViewUtil.findStubById(view, R.id.sms_export_stub);
registerButton = view.findViewById(R.id.register_button);
container = view.findViewById(R.id.layout_container);
reminderView = ViewUtil.findStubById(view, R.id.reminder_stub);
@@ -2028,6 +2044,7 @@ public class ConversationParentFragment extends Fragment
joinGroupCallButton = view.findViewById(R.id.conversation_group_call_join);
sendButton.setPopupContainer((ViewGroup) view);
+ sendButton.setSnackbarContainer(view.findViewById(R.id.fragment_content));
container.setIsBubble(isInBubble());
container.addOnKeyboardShownListener(this);
@@ -2067,7 +2084,6 @@ public class ConversationParentFragment extends Fragment
titleView.setOnClickListener(v -> handleConversationSettings());
titleView.setOnLongClickListener(v -> handleDisplayQuickContact());
unblockButton.setOnClickListener(v -> handleUnblock());
- makeDefaultSmsButton.setOnClickListener(v -> handleMakeDefaultSms());
registerButton.setOnClickListener(v -> handleRegisterForSignal());
composeText.setOnKeyListener(composeKeyPressedListener);
@@ -2708,17 +2724,29 @@ public class ConversationParentFragment extends Fragment
if (!conversationSecurityInfo.isPushAvailable() && isPushGroupConversation()) {
unblockButton.setVisibility(View.GONE);
inputPanel.setHideForBlockedState(true);
- makeDefaultSmsButton.setVisibility(View.GONE);
+ smsExportStub.setVisibility(View.GONE);
registerButton.setVisibility(View.VISIBLE);
- } else if (!conversationSecurityInfo.isPushAvailable() && !conversationSecurityInfo.isDefaultSmsApplication() && recipient.hasSmsAddress()) {
+ } else if (!conversationSecurityInfo.isPushAvailable() && !(SignalStore.misc().getSmsExportPhase().isSmsSupported() && conversationSecurityInfo.isDefaultSmsApplication()) && recipient.hasSmsAddress()) {
unblockButton.setVisibility(View.GONE);
inputPanel.setHideForBlockedState(true);
- makeDefaultSmsButton.setVisibility(View.VISIBLE);
+ smsExportStub.setVisibility(View.VISIBLE);
registerButton.setVisibility(View.GONE);
+
+ TextView message = smsExportStub.get().findViewById(R.id.export_sms_message);
+ MaterialButton actionButton = smsExportStub.get().findViewById(R.id.export_sms_button);
+ if (conversationSecurityInfo.getHasUnexportedInsecureMessages()) {
+ message.setText(R.string.ConversationActivity__sms_messaging_is_no_longer_supported_in_signal_you_can_export_your_messages_to_another_app_on_your_phone);
+ actionButton.setText(R.string.ConversationActivity__export_sms_messages);
+ actionButton.setOnClickListener(v -> startActivity(SmsExportActivity.createIntent(requireContext())));
+ } else {
+ message.setText(requireContext().getString(R.string.ConversationActivity__sms_messaging_is_no_longer_supported_in_signal_invite_s_to_to_signal_to_keep_the_conversation_here, recipient.getDisplayName(requireContext())));
+ actionButton.setText(R.string.ConversationActivity__invite_to_signal);
+ actionButton.setOnClickListener(v -> handleInviteLink());
+ }
} else if (recipient.isReleaseNotes() && !recipient.isBlocked()) {
unblockButton.setVisibility(View.GONE);
inputPanel.setHideForBlockedState(true);
- makeDefaultSmsButton.setVisibility(View.GONE);
+ smsExportStub.setVisibility(View.GONE);
registerButton.setVisibility(View.GONE);
if (recipient.isMuted()) {
@@ -2735,7 +2763,7 @@ public class ConversationParentFragment extends Fragment
boolean inactivePushGroup = isPushGroupConversation() && !recipient.isActiveGroup();
inputPanel.setHideForBlockedState(inactivePushGroup);
unblockButton.setVisibility(View.GONE);
- makeDefaultSmsButton.setVisibility(View.GONE);
+ smsExportStub.setVisibility(View.GONE);
registerButton.setVisibility(View.GONE);
}
@@ -3796,6 +3824,11 @@ public class ConversationParentFragment extends Fragment
voiceNoteMediaController.getVoiceNotePlaybackState().removeObserver(onPlaybackStartObserver);
}
+ @Override
+ public void onInviteToSignal() {
+ handleInviteLink();
+ }
+
@Override
public void onCursorChanged() {
if (!reactionDelegate.isShowing()) {
diff --git a/app/src/main/java/org/thoughtcrime/securesms/conversation/ConversationRepository.java b/app/src/main/java/org/thoughtcrime/securesms/conversation/ConversationRepository.java
index ba840b7751..02e54819b2 100644
--- a/app/src/main/java/org/thoughtcrime/securesms/conversation/ConversationRepository.java
+++ b/app/src/main/java/org/thoughtcrime/securesms/conversation/ConversationRepository.java
@@ -170,11 +170,16 @@ class ConversationRepository {
}
}
+ long threadId = SignalDatabase.threads().getThreadIdIfExistsFor(recipient.getId());
+
+ boolean hasUnexportedInsecureMessages = threadId != -1 && SignalDatabase.mmsSms().getUnexportedInsecureMessagesCount(threadId) > 0;
+
Log.i(TAG, "Returning registered state...");
return new ConversationSecurityInfo(recipient.getId(),
registeredState == RecipientDatabase.RegisteredState.REGISTERED && signalEnabled,
Util.isDefaultSmsProvider(context),
- true);
+ true,
+ hasUnexportedInsecureMessages);
}).subscribeOn(Schedulers.io());
}
@@ -193,4 +198,18 @@ class ConversationRepository {
listener.onChanged();
}).subscribeOn(Schedulers.io());
}
+
+ public void insertSmsExportUpdateEvent(Recipient recipient) {
+ SignalExecutors.BOUNDED.execute(() -> {
+ long threadId = SignalDatabase.threads().getThreadIdIfExistsFor(recipient.getId());
+
+ if (threadId == -1 || !Util.isDefaultSmsProvider(context)) {
+ return;
+ }
+
+ if (RecipientUtil.isSmsOnly(threadId, recipient) && (!recipient.isMmsGroup() || Util.isDefaultSmsProvider(context))) {
+ SignalDatabase.sms().insertSmsExportMessage(recipient.getId(), threadId);
+ }
+ });
+ }
}
diff --git a/app/src/main/java/org/thoughtcrime/securesms/conversation/ConversationSecurityInfo.kt b/app/src/main/java/org/thoughtcrime/securesms/conversation/ConversationSecurityInfo.kt
index 319cc38174..d92ee665f3 100644
--- a/app/src/main/java/org/thoughtcrime/securesms/conversation/ConversationSecurityInfo.kt
+++ b/app/src/main/java/org/thoughtcrime/securesms/conversation/ConversationSecurityInfo.kt
@@ -6,5 +6,6 @@ data class ConversationSecurityInfo(
val recipientId: RecipientId = RecipientId.UNKNOWN,
val isPushAvailable: Boolean = false,
val isDefaultSmsApplication: Boolean = false,
- val isInitialized: Boolean = false
+ val isInitialized: Boolean = false,
+ val hasUnexportedInsecureMessages: Boolean = false
)
diff --git a/app/src/main/java/org/thoughtcrime/securesms/conversation/ConversationUpdateItem.java b/app/src/main/java/org/thoughtcrime/securesms/conversation/ConversationUpdateItem.java
index bc1e9c5e7b..04179ea96b 100644
--- a/app/src/main/java/org/thoughtcrime/securesms/conversation/ConversationUpdateItem.java
+++ b/app/src/main/java/org/thoughtcrime/securesms/conversation/ConversationUpdateItem.java
@@ -535,6 +535,15 @@ public final class ConversationUpdateItem extends FrameLayout
});
actionButton.setText(R.string.ConversationUpdateItem_donate);
+ } else if (conversationMessage.getMessageRecord().isSmsExportType()) {
+ actionButton.setVisibility(View.VISIBLE);
+ actionButton.setOnClickListener(v -> {
+ if (batchSelected.isEmpty() && eventListener != null) {
+ eventListener.onInviteToSignalClicked();
+ }
+ });
+
+ actionButton.setText(R.string.ConversationActivity__invite_to_signal);
} else {
actionButton.setVisibility(GONE);
actionButton.setOnClickListener(null);
diff --git a/app/src/main/java/org/thoughtcrime/securesms/conversation/ConversationViewModel.java b/app/src/main/java/org/thoughtcrime/securesms/conversation/ConversationViewModel.java
index 17e15105c9..ec389890ff 100644
--- a/app/src/main/java/org/thoughtcrime/securesms/conversation/ConversationViewModel.java
+++ b/app/src/main/java/org/thoughtcrime/securesms/conversation/ConversationViewModel.java
@@ -442,6 +442,10 @@ public class ConversationViewModel extends ViewModel {
EventBus.getDefault().unregister(this);
}
+ public void insertSmsExportUpdateEvent(@NonNull Recipient recipient) {
+ conversationRepository.insertSmsExportUpdateEvent(recipient);
+ }
+
enum Event {
SHOW_RECAPTCHA
}
diff --git a/app/src/main/java/org/thoughtcrime/securesms/conversation/MessageSendType.kt b/app/src/main/java/org/thoughtcrime/securesms/conversation/MessageSendType.kt
index 00c0376030..5555416309 100644
--- a/app/src/main/java/org/thoughtcrime/securesms/conversation/MessageSendType.kt
+++ b/app/src/main/java/org/thoughtcrime/securesms/conversation/MessageSendType.kt
@@ -9,10 +9,12 @@ import androidx.annotation.StringRes
import kotlinx.parcelize.Parcelize
import org.signal.core.util.logging.Log
import org.thoughtcrime.securesms.R
+import org.thoughtcrime.securesms.keyvalue.SignalStore
import org.thoughtcrime.securesms.util.CharacterCalculator
import org.thoughtcrime.securesms.util.MmsCharacterCalculator
import org.thoughtcrime.securesms.util.PushCharacterCalculator
import org.thoughtcrime.securesms.util.SmsCharacterCalculator
+import org.thoughtcrime.securesms.util.Util
import org.thoughtcrime.securesms.util.dualsim.SubscriptionInfoCompat
import org.thoughtcrime.securesms.util.dualsim.SubscriptionManagerCompat
import java.lang.IllegalArgumentException
@@ -138,22 +140,24 @@ sealed class MessageSendType(
options += SignalMessageSendType
- try {
- val subscriptions: Collection = SubscriptionManagerCompat(context).activeAndReadySubscriptionInfos
+ if (Util.isDefaultSmsProvider(context) && SignalStore.misc().smsExportPhase.isSmsSupported()) {
+ try {
+ val subscriptions: Collection = SubscriptionManagerCompat(context).activeAndReadySubscriptionInfos
- if (subscriptions.size < 2) {
- options += if (isMedia) MmsMessageSendType() else SmsMessageSendType()
- } else {
- options += subscriptions.map {
- if (isMedia) {
- MmsMessageSendType(simName = it.displayName, simSubscriptionId = it.subscriptionId)
- } else {
- SmsMessageSendType(simName = it.displayName, simSubscriptionId = it.subscriptionId)
+ if (subscriptions.size < 2) {
+ options += if (isMedia) MmsMessageSendType() else SmsMessageSendType()
+ } else {
+ options += subscriptions.map {
+ if (isMedia) {
+ MmsMessageSendType(simName = it.displayName, simSubscriptionId = it.subscriptionId)
+ } else {
+ SmsMessageSendType(simName = it.displayName, simSubscriptionId = it.subscriptionId)
+ }
}
}
+ } catch (e: SecurityException) {
+ Log.w(TAG, "Did not have permission to get SMS subscription details!")
}
- } catch (e: SecurityException) {
- Log.w(TAG, "Did not have permission to get SMS subscription details!")
}
return options
diff --git a/app/src/main/java/org/thoughtcrime/securesms/conversationlist/ConversationListFragment.java b/app/src/main/java/org/thoughtcrime/securesms/conversationlist/ConversationListFragment.java
index 5c54b9ecba..13102fb9bc 100644
--- a/app/src/main/java/org/thoughtcrime/securesms/conversationlist/ConversationListFragment.java
+++ b/app/src/main/java/org/thoughtcrime/securesms/conversationlist/ConversationListFragment.java
@@ -115,6 +115,7 @@ import org.thoughtcrime.securesms.database.ThreadDatabase;
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.insights.InsightsLauncher;
import org.thoughtcrime.securesms.jobs.ServiceOutageDetectionJob;
import org.thoughtcrime.securesms.keyvalue.SignalStore;
@@ -126,6 +127,7 @@ 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;
@@ -179,6 +181,7 @@ import java.util.Set;
import java.util.UUID;
import java.util.stream.Collectors;
+import static android.app.Activity.RESULT_CANCELED;
import static android.app.Activity.RESULT_OK;
@@ -510,11 +513,16 @@ public class ConversationListFragment extends MainFragment implements ActionMode
@Override
public void onActivityResult(int requestCode, int resultCode, @Nullable Intent data) {
- if (resultCode != RESULT_OK) {
- return;
+ if (requestCode == SmsExportMegaphoneActivity.REQUEST_CODE && SignalStore.misc().getSmsExportPhase().isFullscreen()) {
+ 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 (requestCode == CreateKbsPinActivity.REQUEST_NEW_PIN) {
+ if (resultCode == RESULT_OK && requestCode == CreateKbsPinActivity.REQUEST_NEW_PIN) {
Snackbar.make(fab, R.string.ConfirmKbsPinFragment__pin_created, Snackbar.LENGTH_LONG).show();
viewModel.onMegaphoneCompleted(Megaphones.Event.PINS_FOR_ALL);
}
diff --git a/app/src/main/java/org/thoughtcrime/securesms/database/MessageDatabase.java b/app/src/main/java/org/thoughtcrime/securesms/database/MessageDatabase.java
index ace2a1f06c..7a719e7313 100644
--- a/app/src/main/java/org/thoughtcrime/securesms/database/MessageDatabase.java
+++ b/app/src/main/java/org/thoughtcrime/securesms/database/MessageDatabase.java
@@ -13,6 +13,7 @@ import com.google.android.mms.pdu_alt.NotificationInd;
import net.zetetic.database.sqlcipher.SQLiteStatement;
import org.signal.core.util.CursorUtil;
+import org.signal.core.util.SQLiteDatabaseExtensionsKt;
import org.signal.core.util.SqlUtil;
import org.signal.core.util.logging.Log;
import org.signal.libsignal.protocol.IdentityKey;
@@ -97,7 +98,6 @@ public abstract class MessageDatabase extends Database implements MmsSmsColumns,
public abstract List getProfileChangeDetailsRecords(long threadId, long afterTimestamp);
public abstract Set getAllRateLimitedMessageIds();
public abstract Cursor getUnexportedInsecureMessages(int limit);
- public abstract int getInsecureMessageCount();
public abstract void deleteExportedMessages();
public abstract void markExpireStarted(long messageId);
@@ -177,6 +177,7 @@ public abstract class MessageDatabase extends Database implements MmsSmsColumns,
public abstract void insertNumberChangeMessages(@NonNull Recipient recipient);
public abstract void insertBoostRequestMessage(@NonNull RecipientId recipientId, long threadId);
public abstract void insertThreadMergeEvent(@NonNull RecipientId recipientId, long threadId, @NonNull ThreadMergeEvent event);
+ public abstract void insertSmsExportMessage(@NonNull RecipientId recipientId, long threadId);
public abstract boolean deleteMessage(long messageId);
abstract void deleteThread(long threadId);
@@ -247,6 +248,20 @@ public abstract class MessageDatabase extends Database implements MmsSmsColumns,
return getMessageCountForRecipientsAndType(getOutgoingInsecureMessageClause());
}
+ public int getInsecureMessageCount() {
+ try (Cursor cursor = getReadableDatabase().query(getTableName(), SqlUtil.COUNT, getInsecureMessageClause(), null, null, null, null)) {
+ if (cursor.moveToFirst()) {
+ return cursor.getInt(0);
+ }
+ }
+
+ return 0;
+ }
+
+ public boolean hasSmsExportMessage(long threadId) {
+ return SQLiteDatabaseExtensionsKt.exists(getReadableDatabase(), getTableName(), THREAD_ID_WHERE + " AND " + getTypeField() + " = ?", threadId, Types.SMS_EXPORT_TYPE);
+ }
+
final int getSecureMessageCountForInsights() {
return getMessageCountForRecipientsAndType(getOutgoingSecureMessageClause());
}
@@ -360,16 +375,30 @@ public abstract class MessageDatabase extends Database implements MmsSmsColumns,
}
protected String getInsecureMessageClause() {
+ return getInsecureMessageClause(-1);
+ }
+
+ protected String getInsecureMessageClause(long threadId) {
String isSent = "(" + getTypeField() + " & " + Types.BASE_TYPE_MASK + ") = " + Types.BASE_SENT_TYPE;
String isReceived = "(" + getTypeField() + " & " + Types.BASE_TYPE_MASK + ") = " + Types.BASE_INBOX_TYPE;
String isSecure = "(" + getTypeField() + " & " + (Types.SECURE_MESSAGE_BIT | Types.PUSH_MESSAGE_BIT) + ")";
String isNotSecure = "(" + getTypeField() + " <= " + (Types.BASE_TYPE_MASK | Types.MESSAGE_ATTRIBUTE_MASK) + ")";
- return String.format(Locale.ENGLISH, "(%s OR %s) AND NOT %s AND %s", isSent, isReceived, isSecure, isNotSecure);
+ String whereClause = String.format(Locale.ENGLISH, "(%s OR %s) AND NOT %s AND %s", isSent, isReceived, isSecure, isNotSecure);
+
+ if (threadId != -1) {
+ whereClause += " AND " + THREAD_ID + " = " + threadId;
+ }
+
+ return whereClause;
}
public int getUnexportedInsecureMessagesCount() {
- try (Cursor cursor = getWritableDatabase().query(getTableName(), SqlUtil.COUNT, getInsecureMessageClause() + " AND NOT " + EXPORTED, null, null, null, null)) {
+ return getUnexportedInsecureMessagesCount(-1);
+ }
+
+ public int getUnexportedInsecureMessagesCount(long threadId) {
+ try (Cursor cursor = getWritableDatabase().query(getTableName(), SqlUtil.COUNT, getInsecureMessageClause(threadId) + " AND NOT " + EXPORTED, null, null, null, null)) {
if (cursor.moveToFirst()) {
return cursor.getInt(0);
}
diff --git a/app/src/main/java/org/thoughtcrime/securesms/database/MmsDatabase.java b/app/src/main/java/org/thoughtcrime/securesms/database/MmsDatabase.java
index 0f81dde45f..03d2d2d489 100644
--- a/app/src/main/java/org/thoughtcrime/securesms/database/MmsDatabase.java
+++ b/app/src/main/java/org/thoughtcrime/securesms/database/MmsDatabase.java
@@ -578,6 +578,11 @@ public class MmsDatabase extends MessageDatabase {
throw new UnsupportedOperationException();
}
+ @Override
+ public void insertSmsExportMessage(@NonNull RecipientId recipientId, long threadId) {
+ throw new UnsupportedOperationException();
+ }
+
@Override
public void endTransaction(SQLiteDatabase database) {
database.endTransaction();
@@ -2456,17 +2461,6 @@ public class MmsDatabase extends MessageDatabase {
);
}
- @Override
- public int getInsecureMessageCount() {
- try (Cursor cursor = getWritableDatabase().query(TABLE_NAME, SqlUtil.COUNT, getInsecureMessageClause(), null, null, null, null)) {
- if (cursor.moveToFirst()) {
- return cursor.getInt(0);
- }
- }
-
- return 0;
- }
-
@Override
public void deleteExportedMessages() {
beginTransaction();
diff --git a/app/src/main/java/org/thoughtcrime/securesms/database/MmsSmsColumns.java b/app/src/main/java/org/thoughtcrime/securesms/database/MmsSmsColumns.java
index 3623af4f0a..0d49bdf915 100644
--- a/app/src/main/java/org/thoughtcrime/securesms/database/MmsSmsColumns.java
+++ b/app/src/main/java/org/thoughtcrime/securesms/database/MmsSmsColumns.java
@@ -82,6 +82,7 @@ public interface MmsSmsColumns {
protected static final long CHANGE_NUMBER_TYPE = 14;
protected static final long BOOST_REQUEST_TYPE = 15;
protected static final long THREAD_MERGE_TYPE = 16;
+ protected static final long SMS_EXPORT_TYPE = 17;
protected static final long BASE_INBOX_TYPE = 20;
protected static final long BASE_OUTBOX_TYPE = 21;
@@ -366,6 +367,10 @@ public interface MmsSmsColumns {
return type == BOOST_REQUEST_TYPE;
}
+ public static boolean isSmsExport(long type) {
+ return type == SMS_EXPORT_TYPE;
+ }
+
public static boolean isGroupV2LeaveOnly(long type) {
return (type & GROUP_V2_LEAVE_BITS) == GROUP_V2_LEAVE_BITS;
}
diff --git a/app/src/main/java/org/thoughtcrime/securesms/database/MmsSmsDatabase.java b/app/src/main/java/org/thoughtcrime/securesms/database/MmsSmsDatabase.java
index 2816e38757..79331c80cd 100644
--- a/app/src/main/java/org/thoughtcrime/securesms/database/MmsSmsDatabase.java
+++ b/app/src/main/java/org/thoughtcrime/securesms/database/MmsSmsDatabase.java
@@ -121,7 +121,7 @@ public class MmsSmsDatabase extends Database {
MmsDatabase.PARENT_STORY_ID};
private static final String SNIPPET_QUERY = "SELECT " + MmsSmsColumns.ID + ", 0 AS " + TRANSPORT + ", " + SmsDatabase.TYPE + " AS " + MmsSmsColumns.NORMALIZED_TYPE + ", " + SmsDatabase.DATE_RECEIVED + " AS " + MmsSmsColumns.NORMALIZED_DATE_RECEIVED + " FROM " + SmsDatabase.TABLE_NAME + " " +
- "WHERE " + MmsSmsColumns.THREAD_ID + " = ? AND " + SmsDatabase.TYPE + " NOT IN (" + SmsDatabase.Types.PROFILE_CHANGE_TYPE + ", " + SmsDatabase.Types.GV1_MIGRATION_TYPE + ", " + SmsDatabase.Types.CHANGE_NUMBER_TYPE + ", " + SmsDatabase.Types.BOOST_REQUEST_TYPE + ") AND " + SmsDatabase.TYPE + " & " + GROUP_V2_LEAVE_BITS + " != " + GROUP_V2_LEAVE_BITS + " " +
+ "WHERE " + MmsSmsColumns.THREAD_ID + " = ? AND " + SmsDatabase.TYPE + " NOT IN (" + SmsDatabase.Types.PROFILE_CHANGE_TYPE + ", " + SmsDatabase.Types.GV1_MIGRATION_TYPE + ", " + SmsDatabase.Types.CHANGE_NUMBER_TYPE + ", " + SmsDatabase.Types.BOOST_REQUEST_TYPE + ", " + SmsDatabase.Types.SMS_EXPORT_TYPE + ") AND " + SmsDatabase.TYPE + " & " + GROUP_V2_LEAVE_BITS + " != " + GROUP_V2_LEAVE_BITS + " " +
"UNION ALL " +
"SELECT " + MmsSmsColumns.ID + ", 1 AS " + TRANSPORT + ", " + MmsDatabase.MESSAGE_BOX + " AS " + MmsSmsColumns.NORMALIZED_TYPE + ", " + MmsDatabase.DATE_RECEIVED + " AS " + MmsSmsColumns.NORMALIZED_DATE_RECEIVED + " FROM " + MmsDatabase.TABLE_NAME + " " +
"WHERE " + MmsSmsColumns.THREAD_ID + " = ? AND " + MmsDatabase.MESSAGE_BOX + " & " + GROUP_V2_LEAVE_BITS + " != " + GROUP_V2_LEAVE_BITS + " AND " + MmsDatabase.STORY_TYPE + " = 0 AND " + MmsDatabase.PARENT_STORY_ID + " <= 0 " +
@@ -401,6 +401,17 @@ public class MmsSmsDatabase extends Database {
return count;
}
+ public int getUnexportedInsecureMessagesCount() {
+ return getUnexportedInsecureMessagesCount(-1);
+ }
+
+ public int getUnexportedInsecureMessagesCount(long threadId) {
+ int count = SignalDatabase.sms().getUnexportedInsecureMessagesCount(threadId);
+ count += SignalDatabase.mms().getUnexportedInsecureMessagesCount(threadId);
+
+ return count;
+ }
+
public int getIncomingMeaningfulMessageCountSince(long threadId, long afterTime) {
int count = SignalDatabase.sms().getIncomingMeaningfulMessageCountSince(threadId, afterTime);
count += SignalDatabase.mms().getIncomingMeaningfulMessageCountSince(threadId, afterTime);
diff --git a/app/src/main/java/org/thoughtcrime/securesms/database/SmsDatabase.java b/app/src/main/java/org/thoughtcrime/securesms/database/SmsDatabase.java
index d4ee508a85..6646a17ada 100644
--- a/app/src/main/java/org/thoughtcrime/securesms/database/SmsDatabase.java
+++ b/app/src/main/java/org/thoughtcrime/securesms/database/SmsDatabase.java
@@ -34,6 +34,7 @@ import com.google.protobuf.InvalidProtocolBufferException;
import net.zetetic.database.sqlcipher.SQLiteStatement;
import org.signal.core.util.CursorUtil;
+import org.signal.core.util.SQLiteDatabaseExtensionsKt;
import org.signal.core.util.SqlUtil;
import org.signal.core.util.logging.Log;
import org.signal.libsignal.protocol.util.Pair;
@@ -300,8 +301,8 @@ public class SmsDatabase extends MessageDatabase {
}
private @NonNull SqlUtil.Query buildMeaningfulMessagesQuery(long threadId) {
- String query = THREAD_ID + " = ? AND (NOT " + TYPE + " & ? AND " + TYPE + " != ? AND " + TYPE + " != ? AND " + TYPE + " != ? AND " + TYPE + " & " + GROUP_V2_LEAVE_BITS + " != " + GROUP_V2_LEAVE_BITS + ")";
- return SqlUtil.buildQuery(query, threadId, IGNORABLE_TYPESMASK_WHEN_COUNTING, Types.PROFILE_CHANGE_TYPE, Types.CHANGE_NUMBER_TYPE, Types.BOOST_REQUEST_TYPE);
+ String query = THREAD_ID + " = ? AND (NOT " + TYPE + " & ? AND " + TYPE + " != ? AND " + TYPE + " != ? AND " + TYPE + " != ? AND " + TYPE + " != ? AND " + TYPE + " & " + GROUP_V2_LEAVE_BITS + " != " + GROUP_V2_LEAVE_BITS + ")";
+ return SqlUtil.buildQuery(query, threadId, IGNORABLE_TYPESMASK_WHEN_COUNTING, Types.PROFILE_CHANGE_TYPE, Types.CHANGE_NUMBER_TYPE, Types.SMS_EXPORT_TYPE, Types.BOOST_REQUEST_TYPE);
}
@Override
@@ -918,17 +919,6 @@ public class SmsDatabase extends MessageDatabase {
);
}
- @Override
- public int getInsecureMessageCount() {
- try (Cursor cursor = getWritableDatabase().query(TABLE_NAME, SqlUtil.COUNT, getInsecureMessageClause(), null, null, null, null)) {
- if (cursor.moveToFirst()) {
- return cursor.getInt(0);
- }
- }
-
- return 0;
- }
-
@Override
public void deleteExportedMessages() {
beginTransaction();
@@ -1139,6 +1129,32 @@ public class SmsDatabase extends MessageDatabase {
ApplicationDependencies.getDatabaseObserver().notifyConversationListeners(threadId);
}
+ @Override
+ public void insertSmsExportMessage(@NonNull RecipientId recipientId, long threadId) {
+ ContentValues values = new ContentValues();
+ values.put(RECIPIENT_ID, recipientId.serialize());
+ values.put(ADDRESS_DEVICE_ID, 1);
+ values.put(DATE_RECEIVED, System.currentTimeMillis());
+ values.put(DATE_SENT, System.currentTimeMillis());
+ values.put(READ, 1);
+ values.put(TYPE, Types.SMS_EXPORT_TYPE);
+ values.put(THREAD_ID, threadId);
+ values.putNull(BODY);
+
+ boolean updated = SQLiteDatabaseExtensionsKt.withinTransaction(getWritableDatabase(), db -> {
+ if (SignalDatabase.sms().hasSmsExportMessage(threadId)) {
+ return false;
+ } else {
+ db.insert(TABLE_NAME, null, values);
+ return true;
+ }
+ });
+
+ if (updated) {
+ ApplicationDependencies.getDatabaseObserver().notifyConversationListeners(threadId);
+ }
+ }
+
@Override
public Optional insertMessageInbox(IncomingTextMessage message, long type) {
boolean tryToCollapseJoinRequestEvents = false;
diff --git a/app/src/main/java/org/thoughtcrime/securesms/database/model/MessageRecord.java b/app/src/main/java/org/thoughtcrime/securesms/database/model/MessageRecord.java
index 870bb43d05..c456749781 100644
--- a/app/src/main/java/org/thoughtcrime/securesms/database/model/MessageRecord.java
+++ b/app/src/main/java/org/thoughtcrime/securesms/database/model/MessageRecord.java
@@ -236,6 +236,10 @@ public abstract class MessageRecord extends DisplayRecord {
} catch (InvalidProtocolBufferException e) {
throw new AssertionError(e);
}
+ } else if (isSmsExportType()) {
+ int messageResource = SignalStore.misc().getSmsExportPhase().isSmsSupported() ? R.string.MessageRecord__you_will_on_longer_be_able_to_send_sms_messages_from_signal_soon
+ : R.string.MessageRecord__you_can_no_longer_send_sms_messages_in_signal;
+ return fromRecipient(getIndividualRecipient(), r -> context.getString(messageResource, r.getDisplayName(context)), R.drawable.ic_update_info_16);
}
return null;
@@ -542,6 +546,10 @@ public abstract class MessageRecord extends DisplayRecord {
return MmsSmsColumns.Types.isThreadMergeType(type);
}
+ public boolean isSmsExportType() {
+ return MmsSmsColumns.Types.isSmsExport(type);
+ }
+
public boolean isInvalidVersionKeyExchange() {
return SmsDatabase.Types.isInvalidVersionKeyExchange(type);
}
@@ -562,7 +570,7 @@ public abstract class MessageRecord extends DisplayRecord {
return isGroupAction() || isJoined() || isExpirationTimerUpdate() || isCallLog() ||
isEndSession() || isIdentityUpdate() || isIdentityVerified() || isIdentityDefault() ||
isProfileChange() || isGroupV1MigrationEvent() || isChatSessionRefresh() || isBadDecryptType() ||
- isChangeNumber() || isBoostRequest() || isThreadMergeEventType();
+ isChangeNumber() || isBoostRequest() || isThreadMergeEventType() || isSmsExportType();
}
public boolean isMediaPending() {
diff --git a/app/src/main/java/org/thoughtcrime/securesms/exporter/SignalSmsExportService.kt b/app/src/main/java/org/thoughtcrime/securesms/exporter/SignalSmsExportService.kt
index 9e76bbf870..26c36a8791 100644
--- a/app/src/main/java/org/thoughtcrime/securesms/exporter/SignalSmsExportService.kt
+++ b/app/src/main/java/org/thoughtcrime/securesms/exporter/SignalSmsExportService.kt
@@ -5,6 +5,7 @@ import android.content.Intent
import androidx.core.app.NotificationCompat
import androidx.core.content.ContextCompat
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
@@ -12,8 +13,11 @@ import org.thoughtcrime.securesms.attachments.AttachmentId
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.notifications.NotificationChannels
import org.thoughtcrime.securesms.notifications.NotificationIds
+import org.thoughtcrime.securesms.notifications.v2.NotificationPendingIntentHelper
import org.thoughtcrime.securesms.util.JsonUtils
import java.io.InputStream
@@ -34,16 +38,47 @@ class SignalSmsExportService : SmsExportService() {
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.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.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 getUnexportedMessageCount(): Int {
ensureReader()
return reader!!.getCount()
diff --git a/app/src/main/java/org/thoughtcrime/securesms/exporter/flow/ExportSmsCompleteFragment.kt b/app/src/main/java/org/thoughtcrime/securesms/exporter/flow/ExportSmsCompleteFragment.kt
new file mode 100644
index 0000000000..a39238be8c
--- /dev/null
+++ b/app/src/main/java/org/thoughtcrime/securesms/exporter/flow/ExportSmsCompleteFragment.kt
@@ -0,0 +1,25 @@
+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.databinding.ExportSmsCompleteFragmentBinding
+import org.thoughtcrime.securesms.util.navigation.safeNavigate
+
+/**
+ * Shown when export sms completes.
+ */
+class ExportSmsCompleteFragment : Fragment(R.layout.export_sms_complete_fragment) {
+
+ val args: ExportSmsCompleteFragmentArgs by navArgs()
+
+ override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
+ val binding = ExportSmsCompleteFragmentBinding.bind(view)
+
+ binding.exportCompleteNext.setOnClickListener { findNavController().safeNavigate(ExportSmsCompleteFragmentDirections.actionExportingSmsMessagesFragmentToChooseANewDefaultSmsAppFragment()) }
+ binding.exportCompleteStatus.text = getString(R.string.ExportSmsCompleteFragment__d_of_d_messages_exported, args.exportMessageCount, args.exportMessageCount)
+ }
+}
diff --git a/app/src/main/java/org/thoughtcrime/securesms/exporter/flow/ExportYourSmsMessagesFragment.kt b/app/src/main/java/org/thoughtcrime/securesms/exporter/flow/ExportYourSmsMessagesFragment.kt
index 594f07f853..dd4914f41e 100644
--- a/app/src/main/java/org/thoughtcrime/securesms/exporter/flow/ExportYourSmsMessagesFragment.kt
+++ b/app/src/main/java/org/thoughtcrime/securesms/exporter/flow/ExportYourSmsMessagesFragment.kt
@@ -4,8 +4,13 @@ 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.SmsExportDirections
import org.thoughtcrime.securesms.databinding.ExportYourSmsMessagesFragmentBinding
import org.thoughtcrime.securesms.util.Material3OnScrollHelper
import org.thoughtcrime.securesms.util.navigation.safeNavigate
@@ -15,6 +20,8 @@ import org.thoughtcrime.securesms.util.navigation.safeNavigate
*/
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)
@@ -32,4 +39,23 @@ class ExportYourSmsMessagesFragment : Fragment(R.layout.export_your_sms_messages
Material3OnScrollHelper(requireActivity(), binding.toolbar).attach(binding.scrollView)
}
+
+ override fun onResume() {
+ super.onResume()
+ navigationDisposable = SmsExportService
+ .progressState
+ .observeOn(AndroidSchedulers.mainThread())
+ .subscribe {
+ if (it is SmsExportProgress.Done) {
+ findNavController().safeNavigate(SmsExportDirections.actionDirectToExportSmsCompleteFragment(it.progress))
+ } else if (it is SmsExportProgress.InProgress) {
+ findNavController().safeNavigate(ExportYourSmsMessagesFragmentDirections.actionExportYourSmsMessagesFragmentToExportingSmsMessagesFragment())
+ }
+ }
+ }
+
+ override fun onPause() {
+ super.onPause()
+ navigationDisposable.dispose()
+ }
}
diff --git a/app/src/main/java/org/thoughtcrime/securesms/exporter/flow/ExportingSmsMessagesFragment.kt b/app/src/main/java/org/thoughtcrime/securesms/exporter/flow/ExportingSmsMessagesFragment.kt
index a8ea19ebf9..94f04e6e4f 100644
--- a/app/src/main/java/org/thoughtcrime/securesms/exporter/flow/ExportingSmsMessagesFragment.kt
+++ b/app/src/main/java/org/thoughtcrime/securesms/exporter/flow/ExportingSmsMessagesFragment.kt
@@ -39,7 +39,7 @@ class ExportingSmsMessagesFragment : Fragment(R.layout.exporting_sms_messages_fr
.observeOn(AndroidSchedulers.mainThread())
.subscribe {
if (it is SmsExportProgress.Done) {
- findNavController().safeNavigate(ExportingSmsMessagesFragmentDirections.actionExportingSmsMessagesFragmentToChooseANewDefaultSmsAppFragment())
+ findNavController().safeNavigate(ExportingSmsMessagesFragmentDirections.actionExportingSmsMessagesFragmentToExportSmsCompleteFragment(it.progress))
}
}
}
@@ -55,7 +55,7 @@ class ExportingSmsMessagesFragment : Fragment(R.layout.exporting_sms_messages_fr
lifecycleDisposable.bindTo(viewLifecycleOwner)
lifecycleDisposable += SmsExportService.progressState.observeOn(AndroidSchedulers.mainThread()).subscribe {
when (it) {
- SmsExportProgress.Done -> Unit
+ is SmsExportProgress.Done -> Unit
is SmsExportProgress.InProgress -> {
binding.progress.isIndeterminate = false
binding.progress.max = it.total
diff --git a/app/src/main/java/org/thoughtcrime/securesms/exporter/flow/SmsExportActivity.kt b/app/src/main/java/org/thoughtcrime/securesms/exporter/flow/SmsExportActivity.kt
index ef55771be2..d4e9b4641d 100644
--- a/app/src/main/java/org/thoughtcrime/securesms/exporter/flow/SmsExportActivity.kt
+++ b/app/src/main/java/org/thoughtcrime/securesms/exporter/flow/SmsExportActivity.kt
@@ -2,10 +2,12 @@ package org.thoughtcrime.securesms.exporter.flow
import android.content.Context
import android.content.Intent
+import androidx.core.app.NotificationManagerCompat
import androidx.fragment.app.Fragment
import androidx.navigation.fragment.NavHostFragment
import org.thoughtcrime.securesms.R
import org.thoughtcrime.securesms.components.FragmentWrapperActivity
+import org.thoughtcrime.securesms.notifications.NotificationIds
import org.thoughtcrime.securesms.util.WindowUtil
class SmsExportActivity : FragmentWrapperActivity() {
@@ -13,6 +15,7 @@ class SmsExportActivity : FragmentWrapperActivity() {
override fun onResume() {
super.onResume()
WindowUtil.setLightStatusBarFromTheme(this)
+ NotificationManagerCompat.from(this).cancel(NotificationIds.SMS_EXPORT_COMPLETE)
}
override fun getFragment(): Fragment {
@@ -20,6 +23,7 @@ class SmsExportActivity : FragmentWrapperActivity() {
}
companion object {
+ @JvmStatic
fun createIntent(context: Context): Intent = Intent(context, SmsExportActivity::class.java)
}
}
diff --git a/app/src/main/java/org/thoughtcrime/securesms/exporter/flow/SmsExportDialogs.kt b/app/src/main/java/org/thoughtcrime/securesms/exporter/flow/SmsExportDialogs.kt
new file mode 100644
index 0000000000..ee13ddab9e
--- /dev/null
+++ b/app/src/main/java/org/thoughtcrime/securesms/exporter/flow/SmsExportDialogs.kt
@@ -0,0 +1,29 @@
+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.sms.deleteExportedMessages()
+ SignalDatabase.mms.deleteExportedMessages()
+ }
+ Snackbar.make(view, R.string.SmsSettingsFragment__removing_sms_messages_from_signal, Snackbar.LENGTH_SHORT).show()
+ }
+ .show()
+ }
+}
diff --git a/app/src/main/java/org/thoughtcrime/securesms/groups/ui/creategroup/CreateGroupActivity.java b/app/src/main/java/org/thoughtcrime/securesms/groups/ui/creategroup/CreateGroupActivity.java
index a8a011ef7d..b433c4a3e0 100644
--- a/app/src/main/java/org/thoughtcrime/securesms/groups/ui/creategroup/CreateGroupActivity.java
+++ b/app/src/main/java/org/thoughtcrime/securesms/groups/ui/creategroup/CreateGroupActivity.java
@@ -21,6 +21,7 @@ import org.thoughtcrime.securesms.contacts.ContactsCursorLoader;
import org.thoughtcrime.securesms.contacts.sync.ContactDiscovery;
import org.thoughtcrime.securesms.database.RecipientDatabase;
import org.thoughtcrime.securesms.groups.ui.creategroup.details.AddGroupDetailsActivity;
+import org.thoughtcrime.securesms.keyvalue.SignalStore;
import org.thoughtcrime.securesms.recipients.Recipient;
import org.thoughtcrime.securesms.recipients.RecipientId;
import org.thoughtcrime.securesms.util.FeatureFlags;
@@ -50,8 +51,9 @@ public class CreateGroupActivity extends ContactSelectionActivity {
intent.putExtra(ContactSelectionListFragment.REFRESHABLE, false);
intent.putExtra(ContactSelectionActivity.EXTRA_LAYOUT_RES_ID, R.layout.create_group_activity);
- int displayMode = Util.isDefaultSmsProvider(context) ? ContactsCursorLoader.DisplayMode.FLAG_SMS | ContactsCursorLoader.DisplayMode.FLAG_PUSH
- : ContactsCursorLoader.DisplayMode.FLAG_PUSH;
+ boolean smsEnabled = Util.isDefaultSmsProvider(context) && SignalStore.misc().getSmsExportPhase().isSmsSupported();
+ int displayMode = smsEnabled ? ContactsCursorLoader.DisplayMode.FLAG_SMS | ContactsCursorLoader.DisplayMode.FLAG_PUSH
+ : ContactsCursorLoader.DisplayMode.FLAG_PUSH;
intent.putExtra(ContactSelectionListFragment.DISPLAY_MODE, displayMode);
intent.putExtra(ContactSelectionListFragment.SELECTION_LIMITS, FeatureFlags.groupLimits().excludingSelf());
diff --git a/app/src/main/java/org/thoughtcrime/securesms/keyvalue/MiscellaneousValues.java b/app/src/main/java/org/thoughtcrime/securesms/keyvalue/MiscellaneousValues.java
index 104636d9b9..b904ba17c3 100644
--- a/app/src/main/java/org/thoughtcrime/securesms/keyvalue/MiscellaneousValues.java
+++ b/app/src/main/java/org/thoughtcrime/securesms/keyvalue/MiscellaneousValues.java
@@ -5,7 +5,7 @@ import androidx.annotation.Nullable;
import org.thoughtcrime.securesms.database.model.databaseprotos.PendingChangeNumberMetadata;
-import java.util.Collections;
+import java.util.Arrays;
import java.util.List;
public final class MiscellaneousValues extends SignalStoreValues {
@@ -26,6 +26,7 @@ public final class MiscellaneousValues extends SignalStoreValues {
private static final String LAST_FCM_FOREGROUND_TIME = "misc.last_fcm_foreground_time";
private static final String LAST_FOREGROUND_TIME = "misc.last_foreground_time";
private static final String PNI_INITIALIZED_DEVICES = "misc.pni_initialized_devices";
+ private static final String SMS_EXPORT_TIME = "misc.sms_export_time";
MiscellaneousValues(@NonNull KeyValueStore store) {
super(store);
@@ -38,7 +39,9 @@ public final class MiscellaneousValues extends SignalStoreValues {
@Override
@NonNull List getKeysToIncludeInBackup() {
- return Collections.emptyList();
+ return Arrays.asList(
+ SMS_EXPORT_TIME
+ );
}
public long getLastPrekeyRefreshTime() {
@@ -184,4 +187,15 @@ public final class MiscellaneousValues extends SignalStoreValues {
public void setPniInitializedDevices(boolean value) {
putBoolean(PNI_INITIALIZED_DEVICES, value);
}
+
+ public void setHasSeenSmsExportMegaphone() {
+ if (!getStore().containsKey(SMS_EXPORT_TIME)) {
+ putLong(SMS_EXPORT_TIME, System.currentTimeMillis());
+ }
+ }
+
+ public @NonNull SmsExportPhase getSmsExportPhase() {
+ long now = System.currentTimeMillis();
+ return SmsExportPhase.getCurrentPhase(now - getLong(SMS_EXPORT_TIME, now));
+ }
}
diff --git a/app/src/main/java/org/thoughtcrime/securesms/keyvalue/SmsExportPhase.kt b/app/src/main/java/org/thoughtcrime/securesms/keyvalue/SmsExportPhase.kt
new file mode 100644
index 0000000000..bca6f13f87
--- /dev/null
+++ b/app/src/main/java/org/thoughtcrime/securesms/keyvalue/SmsExportPhase.kt
@@ -0,0 +1,28 @@
+package org.thoughtcrime.securesms.keyvalue
+
+import kotlin.time.Duration.Companion.days
+
+enum class SmsExportPhase(val duration: Long) {
+ PHASE_1(0.days.inWholeMilliseconds),
+ PHASE_2(45.days.inWholeMilliseconds),
+ PHASE_3(105.days.inWholeMilliseconds);
+
+ fun isSmsSupported(): Boolean {
+ return this != PHASE_3
+ }
+
+ fun isFullscreen(): Boolean {
+ return this != PHASE_1
+ }
+
+ fun isBlockingUi(): Boolean {
+ return this == PHASE_3
+ }
+
+ companion object {
+ @JvmStatic
+ fun getCurrentPhase(duration: Long): SmsExportPhase {
+ return values().findLast { duration >= it.duration }!!
+ }
+ }
+}
diff --git a/app/src/main/java/org/thoughtcrime/securesms/megaphone/MegaphoneSchedule.java b/app/src/main/java/org/thoughtcrime/securesms/megaphone/MegaphoneSchedule.java
index 09e7ecfa07..95f4fbd6d0 100644
--- a/app/src/main/java/org/thoughtcrime/securesms/megaphone/MegaphoneSchedule.java
+++ b/app/src/main/java/org/thoughtcrime/securesms/megaphone/MegaphoneSchedule.java
@@ -1,5 +1,8 @@
package org.thoughtcrime.securesms.megaphone;
+import androidx.annotation.WorkerThread;
+
public interface MegaphoneSchedule {
+ @WorkerThread
boolean shouldDisplay(int seenCount, long lastSeen, long firstVisible, long currentTime);
}
diff --git a/app/src/main/java/org/thoughtcrime/securesms/megaphone/Megaphones.java b/app/src/main/java/org/thoughtcrime/securesms/megaphone/Megaphones.java
index 50dd51bbc6..9d1e8c01d7 100644
--- a/app/src/main/java/org/thoughtcrime/securesms/megaphone/Megaphones.java
+++ b/app/src/main/java/org/thoughtcrime/securesms/megaphone/Megaphones.java
@@ -3,7 +3,6 @@ package org.thoughtcrime.securesms.megaphone;
import android.annotation.SuppressLint;
import android.content.Context;
import android.content.Intent;
-import android.net.Uri;
import android.os.Build;
import android.provider.Settings;
@@ -22,8 +21,10 @@ import org.thoughtcrime.securesms.components.settings.app.AppSettingsActivity;
import org.thoughtcrime.securesms.database.model.MegaphoneRecord;
import org.thoughtcrime.securesms.database.model.RemoteMegaphoneRecord;
import org.thoughtcrime.securesms.dependencies.ApplicationDependencies;
+import org.thoughtcrime.securesms.exporter.flow.SmsExportActivity;
import org.thoughtcrime.securesms.jobmanager.impl.NetworkConstraint;
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.CreateKbsPinActivity;
@@ -39,7 +40,6 @@ import org.thoughtcrime.securesms.util.PlayServicesUtil;
import org.thoughtcrime.securesms.util.ServiceUtil;
import org.thoughtcrime.securesms.util.VersionTracker;
import org.thoughtcrime.securesms.util.dynamiclanguage.DynamicLanguageContextWrapper;
-import org.thoughtcrime.securesms.wallpaper.ChatWallpaperActivity;
import java.util.LinkedHashMap;
import java.util.List;
@@ -108,6 +108,7 @@ public final class Megaphones {
put(Event.PINS_FOR_ALL, new PinsForAllSchedule());
put(Event.CLIENT_DEPRECATED, SignalStore.misc().isClientDeprecated() ? ALWAYS : NEVER);
put(Event.NOTIFICATIONS, shouldShowNotificationsMegaphone(context) ? RecurringSchedule.every(TimeUnit.DAYS.toMillis(30)) : 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);
@@ -144,6 +145,8 @@ public final class Megaphones {
return buildRemoteMegaphone(context);
case BACKUP_SCHEDULE_PERMISSION:
return buildBackupPermissionMegaphone(context);
+ case SMS_EXPORT:
+ return buildSmsExportMegaphone(context);
default:
throw new IllegalArgumentException("Event not handled!");
}
@@ -356,6 +359,35 @@ public final class Megaphones {
.build();
}
+ private static @NonNull Megaphone buildSmsExportMegaphone(@NonNull Context context) {
+ SmsExportPhase phase = SignalStore.misc().getSmsExportPhase();
+
+ if (phase == SmsExportPhase.PHASE_1) {
+ return new Megaphone.Builder(Event.SMS_EXPORT, Megaphone.Style.BASIC)
+ .setTitle(R.string.SmsExportMegaphone__sms_support_going_away)
+ .setImage(R.drawable.sms_megaphone)
+ .setBody(R.string.SmsExportMegaphone__sms_support_will_be_removed_soon_to_focus_on_encrypted_messaging)
+ .setActionButton(R.string.SmsExportMegaphone__export_sms, (megaphone, controller) -> controller.onMegaphoneNavigationRequested(SmsExportActivity.createIntent(context), SmsExportMegaphoneActivity.REQUEST_CODE))
+ .setSecondaryButton(R.string.Megaphones_remind_me_later, (megaphone, controller) -> controller.onMegaphoneSnooze(Event.SMS_EXPORT))
+ .setOnVisibleListener((megaphone, controller) -> SignalStore.misc().setHasSeenSmsExportMegaphone())
+ .build();
+ } else {
+ 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();
+ }
+ }
+
private static boolean shouldShowDonateMegaphone(@NonNull Context context, @NonNull Event event, @NonNull Map records) {
long timeSinceLastDonatePrompt = timeSinceLastDonatePrompt(event, records);
@@ -452,7 +484,8 @@ public final class Megaphones {
DONATE_Q2_2022("donate_q2_2022"),
TURN_OFF_CENSORSHIP_CIRCUMVENTION("turn_off_censorship_circumvention"),
REMOTE_MEGAPHONE("remote_megaphone"),
- BACKUP_SCHEDULE_PERMISSION("backup_schedule_permission");
+ BACKUP_SCHEDULE_PERMISSION("backup_schedule_permission"),
+ SMS_EXPORT("sms_export");
private final String key;
diff --git a/app/src/main/java/org/thoughtcrime/securesms/megaphone/SmsExportMegaphoneActivity.kt b/app/src/main/java/org/thoughtcrime/securesms/megaphone/SmsExportMegaphoneActivity.kt
new file mode 100644
index 0000000000..0e6db7b462
--- /dev/null
+++ b/app/src/main/java/org/thoughtcrime/securesms/megaphone/SmsExportMegaphoneActivity.kt
@@ -0,0 +1,80 @@
+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.SmsExportMegaphoneActivityBinding
+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
+
+class SmsExportMegaphoneActivity : PassphraseRequiredActivity() {
+
+ companion object {
+ const val REQUEST_CODE: Short = 5343
+ }
+
+ private val theme: DynamicTheme = DynamicNoActionBarTheme()
+ private lateinit var binding: SmsExportMegaphoneActivityBinding
+ private lateinit var smsExportLauncher: ActivityResultLauncher
+
+ override fun onPreCreate() {
+ theme.onCreate(this)
+ }
+
+ override fun onCreate(savedInstanceState: Bundle?, ready: Boolean) {
+ binding = SmsExportMegaphoneActivityBinding.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() }
+
+ if (SignalStore.misc().smsExportPhase.isBlockingUi()) {
+ binding.headline.setText(R.string.SmsExportMegaphoneActivity__signal_no_longer_supports_sms)
+ binding.description.setText(R.string.SmsExportMegaphoneActivity__signal_has_removed_support_for_sending_sms_messages)
+ binding.description.setLearnMoreVisible(false)
+ binding.laterButton.setText(R.string.SmsExportMegaphoneActivity__learn_more)
+ binding.laterButton.setOnClickListener {
+ CommunicationActions.openBrowserLink(this, getString(R.string.sms_export_url))
+ }
+ } else {
+ binding.headline.setText(R.string.SmsExportMegaphoneActivity__signal_will_no_longer_support_sms)
+ binding.description.setText(R.string.SmsExportMegaphoneActivity__signal_will_soon_remove_support_for_sending_sms_messages)
+ binding.description.setLearnMoreVisible(true)
+ binding.description.setLink(getString(R.string.sms_export_url))
+ binding.laterButton.setText(R.string.SmsExportMegaphoneActivity__remind_me_later)
+ binding.laterButton.setOnClickListener {
+ onBackPressed()
+ }
+ }
+
+ binding.exportButton.setOnClickListener {
+ smsExportLauncher.launch(SmsExportActivity.createIntent(this))
+ }
+ }
+
+ override fun onBackPressed() {
+ ApplicationDependencies.getMegaphoneRepository().markSeen(Megaphones.Event.SMS_EXPORT)
+ setResult(Activity.RESULT_CANCELED)
+ super.onBackPressed()
+ }
+
+ override fun onResume() {
+ super.onResume()
+ theme.onResume(this)
+ }
+}
diff --git a/app/src/main/java/org/thoughtcrime/securesms/megaphone/SmsExportReminderSchedule.kt b/app/src/main/java/org/thoughtcrime/securesms/megaphone/SmsExportReminderSchedule.kt
new file mode 100644
index 0000000000..f6c0da0b2c
--- /dev/null
+++ b/app/src/main/java/org/thoughtcrime/securesms/megaphone/SmsExportReminderSchedule.kt
@@ -0,0 +1,39 @@
+package org.thoughtcrime.securesms.megaphone
+
+import android.content.Context
+import androidx.annotation.WorkerThread
+import org.thoughtcrime.securesms.database.SignalDatabase.Companion.mmsSms
+import org.thoughtcrime.securesms.keyvalue.SignalStore
+import org.thoughtcrime.securesms.keyvalue.SmsExportPhase
+import org.thoughtcrime.securesms.util.FeatureFlags
+import org.thoughtcrime.securesms.util.Util
+import kotlin.time.Duration.Companion.days
+
+class SmsExportReminderSchedule(private val context: Context) : MegaphoneSchedule {
+
+ companion object {
+ @JvmStatic
+ var showPhase3Megaphone = true
+ }
+
+ private val basicMegaphoneSchedule = RecurringSchedule(3.days.inWholeMilliseconds)
+ private val fullScreenSchedule = RecurringSchedule(1.days.inWholeMilliseconds)
+
+ @WorkerThread
+ override fun shouldDisplay(seenCount: Int, lastSeen: Long, firstVisible: Long, currentTime: Long): Boolean {
+ return if (shouldShowMegaphone()) {
+ when (SignalStore.misc().smsExportPhase) {
+ SmsExportPhase.PHASE_1 -> basicMegaphoneSchedule.shouldDisplay(seenCount, lastSeen, firstVisible, currentTime)
+ SmsExportPhase.PHASE_2 -> fullScreenSchedule.shouldDisplay(seenCount, lastSeen, firstVisible, currentTime)
+ SmsExportPhase.PHASE_3 -> showPhase3Megaphone
+ }
+ } else {
+ false
+ }
+ }
+
+ @WorkerThread
+ fun shouldShowMegaphone(): Boolean {
+ return FeatureFlags.smsExporter() && (Util.isDefaultSmsProvider(context) || mmsSms.unexportedInsecureMessagesCount > 0)
+ }
+}
diff --git a/app/src/main/java/org/thoughtcrime/securesms/notifications/NotificationChannels.java b/app/src/main/java/org/thoughtcrime/securesms/notifications/NotificationChannels.java
index c4125c3870..0e37004e64 100644
--- a/app/src/main/java/org/thoughtcrime/securesms/notifications/NotificationChannels.java
+++ b/app/src/main/java/org/thoughtcrime/securesms/notifications/NotificationChannels.java
@@ -74,6 +74,7 @@ public class NotificationChannels {
public static final String JOIN_EVENTS = "join_events";
public static final String BACKGROUND = "background_connection";
public static final String CALL_STATUS = "call_status";
+ public static final String APP_ALERTS = "app_alerts";
/**
* Ensures all of the notification channels are created. No harm in repeat calls. Call is safely
@@ -604,6 +605,7 @@ public class NotificationChannels {
NotificationChannel joinEvents = new NotificationChannel(JOIN_EVENTS, context.getString(R.string.NotificationChannel_contact_joined_signal), NotificationManager.IMPORTANCE_DEFAULT);
NotificationChannel background = new NotificationChannel(BACKGROUND, context.getString(R.string.NotificationChannel_background_connection), getDefaultBackgroundChannelImportance(notificationManager));
NotificationChannel callStatus = new NotificationChannel(CALL_STATUS, context.getString(R.string.NotificationChannel_call_status), NotificationManager.IMPORTANCE_LOW);
+ NotificationChannel appAlerts = new NotificationChannel(APP_ALERTS, context.getString(R.string.NotificationChannel_critical_app_alerts), NotificationManager.IMPORTANCE_HIGH);
messages.setGroup(CATEGORY_MESSAGES);
setVibrationEnabled(messages, SignalStore.settings().isMessageVibrateEnabled());
@@ -619,8 +621,9 @@ public class NotificationChannels {
joinEvents.setShowBadge(false);
background.setShowBadge(false);
callStatus.setShowBadge(false);
+ appAlerts.setShowBadge(false);
- notificationManager.createNotificationChannels(Arrays.asList(messages, calls, failures, backups, lockedStatus, other, voiceNotes, joinEvents, background, callStatus));
+ notificationManager.createNotificationChannels(Arrays.asList(messages, calls, failures, backups, lockedStatus, other, voiceNotes, joinEvents, background, callStatus, appAlerts));
if (BuildConfig.PLAY_STORE_DISABLED) {
NotificationChannel appUpdates = new NotificationChannel(APP_UPDATES, context.getString(R.string.NotificationChannel_app_updates), NotificationManager.IMPORTANCE_HIGH);
diff --git a/app/src/main/java/org/thoughtcrime/securesms/notifications/NotificationIds.java b/app/src/main/java/org/thoughtcrime/securesms/notifications/NotificationIds.java
index 095cdef950..f6cda97880 100644
--- a/app/src/main/java/org/thoughtcrime/securesms/notifications/NotificationIds.java
+++ b/app/src/main/java/org/thoughtcrime/securesms/notifications/NotificationIds.java
@@ -20,6 +20,7 @@ public final class NotificationIds {
public static final int DONOR_BADGE_FAILURE = 630001;
public static final int FCM_FETCH = 630002;
public static final int SMS_EXPORT_SERVICE = 630003;
+ public static final int SMS_EXPORT_COMPLETE = 630004;
public static final int STORY_THREAD = 700000;
public static final int MESSAGE_DELIVERY_FAILURE = 800000;
public static final int STORY_MESSAGE_DELIVERY_FAILURE = 900000;
diff --git a/app/src/main/java/org/thoughtcrime/securesms/recipients/RecipientUtil.java b/app/src/main/java/org/thoughtcrime/securesms/recipients/RecipientUtil.java
index 309d31411a..8ac34b8bc1 100644
--- a/app/src/main/java/org/thoughtcrime/securesms/recipients/RecipientUtil.java
+++ b/app/src/main/java/org/thoughtcrime/securesms/recipients/RecipientUtil.java
@@ -352,6 +352,11 @@ public class RecipientUtil {
return threadId != null && SignalDatabase.mmsSms().getOutgoingSecureConversationCount(threadId) != 0;
}
+ public static boolean isSmsOnly(long threadId, @NonNull Recipient threadRecipient) {
+ return !threadRecipient.isRegistered() ||
+ noSecureMessagesAndNoCallsInThread(threadId);
+ }
+
@WorkerThread
private static boolean noSecureMessagesAndNoCallsInThread(@Nullable Long threadId) {
if (threadId == null) {
diff --git a/app/src/main/java/org/thoughtcrime/securesms/recipients/ui/bottomsheet/RecipientBottomSheetDialogFragment.java b/app/src/main/java/org/thoughtcrime/securesms/recipients/ui/bottomsheet/RecipientBottomSheetDialogFragment.java
index 5a01f67b39..476a105e65 100644
--- a/app/src/main/java/org/thoughtcrime/securesms/recipients/ui/bottomsheet/RecipientBottomSheetDialogFragment.java
+++ b/app/src/main/java/org/thoughtcrime/securesms/recipients/ui/bottomsheet/RecipientBottomSheetDialogFragment.java
@@ -33,6 +33,7 @@ import org.thoughtcrime.securesms.components.settings.conversation.preferences.B
import org.thoughtcrime.securesms.contacts.avatars.FallbackContactPhoto;
import org.thoughtcrime.securesms.contacts.avatars.FallbackPhoto80dp;
import org.thoughtcrime.securesms.groups.GroupId;
+import org.thoughtcrime.securesms.keyvalue.SignalStore;
import org.thoughtcrime.securesms.phonenumbers.PhoneNumberFormatter;
import org.thoughtcrime.securesms.recipients.Recipient;
import org.thoughtcrime.securesms.recipients.RecipientExporter;
@@ -221,10 +222,16 @@ public final class RecipientBottomSheetDialogFragment extends BottomSheetDialogF
unblockButton.setVisibility(View.GONE);
}
+ boolean isAudioAvailable = (recipient.isRegistered() || (Util.isDefaultSmsProvider(requireContext()) && SignalStore.misc().getSmsExportPhase().isSmsSupported())) &&
+ !recipient.isGroup() &&
+ !recipient.isBlocked() &&
+ !recipient.isSelf() &&
+ !recipient.isReleaseNotes();
+
ButtonStripPreference.State buttonStripState = new ButtonStripPreference.State(
/* isMessageAvailable = */ !recipient.isBlocked() && !recipient.isSelf() && !recipient.isReleaseNotes(),
/* isVideoAvailable = */ !recipient.isBlocked() && !recipient.isSelf() && recipient.isRegistered(),
- /* isAudioAvailable = */ !recipient.isBlocked() && !recipient.isSelf() && !recipient.isReleaseNotes(),
+ /* isAudioAvailable = */ isAudioAvailable,
/* isMuteAvailable = */ false,
/* isSearchAvailable = */ false,
/* isAudioSecure = */ recipient.isRegistered(),
diff --git a/app/src/main/java/org/thoughtcrime/securesms/util/views/Stub.java b/app/src/main/java/org/thoughtcrime/securesms/util/views/Stub.java
index 7d96484446..dd20d35fad 100644
--- a/app/src/main/java/org/thoughtcrime/securesms/util/views/Stub.java
+++ b/app/src/main/java/org/thoughtcrime/securesms/util/views/Stub.java
@@ -1,14 +1,15 @@
package org.thoughtcrime.securesms.util.views;
+import android.view.View;
import android.view.ViewStub;
import androidx.annotation.NonNull;
-public class Stub {
+public class Stub {
private ViewStub viewStub;
- private T view;
+ private T view;
public Stub(@NonNull ViewStub viewStub) {
this.viewStub = viewStub;
@@ -16,7 +17,8 @@ public class Stub {
public T get() {
if (view == null) {
- view = (T)viewStub.inflate();
+ //noinspection unchecked
+ view = (T) viewStub.inflate();
viewStub = null;
}
@@ -27,4 +29,10 @@ public class Stub {
return view != null;
}
+ public void setVisibility(int visibility) {
+ if (resolved()) {
+ get().setVisibility(visibility);
+ }
+ }
+
}
diff --git a/app/src/main/res/drawable-night/sms_megaphone.xml b/app/src/main/res/drawable-night/sms_megaphone.xml
new file mode 100644
index 0000000000..4ee39d0ece
--- /dev/null
+++ b/app/src/main/res/drawable-night/sms_megaphone.xml
@@ -0,0 +1,18 @@
+
+
+
+
+
diff --git a/app/src/main/res/drawable/ic_complete_64.xml b/app/src/main/res/drawable/ic_complete_64.xml
new file mode 100644
index 0000000000..09ea6c8a74
--- /dev/null
+++ b/app/src/main/res/drawable/ic_complete_64.xml
@@ -0,0 +1,10 @@
+
+
+
diff --git a/app/src/main/res/drawable/sms_megaphone.xml b/app/src/main/res/drawable/sms_megaphone.xml
new file mode 100644
index 0000000000..b0eadff7ce
--- /dev/null
+++ b/app/src/main/res/drawable/sms_megaphone.xml
@@ -0,0 +1,18 @@
+
+
+
+
+
diff --git a/app/src/main/res/layout/add_group_details_fragment.xml b/app/src/main/res/layout/add_group_details_fragment.xml
index bde49ad434..b6dc6a0c01 100644
--- a/app/src/main/res/layout/add_group_details_fragment.xml
+++ b/app/src/main/res/layout/add_group_details_fragment.xml
@@ -111,7 +111,7 @@
android:paddingTop="16dp"
android:paddingEnd="16dp"
android:paddingBottom="16dp"
- android:text="@string/AddGroupDetailsFragment__youve_selected_a_contact_that_doesnt_support"
+ android:text="@string/AddGroupDetailsFragment__youve_selected_a_contact_that_doesnt_support_signal_groups_mms_removal"
android:textAppearance="@style/Signal.Text.BodyMedium"
android:textColor="@color/signal_colorOnSurfaceVariant" />
diff --git a/app/src/main/res/layout/conversation_activity.xml b/app/src/main/res/layout/conversation_activity.xml
index 020c9af498..6b6bb97e0c 100644
--- a/app/src/main/res/layout/conversation_activity.xml
+++ b/app/src/main/res/layout/conversation_activity.xml
@@ -134,13 +134,12 @@
android:text="@string/ConversationActivity_unblock"
android:visibility="gone" />
-
+ android:inflatedId="@+id/sms_export_view"
+ android:layout="@layout/conversation_activity_sms_export_stub" />
+
+
+
+
+
diff --git a/app/src/main/res/layout/conversation_activity_sms_export_stub.xml b/app/src/main/res/layout/conversation_activity_sms_export_stub.xml
new file mode 100644
index 0000000000..3880db9947
--- /dev/null
+++ b/app/src/main/res/layout/conversation_activity_sms_export_stub.xml
@@ -0,0 +1,27 @@
+
+
+
+
+
+
+
+
diff --git a/app/src/main/res/layout/dsl_outlined_learn_more.xml b/app/src/main/res/layout/dsl_outlined_learn_more.xml
new file mode 100644
index 0000000000..48cccfa096
--- /dev/null
+++ b/app/src/main/res/layout/dsl_outlined_learn_more.xml
@@ -0,0 +1,12 @@
+
+
diff --git a/app/src/main/res/layout/export_sms_complete_fragment.xml b/app/src/main/res/layout/export_sms_complete_fragment.xml
new file mode 100644
index 0000000000..86fbc69b95
--- /dev/null
+++ b/app/src/main/res/layout/export_sms_complete_fragment.xml
@@ -0,0 +1,58 @@
+
+
+
+
+
+
+
+
+
+
+
+
\ No newline at end of file
diff --git a/app/src/main/res/layout/exporting_sms_messages_fragment.xml b/app/src/main/res/layout/exporting_sms_messages_fragment.xml
index 6763892379..7963b1d1ff 100644
--- a/app/src/main/res/layout/exporting_sms_messages_fragment.xml
+++ b/app/src/main/res/layout/exporting_sms_messages_fragment.xml
@@ -1,9 +1,9 @@
+ android:layout_height="match_parent">
-
+ android:textColor="@color/signal_colorOnSurfaceVariant"
+ app:layout_constraintTop_toBottomOf="@id/progress"
+ tools:text="Exporting 5 of 264..." />
\ No newline at end of file
diff --git a/app/src/main/res/layout/sms_export_megaphone_activity.xml b/app/src/main/res/layout/sms_export_megaphone_activity.xml
new file mode 100644
index 0000000000..a5e02cdcbd
--- /dev/null
+++ b/app/src/main/res/layout/sms_export_megaphone_activity.xml
@@ -0,0 +1,99 @@
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
diff --git a/app/src/main/res/navigation/sms_export.xml b/app/src/main/res/navigation/sms_export.xml
index 62e3272da2..e7d5539853 100644
--- a/app/src/main/res/navigation/sms_export.xml
+++ b/app/src/main/res/navigation/sms_export.xml
@@ -34,7 +34,9 @@
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:popExitAnim="@anim/fragment_close_exit"
+ app:popUpTo="@+id/exportYourSmsMessagesFragment"
+ app:popUpToInclusive="true" />
+
+
+
+
+
+
+
+
+
+
\ No newline at end of file
diff --git a/app/src/main/res/values/strings.xml b/app/src/main/res/values/strings.xml
index 60e055b58c..195797f4dc 100644
--- a/app/src/main/res/values/strings.xml
+++ b/app/src/main/res/values/strings.xml
@@ -11,6 +11,7 @@
https://support.signal.org/hc/articles/4408365318426
https://pay.google.com
https://support.signal.org/hc/articles/4408365318426#errors
+ https://support.signal.org/hc/articles/360007321171
https://signal.me/#u/%1$s
signal.me/#u/%1$s
@@ -343,6 +344,17 @@
Reported as spam and blocked.
+
+ SMS messaging is no longer supported in Signal. You can export your messages to another app on your phone.
+
+ Export SMS messages
+
+ SMS messaging is no longer supported in Signal. Invite %1$s to Signal to keep the conversation here.
+
+ Invite to Signal
+
+ You will be reminded again soon.
+
- %d unread message
@@ -802,11 +814,11 @@
This field is required.
Group creation failed.
Try again later.
-
- You\'ve selected a contact that doesn\'t support Signal groups, so this group will be MMS. Custom MMS group names and photos will only be visible to you.
Remove
SMS contact
Remove %1$s from this group?
+
+ You\'ve selected a contact that dosen\'t support Signal groups, this group will be MMS. Custom MMS group names and photos will only be visible to you. Support for MMS groups will be removed soon to focus on encrypted messaging.
Member requests & invites
@@ -990,6 +1002,8 @@
Tap and hold to record a voice message, release to send
+
+ SMS messaging is no longer supported in Signal.
Share
@@ -1375,6 +1389,11 @@
- %1$s, %2$s, and %3$d others are in the group call
+
+ You will on longer be able to send SMS messages from Signal soon. Invite %1$s to Signal to keep the conversation here.
+
+ You can no longer send SMS messages in Signal. Invite %1$s to Signal to keep the conversation here.
+
Accept
Continue
@@ -2017,6 +2036,9 @@
Background connection
Call status
+
+ Critical app alerts
+
@@ -3976,6 +3998,12 @@
Removing SMS messages from Signal…
You can remove SMS messages from Signal in Settings at any time.
+
+ You can export your SMS messages to your phone\'s SMS database
+
+ Remove SMS messages from Signal to clear up storage space.
+
+ SMS support will be removed soon to focus on encrypted messaging.
Messages
@@ -4236,6 +4264,8 @@
This person is saved to your device\'s Contacts. Delete them from your Contacts and try again.
View contact
+
+ %1$s is not a Signal user
Search name or number
@@ -5289,6 +5319,10 @@
Exporting messages…
+
+ Signal SMS Export Complete
+
+ Tap to return to Signal
@@ -5301,6 +5335,8 @@
Exporting SMS messages
+
+ This may take awhile
Exporting %1$d of %2$d…
@@ -5368,6 +5404,35 @@
Go to settings
+
+
+ Signal will no longer support SMS
+
+ Signal no longer supports SMS
+
+ Signal will soon remove support for sending SMS messages because Signal messages provide end-to-end encryption and strong privacy that SMS messages don\'t. This will also allow us to improve the Signal messaging experience.
+
+ Signal has removed support for sending SMS messages because Signal messages provide end-to-end encryption and strong privacy that SMS messages don\'t. This will also allow us to improve the Signal messaging experience.
+
+ Export SMS
+
+ Remind me later
+
+ Learn more
+
+
+ SMS support going away
+
+ SMS support will be removed soon to focus on encrypted messaging.
+
+ Export SMS
+
+ Export Complete
+
+ Next
+
+ %1$d of %2$d messages exported
+
diff --git a/app/src/spinner/java/org/thoughtcrime/securesms/database/MessageBitmaskColumnTransformer.kt b/app/src/spinner/java/org/thoughtcrime/securesms/database/MessageBitmaskColumnTransformer.kt
index 6e75688b9b..3d95d88e18 100644
--- a/app/src/spinner/java/org/thoughtcrime/securesms/database/MessageBitmaskColumnTransformer.kt
+++ b/app/src/spinner/java/org/thoughtcrime/securesms/database/MessageBitmaskColumnTransformer.kt
@@ -50,9 +50,11 @@ import org.thoughtcrime.securesms.database.MmsSmsColumns.Types.OUTGOING_VIDEO_CA
import org.thoughtcrime.securesms.database.MmsSmsColumns.Types.PROFILE_CHANGE_TYPE
import org.thoughtcrime.securesms.database.MmsSmsColumns.Types.PUSH_MESSAGE_BIT
import org.thoughtcrime.securesms.database.MmsSmsColumns.Types.SECURE_MESSAGE_BIT
+import org.thoughtcrime.securesms.database.MmsSmsColumns.Types.SMS_EXPORT_TYPE
import org.thoughtcrime.securesms.database.MmsSmsColumns.Types.SPECIAL_TYPES_MASK
import org.thoughtcrime.securesms.database.MmsSmsColumns.Types.SPECIAL_TYPE_GIFT_BADGE
import org.thoughtcrime.securesms.database.MmsSmsColumns.Types.SPECIAL_TYPE_STORY_REACTION
+import org.thoughtcrime.securesms.database.MmsSmsColumns.Types.THREAD_MERGE_TYPE
import org.thoughtcrime.securesms.database.MmsSmsColumns.Types.UNSUPPORTED_MESSAGE_TYPE
object MessageBitmaskColumnTransformer : ColumnTransformer {
@@ -111,6 +113,8 @@ object MessageBitmaskColumnTransformer : ColumnTransformer {
isGroupV1MigrationEvent:${type == GV1_MIGRATION_TYPE}
isChangeNumber:${type == CHANGE_NUMBER_TYPE}
isBoostRequest:${type == BOOST_REQUEST_TYPE}
+ isThreadMerge:${type == THREAD_MERGE_TYPE}
+ isSmsExport:${type == SMS_EXPORT_TYPE}
isGroupV2LeaveOnly:${type and GROUP_V2_LEAVE_BITS == GROUP_V2_LEAVE_BITS}
isSpecialType:${type and SPECIAL_TYPES_MASK != 0L}
isStoryReaction:${type and SPECIAL_TYPES_MASK == SPECIAL_TYPE_STORY_REACTION}
diff --git a/app/src/test/java/org/thoughtcrime/securesms/database/SmsDatabaseTest.kt b/app/src/test/java/org/thoughtcrime/securesms/database/SmsDatabaseTest.kt
index d88e9ed79e..a7b19eb954 100644
--- a/app/src/test/java/org/thoughtcrime/securesms/database/SmsDatabaseTest.kt
+++ b/app/src/test/java/org/thoughtcrime/securesms/database/SmsDatabaseTest.kt
@@ -81,6 +81,9 @@ class SmsDatabaseTest {
TestSms.insert(db, type = MmsSmsColumns.Types.BOOST_REQUEST_TYPE)
assertFalse(smsDatabase.hasMeaningfulMessage(1))
+ TestSms.insert(db, type = MmsSmsColumns.Types.SMS_EXPORT_TYPE)
+ assertFalse(smsDatabase.hasMeaningfulMessage(1))
+
TestSms.insert(db, type = MmsSmsColumns.Types.BASE_INBOX_TYPE or MmsSmsColumns.Types.GROUP_V2_LEAVE_BITS)
assertFalse(smsDatabase.hasMeaningfulMessage(1))
}
diff --git a/core-util/src/main/java/org/signal/core/util/SQLiteDatabaseExtensions.kt b/core-util/src/main/java/org/signal/core/util/SQLiteDatabaseExtensions.kt
index a92fd55984..ead9993b5c 100644
--- a/core-util/src/main/java/org/signal/core/util/SQLiteDatabaseExtensions.kt
+++ b/core-util/src/main/java/org/signal/core/util/SQLiteDatabaseExtensions.kt
@@ -50,6 +50,13 @@ fun SupportSQLiteDatabase.select(vararg columns: String): SelectBuilderPart1 {
return SelectBuilderPart1(this, arrayOf(*columns))
}
+/**
+ * Begins a COUNT statement with a helpful builder pattern.
+ */
+fun SupportSQLiteDatabase.count(): SelectBuilderPart1 {
+ return SelectBuilderPart1(this, SqlUtil.COUNT)
+}
+
/**
* Begins an UPDATE statement with a helpful builder pattern.
*/
diff --git a/sms-exporter/app/src/main/java/org/signal/smsexporter/app/MainActivity.kt b/sms-exporter/app/src/main/java/org/signal/smsexporter/app/MainActivity.kt
index a32fd25033..cba4ea379a 100644
--- a/sms-exporter/app/src/main/java/org/signal/smsexporter/app/MainActivity.kt
+++ b/sms-exporter/app/src/main/java/org/signal/smsexporter/app/MainActivity.kt
@@ -38,7 +38,7 @@ class MainActivity : AppCompatActivity(R.layout.main_activity) {
disposables += SmsExportService.progressState.onBackpressureLatest().subscribeOn(Schedulers.computation()).observeOn(AndroidSchedulers.mainThread()).subscribe {
when (it) {
- SmsExportProgress.Done -> {
+ is SmsExportProgress.Done -> {
exportStatus.text = "Done"
exportProgress.isVisible = true
}
diff --git a/sms-exporter/app/src/main/java/org/signal/smsexporter/app/TestSmsExportService.kt b/sms-exporter/app/src/main/java/org/signal/smsexporter/app/TestSmsExportService.kt
index 6b8aceaee7..94efeba52d 100644
--- a/sms-exporter/app/src/main/java/org/signal/smsexporter/app/TestSmsExportService.kt
+++ b/sms-exporter/app/src/main/java/org/signal/smsexporter/app/TestSmsExportService.kt
@@ -33,6 +33,10 @@ class TestSmsExportService : SmsExportService() {
)
}
+ override fun getExportCompleteNotification(): ExportNotification? {
+ return null
+ }
+
override fun getUnexportedMessageCount(): Int {
return 50
}
diff --git a/sms-exporter/lib/src/main/java/org/signal/smsexporter/SmsExportProgress.kt b/sms-exporter/lib/src/main/java/org/signal/smsexporter/SmsExportProgress.kt
index e2d3ea0042..001ce7e628 100644
--- a/sms-exporter/lib/src/main/java/org/signal/smsexporter/SmsExportProgress.kt
+++ b/sms-exporter/lib/src/main/java/org/signal/smsexporter/SmsExportProgress.kt
@@ -25,5 +25,5 @@ sealed class SmsExportProgress {
/**
* All done.
*/
- object Done : SmsExportProgress()
+ data class Done(val progress: Int) : SmsExportProgress()
}
diff --git a/sms-exporter/lib/src/main/java/org/signal/smsexporter/SmsExportService.kt b/sms-exporter/lib/src/main/java/org/signal/smsexporter/SmsExportService.kt
index 0135a5e4c3..86d53670a1 100644
--- a/sms-exporter/lib/src/main/java/org/signal/smsexporter/SmsExportService.kt
+++ b/sms-exporter/lib/src/main/java/org/signal/smsexporter/SmsExportService.kt
@@ -4,6 +4,7 @@ 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
@@ -78,7 +79,12 @@ abstract class SmsExportService : Service() {
}
onExportPassCompleted()
- progressState.onNext(SmsExportProgress.Done)
+ progressState.onNext(SmsExportProgress.Done(progress))
+
+ getExportCompleteNotification()?.let { notification ->
+ NotificationManagerCompat.from(this).notify(notification.id, notification.notification)
+ }
+
stopForeground(true)
isStarted = false
}
@@ -97,6 +103,13 @@ abstract class SmsExportService : Service() {
*/
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?
+
/**
* Gets the total number of messages to process. This is only used for the notification and
* progress events.