Improve contact sync for individual contacts.
|
@ -8,10 +8,8 @@ import android.os.RemoteException
|
||||||
import android.text.TextUtils
|
import android.text.TextUtils
|
||||||
import androidx.annotation.WorkerThread
|
import androidx.annotation.WorkerThread
|
||||||
import org.signal.contacts.ContactLinkConfiguration
|
import org.signal.contacts.ContactLinkConfiguration
|
||||||
import org.signal.contacts.SystemContactsRepository.addMessageAndCallLinksToContacts
|
import org.signal.contacts.SystemContactsRepository
|
||||||
import org.signal.contacts.SystemContactsRepository.getAllSystemContacts
|
import org.signal.core.util.StringUtil
|
||||||
import org.signal.contacts.SystemContactsRepository.getOrCreateSystemAccount
|
|
||||||
import org.signal.contacts.SystemContactsRepository.removeDeletedRawContactsForAccount
|
|
||||||
import org.signal.core.util.logging.Log
|
import org.signal.core.util.logging.Log
|
||||||
import org.thoughtcrime.securesms.BuildConfig
|
import org.thoughtcrime.securesms.BuildConfig
|
||||||
import org.thoughtcrime.securesms.R
|
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.notifications.NotificationChannels
|
||||||
import org.thoughtcrime.securesms.permissions.Permissions
|
import org.thoughtcrime.securesms.permissions.Permissions
|
||||||
import org.thoughtcrime.securesms.phonenumbers.PhoneNumberFormatter
|
import org.thoughtcrime.securesms.phonenumbers.PhoneNumberFormatter
|
||||||
|
import org.thoughtcrime.securesms.profiles.ProfileName
|
||||||
import org.thoughtcrime.securesms.recipients.Recipient
|
import org.thoughtcrime.securesms.recipients.Recipient
|
||||||
import org.thoughtcrime.securesms.recipients.RecipientId
|
import org.thoughtcrime.securesms.recipients.RecipientId
|
||||||
import org.thoughtcrime.securesms.registration.RegistrationUtil
|
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 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 CALL_MIMETYPE = "vnd.android.cursor.item/vnd.org.thoughtcrime.securesms.call"
|
||||||
private const val CONTACT_TAG = "__TS"
|
private const val CONTACT_TAG = "__TS"
|
||||||
|
private const val FULL_SYSTEM_CONTACT_SYNC_THRESHOLD = 3
|
||||||
|
|
||||||
@JvmStatic
|
@JvmStatic
|
||||||
@Throws(IOException::class)
|
@Throws(IOException::class)
|
||||||
|
@ -133,6 +133,10 @@ object ContactDiscovery {
|
||||||
syncRecipientsWithSystemContacts(context, emptyMap())
|
syncRecipientsWithSystemContacts(context, emptyMap())
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private fun phoneNumberFormatter(context: Context): (String) -> String {
|
||||||
|
return { PhoneNumberFormatter.get(context).format(it) }
|
||||||
|
}
|
||||||
|
|
||||||
private fun refreshRecipients(
|
private fun refreshRecipients(
|
||||||
context: Context,
|
context: Context,
|
||||||
descriptor: String,
|
descriptor: String,
|
||||||
|
@ -152,7 +156,23 @@ object ContactDiscovery {
|
||||||
addSystemContactLinks(context, result.registeredIds, removeSystemContactLinksIfMissing)
|
addSystemContactLinks(context, result.registeredIds, removeSystemContactLinksIfMissing)
|
||||||
stopwatch.split("contact-links")
|
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")
|
stopwatch.split("contact-sync")
|
||||||
|
|
||||||
if (TextSecurePreferences.hasSuccessfullyRetrievedDirectory(context) && notifyOfNewUsers) {
|
if (TextSecurePreferences.hasSuccessfullyRetrievedDirectory(context) && notifyOfNewUsers) {
|
||||||
|
@ -227,7 +247,7 @@ object ContactDiscovery {
|
||||||
|
|
||||||
val stopwatch = Stopwatch("contact-links")
|
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) {
|
if (account == null) {
|
||||||
Log.w(TAG, "[addSystemContactLinks] Failed to create an account!")
|
Log.w(TAG, "[addSystemContactLinks] Failed to create an account!")
|
||||||
return
|
return
|
||||||
|
@ -237,10 +257,10 @@ object ContactDiscovery {
|
||||||
val registeredE164s: Set<String> = SignalDatabase.recipients.getE164sForIds(registeredIds)
|
val registeredE164s: Set<String> = SignalDatabase.recipients.getE164sForIds(registeredIds)
|
||||||
stopwatch.split("fetch-e164s")
|
stopwatch.split("fetch-e164s")
|
||||||
|
|
||||||
removeDeletedRawContactsForAccount(context, account)
|
SystemContactsRepository.removeDeletedRawContactsForAccount(context, account)
|
||||||
stopwatch.split("delete-stragglers")
|
stopwatch.split("delete-stragglers")
|
||||||
|
|
||||||
addMessageAndCallLinksToContacts(
|
SystemContactsRepository.addMessageAndCallLinksToContacts(
|
||||||
context = context,
|
context = context,
|
||||||
config = buildContactLinkConfiguration(context, account),
|
config = buildContactLinkConfiguration(context, account),
|
||||||
targetE164s = registeredE164s,
|
targetE164s = registeredE164s,
|
||||||
|
@ -259,30 +279,38 @@ object ContactDiscovery {
|
||||||
/**
|
/**
|
||||||
* Synchronizes info from the system contacts (name, avatar, etc)
|
* 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()
|
val handle = SignalDatabase.recipients.beginBulkSystemContactUpdate()
|
||||||
try {
|
try {
|
||||||
getAllSystemContacts(context) { PhoneNumberFormatter.get(context).format(it) }.use { iterator ->
|
contactsProvider().use { iterator ->
|
||||||
while (iterator.hasNext()) {
|
while (iterator.hasNext()) {
|
||||||
val details = iterator.next()
|
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 {
|
for (phoneDetails in details.numbers) {
|
||||||
setStructuredNameRecord(name)
|
val realNumber: String = Util.getFirstNonEmpty(rewrites[phoneDetails.number], phoneDetails.number)
|
||||||
addPhoneNumberRecords(phones)
|
|
||||||
}.commit(handle)
|
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) {
|
} 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>
|
||||||
|
|
||||||
<activity
|
<activity
|
||||||
android:name=".ContactsActivity"
|
android:name=".ContactListActivity"
|
||||||
|
android:exported="false" />
|
||||||
|
|
||||||
|
<activity
|
||||||
|
android:name=".ContactLookupActivity"
|
||||||
android:exported="false" />
|
android:exported="false" />
|
||||||
|
|
||||||
<service
|
<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.accounts.Account
|
||||||
import android.app.Application
|
import android.app.Application
|
||||||
|
import android.telephony.PhoneNumberUtils
|
||||||
import androidx.lifecycle.AndroidViewModel
|
import androidx.lifecycle.AndroidViewModel
|
||||||
import androidx.lifecycle.LiveData
|
import androidx.lifecycle.LiveData
|
||||||
import androidx.lifecycle.MutableLiveData
|
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.concurrent.SignalExecutors
|
||||||
import org.signal.core.util.logging.Log
|
import org.signal.core.util.logging.Log
|
||||||
|
|
||||||
class ContactsViewModel(application: Application) : AndroidViewModel(application) {
|
class ContactListViewModel(application: Application) : AndroidViewModel(application) {
|
||||||
|
|
||||||
companion object {
|
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()
|
private val _contacts: MutableLiveData<List<ContactDetails>> = MutableLiveData()
|
||||||
|
@ -30,16 +31,20 @@ class ContactsViewModel(application: Application) : AndroidViewModel(application
|
||||||
accountDisplayName = "Test"
|
accountDisplayName = "Test"
|
||||||
)
|
)
|
||||||
|
|
||||||
|
val startTime: Long = System.currentTimeMillis()
|
||||||
|
|
||||||
if (account != null) {
|
if (account != null) {
|
||||||
val contactList: List<ContactDetails> = SystemContactsRepository.getAllSystemContacts(
|
val contactList: List<ContactDetails> = SystemContactsRepository.getAllSystemContacts(
|
||||||
context = application,
|
context = application,
|
||||||
e164Formatter = { number -> number }
|
e164Formatter = { number -> PhoneNumberUtils.formatNumberToE164(number, "US") ?: number }
|
||||||
).use { it.toList() }
|
).use { it.toList() }
|
||||||
|
|
||||||
_contacts.postValue(contactList)
|
_contacts.postValue(contactList)
|
||||||
} else {
|
} else {
|
||||||
Log.w(TAG, "Failed to create an account!")
|
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 ->
|
findViewById<Button>(R.id.contact_list_button).setOnClickListener { v ->
|
||||||
if (hasPermission(Manifest.permission.READ_CONTACTS) && hasPermission(Manifest.permission.WRITE_CONTACTS)) {
|
if (hasPermission(Manifest.permission.READ_CONTACTS) && hasPermission(Manifest.permission.WRITE_CONTACTS)) {
|
||||||
startActivity(Intent(this, ContactsActivity::class.java))
|
startActivity(Intent(this, ContactListActivity::class.java))
|
||||||
finish()
|
} 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 {
|
} else {
|
||||||
ActivityCompat.requestPermissions(this, arrayOf(Manifest.permission.READ_CONTACTS, Manifest.permission.WRITE_CONTACTS), PERMISSION_CODE)
|
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) {
|
override fun onRequestPermissionsResult(requestCode: Int, permissions: Array<out String>, grantResults: IntArray) {
|
||||||
if (requestCode == PERMISSION_CODE) {
|
if (requestCode == PERMISSION_CODE) {
|
||||||
if (grantResults.isNotEmpty() && grantResults.all { it == PackageManager.PERMISSION_GRANTED }) {
|
if (grantResults.isNotEmpty() && grantResults.all { it == PackageManager.PERMISSION_GRANTED }) {
|
||||||
startActivity(Intent(this, ContactsActivity::class.java))
|
startActivity(Intent(this, ContactListActivity::class.java))
|
||||||
finish()
|
|
||||||
} else {
|
} else {
|
||||||
Toast.makeText(this, "You must provide permissions to continue.", Toast.LENGTH_SHORT).show()
|
Toast.makeText(this, "You must provide permissions to continue.", Toast.LENGTH_SHORT).show()
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
super.onRequestPermissionsResult(requestCode, permissions, grantResults)
|
||||||
}
|
}
|
||||||
|
|
||||||
private fun hasPermission(permission: String): Boolean {
|
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>
|
|
@ -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_width="wrap_content"
|
||||||
android:layout_height="wrap_content"
|
android:layout_height="wrap_content"
|
||||||
android:text="Contact List"
|
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_constraintEnd_toEndOf="parent"
|
||||||
app:layout_constraintStart_toStartOf="parent"
|
app:layout_constraintStart_toStartOf="parent"
|
||||||
app:layout_constraintTop_toTopOf="parent"
|
app:layout_constraintTop_toTopOf="parent"
|
||||||
app:layout_constraintVertical_chainStyle="packed"/>
|
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
|
<Button
|
||||||
android:id="@+id/link_contacts_button"
|
android:id="@+id/link_contacts_button"
|
||||||
android:layout_width="wrap_content"
|
android:layout_width="wrap_content"
|
||||||
|
@ -25,7 +36,7 @@
|
||||||
app:layout_constraintBottom_toTopOf="@id/unlink_contact_button"
|
app:layout_constraintBottom_toTopOf="@id/unlink_contact_button"
|
||||||
app:layout_constraintEnd_toEndOf="parent"
|
app:layout_constraintEnd_toEndOf="parent"
|
||||||
app:layout_constraintStart_toStartOf="parent"
|
app:layout_constraintStart_toStartOf="parent"
|
||||||
app:layout_constraintTop_toBottomOf="@id/contact_list_button" />
|
app:layout_constraintTop_toBottomOf="@id/contact_lookup_button" />
|
||||||
|
|
||||||
<Button
|
<Button
|
||||||
android:id="@+id/unlink_contact_button"
|
android:id="@+id/unlink_contact_button"
|
||||||
|
|
|
@ -1,6 +1,5 @@
|
||||||
<?xml version="1.0" encoding="utf-8"?>
|
<?xml version="1.0" encoding="utf-8"?>
|
||||||
<adaptive-icon
|
<adaptive-icon xmlns:android="http://schemas.android.com/apk/res/android">
|
||||||
xmlns:android="http://schemas.android.com/apk/res/android">
|
<background android:drawable="@color/ic_launcher_background"/>
|
||||||
<background android:drawable="@drawable/ic_launcher_background" />
|
<foreground android:drawable="@drawable/ic_launcher_foreground"/>
|
||||||
<foreground android:drawable="@drawable/ic_launcher_foreground" />
|
|
||||||
</adaptive-icon>
|
</adaptive-icon>
|
|
@ -1,6 +1,5 @@
|
||||||
<?xml version="1.0" encoding="utf-8"?>
|
<?xml version="1.0" encoding="utf-8"?>
|
||||||
<adaptive-icon
|
<adaptive-icon xmlns:android="http://schemas.android.com/apk/res/android">
|
||||||
xmlns:android="http://schemas.android.com/apk/res/android">
|
<background android:drawable="@color/ic_launcher_background"/>
|
||||||
<background android:drawable="@drawable/ic_launcher_background" />
|
<foreground android:drawable="@drawable/ic_launcher_foreground"/>
|
||||||
<foreground android:drawable="@drawable/ic_launcher_foreground" />
|
|
||||||
</adaptive-icon>
|
</adaptive-icon>
|
Przed Szerokość: | Wysokość: | Rozmiar: 3.5 KiB |
Przed Szerokość: | Wysokość: | Rozmiar: 5.2 KiB |
Przed Szerokość: | Wysokość: | Rozmiar: 2.6 KiB |
Przed Szerokość: | Wysokość: | Rozmiar: 3.3 KiB |
Przed Szerokość: | Wysokość: | Rozmiar: 4.8 KiB |
Przed Szerokość: | Wysokość: | Rozmiar: 7.3 KiB |
Przed Szerokość: | Wysokość: | Rozmiar: 7.7 KiB |
Przed Szerokość: | Wysokość: | Rozmiar: 12 KiB |
Przed Szerokość: | Wysokość: | Rozmiar: 10 KiB |
Przed Szerokość: | Wysokość: | Rozmiar: 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. -->
|
<!-- Base application theme. -->
|
||||||
<style name="Theme.ContactsTest" parent="Theme.MaterialComponents.DayNight.DarkActionBar">
|
<style name="Theme.ContactsTest" parent="Theme.MaterialComponents.DayNight.DarkActionBar">
|
||||||
<!-- Primary brand color. -->
|
<!-- Primary brand color. -->
|
||||||
<item name="colorPrimary">@color/purple_500</item>
|
<item name="colorPrimary">#2c6bed</item>
|
||||||
<item name="colorPrimaryVariant">@color/purple_700</item>
|
<item name="colorPrimaryVariant">#1851b4</item>
|
||||||
<item name="colorOnPrimary">@color/white</item>
|
<item name="colorOnPrimary">@color/white</item>
|
||||||
<!-- Secondary brand color. -->
|
<!-- Secondary brand color. -->
|
||||||
<item name="colorSecondary">@color/teal_200</item>
|
<item name="colorSecondary">@color/teal_200</item>
|
||||||
|
|
|
@ -38,7 +38,7 @@ object SystemContactsRepository {
|
||||||
@JvmStatic
|
@JvmStatic
|
||||||
fun getAllSystemContacts(context: Context, e164Formatter: (String) -> String): ContactIterator {
|
fun getAllSystemContacts(context: Context, e164Formatter: (String) -> String): ContactIterator {
|
||||||
val uri = ContactsContract.Data.CONTENT_URI
|
val uri = ContactsContract.Data.CONTENT_URI
|
||||||
val projection = SqlUtil.buildArgs(
|
val projection = arrayOf(
|
||||||
ContactsContract.Data.MIMETYPE,
|
ContactsContract.Data.MIMETYPE,
|
||||||
ContactsContract.CommonDataKinds.Phone.NUMBER,
|
ContactsContract.CommonDataKinds.Phone.NUMBER,
|
||||||
ContactsContract.CommonDataKinds.Phone.DISPLAY_NAME,
|
ContactsContract.CommonDataKinds.Phone.DISPLAY_NAME,
|
||||||
|
@ -59,6 +59,50 @@ object SystemContactsRepository {
|
||||||
return CursorContactIterator(cursor, e164Formatter)
|
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)
|
* Retrieves all unique display numbers in the system contacts. (By display, we mean not-E164-formatted)
|
||||||
*/
|
*/
|
||||||
|
|