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" /> -