kopia lustrzana https://github.com/ryukoposting/Signal-Android
Refactor more ContactDiscovery code.
rodzic
d409278dd5
commit
a32d5bef20
|
@ -1,20 +1,46 @@
|
||||||
package org.thoughtcrime.securesms.contacts.sync
|
package org.thoughtcrime.securesms.contacts.sync
|
||||||
|
|
||||||
|
import android.Manifest
|
||||||
import android.accounts.Account
|
import android.accounts.Account
|
||||||
import android.content.Context
|
import android.content.Context
|
||||||
|
import android.content.OperationApplicationException
|
||||||
|
import android.os.RemoteException
|
||||||
|
import android.text.TextUtils
|
||||||
import androidx.annotation.WorkerThread
|
import androidx.annotation.WorkerThread
|
||||||
import org.signal.contacts.ContactLinkConfiguration
|
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.R
|
||||||
import org.thoughtcrime.securesms.database.RecipientDatabase
|
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.phonenumbers.PhoneNumberFormatter
|
||||||
import org.thoughtcrime.securesms.recipients.Recipient
|
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.io.IOException
|
||||||
|
import java.util.Calendar
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Methods for discovering which users are registered and marking them as such in the database.
|
* Methods for discovering which users are registered and marking them as such in the database.
|
||||||
*/
|
*/
|
||||||
object ContactDiscovery {
|
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 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 CALL_MIMETYPE = "vnd.android.cursor.item/vnd.org.thoughtcrime.securesms.call"
|
||||||
private const val CONTACT_TAG = "__TS"
|
private const val CONTACT_TAG = "__TS"
|
||||||
|
@ -23,31 +49,137 @@ object ContactDiscovery {
|
||||||
@Throws(IOException::class)
|
@Throws(IOException::class)
|
||||||
@WorkerThread
|
@WorkerThread
|
||||||
fun refreshAll(context: Context, notifyOfNewUsers: Boolean) {
|
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
|
@JvmStatic
|
||||||
@Throws(IOException::class)
|
@Throws(IOException::class)
|
||||||
@WorkerThread
|
@WorkerThread
|
||||||
fun refresh(context: Context, recipients: List<Recipient>, notifyOfNewUsers: Boolean) {
|
fun refresh(context: Context, recipients: List<Recipient>, notifyOfNewUsers: Boolean) {
|
||||||
return DirectoryHelper.refresh(context, recipients, notifyOfNewUsers)
|
refreshRecipients(
|
||||||
|
context = context,
|
||||||
|
descriptor = "refresh-multiple",
|
||||||
|
refresh = {
|
||||||
|
DirectoryHelper.refresh(context, recipients)
|
||||||
|
},
|
||||||
|
removeSystemContactLinksIfMissing = false,
|
||||||
|
notifyOfNewUsers = notifyOfNewUsers
|
||||||
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
@JvmStatic
|
@JvmStatic
|
||||||
@Throws(IOException::class)
|
@Throws(IOException::class)
|
||||||
@WorkerThread
|
@WorkerThread
|
||||||
fun refresh(context: Context, recipient: Recipient, notifyOfNewUsers: Boolean): RecipientDatabase.RegisteredState {
|
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
|
@JvmStatic
|
||||||
@WorkerThread
|
@WorkerThread
|
||||||
fun syncRecipientInfoWithSystemContacts(context: Context) {
|
fun syncRecipientInfoWithSystemContacts(context: Context) {
|
||||||
DirectoryHelper.syncRecipientInfoWithSystemContacts(context)
|
syncRecipientsWithSystemContacts(context, emptyMap())
|
||||||
}
|
}
|
||||||
|
|
||||||
@JvmStatic
|
private fun refreshRecipients(
|
||||||
fun buildContactLinkConfiguration(context: Context, account: Account): ContactLinkConfiguration {
|
context: Context,
|
||||||
|
descriptor: String,
|
||||||
|
refresh: () -> RefreshResult,
|
||||||
|
removeSystemContactLinksIfMissing: Boolean,
|
||||||
|
notifyOfNewUsers: Boolean
|
||||||
|
): RefreshResult {
|
||||||
|
val stopwatch = Stopwatch(descriptor)
|
||||||
|
|
||||||
|
val preExistingRegisteredIds: Set<RecipientId> = 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<RecipientId> = SignalDatabase.recipients.getSystemContacts().toSet()
|
||||||
|
val newlyRegisteredSystemContacts: Set<RecipientId> = (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<RecipientId>) {
|
||||||
|
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(
|
return ContactLinkConfiguration(
|
||||||
account = account,
|
account = account,
|
||||||
appName = context.getString(R.string.app_name),
|
appName = context.getString(R.string.app_name),
|
||||||
|
@ -59,4 +191,123 @@ object ContactDiscovery {
|
||||||
syncTag = CONTACT_TAG
|
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<RecipientId>, 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<String> = 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<String, String>) {
|
||||||
|
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<RecipientId>,
|
||||||
|
val rewrites: Map<String, String>
|
||||||
|
)
|
||||||
}
|
}
|
||||||
|
|
|
@ -1,11 +1,6 @@
|
||||||
package org.thoughtcrime.securesms.contacts.sync;
|
package org.thoughtcrime.securesms.contacts.sync;
|
||||||
|
|
||||||
import android.Manifest;
|
|
||||||
import android.accounts.Account;
|
|
||||||
import android.content.Context;
|
import android.content.Context;
|
||||||
import android.content.OperationApplicationException;
|
|
||||||
import android.os.RemoteException;
|
|
||||||
import android.text.TextUtils;
|
|
||||||
|
|
||||||
import androidx.annotation.NonNull;
|
import androidx.annotation.NonNull;
|
||||||
import androidx.annotation.WorkerThread;
|
import androidx.annotation.WorkerThread;
|
||||||
|
@ -14,30 +9,18 @@ import com.annimon.stream.Collectors;
|
||||||
import com.annimon.stream.Stream;
|
import com.annimon.stream.Stream;
|
||||||
|
|
||||||
import org.signal.contacts.SystemContactsRepository;
|
import org.signal.contacts.SystemContactsRepository;
|
||||||
import org.signal.contacts.SystemContactsRepository.ContactDetails;
|
|
||||||
import org.signal.core.util.logging.Log;
|
import org.signal.core.util.logging.Log;
|
||||||
import org.signal.libsignal.protocol.SignalProtocolAddress;
|
|
||||||
import org.signal.libsignal.protocol.util.Pair;
|
import org.signal.libsignal.protocol.util.Pair;
|
||||||
import org.thoughtcrime.securesms.BuildConfig;
|
import org.thoughtcrime.securesms.contacts.sync.ContactDiscovery.RefreshResult;
|
||||||
import org.thoughtcrime.securesms.R;
|
|
||||||
import org.thoughtcrime.securesms.database.MessageDatabase.InsertResult;
|
|
||||||
import org.thoughtcrime.securesms.database.RecipientDatabase;
|
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.database.SignalDatabase;
|
||||||
import org.thoughtcrime.securesms.dependencies.ApplicationDependencies;
|
import org.thoughtcrime.securesms.dependencies.ApplicationDependencies;
|
||||||
import org.thoughtcrime.securesms.jobs.MultiDeviceContactUpdateJob;
|
import org.thoughtcrime.securesms.jobs.MultiDeviceContactUpdateJob;
|
||||||
import org.thoughtcrime.securesms.jobs.RetrieveProfileJob;
|
import org.thoughtcrime.securesms.jobs.RetrieveProfileJob;
|
||||||
import org.thoughtcrime.securesms.jobs.StorageSyncJob;
|
|
||||||
import org.thoughtcrime.securesms.keyvalue.SignalStore;
|
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.phonenumbers.PhoneNumberFormatter;
|
||||||
import org.thoughtcrime.securesms.recipients.Recipient;
|
import org.thoughtcrime.securesms.recipients.Recipient;
|
||||||
import org.thoughtcrime.securesms.recipients.RecipientId;
|
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.FeatureFlags;
|
||||||
import org.thoughtcrime.securesms.util.ProfileUtil;
|
import org.thoughtcrime.securesms.util.ProfileUtil;
|
||||||
import org.signal.core.util.SetUtil;
|
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.ProfileAndCredential;
|
||||||
import org.whispersystems.signalservice.api.profiles.SignalServiceProfile;
|
import org.whispersystems.signalservice.api.profiles.SignalServiceProfile;
|
||||||
import org.whispersystems.signalservice.api.push.ACI;
|
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.api.services.ProfileService;
|
||||||
import org.whispersystems.signalservice.internal.ServiceResponse;
|
import org.whispersystems.signalservice.internal.ServiceResponse;
|
||||||
|
|
||||||
import java.io.IOException;
|
import java.io.IOException;
|
||||||
import java.util.Calendar;
|
|
||||||
import java.util.Collection;
|
|
||||||
import java.util.Collections;
|
import java.util.Collections;
|
||||||
import java.util.HashSet;
|
import java.util.HashSet;
|
||||||
import java.util.List;
|
import java.util.List;
|
||||||
import java.util.Map;
|
import java.util.Map;
|
||||||
import java.util.Optional;
|
|
||||||
import java.util.Set;
|
import java.util.Set;
|
||||||
import java.util.concurrent.TimeUnit;
|
import java.util.concurrent.TimeUnit;
|
||||||
|
|
||||||
|
@ -72,36 +51,18 @@ class DirectoryHelper {
|
||||||
private static final String TAG = Log.tag(DirectoryHelper.class);
|
private static final String TAG = Log.tag(DirectoryHelper.class);
|
||||||
|
|
||||||
@WorkerThread
|
@WorkerThread
|
||||||
static void refreshAll(@NonNull Context context, boolean notifyOfNewUsers) throws IOException {
|
static @NonNull RefreshResult refreshAll(@NonNull Context context) 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;
|
|
||||||
}
|
|
||||||
|
|
||||||
RecipientDatabase recipientDatabase = SignalDatabase.recipients();
|
RecipientDatabase recipientDatabase = SignalDatabase.recipients();
|
||||||
Set<String> databaseE164s = sanitizeNumbers(recipientDatabase.getAllE164s());
|
Set<String> databaseE164s = sanitizeNumbers(recipientDatabase.getAllE164s());
|
||||||
Set<String> systemE164s = sanitizeNumbers(Stream.of(SystemContactsRepository.getAllDisplayNumbers(context))
|
Set<String> systemE164s = sanitizeNumbers(Stream.of(SystemContactsRepository.getAllDisplayNumbers(context))
|
||||||
.map(number -> PhoneNumberFormatter.get(context).format(number))
|
.map(number -> PhoneNumberFormatter.get(context).format(number))
|
||||||
.collect(Collectors.toSet()));
|
.collect(Collectors.toSet()));
|
||||||
|
|
||||||
refreshNumbers(context, databaseE164s, systemE164s, notifyOfNewUsers, true);
|
return refreshNumbers(context, databaseE164s, systemE164s);
|
||||||
|
|
||||||
StorageSyncHelper.scheduleSyncForDataChange();
|
|
||||||
}
|
}
|
||||||
|
|
||||||
@WorkerThread
|
@WorkerThread
|
||||||
static void refresh(@NonNull Context context, @NonNull List<Recipient> recipients, boolean notifyOfNewUsers) throws IOException {
|
static @NonNull RefreshResult refresh(@NonNull Context context, @NonNull List<Recipient> recipients) throws IOException {
|
||||||
RecipientDatabase recipientDatabase = SignalDatabase.recipients();
|
RecipientDatabase recipientDatabase = SignalDatabase.recipients();
|
||||||
|
|
||||||
for (Recipient recipient : recipients) {
|
for (Recipient recipient : recipients) {
|
||||||
|
@ -119,107 +80,17 @@ class DirectoryHelper {
|
||||||
.map(Recipient::requireE164)
|
.map(Recipient::requireE164)
|
||||||
.collect(Collectors.toSet());
|
.collect(Collectors.toSet());
|
||||||
|
|
||||||
refreshNumbers(context, numbers, numbers, notifyOfNewUsers, false);
|
return refreshNumbers(context, numbers, numbers);
|
||||||
}
|
}
|
||||||
|
|
||||||
@WorkerThread
|
@WorkerThread
|
||||||
static RegisteredState refresh(@NonNull Context context, @NonNull Recipient recipient, boolean notifyOfNewUsers) throws IOException {
|
private static RefreshResult refreshNumbers(@NonNull Context context, @NonNull Set<String> databaseNumbers, @NonNull Set<String> systemNumbers) 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<String> databaseNumbers, @NonNull Set<String> systemNumbers, boolean notifyOfNewUsers, boolean removeSystemContactEntryForMissing) throws IOException {
|
|
||||||
RecipientDatabase recipientDatabase = SignalDatabase.recipients();
|
RecipientDatabase recipientDatabase = SignalDatabase.recipients();
|
||||||
Set<String> allNumbers = SetUtil.union(databaseNumbers, systemNumbers);
|
Set<String> allNumbers = SetUtil.union(databaseNumbers, systemNumbers);
|
||||||
|
|
||||||
if (allNumbers.isEmpty()) {
|
if (allNumbers.isEmpty()) {
|
||||||
Log.w(TAG, "No numbers to refresh!");
|
Log.w(TAG, "No numbers to refresh!");
|
||||||
return;
|
return new RefreshResult(Collections.emptySet(), Collections.emptyMap());
|
||||||
}
|
}
|
||||||
|
|
||||||
Stopwatch stopwatch = new Stopwatch("refresh");
|
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.");
|
Log.i(TAG, "Some profile fetches failed to resolve. Assuming not-inactive for now and scheduling a retry.");
|
||||||
RetrieveProfileJob.enqueue(unlistedResult.getRetries());
|
RetrieveProfileJob.enqueue(unlistedResult.getRetries());
|
||||||
}
|
}
|
||||||
|
|
||||||
stopwatch.split("handle-unlisted");
|
stopwatch.split("handle-unlisted");
|
||||||
|
|
||||||
Set<RecipientId> preExistingRegisteredUsers = new HashSet<>(recipientDatabase.getRegistered());
|
|
||||||
|
|
||||||
recipientDatabase.bulkUpdatedRegisteredStatus(aciMap, inactiveIds);
|
recipientDatabase.bulkUpdatedRegisteredStatus(aciMap, inactiveIds);
|
||||||
|
|
||||||
stopwatch.split("update-registered");
|
stopwatch.split("update-registered");
|
||||||
|
|
||||||
updateContactsDatabase(context, activeIds, removeSystemContactEntryForMissing, result.getNumberRewrites());
|
|
||||||
|
|
||||||
stopwatch.split("contacts-db");
|
|
||||||
|
|
||||||
if (TextSecurePreferences.isMultiDevice(context)) {
|
if (TextSecurePreferences.isMultiDevice(context)) {
|
||||||
ApplicationDependencies.getJobManager().add(new MultiDeviceContactUpdateJob());
|
ApplicationDependencies.getJobManager().add(new MultiDeviceContactUpdateJob());
|
||||||
}
|
}
|
||||||
|
|
||||||
if (TextSecurePreferences.hasSuccessfullyRetrievedDirectory(context) && notifyOfNewUsers) {
|
|
||||||
Set<RecipientId> systemContacts = new HashSet<>(recipientDatabase.getSystemContacts());
|
|
||||||
Set<RecipientId> newlyRegisteredSystemContacts = new HashSet<>(activeIds);
|
|
||||||
|
|
||||||
newlyRegisteredSystemContacts.removeAll(preExistingRegisteredUsers);
|
|
||||||
newlyRegisteredSystemContacts.retainAll(systemContacts);
|
|
||||||
|
|
||||||
notifyNewUsers(context, newlyRegisteredSystemContacts);
|
|
||||||
} else {
|
|
||||||
TextSecurePreferences.setHasSuccessfullyRetrievedDirectory(context, true);
|
|
||||||
}
|
|
||||||
|
|
||||||
stopwatch.stop(TAG);
|
stopwatch.stop(TAG);
|
||||||
}
|
|
||||||
|
|
||||||
private static void updateContactsDatabase(@NonNull Context context,
|
return new RefreshResult(activeIds, result.getNumberRewrites());
|
||||||
@NonNull Collection<RecipientId> activeIds,
|
|
||||||
boolean removeMissing,
|
|
||||||
@NonNull Map<String, String> 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<String> 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<String, String> 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<PhoneNumberRecord> 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<RecipientId> 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> 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);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
private static Set<String> sanitizeNumbers(@NonNull Set<String> numbers) {
|
private static Set<String> sanitizeNumbers(@NonNull Set<String> numbers) {
|
||||||
|
|
|
@ -1110,6 +1110,32 @@ open class RecipientDatabase(context: Context, databaseHelper: SignalDatabase) :
|
||||||
return out
|
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<RecipientId>): Set<String> {
|
||||||
|
val queries: List<SqlUtil.Query> = SqlUtil.buildCustomCollectionQuery(
|
||||||
|
"$ID = ?",
|
||||||
|
ids.map { arrayOf(it.serialize()) }.toList()
|
||||||
|
)
|
||||||
|
|
||||||
|
val out: MutableSet<String> = 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 {
|
fun beginBulkSystemContactUpdate(): BulkOperationsHandle {
|
||||||
val db = writableDatabase
|
val db = writableDatabase
|
||||||
val contentValues = ContentValues(1).apply {
|
val contentValues = ContentValues(1).apply {
|
||||||
|
|
|
@ -33,7 +33,6 @@ class ContactsViewModel(application: Application) : AndroidViewModel(application
|
||||||
if (account != null) {
|
if (account != null) {
|
||||||
val contactList: List<ContactDetails> = SystemContactsRepository.getAllSystemContacts(
|
val contactList: List<ContactDetails> = SystemContactsRepository.getAllSystemContacts(
|
||||||
context = application,
|
context = application,
|
||||||
rewrites = emptyMap(),
|
|
||||||
e164Formatter = { number -> number }
|
e164Formatter = { number -> number }
|
||||||
).use { it.toList() }
|
).use { it.toList() }
|
||||||
|
|
||||||
|
|
|
@ -27,7 +27,7 @@ object SystemContactsRepository {
|
||||||
|
|
||||||
private val TAG = Log.tag(SystemContactsRepository::class.java)
|
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_TAG = ContactsContract.Data.SYNC2
|
||||||
private const val FIELD_SUPPORTS_VOICE = ContactsContract.RawContacts.SYNC4
|
private const val FIELD_SUPPORTS_VOICE = ContactsContract.RawContacts.SYNC4
|
||||||
|
|
||||||
|
@ -36,7 +36,7 @@ object SystemContactsRepository {
|
||||||
* structured name data.
|
* structured name data.
|
||||||
*/
|
*/
|
||||||
@JvmStatic
|
@JvmStatic
|
||||||
fun getAllSystemContacts(context: Context, rewrites: Map<String, String>, e164Formatter: (String) -> String): ContactIterator {
|
fun getAllSystemContacts(context: Context, e164Formatter: (String) -> String): ContactIterator {
|
||||||
val uri = ContactsContract.Data.CONTENT_URI
|
val uri = ContactsContract.Data.CONTENT_URI
|
||||||
val projection = SqlUtil.buildArgs(
|
val projection = SqlUtil.buildArgs(
|
||||||
ContactsContract.Data.MIMETYPE,
|
ContactsContract.Data.MIMETYPE,
|
||||||
|
@ -56,9 +56,12 @@ object SystemContactsRepository {
|
||||||
|
|
||||||
val cursor: Cursor = context.contentResolver.query(uri, projection, where, args, orderBy) ?: return EmptyContactIterator()
|
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
|
@JvmStatic
|
||||||
fun getAllDisplayNumbers(context: Context): Set<String> {
|
fun getAllDisplayNumbers(context: Context): Set<String> {
|
||||||
val results: MutableSet<String> = mutableSetOf()
|
val results: MutableSet<String> = mutableSetOf()
|
||||||
|
@ -116,14 +119,14 @@ object SystemContactsRepository {
|
||||||
.appendQueryParameter(ContactsContract.CALLER_IS_SYNCADAPTER, "true")
|
.appendQueryParameter(ContactsContract.CALLER_IS_SYNCADAPTER, "true")
|
||||||
.build()
|
.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)?
|
// 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 ->
|
context.contentResolver.query(currentContactsUri, projection, "${ContactsContract.RawContacts.DELETED} = ?", SqlUtil.buildArgs(1), null)?.use { cursor ->
|
||||||
while (cursor.moveToNext()) {
|
while (cursor.moveToNext()) {
|
||||||
val rawContactId = cursor.requireLong(BaseColumns._ID)
|
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))
|
context.contentResolver.delete(currentContactsUri, "${ContactsContract.RawContacts._ID} = ?", SqlUtil.buildArgs(rawContactId))
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -375,7 +378,7 @@ object SystemContactsRepository {
|
||||||
ContentProviderOperation.newInsert(ContactsContract.RawContacts.CONTENT_URI)
|
ContentProviderOperation.newInsert(ContactsContract.RawContacts.CONTENT_URI)
|
||||||
.withValue(ContactsContract.RawContacts.ACCOUNT_NAME, linkConfig.account.name)
|
.withValue(ContactsContract.RawContacts.ACCOUNT_NAME, linkConfig.account.name)
|
||||||
.withValue(ContactsContract.RawContacts.ACCOUNT_TYPE, linkConfig.account.type)
|
.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())
|
.withValue(FIELD_SUPPORTS_VOICE, true.toString())
|
||||||
.build(),
|
.build(),
|
||||||
|
|
||||||
|
@ -388,7 +391,7 @@ object SystemContactsRepository {
|
||||||
ContentProviderOperation.newInsert(dataUri)
|
ContentProviderOperation.newInsert(dataUri)
|
||||||
.withValueBackReference(ContactsContract.CommonDataKinds.Phone.RAW_CONTACT_ID, operationIndex)
|
.withValueBackReference(ContactsContract.CommonDataKinds.Phone.RAW_CONTACT_ID, operationIndex)
|
||||||
.withValue(ContactsContract.Data.MIMETYPE, ContactsContract.CommonDataKinds.Phone.CONTENT_ITEM_TYPE)
|
.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(ContactsContract.CommonDataKinds.Phone.TYPE, systemContactInfo.type)
|
||||||
.withValue(FIELD_TAG, linkConfig.syncTag)
|
.withValue(FIELD_TAG, linkConfig.syncTag)
|
||||||
.build(),
|
.build(),
|
||||||
|
@ -396,18 +399,18 @@ object SystemContactsRepository {
|
||||||
ContentProviderOperation.newInsert(dataUri)
|
ContentProviderOperation.newInsert(dataUri)
|
||||||
.withValueBackReference(ContactsContract.Data.RAW_CONTACT_ID, operationIndex)
|
.withValueBackReference(ContactsContract.Data.RAW_CONTACT_ID, operationIndex)
|
||||||
.withValue(ContactsContract.Data.MIMETYPE, linkConfig.messageMimetype)
|
.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.DATA2, linkConfig.appName)
|
||||||
.withValue(ContactsContract.Data.DATA3, linkConfig.messagePrompt(systemContactInfo.formattedPhone))
|
.withValue(ContactsContract.Data.DATA3, linkConfig.messagePrompt(systemContactInfo.displayPhone))
|
||||||
.withYieldAllowed(true)
|
.withYieldAllowed(true)
|
||||||
.build(),
|
.build(),
|
||||||
|
|
||||||
ContentProviderOperation.newInsert(dataUri)
|
ContentProviderOperation.newInsert(dataUri)
|
||||||
.withValueBackReference(ContactsContract.Data.RAW_CONTACT_ID, operationIndex)
|
.withValueBackReference(ContactsContract.Data.RAW_CONTACT_ID, operationIndex)
|
||||||
.withValue(ContactsContract.Data.MIMETYPE, linkConfig.callMimetype)
|
.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.DATA2, linkConfig.appName)
|
||||||
.withValue(ContactsContract.Data.DATA3, linkConfig.callPrompt(systemContactInfo.formattedPhone))
|
.withValue(ContactsContract.Data.DATA3, linkConfig.callPrompt(systemContactInfo.displayPhone))
|
||||||
.withYieldAllowed(true)
|
.withYieldAllowed(true)
|
||||||
.build(),
|
.build(),
|
||||||
|
|
||||||
|
@ -440,7 +443,7 @@ object SystemContactsRepository {
|
||||||
.appendQueryParameter(ContactsContract.RawContacts.ACCOUNT_TYPE, account.type).build()
|
.appendQueryParameter(ContactsContract.RawContacts.ACCOUNT_TYPE, account.type).build()
|
||||||
val projection = arrayOf(
|
val projection = arrayOf(
|
||||||
BaseColumns._ID,
|
BaseColumns._ID,
|
||||||
FIELD_FORMATTED_PHONE,
|
FIELD_DISPLAY_PHONE,
|
||||||
FIELD_SUPPORTS_VOICE,
|
FIELD_SUPPORTS_VOICE,
|
||||||
ContactsContract.RawContacts.CONTACT_ID,
|
ContactsContract.RawContacts.CONTACT_ID,
|
||||||
ContactsContract.RawContacts.DISPLAY_NAME_PRIMARY,
|
ContactsContract.RawContacts.DISPLAY_NAME_PRIMARY,
|
||||||
|
@ -451,10 +454,10 @@ object SystemContactsRepository {
|
||||||
|
|
||||||
context.contentResolver.query(currentContactsUri, projection, null, null, null)?.use { cursor ->
|
context.contentResolver.query(currentContactsUri, projection, null, null, null)?.use { cursor ->
|
||||||
while (cursor.moveToNext()) {
|
while (cursor.moveToNext()) {
|
||||||
val formattedPhone = cursor.requireString(FIELD_FORMATTED_PHONE)
|
val displayPhone = cursor.requireString(FIELD_DISPLAY_PHONE)
|
||||||
|
|
||||||
if (formattedPhone != null) {
|
if (displayPhone != null) {
|
||||||
val e164 = e164Formatter(formattedPhone)
|
val e164 = e164Formatter(displayPhone)
|
||||||
|
|
||||||
contactsDetails[e164] = LinkedContactDetails(
|
contactsDetails[e164] = LinkedContactDetails(
|
||||||
id = cursor.requireLong(BaseColumns._ID),
|
id = cursor.requireLong(BaseColumns._ID),
|
||||||
|
@ -489,7 +492,7 @@ object SystemContactsRepository {
|
||||||
if (idCursor.moveToNext()) {
|
if (idCursor.moveToNext()) {
|
||||||
return SystemContactInfo(
|
return SystemContactInfo(
|
||||||
displayName = contactCursor.requireString(ContactsContract.PhoneLookup.DISPLAY_NAME),
|
displayName = contactCursor.requireString(ContactsContract.PhoneLookup.DISPLAY_NAME),
|
||||||
formattedPhone = systemNumber,
|
displayPhone = systemNumber,
|
||||||
rawContactId = idCursor.requireLong(ContactsContract.RawContacts._ID),
|
rawContactId = idCursor.requireLong(ContactsContract.RawContacts._ID),
|
||||||
type = contactCursor.requireInt(ContactsContract.PhoneLookup.TYPE)
|
type = contactCursor.requireInt(ContactsContract.PhoneLookup.TYPE)
|
||||||
)
|
)
|
||||||
|
@ -559,7 +562,6 @@ object SystemContactsRepository {
|
||||||
*/
|
*/
|
||||||
private class CursorContactIterator(
|
private class CursorContactIterator(
|
||||||
private val cursor: Cursor,
|
private val cursor: Cursor,
|
||||||
private val e164Rewrites: Map<String, String>,
|
|
||||||
private val e164Formatter: (String) -> String
|
private val e164Formatter: (String) -> String
|
||||||
) : ContactIterator {
|
) : ContactIterator {
|
||||||
|
|
||||||
|
@ -599,17 +601,14 @@ object SystemContactsRepository {
|
||||||
val phoneDetails: MutableList<ContactPhoneDetails> = mutableListOf()
|
val phoneDetails: MutableList<ContactPhoneDetails> = mutableListOf()
|
||||||
|
|
||||||
while (!cursor.isAfterLast && lookupKey == cursor.getLookupKey() && cursor.isPhoneMimeType()) {
|
while (!cursor.isAfterLast && lookupKey == cursor.getLookupKey() && cursor.isPhoneMimeType()) {
|
||||||
val formattedNumber: String? = cursor.requireString(ContactsContract.CommonDataKinds.Phone.NUMBER)
|
val displayNumber: String? = cursor.requireString(ContactsContract.CommonDataKinds.Phone.NUMBER)
|
||||||
|
|
||||||
if (formattedNumber != null && formattedNumber.isNotEmpty()) {
|
|
||||||
val e164: String = e164Formatter(formattedNumber)
|
|
||||||
val realE164: String = firstNonEmpty(e164Rewrites[e164], e164)
|
|
||||||
|
|
||||||
|
if (displayNumber != null && displayNumber.isNotEmpty()) {
|
||||||
phoneDetails += ContactPhoneDetails(
|
phoneDetails += ContactPhoneDetails(
|
||||||
contactUri = ContactsContract.Contacts.getLookupUri(cursor.requireLong(ContactsContract.CommonDataKinds.Phone._ID), lookupKey),
|
contactUri = ContactsContract.Contacts.getLookupUri(cursor.requireLong(ContactsContract.CommonDataKinds.Phone._ID), lookupKey),
|
||||||
displayName = cursor.requireString(ContactsContract.CommonDataKinds.Phone.DISPLAY_NAME),
|
displayName = cursor.requireString(ContactsContract.CommonDataKinds.Phone.DISPLAY_NAME),
|
||||||
photoUri = cursor.requireString(ContactsContract.CommonDataKinds.Phone.PHOTO_URI),
|
photoUri = cursor.requireString(ContactsContract.CommonDataKinds.Phone.PHOTO_URI),
|
||||||
number = realE164,
|
number = e164Formatter(displayNumber),
|
||||||
type = cursor.requireInt(ContactsContract.CommonDataKinds.Phone.TYPE),
|
type = cursor.requireInt(ContactsContract.CommonDataKinds.Phone.TYPE),
|
||||||
label = cursor.requireString(ContactsContract.CommonDataKinds.Phone.LABEL),
|
label = cursor.requireString(ContactsContract.CommonDataKinds.Phone.LABEL),
|
||||||
)
|
)
|
||||||
|
@ -717,7 +716,7 @@ object SystemContactsRepository {
|
||||||
|
|
||||||
private data class SystemContactInfo(
|
private data class SystemContactInfo(
|
||||||
val displayName: String?,
|
val displayName: String?,
|
||||||
val formattedPhone: String,
|
val displayPhone: String,
|
||||||
val rawContactId: Long,
|
val rawContactId: Long,
|
||||||
val type: Int
|
val type: Int
|
||||||
)
|
)
|
||||||
|
|
Ładowanie…
Reference in New Issue