Improve contact pull-to-refresh performance.

main
Greyson Parrelli 2022-10-27 15:06:45 -04:00 zatwierdzone przez Cody Henthorne
rodzic 2cfa685ae2
commit 4077dc829a
10 zmienionych plików z 229 dodań i 89 usunięć

Wyświetl plik

@ -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)
*/

Wyświetl plik

@ -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")

Wyświetl plik

@ -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.

Wyświetl plik

@ -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());

Wyświetl plik

@ -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"
}
}

Wyświetl plik

@ -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()
}

Wyświetl plik

@ -48,4 +48,5 @@
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintTop_toBottomOf="@id/link_contacts_button" />
</androidx.constraintlayout.widget.ConstraintLayout>

Wyświetl plik

@ -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
)

Wyświetl plik

@ -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

Wyświetl plik

@ -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);