The Android Arsenal – Content Providers

Written with ♥️ and 🔥 since December 2018. Open sourced since October 2021.

This library provides a complete set of APIs to do everything you need with Contacts in Android. You no longer have to deal with the Contacts Provider, database operations, and cursors.

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 (AOSP) Android Contacts app and Google Contacts app, this library is for you!

Please help support this project 🙏❤️⭐️

Quick links

Features

The core module provides,

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,

There are also more features that are on the way!

  1. ☢️ SIM card query, insert, update, and delete.
  2. ☢️ Work profile contacts
  3. ☢️ Dynamically integrate custom data from other apps
  4. ☢️ Read/write from/to .VCF file.

Installation

First, include JitPack in the repositories list,

repositories 
    maven  url "https://jitpack.io" 

To import all modules,

dependencies 
     implementation 'com.github.vestrel00:contacts-android:0.1.10'

To import specific modules,

dependencies 
     implementation 'com.github.vestrel00.contacts-android:core:0.1.10'

Notice that when importing specific modules/subprojects, the first “:” comes after “contacts-android”.

SNAPSHOTs of branches are also available,

dependencies 
     implementation 'com.github.vestrel00:contacts-android:main-SNAPSHOT'

This library is a multi-module project published with JitPack

Quick Start

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

For more info, read Query contacts.

Something a bit more advanced…

To retrieve the first 5 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 “[email protected]” or “[email protected]
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("[email protected]", "com.google"),
        Account("[email protected]", "com.myspace"),
    )
    .include  setOf(
        Contact.Id,
        Contact.DisplayNamePrimary,
        Phone.Number
    ) 
    .orderBy(ContactsFields.DisplayNamePrimary.desc())
    .offset(0)
    .limit(5)
    .find()

Fore more info, read Query contacts (advanced).

Once you have the contacts, you now have access to all of their data!

val contact: Contact
Log.d(
    "Contact",
    """
        ID: $contact.id
        Lookup Key: $contact.lookupKey

        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 of the contact
        -----------------------------------
        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.
)

For more info, read about API Entities.

Setup

There is no setup required. It’s up to you how you want to create and retain instances of the contacts.core.Contacts(context) API. For more info, read Contacts API Setup.

It is also useful to read about API Entities.

More than enough APIs that will allow you to build your own contacts app!

This library is capable of doing more than just querying contacts. Actually, you can build your own full-fledged contacts app with it!

Let’s take a look at a few other APIs this library provides…

To get the first 20 gmail emails ordered by email address in descending order,

val emails = Contacts(context)
    .data()
    .query()
    .emails()
    .where  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 data kinds (including custom data).

For more info, read Query specific data kinds.

To CREATE/INSERT a contact with a name of “John Doe” who works at Amazon with a work email of “[email protected]” (in Kotlin),

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 = "[email protected]"
            type = EmailEntity.Type.WORK
        )
    )
    .commit()

Or alternatively, in a more Kotlinized style using named arguments,

val insertResult = Contacts(context)
    .insert()
    .rawContacts(NewRawContact(
        name = NewName(
            givenName = "John",
            familyName = "Doe"
        ),
        organization = NewOrganization(
            company = "Amazon",
            title = "Superstar"
        ),
        emails = mutableListOf(NewEmail(
            address = "[email protected]",
            type = EmailEntity.Type.WORK
        ))
    ))
    .commit()

Or alternatively, using extension functions,

val insertResult = Contacts(context)
    .insert()
    .rawContact 
        setName 
            givenName = "John"
            familyName = "Doe"
        
        setOrganization 
            company = "Amazon"
            title = "Superstar"
        
        addEmail 
            address = "[email protected]"
            type = EmailEntity.Type.WORK
        
    
    .commit()

For more info, read 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 = "[email protected]"
        
    )
    .commit()

For more info, read 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 Delete Contacts.

Threading and permissions

This library provides Kotlin coroutine extensions in the permissions module for all API functions to handle permissions and async module for 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 Permissions handling using coroutines and Execute 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.

Full documentation, guides, and samples

The above examples barely scratches the surface of what this library provides. For more in-depth documentation, visit the docs or visit the GitHub Pages. For a sample app reference, take a look at and run the sample module.

All APIs in the library are optimized!

Some other APIs or util functions out there typically perform one internal database query per contact returned. They do this to fetch the data per contact. This means that if there are 1,000 matching contacts, then an extra 1,000 internal database queries are performed! This is not cool!

To address this issue, the query APIs provided in the Contacts, Reborn library, perform only at least two and at most six or seven internal database queries no matter how many contacts are matched! Even if there are 100,000 contacts matched, the library will only perform two to seven internal database queries (depending on your query parameters).

Of course, if you don’t want to fetch all hundreds of thousands of contacts, the query APIs support pagination with limit and offset functions 😎

Cancellations are also supported! To cancel a query amid execution,

.find  returnTrueIfQueryShouldBeCancelled() 

The find function optionally takes in a function that, if it returns true, will cancel query processing as soon as possible. The function is called numerous times during query processing to check if processing should stop or continue. This gives you the option to cancel the query.

This is useful when used in multi-threaded environments. One scenario where this would be frequently used is when performing queries as the user types a search text. You are able to cancel the current query when the user enters new text.

For example, to automatically cancel the query inside a Kotlin coroutine when the coroutine is cancelled,

launch 
    withContext(coroutineContext) 
        val contacts = query.find  !isActive 
    
    // Or, using the coroutine extensions in the async module...
    val contacts = query.findWithContext()

All core APIs are framework-agnostic and works well with Java and Kotlin

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. Don’t believe me? Take a look at the dependencies in the core/build.gradle 😀

So, feel free to use the core API however you want with whatever libraries or 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.

I also made sure that all core functions and entities are interoperable with Java. So, if you were wondering why I’m using a semi-builder pattern instead of using named arguments with default values, that is why. I’ve also made some other intentional decisions about API design to ensure the best possible experience for both Kotlin and Java consumers without sacrificing Kotlin language standards. It is Kotlin-first, Java-second (with love and care).

Modules other than the core module are not guaranteed to be compatible with Java.

Requirements

Proguard

If you use Proguard and the async and/or permissions, you may need to add rules for Coroutines.

License

Copyright 2022 Contacts Contributors

Licensed under the Apache License, Version 2.0 (the "License");
you may not use this file except in compliance with the License.
You may obtain a copy of the License at

   https://www.apache.org/licenses/LICENSE-2.0

Unless required by applicable law or agreed to in writing, software
distributed under the License is distributed on an "AS IS" BASIS,
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
See the License for the specific language governing permissions and
limitations under the License.

Next Post

Don't Ignore Email Marketing

Have you at any time scrolled by way of your inbox and had an psychological response whilst looking through an electronic mail topic line? This happened to me the other day. The e mail topic explained, “Want to know why your 6-thirty day period-old puppy stopped listening?” Why did this […]
Don’t Ignore Email Marketing

You May Like