From 54782853627523a193bb69f56dc47cc79a51a3a2 Mon Sep 17 00:00:00 2001 From: Greyson Parrelli Date: Fri, 22 Apr 2022 07:47:49 -0400 Subject: [PATCH] Improve contact sync for individual contacts. --- .../contacts/sync/ContactDiscovery.kt | 82 ++++++--- .../contacts/sync/ContactHolder.java | 54 ------ .../contacts/sync/PhoneNumberRecord.java | 99 ---------- .../contacts/sync/StructuredNameRecord.java | 27 --- contacts/app/src/main/AndroidManifest.xml | 6 +- .../contactstest/ContactListActivity.kt | 31 ++++ ...tsViewModel.kt => ContactListViewModel.kt} | 11 +- .../contactstest/ContactLookupActivity.kt | 40 ++++ .../contactstest/ContactLookupViewModel.kt | 57 ++++++ .../signal/contactstest/ContactsActivity.kt | 117 ------------ .../signal/contactstest/ContactsAdapter.kt | 50 +++++ .../org/signal/contactstest/MainActivity.kt | 16 +- .../org/signal/contactstest/PhoneAdapter.kt | 53 ++++++ .../drawable-v24/ic_launcher_foreground.xml | 31 ---- .../res/drawable/ic_launcher_background.xml | 171 ------------------ .../res/drawable/ic_launcher_foreground.xml | 15 ++ ...contacts.xml => activity_contact_list.xml} | 0 .../res/layout/activity_contact_lookup.xml | 35 ++++ .../app/src/main/res/layout/activity_main.xml | 15 +- .../res/mipmap-anydpi-v26/ic_launcher.xml | 7 +- .../mipmap-anydpi-v26/ic_launcher_round.xml | 7 +- .../src/main/res/mipmap-hdpi/ic_launcher.png | Bin 3593 -> 0 bytes .../res/mipmap-hdpi/ic_launcher_round.png | Bin 5339 -> 0 bytes .../src/main/res/mipmap-mdpi/ic_launcher.png | Bin 2636 -> 0 bytes .../res/mipmap-mdpi/ic_launcher_round.png | Bin 3388 -> 0 bytes .../src/main/res/mipmap-xhdpi/ic_launcher.png | Bin 4926 -> 0 bytes .../res/mipmap-xhdpi/ic_launcher_round.png | Bin 7472 -> 0 bytes .../main/res/mipmap-xxhdpi/ic_launcher.png | Bin 7909 -> 0 bytes .../res/mipmap-xxhdpi/ic_launcher_round.png | Bin 11873 -> 0 bytes .../main/res/mipmap-xxxhdpi/ic_launcher.png | Bin 10652 -> 0 bytes .../res/mipmap-xxxhdpi/ic_launcher_round.png | Bin 16570 -> 0 bytes .../res/values/ic_launcher_background.xml | 4 + contacts/app/src/main/res/values/themes.xml | 4 +- .../contacts/SystemContactsRepository.kt | 46 ++++- 34 files changed, 431 insertions(+), 547 deletions(-) delete mode 100644 app/src/main/java/org/thoughtcrime/securesms/contacts/sync/ContactHolder.java delete mode 100644 app/src/main/java/org/thoughtcrime/securesms/contacts/sync/PhoneNumberRecord.java delete mode 100644 app/src/main/java/org/thoughtcrime/securesms/contacts/sync/StructuredNameRecord.java create mode 100644 contacts/app/src/main/java/org/signal/contactstest/ContactListActivity.kt rename contacts/app/src/main/java/org/signal/contactstest/{ContactsViewModel.kt => ContactListViewModel.kt} (76%) create mode 100644 contacts/app/src/main/java/org/signal/contactstest/ContactLookupActivity.kt create mode 100644 contacts/app/src/main/java/org/signal/contactstest/ContactLookupViewModel.kt delete mode 100644 contacts/app/src/main/java/org/signal/contactstest/ContactsActivity.kt create mode 100644 contacts/app/src/main/java/org/signal/contactstest/ContactsAdapter.kt create mode 100644 contacts/app/src/main/java/org/signal/contactstest/PhoneAdapter.kt delete mode 100644 contacts/app/src/main/res/drawable-v24/ic_launcher_foreground.xml delete mode 100644 contacts/app/src/main/res/drawable/ic_launcher_background.xml create mode 100644 contacts/app/src/main/res/drawable/ic_launcher_foreground.xml rename contacts/app/src/main/res/layout/{activity_contacts.xml => activity_contact_list.xml} (100%) create mode 100644 contacts/app/src/main/res/layout/activity_contact_lookup.xml delete mode 100644 contacts/app/src/main/res/mipmap-hdpi/ic_launcher.png delete mode 100644 contacts/app/src/main/res/mipmap-hdpi/ic_launcher_round.png delete mode 100644 contacts/app/src/main/res/mipmap-mdpi/ic_launcher.png delete mode 100644 contacts/app/src/main/res/mipmap-mdpi/ic_launcher_round.png delete mode 100644 contacts/app/src/main/res/mipmap-xhdpi/ic_launcher.png delete mode 100644 contacts/app/src/main/res/mipmap-xhdpi/ic_launcher_round.png delete mode 100644 contacts/app/src/main/res/mipmap-xxhdpi/ic_launcher.png delete mode 100644 contacts/app/src/main/res/mipmap-xxhdpi/ic_launcher_round.png delete mode 100644 contacts/app/src/main/res/mipmap-xxxhdpi/ic_launcher.png delete mode 100644 contacts/app/src/main/res/mipmap-xxxhdpi/ic_launcher_round.png create mode 100644 contacts/app/src/main/res/values/ic_launcher_background.xml diff --git a/app/src/main/java/org/thoughtcrime/securesms/contacts/sync/ContactDiscovery.kt b/app/src/main/java/org/thoughtcrime/securesms/contacts/sync/ContactDiscovery.kt index 511ea8bb73..8d2a88573c 100644 --- a/app/src/main/java/org/thoughtcrime/securesms/contacts/sync/ContactDiscovery.kt +++ b/app/src/main/java/org/thoughtcrime/securesms/contacts/sync/ContactDiscovery.kt @@ -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 = 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) { + private fun syncRecipientsWithSystemContacts( + context: Context, + rewrites: Map, + 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) { diff --git a/app/src/main/java/org/thoughtcrime/securesms/contacts/sync/ContactHolder.java b/app/src/main/java/org/thoughtcrime/securesms/contacts/sync/ContactHolder.java deleted file mode 100644 index d869932ca4..0000000000 --- a/app/src/main/java/org/thoughtcrime/securesms/contacts/sync/ContactHolder.java +++ /dev/null @@ -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 phoneNumberRecords = new LinkedList<>(); - - private StructuredNameRecord structuredNameRecord; - - public void addPhoneNumberRecords(@NonNull List 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; - } - } -} diff --git a/app/src/main/java/org/thoughtcrime/securesms/contacts/sync/PhoneNumberRecord.java b/app/src/main/java/org/thoughtcrime/securesms/contacts/sync/PhoneNumberRecord.java deleted file mode 100644 index 3edb01bad0..0000000000 --- a/app/src/main/java/org/thoughtcrime/securesms/contacts/sync/PhoneNumberRecord.java +++ /dev/null @@ -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); - } - } -} diff --git a/app/src/main/java/org/thoughtcrime/securesms/contacts/sync/StructuredNameRecord.java b/app/src/main/java/org/thoughtcrime/securesms/contacts/sync/StructuredNameRecord.java deleted file mode 100644 index a7e51b94f4..0000000000 --- a/app/src/main/java/org/thoughtcrime/securesms/contacts/sync/StructuredNameRecord.java +++ /dev/null @@ -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); - } -} diff --git a/contacts/app/src/main/AndroidManifest.xml b/contacts/app/src/main/AndroidManifest.xml index 29356491c8..f4ea47e829 100644 --- a/contacts/app/src/main/AndroidManifest.xml +++ b/contacts/app/src/main/AndroidManifest.xml @@ -20,7 +20,11 @@ + + + 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) } + } +} diff --git a/contacts/app/src/main/java/org/signal/contactstest/ContactsViewModel.kt b/contacts/app/src/main/java/org/signal/contactstest/ContactListViewModel.kt similarity index 76% rename from contacts/app/src/main/java/org/signal/contactstest/ContactsViewModel.kt rename to contacts/app/src/main/java/org/signal/contactstest/ContactListViewModel.kt index a49fe9379e..9f356b1e9f 100644 --- a/contacts/app/src/main/java/org/signal/contactstest/ContactsViewModel.kt +++ b/contacts/app/src/main/java/org/signal/contactstest/ContactListViewModel.kt @@ -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> = MutableLiveData() @@ -30,16 +31,20 @@ class ContactsViewModel(application: Application) : AndroidViewModel(application accountDisplayName = "Test" ) + val startTime: Long = System.currentTimeMillis() + if (account != null) { val contactList: List = 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.") } } diff --git a/contacts/app/src/main/java/org/signal/contactstest/ContactLookupActivity.kt b/contacts/app/src/main/java/org/signal/contactstest/ContactLookupActivity.kt new file mode 100644 index 0000000000..47de0f19e6 --- /dev/null +++ b/contacts/app/src/main/java/org/signal/contactstest/ContactLookupActivity.kt @@ -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()) + } + } +} diff --git a/contacts/app/src/main/java/org/signal/contactstest/ContactLookupViewModel.kt b/contacts/app/src/main/java/org/signal/contactstest/ContactLookupViewModel.kt new file mode 100644 index 0000000000..06555097dc --- /dev/null +++ b/contacts/app/src/main/java/org/signal/contactstest/ContactLookupViewModel.kt @@ -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> = MutableLiveData() + + val contacts: LiveData> + 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 = 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 { + val list: MutableList = mutableListOf() + forEach { list += it } + return list + } +} diff --git a/contacts/app/src/main/java/org/signal/contactstest/ContactsActivity.kt b/contacts/app/src/main/java/org/signal/contactstest/ContactsActivity.kt deleted file mode 100644 index 3dec959687..0000000000 --- a/contacts/app/src/main/java/org/signal/contactstest/ContactsActivity.kt +++ /dev/null @@ -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(object : DiffUtil.ItemCallback() { - 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(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(object : DiffUtil.ItemCallback() { - 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 - } - ) - } - } - } -} diff --git a/contacts/app/src/main/java/org/signal/contactstest/ContactsAdapter.kt b/contacts/app/src/main/java/org/signal/contactstest/ContactsAdapter.kt new file mode 100644 index 0000000000..aacde2d09d --- /dev/null +++ b/contacts/app/src/main/java/org/signal/contactstest/ContactsAdapter.kt @@ -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(object : DiffUtil.ItemCallback() { + 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(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) + } + } +} diff --git a/contacts/app/src/main/java/org/signal/contactstest/MainActivity.kt b/contacts/app/src/main/java/org/signal/contactstest/MainActivity.kt index 89fe336a58..10b7be3e3d 100644 --- a/contacts/app/src/main/java/org/signal/contactstest/MainActivity.kt +++ b/contacts/app/src/main/java/org/signal/contactstest/MainActivity.kt @@ -30,8 +30,15 @@ class MainActivity : AppCompatActivity() { findViewById