Migrate contact interactions to SystemContactsRepository.

fork-5.53.8
Greyson Parrelli 2022-03-23 09:50:47 -04:00
rodzic db309b7930
commit c2627dda8d
8 zmienionych plików z 597 dodań i 662 usunięć

Wyświetl plik

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

Wyświetl plik

@ -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 <http://www.gnu.org/licenses/>.
*/
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<String> registeredAddressList,
boolean remove)
throws RemoteException, OperationApplicationException
{
Set<String> registeredAddressSet = new HashSet<>(registeredAddressList);
ArrayList<ContentProviderOperation> operations = new ArrayList<>();
Map<String, SignalContact> currentContacts = getSignalRawContacts(account);
List<List<String>> registeredChunks = Util.chunk(registeredAddressList, 50);
for (List<String> registeredChunk : registeredChunks) {
for (String registeredAddress : registeredChunk) {
if (!currentContacts.containsKey(registeredAddress)) {
Optional<SystemContactInfo> 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<String, SignalContact> 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<ContentProviderOperation> 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<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 addTextSecureRawContact(List<ContentProviderOperation> 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<ContentProviderOperation> 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<String, SignalContact> getSignalRawContacts(@NonNull Account account) {
Uri currentContactsUri = RawContacts.CONTENT_URI.buildUpon()
.appendQueryParameter(RawContacts.ACCOUNT_NAME, account.name)
.appendQueryParameter(RawContacts.ACCOUNT_TYPE, account.type).build();
Map<String, SignalContact> 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<SystemContactInfo> 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<ContentProviderOperation> operations,
int batchSize)
throws OperationApplicationException, RemoteException
{
List<List<ContentProviderOperation>> batches = Util.chunk(operations, batchSize);
for (List<ContentProviderOperation> 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;
}
}
}

Wyświetl plik

@ -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<String> activeAddresses = Stream.of(activeIds)
.map(Recipient::resolved)
.filter(Recipient::hasE164)
.map(Recipient::requireE164)
.toList();
List<String> 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<RecipientId> 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;
}
}
}

Wyświetl plik

@ -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<Account> = 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<String>,
remove: Boolean
) {
val registeredAddressSet: Set<String> = registeredAddressList.toSet()
val operations: ArrayList<ContentProviderOperation> = ArrayList()
val currentContacts: Map<String, SignalContact> = getSignalRawContacts(context, account)
val registeredChunks: List<List<String>> = 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<PhoneDetails> {
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<PhoneDetails> = 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<EmailDetails> {
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<EmailDetails> = 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<PostalAddressDetails> {
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<PostalAddressDetails> = 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<ContentProviderOperation>, 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<ContentProviderOperation>, 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<ContentProviderOperation>,
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<ContentProviderOperation>, 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<String, SignalContact> {
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<String, SignalContact> = 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<ContentProviderOperation>,
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?
)
}

Wyświetl plik

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

Wyświetl plik

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

Wyświetl plik

@ -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<Uri> contactUris, @NonNull ValueCallback<List<Contact>> 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<Phone> getPhoneNumbers(long contactId) {
Map<String, Phone> 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<String, Phone> numberMap = new HashMap<>();
List<PhoneDetails> 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<Email> getEmails(long contactId) {
List<Email> 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<PostalAddress> getPostalAddresses(long contactId) {
List<PostalAddress> 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);
}

Wyświetl plik

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