Source added

This commit is contained in:
Fr4nz D13trich 2025-11-20 09:26:33 +01:00
parent b2864b500e
commit ba28ca859e
8352 changed files with 1487182 additions and 1 deletions

16
contacts/app/build.gradle Normal file
View file

@ -0,0 +1,16 @@
plugins {
id 'signal-sample-app'
alias(libs.plugins.compose.compiler)
}
android {
namespace 'org.signal.contactstest'
defaultConfig {
applicationId "org.signal.contactstest"
}
}
dependencies {
implementation project(':contacts')
}

View file

@ -0,0 +1,56 @@
<?xml version="1.0" encoding="utf-8"?>
<manifest xmlns:android="http://schemas.android.com/apk/res/android">
<application
android:allowBackup="true"
android:icon="@mipmap/ic_launcher"
android:label="@string/app_name"
android:roundIcon="@mipmap/ic_launcher_round"
android:supportsRtl="true"
android:theme="@style/Theme.ContactsTest">
<activity
android:name=".MainActivity"
android:exported="true">
<intent-filter>
<action android:name="android.intent.action.MAIN" />
<category android:name="android.intent.category.LAUNCHER" />
</intent-filter>
</activity>
<activity
android:name=".ContactListActivity"
android:exported="false" />
<activity
android:name=".ContactLookupActivity"
android:exported="false" />
<service
android:name=".AccountAuthenticatorService"
android:exported="true">
<intent-filter>
<action android:name="android.accounts.AccountAuthenticator" />
</intent-filter>
<meta-data
android:name="android.accounts.AccountAuthenticator"
android:resource="@xml/authenticator" />
</service>
<service
android:name=".ContactsSyncAdapterService"
android:exported="true">
<intent-filter>
<action android:name="android.content.SyncAdapter" />
</intent-filter>
<meta-data
android:name="android.content.SyncAdapter"
android:resource="@xml/syncadapter" />
<meta-data
android:name="android.provider.CONTACTS_STRUCTURE"
android:resource="@xml/contactsformat" />
</service>
</application>
</manifest>

View file

@ -0,0 +1,63 @@
package org.signal.contactstest
import android.accounts.AbstractAccountAuthenticator
import android.accounts.Account
import android.accounts.AccountAuthenticatorResponse
import android.accounts.AccountManager
import android.app.Service
import android.content.Context
import android.content.Intent
import android.os.Bundle
import android.os.IBinder
class AccountAuthenticatorService : Service() {
companion object {
private var accountAuthenticator: AccountAuthenticatorImpl? = null
}
override fun onBind(intent: Intent): IBinder? {
return if (intent.action == AccountManager.ACTION_AUTHENTICATOR_INTENT) {
getOrCreateAuthenticator().iBinder
} else {
null
}
}
@Synchronized
private fun getOrCreateAuthenticator(): AccountAuthenticatorImpl {
if (accountAuthenticator == null) {
accountAuthenticator = AccountAuthenticatorImpl(this)
}
return accountAuthenticator as AccountAuthenticatorImpl
}
private class AccountAuthenticatorImpl(context: Context) : AbstractAccountAuthenticator(context) {
override fun addAccount(response: AccountAuthenticatorResponse, accountType: String, authTokenType: String, requiredFeatures: Array<String>, options: Bundle): Bundle? {
return null
}
override fun confirmCredentials(response: AccountAuthenticatorResponse, account: Account, options: Bundle): Bundle? {
return null
}
override fun editProperties(response: AccountAuthenticatorResponse, accountType: String): Bundle? {
return null
}
override fun getAuthToken(response: AccountAuthenticatorResponse, account: Account, authTokenType: String, options: Bundle): Bundle? {
return null
}
override fun getAuthTokenLabel(authTokenType: String): String? {
return null
}
override fun hasFeatures(response: AccountAuthenticatorResponse, account: Account, features: Array<String>): Bundle? {
return null
}
override fun updateCredentials(response: AccountAuthenticatorResponse, account: Account, authTokenType: String, options: Bundle): Bundle? {
return null
}
}
}

View file

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

View file

