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 840518925..27c8de587 100644
--- a/app/src/main/java/org/thoughtcrime/securesms/contacts/ContactRepository.java
+++ b/app/src/main/java/org/thoughtcrime/securesms/contacts/ContactRepository.java
@@ -27,8 +27,8 @@ import java.util.Map;
* Repository for all contacts. Allows you to filter them via queries.
*
* Currently this is implemented to return cursors. This is to ease the migration between this class
- * and the previous way we'd query contacts: {@link ContactsDatabase}. It's much easier in the
- * short-term to mock the cursor interface rather than try to switch everything over to models.
+ * and the previous way we'd query contacts. It's much easier in the short-term to mock the cursor
+ * interface rather than try to switch everything over to models.
*/
public class ContactRepository {
diff --git a/app/src/main/java/org/thoughtcrime/securesms/contacts/ContactsDatabase.java b/app/src/main/java/org/thoughtcrime/securesms/contacts/ContactsDatabase.java
deleted file mode 100644
index 0a294fc76..000000000
--- a/app/src/main/java/org/thoughtcrime/securesms/contacts/ContactsDatabase.java
+++ /dev/null
@@ -1,506 +0,0 @@
-/*
- * Copyright (C) 2013 Open Whisper Systems
- *
- * This program is free software: you can redistribute it and/or modify
- * it under the terms of the GNU General Public License as published by
- * the Free Software Foundation, either version 3 of the License, or
- * (at your option) any later version.
- *
- * This program is distributed in the hope that it will be useful,
- * but WITHOUT ANY WARRANTY; without even the implied warranty of
- * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
- * GNU General Public License for more details.
- *
- * You should have received a copy of the GNU General Public License
- * along with this program. If not, see .
- */
-package org.thoughtcrime.securesms.contacts;
-
-import android.accounts.Account;
-import android.content.ContentProviderOperation;
-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.BaseColumns;
-import android.provider.ContactsContract;
-import android.provider.ContactsContract.RawContacts;
-
-import androidx.annotation.NonNull;
-import androidx.annotation.Nullable;
-
-import org.signal.core.util.logging.Log;
-import org.thoughtcrime.securesms.R;
-import org.thoughtcrime.securesms.phonenumbers.PhoneNumberFormatter;
-import org.thoughtcrime.securesms.util.Util;
-
-import java.util.ArrayList;
-import java.util.HashMap;
-import java.util.HashSet;
-import java.util.List;
-import java.util.Map;
-import java.util.Optional;
-import java.util.Set;
-
-/**
- * Database to supply all types of contacts that TextSecure needs to know about
- *
- * @author Jake McGinty
- */
-public class ContactsDatabase {
-
- private static final String TAG = Log.tag(ContactsDatabase.class);
- private static final String CONTACT_MIMETYPE = "vnd.android.cursor.item/vnd.org.thoughtcrime.securesms.contact";
- private static final String CALL_MIMETYPE = "vnd.android.cursor.item/vnd.org.thoughtcrime.securesms.call";
- private static final String SYNC = "__TS";
-
- private final Context context;
-
- public ContactsDatabase(Context context) {
- this.context = context;
- }
-
- public synchronized void removeDeletedRawContacts(@NonNull Account account) {
- Uri currentContactsUri = RawContacts.CONTENT_URI.buildUpon()
- .appendQueryParameter(RawContacts.ACCOUNT_NAME, account.name)
- .appendQueryParameter(RawContacts.ACCOUNT_TYPE, account.type)
- .appendQueryParameter(ContactsContract.CALLER_IS_SYNCADAPTER, "true")
- .build();
-
- String[] projection = new String[] {BaseColumns._ID, RawContacts.SYNC1};
-
- try (Cursor cursor = context.getContentResolver().query(currentContactsUri, projection, RawContacts.DELETED + " = ?", new String[] {"1"}, null)) {
- while (cursor != null && cursor.moveToNext()) {
- long rawContactId = cursor.getLong(0);
- Log.i(TAG, "Deleting raw contact: " + cursor.getString(1) + ", " + rawContactId);
-
- context.getContentResolver().delete(currentContactsUri, RawContacts._ID + " = ?", new String[] {String.valueOf(rawContactId)});
- }
- }
- }
-
- public synchronized void setRegisteredUsers(@NonNull Account account,
- @NonNull List registeredAddressList,
- boolean remove)
- throws RemoteException, OperationApplicationException
- {
- Set registeredAddressSet = new HashSet<>(registeredAddressList);
- ArrayList operations = new ArrayList<>();
- Map currentContacts = getSignalRawContacts(account);
- List> registeredChunks = Util.chunk(registeredAddressList, 50);
-
- for (List registeredChunk : registeredChunks) {
- for (String registeredAddress : registeredChunk) {
- if (!currentContacts.containsKey(registeredAddress)) {
- Optional systemContactInfo = getSystemContactInfo(registeredAddress);
-
- if (systemContactInfo.isPresent()) {
- Log.i(TAG, "Adding number: " + registeredAddress);
- addTextSecureRawContact(operations, account, systemContactInfo.get().number,
- systemContactInfo.get().name, systemContactInfo.get().id);
- }
- }
- }
- if (!operations.isEmpty()) {
- context.getContentResolver().applyBatch(ContactsContract.AUTHORITY, operations);
- operations.clear();
- }
- }
-
- for (Map.Entry currentContactEntry : currentContacts.entrySet()) {
- if (!registeredAddressSet.contains(currentContactEntry.getKey())) {
- if (remove) {
- Log.i(TAG, "Removing number: " + currentContactEntry.getKey());
- removeTextSecureRawContact(operations, account, currentContactEntry.getValue().getId());
- }
- } else if (!currentContactEntry.getValue().isVoiceSupported()) {
- Log.i(TAG, "Adding voice support: " + currentContactEntry.getKey());
- addContactVoiceSupport(operations, currentContactEntry.getKey(), currentContactEntry.getValue().getId());
- } else if (!Util.isStringEquals(currentContactEntry.getValue().getRawDisplayName(),
- currentContactEntry.getValue().getAggregateDisplayName()))
- {
- Log.i(TAG, "Updating display name: " + currentContactEntry.getKey());
- updateDisplayName(operations, currentContactEntry.getValue().getAggregateDisplayName(), currentContactEntry.getValue().getId(), currentContactEntry.getValue().getDisplayNameSource());
- }
- }
-
- if (!operations.isEmpty()) {
- applyOperationsInBatches(context.getContentResolver(), ContactsContract.AUTHORITY, operations, 50);
- }
- }
-
- public @Nullable Cursor getNameDetails(long contactId) {
- String[] projection = new String[] { ContactsContract.CommonDataKinds.StructuredName.DISPLAY_NAME,
- ContactsContract.CommonDataKinds.StructuredName.GIVEN_NAME,
- ContactsContract.CommonDataKinds.StructuredName.FAMILY_NAME,
- ContactsContract.CommonDataKinds.StructuredName.PREFIX,
- ContactsContract.CommonDataKinds.StructuredName.SUFFIX,
- ContactsContract.CommonDataKinds.StructuredName.MIDDLE_NAME };
- String selection = ContactsContract.Data.CONTACT_ID + " = ? AND " + ContactsContract.Data.MIMETYPE + " = ?";
- String[] args = new String[] { String.valueOf(contactId), ContactsContract.CommonDataKinds.StructuredName.CONTENT_ITEM_TYPE };
-
- return context.getContentResolver().query(ContactsContract.Data.CONTENT_URI,
- projection,
- selection,
- args,
- null);
- }
-
- public @Nullable String getOrganizationName(long contactId) {
- String[] projection = new String[] { ContactsContract.CommonDataKinds.Organization.COMPANY };
- String selection = ContactsContract.Data.CONTACT_ID + " = ? AND " + ContactsContract.Data.MIMETYPE + " = ?";
- String[] args = new String[] { String.valueOf(contactId), ContactsContract.CommonDataKinds.Organization.CONTENT_ITEM_TYPE };
-
- try (Cursor cursor = context.getContentResolver().query(ContactsContract.Data.CONTENT_URI,
- projection,
- selection,
- args,
- null))
- {
- if (cursor != null && cursor.moveToFirst()) {
- return cursor.getString(0);
- }
- }
-
- return null;
- }
-
- public @Nullable Cursor getPhoneDetails(long contactId) {
- String[] projection = new String[] { ContactsContract.CommonDataKinds.Phone.NUMBER,
- ContactsContract.CommonDataKinds.Phone.TYPE,
- ContactsContract.CommonDataKinds.Phone.LABEL };
- String selection = ContactsContract.Data.CONTACT_ID + " = ? AND " + ContactsContract.Data.MIMETYPE + " = ?";
- String[] args = new String[] { String.valueOf(contactId), ContactsContract.CommonDataKinds.Phone.CONTENT_ITEM_TYPE };
-
- return context.getContentResolver().query(ContactsContract.Data.CONTENT_URI,
- projection,
- selection,
- args,
- null);
- }
-
- public @Nullable Cursor getEmailDetails(long contactId) {
- String[] projection = new String[] { ContactsContract.CommonDataKinds.Email.ADDRESS,
- ContactsContract.CommonDataKinds.Email.TYPE,
- ContactsContract.CommonDataKinds.Email.LABEL };
- String selection = ContactsContract.Data.CONTACT_ID + " = ? AND " + ContactsContract.Data.MIMETYPE + " = ?";
- String[] args = new String[] { String.valueOf(contactId), ContactsContract.CommonDataKinds.Email.CONTENT_ITEM_TYPE };
-
- return context.getContentResolver().query(ContactsContract.Data.CONTENT_URI,
- projection,
- selection,
- args,
- null);
- }
-
- public @Nullable Cursor getPostalAddressDetails(long contactId) {
- String[] projection = new String[] { ContactsContract.CommonDataKinds.StructuredPostal.TYPE,
- ContactsContract.CommonDataKinds.StructuredPostal.LABEL,
- ContactsContract.CommonDataKinds.StructuredPostal.STREET,
- ContactsContract.CommonDataKinds.StructuredPostal.POBOX,
- ContactsContract.CommonDataKinds.StructuredPostal.NEIGHBORHOOD,
- ContactsContract.CommonDataKinds.StructuredPostal.CITY,
- ContactsContract.CommonDataKinds.StructuredPostal.REGION,
- ContactsContract.CommonDataKinds.StructuredPostal.POSTCODE,
- ContactsContract.CommonDataKinds.StructuredPostal.COUNTRY };
- String selection = ContactsContract.Data.CONTACT_ID + " = ? AND " + ContactsContract.Data.MIMETYPE + " = ?";
- String[] args = new String[] { String.valueOf(contactId), ContactsContract.CommonDataKinds.StructuredPostal.CONTENT_ITEM_TYPE };
-
- return context.getContentResolver().query(ContactsContract.Data.CONTENT_URI,
- projection,
- selection,
- args,
- null);
- }
-
- public @Nullable Uri getAvatarUri(long contactId) {
- String[] projection = new String[] { ContactsContract.CommonDataKinds.Photo.PHOTO_URI };
- String selection = ContactsContract.Data.CONTACT_ID + " = ? AND " + ContactsContract.Data.MIMETYPE + " = ?";
- String[] args = new String[] { String.valueOf(contactId), ContactsContract.CommonDataKinds.Photo.CONTENT_ITEM_TYPE };
-
- try (Cursor cursor = context.getContentResolver().query(ContactsContract.Data.CONTENT_URI,
- projection,
- selection,
- args,
- null))
- {
- if (cursor != null && cursor.moveToFirst()) {
- String uri = cursor.getString(0);
- if (uri != null) {
- return Uri.parse(uri);
- }
- }
- }
-
- return null;
- }
-
-
-
- private void addContactVoiceSupport(List operations,
- @NonNull String address, long rawContactId)
- {
- operations.add(ContentProviderOperation.newUpdate(RawContacts.CONTENT_URI)
- .withSelection(RawContacts._ID + " = ?", new String[] {String.valueOf(rawContactId)})
- .withValue(RawContacts.SYNC4, "true")
- .build());
-
- operations.add(ContentProviderOperation.newInsert(ContactsContract.Data.CONTENT_URI.buildUpon().appendQueryParameter(ContactsContract.CALLER_IS_SYNCADAPTER, "true").build())
- .withValue(ContactsContract.Data.RAW_CONTACT_ID, rawContactId)
- .withValue(ContactsContract.Data.MIMETYPE, CALL_MIMETYPE)
- .withValue(ContactsContract.Data.DATA1, address)
- .withValue(ContactsContract.Data.DATA2, context.getString(R.string.app_name))
- .withValue(ContactsContract.Data.DATA3, context.getString(R.string.ContactsDatabase_signal_call_s, address))
- .withYieldAllowed(true)
- .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 addTextSecureRawContact(List operations,
- Account account, String e164number, String displayName,
- long aggregateId)
- {
- int index = operations.size();
- Uri dataUri = ContactsContract.Data.CONTENT_URI.buildUpon()
- .appendQueryParameter(ContactsContract.CALLER_IS_SYNCADAPTER, "true")
- .build();
-
- operations.add(ContentProviderOperation.newInsert(RawContacts.CONTENT_URI)
- .withValue(RawContacts.ACCOUNT_NAME, account.name)
- .withValue(RawContacts.ACCOUNT_TYPE, account.type)
- .withValue(RawContacts.SYNC1, e164number)
- .withValue(RawContacts.SYNC4, String.valueOf(true))
- .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)
- .withValue(ContactsContract.CommonDataKinds.Phone.NUMBER, e164number)
- .withValue(ContactsContract.CommonDataKinds.Phone.TYPE, ContactsContract.CommonDataKinds.Phone.TYPE_OTHER)
- .withValue(ContactsContract.Data.SYNC2, SYNC)
- .build());
-
- operations.add(ContentProviderOperation.newInsert(dataUri)
- .withValueBackReference(ContactsContract.Data.RAW_CONTACT_ID, index)
- .withValue(ContactsContract.Data.MIMETYPE, CONTACT_MIMETYPE)
- .withValue(ContactsContract.Data.DATA1, e164number)
- .withValue(ContactsContract.Data.DATA2, context.getString(R.string.app_name))
- .withValue(ContactsContract.Data.DATA3, context.getString(R.string.ContactsDatabase_message_s, e164number))
- .withYieldAllowed(true)
- .build());
-
- operations.add(ContentProviderOperation.newInsert(dataUri)
- .withValueBackReference(ContactsContract.Data.RAW_CONTACT_ID, index)
- .withValue(ContactsContract.Data.MIMETYPE, CALL_MIMETYPE)
- .withValue(ContactsContract.Data.DATA1, e164number)
- .withValue(ContactsContract.Data.DATA2, context.getString(R.string.app_name))
- .withValue(ContactsContract.Data.DATA3, context.getString(R.string.ContactsDatabase_signal_call_s, e164number))
- .withYieldAllowed(true)
- .build());
-
- operations.add(ContentProviderOperation.newUpdate(ContactsContract.AggregationExceptions.CONTENT_URI)
- .withValue(ContactsContract.AggregationExceptions.RAW_CONTACT_ID1, aggregateId)
- .withValueBackReference(ContactsContract.AggregationExceptions.RAW_CONTACT_ID2, index)
- .withValue(ContactsContract.AggregationExceptions.TYPE, ContactsContract.AggregationExceptions.TYPE_KEEP_TOGETHER)
- .build());
- }
-
- private void removeTextSecureRawContact(List operations,
- Account account, long rowId)
- {
- operations.add(ContentProviderOperation.newDelete(RawContacts.CONTENT_URI.buildUpon()
- .appendQueryParameter(RawContacts.ACCOUNT_NAME, account.name)
- .appendQueryParameter(RawContacts.ACCOUNT_TYPE, account.type)
- .appendQueryParameter(ContactsContract.CALLER_IS_SYNCADAPTER, "true").build())
- .withYieldAllowed(true)
- .withSelection(BaseColumns._ID + " = ?", new String[] {String.valueOf(rowId)})
- .build());
- }
-
- private @NonNull Map getSignalRawContacts(@NonNull Account account) {
- Uri currentContactsUri = RawContacts.CONTENT_URI.buildUpon()
- .appendQueryParameter(RawContacts.ACCOUNT_NAME, account.name)
- .appendQueryParameter(RawContacts.ACCOUNT_TYPE, account.type).build();
-
- Map signalContacts = new HashMap<>();
- Cursor cursor = null;
-
- try {
- String[] projection = new String[] {BaseColumns._ID, RawContacts.SYNC1, RawContacts.SYNC4, RawContacts.CONTACT_ID, RawContacts.DISPLAY_NAME_PRIMARY, RawContacts.DISPLAY_NAME_SOURCE};
-
- cursor = context.getContentResolver().query(currentContactsUri, projection, null, null, null);
-
- while (cursor != null && cursor.moveToNext()) {
- String currentAddress = PhoneNumberFormatter.get(context).format(cursor.getString(1));
- long rawContactId = cursor.getLong(0);
- long contactId = cursor.getLong(3);
- String supportsVoice = cursor.getString(2);
- String rawContactDisplayName = cursor.getString(4);
- String aggregateDisplayName = getDisplayName(contactId);
- int rawContactDisplayNameSource = cursor.getInt(5);
-
- signalContacts.put(currentAddress, new SignalContact(rawContactId, supportsVoice, rawContactDisplayName, aggregateDisplayName, rawContactDisplayNameSource));
- }
- } finally {
- if (cursor != null)
- cursor.close();
- }
-
- return signalContacts;
- }
-
- private Optional getSystemContactInfo(@NonNull String address)
- {
- Uri uri = Uri.withAppendedPath(ContactsContract.PhoneLookup.CONTENT_FILTER_URI, Uri.encode(address));
- String[] projection = {ContactsContract.PhoneLookup.NUMBER,
- ContactsContract.PhoneLookup._ID,
- ContactsContract.PhoneLookup.DISPLAY_NAME};
- Cursor numberCursor = null;
- Cursor idCursor = null;
-
- try {
- numberCursor = context.getContentResolver().query(uri, projection, null, null, null);
-
- while (numberCursor != null && numberCursor.moveToNext()) {
- String systemNumber = numberCursor.getString(0);
- String systemAddress = PhoneNumberFormatter.get(context).format(systemNumber);
-
- if (systemAddress.equals(address)) {
- idCursor = context.getContentResolver().query(RawContacts.CONTENT_URI,
- new String[] {RawContacts._ID},
- RawContacts.CONTACT_ID + " = ? ",
- new String[] {String.valueOf(numberCursor.getLong(1))},
- null);
-
- if (idCursor != null && idCursor.moveToNext()) {
- return Optional.of(new SystemContactInfo(numberCursor.getString(2),
- numberCursor.getString(0),
- idCursor.getLong(0)));
- }
- }
- }
- } finally {
- if (numberCursor != null) numberCursor.close();
- if (idCursor != null) idCursor.close();
- }
-
- return Optional.empty();
- }
-
- 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 void applyOperationsInBatches(@NonNull ContentResolver contentResolver,
- @NonNull String authority,
- @NonNull List operations,
- int batchSize)
- throws OperationApplicationException, RemoteException
- {
- List> batches = Util.chunk(operations, batchSize);
- for (List batch : batches) {
- contentResolver.applyBatch(authority, new ArrayList<>(batch));
- }
- }
-
- private static class SystemContactInfo {
- private final String name;
- private final String number;
- private final long id;
-
- private SystemContactInfo(String name, String number, long id) {
- this.name = name;
- this.number = number;
- this.id = id;
- }
- }
-
- 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;
-
- 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;
- }
-
- boolean isVoiceSupported() {
- return "true".equals(supportsVoice);
- }
-
- @Nullable
- String getRawDisplayName() {
- return rawDisplayName;
- }
-
- @Nullable
- String getAggregateDisplayName() {
- return aggregateDisplayName;
- }
-
- int getDisplayNameSource() {
- return displayNameSource;
- }
- }
-}
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 38d5d6d25..75c59eb74 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
@@ -19,10 +19,7 @@ import com.annimon.stream.Collectors;
import com.annimon.stream.Stream;
import org.signal.core.util.logging.Log;
-import org.thoughtcrime.securesms.BuildConfig;
-import org.thoughtcrime.securesms.R;
import org.thoughtcrime.securesms.contacts.ContactAccessor;
-import org.thoughtcrime.securesms.contacts.ContactsDatabase;
import org.thoughtcrime.securesms.database.MessageDatabase.InsertResult;
import org.thoughtcrime.securesms.database.RecipientDatabase;
import org.thoughtcrime.securesms.database.RecipientDatabase.BulkOperationsHandle;
@@ -307,7 +304,7 @@ class DirectoryHelper {
return;
}
- AccountHolder account = getOrCreateSystemAccount(context);
+ Account account = SystemContactsRepository.getOrCreateSystemAccount(context);
if (account == null) {
Log.w(TAG, "Failed to create an account!");
@@ -315,15 +312,14 @@ class DirectoryHelper {
}
try {
- ContactsDatabase contactsDatabase = SignalDatabase.contacts();
- List activeAddresses = Stream.of(activeIds)
- .map(Recipient::resolved)
- .filter(Recipient::hasE164)
- .map(Recipient::requireE164)
- .toList();
+ List activeAddresses = Stream.of(activeIds)
+ .map(Recipient::resolved)
+ .filter(Recipient::hasE164)
+ .map(Recipient::requireE164)
+ .toList();
- contactsDatabase.removeDeletedRawContacts(account.getAccount());
- contactsDatabase.setRegisteredUsers(account.getAccount(), activeAddresses, removeMissing);
+ SystemContactsRepository.removeDeletedRawContacts(context, account);
+ SystemContactsRepository.setRegisteredUsers(context, account, activeAddresses, removeMissing);
syncRecipientInfoWithSystemContacts(context, rewrites);
} catch (RemoteException | OperationApplicationException e) {
@@ -425,39 +421,6 @@ class DirectoryHelper {
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);
-
- AccountHolder account;
-
- if (accounts.length == 0) {
- account = createAccount(context);
- } else {
- account = new AccountHolder(accounts[0], false);
- }
-
- if (account != null && !ContentResolver.getSyncAutomatically(account.getAccount(), ContactsContract.AUTHORITY)) {
- ContentResolver.setSyncAutomatically(account.getAccount(), ContactsContract.AUTHORITY, true);
- }
-
- return account;
- }
-
- private static @Nullable AccountHolder createAccount(Context context) {
- AccountManager accountManager = AccountManager.get(context);
- Account account = new Account(context.getString(R.string.app_name), BuildConfig.APPLICATION_ID);
-
- if (accountManager.addAccountExplicitly(account, null, null)) {
- Log.i(TAG, "Created new account...");
- ContentResolver.setIsSyncable(account, ContactsContract.AUTHORITY, 1);
- return new AccountHolder(account, true);
- } else {
- Log.w(TAG, "Failed to create account!");
- return null;
- }
- }
-
private static void notifyNewUsers(@NonNull Context context,
@NonNull Collection newUsers)
{
@@ -611,23 +574,4 @@ class DirectoryHelper {
}
}
}
-
- private static class AccountHolder {
- private final boolean fresh;
- private final Account account;
-
- private AccountHolder(Account account, boolean fresh) {
- this.fresh = fresh;
- this.account = account;
- }
-
- @SuppressWarnings("unused")
- public boolean isFresh() {
- return fresh;
- }
-
- public Account getAccount() {
- return account;
- }
- }
}
diff --git a/app/src/main/java/org/thoughtcrime/securesms/contacts/sync/SystemContactsRepository.kt b/app/src/main/java/org/thoughtcrime/securesms/contacts/sync/SystemContactsRepository.kt
new file mode 100644
index 000000000..d25a721e0
--- /dev/null
+++ b/app/src/main/java/org/thoughtcrime/securesms/contacts/sync/SystemContactsRepository.kt
@@ -0,0 +1,539 @@
+package org.thoughtcrime.securesms.contacts.sync
+
+import android.accounts.Account
+import android.accounts.AccountManager
+import android.content.ContentProviderOperation
+import android.content.ContentResolver
+import android.content.Context
+import android.content.OperationApplicationException
+import android.net.Uri
+import android.os.RemoteException
+import android.provider.BaseColumns
+import android.provider.ContactsContract
+import org.signal.core.util.logging.Log
+import org.thoughtcrime.securesms.BuildConfig
+import org.thoughtcrime.securesms.R
+import org.thoughtcrime.securesms.database.requireInt
+import org.thoughtcrime.securesms.database.requireString
+import org.thoughtcrime.securesms.phonenumbers.PhoneNumberFormatter
+import org.thoughtcrime.securesms.util.SqlUtil
+import org.thoughtcrime.securesms.util.Util
+import java.util.ArrayList
+import java.util.HashMap
+
+/**
+ * A way to retrieve and update data in the Android system contacts.
+ */
+object SystemContactsRepository {
+
+ private val TAG = Log.tag(SystemContactsRepository::class.java)
+ private const val CONTACT_MIMETYPE = "vnd.android.cursor.item/vnd.org.thoughtcrime.securesms.contact"
+ private const val CALL_MIMETYPE = "vnd.android.cursor.item/vnd.org.thoughtcrime.securesms.call"
+ private const val SYNC = "__TS"
+
+ @JvmStatic
+ fun getOrCreateSystemAccount(context: Context): Account? {
+ val accountManager: AccountManager = AccountManager.get(context)
+ val accounts: Array = accountManager.getAccountsByType(BuildConfig.APPLICATION_ID)
+ var account: Account? = if (accounts.isNotEmpty()) accounts[0] else null
+
+ if (account == null) {
+ Log.i(TAG, "Attempting to create a new account...")
+ val newAccount = Account(context.getString(R.string.app_name), BuildConfig.APPLICATION_ID)
+
+ if (accountManager.addAccountExplicitly(newAccount, null, null)) {
+ Log.i(TAG, "Successfully created a new account.")
+ ContentResolver.setIsSyncable(newAccount, ContactsContract.AUTHORITY, 1)
+ account = newAccount
+ } else {
+ Log.w(TAG, "Failed to create a new account!")
+ }
+ }
+
+ if (account != null && !ContentResolver.getSyncAutomatically(account, ContactsContract.AUTHORITY)) {
+ Log.i(TAG, "Updated account to sync automatically.")
+ ContentResolver.setSyncAutomatically(account, ContactsContract.AUTHORITY, true)
+ }
+
+ return account
+ }
+
+ @JvmStatic
+ @Synchronized
+ fun removeDeletedRawContacts(context: Context, account: Account) {
+ val currentContactsUri = ContactsContract.RawContacts.CONTENT_URI.buildUpon()
+ .appendQueryParameter(ContactsContract.RawContacts.ACCOUNT_NAME, account.name)
+ .appendQueryParameter(ContactsContract.RawContacts.ACCOUNT_TYPE, account.type)
+ .appendQueryParameter(ContactsContract.CALLER_IS_SYNCADAPTER, "true")
+ .build()
+
+ val projection = arrayOf(BaseColumns._ID, ContactsContract.RawContacts.SYNC1)
+
+ context.contentResolver.query(currentContactsUri, projection, "${ContactsContract.RawContacts.DELETED} = ?", SqlUtil.buildArgs(1), null)?.use { cursor ->
+ while (cursor.moveToNext()) {
+ val rawContactId = cursor.getLong(0)
+
+ Log.i(TAG, """Deleting raw contact: ${cursor.getString(1)}, $rawContactId""")
+ context.contentResolver.delete(currentContactsUri, "${ContactsContract.RawContacts._ID} = ?", arrayOf(rawContactId.toString()))
+ }
+ }
+ }
+
+ @JvmStatic
+ @Synchronized
+ @Throws(RemoteException::class, OperationApplicationException::class)
+ fun setRegisteredUsers(
+ context: Context,
+ account: Account,
+ registeredAddressList: List,
+ remove: Boolean
+ ) {
+ val registeredAddressSet: Set = registeredAddressList.toSet()
+ val operations: ArrayList = ArrayList()
+ val currentContacts: Map = getSignalRawContacts(context, account)
+
+ val registeredChunks: List> = Util.chunk(registeredAddressList, 50)
+ for (registeredChunk in registeredChunks) {
+ for (registeredAddress in registeredChunk) {
+ if (!currentContacts.containsKey(registeredAddress)) {
+ val systemContactInfo: SystemContactInfo? = getSystemContactInfo(context, registeredAddress)
+ if (systemContactInfo != null) {
+ Log.i(TAG, "Adding number: $registeredAddress")
+ addTextSecureRawContact(
+ context = context,
+ operations = operations,
+ account = account,
+ e164number = systemContactInfo.number,
+ displayName = systemContactInfo.name,
+ aggregateId = systemContactInfo.id
+ )
+ }
+ }
+ }
+
+ if (operations.isNotEmpty()) {
+ context.contentResolver.applyBatch(ContactsContract.AUTHORITY, operations)
+ operations.clear()
+ }
+ }
+
+ for ((key, value) in currentContacts) {
+ if (!registeredAddressSet.contains(key)) {
+ if (remove) {
+ Log.i(TAG, "Removing number: $key")
+ removeTextSecureRawContact(operations, account, value.id)
+ }
+ } else if (!value.isVoiceSupported()) {
+ Log.i(TAG, "Adding voice support: $key")
+ addContactVoiceSupport(context, operations, key, value.id)
+ } else if (!Util.isStringEquals(value.rawDisplayName, value.aggregateDisplayName)) {
+ Log.i(TAG, "Updating display name: $key")
+ updateDisplayName(operations, value.aggregateDisplayName, value.id, value.displayNameSource)
+ }
+ }
+
+ if (operations.isNotEmpty()) {
+ applyOperationsInBatches(context.contentResolver, ContactsContract.AUTHORITY, operations, 50)
+ }
+ }
+
+ @JvmStatic
+ fun getNameDetails(context: Context, contactId: Long): NameDetails? {
+ val projection = arrayOf(
+ ContactsContract.CommonDataKinds.StructuredName.DISPLAY_NAME,
+ ContactsContract.CommonDataKinds.StructuredName.GIVEN_NAME,
+ ContactsContract.CommonDataKinds.StructuredName.FAMILY_NAME,
+ ContactsContract.CommonDataKinds.StructuredName.PREFIX,
+ ContactsContract.CommonDataKinds.StructuredName.SUFFIX,
+ ContactsContract.CommonDataKinds.StructuredName.MIDDLE_NAME
+ )
+ val selection = "${ContactsContract.Data.CONTACT_ID} = ? AND ${ContactsContract.Data.MIMETYPE} = ?"
+ val args = SqlUtil.buildArgs(contactId, ContactsContract.CommonDataKinds.StructuredName.CONTENT_ITEM_TYPE)
+
+ return context.contentResolver.query(ContactsContract.Data.CONTENT_URI, projection, selection, args, null)?.use { cursor ->
+ if (cursor.moveToFirst()) {
+ NameDetails(
+ displayName = cursor.requireString(ContactsContract.CommonDataKinds.StructuredName.DISPLAY_NAME),
+ givenName = cursor.requireString(ContactsContract.CommonDataKinds.StructuredName.GIVEN_NAME),
+ familyName = cursor.requireString(ContactsContract.CommonDataKinds.StructuredName.FAMILY_NAME),
+ prefix = cursor.requireString(ContactsContract.CommonDataKinds.StructuredName.PREFIX),
+ suffix = cursor.requireString(ContactsContract.CommonDataKinds.StructuredName.SUFFIX),
+ middleName = cursor.requireString(ContactsContract.CommonDataKinds.StructuredName.MIDDLE_NAME)
+ )
+ } else {
+ null
+ }
+ }
+ }
+
+ @JvmStatic
+ fun getOrganizationName(context: Context, contactId: Long): String? {
+ val projection = arrayOf(ContactsContract.CommonDataKinds.Organization.COMPANY)
+ val selection = "${ContactsContract.Data.CONTACT_ID} = ? AND ${ContactsContract.Data.MIMETYPE} = ?"
+ val args = SqlUtil.buildArgs(contactId, ContactsContract.CommonDataKinds.Organization.CONTENT_ITEM_TYPE)
+
+ context.contentResolver.query(ContactsContract.Data.CONTENT_URI, projection, selection, args, null)?.use { cursor ->
+ if (cursor.moveToFirst()) {
+ return cursor.getString(0)
+ }
+ }
+
+ return null
+ }
+
+ @JvmStatic
+ fun getPhoneDetails(context: Context, contactId: Long): List {
+ val projection = arrayOf(
+ ContactsContract.CommonDataKinds.Phone.NUMBER,
+ ContactsContract.CommonDataKinds.Phone.TYPE,
+ ContactsContract.CommonDataKinds.Phone.LABEL
+ )
+ val selection = "${ContactsContract.Data.CONTACT_ID} = ? AND ${ContactsContract.Data.MIMETYPE} = ?"
+ val args = SqlUtil.buildArgs(contactId, ContactsContract.CommonDataKinds.Phone.CONTENT_ITEM_TYPE)
+
+ val phoneDetails: MutableList = mutableListOf()
+
+ context.contentResolver.query(ContactsContract.Data.CONTENT_URI, projection, selection, args, null)?.use { cursor ->
+ while (cursor.moveToNext()) {
+ phoneDetails += PhoneDetails(
+ number = cursor.requireString(ContactsContract.CommonDataKinds.Phone.NUMBER),
+ type = cursor.requireInt(ContactsContract.CommonDataKinds.Phone.TYPE),
+ label = cursor.requireString(ContactsContract.CommonDataKinds.Phone.LABEL)
+ )
+ }
+ }
+
+ return phoneDetails
+ }
+
+ @JvmStatic
+ fun getEmailDetails(context: Context, contactId: Long): List {
+ val projection = arrayOf(
+ ContactsContract.CommonDataKinds.Email.ADDRESS,
+ ContactsContract.CommonDataKinds.Email.TYPE,
+ ContactsContract.CommonDataKinds.Email.LABEL
+ )
+ val selection = "${ContactsContract.Data.CONTACT_ID} = ? AND ${ContactsContract.Data.MIMETYPE} = ?"
+ val args = SqlUtil.buildArgs(contactId, ContactsContract.CommonDataKinds.Email.CONTENT_ITEM_TYPE)
+
+ val emailDetails: MutableList = mutableListOf()
+ context.contentResolver.query(ContactsContract.Data.CONTENT_URI, projection, selection, args, null)?.use { cursor ->
+ while (cursor.moveToNext()) {
+ emailDetails += EmailDetails(
+ address = cursor.requireString(ContactsContract.CommonDataKinds.Email.ADDRESS),
+ type = cursor.requireInt(ContactsContract.CommonDataKinds.Email.TYPE),
+ label = cursor.requireString(ContactsContract.CommonDataKinds.Email.LABEL)
+ )
+ }
+ }
+
+ return emailDetails
+ }
+
+ @JvmStatic
+ fun getPostalAddressDetails(context: Context, contactId: Long): List {
+ val projection = arrayOf(
+ ContactsContract.CommonDataKinds.StructuredPostal.TYPE,
+ ContactsContract.CommonDataKinds.StructuredPostal.LABEL,
+ ContactsContract.CommonDataKinds.StructuredPostal.STREET,
+ ContactsContract.CommonDataKinds.StructuredPostal.POBOX,
+ ContactsContract.CommonDataKinds.StructuredPostal.NEIGHBORHOOD,
+ ContactsContract.CommonDataKinds.StructuredPostal.CITY,
+ ContactsContract.CommonDataKinds.StructuredPostal.REGION,
+ ContactsContract.CommonDataKinds.StructuredPostal.POSTCODE,
+ ContactsContract.CommonDataKinds.StructuredPostal.COUNTRY
+ )
+ val selection = "${ContactsContract.Data.CONTACT_ID} = ? AND ${ContactsContract.Data.MIMETYPE} = ?"
+ val args = SqlUtil.buildArgs(contactId, ContactsContract.CommonDataKinds.StructuredPostal.CONTENT_ITEM_TYPE)
+
+ val postalDetails: MutableList = mutableListOf()
+
+ context.contentResolver.query(ContactsContract.Data.CONTENT_URI, projection, selection, args, null)?.use { cursor ->
+ while (cursor.moveToNext()) {
+ postalDetails += PostalAddressDetails(
+ type = cursor.requireInt(ContactsContract.CommonDataKinds.StructuredPostal.TYPE),
+ label = cursor.requireString(ContactsContract.CommonDataKinds.StructuredPostal.LABEL),
+ street = cursor.requireString(ContactsContract.CommonDataKinds.StructuredPostal.STREET),
+ poBox = cursor.requireString(ContactsContract.CommonDataKinds.StructuredPostal.POBOX),
+ neighborhood = cursor.requireString(ContactsContract.CommonDataKinds.StructuredPostal.NEIGHBORHOOD),
+ city = cursor.requireString(ContactsContract.CommonDataKinds.StructuredPostal.CITY),
+ region = cursor.requireString(ContactsContract.CommonDataKinds.StructuredPostal.REGION),
+ postal = cursor.requireString(ContactsContract.CommonDataKinds.StructuredPostal.POSTCODE),
+ country = cursor.requireString(ContactsContract.CommonDataKinds.StructuredPostal.COUNTRY)
+ )
+ }
+ }
+
+ return postalDetails
+ }
+
+ @JvmStatic
+ fun getAvatarUri(context: Context, contactId: Long): Uri? {
+ val projection = arrayOf(ContactsContract.CommonDataKinds.Photo.PHOTO_URI)
+ val selection = "${ContactsContract.Data.CONTACT_ID} = ? AND ${ContactsContract.Data.MIMETYPE} = ?"
+ val args = SqlUtil.buildArgs(contactId, ContactsContract.CommonDataKinds.Photo.CONTENT_ITEM_TYPE)
+
+ context.contentResolver.query(ContactsContract.Data.CONTENT_URI, projection, selection, args, null)?.use { cursor ->
+ if (cursor.moveToFirst()) {
+ val uri = cursor.getString(0)
+ if (uri != null) {
+ return Uri.parse(uri)
+ }
+ }
+ }
+
+ return null
+ }
+
+ private fun addContactVoiceSupport(context: Context, operations: MutableList, address: String, rawContactId: Long) {
+ operations.add(
+ ContentProviderOperation.newUpdate(ContactsContract.RawContacts.CONTENT_URI)
+ .withSelection("${ContactsContract.RawContacts._ID} = ?", arrayOf(rawContactId.toString()))
+ .withValue(ContactsContract.RawContacts.SYNC4, "true")
+ .build()
+ )
+
+ operations.add(
+ ContentProviderOperation.newInsert(ContactsContract.Data.CONTENT_URI.buildUpon().appendQueryParameter(ContactsContract.CALLER_IS_SYNCADAPTER, "true").build())
+ .withValue(ContactsContract.Data.RAW_CONTACT_ID, rawContactId)
+ .withValue(ContactsContract.Data.MIMETYPE, CALL_MIMETYPE)
+ .withValue(ContactsContract.Data.DATA1, address)
+ .withValue(ContactsContract.Data.DATA2, context.getString(R.string.app_name))
+ .withValue(ContactsContract.Data.DATA3, context.getString(R.string.ContactsDatabase_signal_call_s, address))
+ .withYieldAllowed(true)
+ .build()
+ )
+ }
+
+ private fun updateDisplayName(operations: MutableList, displayName: String?, rawContactId: Long, displayNameSource: Int) {
+ val 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} = ?", SqlUtil.buildArgs(rawContactId.toString(), 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 fun addTextSecureRawContact(
+ context: Context,
+ operations: MutableList,
+ account: Account,
+ e164number: String,
+ displayName: String,
+ aggregateId: Long
+ ) {
+ val index = operations.size
+ val dataUri = ContactsContract.Data.CONTENT_URI.buildUpon()
+ .appendQueryParameter(ContactsContract.CALLER_IS_SYNCADAPTER, "true")
+ .build()
+
+ operations.add(
+ ContentProviderOperation.newInsert(ContactsContract.RawContacts.CONTENT_URI)
+ .withValue(ContactsContract.RawContacts.ACCOUNT_NAME, account.name)
+ .withValue(ContactsContract.RawContacts.ACCOUNT_TYPE, account.type)
+ .withValue(ContactsContract.RawContacts.SYNC1, e164number)
+ .withValue(ContactsContract.RawContacts.SYNC4, true.toString())
+ .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)
+ .withValue(ContactsContract.CommonDataKinds.Phone.NUMBER, e164number)
+ .withValue(ContactsContract.CommonDataKinds.Phone.TYPE, ContactsContract.CommonDataKinds.Phone.TYPE_OTHER)
+ .withValue(ContactsContract.Data.SYNC2, SYNC)
+ .build()
+ )
+ operations.add(
+ ContentProviderOperation.newInsert(dataUri)
+ .withValueBackReference(ContactsContract.Data.RAW_CONTACT_ID, index)
+ .withValue(ContactsContract.Data.MIMETYPE, CONTACT_MIMETYPE)
+ .withValue(ContactsContract.Data.DATA1, e164number)
+ .withValue(ContactsContract.Data.DATA2, context.getString(R.string.app_name))
+ .withValue(ContactsContract.Data.DATA3, context.getString(R.string.ContactsDatabase_message_s, e164number))
+ .withYieldAllowed(true)
+ .build()
+ )
+ operations.add(
+ ContentProviderOperation.newInsert(dataUri)
+ .withValueBackReference(ContactsContract.Data.RAW_CONTACT_ID, index)
+ .withValue(ContactsContract.Data.MIMETYPE, CALL_MIMETYPE)
+ .withValue(ContactsContract.Data.DATA1, e164number)
+ .withValue(ContactsContract.Data.DATA2, context.getString(R.string.app_name))
+ .withValue(ContactsContract.Data.DATA3, context.getString(R.string.ContactsDatabase_signal_call_s, e164number))
+ .withYieldAllowed(true)
+ .build()
+ )
+ operations.add(
+ ContentProviderOperation.newUpdate(ContactsContract.AggregationExceptions.CONTENT_URI)
+ .withValue(ContactsContract.AggregationExceptions.RAW_CONTACT_ID1, aggregateId)
+ .withValueBackReference(ContactsContract.AggregationExceptions.RAW_CONTACT_ID2, index)
+ .withValue(ContactsContract.AggregationExceptions.TYPE, ContactsContract.AggregationExceptions.TYPE_KEEP_TOGETHER)
+ .build()
+ )
+ }
+
+ private fun removeTextSecureRawContact(operations: MutableList, account: Account, rowId: Long) {
+ operations.add(
+ ContentProviderOperation.newDelete(
+ ContactsContract.RawContacts.CONTENT_URI.buildUpon()
+ .appendQueryParameter(ContactsContract.RawContacts.ACCOUNT_NAME, account.name)
+ .appendQueryParameter(ContactsContract.RawContacts.ACCOUNT_TYPE, account.type)
+ .appendQueryParameter(ContactsContract.CALLER_IS_SYNCADAPTER, "true").build()
+ )
+ .withYieldAllowed(true)
+ .withSelection("${BaseColumns._ID} = ?", SqlUtil.buildArgs(rowId))
+ .build()
+ )
+ }
+
+ private fun getSignalRawContacts(context: Context, account: Account): Map {
+ val currentContactsUri = ContactsContract.RawContacts.CONTENT_URI.buildUpon()
+ .appendQueryParameter(ContactsContract.RawContacts.ACCOUNT_NAME, account.name)
+ .appendQueryParameter(ContactsContract.RawContacts.ACCOUNT_TYPE, account.type).build()
+ val projection = arrayOf(BaseColumns._ID, ContactsContract.RawContacts.SYNC1, ContactsContract.RawContacts.SYNC4, ContactsContract.RawContacts.CONTACT_ID, ContactsContract.RawContacts.DISPLAY_NAME_PRIMARY, ContactsContract.RawContacts.DISPLAY_NAME_SOURCE)
+
+ val signalContacts: MutableMap = HashMap()
+
+ context.contentResolver.query(currentContactsUri, projection, null, null, null)?.use { cursor ->
+ while (cursor.moveToNext()) {
+ val currentAddress = PhoneNumberFormatter.get(context).format(cursor.getString(1))
+
+ signalContacts[currentAddress] = SignalContact(
+ id = cursor.getLong(0),
+ supportsVoice = cursor.getString(2),
+ rawDisplayName = cursor.getString(4),
+ aggregateDisplayName = getDisplayName(context, cursor.getLong(3)),
+ displayNameSource = cursor.getInt(5)
+ )
+ }
+ }
+
+ return signalContacts
+ }
+
+ private fun getSystemContactInfo(context: Context, address: String): SystemContactInfo? {
+ val uri = Uri.withAppendedPath(ContactsContract.PhoneLookup.CONTENT_FILTER_URI, Uri.encode(address))
+ val projection = arrayOf(
+ ContactsContract.PhoneLookup.NUMBER,
+ ContactsContract.PhoneLookup._ID,
+ ContactsContract.PhoneLookup.DISPLAY_NAME
+ )
+
+ context.contentResolver.query(uri, projection, null, null, null)?.use { numberCursor ->
+ while (numberCursor.moveToNext()) {
+ val systemNumber = numberCursor.getString(0)
+ val systemAddress = PhoneNumberFormatter.get(context).format(systemNumber)
+ if (systemAddress == address) {
+ context.contentResolver.query(ContactsContract.RawContacts.CONTENT_URI, arrayOf(ContactsContract.RawContacts._ID), "${ContactsContract.RawContacts.CONTACT_ID} = ? ", SqlUtil.buildArgs(numberCursor.getLong(1)), null)?.use { idCursor ->
+ if (idCursor.moveToNext()) {
+ return SystemContactInfo(
+ name = numberCursor.getString(2),
+ number = numberCursor.getString(0),
+ id = idCursor.getLong(0)
+ )
+ }
+ }
+ }
+ }
+ }
+
+ return null
+ }
+
+ private fun getDisplayName(context: Context, contactId: Long): String? {
+ val projection = arrayOf(ContactsContract.Contacts.DISPLAY_NAME)
+ val selection = "${ContactsContract.Contacts._ID} = ?"
+ val args = SqlUtil.buildArgs(contactId)
+
+ context.contentResolver.query(ContactsContract.Contacts.CONTENT_URI, projection, selection, args, null)?.use { cursor ->
+ if (cursor.moveToFirst()) {
+ return cursor.getString(0)
+ }
+ }
+
+ return null
+ }
+
+ @Throws(OperationApplicationException::class, RemoteException::class)
+ private fun applyOperationsInBatches(
+ contentResolver: ContentResolver,
+ authority: String,
+ operations: List,
+ batchSize: Int
+ ) {
+ val batches = Util.chunk(operations, batchSize)
+ for (batch in batches) {
+ contentResolver.applyBatch(authority, ArrayList(batch))
+ }
+ }
+
+ private data class SystemContactInfo(val name: String, val number: String, val id: Long)
+
+ private data class SignalContact(
+ val id: Long,
+ val supportsVoice: String?,
+ val rawDisplayName: String?,
+ val aggregateDisplayName: String?,
+ val displayNameSource: Int
+ ) {
+ fun isVoiceSupported(): Boolean {
+ return "true" == supportsVoice
+ }
+ }
+
+ data class NameDetails(
+ val displayName: String?,
+ val givenName: String?,
+ val familyName: String?,
+ val prefix: String?,
+ val suffix: String?,
+ val middleName: String?
+ )
+
+ data class PhoneDetails(
+ val number: String?,
+ val type: Int,
+ val label: String?
+ )
+
+ data class EmailDetails(
+ val address: String?,
+ val type: Int,
+ val label: String?
+ )
+
+ data class PostalAddressDetails(
+ val type: Int,
+ val label: String?,
+ val street: String?,
+ val poBox: String?,
+ val neighborhood: String?,
+ val city: String?,
+ val region: String?,
+ val postal: String?,
+ val country: String?
+ )
+}
diff --git a/app/src/main/java/org/thoughtcrime/securesms/contactshare/ContactShareEditActivity.java b/app/src/main/java/org/thoughtcrime/securesms/contactshare/ContactShareEditActivity.java
index 6731aa789..5c64c9297 100644
--- a/app/src/main/java/org/thoughtcrime/securesms/contactshare/ContactShareEditActivity.java
+++ b/app/src/main/java/org/thoughtcrime/securesms/contactshare/ContactShareEditActivity.java
@@ -77,9 +77,7 @@ public class ContactShareEditActivity extends PassphraseRequiredActivity impleme
ContactShareEditAdapter contactAdapter = new ContactShareEditAdapter(GlideApp.with(this), dynamicLanguage.getCurrentLocale(), this);
contactList.setAdapter(contactAdapter);
- SharedContactRepository contactRepository = new SharedContactRepository(this,
- AsyncTask.THREAD_POOL_EXECUTOR,
- SignalDatabase.contacts());
+ SharedContactRepository contactRepository = new SharedContactRepository(this, AsyncTask.THREAD_POOL_EXECUTOR);
viewModel = ViewModelProviders.of(this, new Factory(contactUris, contactRepository)).get(ContactShareEditViewModel.class);
viewModel.getContacts().observe(this, contacts -> {
diff --git a/app/src/main/java/org/thoughtcrime/securesms/contactshare/ContactUtil.java b/app/src/main/java/org/thoughtcrime/securesms/contactshare/ContactUtil.java
index 0ec507f85..5a47013f6 100644
--- a/app/src/main/java/org/thoughtcrime/securesms/contactshare/ContactUtil.java
+++ b/app/src/main/java/org/thoughtcrime/securesms/contactshare/ContactUtil.java
@@ -113,7 +113,7 @@ public final class ContactUtil {
}
}
- public static @NonNull String getNormalizedPhoneNumber(@NonNull Context context, @NonNull String number) {
+ public static @NonNull String getNormalizedPhoneNumber(@NonNull Context context, @Nullable String number) {
return PhoneNumberFormatter.get(context).format(number);
}
diff --git a/app/src/main/java/org/thoughtcrime/securesms/contactshare/SharedContactRepository.java b/app/src/main/java/org/thoughtcrime/securesms/contactshare/SharedContactRepository.java
index 2be7c642e..6d8adf382 100644
--- a/app/src/main/java/org/thoughtcrime/securesms/contactshare/SharedContactRepository.java
+++ b/app/src/main/java/org/thoughtcrime/securesms/contactshare/SharedContactRepository.java
@@ -1,7 +1,6 @@
package org.thoughtcrime.securesms.contactshare;
import android.content.Context;
-import android.database.Cursor;
import android.net.Uri;
import android.provider.ContactsContract;
import android.text.TextUtils;
@@ -11,8 +10,10 @@ import androidx.annotation.Nullable;
import androidx.annotation.WorkerThread;
import org.signal.core.util.logging.Log;
-import org.thoughtcrime.securesms.contacts.ContactsDatabase;
import org.thoughtcrime.securesms.contacts.avatars.ContactPhoto;
+import org.thoughtcrime.securesms.contacts.sync.SystemContactsRepository;
+import org.thoughtcrime.securesms.contacts.sync.SystemContactsRepository.NameDetails;
+import org.thoughtcrime.securesms.contacts.sync.SystemContactsRepository.PhoneDetails;
import org.thoughtcrime.securesms.contactshare.Contact.Email;
import org.thoughtcrime.securesms.contactshare.Contact.Name;
import org.thoughtcrime.securesms.contactshare.Contact.Phone;
@@ -26,10 +27,11 @@ import java.io.IOException;
import java.io.InputStream;
import java.util.ArrayList;
import java.util.HashMap;
-import java.util.LinkedList;
import java.util.List;
import java.util.Map;
+import java.util.Objects;
import java.util.concurrent.Executor;
+import java.util.stream.Collectors;
import ezvcard.Ezvcard;
import ezvcard.VCard;
@@ -40,17 +42,12 @@ public class SharedContactRepository {
private static final String TAG = Log.tag(SharedContactRepository.class);
- private final Context context;
- private final Executor executor;
- private final ContactsDatabase contactsDatabase;
+ private final Context context;
+ private final Executor executor;
- SharedContactRepository(@NonNull Context context,
- @NonNull Executor executor,
- @NonNull ContactsDatabase contactsDatabase)
- {
- this.context = context.getApplicationContext();
- this.executor = executor;
- this.contactsDatabase = contactsDatabase;
+ SharedContactRepository(@NonNull Context context, @NonNull Executor executor) {
+ this.context = context.getApplicationContext();
+ this.executor = executor;
}
void getContacts(@NonNull List contactUris, @NonNull ValueCallback> callback) {
@@ -108,23 +105,16 @@ public class SharedContactRepository {
@WorkerThread
private @Nullable Name getName(long contactId) {
- try (Cursor cursor = contactsDatabase.getNameDetails(contactId)) {
- if (cursor != null && cursor.moveToFirst()) {
- String cursorDisplayName = cursor.getString(cursor.getColumnIndexOrThrow(ContactsContract.CommonDataKinds.StructuredName.DISPLAY_NAME));
- String cursorGivenName = cursor.getString(cursor.getColumnIndexOrThrow(ContactsContract.CommonDataKinds.StructuredName.GIVEN_NAME));
- String cursorFamilyName = cursor.getString(cursor.getColumnIndexOrThrow(ContactsContract.CommonDataKinds.StructuredName.FAMILY_NAME));
- String cursorPrefix = cursor.getString(cursor.getColumnIndexOrThrow(ContactsContract.CommonDataKinds.StructuredName.PREFIX));
- String cursorSuffix = cursor.getString(cursor.getColumnIndexOrThrow(ContactsContract.CommonDataKinds.StructuredName.SUFFIX));
- String cursorMiddleName = cursor.getString(cursor.getColumnIndexOrThrow(ContactsContract.CommonDataKinds.StructuredName.MIDDLE_NAME));
+ NameDetails nameDetails = SystemContactsRepository.getNameDetails(context, contactId);
- Name name = new Name(cursorDisplayName, cursorGivenName, cursorFamilyName, cursorPrefix, cursorSuffix, cursorMiddleName);
- if (!name.isEmpty()) {
- return name;
- }
+ if (nameDetails != null) {
+ Name name = new Name(nameDetails.getDisplayName(), nameDetails.getGivenName(), nameDetails.getFamilyName(), nameDetails.getPrefix(), nameDetails.getSuffix(), nameDetails.getMiddleName());
+ if (!name.isEmpty()) {
+ return name;
}
}
- String org = contactsDatabase.getOrganizationName(contactId);
+ String org = SystemContactsRepository.getOrganizationName(context, contactId);
if (!TextUtils.isEmpty(org)) {
return new Name(org, org, null, null, null, null);
}
@@ -134,20 +124,16 @@ public class SharedContactRepository {
@WorkerThread
private @NonNull List getPhoneNumbers(long contactId) {
- Map numberMap = new HashMap<>();
- try (Cursor cursor = contactsDatabase.getPhoneDetails(contactId)) {
- while (cursor != null && cursor.moveToNext()) {
- String cursorNumber = cursor.getString(cursor.getColumnIndexOrThrow(ContactsContract.CommonDataKinds.Phone.NUMBER));
- int cursorType = cursor.getInt(cursor.getColumnIndexOrThrow(ContactsContract.CommonDataKinds.Phone.TYPE));
- String cursorLabel = cursor.getString(cursor.getColumnIndexOrThrow(ContactsContract.CommonDataKinds.Phone.LABEL));
+ Map numberMap = new HashMap<>();
+ List phoneDetails = SystemContactsRepository.getPhoneDetails(context, contactId);
- String number = ContactUtil.getNormalizedPhoneNumber(context, cursorNumber);
- Phone existing = numberMap.get(number);
- Phone candidate = new Phone(number, VCardUtil.phoneTypeFromContactType(cursorType), cursorLabel);
+ for (PhoneDetails phone : phoneDetails) {
+ String number = ContactUtil.getNormalizedPhoneNumber(context, phone.getNumber());
+ Phone existing = numberMap.get(number);
+ Phone candidate = new Phone(number, VCardUtil.phoneTypeFromContactType(phone.getType()), phone.getLabel());
- if (existing == null || (existing.getType() == Phone.Type.CUSTOM && existing.getLabel() == null)) {
- numberMap.put(number, candidate);
- }
+ if (existing == null || (existing.getType() == Phone.Type.CUSTOM && existing.getLabel() == null)) {
+ numberMap.put(number, candidate);
}
}
@@ -158,50 +144,31 @@ public class SharedContactRepository {
@WorkerThread
private @NonNull List getEmails(long contactId) {
- List emails = new LinkedList<>();
-
- try (Cursor cursor = contactsDatabase.getEmailDetails(contactId)) {
- while (cursor != null && cursor.moveToNext()) {
- String cursorEmail = cursor.getString(cursor.getColumnIndexOrThrow(ContactsContract.CommonDataKinds.Email.ADDRESS));
- int cursorType = cursor.getInt(cursor.getColumnIndexOrThrow(ContactsContract.CommonDataKinds.Email.TYPE));
- String cursorLabel = cursor.getString(cursor.getColumnIndexOrThrow(ContactsContract.CommonDataKinds.Email.LABEL));
-
- emails.add(new Email(cursorEmail, VCardUtil.emailTypeFromContactType(cursorType), cursorLabel));
- }
- }
-
- return emails;
+ return SystemContactsRepository.getEmailDetails(context, contactId)
+ .stream()
+ .filter(Objects::nonNull)
+ .map(email -> new Email(Objects.requireNonNull(email.getAddress()),
+ VCardUtil.emailTypeFromContactType(email.getType()),
+ email.getLabel()))
+ .collect(Collectors.toList());
}
@WorkerThread
private @NonNull List getPostalAddresses(long contactId) {
- List postalAddresses = new LinkedList<>();
-
- try (Cursor cursor = contactsDatabase.getPostalAddressDetails(contactId)) {
- while (cursor != null && cursor.moveToNext()) {
- int cursorType = cursor.getInt(cursor.getColumnIndexOrThrow(ContactsContract.CommonDataKinds.StructuredPostal.TYPE));
- String cursorLabel = cursor.getString(cursor.getColumnIndexOrThrow(ContactsContract.CommonDataKinds.StructuredPostal.LABEL));
- String cursorStreet = cursor.getString(cursor.getColumnIndexOrThrow(ContactsContract.CommonDataKinds.StructuredPostal.STREET));
- String cursorPoBox = cursor.getString(cursor.getColumnIndexOrThrow(ContactsContract.CommonDataKinds.StructuredPostal.POBOX));
- String cursorNeighborhood = cursor.getString(cursor.getColumnIndexOrThrow(ContactsContract.CommonDataKinds.StructuredPostal.NEIGHBORHOOD));
- String cursorCity = cursor.getString(cursor.getColumnIndexOrThrow(ContactsContract.CommonDataKinds.StructuredPostal.CITY));
- String cursorRegion = cursor.getString(cursor.getColumnIndexOrThrow(ContactsContract.CommonDataKinds.StructuredPostal.REGION));
- String cursorPostal = cursor.getString(cursor.getColumnIndexOrThrow(ContactsContract.CommonDataKinds.StructuredPostal.POSTCODE));
- String cursorCountry = cursor.getString(cursor.getColumnIndexOrThrow(ContactsContract.CommonDataKinds.StructuredPostal.COUNTRY));
-
- postalAddresses.add(new PostalAddress(VCardUtil.postalAddressTypeFromContactType(cursorType),
- cursorLabel,
- cursorStreet,
- cursorPoBox,
- cursorNeighborhood,
- cursorCity,
- cursorRegion,
- cursorPostal,
- cursorCountry));
- }
- }
-
- return postalAddresses;
+ return SystemContactsRepository.getPostalAddressDetails(context, contactId)
+ .stream()
+ .map(address -> {
+ return new PostalAddress(VCardUtil.postalAddressTypeFromContactType(address.getType()),
+ address.getLabel(),
+ address.getStreet(),
+ address.getPoBox(),
+ address.getNeighborhood(),
+ address.getCity(),
+ address.getRegion(),
+ address.getPostal(),
+ address.getCountry());
+ })
+ .collect(Collectors.toList());
}
@WorkerThread
@@ -223,7 +190,7 @@ public class SharedContactRepository {
@WorkerThread
private @Nullable AvatarInfo getSystemAvatarInfo(long contactId) {
- Uri uri = contactsDatabase.getAvatarUri(contactId);
+ Uri uri = SystemContactsRepository.getAvatarUri(context, contactId);
if (uri != null) {
return new AvatarInfo(uri, false);
}
diff --git a/app/src/main/java/org/thoughtcrime/securesms/database/SignalDatabase.kt b/app/src/main/java/org/thoughtcrime/securesms/database/SignalDatabase.kt
index 62440864b..74c6a121b 100644
--- a/app/src/main/java/org/thoughtcrime/securesms/database/SignalDatabase.kt
+++ b/app/src/main/java/org/thoughtcrime/securesms/database/SignalDatabase.kt
@@ -4,7 +4,6 @@ import android.app.Application
import android.content.Context
import net.zetetic.database.sqlcipher.SQLiteOpenHelper
import org.signal.core.util.logging.Log
-import org.thoughtcrime.securesms.contacts.ContactsDatabase
import org.thoughtcrime.securesms.crypto.AttachmentSecret
import org.thoughtcrime.securesms.crypto.DatabaseSecret
import org.thoughtcrime.securesms.crypto.MasterSecret
@@ -49,7 +48,6 @@ open class SignalDatabase(private val context: Application, databaseSecret: Data
val pushDatabase: PushDatabase = PushDatabase(context, this)
val groupDatabase: GroupDatabase = GroupDatabase(context, this)
val recipientDatabase: RecipientDatabase = RecipientDatabase(context, this)
- val contactsDatabase: ContactsDatabase = ContactsDatabase(context)
val groupReceiptDatabase: GroupReceiptDatabase = GroupReceiptDatabase(context, this)
val preKeyDatabase: OneTimePreKeyDatabase = OneTimePreKeyDatabase(context, this)
val signedPreKeyDatabase: SignedPreKeyDatabase = SignedPreKeyDatabase(context, this)
@@ -332,11 +330,6 @@ open class SignalDatabase(private val context: Application, databaseSecret: Data
val chatColors: ChatColorsDatabase
get() = instance!!.chatColorsDatabase
- @get:JvmStatic
- @get:JvmName("contacts")
- val contacts: ContactsDatabase
- get() = instance!!.contactsDatabase
-
@get:JvmStatic
@get:JvmName("distributionLists")
val distributionLists: DistributionListDatabase