If you want to learn how to read and write contacts in your android app, then hurrah! you’re at right place. Today we will learn following topics in details and make our custom Contacts App using Jetpack Compose from scratch.
- What are Content Providers
- How contacts are stored in Android Device
- How to read contacts from Device
- How to write contacts with your app
- How to write contacts using Default Contact App.
Content Providers
Content Providers are common storage which can be used by any when they want to make their data accessible by other apps. On contrary to other storage types like Internal Storage or Room Database or Shared Preferences, data stored in content providers can be accessed as well as modify data by any app with proper permission which is set by Content Provider app. All your system defaults apps like Contacts app, Call logs app, SMS app, Gallery app, File Manager app, Music app uses content provider for their data storage.
Before starting to make our contacts app you must know these things in brief about Content Provider or you can read full detailed article solely on Content provider here.
- Type of Data Stored- In CP you can store any of type of data whether its you apps raw data like Strings, Integers, Lists etc. as well as structured data like Images, Videos and Music files.
- Content URI- There are multiple Content Providers available on android device for each individual app. So, each content provider has its own address or say location which can be used by other apps to access the provider. In android we call this address as Content URI means its a Resource identification of a Content.
- Permissions- When an apps create its own content provider to stores his data for its own use then it must implement some permissions so that all apps don’t misuses the resources of this app.
- SQLite Database- It is lite version of SQL database which is used by android for offline or local storage for easy and fast data manipulations. Our content provider uses this same database for storing apps data. SQL Database uses SQL which is Structured Query Language used to do CRUD operations on database, which means content providers also uses same functions for its operations.
- Content Resolver- It is main link b/w a data seeker app and a content provider app. Android has this built in feature for security reasons that any app can’t directly use the content provider of other apps. Other apps need to interact with content resolver only and he will pass your query to content provider. Its a middle man for data exchange b/w apps.
Now we will write our code to access data from the contacts app. For that we have created a ContactsManager class in which we will handle our read and write operations. You need to pass application context to this class when initialize it. before that we need to make POJO class or say data class to save our contact Info. This class will store 3 data as contact ID, contact Name and Contact Number.
// Data class for Contact
data class Contact(
val id: String,
val name: String,
val phoneNumber: String
)
Now, we will make an instance of content provider in our Manager class and define 2 functions to inset and fetch contacts.
// Contact Manager Class
class ContactManager(private val context: Context) {
private val contentResolver: ContentResolver = context.contentResolver
fun readContacts(): List<Contact> {
}
fun insertContact(name: String, phoneNumber: String) {
}
}
Read Contacts
First take a look at our readContacts function which will return us the list of our Contact model. In this function we make a local variable to store our single contacts one by one as we get from provider and finally return that list of contacts. We will call the query function of content resolver which will take following data as inputs:
- Uri- we need to pass the Content URI means the address of our provider.
- projection : Array<String> – here you can pass the name of columns whose data you want to access or pass null to all all data from the table
- selection : String – it is like the where clause of our SQL query in which you can set a condition on any column like you want to fetch all contacts where email is ? (selection Args- our next parameter of query function)
- selectionArgs : Array<String> – here you can pass the arguments for the selection
- sortOrder : when you want to fetch you data in sorting order like ASC or DES based on any column then you pass that info here
after this our query function return our data in form of a cursor which contains all data as list of key value pairs. you need to loop through it and get each column based in index.
fun readContacts(): List<Contact> {
val contacts = mutableListOf<Contact>()
val projection = arrayOf(
ContactsContract.CommonDataKinds.Phone.CONTACT_ID,
ContactsContract.CommonDataKinds.Phone.DISPLAY_NAME,
ContactsContract.CommonDataKinds.Phone.NUMBER
)
contentResolver.query(
ContactsContract.CommonDataKinds.Phone.CONTENT_URI,
projection,
null,
null,
ContactsContract.CommonDataKinds.Phone.DISPLAY_NAME
)?.use { cursor ->
while (cursor.moveToNext()) {
val id = cursor.getString(0)
val name = cursor.getString(1)
val number = cursor.getString(2)
contacts.add(Contact(id, name, number))
}
}
return contacts
}
Next is our insert function which is little complicated but you can understand it with me.
Write Contacts
In this function we need ContentProviderOperation to insert contact information in multiple operations so we need to create a list of operations which are of type ContentProviderOperation.
val operations = ArrayList<ContentProviderOperation>()
val operations
– Declares a read-only variable to store a list ofContentProviderOperation
objects.ArrayList<ContentProviderOperation>()
– Initializes an empty list to hold the operations required to insert a contact. Each operation will represent a different part of the contact (raw contact, name, phone number).
Now lets create our first operation in which we will create a empty row in contacts table. Creates a new, empty raw contact that serves as the base for adding details (name, phone). The contact itself doesn’t exist yet; it’s just a placeholder.
operations.add(
ContentProviderOperation.newInsert(ContactsContract.RawContacts.CONTENT_URI)
.withValue(ContactsContract.RawContacts.ACCOUNT_NAME, null)
.build()
)
ContentProviderOperation.newInsert
– Creates an insert operation.ContactsContract.RawContacts.CONTENT_URI
– The URI for the raw contacts table. This step inserts a new row into theRawContacts
table to represent the new contact.withValue
– Adds values to the new row.RawContacts.ACCOUNT_NAME, null
– The contact will not be associated with any account (e.g., Google, WhatsApp). This results in a local-only contact.build()
– Finalizes the operation and returns theContentProviderOperation
object.operations.add
– Adds this operation to the list.
Now comes our 2nd operation in which we will inset name details of our contact by taking reference of first operation row Id. Inserts the name of the contact, associating it with the raw contact created in step 1.
codeoperations.add(
ContentProviderOperation.newInsert(ContactsContract.Data.CONTENT_URI)
.withValueBackReference(ContactsContract.Data.RAW_CONTACT_ID, 0)
.withValue(ContactsContract.Data.MIMETYPE, ContactsContract.CommonDataKinds.StructuredName.CONTENT_ITEM_TYPE)
.withValue(ContactsContract.CommonDataKinds.StructuredName.DISPLAY_NAME, name)
.build()
);
ContactsContract.Data.CONTENT_URI
– URI for theData
table where contact details (name, phone, email) are stored.withValueBackReference
– Links this data entry to the raw contact created in Step 1.RAW_CONTACT_ID, 0
– Uses a back-reference to the first operation (index 0
). This means the raw contact ID will be assigned automatically.
MIMETYPE
– Specifies the type of data being inserted.StructuredName.CONTENT_ITEM_TYPE
– Indicates that this operation inserts a structured name (full name).
DISPLAY_NAME
– The contact’s name.build()
– Finalizes the operation.
Now make our final operation to insert the number details in the contact. Adds the phone number to the contact, linking it to the same raw contact.
codeoperations.add(
ContentProviderOperation.newInsert(ContactsContract.Data.CONTENT_URI)
.withValueBackReference(ContactsContract.Data.RAW_CONTACT_ID, 0)
.withValue(ContactsContract.Data.MIMETYPE, ContactsContract.CommonDataKinds.Phone.CONTENT_ITEM_TYPE)
.withValue(ContactsContract.CommonDataKinds.Phone.NUMBER, phoneNumber)
.withValue(ContactsContract.CommonDataKinds.Phone.TYPE, ContactsContract.CommonDataKinds.Phone.TYPE_MOBILE)
.build()
);
ContactsContract.Data.CONTENT_URI
– Again, we use theData
table for phone details.withValueBackReference
– Links this phone entry to the raw contact created in step 1 (index 0).MIMETYPE
– Specifies the type of data (phone number).NUMBER
– The phone number to be saved.TYPE
– The type of phone number (mobile, home, work).TYPE_MOBILE
– Specifies the phone number as a mobile number.
build()
– Finalizes the operation.
Finally execute these all operations in try catch block. Attempts to insert the new contact. If anything fails (e.g., permission issues), it catches the exception to avoid app crashes.
try {
contentResolver.applyBatch(ContactsContract.AUTHORITY, operations)
} catch (e: Exception) {
e.printStackTrace()
}
try {}
– Surrounds the batch operation to handle any errors that might occur.contentResolver.applyBatch
– Executes all the insert operations in one go.ContactsContract.AUTHORITY
– The authority for theContactsContract
provider.operations
– The list of operations (raw contact, name, phone).
catch (e: Exception)
– Catches any exceptions during the batch execution.e.printStackTrace()
– Logs the error to the console.
App UI
Lets create a basic UI in Jetpack Compose to insert contact and show existing contacts in list. In this UI we have shown two text fields which we collect the name and number input for the contact then 2 buttons one for the inserting contact by using our custom insert function and second button will uses a Intent to Insert contact which will take you to default contact app where you can insert contact which is good because that can avoid error of inserting invalid data in the contact table.
// Compose UI
@Composable
fun ContactScreen(contactManager: ContactManager, context: Activity) {
val contacts = contactManager.readContacts()
var name by remember { mutableStateOf("") }
var phoneNumber by remember { mutableStateOf("") }
Column(
modifier = Modifier
.fillMaxSize()
.padding(16.dp)
) {
// Input Fields
OutlinedTextField(
value = name,
onValueChange = { name = it },
label = { Text("Name") },
modifier = Modifier
.fillMaxWidth()
.padding(vertical = 4.dp)
)
OutlinedTextField(
value = phoneNumber,
onValueChange = { phoneNumber = it },
label = { Text("Phone Number") },
keyboardOptions = KeyboardOptions(keyboardType = KeyboardType.Phone),
modifier = Modifier
.fillMaxWidth()
.padding(vertical = 4.dp)
)
Button(
onClick = {
if (name.isNotBlank() && phoneNumber.isNotBlank()) {
contactManager.insertContact(name, phoneNumber)
name = ""
phoneNumber = ""
contactManager.readContacts()
}
},
modifier = Modifier
.fillMaxWidth()
.padding(vertical = 8.dp)
) {
Text("Add Contact")
}
Button(
onClick = {
val intent = Intent(ContactsContract.Intents.Insert.ACTION).apply {
type = ContactsContract.RawContacts.CONTENT_TYPE
putExtra(ContactsContract.Intents.Insert.NAME, name)
putExtra(ContactsContract.Intents.Insert.PHONE, phoneNumber)
}
context.startActivity(intent)
},
modifier = Modifier
.fillMaxWidth()
.padding(vertical = 8.dp)
) {
Text("Add Contact in Contacts")
}
// Contacts List
LazyColumn(
modifier = Modifier.fillMaxWidth()
) {
items(contacts) { contact ->
Card(
modifier = Modifier
.fillMaxWidth()
.padding(vertical = 4.dp)
) {
Column(
modifier = Modifier
.padding(16.dp)
.fillMaxWidth()
) {
Text(text = contact.name, style = MaterialTheme.typography.titleMedium)
Text(
text = contact.phoneNumber,
style = MaterialTheme.typography.bodyMedium
)
}
}
}
}
}
}
You need to create a function to check that the required permissions are granted or not.
// Permission checker
fun checkContactsPermission(context: Context): Boolean {
return ContextCompat.checkSelfPermission(
context,
Manifest.permission.READ_CONTACTS
) == PackageManager.PERMISSION_GRANTED &&
ContextCompat.checkSelfPermission(
context,
Manifest.permission.WRITE_CONTACTS
) == PackageManager.PERMISSION_GRANTED
}
Now use this function in your main activity by create object of permission class for permission handling and object of contact Manager class. Also define the permission which you gonna need to access or modify contacts. Then we will first check condition that our permissions are granted or not based on that we will decide that either we show Contacts UI or button to ask permission.
class MainActivity : ComponentActivity() {
@RequiresApi(Build.VERSION_CODES.TIRAMISU)
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
val permissionManager = PermissionManager(this@MainActivity)
val contactManager = ContactManager(this)
val permissions = listOf(
android.Manifest.permission.READ_CONTACTS,
android.Manifest.permission.WRITE_CONTACTS,
)
setContent {
Column(
modifier = Modifier
.fillMaxSize()
.padding(16.dp),
verticalArrangement = Arrangement.Center,
horizontalAlignment = Alignment.CenterHorizontally
) {
// Check permissions before showing the UI
if (checkContactsPermission(this@MainActivity)) {
ContactScreen(contactManager,this@MainActivity)
} else {
Button(onClick = {
permissionManager.requestPermission(permissions){ results ->
finishAndRemoveTask()
}
}) {
Text("Read Contacts")
}
}
}
}
}
}
Finally don’t forget to add these 2 permissions in app manifest file.
<uses-permission android:name="android.permission.READ_CONTACTS"/>
<uses-permission android:name="android.permission.WRITE_CONTACTS"/>
For full code see this GitHub Gist here