@ -0,0 +1,56 @@
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 ContactListViewModel(application: Application) : AndroidViewModel(application) {
companion object {
private val TAG = Log.tag(ContactListViewModel::class.java)
}
private val _contacts: MutableLiveData<List<ContactDetails>> = MutableLiveData()
val contacts: LiveData<List<ContactDetails>>
get() = _contacts
init {
SignalExecutors.BOUNDED.execute {
val account: Account? = SystemContactsRepository.getOrCreateSystemAccount(
context = application,
applicationId = BuildConfig.APPLICATION_ID,
accountDisplayName = "Test"
)
val startTime: Long = System.currentTimeMillis()
if (account != null) {
val contactList: List<ContactDetails> = SystemContactsRepository.getAllSystemContacts(
context = application,
e164Formatter = { number -> PhoneNumberUtils.formatNumberToE164(number, "US") ?: number }
).use { it.toList().sortedBy { c -> c.givenName } }
_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
}
}

View file

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

View file

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

View file

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

View file

@ -0,0 +1,33 @@
package org.signal.contactstest
import android.accounts.Account
import android.content.AbstractThreadedSyncAdapter
import android.content.ContentProviderClient
import android.content.Context
import android.content.SyncResult
import android.os.Bundle
import org.signal.core.util.logging.Log
class ContactsSyncAdapter(context: Context?, autoInitialize: Boolean) : AbstractThreadedSyncAdapter(context, autoInitialize) {
override fun onPerformSync(
account: Account,
extras: Bundle,
authority: String,
provider: ContentProviderClient,
syncResult: SyncResult
) {
Log.i(TAG, "onPerformSync()")
}
override fun onSyncCanceled() {
Log.w(TAG, "onSyncCanceled()")
}
override fun onSyncCanceled(thread: Thread) {
Log.w(TAG, "onSyncCanceled($thread)")
}
companion object {
private val TAG = Log.tag(ContactsSyncAdapter::class.java)
}
}

View file

@ -0,0 +1,22 @@
package org.signal.contactstest
import android.app.Service
import android.content.Intent
import android.os.IBinder
class ContactsSyncAdapterService : Service() {
@Synchronized
override fun onCreate() {
if (syncAdapter == null) {
syncAdapter = ContactsSyncAdapter(this, true)
}
}
override fun onBind(intent: Intent): IBinder? {
return syncAdapter!!.syncAdapterBinder
}
companion object {
private var syncAdapter: ContactsSyncAdapter? = null
}
}

View file

@ -0,0 +1,131 @@
package org.signal.contactstest
import android.Manifest
import android.accounts.Account
import android.content.Intent
import android.content.pm.PackageManager
import android.os.Bundle
import android.telephony.PhoneNumberUtils
import android.widget.Button
import android.widget.Toast
import androidx.appcompat.app.AppCompatActivity
import androidx.core.app.ActivityCompat
import androidx.core.content.ContextCompat
import org.signal.contacts.ContactLinkConfiguration
import org.signal.contacts.SystemContactsRepository
import org.signal.core.util.concurrent.SimpleTask
import org.signal.core.util.logging.Log
class MainActivity : AppCompatActivity() {
companion object {
private val TAG = Log.tag(MainActivity::class.java)
private const val PERMISSION_CODE = 7
}
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
setContentView(R.layout.activity_main)
findViewById<Button>(R.id.contact_list_button).setOnClickListener { v ->
if (hasPermission(Manifest.permission.READ_CONTACTS) && hasPermission(Manifest.permission.WRITE_CONTACTS)) {
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)
}
}
findViewById<Button>(R.id.link_contacts_button).setOnClickListener { v ->
val startTime = System.currentTimeMillis()
if (hasPermission(Manifest.permission.READ_CONTACTS) && hasPermission(Manifest.permission.WRITE_CONTACTS)) {
SimpleTask.run({
val allE164s: Set<String> = SystemContactsRepository.getAllDisplayNumbers(this).map { PhoneNumberUtils.formatNumberToE164(it, "US") }.toSet()
val account: Account = SystemContactsRepository.getOrCreateSystemAccount(this, BuildConfig.APPLICATION_ID, "Contact Test") ?: return@run false
SystemContactsRepository.addMessageAndCallLinksToContacts(
context = this,
config = buildLinkConfig(account),
targetE164s = allE164s,
removeIfMissing = true
)
return@run true
}, { success ->
if (success) {
Toast.makeText(this, "Success! Took ${System.currentTimeMillis() - startTime} ms", Toast.LENGTH_SHORT).show()
} else {
Toast.makeText(this, "Failed to create account!", Toast.LENGTH_SHORT).show()
}
})
} else {
ActivityCompat.requestPermissions(this, arrayOf(Manifest.permission.READ_CONTACTS, Manifest.permission.WRITE_CONTACTS), PERMISSION_CODE)
}
}
findViewById<Button>(R.id.unlink_contact_button).setOnClickListener { v ->
val startTime = System.currentTimeMillis()
if (hasPermission(Manifest.permission.READ_CONTACTS) && hasPermission(Manifest.permission.WRITE_CONTACTS)) {
SimpleTask.run({
val account: Account = SystemContactsRepository.getOrCreateSystemAccount(this, BuildConfig.APPLICATION_ID, "Contact Test") ?: return@run false
SystemContactsRepository.addMessageAndCallLinksToContacts(
context = this,
config = buildLinkConfig(account),
targetE164s = emptySet(),
removeIfMissing = true
)
return@run true
}, { success ->
if (success) {
Toast.makeText(this, "Success! Took ${System.currentTimeMillis() - startTime} ms", Toast.LENGTH_SHORT).show()
} else {
Toast.makeText(this, "Failed to create account!", Toast.LENGTH_SHORT).show()
}
})
} else {
ActivityCompat.requestPermissions(this, arrayOf(Manifest.permission.READ_CONTACTS, Manifest.permission.WRITE_CONTACTS), PERMISSION_CODE)
}
}
}
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, 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 {
return ContextCompat.checkSelfPermission(this, permission) == PackageManager.PERMISSION_GRANTED
}
private fun buildLinkConfig(account: Account): ContactLinkConfiguration {
return ContactLinkConfiguration(
account = account,
appName = "Contact Test",
messagePrompt = { "(Test) Message $it" },
callPrompt = { "(Test) Call $it" },
e164Formatter = { PhoneNumberUtils.formatNumberToE164(it, "US") },
messageMimetype = "vnd.android.cursor.item/vnd.org.signal.contacts.test.message",
callMimetype = "vnd.android.cursor.item/vnd.org.signal.contacts.test.call",
syncTag = "__TEST",
videoCallMimetype = "",
videoCallPrompt = { "" }
)
}
}

