Improve contact pull-to-refresh performance.
This commit is contained in:
parent
2cfa685ae2
commit
4077dc829a
10 changed files with 229 additions and 89 deletions
|
@ -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<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 = 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<String> = 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)
|
||||
*/
|
||||
|
|
|
@ -172,7 +172,10 @@ object ContactDiscoveryRefreshV2 {
|
|||
stopwatch.split("process-result")
|
||||
|
||||
val existingIds: Set<RecipientId> = SignalDatabase.recipients.getAllPossiblyRegisteredByE164(recipientE164s + rewrites.values)
|
||||
stopwatch.split("get-ids")
|
||||
|
||||
val inactiveIds: Set<RecipientId> = (existingIds - registeredIds).removeRegisteredButUnlisted()
|
||||
stopwatch.split("registered-but-unlisted")
|
||||
|
||||
SignalDatabase.recipients.bulkUpdatedRegisteredStatus(aciMap, inactiveIds)
|
||||
stopwatch.split("update-registered")
|
||||
|
|
|
@ -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<RecipientId, ServiceId?>, unregistered: Collection<RecipientId>) {
|
||||
val db = writableDatabase
|
||||
writableDatabase.withinTransaction { db ->
|
||||
val registeredWithServiceId: Set<RecipientId> = getRegisteredWithServiceIds()
|
||||
val needsMarkRegistered: Map<RecipientId, ServiceId?> = 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<RecipientId> {
|
||||
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<RecipientId> {
|
||||
val results: MutableList<RecipientId> = LinkedList()
|
||||
|
||||
|
@ -2919,6 +2927,17 @@ open class RecipientDatabase(context: Context, databaseHelper: SignalDatabase) :
|
|||
return results
|
||||
}
|
||||
|
||||
fun getRegisteredE164s(): Set<String> {
|
||||
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.
|
||||
|
|
|
@ -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());
|
||||
|
|
|
@ -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<String> = 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<SyncSystemContactLinksJob> {
|
||||
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"
|
||||
}
|
||||
}
|
|
@ -45,6 +45,7 @@ class MainActivity : AppCompatActivity() {
|
|||
}
|
||||
|
||||
findViewById<Button>(R.id.link_contacts_button).setOnClickListener { v ->
|
||||
val startTime = System.currentTimeMillis()
|
||||
if (hasPermission(Manifest.permission.READ_CONTACTS) && hasPermission(Manifest.permission.WRITE_CONTACTS)) {
|
||||
SimpleTask.run({
|
||||
val allE164s: Set<String> = SystemContactsRepository.getAllDisplayNumbers(this).map { PhoneNumberUtils.formatNumberToE164(it, "US") }.toSet()
|
||||
|
@ -60,7 +61,7 @@ class MainActivity : AppCompatActivity() {
|
|||
return@run true
|
||||
}, { success ->
|
||||
if (success) {
|
||||
Toast.makeText(this, "Success!", Toast.LENGTH_SHORT).show()
|
||||
Toast.makeText(this, "Success! Took ${System.currentTimeMillis() - startTime} ms", Toast.LENGTH_SHORT).show()
|
||||
} else {
|
||||
Toast.makeText(this, "Failed to create account!", Toast.LENGTH_SHORT).show()
|
||||
}
|
||||
|
@ -71,6 +72,7 @@ class MainActivity : AppCompatActivity() {
|
|||
}
|
||||
|
||||
findViewById<Button>(R.id.unlink_contact_button).setOnClickListener { v ->
|
||||
val startTime = System.currentTimeMillis()
|
||||
if (hasPermission(Manifest.permission.READ_CONTACTS) && hasPermission(Manifest.permission.WRITE_CONTACTS)) {
|
||||
SimpleTask.run({
|
||||
val account: Account = SystemContactsRepository.getOrCreateSystemAccount(this, BuildConfig.APPLICATION_ID, "Contact Test") ?: return@run false
|
||||
|
@ -85,7 +87,7 @@ class MainActivity : AppCompatActivity() {
|
|||
return@run true
|
||||
}, { success ->
|
||||
if (success) {
|
||||
Toast.makeText(this, "Success!", Toast.LENGTH_SHORT).show()
|
||||
Toast.makeText(this, "Success! Took ${System.currentTimeMillis() - startTime} ms", Toast.LENGTH_SHORT).show()
|
||||
} else {
|
||||
Toast.makeText(this, "Failed to create account!", Toast.LENGTH_SHORT).show()
|
||||
}
|
||||
|
|
|
@ -48,4 +48,5 @@
|
|||
app:layout_constraintStart_toStartOf="parent"
|
||||
app:layout_constraintTop_toBottomOf="@id/link_contacts_button" />
|
||||
|
||||
|
||||
</androidx.constraintlayout.widget.ConstraintLayout>
|
|
@ -22,6 +22,58 @@ import java.util.Objects
|
|||
|
||||
/**
|
||||
* A way to retrieve and update data in the Android system contacts.
|
||||
*
|
||||
* Contacts in Android are miserable, but they're reasonably well-documented here:
|
||||
* https://developer.android.com/guide/topics/providers/contacts-provider
|
||||
*
|
||||
* But here's a summary of how contacts are stored.
|
||||
*
|
||||
* There's three main entities:
|
||||
* - Contacts
|
||||
* - RawContacts
|
||||
* - ContactData
|
||||
*
|
||||
* Each Contact can have multiple RawContacts associated with it, and each RawContact can have multiple ContactDatas associated with it.
|
||||
*
|
||||
* ┌───────Contact────────┐
|
||||
* │ │ │
|
||||
* ▼ ▼ ▼
|
||||
* RawContact RawContact RawContact
|
||||
* │ │ │
|
||||
* ├─►Data ├─►Data ├─►Data
|
||||
* │ │ │
|
||||
* ├─►Data ├─►Data ├─►Data
|
||||
* │ │ │
|
||||
* └─►Data └─►Data └─►Data
|
||||
*
|
||||
* (Shortened ContactData -> Data for space)
|
||||
*
|
||||
* How are they linked together?
|
||||
* - Each RawContact has a [ContactsContract.RawContacts.CONTACT_ID] that links to a [ContactsContract.Contacts._ID]
|
||||
* - Each ContactData has a [ContactsContract.Data.RAW_CONTACT_ID] column that links to a [ContactsContract.RawContacts._ID]
|
||||
* - Each ContactData has a [ContactsContract.Data.CONTACT_ID] column that links to a [ContactsContract.Contacts._ID]
|
||||
* - Each ContactData has a [ContactsContract.Data.LOOKUP_KEY] column that links to a [ContactsContract.Contacts.LOOKUP_KEY]
|
||||
* - The lookup key is a way to link back to a Contact in a more stable way. Apparently linking using the CONTACT_ID can lead to unstable results if a sync
|
||||
* is happening or data is otherwise corrupted.
|
||||
*
|
||||
* What type of stuff are stored in each?
|
||||
* - Contact only really has metadata about the contact. Basically the stuff you see at the top of the contact entry in the contacts app, like:
|
||||
* - Photo
|
||||
* - Display name (*not* structured name)
|
||||
* - Whether or not it's starred
|
||||
* - RawContact also only really has metadata, largely about which account it's bound to
|
||||
* - ContactData is where all the actual contact details are, stuff like:
|
||||
* - Phone
|
||||
* - Email
|
||||
* - Structured name
|
||||
* - Address
|
||||
* - ContactData has a [ContactsContract.Data.MIMETYPE] that will tell you what kind of data is it. Common ones are [ContactsContract.CommonDataKinds.Phone.CONTENT_ITEM_TYPE]
|
||||
* and [ContactsContract.CommonDataKinds.StructuredName.CONTENT_ITEM_TYPE]
|
||||
* - You can imagine that it's tricky to come up with a schema that can store arbitrary contact data -- that's why a lot of the columns in ContactData are just
|
||||
* generic things, like [ContactsContract.Data.DATA1]. Thankfully aliases have been provided for common types, like [ContactsContract.CommonDataKinds.Phone.NUMBER],
|
||||
* which is an alias for [ContactsContract.Data.DATA1].
|
||||
*
|
||||
*
|
||||
*/
|
||||
object SystemContactsRepository {
|
||||
|
||||
|
@ -32,8 +84,10 @@ object SystemContactsRepository {
|
|||
private const val FIELD_SUPPORTS_VOICE = ContactsContract.RawContacts.SYNC4
|
||||
|
||||
/**
|
||||
* Gets and returns a cursor of data for all contacts, containing both phone number data and
|
||||
* structured name data.
|
||||
* Gets and returns an iterator over data for all contacts, containing both phone number data and structured name data.
|
||||
*
|
||||
* In order to get all of this in one query, we have to query all of the ContactData items with the appropriate mimetypes, and then group it together by
|
||||
* lookup key.
|
||||
*/
|
||||
@JvmStatic
|
||||
fun getAllSystemContacts(context: Context, e164Formatter: (String) -> String): ContactIterator {
|
||||
|
@ -423,6 +477,7 @@ object SystemContactsRepository {
|
|||
.build()
|
||||
|
||||
return listOf(
|
||||
// RawContact entry
|
||||
ContentProviderOperation.newInsert(ContactsContract.RawContacts.CONTENT_URI)
|
||||
.withValue(ContactsContract.RawContacts.ACCOUNT_NAME, linkConfig.account.name)
|
||||
.withValue(ContactsContract.RawContacts.ACCOUNT_TYPE, linkConfig.account.type)
|
||||
|
@ -430,12 +485,14 @@ object SystemContactsRepository {
|
|||
.withValue(FIELD_SUPPORTS_VOICE, true.toString())
|
||||
.build(),
|
||||
|
||||
// Data entry for name
|
||||
ContentProviderOperation.newInsert(dataUri)
|
||||
.withValueBackReference(ContactsContract.CommonDataKinds.StructuredName.RAW_CONTACT_ID, operationIndex)
|
||||
.withValue(ContactsContract.CommonDataKinds.StructuredName.DISPLAY_NAME, systemContactInfo.displayName)
|
||||
.withValue(ContactsContract.Data.MIMETYPE, ContactsContract.CommonDataKinds.StructuredName.CONTENT_ITEM_TYPE)
|
||||
.build(),
|
||||
|
||||
// Data entry for number (Note: This may not be necessary)
|
||||
ContentProviderOperation.newInsert(dataUri)
|
||||
.withValueBackReference(ContactsContract.CommonDataKinds.Phone.RAW_CONTACT_ID, operationIndex)
|
||||
.withValue(ContactsContract.Data.MIMETYPE, ContactsContract.CommonDataKinds.Phone.CONTENT_ITEM_TYPE)
|
||||
|
@ -444,6 +501,7 @@ object SystemContactsRepository {
|
|||
.withValue(FIELD_TAG, linkConfig.syncTag)
|
||||
.build(),
|
||||
|
||||
// Data entry for sending a message
|
||||
ContentProviderOperation.newInsert(dataUri)
|
||||
.withValueBackReference(ContactsContract.Data.RAW_CONTACT_ID, operationIndex)
|
||||
.withValue(ContactsContract.Data.MIMETYPE, linkConfig.messageMimetype)
|
||||
|
@ -453,6 +511,7 @@ object SystemContactsRepository {
|
|||
.withYieldAllowed(true)
|
||||
.build(),
|
||||
|
||||
// Data entry for making a call
|
||||
ContentProviderOperation.newInsert(dataUri)
|
||||
.withValueBackReference(ContactsContract.Data.RAW_CONTACT_ID, operationIndex)
|
||||
.withValue(ContactsContract.Data.MIMETYPE, linkConfig.callMimetype)
|
||||
|
@ -462,8 +521,9 @@ object SystemContactsRepository {
|
|||
.withYieldAllowed(true)
|
||||
.build(),
|
||||
|
||||
// Ensures that this RawContact entry is shown next to another RawContact entry we found for this contact
|
||||
ContentProviderOperation.newUpdate(ContactsContract.AggregationExceptions.CONTENT_URI)
|
||||
.withValue(ContactsContract.AggregationExceptions.RAW_CONTACT_ID1, systemContactInfo.rawContactId)
|
||||
.withValue(ContactsContract.AggregationExceptions.RAW_CONTACT_ID1, systemContactInfo.siblingRawContactId)
|
||||
.withValueBackReference(ContactsContract.AggregationExceptions.RAW_CONTACT_ID2, operationIndex)
|
||||
.withValue(ContactsContract.AggregationExceptions.TYPE, ContactsContract.AggregationExceptions.TYPE_KEEP_TOGETHER)
|
||||
.build()
|
||||
|
@ -522,12 +582,13 @@ object SystemContactsRepository {
|
|||
}
|
||||
|
||||
private fun getSystemContactInfo(context: Context, e164: String, e164Formatter: (String) -> String): SystemContactInfo? {
|
||||
ContactsContract.RawContactsEntity.RAW_CONTACT_ID
|
||||
val uri = Uri.withAppendedPath(ContactsContract.PhoneLookup.CONTENT_FILTER_URI, Uri.encode(e164))
|
||||
val projection = arrayOf(
|
||||
ContactsContract.PhoneLookup.NUMBER,
|
||||
ContactsContract.PhoneLookup._ID,
|
||||
ContactsContract.PhoneLookup.DISPLAY_NAME,
|
||||
ContactsContract.PhoneLookup.TYPE
|
||||
ContactsContract.PhoneLookup.TYPE,
|
||||
)
|
||||
|
||||
context.contentResolver.query(uri, projection, null, null, null)?.use { contactCursor ->
|
||||
|
@ -541,7 +602,7 @@ object SystemContactsRepository {
|
|||
return SystemContactInfo(
|
||||
displayName = contactCursor.requireString(ContactsContract.PhoneLookup.DISPLAY_NAME),
|
||||
displayPhone = systemNumber,
|
||||
rawContactId = idCursor.requireLong(ContactsContract.RawContacts._ID),
|
||||
siblingRawContactId = idCursor.requireLong(ContactsContract.RawContacts._ID),
|
||||
type = contactCursor.requireInt(ContactsContract.PhoneLookup.TYPE)
|
||||
)
|
||||
}
|
||||
|
@ -765,7 +826,7 @@ object SystemContactsRepository {
|
|||
private data class SystemContactInfo(
|
||||
val displayName: String?,
|
||||
val displayPhone: String,
|
||||
val rawContactId: Long,
|
||||
val siblingRawContactId: Long,
|
||||
val type: Int
|
||||
)
|
||||
|
||||
|
|
|
@ -91,4 +91,17 @@ inline fun <T> Cursor.readToList(predicate: (T) -> Boolean = { true }, mapper: (
|
|||
return list
|
||||
}
|
||||
|
||||
inline fun <T> Cursor.readToSet(predicate: (T) -> Boolean = { true }, mapper: (Cursor) -> T): Set<T> {
|
||||
val set = mutableSetOf<T>()
|
||||
use {
|
||||
while (moveToNext()) {
|
||||
val record = mapper(this)
|
||||
if (predicate(record)) {
|
||||
set += mapper(this)
|
||||
}
|
||||
}
|
||||
}
|
||||
return set
|
||||
}
|
||||
|
||||
fun Boolean.toInt(): Int = if (this) 1 else 0
|
||||
|
|
|
@ -127,7 +127,7 @@ public final class Log {
|
|||
logger.e(tag, message, t, keepLonger);
|
||||
}
|
||||
|
||||
public static String tag(Class<?> clazz) {
|
||||
public static @NonNull String tag(Class<?> clazz) {
|
||||
String simpleName = clazz.getSimpleName();
|
||||
if (simpleName.length() > 23) {
|
||||
return simpleName.substring(0, 23);
|
||||
|
|
Loading…
Add table
Reference in a new issue