One of the main functions of a mobile phone was to store contacts information. It still is to this day, I think 🤔. Nowadays, phones are much smarter, capable of way more than just being a digital phone number/address book. However, that doesn’t take away from the importance of keeping contact information of our friends and family… just in case…right?
TLDR; This article is my way of sharing my three year long passion project that I just open-sourced; https://github.com/vestrel00/contacts-android
Whether you just need to get all or some Contacts for a small part of your app (written in Kotlin or Java), or you are looking to create your own full-fledged Contacts app with the same capabilities as the native Android Contacts app, this library has you covered!
Documentation and how-to guides are all available and linked in the repository. You can browse the Howto pages or visit the GitHub Pages. Both contain the same info but the GitHub pages are not guaranteed to be up-to-date. The GitHub wiki hosts the project roadmap. It contains all planned work and release schedules, which are organized using issues, milestones, and projects.
The repo was open-sourced on October 4, 2021. It was private prior to that.
By the way, I also wrote an article about this in DEV.TO. It’s not a copy-paste of this one. It has a lot of things that I did not discuss in this article, so check it out!
Dawn of a new era
https://www.shutterstock.com/image-vector/hands-smartphones-doodle-set-human-palms-1850275141
In the dawn of the 21st century when flip phones with physical number pads and keyboards reigned supreme, Steve Jobs disrupted the “dumb” mobile phone industry with the announcement of the world’s “first smart phone” back in January 9 of 2007. The iPhone was not only a phone (in the old sense of the word) with a phone/address book, but it was also a music player and provided access to the internet. Needless to say, Apple took the industry by storm and headed off to command an entire mobile ecosystem with the closed-source iOS. There was no (unified) competition…
I think that this event forced Google’s plan into motion… Back in 2005, Google acquired a certain company and obtained their operating system, which began as an OS for digital cameras but quickly evolved into an OS for smartphones. Later, that same year as the announcement of the iPhone, Google founded the Open Handset Alliance. This was Google’s response to Apple… Actually, it wasn’t only Google. Among the dozens of companies, Intel, Motorola, NVIDIA, Texas Instruments, LG, Samsung, Sprint, and T-Mobile were part of the consortium.
https://en.wikipedia.org/wiki/Open_Handset_Alliance#/media/File:Open_Handset_Alliance_logo.svg
Whereas Apple took the close source approach, allowing them to command an entire ecosystem, Google spearheaded the open source answer to mobile computing, unifying a ragtag group of non-iOS systems into a singular, unified ecosystem. The following year, Android was born.
Deprecation of Contacts
https://www.shutterstock.com/image-photo/time-say-goodbye-typed-words-on-531209353
It’s been well over a decade since Android was open-sourced. Android API version 1.0 was released back in September of 2008. It included the android.provider.Contacts
, which “stores all information about contacts.” However, it did;
- Not allow access to multiple accounts.
- Not support aggregation of similar contacts.
It only supported storing contacts of one Google account.
Very quickly, the developers of Android realized that the Contacts API it included in the SDK was not going to scale well as the Android ecosystem immediately grew, exponentially. Android’s popularity for being the world’s open-sourced smartphone OS of choice was clear from the get-go.
To support the explosion of devices operated by the Android OS around the globe and the booming list of new social media accounts, the Contacts API had to be deprecated… Quickly.
Hello ContactsContract
https://www.shutterstock.com/image-vector/international-group-people-saying-hi-different-1182493903
In just over a year since launch, Android 2.0 Eclair (API 5) was released. There was a clear theme for the 2.0 version. The Android team overhauled the initial contacts and accounts mechanism set in place with the android.provider.Contacts
and replaced it with android.provider.ContactsContract
.
https://developer.android.com/reference/android/provider/Contacts
The goal was to fix the lack of scalability put in place by the deprecated counterpart. It worked. To this day, 26 API versions later, ContactsContract
is alive and well in Android 12 (API 31). The Android team accomplished their mission. The architecture of the new contacts API was simple and it scaled well. This figure shows it all.
https://developer.android.com/guide/topics/providers/contacts-provider
The above figure is taken from the official Android developer docs. I’ll also copy and paste the description of the figure as I am not able to word it any better.
ContactsContract.Contacts table
Rows representing different people, based on aggregations of raw contact rows.
ContactsContract.RawContacts table
Rows containing a summary of a person’s data, specific to a user account and type.
ContactsContract.Data table
Rows containing the details for raw contact, such as email addresses or phone numbers.
RawContacts
is tied to an Account. For example, a Yahoo account, a Hotmail account, a Google account, would all be separate RawContacts but could all represent the same person (a single entry in the Contacts
table).
ContactsContract is simple, yet complex
https://www.shutterstock.com/image-photo/man-think-how-solve-mathematical-problem-766012597
Though ContactsContract
may be simple, the amount of time required for someone to truly understand and master the ins-and-outs of the API is large. Furthermore, the API is littered with unsightly constants, database content provider operations and cursors, all of which can be abstracted away and wrapped into a beginner-friendly package =)
Let’s take a quick look at how we, as a community, have been using ContactsContract to get a list of contacts. I don’t have the time to write a code snippet for this myself so I’ll use this util file written by someone else as an example.
// First, we define a structure to store contact data we want to retrieve | |
data class ContactData( | |
val contactId: Long, | |
val name: String, | |
val phoneNumber: List<String>, | |
val avatar: Uri? | |
) | |
// Here are the functions to retrieve all contacts matching the search pattern | |
fun Context.retrieveAllContacts( | |
searchPattern: String = "", | |
retrieveAvatar: Boolean = true, | |
limit: Int = -1, | |
offset: Int = -1 | |
): List<ContactData> { | |
val result: MutableList<ContactData> = mutableListOf() | |
contentResolver.query( | |
ContactsContract.Contacts.CONTENT_URI, | |
CONTACT_PROJECTION, | |
if (searchPattern.isNotBlank()) "${ContactsContract.Contacts.DISPLAY_NAME_PRIMARY} LIKE '%?%'" else null, | |
if (searchPattern.isNotBlank()) arrayOf(searchPattern) else null, | |
if (limit > 0 && offset > -1) "${ContactsContract.Contacts.DISPLAY_NAME_PRIMARY} ASC LIMIT $limit OFFSET $offset" | |
else ContactsContract.Contacts.DISPLAY_NAME_PRIMARY + " ASC" | |
)?.use { | |
if (it.moveToFirst()) { | |
do { | |
val contactId = it.getLong(it.getColumnIndex(CONTACT_PROJECTION[0])) | |
val name = it.getString(it.getColumnIndex(CONTACT_PROJECTION[2])) ?: "" | |
val hasPhoneNumber = it.getString(it.getColumnIndex(CONTACT_PROJECTION[3])).toInt() | |
val phoneNumber: List<String> = if (hasPhoneNumber > 0) { | |
retrievePhoneNumber(contactId) | |
} else mutableListOf() | |
val avatar = if (retrieveAvatar) retrieveAvatar(contactId) else null | |
result.add(ContactData(contactId, name, phoneNumber, avatar)) | |
} while (it.moveToNext()) | |
} | |
} | |
return result | |
} | |
private fun Context.retrievePhoneNumber(contactId: Long): List<String> { | |
val result: MutableList<String> = mutableListOf() | |
contentResolver.query( | |
ContactsContract.CommonDataKinds.Phone.CONTENT_URI, | |
null, | |
"${ContactsContract.CommonDataKinds.Phone.CONTACT_ID} =?", | |
arrayOf(contactId.toString()), | |
null | |
)?.use { | |
if (it.moveToFirst()) { | |
do { | |
result.add(it.getString(it.getColumnIndex(ContactsContract.CommonDataKinds.Phone.NUMBER))) | |
} while (it.moveToNext()) | |
} | |
} | |
return result | |
} | |
private fun Context.retrieveAvatar(contactId: Long): Uri? { | |
return contentResolver.query( | |
ContactsContract.Data.CONTENT_URI, | |
null, | |
"${ContactsContract.Data.CONTACT_ID} =? AND ${ContactsContract.Data.MIMETYPE} = '${ContactsContract.CommonDataKinds.Photo.CONTENT_ITEM_TYPE}'", | |
arrayOf(contactId.toString()), | |
null | |
)?.use { | |
if (it.moveToFirst()) { | |
val contactUri = ContentUris.withAppendedId( | |
ContactsContract.Contacts.CONTENT_URI, | |
contactId | |
) | |
Uri.withAppendedPath( | |
contactUri, | |
ContactsContract.Contacts.Photo.CONTENT_DIRECTORY | |
) | |
} else null | |
} | |
} | |
private val CONTACT_PROJECTION = arrayOf( | |
ContactsContract.Contacts._ID, | |
ContactsContract.Contacts.LOOKUP_KEY, | |
ContactsContract.Contacts.DISPLAY_NAME_PRIMARY, | |
ContactsContract.Contacts.HAS_PHONE_NUMBER | |
) |
This code snippet is taken from https://github.com/aminography/CommonUtils/blob/master/library
/src/main/java/com/aminography/commonutils/ContactUtils.kt
I take no credit for this code snippet. I’m just using it as an example for this blog.
Let’s digest that code snippet above. What is it doing? If you are unfamiliar with the ContactsContract API, you will have no idea 😕 Though I am not able to verify the correctness of the above code, I’ll explain what it is trying to do. The function retrieveAllContacts()
;
- returns all contacts, each containing the following data; contact id, display name, phone numbers, and photo URIs.
- matches contacts by display name
- optionally includes photo URI in the resulting data set
- offers limiting and offsetting results for pagination, which is important for users that have thousands of contacts (who has that many?)
A few things to note,
- Look at all of the ContactsContract constants.
- Look at all of the queries.
- Look at all of the cursors.
- Permissions are not handled.
- The work is not done asynchronously.
Lots of icky stuff there!
For fun, let’s find out how many queries it makes to the contacts database.
- First, it makes a query to retrieve all matching contacts from the Contacts table.
- It then iterates over every single contact and makes a query to get the phone numbers list for each contact
- and optionally makes another query to get the photo Uri for each Contact.
If there are 100 matching contacts, then this function makes up to:
1 query for matching contacts + 100 queries for phone numbers + 100 queries for photo Uris = 300 queries
I probably did not get the math right. Let’s just say it’s 300 😉
That’s a lot of queries. This can probably be optimized by a lot. Keep this number in mind for later…300
How would the above code snippet change if you want to get all contacts data (address, email, event, group membership, IM, name, nickname, note, organization, phone, photo, relation, SIP address, and website)? How about other functions like contact creation, updates, and deletion? What about contact linking/unlinking? Trust me, you don’t even want to imagine it!
I understand that that util function was probably created for a small piece of an app. It does not have to look nice or be that performant. But still, it shows that we as a community still have yet to find a standard pretty, complete, and performant way to perform all Contacts provider functions (without having to deal with icky stuff).
To this day, the “complexity” remains unsolved… AFAIK, there is no library out there that provides all of the functions required to rebuild a full-fledged Contacts app that can stand toe-to-toe with the one that comes with almost every copy of Android.
Take, take, and finally, give back
https://www.shutterstock.com/image-photo/give-back-385772176
Since the start of my professional career (and even before that), I have just been taking and taking from the open source community without ever giving anything in return. That is the nature of the world of open source, I guess. To give and expect nothing in return, except hope that the community may find it useful and contribute back to the open sourced code, is just the way it is.
From full stack web (front and backend) engineer to Android specialist, I have never looked back. There was nothing wrong with web and backend engineering but mobile was what really caught my fancy. Everyone was starting to have a smartphone. It was all the rave. I had been lucky enough to specialize in the new golden age of mobile engineering, which until this retrospective I had been taking for granted…
Without Android and the open source community built around it, I would not be where I am today. From birth and childhood in the Philippines to higher education in the US, Android has provided me a life I thought I could only dream of living. To this day, it is providing for me and my family. Needless to say, I am eternally and sincerely grateful… From the bottom of my heart, thanks to the Android community ❤️
With all that gratitude, why have I not given anything back? It’s time.
Rebirth of Contacts
https://www.shutterstock.com/image-illustration/wizard-summoning-phoenix-hell-digital-art-1402715897
The android.provider.Contacts
was deprecated by ContactsContract
. I hope to revive Contacts
in a different package but under the same name; contacts.core.Contacts
. Whereas ContactsContract
deprecated android.provider.Contacts
, contacts.core.Contacts
would wrap ContactsContract
and simplify it.
Side note, I originally had the core module package set as
contacts
instead ofcontacts.core
but I started getting compile errors at some point about it 😬
Without further ado, here is the link to the GitHub repo containing complete installation instructions and usage guides; https://github.com/vestrel00/contacts-android
The goals of contacts.core.Contacts are to;
- Eliminate the use of unsightly ContentResolver and Cursors for all
ContactsContract
functions. - Eliminate the use of unsightly
ContactsContract
constants. - Eliminate the hours and days required to fully understand everything in
ContactsContract
. - Provide all of the functions required to recreate the native Contacts app without ever having to do any “dirty work”.
- Provide a simple API that any new Kotlin/Java/Android engineer can pick up and use immediately without much thought. A true “JetPack” component.
To harness the full power of this API, users would only need to remember the one-to-many relationship between the Contacts, RawContacts, and Data tables respectively.
Contacts is one of the most basic APIs of Android (or any mobile phone for that matter) that’s been there from the beginning. One of the main reasons for the existence of mobile phones was (and still is) to keep track of our friends, family, and coworkers’ contact information.
After a decade, it’s time that contacts gets a face lift… not that it needs one. It may be very presumptuous of me to think that the Contacts API library that I will be open sourcing will get traction from the Android community… However, if I can help just a handful of Android engineers out there somewhere in the world with this API, then mission is accomplished!
Today marks the official open sourcing of Contacts, Reborn 0.1.0! Android Contacts API Library written in Kotlin with Java interoperability. It is written purely in Kotlin but does provide Java interoperability.
The core library supports;
- All of the kinds of Data in the Contacts Provider; address, email, event, group membership, IM, name, nickname, note, organization, phone, photo, relation, SIP address, and website.
- Custom data integration.
- Broad queries and advanced queries of Contacts and RawContacts from zero or more Accounts. Include only desired fields in the results (e.g. name and phone number) for optimization. Specify matching criteria in an SQL WHERE clause fashion using Kotlin infix functions. Order by contact table columns. Limit and offset functions.
- Insert one or more RawContacts with an associated Account, which leads to the insertion of a new Contact subject to automatic aggregation by the Contacts Provider.
- Update one or more Contacts, RawContacts, and Data.
- Delete one or more Contacts, RawContacts, and Data.
- Query, insert, update, and delete Profile (device owner) Contact, RawContact, and Data.
- Query, insert, update, and delete Groups per Account.
- Query, insert, update, and delete specific kinds of Data.
- Query, insert, update, and delete custom Data.
- Query for Accounts in the system or RawContacts table.
- Query for just RawContacts.
- Associate local RawContacts (no Account) to an Account.
- Join/merge/link and separate/unmerge/unlink two or more Contacts.
- Get and set Contact and RawContact Options; starred (favorite), custom ringtone, send to voicemail.
- Get and set Contacts/RawContact photo and thumbnail.
- Get and set default (primary) Contact Data (e.g. default/primary phone number, email, etc).
- Miscellaneous convenience functions.
- Contact data is synced automatically across devices.
There are also extensions that add functionality to every core function;
Also included are some pre-baked goodies to be used as is or just for reference;
- Gender custom Data.
- Handle name custom Data.
- Rudimentary contacts-integrated UI components.
- Debug functions to aid in development
There are also more features that are on the way!
- Blocked phone numbers.
- SIM card query, insert, update, and delete.
- Read/write from/to .VCF file.
- Social media custom data (WhatsApp, Twitter, Facebook, etc).
Framework-agnostic design
The API does not and will not force you to use any frameworks (e.g. RxJava or Coroutines/Flow)! All core functions of the API live in the core
module, which you can import to your project all by itself.
So, feel free to use the core API however you want with whatever frameworks you want, such as Reactive, Coroutines/Flow, AsyncTask (hope not), WorkManager, and whatever permissions handling APIs you want to use.
All other modules in this library are optional and are just there for your convenience or for reference.
To tie it all up, there is of course a sample application that showcases all of the above. It is intentionally barebones and not made to look pretty. It’s just there to be used as reference. It would be an honor and my pleasure as I’m looking forward to the apps and UIs the community will build using this library!
I hope that this library will be received well by the Android community or at the very least, ignored (rather than abhorred) 🤞😂. I put every ounce of talent I have (if I actually have any) into building this. I also gave it all the love I can give it. ❤️❤️❤️❤️❤️
Revisiting how to retrieve all Contacts
In the previous sections, we looked at a code snippet for the function retrieveAllContacts
. Let’s take a look at what that would look like if we use Contacts, Reborn.
To retrieve all contacts containing all available contact data,
val contacts = Contacts(context).query().find() |
To simply search for Contacts, yielding the exact same results as the native Contacts app,
val contacts = Contacts(context) | |
.broadQuery() | |
.whereAnyContactDataPartiallyMatches(searchText) | |
.find() |
Job Offers
That’s it! BUT, THAT IS BORING! Let’s take a look at something more advanced…😉
To retrieve the first five contacts (including only the contact id, display name, and phone numbers in the results) ordered by display name in descending order, matching ALL of these rules;
- a first name starting with “leo”
- has emails from gmail or hotmail
- lives in the US
- has been born prior to making this query
- is favorited (starred)
- has a nickname of “DarEdEvil” (case sensitive)
- works for Facebook
- has a note
- belongs to the account of “jerry@gmail.com” or “jerry@myspace.com”
val contacts = Contacts(context) | |
.query() | |
.where { | |
(Name.GivenName startsWith "leo") and | |
(Email.Address { endsWith("gmail.com") or endsWith("hotmail.com") }) and | |
(Address.Country equalToIgnoreCase "us") and | |
(Event { (Date lessThan Date().toWhereString()) and (Type equalTo EventEntity.Type.BIRTHDAY) }) and | |
(Contact.Options.Starred equalTo true) and | |
(Nickname.Name equalTo "DarEdEvil") and | |
(Organization.Company `in` listOf("facebook", "FB")) and | |
(Note.Note.isNotNullOrEmpty()) | |
} | |
.accounts( | |
Account("john.doe@gmail.com", "com.google"), | |
Account("john.doe@myspace.com", "com.myspace"), | |
) | |
.include { setOf( | |
Contact.Id, | |
Contact.DisplayNamePrimary, | |
Phone.Number | |
) } | |
.orderBy(ContactsFields.DisplayNamePrimary.desc()) | |
.offset(0) | |
.limit(5) | |
.find() |
For more info, read How do I get a list of contacts in the simplest way? and How do I get a list of contacts in a more advanced way?
Imagine what this would look like if you use ContactsContract directly. Now, you don’t have to! The above snippet is in Kotlin but, like I mentioned, all of the core APIs are usable in Java too (though it won’t look as pretty).
So… How many database queries are performed by these functions internally? Depending on function call parameters, at least two and at most six or sevenn. That’s much better than a linearly growing 300 (for 100 matching contacts)!
Once you have the contacts, you now have access to all of their data 💥💥💥
Log.d( | |
"Contacts", | |
contacts.joinToString("\n\n") { contact -> | |
""" | |
ID: ${contact.id} | |
Display name: ${contact.displayNamePrimary} | |
Display name alt: ${contact.displayNameAlt} | |
Photo Uri: ${contact.photoUri} | |
Thumbnail Uri: ${contact.photoThumbnailUri} | |
Last updated: ${contact.lastUpdatedTimestamp} | |
Starred?: ${contact.options?.starred} | |
Send to voicemail?: ${contact.options?.sendToVoicemail} | |
Ringtone: ${contact.options?.customRingtone} | |
Aggregate data from all RawContacts | |
----------------------------------- | |
Addresses: ${contact.addressList()} | |
Emails: ${contact.emailList()} | |
Events: ${contact.eventList()} | |
Group memberships: ${contact.groupMembershipList()} | |
IMs: ${contact.imList()} | |
Names: ${contact.nameList()} | |
Nicknames: ${contact.nicknameList()} | |
Notes: ${contact.noteList()} | |
Organizations: ${contact.organizationList()} | |
Phones: ${contact.phoneList()} | |
Relations: ${contact.relationList()} | |
SipAddresses: ${contact.sipAddressList()} | |
Websites: ${contact.websiteList()} | |
----------------------------------- | |
""".trimIndent() | |
} | |
) | |
// There are also aggregate data functions that return a sequence instead of a list. | |
// Each Contact may have more than one of the following data if the Contact is made up of 2 or more RawContacts; | |
// name, nickname, note, organization, sip address. |
For more info, read How do I learn more about the API entities?
There’s a lot more
As previously mentioned in the v1 feature set, this library is capable of doing more than just querying contacts. Let’s take a look at a few of them here.
To get the first 20 Gmail emails ordered by email address in descending order,
val emails = Contacts(context).data() | |
.query() | |
.emails() | |
.where(Fields.Email.Address endsWith "gmail.com") | |
.orderBy(Fields.Email.Address.desc(ignoreCase = true)) | |
.offset(0) | |
.limit(20) | |
.find() |
It’s not just for emails. It’s for all common data kinds (including custom data);
For more info, read How do I get a list of specific data kinds?
To CREATE/INSERT a contact with a name of “John Doe” who works at Amazon with a work email of “john.doe@amazon.com”,
val insertResult = Contacts(context) | |
.insert() | |
.rawContacts(NewRawContact().apply { | |
name = NewName().apply { | |
givenName = "John" | |
familyName = "Doe" | |
} | |
organization = NewOrganization().apply { | |
company = "Amazon" | |
title = "Superstar" | |
} | |
emails.add(NewEmail().apply { | |
address = "john.doe@amazon.com" | |
type = Email.Type.WORK | |
}) | |
}) | |
.commit() |
Or alternatively, in a more Kotlinized style,
val insertResult = Contacts(context) | |
.insert() | |
.rawContact { | |
setName { | |
givenName = "John" | |
familyName = "Doe" | |
} | |
setOrganization { | |
company = "Amazon" | |
title = "Superstar" | |
} | |
addEmail { | |
address = "john.doe@amazon.com" | |
type = Email.Type.WORK | |
} | |
} | |
.commit() |
For more info, read How do I create/insert contacts?
If John Doe switches jobs and heads over to Microsoft, we can UPDATE his data,
Contacts(context) | |
.update() | |
.contacts(johnDoe.mutableCopy { | |
setOrganization { | |
company = "Microsoft" | |
title = "Newb" | |
} | |
emails().first().apply { | |
address = "john.doe@microsoft.com" | |
} | |
}) | |
.commit() |
For more info, read How do I update contacts?
If we no longer like John Doe, we can DELETE him from our life,
Contacts(context) | |
.delete() | |
.contacts(johnDoe) | |
.commit() |
For more info, read How do I delete contacts?
There’s even more…
All of the function calls I’ve shown here do not handle permissions and also does the work on the call-site thread.
The Contacts, Reborn library provides Kotlin coroutine extensions for all API functions to handle permissions and executing work in background threads.
launch { | |
val contacts = Contacts(context) | |
.queryWithPermission() | |
... | |
.findWithContext() | |
val deferredResult = Contacts(context) | |
.insertWithPermission() | |
... | |
.commitAsync() | |
val result = deferredResult.await() | |
} |
For more info, read How do I use the permissions extensions to simplify permission handling using coroutines? and How do I use the async extensions to simplify executing work outside of the UI thread using coroutines?
So, if we call the above function and we don’t yet have permission. The user will be prompted to give the appropriate permissions before the query proceeds. Then, the work is done in the coroutine context of choice (default is Dispatchers.IO). If the user does not give permission, the query will return no results.
Extensions for Kotlin Flow and RxJava are also in the v1 roadmap, which includes APIs for listening to Contacts database changes.
There’s too much…
There is a lot more that I will not cover here. I probably missed a lot of important stuff because there is simply too much. This blog is getting insanely long. It was not meant to be a place to host API documentation.
Please visit GitHub for complete documentation, issues, milestones, andprojects. I am actively working on this project! Contributors are welcome!
P.S.
I have been working on this library on and off for the past three years. It has undergone several big refactors as the years went by. There were several instances where I wanted to just give up and scrap it. What gave me strength to keep going was my sense of duty to give back. I need to help at least one other person in the open source community the same way that many others have helped me…
I actually was not planning on open sourcing this until v1.0.0. However, getting to v1.0.0 would have taken another year at the pace that I’m going. With work and life being busy, it is very difficult to find time with passion projects such as this. Therefore, I decided to open source at v0.1.0. To be fair, all v1 functions have been implemented in v0.1.0 already. We just need to prove that it is solid 😅 Hopefully, over time, there will be others in the community that will rise to the challenge of contributing 🔥
Hopefully, this API provides a modern answer to all of these ancient StackOverflow questions.
- https://stackoverflow.com/search?q=get+android+contacts
- https://stackoverflow.com/search?q=insert+android+contacts
- https://stackoverflow.com/search?q=update+android+contacts
- https://stackoverflow.com/search?q=delete+android+contacts
- https://stackoverflow.com/search?q=combine+android+contacts
- https://stackoverflow.com/search?q=join+android+contacts
- https://stackoverflow.com/search?q=android+contact+groups
- https://stackoverflow.com/search?q=android+contact+profile
- https://stackoverflow.com/search?q=android+contact+starred
- https://stackoverflow.com/search?q=android+contact+ringtone
- https://stackoverflow.com/search?q=android+contact+voicemail
- https://stackoverflow.com/search?q=android+contact+photo
- https://stackoverflow.com/search?q=android+contact+custom+data
Sources
- https://github.com/vestrel00/contacts-android
- https://en.m.wikipedia.org/wiki/IPhone
- https://en.m.wikipedia.org/wiki/IOS_version_history
- https://www.britannica.com/technology/Android-operating-system
- https://en.m.wikipedia.org/wiki/Android_version_history
- https://developer.android.com/reference/android/provider/Contacts
- https://developer.android.com/reference/android/provider/ContactsContract
- https://developer.android.com/guide/topics/providers/contacts-provider