Improve and centralize e164 utils.

This commit is contained in:
Greyson Parrelli
2025-03-03 10:42:21 -05:00
parent 0fdcc1c027
commit 9c473fb570
99 changed files with 748 additions and 1826 deletions

View File

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

View File

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

View File

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

View File

@@ -58,7 +58,7 @@ public class PushContactSelectionActivity extends ContactSelectionActivity {
protected final void onFinishedSelection() {
Intent resultIntent = getIntent();
List<SelectedContact> selectedContacts = contactsFragment.getSelectedContacts();
List<RecipientId> recipients = Stream.of(selectedContacts).map(sc -> sc.getOrCreateRecipientId(this)).toList();
List<RecipientId> recipients = Stream.of(selectedContacts).map(sc -> sc.getOrCreateRecipientId()).toList();
resultIntent.putParcelableArrayListExtra(KEY_SELECTED_RECIPIENTS, new ArrayList<>(recipients));

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@@ -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<RecipientId> {
return Single.fromCallable {
selectedContact.getOrCreateRecipientId(AppDependencies.application)
selectedContact.getOrCreateRecipientId()
}
}
}

View File

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

View File

@@ -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<String> 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<String> knownSystemE164s = SignalDatabase.recipients().getAllE164s();
Set<String> unknownSystemE164s = SetUtil.difference(allSystemE164s, knownSystemE164s);
@@ -71,7 +72,8 @@ public class ContactsSyncAdapter extends AbstractThreadedSyncAdapter {
} else if (unknownSystemE164s.size() > 0) {
List<Recipient> 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.");

View File

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

View File

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

View File

@@ -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<String> = SignalDatabase.recipients.getAllE164s().sanitize()
val systemE164s: Set<String> = SystemContactsRepository.getAllDisplayNumbers(context).toE164s(context).sanitize()
val systemE164s: Set<String> = SystemContactsRepository.getAllDisplayNumbers(context).toE164s().sanitize()
return refreshInternal(
recipientE164s = recipientE164s,
@@ -246,8 +246,8 @@ object ContactDiscoveryRefreshV2 {
.toSet()
}
private fun Set<String>.toE164s(context: Context): Set<String> {
return this.map { PhoneNumberFormatter.get(context).format(it) }.toSet()
private fun Set<String>.toE164s(): Set<String> {
return this.mapNotNull { SignalE164Util.formatAsE164(it) }.toSet()
}
private fun Set<String>.sanitize(): Set<String> {

View File

@@ -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<RecipientId> getRecipients(@NonNull Context context, @NonNull Contact contact) {
public static List<RecipientId> 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());

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@@ -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<ArrayList<Map<String, String>>> {
public CountryListLoader(Context context) {
super(context);
}
@Override
public ArrayList<Map<String, String>> loadInBackground() {
Set<String> regions = PhoneNumberUtil.getInstance().getSupportedRegions();
ArrayList<Map<String, String>> results = new ArrayList<>(regions.size());
for (String region : regions) {
Map<String, String> 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<Map<String, String>> {
private final Collator collator;
RegionComparator() {
collator = Collator.getInstance();
collator.setStrength(Collator.PRIMARY);
}
@Override
public int compare(Map<String, String> lhs, Map<String, String> rhs) {
String a = lhs.get("country_name");
String b = rhs.get("country_name");
return collator.compare(a, b);
}
}
}

View File

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

View File

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

View File

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

View File

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

View File

@@ -23,7 +23,7 @@ final class AddMembersRepository {
@WorkerThread
RecipientId getOrCreateRecipientId(@NonNull SelectedContact selectedContact) {
return selectedContact.getOrCreateRecipientId(context);
return selectedContact.getOrCreateRecipientId();
}
@WorkerThread

View File

@@ -159,7 +159,7 @@ public final class AddToGroupsActivity extends ContactSelectionActivity {
private void handleNextPressed() {
List<RecipientId> groupsRecipientIds = Stream.of(contactsFragment.getSelectedContacts())
.map(selectedContact -> selectedContact.getOrCreateRecipientId(this))
.map(selectedContact -> selectedContact.getOrCreateRecipientId())
.toList();
viewModel.onContinueWithSelection(groupsRecipientIds);

View File

@@ -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<RecipientId> ids = contactsFragment.getSelectedContacts()
.stream()
.map(selectedContact -> selectedContact.getOrCreateRecipientId(this))
.map(selectedContact -> selectedContact.getOrCreateRecipientId())
.collect(Collectors.toList());
List<Recipient> resolved = Recipient.resolvedList(ids);

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@@ -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<JobMigration> 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(),

View File

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

View File

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

View File

@@ -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<SendReadReceiptJob> {
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<String> rawMessageIds = data.hasStringArray(KEY_MESSAGE_IDS) ? data.getStringArrayAsList(KEY_MESSAGE_IDS) : Collections.emptyList();
List<MessageId> 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);

View File

@@ -248,8 +248,7 @@ public class SendViewedReceiptJob extends BaseJob {
List<String> rawMessageIds = data.hasStringArray(KEY_MESSAGE_IDS) ? data.getStringArrayAsList(KEY_MESSAGE_IDS) : Collections.emptyList();
List<MessageId> 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);
}

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@@ -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<String> EXCLUDE_FROM_MANUAL_SHORTCODE_4 = SetUtil.newHashSet("AC", "NC", "NU", "TK");
private static final Set<Integer> 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<Pair<String, PhoneNumberFormatter>> cachedFormatter = new AtomicReference<>();
private final Optional<PhoneNumber> 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<String, PhoneNumberFormatter> 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<PhoneNumber> 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<PhoneNumber> 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<String> 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<String> getAreaCode() {
return areaCode;
}
}
}

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@@ -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<String, E164Util.Formatter> = 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
}
}
}

View File

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

View File

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

View File

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

View File

@@ -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<LiveRecipient>(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<Application>())
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<Application>())
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<Application>())
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<Application>())
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<Application>())
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<Application>())
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<Application>())
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<Application>())
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<Application>())
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<Application>())
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<Application>())
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<Application>())
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<Recipient> {
every { this@mockk.id } returns RecipientId.from(id)
}
}
}

View File

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

View File

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

View File

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

View File

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

View File

@@ -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<String>, e164Formatter: (String) -> String): ContactIterator {
fun getContactDetailsByQueries(context: Context, queries: List<String>, e164Formatter: (String) -> String?): ContactIterator {
val lookupKeys: MutableSet<String> = mutableSetOf()
for (query in queries) {
@@ -560,7 +560,7 @@ object SystemContactsRepository {
)
}
private fun getLinkedContactsByE164(context: Context, account: Account, e164Formatter: (String) -> String): Map<String, LinkedContactDetails> {
private fun getLinkedContactsByE164(context: Context, account: Account, e164Formatter: (String) -> String?): Map<String, LinkedContactDetails> {
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)
)

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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