Provide the caller id for incoming call from my own app

I want to write an App that can identify the phone number of incoming (unknown) calls by looking into a table inside my app (e.g. an SQLite Database Table).

I already implemented this in iOS using a Call Directory Extension, but for Android the only option I can find is using a BroadcastReceiver with a popup above the native incoming call screen to show contact information.

ContactsContract.Directory seems to offer the possibility to create custom directories where the native call app could look up the caller id. Unfortunately, I can't find any good examples on how to accomplish this.

Does anybody know if it is possible to implement the caller id using the ContactsContract.Directory or if there is something similar in Android to the Call Directory Extension in Android? If that is the case, an example code would be really helpful.


I just barely wrote a tutorial on how to get this working. Check it out here: https://simplenexus.dev/2019/08/27/android-caller-id.html

The basics of how to get this working are:

AndroidManifest.xml

<provider
android:name=".callerid.CallerIDProvider"
android:authorities="@string/callerid_authority"
android:readPermission="android.permission.READ_CONTACTS"
android:enabled="true"
android:exported="true">
<meta-data
  android:name="android.content.ContactDirectory"
  android:value="true"/></provider>

CallerIDProvider.kt

private var userRepository: UserRepository? = null

private val uriMatcher = UriMatcher(UriMatcher.NO_MATCH)

override fun onCreate(): Boolean {
    context?.let {
        val userDao = UserDatabase.getDatabase(it).userDao()
        userRepository = UserRepository(userDao)
        val authority = it.getString(R.string.callerid_authority)
        uriMatcher.addURI(authority, "directories", DIRECTORIES)
        uriMatcher.addURI(authority, "phone_lookup/*", PHONE_LOOKUP)
    }
    return true
}

override fun query(uri: Uri, projection: Array<out String>?, selection: String?, selectionArgs: Array<out String>?, sortOrder: String?): Cursor? {
    when (uriMatcher.match(uri)) {
        DIRECTORIES -> {
            val label = context?.getString(R.string.app_name) ?: return null
            val cursor = MatrixCursor(projection)
            projection?.map { column ->
                when (column) {
                    Directory.ACCOUNT_NAME,
                    Directory.ACCOUNT_TYPE,
                    Directory.DISPLAY_NAME -> label
                    Directory.TYPE_RESOURCE_ID -> R.string.app_name
                    Directory.EXPORT_SUPPORT -> Directory.EXPORT_SUPPORT_SAME_ACCOUNT_ONLY
                    Directory.SHORTCUT_SUPPORT -> Directory.SHORTCUT_SUPPORT_NONE
                    else -> null
                }
            }?.let { cursor.addRow(it) }
            return cursor
        }
        PHONE_LOOKUP -> {
            userRepository?.let { userRepo ->
                val phoneNumber = uri.pathSegments[1]
                val cursor = MatrixCursor(projection)
                val user = runBlocking(Dispatchers.IO) { userRepo.getUser(phoneNumber) }
                user?.let { u ->
                    projection?.map { column ->
                        when (column) {
                            PhoneLookup._ID -> -1
                            PhoneLookup.DISPLAY_NAME -> u.fullName
                            PhoneLookup.LABEL -> u.phoneLabel
                            else -> null
                        }
                    }?.let { cursor.addRow(it) }
                }
                return cursor
            }
        }
    }
    return null
}