Improve contact sync for individual contacts.
|
@ -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) {
|
||||
|
|
|
@ -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;
|
||||
}
|
||||
}
|
||||
}
|
|
@ -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);
|
||||
}
|
||||
}
|
||||
}
|
|
@ -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);
|
||||
}
|
||||
}
|
|
@ -20,7 +20,11 @@
|
|||
</activity>
|
||||
|
||||
<activity
|
||||
android:name=".ContactsActivity"
|
||||
android:name=".ContactListActivity"
|
||||
android:exported="false" />
|
||||
|
||||
<activity
|
||||
android:name=".ContactLookupActivity"
|
||||
android:exported="false" />
|
||||
|
||||
<service
|
||||
|
|
|
@ -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) }
|
||||
}
|
||||
}
|
|
@ -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.")
|
||||
}
|
||||
}
|
||||
|
|
@ -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())
|
||||
}
|
||||
}
|
||||
}
|
|
@ -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
|
||||
}
|
||||
}
|
|
@ -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
|
||||
}
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
|
@ -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)
|
||||
}
|
||||
}
|
||||
}
|
|
@ -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 {
|
||||
|
|
|
@ -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) }
|
||||
}
|
||||
}
|
||||
}
|
|
@ -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>
|
|
@ -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>
|
|
@ -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>
|
35
contacts/app/src/main/res/layout/activity_contact_lookup.xml
Normal file
|
@ -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>
|
|
@ -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"
|
||||
|
|
|
@ -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>
|
|
@ -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>
|
Before Width: | Height: | Size: 3.5 KiB |
Before Width: | Height: | Size: 5.2 KiB |
Before Width: | Height: | Size: 2.6 KiB |
Before Width: | Height: | Size: 3.3 KiB |
Before Width: | Height: | Size: 4.8 KiB |
Before Width: | Height: | Size: 7.3 KiB |
Before Width: | Height: | Size: 7.7 KiB |
Before Width: | Height: | Size: 12 KiB |
Before Width: | Height: | Size: 10 KiB |
Before Width: | Height: | Size: 16 KiB |
|
@ -0,0 +1,4 @@
|
|||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<resources>
|
||||
<color name="ic_launcher_background">#3A76F0</color>
|
||||
</resources>
|
|
@ -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>
|
||||
|
|
|
@ -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)
|
||||
*/
|
||||
|
|