View file

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

View file

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

View file

@ -0,0 +1,17 @@
<?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">
<androidx.recyclerview.widget.RecyclerView
android:id="@+id/list"
android:layout_width="match_parent"
android:layout_height="match_parent"
android:clipToPadding="false"
android:clipChildren="false" />
</androidx.constraintlayout.widget.ConstraintLayout>

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

View file

@ -0,0 +1,52 @@
<?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">
<Button
android:id="@+id/contact_list_button"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:text="Contact List"
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"
android:layout_height="wrap_content"
android:text="Link Contacts"
app:layout_constraintBottom_toTopOf="@id/unlink_contact_button"
app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintTop_toBottomOf="@id/contact_lookup_button" />
<Button
android:id="@+id/unlink_contact_button"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:text="Unlink Contacts"
app:layout_constraintBottom_toBottomOf="parent"
app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintTop_toBottomOf="@id/link_contacts_button" />
</androidx.constraintlayout.widget.ConstraintLayout>

View file

@ -0,0 +1,66 @@
<?xml version="1.0" encoding="utf-8"?>
<com.google.android.material.card.MaterialCardView 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="wrap_content"
android:layout_marginBottom="4dp"
android:clipChildren="false"
android:clipToPadding="false"
app:cardCornerRadius="5dp"
app:cardElevation="5dp">
<androidx.constraintlayout.widget.ConstraintLayout
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:padding="4dp">
<ImageView
android:id="@+id/contact_photo"
android:layout_width="40dp"
android:layout_height="40dp"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintTop_toTopOf="parent"
app:layout_constraintBottom_toBottomOf="parent"/>
<TextView
android:id="@+id/display_name"
android:layout_width="0dp"
android:layout_height="wrap_content"
android:layout_marginStart="4dp"
android:layout_marginBottom="2dp"
tools:text="Spider-Man"
app:layout_constraintTop_toTopOf="parent"
app:layout_constraintStart_toEndOf="@id/contact_photo"
app:layout_constraintEnd_toEndOf="parent"/>
<TextView
android:id="@+id/number"
android:layout_width="0dp"
android:layout_height="wrap_content"
tools:text="(111) 222-3333"
app:layout_constraintTop_toBottomOf="@id/display_name"
app:layout_constraintStart_toStartOf="@id/display_name"
app:layout_constraintEnd_toEndOf="parent" />
<TextView
android:id="@+id/type"
android:layout_width="0dp"
android:layout_height="wrap_content"
tools:text="Mobile"
app:layout_constraintTop_toBottomOf="@id/number"
app:layout_constraintStart_toStartOf="@id/number"
app:layout_constraintEnd_toEndOf="parent" />
<com.google.android.material.button.MaterialButton
android:id="@+id/go_button"
android:layout_width="60dp"
android:layout_height="0dp"
android:text="Go"
app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintTop_toTopOf="parent"
app:layout_constraintBottom_toBottomOf="parent" />
</androidx.constraintlayout.widget.ConstraintLayout>
</com.google.android.material.card.MaterialCardView>

View file

