Improve contact sync for individual contacts.

This commit is contained in:
Greyson Parrelli
2022-04-22 07:47:49 -04:00
committed by Cody Henthorne
parent e2292dfa34
commit 5478285362
34 changed files with 431 additions and 547 deletions

View File

@@ -8,10 +8,8 @@ import android.os.RemoteException
import android.text.TextUtils
import androidx.annotation.WorkerThread
import org.signal.contacts.ContactLinkConfiguration
import org.signal.contacts.SystemContactsRepository.addMessageAndCallLinksToContacts
import org.signal.contacts.SystemContactsRepository.getAllSystemContacts
import org.signal.contacts.SystemContactsRepository.getOrCreateSystemAccount
import org.signal.contacts.SystemContactsRepository.removeDeletedRawContactsForAccount
import org.signal.contacts.SystemContactsRepository
import org.signal.core.util.StringUtil
import org.signal.core.util.logging.Log
import org.thoughtcrime.securesms.BuildConfig
import org.thoughtcrime.securesms.R
@@ -22,6 +20,7 @@ import org.thoughtcrime.securesms.keyvalue.SignalStore
import org.thoughtcrime.securesms.notifications.NotificationChannels
import org.thoughtcrime.securesms.permissions.Permissions
import org.thoughtcrime.securesms.phonenumbers.PhoneNumberFormatter
import org.thoughtcrime.securesms.profiles.ProfileName
import org.thoughtcrime.securesms.recipients.Recipient
import org.thoughtcrime.securesms.recipients.RecipientId
import org.thoughtcrime.securesms.registration.RegistrationUtil
@@ -45,6 +44,7 @@ object ContactDiscovery {
private const val MESSAGE_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 CONTACT_TAG = "__TS"
private const val FULL_SYSTEM_CONTACT_SYNC_THRESHOLD = 3
@JvmStatic
@Throws(IOException::class)
@@ -133,6 +133,10 @@ object ContactDiscovery {
syncRecipientsWithSystemContacts(context, emptyMap())
}
private fun phoneNumberFormatter(context: Context): (String) -> String {
return { PhoneNumberFormatter.get(context).format(it) }
}
private fun refreshRecipients(
context: Context,
descriptor: String,
@@ -152,7 +156,23 @@ object ContactDiscovery {
addSystemContactLinks(context, result.registeredIds, removeSystemContactLinksIfMissing)
stopwatch.split("contact-links")
syncRecipientsWithSystemContacts(context, result.rewrites)
syncRecipientsWithSystemContacts(
context = context,
rewrites = result.rewrites,
contactsProvider = {
if (result.registeredIds.size > FULL_SYSTEM_CONTACT_SYNC_THRESHOLD) {
Log.d(TAG, "Doing a full system contact sync because there are ${result.registeredIds.size} contacts to get info for.")
SystemContactsRepository.getAllSystemContacts(context, phoneNumberFormatter(context))
} else {
Log.d(TAG, "Doing a partial system contact sync because there are ${result.registeredIds.size} contacts to get info for.")
SystemContactsRepository.getContactDetailsByQueries(
context = context,
queries = Recipient.resolvedList(result.registeredIds).mapNotNull { it.e164.orElse(null) },
e164Formatter = phoneNumberFormatter(context)
)
}
}
)
stopwatch.split("contact-sync")
if (TextSecurePreferences.hasSuccessfullyRetrievedDirectory(context) && notifyOfNewUsers) {
@@ -227,7 +247,7 @@ object ContactDiscovery {
val stopwatch = Stopwatch("contact-links")
val account = getOrCreateSystemAccount(context, BuildConfig.APPLICATION_ID, context.getString(R.string.app_name))
val account = SystemContactsRepository.getOrCreateSystemAccount(context, BuildConfig.APPLICATION_ID, context.getString(R.string.app_name))
if (account == null) {
Log.w(TAG, "[addSystemContactLinks] Failed to create an account!")
return
@@ -237,10 +257,10 @@ object ContactDiscovery {
val registeredE164s: Set<String> = SignalDatabase.recipients.getE164sForIds(registeredIds)
stopwatch.split("fetch-e164s")
removeDeletedRawContactsForAccount(context, account)
SystemContactsRepository.removeDeletedRawContactsForAccount(context, account)
stopwatch.split("delete-stragglers")
addMessageAndCallLinksToContacts(
SystemContactsRepository.addMessageAndCallLinksToContacts(
context = context,
config = buildContactLinkConfiguration(context, account),
targetE164s = registeredE164s,
@@ -259,30 +279,38 @@ object ContactDiscovery {
/**
* Synchronizes info from the system contacts (name, avatar, etc)
*/
private fun syncRecipientsWithSystemContacts(context: Context, rewrites: Map<String, String>) {
private fun syncRecipientsWithSystemContacts(
context: Context,
rewrites: Map<String, String>,
contactsProvider: () -> SystemContactsRepository.ContactIterator = { SystemContactsRepository.getAllSystemContacts(context, phoneNumberFormatter(context)) }
) {
val handle = SignalDatabase.recipients.beginBulkSystemContactUpdate()
try {
getAllSystemContacts(context) { PhoneNumberFormatter.get(context).format(it) }.use { iterator ->
contactsProvider().use { iterator ->
while (iterator.hasNext()) {
val details = iterator.next()
val name = StructuredNameRecord(details.givenName, details.familyName)
val phones = details.numbers
.map { phoneDetails ->
val realNumber = Util.getFirstNonEmpty(rewrites[phoneDetails.number], phoneDetails.number)
PhoneNumberRecord.Builder()
.withRecipientId(Recipient.externalContact(context, realNumber).id)
.withContactUri(phoneDetails.contactUri)
.withDisplayName(phoneDetails.displayName)
.withContactPhotoUri(phoneDetails.photoUri)
.withContactLabel(phoneDetails.label)
.build()
}
.toList()
ContactHolder().apply {
setStructuredNameRecord(name)
addPhoneNumberRecords(phones)
}.commit(handle)
for (phoneDetails in details.numbers) {
val realNumber: String = Util.getFirstNonEmpty(rewrites[phoneDetails.number], phoneDetails.number)
val profileName: ProfileName = if (!StringUtil.isEmpty(details.givenName)) {
ProfileName.fromParts(details.givenName, details.familyName)
} else if (!StringUtil.isEmpty(phoneDetails.displayName)) {
ProfileName.asGiven(phoneDetails.displayName)
} else {
ProfileName.EMPTY
}
handle.setSystemContactInfo(
Recipient.externalContact(context, realNumber).id,
profileName,
phoneDetails.displayName,
phoneDetails.photoUri,
phoneDetails.label,
phoneDetails.type,
phoneDetails.contactUri.toString()
)
}
}
}
} catch (e: IllegalStateException) {

View File

@@ -1,54 +0,0 @@
package org.thoughtcrime.securesms.contacts.sync;
import android.net.Uri;
import androidx.annotation.NonNull;
import androidx.annotation.Nullable;
import org.signal.core.util.logging.Log;
import org.thoughtcrime.securesms.database.RecipientDatabase;
import org.thoughtcrime.securesms.profiles.ProfileName;
import java.util.LinkedList;
import java.util.List;
import java.util.Optional;
final class ContactHolder {
private static final String TAG = Log.tag(ContactHolder.class);
private final List<PhoneNumberRecord> phoneNumberRecords = new LinkedList<>();
private StructuredNameRecord structuredNameRecord;
public void addPhoneNumberRecords(@NonNull List<PhoneNumberRecord> phoneNumberRecords) {
this.phoneNumberRecords.addAll(phoneNumberRecords);
}
public void setStructuredNameRecord(@NonNull StructuredNameRecord structuredNameRecord) {
this.structuredNameRecord = structuredNameRecord;
}
void commit(@NonNull RecipientDatabase.BulkOperationsHandle handle) {
for (PhoneNumberRecord phoneNumberRecord : phoneNumberRecords) {
handle.setSystemContactInfo(phoneNumberRecord.getRecipientId(),
getProfileName(phoneNumberRecord.getDisplayName()),
phoneNumberRecord.getDisplayName(),
phoneNumberRecord.getContactPhotoUri(),
phoneNumberRecord.getContactLabel(),
phoneNumberRecord.getPhoneType(),
Optional.ofNullable(phoneNumberRecord.getContactUri()).map(Uri::toString).orElse(null));
}
}
private @NonNull ProfileName getProfileName(@Nullable String displayName) {
if (structuredNameRecord != null && structuredNameRecord.hasGivenName()) {
return structuredNameRecord.asProfileName();
} else if (displayName != null) {
return ProfileName.asGiven(displayName);
} else {
Log.w(TAG, "Failed to find a suitable display name!");
return ProfileName.EMPTY;
}
}
}

View File

@@ -1,99 +0,0 @@
package org.thoughtcrime.securesms.contacts.sync;
import android.net.Uri;
import androidx.annotation.NonNull;
import androidx.annotation.Nullable;
import org.thoughtcrime.securesms.recipients.RecipientId;
import java.util.Objects;
/**
* Represents all the data we pull from a Phone data cursor row from the contacts database.
*/
final class PhoneNumberRecord {
private final RecipientId recipientId;
private final String displayName;
private final String contactPhotoUri;
private final String contactLabel;
private final int phoneType;
private final Uri contactUri;
private PhoneNumberRecord(@NonNull PhoneNumberRecord.Builder builder) {
recipientId = Objects.requireNonNull(builder.recipientId);
displayName = builder.displayName;
contactPhotoUri = builder.contactPhotoUri;
contactLabel = builder.contactLabel;
phoneType = builder.phoneType;
contactUri = builder.contactUri;
}
@NonNull RecipientId getRecipientId() {
return recipientId;
}
@Nullable String getDisplayName() {
return displayName;
}
@Nullable String getContactPhotoUri() {
return contactPhotoUri;
}
@Nullable String getContactLabel() {
return contactLabel;
}
int getPhoneType() {
return phoneType;
}
@Nullable Uri getContactUri() {
return contactUri;
}
final static class Builder {
private RecipientId recipientId;
private String displayName;
private String contactPhotoUri;
private String contactLabel;
private int phoneType;
private Uri contactUri;
@NonNull Builder withRecipientId(@NonNull RecipientId recipientId) {
this.recipientId = recipientId;
return this;
}
@NonNull Builder withDisplayName(@Nullable String displayName) {
this.displayName = displayName;
return this;
}
@NonNull Builder withContactUri(@Nullable Uri contactUri) {
this.contactUri = contactUri;
return this;
}
@NonNull Builder withContactLabel(@Nullable String contactLabel) {
this.contactLabel = contactLabel;
return this;
}
@NonNull Builder withContactPhotoUri(@Nullable String contactPhotoUri) {
this.contactPhotoUri = contactPhotoUri;
return this;
}
@NonNull Builder withPhoneType(int phoneType) {
this.phoneType = phoneType;
return this;
}
@NonNull PhoneNumberRecord build() {
return new PhoneNumberRecord(this);
}
}
}

View File

@@ -1,27 +0,0 @@
package org.thoughtcrime.securesms.contacts.sync;
import androidx.annotation.NonNull;
import androidx.annotation.Nullable;
import org.thoughtcrime.securesms.profiles.ProfileName;
/**
* Represents the data pulled from a StructuredName row of a Contacts data cursor.
*/
final class StructuredNameRecord {
private final String givenName;
private final String familyName;
public StructuredNameRecord(@Nullable String givenName, @Nullable String familyName) {
this.givenName = givenName;
this.familyName = familyName;
}
public boolean hasGivenName() {
return givenName != null;
}
public @NonNull ProfileName asProfileName() {
return ProfileName.fromParts(givenName, familyName);
}
}