Store display name in Signal contacts

Fixes #5974
// FREEBIE
This commit is contained in:
Moxie Marlinspike 2017-01-05 12:42:28 -08:00
parent 57cdbaedd6
commit 884d8b7f72
3 changed files with 204 additions and 52 deletions

View file

@ -35,6 +35,7 @@ import android.util.Log;
import android.util.Pair; import android.util.Pair;
import org.thoughtcrime.securesms.R; import org.thoughtcrime.securesms.R;
import org.thoughtcrime.securesms.util.Util;
import org.whispersystems.libsignal.util.guava.Optional; import org.whispersystems.libsignal.util.guava.Optional;
import org.whispersystems.signalservice.api.push.ContactTokenDetails; import org.whispersystems.signalservice.api.push.ContactTokenDetails;
import org.whispersystems.signalservice.api.util.InvalidNumberException; 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 CALL_MIMETYPE = "vnd.android.cursor.item/vnd.org.thoughtcrime.securesms.call";
private static final String SYNC = "__TS"; private static final String SYNC = "__TS";
public static final String ID_COLUMN = "_id"; static final String ID_COLUMN = "_id";
public static final String NAME_COLUMN = "name"; static final String NAME_COLUMN = "name";
public static final String NUMBER_COLUMN = "number"; static final String NUMBER_COLUMN = "number";
public static final String NUMBER_TYPE_COLUMN = "number_type"; static final String NUMBER_TYPE_COLUMN = "number_type";
public static final String LABEL_COLUMN = "label"; static final String LABEL_COLUMN = "label";
public static final String CONTACT_TYPE_COLUMN = "contact_type"; static final String CONTACT_TYPE_COLUMN = "contact_type";
public static final int NORMAL_TYPE = 0; static final int NORMAL_TYPE = 0;
public static final int PUSH_TYPE = 1; static final int PUSH_TYPE = 1;
public static final int NEW_TYPE = 2; static final int NEW_TYPE = 2;
private final Context context; private final Context context;
@ -99,7 +100,8 @@ public class ContactsDatabase {
Log.w(TAG, "Adding number: " + registeredNumber); Log.w(TAG, "Adding number: " + registeredNumber);
addedNumbers.add(registeredNumber); addedNumbers.add(registeredNumber);
addTextSecureRawContact(operations, account, systemContactInfo.get().number, 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()) { } else if (!tokenDetails.isVoice() && currentContactEntry.getValue().isVoiceSupported()) {
Log.w(TAG, "Removing voice support: " + currentContactEntry.getKey()); Log.w(TAG, "Removing voice support: " + currentContactEntry.getKey());
removeContactVoiceSupport(operations, currentContactEntry.getValue().getId()); 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; return addedNumbers;
} }
public @NonNull Cursor querySystemContacts(String filter) { @NonNull Cursor querySystemContacts(String filter) {
Uri uri; Uri uri;
if (!TextUtils.isEmpty(filter)) { if (!TextUtils.isEmpty(filter)) {
@ -176,7 +183,7 @@ public class ContactsDatabase {
new Pair<String, Object>(CONTACT_TYPE_COLUMN, NORMAL_TYPE)); new Pair<String, Object>(CONTACT_TYPE_COLUMN, NORMAL_TYPE));
} }
public @NonNull Cursor queryTextSecureContacts(String filter) { @NonNull Cursor queryTextSecureContacts(String filter) {
String[] projection = new String[] {ContactsContract.Data._ID, String[] projection = new String[] {ContactsContract.Data._ID,
ContactsContract.Contacts.DISPLAY_NAME, ContactsContract.Contacts.DISPLAY_NAME,
ContactsContract.Data.DATA1}; ContactsContract.Data.DATA1};
@ -231,6 +238,30 @@ public class ContactsDatabase {
.build()); .build());
} }
private void updateDisplayName(List<ContentProviderOperation> 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<ContentProviderOperation> operations, long rawContactId) { private void removeContactVoiceSupport(List<ContentProviderOperation> operations, long rawContactId) {
operations.add(ContentProviderOperation.newUpdate(RawContacts.CONTENT_URI) operations.add(ContentProviderOperation.newUpdate(RawContacts.CONTENT_URI)
.withSelection(RawContacts._ID + " = ?", new String[] {String.valueOf(rawContactId)}) .withSelection(RawContacts._ID + " = ?", new String[] {String.valueOf(rawContactId)})
@ -245,7 +276,7 @@ public class ContactsDatabase {
} }
private void addTextSecureRawContact(List<ContentProviderOperation> operations, private void addTextSecureRawContact(List<ContentProviderOperation> operations,
Account account, String e164number, Account account, String e164number, String displayName,
long aggregateId, boolean supportsVoice) long aggregateId, boolean supportsVoice)
{ {
int index = operations.size(); int index = operations.size();
@ -260,6 +291,12 @@ public class ContactsDatabase {
.withValue(RawContacts.SYNC4, String.valueOf(supportsVoice)) .withValue(RawContacts.SYNC4, String.valueOf(supportsVoice))
.build()); .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) operations.add(ContentProviderOperation.newInsert(dataUri)
.withValueBackReference(ContactsContract.CommonDataKinds.Phone.RAW_CONTACT_ID, index) .withValueBackReference(ContactsContract.CommonDataKinds.Phone.RAW_CONTACT_ID, index)
.withValue(ContactsContract.Data.MIMETYPE, ContactsContract.CommonDataKinds.Phone.CONTENT_ITEM_TYPE) .withValue(ContactsContract.Data.MIMETYPE, ContactsContract.CommonDataKinds.Phone.CONTENT_ITEM_TYPE)
@ -321,7 +358,15 @@ public class ContactsDatabase {
Cursor cursor = null; Cursor cursor = null;
try { 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()) { while (cursor != null && cursor.moveToNext()) {
String currentNumber; String currentNumber;
@ -333,7 +378,20 @@ public class ContactsDatabase {
currentNumber = cursor.getString(1); 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 { } finally {
if (cursor != null) if (cursor != null)
@ -386,15 +444,33 @@ public class ContactsDatabase {
return Optional.absent(); 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 static class ProjectionMappingCursor extends CursorWrapper {
private final Map<String, String> projectionMap; private final Map<String, String> projectionMap;
private final Pair<String, Object>[] extras; private final Pair<String, Object>[] extras;
@SafeVarargs @SafeVarargs
public ProjectionMappingCursor(Cursor cursor, ProjectionMappingCursor(Cursor cursor,
Map<String, String> projectionMap, Map<String, String> projectionMap,
Pair<String, Object>... extras) Pair<String, Object>... extras)
{ {
super(cursor); super(cursor);
this.projectionMap = projectionMap; this.projectionMap = projectionMap;
@ -498,20 +574,46 @@ public class ContactsDatabase {
} }
private static class SignalContact { private static class SignalContact {
private final long id; private final long id;
@Nullable private final String supportsVoice; @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) { SignalContact(long id,
this.id = id; @Nullable String supportsVoice,
this.supportsVoice = 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() { public long getId() {
return id; return id;
} }
public boolean isVoiceSupported() { boolean isVoiceSupported() {
return "true".equals(supportsVoice); return "true".equals(supportsVoice);
} }
@Nullable
String getRawDisplayName() {
return rawDisplayName;
}
@Nullable
String getAggregateDisplayName() {
return aggregateDisplayName;
}
int getDisplayNameSource() {
return displayNameSource;
}
} }
} }

View file

@ -69,22 +69,24 @@ public class DirectoryHelper {
public static void refreshDirectory(@NonNull Context context, @Nullable MasterSecret masterSecret) public static void refreshDirectory(@NonNull Context context, @Nullable MasterSecret masterSecret)
throws IOException throws IOException
{ {
List<String> newUsers = refreshDirectory(context, RefreshResult result = refreshDirectory(context,
AccountManagerFactory.createManager(context), AccountManagerFactory.createManager(context),
TextSecurePreferences.getLocalNumber(context)); TextSecurePreferences.getLocalNumber(context));
if (!newUsers.isEmpty() && TextSecurePreferences.isMultiDevice(context)) { if (!result.getNewUsers().isEmpty() && TextSecurePreferences.isMultiDevice(context)) {
ApplicationContext.getInstance(context) ApplicationContext.getInstance(context)
.getJobManager() .getJobManager()
.add(new MultiDeviceContactUpdateJob(context)); .add(new MultiDeviceContactUpdateJob(context));
} }
notifyNewUsers(context, masterSecret, newUsers); if (!result.isFresh()) {
notifyNewUsers(context, masterSecret, result.getNewUsers());
}
} }
public static @NonNull List<String> refreshDirectory(@NonNull Context context, public static @NonNull RefreshResult refreshDirectory(@NonNull Context context,
@NonNull SignalServiceAccountManager accountManager, @NonNull SignalServiceAccountManager accountManager,
@NonNull String localNumber) @NonNull String localNumber)
throws IOException throws IOException
{ {
TextSecureDirectory directory = TextSecureDirectory.getInstance(context); TextSecureDirectory directory = TextSecureDirectory.getInstance(context);
@ -101,7 +103,7 @@ public class DirectoryHelper {
return updateContactsDatabase(context, localNumber, activeTokens, true); return updateContactsDatabase(context, localNumber, activeTokens, true);
} }
return new LinkedList<>(); return new RefreshResult(new LinkedList<String>(), false);
} }
public static UserCapabilities refreshDirectoryFor(@NonNull Context context, public static UserCapabilities refreshDirectoryFor(@NonNull Context context,
@ -119,13 +121,15 @@ public class DirectoryHelper {
if (details.isPresent()) { if (details.isPresent()) {
directory.setNumber(details.get(), true); directory.setNumber(details.get(), true);
List<String> 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)); 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); return new UserCapabilities(Capability.SUPPORTED, details.get().isVoice() ? Capability.SUPPORTED : Capability.UNSUPPORTED);
} else { } else {
@ -181,32 +185,34 @@ public class DirectoryHelper {
} }
} }
private static @NonNull List<String> updateContactsDatabase(@NonNull Context context, private static @NonNull RefreshResult updateContactsDatabase(@NonNull Context context,
@NonNull String localNumber, @NonNull String localNumber,
@NonNull final ContactTokenDetails activeToken) @NonNull final ContactTokenDetails activeToken)
{ {
return updateContactsDatabase(context, localNumber, return updateContactsDatabase(context, localNumber,
new LinkedList<ContactTokenDetails>() {{add(activeToken);}}, new LinkedList<ContactTokenDetails>() {{add(activeToken);}},
false); false);
} }
private static @NonNull List<String> updateContactsDatabase(@NonNull Context context, private static @NonNull RefreshResult updateContactsDatabase(@NonNull Context context,
@NonNull String localNumber, @NonNull String localNumber,
@NonNull List<ContactTokenDetails> activeTokens, @NonNull List<ContactTokenDetails> activeTokens,
boolean removeMissing) boolean removeMissing)
{ {
Optional<Account> account = getOrCreateAccount(context); Optional<AccountHolder> account = getOrCreateAccount(context);
if (account.isPresent()) { if (account.isPresent()) {
try { try {
return DatabaseFactory.getContactsDatabase(context) List<String> newUsers = DatabaseFactory.getContactsDatabase(context)
.setRegisteredUsers(account.get(), localNumber, activeTokens, removeMissing); .setRegisteredUsers(account.get().getAccount(), localNumber, activeTokens, removeMissing);
return new RefreshResult(newUsers, account.get().isFresh());
} catch (RemoteException | OperationApplicationException e) { } catch (RemoteException | OperationApplicationException e) {
Log.w(TAG, e); Log.w(TAG, e);
} }
} }
return new LinkedList<>(); return new RefreshResult(new LinkedList<String>(), false);
} }
private static void notifyNewUsers(@NonNull Context context, private static void notifyNewUsers(@NonNull Context context,
@ -230,33 +236,73 @@ public class DirectoryHelper {
} }
} }
private static Optional<Account> getOrCreateAccount(Context context) { private static Optional<AccountHolder> getOrCreateAccount(Context context) {
AccountManager accountManager = AccountManager.get(context); AccountManager accountManager = AccountManager.get(context);
Account[] accounts = accountManager.getAccountsByType("org.thoughtcrime.securesms"); Account[] accounts = accountManager.getAccountsByType("org.thoughtcrime.securesms");
Optional<Account> account; Optional<AccountHolder> account;
if (accounts.length == 0) account = createAccount(context); 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)) { if (account.isPresent() && !ContentResolver.getSyncAutomatically(account.get().getAccount(), ContactsContract.AUTHORITY)) {
ContentResolver.setSyncAutomatically(account.get(), ContactsContract.AUTHORITY, true); ContentResolver.setSyncAutomatically(account.get().getAccount(), ContactsContract.AUTHORITY, true);
} }
return account; return account;
} }
private static Optional<Account> createAccount(Context context) { private static Optional<AccountHolder> createAccount(Context context) {
AccountManager accountManager = AccountManager.get(context); AccountManager accountManager = AccountManager.get(context);
Account account = new Account(context.getString(R.string.app_name), "org.thoughtcrime.securesms"); Account account = new Account(context.getString(R.string.app_name), "org.thoughtcrime.securesms");
if (accountManager.addAccountExplicitly(account, null, null)) { if (accountManager.addAccountExplicitly(account, null, null)) {
Log.w(TAG, "Created new account..."); Log.w(TAG, "Created new account...");
ContentResolver.setIsSyncable(account, ContactsContract.AUTHORITY, 1); ContentResolver.setIsSyncable(account, ContactsContract.AUTHORITY, 1);
return Optional.of(account); return Optional.of(new AccountHolder(account, true));
} else { } else {
Log.w(TAG, "Failed to create account!"); Log.w(TAG, "Failed to create account!");
return Optional.absent(); 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<String> newUsers;
private final boolean fresh;
private RefreshResult(List<String> newUsers, boolean fresh) {
this.newUsers = newUsers;
this.fresh = fresh;
}
public List<String> getNewUsers() {
return newUsers;
}
public boolean isFresh() {
return fresh;
}
}
} }

View file

@ -455,4 +455,8 @@ public class Util {
return (int)value; return (int)value;
} }
public static boolean isStringEquals(String first, String second) {
if (first == null) return second == null;
return first.equals(second);
}
} }