diff --git a/app/src/main/java/org/thoughtcrime/securesms/ContactSelectionListFragment.java b/app/src/main/java/org/thoughtcrime/securesms/ContactSelectionListFragment.java index 5548c592b5..34d38114d3 100644 --- a/app/src/main/java/org/thoughtcrime/securesms/ContactSelectionListFragment.java +++ b/app/src/main/java/org/thoughtcrime/securesms/ContactSelectionListFragment.java @@ -684,7 +684,7 @@ public final class ContactSelectionListFragment extends LoggingFragment { boolean isUnknown = contact instanceof ContactSearchKey.UnknownRecipientKey; SelectedContact selectedContact = contact.requireSelectedContact(); - if (!canSelectSelf && !selectedContact.hasUsername() && Recipient.self().getId().equals(selectedContact.getOrCreateRecipientId(requireContext()))) { + if (!canSelectSelf && !selectedContact.hasUsername() && Recipient.self().getId().equals(selectedContact.getOrCreateRecipientId())) { Toast.makeText(requireContext(), R.string.ContactSelectionListFragment_you_do_not_need_to_add_yourself_to_the_group, Toast.LENGTH_SHORT).show(); return; } @@ -840,7 +840,7 @@ public final class ContactSelectionListFragment extends LoggingFragment { contactChipViewModel.add(selectedContact); } else { SimpleTask.run(getViewLifecycleOwner().getLifecycle(), - () -> Recipient.resolved(selectedContact.getOrCreateRecipientId(requireContext())), + () -> Recipient.resolved(selectedContact.getOrCreateRecipientId()), resolved -> contactChipViewModel.add(selectedContact)); } } diff --git a/app/src/main/java/org/thoughtcrime/securesms/InviteActivity.java b/app/src/main/java/org/thoughtcrime/securesms/InviteActivity.java index aabdefa56f..b7d23a9e73 100644 --- a/app/src/main/java/org/thoughtcrime/securesms/InviteActivity.java +++ b/app/src/main/java/org/thoughtcrime/securesms/InviteActivity.java @@ -237,7 +237,7 @@ public class InviteActivity extends PassphraseRequiredActivity implements Contac if (context == null) return null; for (SelectedContact contact : contacts) { - RecipientId recipientId = contact.getOrCreateRecipientId(context); + RecipientId recipientId = contact.getOrCreateRecipientId(); Recipient recipient = Recipient.resolved(recipientId); MessageSender.send(context, OutgoingMessage.sms(recipient, message), -1L, MessageSender.SendType.SMS, null, null); diff --git a/app/src/main/java/org/thoughtcrime/securesms/NewConversationActivity.java b/app/src/main/java/org/thoughtcrime/securesms/NewConversationActivity.java index 756a0a7cf3..6638254d4c 100644 --- a/app/src/main/java/org/thoughtcrime/securesms/NewConversationActivity.java +++ b/app/src/main/java/org/thoughtcrime/securesms/NewConversationActivity.java @@ -130,7 +130,7 @@ public class NewConversationActivity extends ContactSelectionActivity AlertDialog progress = SimpleProgressDialog.show(this); - SimpleTask.run(getLifecycle(), () -> RecipientRepository.lookupNewE164(this, number), result -> { + SimpleTask.run(getLifecycle(), () -> RecipientRepository.lookupNewE164(number), result -> { progress.dismiss(); if (result instanceof RecipientRepository.LookupResult.Success) { diff --git a/app/src/main/java/org/thoughtcrime/securesms/PushContactSelectionActivity.java b/app/src/main/java/org/thoughtcrime/securesms/PushContactSelectionActivity.java index 6d724cb007..2e7cf946e5 100644 --- a/app/src/main/java/org/thoughtcrime/securesms/PushContactSelectionActivity.java +++ b/app/src/main/java/org/thoughtcrime/securesms/PushContactSelectionActivity.java @@ -58,7 +58,7 @@ public class PushContactSelectionActivity extends ContactSelectionActivity { protected final void onFinishedSelection() { Intent resultIntent = getIntent(); List selectedContacts = contactsFragment.getSelectedContacts(); - List recipients = Stream.of(selectedContacts).map(sc -> sc.getOrCreateRecipientId(this)).toList(); + List recipients = Stream.of(selectedContacts).map(sc -> sc.getOrCreateRecipientId()).toList(); resultIntent.putParcelableArrayListExtra(KEY_SELECTED_RECIPIENTS, new ArrayList<>(recipients)); diff --git a/app/src/main/java/org/thoughtcrime/securesms/SmsSendtoActivity.java b/app/src/main/java/org/thoughtcrime/securesms/SmsSendtoActivity.java index b349b7c764..d051114983 100644 --- a/app/src/main/java/org/thoughtcrime/securesms/SmsSendtoActivity.java +++ b/app/src/main/java/org/thoughtcrime/securesms/SmsSendtoActivity.java @@ -47,12 +47,19 @@ public class SmsSendtoActivity extends Activity { nextIntent.putExtra(Intent.EXTRA_TEXT, destination.getBody()); Toast.makeText(this, R.string.ConversationActivity_specify_recipient, Toast.LENGTH_LONG).show(); } else { - Recipient recipient = Recipient.external(this, destination.getDestination()); - long threadId = SignalDatabase.threads().getOrCreateThreadIdFor(recipient); + Recipient recipient = Recipient.external(destination.getDestination()); - nextIntent = ConversationIntents.createBuilderSync(this, recipient.getId(), threadId) - .withDraftText(destination.getBody()) - .build(); + if (recipient != null) { + long threadId = SignalDatabase.threads().getOrCreateThreadIdFor(recipient); + + nextIntent = ConversationIntents.createBuilderSync(this, recipient.getId(), threadId) + .withDraftText(destination.getBody()) + .build(); + } else { + nextIntent = new Intent(this, NewConversationActivity.class); + nextIntent.putExtra(Intent.EXTRA_TEXT, destination.getBody()); + Toast.makeText(this, R.string.ConversationActivity_specify_recipient, Toast.LENGTH_LONG).show(); + } } return nextIntent; } diff --git a/app/src/main/java/org/thoughtcrime/securesms/backup/v2/importer/ContactArchiveImporter.kt b/app/src/main/java/org/thoughtcrime/securesms/backup/v2/importer/ContactArchiveImporter.kt index 74ec7920ce..d939c471ae 100644 --- a/app/src/main/java/org/thoughtcrime/securesms/backup/v2/importer/ContactArchiveImporter.kt +++ b/app/src/main/java/org/thoughtcrime/securesms/backup/v2/importer/ContactArchiveImporter.kt @@ -16,11 +16,10 @@ import org.thoughtcrime.securesms.database.RecipientTable import org.thoughtcrime.securesms.database.SQLiteDatabase import org.thoughtcrime.securesms.database.SignalDatabase import org.thoughtcrime.securesms.database.model.databaseprotos.RecipientExtras -import org.thoughtcrime.securesms.dependencies.AppDependencies -import org.thoughtcrime.securesms.phonenumbers.PhoneNumberFormatter import org.thoughtcrime.securesms.profiles.ProfileName import org.thoughtcrime.securesms.recipients.Recipient import org.thoughtcrime.securesms.recipients.RecipientId +import org.thoughtcrime.securesms.util.SignalE164Util import org.whispersystems.signalservice.api.push.ServiceId.ACI import org.whispersystems.signalservice.api.push.ServiceId.PNI @@ -112,6 +111,6 @@ private fun Contact.toLocalExtras(): RecipientExtras { private val Contact.formattedE164: String? get() { return e164?.let { - PhoneNumberFormatter.get(AppDependencies.application).format(e164.toString()) + SignalE164Util.formatAsE164(e164.toString()) } } diff --git a/app/src/main/java/org/thoughtcrime/securesms/blocked/BlockedUsersRepository.java b/app/src/main/java/org/thoughtcrime/securesms/blocked/BlockedUsersRepository.java index 0b7d53d623..f9ae4e2aa7 100644 --- a/app/src/main/java/org/thoughtcrime/securesms/blocked/BlockedUsersRepository.java +++ b/app/src/main/java/org/thoughtcrime/securesms/blocked/BlockedUsersRepository.java @@ -7,7 +7,6 @@ import androidx.core.util.Consumer; import org.signal.core.util.concurrent.SignalExecutors; import org.signal.core.util.logging.Log; -import org.thoughtcrime.securesms.database.RecipientTable; import org.thoughtcrime.securesms.database.SignalDatabase; import org.thoughtcrime.securesms.database.model.RecipientRecord; import org.thoughtcrime.securesms.groups.GroupChangeBusyException; @@ -17,8 +16,6 @@ import org.thoughtcrime.securesms.recipients.RecipientId; import org.thoughtcrime.securesms.recipients.RecipientUtil; import java.io.IOException; -import java.util.ArrayList; -import java.util.Collections; import java.util.List; import java.util.stream.Collectors; @@ -56,7 +53,12 @@ class BlockedUsersRepository { void createAndBlock(@NonNull String number, @NonNull Runnable success) { SignalExecutors.BOUNDED.execute(() -> { - RecipientUtil.blockNonGroup(context, Recipient.external(context, number)); + Recipient recipient = Recipient.external(number); + if (recipient != null) { + RecipientUtil.blockNonGroup(context, recipient); + } else { + Log.w(TAG, "Failed to create Recipient for number! Invalid input."); + } success.run(); }); } diff --git a/app/src/main/java/org/thoughtcrime/securesms/calls/new/NewCallActivity.kt b/app/src/main/java/org/thoughtcrime/securesms/calls/new/NewCallActivity.kt index cab1820b48..9d8938ec06 100644 --- a/app/src/main/java/org/thoughtcrime/securesms/calls/new/NewCallActivity.kt +++ b/app/src/main/java/org/thoughtcrime/securesms/calls/new/NewCallActivity.kt @@ -49,7 +49,7 @@ class NewCallActivity : ContactSelectionActivity(), ContactSelectionListFragment val progress = SimpleProgressDialog.show(this) - SimpleTask.run(lifecycle, { RecipientRepository.lookupNewE164(this, number!!) }, { result -> + SimpleTask.run(lifecycle, { RecipientRepository.lookupNewE164(number!!) }, { result -> progress.dismiss() when (result) { diff --git a/app/src/main/java/org/thoughtcrime/securesms/components/ComposeText.java b/app/src/main/java/org/thoughtcrime/securesms/components/ComposeText.java index 62e1e47f0b..089fce83c3 100644 --- a/app/src/main/java/org/thoughtcrime/securesms/components/ComposeText.java +++ b/app/src/main/java/org/thoughtcrime/securesms/components/ComposeText.java @@ -7,7 +7,6 @@ import android.os.Build; import android.os.Bundle; import android.text.Annotation; import android.text.Editable; -import android.text.InputType; import android.text.Selection; import android.text.SpannableString; import android.text.SpannableStringBuilder; diff --git a/app/src/main/java/org/thoughtcrime/securesms/components/SharedContactView.java b/app/src/main/java/org/thoughtcrime/securesms/components/SharedContactView.java index 887045908c..59b0497b84 100644 --- a/app/src/main/java/org/thoughtcrime/securesms/components/SharedContactView.java +++ b/app/src/main/java/org/thoughtcrime/securesms/components/SharedContactView.java @@ -121,7 +121,7 @@ public class SharedContactView extends LinearLayout implements RecipientForeverO presentContact(contact); presentAvatar(contact.getAvatarAttachment() != null ? contact.getAvatarAttachment().getUri() : null); - presentActionButtons(ContactUtil.getRecipients(getContext(), contact)); + presentActionButtons(ContactUtil.getRecipients(contact)); for (LiveRecipient recipient : activeRecipients.values()) { recipient.observeForever(this); diff --git a/app/src/main/java/org/thoughtcrime/securesms/components/emoji/EmojiUtil.java b/app/src/main/java/org/thoughtcrime/securesms/components/emoji/EmojiUtil.java index 0e47d14a98..16e57bde2b 100644 --- a/app/src/main/java/org/thoughtcrime/securesms/components/emoji/EmojiUtil.java +++ b/app/src/main/java/org/thoughtcrime/securesms/components/emoji/EmojiUtil.java @@ -6,10 +6,10 @@ import android.graphics.drawable.Drawable; import androidx.annotation.NonNull; import androidx.annotation.Nullable; +import org.signal.core.util.StringUtil; import org.thoughtcrime.securesms.components.emoji.parsing.EmojiParser; import org.thoughtcrime.securesms.emoji.EmojiSource; import org.thoughtcrime.securesms.emoji.ObsoleteEmoji; -import org.signal.core.util.StringUtil; import org.thoughtcrime.securesms.util.Util; import java.util.HashSet; diff --git a/app/src/main/java/org/thoughtcrime/securesms/components/settings/app/AppSettingsActivity.kt b/app/src/main/java/org/thoughtcrime/securesms/components/settings/app/AppSettingsActivity.kt index a82ed500da..3dc65e06db 100644 --- a/app/src/main/java/org/thoughtcrime/securesms/components/settings/app/AppSettingsActivity.kt +++ b/app/src/main/java/org/thoughtcrime/securesms/components/settings/app/AppSettingsActivity.kt @@ -18,11 +18,11 @@ import org.thoughtcrime.securesms.components.settings.app.subscription.StripeRep import org.thoughtcrime.securesms.help.HelpFragment import org.thoughtcrime.securesms.keyvalue.SettingsValues import org.thoughtcrime.securesms.keyvalue.SignalStore -import org.thoughtcrime.securesms.phonenumbers.PhoneNumberFormatter import org.thoughtcrime.securesms.recipients.Recipient import org.thoughtcrime.securesms.service.KeyCachingService import org.thoughtcrime.securesms.util.CachedInflater import org.thoughtcrime.securesms.util.DynamicTheme +import org.thoughtcrime.securesms.util.SignalE164Util import org.thoughtcrime.securesms.util.navigation.safeNavigate private const val START_LOCATION = "app.settings.start.location" @@ -106,7 +106,7 @@ class AppSettingsActivity : DSLSettingsActivity(), InAppPaymentComponent { when (intent.getStringExtra(EXTRA_PERFORM_ACTION_ON_CREATE)) { ACTION_CHANGE_NUMBER_SUCCESS -> { MaterialAlertDialogBuilder(this) - .setMessage(getString(R.string.ChangeNumber__your_phone_number_has_changed_to_s, PhoneNumberFormatter.prettyPrint(Recipient.self().requireE164()))) + .setMessage(getString(R.string.ChangeNumber__your_phone_number_has_changed_to_s, SignalE164Util.prettyPrint(Recipient.self().requireE164()))) .setPositiveButton(R.string.ChangeNumber__okay, null) .show() } diff --git a/app/src/main/java/org/thoughtcrime/securesms/components/settings/app/AppSettingsFragment.kt b/app/src/main/java/org/thoughtcrime/securesms/components/settings/app/AppSettingsFragment.kt index 2907c1e6f1..0a4643e160 100644 --- a/app/src/main/java/org/thoughtcrime/securesms/components/settings/app/AppSettingsFragment.kt +++ b/app/src/main/java/org/thoughtcrime/securesms/components/settings/app/AppSettingsFragment.kt @@ -71,10 +71,10 @@ import org.thoughtcrime.securesms.components.settings.app.subscription.completed import org.thoughtcrime.securesms.compose.ComposeFragment import org.thoughtcrime.securesms.compose.StatusBarColorNestedScrollConnection import org.thoughtcrime.securesms.database.model.InAppPaymentSubscriberRecord -import org.thoughtcrime.securesms.phonenumbers.PhoneNumberFormatter import org.thoughtcrime.securesms.profiles.ProfileName import org.thoughtcrime.securesms.recipients.Recipient import org.thoughtcrime.securesms.util.CommunicationActions +import org.thoughtcrime.securesms.util.SignalE164Util import org.thoughtcrime.securesms.util.Util import org.thoughtcrime.securesms.util.navigation.safeNavigate @@ -575,7 +575,7 @@ private fun BioRow( self.e164 } else { remember(self.e164) { - PhoneNumberFormatter.prettyPrint(self.e164) + SignalE164Util.prettyPrint(self.e164) } } diff --git a/app/src/main/java/org/thoughtcrime/securesms/components/settings/app/changenumber/ChangeNumberLockActivity.kt b/app/src/main/java/org/thoughtcrime/securesms/components/settings/app/changenumber/ChangeNumberLockActivity.kt index 04432b3d94..0cf329fa03 100644 --- a/app/src/main/java/org/thoughtcrime/securesms/components/settings/app/changenumber/ChangeNumberLockActivity.kt +++ b/app/src/main/java/org/thoughtcrime/securesms/components/settings/app/changenumber/ChangeNumberLockActivity.kt @@ -17,9 +17,9 @@ import org.thoughtcrime.securesms.PassphraseRequiredActivity import org.thoughtcrime.securesms.R import org.thoughtcrime.securesms.keyvalue.SignalStore import org.thoughtcrime.securesms.logsubmit.SubmitDebugLogActivity -import org.thoughtcrime.securesms.phonenumbers.PhoneNumberFormatter import org.thoughtcrime.securesms.util.DynamicNoActionBarTheme import org.thoughtcrime.securesms.util.DynamicTheme +import org.thoughtcrime.securesms.util.SignalE164Util /** * A captive activity that can determine if an interrupted/erred change number request @@ -72,7 +72,7 @@ class ChangeNumberLockActivity : PassphraseRequiredActivity() { MaterialAlertDialogBuilder(this) .setTitle(R.string.ChangeNumberLockActivity__change_status_confirmed) - .setMessage(getString(R.string.ChangeNumberLockActivity__your_number_has_been_confirmed_as_s, PhoneNumberFormatter.prettyPrint(SignalStore.account.e164!!))) + .setMessage(getString(R.string.ChangeNumberLockActivity__your_number_has_been_confirmed_as_s, SignalE164Util.prettyPrint(SignalStore.account.e164!!))) .setPositiveButton(android.R.string.ok) { _, _ -> startActivity(MainActivity.clearTop(this)) finish() diff --git a/app/src/main/java/org/thoughtcrime/securesms/components/settings/app/privacy/advanced/AdvancedPrivacySettingsFragment.kt b/app/src/main/java/org/thoughtcrime/securesms/components/settings/app/privacy/advanced/AdvancedPrivacySettingsFragment.kt index 79b2ac8015..1ce5ce5d6f 100644 --- a/app/src/main/java/org/thoughtcrime/securesms/components/settings/app/privacy/advanced/AdvancedPrivacySettingsFragment.kt +++ b/app/src/main/java/org/thoughtcrime/securesms/components/settings/app/privacy/advanced/AdvancedPrivacySettingsFragment.kt @@ -18,8 +18,6 @@ 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.keyvalue.SignalStore -import org.thoughtcrime.securesms.phonenumbers.PhoneNumberFormatter import org.thoughtcrime.securesms.util.CommunicationActions import org.thoughtcrime.securesms.util.SpanUtil import org.thoughtcrime.securesms.util.ViewUtil @@ -162,14 +160,6 @@ class AdvancedPrivacySettingsFragment : DSLSettingsFragment(R.string.preferences } } - private fun getPushToggleSummary(isPushEnabled: Boolean): String { - return if (isPushEnabled) { - PhoneNumberFormatter.prettyPrint(SignalStore.account.e164!!) - } else { - getString(R.string.preferences__free_private_messages_and_calls) - } - } - @Suppress("DEPRECATION") private fun registerNetworkReceiver() { val context: Context? = context diff --git a/app/src/main/java/org/thoughtcrime/securesms/components/settings/app/privacy/advanced/AdvancedPrivacySettingsViewModel.kt b/app/src/main/java/org/thoughtcrime/securesms/components/settings/app/privacy/advanced/AdvancedPrivacySettingsViewModel.kt index dc7b03f522..a125281459 100644 --- a/app/src/main/java/org/thoughtcrime/securesms/components/settings/app/privacy/advanced/AdvancedPrivacySettingsViewModel.kt +++ b/app/src/main/java/org/thoughtcrime/securesms/components/settings/app/privacy/advanced/AdvancedPrivacySettingsViewModel.kt @@ -12,7 +12,7 @@ import org.thoughtcrime.securesms.jobs.RefreshAttributesJob import org.thoughtcrime.securesms.jobs.RefreshOwnProfileJob import org.thoughtcrime.securesms.keyvalue.SettingsValues import org.thoughtcrime.securesms.keyvalue.SignalStore -import org.thoughtcrime.securesms.phonenumbers.PhoneNumberFormatter +import org.thoughtcrime.securesms.util.SignalE164Util import org.thoughtcrime.securesms.util.SingleLiveEvent import org.thoughtcrime.securesms.util.TextSecurePreferences import org.thoughtcrime.securesms.util.livedata.Store @@ -89,7 +89,7 @@ class AdvancedPrivacySettingsViewModel( } private fun getCensorshipCircumventionState(): CensorshipCircumventionState { - val countryCode: Int = PhoneNumberFormatter.getLocalCountryCode() + val countryCode: Int = SignalE164Util.getLocalCountryCode() val isCountryCodeCensoredByDefault: Boolean = AppDependencies.signalServiceNetworkAccess.isCountryCodeCensoredByDefault(countryCode) val enabledState: SettingsValues.CensorshipCircumventionEnabled = SignalStore.settings.censorshipCircumventionEnabled val hasInternet: Boolean = NetworkConstraint.isMet(AppDependencies.application) diff --git a/app/src/main/java/org/thoughtcrime/securesms/contacts/ContactChipViewModel.kt b/app/src/main/java/org/thoughtcrime/securesms/contacts/ContactChipViewModel.kt index 15ae8b2a8c..283bd823d2 100644 --- a/app/src/main/java/org/thoughtcrime/securesms/contacts/ContactChipViewModel.kt +++ b/app/src/main/java/org/thoughtcrime/securesms/contacts/ContactChipViewModel.kt @@ -9,7 +9,6 @@ import io.reactivex.rxjava3.disposables.CompositeDisposable import io.reactivex.rxjava3.disposables.Disposable import io.reactivex.rxjava3.kotlin.plusAssign import io.reactivex.rxjava3.schedulers.Schedulers -import org.thoughtcrime.securesms.dependencies.AppDependencies import org.thoughtcrime.securesms.recipients.Recipient import org.thoughtcrime.securesms.recipients.RecipientId import org.thoughtcrime.securesms.util.rx.RxStore @@ -74,7 +73,7 @@ class ContactChipViewModel : ViewModel() { private fun getOrCreateRecipientId(selectedContact: SelectedContact): Single { return Single.fromCallable { - selectedContact.getOrCreateRecipientId(AppDependencies.application) + selectedContact.getOrCreateRecipientId() } } } diff --git a/app/src/main/java/org/thoughtcrime/securesms/contacts/ContactRepository.java b/app/src/main/java/org/thoughtcrime/securesms/contacts/ContactRepository.java index 348f08c899..8ac84d931d 100644 --- a/app/src/main/java/org/thoughtcrime/securesms/contacts/ContactRepository.java +++ b/app/src/main/java/org/thoughtcrime/securesms/contacts/ContactRepository.java @@ -12,7 +12,7 @@ import org.signal.libsignal.protocol.util.Pair; import org.thoughtcrime.securesms.contacts.paged.ContactSearchSortOrder; import org.thoughtcrime.securesms.database.RecipientTable; import org.thoughtcrime.securesms.database.SignalDatabase; -import org.thoughtcrime.securesms.phonenumbers.PhoneNumberFormatter; +import org.thoughtcrime.securesms.util.SignalE164Util; import org.thoughtcrime.securesms.util.Util; import java.util.ArrayList; @@ -59,7 +59,7 @@ public class ContactRepository { String email = CursorUtil.requireString(cursor, RecipientTable.EMAIL); if (phone != null) { - phone = PhoneNumberFormatter.prettyPrint(phone); + phone = SignalE164Util.prettyPrint(phone); } return Util.getFirstNonEmpty(phone, email); diff --git a/app/src/main/java/org/thoughtcrime/securesms/contacts/ContactsSyncAdapter.java b/app/src/main/java/org/thoughtcrime/securesms/contacts/ContactsSyncAdapter.java index 7c1cb61e9c..5b30b6ffab 100644 --- a/app/src/main/java/org/thoughtcrime/securesms/contacts/ContactsSyncAdapter.java +++ b/app/src/main/java/org/thoughtcrime/securesms/contacts/ContactsSyncAdapter.java @@ -16,9 +16,9 @@ import org.thoughtcrime.securesms.database.SignalDatabase; import org.thoughtcrime.securesms.dependencies.AppDependencies; import org.thoughtcrime.securesms.jobs.DirectoryRefreshJob; import org.thoughtcrime.securesms.keyvalue.SignalStore; -import org.thoughtcrime.securesms.phonenumbers.PhoneNumberFormatter; import org.thoughtcrime.securesms.recipients.Recipient; import org.signal.core.util.SetUtil; +import org.thoughtcrime.securesms.util.SignalE164Util; import java.io.IOException; import java.util.List; @@ -56,7 +56,8 @@ public class ContactsSyncAdapter extends AbstractThreadedSyncAdapter { Set allSystemE164s = SystemContactsRepository.getAllDisplayNumbers(context) .stream() - .map(number -> PhoneNumberFormatter.get(context).format(number)) + .map(number -> SignalE164Util.formatAsE164(number)) + .filter(it -> it != null) .collect(Collectors.toSet()); Set knownSystemE164s = SignalDatabase.recipients().getAllE164s(); Set unknownSystemE164s = SetUtil.difference(allSystemE164s, knownSystemE164s); @@ -71,7 +72,8 @@ public class ContactsSyncAdapter extends AbstractThreadedSyncAdapter { } else if (unknownSystemE164s.size() > 0) { List recipients = Stream.of(unknownSystemE164s) .filter(s -> s.startsWith("+")) - .map(s -> Recipient.external(getContext(), s)) + .map(s -> Recipient.external(s)) + .filter(it -> it != null) .toList(); Log.i(TAG, "There are " + unknownSystemE164s.size() + " unknown E164s, which are now " + recipients.size() + " recipients. Only syncing these specific contacts."); diff --git a/app/src/main/java/org/thoughtcrime/securesms/contacts/SelectedContact.java b/app/src/main/java/org/thoughtcrime/securesms/contacts/SelectedContact.java index c5ff892426..f8fed2867c 100644 --- a/app/src/main/java/org/thoughtcrime/securesms/contacts/SelectedContact.java +++ b/app/src/main/java/org/thoughtcrime/securesms/contacts/SelectedContact.java @@ -1,7 +1,5 @@ package org.thoughtcrime.securesms.contacts; -import android.content.Context; - import androidx.annotation.NonNull; import androidx.annotation.Nullable; @@ -45,11 +43,16 @@ public final class SelectedContact { this.chatType = chatType; } - public @NonNull RecipientId getOrCreateRecipientId(@NonNull Context context) { + public @NonNull RecipientId getOrCreateRecipientId() { if (recipientId != null) { return recipientId; } else if (number != null) { - return Recipient.external(context, number).getId(); + Recipient recipient = Recipient.external(number); + if (recipient != null) { + return recipient.getId(); + } else { + throw new AssertionError("Invalid phone number provided!"); + } } else { throw new AssertionError(); } diff --git a/app/src/main/java/org/thoughtcrime/securesms/contacts/sync/ContactDiscovery.kt b/app/src/main/java/org/thoughtcrime/securesms/contacts/sync/ContactDiscovery.kt index 7c9be332b2..a50460555c 100644 --- a/app/src/main/java/org/thoughtcrime/securesms/contacts/sync/ContactDiscovery.kt +++ b/app/src/main/java/org/thoughtcrime/securesms/contacts/sync/ContactDiscovery.kt @@ -7,6 +7,7 @@ import androidx.annotation.WorkerThread import org.signal.contacts.SystemContactsRepository import org.signal.contacts.SystemContactsRepository.ContactIterator import org.signal.contacts.SystemContactsRepository.ContactPhoneDetails +import org.signal.core.util.E164Util import org.signal.core.util.Stopwatch import org.signal.core.util.StringUtil import org.signal.core.util.logging.Log @@ -19,7 +20,6 @@ import org.thoughtcrime.securesms.mms.IncomingMessage import org.thoughtcrime.securesms.notifications.NotificationChannels import org.thoughtcrime.securesms.notifications.v2.ConversationId import org.thoughtcrime.securesms.permissions.Permissions -import org.thoughtcrime.securesms.phonenumbers.PhoneNumberFormatter import org.thoughtcrime.securesms.profiles.ProfileName import org.thoughtcrime.securesms.recipients.Recipient import org.thoughtcrime.securesms.recipients.RecipientId @@ -141,8 +141,9 @@ object ContactDiscovery { ) } - private fun phoneNumberFormatter(context: Context): (String) -> String { - return { PhoneNumberFormatter.get(context).format(it) } + private fun phoneNumberFormatter(): (String) -> String? { + val formatter = E164Util.createFormatterForE164(SignalStore.account.e164!!) + return { formatter.formatAsE164(it) } } private fun refreshRecipients( @@ -171,13 +172,13 @@ object ContactDiscovery { contactsProvider = { if (useFullSync) { Log.d(TAG, "Doing a full system contact sync. There are ${result.registeredIds.size} contacts to get info for.") - SystemContactsRepository.getAllSystemContacts(context, phoneNumberFormatter(context)) + SystemContactsRepository.getAllSystemContacts(context, phoneNumberFormatter()) } else { Log.d(TAG, "Doing a partial system contact sync. There are ${result.registeredIds.size} contacts to get info for.") SystemContactsRepository.getContactDetailsByQueries( context = context, queries = Recipient.resolvedList(result.registeredIds).mapNotNull { it.e164.orElse(null) }, - e164Formatter = phoneNumberFormatter(context) + e164Formatter = phoneNumberFormatter() ) } }, @@ -235,7 +236,7 @@ object ContactDiscovery { private fun syncRecipientsWithSystemContacts( context: Context, rewrites: Map, - contactsProvider: () -> ContactIterator = { SystemContactsRepository.getAllSystemContacts(context, phoneNumberFormatter(context)) }, + contactsProvider: () -> ContactIterator = { SystemContactsRepository.getAllSystemContacts(context, phoneNumberFormatter()) }, clearInfoForMissingContacts: Boolean ) { val localNumber: String = SignalStore.account.e164 ?: "" @@ -259,8 +260,10 @@ object ContactDiscovery { ProfileName.EMPTY } + val recipient: Recipient = Recipient.externalContact(realNumber) ?: continue + handle.setSystemContactInfo( - Recipient.externalContact(realNumber).id, + recipient.id, profileName, phoneDetails.displayName, phoneDetails.photoUri, diff --git a/app/src/main/java/org/thoughtcrime/securesms/contacts/sync/ContactDiscoveryRefreshV2.kt b/app/src/main/java/org/thoughtcrime/securesms/contacts/sync/ContactDiscoveryRefreshV2.kt index 4980ea0caf..011a42f964 100644 --- a/app/src/main/java/org/thoughtcrime/securesms/contacts/sync/ContactDiscoveryRefreshV2.kt +++ b/app/src/main/java/org/thoughtcrime/securesms/contacts/sync/ContactDiscoveryRefreshV2.kt @@ -11,10 +11,10 @@ import org.thoughtcrime.securesms.database.RecipientTable.CdsV2Result import org.thoughtcrime.securesms.database.SignalDatabase import org.thoughtcrime.securesms.dependencies.AppDependencies import org.thoughtcrime.securesms.keyvalue.SignalStore -import org.thoughtcrime.securesms.phonenumbers.PhoneNumberFormatter import org.thoughtcrime.securesms.recipients.Recipient import org.thoughtcrime.securesms.recipients.RecipientId import org.thoughtcrime.securesms.util.RemoteConfig +import org.thoughtcrime.securesms.util.SignalE164Util import org.whispersystems.signalservice.api.push.exceptions.CdsiInvalidTokenException import org.whispersystems.signalservice.api.push.exceptions.CdsiResourceExhaustedException import org.whispersystems.signalservice.api.services.CdsiV2Service @@ -45,7 +45,7 @@ object ContactDiscoveryRefreshV2 { @JvmStatic fun refreshAll(context: Context, timeoutMs: Long? = null): ContactDiscovery.RefreshResult { val recipientE164s: Set = SignalDatabase.recipients.getAllE164s().sanitize() - val systemE164s: Set = SystemContactsRepository.getAllDisplayNumbers(context).toE164s(context).sanitize() + val systemE164s: Set = SystemContactsRepository.getAllDisplayNumbers(context).toE164s().sanitize() return refreshInternal( recipientE164s = recipientE164s, @@ -246,8 +246,8 @@ object ContactDiscoveryRefreshV2 { .toSet() } - private fun Set.toE164s(context: Context): Set { - return this.map { PhoneNumberFormatter.get(context).format(it) }.toSet() + private fun Set.toE164s(): Set { + return this.mapNotNull { SignalE164Util.formatAsE164(it) }.toSet() } private fun Set.sanitize(): Set { diff --git a/app/src/main/java/org/thoughtcrime/securesms/contactshare/ContactUtil.java b/app/src/main/java/org/thoughtcrime/securesms/contactshare/ContactUtil.java index abcb4caabc..752d62e907 100644 --- a/app/src/main/java/org/thoughtcrime/securesms/contactshare/ContactUtil.java +++ b/app/src/main/java/org/thoughtcrime/securesms/contactshare/ContactUtil.java @@ -27,10 +27,10 @@ import org.thoughtcrime.securesms.contactshare.Contact.Phone; import org.thoughtcrime.securesms.contactshare.Contact.PostalAddress; import org.thoughtcrime.securesms.database.SignalDatabase; import org.thoughtcrime.securesms.mms.PartAuthority; -import org.thoughtcrime.securesms.phonenumbers.PhoneNumberFormatter; import org.thoughtcrime.securesms.profiles.ProfileName; import org.thoughtcrime.securesms.recipients.Recipient; import org.thoughtcrime.securesms.recipients.RecipientId; +import org.thoughtcrime.securesms.util.SignalE164Util; import org.thoughtcrime.securesms.util.SpanUtil; import java.io.IOException; @@ -120,8 +120,8 @@ public final class ContactUtil { } } - public static @NonNull String getNormalizedPhoneNumber(@NonNull Context context, @Nullable String number) { - return PhoneNumberFormatter.get(context).format(number); + public static @Nullable String getNormalizedPhoneNumber(@Nullable String number) { + return SignalE164Util.formatAsE164(number != null ? number : ""); } @MainThread @@ -142,11 +142,11 @@ public final class ContactUtil { } } - public static List getRecipients(@NonNull Context context, @NonNull Contact contact) { + public static List getRecipients(@NonNull Contact contact) { return contact .getPhoneNumbers() .stream() - .map(phone -> PhoneNumberFormatter.get(context).formatOrNull(phone.getNumber())) + .map(phone -> SignalE164Util.formatAsE164(phone.getNumber())) .filter(number -> number != null) .map(phone -> SignalDatabase.recipients().getOrInsertFromE164(phone)) .collect(Collectors.toList()); diff --git a/app/src/main/java/org/thoughtcrime/securesms/contactshare/SharedContactDetailsActivity.java b/app/src/main/java/org/thoughtcrime/securesms/contactshare/SharedContactDetailsActivity.java index cbf399eedf..a2070b599b 100644 --- a/app/src/main/java/org/thoughtcrime/securesms/contactshare/SharedContactDetailsActivity.java +++ b/app/src/main/java/org/thoughtcrime/securesms/contactshare/SharedContactDetailsActivity.java @@ -97,7 +97,7 @@ public class SharedContactDetailsActivity extends PassphraseRequiredActivity { initViews(); presentContact(contact); - presentActionButtons(ContactUtil.getRecipients(this, contact)); + presentActionButtons(ContactUtil.getRecipients(contact)); presentAvatar(contact.getAvatarAttachment() != null ? contact.getAvatarAttachment().getUri() : null); for (LiveRecipient recipient : activeRecipients.values()) { diff --git a/app/src/main/java/org/thoughtcrime/securesms/contactshare/SharedContactRepository.java b/app/src/main/java/org/thoughtcrime/securesms/contactshare/SharedContactRepository.java index 9aab132928..5090c0353e 100644 --- a/app/src/main/java/org/thoughtcrime/securesms/contactshare/SharedContactRepository.java +++ b/app/src/main/java/org/thoughtcrime/securesms/contactshare/SharedContactRepository.java @@ -19,9 +19,9 @@ import org.thoughtcrime.securesms.contactshare.Contact.Name; import org.thoughtcrime.securesms.contactshare.Contact.Phone; import org.thoughtcrime.securesms.contactshare.Contact.PostalAddress; import org.thoughtcrime.securesms.mms.PartAuthority; -import org.thoughtcrime.securesms.phonenumbers.PhoneNumberFormatter; import org.thoughtcrime.securesms.providers.BlobProvider; import org.thoughtcrime.securesms.recipients.Recipient; +import org.thoughtcrime.securesms.util.SignalE164Util; import java.io.IOException; import java.io.InputStream; @@ -130,7 +130,11 @@ public class SharedContactRepository { List phoneDetails = SystemContactsRepository.getPhoneDetails(context, contactId); for (PhoneDetails phone : phoneDetails) { - String number = ContactUtil.getNormalizedPhoneNumber(context, phone.getNumber()); + String number = ContactUtil.getNormalizedPhoneNumber(phone.getNumber()); + if (number == null) { + continue; + } + Phone existing = numberMap.get(number); Phone candidate = new Phone(number, VCardUtil.phoneTypeFromContactType(phone.getType()), phone.getLabel()); @@ -182,7 +186,12 @@ public class SharedContactRepository { } for (Phone phoneNumber : phoneNumbers) { - AvatarInfo recipientAvatar = getRecipientAvatarInfo(PhoneNumberFormatter.get(context).format(phoneNumber.getNumber())); + String formattedNumber = SignalE164Util.formatAsE164(phoneNumber.getNumber()); + if (formattedNumber == null) { + continue; + } + + AvatarInfo recipientAvatar = getRecipientAvatarInfo(formattedNumber); if (recipientAvatar != null) { return recipientAvatar; } @@ -202,7 +211,11 @@ public class SharedContactRepository { @WorkerThread private @Nullable AvatarInfo getRecipientAvatarInfo(String address) { - Recipient recipient = Recipient.external(context, address); + Recipient recipient = Recipient.external(address); + if (recipient == null) { + return null; + } + ContactPhoto contactPhoto = recipient.getContactPhoto(); if (contactPhoto != null) { diff --git a/app/src/main/java/org/thoughtcrime/securesms/conversation/ConversationItem.java b/app/src/main/java/org/thoughtcrime/securesms/conversation/ConversationItem.java index ce4511951a..b4cf1cf7ba 100644 --- a/app/src/main/java/org/thoughtcrime/securesms/conversation/ConversationItem.java +++ b/app/src/main/java/org/thoughtcrime/securesms/conversation/ConversationItem.java @@ -57,7 +57,6 @@ import androidx.annotation.DimenRes; import androidx.annotation.NonNull; import androidx.annotation.Nullable; import androidx.core.content.ContextCompat; -import androidx.core.graphics.ColorUtils; import androidx.lifecycle.LifecycleOwner; import androidx.media3.common.MediaItem; import androidx.recyclerview.widget.RecyclerView; @@ -82,7 +81,6 @@ import org.thoughtcrime.securesms.attachments.DatabaseAttachment; import org.thoughtcrime.securesms.badges.BadgeImageView; import org.thoughtcrime.securesms.badges.gifts.GiftMessageView; import org.thoughtcrime.securesms.badges.gifts.OpenableGift; -import org.thoughtcrime.securesms.billing.upgrade.UpgradeToStartMediaBackupSheet; import org.thoughtcrime.securesms.calls.links.CallLinks; import org.thoughtcrime.securesms.components.AlertView; import org.thoughtcrime.securesms.components.AudioView; @@ -146,7 +144,6 @@ import org.thoughtcrime.securesms.util.MessageRecordUtil; import org.thoughtcrime.securesms.util.PlaceholderURLSpan; import org.thoughtcrime.securesms.util.Projection; import org.thoughtcrime.securesms.util.ProjectionList; -import org.thoughtcrime.securesms.util.RemoteConfig; import org.thoughtcrime.securesms.util.SearchUtil; import org.thoughtcrime.securesms.util.ThemeUtil; import org.thoughtcrime.securesms.util.UrlClickHandler; diff --git a/app/src/main/java/org/thoughtcrime/securesms/conversation/v2/ConversationAdapterV2.kt b/app/src/main/java/org/thoughtcrime/securesms/conversation/v2/ConversationAdapterV2.kt index 63d31e4a77..e690edd341 100644 --- a/app/src/main/java/org/thoughtcrime/securesms/conversation/v2/ConversationAdapterV2.kt +++ b/app/src/main/java/org/thoughtcrime/securesms/conversation/v2/ConversationAdapterV2.kt @@ -55,12 +55,12 @@ import org.thoughtcrime.securesms.databinding.V2ConversationItemTextOnlyOutgoing import org.thoughtcrime.securesms.giph.mp4.GiphyMp4PlaybackPolicyEnforcer import org.thoughtcrime.securesms.groups.v2.GroupDescriptionUtil import org.thoughtcrime.securesms.keyvalue.SignalStore -import org.thoughtcrime.securesms.phonenumbers.PhoneNumberFormatter import org.thoughtcrime.securesms.recipients.Recipient import org.thoughtcrime.securesms.recipients.ui.about.AboutSheet import org.thoughtcrime.securesms.util.CachedInflater import org.thoughtcrime.securesms.util.Projection import org.thoughtcrime.securesms.util.ProjectionList +import org.thoughtcrime.securesms.util.SignalE164Util import org.thoughtcrime.securesms.util.adapter.mapping.MappingViewHolder import org.thoughtcrime.securesms.util.adapter.mapping.PagingMappingAdapter import java.util.Locale @@ -576,7 +576,7 @@ class ConversationAdapterV2( conversationBanner.hideUnverifiedNameSubtitle() } - val subtitle: String? = recipient.takeIf { it.shouldShowE164 }?.e164?.map { e164: String? -> PhoneNumberFormatter.prettyPrint(e164!!) }?.orElse(null) + val subtitle: String? = recipient.takeIf { it.shouldShowE164 }?.e164?.map { e164: String? -> SignalE164Util.prettyPrint(e164!!) }?.orElse(null) if (subtitle == null || subtitle == title) { conversationBanner.hideSubtitle() } else { diff --git a/app/src/main/java/org/thoughtcrime/securesms/conversationlist/ConversationListItem.java b/app/src/main/java/org/thoughtcrime/securesms/conversationlist/ConversationListItem.java index 5835d5a5fa..60b56d15af 100644 --- a/app/src/main/java/org/thoughtcrime/securesms/conversationlist/ConversationListItem.java +++ b/app/src/main/java/org/thoughtcrime/securesms/conversationlist/ConversationListItem.java @@ -74,7 +74,6 @@ import org.thoughtcrime.securesms.database.model.ThreadRecord; import org.thoughtcrime.securesms.database.model.UpdateDescription; import org.thoughtcrime.securesms.glide.GlideLiveDataTarget; import org.thoughtcrime.securesms.mms.DecryptableStreamUriLoader; -import org.thoughtcrime.securesms.phonenumbers.PhoneNumberFormatter; import org.thoughtcrime.securesms.recipients.LiveRecipient; import org.thoughtcrime.securesms.recipients.Recipient; import org.thoughtcrime.securesms.recipients.RecipientId; @@ -84,6 +83,7 @@ import org.thoughtcrime.securesms.util.DateUtils; import org.thoughtcrime.securesms.util.ExpirationUtil; import org.thoughtcrime.securesms.util.MediaUtil; import org.thoughtcrime.securesms.util.SearchUtil; +import org.thoughtcrime.securesms.util.SignalE164Util; import org.thoughtcrime.securesms.util.SpanUtil; import org.thoughtcrime.securesms.util.Util; import org.thoughtcrime.securesms.util.livedata.LiveDataUtil; @@ -660,7 +660,7 @@ public final class ConversationListItem extends ConstraintLayout implements Bind return emphasisAdded(context, context.getString(R.string.ThreadRecord_message_history_has_been_merged), defaultTint); } else if (MessageTypes.isSessionSwitchoverType(thread.getType())) { if (thread.getRecipient().getE164().isPresent()) { - return emphasisAdded(context, context.getString(R.string.ThreadRecord_s_belongs_to_s, PhoneNumberFormatter.prettyPrint(thread.getRecipient().requireE164()), thread.getRecipient().getDisplayName(context)), defaultTint); + return emphasisAdded(context, context.getString(R.string.ThreadRecord_s_belongs_to_s, SignalE164Util.prettyPrint(thread.getRecipient().requireE164()), thread.getRecipient().getDisplayName(context)), defaultTint); } else { return emphasisAdded(context, context.getString(R.string.ThreadRecord_safety_number_changed), defaultTint); } diff --git a/app/src/main/java/org/thoughtcrime/securesms/database/documents/IdentityKeyMismatch.java b/app/src/main/java/org/thoughtcrime/securesms/database/documents/IdentityKeyMismatch.java index aced0f253d..cf1299cf92 100644 --- a/app/src/main/java/org/thoughtcrime/securesms/database/documents/IdentityKeyMismatch.java +++ b/app/src/main/java/org/thoughtcrime/securesms/database/documents/IdentityKeyMismatch.java @@ -16,7 +16,6 @@ import com.fasterxml.jackson.databind.annotation.JsonSerialize; import org.signal.core.util.logging.Log; import org.signal.libsignal.protocol.IdentityKey; import org.signal.libsignal.protocol.InvalidKeyException; -import org.thoughtcrime.securesms.dependencies.AppDependencies; import org.thoughtcrime.securesms.recipients.Recipient; import org.thoughtcrime.securesms.recipients.RecipientId; import org.signal.core.util.Base64; @@ -53,7 +52,12 @@ public class IdentityKeyMismatch { if (!TextUtils.isEmpty(recipientId)) { return RecipientId.from(recipientId); } else { - return Recipient.external(AppDependencies.getApplication(), address).getId(); + Recipient recipient = Recipient.external(address); + if (recipient != null) { + return recipient.getId(); + } else { + return RecipientId.UNKNOWN; + } } } diff --git a/app/src/main/java/org/thoughtcrime/securesms/database/documents/NetworkFailure.java b/app/src/main/java/org/thoughtcrime/securesms/database/documents/NetworkFailure.java index 6a501c45c1..832ae80b54 100644 --- a/app/src/main/java/org/thoughtcrime/securesms/database/documents/NetworkFailure.java +++ b/app/src/main/java/org/thoughtcrime/securesms/database/documents/NetworkFailure.java @@ -7,7 +7,6 @@ import androidx.annotation.NonNull; import com.fasterxml.jackson.annotation.JsonIgnore; import com.fasterxml.jackson.annotation.JsonProperty; -import org.thoughtcrime.securesms.dependencies.AppDependencies; import org.thoughtcrime.securesms.recipients.Recipient; import org.thoughtcrime.securesms.recipients.RecipientId; @@ -34,7 +33,12 @@ public class NetworkFailure { if (!TextUtils.isEmpty(recipientId)) { return RecipientId.from(recipientId); } else { - return Recipient.external(AppDependencies.getApplication(), address).getId(); + Recipient recipient = Recipient.external(address); + if (recipient != null) { + return recipient.getId(); + } else { + return RecipientId.UNKNOWN; + } } } diff --git a/app/src/main/java/org/thoughtcrime/securesms/database/helpers/migration/V149_LegacyMigrations.kt b/app/src/main/java/org/thoughtcrime/securesms/database/helpers/migration/V149_LegacyMigrations.kt index b57616ece9..a37694da59 100644 --- a/app/src/main/java/org/thoughtcrime/securesms/database/helpers/migration/V149_LegacyMigrations.kt +++ b/app/src/main/java/org/thoughtcrime/securesms/database/helpers/migration/V149_LegacyMigrations.kt @@ -39,11 +39,11 @@ import org.thoughtcrime.securesms.groups.GroupId import org.thoughtcrime.securesms.jobs.PreKeysSyncJob import org.thoughtcrime.securesms.keyvalue.SignalStore import org.thoughtcrime.securesms.notifications.NotificationChannels -import org.thoughtcrime.securesms.phonenumbers.PhoneNumberFormatter import org.thoughtcrime.securesms.profiles.ProfileName import org.thoughtcrime.securesms.storage.StorageSyncHelper import org.thoughtcrime.securesms.util.FileUtils import org.thoughtcrime.securesms.util.ServiceUtil +import org.thoughtcrime.securesms.util.SignalE164Util import org.thoughtcrime.securesms.util.Triple import org.thoughtcrime.securesms.util.Util import org.whispersystems.signalservice.api.push.DistributionId @@ -379,7 +379,7 @@ object V149_LegacyMigrations : SignalDatabaseMigration { db.rawQuery("SELECT recipient_ids, system_display_name, signal_profile_name, notification, vibrate FROM recipient_preferences WHERE notification NOT NULL OR vibrate != 0", null).use { cursor -> while (cursor != null && cursor.moveToNext()) { val rawAddress: String = cursor.getString(cursor.getColumnIndexOrThrow("recipient_ids")) - val address: String = PhoneNumberFormatter.get(context).format(rawAddress) + val address: String = SignalE164Util.formatAsE164(rawAddress) ?: "" val systemName: String = cursor.getString(cursor.getColumnIndexOrThrow("system_display_name")) val profileName: String = cursor.getString(cursor.getColumnIndexOrThrow("signal_profile_name")) val messageSound: String? = cursor.getString(cursor.getColumnIndexOrThrow("notification")) diff --git a/app/src/main/java/org/thoughtcrime/securesms/database/loaders/CountryListLoader.java b/app/src/main/java/org/thoughtcrime/securesms/database/loaders/CountryListLoader.java deleted file mode 100644 index f55a7579e1..0000000000 --- a/app/src/main/java/org/thoughtcrime/securesms/database/loaders/CountryListLoader.java +++ /dev/null @@ -1,58 +0,0 @@ -package org.thoughtcrime.securesms.database.loaders; - -import android.content.Context; - -import androidx.loader.content.AsyncTaskLoader; - -import com.google.i18n.phonenumbers.PhoneNumberUtil; - -import org.whispersystems.signalservice.api.util.PhoneNumberFormatter; - -import java.text.Collator; -import java.util.ArrayList; -import java.util.Collections; -import java.util.Comparator; -import java.util.HashMap; -import java.util.Map; -import java.util.Set; - -public final class CountryListLoader extends AsyncTaskLoader>> { - - public CountryListLoader(Context context) { - super(context); - } - - @Override - public ArrayList> loadInBackground() { - Set regions = PhoneNumberUtil.getInstance().getSupportedRegions(); - ArrayList> results = new ArrayList<>(regions.size()); - - for (String region : regions) { - Map data = new HashMap<>(2); - data.put("country_name", PhoneNumberFormatter.getRegionDisplayNameLegacy(region)); - data.put("country_code", "+" +PhoneNumberUtil.getInstance().getCountryCodeForRegion(region)); - results.add(data); - } - - Collections.sort(results, new RegionComparator()); - - return results; - } - - private static class RegionComparator implements Comparator> { - - private final Collator collator; - - RegionComparator() { - collator = Collator.getInstance(); - collator.setStrength(Collator.PRIMARY); - } - - @Override - public int compare(Map lhs, Map rhs) { - String a = lhs.get("country_name"); - String b = rhs.get("country_name"); - return collator.compare(a, b); - } - } -} diff --git a/app/src/main/java/org/thoughtcrime/securesms/database/model/GroupsV2UpdateMessageProducer.java b/app/src/main/java/org/thoughtcrime/securesms/database/model/GroupsV2UpdateMessageProducer.java index bb1d75000d..6aad8571b5 100644 --- a/app/src/main/java/org/thoughtcrime/securesms/database/model/GroupsV2UpdateMessageProducer.java +++ b/app/src/main/java/org/thoughtcrime/securesms/database/model/GroupsV2UpdateMessageProducer.java @@ -13,7 +13,6 @@ import androidx.annotation.VisibleForTesting; import androidx.core.content.ContextCompat; import org.signal.core.util.BidiUtil; -import org.signal.core.util.StringUtil; import org.signal.storageservice.protos.groups.AccessControl; import org.signal.storageservice.protos.groups.Member; import org.signal.storageservice.protos.groups.local.DecryptedApproveMember; @@ -61,22 +60,16 @@ import org.thoughtcrime.securesms.backup.v2.proto.GroupV2MigrationSelfInvitedUpd import org.thoughtcrime.securesms.backup.v2.proto.GroupV2MigrationUpdate; import org.thoughtcrime.securesms.backup.v2.proto.SelfInvitedOtherUserToGroupUpdate; import org.thoughtcrime.securesms.backup.v2.proto.SelfInvitedToGroupUpdate; -import org.thoughtcrime.securesms.database.model.databaseprotos.DecryptedGroupV2Context; -import org.thoughtcrime.securesms.database.model.databaseprotos.GV2UpdateDescription; import org.thoughtcrime.securesms.groups.GV2AccessLevelUtil; -import org.thoughtcrime.securesms.groups.GroupMigrationMembershipChange; import org.thoughtcrime.securesms.recipients.Recipient; import org.thoughtcrime.securesms.recipients.RecipientId; import org.thoughtcrime.securesms.util.ExpirationUtil; import org.thoughtcrime.securesms.util.SpanUtil; -import org.thoughtcrime.securesms.util.Util; import org.whispersystems.signalservice.api.groupsv2.DecryptedGroupUtil; import org.whispersystems.signalservice.api.push.ServiceId; -import org.whispersystems.signalservice.api.push.ServiceId.ACI; import org.whispersystems.signalservice.api.push.ServiceIds; import org.whispersystems.signalservice.api.util.UuidUtil; -import java.util.ArrayList; import java.util.Arrays; import java.util.Collections; import java.util.HashMap; 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 497f5b97dc..9018082f46 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 @@ -35,7 +35,6 @@ import com.annimon.stream.Stream; import org.signal.core.util.Base64; import org.signal.core.util.BidiUtil; -import org.signal.core.util.StringUtil; import org.signal.core.util.logging.Log; import org.signal.storageservice.protos.groups.local.DecryptedGroup; import org.signal.storageservice.protos.groups.local.DecryptedGroupChange; @@ -56,13 +55,13 @@ import org.thoughtcrime.securesms.emoji.EmojiSource; import org.thoughtcrime.securesms.emoji.JumboEmoji; import org.thoughtcrime.securesms.groups.GroupMigrationMembershipChange; import org.thoughtcrime.securesms.keyvalue.SignalStore; -import org.thoughtcrime.securesms.phonenumbers.PhoneNumberFormatter; import org.thoughtcrime.securesms.profiles.ProfileName; import org.thoughtcrime.securesms.recipients.Recipient; import org.thoughtcrime.securesms.recipients.RecipientId; import org.thoughtcrime.securesms.util.DateUtils; import org.thoughtcrime.securesms.util.ExpirationUtil; import org.thoughtcrime.securesms.util.GroupUtil; +import org.thoughtcrime.securesms.util.SignalE164Util; import org.thoughtcrime.securesms.util.Util; import org.whispersystems.signalservice.api.groupsv2.DecryptedGroupUtil; import org.whispersystems.signalservice.api.push.ServiceId; @@ -254,7 +253,7 @@ public abstract class MessageRecord extends DisplayRecord { if (event.previousE164.isEmpty()) { return fromRecipient(getFromRecipient(), r -> context.getString(R.string.MessageRecord_your_message_history_with_s_and_another_chat_has_been_merged, r.getDisplayName(context)), R.drawable.ic_thread_merge_16); } else { - return fromRecipient(getFromRecipient(), r -> context.getString(R.string.MessageRecord_your_message_history_with_s_and_their_number_s_has_been_merged, r.getDisplayName(context), PhoneNumberFormatter.prettyPrint(event.previousE164)), R.drawable.ic_thread_merge_16); + return fromRecipient(getFromRecipient(), r -> context.getString(R.string.MessageRecord_your_message_history_with_s_and_their_number_s_has_been_merged, r.getDisplayName(context), SignalE164Util.prettyPrint(event.previousE164)), R.drawable.ic_thread_merge_16); } } catch (IOException e) { throw new AssertionError(e); @@ -266,7 +265,7 @@ public abstract class MessageRecord extends DisplayRecord { if (event.e164.isEmpty()) { return fromRecipient(getFromRecipient(), r -> context.getString(R.string.MessageRecord_your_safety_number_with_s_has_changed, r.getDisplayName(context)), R.drawable.ic_update_safety_number_16); } else { - return fromRecipient(getFromRecipient(), r -> context.getString(R.string.MessageRecord_s_belongs_to_s, PhoneNumberFormatter.prettyPrint(event.e164), r.getDisplayName(context)), R.drawable.ic_update_info_16); + return fromRecipient(getFromRecipient(), r -> context.getString(R.string.MessageRecord_s_belongs_to_s, SignalE164Util.prettyPrint(event.e164), r.getDisplayName(context)), R.drawable.ic_update_info_16); } } catch (IOException e) { throw new AssertionError(e); @@ -487,7 +486,7 @@ public abstract class MessageRecord extends DisplayRecord { } else if (profileChangeDetails.learnedProfileName != null) { String previouslyKnownAs; if (!Util.isEmpty(profileChangeDetails.learnedProfileName.e164)) { - previouslyKnownAs = PhoneNumberFormatter.prettyPrint(profileChangeDetails.learnedProfileName.e164); + previouslyKnownAs = SignalE164Util.prettyPrint(profileChangeDetails.learnedProfileName.e164); } else { previouslyKnownAs = profileChangeDetails.learnedProfileName.username; } diff --git a/app/src/main/java/org/thoughtcrime/securesms/delete/DeleteAccountRepository.java b/app/src/main/java/org/thoughtcrime/securesms/delete/DeleteAccountRepository.java index 0f4994fad3..4fe1a63bde 100644 --- a/app/src/main/java/org/thoughtcrime/securesms/delete/DeleteAccountRepository.java +++ b/app/src/main/java/org/thoughtcrime/securesms/delete/DeleteAccountRepository.java @@ -6,6 +6,7 @@ import androidx.core.util.Consumer; import com.annimon.stream.Stream; import com.google.i18n.phonenumbers.PhoneNumberUtil; +import org.signal.core.util.E164Util; import org.signal.core.util.concurrent.SignalExecutors; import org.signal.core.util.logging.Log; import org.thoughtcrime.securesms.components.settings.app.subscription.InAppPaymentsRepository; @@ -16,7 +17,6 @@ import org.thoughtcrime.securesms.database.model.InAppPaymentSubscriberRecord; import org.thoughtcrime.securesms.dependencies.AppDependencies; import org.thoughtcrime.securesms.groups.GroupManager; import org.thoughtcrime.securesms.util.ServiceUtil; -import org.whispersystems.signalservice.api.util.PhoneNumberFormatter; import org.whispersystems.signalservice.internal.EmptyResponse; import org.whispersystems.signalservice.internal.ServiceResponse; @@ -36,7 +36,7 @@ class DeleteAccountRepository { } @NonNull String getRegionDisplayName(@NonNull String region) { - return PhoneNumberFormatter.getRegionDisplayName(region).orElse(""); + return E164Util.getRegionDisplayName(region).orElse(""); } int getRegionCountryCode(@NonNull String region) { @@ -121,7 +121,7 @@ class DeleteAccountRepository { } private static @NonNull Country getCountryForRegion(@NonNull String region) { - return new Country(PhoneNumberFormatter.getRegionDisplayName(region).orElse(""), + return new Country(E164Util.getRegionDisplayName(region).orElse(""), PhoneNumberUtil.getInstance().getCountryCodeForRegion(region), region); } diff --git a/app/src/main/java/org/thoughtcrime/securesms/groups/ui/addmembers/AddMembersActivity.java b/app/src/main/java/org/thoughtcrime/securesms/groups/ui/addmembers/AddMembersActivity.java index e9dcd145a9..be1bbf1841 100644 --- a/app/src/main/java/org/thoughtcrime/securesms/groups/ui/addmembers/AddMembersActivity.java +++ b/app/src/main/java/org/thoughtcrime/securesms/groups/ui/addmembers/AddMembersActivity.java @@ -116,7 +116,7 @@ public class AddMembersActivity extends PushContactSelectionActivity implements AlertDialog progress = SimpleProgressDialog.show(this); - SimpleTask.run(getLifecycle(), () -> RecipientRepository.lookupNewE164(this, number), result -> { + SimpleTask.run(getLifecycle(), () -> RecipientRepository.lookupNewE164(number), result -> { progress.dismiss(); if (result instanceof RecipientRepository.LookupResult.Success) { diff --git a/app/src/main/java/org/thoughtcrime/securesms/groups/ui/addmembers/AddMembersRepository.java b/app/src/main/java/org/thoughtcrime/securesms/groups/ui/addmembers/AddMembersRepository.java index e1b7073607..8643d0948d 100644 --- a/app/src/main/java/org/thoughtcrime/securesms/groups/ui/addmembers/AddMembersRepository.java +++ b/app/src/main/java/org/thoughtcrime/securesms/groups/ui/addmembers/AddMembersRepository.java @@ -23,7 +23,7 @@ final class AddMembersRepository { @WorkerThread RecipientId getOrCreateRecipientId(@NonNull SelectedContact selectedContact) { - return selectedContact.getOrCreateRecipientId(context); + return selectedContact.getOrCreateRecipientId(); } @WorkerThread diff --git a/app/src/main/java/org/thoughtcrime/securesms/groups/ui/addtogroup/AddToGroupsActivity.java b/app/src/main/java/org/thoughtcrime/securesms/groups/ui/addtogroup/AddToGroupsActivity.java index 6f27c54ee7..81f156b142 100644 --- a/app/src/main/java/org/thoughtcrime/securesms/groups/ui/addtogroup/AddToGroupsActivity.java +++ b/app/src/main/java/org/thoughtcrime/securesms/groups/ui/addtogroup/AddToGroupsActivity.java @@ -159,7 +159,7 @@ public final class AddToGroupsActivity extends ContactSelectionActivity { private void handleNextPressed() { List groupsRecipientIds = Stream.of(contactsFragment.getSelectedContacts()) - .map(selectedContact -> selectedContact.getOrCreateRecipientId(this)) + .map(selectedContact -> selectedContact.getOrCreateRecipientId()) .toList(); viewModel.onContinueWithSelection(groupsRecipientIds); 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 1c4d280d33..5dc9a31672 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 @@ -124,7 +124,7 @@ public class CreateGroupActivity extends ContactSelectionActivity implements Con AlertDialog progress = SimpleProgressDialog.show(this); - SimpleTask.run(getLifecycle(), () -> RecipientRepository.lookupNewE164(this, number), result -> { + SimpleTask.run(getLifecycle(), () -> RecipientRepository.lookupNewE164(number), result -> { progress.dismiss(); if (result instanceof RecipientRepository.LookupResult.Success) { @@ -194,7 +194,7 @@ public class CreateGroupActivity extends ContactSelectionActivity implements Con SimpleTask.run(getLifecycle(), () -> { List ids = contactsFragment.getSelectedContacts() .stream() - .map(selectedContact -> selectedContact.getOrCreateRecipientId(this)) + .map(selectedContact -> selectedContact.getOrCreateRecipientId()) .collect(Collectors.toList()); List resolved = Recipient.resolvedList(ids); diff --git a/app/src/main/java/org/thoughtcrime/securesms/jobmanager/migrations/DeprecatedJobMigration.kt b/app/src/main/java/org/thoughtcrime/securesms/jobmanager/migrations/DeprecatedJobMigration.kt new file mode 100644 index 0000000000..a4a37ac7fb --- /dev/null +++ b/app/src/main/java/org/thoughtcrime/securesms/jobmanager/migrations/DeprecatedJobMigration.kt @@ -0,0 +1,15 @@ +/* + * Copyright 2025 Signal Messenger, LLC + * SPDX-License-Identifier: AGPL-3.0-only + */ + +package org.thoughtcrime.securesms.jobmanager.migrations + +import org.thoughtcrime.securesms.jobmanager.JobMigration + +/** + * Used as a replacement for another JobMigration that is no longer necessary. + */ +class DeprecatedJobMigration(version: Int) : JobMigration(version) { + override fun migrate(jobData: JobData): JobData = jobData +} diff --git a/app/src/main/java/org/thoughtcrime/securesms/jobmanager/migrations/PushProcessMessageQueueJobMigration.java b/app/src/main/java/org/thoughtcrime/securesms/jobmanager/migrations/PushProcessMessageQueueJobMigration.java deleted file mode 100644 index 89292116c5..0000000000 --- a/app/src/main/java/org/thoughtcrime/securesms/jobmanager/migrations/PushProcessMessageQueueJobMigration.java +++ /dev/null @@ -1,90 +0,0 @@ -package org.thoughtcrime.securesms.jobmanager.migrations; - -import android.content.Context; - -import androidx.annotation.NonNull; - -import org.signal.core.util.logging.Log; -import org.signal.libsignal.zkgroup.InvalidInputException; -import org.signal.libsignal.zkgroup.groups.GroupMasterKey; -import org.thoughtcrime.securesms.groups.GroupId; -import org.thoughtcrime.securesms.jobmanager.JobMigration; -import org.thoughtcrime.securesms.jobmanager.JsonJobData; -import org.thoughtcrime.securesms.recipients.Recipient; -import org.thoughtcrime.securesms.recipients.RecipientId; -import org.signal.core.util.Base64; -import org.whispersystems.signalservice.api.push.ServiceId; -import org.whispersystems.signalservice.api.push.SignalServiceAddress; -import org.whispersystems.signalservice.internal.serialize.protos.SignalServiceContentProto; - -import java.io.IOException; -import java.util.Optional; - -/** - * We changed the format of the queue key for legacy PushProcessMessageJob - * to have the recipient ID in it, so this migrates existing jobs to be in that format. - */ -public class PushProcessMessageQueueJobMigration extends JobMigration { - - private static final String TAG = Log.tag(PushProcessMessageQueueJobMigration.class); - - private final Context context; - - public PushProcessMessageQueueJobMigration(@NonNull Context context) { - super(6); - this.context = context; - } - - @Override - public @NonNull JobData migrate(@NonNull JobData jobData) { - if ("PushProcessJob".equals(jobData.getFactoryKey())) { - Log.i(TAG, "Found a PushProcessMessageJob to migrate."); - try { - return migratePushProcessMessageJob(context, jobData); - } catch (IOException | InvalidInputException e) { - Log.w(TAG, "Failed to migrate message job.", e); - return jobData; - } - } - return jobData; - } - - private static @NonNull JobData migratePushProcessMessageJob(@NonNull Context context, @NonNull JobData jobData) throws IOException, InvalidInputException { - JsonJobData data = JsonJobData.deserialize(jobData.getData()); - - String suffix = ""; - - if (data.getInt("message_state") == 0) { - SignalServiceContentProto proto = SignalServiceContentProto.ADAPTER.decode(Base64.decode(data.getString("message_content"))); - - if (proto != null && proto.content != null && proto.content.dataMessage != null && proto.content.dataMessage.groupV2 != null) { - Log.i(TAG, "Migrating a group message."); - - GroupId groupId = GroupId.v2(new GroupMasterKey(proto.content.dataMessage.groupV2.masterKey.toByteArray())); - Recipient recipient = Recipient.externalGroupExact(groupId); - - suffix = recipient.getId().toQueueKey(); - } else if (proto != null && proto.metadata != null && proto.metadata.address != null) { - Log.i(TAG, "Migrating an individual message."); - ServiceId senderServiceId = ServiceId.parseOrThrow(proto.metadata.address.uuid); - String senderE164 = proto.metadata.address.e164; - SignalServiceAddress sender = new SignalServiceAddress(senderServiceId, Optional.ofNullable(senderE164)); - - suffix = RecipientId.from(sender).toQueueKey(); - } - } else { - Log.i(TAG, "Migrating an exception message."); - - String exceptionSender = data.getString("exception_sender"); - GroupId exceptionGroup = GroupId.parseNullableOrThrow(data.getStringOrDefault("exception_groupId", null)); - - if (exceptionGroup != null) { - suffix = Recipient.externalGroupExact(exceptionGroup).getId().toQueueKey(); - } else if (exceptionSender != null) { - suffix = Recipient.external(context, exceptionSender).getId().toQueueKey(); - } - } - - return jobData.withQueueKey("__PUSH_PROCESS_JOB__" + suffix); - } -} diff --git a/app/src/main/java/org/thoughtcrime/securesms/jobmanager/migrations/RecipientIdFollowUpJobMigration.java b/app/src/main/java/org/thoughtcrime/securesms/jobmanager/migrations/RecipientIdFollowUpJobMigration.java deleted file mode 100644 index bfec388241..0000000000 --- a/app/src/main/java/org/thoughtcrime/securesms/jobmanager/migrations/RecipientIdFollowUpJobMigration.java +++ /dev/null @@ -1,59 +0,0 @@ -package org.thoughtcrime.securesms.jobmanager.migrations; - -import androidx.annotation.NonNull; - -import org.thoughtcrime.securesms.jobmanager.JsonJobData; -import org.thoughtcrime.securesms.jobmanager.JobMigration; - -/** - * Fixes things that went wrong in {@link RecipientIdJobMigration}. In particular, some jobs didn't - * have some necessary data fields carried over. Thankfully they're relatively non-critical, so - * we'll just swap them out with failing jobs if they're missing something. - */ -public class RecipientIdFollowUpJobMigration extends JobMigration { - - public RecipientIdFollowUpJobMigration() { - this(3); - } - - RecipientIdFollowUpJobMigration(int endVersion) { - super(endVersion); - } - - @Override - public @NonNull JobData migrate(@NonNull JobData jobData) { - switch(jobData.getFactoryKey()) { - case "RequestGroupInfoJob": return migrateRequestGroupInfoJob(jobData); - case "SendDeliveryReceiptJob": return migrateSendDeliveryReceiptJob(jobData); - default: - return jobData; - } - } - - private static @NonNull JobData migrateRequestGroupInfoJob(@NonNull JobData jobData) { - JsonJobData data = JsonJobData.deserialize(jobData.getData()); - - if (!data.hasString("source") || !data.hasString("group_id")) { - return failingJobData(); - } else { - return jobData; - } - } - - private static @NonNull JobData migrateSendDeliveryReceiptJob(@NonNull JobData jobData) { - JsonJobData data = JsonJobData.deserialize(jobData.getData()); - - if (!data.hasString("recipient") || - !data.hasLong("message_id") || - !data.hasLong("timestamp")) - { - return failingJobData(); - } else { - return jobData; - } - } - - private static JobData failingJobData() { - return JobData.FAILING_JOB_DATA; - } -} diff --git a/app/src/main/java/org/thoughtcrime/securesms/jobmanager/migrations/RecipientIdFollowUpJobMigration2.java b/app/src/main/java/org/thoughtcrime/securesms/jobmanager/migrations/RecipientIdFollowUpJobMigration2.java deleted file mode 100644 index cb463ddf3f..0000000000 --- a/app/src/main/java/org/thoughtcrime/securesms/jobmanager/migrations/RecipientIdFollowUpJobMigration2.java +++ /dev/null @@ -1,11 +0,0 @@ -package org.thoughtcrime.securesms.jobmanager.migrations; - -/** - * Unfortunately there was a bug in {@link RecipientIdFollowUpJobMigration} that requires it to be - * run again. - */ -public class RecipientIdFollowUpJobMigration2 extends RecipientIdFollowUpJobMigration { - public RecipientIdFollowUpJobMigration2() { - super(4); - } -} diff --git a/app/src/main/java/org/thoughtcrime/securesms/jobmanager/migrations/RecipientIdJobMigration.java b/app/src/main/java/org/thoughtcrime/securesms/jobmanager/migrations/RecipientIdJobMigration.java deleted file mode 100644 index 869b7f1510..0000000000 --- a/app/src/main/java/org/thoughtcrime/securesms/jobmanager/migrations/RecipientIdJobMigration.java +++ /dev/null @@ -1,263 +0,0 @@ -package org.thoughtcrime.securesms.jobmanager.migrations; - -import android.app.Application; - -import androidx.annotation.NonNull; -import androidx.annotation.VisibleForTesting; - -import com.fasterxml.jackson.annotation.JsonProperty; - -import org.thoughtcrime.securesms.jobmanager.JsonJobData; -import org.thoughtcrime.securesms.jobmanager.JobMigration; -import org.thoughtcrime.securesms.recipients.Recipient; -import org.thoughtcrime.securesms.recipients.RecipientId; -import org.thoughtcrime.securesms.util.JsonUtils; - -import java.io.IOException; -import java.io.Serializable; - -public class RecipientIdJobMigration extends JobMigration { - - private final Application application; - - public RecipientIdJobMigration(@NonNull Application application) { - super(2); - this.application = application; - } - - @Override - public @NonNull JobData migrate(@NonNull JobData jobData) { - switch(jobData.getFactoryKey()) { - case "MultiDeviceContactUpdateJob": return migrateMultiDeviceContactUpdateJob(jobData); - case "MultiDeviceRevealUpdateJob": return migrateMultiDeviceViewOnceOpenJob(jobData); - case "RequestGroupInfoJob": return migrateRequestGroupInfoJob(jobData); - case "SendDeliveryReceiptJob": return migrateSendDeliveryReceiptJob(jobData); - case "MultiDeviceVerifiedUpdateJob": return migrateMultiDeviceVerifiedUpdateJob(jobData); - case "RetrieveProfileJob": return migrateRetrieveProfileJob(jobData); - case "PushGroupSendJob": return migratePushGroupSendJob(jobData); - case "PushGroupUpdateJob": return migratePushGroupUpdateJob(jobData); - case "DirectoryRefreshJob": return migrateDirectoryRefreshJob(jobData); - case "RetrieveProfileAvatarJob": return migrateRetrieveProfileAvatarJob(jobData); - case "MultiDeviceReadUpdateJob": return migrateMultiDeviceReadUpdateJob(jobData); - case "PushTextSendJob": return migratePushTextSendJob(jobData); - case "PushMediaSendJob": return migratePushMediaSendJob(jobData); - case "SmsSendJob": return migrateSmsSendJob(jobData); - default: return jobData; - } - } - - private @NonNull JobData migrateMultiDeviceContactUpdateJob(@NonNull JobData jobData) { - JsonJobData data = JsonJobData.deserialize(jobData.getData()); - - String address = data.hasString("address") ? data.getString("address") : null; - JsonJobData updatedData = new JsonJobData.Builder().putString("recipient", address != null ? Recipient.external(application, address).getId().serialize() : null) - .putBoolean("force_sync", data.getBoolean("force_sync")) - .build(); - - return jobData.withData(updatedData.serialize()); - } - - private @NonNull JobData migrateMultiDeviceViewOnceOpenJob(@NonNull JobData jobData) { - JsonJobData data = JsonJobData.deserialize(jobData.getData()); - - try { - String rawOld = data.getString("message_id"); - OldSerializableSyncMessageId old = JsonUtils.fromJson(rawOld, OldSerializableSyncMessageId.class); - Recipient recipient = Recipient.external(application, old.sender); - NewSerializableSyncMessageId updated = new NewSerializableSyncMessageId(recipient.getId().serialize(), old.timestamp); - String rawUpdated = JsonUtils.toJson(updated); - JsonJobData updatedData = new JsonJobData.Builder().putString("message_id", rawUpdated).build(); - - return jobData.withData(updatedData.serialize()); - } catch (IOException e) { - throw new AssertionError(e); - } - } - - private @NonNull JobData migrateRequestGroupInfoJob(@NonNull JobData jobData) { - JsonJobData data = JsonJobData.deserialize(jobData.getData()); - - String address = data.getString("source"); - Recipient recipient = Recipient.external(application, address); - JsonJobData updatedData = new JsonJobData.Builder().putString("source", recipient.getId().serialize()) - .putString("group_id", data.getString("group_id")) - .build(); - - return jobData.withData(updatedData.serialize()); - } - - private @NonNull JobData migrateSendDeliveryReceiptJob(@NonNull JobData jobData) { - JsonJobData data = JsonJobData.deserialize(jobData.getData()); - - String address = data.getString("address"); - Recipient recipient = Recipient.external(application, address); - JsonJobData updatedData = new JsonJobData.Builder().putString("recipient", recipient.getId().serialize()) - .putLong("message_id", data.getLong("message_id")) - .putLong("timestamp", data.getLong("timestamp")) - .build(); - - return jobData.withData(updatedData.serialize()); - } - - private @NonNull JobData migrateMultiDeviceVerifiedUpdateJob(@NonNull JobData jobData) { - JsonJobData data = JsonJobData.deserialize(jobData.getData()); - - String address = data.getString("destination"); - Recipient recipient = Recipient.external(application, address); - JsonJobData updatedData = new JsonJobData.Builder().putString("destination", recipient.getId().serialize()) - .putString("identity_key", data.getString("identity_key")) - .putInt("verified_status", data.getInt("verified_status")) - .putLong("timestamp", data.getLong("timestamp")) - .build(); - - return jobData.withData(updatedData.serialize()); - } - - private @NonNull JobData migrateRetrieveProfileJob(@NonNull JobData jobData) { - JsonJobData data = JsonJobData.deserialize(jobData.getData()); - - String address = data.getString("address"); - Recipient recipient = Recipient.external(application, address); - JsonJobData updatedData = new JsonJobData.Builder().putString("recipient", recipient.getId().serialize()).build(); - - return jobData.withData(updatedData.serialize()); - } - - private @NonNull JobData migratePushGroupSendJob(@NonNull JobData jobData) { - JsonJobData data = JsonJobData.deserialize(jobData.getData()); - - // noinspection ConstantConditions - Recipient queueRecipient = Recipient.external(application, jobData.getQueueKey()); - String address = data.hasString("filter_address") ? data.getString("filter_address") : null; - RecipientId recipientId = address != null ? Recipient.external(application, address).getId() : null; - JsonJobData updatedData = new JsonJobData.Builder().putString("filter_recipient", recipientId != null ? recipientId.serialize() : null) - .putLong("message_id", data.getLong("message_id")) - .build(); - - return jobData.withQueueKey(queueRecipient.getId().toQueueKey()) - .withData(updatedData.serialize()); - } - - private @NonNull JobData migratePushGroupUpdateJob(@NonNull JobData jobData) { - JsonJobData data = JsonJobData.deserialize(jobData.getData()); - - String address = data.getString("source"); - Recipient recipient = Recipient.external(application, address); - JsonJobData updatedData = new JsonJobData.Builder().putString("source", recipient.getId().serialize()) - .putString("group_id", data.getString("group_id")) - .build(); - - return jobData.withData(updatedData.serialize()); - } - - private @NonNull JobData migrateDirectoryRefreshJob(@NonNull JobData jobData) { - JsonJobData data = JsonJobData.deserialize(jobData.getData()); - - String address = data.hasString("address") ? data.getString("address") : null; - Recipient recipient = address != null ? Recipient.external(application, address) : null; - JsonJobData updatedData = new JsonJobData.Builder().putString("recipient", recipient != null ? recipient.getId().serialize() : null) - .putBoolean("notify_of_new_users", data.getBoolean("notify_of_new_users")) - .build(); - - return jobData.withData(updatedData.serialize()); - } - - private @NonNull JobData migrateRetrieveProfileAvatarJob(@NonNull JobData jobData) { - JsonJobData data = JsonJobData.deserialize(jobData.getData()); - - //noinspection ConstantConditions - String queueAddress = jobData.getQueueKey().substring("RetrieveProfileAvatarJob".length()); - Recipient queueRecipient = Recipient.external(application, queueAddress); - String address = data.getString("address"); - Recipient recipient = Recipient.external(application, address); - JsonJobData updatedData = new JsonJobData.Builder().putString("recipient", recipient.getId().serialize()) - .putString("profile_avatar", data.getString("profile_avatar")) - .build(); - - return jobData.withQueueKey("RetrieveProfileAvatarJob::" + queueRecipient.getId().toQueueKey()) - .withData(updatedData.serialize()); - } - - private @NonNull JobData migrateMultiDeviceReadUpdateJob(@NonNull JobData jobData) { - JsonJobData data = JsonJobData.deserialize(jobData.getData()); - - try { - String[] rawOld = data.getStringArray("message_ids"); - String[] rawUpdated = new String[rawOld.length]; - - for (int i = 0; i < rawOld.length; i++) { - OldSerializableSyncMessageId old = JsonUtils.fromJson(rawOld[i], OldSerializableSyncMessageId.class); - Recipient recipient = Recipient.external(application, old.sender); - NewSerializableSyncMessageId updated = new NewSerializableSyncMessageId(recipient.getId().serialize(), old.timestamp); - - rawUpdated[i] = JsonUtils.toJson(updated); - } - - JsonJobData updatedData = new JsonJobData.Builder().putStringArray("message_ids", rawUpdated).build(); - - return jobData.withData(updatedData.serialize()); - } catch (IOException e) { - throw new AssertionError(e); - } - } - - private @NonNull JobData migratePushTextSendJob(@NonNull JobData jobData) { - JsonJobData data = JsonJobData.deserialize(jobData.getData()); - - //noinspection ConstantConditions - Recipient recipient = Recipient.external(application, jobData.getQueueKey()); - return jobData.withQueueKey(recipient.getId().toQueueKey()); - } - - private @NonNull JobData migratePushMediaSendJob(@NonNull JobData jobData) { - JsonJobData data = JsonJobData.deserialize(jobData.getData()); - - //noinspection ConstantConditions - Recipient recipient = Recipient.external(application, jobData.getQueueKey()); - return jobData.withQueueKey(recipient.getId().toQueueKey()); - } - - private @NonNull JobData migrateSmsSendJob(@NonNull JobData jobData) { - JsonJobData data = JsonJobData.deserialize(jobData.getData()); - - //noinspection ConstantConditions - if (jobData.getQueueKey() != null) { - Recipient recipient = Recipient.external(application, jobData.getQueueKey()); - return jobData.withQueueKey(recipient.getId().toQueueKey()); - } else { - return jobData; - } - } - - @VisibleForTesting - static class OldSerializableSyncMessageId implements Serializable { - - private static final long serialVersionUID = 1L; - - @JsonProperty - private final String sender; - @JsonProperty - private final long timestamp; - - OldSerializableSyncMessageId(@JsonProperty("sender") String sender, @JsonProperty("timestamp") long timestamp) { - this.sender = sender; - this.timestamp = timestamp; - } - } - - @VisibleForTesting - static class NewSerializableSyncMessageId implements Serializable { - - private static final long serialVersionUID = 1L; - - @JsonProperty - private final String recipientId; - @JsonProperty - private final long timestamp; - - NewSerializableSyncMessageId(@JsonProperty("recipientId") String recipientId, @JsonProperty("timestamp") long timestamp) { - this.recipientId = recipientId; - this.timestamp = timestamp; - } - } -} diff --git a/app/src/main/java/org/thoughtcrime/securesms/jobs/IndividualSendJob.java b/app/src/main/java/org/thoughtcrime/securesms/jobs/IndividualSendJob.java index 609861a6d5..307bac9249 100644 --- a/app/src/main/java/org/thoughtcrime/securesms/jobs/IndividualSendJob.java +++ b/app/src/main/java/org/thoughtcrime/securesms/jobs/IndividualSendJob.java @@ -28,7 +28,6 @@ import org.thoughtcrime.securesms.mms.MmsException; import org.thoughtcrime.securesms.mms.OutgoingMessage; import org.thoughtcrime.securesms.ratelimit.ProofRequiredExceptionHandler; import org.thoughtcrime.securesms.recipients.Recipient; -import org.thoughtcrime.securesms.recipients.RecipientId; import org.thoughtcrime.securesms.recipients.RecipientUtil; import org.thoughtcrime.securesms.service.ExpiringMessageManager; import org.thoughtcrime.securesms.transport.RetryLaterException; @@ -211,10 +210,14 @@ public class IndividualSendJob extends PushSendJob { AppDependencies.getJobManager().add(new DirectoryRefreshJob(false)); } catch (UntrustedIdentityException uie) { warn(TAG, "Failure", uie); - RecipientId recipientId = Recipient.external(context, uie.getIdentifier()).getId(); - database.addMismatchedIdentity(messageId, recipientId, uie.getIdentityKey()); + Recipient recipient = Recipient.external(uie.getIdentifier()); + if (recipient == null) { + Log.w(TAG, "Failed to create a Recipient for the identifier!"); + return; + } + database.addMismatchedIdentity(messageId, recipient.getId(), uie.getIdentityKey()); database.markAsSentFailed(messageId); - RetrieveProfileJob.enqueue(recipientId); + RetrieveProfileJob.enqueue(recipient.getId()); } catch (ProofRequiredException e) { ProofRequiredExceptionHandler.Result result = ProofRequiredExceptionHandler.handle(context, e, SignalDatabase.threads().getRecipientForThreadId(threadId), threadId, messageId); if (result.isRetry()) { diff --git a/app/src/main/java/org/thoughtcrime/securesms/jobs/JobManagerFactories.java b/app/src/main/java/org/thoughtcrime/securesms/jobs/JobManagerFactories.java index 053464addf..6e2486e128 100644 --- a/app/src/main/java/org/thoughtcrime/securesms/jobs/JobManagerFactories.java +++ b/app/src/main/java/org/thoughtcrime/securesms/jobs/JobManagerFactories.java @@ -30,14 +30,11 @@ import org.thoughtcrime.securesms.jobmanager.impl.RestoreAttachmentConstraintObs import org.thoughtcrime.securesms.jobmanager.impl.SqlCipherMigrationConstraint; import org.thoughtcrime.securesms.jobmanager.impl.SqlCipherMigrationConstraintObserver; import org.thoughtcrime.securesms.jobmanager.impl.WifiConstraint; +import org.thoughtcrime.securesms.jobmanager.migrations.DeprecatedJobMigration; import org.thoughtcrime.securesms.jobmanager.migrations.DonationReceiptRedemptionJobMigration; import org.thoughtcrime.securesms.jobmanager.migrations.GroupCallPeekJobDataMigration; import org.thoughtcrime.securesms.jobmanager.migrations.PushDecryptMessageJobEnvelopeMigration; import org.thoughtcrime.securesms.jobmanager.migrations.PushProcessMessageJobMigration; -import org.thoughtcrime.securesms.jobmanager.migrations.PushProcessMessageQueueJobMigration; -import org.thoughtcrime.securesms.jobmanager.migrations.RecipientIdFollowUpJobMigration; -import org.thoughtcrime.securesms.jobmanager.migrations.RecipientIdFollowUpJobMigration2; -import org.thoughtcrime.securesms.jobmanager.migrations.RecipientIdJobMigration; import org.thoughtcrime.securesms.jobmanager.migrations.RetrieveProfileJobMigration; import org.thoughtcrime.securesms.jobmanager.migrations.SendReadReceiptsJobMigration; import org.thoughtcrime.securesms.jobmanager.migrations.SenderKeyDistributionSendJobRecipientMigration; @@ -243,7 +240,7 @@ public final class JobManagerFactories { put(SenderKeyDistributionSendJob.KEY, new SenderKeyDistributionSendJob.Factory()); put(SendDeliveryReceiptJob.KEY, new SendDeliveryReceiptJob.Factory()); put(SendPaymentsActivatedJob.KEY, new SendPaymentsActivatedJob.Factory()); - put(SendReadReceiptJob.KEY, new SendReadReceiptJob.Factory(application)); + put(SendReadReceiptJob.KEY, new SendReadReceiptJob.Factory()); put(SendRetryReceiptJob.KEY, new SendRetryReceiptJob.Factory()); put(SendViewedReceiptJob.KEY, new SendViewedReceiptJob.Factory(application)); put(StorageRotateManifestJob.KEY, new StorageRotateManifestJob.Factory()); @@ -415,11 +412,11 @@ public final class JobManagerFactories { } public static List getJobMigrations(@NonNull Application application) { - return Arrays.asList(new RecipientIdJobMigration(application), - new RecipientIdFollowUpJobMigration(), - new RecipientIdFollowUpJobMigration2(), + return Arrays.asList(new DeprecatedJobMigration(2), + new DeprecatedJobMigration(3), + new DeprecatedJobMigration(4), new SendReadReceiptsJobMigration(SignalDatabase.messages()), - new PushProcessMessageQueueJobMigration(application), + new DeprecatedJobMigration(6), new RetrieveProfileJobMigration(), new PushDecryptMessageJobEnvelopeMigration(), new SenderKeyDistributionSendJobRecipientMigration(), diff --git a/app/src/main/java/org/thoughtcrime/securesms/jobs/MultiDeviceContactSyncJob.kt b/app/src/main/java/org/thoughtcrime/securesms/jobs/MultiDeviceContactSyncJob.kt index 75e919d5d0..1e4bda053f 100644 --- a/app/src/main/java/org/thoughtcrime/securesms/jobs/MultiDeviceContactSyncJob.kt +++ b/app/src/main/java/org/thoughtcrime/securesms/jobs/MultiDeviceContactSyncJob.kt @@ -71,14 +71,17 @@ class MultiDeviceContactSyncJob(parameters: Parameters, private val attachmentPo private fun processContactFile(inputStream: InputStream) { val deviceContacts = DeviceContactsInputStream(inputStream) val recipients = SignalDatabase.recipients - val threads = SignalDatabase.threads var contact: DeviceContact? = deviceContacts.read() while (contact != null) { - val recipient = if (contact.aci.isPresent) { + val recipient: Recipient? = if (contact.aci.isPresent) { Recipient.externalPush(SignalServiceAddress(contact.aci.get(), contact.e164.orElse(null))) } else { - Recipient.external(context, contact.e164.get()) + Recipient.external(contact.e164.get()) + } + + if (recipient == null) { + continue } if (recipient.isSelf) { diff --git a/app/src/main/java/org/thoughtcrime/securesms/jobs/PushProcessMessageErrorJob.kt b/app/src/main/java/org/thoughtcrime/securesms/jobs/PushProcessMessageErrorJob.kt index 98bc2b882e..3da3e66934 100644 --- a/app/src/main/java/org/thoughtcrime/securesms/jobs/PushProcessMessageErrorJob.kt +++ b/app/src/main/java/org/thoughtcrime/securesms/jobs/PushProcessMessageErrorJob.kt @@ -5,10 +5,8 @@ package org.thoughtcrime.securesms.jobs -import android.content.Context import androidx.annotation.WorkerThread import org.signal.core.util.logging.Log -import org.thoughtcrime.securesms.dependencies.AppDependencies import org.thoughtcrime.securesms.groups.GroupId import org.thoughtcrime.securesms.jobmanager.Job import org.thoughtcrime.securesms.jobmanager.JsonJobData @@ -92,14 +90,16 @@ class PushProcessMessageErrorJob private constructor( @WorkerThread private fun createParameters(exceptionMetadata: ExceptionMetadata): Parameters { - val context: Context = AppDependencies.application + val recipient: Recipient? = exceptionMetadata.groupId?.let { Recipient.externalPossiblyMigratedGroup(it) } ?: Recipient.external(exceptionMetadata.sender) - val recipient = exceptionMetadata.groupId?.let { Recipient.externalPossiblyMigratedGroup(it) } ?: Recipient.external(context, exceptionMetadata.sender) + if (recipient == null) { + Log.w(TAG, "Unable to create Recipient for the requested identifier!") + } return Parameters.Builder() .setMaxAttempts(Parameters.UNLIMITED) .addConstraint(ChangeNumberConstraint.KEY) - .setQueue(PushProcessMessageJob.getQueueName(recipient.id)) + .setQueue(recipient?.let { PushProcessMessageJob.getQueueName(it.id) }) .build() } } diff --git a/app/src/main/java/org/thoughtcrime/securesms/jobs/SendReadReceiptJob.java b/app/src/main/java/org/thoughtcrime/securesms/jobs/SendReadReceiptJob.java index b884340258..6e057fab7e 100644 --- a/app/src/main/java/org/thoughtcrime/securesms/jobs/SendReadReceiptJob.java +++ b/app/src/main/java/org/thoughtcrime/securesms/jobs/SendReadReceiptJob.java @@ -1,8 +1,6 @@ package org.thoughtcrime.securesms.jobs; -import android.app.Application; - import androidx.annotation.NonNull; import androidx.annotation.Nullable; import androidx.annotation.VisibleForTesting; @@ -223,12 +221,6 @@ public class SendReadReceiptJob extends BaseJob { public static final class Factory implements Job.Factory { - private final Application application; - - public Factory(@NonNull Application application) { - this.application = application; - } - @Override public @NonNull SendReadReceiptJob create(@NonNull Parameters parameters, @Nullable byte[] serializedData) { JsonJobData data = JsonJobData.deserialize(serializedData); @@ -239,8 +231,7 @@ public class SendReadReceiptJob extends BaseJob { List rawMessageIds = data.hasStringArray(KEY_MESSAGE_IDS) ? data.getStringArrayAsList(KEY_MESSAGE_IDS) : Collections.emptyList(); List messageIds = rawMessageIds.stream().map(MessageId::deserialize).collect(Collectors.toList()); long threadId = data.getLong(KEY_THREAD); - RecipientId recipientId = data.hasString(KEY_RECIPIENT) ? RecipientId.from(data.getString(KEY_RECIPIENT)) - : Recipient.external(application, data.getString(KEY_ADDRESS)).getId(); + RecipientId recipientId = RecipientId.from(data.getString(KEY_RECIPIENT)); for (long id : ids) { sentTimestamps.add(id); diff --git a/app/src/main/java/org/thoughtcrime/securesms/jobs/SendViewedReceiptJob.java b/app/src/main/java/org/thoughtcrime/securesms/jobs/SendViewedReceiptJob.java index 6d9cd00862..834ce48e21 100644 --- a/app/src/main/java/org/thoughtcrime/securesms/jobs/SendViewedReceiptJob.java +++ b/app/src/main/java/org/thoughtcrime/securesms/jobs/SendViewedReceiptJob.java @@ -248,8 +248,7 @@ public class SendViewedReceiptJob extends BaseJob { List rawMessageIds = data.hasStringArray(KEY_MESSAGE_IDS) ? data.getStringArrayAsList(KEY_MESSAGE_IDS) : Collections.emptyList(); List messageIds = rawMessageIds.stream().map(MessageId::deserialize).collect(Collectors.toList()); long threadId = data.getLong(KEY_THREAD); - RecipientId recipientId = data.hasString(KEY_RECIPIENT) ? RecipientId.from(data.getString(KEY_RECIPIENT)) - : Recipient.external(application, data.getString(KEY_ADDRESS)).getId(); + RecipientId recipientId = RecipientId.from(data.getString(KEY_RECIPIENT)); return new SendViewedReceiptJob(parameters, threadId, recipientId, syncTimestamps, messageIds, timestamp); } diff --git a/app/src/main/java/org/thoughtcrime/securesms/jobs/SyncSystemContactLinksJob.kt b/app/src/main/java/org/thoughtcrime/securesms/jobs/SyncSystemContactLinksJob.kt index d548dbb88a..1c9bb174d3 100644 --- a/app/src/main/java/org/thoughtcrime/securesms/jobs/SyncSystemContactLinksJob.kt +++ b/app/src/main/java/org/thoughtcrime/securesms/jobs/SyncSystemContactLinksJob.kt @@ -15,8 +15,8 @@ import org.thoughtcrime.securesms.contacts.sync.ContactDiscovery import org.thoughtcrime.securesms.database.SignalDatabase import org.thoughtcrime.securesms.jobmanager.Job import org.thoughtcrime.securesms.permissions.Permissions -import org.thoughtcrime.securesms.phonenumbers.PhoneNumberFormatter import org.thoughtcrime.securesms.storage.StorageSyncHelper +import org.thoughtcrime.securesms.util.SignalE164Util import java.lang.Exception /** @@ -109,7 +109,7 @@ class SyncSystemContactLinksJob private constructor(parameters: Parameters) : Ba messagePrompt = { e164 -> context.getString(R.string.ContactsDatabase_message_s, e164) }, callPrompt = { e164 -> context.getString(R.string.ContactsDatabase_signal_call_s, e164) }, videoCallPrompt = { e164 -> context.getString(R.string.ContactsDatabase_signal_video_call_s, e164) }, - e164Formatter = { number -> PhoneNumberFormatter.get(context).format(number) }, + e164Formatter = { number -> SignalE164Util.formatAsE164(number) }, messageMimetype = MESSAGE_MIMETYPE, callMimetype = CALL_MIMETYPE, videoCallMimetype = VIDEO_CALL_MIMETYPE, diff --git a/app/src/main/java/org/thoughtcrime/securesms/messages/MessageContentProcessor.kt b/app/src/main/java/org/thoughtcrime/securesms/messages/MessageContentProcessor.kt index 2cb2bfede6..132f9a9254 100644 --- a/app/src/main/java/org/thoughtcrime/securesms/messages/MessageContentProcessor.kt +++ b/app/src/main/java/org/thoughtcrime/securesms/messages/MessageContentProcessor.kt @@ -342,7 +342,12 @@ open class MessageContentProcessor(private val context: Context) { } fun processException(messageState: MessageState, exceptionMetadata: ExceptionMetadata, timestamp: Long) { - val sender = Recipient.external(context, exceptionMetadata.sender) + val sender = Recipient.external(exceptionMetadata.sender) + + if (sender == null) { + warn("Failed to create Recipient for identifier: $messageState") + return + } if (sender.isBlocked) { warn("Ignoring exception content from blocked sender, message state: $messageState") diff --git a/app/src/main/java/org/thoughtcrime/securesms/messages/MessageDecryptor.kt b/app/src/main/java/org/thoughtcrime/securesms/messages/MessageDecryptor.kt index 9a88c70833..c5f3ff8b11 100644 --- a/app/src/main/java/org/thoughtcrime/securesms/messages/MessageDecryptor.kt +++ b/app/src/main/java/org/thoughtcrime/securesms/messages/MessageDecryptor.kt @@ -10,6 +10,7 @@ import com.squareup.wire.internal.toUnmodifiableList import org.signal.core.util.PendingIntentFlags import org.signal.core.util.isAbsent import org.signal.core.util.logging.Log +import org.signal.core.util.logging.logW import org.signal.core.util.roundedString import org.signal.libsignal.metadata.InvalidMetadataMessageException import org.signal.libsignal.metadata.InvalidMetadataVersionException @@ -223,8 +224,9 @@ object MessageDecryptor { Log.w(TAG, "${logPrefix(envelope, e)} Retry receipts disabled! Enqueuing a session reset job, which will also insert an error message.", e, true) followUpOperations += FollowUpOperation { - val sender: Recipient = Recipient.external(context, e.sender) - AutomaticSessionResetJob(sender.id, e.senderDevice, envelope.timestamp!!).asChain() + Recipient.external(e.sender)?.let { + AutomaticSessionResetJob(it.id, e.senderDevice, envelope.timestamp!!).asChain() + } ?: null.logW(TAG, "${logPrefix(envelope, e)} Failed to create a recipient with the provided identifier!") } Result.Ignore(envelope, serverDeliveredTimestamp, followUpOperations.toUnmodifiableList()) @@ -281,7 +283,7 @@ object MessageDecryptor { val contentHint: ContentHint = ContentHint.fromType(protocolException.contentHint) val senderDevice: Int = protocolException.senderDevice val receivedTimestamp: Long = System.currentTimeMillis() - val sender: Recipient = Recipient.external(context, protocolException.sender) + val sender: Recipient = Recipient.external(protocolException.sender) ?: return Result.Ignore(envelope, serverDeliveredTimestamp, followUpOperations) val senderServiceId: ServiceId? = ServiceId.parseOrNull(protocolException.sender) if (sender.isSelf) { diff --git a/app/src/main/java/org/thoughtcrime/securesms/messages/SyncMessageProcessor.kt b/app/src/main/java/org/thoughtcrime/securesms/messages/SyncMessageProcessor.kt index a64422ba43..e472fea13f 100644 --- a/app/src/main/java/org/thoughtcrime/securesms/messages/SyncMessageProcessor.kt +++ b/app/src/main/java/org/thoughtcrime/securesms/messages/SyncMessageProcessor.kt @@ -99,6 +99,7 @@ import org.thoughtcrime.securesms.util.IdentityUtil import org.thoughtcrime.securesms.util.MediaUtil import org.thoughtcrime.securesms.util.MessageConstraintsUtil import org.thoughtcrime.securesms.util.RemoteConfig +import org.thoughtcrime.securesms.util.SignalE164Util import org.thoughtcrime.securesms.util.TextSecurePreferences import org.thoughtcrime.securesms.util.Util import org.whispersystems.signalservice.api.crypto.EnvelopeMetadata @@ -1724,7 +1725,9 @@ object SyncMessageProcessor { } threadE164 != null -> { - SignalDatabase.recipients.getOrInsertFromE164(threadE164!!) + SignalE164Util.formatAsE164(threadE164!!)?.let { + SignalDatabase.recipients.getOrInsertFromE164(threadE164!!) + } } else -> null diff --git a/app/src/main/java/org/thoughtcrime/securesms/migrations/AvatarMigrationJob.java b/app/src/main/java/org/thoughtcrime/securesms/migrations/AvatarMigrationJob.java index 250cb6de06..69a7a5ad63 100644 --- a/app/src/main/java/org/thoughtcrime/securesms/migrations/AvatarMigrationJob.java +++ b/app/src/main/java/org/thoughtcrime/securesms/migrations/AvatarMigrationJob.java @@ -62,10 +62,12 @@ public class AvatarMigrationJob extends MigrationJob { for (File file : files) { try { if (isValidFileName(file.getName())) { - Recipient recipient = Recipient.external(context, file.getName()); + Recipient recipient = Recipient.external(file.getName()); byte[] data = StreamUtil.readFully(new FileInputStream(file)); - AvatarHelper.setAvatar(context, recipient.getId(), new ByteArrayInputStream(data)); + if (recipient != null) { + AvatarHelper.setAvatar(context, recipient.getId(), new ByteArrayInputStream(data)); + } } else { Log.w(TAG, "Invalid file name! Can't migrate this file. It'll just get deleted."); } diff --git a/app/src/main/java/org/thoughtcrime/securesms/migrations/BadE164MigrationJob.kt b/app/src/main/java/org/thoughtcrime/securesms/migrations/BadE164MigrationJob.kt index 03deed8d4e..8c71301395 100644 --- a/app/src/main/java/org/thoughtcrime/securesms/migrations/BadE164MigrationJob.kt +++ b/app/src/main/java/org/thoughtcrime/securesms/migrations/BadE164MigrationJob.kt @@ -19,8 +19,8 @@ import org.thoughtcrime.securesms.database.RecipientTable.Companion.PNI_COLUMN import org.thoughtcrime.securesms.database.RecipientTable.Companion.TABLE_NAME import org.thoughtcrime.securesms.database.SignalDatabase import org.thoughtcrime.securesms.jobmanager.Job -import org.thoughtcrime.securesms.phonenumbers.PhoneNumberFormatter import org.thoughtcrime.securesms.recipients.RecipientId +import org.thoughtcrime.securesms.util.SignalE164Util /** * Through testing, we've discovered that some badly-formatted e164's wound up in the e164 column of the recipient table. @@ -89,7 +89,7 @@ internal class BadE164MigrationJob( continue } - val formattedNumber = PhoneNumberFormatter.get(context).formatOrNull(entry.e164) + val formattedNumber = SignalE164Util.formatAsE164(entry.e164) if (formattedNumber == null) { Log.w(TAG, "We have encountered an e164-only recipient, with messages, whose value characters are all valid, but still remains completely unparsable.") continue diff --git a/app/src/main/java/org/thoughtcrime/securesms/mms/QuoteId.java b/app/src/main/java/org/thoughtcrime/securesms/mms/QuoteId.java index 0f27690cdc..580f855a00 100644 --- a/app/src/main/java/org/thoughtcrime/securesms/mms/QuoteId.java +++ b/app/src/main/java/org/thoughtcrime/securesms/mms/QuoteId.java @@ -54,8 +54,17 @@ public class QuoteId { public static @Nullable QuoteId deserialize(@NonNull Context context, @NonNull String serialized) { try { JSONObject json = new JSONObject(serialized); - RecipientId id = json.has(AUTHOR) ? RecipientId.from(json.getString(AUTHOR)) - : Recipient.external(context, json.getString(AUTHOR_DEPRECATED)).getId(); + RecipientId id; + if (json.has(AUTHOR)) { + id = RecipientId.from(json.getString(AUTHOR)); + } else { + Recipient recipient = Recipient.external(json.getString(AUTHOR_DEPRECATED)); + if (recipient != null) { + id = recipient.getId(); + } else { + return null; + } + } return new QuoteId(json.getLong(ID), id); } catch (JSONException e) { diff --git a/app/src/main/java/org/thoughtcrime/securesms/payments/confirm/ConfirmPaymentFragment.java b/app/src/main/java/org/thoughtcrime/securesms/payments/confirm/ConfirmPaymentFragment.java index f35f4ce5c3..e46f52c398 100644 --- a/app/src/main/java/org/thoughtcrime/securesms/payments/confirm/ConfirmPaymentFragment.java +++ b/app/src/main/java/org/thoughtcrime/securesms/payments/confirm/ConfirmPaymentFragment.java @@ -28,6 +28,7 @@ import com.google.android.material.bottomsheet.BottomSheetDialog; import com.google.android.material.bottomsheet.BottomSheetDialogFragment; import com.google.android.material.dialog.MaterialAlertDialogBuilder; +import org.signal.core.util.StringUtil; import org.signal.core.util.ThreadUtil; import org.signal.core.util.logging.Log; import org.thoughtcrime.securesms.BiometricDeviceAuthentication; @@ -42,7 +43,6 @@ import org.thoughtcrime.securesms.payments.preferences.PaymentsHomeFragmentDirec import org.thoughtcrime.securesms.payments.preferences.RecipientHasNotEnabledPaymentsDialog; import org.thoughtcrime.securesms.recipients.Recipient; import org.thoughtcrime.securesms.util.BottomSheetUtil; -import org.signal.core.util.StringUtil; import org.thoughtcrime.securesms.util.ServiceUtil; import org.thoughtcrime.securesms.util.WindowUtil; import org.thoughtcrime.securesms.util.adapter.mapping.MappingModelList; diff --git a/app/src/main/java/org/thoughtcrime/securesms/payments/preferences/details/PaymentDetailsFragment.java b/app/src/main/java/org/thoughtcrime/securesms/payments/preferences/details/PaymentDetailsFragment.java index ca27d723e4..1bd947c483 100644 --- a/app/src/main/java/org/thoughtcrime/securesms/payments/preferences/details/PaymentDetailsFragment.java +++ b/app/src/main/java/org/thoughtcrime/securesms/payments/preferences/details/PaymentDetailsFragment.java @@ -20,6 +20,7 @@ import androidx.fragment.app.FragmentActivity; import androidx.lifecycle.ViewModelProvider; import androidx.navigation.Navigation; +import org.signal.core.util.StringUtil; import org.signal.core.util.logging.Log; import org.thoughtcrime.securesms.LoggingFragment; import org.thoughtcrime.securesms.R; @@ -34,7 +35,6 @@ import org.thoughtcrime.securesms.payments.State; import org.thoughtcrime.securesms.recipients.Recipient; import org.thoughtcrime.securesms.util.DateUtils; import org.thoughtcrime.securesms.util.SpanUtil; -import org.signal.core.util.StringUtil; import org.thoughtcrime.securesms.util.views.LearnMoreTextView; import org.whispersystems.signalservice.api.payments.Money; diff --git a/app/src/main/java/org/thoughtcrime/securesms/phonenumbers/PhoneNumberFormatter.java b/app/src/main/java/org/thoughtcrime/securesms/phonenumbers/PhoneNumberFormatter.java deleted file mode 100644 index c8bce00089..0000000000 --- a/app/src/main/java/org/thoughtcrime/securesms/phonenumbers/PhoneNumberFormatter.java +++ /dev/null @@ -1,244 +0,0 @@ -package org.thoughtcrime.securesms.phonenumbers; - -import android.content.Context; - -import androidx.annotation.NonNull; -import androidx.annotation.Nullable; - -import com.google.i18n.phonenumbers.NumberParseException; -import com.google.i18n.phonenumbers.PhoneNumberUtil; -import com.google.i18n.phonenumbers.Phonenumber; -import com.google.i18n.phonenumbers.ShortNumberInfo; - -import org.signal.core.util.BidiUtil; -import org.signal.core.util.logging.Log; -import org.signal.libsignal.protocol.util.Pair; -import org.thoughtcrime.securesms.dependencies.AppDependencies; -import org.thoughtcrime.securesms.groups.GroupId; -import org.thoughtcrime.securesms.keyvalue.SignalStore; -import org.signal.core.util.SetUtil; -import org.signal.core.util.StringUtil; -import org.thoughtcrime.securesms.util.Util; - -import java.util.Optional; -import java.util.Set; -import java.util.concurrent.atomic.AtomicReference; -import java.util.regex.Matcher; -import java.util.regex.Pattern; - -public class PhoneNumberFormatter { - - private static final String TAG = Log.tag(PhoneNumberFormatter.class); - - private static final String UNKNOWN_NUMBER = "Unknown"; - - private static final Set EXCLUDE_FROM_MANUAL_SHORTCODE_4 = SetUtil.newHashSet("AC", "NC", "NU", "TK"); - private static final Set NATIONAL_FORMAT_COUNTRY_CODES = SetUtil.newHashSet(1 /*US*/, 44 /*UK*/); - - private static final Pattern US_NO_AREACODE = Pattern.compile("^(\\d{7})$"); - private static final Pattern BR_NO_AREACODE = Pattern.compile("^(9?\\d{8})$"); - - private static final AtomicReference> cachedFormatter = new AtomicReference<>(); - - private final Optional localNumber; - private final String localCountryCode; - - private final PhoneNumberUtil phoneNumberUtil = PhoneNumberUtil.getInstance(); - private final Pattern ALPHA_PATTERN = Pattern.compile("[a-zA-Z]"); - - public static @NonNull PhoneNumberFormatter get(Context context) { - String localNumber = SignalStore.account().getE164(); - - if (!Util.isEmpty(localNumber)) { - Pair cached = cachedFormatter.get(); - - if (cached != null && cached.first().equals(localNumber)) return cached.second(); - - PhoneNumberFormatter formatter = new PhoneNumberFormatter(localNumber); - cachedFormatter.set(new Pair<>(localNumber, formatter)); - - return formatter; - } else { - return new PhoneNumberFormatter(Util.getSimCountryIso(context).orElse("US"), true); - } - } - - PhoneNumberFormatter(@NonNull String localNumberString) { - try { - Phonenumber.PhoneNumber libNumber = phoneNumberUtil.parse(localNumberString, null); - int countryCode = libNumber.getCountryCode(); - - this.localNumber = Optional.of(new PhoneNumber(localNumberString, countryCode, parseAreaCode(localNumberString, countryCode))); - this.localCountryCode = phoneNumberUtil.getRegionCodeForNumber(libNumber); - } catch (NumberParseException e) { - throw new AssertionError(e); - } - } - - PhoneNumberFormatter(@NonNull String localCountryCode, boolean countryCode) { - this.localNumber = Optional.empty(); - this.localCountryCode = localCountryCode; - } - - public static @NonNull String prettyPrint(@NonNull String e164) { - return BidiUtil.forceLtr(get(AppDependencies.getApplication()).prettyPrintFormat(e164)); - } - - public @NonNull String prettyPrintFormat(@NonNull String e164) { - try { - Phonenumber.PhoneNumber parsedNumber = phoneNumberUtil.parse(e164, localCountryCode); - - if (localNumber.isPresent() && - localNumber.get().countryCode == parsedNumber.getCountryCode() && - NATIONAL_FORMAT_COUNTRY_CODES.contains(localNumber.get().getCountryCode())) - { - return BidiUtil.isolateBidi(phoneNumberUtil.format(parsedNumber, PhoneNumberUtil.PhoneNumberFormat.NATIONAL)); - } else { - return BidiUtil.isolateBidi(phoneNumberUtil.format(parsedNumber, PhoneNumberUtil.PhoneNumberFormat.INTERNATIONAL)); - } - } catch (NumberParseException e) { - Log.w(TAG, "Failed to format number: " + e.toString()); - return BidiUtil.isolateBidi(e164); - } - } - - public static int getLocalCountryCode() { - Optional localNumber = get(AppDependencies.getApplication()).localNumber; - return localNumber != null && localNumber.isPresent() ? localNumber.get().countryCode : 0; - } - - public @Nullable String formatOrNull(@Nullable String number) { - String formatted = format(number); - if (formatted.equals(UNKNOWN_NUMBER)) { - return null; - } - return formatted; - } - - - public @NonNull String format(@Nullable String number) { - if (number == null) return UNKNOWN_NUMBER; - if (GroupId.isEncodedGroup(number)) return number; - if (ALPHA_PATTERN.matcher(number).find()) return number.trim(); - - String bareNumber = number.replaceAll("[^0-9+]", ""); - - if (bareNumber.length() == 0) { - if (number.trim().length() == 0) return "Unknown"; - else return number.trim(); - } - - if (bareNumber.length() <= 6) { - return bareNumber; - } - - if (isShortCode(bareNumber, localCountryCode)) { - Log.i(TAG, "Recognized number as short code."); - return bareNumber; - } - - String processedNumber = applyAreaCodeRules(localNumber, bareNumber); - - try { - Phonenumber.PhoneNumber parsedNumber = phoneNumberUtil.parse(processedNumber, localCountryCode); - String formatted = phoneNumberUtil.format(parsedNumber, PhoneNumberUtil.PhoneNumberFormat.E164); - - if (formatted.startsWith("+")) { - return formatted; - } else { - throw new NumberParseException(NumberParseException.ErrorType.NOT_A_NUMBER, "After formatting, the number did not start with +! hasRawInput: " + parsedNumber.hasRawInput()); - } - } catch (NumberParseException e) { - Log.w(TAG, e.toString()); - if (bareNumber.charAt(0) == '+') { - return bareNumber; - } - - if (localNumber.isPresent()) { - String localNumberImprecise = localNumber.get().getE164Number(); - - if (localNumberImprecise.charAt(0) == '+') { - localNumberImprecise = localNumberImprecise.substring(1); - } - - if (localNumberImprecise.length() == bareNumber.length() || bareNumber.length() > localNumberImprecise.length()) { - return "+" + number; - } - - int difference = localNumberImprecise.length() - bareNumber.length(); - - return "+" + localNumberImprecise.substring(0, difference) + bareNumber; - } else { - String countryCode = String.valueOf(phoneNumberUtil.getCountryCodeForRegion(localCountryCode)); - return "+" + (bareNumber.startsWith(countryCode) ? bareNumber : countryCode + bareNumber); - } - } - } - - private boolean isShortCode(@NonNull String bareNumber, String localCountryCode) { - try { - Phonenumber.PhoneNumber parsedNumber = phoneNumberUtil.parse(bareNumber, localCountryCode); - return ShortNumberInfo.getInstance().isValidShortNumberForRegion(parsedNumber, localCountryCode); - } catch (NumberParseException e) { - return false; - } - } - - private @Nullable String parseAreaCode(@NonNull String e164Number, int countryCode) { - switch (countryCode) { - case 1: - return e164Number.substring(2, 5); - case 55: - return e164Number.substring(3, 5); - } - return null; - } - - - private @NonNull String applyAreaCodeRules(@NonNull Optional localNumber, @NonNull String testNumber) { - if (!localNumber.isPresent() || !localNumber.get().getAreaCode().isPresent()) { - return testNumber; - } - - Matcher matcher; - switch (localNumber.get().getCountryCode()) { - case 1: - matcher = US_NO_AREACODE.matcher(testNumber); - if (matcher.matches()) { - return localNumber.get().getAreaCode() + matcher.group(); - } - break; - - case 55: - matcher = BR_NO_AREACODE.matcher(testNumber); - if (matcher.matches()) { - return localNumber.get().getAreaCode() + matcher.group(); - } - } - return testNumber; - } - - private static class PhoneNumber { - private final String e164Number; - private final int countryCode; - private final Optional areaCode; - - PhoneNumber(String e164Number, int countryCode, @Nullable String areaCode) { - this.e164Number = e164Number; - this.countryCode = countryCode; - this.areaCode = Optional.ofNullable(areaCode); - } - - String getE164Number() { - return e164Number; - } - - int getCountryCode() { - return countryCode; - } - - Optional getAreaCode() { - return areaCode; - } - } -} diff --git a/app/src/main/java/org/thoughtcrime/securesms/profiles/edit/EditProfileViewModel.java b/app/src/main/java/org/thoughtcrime/securesms/profiles/edit/EditProfileViewModel.java index 7738c78b4a..d0017480bd 100644 --- a/app/src/main/java/org/thoughtcrime/securesms/profiles/edit/EditProfileViewModel.java +++ b/app/src/main/java/org/thoughtcrime/securesms/profiles/edit/EditProfileViewModel.java @@ -9,17 +9,16 @@ import androidx.lifecycle.ViewModel; import androidx.lifecycle.ViewModelProvider; import org.signal.core.util.BidiUtil; +import org.signal.core.util.StringUtil; import org.signal.core.util.logging.Log; import org.thoughtcrime.securesms.conversation.colors.AvatarColor; import org.thoughtcrime.securesms.groups.GroupId; -import org.thoughtcrime.securesms.keyvalue.PhoneNumberPrivacyValues; import org.thoughtcrime.securesms.keyvalue.PhoneNumberPrivacyValues.PhoneNumberDiscoverabilityMode; import org.thoughtcrime.securesms.keyvalue.SignalStore; import org.thoughtcrime.securesms.mediasend.Media; import org.thoughtcrime.securesms.profiles.ProfileName; import org.thoughtcrime.securesms.profiles.edit.EditProfileRepository.UploadResult; import org.thoughtcrime.securesms.util.SingleLiveEvent; -import org.signal.core.util.StringUtil; import org.thoughtcrime.securesms.util.livedata.LiveDataUtil; import java.util.Arrays; diff --git a/app/src/main/java/org/thoughtcrime/securesms/profiles/manage/EditAboutFragment.java b/app/src/main/java/org/thoughtcrime/securesms/profiles/manage/EditAboutFragment.java index 20c590ee5d..8d105e0621 100644 --- a/app/src/main/java/org/thoughtcrime/securesms/profiles/manage/EditAboutFragment.java +++ b/app/src/main/java/org/thoughtcrime/securesms/profiles/manage/EditAboutFragment.java @@ -1,6 +1,5 @@ package org.thoughtcrime.securesms.profiles.manage; -import android.content.res.ColorStateList; import android.graphics.drawable.Drawable; import android.os.Bundle; import android.text.Editable; @@ -16,7 +15,6 @@ import androidx.annotation.NonNull; import androidx.annotation.Nullable; import androidx.annotation.StringRes; import androidx.appcompat.widget.Toolbar; -import androidx.core.content.ContextCompat; import androidx.fragment.app.Fragment; import androidx.lifecycle.ViewModelProvider; import androidx.navigation.Navigation; diff --git a/app/src/main/java/org/thoughtcrime/securesms/profiles/manage/EditProfileNameFragment.java b/app/src/main/java/org/thoughtcrime/securesms/profiles/manage/EditProfileNameFragment.java index c5d29a3f08..3d9fd17a33 100644 --- a/app/src/main/java/org/thoughtcrime/securesms/profiles/manage/EditProfileNameFragment.java +++ b/app/src/main/java/org/thoughtcrime/securesms/profiles/manage/EditProfileNameFragment.java @@ -18,10 +18,10 @@ import androidx.lifecycle.ViewModelProvider; import androidx.navigation.Navigation; import org.signal.core.util.EditTextUtil; +import org.signal.core.util.StringUtil; import org.thoughtcrime.securesms.R; import org.thoughtcrime.securesms.profiles.ProfileName; import org.thoughtcrime.securesms.recipients.Recipient; -import org.signal.core.util.StringUtil; import org.thoughtcrime.securesms.util.ViewUtil; import org.thoughtcrime.securesms.util.text.AfterTextChanged; import org.thoughtcrime.securesms.util.views.CircularProgressMaterialButton; diff --git a/app/src/main/java/org/thoughtcrime/securesms/profiles/manage/EditProfileNameViewModel.java b/app/src/main/java/org/thoughtcrime/securesms/profiles/manage/EditProfileNameViewModel.java index b6c71fdffd..8df3055fab 100644 --- a/app/src/main/java/org/thoughtcrime/securesms/profiles/manage/EditProfileNameViewModel.java +++ b/app/src/main/java/org/thoughtcrime/securesms/profiles/manage/EditProfileNameViewModel.java @@ -8,9 +8,9 @@ import androidx.lifecycle.MutableLiveData; import androidx.lifecycle.Transformations; import androidx.lifecycle.ViewModel; +import org.signal.core.util.StringUtil; import org.thoughtcrime.securesms.profiles.ProfileName; import org.thoughtcrime.securesms.util.SingleLiveEvent; -import org.signal.core.util.StringUtil; public final class EditProfileNameViewModel extends ViewModel { diff --git a/app/src/main/java/org/thoughtcrime/securesms/recipients/Recipient.kt b/app/src/main/java/org/thoughtcrime/securesms/recipients/Recipient.kt index b038b3bdb4..c7cc3d5a32 100644 --- a/app/src/main/java/org/thoughtcrime/securesms/recipients/Recipient.kt +++ b/app/src/main/java/org/thoughtcrime/securesms/recipients/Recipient.kt @@ -40,9 +40,9 @@ import org.thoughtcrime.securesms.groups.GroupId import org.thoughtcrime.securesms.keyvalue.SignalStore import org.thoughtcrime.securesms.notifications.NotificationChannels import org.thoughtcrime.securesms.phonenumbers.NumberUtil -import org.thoughtcrime.securesms.phonenumbers.PhoneNumberFormatter import org.thoughtcrime.securesms.profiles.ProfileName import org.thoughtcrime.securesms.service.webrtc.links.CallLinkRoomId +import org.thoughtcrime.securesms.util.SignalE164Util import org.thoughtcrime.securesms.util.UsernameUtil.isValidUsernameForSearch import org.thoughtcrime.securesms.util.Util import org.thoughtcrime.securesms.wallpaper.ChatWallpaper @@ -556,7 +556,7 @@ class Recipient( } if (name.isBlank() && e164Value.isNotNullOrBlank()) { - name = PhoneNumberFormatter.prettyPrint(e164Value) + name = SignalE164Util.prettyPrint(e164Value) } if (name.isBlank() && emailValue != null) { @@ -587,7 +587,7 @@ class Recipient( } if (name.isBlank() && e164Value.isNotNullOrBlank()) { - name = PhoneNumberFormatter.prettyPrint(e164Value) + name = SignalE164Util.prettyPrint(e164Value) } if (name.isBlank()) { @@ -901,6 +901,37 @@ class Recipient( return externalPush(serviceId, null) } + /** + * Returns a fully-populated [Recipient] based off of a ServiceId and phone number, creating one + * in the database if necessary. We want both piece of information so we're able to associate them + * both together, depending on which are available. + * + * In particular, while we may eventually get the ACI of a user created via a phone number + * (through a directory sync), the only way we can store the phone number is by retrieving it from + * sent messages and whatnot. So we should store it when available. + */ + @JvmStatic + @WorkerThread + private fun externalPush(serviceId: ServiceId, e164: String?): Recipient { + if (ACI.UNKNOWN == serviceId || PNI.UNKNOWN == serviceId) { + throw AssertionError() + } + + val recipientId = RecipientId.from(SignalServiceAddress(serviceId, e164)) + val resolved = resolved(recipientId) + + if (resolved.id != recipientId) { + Log.w(TAG, "Resolved $recipientId, but got back a recipient with ${resolved.id}") + } + + if (!resolved.isRegistered) { + Log.w(TAG, "External push was locally marked unregistered. Marking as registered.") + SignalDatabase.recipients.markRegistered(recipientId, serviceId) + } + + return resolved + } + /** * Create a recipient with a full (ACI, PNI, E164) tuple. It is assumed that the association between the PNI and serviceId is trusted. * That means it must be from either storage service (with the verified field set) or a PNI verification message. @@ -927,39 +958,6 @@ class Recipient( return resolved } - /** - * Returns a fully-populated [Recipient] based off of a ServiceId and phone number, creating one - * in the database if necessary. We want both piece of information so we're able to associate them - * both together, depending on which are available. - * - * In particular, while we may eventually get the ACI of a user created via a phone number - * (through a directory sync), the only way we can store the phone number is by retrieving it from - * sent messages and whatnot. So we should store it when available. - */ - @JvmStatic - @WorkerThread - fun externalPush(serviceId: ServiceId?, e164: String?): Recipient { - if (ACI.UNKNOWN == serviceId || PNI.UNKNOWN == serviceId) { - throw AssertionError() - } - - val recipientId = RecipientId.from(SignalServiceAddress(serviceId, e164)) - val resolved = resolved(recipientId) - - if (resolved.id != recipientId) { - Log.w(TAG, "Resolved $recipientId, but got back a recipient with ${resolved.id}") - } - - if (!resolved.isRegistered && serviceId != null) { - Log.w(TAG, "External push was locally marked unregistered. Marking as registered.") - SignalDatabase.recipients.markRegistered(recipientId, serviceId) - } else if (!resolved.isRegistered) { - Log.w(TAG, "External push was locally marked unregistered, but we don't have an ACI, so we can't do anything.", Throwable()) - } - - return resolved - } - /** * A safety wrapper around [.external] for when you know you're using an * identifier for a system contact, and therefore always want to prevent interpreting it as a @@ -969,13 +967,14 @@ class Recipient( */ @JvmStatic @WorkerThread - fun externalContact(identifier: String): Recipient { + fun externalContact(identifier: String): Recipient? { val id: RecipientId = if (UuidUtil.isUuid(identifier)) { throw AssertionError("UUIDs are not valid system contact identifiers!") } else if (NumberUtil.isValidEmail(identifier)) { SignalDatabase.recipients.getOrInsertFromEmail(identifier) } else { - SignalDatabase.recipients.getOrInsertFromE164(identifier) + val e164 = SignalE164Util.formatAsE164(identifier) ?: return null + SignalDatabase.recipients.getOrInsertFromE164(e164) } return resolved(id) @@ -1027,10 +1026,13 @@ class Recipient( * If the identifier is a UUID of a Signal user, prefer using * [.externalPush] or its overload, as this will let us associate * the phone number with the recipient. + * + * Important: If the identifier cannot be considered a valid UUID, groupId, email, or phone number, + * this will return null. */ @JvmStatic @WorkerThread - fun external(context: Context, identifier: String): Recipient { + fun external(identifier: String): Recipient? { val serviceId = ServiceId.parseOrNull(identifier, logFailures = false) val id: RecipientId = if (serviceId != null) { @@ -1042,7 +1044,7 @@ class Recipient( } else if (isValidUsernameForSearch(identifier)) { throw IllegalArgumentException("Creating a recipient based on username alone is not supported!") } else { - val e164 = PhoneNumberFormatter.get(context).format(identifier) + val e164: String = SignalE164Util.formatAsE164(identifier) ?: return null SignalDatabase.recipients.getOrInsertFromE164(e164) } diff --git a/app/src/main/java/org/thoughtcrime/securesms/recipients/RecipientRepository.kt b/app/src/main/java/org/thoughtcrime/securesms/recipients/RecipientRepository.kt index db0f1e0caf..b119d42561 100644 --- a/app/src/main/java/org/thoughtcrime/securesms/recipients/RecipientRepository.kt +++ b/app/src/main/java/org/thoughtcrime/securesms/recipients/RecipientRepository.kt @@ -5,13 +5,12 @@ package org.thoughtcrime.securesms.recipients -import android.content.Context import androidx.annotation.WorkerThread import org.signal.core.util.logging.Log import org.thoughtcrime.securesms.contacts.sync.ContactDiscovery import org.thoughtcrime.securesms.database.SignalDatabase import org.thoughtcrime.securesms.phonenumbers.NumberUtil -import org.thoughtcrime.securesms.phonenumbers.PhoneNumberFormatter +import org.thoughtcrime.securesms.util.SignalE164Util import java.io.IOException /** @@ -29,10 +28,10 @@ object RecipientRepository { */ @WorkerThread @JvmStatic - fun lookupNewE164(context: Context, inputE164: String): LookupResult { - val e164 = PhoneNumberFormatter.get(context).format(inputE164) + fun lookupNewE164(inputE164: String): LookupResult { + val e164 = SignalE164Util.formatAsE164(inputE164) - if (!NumberUtil.isVisuallyValidNumber(e164)) { + if (e164 == null || !NumberUtil.isVisuallyValidNumber(e164)) { return LookupResult.InvalidEntry } diff --git a/app/src/main/java/org/thoughtcrime/securesms/recipients/ui/about/AboutSheet.kt b/app/src/main/java/org/thoughtcrime/securesms/recipients/ui/about/AboutSheet.kt index 51d1177bf1..0b3e3fc166 100644 --- a/app/src/main/java/org/thoughtcrime/securesms/recipients/ui/about/AboutSheet.kt +++ b/app/src/main/java/org/thoughtcrime/securesms/recipients/ui/about/AboutSheet.kt @@ -51,10 +51,10 @@ import org.thoughtcrime.securesms.components.emoji.EmojiTextView import org.thoughtcrime.securesms.compose.ComposeBottomSheetDialogFragment import org.thoughtcrime.securesms.conversation.v2.UnverifiedProfileNameBottomSheet import org.thoughtcrime.securesms.nicknames.ViewNoteSheet -import org.thoughtcrime.securesms.phonenumbers.PhoneNumberFormatter import org.thoughtcrime.securesms.recipients.Recipient import org.thoughtcrime.securesms.recipients.RecipientId import org.thoughtcrime.securesms.stories.settings.my.SignalConnectionsBottomSheetDialogFragment +import org.thoughtcrime.securesms.util.SignalE164Util import org.thoughtcrime.securesms.util.viewModel /** @@ -103,7 +103,7 @@ class AboutSheet : ComposeBottomSheetDialogFragment() { hasAvatar = recipient.get().profileAvatarFileDetails.hasFile(), recipientForAvatar = recipient.get(), formattedE164 = if (recipient.get().hasE164 && recipient.get().shouldShowE164) { - PhoneNumberFormatter.get(requireContext()).prettyPrintFormat(recipient.get().requireE164()) + SignalE164Util.prettyPrint(recipient.get().requireE164()) } else { null }, diff --git a/app/src/main/java/org/thoughtcrime/securesms/recipients/ui/findby/FindByActivity.kt b/app/src/main/java/org/thoughtcrime/securesms/recipients/ui/findby/FindByActivity.kt index 983b9811bb..20fd9c52d3 100644 --- a/app/src/main/java/org/thoughtcrime/securesms/recipients/ui/findby/FindByActivity.kt +++ b/app/src/main/java/org/thoughtcrime/securesms/recipients/ui/findby/FindByActivity.kt @@ -68,6 +68,7 @@ import org.signal.core.ui.Previews import org.signal.core.ui.Scaffolds import org.signal.core.ui.TextFields import org.signal.core.ui.theme.SignalTheme +import org.signal.core.util.E164Util import org.signal.core.util.getParcelableExtraCompat import org.thoughtcrime.securesms.PassphraseRequiredActivity import org.thoughtcrime.securesms.R @@ -80,7 +81,6 @@ import org.thoughtcrime.securesms.registration.ui.countrycode.CountryCodeSelectS import org.thoughtcrime.securesms.registration.ui.countrycode.CountryCodeState import org.thoughtcrime.securesms.util.DynamicNoActionBarTheme import org.thoughtcrime.securesms.util.viewModel -import org.whispersystems.signalservice.api.util.PhoneNumberFormatter import org.signal.core.ui.R as CoreUiR /** @@ -193,9 +193,9 @@ class FindByActivity : PassphraseRequiredActivity() { } else { val formattedNumber = remember(state.userEntry) { val cleansed = state.userEntry.removePrefix(state.selectedCountry.countryCode.toString()) - PhoneNumberFormatter.formatE164(state.selectedCountry.countryCode.toString(), cleansed) + E164Util.formatAsE164WithCountryCodeForDisplay(state.selectedCountry.countryCode.toString(), cleansed) } - stringResource(id = R.string.FindByActivity__s_is_not_a_valid_phone_number, formattedNumber) + stringResource(id = R.string.FindByActivity__s_is_not_a_valid_phone_number, state.userEntry) } Dialogs.SimpleAlertDialog( @@ -232,7 +232,7 @@ class FindByActivity : PassphraseRequiredActivity() { } else { val formattedNumber = remember(state.userEntry) { val cleansed = state.userEntry.removePrefix(state.selectedCountry.countryCode.toString()) - PhoneNumberFormatter.formatE164(state.selectedCountry.countryCode.toString(), cleansed) + E164Util.formatAsE164WithCountryCodeForDisplay(state.selectedCountry.countryCode.toString(), cleansed) } stringResource(id = R.string.FindByActivity__s_is_not_a_signal_user_would, formattedNumber) } diff --git a/app/src/main/java/org/thoughtcrime/securesms/recipients/ui/findby/FindByViewModel.kt b/app/src/main/java/org/thoughtcrime/securesms/recipients/ui/findby/FindByViewModel.kt index 418e0eee93..a29ac52a5f 100644 --- a/app/src/main/java/org/thoughtcrime/securesms/recipients/ui/findby/FindByViewModel.kt +++ b/app/src/main/java/org/thoughtcrime/securesms/recipients/ui/findby/FindByViewModel.kt @@ -81,7 +81,7 @@ class FindByViewModel( val e164 = "+$countryCode$nationalNumber" - return when (val result = RecipientRepository.lookupNewE164(context, e164)) { + return when (val result = RecipientRepository.lookupNewE164(e164)) { RecipientRepository.LookupResult.InvalidEntry -> FindByResult.InvalidEntry RecipientRepository.LookupResult.NetworkError -> FindByResult.NetworkError is RecipientRepository.LookupResult.NotFound -> FindByResult.NotFound(result.recipientId) diff --git a/app/src/main/java/org/thoughtcrime/securesms/registration/ui/countrycode/CountryUtils.kt b/app/src/main/java/org/thoughtcrime/securesms/registration/ui/countrycode/CountryUtils.kt index 858dc8cda3..1dd70dbf19 100644 --- a/app/src/main/java/org/thoughtcrime/securesms/registration/ui/countrycode/CountryUtils.kt +++ b/app/src/main/java/org/thoughtcrime/securesms/registration/ui/countrycode/CountryUtils.kt @@ -6,7 +6,7 @@ package org.thoughtcrime.securesms.registration.ui.countrycode import com.google.i18n.phonenumbers.PhoneNumberUtil -import org.whispersystems.signalservice.api.util.PhoneNumberFormatter +import org.signal.core.util.E164Util import java.text.Collator import java.util.Locale @@ -25,7 +25,7 @@ object CountryUtils { return PhoneNumberUtil.getInstance().supportedRegions .map { region -> Country( - name = PhoneNumberFormatter.getRegionDisplayName(region).orElse(""), + name = E164Util.getRegionDisplayName(region).orElse(""), emoji = countryToEmoji(region), countryCode = PhoneNumberUtil.getInstance().getCountryCodeForRegion(region), regionCode = region @@ -39,7 +39,7 @@ object CountryUtils { return COMMON_COUNTRIES .map { region -> Country( - name = PhoneNumberFormatter.getRegionDisplayName(region).orElse(""), + name = E164Util.getRegionDisplayName(region).orElse(""), emoji = countryToEmoji(region), countryCode = PhoneNumberUtil.getInstance().getCountryCodeForRegion(region), regionCode = region diff --git a/app/src/main/java/org/thoughtcrime/securesms/registration/ui/phonenumber/EnterPhoneNumberFragment.kt b/app/src/main/java/org/thoughtcrime/securesms/registration/ui/phonenumber/EnterPhoneNumberFragment.kt index e0899135fb..45a2f68397 100644 --- a/app/src/main/java/org/thoughtcrime/securesms/registration/ui/phonenumber/EnterPhoneNumberFragment.kt +++ b/app/src/main/java/org/thoughtcrime/securesms/registration/ui/phonenumber/EnterPhoneNumberFragment.kt @@ -45,7 +45,6 @@ import org.thoughtcrime.securesms.R import org.thoughtcrime.securesms.components.ViewBinderDelegate import org.thoughtcrime.securesms.databinding.FragmentRegistrationEnterPhoneNumberBinding import org.thoughtcrime.securesms.dependencies.AppDependencies -import org.thoughtcrime.securesms.phonenumbers.PhoneNumberFormatter import org.thoughtcrime.securesms.registration.data.RegistrationRepository import org.thoughtcrime.securesms.registration.data.network.Challenge import org.thoughtcrime.securesms.registration.data.network.RegisterAccountResult @@ -65,6 +64,7 @@ import org.thoughtcrime.securesms.registration.util.CountryPrefix import org.thoughtcrime.securesms.util.CommunicationActions import org.thoughtcrime.securesms.util.Dialogs import org.thoughtcrime.securesms.util.PlayServicesUtil +import org.thoughtcrime.securesms.util.SignalE164Util import org.thoughtcrime.securesms.util.SpanUtil import org.thoughtcrime.securesms.util.SupportEmailUtil import org.thoughtcrime.securesms.util.ViewUtil @@ -619,7 +619,7 @@ class EnterPhoneNumberFragment : LoggingFragment(R.layout.fragment_registration_ } val message: CharSequence = SpannableStringBuilder().apply { - append(SpanUtil.bold(PhoneNumberFormatter.prettyPrint(phoneNumber.toE164()))) + append(SpanUtil.bold(SignalE164Util.prettyPrint(phoneNumber.toE164()))) if (!canSkipSms) { append("\n\n") append(getString(R.string.RegistrationActivity_a_verification_code_will_be_sent_to_this_number)) diff --git a/app/src/main/java/org/thoughtcrime/securesms/registration/ui/phonenumber/EnterPhoneNumberViewModel.kt b/app/src/main/java/org/thoughtcrime/securesms/registration/ui/phonenumber/EnterPhoneNumberViewModel.kt index a9ba4e4ad9..5c5090d5e8 100644 --- a/app/src/main/java/org/thoughtcrime/securesms/registration/ui/phonenumber/EnterPhoneNumberViewModel.kt +++ b/app/src/main/java/org/thoughtcrime/securesms/registration/ui/phonenumber/EnterPhoneNumberViewModel.kt @@ -13,13 +13,13 @@ import com.google.i18n.phonenumbers.PhoneNumberUtil import com.google.i18n.phonenumbers.Phonenumber.PhoneNumber import kotlinx.coroutines.flow.MutableStateFlow import kotlinx.coroutines.flow.update +import org.signal.core.util.E164Util import org.signal.core.util.logging.Log import org.thoughtcrime.securesms.registration.data.RegistrationRepository import org.thoughtcrime.securesms.registration.ui.countrycode.Country import org.thoughtcrime.securesms.registration.ui.countrycode.CountryUtils import org.thoughtcrime.securesms.registration.util.CountryPrefix import org.thoughtcrime.securesms.util.Util -import org.whispersystems.signalservice.api.util.PhoneNumberFormatter /** * ViewModel for the phone number entry screen. @@ -74,7 +74,7 @@ class EnterPhoneNumberViewModel : ViewModel() { countryPrefixIndex = prefixIndex, phoneNumberRegionCode = regionCode, country = existingCountry ?: Country( - name = PhoneNumberFormatter.getRegionDisplayName(regionCode).orElse(""), + name = E164Util.getRegionDisplayName(regionCode).orElse(""), emoji = CountryUtils.countryToEmoji(regionCode), countryCode = countryCode, regionCode = regionCode @@ -122,7 +122,7 @@ class EnterPhoneNumberViewModel : ViewModel() { val regionCode = supportedCountryPrefixes[matchingIndex].regionCode val matchedCountry = Country( - name = PhoneNumberFormatter.getRegionDisplayName(regionCode).orElse(""), + name = E164Util.getRegionDisplayName(regionCode).orElse(""), emoji = CountryUtils.countryToEmoji(regionCode), countryCode = digits, regionCode = regionCode diff --git a/app/src/main/java/org/thoughtcrime/securesms/registration/util/ChangeNumberInputController.java b/app/src/main/java/org/thoughtcrime/securesms/registration/util/ChangeNumberInputController.java index ea6b1eefbc..7d36d1985a 100644 --- a/app/src/main/java/org/thoughtcrime/securesms/registration/util/ChangeNumberInputController.java +++ b/app/src/main/java/org/thoughtcrime/securesms/registration/util/ChangeNumberInputController.java @@ -19,12 +19,12 @@ import androidx.annotation.Nullable; import com.google.i18n.phonenumbers.AsYouTypeFormatter; import com.google.i18n.phonenumbers.PhoneNumberUtil; +import org.signal.core.util.E164Util; import org.thoughtcrime.securesms.R; import org.thoughtcrime.securesms.components.LabeledEditText; import org.thoughtcrime.securesms.registration.ui.countrycode.Country; import org.thoughtcrime.securesms.registration.ui.countrycode.CountryUtils; import org.thoughtcrime.securesms.registration.viewmodel.NumberViewState; -import org.whispersystems.signalservice.api.util.PhoneNumberFormatter; /** * Handle the logic and formatting of phone number input specifically for change number flows. @@ -248,7 +248,7 @@ public final class ChangeNumberInputController { } if (!isUpdating) { - Country country = new Country(CountryUtils.countryToEmoji(regionCode), PhoneNumberFormatter.getRegionDisplayName(regionCode).orElse(""), countryCode, regionCode); + Country country = new Country(CountryUtils.countryToEmoji(regionCode), E164Util.getRegionDisplayName(regionCode).orElse(""), countryCode, regionCode); callbacks.setCountry(country); } } diff --git a/app/src/main/java/org/thoughtcrime/securesms/registration/viewmodel/NumberViewState.java b/app/src/main/java/org/thoughtcrime/securesms/registration/viewmodel/NumberViewState.java index 2f59fbcbda..09d2ffcd23 100644 --- a/app/src/main/java/org/thoughtcrime/securesms/registration/viewmodel/NumberViewState.java +++ b/app/src/main/java/org/thoughtcrime/securesms/registration/viewmodel/NumberViewState.java @@ -10,7 +10,7 @@ import com.google.i18n.phonenumbers.NumberParseException; import com.google.i18n.phonenumbers.PhoneNumberUtil; import com.google.i18n.phonenumbers.Phonenumber; -import org.whispersystems.signalservice.api.util.PhoneNumberFormatter; +import org.signal.core.util.E164Util; import java.util.Objects; @@ -42,7 +42,7 @@ public final class NumberViewState implements Parcelable { return nationalNumber; } - public String getCountryDisplayName() { + public @Nullable String getCountryDisplayName() { if (selectedCountryName != null) { return selectedCountryName; } @@ -58,7 +58,7 @@ public final class NumberViewState implements Parcelable { } String regionCode = util.getRegionCodeForCountryCode(countryCode); - return PhoneNumberFormatter.getRegionDisplayNameLegacy(regionCode); + return E164Util.getRegionDisplayName(regionCode).orElse(null); } /** @@ -70,7 +70,7 @@ public final class NumberViewState implements Parcelable { String regionCode = util.getRegionCodeForNumber(phoneNumber); if (regionCode != null) { - return PhoneNumberFormatter.getRegionDisplayNameLegacy(regionCode); + return E164Util.getRegionDisplayName(regionCode).orElse(null); } } catch (NumberParseException e) { @@ -80,7 +80,7 @@ public final class NumberViewState implements Parcelable { } public boolean isValid() { - return PhoneNumberFormatter.isValidNumber(getE164Number(), Integer.toString(getCountryCode())); + return E164Util.isValidNumberForRegistration(Integer.toString(getCountryCode()), getE164Number()); } @Override @@ -123,7 +123,7 @@ public final class NumberViewState implements Parcelable { } private static String getConfiguredE164Number(int countryCode, String number) { - return PhoneNumberFormatter.formatE164(String.valueOf(countryCode), number); + return E164Util.formatAsE164WithCountryCodeForDisplay(String.valueOf(countryCode), number); } private static Phonenumber.PhoneNumber getPhoneNumber(@NonNull PhoneNumberUtil util, @NonNull String e164Number) diff --git a/app/src/main/java/org/thoughtcrime/securesms/registrationv3/ui/phonenumber/EnterPhoneNumberFragment.kt b/app/src/main/java/org/thoughtcrime/securesms/registrationv3/ui/phonenumber/EnterPhoneNumberFragment.kt index 07cfce10e6..68eeace0e4 100644 --- a/app/src/main/java/org/thoughtcrime/securesms/registrationv3/ui/phonenumber/EnterPhoneNumberFragment.kt +++ b/app/src/main/java/org/thoughtcrime/securesms/registrationv3/ui/phonenumber/EnterPhoneNumberFragment.kt @@ -46,7 +46,6 @@ import org.thoughtcrime.securesms.R import org.thoughtcrime.securesms.components.ViewBinderDelegate import org.thoughtcrime.securesms.databinding.FragmentRegistrationEnterPhoneNumberBinding import org.thoughtcrime.securesms.dependencies.AppDependencies -import org.thoughtcrime.securesms.phonenumbers.PhoneNumberFormatter import org.thoughtcrime.securesms.registration.data.network.Challenge import org.thoughtcrime.securesms.registration.data.network.RegisterAccountResult import org.thoughtcrime.securesms.registration.data.network.RegistrationResult @@ -66,6 +65,7 @@ import org.thoughtcrime.securesms.registrationv3.ui.RegistrationViewModel import org.thoughtcrime.securesms.util.CommunicationActions import org.thoughtcrime.securesms.util.Dialogs import org.thoughtcrime.securesms.util.PlayServicesUtil +import org.thoughtcrime.securesms.util.SignalE164Util import org.thoughtcrime.securesms.util.SpanUtil import org.thoughtcrime.securesms.util.SupportEmailUtil import org.thoughtcrime.securesms.util.ViewUtil @@ -644,7 +644,7 @@ class EnterPhoneNumberFragment : LoggingFragment(R.layout.fragment_registration_ } val message: CharSequence = SpannableStringBuilder().apply { - append(SpanUtil.bold(PhoneNumberFormatter.prettyPrint(phoneNumber.toE164()))) + append(SpanUtil.bold(SignalE164Util.prettyPrint(phoneNumber.toE164()))) if (!canSkipSms) { append("\n\n") append(getString(R.string.RegistrationActivity_a_verification_code_will_be_sent_to_this_number)) diff --git a/app/src/main/java/org/thoughtcrime/securesms/registrationv3/ui/phonenumber/EnterPhoneNumberViewModel.kt b/app/src/main/java/org/thoughtcrime/securesms/registrationv3/ui/phonenumber/EnterPhoneNumberViewModel.kt index b0ac68ce2a..476d931d6a 100644 --- a/app/src/main/java/org/thoughtcrime/securesms/registrationv3/ui/phonenumber/EnterPhoneNumberViewModel.kt +++ b/app/src/main/java/org/thoughtcrime/securesms/registrationv3/ui/phonenumber/EnterPhoneNumberViewModel.kt @@ -13,13 +13,13 @@ import com.google.i18n.phonenumbers.PhoneNumberUtil import com.google.i18n.phonenumbers.Phonenumber.PhoneNumber import kotlinx.coroutines.flow.MutableStateFlow import kotlinx.coroutines.flow.update +import org.signal.core.util.E164Util import org.signal.core.util.logging.Log import org.thoughtcrime.securesms.registration.ui.countrycode.Country import org.thoughtcrime.securesms.registration.ui.countrycode.CountryUtils import org.thoughtcrime.securesms.registration.util.CountryPrefix import org.thoughtcrime.securesms.registrationv3.data.RegistrationRepository import org.thoughtcrime.securesms.util.Util -import org.whispersystems.signalservice.api.util.PhoneNumberFormatter /** * ViewModel for the phone number entry screen. @@ -74,7 +74,7 @@ class EnterPhoneNumberViewModel : ViewModel() { countryPrefixIndex = prefixIndex, phoneNumberRegionCode = regionCode, country = existingCountry ?: Country( - name = PhoneNumberFormatter.getRegionDisplayName(regionCode).orElse(""), + name = E164Util.getRegionDisplayName(regionCode).orElse(""), emoji = CountryUtils.countryToEmoji(regionCode), countryCode = countryCode, regionCode = regionCode @@ -122,7 +122,7 @@ class EnterPhoneNumberViewModel : ViewModel() { val regionCode = supportedCountryPrefixes[matchingIndex].regionCode val matchedCountry = Country( - name = PhoneNumberFormatter.getRegionDisplayName(regionCode).orElse(""), + name = E164Util.getRegionDisplayName(regionCode).orElse(""), emoji = CountryUtils.countryToEmoji(regionCode), countryCode = digits, regionCode = regionCode diff --git a/app/src/main/java/org/thoughtcrime/securesms/stories/settings/select/BaseStoryRecipientSelectionFragment.kt b/app/src/main/java/org/thoughtcrime/securesms/stories/settings/select/BaseStoryRecipientSelectionFragment.kt index e36220d528..7feb13e1f4 100644 --- a/app/src/main/java/org/thoughtcrime/securesms/stories/settings/select/BaseStoryRecipientSelectionFragment.kt +++ b/app/src/main/java/org/thoughtcrime/securesms/stories/settings/select/BaseStoryRecipientSelectionFragment.kt @@ -92,7 +92,7 @@ abstract class BaseStoryRecipientSelectionFragment : Fragment(R.layout.stories_b when (action) { is BaseStoryRecipientSelectionViewModel.Action.ExitFlow -> exitFlow() is BaseStoryRecipientSelectionViewModel.Action.GoToNextScreen -> goToNextScreen( - getAttachedContactSelectionFragment().selectedContacts.map { it.getOrCreateRecipientId(requireContext()) }.toSet() + getAttachedContactSelectionFragment().selectedContacts.map { it.getOrCreateRecipientId() }.toSet() ) } } diff --git a/app/src/main/java/org/thoughtcrime/securesms/util/CommunicationActions.java b/app/src/main/java/org/thoughtcrime/securesms/util/CommunicationActions.java index f6b005dcc4..059e4c3350 100644 --- a/app/src/main/java/org/thoughtcrime/securesms/util/CommunicationActions.java +++ b/app/src/main/java/org/thoughtcrime/securesms/util/CommunicationActions.java @@ -310,7 +310,7 @@ public class CommunicationActions { * If the url is a signal.me link it will handle it. */ public static void handlePotentialSignalMeUrl(@NonNull FragmentActivity activity, @NonNull String potentialUrl) { - String e164 = SignalMeUtil.parseE164FromLink(activity, potentialUrl); + String e164 = SignalMeUtil.parseE164FromLink(potentialUrl); UsernameLinkComponents username = UsernameRepository.parseLink(potentialUrl); if (e164 != null) { @@ -426,7 +426,10 @@ public class CommunicationActions { SimpleProgressDialog.DismissibleDialog dialog = SimpleProgressDialog.showDelayed(activity, 500, 500); SimpleTask.run(() -> { - Recipient recipient = Recipient.external(activity, e164); + Recipient recipient = Recipient.external(e164); + if (recipient == null) { + return null; + } if (!recipient.isRegistered() || !recipient.getHasServiceId()) { try { @@ -441,7 +444,7 @@ public class CommunicationActions { }, recipient -> { dialog.dismiss(); - if (recipient.isRegistered() && recipient.getHasServiceId()) { + if (recipient != null && recipient.isRegistered() && recipient.getHasServiceId()) { startConversation(activity, recipient, null); } else { new MaterialAlertDialogBuilder(activity) diff --git a/app/src/main/java/org/thoughtcrime/securesms/util/GroupUtil.java b/app/src/main/java/org/thoughtcrime/securesms/util/GroupUtil.java index f76f85eab5..18f0e96acf 100644 --- a/app/src/main/java/org/thoughtcrime/securesms/util/GroupUtil.java +++ b/app/src/main/java/org/thoughtcrime/securesms/util/GroupUtil.java @@ -7,7 +7,6 @@ import androidx.annotation.Nullable; import androidx.annotation.WorkerThread; import org.signal.core.util.BidiUtil; -import org.signal.core.util.StringUtil; import org.signal.core.util.logging.Log; import org.signal.libsignal.zkgroup.InvalidInputException; import org.signal.libsignal.zkgroup.groups.GroupMasterKey; diff --git a/app/src/main/java/org/thoughtcrime/securesms/util/SignalE164Util.kt b/app/src/main/java/org/thoughtcrime/securesms/util/SignalE164Util.kt new file mode 100644 index 0000000000..72709e73a8 --- /dev/null +++ b/app/src/main/java/org/thoughtcrime/securesms/util/SignalE164Util.kt @@ -0,0 +1,74 @@ +/* + * Copyright 2025 Signal Messenger, LLC + * SPDX-License-Identifier: AGPL-3.0-only + */ + +package org.thoughtcrime.securesms.util + +import org.signal.core.util.E164Util +import org.thoughtcrime.securesms.dependencies.AppDependencies +import org.thoughtcrime.securesms.keyvalue.SignalStore + +/** + * A wrapper around [E164Util] that automatically handles fetching our own number and caching formatters. + */ +object SignalE164Util { + + private val cachedFormatters: MutableMap = LRUCache(2) + private val defaultFormatter: E164Util.Formatter by lazy { + E164Util.Formatter( + localNumber = null, + localAreaCode = null, + localRegionCode = Util.getSimCountryIso(AppDependencies.application).orElse("US") + ) + } + + /** + * Formats the number for human-readable display. e.g. "(555) 555-5555" + */ + @JvmStatic + fun prettyPrint(input: String): String { + return getFormatter().prettyPrint(input) + } + + /** + * Returns the country code for the local number, if present. Otherwise, it returns 0. + */ + fun getLocalCountryCode(): Int { + return getFormatter().localNumber?.countryCode ?: 0 + } + + /** + * Formats the number as an E164, or null if the number cannot be reasonably interpreted as a phone number. + * This does not check if the number is *valid* for a given region. Instead, it's very lenient and just + * does it's best to interpret the input string as a number that could be put into the E164 format. + * + * Note that shortcodes will not have leading '+' signs. + * + * In other words, if this method returns null, you likely do not have anything that could be considered + * a phone number. + */ + @JvmStatic + fun formatAsE164(input: String): String? { + return getFormatter().formatAsE164(input) + } + + private fun getFormatter(): E164Util.Formatter { + val localNumber = SignalStore.account.e164 ?: return defaultFormatter + val formatter = cachedFormatters[localNumber] + if (formatter != null) { + return formatter + } + + synchronized(cachedFormatters) { + val formatter = cachedFormatters[localNumber] + if (formatter != null) { + return formatter + } + + val newFormatter = E164Util.createFormatterForE164(localNumber) + cachedFormatters[localNumber] = newFormatter + return newFormatter + } + } +} diff --git a/app/src/main/java/org/thoughtcrime/securesms/util/SignalMeUtil.kt b/app/src/main/java/org/thoughtcrime/securesms/util/SignalMeUtil.kt index 53f3f6d6d4..7fba8d4b9f 100644 --- a/app/src/main/java/org/thoughtcrime/securesms/util/SignalMeUtil.kt +++ b/app/src/main/java/org/thoughtcrime/securesms/util/SignalMeUtil.kt @@ -1,8 +1,6 @@ package org.thoughtcrime.securesms.util -import android.content.Context import com.google.i18n.phonenumbers.PhoneNumberUtil -import org.thoughtcrime.securesms.phonenumbers.PhoneNumberFormatter import java.util.Locale internal object SignalMeUtil { @@ -12,7 +10,7 @@ internal object SignalMeUtil { * If this is a valid signal.me link and has a valid e164, it will return the e164. Otherwise, it will return null. */ @JvmStatic - fun parseE164FromLink(context: Context, link: String?): String? { + fun parseE164FromLink(link: String?): String? { if (link.isNullOrBlank()) { return null } @@ -21,7 +19,7 @@ internal object SignalMeUtil { val e164: String = match.groups[2]?.value ?: return@let null if (PhoneNumberUtil.getInstance().isPossibleNumber(e164, Locale.getDefault().country)) { - PhoneNumberFormatter.get(context).format(e164) + SignalE164Util.formatAsE164(e164) } else { null } diff --git a/app/src/main/java/org/thoughtcrime/securesms/webrtc/VoiceCallShare.java b/app/src/main/java/org/thoughtcrime/securesms/webrtc/VoiceCallShare.java index 821d9d5146..42b0c7bd80 100644 --- a/app/src/main/java/org/thoughtcrime/securesms/webrtc/VoiceCallShare.java +++ b/app/src/main/java/org/thoughtcrime/securesms/webrtc/VoiceCallShare.java @@ -32,8 +32,8 @@ public class VoiceCallShare extends Activity { if (cursor != null && cursor.moveToNext()) { String destination = cursor.getString(cursor.getColumnIndexOrThrow(ContactsContract.RawContacts.Data.DATA1)); - SimpleTask.run(() -> Recipient.external(this, destination), recipient -> { - if (!TextUtils.isEmpty(destination)) { + SimpleTask.run(() -> Recipient.external(destination), recipient -> { + if (recipient != null && !TextUtils.isEmpty(destination)) { if (VIDEO_CALL_MIME_TYPE.equals(getIntent().getType())) { AppDependencies.getSignalCallManager().startOutgoingVideoCall(recipient); } else { diff --git a/app/src/test/java/org/thoughtcrime/securesms/jobmanager/migrations/RecipientIdFollowUpJobMigrationTest.kt b/app/src/test/java/org/thoughtcrime/securesms/jobmanager/migrations/RecipientIdFollowUpJobMigrationTest.kt deleted file mode 100644 index 6899352352..0000000000 --- a/app/src/test/java/org/thoughtcrime/securesms/jobmanager/migrations/RecipientIdFollowUpJobMigrationTest.kt +++ /dev/null @@ -1,57 +0,0 @@ -package org.thoughtcrime.securesms.jobmanager.migrations - -import org.junit.Assert.assertEquals -import org.junit.Assert.assertNull -import org.junit.Test -import org.thoughtcrime.securesms.jobmanager.Job -import org.thoughtcrime.securesms.jobmanager.JobMigration.JobData -import org.thoughtcrime.securesms.jobmanager.JsonJobData -import org.thoughtcrime.securesms.jobs.FailingJob -import org.thoughtcrime.securesms.jobs.SendDeliveryReceiptJob - -class RecipientIdFollowUpJobMigrationTest { - @Test - fun migrate_sendDeliveryReceiptJob_good() { - val testData = JobData( - "SendDeliveryReceiptJob", - null, - -1, - -1, - JsonJobData.Builder().putString("recipient", "1") - .putLong("message_id", 1) - .putLong("timestamp", 2) - .serialize() - ) - val subject = RecipientIdFollowUpJobMigration() - val converted = subject.migrate(testData) - - assertEquals("SendDeliveryReceiptJob", converted.factoryKey) - assertNull(converted.queueKey) - - val data = JsonJobData.deserialize(converted.data) - assertEquals("1", data.getString("recipient")) - assertEquals(1, data.getLong("message_id")) - assertEquals(2, data.getLong("timestamp")) - - SendDeliveryReceiptJob.Factory().create(Job.Parameters.Builder().build(), converted.data) - } - - @Test - fun migrate_sendDeliveryReceiptJob_bad() { - val testData = JobData( - "SendDeliveryReceiptJob", - null, - -1, - -1, - JsonJobData.Builder().putString("recipient", "1") - .serialize() - ) - val subject = RecipientIdFollowUpJobMigration() - val converted = subject.migrate(testData) - - assertEquals("FailingJob", converted.factoryKey) - assertNull(converted.queueKey) - - FailingJob.Factory().create(Job.Parameters.Builder().build(), converted.data) - } -} diff --git a/app/src/test/java/org/thoughtcrime/securesms/jobmanager/migrations/RecipientIdJobMigrationTest.kt b/app/src/test/java/org/thoughtcrime/securesms/jobmanager/migrations/RecipientIdJobMigrationTest.kt deleted file mode 100644 index c3d9692d9c..0000000000 --- a/app/src/test/java/org/thoughtcrime/securesms/jobmanager/migrations/RecipientIdJobMigrationTest.kt +++ /dev/null @@ -1,365 +0,0 @@ -package org.thoughtcrime.securesms.jobmanager.migrations - -import android.app.Application -import io.mockk.every -import io.mockk.mockk -import io.mockk.mockkObject -import io.mockk.unmockkAll -import org.junit.After -import org.junit.Assert.assertEquals -import org.junit.Assert.assertFalse -import org.junit.Assert.assertNull -import org.junit.Assert.assertTrue -import org.junit.Before -import org.junit.Test -import org.thoughtcrime.securesms.jobmanager.Job -import org.thoughtcrime.securesms.jobmanager.JobMigration.JobData -import org.thoughtcrime.securesms.jobmanager.JsonJobData -import org.thoughtcrime.securesms.jobmanager.migrations.RecipientIdJobMigration.NewSerializableSyncMessageId -import org.thoughtcrime.securesms.jobmanager.migrations.RecipientIdJobMigration.OldSerializableSyncMessageId -import org.thoughtcrime.securesms.jobs.DirectoryRefreshJob -import org.thoughtcrime.securesms.jobs.IndividualSendJob -import org.thoughtcrime.securesms.jobs.MultiDeviceContactUpdateJob -import org.thoughtcrime.securesms.jobs.MultiDeviceReadUpdateJob -import org.thoughtcrime.securesms.jobs.MultiDeviceVerifiedUpdateJob -import org.thoughtcrime.securesms.jobs.MultiDeviceViewOnceOpenJob -import org.thoughtcrime.securesms.jobs.PushGroupSendJob -import org.thoughtcrime.securesms.jobs.RetrieveProfileAvatarJob -import org.thoughtcrime.securesms.jobs.SendDeliveryReceiptJob -import org.thoughtcrime.securesms.recipients.LiveRecipient -import org.thoughtcrime.securesms.recipients.Recipient -import org.thoughtcrime.securesms.recipients.RecipientId -import org.thoughtcrime.securesms.util.JsonUtils - -class RecipientIdJobMigrationTest { - @Before - fun setup() { - mockkObject(Recipient) - every { Recipient.live(any()) } returns mockk(relaxed = true) - } - - @After - fun cleanup() { - unmockkAll() - } - - @Test - fun migrate_multiDeviceContactUpdateJob() { - val testData = JobData( - factoryKey = "MultiDeviceContactUpdateJob", - queueKey = "MultiDeviceContactUpdateJob", - maxAttempts = -1, - lifespan = -1, - data = JsonJobData.Builder().putBoolean("force_sync", false).putString("address", "+16101234567").serialize() - ) - mockRecipientResolve("+16101234567", 1) - - val subject = RecipientIdJobMigration(mockk()) - val converted = subject.migrate(testData) - val data = JsonJobData.deserialize(converted.data) - - assertEquals("MultiDeviceContactUpdateJob", converted.factoryKey) - assertEquals("MultiDeviceContactUpdateJob", converted.queueKey) - assertFalse(data.getBoolean("force_sync")) - assertFalse(data.hasString("address")) - assertEquals("1", data.getString("recipient")) - - MultiDeviceContactUpdateJob.Factory().create(Job.Parameters.Builder().build(), converted.data) - } - - @Test - fun migrate_multiDeviceViewOnceOpenJob() { - val oldId = OldSerializableSyncMessageId("+16101234567", 1) - val testData = JobData( - factoryKey = "MultiDeviceRevealUpdateJob", - queueKey = null, - maxAttempts = -1, - lifespan = -1, - data = JsonJobData.Builder().putString("message_id", JsonUtils.toJson(oldId)).serialize() - ) - mockRecipientResolve("+16101234567", 1) - - val subject = RecipientIdJobMigration(mockk()) - val converted = subject.migrate(testData) - val data = JsonJobData.deserialize(converted.data) - - assertEquals("MultiDeviceRevealUpdateJob", converted.factoryKey) - assertNull(converted.queueKey) - assertEquals(JsonUtils.toJson(NewSerializableSyncMessageId("1", 1)), data.getString("message_id")) - - MultiDeviceViewOnceOpenJob.Factory().create(Job.Parameters.Builder().build(), converted.data) - } - - @Test - fun migrate_sendDeliveryReceiptJob() { - val testData = JobData( - factoryKey = "SendDeliveryReceiptJob", - queueKey = null, - maxAttempts = -1, - lifespan = -1, - data = JsonJobData.Builder().putString("address", "+16101234567") - .putLong("message_id", 1) - .putLong("timestamp", 2) - .serialize() - ) - mockRecipientResolve("+16101234567", 1) - - val subject = RecipientIdJobMigration(mockk()) - val converted = subject.migrate(testData) - val data = JsonJobData.deserialize(converted.data) - - assertEquals("SendDeliveryReceiptJob", converted.factoryKey) - assertNull(converted.queueKey) - assertEquals("1", data.getString("recipient")) - assertEquals(1, data.getLong("message_id")) - assertEquals(2, data.getLong("timestamp")) - - SendDeliveryReceiptJob.Factory().create(Job.Parameters.Builder().build(), converted.data) - } - - @Test - fun migrate_multiDeviceVerifiedUpdateJob() { - val testData = JobData( - factoryKey = "MultiDeviceVerifiedUpdateJob", - queueKey = "__MULTI_DEVICE_VERIFIED_UPDATE__", - maxAttempts = -1, - lifespan = -1, - data = JsonJobData.Builder().putString("destination", "+16101234567") - .putString("identity_key", "abcd") - .putInt("verified_status", 1) - .putLong("timestamp", 123) - .serialize() - ) - mockRecipientResolve("+16101234567", 1) - - val subject = RecipientIdJobMigration(mockk()) - val converted = subject.migrate(testData) - val data = JsonJobData.deserialize(converted.data) - - assertEquals("MultiDeviceVerifiedUpdateJob", converted.factoryKey) - assertEquals("__MULTI_DEVICE_VERIFIED_UPDATE__", converted.queueKey) - assertEquals("abcd", data.getString("identity_key")) - assertEquals(1, data.getInt("verified_status")) - assertEquals(123, data.getLong("timestamp")) - assertEquals("1", data.getString("destination")) - - MultiDeviceVerifiedUpdateJob.Factory().create(Job.Parameters.Builder().build(), converted.data) - } - - @Test - fun migrate_pushGroupSendJob_null() { - val testData = JobData( - factoryKey = "PushGroupSendJob", - queueKey = "someGroupId", - maxAttempts = -1, - lifespan = -1, - data = JsonJobData.Builder().putString("filter_address", null) - .putLong("message_id", 123) - .serialize() - ) - mockRecipientResolve("someGroupId", 5) - - val subject = RecipientIdJobMigration(mockk()) - val converted = subject.migrate(testData) - val data = JsonJobData.deserialize(converted.data) - - assertEquals("PushGroupSendJob", converted.factoryKey) - assertEquals(RecipientId.from(5).toQueueKey(), converted.queueKey) - assertNull(data.getString("filter_recipient")) - assertFalse(data.hasString("filter_address")) - - PushGroupSendJob.Factory().create(Job.Parameters.Builder().build(), converted.data) - } - - @Test - fun migrate_pushGroupSendJob_nonNull() { - val testData = JobData( - factoryKey = "PushGroupSendJob", - queueKey = "someGroupId", - maxAttempts = -1, - lifespan = -1, - data = JsonJobData.Builder().putString("filter_address", "+16101234567") - .putLong("message_id", 123) - .serialize() - ) - mockRecipientResolve("+16101234567", 1) - mockRecipientResolve("someGroupId", 5) - - val subject = RecipientIdJobMigration(mockk()) - val converted = subject.migrate(testData) - val data = JsonJobData.deserialize(converted.data) - - assertEquals("PushGroupSendJob", converted.factoryKey) - assertEquals(RecipientId.from(5).toQueueKey(), converted.queueKey) - assertEquals("1", data.getString("filter_recipient")) - assertFalse(data.hasString("filter_address")) - - PushGroupSendJob.Factory().create(Job.Parameters.Builder().build(), converted.data) - } - - @Test - fun migrate_directoryRefreshJob_null() { - val testData = JobData( - factoryKey = "DirectoryRefreshJob", - queueKey = "DirectoryRefreshJob", - maxAttempts = -1, - lifespan = -1, - data = JsonJobData.Builder() - .putString("address", null) - .putBoolean("notify_of_new_users", true) - .serialize() - ) - - val subject = RecipientIdJobMigration(mockk()) - val converted = subject.migrate(testData) - val data = JsonJobData.deserialize(converted.data) - - assertEquals("DirectoryRefreshJob", converted.factoryKey) - assertEquals("DirectoryRefreshJob", converted.queueKey) - assertNull(data.getString("recipient")) - assertTrue(data.getBoolean("notify_of_new_users")) - assertFalse(data.hasString("address")) - - DirectoryRefreshJob.Factory().create(Job.Parameters.Builder().build(), converted.data) - } - - @Test - fun migrate_directoryRefreshJob_nonNull() { - val testData = JobData( - factoryKey = "DirectoryRefreshJob", - queueKey = "DirectoryRefreshJob", - maxAttempts = -1, - lifespan = -1, - data = JsonJobData.Builder() - .putString("address", "+16101234567") - .putBoolean("notify_of_new_users", true) - .serialize() - ) - mockRecipientResolve("+16101234567", 1) - - val subject = RecipientIdJobMigration(mockk()) - val converted = subject.migrate(testData) - val data = JsonJobData.deserialize(converted.data) - - assertEquals("DirectoryRefreshJob", converted.factoryKey) - assertEquals("DirectoryRefreshJob", converted.queueKey) - assertTrue(data.getBoolean("notify_of_new_users")) - assertEquals("1", data.getString("recipient")) - assertFalse(data.hasString("address")) - - DirectoryRefreshJob.Factory().create(Job.Parameters.Builder().build(), converted.data) - } - - @Test - fun migrate_retrieveProfileAvatarJob() { - val testData = JobData( - factoryKey = "RetrieveProfileAvatarJob", - queueKey = "RetrieveProfileAvatarJob+16101234567", - maxAttempts = -1, - lifespan = -1, - data = JsonJobData.Builder() - .putString("address", "+16101234567") - .putString("profile_avatar", "abc") - .serialize() - ) - mockRecipientResolve("+16101234567", 1) - - val subject = RecipientIdJobMigration(mockk()) - val converted = subject.migrate(testData) - val data = JsonJobData.deserialize(converted.data) - - assertEquals("RetrieveProfileAvatarJob", converted.factoryKey) - assertEquals("RetrieveProfileAvatarJob::" + RecipientId.from(1).toQueueKey(), converted.queueKey) - assertEquals("1", data.getString("recipient")) - - RetrieveProfileAvatarJob.Factory().create(Job.Parameters.Builder().build(), converted.data) - } - - @Test - fun migrate_multiDeviceReadUpdateJob_empty() { - val testData = JobData( - factoryKey = "MultiDeviceReadUpdateJob", - queueKey = null, - maxAttempts = -1, - lifespan = -1, - data = JsonJobData.Builder() - .putStringArray("message_ids", arrayOfNulls(0)) - .serialize() - ) - - val subject = RecipientIdJobMigration(mockk()) - val converted = subject.migrate(testData) - val data = JsonJobData.deserialize(converted.data) - - assertEquals("MultiDeviceReadUpdateJob", converted.factoryKey) - assertNull(converted.queueKey) - assertEquals(0, data.getStringArray("message_ids").size) - - MultiDeviceReadUpdateJob.Factory().create(Job.Parameters.Builder().build(), converted.data) - } - - @Test - fun migrate_multiDeviceReadUpdateJob_twoIds() { - val id1 = OldSerializableSyncMessageId("+16101234567", 1) - val id2 = OldSerializableSyncMessageId("+16101112222", 2) - - val testData = JobData( - factoryKey = "MultiDeviceReadUpdateJob", - queueKey = null, - maxAttempts = -1, - lifespan = -1, - data = JsonJobData.Builder() - .putStringArray("message_ids", arrayOf(JsonUtils.toJson(id1), JsonUtils.toJson(id2))) - .serialize() - ) - mockRecipientResolve("+16101234567", 1) - mockRecipientResolve("+16101112222", 2) - - val subject = RecipientIdJobMigration(mockk()) - val converted = subject.migrate(testData) - val data = JsonJobData.deserialize(converted.data) - - assertEquals("MultiDeviceReadUpdateJob", converted.factoryKey) - assertNull(converted.queueKey) - - val updated = data.getStringArray("message_ids") - assertEquals(2, updated.size) - - assertEquals(JsonUtils.toJson(NewSerializableSyncMessageId("1", 1)), updated[0]) - assertEquals(JsonUtils.toJson(NewSerializableSyncMessageId("2", 2)), updated[1]) - - MultiDeviceReadUpdateJob.Factory().create(Job.Parameters.Builder().build(), converted.data) - } - - @Test - fun migrate_pushMediaSendJob() { - val testData = JobData( - factoryKey = "PushMediaSendJob", - queueKey = "+16101234567", - maxAttempts = -1, - lifespan = -1, - data = JsonJobData.Builder().putLong("message_id", 1).serialize() - ) - mockRecipientResolve("+16101234567", 1) - - val subject = RecipientIdJobMigration(mockk()) - val converted = subject.migrate(testData) - val data = JsonJobData.deserialize(converted.data) - - assertEquals("PushMediaSendJob", converted.factoryKey) - assertEquals(RecipientId.from(1).toQueueKey(), converted.queueKey) - assertEquals(1, data.getLong("message_id")) - - IndividualSendJob.Factory().create(Job.Parameters.Builder().build(), converted.data) - } - - private fun mockRecipientResolve(address: String, recipientId: Long) { - every { Recipient.external(any(), address) } returns mockRecipient(recipientId) - } - - private fun mockRecipient(id: Long): Recipient { - return mockk { - every { this@mockk.id } returns RecipientId.from(id) - } - } -} diff --git a/app/src/test/java/org/thoughtcrime/securesms/phonenumbers/PhoneNumberFormatterTest.java b/app/src/test/java/org/thoughtcrime/securesms/phonenumbers/PhoneNumberFormatterTest.java deleted file mode 100644 index 940282031e..0000000000 --- a/app/src/test/java/org/thoughtcrime/securesms/phonenumbers/PhoneNumberFormatterTest.java +++ /dev/null @@ -1,102 +0,0 @@ -package org.thoughtcrime.securesms.phonenumbers; - -import org.junit.Before; -import org.junit.Test; -import org.signal.core.util.logging.Log; -import org.thoughtcrime.securesms.testutil.EmptyLogger; - -import static org.junit.Assert.assertEquals; - -public class PhoneNumberFormatterTest { - - @Before - public void setup() { - Log.initialize(new EmptyLogger()); - } - - @Test - public void testAddressString() { - PhoneNumberFormatter formatter = new PhoneNumberFormatter("+14152222222"); - assertEquals("bonbon", formatter.format("bonbon")); - } - - @Test - public void testAddressShortCode() { - PhoneNumberFormatter formatter = new PhoneNumberFormatter("+14152222222"); - assertEquals("40404", formatter.format("40404")); - } - - @Test - public void testEmailAddress() { - PhoneNumberFormatter formatter = new PhoneNumberFormatter("+14152222222"); - assertEquals("junk@junk.net", formatter.format("junk@junk.net")); - } - - @Test - public void testNumberArbitrary() { - PhoneNumberFormatter formatter = new PhoneNumberFormatter("+14152222222"); - assertEquals("+14151111122", formatter.format("(415) 111-1122")); - assertEquals("+14151111123", formatter.format("(415) 111 1123")); - assertEquals("+14151111124", formatter.format("415-111-1124")); - assertEquals("+14151111125", formatter.format("415.111.1125")); - assertEquals("+14151111126", formatter.format("+1 415.111.1126")); - assertEquals("+14151111127", formatter.format("+1 415 111 1127")); - assertEquals("+14151111128", formatter.format("+1 (415) 111 1128")); - assertEquals("911", formatter.format("911")); - assertEquals("+4567890", formatter.format("+456-7890")); - - formatter = new PhoneNumberFormatter("+442079460010"); - assertEquals("+442079460018", formatter.format("(020) 7946 0018")); - } - - @Test - public void testUsNumbers() { - PhoneNumberFormatter formatter = new PhoneNumberFormatter("+16105880522"); - - assertEquals("+551234567890", formatter.format("+551234567890")); - assertEquals("+11234567890", formatter.format("(123) 456-7890")); - assertEquals("+11234567890", formatter.format("1234567890")); - assertEquals("+16104567890", formatter.format("456-7890")); - assertEquals("+16104567890", formatter.format("4567890")); - assertEquals("+11234567890", formatter.format("011 1 123 456 7890")); - assertEquals("+5511912345678", formatter.format("0115511912345678")); - assertEquals("+16105880522", formatter.format("+16105880522")); - } - - @Test - public void testBrNumbers() { - PhoneNumberFormatter formatter = new PhoneNumberFormatter("+5521912345678"); - - assertEquals("+16105880522", formatter.format("+16105880522")); - assertEquals("+552187654321", formatter.format("8765 4321")); - assertEquals("+5521987654321", formatter.format("9 8765 4321")); - assertEquals("+552287654321", formatter.format("22 8765 4321")); - assertEquals("+5522987654321", formatter.format("22 9 8765 4321")); - assertEquals("+551234567890", formatter.format("+55 (123) 456-7890")); - assertEquals("+14085048577", formatter.format("002214085048577")); - assertEquals("+5511912345678", formatter.format("011912345678")); - assertEquals("+5511912345678", formatter.format("02111912345678")); - assertEquals("+551234567", formatter.format("1234567")); - assertEquals("+5521912345678", formatter.format("+5521912345678")); - assertEquals("+552112345678", formatter.format("+552112345678")); - } - - @Test - public void testGroup() { - PhoneNumberFormatter formatter = new PhoneNumberFormatter("+14152222222"); - assertEquals("__textsecure_group__!foobar", formatter.format("__textsecure_group__!foobar")); - } - - @Test - public void testLostLocalNumber() { - PhoneNumberFormatter formatter = new PhoneNumberFormatter("US", true); - assertEquals("+14151111122", formatter.format("(415) 111-1122")); - } - - @Test - public void testParseNumberFailWithoutLocalNumber() { - PhoneNumberFormatter formatter = new PhoneNumberFormatter("US", true); - assertEquals("+144444444441234512312312312312312312312", formatter.format("44444444441234512312312312312312312312")); - assertEquals("+144444444441234512312312312312312312312", formatter.format("144444444441234512312312312312312312312")); - } -} diff --git a/app/src/test/java/org/thoughtcrime/securesms/util/PhoneNumberFormatterTest.java b/app/src/test/java/org/thoughtcrime/securesms/util/PhoneNumberFormatterTest.java deleted file mode 100644 index afc077357d..0000000000 --- a/app/src/test/java/org/thoughtcrime/securesms/util/PhoneNumberFormatterTest.java +++ /dev/null @@ -1,63 +0,0 @@ -package org.thoughtcrime.securesms.util; - -import org.junit.Test; -import org.whispersystems.signalservice.api.util.InvalidNumberException; -import org.whispersystems.signalservice.api.util.PhoneNumberFormatter; - -import static org.junit.Assert.assertEquals; -import static org.junit.Assert.assertNotEquals; -import static org.junit.Assert.assertThrows; - -public class PhoneNumberFormatterTest { - private static final String LOCAL_NUMBER_US = "+15555555555"; - private static final String NUMBER_CH = "+41446681800"; - private static final String NUMBER_UK = "+442079460018"; - private static final String NUMBER_DE = "+4930123456"; - private static final String NUMBER_MOBILE_DE = "+49171123456"; - private static final String COUNTRY_CODE_CH = "41"; - private static final String COUNTRY_CODE_UK = "44"; - private static final String COUNTRY_CODE_DE = "49"; - - @Test - public void testFormatNumber() throws InvalidNumberException { - assertEquals(LOCAL_NUMBER_US, PhoneNumberFormatter.formatNumber("(555) 555-5555", LOCAL_NUMBER_US)); - assertEquals(LOCAL_NUMBER_US, PhoneNumberFormatter.formatNumber("555-5555", LOCAL_NUMBER_US)); - assertNotEquals(LOCAL_NUMBER_US, PhoneNumberFormatter.formatNumber("(123) 555-5555", LOCAL_NUMBER_US)); - } - - @Test - public void testFormatNumberEmail() { - assertThrows( - "should have thrown on email", - InvalidNumberException.class, - () -> PhoneNumberFormatter.formatNumber("person@domain.com", LOCAL_NUMBER_US) - ); - } - - @Test - public void testFormatNumberE164() { - assertEquals(NUMBER_UK, PhoneNumberFormatter.formatE164(COUNTRY_CODE_UK, "(020) 7946 0018")); -// assertEquals(NUMBER_UK, PhoneNumberFormatter.formatE164(COUNTRY_CODE_UK, "044 20 7946 0018")); - assertEquals(NUMBER_UK, PhoneNumberFormatter.formatE164(COUNTRY_CODE_UK, "+442079460018")); - - assertEquals(NUMBER_CH, PhoneNumberFormatter.formatE164(COUNTRY_CODE_CH, "+41 44 668 18 00")); - assertEquals(NUMBER_CH, PhoneNumberFormatter.formatE164(COUNTRY_CODE_CH, "+41 (044) 6681800")); - - assertEquals(NUMBER_DE, PhoneNumberFormatter.formatE164(COUNTRY_CODE_DE, "0049 030 123456")); - assertEquals(NUMBER_DE, PhoneNumberFormatter.formatE164(COUNTRY_CODE_DE, "0049 (0)30123456")); - assertEquals(NUMBER_DE, PhoneNumberFormatter.formatE164(COUNTRY_CODE_DE, "0049((0)30)123456")); - assertEquals(NUMBER_DE, PhoneNumberFormatter.formatE164(COUNTRY_CODE_DE, "+49 (0) 30 1 2 3 45 6 ")); - assertEquals(NUMBER_DE, PhoneNumberFormatter.formatE164(COUNTRY_CODE_DE, "030 123456")); - - assertEquals(NUMBER_MOBILE_DE, PhoneNumberFormatter.formatE164(COUNTRY_CODE_DE, "0171123456")); - assertEquals(NUMBER_MOBILE_DE, PhoneNumberFormatter.formatE164(COUNTRY_CODE_DE, "0171/123456")); - assertEquals(NUMBER_MOBILE_DE, PhoneNumberFormatter.formatE164(COUNTRY_CODE_DE, "+490171/123456")); - assertEquals(NUMBER_MOBILE_DE, PhoneNumberFormatter.formatE164(COUNTRY_CODE_DE, "00490171/123456")); - assertEquals(NUMBER_MOBILE_DE, PhoneNumberFormatter.formatE164(COUNTRY_CODE_DE, "0049171/123456")); - } - - @Test - public void testFormatRemoteNumberE164() throws InvalidNumberException { - assertEquals(NUMBER_UK, PhoneNumberFormatter.formatNumber("+4402079460018", LOCAL_NUMBER_US)); - } -} diff --git a/app/src/test/java/org/thoughtcrime/securesms/util/SignalMeUtilText_parseE164FromLink.kt b/app/src/test/java/org/thoughtcrime/securesms/util/SignalMeUtilText_parseE164FromLink.kt index df4de4de3d..2ff1e74081 100644 --- a/app/src/test/java/org/thoughtcrime/securesms/util/SignalMeUtilText_parseE164FromLink.kt +++ b/app/src/test/java/org/thoughtcrime/securesms/util/SignalMeUtilText_parseE164FromLink.kt @@ -12,7 +12,6 @@ import org.junit.Test import org.junit.runner.RunWith import org.robolectric.ParameterizedRobolectricTestRunner import org.robolectric.annotation.Config -import org.thoughtcrime.securesms.dependencies.AppDependencies.application import org.thoughtcrime.securesms.keyvalue.SignalStore import org.thoughtcrime.securesms.testutil.MockAppDependenciesRule import org.thoughtcrime.securesms.util.SignalMeUtil.parseE164FromLink @@ -37,7 +36,7 @@ class SignalMeUtilText_parseE164FromLink(private val input: String?, private val @Test fun parse() { - assertEquals(output, parseE164FromLink(application, input)) + assertEquals(output, parseE164FromLink(input)) } companion object { diff --git a/contacts/lib/src/main/java/org/signal/contacts/ContactLinkConfiguration.kt b/contacts/lib/src/main/java/org/signal/contacts/ContactLinkConfiguration.kt index 18d1752efc..7704f4a17f 100644 --- a/contacts/lib/src/main/java/org/signal/contacts/ContactLinkConfiguration.kt +++ b/contacts/lib/src/main/java/org/signal/contacts/ContactLinkConfiguration.kt @@ -18,7 +18,7 @@ class ContactLinkConfiguration( val messagePrompt: (String) -> String, val callPrompt: (String) -> String, val videoCallPrompt: (String) -> String, - val e164Formatter: (String) -> String, + val e164Formatter: (String) -> String?, val messageMimetype: String, val callMimetype: String, val videoCallMimetype: String, diff --git a/contacts/lib/src/main/java/org/signal/contacts/SystemContactsRepository.kt b/contacts/lib/src/main/java/org/signal/contacts/SystemContactsRepository.kt index 43a1092367..5981b1e508 100644 --- a/contacts/lib/src/main/java/org/signal/contacts/SystemContactsRepository.kt +++ b/contacts/lib/src/main/java/org/signal/contacts/SystemContactsRepository.kt @@ -90,7 +90,7 @@ object SystemContactsRepository { * lookup key. */ @JvmStatic - fun getAllSystemContacts(context: Context, e164Formatter: (String) -> String): ContactIterator { + fun getAllSystemContacts(context: Context, e164Formatter: (String) -> String?): ContactIterator { val uri = ContactsContract.Data.CONTENT_URI val projection = arrayOf( ContactsContract.Data.MIMETYPE, @@ -114,7 +114,7 @@ object SystemContactsRepository { } @JvmStatic - fun getContactDetailsByQueries(context: Context, queries: List, e164Formatter: (String) -> String): ContactIterator { + fun getContactDetailsByQueries(context: Context, queries: List, e164Formatter: (String) -> String?): ContactIterator { val lookupKeys: MutableSet = mutableSetOf() for (query in queries) { @@ -560,7 +560,7 @@ object SystemContactsRepository { ) } - private fun getLinkedContactsByE164(context: Context, account: Account, e164Formatter: (String) -> String): Map { + private fun getLinkedContactsByE164(context: Context, account: Account, e164Formatter: (String) -> String?): Map { val currentContactsUri = ContactsContract.RawContacts.CONTENT_URI.buildUpon() .appendQueryParameter(ContactsContract.RawContacts.ACCOUNT_NAME, account.name) .appendQueryParameter(ContactsContract.RawContacts.ACCOUNT_TYPE, account.type).build() @@ -580,7 +580,7 @@ object SystemContactsRepository { val displayPhone = cursor.requireString(FIELD_DISPLAY_PHONE) if (displayPhone != null) { - val e164 = e164Formatter(displayPhone) + val e164 = e164Formatter(displayPhone) ?: continue contactsDetails[e164] = LinkedContactDetails( id = cursor.requireLong(BaseColumns._ID), @@ -596,7 +596,7 @@ object SystemContactsRepository { return contactsDetails } - private fun getSystemContactInfo(context: Context, e164: String, e164Formatter: (String) -> String): SystemContactInfo? { + private fun getSystemContactInfo(context: Context, e164: String, e164Formatter: (String) -> String?): SystemContactInfo? { ContactsContract.RawContactsEntity.RAW_CONTACT_ID val uri = Uri.withAppendedPath(ContactsContract.PhoneLookup.CONTENT_FILTER_URI, Uri.encode(e164)) val projection = arrayOf( @@ -712,7 +712,7 @@ object SystemContactsRepository { */ private class CursorContactIterator( private val cursor: Cursor, - private val e164Formatter: (String) -> String + private val e164Formatter: (String) -> String? ) : ContactIterator { init { @@ -752,13 +752,14 @@ object SystemContactsRepository { while (!cursor.isAfterLast && lookupKey == cursor.getLookupKey() && cursor.isPhoneMimeType()) { val displayNumber: String? = cursor.requireString(ContactsContract.CommonDataKinds.Phone.NUMBER) + val formattedNumber: String? = displayNumber?.let { e164Formatter(it) } - if (!displayNumber.isNullOrEmpty()) { + if (!displayNumber.isNullOrEmpty() && !formattedNumber.isNullOrEmpty()) { phoneDetails += ContactPhoneDetails( contactUri = ContactsContract.Contacts.getLookupUri(cursor.requireLong(ContactsContract.CommonDataKinds.Phone._ID), lookupKey), displayName = cursor.requireString(ContactsContract.CommonDataKinds.Phone.DISPLAY_NAME), photoUri = cursor.requireString(ContactsContract.CommonDataKinds.Phone.PHOTO_URI), - number = e164Formatter(displayNumber), + number = formattedNumber, type = cursor.requireInt(ContactsContract.CommonDataKinds.Phone.TYPE), label = cursor.requireString(ContactsContract.CommonDataKinds.Phone.LABEL) ) diff --git a/core-util-jvm/build.gradle.kts b/core-util-jvm/build.gradle.kts index 3dea6dbaa0..4e8b0af1b2 100644 --- a/core-util-jvm/build.gradle.kts +++ b/core-util-jvm/build.gradle.kts @@ -53,6 +53,7 @@ dependencies { implementation(libs.kotlin.reflect) implementation(libs.kotlinx.coroutines.core) implementation(libs.kotlinx.coroutines.core.jvm) + implementation(libs.google.libphonenumber) testImplementation(testLibs.junit.junit) testImplementation(testLibs.assertk) diff --git a/core-util-jvm/src/main/java/org/signal/core/util/E164Util.kt b/core-util-jvm/src/main/java/org/signal/core/util/E164Util.kt new file mode 100644 index 0000000000..ffc9e424e4 --- /dev/null +++ b/core-util-jvm/src/main/java/org/signal/core/util/E164Util.kt @@ -0,0 +1,253 @@ +/* + * Copyright 2025 Signal Messenger, LLC + * SPDX-License-Identifier: AGPL-3.0-only + */ + +package org.signal.core.util + +import com.google.i18n.phonenumbers.NumberParseException +import com.google.i18n.phonenumbers.PhoneNumberUtil +import com.google.i18n.phonenumbers.Phonenumber.PhoneNumber +import com.google.i18n.phonenumbers.ShortNumberInfo +import org.signal.core.util.logging.Log +import java.util.Locale +import java.util.Optional +import java.util.regex.Matcher +import java.util.regex.Pattern + +/** + * Contains a bunch of utility functions to parse and format phone numbers. + */ +object E164Util { + private val TAG = Log.tag(E164Util::class) + + private const val COUNTRY_CODE_BR = "55" + private const val COUNTRY_CODE_US = "1" + + private val US_NO_AREACODE: Pattern = Pattern.compile("^(\\d{7})$") + private val BR_NO_AREACODE: Pattern = Pattern.compile("^(9?\\d{8})$") + + private const val COUNTRY_CODE_US_INT = 1 + private const val COUNTRY_CODE_UK_INT = 44 + + /** A set of country codes representing countries where we'd like to use the (555) 555-5555 number format for pretty printing. */ + private val NATIONAL_FORMAT_COUNTRY_CODES = setOf(COUNTRY_CODE_US_INT, COUNTRY_CODE_UK_INT) + + /** + * Creates a formatter based on the provided local number. This is largely an improvement in performance/convenience + * over parsing out the various number attributes themselves and caching them manually. + * + * It is assumed that this number is properly formatted. If it is not, this may throw a [NumberParseException]. + * + * @throws NumberParseException + */ + fun createFormatterForE164(localNumber: String): Formatter { + val phoneNumber = PhoneNumberUtil.getInstance().parse(localNumber, null) + val regionCode = PhoneNumberUtil.getInstance().getRegionCodeForNumber(phoneNumber) ?: PhoneNumberUtil.getInstance().getRegionCodeForCountryCode(phoneNumber.countryCode) + val areaCode = parseAreaCode(localNumber, phoneNumber.countryCode) + + return Formatter(localNumber = phoneNumber, localAreaCode = areaCode, localRegionCode = regionCode) + } + + /** + * Creates a formatter based on the provided region code. This is largely an improvement in performance/convenience + * over parsing out the various number attributes themselves and caching them manually. + */ + fun createFormatterForRegionCode(regionCode: String): Formatter { + return Formatter(localNumber = null, localAreaCode = null, localRegionCode = regionCode) + } + + /** + * The same as [formatAsE164WithCountryCode], but if we determine the number to be invalid, + * we will do some cleanup to *roughly* format it as E164. + * + * IMPORTANT: Do not use this for actual number storage! There is no guarantee that this + * will be a properly-formatted E164 number. It should only be used in situations where a + * value is needed for user display. + */ + @JvmStatic + fun formatAsE164WithCountryCodeForDisplay(countryCode: String, input: String): String { + val result: String? = formatAsE164WithCountryCode(countryCode, input) + if (result != null) { + return result + } + + val cleanCountryCode = countryCode + .numbersOnly() + .replace("^0*".toRegex(), "") + val cleanNumber = input.numbersOnly() + + return "+$cleanCountryCode$cleanNumber" + } + + /** + * Returns whether or not an input number is valid for registration. Besides checking to ensure that libphonenumber thinks it's a possible number at all, + * we also have a few country-specific checks, as well as some of our own length and formatting checks. + */ + @JvmStatic + fun isValidNumberForRegistration(countryCode: String, input: String): Boolean { + if (!PhoneNumberUtil.getInstance().isPossibleNumber(input, countryCode)) { + Log.w(TAG, "Failed isPossibleNumber()") + return false + } + + if (COUNTRY_CODE_US == countryCode && !Pattern.matches("^\\+1[0-9]{10}$", input)) { + Log.w(TAG, "Failed US number format check") + return false + } + + if (COUNTRY_CODE_BR == countryCode && !Pattern.matches("^\\+55[0-9]{2}9?[0-9]{8}$", input)) { + Log.w(TAG, "Failed Brazil number format check") + return false + } + + return input.matches("^\\+[1-9][0-9]{6,14}$".toRegex()) + } + + /** + * Given a regionCode, this will attempt to provide the display name for that region. + */ + @JvmStatic + fun getRegionDisplayName(regionCode: String?): Optional { + if (regionCode == null || regionCode == "ZZ" || regionCode == PhoneNumberUtil.REGION_CODE_FOR_NON_GEO_ENTITY) { + return Optional.empty() + } + + val displayCountry: String? = Locale("", regionCode).getDisplayCountry(Locale.getDefault()).nullIfBlank() + return Optional.ofNullable(displayCountry) + } + + /** + * Identical to [formatAsE164WithRegionCode], except rather than supply the region code, you supply the + * country code (i.e. "1" for the US). This will convert the country code to a region code on your behalf. + * See [formatAsE164WithRegionCode] for behavior. + */ + private fun formatAsE164WithCountryCode(countryCode: String, input: String): String? { + val regionCode = try { + val countryCodeInt = countryCode.toInt() + PhoneNumberUtil.getInstance().getRegionCodeForCountryCode(countryCodeInt) + } catch (e: NumberFormatException) { + return null + } + + return formatAsE164WithRegionCode( + localNumber = null, + localAreaCode = null, + regionCode = regionCode, + input = input + ) + } + + /** + * Formats the number as an E164, or null if the number cannot be reasonably interpreted as a phone number. + * This does not check if the number is *valid* for a given region. Instead, it's very lenient and just + * does it's best to interpret the input string as a number that could be put into the E164 format. + * + * Note that shortcodes will not have leading '+' signs. + * + * In other words, if this method returns null, you likely do not have anything that could be considered + * a phone number. + */ + private fun formatAsE164WithRegionCode(localNumber: PhoneNumber?, localAreaCode: String?, regionCode: String, input: String): String? { + try { + val withAreaCodeRules: String = applyAreaCodeRules(localNumber, localAreaCode, input.e164CharsOnly()) + val parsedNumber: PhoneNumber = PhoneNumberUtil.getInstance().parse(withAreaCodeRules, regionCode) + + val isShortCode = ShortNumberInfo.getInstance().isValidShortNumberForRegion(parsedNumber, regionCode) || withAreaCodeRules.length <= 5 + if (isShortCode) { + return input.numbersOnly() + } + + return PhoneNumberUtil.getInstance().format(parsedNumber, PhoneNumberUtil.PhoneNumberFormat.E164) + } catch (e: NumberParseException) { + return null + } + } + + /** + * Attempts to parse the area code out of an e164-formatted number provided that it's in one of the supported countries. + */ + private fun parseAreaCode(e164Number: String, countryCode: Int): String? { + when (countryCode) { + 1 -> return e164Number.substring(2, 5) + 55 -> return e164Number.substring(3, 5) + } + return null + } + + /** + * Given an input number, this will attempt to add in an area code for certain locales if we have one in the local number. + * For example, in the US, if your local number is (610) 555-5555, and we're given a `testNumber` of 123-4567, we could + * assume that the full number would be (610) 123-4567. + */ + private fun applyAreaCodeRules(localNumber: PhoneNumber?, localAreaCode: String?, testNumber: String): String { + if (localNumber === null || localAreaCode == null) { + return testNumber + } + + val matcher: Matcher? = when (localNumber.countryCode) { + 1 -> US_NO_AREACODE.matcher(testNumber) + 55 -> BR_NO_AREACODE.matcher(testNumber) + else -> null + } + + if (matcher != null && matcher.matches()) { + return localAreaCode + matcher.group() + } + + return testNumber + } + + private fun String.numbersOnly(): String { + return this.filter { it.isDigit() } + } + + private fun String.e164CharsOnly(): String { + return this.filter { it.isDigit() || it == '+' } + } + + class Formatter( + val localNumber: PhoneNumber?, + val localAreaCode: String?, + val localRegionCode: String + ) { + /** + * Formats the number as an E164, or null if the number cannot be reasonably interpreted as a phone number. + * This does not check if the number is *valid* for a given region. Instead, it's very lenient and just + * does it's best to interpret the input string as a number that could be put into the E164 format. + * + * Note that shortcodes will not have leading '+' signs. + * + * In other words, if this method returns null, you likely do not have anything that could be considered + * a phone number. + */ + fun formatAsE164(input: String): String? { + return formatAsE164WithRegionCode( + localNumber = localNumber, + localAreaCode = localAreaCode, + regionCode = localRegionCode, + input = input + ) + } + + /** + * Formats the number for human-readable display. e.g. "(555) 555-5555" + */ + fun prettyPrint(input: String): String { + val raw = try { + val parsedNumber: PhoneNumber = PhoneNumberUtil.getInstance().parse(input, localRegionCode) + + return if (localNumber != null && localNumber.countryCode == parsedNumber.countryCode && NATIONAL_FORMAT_COUNTRY_CODES.contains(localNumber.countryCode)) { + PhoneNumberUtil.getInstance().format(parsedNumber, PhoneNumberUtil.PhoneNumberFormat.NATIONAL) + } else { + PhoneNumberUtil.getInstance().format(parsedNumber, PhoneNumberUtil.PhoneNumberFormat.INTERNATIONAL) + } + } catch (e: NumberParseException) { + Log.w(TAG, "Failed to format number: $e") + input + } + + return BidiUtil.forceLtr(BidiUtil.isolateBidi(raw)) + } + } +} diff --git a/core-util-jvm/src/test/java/org/signal/core/util/E164UtilTest.kt b/core-util-jvm/src/test/java/org/signal/core/util/E164UtilTest.kt new file mode 100644 index 0000000000..9413e0b1e3 --- /dev/null +++ b/core-util-jvm/src/test/java/org/signal/core/util/E164UtilTest.kt @@ -0,0 +1,126 @@ +/* + * Copyright 2025 Signal Messenger, LLC + * SPDX-License-Identifier: AGPL-3.0-only + */ + +package org.signal.core.util + +import org.junit.Assert +import org.junit.Test + +class E164UtilTest { + + @Test + fun `formatAsE164WithCountryCodeForDisplay - generic`() { + // UK + Assert.assertEquals("+442079460018", E164Util.formatAsE164WithCountryCodeForDisplay("44", "(020) 7946 0018")) + Assert.assertEquals("+442079460018", E164Util.formatAsE164WithCountryCodeForDisplay("44", "+442079460018")) + + // CH + Assert.assertEquals("+41446681800", E164Util.formatAsE164WithCountryCodeForDisplay("41", "+41 44 668 18 00")) + Assert.assertEquals("+41446681800", E164Util.formatAsE164WithCountryCodeForDisplay("41", "+41 (044) 6681800")) + + // DE + Assert.assertEquals("+4930123456", E164Util.formatAsE164WithCountryCodeForDisplay("49", "0049 030 123456")) + Assert.assertEquals("+4930123456", E164Util.formatAsE164WithCountryCodeForDisplay("49", "0049 (0)30123456")) + Assert.assertEquals("+4930123456", E164Util.formatAsE164WithCountryCodeForDisplay("49", "0049((0)30)123456")) + Assert.assertEquals("+4930123456", E164Util.formatAsE164WithCountryCodeForDisplay("49", "+49 (0) 30 1 2 3 45 6 ")) + Assert.assertEquals("+4930123456", E164Util.formatAsE164WithCountryCodeForDisplay("49", "030 123456")) + + // DE + Assert.assertEquals("+49171123456", E164Util.formatAsE164WithCountryCodeForDisplay("49", "0171123456")) + Assert.assertEquals("+49171123456", E164Util.formatAsE164WithCountryCodeForDisplay("49", "0171/123456")) + Assert.assertEquals("+49171123456", E164Util.formatAsE164WithCountryCodeForDisplay("49", "+490171/123456")) + Assert.assertEquals("+49171123456", E164Util.formatAsE164WithCountryCodeForDisplay("49", "00490171/123456")) + Assert.assertEquals("+49171123456", E164Util.formatAsE164WithCountryCodeForDisplay("49", "0049171/123456")) + } + + @Test + fun `formatAsE164 - generic`() { + var formatter: E164Util.Formatter = E164Util.createFormatterForE164("+14152222222") + Assert.assertEquals("+14151111122", formatter.formatAsE164("(415) 111-1122")) + Assert.assertEquals("+14151111123", formatter.formatAsE164("(415) 111 1123")) + Assert.assertEquals("+14151111124", formatter.formatAsE164("415-111-1124")) + Assert.assertEquals("+14151111125", formatter.formatAsE164("415.111.1125")) + Assert.assertEquals("+14151111126", formatter.formatAsE164("+1 415.111.1126")) + Assert.assertEquals("+14151111127", formatter.formatAsE164("+1 415 111 1127")) + Assert.assertEquals("+14151111128", formatter.formatAsE164("+1 (415) 111 1128")) + Assert.assertEquals("911", formatter.formatAsE164("911")) + Assert.assertEquals("+4567890", formatter.formatAsE164("+456-7890")) + + formatter = E164Util.createFormatterForE164("+442079460010") + Assert.assertEquals("+442079460018", formatter.formatAsE164("(020) 7946 0018")) + } + + @Test + fun `formatAsE164 - US mix`() { + val formatter: E164Util.Formatter = E164Util.createFormatterForE164("+16105880522") + + Assert.assertEquals("+551234567890", formatter.formatAsE164("+551234567890")) + Assert.assertEquals("+11234567890", formatter.formatAsE164("(123) 456-7890")) + Assert.assertEquals("+11234567890", formatter.formatAsE164("1234567890")) + Assert.assertEquals("+16104567890", formatter.formatAsE164("456-7890")) + Assert.assertEquals("+16104567890", formatter.formatAsE164("4567890")) + Assert.assertEquals("+11234567890", formatter.formatAsE164("011 1 123 456 7890")) + Assert.assertEquals("+5511912345678", formatter.formatAsE164("0115511912345678")) + Assert.assertEquals("+16105880522", formatter.formatAsE164("+16105880522")) + } + + @Test + fun `formatAsE164 - Brazil mix`() { + val formatter: E164Util.Formatter = E164Util.createFormatterForE164("+5521912345678") + + Assert.assertEquals("+16105880522", formatter.formatAsE164("+16105880522")) + Assert.assertEquals("+552187654321", formatter.formatAsE164("8765 4321")) + Assert.assertEquals("+5521987654321", formatter.formatAsE164("9 8765 4321")) + Assert.assertEquals("+552287654321", formatter.formatAsE164("22 8765 4321")) + Assert.assertEquals("+5522987654321", formatter.formatAsE164("22 9 8765 4321")) + Assert.assertEquals("+551234567890", formatter.formatAsE164("+55 (123) 456-7890")) + Assert.assertEquals("+14085048577", formatter.formatAsE164("002214085048577")) + Assert.assertEquals("+5511912345678", formatter.formatAsE164("011912345678")) + Assert.assertEquals("+5511912345678", formatter.formatAsE164("02111912345678")) + Assert.assertEquals("+551234567", formatter.formatAsE164("1234567")) + Assert.assertEquals("+5521912345678", formatter.formatAsE164("+5521912345678")) + Assert.assertEquals("+552112345678", formatter.formatAsE164("+552112345678")) + } + + @Test + fun `formatAsE164 - short codes`() { + val formatter: E164Util.Formatter = E164Util.createFormatterForE164("+14152222222") + Assert.assertEquals("40404", formatter.formatAsE164("40404")) + Assert.assertEquals("40404", formatter.formatAsE164("40404")) + Assert.assertEquals("7726", formatter.formatAsE164("7726")) + Assert.assertEquals("22000", formatter.formatAsE164("22000")) + Assert.assertEquals("265080", formatter.formatAsE164("265080")) + Assert.assertEquals("32665", formatter.formatAsE164("32665")) + Assert.assertEquals("732873", formatter.formatAsE164("732873")) + Assert.assertEquals("73822", formatter.formatAsE164("73822")) + Assert.assertEquals("83547", formatter.formatAsE164("83547")) + Assert.assertEquals("84639", formatter.formatAsE164("84639")) + Assert.assertEquals("89887", formatter.formatAsE164("89887")) + Assert.assertEquals("99000", formatter.formatAsE164("99000")) + Assert.assertEquals("911", formatter.formatAsE164("911")) + Assert.assertEquals("112", formatter.formatAsE164("112")) + Assert.assertEquals("311", formatter.formatAsE164("311")) + Assert.assertEquals("611", formatter.formatAsE164("611")) + Assert.assertEquals("988", formatter.formatAsE164("988")) + Assert.assertEquals("999", formatter.formatAsE164("999")) + Assert.assertEquals("118", formatter.formatAsE164("118")) + } + + @Test + fun `formatAsE164 - invalid`() { + val formatter: E164Util.Formatter = E164Util.createFormatterForE164("+14152222222") + Assert.assertEquals(null, formatter.formatAsE164("junk@junk.net")) + Assert.assertEquals(null, formatter.formatAsE164("__textsecure_group__!foobar")) + Assert.assertEquals(null, formatter.formatAsE164("bonbon")) + Assert.assertEquals(null, formatter.formatAsE164("44444444441234512312312312312312312312")) + Assert.assertEquals(null, formatter.formatAsE164("144444444441234512312312312312312312312")) + } + + @Test + fun `formatAsE164 - no local number`() { + val formatter: E164Util.Formatter = E164Util.createFormatterForRegionCode("US") + Assert.assertEquals("+14151111122", formatter.formatAsE164("(415) 111-1122")) + } +} diff --git a/core-util/src/test/java/org/signal/core/util/StringUtilTest_abbreviateInMiddle.java b/core-util/src/test/java/org/signal/core/util/StringUtilTest_abbreviateInMiddle.java index 79cda86919..1244d25d68 100644 --- a/core-util/src/test/java/org/signal/core/util/StringUtilTest_abbreviateInMiddle.java +++ b/core-util/src/test/java/org/signal/core/util/StringUtilTest_abbreviateInMiddle.java @@ -3,7 +3,6 @@ package org.signal.core.util; import org.junit.Test; import org.junit.runner.RunWith; import org.junit.runners.Parameterized; -import org.signal.core.util.StringUtil; import java.util.Arrays; import java.util.Collection; diff --git a/core-util/src/test/java/org/signal/core/util/StringUtilTest_hasMixedTextDirection.java b/core-util/src/test/java/org/signal/core/util/StringUtilTest_hasMixedTextDirection.java index caa74b702f..826528452b 100644 --- a/core-util/src/test/java/org/signal/core/util/StringUtilTest_hasMixedTextDirection.java +++ b/core-util/src/test/java/org/signal/core/util/StringUtilTest_hasMixedTextDirection.java @@ -3,7 +3,6 @@ package org.signal.core.util; import org.junit.Test; import org.junit.runner.RunWith; import org.junit.runners.Parameterized; -import org.signal.core.util.StringUtil; import java.util.Arrays; import java.util.Collection; diff --git a/core-util/src/test/java/org/signal/core/util/StringUtilTest_trim.java b/core-util/src/test/java/org/signal/core/util/StringUtilTest_trim.java index f4e74e55d0..a56b9969c5 100644 --- a/core-util/src/test/java/org/signal/core/util/StringUtilTest_trim.java +++ b/core-util/src/test/java/org/signal/core/util/StringUtilTest_trim.java @@ -3,7 +3,6 @@ package org.signal.core.util; import org.junit.Test; import org.junit.runner.RunWith; import org.junit.runners.Parameterized; -import org.signal.core.util.StringUtil; import java.util.Arrays; import java.util.Collection; diff --git a/core-util/src/test/java/org/signal/core/util/StringUtilTest_trimToFit.java b/core-util/src/test/java/org/signal/core/util/StringUtilTest_trimToFit.java index 9bc06e23e0..aced7f722d 100644 --- a/core-util/src/test/java/org/signal/core/util/StringUtilTest_trimToFit.java +++ b/core-util/src/test/java/org/signal/core/util/StringUtilTest_trimToFit.java @@ -7,7 +7,6 @@ import org.junit.Test; import org.junit.runner.RunWith; import org.robolectric.RobolectricTestRunner; import org.robolectric.annotation.Config; -import org.signal.core.util.StringUtil; import static org.junit.Assert.assertEquals; import static org.junit.Assume.assumeTrue; diff --git a/core-util/src/test/java/org/signal/core/util/StringUtilTest_whitespace_handling.java b/core-util/src/test/java/org/signal/core/util/StringUtilTest_whitespace_handling.java index 4ea5de8db1..fd4b8967b9 100644 --- a/core-util/src/test/java/org/signal/core/util/StringUtilTest_whitespace_handling.java +++ b/core-util/src/test/java/org/signal/core/util/StringUtilTest_whitespace_handling.java @@ -3,7 +3,6 @@ package org.signal.core.util; import org.junit.Test; import org.junit.runner.RunWith; import org.junit.runners.Parameterized; -import org.signal.core.util.StringUtil; import java.util.Arrays; import java.util.Collection; diff --git a/libsignal-service/src/main/java/org/whispersystems/signalservice/api/util/PhoneNumberFormatter.java b/libsignal-service/src/main/java/org/whispersystems/signalservice/api/util/PhoneNumberFormatter.java deleted file mode 100644 index bb9796c6d6..0000000000 --- a/libsignal-service/src/main/java/org/whispersystems/signalservice/api/util/PhoneNumberFormatter.java +++ /dev/null @@ -1,163 +0,0 @@ -/** - * Copyright (C) 2014-2016 Open Whisper Systems - * - * Licensed according to the LICENSE file in this repository. - */ - -package org.whispersystems.signalservice.api.util; - -import com.google.i18n.phonenumbers.NumberParseException; -import com.google.i18n.phonenumbers.PhoneNumberUtil; -import com.google.i18n.phonenumbers.PhoneNumberUtil.PhoneNumberFormat; -import com.google.i18n.phonenumbers.Phonenumber.PhoneNumber; - -import org.signal.libsignal.protocol.logging.Log; -import org.whispersystems.signalservice.internal.util.Util; - -import java.util.Locale; -import java.util.Optional; -import java.util.regex.Pattern; - -/** - * Phone number formats are a pain. - * - * @author Moxie Marlinspike - * - */ -public class PhoneNumberFormatter { - - private static final String TAG = PhoneNumberFormatter.class.getSimpleName(); - - private static final String COUNTRY_CODE_BR = "55"; - private static final String COUNTRY_CODE_US = "1"; - - public static boolean isValidNumber(String e164Number, String countryCode) { - if (!PhoneNumberUtil.getInstance().isPossibleNumber(e164Number, countryCode)) { - Log.w(TAG, "Failed isPossibleNumber()"); - return false; - } - - if (COUNTRY_CODE_US.equals(countryCode) && !Pattern.matches("^\\+1[0-9]{10}$", e164Number)) { - Log.w(TAG, "Failed US number format check"); - return false; - } - - if (COUNTRY_CODE_BR.equals(countryCode) && !Pattern.matches("^\\+55[0-9]{2}9?[0-9]{8}$", e164Number)) { - Log.w(TAG, "Failed Brazil number format check"); - return false; - } - - return e164Number.matches("^\\+[1-9][0-9]{6,14}$"); - } - - private static String impreciseFormatNumber(String number, String localNumber) - throws InvalidNumberException - { - number = number.replaceAll("[^0-9+]", ""); - - if (number.charAt(0) == '+') - return number; - - if (localNumber.charAt(0) == '+') - localNumber = localNumber.substring(1); - - if (localNumber.length() == number.length() || number.length() > localNumber.length()) - return "+" + number; - - int difference = localNumber.length() - number.length(); - - return "+" + localNumber.substring(0, difference) + number; - } - - public static String formatNumberInternational(String number) { - try { - PhoneNumberUtil util = PhoneNumberUtil.getInstance(); - PhoneNumber parsedNumber = util.parse(number, null); - return util.format(parsedNumber, PhoneNumberFormat.INTERNATIONAL); - } catch (NumberParseException e) { - Log.w(TAG, e); - return number; - } - } - - public static String formatNumber(String number, String localNumber) - throws InvalidNumberException - { - if (number == null) { - throw new InvalidNumberException("Null String passed as number."); - } - - if (number.contains("@")) { - throw new InvalidNumberException("Possible attempt to use email address."); - } - - number = number.replaceAll("[^0-9+]", ""); - - if (number.length() == 0) { - throw new InvalidNumberException("No valid characters found."); - } - -// if (number.charAt(0) == '+') -// return number; - - try { - PhoneNumberUtil util = PhoneNumberUtil.getInstance(); - PhoneNumber localNumberObject = util.parse(localNumber, null); - - String localCountryCode = util.getRegionCodeForNumber(localNumberObject); - Log.w(TAG, "Got local CC: " + localCountryCode); - - PhoneNumber numberObject = util.parse(number, localCountryCode); - return util.format(numberObject, PhoneNumberFormat.E164); - } catch (NumberParseException e) { - Log.d(TAG, e.getClass().getSimpleName() + ": " + e.getMessage()); - return impreciseFormatNumber(number, localNumber); - } - } - - /** - * @deprecated Use {@link #getRegionDisplayName} as it can be localized when the region is not found. - */ - @Deprecated - public static String getRegionDisplayNameLegacy(String regionCode) { - return getRegionDisplayName(regionCode).orElse("Unknown country"); - } - - public static Optional getRegionDisplayName(String regionCode) { - if (regionCode != null && !regionCode.equals("ZZ") && !regionCode.equals(PhoneNumberUtil.REGION_CODE_FOR_NON_GEO_ENTITY)) { - String displayCountry = new Locale("", regionCode).getDisplayCountry(Locale.getDefault()); - if (!Util.isEmpty(displayCountry)) { - return Optional.of(displayCountry); - } - } - return Optional.empty(); - } - - public static String formatE164(String countryCode, String number) { - try { - PhoneNumberUtil util = PhoneNumberUtil.getInstance(); - int parsedCountryCode = Integer.parseInt(countryCode); - PhoneNumber parsedNumber = util.parse(number, - util.getRegionCodeForCountryCode(parsedCountryCode)); - - return util.format(parsedNumber, PhoneNumberUtil.PhoneNumberFormat.E164); - } catch (NumberParseException | NumberFormatException npe) { - Log.d(TAG, npe.getClass().getSimpleName() + ": " + npe.getMessage()); - } - - return "+" + - countryCode.replaceAll("[^0-9]", "").replaceAll("^0*", "") + - (number != null ? number.replaceAll("[^0-9]", "") : ""); - } - - public static String getInternationalFormatFromE164(String e164number) { - try { - PhoneNumberUtil util = PhoneNumberUtil.getInstance(); - PhoneNumber parsedNumber = util.parse(e164number, null); - return util.format(parsedNumber, PhoneNumberFormat.INTERNATIONAL); - } catch (NumberParseException e) { - Log.w(TAG, e); - return e164number; - } - } -} diff --git a/libsignal-service/src/test/java/org/whispersystems/signalservice/api/util/PhoneNumberFormatterTest.java b/libsignal-service/src/test/java/org/whispersystems/signalservice/api/util/PhoneNumberFormatterTest.java deleted file mode 100644 index 8d92d04e98..0000000000 --- a/libsignal-service/src/test/java/org/whispersystems/signalservice/api/util/PhoneNumberFormatterTest.java +++ /dev/null @@ -1,93 +0,0 @@ -package org.whispersystems.signalservice.api.util; - -import org.junit.Test; - -import static org.junit.Assert.assertEquals; -import static org.junit.Assert.assertFalse; -import static org.junit.Assert.assertNotEquals; -import static org.junit.Assert.assertThrows; -import static org.junit.Assert.assertTrue; - -public class PhoneNumberFormatterTest { - private static final String LOCAL_NUMBER_US = "+15555555555"; - private static final String NUMBER_CH = "+41446681800"; - private static final String NUMBER_UK = "+442079460018"; - private static final String NUMBER_DE = "+4930123456"; - private static final String NUMBER_MOBILE_DE = "+49171123456"; - private static final String COUNTRY_CODE_CH = "41"; - private static final String COUNTRY_CODE_UK = "44"; - private static final String COUNTRY_CODE_DE = "49"; - - @Test - public void testIsValidNumber() { - assertTrue(PhoneNumberFormatter.isValidNumber("+6831234", "683")); - assertTrue(PhoneNumberFormatter.isValidNumber("+35851234", "358")); - assertTrue(PhoneNumberFormatter.isValidNumber("+358512345", "358")); - - assertTrue(PhoneNumberFormatter.isValidNumber("+5521912345678", "55")); - assertTrue(PhoneNumberFormatter.isValidNumber("+552112345678", "55")); - assertTrue(PhoneNumberFormatter.isValidNumber("+16105880522", "1")); - - assertFalse(PhoneNumberFormatter.isValidNumber("+014085041212", "0")); - assertFalse(PhoneNumberFormatter.isValidNumber("+014085041212", "1")); - assertFalse(PhoneNumberFormatter.isValidNumber("+5512345678", "55")); - assertFalse(PhoneNumberFormatter.isValidNumber("+161058805220", "1")); - assertFalse(PhoneNumberFormatter.isValidNumber("+1610588052", "1")); - assertFalse(PhoneNumberFormatter.isValidNumber("+15880522", "1")); - - assertTrue(PhoneNumberFormatter.isValidNumber("+971812345678901", "971")); - assertFalse(PhoneNumberFormatter.isValidNumber("+9718123456789012", "971")); - } - - @Test - public void testFormatNumber() throws InvalidNumberException { - assertEquals(LOCAL_NUMBER_US, PhoneNumberFormatter.formatNumber("(555) 555-5555", LOCAL_NUMBER_US)); - assertEquals(LOCAL_NUMBER_US, PhoneNumberFormatter.formatNumber("555-5555", LOCAL_NUMBER_US)); - assertNotEquals(LOCAL_NUMBER_US, PhoneNumberFormatter.formatNumber("(123) 555-5555", LOCAL_NUMBER_US)); - } - - @Test - public void testFormatNumberEmail() { - assertThrows( - "should have thrown on email", - InvalidNumberException.class, - () -> PhoneNumberFormatter.formatNumber("person@domain.com", LOCAL_NUMBER_US) - ); - } - - @Test - public void testFormatNumberE164() { - assertEquals(NUMBER_UK, PhoneNumberFormatter.formatE164(COUNTRY_CODE_UK, "(020) 7946 0018")); -// assertEquals(NUMBER_UK, PhoneNumberFormatter.formatE164(COUNTRY_CODE_UK, "044 20 7946 0018")); - assertEquals(NUMBER_UK, PhoneNumberFormatter.formatE164(COUNTRY_CODE_UK, "+442079460018")); - assertEquals(NUMBER_UK, PhoneNumberFormatter.formatE164(COUNTRY_CODE_UK, "+4402079460018")); - - assertEquals(NUMBER_CH, PhoneNumberFormatter.formatE164(COUNTRY_CODE_CH, "+41 44 668 18 00")); - assertEquals(NUMBER_CH, PhoneNumberFormatter.formatE164(COUNTRY_CODE_CH, "+41 (044) 6681800")); - - assertEquals(NUMBER_DE, PhoneNumberFormatter.formatE164(COUNTRY_CODE_DE, "0049 030 123456")); - assertEquals(NUMBER_DE, PhoneNumberFormatter.formatE164(COUNTRY_CODE_DE, "0049 (0)30123456")); - assertEquals(NUMBER_DE, PhoneNumberFormatter.formatE164(COUNTRY_CODE_DE, "0049((0)30)123456")); - assertEquals(NUMBER_DE, PhoneNumberFormatter.formatE164(COUNTRY_CODE_DE, "+49 (0) 30 1 2 3 45 6 ")); - assertEquals(NUMBER_DE, PhoneNumberFormatter.formatE164(COUNTRY_CODE_DE, "030 123456")); - - assertEquals(NUMBER_MOBILE_DE, PhoneNumberFormatter.formatE164(COUNTRY_CODE_DE, "0171123456")); - assertEquals(NUMBER_MOBILE_DE, PhoneNumberFormatter.formatE164(COUNTRY_CODE_DE, "0171/123456")); - assertEquals(NUMBER_MOBILE_DE, PhoneNumberFormatter.formatE164(COUNTRY_CODE_DE, "+490171/123456")); - assertEquals(NUMBER_MOBILE_DE, PhoneNumberFormatter.formatE164(COUNTRY_CODE_DE, "00490171/123456")); - assertEquals(NUMBER_MOBILE_DE, PhoneNumberFormatter.formatE164(COUNTRY_CODE_DE, "0049171/123456")); - } - - @Test - public void testFormatRemoteNumberE164() throws InvalidNumberException { - assertEquals(LOCAL_NUMBER_US, PhoneNumberFormatter.formatNumber(LOCAL_NUMBER_US, NUMBER_UK)); - assertEquals(LOCAL_NUMBER_US, PhoneNumberFormatter.formatNumber(LOCAL_NUMBER_US, LOCAL_NUMBER_US)); - - assertEquals(NUMBER_UK, PhoneNumberFormatter.formatNumber(NUMBER_UK, NUMBER_UK)); - assertEquals(NUMBER_CH, PhoneNumberFormatter.formatNumber(NUMBER_CH, NUMBER_CH)); - assertEquals(NUMBER_DE, PhoneNumberFormatter.formatNumber(NUMBER_DE, NUMBER_DE)); - assertEquals(NUMBER_MOBILE_DE, PhoneNumberFormatter.formatNumber(NUMBER_MOBILE_DE, NUMBER_DE)); - - assertEquals(NUMBER_UK, PhoneNumberFormatter.formatNumber("+4402079460018", LOCAL_NUMBER_US)); - } -}