Improve contact sync for individual contacts.

fork-5.53.8
Greyson Parrelli 2022-04-22 07:47:49 -04:00 zatwierdzone przez Cody Henthorne
rodzic e2292dfa34
commit 5478285362
34 zmienionych plików z 431 dodań i 547 usunięć

Wyświetl plik

@ -8,10 +8,8 @@ import android.os.RemoteException
import android.text.TextUtils
import androidx.annotation.WorkerThread
import org.signal.contacts.ContactLinkConfiguration
import org.signal.contacts.SystemContactsRepository.addMessageAndCallLinksToContacts
import org.signal.contacts.SystemContactsRepository.getAllSystemContacts
import org.signal.contacts.SystemContactsRepository.getOrCreateSystemAccount
import org.signal.contacts.SystemContactsRepository.removeDeletedRawContactsForAccount
import org.signal.contacts.SystemContactsRepository
import org.signal.core.util.StringUtil
import org.signal.core.util.logging.Log
import org.thoughtcrime.securesms.BuildConfig
import org.thoughtcrime.securesms.R
@ -22,6 +20,7 @@ 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.profiles.ProfileName
import org.thoughtcrime.securesms.recipients.Recipient
import org.thoughtcrime.securesms.recipients.RecipientId
import org.thoughtcrime.securesms.registration.RegistrationUtil
@ -45,6 +44,7 @@ object ContactDiscovery {
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
@Throws(IOException::class)
@ -133,6 +133,10 @@ object ContactDiscovery {
syncRecipientsWithSystemContacts(context, emptyMap())
}
private fun phoneNumberFormatter(context: Context): (String) -> String {
return { PhoneNumberFormatter.get(context).format(it) }
}
private fun refreshRecipients(
context: Context,
descriptor: String,
@ -152,7 +156,23 @@ object ContactDiscovery {
addSystemContactLinks(context, result.registeredIds, removeSystemContactLinksIfMissing)
stopwatch.split("contact-links")
syncRecipientsWithSystemContacts(context, result.rewrites)
syncRecipientsWithSystemContacts(
context = context,
rewrites = result.rewrites,
contactsProvider = {
if (result.registeredIds.size > FULL_SYSTEM_CONTACT_SYNC_THRESHOLD) {
Log.d(TAG, "Doing a full system contact sync because there are ${result.registeredIds.size} contacts to get info for.")
SystemContactsRepository.getAllSystemContacts(context, phoneNumberFormatter(context))
} else {
Log.d(TAG, "Doing a partial system contact sync because there are ${result.registeredIds.size} contacts to get info for.")
SystemContactsRepository.getContactDetailsByQueries(
context = context,
queries = Recipient.resolvedList(result.registeredIds).mapNotNull { it.e164.orElse(null) },
e164Formatter = phoneNumberFormatter(context)
)
}
}
)
stopwatch.split("contact-sync")
if (TextSecurePreferences.hasSuccessfullyRetrievedDirectory(context) && notifyOfNewUsers) {
@ -227,7 +247,7 @@ object ContactDiscovery {
val stopwatch = Stopwatch("contact-links")
val account = getOrCreateSystemAccount(context, BuildConfig.APPLICATION_ID, context.getString(R.string.app_name))
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
@ -237,10 +257,10 @@ object ContactDiscovery {
val registeredE164s: Set<String> = SignalDatabase.recipients.getE164sForIds(registeredIds)
stopwatch.split("fetch-e164s")
removeDeletedRawContactsForAccount(context, account)
SystemContactsRepository.removeDeletedRawContactsForAccount(context, account)
stopwatch.split("delete-stragglers")
addMessageAndCallLinksToContacts(
SystemContactsRepository.addMessageAndCallLinksToContacts(
context = context,
config = buildContactLinkConfiguration(context, account),
targetE164s = registeredE164s,
@ -259,30 +279,38 @@ object ContactDiscovery {
/**
* Synchronizes info from the system contacts (name, avatar, etc)
*/
private fun syncRecipientsWithSystemContacts(context: Context, rewrites: Map<String, String>) {
private fun syncRecipientsWithSystemContacts(
context: Context,
rewrites: Map<String, String>,
contactsProvider: () -> SystemContactsRepository.ContactIterator = { SystemContactsRepository.getAllSystemContacts(context, phoneNumberFormatter(context)) }
) {
val handle = SignalDatabase.recipients.beginBulkSystemContactUpdate()
try {
getAllSystemContacts(context) { PhoneNumberFormatter.get(context).format(it) }.use { iterator ->
contactsProvider().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)
for (phoneDetails in details.numbers) {
val realNumber: String = Util.getFirstNonEmpty(rewrites[phoneDetails.number], phoneDetails.number)
val profileName: ProfileName = if (!StringUtil.isEmpty(details.givenName)) {
ProfileName.fromParts(details.givenName, details.familyName)
} else if (!StringUtil.isEmpty(phoneDetails.displayName)) {
ProfileName.asGiven(phoneDetails.displayName)
} else {
ProfileName.EMPTY
}
handle.setSystemContactInfo(
Recipient.externalContact(context, realNumber).id,
profileName,
phoneDetails.displayName,
phoneDetails.photoUri,
phoneDetails.label,
phoneDetails.type,
phoneDetails.contactUri.toString()
)
}
}
}
} catch (e: IllegalStateException) {

Wyświetl plik

@ -1,54 +0,0 @@
package org.thoughtcrime.securesms.contacts.sync;
import android.net.Uri;
import androidx.annotation.NonNull;
import androidx.annotation.Nullable;
import org.signal.core.util.logging.Log;
import org.thoughtcrime.securesms.database.RecipientDatabase;
import org.thoughtcrime.securesms.profiles.ProfileName;
import java.util.LinkedList;
import java.util.List;
import java.util.Optional;
final class ContactHolder {
private static final String TAG = Log.tag(ContactHolder.class);
private final List<PhoneNumberRecord> phoneNumberRecords = new LinkedList<>();
private StructuredNameRecord structuredNameRecord;
public void addPhoneNumberRecords(@NonNull List<PhoneNumberRecord> phoneNumberRecords) {
this.phoneNumberRecords.addAll(phoneNumberRecords);
}
public void setStructuredNameRecord(@NonNull StructuredNameRecord structuredNameRecord) {
this.structuredNameRecord = structuredNameRecord;
}
void commit(@NonNull RecipientDatabase.BulkOperationsHandle handle) {
for (PhoneNumberRecord phoneNumberRecord : phoneNumberRecords) {
handle.setSystemContactInfo(phoneNumberRecord.getRecipientId(),
getProfileName(phoneNumberRecord.getDisplayName()),
phoneNumberRecord.getDisplayName(),
phoneNumberRecord.getContactPhotoUri(),
phoneNumberRecord.getContactLabel(),
phoneNumberRecord.getPhoneType(),
Optional.ofNullable(phoneNumberRecord.getContactUri()).map(Uri::toString).orElse(null));
}
}
private @NonNull ProfileName getProfileName(@Nullable String displayName) {
if (structuredNameRecord != null && structuredNameRecord.hasGivenName()) {
return structuredNameRecord.asProfileName();
} else if (displayName != null) {
return ProfileName.asGiven(displayName);
} else {
Log.w(TAG, "Failed to find a suitable display name!");
return ProfileName.EMPTY;
}
}
}

Wyświetl plik

@ -1,99 +0,0 @@
package org.thoughtcrime.securesms.contacts.sync;
import android.net.Uri;
import androidx.annotation.NonNull;
import androidx.annotation.Nullable;
import org.thoughtcrime.securesms.recipients.RecipientId;
import java.util.Objects;
/**
* Represents all the data we pull from a Phone data cursor row from the contacts database.
*/
final class PhoneNumberRecord {
private final RecipientId recipientId;
private final String displayName;
private final String contactPhotoUri;
private final String contactLabel;
private final int phoneType;
private final Uri contactUri;
private PhoneNumberRecord(@NonNull PhoneNumberRecord.Builder builder) {
recipientId = Objects.requireNonNull(builder.recipientId);
displayName = builder.displayName;
contactPhotoUri = builder.contactPhotoUri;
contactLabel = builder.contactLabel;
phoneType = builder.phoneType;
contactUri = builder.contactUri;
}
@NonNull RecipientId getRecipientId() {
return recipientId;
}
@Nullable String getDisplayName() {
return displayName;
}
@Nullable String getContactPhotoUri() {
return contactPhotoUri;
}
@Nullable String getContactLabel() {
return contactLabel;
}
int getPhoneType() {
return phoneType;
}
@Nullable Uri getContactUri() {
return contactUri;
}
final static class Builder {
private RecipientId recipientId;
private String displayName;
private String contactPhotoUri;
private String contactLabel;
private int phoneType;
private Uri contactUri;
@NonNull Builder withRecipientId(@NonNull RecipientId recipientId) {
this.recipientId = recipientId;
return this;
}
@NonNull Builder withDisplayName(@Nullable String displayName) {
this.displayName = displayName;
return this;
}
@NonNull Builder withContactUri(@Nullable Uri contactUri) {
this.contactUri = contactUri;
return this;
}
@NonNull Builder withContactLabel(@Nullable String contactLabel) {
this.contactLabel = contactLabel;
return this;
}
@NonNull Builder withContactPhotoUri(@Nullable String contactPhotoUri) {
this.contactPhotoUri = contactPhotoUri;
return this;
}
@NonNull Builder withPhoneType(int phoneType) {
this.phoneType = phoneType;
return this;
}
@NonNull PhoneNumberRecord build() {
return new PhoneNumberRecord(this);
}
}
}

Wyświetl plik

@ -1,27 +0,0 @@
package org.thoughtcrime.securesms.contacts.sync;
import androidx.annotation.NonNull;
import androidx.annotation.Nullable;
import org.thoughtcrime.securesms.profiles.ProfileName;
/**
* Represents the data pulled from a StructuredName row of a Contacts data cursor.
*/
final class StructuredNameRecord {
private final String givenName;
private final String familyName;
public StructuredNameRecord(@Nullable String givenName, @Nullable String familyName) {
this.givenName = givenName;
this.familyName = familyName;
}
public boolean hasGivenName() {
return givenName != null;
}
public @NonNull ProfileName asProfileName() {
return ProfileName.fromParts(givenName, familyName);
}
}

Wyświetl plik

@ -20,7 +20,11 @@
</activity>
<activity
android:name=".ContactsActivity"
android:name=".ContactListActivity"
android:exported="false" />
<activity
android:name=".ContactLookupActivity"
android:exported="false" />
<service

Wyświetl plik

@ -0,0 +1,31 @@
package org.signal.contactstest
import android.content.Intent
import android.os.Bundle
import androidx.activity.viewModels
import androidx.appcompat.app.AppCompatActivity
import androidx.recyclerview.widget.LinearLayoutManager
import androidx.recyclerview.widget.RecyclerView
class ContactListActivity : AppCompatActivity() {
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
setContentView(R.layout.activity_contact_list)
val list: RecyclerView = findViewById(R.id.list)
val adapter = ContactsAdapter { uri ->
startActivity(
Intent(Intent.ACTION_VIEW).apply {
data = uri
}
)
}
list.layoutManager = LinearLayoutManager(this)
list.adapter = adapter
val viewModel: ContactListViewModel by viewModels()
viewModel.contacts.observe(this) { adapter.submitList(it) }
}
}

Wyświetl plik

@ -2,6 +2,7 @@ package org.signal.contactstest
import android.accounts.Account
import android.app.Application
import android.telephony.PhoneNumberUtils
import androidx.lifecycle.AndroidViewModel
import androidx.lifecycle.LiveData
import androidx.lifecycle.MutableLiveData
@ -11,10 +12,10 @@ import org.signal.contacts.SystemContactsRepository.ContactIterator
import org.signal.core.util.concurrent.SignalExecutors
import org.signal.core.util.logging.Log
class ContactsViewModel(application: Application) : AndroidViewModel(application) {
class ContactListViewModel(application: Application) : AndroidViewModel(application) {
companion object {
private val TAG = Log.tag(ContactsViewModel::class.java)
private val TAG = Log.tag(ContactListViewModel::class.java)
}
private val _contacts: MutableLiveData<List<ContactDetails>> = MutableLiveData()
@ -30,16 +31,20 @@ class ContactsViewModel(application: Application) : AndroidViewModel(application
accountDisplayName = "Test"
)
val startTime: Long = System.currentTimeMillis()
if (account != null) {
val contactList: List<ContactDetails> = SystemContactsRepository.getAllSystemContacts(
context = application,
e164Formatter = { number -> number }
e164Formatter = { number -> PhoneNumberUtils.formatNumberToE164(number, "US") ?: number }
).use { it.toList() }
_contacts.postValue(contactList)
} else {
Log.w(TAG, "Failed to create an account!")
}
Log.d(TAG, "Took ${System.currentTimeMillis() - startTime} ms to fetch contacts.")
}
}

Wyświetl plik

@ -0,0 +1,40 @@
package org.signal.contactstest
import android.content.Intent
import android.os.Bundle
import android.widget.Button
import android.widget.TextView
import androidx.activity.viewModels
import androidx.appcompat.app.AppCompatActivity
import androidx.recyclerview.widget.LinearLayoutManager
import androidx.recyclerview.widget.RecyclerView
class ContactLookupActivity : AppCompatActivity() {
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
setContentView(R.layout.activity_contact_lookup)
val list: RecyclerView = findViewById(R.id.list)
val adapter = ContactsAdapter { uri ->
startActivity(
Intent(Intent.ACTION_VIEW).apply {
data = uri
}
)
}
list.layoutManager = LinearLayoutManager(this)
list.adapter = adapter
val viewModel: ContactLookupViewModel by viewModels()
viewModel.contacts.observe(this) { adapter.submitList(it) }
val lookupText: TextView = findViewById(R.id.lookup_text)
val lookupButton: Button = findViewById(R.id.lookup_button)
lookupButton.setOnClickListener {
viewModel.onLookup(lookupText.text.toString())
}
}
}

Wyświetl plik

@ -0,0 +1,57 @@
package org.signal.contactstest
import android.accounts.Account
import android.app.Application
import android.telephony.PhoneNumberUtils
import androidx.lifecycle.AndroidViewModel
import androidx.lifecycle.LiveData
import androidx.lifecycle.MutableLiveData
import org.signal.contacts.SystemContactsRepository
import org.signal.contacts.SystemContactsRepository.ContactDetails
import org.signal.contacts.SystemContactsRepository.ContactIterator
import org.signal.core.util.concurrent.SignalExecutors
import org.signal.core.util.logging.Log
class ContactLookupViewModel(application: Application) : AndroidViewModel(application) {
companion object {
private val TAG = Log.tag(ContactLookupViewModel::class.java)
}
private val _contacts: MutableLiveData<List<ContactDetails>> = MutableLiveData()
val contacts: LiveData<List<ContactDetails>>
get() = _contacts
fun onLookup(lookup: String) {
SignalExecutors.BOUNDED.execute {
val account: Account? = SystemContactsRepository.getOrCreateSystemAccount(
context = getApplication(),
applicationId = BuildConfig.APPLICATION_ID,
accountDisplayName = "Test"
)
val startTime: Long = System.currentTimeMillis()
if (account != null) {
val contactList: List<ContactDetails> = SystemContactsRepository.getContactDetailsByQueries(
context = getApplication(),
queries = listOf(lookup),
e164Formatter = { number -> PhoneNumberUtils.formatNumberToE164(number, "US") ?: number }
).use { it.toList() }
_contacts.postValue(contactList)
} else {
Log.w(TAG, "Failed to create an account!")
}
Log.d(TAG, "Took ${System.currentTimeMillis() - startTime} ms to fetch contacts.")
}
}
private fun ContactIterator.toList(): List<ContactDetails> {
val list: MutableList<ContactDetails> = mutableListOf()
forEach { list += it }
return list
}
}

Wyświetl plik

@ -1,117 +0,0 @@
package org.signal.contactstest
import android.content.Intent
import android.graphics.BitmapFactory
import android.net.Uri
import android.os.Bundle
import android.provider.ContactsContract
import android.view.LayoutInflater
import android.view.View
import android.view.ViewGroup
import android.widget.ImageView
import android.widget.TextView
import androidx.activity.viewModels
import androidx.appcompat.app.AppCompatActivity
import androidx.recyclerview.widget.DiffUtil
import androidx.recyclerview.widget.LinearLayoutManager
import androidx.recyclerview.widget.ListAdapter
import androidx.recyclerview.widget.RecyclerView
import org.signal.contacts.SystemContactsRepository.ContactDetails
import org.signal.contacts.SystemContactsRepository.ContactPhoneDetails
class ContactsActivity : AppCompatActivity() {
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
setContentView(R.layout.activity_contacts)
val list: RecyclerView = findViewById(R.id.list)
val adapter = ContactsAdapter()
list.layoutManager = LinearLayoutManager(this)
list.adapter = adapter
val viewModel: ContactsViewModel by viewModels()
viewModel.contacts.observe(this) { adapter.submitList(it) }
}
private inner class ContactsAdapter : ListAdapter<ContactDetails, ContactViewHolder>(object : DiffUtil.ItemCallback<ContactDetails>() {
override fun areItemsTheSame(oldItem: ContactDetails, newItem: ContactDetails): Boolean {
return oldItem == newItem
}
override fun areContentsTheSame(oldItem: ContactDetails, newItem: ContactDetails): Boolean {
return oldItem == newItem
}
}) {
override fun onCreateViewHolder(parent: ViewGroup, viewType: Int): ContactViewHolder {
return ContactViewHolder(LayoutInflater.from(parent.context).inflate(R.layout.parent_item, parent, false))
}
override fun onBindViewHolder(holder: ContactViewHolder, position: Int) {
holder.bind(getItem(position))
}
}
private inner class ContactViewHolder(itemView: View) : RecyclerView.ViewHolder(itemView) {
val givenName: TextView = itemView.findViewById(R.id.given_name)
val familyName: TextView = itemView.findViewById(R.id.family_name)
val phoneAdapter: PhoneAdapter = PhoneAdapter()
val phoneList: RecyclerView = itemView.findViewById<RecyclerView?>(R.id.phone_list).apply {
layoutManager = LinearLayoutManager(itemView.context)
adapter = phoneAdapter
}
fun bind(contact: ContactDetails) {
givenName.text = "Given Name: ${contact.givenName}"
familyName.text = "Family Name: ${contact.familyName}"
phoneAdapter.submitList(contact.numbers)
}
}
private inner class PhoneAdapter : ListAdapter<ContactPhoneDetails, PhoneViewHolder>(object : DiffUtil.ItemCallback<ContactPhoneDetails>() {
override fun areItemsTheSame(oldItem: ContactPhoneDetails, newItem: ContactPhoneDetails): Boolean {
return oldItem == newItem
}
override fun areContentsTheSame(oldItem: ContactPhoneDetails, newItem: ContactPhoneDetails): Boolean {
return oldItem == newItem
}
}) {
override fun onCreateViewHolder(parent: ViewGroup, viewType: Int): PhoneViewHolder {
return PhoneViewHolder(LayoutInflater.from(parent.context).inflate(R.layout.child_item, parent, false))
}
override fun onBindViewHolder(holder: PhoneViewHolder, position: Int) {
holder.bind(getItem(position))
}
}
private inner class PhoneViewHolder(itemView: View) : RecyclerView.ViewHolder(itemView) {
val photo: ImageView = itemView.findViewById(R.id.contact_photo)
val displayName: TextView = itemView.findViewById(R.id.display_name)
val number: TextView = itemView.findViewById(R.id.number)
val type: TextView = itemView.findViewById(R.id.type)
val goButton: View = itemView.findViewById(R.id.go_button)
fun bind(details: ContactPhoneDetails) {
if (details.photoUri != null) {
photo.setImageBitmap(BitmapFactory.decodeStream(itemView.context.contentResolver.openInputStream(Uri.parse(details.photoUri))))
} else {
photo.setImageBitmap(null)
}
displayName.text = details.displayName
number.text = details.number
type.text = ContactsContract.CommonDataKinds.Phone.getTypeLabel(itemView.resources, details.type, details.label)
goButton.setOnClickListener {
startActivity(
Intent(Intent.ACTION_VIEW).apply {
data = details.contactUri
}
)
}
}
}
}

Wyświetl plik

@ -0,0 +1,50 @@
package org.signal.contactstest
import android.net.Uri
import android.view.LayoutInflater
import android.view.View
import android.view.ViewGroup
import android.widget.TextView
import androidx.recyclerview.widget.DiffUtil
import androidx.recyclerview.widget.LinearLayoutManager
import androidx.recyclerview.widget.ListAdapter
import androidx.recyclerview.widget.RecyclerView
import org.signal.contacts.SystemContactsRepository
class ContactsAdapter(private val onContactClickedListener: (Uri) -> Unit) : ListAdapter<SystemContactsRepository.ContactDetails, ContactsAdapter.ContactViewHolder>(object : DiffUtil.ItemCallback<SystemContactsRepository.ContactDetails>() {
override fun areItemsTheSame(oldItem: SystemContactsRepository.ContactDetails, newItem: SystemContactsRepository.ContactDetails): Boolean {
return oldItem == newItem
}
override fun areContentsTheSame(oldItem: SystemContactsRepository.ContactDetails, newItem: SystemContactsRepository.ContactDetails): Boolean {
return oldItem == newItem
}
}) {
override fun onCreateViewHolder(parent: ViewGroup, viewType: Int): ContactViewHolder {
return ContactViewHolder(LayoutInflater.from(parent.context).inflate(R.layout.parent_item, parent, false))
}
override fun onBindViewHolder(holder: ContactViewHolder, position: Int) {
holder.bind(getItem(position))
}
inner class ContactViewHolder(itemView: View) : RecyclerView.ViewHolder(itemView) {
private val givenName: TextView = itemView.findViewById(R.id.given_name)
private val familyName: TextView = itemView.findViewById(R.id.family_name)
private val phoneAdapter: PhoneAdapter = PhoneAdapter(onContactClickedListener)
init {
itemView.findViewById<RecyclerView?>(R.id.phone_list).apply {
layoutManager = LinearLayoutManager(itemView.context)
adapter = phoneAdapter
}
}
fun bind(contact: SystemContactsRepository.ContactDetails) {
givenName.text = "Given Name: ${contact.givenName}"
familyName.text = "Family Name: ${contact.familyName}"
phoneAdapter.submitList(contact.numbers)
}
}
}

Wyświetl plik

@ -30,8 +30,15 @@ class MainActivity : AppCompatActivity() {
findViewById<Button>(R.id.contact_list_button).setOnClickListener { v ->
if (hasPermission(Manifest.permission.READ_CONTACTS) && hasPermission(Manifest.permission.WRITE_CONTACTS)) {
startActivity(Intent(this, ContactsActivity::class.java))
finish()
startActivity(Intent(this, ContactListActivity::class.java))
} else {
ActivityCompat.requestPermissions(this, arrayOf(Manifest.permission.READ_CONTACTS, Manifest.permission.WRITE_CONTACTS), PERMISSION_CODE)
}
}
findViewById<Button>(R.id.contact_lookup_button).setOnClickListener { v ->
if (hasPermission(Manifest.permission.READ_CONTACTS) && hasPermission(Manifest.permission.WRITE_CONTACTS)) {
startActivity(Intent(this, ContactLookupActivity::class.java))
} else {
ActivityCompat.requestPermissions(this, arrayOf(Manifest.permission.READ_CONTACTS, Manifest.permission.WRITE_CONTACTS), PERMISSION_CODE)
}
@ -92,12 +99,13 @@ class MainActivity : AppCompatActivity() {
override fun onRequestPermissionsResult(requestCode: Int, permissions: Array<out String>, grantResults: IntArray) {
if (requestCode == PERMISSION_CODE) {
if (grantResults.isNotEmpty() && grantResults.all { it == PackageManager.PERMISSION_GRANTED }) {
startActivity(Intent(this, ContactsActivity::class.java))
finish()
startActivity(Intent(this, ContactListActivity::class.java))
} else {
Toast.makeText(this, "You must provide permissions to continue.", Toast.LENGTH_SHORT).show()
}
}
super.onRequestPermissionsResult(requestCode, permissions, grantResults)
}
private fun hasPermission(permission: String): Boolean {

Wyświetl plik

@ -0,0 +1,53 @@
package org.signal.contactstest
import android.graphics.BitmapFactory
import android.net.Uri
import android.provider.ContactsContract
import android.view.LayoutInflater
import android.view.View
import android.view.ViewGroup
import android.widget.ImageView
import android.widget.TextView
import androidx.recyclerview.widget.DiffUtil
import androidx.recyclerview.widget.ListAdapter
import androidx.recyclerview.widget.RecyclerView
import org.signal.contacts.SystemContactsRepository
class PhoneAdapter(private val onContactClickedListener: (Uri) -> Unit) : ListAdapter<SystemContactsRepository.ContactPhoneDetails, PhoneAdapter.PhoneViewHolder>(object : DiffUtil.ItemCallback<SystemContactsRepository.ContactPhoneDetails>() {
override fun areItemsTheSame(oldItem: SystemContactsRepository.ContactPhoneDetails, newItem: SystemContactsRepository.ContactPhoneDetails): Boolean {
return oldItem == newItem
}
override fun areContentsTheSame(oldItem: SystemContactsRepository.ContactPhoneDetails, newItem: SystemContactsRepository.ContactPhoneDetails): Boolean {
return oldItem == newItem
}
}) {
override fun onCreateViewHolder(parent: ViewGroup, viewType: Int): PhoneViewHolder {
return PhoneViewHolder(LayoutInflater.from(parent.context).inflate(R.layout.child_item, parent, false))
}
override fun onBindViewHolder(holder: PhoneViewHolder, position: Int) {
holder.bind(getItem(position))
}
inner class PhoneViewHolder(itemView: View) : RecyclerView.ViewHolder(itemView) {
private val photo: ImageView = itemView.findViewById(R.id.contact_photo)
private val displayName: TextView = itemView.findViewById(R.id.display_name)
private val number: TextView = itemView.findViewById(R.id.number)
private val type: TextView = itemView.findViewById(R.id.type)
private val goButton: View = itemView.findViewById(R.id.go_button)
fun bind(details: SystemContactsRepository.ContactPhoneDetails) {
if (details.photoUri != null) {
photo.setImageBitmap(BitmapFactory.decodeStream(itemView.context.contentResolver.openInputStream(Uri.parse(details.photoUri))))
} else {
photo.setImageBitmap(null)
}
displayName.text = details.displayName
number.text = details.number
type.text = ContactsContract.CommonDataKinds.Phone.getTypeLabel(itemView.resources, details.type, details.label)
goButton.setOnClickListener { onContactClickedListener(details.contactUri) }
}
}
}

Wyświetl plik

@ -1,31 +0,0 @@
<vector
xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:aapt="http://schemas.android.com/aapt"
android:width="108dp"
android:height="108dp"
android:viewportWidth="108"
android:viewportHeight="108">
<path android:pathData="M31,63.928c0,0 6.4,-11 12.1,-13.1c7.2,-2.6 26,-1.4 26,-1.4l38.1,38.1L107,108.928l-32,-1L31,63.928z">
<aapt:attr name="android:fillColor">
<gradient
android:endX="85.84757"
android:endY="92.4963"
android:startX="42.9492"
android:startY="49.59793"
android:type="linear">
<item
android:color="#44000000"
android:offset="0.0" />
<item
android:color="#00000000"
android:offset="1.0" />
</gradient>
</aapt:attr>
</path>
<path
android:fillColor="#FFFFFF"
android:fillType="nonZero"
android:pathData="M65.3,45.828l3.8,-6.6c0.2,-0.4 0.1,-0.9 -0.3,-1.1c-0.4,-0.2 -0.9,-0.1 -1.1,0.3l-3.9,6.7c-6.3,-2.8 -13.4,-2.8 -19.7,0l-3.9,-6.7c-0.2,-0.4 -0.7,-0.5 -1.1,-0.3C38.8,38.328 38.7,38.828 38.9,39.228l3.8,6.6C36.2,49.428 31.7,56.028 31,63.928h46C76.3,56.028 71.8,49.428 65.3,45.828zM43.4,57.328c-0.8,0 -1.5,-0.5 -1.8,-1.2c-0.3,-0.7 -0.1,-1.5 0.4,-2.1c0.5,-0.5 1.4,-0.7 2.1,-0.4c0.7,0.3 1.2,1 1.2,1.8C45.3,56.528 44.5,57.328 43.4,57.328L43.4,57.328zM64.6,57.328c-0.8,0 -1.5,-0.5 -1.8,-1.2s-0.1,-1.5 0.4,-2.1c0.5,-0.5 1.4,-0.7 2.1,-0.4c0.7,0.3 1.2,1 1.2,1.8C66.5,56.528 65.6,57.328 64.6,57.328L64.6,57.328z"
android:strokeWidth="1"
android:strokeColor="#00000000" />
</vector>

Wyświetl plik

@ -1,171 +0,0 @@
<?xml version="1.0" encoding="utf-8"?>
<vector
xmlns:android="http://schemas.android.com/apk/res/android"
android:width="108dp"
android:height="108dp"
android:viewportWidth="108"
android:viewportHeight="108">
<path
android:fillColor="#3DDC84"
android:pathData="M0,0h108v108h-108z" />
<path
android:fillColor="#00000000"
android:pathData="M9,0L9,108"
android:strokeWidth="0.8"
android:strokeColor="#33FFFFFF" />
<path
android:fillColor="#00000000"
android:pathData="M19,0L19,108"
android:strokeWidth="0.8"
android:strokeColor="#33FFFFFF" />
<path
android:fillColor="#00000000"
android:pathData="M29,0L29,108"
android:strokeWidth="0.8"
android:strokeColor="#33FFFFFF" />
<path
android:fillColor="#00000000"
android:pathData="M39,0L39,108"
android:strokeWidth="0.8"
android:strokeColor="#33FFFFFF" />
<path
android:fillColor="#00000000"
android:pathData="M49,0L49,108"
android:strokeWidth="0.8"
android:strokeColor="#33FFFFFF" />
<path
android:fillColor="#00000000"
android:pathData="M59,0L59,108"
android:strokeWidth="0.8"
android:strokeColor="#33FFFFFF" />
<path
android:fillColor="#00000000"
android:pathData="M69,0L69,108"
android:strokeWidth="0.8"
android:strokeColor="#33FFFFFF" />
<path
android:fillColor="#00000000"
android:pathData="M79,0L79,108"
android:strokeWidth="0.8"
android:strokeColor="#33FFFFFF" />
<path
android:fillColor="#00000000"
android:pathData="M89,0L89,108"
android:strokeWidth="0.8"
android:strokeColor="#33FFFFFF" />
<path
android:fillColor="#00000000"
android:pathData="M99,0L99,108"
android:strokeWidth="0.8"
android:strokeColor="#33FFFFFF" />
<path
android:fillColor="#00000000"
android:pathData="M0,9L108,9"
android:strokeWidth="0.8"
android:strokeColor="#33FFFFFF" />
<path
android:fillColor="#00000000"
android:pathData="M0,19L108,19"
android:strokeWidth="0.8"
android:strokeColor="#33FFFFFF" />
<path
android:fillColor="#00000000"
android:pathData="M0,29L108,29"
android:strokeWidth="0.8"
android:strokeColor="#33FFFFFF" />
<path
android:fillColor="#00000000"
android:pathData="M0,39L108,39"
android:strokeWidth="0.8"
android:strokeColor="#33FFFFFF" />
<path
android:fillColor="#00000000"
android:pathData="M0,49L108,49"
android:strokeWidth="0.8"
android:strokeColor="#33FFFFFF" />
<path
android:fillColor="#00000000"
android:pathData="M0,59L108,59"
android:strokeWidth="0.8"
android:strokeColor="#33FFFFFF" />
<path
android:fillColor="#00000000"
android:pathData="M0,69L108,69"
android:strokeWidth="0.8"
android:strokeColor="#33FFFFFF" />
<path
android:fillColor="#00000000"
android:pathData="M0,79L108,79"
android:strokeWidth="0.8"
android:strokeColor="#33FFFFFF" />
<path
android:fillColor="#00000000"
android:pathData="M0,89L108,89"
android:strokeWidth="0.8"
android:strokeColor="#33FFFFFF" />
<path
android:fillColor="#00000000"
android:pathData="M0,99L108,99"
android:strokeWidth="0.8"
android:strokeColor="#33FFFFFF" />
<path
android:fillColor="#00000000"
android:pathData="M19,29L89,29"
android:strokeWidth="0.8"
android:strokeColor="#33FFFFFF" />
<path
android:fillColor="#00000000"
android:pathData="M19,39L89,39"
android:strokeWidth="0.8"
android:strokeColor="#33FFFFFF" />
<path
android:fillColor="#00000000"
android:pathData="M19,49L89,49"
android:strokeWidth="0.8"
android:strokeColor="#33FFFFFF" />
<path
android:fillColor="#00000000"
android:pathData="M19,59L89,59"
android:strokeWidth="0.8"
android:strokeColor="#33FFFFFF" />
<path
android:fillColor="#00000000"
android:pathData="M19,69L89,69"
android:strokeWidth="0.8"
android:strokeColor="#33FFFFFF" />
<path
android:fillColor="#00000000"
android:pathData="M19,79L89,79"
android:strokeWidth="0.8"
android:strokeColor="#33FFFFFF" />
<path
android:fillColor="#00000000"
android:pathData="M29,19L29,89"
android:strokeWidth="0.8"
android:strokeColor="#33FFFFFF" />
<path
android:fillColor="#00000000"
android:pathData="M39,19L39,89"
android:strokeWidth="0.8"
android:strokeColor="#33FFFFFF" />
<path
android:fillColor="#00000000"
android:pathData="M49,19L49,89"
android:strokeWidth="0.8"
android:strokeColor="#33FFFFFF" />
<path
android:fillColor="#00000000"
android:pathData="M59,19L59,89"
android:strokeWidth="0.8"
android:strokeColor="#33FFFFFF" />
<path
android:fillColor="#00000000"
android:pathData="M69,19L69,89"
android:strokeWidth="0.8"
android:strokeColor="#33FFFFFF" />
<path
android:fillColor="#00000000"
android:pathData="M79,19L79,89"
android:strokeWidth="0.8"
android:strokeColor="#33FFFFFF" />
</vector>

Wyświetl plik

@ -0,0 +1,15 @@
<vector xmlns:android="http://schemas.android.com/apk/res/android"
android:width="108dp"
android:height="108dp"
android:viewportWidth="108"
android:viewportHeight="108"
android:tint="#FFFFFF">
<group android:scaleX="2.0097"
android:scaleY="2.0097"
android:translateX="29.8836"
android:translateY="29.8836">
<path
android:fillColor="@android:color/white"
android:pathData="M20,0L4,0v2h16L20,0zM4,24h16v-2L4,22v2zM20,4L4,4c-1.1,0 -2,0.9 -2,2v12c0,1.1 0.9,2 2,2h16c1.1,0 2,-0.9 2,-2L22,6c0,-1.1 -0.9,-2 -2,-2zM12,6.75c1.24,0 2.25,1.01 2.25,2.25s-1.01,2.25 -2.25,2.25S9.75,10.24 9.75,9 10.76,6.75 12,6.75zM17,17L7,17v-1.5c0,-1.67 3.33,-2.5 5,-2.5s5,0.83 5,2.5L17,17z"/>
</group>
</vector>

Wyświetl plik

@ -0,0 +1,35 @@
<?xml version="1.0" encoding="utf-8"?>
<androidx.constraintlayout.widget.ConstraintLayout
xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:app="http://schemas.android.com/apk/res-auto"
xmlns:tools="http://schemas.android.com/tools"
android:layout_width="match_parent"
android:layout_height="match_parent"
tools:context=".MainActivity">
<EditText
android:id="@+id/lookup_text"
android:layout_width="0dp"
android:layout_height="wrap_content"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintEnd_toStartOf="@id/lookup_button"
app:layout_constraintTop_toTopOf="parent"/>
<Button
android:id="@+id/lookup_button"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:text="Lookup"
app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintTop_toTopOf="parent"/>
<androidx.recyclerview.widget.RecyclerView
android:id="@+id/list"
android:layout_width="match_parent"
android:layout_height="0dp"
android:clipToPadding="false"
android:clipChildren="false"
app:layout_constraintTop_toBottomOf="@id/lookup_text"
app:layout_constraintBottom_toBottomOf="parent"/>
</androidx.constraintlayout.widget.ConstraintLayout>

Wyświetl plik

@ -11,12 +11,23 @@
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:text="Contact List"
app:layout_constraintBottom_toTopOf="@id/link_contacts_button"
app:layout_constraintBottom_toTopOf="@id/contact_lookup_button"
app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintTop_toTopOf="parent"
app:layout_constraintVertical_chainStyle="packed"/>
<Button
android:id="@+id/contact_lookup_button"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:text="Contact Lookup"
app:layout_constraintBottom_toTopOf="@id/link_contacts_button"
app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintTop_toBottomOf="@id/contact_list_button"
app:layout_constraintVertical_chainStyle="packed"/>
<Button
android:id="@+id/link_contacts_button"
android:layout_width="wrap_content"
@ -25,7 +36,7 @@
app:layout_constraintBottom_toTopOf="@id/unlink_contact_button"
app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintTop_toBottomOf="@id/contact_list_button" />
app:layout_constraintTop_toBottomOf="@id/contact_lookup_button" />
<Button
android:id="@+id/unlink_contact_button"

Wyświetl plik

@ -1,6 +1,5 @@
<?xml version="1.0" encoding="utf-8"?>
<adaptive-icon
xmlns:android="http://schemas.android.com/apk/res/android">
<background android:drawable="@drawable/ic_launcher_background" />
<foreground android:drawable="@drawable/ic_launcher_foreground" />
<adaptive-icon xmlns:android="http://schemas.android.com/apk/res/android">
<background android:drawable="@color/ic_launcher_background"/>
<foreground android:drawable="@drawable/ic_launcher_foreground"/>
</adaptive-icon>

Wyświetl plik

@ -1,6 +1,5 @@
<?xml version="1.0" encoding="utf-8"?>
<adaptive-icon
xmlns:android="http://schemas.android.com/apk/res/android">
<background android:drawable="@drawable/ic_launcher_background" />
<foreground android:drawable="@drawable/ic_launcher_foreground" />
<adaptive-icon xmlns:android="http://schemas.android.com/apk/res/android">
<background android:drawable="@color/ic_launcher_background"/>
<foreground android:drawable="@drawable/ic_launcher_foreground"/>
</adaptive-icon>

Plik binarny nie jest wyświetlany.

Przed

Szerokość:  |  Wysokość:  |  Rozmiar: 3.5 KiB

Plik binarny nie jest wyświetlany.

Przed

Szerokość:  |  Wysokość:  |  Rozmiar: 5.2 KiB

Plik binarny nie jest wyświetlany.

Przed

Szerokość:  |  Wysokość:  |  Rozmiar: 2.6 KiB

Plik binarny nie jest wyświetlany.

Przed

Szerokość:  |  Wysokość:  |  Rozmiar: 3.3 KiB

Plik binarny nie jest wyświetlany.

Przed

Szerokość:  |  Wysokość:  |  Rozmiar: 4.8 KiB

Plik binarny nie jest wyświetlany.

Przed

Szerokość:  |  Wysokość:  |  Rozmiar: 7.3 KiB

Plik binarny nie jest wyświetlany.

Przed

Szerokość:  |  Wysokość:  |  Rozmiar: 7.7 KiB

Plik binarny nie jest wyświetlany.

Przed

Szerokość:  |  Wysokość:  |  Rozmiar: 12 KiB

Plik binarny nie jest wyświetlany.

Przed

Szerokość:  |  Wysokość:  |  Rozmiar: 10 KiB

Plik binarny nie jest wyświetlany.

Przed

Szerokość:  |  Wysokość:  |  Rozmiar: 16 KiB

Wyświetl plik

@ -0,0 +1,4 @@
<?xml version="1.0" encoding="utf-8"?>
<resources>
<color name="ic_launcher_background">#3A76F0</color>
</resources>

Wyświetl plik

@ -2,8 +2,8 @@
<!-- Base application theme. -->
<style name="Theme.ContactsTest" parent="Theme.MaterialComponents.DayNight.DarkActionBar">
<!-- Primary brand color. -->
<item name="colorPrimary">@color/purple_500</item>
<item name="colorPrimaryVariant">@color/purple_700</item>
<item name="colorPrimary">#2c6bed</item>
<item name="colorPrimaryVariant">#1851b4</item>
<item name="colorOnPrimary">@color/white</item>
<!-- Secondary brand color. -->
<item name="colorSecondary">@color/teal_200</item>

Wyświetl plik

@ -38,7 +38,7 @@ object SystemContactsRepository {
@JvmStatic
fun getAllSystemContacts(context: Context, e164Formatter: (String) -> String): ContactIterator {
val uri = ContactsContract.Data.CONTENT_URI
val projection = SqlUtil.buildArgs(
val projection = arrayOf(
ContactsContract.Data.MIMETYPE,
ContactsContract.CommonDataKinds.Phone.NUMBER,
ContactsContract.CommonDataKinds.Phone.DISPLAY_NAME,
@ -59,6 +59,50 @@ object SystemContactsRepository {
return CursorContactIterator(cursor, e164Formatter)
}
@JvmStatic
fun getContactDetailsByQueries(context: Context, queries: List<String>, e164Formatter: (String) -> String): ContactIterator {
val lookupKeys: MutableSet<String> = mutableSetOf()
for (query in queries) {
val lookupKeyUri: Uri = Uri.withAppendedPath(ContactsContract.Contacts.CONTENT_FILTER_URI, Uri.encode(query))
context.contentResolver.query(lookupKeyUri, arrayOf(ContactsContract.CommonDataKinds.Phone.LOOKUP_KEY), null, null, null).use { cursor ->
while (cursor != null && cursor.moveToNext()) {
val lookup: String? = cursor.requireString(ContactsContract.CommonDataKinds.Phone.LOOKUP_KEY)
if (lookup != null) {
lookupKeys += lookup
}
}
}
}
if (lookupKeys.isEmpty()) {
return EmptyContactIterator()
}
val uri = ContactsContract.Data.CONTENT_URI
val projection = arrayOf(
ContactsContract.Data.MIMETYPE,
ContactsContract.CommonDataKinds.Phone.NUMBER,
ContactsContract.CommonDataKinds.Phone.DISPLAY_NAME,
ContactsContract.CommonDataKinds.Phone.LABEL,
ContactsContract.CommonDataKinds.Phone.PHOTO_URI,
ContactsContract.CommonDataKinds.Phone._ID,
ContactsContract.CommonDataKinds.Phone.LOOKUP_KEY,
ContactsContract.CommonDataKinds.Phone.TYPE,
ContactsContract.CommonDataKinds.StructuredName.GIVEN_NAME,
ContactsContract.CommonDataKinds.StructuredName.FAMILY_NAME
)
val lookupPlaceholder = lookupKeys.map { "?" }.joinToString(separator = ",")
val where = "${ContactsContract.CommonDataKinds.Phone.LOOKUP_KEY} IN ($lookupPlaceholder) AND ${ContactsContract.Data.MIMETYPE} IN (?, ?)"
val args = lookupKeys.toTypedArray() + SqlUtil.buildArgs(ContactsContract.CommonDataKinds.Phone.CONTENT_ITEM_TYPE, ContactsContract.CommonDataKinds.StructuredName.CONTENT_ITEM_TYPE)
val orderBy = "${ContactsContract.CommonDataKinds.Phone.LOOKUP_KEY} ASC, ${ContactsContract.Data.MIMETYPE} DESC, ${ContactsContract.CommonDataKinds.Phone._ID} DESC"
val cursor: Cursor = context.contentResolver.query(uri, projection, where, args, orderBy) ?: return EmptyContactIterator()
return CursorContactIterator(cursor, e164Formatter)
}
/**
* Retrieves all unique display numbers in the system contacts. (By display, we mean not-E164-formatted)
*/