From 884d8b7f7257f36e72ec96dbf99191456b35240b Mon Sep 17 00:00:00 2001 From: Moxie Marlinspike Date: Thu, 5 Jan 2017 12:42:28 -0800 Subject: [PATCH] Store display name in Signal contacts Fixes #5974 // FREEBIE --- .../securesms/contacts/ContactsDatabase.java | 146 +++++++++++++++--- .../securesms/util/DirectoryHelper.java | 106 +++++++++---- src/org/thoughtcrime/securesms/util/Util.java | 4 + 3 files changed, 204 insertions(+), 52 deletions(-) diff --git a/src/org/thoughtcrime/securesms/contacts/ContactsDatabase.java b/src/org/thoughtcrime/securesms/contacts/ContactsDatabase.java index a55c1d9e3..4d0ca414d 100644 --- a/src/org/thoughtcrime/securesms/contacts/ContactsDatabase.java +++ b/src/org/thoughtcrime/securesms/contacts/ContactsDatabase.java @@ -35,6 +35,7 @@ import android.util.Log; import android.util.Pair; import org.thoughtcrime.securesms.R; +import org.thoughtcrime.securesms.util.Util; import org.whispersystems.libsignal.util.guava.Optional; import org.whispersystems.signalservice.api.push.ContactTokenDetails; import org.whispersystems.signalservice.api.util.InvalidNumberException; @@ -58,16 +59,16 @@ public class ContactsDatabase { private static final String CALL_MIMETYPE = "vnd.android.cursor.item/vnd.org.thoughtcrime.securesms.call"; private static final String SYNC = "__TS"; - public static final String ID_COLUMN = "_id"; - public static final String NAME_COLUMN = "name"; - public static final String NUMBER_COLUMN = "number"; - public static final String NUMBER_TYPE_COLUMN = "number_type"; - public static final String LABEL_COLUMN = "label"; - public static final String CONTACT_TYPE_COLUMN = "contact_type"; + static final String ID_COLUMN = "_id"; + static final String NAME_COLUMN = "name"; + static final String NUMBER_COLUMN = "number"; + static final String NUMBER_TYPE_COLUMN = "number_type"; + static final String LABEL_COLUMN = "label"; + static final String CONTACT_TYPE_COLUMN = "contact_type"; - public static final int NORMAL_TYPE = 0; - public static final int PUSH_TYPE = 1; - public static final int NEW_TYPE = 2; + static final int NORMAL_TYPE = 0; + static final int PUSH_TYPE = 1; + static final int NEW_TYPE = 2; private final Context context; @@ -99,7 +100,8 @@ public class ContactsDatabase { Log.w(TAG, "Adding number: " + registeredNumber); addedNumbers.add(registeredNumber); addTextSecureRawContact(operations, account, systemContactInfo.get().number, - systemContactInfo.get().id, registeredContact.isVoice()); + systemContactInfo.get().name, systemContactInfo.get().id, + registeredContact.isVoice()); } } } @@ -118,6 +120,11 @@ public class ContactsDatabase { } else if (!tokenDetails.isVoice() && currentContactEntry.getValue().isVoiceSupported()) { Log.w(TAG, "Removing voice support: " + currentContactEntry.getKey()); removeContactVoiceSupport(operations, currentContactEntry.getValue().getId()); + } else if (!Util.isStringEquals(currentContactEntry.getValue().getRawDisplayName(), + currentContactEntry.getValue().getAggregateDisplayName())) + { + Log.w(TAG, "Updating display name: " + currentContactEntry.getKey()); + updateDisplayName(operations, currentContactEntry.getValue().getAggregateDisplayName(), currentContactEntry.getValue().getId(), currentContactEntry.getValue().getDisplayNameSource()); } } @@ -128,7 +135,7 @@ public class ContactsDatabase { return addedNumbers; } - public @NonNull Cursor querySystemContacts(String filter) { + @NonNull Cursor querySystemContacts(String filter) { Uri uri; if (!TextUtils.isEmpty(filter)) { @@ -176,7 +183,7 @@ public class ContactsDatabase { new Pair(CONTACT_TYPE_COLUMN, NORMAL_TYPE)); } - public @NonNull Cursor queryTextSecureContacts(String filter) { + @NonNull Cursor queryTextSecureContacts(String filter) { String[] projection = new String[] {ContactsContract.Data._ID, ContactsContract.Contacts.DISPLAY_NAME, ContactsContract.Data.DATA1}; @@ -231,6 +238,30 @@ public class ContactsDatabase { .build()); } + private void updateDisplayName(List operations, + @Nullable String displayName, + long rawContactId, int displayNameSource) + { + Uri dataUri = ContactsContract.Data.CONTENT_URI.buildUpon() + .appendQueryParameter(ContactsContract.CALLER_IS_SYNCADAPTER, "true") + .build(); + + if (displayNameSource != ContactsContract.DisplayNameSources.STRUCTURED_NAME) { + operations.add(ContentProviderOperation.newInsert(dataUri) + .withValue(ContactsContract.CommonDataKinds.StructuredName.RAW_CONTACT_ID, rawContactId) + .withValue(ContactsContract.CommonDataKinds.StructuredName.DISPLAY_NAME, displayName) + .withValue(ContactsContract.Data.MIMETYPE, ContactsContract.CommonDataKinds.StructuredName.CONTENT_ITEM_TYPE) + .build()); + } else { + operations.add(ContentProviderOperation.newUpdate(dataUri) + .withSelection(ContactsContract.CommonDataKinds.StructuredName.RAW_CONTACT_ID + " = ? AND " + ContactsContract.Data.MIMETYPE + " = ?", + new String[] {String.valueOf(rawContactId), ContactsContract.CommonDataKinds.StructuredName.CONTENT_ITEM_TYPE}) + .withValue(ContactsContract.CommonDataKinds.StructuredName.DISPLAY_NAME, displayName) + .withValue(ContactsContract.Data.MIMETYPE, ContactsContract.CommonDataKinds.StructuredName.CONTENT_ITEM_TYPE) + .build()); + } + } + private void removeContactVoiceSupport(List operations, long rawContactId) { operations.add(ContentProviderOperation.newUpdate(RawContacts.CONTENT_URI) .withSelection(RawContacts._ID + " = ?", new String[] {String.valueOf(rawContactId)}) @@ -245,7 +276,7 @@ public class ContactsDatabase { } private void addTextSecureRawContact(List operations, - Account account, String e164number, + Account account, String e164number, String displayName, long aggregateId, boolean supportsVoice) { int index = operations.size(); @@ -260,6 +291,12 @@ public class ContactsDatabase { .withValue(RawContacts.SYNC4, String.valueOf(supportsVoice)) .build()); + operations.add(ContentProviderOperation.newInsert(dataUri) + .withValueBackReference(ContactsContract.CommonDataKinds.StructuredName.RAW_CONTACT_ID, index) + .withValue(ContactsContract.CommonDataKinds.StructuredName.DISPLAY_NAME, displayName) + .withValue(ContactsContract.Data.MIMETYPE, ContactsContract.CommonDataKinds.StructuredName.CONTENT_ITEM_TYPE) + .build()); + operations.add(ContentProviderOperation.newInsert(dataUri) .withValueBackReference(ContactsContract.CommonDataKinds.Phone.RAW_CONTACT_ID, index) .withValue(ContactsContract.Data.MIMETYPE, ContactsContract.CommonDataKinds.Phone.CONTENT_ITEM_TYPE) @@ -321,7 +358,15 @@ public class ContactsDatabase { Cursor cursor = null; try { - cursor = context.getContentResolver().query(currentContactsUri, new String[] {BaseColumns._ID, RawContacts.SYNC1, RawContacts.SYNC4}, null, null, null); + String[] projection; + + if (Build.VERSION.SDK_INT >= 11) { + projection = new String[] {BaseColumns._ID, RawContacts.SYNC1, RawContacts.SYNC4, RawContacts.CONTACT_ID, RawContacts.DISPLAY_NAME_PRIMARY, RawContacts.DISPLAY_NAME_SOURCE}; + } else{ + projection = new String[] {BaseColumns._ID, RawContacts.SYNC1, RawContacts.SYNC4, RawContacts.CONTACT_ID}; + } + + cursor = context.getContentResolver().query(currentContactsUri, projection, null, null, null); while (cursor != null && cursor.moveToNext()) { String currentNumber; @@ -333,7 +378,20 @@ public class ContactsDatabase { currentNumber = cursor.getString(1); } - signalContacts.put(currentNumber, new SignalContact(cursor.getLong(0), cursor.getString(2))); + long rawContactId = cursor.getLong(0); + long contactId = cursor.getLong(3); + String supportsVoice = cursor.getString(2); + String rawContactDisplayName = null; + String aggregateDisplayName = null; + int rawContactDisplayNameSource = 0; + + if (Build.VERSION.SDK_INT >= 11) { + rawContactDisplayName = cursor.getString(4); + rawContactDisplayNameSource = cursor.getInt(5); + aggregateDisplayName = getDisplayName(contactId); + } + + signalContacts.put(currentNumber, new SignalContact(rawContactId, supportsVoice, rawContactDisplayName, aggregateDisplayName, rawContactDisplayNameSource)); } } finally { if (cursor != null) @@ -386,15 +444,33 @@ public class ContactsDatabase { return Optional.absent(); } + private @Nullable String getDisplayName(long contactId) { + Cursor cursor = context.getContentResolver().query(ContactsContract.Contacts.CONTENT_URI, + new String[]{ContactsContract.Contacts.DISPLAY_NAME}, + ContactsContract.Contacts._ID + " = ?", + new String[] {String.valueOf(contactId)}, + null); + + try { + if (cursor != null && cursor.moveToFirst()) { + return cursor.getString(0); + } else { + return null; + } + } finally { + if (cursor != null) cursor.close(); + } + } + private static class ProjectionMappingCursor extends CursorWrapper { private final Map projectionMap; private final Pair[] extras; @SafeVarargs - public ProjectionMappingCursor(Cursor cursor, - Map projectionMap, - Pair... extras) + ProjectionMappingCursor(Cursor cursor, + Map projectionMap, + Pair... extras) { super(cursor); this.projectionMap = projectionMap; @@ -498,20 +574,46 @@ public class ContactsDatabase { } private static class SignalContact { + private final long id; @Nullable private final String supportsVoice; + @Nullable private final String rawDisplayName; + @Nullable private final String aggregateDisplayName; + private final int displayNameSource; - public SignalContact(long id, @Nullable String supportsVoice) { - this.id = id; - this.supportsVoice = supportsVoice; + SignalContact(long id, + @Nullable String supportsVoice, + @Nullable String rawDisplayName, + @Nullable String aggregateDisplayName, + int displayNameSource) + { + this.id = id; + this.supportsVoice = supportsVoice; + this.rawDisplayName = rawDisplayName; + this.aggregateDisplayName = aggregateDisplayName; + this.displayNameSource = displayNameSource; } public long getId() { return id; } - public boolean isVoiceSupported() { + boolean isVoiceSupported() { return "true".equals(supportsVoice); } + + @Nullable + String getRawDisplayName() { + return rawDisplayName; + } + + @Nullable + String getAggregateDisplayName() { + return aggregateDisplayName; + } + + int getDisplayNameSource() { + return displayNameSource; + } } } diff --git a/src/org/thoughtcrime/securesms/util/DirectoryHelper.java b/src/org/thoughtcrime/securesms/util/DirectoryHelper.java index 709449fca..70d0c0d72 100644 --- a/src/org/thoughtcrime/securesms/util/DirectoryHelper.java +++ b/src/org/thoughtcrime/securesms/util/DirectoryHelper.java @@ -69,22 +69,24 @@ public class DirectoryHelper { public static void refreshDirectory(@NonNull Context context, @Nullable MasterSecret masterSecret) throws IOException { - List newUsers = refreshDirectory(context, - AccountManagerFactory.createManager(context), - TextSecurePreferences.getLocalNumber(context)); + RefreshResult result = refreshDirectory(context, + AccountManagerFactory.createManager(context), + TextSecurePreferences.getLocalNumber(context)); - if (!newUsers.isEmpty() && TextSecurePreferences.isMultiDevice(context)) { + if (!result.getNewUsers().isEmpty() && TextSecurePreferences.isMultiDevice(context)) { ApplicationContext.getInstance(context) .getJobManager() .add(new MultiDeviceContactUpdateJob(context)); } - notifyNewUsers(context, masterSecret, newUsers); + if (!result.isFresh()) { + notifyNewUsers(context, masterSecret, result.getNewUsers()); + } } - public static @NonNull List refreshDirectory(@NonNull Context context, - @NonNull SignalServiceAccountManager accountManager, - @NonNull String localNumber) + public static @NonNull RefreshResult refreshDirectory(@NonNull Context context, + @NonNull SignalServiceAccountManager accountManager, + @NonNull String localNumber) throws IOException { TextSecureDirectory directory = TextSecureDirectory.getInstance(context); @@ -101,7 +103,7 @@ public class DirectoryHelper { return updateContactsDatabase(context, localNumber, activeTokens, true); } - return new LinkedList<>(); + return new RefreshResult(new LinkedList(), false); } public static UserCapabilities refreshDirectoryFor(@NonNull Context context, @@ -119,13 +121,15 @@ public class DirectoryHelper { if (details.isPresent()) { directory.setNumber(details.get(), true); - List newUsers = updateContactsDatabase(context, localNumber, details.get()); + RefreshResult result = updateContactsDatabase(context, localNumber, details.get()); - if (!newUsers.isEmpty() && TextSecurePreferences.isMultiDevice(context)) { + if (!result.getNewUsers().isEmpty() && TextSecurePreferences.isMultiDevice(context)) { ApplicationContext.getInstance(context).getJobManager().add(new MultiDeviceContactUpdateJob(context)); } - notifyNewUsers(context, masterSecret, newUsers); + if (!result.isFresh()) { + notifyNewUsers(context, masterSecret, result.getNewUsers()); + } return new UserCapabilities(Capability.SUPPORTED, details.get().isVoice() ? Capability.SUPPORTED : Capability.UNSUPPORTED); } else { @@ -181,32 +185,34 @@ public class DirectoryHelper { } } - private static @NonNull List updateContactsDatabase(@NonNull Context context, - @NonNull String localNumber, - @NonNull final ContactTokenDetails activeToken) + private static @NonNull RefreshResult updateContactsDatabase(@NonNull Context context, + @NonNull String localNumber, + @NonNull final ContactTokenDetails activeToken) { return updateContactsDatabase(context, localNumber, new LinkedList() {{add(activeToken);}}, false); } - private static @NonNull List updateContactsDatabase(@NonNull Context context, - @NonNull String localNumber, - @NonNull List activeTokens, - boolean removeMissing) + private static @NonNull RefreshResult updateContactsDatabase(@NonNull Context context, + @NonNull String localNumber, + @NonNull List activeTokens, + boolean removeMissing) { - Optional account = getOrCreateAccount(context); + Optional account = getOrCreateAccount(context); if (account.isPresent()) { try { - return DatabaseFactory.getContactsDatabase(context) - .setRegisteredUsers(account.get(), localNumber, activeTokens, removeMissing); + List newUsers = DatabaseFactory.getContactsDatabase(context) + .setRegisteredUsers(account.get().getAccount(), localNumber, activeTokens, removeMissing); + + return new RefreshResult(newUsers, account.get().isFresh()); } catch (RemoteException | OperationApplicationException e) { Log.w(TAG, e); } } - return new LinkedList<>(); + return new RefreshResult(new LinkedList(), false); } private static void notifyNewUsers(@NonNull Context context, @@ -230,33 +236,73 @@ public class DirectoryHelper { } } - private static Optional getOrCreateAccount(Context context) { + private static Optional getOrCreateAccount(Context context) { AccountManager accountManager = AccountManager.get(context); Account[] accounts = accountManager.getAccountsByType("org.thoughtcrime.securesms"); - Optional account; + Optional account; if (accounts.length == 0) account = createAccount(context); - else account = Optional.of(accounts[0]); + else account = Optional.of(new AccountHolder(accounts[0], false)); - if (account.isPresent() && !ContentResolver.getSyncAutomatically(account.get(), ContactsContract.AUTHORITY)) { - ContentResolver.setSyncAutomatically(account.get(), ContactsContract.AUTHORITY, true); + if (account.isPresent() && !ContentResolver.getSyncAutomatically(account.get().getAccount(), ContactsContract.AUTHORITY)) { + ContentResolver.setSyncAutomatically(account.get().getAccount(), ContactsContract.AUTHORITY, true); } return account; } - private static Optional createAccount(Context context) { + private static Optional createAccount(Context context) { AccountManager accountManager = AccountManager.get(context); Account account = new Account(context.getString(R.string.app_name), "org.thoughtcrime.securesms"); if (accountManager.addAccountExplicitly(account, null, null)) { Log.w(TAG, "Created new account..."); ContentResolver.setIsSyncable(account, ContactsContract.AUTHORITY, 1); - return Optional.of(account); + return Optional.of(new AccountHolder(account, true)); } else { Log.w(TAG, "Failed to create account!"); return Optional.absent(); } } + + private static class AccountHolder { + + private final boolean fresh; + private final Account account; + + private AccountHolder(Account account, boolean fresh) { + this.fresh = fresh; + this.account = account; + } + + public boolean isFresh() { + return fresh; + } + + public Account getAccount() { + return account; + } + + } + + private static class RefreshResult { + + private final List newUsers; + private final boolean fresh; + + private RefreshResult(List newUsers, boolean fresh) { + this.newUsers = newUsers; + this.fresh = fresh; + } + + public List getNewUsers() { + return newUsers; + } + + public boolean isFresh() { + return fresh; + } + } + } diff --git a/src/org/thoughtcrime/securesms/util/Util.java b/src/org/thoughtcrime/securesms/util/Util.java index 0bf7968a0..d0665ae42 100644 --- a/src/org/thoughtcrime/securesms/util/Util.java +++ b/src/org/thoughtcrime/securesms/util/Util.java @@ -455,4 +455,8 @@ public class Util { return (int)value; } + public static boolean isStringEquals(String first, String second) { + if (first == null) return second == null; + return first.equals(second); + } }