From a32d5bef2098ddde71ca0a97fe27eb50110c336d Mon Sep 17 00:00:00 2001 From: Greyson Parrelli Date: Mon, 4 Apr 2022 16:49:49 -0400 Subject: [PATCH] Refactor more ContactDiscovery code. --- .../contacts/sync/ContactDiscovery.kt | 263 +++++++++++++++- .../contacts/sync/DirectoryHelper.java | 284 +----------------- .../securesms/database/RecipientDatabase.kt | 26 ++ .../signal/contactstest/ContactsViewModel.kt | 1 - .../contacts/SystemContactsRepository.kt | 47 ++- 5 files changed, 314 insertions(+), 307 deletions(-) diff --git a/app/src/main/java/org/thoughtcrime/securesms/contacts/sync/ContactDiscovery.kt b/app/src/main/java/org/thoughtcrime/securesms/contacts/sync/ContactDiscovery.kt index 75ced05c1f..1924f85fba 100644 --- a/app/src/main/java/org/thoughtcrime/securesms/contacts/sync/ContactDiscovery.kt +++ b/app/src/main/java/org/thoughtcrime/securesms/contacts/sync/ContactDiscovery.kt @@ -1,20 +1,46 @@ package org.thoughtcrime.securesms.contacts.sync +import android.Manifest import android.accounts.Account import android.content.Context +import android.content.OperationApplicationException +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.core.util.logging.Log +import org.thoughtcrime.securesms.BuildConfig import org.thoughtcrime.securesms.R import org.thoughtcrime.securesms.database.RecipientDatabase +import org.thoughtcrime.securesms.database.SignalDatabase +import org.thoughtcrime.securesms.dependencies.ApplicationDependencies +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.recipients.Recipient +import org.thoughtcrime.securesms.recipients.RecipientId +import org.thoughtcrime.securesms.registration.RegistrationUtil +import org.thoughtcrime.securesms.sms.IncomingJoinedMessage +import org.thoughtcrime.securesms.storage.StorageSyncHelper +import org.thoughtcrime.securesms.util.Stopwatch +import org.thoughtcrime.securesms.util.TextSecurePreferences +import org.thoughtcrime.securesms.util.Util +import org.whispersystems.signalservice.api.push.SignalServiceAddress import java.io.IOException +import java.util.Calendar /** * Methods for discovering which users are registered and marking them as such in the database. */ object ContactDiscovery { + private val TAG = Log.tag(ContactDiscovery::class.java) + 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" @@ -23,31 +49,137 @@ object ContactDiscovery { @Throws(IOException::class) @WorkerThread fun refreshAll(context: Context, notifyOfNewUsers: Boolean) { - DirectoryHelper.refreshAll(context, notifyOfNewUsers) + if (TextUtils.isEmpty(SignalStore.account().e164)) { + Log.w(TAG, "Have not yet set our own local number. Skipping.") + return + } + + if (!hasContactsPermissions(context)) { + Log.w(TAG, "No contact permissions. Skipping.") + return + } + + if (!SignalStore.registrationValues().isRegistrationComplete) { + Log.w(TAG, "Registration is not yet complete. Skipping, but running a routine to possibly mark it complete.") + RegistrationUtil.maybeMarkRegistrationComplete(context) + return + } + + refreshRecipients( + context = context, + descriptor = "refresh-all", + refresh = { + DirectoryHelper.refreshAll(context) + }, + removeSystemContactLinksIfMissing = true, + notifyOfNewUsers = notifyOfNewUsers + ) + + StorageSyncHelper.scheduleSyncForDataChange() } @JvmStatic @Throws(IOException::class) @WorkerThread fun refresh(context: Context, recipients: List, notifyOfNewUsers: Boolean) { - return DirectoryHelper.refresh(context, recipients, notifyOfNewUsers) + refreshRecipients( + context = context, + descriptor = "refresh-multiple", + refresh = { + DirectoryHelper.refresh(context, recipients) + }, + removeSystemContactLinksIfMissing = false, + notifyOfNewUsers = notifyOfNewUsers + ) } @JvmStatic @Throws(IOException::class) @WorkerThread fun refresh(context: Context, recipient: Recipient, notifyOfNewUsers: Boolean): RecipientDatabase.RegisteredState { - return DirectoryHelper.refresh(context, recipient, notifyOfNewUsers) + val result: RefreshResult = refreshRecipients( + context = context, + descriptor = "refresh-single", + refresh = { + DirectoryHelper.refresh(context, listOf(recipient)) + }, + removeSystemContactLinksIfMissing = false, + notifyOfNewUsers = notifyOfNewUsers + ) + + return if (result.registeredIds.contains(recipient.id)) { + RecipientDatabase.RegisteredState.REGISTERED + } else { + RecipientDatabase.RegisteredState.NOT_REGISTERED + } } @JvmStatic @WorkerThread fun syncRecipientInfoWithSystemContacts(context: Context) { - DirectoryHelper.syncRecipientInfoWithSystemContacts(context) + syncRecipientsWithSystemContacts(context, emptyMap()) } - @JvmStatic - fun buildContactLinkConfiguration(context: Context, account: Account): ContactLinkConfiguration { + private fun refreshRecipients( + context: Context, + descriptor: String, + refresh: () -> RefreshResult, + removeSystemContactLinksIfMissing: Boolean, + notifyOfNewUsers: Boolean + ): RefreshResult { + val stopwatch = Stopwatch(descriptor) + + val preExistingRegisteredIds: Set = SignalDatabase.recipients.getRegistered().toSet() + stopwatch.split("pre-existing") + + val result: RefreshResult = refresh() + stopwatch.split("cds") + + if (hasContactsPermissions(context)) { + addSystemContactLinks(context, result.registeredIds, removeSystemContactLinksIfMissing) + stopwatch.split("contact-links") + + syncRecipientsWithSystemContacts(context, result.rewrites) + stopwatch.split("contact-sync") + + if (TextSecurePreferences.hasSuccessfullyRetrievedDirectory(context) && notifyOfNewUsers) { + val systemContacts: Set = SignalDatabase.recipients.getSystemContacts().toSet() + val newlyRegisteredSystemContacts: Set = (result.registeredIds - preExistingRegisteredIds).intersect(systemContacts) + + notifyNewUsers(context, newlyRegisteredSystemContacts) + } else { + TextSecurePreferences.setHasSuccessfullyRetrievedDirectory(context, true) + } + stopwatch.split("notify") + } else { + Log.w(TAG, "No contacts permission, can't sync with system contacts.") + } + + stopwatch.stop(TAG) + + return result + } + + private fun notifyNewUsers(context: Context, newUserIds: Collection) { + if (!SignalStore.settings().isNotifyWhenContactJoinsSignal) return + + Recipient.resolvedList(newUserIds) + .filter { !it.isSelf && it.hasAUserSetDisplayName(context) && !hasSession(it.id) } + .map { IncomingJoinedMessage(it.id) } + .map { SignalDatabase.sms.insertMessageInbox(it) } + .filter { it.isPresent } + .map { it.get() } + .forEach { result -> + val hour = Calendar.getInstance()[Calendar.HOUR_OF_DAY] + if (hour in 9..22) { + ApplicationDependencies.getMessageNotifier().updateNotification(context, result.threadId, true) + } else { + Log.i(TAG, "Not notifying of a new user due to the time of day. (Hour: $hour)") + } + } + } + + private fun buildContactLinkConfiguration(context: Context, account: Account): ContactLinkConfiguration { return ContactLinkConfiguration( account = account, appName = context.getString(R.string.app_name), @@ -59,4 +191,123 @@ object ContactDiscovery { syncTag = CONTACT_TAG ) } + + private fun hasContactsPermissions(context: Context): Boolean { + return Permissions.hasAll(context, Manifest.permission.READ_CONTACTS, Manifest.permission.WRITE_CONTACTS) + } + + /** + * Adds the "Message/Call $number with Signal" link to registered users in the system contacts. + * @param registeredIds A list of registered [RecipientId]s + * @param removeIfMissing If true, this will remove links from every currently-linked system contact that is *not* in the [registeredIds] list. + */ + private fun addSystemContactLinks(context: Context, registeredIds: Collection, removeIfMissing: Boolean) { + if (!Permissions.hasAll(context, Manifest.permission.READ_CONTACTS, Manifest.permission.WRITE_CONTACTS)) { + Log.w(TAG, "[addSystemContactLinks] No contact permissions. Skipping.") + return + } + + if (registeredIds.isEmpty()) { + Log.w(TAG, "[addSystemContactLinks] No registeredIds. Skipping.") + return + } + + val stopwatch = Stopwatch("contact-links") + + val account = getOrCreateSystemAccount(context, BuildConfig.APPLICATION_ID, context.getString(R.string.app_name)) + if (account == null) { + Log.w(TAG, "[addSystemContactLinks] Failed to create an account!") + return + } + + try { + val registeredE164s: Set = SignalDatabase.recipients.getE164sForIds(registeredIds) + stopwatch.split("fetch-e164s") + + removeDeletedRawContactsForAccount(context, account) + stopwatch.split("delete-stragglers") + + addMessageAndCallLinksToContacts( + context = context, + config = buildContactLinkConfiguration(context, account), + targetE164s = registeredE164s, + removeIfMissing = removeIfMissing + ) + stopwatch.split("add-links") + + stopwatch.stop(TAG) + } catch (e: RemoteException) { + Log.w(TAG, "[addSystemContactLinks] Failed to add links to contacts.", e) + } catch (e: OperationApplicationException) { + Log.w(TAG, "[addSystemContactLinks] Failed to add links to contacts.", e) + } + } + + /** + * Synchronizes info from the system contacts (name, avatar, etc) + */ + private fun syncRecipientsWithSystemContacts(context: Context, rewrites: Map) { + val handle = SignalDatabase.recipients.beginBulkSystemContactUpdate() + try { + getAllSystemContacts(context) { PhoneNumberFormatter.get(context).format(it) }.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) + } + } + } catch (e: IllegalStateException) { + Log.w(TAG, "Hit an issue with the cursor while reading!", e) + } finally { + handle.finish() + } + + if (NotificationChannels.supported()) { + SignalDatabase.recipients.getRecipientsWithNotificationChannels().use { reader -> + var recipient: Recipient? = reader.getNext() + + while (recipient != null) { + NotificationChannels.updateContactChannelName(context, recipient) + recipient = reader.getNext() + } + } + } + } + + /** + * Whether or not a session exists with the provided recipient. + */ + fun hasSession(id: RecipientId): Boolean { + val recipient = Recipient.resolved(id) + + if (!recipient.hasServiceId()) { + return false + } + + val protocolAddress = Recipient.resolved(id).requireServiceId().toProtocolAddress(SignalServiceAddress.DEFAULT_DEVICE_ID) + + return ApplicationDependencies.getProtocolStore().aci().containsSession(protocolAddress) || + ApplicationDependencies.getProtocolStore().pni().containsSession(protocolAddress) + } + + class RefreshResult( + val registeredIds: Set, + val rewrites: Map + ) } 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 c1b653f43a..d93de7aa3f 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 @@ -1,11 +1,6 @@ package org.thoughtcrime.securesms.contacts.sync; -import android.Manifest; -import android.accounts.Account; import android.content.Context; -import android.content.OperationApplicationException; -import android.os.RemoteException; -import android.text.TextUtils; import androidx.annotation.NonNull; import androidx.annotation.WorkerThread; @@ -14,30 +9,18 @@ import com.annimon.stream.Collectors; import com.annimon.stream.Stream; import org.signal.contacts.SystemContactsRepository; -import org.signal.contacts.SystemContactsRepository.ContactDetails; import org.signal.core.util.logging.Log; -import org.signal.libsignal.protocol.SignalProtocolAddress; import org.signal.libsignal.protocol.util.Pair; -import org.thoughtcrime.securesms.BuildConfig; -import org.thoughtcrime.securesms.R; -import org.thoughtcrime.securesms.database.MessageDatabase.InsertResult; +import org.thoughtcrime.securesms.contacts.sync.ContactDiscovery.RefreshResult; import org.thoughtcrime.securesms.database.RecipientDatabase; -import org.thoughtcrime.securesms.database.RecipientDatabase.BulkOperationsHandle; -import org.thoughtcrime.securesms.database.RecipientDatabase.RegisteredState; import org.thoughtcrime.securesms.database.SignalDatabase; import org.thoughtcrime.securesms.dependencies.ApplicationDependencies; import org.thoughtcrime.securesms.jobs.MultiDeviceContactUpdateJob; import org.thoughtcrime.securesms.jobs.RetrieveProfileJob; -import org.thoughtcrime.securesms.jobs.StorageSyncJob; 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.recipients.Recipient; import org.thoughtcrime.securesms.recipients.RecipientId; -import org.thoughtcrime.securesms.registration.RegistrationUtil; -import org.thoughtcrime.securesms.sms.IncomingJoinedMessage; -import org.thoughtcrime.securesms.storage.StorageSyncHelper; import org.thoughtcrime.securesms.util.FeatureFlags; import org.thoughtcrime.securesms.util.ProfileUtil; import org.signal.core.util.SetUtil; @@ -46,18 +29,14 @@ import org.thoughtcrime.securesms.util.TextSecurePreferences; import org.whispersystems.signalservice.api.profiles.ProfileAndCredential; import org.whispersystems.signalservice.api.profiles.SignalServiceProfile; import org.whispersystems.signalservice.api.push.ACI; -import org.whispersystems.signalservice.api.push.SignalServiceAddress; import org.whispersystems.signalservice.api.services.ProfileService; import org.whispersystems.signalservice.internal.ServiceResponse; import java.io.IOException; -import java.util.Calendar; -import java.util.Collection; import java.util.Collections; import java.util.HashSet; import java.util.List; import java.util.Map; -import java.util.Optional; import java.util.Set; import java.util.concurrent.TimeUnit; @@ -72,36 +51,18 @@ class DirectoryHelper { private static final String TAG = Log.tag(DirectoryHelper.class); @WorkerThread - static void refreshAll(@NonNull Context context, boolean notifyOfNewUsers) throws IOException { - if (TextUtils.isEmpty(SignalStore.account().getE164())) { - Log.w(TAG, "Have not yet set our own local number. Skipping."); - return; - } - - if (!Permissions.hasAll(context, Manifest.permission.READ_CONTACTS, Manifest.permission.WRITE_CONTACTS)) { - Log.w(TAG, "No contact permissions. Skipping."); - return; - } - - if (!SignalStore.registrationValues().isRegistrationComplete()) { - Log.w(TAG, "Registration is not yet complete. Skipping, but running a routine to possibly mark it complete."); - RegistrationUtil.maybeMarkRegistrationComplete(context); - return; - } - + static @NonNull RefreshResult refreshAll(@NonNull Context context) throws IOException { RecipientDatabase recipientDatabase = SignalDatabase.recipients(); Set databaseE164s = sanitizeNumbers(recipientDatabase.getAllE164s()); Set systemE164s = sanitizeNumbers(Stream.of(SystemContactsRepository.getAllDisplayNumbers(context)) .map(number -> PhoneNumberFormatter.get(context).format(number)) .collect(Collectors.toSet())); - refreshNumbers(context, databaseE164s, systemE164s, notifyOfNewUsers, true); - - StorageSyncHelper.scheduleSyncForDataChange(); + return refreshNumbers(context, databaseE164s, systemE164s); } @WorkerThread - static void refresh(@NonNull Context context, @NonNull List recipients, boolean notifyOfNewUsers) throws IOException { + static @NonNull RefreshResult refresh(@NonNull Context context, @NonNull List recipients) throws IOException { RecipientDatabase recipientDatabase = SignalDatabase.recipients(); for (Recipient recipient : recipients) { @@ -119,107 +80,17 @@ class DirectoryHelper { .map(Recipient::requireE164) .collect(Collectors.toSet()); - refreshNumbers(context, numbers, numbers, notifyOfNewUsers, false); + return refreshNumbers(context, numbers, numbers); } @WorkerThread - static RegisteredState refresh(@NonNull Context context, @NonNull Recipient recipient, boolean notifyOfNewUsers) throws IOException { - Stopwatch stopwatch = new Stopwatch("single"); - RecipientDatabase recipientDatabase = SignalDatabase.recipients(); - RegisteredState originalRegisteredState = recipient.resolve().getRegistered(); - RegisteredState newRegisteredState; - - if (recipient.hasServiceId() && !recipient.hasE164()) { - boolean isRegistered = ApplicationDependencies.getSignalServiceAccountManager().isIdentifierRegistered(recipient.requireServiceId()); - stopwatch.split("aci-network"); - if (isRegistered) { - boolean idChanged = recipientDatabase.markRegistered(recipient.getId(), recipient.requireServiceId()); - if (idChanged) { - Log.w(TAG, "ID changed during refresh by UUID."); - } - } else { - recipientDatabase.markUnregistered(recipient.getId()); - } - - stopwatch.split("aci-disk"); - stopwatch.stop(TAG); - - return isRegistered ? RegisteredState.REGISTERED : RegisteredState.NOT_REGISTERED; - } - - if (!recipient.getE164().isPresent()) { - Log.w(TAG, "No ACI or E164?"); - return RegisteredState.NOT_REGISTERED; - } - - DirectoryResult result = ContactDiscoveryV2.getDirectoryResult(context, recipient.getE164().get()); - - stopwatch.split("e164-network"); - - if (result.getNumberRewrites().size() > 0) { - Log.i(TAG, "[getDirectoryResult] Need to rewrite some numbers."); - recipientDatabase.updatePhoneNumbers(result.getNumberRewrites()); - } - - if (result.getRegisteredNumbers().size() > 0) { - ACI aci = result.getRegisteredNumbers().values().iterator().next(); - if (aci != null) { - boolean idChanged = recipientDatabase.markRegistered(recipient.getId(), aci); - if (idChanged) { - recipient = Recipient.resolved(recipientDatabase.getByServiceId(aci).get()); - } - } else { - Log.w(TAG, "Registered number set had a null ACI!"); - } - } else if (recipient.hasServiceId() && recipient.isRegistered() && hasCommunicatedWith(recipient)) { - if (ApplicationDependencies.getSignalServiceAccountManager().isIdentifierRegistered(recipient.requireServiceId())) { - recipientDatabase.markRegistered(recipient.getId(), recipient.requireServiceId()); - } else { - recipientDatabase.markUnregistered(recipient.getId()); - } - stopwatch.split("e164-unlisted-network"); - } else { - recipientDatabase.markUnregistered(recipient.getId()); - } - - if (Permissions.hasAll(context, Manifest.permission.WRITE_CONTACTS)) { - updateContactsDatabase(context, Collections.singletonList(recipient.getId()), false, result.getNumberRewrites()); - } - - newRegisteredState = result.getRegisteredNumbers().size() > 0 ? RegisteredState.REGISTERED : RegisteredState.NOT_REGISTERED; - - if (newRegisteredState != originalRegisteredState) { - ApplicationDependencies.getJobManager().add(new MultiDeviceContactUpdateJob()); - ApplicationDependencies.getJobManager().add(new StorageSyncJob()); - - if (notifyOfNewUsers && newRegisteredState == RegisteredState.REGISTERED && recipient.resolve().isSystemContact()) { - notifyNewUsers(context, Collections.singletonList(recipient.getId())); - } - - StorageSyncHelper.scheduleSyncForDataChange(); - } - - stopwatch.split("e164-disk"); - stopwatch.stop(TAG); - - return newRegisteredState; - } - - /** - * Reads the system contacts and copies over any matching data (like names) int our local store. - */ - 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, boolean removeSystemContactEntryForMissing) throws IOException { + private static RefreshResult refreshNumbers(@NonNull Context context, @NonNull Set databaseNumbers, @NonNull Set systemNumbers) throws IOException { RecipientDatabase recipientDatabase = SignalDatabase.recipients(); Set allNumbers = SetUtil.union(databaseNumbers, systemNumbers); if (allNumbers.isEmpty()) { Log.w(TAG, "No numbers to refresh!"); - return; + return new RefreshResult(Collections.emptySet(), Collections.emptyMap()); } Stopwatch stopwatch = new Stopwatch("refresh"); @@ -258,158 +129,19 @@ class DirectoryHelper { Log.i(TAG, "Some profile fetches failed to resolve. Assuming not-inactive for now and scheduling a retry."); RetrieveProfileJob.enqueue(unlistedResult.getRetries()); } - stopwatch.split("handle-unlisted"); - Set preExistingRegisteredUsers = new HashSet<>(recipientDatabase.getRegistered()); - recipientDatabase.bulkUpdatedRegisteredStatus(aciMap, inactiveIds); - stopwatch.split("update-registered"); - updateContactsDatabase(context, activeIds, removeSystemContactEntryForMissing, result.getNumberRewrites()); - - stopwatch.split("contacts-db"); if (TextSecurePreferences.isMultiDevice(context)) { ApplicationDependencies.getJobManager().add(new MultiDeviceContactUpdateJob()); } - if (TextSecurePreferences.hasSuccessfullyRetrievedDirectory(context) && notifyOfNewUsers) { - Set systemContacts = new HashSet<>(recipientDatabase.getSystemContacts()); - Set newlyRegisteredSystemContacts = new HashSet<>(activeIds); - - newlyRegisteredSystemContacts.removeAll(preExistingRegisteredUsers); - newlyRegisteredSystemContacts.retainAll(systemContacts); - - notifyNewUsers(context, newlyRegisteredSystemContacts); - } else { - TextSecurePreferences.setHasSuccessfullyRetrievedDirectory(context, true); - } - stopwatch.stop(TAG); - } - private static void updateContactsDatabase(@NonNull Context context, - @NonNull Collection activeIds, - boolean removeMissing, - @NonNull Map rewrites) - { - if (!Permissions.hasAll(context, Manifest.permission.READ_CONTACTS, Manifest.permission.WRITE_CONTACTS)) { - Log.w(TAG, "[updateContactsDatabase] No contact permissions. Skipping."); - return; - } - - Stopwatch stopwatch = new Stopwatch("contacts"); - - Account account = SystemContactsRepository.getOrCreateSystemAccount(context, BuildConfig.APPLICATION_ID, context.getString(R.string.app_name)); - stopwatch.split("account"); - - if (account == null) { - Log.w(TAG, "Failed to create an account!"); - return; - } - - try { - Set activeE164s = Stream.of(activeIds) - .map(Recipient::resolved) - .filter(Recipient::hasE164) - .map(Recipient::requireE164) - .collect(Collectors.toSet()); - - SystemContactsRepository.removeDeletedRawContactsForAccount(context, account); - stopwatch.split("remove-deleted"); - SystemContactsRepository.addMessageAndCallLinksToContacts(context, - ContactDiscovery.buildContactLinkConfiguration(context, account), - activeE164s, - removeMissing); - stopwatch.split("add-links"); - - syncRecipientInfoWithSystemContacts(context, rewrites); - stopwatch.split("sync-info"); - stopwatch.stop(TAG); - } catch (RemoteException | OperationApplicationException e) { - Log.w(TAG, "Failed to update contacts.", e); - } - } - - private static void syncRecipientInfoWithSystemContacts(@NonNull Context context, @NonNull Map rewrites) { - RecipientDatabase recipientDatabase = SignalDatabase.recipients(); - BulkOperationsHandle handle = recipientDatabase.beginBulkSystemContactUpdate(); - - try (SystemContactsRepository.ContactIterator iterator = SystemContactsRepository.getAllSystemContacts(context, rewrites, (number) -> PhoneNumberFormatter.get(context).format(number))) { - while (iterator.hasNext()) { - ContactDetails contact = iterator.next(); - ContactHolder holder = new ContactHolder(); - StructuredNameRecord name = new StructuredNameRecord(contact.getGivenName(), contact.getFamilyName()); - List phones = Stream.of(contact.getNumbers()) - .map(number -> { - return new PhoneNumberRecord.Builder() - .withRecipientId(Recipient.externalContact(context, number.getNumber()).getId()) - .withContactUri(number.getContactUri()) - .withDisplayName(number.getDisplayName()) - .withContactPhotoUri(number.getPhotoUri()) - .withContactLabel(number.getLabel()) - .build(); - }).toList(); - - holder.setStructuredNameRecord(name); - holder.addPhoneNumberRecords(phones); - holder.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 = SignalDatabase.recipients().getRecipientsWithNotificationChannels()) { - Recipient recipient; - while ((recipient = recipients.getNext()) != null) { - NotificationChannels.updateContactChannelName(context, recipient); - } - } - } - } - - private static void notifyNewUsers(@NonNull Context context, - @NonNull Collection newUsers) - { - if (!SignalStore.settings().isNotifyWhenContactJoinsSignal()) return; - - for (RecipientId newUser: newUsers) { - Recipient recipient = Recipient.resolved(newUser); - if (!recipient.isSelf() && - recipient.hasAUserSetDisplayName(context) && - !hasSession(recipient.getId())) - { - IncomingJoinedMessage message = new IncomingJoinedMessage(recipient.getId()); - Optional insertResult = SignalDatabase.sms().insertMessageInbox(message); - - if (insertResult.isPresent()) { - int hour = Calendar.getInstance().get(Calendar.HOUR_OF_DAY); - if (hour >= 9 && hour < 23) { - ApplicationDependencies.getMessageNotifier().updateNotification(context, insertResult.get().getThreadId(), true); - } else { - Log.i(TAG, "Not notifying of a new user due to the time of day. (Hour: " + hour + ")"); - } - } - } - } - } - - public static boolean hasSession(@NonNull RecipientId id) { - Recipient recipient = Recipient.resolved(id); - - if (!recipient.hasServiceId()) { - return false; - } - - SignalProtocolAddress protocolAddress = Recipient.resolved(id).requireServiceId().toProtocolAddress(SignalServiceAddress.DEFAULT_DEVICE_ID); - - return ApplicationDependencies.getProtocolStore().aci().containsSession(protocolAddress) || - ApplicationDependencies.getProtocolStore().pni().containsSession(protocolAddress); + return new RefreshResult(activeIds, result.getNumberRewrites()); } private static Set sanitizeNumbers(@NonNull Set numbers) { diff --git a/app/src/main/java/org/thoughtcrime/securesms/database/RecipientDatabase.kt b/app/src/main/java/org/thoughtcrime/securesms/database/RecipientDatabase.kt index 195d9e9b89..3b3519f61e 100644 --- a/app/src/main/java/org/thoughtcrime/securesms/database/RecipientDatabase.kt +++ b/app/src/main/java/org/thoughtcrime/securesms/database/RecipientDatabase.kt @@ -1110,6 +1110,32 @@ open class RecipientDatabase(context: Context, databaseHelper: SignalDatabase) : return out } + /** + * Given a collection of [RecipientId]s, this will do an efficient bulk query to find all matching E164s. + * If one cannot be found, no error thrown, it will just be omitted. + */ + fun getE164sForIds(ids: Collection): Set { + val queries: List = SqlUtil.buildCustomCollectionQuery( + "$ID = ?", + ids.map { arrayOf(it.serialize()) }.toList() + ) + + val out: MutableSet = mutableSetOf() + + for (query in queries) { + readableDatabase.query(TABLE_NAME, arrayOf(PHONE), query.where, query.whereArgs, null, null, null).use { cursor -> + while (cursor.moveToNext()) { + val e164: String? = cursor.requireString(PHONE) + if (e164 != null) { + out.add(e164) + } + } + } + } + + return out + } + fun beginBulkSystemContactUpdate(): BulkOperationsHandle { val db = writableDatabase val contentValues = ContentValues(1).apply { diff --git a/contacts/app/src/main/java/org/signal/contactstest/ContactsViewModel.kt b/contacts/app/src/main/java/org/signal/contactstest/ContactsViewModel.kt index b46018a0e0..a49fe9379e 100644 --- a/contacts/app/src/main/java/org/signal/contactstest/ContactsViewModel.kt +++ b/contacts/app/src/main/java/org/signal/contactstest/ContactsViewModel.kt @@ -33,7 +33,6 @@ class ContactsViewModel(application: Application) : AndroidViewModel(application if (account != null) { val contactList: List = SystemContactsRepository.getAllSystemContacts( context = application, - rewrites = emptyMap(), e164Formatter = { number -> number } ).use { it.toList() } diff --git a/contacts/lib/src/main/java/org/signal/contacts/SystemContactsRepository.kt b/contacts/lib/src/main/java/org/signal/contacts/SystemContactsRepository.kt index 6a3a1ebc18..0a64c8e349 100644 --- a/contacts/lib/src/main/java/org/signal/contacts/SystemContactsRepository.kt +++ b/contacts/lib/src/main/java/org/signal/contacts/SystemContactsRepository.kt @@ -27,7 +27,7 @@ object SystemContactsRepository { private val TAG = Log.tag(SystemContactsRepository::class.java) - private const val FIELD_FORMATTED_PHONE = ContactsContract.RawContacts.SYNC1 + private const val FIELD_DISPLAY_PHONE = ContactsContract.RawContacts.SYNC1 private const val FIELD_TAG = ContactsContract.Data.SYNC2 private const val FIELD_SUPPORTS_VOICE = ContactsContract.RawContacts.SYNC4 @@ -36,7 +36,7 @@ object SystemContactsRepository { * structured name data. */ @JvmStatic - fun getAllSystemContacts(context: Context, rewrites: Map, e164Formatter: (String) -> String): ContactIterator { + fun getAllSystemContacts(context: Context, e164Formatter: (String) -> String): ContactIterator { val uri = ContactsContract.Data.CONTENT_URI val projection = SqlUtil.buildArgs( ContactsContract.Data.MIMETYPE, @@ -56,9 +56,12 @@ object SystemContactsRepository { val cursor: Cursor = context.contentResolver.query(uri, projection, where, args, orderBy) ?: return EmptyContactIterator() - return CursorContactIterator(cursor, rewrites, e164Formatter) + return CursorContactIterator(cursor, e164Formatter) } + /** + * Retrieves all unique display numbers in the system contacts. (By display, we mean not-E164-formatted) + */ @JvmStatic fun getAllDisplayNumbers(context: Context): Set { val results: MutableSet = mutableSetOf() @@ -116,14 +119,14 @@ object SystemContactsRepository { .appendQueryParameter(ContactsContract.CALLER_IS_SYNCADAPTER, "true") .build() - val projection = arrayOf(BaseColumns._ID, FIELD_FORMATTED_PHONE) + val projection = arrayOf(BaseColumns._ID, FIELD_DISPLAY_PHONE) // TODO Could we write this as a single delete(DELETED = true)? context.contentResolver.query(currentContactsUri, projection, "${ContactsContract.RawContacts.DELETED} = ?", SqlUtil.buildArgs(1), null)?.use { cursor -> while (cursor.moveToNext()) { val rawContactId = cursor.requireLong(BaseColumns._ID) - Log.i(TAG, "Deleting raw contact: ${cursor.requireString(FIELD_FORMATTED_PHONE)}, $rawContactId") + Log.i(TAG, "Deleting raw contact: ${cursor.requireString(FIELD_DISPLAY_PHONE)}, $rawContactId") context.contentResolver.delete(currentContactsUri, "${ContactsContract.RawContacts._ID} = ?", SqlUtil.buildArgs(rawContactId)) } } @@ -375,7 +378,7 @@ object SystemContactsRepository { ContentProviderOperation.newInsert(ContactsContract.RawContacts.CONTENT_URI) .withValue(ContactsContract.RawContacts.ACCOUNT_NAME, linkConfig.account.name) .withValue(ContactsContract.RawContacts.ACCOUNT_TYPE, linkConfig.account.type) - .withValue(FIELD_FORMATTED_PHONE, systemContactInfo.formattedPhone) + .withValue(FIELD_DISPLAY_PHONE, systemContactInfo.displayPhone) .withValue(FIELD_SUPPORTS_VOICE, true.toString()) .build(), @@ -388,7 +391,7 @@ object SystemContactsRepository { ContentProviderOperation.newInsert(dataUri) .withValueBackReference(ContactsContract.CommonDataKinds.Phone.RAW_CONTACT_ID, operationIndex) .withValue(ContactsContract.Data.MIMETYPE, ContactsContract.CommonDataKinds.Phone.CONTENT_ITEM_TYPE) - .withValue(ContactsContract.CommonDataKinds.Phone.NUMBER, systemContactInfo.formattedPhone) + .withValue(ContactsContract.CommonDataKinds.Phone.NUMBER, systemContactInfo.displayPhone) .withValue(ContactsContract.CommonDataKinds.Phone.TYPE, systemContactInfo.type) .withValue(FIELD_TAG, linkConfig.syncTag) .build(), @@ -396,18 +399,18 @@ object SystemContactsRepository { ContentProviderOperation.newInsert(dataUri) .withValueBackReference(ContactsContract.Data.RAW_CONTACT_ID, operationIndex) .withValue(ContactsContract.Data.MIMETYPE, linkConfig.messageMimetype) - .withValue(ContactsContract.Data.DATA1, systemContactInfo.formattedPhone) + .withValue(ContactsContract.Data.DATA1, systemContactInfo.displayPhone) .withValue(ContactsContract.Data.DATA2, linkConfig.appName) - .withValue(ContactsContract.Data.DATA3, linkConfig.messagePrompt(systemContactInfo.formattedPhone)) + .withValue(ContactsContract.Data.DATA3, linkConfig.messagePrompt(systemContactInfo.displayPhone)) .withYieldAllowed(true) .build(), ContentProviderOperation.newInsert(dataUri) .withValueBackReference(ContactsContract.Data.RAW_CONTACT_ID, operationIndex) .withValue(ContactsContract.Data.MIMETYPE, linkConfig.callMimetype) - .withValue(ContactsContract.Data.DATA1, systemContactInfo.formattedPhone) + .withValue(ContactsContract.Data.DATA1, systemContactInfo.displayPhone) .withValue(ContactsContract.Data.DATA2, linkConfig.appName) - .withValue(ContactsContract.Data.DATA3, linkConfig.callPrompt(systemContactInfo.formattedPhone)) + .withValue(ContactsContract.Data.DATA3, linkConfig.callPrompt(systemContactInfo.displayPhone)) .withYieldAllowed(true) .build(), @@ -440,7 +443,7 @@ object SystemContactsRepository { .appendQueryParameter(ContactsContract.RawContacts.ACCOUNT_TYPE, account.type).build() val projection = arrayOf( BaseColumns._ID, - FIELD_FORMATTED_PHONE, + FIELD_DISPLAY_PHONE, FIELD_SUPPORTS_VOICE, ContactsContract.RawContacts.CONTACT_ID, ContactsContract.RawContacts.DISPLAY_NAME_PRIMARY, @@ -451,10 +454,10 @@ object SystemContactsRepository { context.contentResolver.query(currentContactsUri, projection, null, null, null)?.use { cursor -> while (cursor.moveToNext()) { - val formattedPhone = cursor.requireString(FIELD_FORMATTED_PHONE) + val displayPhone = cursor.requireString(FIELD_DISPLAY_PHONE) - if (formattedPhone != null) { - val e164 = e164Formatter(formattedPhone) + if (displayPhone != null) { + val e164 = e164Formatter(displayPhone) contactsDetails[e164] = LinkedContactDetails( id = cursor.requireLong(BaseColumns._ID), @@ -489,7 +492,7 @@ object SystemContactsRepository { if (idCursor.moveToNext()) { return SystemContactInfo( displayName = contactCursor.requireString(ContactsContract.PhoneLookup.DISPLAY_NAME), - formattedPhone = systemNumber, + displayPhone = systemNumber, rawContactId = idCursor.requireLong(ContactsContract.RawContacts._ID), type = contactCursor.requireInt(ContactsContract.PhoneLookup.TYPE) ) @@ -559,7 +562,6 @@ object SystemContactsRepository { */ private class CursorContactIterator( private val cursor: Cursor, - private val e164Rewrites: Map, private val e164Formatter: (String) -> String ) : ContactIterator { @@ -599,17 +601,14 @@ object SystemContactsRepository { val phoneDetails: MutableList = mutableListOf() while (!cursor.isAfterLast && lookupKey == cursor.getLookupKey() && cursor.isPhoneMimeType()) { - val formattedNumber: String? = cursor.requireString(ContactsContract.CommonDataKinds.Phone.NUMBER) - - if (formattedNumber != null && formattedNumber.isNotEmpty()) { - val e164: String = e164Formatter(formattedNumber) - val realE164: String = firstNonEmpty(e164Rewrites[e164], e164) + val displayNumber: String? = cursor.requireString(ContactsContract.CommonDataKinds.Phone.NUMBER) + if (displayNumber != null && displayNumber.isNotEmpty()) { phoneDetails += ContactPhoneDetails( contactUri = ContactsContract.Contacts.getLookupUri(cursor.requireLong(ContactsContract.CommonDataKinds.Phone._ID), lookupKey), displayName = cursor.requireString(ContactsContract.CommonDataKinds.Phone.DISPLAY_NAME), photoUri = cursor.requireString(ContactsContract.CommonDataKinds.Phone.PHOTO_URI), - number = realE164, + number = e164Formatter(displayNumber), type = cursor.requireInt(ContactsContract.CommonDataKinds.Phone.TYPE), label = cursor.requireString(ContactsContract.CommonDataKinds.Phone.LABEL), ) @@ -717,7 +716,7 @@ object SystemContactsRepository { private data class SystemContactInfo( val displayName: String?, - val formattedPhone: String, + val displayPhone: String, val rawContactId: Long, val type: Int )