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 98c0c83ec..c591afb45 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,24 +1,19 @@ 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 import org.signal.contacts.SystemContactsRepository.ContactIterator import org.signal.contacts.SystemContactsRepository.ContactPhoneDetails import org.signal.core.util.Stopwatch import org.signal.core.util.StringUtil 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.jobs.SyncSystemContactLinksJob import org.thoughtcrime.securesms.keyvalue.SignalStore import org.thoughtcrime.securesms.notifications.NotificationChannels import org.thoughtcrime.securesms.notifications.v2.ConversationId @@ -45,9 +40,6 @@ 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" private const val FULL_SYSTEM_CONTACT_SYNC_THRESHOLD = 3 @JvmStatic @@ -154,8 +146,7 @@ object ContactDiscovery { stopwatch.split("cds") if (hasContactsPermissions(context)) { - addSystemContactLinks(context, result.registeredIds, removeSystemContactLinksIfMissing) - stopwatch.split("contact-links") + ApplicationDependencies.getJobManager().add(SyncSystemContactLinksJob()) val useFullSync = removeSystemContactLinksIfMissing && result.registeredIds.size > FULL_SYSTEM_CONTACT_SYNC_THRESHOLD syncRecipientsWithSystemContacts( @@ -215,70 +206,10 @@ object ContactDiscovery { } } - private fun buildContactLinkConfiguration(context: Context, account: Account): ContactLinkConfiguration { - return ContactLinkConfiguration( - account = account, - appName = context.getString(R.string.app_name), - messagePrompt = { e164 -> context.getString(R.string.ContactsDatabase_message_s, e164) }, - callPrompt = { e164 -> context.getString(R.string.ContactsDatabase_signal_call_s, e164) }, - e164Formatter = { number -> PhoneNumberFormatter.get(context).format(number) }, - messageMimetype = MESSAGE_MIMETYPE, - callMimetype = CALL_MIMETYPE, - 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 = 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 - } - - try { - val registeredE164s: Set = SignalDatabase.recipients.getE164sForIds(registeredIds) - stopwatch.split("fetch-e164s") - - SystemContactsRepository.removeDeletedRawContactsForAccount(context, account) - stopwatch.split("delete-stragglers") - - SystemContactsRepository.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) */ diff --git a/app/src/main/java/org/thoughtcrime/securesms/contacts/sync/ContactDiscoveryRefreshV2.kt b/app/src/main/java/org/thoughtcrime/securesms/contacts/sync/ContactDiscoveryRefreshV2.kt index b3ea9c44c..75ebf5cc8 100644 --- a/app/src/main/java/org/thoughtcrime/securesms/contacts/sync/ContactDiscoveryRefreshV2.kt +++ b/app/src/main/java/org/thoughtcrime/securesms/contacts/sync/ContactDiscoveryRefreshV2.kt @@ -172,7 +172,10 @@ object ContactDiscoveryRefreshV2 { stopwatch.split("process-result") val existingIds: Set = SignalDatabase.recipients.getAllPossiblyRegisteredByE164(recipientE164s + rewrites.values) + stopwatch.split("get-ids") + val inactiveIds: Set = (existingIds - registeredIds).removeRegisteredButUnlisted() + stopwatch.split("registered-but-unlisted") SignalDatabase.recipients.bulkUpdatedRegisteredStatus(aciMap, inactiveIds) stopwatch.split("update-registered") 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 1feed7341..e0d445fc2 100644 --- a/app/src/main/java/org/thoughtcrime/securesms/database/RecipientDatabase.kt +++ b/app/src/main/java/org/thoughtcrime/securesms/database/RecipientDatabase.kt @@ -22,6 +22,7 @@ import org.signal.core.util.optionalInt import org.signal.core.util.optionalLong import org.signal.core.util.optionalString import org.signal.core.util.or +import org.signal.core.util.readToSet import org.signal.core.util.requireBlob import org.signal.core.util.requireBoolean import org.signal.core.util.requireInt @@ -2201,12 +2202,12 @@ open class RecipientDatabase(context: Context, databaseHelper: SignalDatabase) : } fun bulkUpdatedRegisteredStatus(registered: Map, unregistered: Collection) { - val db = writableDatabase + writableDatabase.withinTransaction { db -> + val registeredWithServiceId: Set = getRegisteredWithServiceIds() + val needsMarkRegistered: Map = registered - registeredWithServiceId - db.beginTransaction() - try { - for ((recipientId, serviceId) in registered) { - val values = ContentValues(2).apply { + for ((recipientId, serviceId) in needsMarkRegistered) { + val values = ContentValues().apply { put(REGISTERED, RegisteredState.REGISTERED.id) put(UNREGISTERED_TIMESTAMP, 0) if (serviceId != null) { @@ -2236,10 +2237,6 @@ open class RecipientDatabase(context: Context, databaseHelper: SignalDatabase) : ApplicationDependencies.getDatabaseObserver().notifyRecipientChanged(id) } } - - db.setTransactionSuccessful() - } finally { - db.endTransaction() } } @@ -2907,6 +2904,17 @@ open class RecipientDatabase(context: Context, databaseHelper: SignalDatabase) : return results } + fun getRegisteredWithServiceIds(): Set { + return readableDatabase + .select(ID) + .from(TABLE_NAME) + .where("$REGISTERED = ? and $HIDDEN = ? AND $SERVICE_ID NOT NULL", 1, 0) + .run() + .readToSet { cursor -> + RecipientId.from(cursor.requireLong(ID)) + } + } + fun getSystemContacts(): List { val results: MutableList = LinkedList() @@ -2919,6 +2927,17 @@ open class RecipientDatabase(context: Context, databaseHelper: SignalDatabase) : return results } + fun getRegisteredE164s(): Set { + return readableDatabase + .select(PHONE) + .from(TABLE_NAME) + .where("$REGISTERED = ? and $HIDDEN = ? AND $PHONE NOT NULL", 1, 0) + .run() + .readToSet { cursor -> + cursor.requireNonNullString(PHONE) + } + } + /** * We no longer automatically generate a chat color. This method is used only * in the case of a legacy migration and otherwise should not be called. diff --git a/app/src/main/java/org/thoughtcrime/securesms/jobs/JobManagerFactories.java b/app/src/main/java/org/thoughtcrime/securesms/jobs/JobManagerFactories.java index b8f5c8102..d8b80a402 100644 --- a/app/src/main/java/org/thoughtcrime/securesms/jobs/JobManagerFactories.java +++ b/app/src/main/java/org/thoughtcrime/securesms/jobs/JobManagerFactories.java @@ -175,6 +175,7 @@ public final class JobManagerFactories { put(SendReadReceiptJob.KEY, new SendReadReceiptJob.Factory(application)); put(SendRetryReceiptJob.KEY, new SendRetryReceiptJob.Factory()); put(SendViewedReceiptJob.KEY, new SendViewedReceiptJob.Factory(application)); + put(SyncSystemContactLinksJob.KEY, new SyncSystemContactLinksJob.Factory()); put(MultiDeviceStorySendSyncJob.KEY, new MultiDeviceStorySendSyncJob.Factory()); put(ServiceOutageDetectionJob.KEY, new ServiceOutageDetectionJob.Factory()); put(SmsReceiveJob.KEY, new SmsReceiveJob.Factory()); diff --git a/app/src/main/java/org/thoughtcrime/securesms/jobs/SyncSystemContactLinksJob.kt b/app/src/main/java/org/thoughtcrime/securesms/jobs/SyncSystemContactLinksJob.kt new file mode 100644 index 000000000..0f4ea6475 --- /dev/null +++ b/app/src/main/java/org/thoughtcrime/securesms/jobs/SyncSystemContactLinksJob.kt @@ -0,0 +1,109 @@ +package org.thoughtcrime.securesms.jobs + +import android.Manifest +import android.accounts.Account +import android.content.Context +import android.content.OperationApplicationException +import android.os.RemoteException +import org.signal.contacts.ContactLinkConfiguration +import org.signal.contacts.SystemContactsRepository +import org.signal.core.util.Stopwatch +import org.signal.core.util.logging.Log +import org.thoughtcrime.securesms.BuildConfig +import org.thoughtcrime.securesms.R +import org.thoughtcrime.securesms.database.SignalDatabase +import org.thoughtcrime.securesms.jobmanager.Data +import org.thoughtcrime.securesms.jobmanager.Job +import org.thoughtcrime.securesms.permissions.Permissions +import org.thoughtcrime.securesms.phonenumbers.PhoneNumberFormatter +import java.lang.Exception + +/** + * This job makes sure all of the contact "links" are up-to-date. The links are the actions you see when you look at a Signal user in your system contacts + * that let you send a message or start a call. + */ +class SyncSystemContactLinksJob private constructor(parameters: Parameters) : BaseJob(parameters) { + + constructor() : this( + Parameters.Builder() + .setQueue("SyncSystemContactLinksJob") + .setMaxAttempts(1) + .setMaxInstancesForQueue(2) + .build() + ) + + override fun serialize(): Data = Data.EMPTY + override fun getFactoryKey() = KEY + override fun onFailure() = Unit + override fun onShouldRetry(e: Exception) = false + + override fun onRun() { + if (!Permissions.hasAll(context, Manifest.permission.READ_CONTACTS, Manifest.permission.WRITE_CONTACTS)) { + Log.w(TAG, "No contact permissions. Skipping.") + return + } + + val stopwatch = Stopwatch("contact-links") + + val registeredE164s: Set = SignalDatabase.recipients.getRegisteredE164s() + + if (registeredE164s.isEmpty()) { + Log.w(TAG, "No registeredE164s. Skipping.") + return + } + + stopwatch.split("fetch-e164s") + + val account = SystemContactsRepository.getOrCreateSystemAccount(context, BuildConfig.APPLICATION_ID, context.getString(R.string.app_name)) + if (account == null) { + Log.w(TAG, "Failed to create an account!") + return + } + + try { + SystemContactsRepository.removeDeletedRawContactsForAccount(context, account) + stopwatch.split("delete-stragglers") + + SystemContactsRepository.addMessageAndCallLinksToContacts( + context = context, + config = buildContactLinkConfiguration(context, account), + targetE164s = registeredE164s, + removeIfMissing = true + ) + 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) + } + } + + private fun buildContactLinkConfiguration(context: Context, account: Account): ContactLinkConfiguration { + return ContactLinkConfiguration( + account = account, + appName = context.getString(R.string.app_name), + messagePrompt = { e164 -> context.getString(R.string.ContactsDatabase_message_s, e164) }, + callPrompt = { e164 -> context.getString(R.string.ContactsDatabase_signal_call_s, e164) }, + e164Formatter = { number -> PhoneNumberFormatter.get(context).format(number) }, + messageMimetype = MESSAGE_MIMETYPE, + callMimetype = CALL_MIMETYPE, + syncTag = CONTACT_TAG + ) + } + + class Factory : Job.Factory { + override fun create(parameters: Parameters, data: Data) = SyncSystemContactLinksJob(parameters) + } + + companion object { + private val TAG = Log.tag(SyncSystemContactLinksJob::class.java) + + const val KEY = "SyncSystemContactLinksJob" + + 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" + } +} diff --git a/contacts/app/src/main/java/org/signal/contactstest/MainActivity.kt b/contacts/app/src/main/java/org/signal/contactstest/MainActivity.kt index 10b7be3e3..396599e7e 100644 --- a/contacts/app/src/main/java/org/signal/contactstest/MainActivity.kt +++ b/contacts/app/src/main/java/org/signal/contactstest/MainActivity.kt @@ -45,6 +45,7 @@ class MainActivity : AppCompatActivity() { } findViewById