From fcc49ae7b6958777158a7daecb7a9d6797d5da97 Mon Sep 17 00:00:00 2001 From: Greyson Parrelli Date: Wed, 7 Apr 2021 16:29:00 -0400 Subject: [PATCH] Limit the directory refresh in response to system contact changes. Previously, we would do a full directory/CDS refresh in response to any change in system contacts. That can be expensive. This changes the behavior to look at how many new contacts there after being notified of a contact change. - If there aren't any, we just sync names and stuff. - If we just have a few new contacts, we'll sync just those specifically. - If we have a lot, we'll do a full sync. --- .../contacts/ContactsSyncAdapter.java | 46 ++++- .../contacts/sync/DirectoryHelper.java | 167 ++++++++++-------- 2 files changed, 133 insertions(+), 80 deletions(-) diff --git a/app/src/main/java/org/thoughtcrime/securesms/contacts/ContactsSyncAdapter.java b/app/src/main/java/org/thoughtcrime/securesms/contacts/ContactsSyncAdapter.java index ea3f790e07..df610095aa 100644 --- a/app/src/main/java/org/thoughtcrime/securesms/contacts/ContactsSyncAdapter.java +++ b/app/src/main/java/org/thoughtcrime/securesms/contacts/ContactsSyncAdapter.java @@ -7,16 +7,30 @@ import android.content.Context; import android.content.SyncResult; import android.os.Bundle; +import androidx.annotation.NonNull; + +import com.annimon.stream.Stream; + import org.signal.core.util.logging.Log; import org.thoughtcrime.securesms.contacts.sync.DirectoryHelper; +import org.thoughtcrime.securesms.database.DatabaseFactory; +import org.thoughtcrime.securesms.dependencies.ApplicationDependencies; +import org.thoughtcrime.securesms.jobs.DirectoryRefreshJob; +import org.thoughtcrime.securesms.recipients.Recipient; +import org.thoughtcrime.securesms.util.SetUtil; import org.thoughtcrime.securesms.util.TextSecurePreferences; import java.io.IOException; +import java.util.List; +import java.util.Map; +import java.util.Set; public class ContactsSyncAdapter extends AbstractThreadedSyncAdapter { private static final String TAG = Log.tag(ContactsSyncAdapter.class); + private static final int FULL_SYNC_THRESHOLD = 10; + public ContactsSyncAdapter(Context context, boolean autoInitialize) { super(context, autoInitialize); } @@ -27,12 +41,40 @@ public class ContactsSyncAdapter extends AbstractThreadedSyncAdapter { { Log.i(TAG, "onPerformSync(" + authority +")"); - if (TextSecurePreferences.isPushRegistered(getContext())) { + Context context = getContext(); + + if (!TextSecurePreferences.isPushRegistered(context)) { + Log.i(TAG, "Not push registered. Just syncing contact info."); + DirectoryHelper.syncRecipientInfoWithSystemContacts(context); + return; + } + + Set allSystemNumbers = ContactAccessor.getInstance().getAllContactsWithNumbers(context); + Set knownSystemNumbers = DatabaseFactory.getRecipientDatabase(context).getAllPhoneNumbers(); + Set unknownSystemNumbers = SetUtil.difference(allSystemNumbers, knownSystemNumbers); + + if (unknownSystemNumbers.size() > FULL_SYNC_THRESHOLD) { + Log.i(TAG, "There are " + unknownSystemNumbers.size() + " unknown contacts. Doing a full sync."); try { - DirectoryHelper.refreshDirectory(getContext(), true); + DirectoryHelper.refreshDirectory(context, true); } catch (IOException e) { Log.w(TAG, e); } + } else if (unknownSystemNumbers.size() > 0) { + Log.i(TAG, "There are " + unknownSystemNumbers.size() + " unknown contacts. Doing an individual sync."); + List recipients = Stream.of(unknownSystemNumbers) + .filter(s -> s.startsWith("+")) + .map(s -> Recipient.external(getContext(), s)) + .toList(); + try { + DirectoryHelper.refreshDirectoryFor(context, recipients, true); + } catch (IOException e) { + Log.w(TAG, "Failed to refresh! Scheduling for later.", e); + ApplicationDependencies.getJobManager().add(new DirectoryRefreshJob(true)); + } + } else { + Log.i(TAG, "No new contacts. Just syncing system contact data."); + DirectoryHelper.syncRecipientInfoWithSystemContacts(context); } } 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 b8cdfa8d4d..ada82849a2 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 @@ -209,6 +209,13 @@ public class DirectoryHelper { return newRegisteredState; } + /** + * Reads the system contacts and copies over any matching data (like names) int our local store. + */ + public static void syncRecipientInfoWithSystemContacts(@NonNull Context context) { + syncRecipientInfoWithSystemContacts(context, Collections.emptyMap()); + } + @WorkerThread private static void refreshNumbers(@NonNull Context context, @NonNull Set databaseNumbers, @NonNull Set systemNumbers, boolean notifyOfNewUsers) throws IOException { RecipientDatabase recipientDatabase = DatabaseFactory.getRecipientDatabase(context); @@ -310,93 +317,97 @@ public class DirectoryHelper { } try { - RecipientDatabase recipientDatabase = DatabaseFactory.getRecipientDatabase(context); - ContactsDatabase contactsDatabase = DatabaseFactory.getContactsDatabase(context); - List activeAddresses = Stream.of(activeIds) - .map(Recipient::resolved) - .filter(Recipient::hasE164) - .map(Recipient::requireE164) - .toList(); + ContactsDatabase contactsDatabase = DatabaseFactory.getContactsDatabase(context); + 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); - BulkOperationsHandle handle = recipientDatabase.beginBulkSystemContactUpdate(); - - try (Cursor cursor = ContactAccessor.getInstance().getAllSystemContacts(context)) { - while (cursor != null && cursor.moveToNext()) { - String mimeType = getMimeType(cursor); - - if (!isPhoneMimeType(mimeType)) { - Log.w(TAG, "Ignoring unwanted mime type: " + mimeType); - continue; - } - - String lookupKey = getLookupKey(cursor); - ContactHolder contactHolder = new ContactHolder(lookupKey); - - while (!cursor.isAfterLast() && getLookupKey(cursor).equals(lookupKey) && isPhoneMimeType(getMimeType(cursor))) { - String number = CursorUtil.requireString(cursor, ContactsContract.CommonDataKinds.Phone.NUMBER); - - if (isValidContactNumber(number)) { - String formattedNumber = PhoneNumberFormatter.get(context).format(number); - String realNumber = Util.getFirstNonEmpty(rewrites.get(formattedNumber), formattedNumber); - - PhoneNumberRecord.Builder builder = new PhoneNumberRecord.Builder(); - - builder.withRecipientId(Recipient.externalContact(context, realNumber).getId()); - builder.withDisplayName(CursorUtil.requireString(cursor, ContactsContract.CommonDataKinds.Phone.DISPLAY_NAME)); - builder.withContactPhotoUri(CursorUtil.requireString(cursor, ContactsContract.CommonDataKinds.Phone.PHOTO_URI)); - builder.withContactLabel(CursorUtil.requireString(cursor, ContactsContract.CommonDataKinds.Phone.LABEL)); - builder.withPhoneType(CursorUtil.requireInt(cursor, ContactsContract.CommonDataKinds.Phone.TYPE)); - builder.withContactUri(ContactsContract.Contacts.getLookupUri(CursorUtil.requireLong(cursor, ContactsContract.CommonDataKinds.Phone._ID), - CursorUtil.requireString(cursor, ContactsContract.CommonDataKinds.Phone.LOOKUP_KEY))); - - contactHolder.addPhoneNumberRecord(builder.build()); - } else { - Log.w(TAG, "Skipping phone entry with invalid number"); - } - - cursor.moveToNext(); - } - - if (!cursor.isAfterLast() && getLookupKey(cursor).equals(lookupKey)) { - if (isStructuredNameMimeType(getMimeType(cursor))) { - StructuredNameRecord.Builder builder = new StructuredNameRecord.Builder(); - - builder.withGivenName(CursorUtil.requireString(cursor, ContactsContract.CommonDataKinds.StructuredName.GIVEN_NAME)); - builder.withFamilyName(CursorUtil.requireString(cursor, ContactsContract.CommonDataKinds.StructuredName.FAMILY_NAME)); - - contactHolder.setStructuredNameRecord(builder.build()); - } else { - Log.i(TAG, "Skipping invalid mimeType " + mimeType); - } - } else { - Log.i(TAG, "No structured name for user, rolling back cursor."); - cursor.moveToPrevious(); - } - - contactHolder.commit(handle); - } - } catch (IllegalStateException e) { - Log.w(TAG, "Hit an issue with the cursor while reading!", e); - } finally { - handle.finish(); - } - - if (NotificationChannels.supported()) { - try (RecipientDatabase.RecipientReader recipients = DatabaseFactory.getRecipientDatabase(context).getRecipientsWithNotificationChannels()) { - Recipient recipient; - while ((recipient = recipients.getNext()) != null) { - NotificationChannels.updateContactChannelName(context, recipient); - } - } - } + syncRecipientInfoWithSystemContacts(context, rewrites); } catch (RemoteException | OperationApplicationException e) { Log.w(TAG, "Failed to update contacts.", e); } } + private static void syncRecipientInfoWithSystemContacts(@NonNull Context context, @NonNull Map rewrites) { + RecipientDatabase recipientDatabase = DatabaseFactory.getRecipientDatabase(context); + BulkOperationsHandle handle = recipientDatabase.beginBulkSystemContactUpdate(); + + try (Cursor cursor = ContactAccessor.getInstance().getAllSystemContacts(context)) { + while (cursor != null && cursor.moveToNext()) { + String mimeType = getMimeType(cursor); + + if (!isPhoneMimeType(mimeType)) { + Log.w(TAG, "Ignoring unwanted mime type: " + mimeType); + continue; + } + + String lookupKey = getLookupKey(cursor); + ContactHolder contactHolder = new ContactHolder(lookupKey); + + while (!cursor.isAfterLast() && getLookupKey(cursor).equals(lookupKey) && isPhoneMimeType(getMimeType(cursor))) { + String number = CursorUtil.requireString(cursor, ContactsContract.CommonDataKinds.Phone.NUMBER); + + if (isValidContactNumber(number)) { + String formattedNumber = PhoneNumberFormatter.get(context).format(number); + String realNumber = Util.getFirstNonEmpty(rewrites.get(formattedNumber), formattedNumber); + + PhoneNumberRecord.Builder builder = new PhoneNumberRecord.Builder(); + + builder.withRecipientId(Recipient.externalContact(context, realNumber).getId()); + builder.withDisplayName(CursorUtil.requireString(cursor, ContactsContract.CommonDataKinds.Phone.DISPLAY_NAME)); + builder.withContactPhotoUri(CursorUtil.requireString(cursor, ContactsContract.CommonDataKinds.Phone.PHOTO_URI)); + builder.withContactLabel(CursorUtil.requireString(cursor, ContactsContract.CommonDataKinds.Phone.LABEL)); + builder.withPhoneType(CursorUtil.requireInt(cursor, ContactsContract.CommonDataKinds.Phone.TYPE)); + builder.withContactUri(ContactsContract.Contacts.getLookupUri(CursorUtil.requireLong(cursor, ContactsContract.CommonDataKinds.Phone._ID), + CursorUtil.requireString(cursor, ContactsContract.CommonDataKinds.Phone.LOOKUP_KEY))); + + contactHolder.addPhoneNumberRecord(builder.build()); + } else { + Log.w(TAG, "Skipping phone entry with invalid number"); + } + + cursor.moveToNext(); + } + + if (!cursor.isAfterLast() && getLookupKey(cursor).equals(lookupKey)) { + if (isStructuredNameMimeType(getMimeType(cursor))) { + StructuredNameRecord.Builder builder = new StructuredNameRecord.Builder(); + + builder.withGivenName(CursorUtil.requireString(cursor, ContactsContract.CommonDataKinds.StructuredName.GIVEN_NAME)); + builder.withFamilyName(CursorUtil.requireString(cursor, ContactsContract.CommonDataKinds.StructuredName.FAMILY_NAME)); + + contactHolder.setStructuredNameRecord(builder.build()); + } else { + Log.i(TAG, "Skipping invalid mimeType " + mimeType); + } + } else { + Log.i(TAG, "No structured name for user, rolling back cursor."); + cursor.moveToPrevious(); + } + + contactHolder.commit(handle); + } + } catch (IllegalStateException e) { + Log.w(TAG, "Hit an issue with the cursor while reading!", e); + } finally { + handle.finish(); + } + + if (NotificationChannels.supported()) { + try (RecipientDatabase.RecipientReader recipients = DatabaseFactory.getRecipientDatabase(context).getRecipientsWithNotificationChannels()) { + Recipient recipient; + while ((recipient = recipients.getNext()) != null) { + NotificationChannels.updateContactChannelName(context, recipient); + } + } + } + } + private static boolean isPhoneMimeType(@NonNull String mimeType) { return ContactsContract.CommonDataKinds.Phone.CONTENT_ITEM_TYPE.equals(mimeType); }