From fd9c420dc8dcf920c1993e256a1139a7c2e2c368 Mon Sep 17 00:00:00 2001 From: Alex Hart Date: Wed, 3 Mar 2021 09:37:30 -0400 Subject: [PATCH] Split system names into first / last. --- .../securesms/contacts/ContactAccessor.java | 108 +++++++++--------- .../securesms/contacts/ContactRepository.java | 2 +- .../contacts/sync/ContactHolder.java | 53 +++++++++ .../contacts/sync/DirectoryHelper.java | 87 +++++++++++--- .../contacts/sync/PhoneNumberRecord.java | 99 ++++++++++++++++ .../contacts/sync/StructuredNameRecord.java | 46 ++++++++ .../securesms/database/RecipientDatabase.java | 95 ++++++++------- .../database/helpers/SQLCipherOpenHelper.java | 9 +- .../insights/InsightsRepository.java | 2 +- .../jobs/MultiDeviceContactUpdateJob.java | 4 +- .../migrations/ApplicationMigrations.java | 5 + .../securesms/profiles/ProfileName.java | 7 ++ .../edit/EditGroupProfileRepository.java | 2 +- .../securesms/recipients/Recipient.java | 80 ++++++++----- .../recipients/RecipientDetails.java | 16 +-- 15 files changed, 458 insertions(+), 157 deletions(-) create mode 100644 app/src/main/java/org/thoughtcrime/securesms/contacts/sync/ContactHolder.java create mode 100644 app/src/main/java/org/thoughtcrime/securesms/contacts/sync/PhoneNumberRecord.java create mode 100644 app/src/main/java/org/thoughtcrime/securesms/contacts/sync/StructuredNameRecord.java diff --git a/app/src/main/java/org/thoughtcrime/securesms/contacts/ContactAccessor.java b/app/src/main/java/org/thoughtcrime/securesms/contacts/ContactAccessor.java index aa104dc4d2..81eca1d7b3 100644 --- a/app/src/main/java/org/thoughtcrime/securesms/contacts/ContactAccessor.java +++ b/app/src/main/java/org/thoughtcrime/securesms/contacts/ContactAccessor.java @@ -23,28 +23,37 @@ import android.database.MergeCursor; import android.net.Uri; import android.os.Parcel; import android.os.Parcelable; +import android.provider.ContactsContract; import android.provider.ContactsContract.CommonDataKinds.Phone; import android.provider.ContactsContract.Contacts; import android.provider.ContactsContract.PhoneLookup; import android.telephony.PhoneNumberUtils; import android.text.TextUtils; +import androidx.annotation.NonNull; + import com.annimon.stream.Stream; import org.thoughtcrime.securesms.R; +import org.thoughtcrime.securesms.contactshare.Contact; import org.thoughtcrime.securesms.database.DatabaseFactory; import org.thoughtcrime.securesms.database.GroupDatabase; import org.thoughtcrime.securesms.database.RecipientDatabase; import org.thoughtcrime.securesms.phonenumbers.PhoneNumberFormatter; +import org.thoughtcrime.securesms.profiles.ProfileName; import org.thoughtcrime.securesms.recipients.Recipient; +import org.thoughtcrime.securesms.util.CursorUtil; +import org.thoughtcrime.securesms.util.SqlUtil; import org.thoughtcrime.securesms.util.TextSecurePreferences; import org.thoughtcrime.securesms.util.Util; import java.util.ArrayList; import java.util.Collection; +import java.util.HashMap; import java.util.HashSet; import java.util.LinkedList; import java.util.List; +import java.util.Map; import java.util.Set; import static org.thoughtcrime.securesms.database.GroupDatabase.GroupRecord; @@ -65,6 +74,9 @@ public class ContactAccessor { public static final String PUSH_COLUMN = "push"; + private static final String GIVEN_NAME = ContactsContract.CommonDataKinds.StructuredName.GIVEN_NAME; + private static final String FAMILY_NAME = ContactsContract.CommonDataKinds.StructuredName.FAMILY_NAME; + private static final ContactAccessor instance = new ContactAccessor(); public static synchronized ContactAccessor getInstance() { @@ -85,55 +97,42 @@ public class ContactAccessor { return results; } + /** + * Gets and returns a cursor of data for all contacts, containing both phone number data and + * structured name data. + * + * Cursor rows are ordered as follows: + * + *
    + *
  1. Contact Lookup Key
  2. + *
  3. Mimetype
  4. + *
  5. id
  6. + *
+ * + * The lookup key is a fixed value that allows you to verify two rows in the database actually + * belong to the same contact, since the contact uri can be unstable (if a sync fails, say.) + * + * We order by id explicitly here for the same contact sync failure error, which could result in + * multiple structured name rows for the same user. By ordering by id DESC, we ensure we get + * whatever the latest input data was. + * + * What this results in is a cursor that looks like: + * + * Alice phone 1 + * Alice phone 2 + * Alice structured name 2 + * Alice structured name 1 + * Bob phone 1 + * ... etc. + */ public Cursor getAllSystemContacts(Context context) { - return context.getContentResolver().query(Phone.CONTENT_URI, new String[] {Phone.NUMBER, Phone.DISPLAY_NAME, Phone.LABEL, Phone.PHOTO_URI, Phone._ID, Phone.LOOKUP_KEY, Phone.TYPE}, null, null, null); - } + Uri uri = ContactsContract.Data.CONTENT_URI; + String[] projection = SqlUtil.buildArgs(ContactsContract.Data.MIMETYPE, Phone.NUMBER, Phone.DISPLAY_NAME, Phone.LABEL, Phone.PHOTO_URI, Phone._ID, Phone.LOOKUP_KEY, Phone.TYPE, GIVEN_NAME, FAMILY_NAME); + String where = ContactsContract.Data.MIMETYPE + " IN (?, ?)"; + String[] args = SqlUtil.buildArgs(Phone.CONTENT_ITEM_TYPE, ContactsContract.CommonDataKinds.StructuredName.CONTENT_ITEM_TYPE); + String orderBy = Phone.LOOKUP_KEY + " ASC, " + ContactsContract.Data.MIMETYPE + " DESC, " + ContactsContract.CommonDataKinds.Phone._ID + " DESC"; - public boolean isSystemContact(Context context, String number) { - Uri uri = Uri.withAppendedPath(PhoneLookup.CONTENT_FILTER_URI, Uri.encode(number)); - String[] projection = new String[]{PhoneLookup.DISPLAY_NAME, PhoneLookup.LOOKUP_KEY, - PhoneLookup._ID, PhoneLookup.NUMBER}; - Cursor cursor = context.getContentResolver().query(uri, projection, null, null, null); - - try { - if (cursor != null && cursor.moveToFirst()) { - return true; - } - } finally { - if (cursor != null) cursor.close(); - } - - return false; - } - - public Collection getContactsWithPush(Context context) { - final ContentResolver resolver = context.getContentResolver(); - final String[] inProjection = new String[]{PhoneLookup._ID, PhoneLookup.DISPLAY_NAME}; - - final List registeredAddresses = Stream.of(DatabaseFactory.getRecipientDatabase(context).getRegistered()) - .map(Recipient::resolved) - .filter(r -> r.getE164().isPresent()) - .map(Recipient::requireE164) - .toList(); - final Collection lookupData = new ArrayList<>(registeredAddresses.size()); - - for (String registeredAddress : registeredAddresses) { - Uri uri = Uri.withAppendedPath(PhoneLookup.CONTENT_FILTER_URI, Uri.encode(registeredAddress)); - Cursor lookupCursor = resolver.query(uri, inProjection, null, null, null); - - try { - if (lookupCursor != null && lookupCursor.moveToFirst()) { - final ContactData contactData = new ContactData(lookupCursor.getLong(0), lookupCursor.getString(1)); - contactData.numbers.add(new NumberData("TextSecure", registeredAddress)); - lookupData.add(contactData); - } - } finally { - if (lookupCursor != null) - lookupCursor.close(); - } - } - - return lookupData; + return context.getContentResolver().query(uri, projection, where, args, orderBy); } public String getNameFromContact(Context context, Uri uri) { @@ -160,13 +159,13 @@ public class ContactAccessor { private ContactData getContactData(Context context, String displayName, long id) { ContactData contactData = new ContactData(id, displayName); - Cursor numberCursor = null; - - try { - numberCursor = context.getContentResolver().query(Phone.CONTENT_URI, null, - Phone.CONTACT_ID + " = ?", - new String[] {contactData.id + ""}, null); + try (Cursor numberCursor = context.getContentResolver().query(Phone.CONTENT_URI, + null, + Phone.CONTACT_ID + " = ?", + new String[] {contactData.id + ""}, + null)) + { while (numberCursor != null && numberCursor.moveToNext()) { int type = numberCursor.getInt(numberCursor.getColumnIndexOrThrow(Phone.TYPE)); String label = numberCursor.getString(numberCursor.getColumnIndexOrThrow(Phone.LABEL)); @@ -175,9 +174,6 @@ public class ContactAccessor { contactData.numbers.add(new NumberData(typeLabel, number)); } - } finally { - if (numberCursor != null) - numberCursor.close(); } return contactData; diff --git a/app/src/main/java/org/thoughtcrime/securesms/contacts/ContactRepository.java b/app/src/main/java/org/thoughtcrime/securesms/contacts/ContactRepository.java index 3696efdc17..7c614b1388 100644 --- a/app/src/main/java/org/thoughtcrime/securesms/contacts/ContactRepository.java +++ b/app/src/main/java/org/thoughtcrime/securesms/contacts/ContactRepository.java @@ -57,7 +57,7 @@ public class ContactRepository { add(new Pair<>(ID_COLUMN, cursor -> CursorUtil.requireLong(cursor, RecipientDatabase.ID))); add(new Pair<>(NAME_COLUMN, cursor -> { - String system = CursorUtil.requireString(cursor, RecipientDatabase.SYSTEM_DISPLAY_NAME); + String system = CursorUtil.requireString(cursor, RecipientDatabase.SYSTEM_JOINED_NAME); String profile = CursorUtil.requireString(cursor, RecipientDatabase.SEARCH_PROFILE_NAME); return Util.getFirstNonEmpty(system, profile); diff --git a/app/src/main/java/org/thoughtcrime/securesms/contacts/sync/ContactHolder.java b/app/src/main/java/org/thoughtcrime/securesms/contacts/sync/ContactHolder.java new file mode 100644 index 0000000000..4a84052523 --- /dev/null +++ b/app/src/main/java/org/thoughtcrime/securesms/contacts/sync/ContactHolder.java @@ -0,0 +1,53 @@ +package org.thoughtcrime.securesms.contacts.sync; + +import androidx.annotation.NonNull; + +import org.thoughtcrime.securesms.database.RecipientDatabase; +import org.thoughtcrime.securesms.profiles.ProfileName; + +import java.util.LinkedList; +import java.util.List; + +final class ContactHolder { + + private final String lookupKey; + private final List phoneNumberRecords = new LinkedList<>(); + + private StructuredNameRecord structuredNameRecord; + + ContactHolder(@NonNull String lookupKey) { + this.lookupKey = lookupKey; + } + + @NonNull String getLookupKey() { + return lookupKey; + } + + public void addPhoneNumberRecord(@NonNull PhoneNumberRecord phoneNumberRecord) { + phoneNumberRecords.add(phoneNumberRecord); + } + + public void setStructuredNameRecord(@NonNull StructuredNameRecord structuredNameRecord) { + this.structuredNameRecord = structuredNameRecord; + } + + void commit(@NonNull RecipientDatabase.BulkOperationsHandle handle) { + for (PhoneNumberRecord phoneNumberRecord : phoneNumberRecords) { + handle.setSystemContactInfo(phoneNumberRecord.getRecipientId(), + getProfileName(phoneNumberRecord.getDisplayName()), + phoneNumberRecord.getContactPhotoUri(), + phoneNumberRecord.getContactLabel(), + phoneNumberRecord.getPhoneType(), + phoneNumberRecord.getContactUri().toString()); + } + } + + private @NonNull ProfileName getProfileName(@NonNull String displayName) { + if (structuredNameRecord.hasGivenName()) { + return structuredNameRecord.asProfileName(); + } else { + return ProfileName.asGiven(displayName); + } + } + +} diff --git a/app/src/main/java/org/thoughtcrime/securesms/contacts/sync/DirectoryHelper.java b/app/src/main/java/org/thoughtcrime/securesms/contacts/sync/DirectoryHelper.java index dbf7015954..128775eee1 100644 --- a/app/src/main/java/org/thoughtcrime/securesms/contacts/sync/DirectoryHelper.java +++ b/app/src/main/java/org/thoughtcrime/securesms/contacts/sync/DirectoryHelper.java @@ -7,7 +7,6 @@ import android.content.ContentResolver; import android.content.Context; import android.content.OperationApplicationException; import android.database.Cursor; -import android.net.Uri; import android.os.RemoteException; import android.provider.ContactsContract; import android.text.TextUtils; @@ -43,6 +42,7 @@ import org.thoughtcrime.securesms.recipients.RecipientId; import org.thoughtcrime.securesms.registration.RegistrationUtil; import org.thoughtcrime.securesms.sms.IncomingJoinedMessage; import org.thoughtcrime.securesms.storage.StorageSyncHelper; +import org.thoughtcrime.securesms.util.CursorUtil; import org.thoughtcrime.securesms.util.ProfileUtil; import org.thoughtcrime.securesms.util.SetUtil; import org.thoughtcrime.securesms.util.Stopwatch; @@ -63,6 +63,7 @@ import java.util.Collections; import java.util.HashSet; import java.util.List; import java.util.Map; +import java.util.Objects; import java.util.Set; import java.util.UUID; import java.util.concurrent.ExecutionException; @@ -320,27 +321,63 @@ public class DirectoryHelper { contactsDatabase.removeDeletedRawContacts(account.getAccount()); contactsDatabase.setRegisteredUsers(account.getAccount(), activeAddresses, removeMissing); - Cursor cursor = ContactAccessor.getInstance().getAllSystemContacts(context); - BulkOperationsHandle handle = recipientDatabase.beginBulkSystemContactUpdate(); + BulkOperationsHandle handle = recipientDatabase.beginBulkSystemContactUpdate(); - try { + ContactHolder old = null; + try (Cursor cursor = ContactAccessor.getInstance().getAllSystemContacts(context)) { while (cursor != null && cursor.moveToNext()) { - String number = cursor.getString(cursor.getColumnIndexOrThrow(ContactsContract.CommonDataKinds.Phone.NUMBER)); + String lookupKey = getLookupKey(cursor); + String mimeType = getMimeType(cursor); + ContactHolder contactHolder = new ContactHolder(lookupKey); - if (isValidContactNumber(number)) { - String formattedNumber = PhoneNumberFormatter.get(context).format(number); - String realNumber = Util.getFirstNonEmpty(rewrites.get(formattedNumber), formattedNumber); - RecipientId recipientId = Recipient.externalContact(context, realNumber).getId(); - String displayName = cursor.getString(cursor.getColumnIndexOrThrow(ContactsContract.CommonDataKinds.Phone.DISPLAY_NAME)); - String contactPhotoUri = cursor.getString(cursor.getColumnIndexOrThrow(ContactsContract.CommonDataKinds.Phone.PHOTO_URI)); - String contactLabel = cursor.getString(cursor.getColumnIndexOrThrow(ContactsContract.CommonDataKinds.Phone.LABEL)); - int phoneType = cursor.getInt(cursor.getColumnIndexOrThrow(ContactsContract.CommonDataKinds.Phone.TYPE)); - Uri contactUri = ContactsContract.Contacts.getLookupUri(cursor.getLong(cursor.getColumnIndexOrThrow(ContactsContract.CommonDataKinds.Phone._ID)), - cursor.getString(cursor.getColumnIndexOrThrow(ContactsContract.CommonDataKinds.Phone.LOOKUP_KEY))); - - handle.setSystemContactInfo(recipientId, displayName, contactPhotoUri, contactLabel, phoneType, contactUri.toString()); + if (!isPhoneMimeType(mimeType)) { + Log.w(TAG, "Ignoring unexpected mime type: " + mimeType); } + + while (getLookupKey(cursor).equals(lookupKey) && isPhoneMimeType(getMimeType(cursor))) { + String number = CursorUtil.requireString(cursor, ContactsContract.CommonDataKinds.Phone.NUMBER); + + if (isValidContactNumber(number)) { + String formattedNumber = PhoneNumberFormatter.get(context).format(number); + String realNumber = Util.getFirstNonEmpty(rewrites.get(formattedNumber), formattedNumber); + + PhoneNumberRecord.Builder builder = new PhoneNumberRecord.Builder(); + + builder.withRecipientId(Recipient.externalContact(context, realNumber).getId()); + builder.withDisplayName(CursorUtil.requireString(cursor, ContactsContract.CommonDataKinds.Phone.DISPLAY_NAME)); + builder.withContactPhotoUri(CursorUtil.requireString(cursor, ContactsContract.CommonDataKinds.Phone.PHOTO_URI)); + builder.withContactLabel(CursorUtil.requireString(cursor, ContactsContract.CommonDataKinds.Phone.LABEL)); + builder.withPhoneType(CursorUtil.requireInt(cursor, ContactsContract.CommonDataKinds.Phone.TYPE)); + builder.withContactUri(ContactsContract.Contacts.getLookupUri(CursorUtil.requireLong(cursor, ContactsContract.CommonDataKinds.Phone._ID), + CursorUtil.requireString(cursor, ContactsContract.CommonDataKinds.Phone.LOOKUP_KEY))); + + contactHolder.addPhoneNumberRecord(builder.build()); + } else { + Log.w(TAG, "Skipping phone entry with invalid number"); + } + + cursor.moveToNext(); + } + + if (getLookupKey(cursor).equals(lookupKey)) { + if (isStructuredNameMimeType(getMimeType(cursor))) { + StructuredNameRecord.Builder builder = new StructuredNameRecord.Builder(); + + builder.withGivenName(CursorUtil.requireString(cursor, ContactsContract.CommonDataKinds.StructuredName.GIVEN_NAME)); + builder.withFamilyName(CursorUtil.requireString(cursor, ContactsContract.CommonDataKinds.StructuredName.FAMILY_NAME)); + + contactHolder.setStructuredNameRecord(builder.build()); + } else { + Log.i(TAG, "Skipping invalid mimeType " + mimeType); + } + } else { + Log.i(TAG, "No structured name for user, rolling back cursor."); + cursor.moveToPrevious(); + } + + contactHolder.commit(handle); } + } finally { handle.finish(); } @@ -358,10 +395,26 @@ public class DirectoryHelper { } } + private static boolean isPhoneMimeType(@NonNull String mimeType) { + return ContactsContract.CommonDataKinds.Phone.CONTENT_ITEM_TYPE.equals(mimeType); + } + + private static boolean isStructuredNameMimeType(@NonNull String mimeType) { + return ContactsContract.CommonDataKinds.StructuredName.CONTENT_ITEM_TYPE.equals(mimeType); + } + private static boolean isValidContactNumber(@Nullable String number) { return !TextUtils.isEmpty(number) && !UuidUtil.isUuid(number); } + private static @NonNull String getLookupKey(@NonNull Cursor cursor) { + return Objects.requireNonNull(CursorUtil.requireString(cursor, ContactsContract.CommonDataKinds.Phone.LOOKUP_KEY)); + } + + private static @NonNull String getMimeType(@NonNull Cursor cursor) { + return CursorUtil.requireString(cursor, ContactsContract.Data.MIMETYPE); + } + private static @Nullable AccountHolder getOrCreateSystemAccount(Context context) { AccountManager accountManager = AccountManager.get(context); Account[] accounts = accountManager.getAccountsByType(BuildConfig.APPLICATION_ID); diff --git a/app/src/main/java/org/thoughtcrime/securesms/contacts/sync/PhoneNumberRecord.java b/app/src/main/java/org/thoughtcrime/securesms/contacts/sync/PhoneNumberRecord.java new file mode 100644 index 0000000000..0393f89410 --- /dev/null +++ b/app/src/main/java/org/thoughtcrime/securesms/contacts/sync/PhoneNumberRecord.java @@ -0,0 +1,99 @@ +package org.thoughtcrime.securesms.contacts.sync; + +import android.net.Uri; + +import androidx.annotation.NonNull; +import androidx.annotation.Nullable; + +import org.thoughtcrime.securesms.recipients.RecipientId; + +import java.util.Objects; + +/** + * Represents all the data we pull from a Phone data cursor row from the contacts database. + */ +final class PhoneNumberRecord { + + private final RecipientId recipientId; + private final String displayName; + private final String contactPhotoUri; + private final String contactLabel; + private final int phoneType; + private final Uri contactUri; + + private PhoneNumberRecord(@NonNull PhoneNumberRecord.Builder builder) { + recipientId = Objects.requireNonNull(builder.recipientId); + displayName = builder.displayName; + contactPhotoUri = builder.contactPhotoUri; + contactLabel = builder.contactLabel; + phoneType = builder.phoneType; + contactUri = builder.contactUri; + } + + @NonNull RecipientId getRecipientId() { + return recipientId; + } + + @Nullable String getDisplayName() { + return displayName; + } + + @Nullable String getContactPhotoUri() { + return contactPhotoUri; + } + + @Nullable String getContactLabel() { + return contactLabel; + } + + int getPhoneType() { + return phoneType; + } + + @Nullable Uri getContactUri() { + return contactUri; + } + + final static class Builder { + private RecipientId recipientId; + private String displayName; + private String contactPhotoUri; + private String contactLabel; + private int phoneType; + private Uri contactUri; + + @NonNull Builder withRecipientId(@NonNull RecipientId recipientId) { + this.recipientId = recipientId; + return this; + } + + @NonNull Builder withDisplayName(@Nullable String displayName) { + this.displayName = displayName; + return this; + } + + @NonNull Builder withContactUri(@Nullable Uri contactUri) { + this.contactUri = contactUri; + return this; + } + + @NonNull Builder withContactLabel(@NonNull String contactLabel) { + this.contactLabel = contactLabel; + return this; + } + + @NonNull Builder withContactPhotoUri(@NonNull String contactPhotoUri) { + this.contactPhotoUri = contactPhotoUri; + return this; + } + + @NonNull Builder withPhoneType(int phoneType) { + this.phoneType = phoneType; + return this; + } + + @NonNull PhoneNumberRecord build() { + return new PhoneNumberRecord(this); + } + } +} diff --git a/app/src/main/java/org/thoughtcrime/securesms/contacts/sync/StructuredNameRecord.java b/app/src/main/java/org/thoughtcrime/securesms/contacts/sync/StructuredNameRecord.java new file mode 100644 index 0000000000..bfcacd2a73 --- /dev/null +++ b/app/src/main/java/org/thoughtcrime/securesms/contacts/sync/StructuredNameRecord.java @@ -0,0 +1,46 @@ +package org.thoughtcrime.securesms.contacts.sync; + +import androidx.annotation.NonNull; +import androidx.annotation.Nullable; + +import org.thoughtcrime.securesms.profiles.ProfileName; + +/** + * Represents the data pulled from a StructuredName row of a Contacts data cursor. + */ +final class StructuredNameRecord { + private final String givenName; + private final String familyName; + + StructuredNameRecord(@NonNull StructuredNameRecord.Builder builder) { + this.givenName = builder.givenName; + this.familyName = builder.familyName; + } + + public boolean hasGivenName() { + return givenName != null; + } + + public @NonNull ProfileName asProfileName() { + return ProfileName.fromParts(givenName, familyName); + } + + final static class Builder { + private String givenName; + private String familyName; + + @NonNull Builder withGivenName(@Nullable String givenName) { + this.givenName = givenName; + return this; + } + + @NonNull Builder withFamilyName(@Nullable String familyName) { + this.familyName = familyName; + return this; + } + + @NonNull StructuredNameRecord build() { + return new StructuredNameRecord(this); + } + } +} diff --git a/app/src/main/java/org/thoughtcrime/securesms/database/RecipientDatabase.java b/app/src/main/java/org/thoughtcrime/securesms/database/RecipientDatabase.java index 7a9a806155..b63de248e2 100644 --- a/app/src/main/java/org/thoughtcrime/securesms/database/RecipientDatabase.java +++ b/app/src/main/java/org/thoughtcrime/securesms/database/RecipientDatabase.java @@ -39,7 +39,6 @@ import org.thoughtcrime.securesms.groups.v2.processing.GroupsV2StateProcessor; import org.thoughtcrime.securesms.jobs.RefreshAttributesJob; import org.thoughtcrime.securesms.jobs.RequestGroupV2InfoJob; import org.thoughtcrime.securesms.jobs.RetrieveProfileJob; -import org.thoughtcrime.securesms.mms.PartAuthority; import org.thoughtcrime.securesms.profiles.AvatarHelper; import org.thoughtcrime.securesms.profiles.ProfileName; import org.thoughtcrime.securesms.recipients.Recipient; @@ -113,7 +112,9 @@ public class RecipientDatabase extends Database { private static final String DEFAULT_SUBSCRIPTION_ID = "default_subscription_id"; private static final String MESSAGE_EXPIRATION_TIME = "message_expiration_time"; public static final String REGISTERED = "registered"; - public static final String SYSTEM_DISPLAY_NAME = "system_display_name"; + public static final String SYSTEM_JOINED_NAME = "system_display_name"; + public static final String SYSTEM_FAMILY_NAME = "system_family_name"; + public static final String SYSTEM_GIVEN_NAME = "system_given_name"; private static final String SYSTEM_PHOTO_URI = "system_photo_uri"; public static final String SYSTEM_PHONE_TYPE = "system_phone_type"; public static final String SYSTEM_PHONE_LABEL = "system_phone_label"; @@ -157,7 +158,7 @@ public class RecipientDatabase extends Database { ID, UUID, USERNAME, PHONE, EMAIL, GROUP_ID, GROUP_TYPE, BLOCKED, MESSAGE_RINGTONE, CALL_RINGTONE, MESSAGE_VIBRATE, CALL_VIBRATE, MUTE_UNTIL, COLOR, SEEN_INVITE_REMINDER, DEFAULT_SUBSCRIPTION_ID, MESSAGE_EXPIRATION_TIME, REGISTERED, PROFILE_KEY, PROFILE_KEY_CREDENTIAL, - SYSTEM_DISPLAY_NAME, SYSTEM_PHOTO_URI, SYSTEM_PHONE_LABEL, SYSTEM_PHONE_TYPE, SYSTEM_CONTACT_URI, + SYSTEM_GIVEN_NAME, SYSTEM_FAMILY_NAME, SYSTEM_PHOTO_URI, SYSTEM_PHONE_LABEL, SYSTEM_PHONE_TYPE, SYSTEM_CONTACT_URI, PROFILE_GIVEN_NAME, PROFILE_FAMILY_NAME, SIGNAL_PROFILE_AVATAR, PROFILE_SHARING, LAST_PROFILE_FETCH, NOTIFICATION_CHANNEL, UNIDENTIFIED_ACCESS_MODE, @@ -170,15 +171,15 @@ public class RecipientDatabase extends Database { }; private static final String[] ID_PROJECTION = new String[]{ID}; - private static final String[] SEARCH_PROJECTION = new String[]{ID, SYSTEM_DISPLAY_NAME, PHONE, EMAIL, SYSTEM_PHONE_LABEL, SYSTEM_PHONE_TYPE, REGISTERED, ABOUT, ABOUT_EMOJI, "COALESCE(" + nullIfEmpty(PROFILE_JOINED_NAME) + ", " + nullIfEmpty(PROFILE_GIVEN_NAME) + ") AS " + SEARCH_PROFILE_NAME, "COALESCE(" + nullIfEmpty(SYSTEM_DISPLAY_NAME) + ", " + nullIfEmpty(PROFILE_JOINED_NAME) + ", " + nullIfEmpty(PROFILE_GIVEN_NAME) + ", " + nullIfEmpty(USERNAME) + ") AS " + SORT_NAME}; - public static final String[] SEARCH_PROJECTION_NAMES = new String[]{ID, SYSTEM_DISPLAY_NAME, PHONE, EMAIL, SYSTEM_PHONE_LABEL, SYSTEM_PHONE_TYPE, REGISTERED, ABOUT, ABOUT_EMOJI, SEARCH_PROFILE_NAME, SORT_NAME}; + private static final String[] SEARCH_PROJECTION = new String[]{ID, SYSTEM_JOINED_NAME, PHONE, EMAIL, SYSTEM_PHONE_LABEL, SYSTEM_PHONE_TYPE, REGISTERED, ABOUT, ABOUT_EMOJI, "COALESCE(" + nullIfEmpty(PROFILE_JOINED_NAME) + ", " + nullIfEmpty(PROFILE_GIVEN_NAME) + ") AS " + SEARCH_PROFILE_NAME, "COALESCE(" + nullIfEmpty(SYSTEM_JOINED_NAME) + ", " + nullIfEmpty(SYSTEM_GIVEN_NAME) + ", " + nullIfEmpty(PROFILE_JOINED_NAME) + ", " + nullIfEmpty(PROFILE_GIVEN_NAME) + ", " + nullIfEmpty(USERNAME) + ") AS " + SORT_NAME}; + public static final String[] SEARCH_PROJECTION_NAMES = new String[]{ID, SYSTEM_JOINED_NAME, PHONE, EMAIL, SYSTEM_PHONE_LABEL, SYSTEM_PHONE_TYPE, REGISTERED, ABOUT, ABOUT_EMOJI, SEARCH_PROFILE_NAME, SORT_NAME}; private static final String[] TYPED_RECIPIENT_PROJECTION = Stream.of(RECIPIENT_PROJECTION) .map(columnName -> TABLE_NAME + "." + columnName) .toList().toArray(new String[0]); static final String[] TYPED_RECIPIENT_PROJECTION_NO_ID = Arrays.copyOfRange(TYPED_RECIPIENT_PROJECTION, 1, TYPED_RECIPIENT_PROJECTION.length); - private static final String[] MENTION_SEARCH_PROJECTION = new String[]{ID, removeWhitespace("COALESCE(" + nullIfEmpty(SYSTEM_DISPLAY_NAME) + ", " + nullIfEmpty(PROFILE_JOINED_NAME) + ", " + nullIfEmpty(PROFILE_GIVEN_NAME) + ", " + nullIfEmpty(USERNAME) + ", " + nullIfEmpty(PHONE) + ")") + " AS " + SORT_NAME}; + private static final String[] MENTION_SEARCH_PROJECTION = new String[]{ID, removeWhitespace("COALESCE(" + nullIfEmpty(SYSTEM_JOINED_NAME) + ", " + nullIfEmpty(SYSTEM_GIVEN_NAME) + ", " + nullIfEmpty(PROFILE_JOINED_NAME) + ", " + nullIfEmpty(PROFILE_GIVEN_NAME) + ", " + nullIfEmpty(USERNAME) + ", " + nullIfEmpty(PHONE) + ")") + " AS " + SORT_NAME}; public static final String[] CREATE_INDEXS = new String[] { "CREATE INDEX IF NOT EXISTS recipient_dirty_index ON " + TABLE_NAME + " (" + DIRTY + ");", @@ -339,7 +340,9 @@ public class RecipientDatabase extends Database { DEFAULT_SUBSCRIPTION_ID + " INTEGER DEFAULT -1, " + MESSAGE_EXPIRATION_TIME + " INTEGER DEFAULT 0, " + REGISTERED + " INTEGER DEFAULT " + RegisteredState.UNKNOWN.getId() + ", " + - SYSTEM_DISPLAY_NAME + " TEXT DEFAULT NULL, " + + SYSTEM_GIVEN_NAME + " TEXT DEFAULT NULL, " + + SYSTEM_FAMILY_NAME + " TEXT DEFAULT NULL, " + + SYSTEM_JOINED_NAME + " TEXT DEFAULT NULL, " + SYSTEM_PHOTO_URI + " TEXT DEFAULT NULL, " + SYSTEM_PHONE_LABEL + " TEXT DEFAULT NULL, " + SYSTEM_PHONE_TYPE + " INTEGER DEFAULT -1, " + @@ -1264,7 +1267,8 @@ public class RecipientDatabase extends Database { int registeredState = CursorUtil.requireInt(cursor, REGISTERED); String profileKeyString = CursorUtil.requireString(cursor, PROFILE_KEY); String profileKeyCredentialString = CursorUtil.requireString(cursor, PROFILE_KEY_CREDENTIAL); - String systemDisplayName = CursorUtil.requireString(cursor, SYSTEM_DISPLAY_NAME); + String systemGivenName = CursorUtil.requireString(cursor, SYSTEM_GIVEN_NAME); + String systemFamilyName = CursorUtil.requireString(cursor, SYSTEM_FAMILY_NAME); String systemContactPhoto = CursorUtil.requireString(cursor, SYSTEM_PHOTO_URI); String systemPhoneLabel = CursorUtil.requireString(cursor, SYSTEM_PHONE_LABEL); String systemContactUri = CursorUtil.requireString(cursor, SYSTEM_CONTACT_URI); @@ -1350,7 +1354,7 @@ public class RecipientDatabase extends Database { RegisteredState.fromId(registeredState), profileKey, profileKeyCredential, - systemDisplayName, + ProfileName.fromParts(systemGivenName, systemFamilyName), systemContactPhoto, systemPhoneLabel, systemContactUri, @@ -1743,7 +1747,7 @@ public class RecipientDatabase extends Database { public @NonNull List getSimilarRecipientIds(@NonNull Recipient recipient) { SQLiteDatabase db = databaseHelper.getReadableDatabase(); - String[] projection = SqlUtil.buildArgs(ID, "COALESCE(" + nullIfEmpty(SYSTEM_DISPLAY_NAME) + ", " + nullIfEmpty(PROFILE_JOINED_NAME) + ") AS checked_name"); + String[] projection = SqlUtil.buildArgs(ID, "COALESCE(" + nullIfEmpty(SYSTEM_JOINED_NAME) + ", " + nullIfEmpty(PROFILE_JOINED_NAME) + ") AS checked_name"); String where = "checked_name = ?"; String[] arguments = SqlUtil.buildArgs(recipient.getProfileName().toString()); @@ -2246,7 +2250,7 @@ public class RecipientDatabase extends Database { SQLiteDatabase db = databaseHelper.getReadableDatabase(); List results = new LinkedList<>(); - try (Cursor cursor = db.query(TABLE_NAME, ID_PROJECTION, SYSTEM_DISPLAY_NAME + " IS NOT NULL AND " + SYSTEM_DISPLAY_NAME + " != \"\"", null, null, null, null)) { + try (Cursor cursor = db.query(TABLE_NAME, ID_PROJECTION, SYSTEM_JOINED_NAME + " IS NOT NULL AND " + SYSTEM_JOINED_NAME + " != \"\"", null, null, null, null)) { while (cursor != null && cursor.moveToNext()) { results.add(RecipientId.from(cursor.getLong(cursor.getColumnIndexOrThrow(ID)))); } @@ -2260,10 +2264,10 @@ public class RecipientDatabase extends Database { Map updates = new HashMap<>(); db.beginTransaction(); - try (Cursor cursor = db.query(TABLE_NAME, new String[] {ID, COLOR, SYSTEM_DISPLAY_NAME}, SYSTEM_DISPLAY_NAME + " IS NOT NULL AND " + SYSTEM_DISPLAY_NAME + " != \"\"", null, null, null, null)) { + try (Cursor cursor = db.query(TABLE_NAME, new String[] {ID, COLOR, SYSTEM_JOINED_NAME}, SYSTEM_JOINED_NAME + " IS NOT NULL AND " + SYSTEM_JOINED_NAME + " != \"\"", null, null, null, null)) { while (cursor != null && cursor.moveToNext()) { long id = cursor.getLong(cursor.getColumnIndexOrThrow(ID)); - MaterialColor newColor = updater.update(cursor.getString(cursor.getColumnIndexOrThrow(SYSTEM_DISPLAY_NAME)), + MaterialColor newColor = updater.update(cursor.getString(cursor.getColumnIndexOrThrow(SYSTEM_JOINED_NAME)), cursor.getString(cursor.getColumnIndexOrThrow(COLOR))); ContentValues contentValues = new ContentValues(1); @@ -2284,7 +2288,7 @@ public class RecipientDatabase extends Database { String selection = BLOCKED + " = ? AND " + REGISTERED + " = ? AND " + GROUP_ID + " IS NULL AND " + - "(" + SYSTEM_DISPLAY_NAME + " NOT NULL OR " + PROFILE_SHARING + " = ?) AND " + + "(" + SYSTEM_JOINED_NAME + " NOT NULL OR " + PROFILE_SHARING + " = ?) AND " + "(" + SORT_NAME + " NOT NULL OR " + USERNAME + " NOT NULL)"; String[] args; @@ -2295,7 +2299,7 @@ public class RecipientDatabase extends Database { args = new String[] { "0", String.valueOf(RegisteredState.REGISTERED.getId()), "1", Recipient.self().getId().serialize() }; } - String orderBy = SORT_NAME + ", " + SYSTEM_DISPLAY_NAME + ", " + SEARCH_PROFILE_NAME + ", " + USERNAME + ", " + PHONE; + String orderBy = SORT_NAME + ", " + SYSTEM_JOINED_NAME + ", " + SEARCH_PROFILE_NAME + ", " + USERNAME + ", " + PHONE; return databaseHelper.getReadableDatabase().query(TABLE_NAME, SEARCH_PROJECTION, selection, args, null, null, orderBy); } @@ -2306,7 +2310,7 @@ public class RecipientDatabase extends Database { String selection = BLOCKED + " = ? AND " + REGISTERED + " = ? AND " + GROUP_ID + " IS NULL AND " + - "(" + SYSTEM_DISPLAY_NAME + " NOT NULL OR " + PROFILE_SHARING + " = ?) AND " + + "(" + SYSTEM_JOINED_NAME + " NOT NULL OR " + PROFILE_SHARING + " = ?) AND " + "(" + PHONE + " GLOB ? OR " + SORT_NAME + " GLOB ? OR " + @@ -2321,7 +2325,7 @@ public class RecipientDatabase extends Database { args = new String[] { "0", String.valueOf(RegisteredState.REGISTERED.getId()), "1", query, query, query, String.valueOf(Recipient.self().getId().toLong()) }; } - String orderBy = SORT_NAME + ", " + SYSTEM_DISPLAY_NAME + ", " + SEARCH_PROFILE_NAME + ", " + PHONE; + String orderBy = SORT_NAME + ", " + SYSTEM_JOINED_NAME + ", " + SEARCH_PROFILE_NAME + ", " + PHONE; return databaseHelper.getReadableDatabase().query(TABLE_NAME, SEARCH_PROJECTION, selection, args, null, null, orderBy); } @@ -2330,10 +2334,10 @@ public class RecipientDatabase extends Database { String selection = BLOCKED + " = ? AND " + REGISTERED + " != ? AND " + GROUP_ID + " IS NULL AND " + - SYSTEM_DISPLAY_NAME + " NOT NULL AND " + + SYSTEM_CONTACT_URI + " NOT NULL AND " + "(" + PHONE + " NOT NULL OR " + EMAIL + " NOT NULL)"; String[] args = new String[] { "0", String.valueOf(RegisteredState.REGISTERED.getId()) }; - String orderBy = SYSTEM_DISPLAY_NAME + ", " + PHONE; + String orderBy = SYSTEM_JOINED_NAME + ", " + PHONE; return databaseHelper.getReadableDatabase().query(TABLE_NAME, SEARCH_PROJECTION, selection, args, null, null, orderBy); } @@ -2344,15 +2348,15 @@ public class RecipientDatabase extends Database { String selection = BLOCKED + " = ? AND " + REGISTERED + " != ? AND " + GROUP_ID + " IS NULL AND " + - SYSTEM_DISPLAY_NAME + " NOT NULL AND " + + SYSTEM_CONTACT_URI + " NOT NULL AND " + "(" + PHONE + " NOT NULL OR " + EMAIL + " NOT NULL) AND " + "(" + - PHONE + " GLOB ? OR " + - EMAIL + " GLOB ? OR " + - SYSTEM_DISPLAY_NAME + " GLOB ?" + + PHONE + " GLOB ? OR " + + EMAIL + " GLOB ? OR " + + SYSTEM_JOINED_NAME + " GLOB ?" + ")"; String[] args = new String[] { "0", String.valueOf(RegisteredState.REGISTERED.getId()), query, query, query }; - String orderBy = SYSTEM_DISPLAY_NAME + ", " + PHONE; + String orderBy = SYSTEM_JOINED_NAME + ", " + PHONE; return databaseHelper.getReadableDatabase().query(TABLE_NAME, SEARCH_PROJECTION, selection, args, null, null, orderBy); } @@ -2429,7 +2433,7 @@ public class RecipientDatabase extends Database { String selection = REGISTERED + " = ? AND " + GROUP_ID + " IS NULL AND " + ID + " != ? AND " + - "(" + SYSTEM_DISPLAY_NAME + " NOT NULL OR " + ID + " IN (" + subquery + "))"; + "(" + SYSTEM_CONTACT_URI + " NOT NULL OR " + ID + " IN (" + subquery + "))"; String[] args = new String[] { String.valueOf(RegisteredState.REGISTERED.getId()), Recipient.self().getId().serialize() }; List recipients = new ArrayList<>(); @@ -2729,7 +2733,9 @@ public class RecipientDatabase extends Database { uuidValues.put(DEFAULT_SUBSCRIPTION_ID, e164Settings.getDefaultSubscriptionId().or(-1)); uuidValues.put(MESSAGE_EXPIRATION_TIME, uuidSettings.getExpireMessages() > 0 ? uuidSettings.getExpireMessages() : e164Settings.getExpireMessages()); uuidValues.put(REGISTERED, RegisteredState.REGISTERED.getId()); - uuidValues.put(SYSTEM_DISPLAY_NAME, e164Settings.getSystemDisplayName()); + uuidValues.put(SYSTEM_GIVEN_NAME, e164Settings.getSystemProfileName().getGivenName()); + uuidValues.put(SYSTEM_FAMILY_NAME, e164Settings.getSystemProfileName().getFamilyName()); + uuidValues.put(SYSTEM_JOINED_NAME, e164Settings.getSystemProfileName().toString()); uuidValues.put(SYSTEM_PHOTO_URI, e164Settings.getSystemContactPhotoUri()); uuidValues.put(SYSTEM_PHONE_LABEL, e164Settings.getSystemPhoneLabel()); uuidValues.put(SYSTEM_CONTACT_URI, e164Settings.getSystemContactUri()); @@ -2840,14 +2846,16 @@ public class RecipientDatabase extends Database { } public void setSystemContactInfo(@NonNull RecipientId id, - @Nullable String displayName, + @NonNull ProfileName systemProfileName, @Nullable String photoUri, @Nullable String systemPhoneLabel, int systemPhoneType, @Nullable String systemContactUri) { ContentValues dirtyQualifyingValues = new ContentValues(); - dirtyQualifyingValues.put(SYSTEM_DISPLAY_NAME, displayName); + dirtyQualifyingValues.put(SYSTEM_GIVEN_NAME, systemProfileName.getGivenName()); + dirtyQualifyingValues.put(SYSTEM_FAMILY_NAME, systemProfileName.getFamilyName()); + dirtyQualifyingValues.put(SYSTEM_JOINED_NAME, systemProfileName.toString()); if (update(id, dirtyQualifyingValues)) { markDirty(id, DirtyState.UPDATE); @@ -2859,11 +2867,12 @@ public class RecipientDatabase extends Database { refreshQualifyingValues.put(SYSTEM_PHONE_TYPE, systemPhoneType); refreshQualifyingValues.put(SYSTEM_CONTACT_URI, systemContactUri); + String joinedName = systemProfileName.toString(); boolean updatedValues = update(id, refreshQualifyingValues); - boolean updatedColor = displayName != null && setColorIfNotSetInternal(id, ContactColors.generateFor(displayName)); + boolean updatedColor = !TextUtils.isEmpty(joinedName) && setColorIfNotSetInternal(id, ContactColors.generateFor(joinedName)); if (updatedValues || updatedColor) { - pendingContactInfoMap.put(id, new PendingContactInfo(displayName, photoUri, systemPhoneLabel, systemContactUri)); + pendingContactInfoMap.put(id, new PendingContactInfo(systemProfileName, photoUri, systemPhoneLabel, systemContactUri)); } ContentValues otherValues = new ContentValues(); @@ -2898,7 +2907,9 @@ public class RecipientDatabase extends Database { ContentValues values = new ContentValues(5); values.put(SYSTEM_INFO_PENDING, 0); - values.put(SYSTEM_DISPLAY_NAME, (String) null); + values.put(SYSTEM_GIVEN_NAME, (String) null); + values.put(SYSTEM_FAMILY_NAME, (String) null); + values.put(SYSTEM_JOINED_NAME, (String) null); values.put(SYSTEM_PHOTO_URI, (String) null); values.put(SYSTEM_PHONE_LABEL, (String) null); values.put(SYSTEM_CONTACT_URI, (String) null); @@ -2939,7 +2950,7 @@ public class RecipientDatabase extends Database { private final RegisteredState registered; private final byte[] profileKey; private final ProfileKeyCredential profileKeyCredential; - private final String systemDisplayName; + private final ProfileName systemProfileName; private final String systemContactPhoto; private final String systemPhoneLabel; private final String systemContactUri; @@ -2981,7 +2992,7 @@ public class RecipientDatabase extends Database { @NonNull RegisteredState registered, @Nullable byte[] profileKey, @Nullable ProfileKeyCredential profileKeyCredential, - @Nullable String systemDisplayName, + @NonNull ProfileName systemProfileName, @Nullable String systemContactPhoto, @Nullable String systemPhoneLabel, @Nullable String systemContactUri, @@ -3021,7 +3032,7 @@ public class RecipientDatabase extends Database { this.registered = registered; this.profileKey = profileKey; this.profileKeyCredential = profileKeyCredential; - this.systemDisplayName = systemDisplayName; + this.systemProfileName = systemProfileName; this.systemContactPhoto = systemContactPhoto; this.systemPhoneLabel = systemPhoneLabel; this.systemContactUri = systemContactUri; @@ -3125,8 +3136,8 @@ public class RecipientDatabase extends Database { return profileKeyCredential; } - public @Nullable String getSystemDisplayName() { - return systemDisplayName; + public @NonNull ProfileName getSystemProfileName() { + return systemProfileName; } public @Nullable String getSystemContactPhotoUri() { @@ -3313,13 +3324,13 @@ public class RecipientDatabase extends Database { private static class PendingContactInfo { - private final String displayName; - private final String photoUri; - private final String phoneLabel; - private final String contactUri; + private final ProfileName profileName; + private final String photoUri; + private final String phoneLabel; + private final String contactUri; - private PendingContactInfo(String displayName, String photoUri, String phoneLabel, String contactUri) { - this.displayName = displayName; + private PendingContactInfo(@NonNull ProfileName systemProfileName, String photoUri, String phoneLabel, String contactUri) { + this.profileName = systemProfileName; this.photoUri = photoUri; this.phoneLabel = phoneLabel; this.contactUri = contactUri; diff --git a/app/src/main/java/org/thoughtcrime/securesms/database/helpers/SQLCipherOpenHelper.java b/app/src/main/java/org/thoughtcrime/securesms/database/helpers/SQLCipherOpenHelper.java index 6bc8189450..95bcbdfe4d 100644 --- a/app/src/main/java/org/thoughtcrime/securesms/database/helpers/SQLCipherOpenHelper.java +++ b/app/src/main/java/org/thoughtcrime/securesms/database/helpers/SQLCipherOpenHelper.java @@ -171,8 +171,9 @@ public class SQLCipherOpenHelper extends SQLiteOpenHelper implements SignalDatab private static final int LAST_RESET_SESSION_TIME = 87; private static final int WALLPAPER = 88; private static final int ABOUT = 89; + private static final int SPLIT_SYSTEM_NAMES = 90; - private static final int DATABASE_VERSION = 89; + private static final int DATABASE_VERSION = 90; private static final String DATABASE_NAME = "signal.db"; private final Context context; @@ -1258,6 +1259,12 @@ public class SQLCipherOpenHelper extends SQLiteOpenHelper implements SignalDatab db.execSQL("ALTER TABLE recipient ADD COLUMN about_emoji TEXT DEFAULT NULL"); } + if (oldVersion < SPLIT_SYSTEM_NAMES) { + db.execSQL("ALTER TABLE recipient ADD COLUMN system_family_name TEXT DEFAULT NULL"); + db.execSQL("ALTER TABLE recipient ADD COLUMN system_given_name TEXT DEFAULT NULL"); + db.execSQL("UPDATE recipient SET system_given_name = system_display_name"); + } + db.setTransactionSuccessful(); } finally { db.endTransaction(); diff --git a/app/src/main/java/org/thoughtcrime/securesms/insights/InsightsRepository.java b/app/src/main/java/org/thoughtcrime/securesms/insights/InsightsRepository.java index cab776f012..467226ee84 100644 --- a/app/src/main/java/org/thoughtcrime/securesms/insights/InsightsRepository.java +++ b/app/src/main/java/org/thoughtcrime/securesms/insights/InsightsRepository.java @@ -66,7 +66,7 @@ public class InsightsRepository implements InsightsDashboardViewModel.Repository public void getUserAvatar(@NonNull Consumer avatarConsumer) { SimpleTask.run(() -> { Recipient self = Recipient.self().resolve(); - String name = Optional.fromNullable(self.getName(context)).or(""); + String name = Optional.fromNullable(self.getDisplayName(context)).or(""); MaterialColor fallbackColor = self.getColor(); if (fallbackColor == ContactColors.UNKNOWN_COLOR && !TextUtils.isEmpty(name)) { diff --git a/app/src/main/java/org/thoughtcrime/securesms/jobs/MultiDeviceContactUpdateJob.java b/app/src/main/java/org/thoughtcrime/securesms/jobs/MultiDeviceContactUpdateJob.java index 9fe83a5485..aa90945a0a 100644 --- a/app/src/main/java/org/thoughtcrime/securesms/jobs/MultiDeviceContactUpdateJob.java +++ b/app/src/main/java/org/thoughtcrime/securesms/jobs/MultiDeviceContactUpdateJob.java @@ -136,7 +136,7 @@ public class MultiDeviceContactUpdateJob extends BaseJob { Set archived = DatabaseFactory.getThreadDatabase(context).getArchivedRecipients(); out.write(new DeviceContact(RecipientUtil.toSignalServiceAddress(context, recipient), - Optional.fromNullable(recipient.getName(context)), + Optional.fromNullable(recipient.isGroup() || recipient.isSystemContact() ? recipient.getDisplayName(context) : null), getAvatar(recipient.getId(), recipient.getContactUri()), Optional.fromNullable(recipient.getColor().serialize()), verifiedMessage, @@ -191,7 +191,7 @@ public class MultiDeviceContactUpdateJob extends BaseJob { for (Recipient recipient : recipients) { Optional identity = DatabaseFactory.getIdentityDatabase(context).getIdentity(recipient.getId()); Optional verified = getVerifiedMessage(recipient, identity); - Optional name = Optional.fromNullable(recipient.getName(context)); + Optional name = Optional.fromNullable(recipient.isSystemContact() ? recipient.getDisplayName(context) : recipient.getGroupName(context)); Optional color = Optional.of(recipient.getColor().serialize()); Optional profileKey = ProfileKeyUtil.profileKeyOptional(recipient.getProfileKey()); boolean blocked = recipient.isBlocked(); diff --git a/app/src/main/java/org/thoughtcrime/securesms/migrations/ApplicationMigrations.java b/app/src/main/java/org/thoughtcrime/securesms/migrations/ApplicationMigrations.java index 1412c4a806..3b5b2eaa76 100644 --- a/app/src/main/java/org/thoughtcrime/securesms/migrations/ApplicationMigrations.java +++ b/app/src/main/java/org/thoughtcrime/securesms/migrations/ApplicationMigrations.java @@ -70,6 +70,7 @@ public class ApplicationMigrations { static final int USER_NOTIFICATION = 25; static final int DAY_BY_DAY_STICKERS = 26; static final int BLOB_LOCATION = 27; + static final int SYSTEM_NAME_SPLIT = 28; } /** @@ -296,6 +297,10 @@ public class ApplicationMigrations { jobs.put(Version.BLOB_LOCATION, new BlobStorageLocationMigrationJob()); } + if (lastSeenVersion < Version.SYSTEM_NAME_SPLIT) { + jobs.put(Version.SYSTEM_NAME_SPLIT, new DirectoryRefreshMigrationJob()); + } + return jobs; } diff --git a/app/src/main/java/org/thoughtcrime/securesms/profiles/ProfileName.java b/app/src/main/java/org/thoughtcrime/securesms/profiles/ProfileName.java index daca12f459..dba8634f13 100644 --- a/app/src/main/java/org/thoughtcrime/securesms/profiles/ProfileName.java +++ b/app/src/main/java/org/thoughtcrime/securesms/profiles/ProfileName.java @@ -89,6 +89,13 @@ public final class ProfileName implements Parcelable { } } + /** + * Creates a profile name that only contains a given name. + */ + public static @NonNull ProfileName asGiven(@Nullable String givenName) { + return fromParts(givenName, null); + } + /** * Creates a profile name, trimming chars until it fits the limits. */ diff --git a/app/src/main/java/org/thoughtcrime/securesms/profiles/edit/EditGroupProfileRepository.java b/app/src/main/java/org/thoughtcrime/securesms/profiles/edit/EditGroupProfileRepository.java index 39a81600c6..3951d39978 100644 --- a/app/src/main/java/org/thoughtcrime/securesms/profiles/edit/EditGroupProfileRepository.java +++ b/app/src/main/java/org/thoughtcrime/securesms/profiles/edit/EditGroupProfileRepository.java @@ -74,7 +74,7 @@ class EditGroupProfileRepository implements EditProfileRepository { String title = groupRecord.getTitle(); return title == null ? "" : title; }) - .or(() -> recipient.getName(context)); + .or(() -> recipient.getGroupName(context)); }, nameConsumer::accept); } diff --git a/app/src/main/java/org/thoughtcrime/securesms/recipients/Recipient.java b/app/src/main/java/org/thoughtcrime/securesms/recipients/Recipient.java index e9da73848d..c2159827b3 100644 --- a/app/src/main/java/org/thoughtcrime/securesms/recipients/Recipient.java +++ b/app/src/main/java/org/thoughtcrime/securesms/recipients/Recipient.java @@ -89,11 +89,11 @@ public class Recipient { private final RegisteredState registered; private final byte[] profileKey; private final ProfileKeyCredential profileKeyCredential; - private final String name; + private final String groupName; private final Uri systemContactPhoto; private final String customLabel; private final Uri contactUri; - private final ProfileName profileName; + private final ProfileName signalProfileName; private final String profileAvatar; private final boolean hasProfileImage; private final boolean profileSharing; @@ -109,6 +109,7 @@ public class Recipient { private final ChatWallpaper wallpaper; private final String about; private final String aboutEmoji; + private final ProfileName systemProfileName; /** @@ -326,11 +327,11 @@ public class Recipient { this.registered = RegisteredState.UNKNOWN; this.profileKey = null; this.profileKeyCredential = null; - this.name = null; + this.groupName = null; this.systemContactPhoto = null; this.customLabel = null; this.contactUri = null; - this.profileName = ProfileName.EMPTY; + this.signalProfileName = ProfileName.EMPTY; this.profileAvatar = null; this.hasProfileImage = false; this.profileSharing = false; @@ -345,6 +346,7 @@ public class Recipient { this.wallpaper = null; this.about = null; this.aboutEmoji = null; + this.systemProfileName = ProfileName.EMPTY; } public Recipient(@NonNull RecipientId id, @NonNull RecipientDetails details, boolean resolved) { @@ -371,11 +373,11 @@ public class Recipient { this.registered = details.registered; this.profileKey = details.profileKey; this.profileKeyCredential = details.profileKeyCredential; - this.name = details.name; + this.groupName = details.groupName; this.systemContactPhoto = details.systemContactPhoto; this.customLabel = details.customLabel; this.contactUri = details.contactUri; - this.profileName = details.profileName; + this.signalProfileName = details.profileName; this.profileAvatar = details.profileAvatar; this.hasProfileImage = details.hasProfileImage; this.profileSharing = details.profileSharing; @@ -390,6 +392,7 @@ public class Recipient { this.wallpaper = details.wallpaper; this.about = details.about; this.aboutEmoji = details.aboutEmoji; + this.systemProfileName = details.systemProfileName; } public @NonNull RecipientId getId() { @@ -404,8 +407,8 @@ public class Recipient { return contactUri; } - public @Nullable String getName(@NonNull Context context) { - if (this.name == null && groupId != null && groupId.isMms()) { + public @Nullable String getGroupName(@NonNull Context context) { + if (this.groupName == null && groupId != null && groupId.isMms()) { List names = new LinkedList<>(); for (Recipient recipient : participants) { @@ -413,27 +416,32 @@ public class Recipient { } return Util.join(names, ", "); - } else if (name == null && groupId != null && groupId.isPush()) { + } else if (groupName == null && groupId != null && groupId.isPush()) { return context.getString(R.string.RecipientProvider_unnamed_group); } else { - return this.name; + return this.groupName; } } public boolean hasName() { - return name != null; + return groupName != null; } /** * False iff it {@link #getDisplayName} would fall back to e164, email or unknown. */ public boolean hasAUserSetDisplayName(@NonNull Context context) { - return !TextUtils.isEmpty(getName(context)) || + return !TextUtils.isEmpty(getGroupName(context)) || + !TextUtils.isEmpty(getSystemProfileName().toString()) || !TextUtils.isEmpty(getProfileName().toString()); } public @NonNull String getDisplayName(@NonNull Context context) { - String name = getName(context); + String name = getGroupName(context); + + if (Util.isEmpty(name)) { + name = getSystemProfileName().toString(); + } if (Util.isEmpty(name)) { name = getProfileName().toString(); @@ -455,7 +463,11 @@ public class Recipient { } public @NonNull String getDisplayNameOrUsername(@NonNull Context context) { - String name = getName(context); + String name = getGroupName(context); + + if (Util.isEmpty(name)) { + name = getSystemProfileName().toString(); + } if (Util.isEmpty(name)) { name = StringUtil.isolateBidi(getProfileName().toString()); @@ -481,11 +493,16 @@ public class Recipient { } public @NonNull String getMentionDisplayName(@NonNull Context context) { - String name = isSelf ? getProfileName().toString() : getName(context); + String name = isSelf ? getProfileName().toString() : getGroupName(context); name = StringUtil.isolateBidi(name); if (Util.isEmpty(name)) { - name = isSelf ? getName(context) : getProfileName().toString(); + name = isSelf ? getGroupName(context) : getSystemProfileName().toString(); + name = StringUtil.isolateBidi(name); + } + + if (Util.isEmpty(name)) { + name = isSelf ? getGroupName(context) : getProfileName().toString(); name = StringUtil.isolateBidi(name); } @@ -505,7 +522,8 @@ public class Recipient { } public @NonNull String getShortDisplayName(@NonNull Context context) { - String name = Util.getFirstNonEmpty(getName(context), + String name = Util.getFirstNonEmpty(getGroupName(context), + getSystemProfileName().getGivenName(), getProfileName().getGivenName(), getDisplayName(context)); @@ -513,7 +531,8 @@ public class Recipient { } public @NonNull String getShortDisplayNameIncludingUsername(@NonNull Context context) { - String name = Util.getFirstNonEmpty(getName(context), + String name = Util.getFirstNonEmpty(getGroupName(context), + getSystemProfileName().getGivenName(), getProfileName().getGivenName(), getDisplayName(context), getUsername().orNull()); @@ -526,7 +545,7 @@ public class Recipient { return MaterialColor.GROUP; } else if (color != null) { return color; - } else if (name != null || profileSharing) { + } else if (groupName != null || profileSharing) { Log.w(TAG, "Had no color for " + id + "! Saving a new one."); Context context = ApplicationDependencies.getApplication(); @@ -672,7 +691,11 @@ public class Recipient { } public @NonNull ProfileName getProfileName() { - return profileName; + return signalProfileName; + } + + private @NonNull ProfileName getSystemProfileName() { + return systemProfileName; } public @Nullable String getProfileAvatar() { @@ -744,12 +767,12 @@ public class Recipient { } public @NonNull FallbackContactPhoto getFallbackContactPhoto(@NonNull FallbackPhotoProvider fallbackPhotoProvider) { - if (isSelf) return fallbackPhotoProvider.getPhotoForLocalNumber(); - else if (isResolving()) return fallbackPhotoProvider.getPhotoForResolvingRecipient(); - else if (isGroupInternal()) return fallbackPhotoProvider.getPhotoForGroup(); - else if (isGroup()) return fallbackPhotoProvider.getPhotoForGroup(); - else if (!TextUtils.isEmpty(name)) return fallbackPhotoProvider.getPhotoForRecipientWithName(name); - else return fallbackPhotoProvider.getPhotoForRecipientWithoutName(); + if (isSelf) return fallbackPhotoProvider.getPhotoForLocalNumber(); + else if (isResolving()) return fallbackPhotoProvider.getPhotoForResolvingRecipient(); + else if (isGroupInternal()) return fallbackPhotoProvider.getPhotoForGroup(); + else if (isGroup()) return fallbackPhotoProvider.getPhotoForGroup(); + else if (!TextUtils.isEmpty(groupName)) return fallbackPhotoProvider.getPhotoForRecipientWithName(groupName); + else return fallbackPhotoProvider.getPhotoForRecipientWithoutName(); } public @Nullable ContactPhoto getContactPhoto() { @@ -1003,11 +1026,12 @@ public class Recipient { registered == other.registered && Arrays.equals(profileKey, other.profileKey) && Objects.equals(profileKeyCredential, other.profileKeyCredential) && - Objects.equals(name, other.name) && + Objects.equals(groupName, other.groupName) && Objects.equals(systemContactPhoto, other.systemContactPhoto) && Objects.equals(customLabel, other.customLabel) && Objects.equals(contactUri, other.contactUri) && - Objects.equals(profileName, other.profileName) && + Objects.equals(signalProfileName, other.signalProfileName) && + Objects.equals(systemProfileName, other.systemProfileName) && Objects.equals(profileAvatar, other.profileAvatar) && Objects.equals(notificationChannel, other.notificationChannel) && unidentifiedAccessMode == other.unidentifiedAccessMode && diff --git a/app/src/main/java/org/thoughtcrime/securesms/recipients/RecipientDetails.java b/app/src/main/java/org/thoughtcrime/securesms/recipients/RecipientDetails.java index d8af3b51af..284fcdd4e4 100644 --- a/app/src/main/java/org/thoughtcrime/securesms/recipients/RecipientDetails.java +++ b/app/src/main/java/org/thoughtcrime/securesms/recipients/RecipientDetails.java @@ -2,7 +2,6 @@ package org.thoughtcrime.securesms.recipients; import android.content.Context; import android.net.Uri; -import android.text.TextUtils; import androidx.annotation.NonNull; import androidx.annotation.Nullable; @@ -33,7 +32,7 @@ public class RecipientDetails { final String e164; final String email; final GroupId groupId; - final String name; + final String groupName; final String customLabel; final Uri systemContactPhoto; final Uri contactUri; @@ -69,8 +68,9 @@ public class RecipientDetails { final ChatWallpaper wallpaper; final String about; final String aboutEmoji; + final ProfileName systemProfileName; - public RecipientDetails(@Nullable String name, + public RecipientDetails(@Nullable String groupName, @NonNull Optional groupAvatarId, boolean systemContact, boolean isSelf, @@ -117,9 +117,8 @@ public class RecipientDetails { this.wallpaper = settings.getWallpaper(); this.about = settings.getAbout(); this.aboutEmoji = settings.getAboutEmoji(); - - if (name == null) this.name = settings.getSystemDisplayName(); - else this.name = name; + this.systemProfileName = settings.getSystemProfileName(); + this.groupName = groupName; } /** @@ -159,7 +158,7 @@ public class RecipientDetails { this.notificationChannel = null; this.unidentifiedAccessMode = UnidentifiedAccessMode.UNKNOWN; this.forceSmsSelection = false; - this.name = null; + this.groupName = null; this.groupsV2Capability = Recipient.Capability.UNKNOWN; this.groupsV1MigrationCapability = Recipient.Capability.UNKNOWN; this.storageId = null; @@ -167,10 +166,11 @@ public class RecipientDetails { this.wallpaper = null; this.about = null; this.aboutEmoji = null; + this.systemProfileName = ProfileName.EMPTY; } public static @NonNull RecipientDetails forIndividual(@NonNull Context context, @NonNull RecipientSettings settings) { - boolean systemContact = !TextUtils.isEmpty(settings.getSystemDisplayName()); + boolean systemContact = !settings.getSystemProfileName().isEmpty(); boolean isSelf = (settings.getE164() != null && settings.getE164().equals(TextSecurePreferences.getLocalNumber(context))) || (settings.getUuid() != null && settings.getUuid().equals(TextSecurePreferences.getLocalUuid(context)));