@ -0,0 +1,52 @@
<?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="wrap_content"
android:padding="5dp"
android:clipToPadding="false"
android:clipChildren="false">
<com.google.android.material.card.MaterialCardView
android:layout_width="match_parent"
android:layout_height="wrap_content"
app:cardElevation="5dp"
app:cardCornerRadius="5dp"
app:layout_constraintTop_toTopOf="parent"
app:layout_constraintStart_toStartOf="parent">
<LinearLayout
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:orientation="vertical"
android:padding="4dp"
android:clipChildren="false"
android:clipToPadding="false">
<TextView
android:id="@+id/given_name"
android:layout_width="match_parent"
android:layout_height="wrap_content"
tools:text="Spider-Man"/>
<TextView
android:id="@+id/family_name"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:layout_marginBottom="4dp"
tools:text="Spider-Man"/>
<androidx.recyclerview.widget.RecyclerView
android:id="@+id/phone_list"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:clipChildren="false"
android:clipToPadding="false"/>
</LinearLayout>
</com.google.android.material.card.MaterialCardView>
</androidx.constraintlayout.widget.ConstraintLayout>

View file

@ -0,0 +1,5 @@
<?xml version="1.0" encoding="utf-8"?>
<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>

View file

@ -0,0 +1,5 @@
<?xml version="1.0" encoding="utf-8"?>
<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>

View file

@ -0,0 +1,16 @@
<resources xmlns:tools="http://schemas.android.com/tools">
<!-- Base application theme. -->
<style name="Theme.ContactsTest" parent="Theme.MaterialComponents.DayNight.DarkActionBar">
<!-- Primary brand color. -->
<item name="colorPrimary">@color/purple_200</item>
<item name="colorPrimaryVariant">@color/purple_700</item>
<item name="colorOnPrimary">@color/black</item>
<!-- Secondary brand color. -->
<item name="colorSecondary">@color/teal_200</item>
<item name="colorSecondaryVariant">@color/teal_200</item>
<item name="colorOnSecondary">@color/black</item>
<!-- Status bar color. -->
<item name="android:statusBarColor" tools:targetApi="l">?attr/colorPrimaryVariant</item>
<!-- Customize your theme here. -->
</style>
</resources>

View file

@ -0,0 +1,10 @@
<?xml version="1.0" encoding="utf-8"?>
<resources>
<color name="purple_200">#FFBB86FC</color>
<color name="purple_500">#FF6200EE</color>
<color name="purple_700">#FF3700B3</color>
<color name="teal_200">#FF03DAC5</color>
<color name="teal_700">#FF018786</color>
<color name="black">#FF000000</color>
<color name="white">#FFFFFFFF</color>
</resources>

View file

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

View file

@ -0,0 +1,3 @@
<resources>
<string name="app_name">ContactsTest</string>
</resources>

View file

@ -0,0 +1,16 @@
<resources xmlns:tools="http://schemas.android.com/tools">
<!-- Base application theme. -->
<style name="Theme.ContactsTest" parent="Theme.MaterialComponents.DayNight.DarkActionBar">
<!-- Primary brand color. -->
<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>
<item name="colorSecondaryVariant">@color/teal_700</item>
<item name="colorOnSecondary">@color/black</item>
<!-- Status bar color. -->
<item name="android:statusBarColor" tools:targetApi="l">?attr/colorPrimaryVariant</item>
<!-- Customize your theme here. -->
</style>
</resources>

View file

@ -0,0 +1,6 @@
<account-authenticator xmlns:android="http://schemas.android.com/apk/res/android"
android:accountType="org.signal.contactstest"
android:icon="@mipmap/ic_launcher"
android:smallIcon="@mipmap/ic_launcher"
android:label="@string/app_name"/>

View file

@ -0,0 +1,16 @@
<?xml version="1.0" encoding="utf-8"?>
<ContactsSource
xmlns:android="http://schemas.android.com/apk/res/android">
<ContactsDataKind
android:detailColumn="data3"
android:detailSocialSummary="true"
android:icon="@mipmap/ic_launcher"
android:mimeType="vnd.android.cursor.item/vnd.org.signal.contacts.test.message"
android:summaryColumn="data2" />
<ContactsDataKind
android:detailColumn="data3"
android:icon="@mipmap/ic_launcher"
android:mimeType="vnd.android.cursor.item/vnd.org.signal.contacts.test.call"
android:summaryColumn="data2" />
</ContactsSource>

View file

@ -0,0 +1,9 @@
<?xml version="1.0" encoding="utf-8"?>
<sync-adapter
xmlns:android="http://schemas.android.com/apk/res/android"
android:accountType="org.signal.contactstest"
android:allowParallelSyncs="false"
android:contentAuthority="com.android.contacts"
android:isAlwaysSyncable="true"
android:supportsUploading="true"
android:userVisible="true" />