DEV Community

Vishwajith Shettigar
Vishwajith Shettigar

Posted on

Understanding ContentProvider and ContentResolver in Android with Kotlin.

from Epic Android Concepts series.

What is ContentProvider and ContentResolver ?
A ContentProvider acts as an abstraction layer that provides a standard interface for accessing and managing data in one application, making it accessible to other applications. It is particularly useful for sharing data across different applications or within different components of the same application. ContentProvider can manage various data sources, including SQLite databases, files, or even network data.

A ContentResolver is an interface that provides methods for interacting with ContentProviders. It acts as a client to communicate with ContentProviders to perform data operations. When an application needs to access data exposed by a ContentProvider, it uses a ContentResolver.

Image description

Below is an example of how exactly a ContentProvider and ContentResolver work, which will help you understand easily :)

Let’s take a scenario: an app named “Chat Me,” which is essentially a chat app. It needs contacts from your phone to display them to you so that you can click on them and chat with your friends. But how does it get that contact list? This is where ContentResolvers come into play. The “Chat Me” app uses ContentResolvers to fetch contacts from your phone’s contacts app and display them to you. But you might have one big question: how does your phone’s contact app allow other apps to fetch contacts? It does this using a ContentProvider.

A ContentProvider can serve its own app’s components as well. If it were only serving its own components, we would never use a ContentProvider; instead, we would use some kind of database like RoomDB, SQLite, etc. However, there is a catch: ContentProviders often use a database, such as SQLite, to manage the data.

Before diving into the implementation of ContentProvider, let’s learn about Content URIs (Uniform Resource Identifiers).
A Content URI is a special type of URI used by Android’s ContentProvider to identify data. Unlike regular URIs used for accessing files or web resources, Content URIs are used specifically to interact with data stored within a ContentProvider.

Structure of a Content URI

content://<authority>/<path>/<id>

Scheme: Always content:// for content providers.

Authority: Identifies the ContentProvider. It is a unique string, typically in the form of a package name or custom identifier (e.g., com.example.myprovider).

Path: Specifies the type of data (e.g., contacts). It can also include specific identifiers (e.g., contacts/1).

ID (Optional): Specifies a specific record (e.g., 1). If omitted, it usually refers to a collection of records.

But why are we learning about Content URIs?

In your ContentProvider, you define which URIs are supported and what data they correspond to. This is typically done using a UriMatcher. When an app queries the ContentProvider, it uses a Content URI to specify what data it needs.

For example, when the “Chat me” app retrieves all contacts from your phone’s contacts app, it uses the Content URI content://com.example.myprovider/contacts with the ContentResolver. In this URI, content is the scheme, com.example.myprovider is the authority, and contacts is the path, which essentially represents the table name.

In your phone’s contacts app, the URI content://com.example.myprovider/contacts must be defined in the ContentProvider to enable data sharing with other apps like "Chat me."

content://com.example.myprovider/contacts/1 represents a single record.
content://com.example/myprovider/contacts represents the whole table.
If you want to build an app that shares contacts with other apps like “Chat me” or with other components of the same app, you need to implement a ContentProvider. On the other hand, if your app only needs to access data from a ContentProvider provided by another app, you only need to implement a ContentResolver.

Now, let’s learn how to implement a ContentProvider using SQLite to support data sharing with apps like "Chat me."

Lets write code for sqlite database helper

class DatabaseHelper(context: Context) : SQLiteOpenHelper(
  context, DATABASE_NAME, null, DATABASE_VERSION
) {

  companion object {
    const val DATABASE_NAME = "mydatabase"
    const val DATABASE_VERSION = 1
    const val TABLE_NAME = "contacts"
    const val TABLE_CREATE_QUERY = "create table $TABLE_NAME" +
      "(_id INTEGER PRIMARY KEY NOT NULL," +
      "name TEXT," +
      "phone TEXT)"
  }

  override fun onCreate(db: SQLiteDatabase?) {
    db?.execSQL(TABLE_CREATE_QUERY)
  }

  override fun onUpgrade(db: SQLiteDatabase?, p1: Int, p2: Int) {
    db?.execSQL("DROP TABLE IF EXISTS $TABLE_NAME")
  }
}
Enter fullscreen mode Exit fullscreen mode

The DatabaseHelper class will be used by the ContentProvider to access and manage data. We will discuss when the onCreate() method is called and how the database will create a table named "contacts" later.

Here’s how you can create the MyContentProvider class:

class MyContentProvider : ContentProvider() {
  companion object {
    const val AUTHORITY = "com.example.mycontentprovider"
    val CONTENT_URI: Uri = Uri.parse("content://$AUTHORITY/$TABLE_NAME")
    const val MYTABLE = 1
    const val MYTABLE_ID = 2

    val uriMatcher = UriMatcher(UriMatcher.NO_MATCH).apply {
      addURI(AUTHORITY, TABLE_NAME, MYTABLE)
      addURI(AUTHORITY, "$TABLE_NAME/#", MYTABLE_ID)
    }
  }

  private lateinit var databaseHelper: DatabaseHelper

  override fun onCreate(): Boolean {
    databaseHelper = DatabaseHelper(context!!)
    return true
  }

  override fun query(
    uri: Uri,
    columns: Array<out String>?,
    selection: String?,
    selectionArgs: Array<out String>?,
    sortOrder: String?,
  ): Cursor? {
    val db = databaseHelper.readableDatabase

    when (uriMatcher.match(uri)) {
      MYTABLE -> {
        return db.query(TABLE_NAME, columns, selection, selectionArgs, null, null, sortOrder)
      }
      MYTABLE_ID -> {
        return db.query(TABLE_NAME, columns, selection, selectionArgs, null, null, sortOrder)
      }
    }
    return null
  }

  override fun getType(uri: Uri): String {
    return when (uriMatcher.match(uri)) {
      MYTABLE -> "vnd.android.cursor.dir/vnd.com.example.$TABLE_NAME"
      MYTABLE_ID -> "vnd.android.cursor.item/vnd.com.example.$TABLE_NAME"
      else -> throw IllegalArgumentException("Unsupported URI: $uri")
    }
  }

  override fun insert(uri: Uri, values: ContentValues?): Uri {
    val db = databaseHelper.writableDatabase
    val id = db.insert(TABLE_NAME, null, values)
    context?.contentResolver?.notifyChange(uri, null)
    val resultUri = ContentUris.withAppendedId(CONTENT_URI, id)
    context?.contentResolver?.notifyChange(resultUri, null, true)
    return resultUri
  }

  override fun delete(uri: Uri, selection: String?, selectionArgs: Array<String>?): Int {
    val db = databaseHelper.writableDatabase
    val count = db.delete(TABLE_NAME, selection, selectionArgs)
    context?.contentResolver?.notifyChange(uri, null)
    return count
  }

  override fun update(
    uri: Uri, values: ContentValues?, selection: String?,
    selectionArgs: Array<String>?,
  ): Int {
    val db = databaseHelper.writableDatabase
    val count = db.update(TABLE_NAME, values, selection, selectionArgs)
    context?.contentResolver?.notifyChange(uri, null)
    return count
  }
}
Enter fullscreen mode Exit fullscreen mode

This class extends the ContentProvider class and overrides the onCreate(), query(), insert(), update(), delete(), and getType() methods.

In the companion object, we defined all the necessary constants, such as:

const val AUTHORITY = "com.example.mycontentprovider"
    val CONTENT_URI: Uri = Uri.parse("content://$AUTHORITY/$TABLE_NAME")
    const val MYTABLE = 1
    const val MYTABLE_ID = 2
Enter fullscreen mode Exit fullscreen mode

Now, our URI looks like content://com.example.mycontentprovider/contacts

val uriMatcher = UriMatcher(UriMatcher.NO_MATCH).apply {
      addURI(AUTHORITY, TABLE_NAME, MYTABLE)
      addURI(AUTHORITY, "$TABLE_NAME/#", MYTABLE_ID)
    }
Enter fullscreen mode Exit fullscreen mode

The UriMatcherwill be explained when we discuss retrieving all records from the table and specific records from the table.

1. onCreate()
In the onCreate() method, we initialize the DatabaseHelper so that we can use it to access the database in later queries.

When is onCreate() called exactly?

When your application starts, the ContentProvider is not immediately created. Instead, it is instantiated lazily. The ContentProvider's onCreate() method is called the first time it is accessed via a ContentResolver. This means that if any part of your application or another application tries to access the content provider's data for the first time (using methods like query(), insert(), update(), delete(), or getType()), the onCreate() method will be triggered.

2. query()
This method is called when another app, or a part of the same application, wants to retrieve records. In the query() method, you can pass a URI, specify the column names you want to retrieve, provide a where clause (such as the selection column), and supply selection arguments to retrieve specific rows. Essentially, this is like executing an SQL query.

Inside this method, we instantiate the database to perform the query operations.
val db = databaseHelper.readableDatabase

At this point, the DatabaseHelper class’s onCreate() method will be called, and it will create a table named "contacts". This code is present in all other methods such as insert, delete, and update, but these methods do not create a new table if it already exists. Instead, they obtain a reference to the database and execute the appropriate queries.

return db.query(TABLE_NAME, columns, selection, selectionArgs, null, null, sortOrder)
Enter fullscreen mode Exit fullscreen mode

let’s dive into UriMatcher.

MYTABLE=1
MYTABLE_ID=2
Enter fullscreen mode Exit fullscreen mode
val uriMatcher = UriMatcher(UriMatcher.NO_MATCH).apply {
addURI(AUTHORITY, TABLE_NAME, MYTABLE) //addURI( "com.example.mycontentprovider","contacts",1)

addURI(AUTHORITY, "$TABLE_NAME/#", MYTABLE_ID) //addURI( "com.example.mycontentprovider","contacts/1",2)
// # will match with any id(eg: 1,2,3, etc)
    }
Enter fullscreen mode Exit fullscreen mode

We are adding URIs to UriMatcher so that later we can use it to match URIs with codes like MYTABLE and MYTABLE_ID. Why do we need to match URIs with codes?

Let’s say the “Chat me” app wants only a single record, so it uses ContentResolver to write a query like this.

// we will discuss more about this later in contentResolver part.
string id=1  
val selection = "_id = ?"
val selectionArgs = arrayOf(id)
val CONTENT_URI="com.example.mycontentprovider/contacts/1"
val cursor = contentResolver.query(CONTENT_URI, null, selection, selectionArgs, null)
Enter fullscreen mode Exit fullscreen mode

This will call the query method in the ContentProvider. In this case, we use UriMatcher in our ContentProvider's query method to determine exactly what is being requested. The provided URI will match with MYTABLE_ID.

when (uriMatcher.match(uri)) {
      MYTABLE -> {
        return db.query(TABLE_NAME, columns, selection, selectionArgs, null, null, sortOrder)
      }
      MYTABLE_ID -> {
     // This query will gets executed
        return db.query(TABLE_NAME, columns, selection, selectionArgs, null, null, sortOrder)
      }
    }
Enter fullscreen mode Exit fullscreen mode

Now, let’s say the “Chat me” app wants to retrieve the whole table.

val CONTENT_URI="com.example.mycontentprovider/contacts"
val cursor = contentResolver.query(CONTENT_URI, null, null, null, null)`

Provided uri will match with MYTABLE. and executes.
`when (uriMatcher.match(uri)) {
      MYTABLE -> {
        // This query will gets executed
        return db.query(TABLE_NAME, columns, selection, selectionArgs, null, null, sortOrder)
      }
      MYTABLE_ID -> {
        return db.query(TABLE_NAME, columns, selection, selectionArgs, null, null, sortOrder)
      }
    }
Enter fullscreen mode Exit fullscreen mode

The URI passed to the ContentProvider's query method serves two purposes: it helps identify the correct data source and notifies observing processes when the data changes. This will be elaborated upon later in the context of insert, update, and delete operations. If you have multiple tables and corresponding URIs, the UriMatcher ensures that data is fetched from the correct table. However, retrieving the correct data ultimately depends on the selection arguments and column names you provide.

3. insert()
This method is used to insert data into the table. It returns the row ID where the data was inserted, and you can append this row ID to the URI to notify observing apps that this particular data is new or has changed. Observing apps will extract the row ID from the URI and use it as a selection argument to fetch the exact row using the query method.

override fun insert(uri: Uri, values: ContentValues?): Uri {
    // Get writable database instance
    val db = databaseHelper.writableDatabase

    // Insert the values into the specified table and get the row ID of the newly inserted row
    val id = db.insert(TABLE_NAME, null, values)

    // Append the row ID to the CONTENT_URI to create a URI for the new row
    val resultUri = ContentUris.withAppendedId(CONTENT_URI, id)
    // resultUri will look like "content://com.example.mycontentprovider/contacts/0"

    // Notify all observing apps or components that the data has changed
    // Provide resultUri so they can query the updated data
    context?.contentResolver?.notifyChange(resultUri, null, true)

    return resultUri
}
Enter fullscreen mode Exit fullscreen mode

4. update()
This method is used to update data in the table. You can pass selection arguments to specify exactly what to update, and it will notify all observing apps of the changes.

override fun update(
    uri: Uri, values: ContentValues?, selection: String?,
    selectionArgs: Array<String>?
): Int {
    val db = databaseHelper.writableDatabase
    // Updates records in the table and returns the count of updated records.
    val count = db.update(TABLE_NAME, values, selection, selectionArgs)
    // Notifies all observing apps that the data at the specified URI has changed.
    context?.contentResolver?.notifyChange(uri, null)
    return count
}
Enter fullscreen mode Exit fullscreen mode

5. delete()
This method used to delete data from table.

override fun delete(uri: Uri, selection: String?, selectionArgs: Array<String>?): Int {
    val db = databaseHelper.writableDatabase
    // Deletes records from the table and returns the count of deleted records.
    val count = db.delete(TABLE_NAME, selection, selectionArgs)
    // Notifies all observing apps that the data at the specified URI has changed.
    context?.contentResolver?.notifyChange(uri, null)
    return count
}
Enter fullscreen mode Exit fullscreen mode

6. getType()
The getType() method in a ContentProvider is used to return the MIME type of the data at the given URI. This is useful for clients (such as other applications) to understand the type of data they are dealing with and how to handle it.

The getType() method typically returns a MIME type string based on the pattern of the URI. The MIME type format follows the structure:

  • vnd.android.cursor.dir/vnd..: Indicates a directory of items.

  • vnd.android.cursor.item/vnd..: Indicates a single item.

 override fun getType(uri: Uri): String {
    return when (uriMatcher.match(uri)) {
      MYTABLE -> "vnd.android.cursor.dir/vnd.com.example.$TABLE_NAME"
      MYTABLE_ID -> "vnd.android.cursor.item/vnd.com.example.$TABLE_NAME"
      else -> throw IllegalArgumentException("Unsupported URI: $uri")
    }
  }
Enter fullscreen mode Exit fullscreen mode

To allow other apps like “Chat Me” to access data from your ContentProvider, you need to declare the ContentProvider in your AndroidManifest.xml file. Here’s how you can do it:

<application
....
....
>
<provider
    android:name="com.example.contentproviders.MyContentProvider"  <!-- This is the path to your ContentProvider class. -->
    android:authorities="com.example.mycontentprovider"  <!-- Authority name. -->
    android:exported="true" />  <!-- Whether the ContentProvider is accessible to other apps. -->
</application>
Enter fullscreen mode Exit fullscreen mode

We’ve completed the implementation of the ContentProvider. Next, we’ll explore how different apps, like “Chat me,” can access data from a ContentProvider.

Before diving into the implementation of ContentResolver and learning how to access data from a ContentProvider, it’s important to understand how applications like “Chat me” obtain the URIs they use to access data. Typically, apps retrieve these URIs from the documentation provided by the content provider.

The documentation usually outlines the URIs available for data access, including how to structure them and what data they can access. This documentation is crucial for ensuring that the app queries the correct data and interacts with the ContentProvider properly.

In summary, the URIs act as a bridge between the app and the ContentProvider, allowing the app to request and retrieve the appropriate data. By consulting the documentation, developers can understand how to construct these URIs and use them effectively in their applications.

Implementing contentResolver.

Image description

Lets say you want all the contacts, you write query like below.

 private fun getAllContacts() :MutableList<Contact> {
val CONTENT_URI=Uri.parse("com.example.mycontentprovider/contacts")
val contactList=mutablelistof()
    val cursor = contentResolver.query(CONTENT_URI, null, null, null, null)

    if (cursor != null) {
      while (cursor.moveToNext()) {

        val id = cursor.getInt(cursor.getColumnIndex("_id"))
        val name = cursor.getString(cursor.getColumnIndex("name"))
        val phone = cursor.getString(cursor.getColumnIndex("phone"))

        ccontactList.add(
          Contact(id, name, phone)
        )
      }
      cursor.close()
    }
  }
Enter fullscreen mode Exit fullscreen mode

val cursor = contentResolver.query(CONTENT_URI, null, null, null, null): This line queries the ContentProvider using the specified CONTENT_URI. The query method returns a Cursor object that can be used to iterate through the results. The null arguments indicate that all columns and rows are being requested without any specific filtering or sorting.

while (cursor.moveToNext()) { ... }: This loop iterates through each row in the cursor. The moveToNext() method moves the cursor to the next row and returns true if there is a next row, and false if there are no more rows.

val id = cursor.getInt(cursor.getColumnIndex("_id")): This line retrieves the integer value of the _id column from the current row.
val name = cursor.getString(cursor.getColumnIndex("name")): This line retrieves the string value of the name column from the current row.
val phone = cursor.getString(cursor.getColumnIndex("phone")): This line retrieves the string value of the phone column from the current row
cursor.close(): This line closes the cursor to release its resources. It is important to close the cursor to avoid memory leaks.

Lets say you want only single contacts you write query like below.

private fun getChangedContact(id: String) {
// id = 0
val CONTENT_URI=Uri.parse("com.example.mycontentprovider/contacts/$id")
  val selection = "_id = ?"
  val selectionArgs = arrayOf(id)
  val cursor = contentResolver.query(CONTENT_URI, null, selection, selectionArgs, null)
  if (cursor != null) {
    while (cursor.moveToNext()) {
      val id = cursor.getInt(cursor.getColumnIndex("_id"))
      val name = cursor.getString(cursor.getColumnIndex("name"))
      val phone = cursor.getString(cursor.getColumnIndex("phone"))

       // Here we got single contact.
        val contact=Contact(id, name, phone)

    }
    cursor.close()
  }
}
Enter fullscreen mode Exit fullscreen mode

Insert new contact

ContentValues Preparation: A ContentValues object is prepared with the name and phone values.
Insert Operation: contentResolver.insert(CONTENT_URI, contentValues) is called to insert the new contact into the ContentProvider.
ID Generation: SQLite will automatically generate a unique ID for the new row.
New URI: The insert method returns a URI pointing to the newly inserted row, which includes the new auto-generated ID.

private fun insertNewContact(name: String, phoneNumber: String) {
val CONTENT_URI=Uri.parse("com.example.mycontentprovider/contacts")
  val contentValues = ContentValues().apply {
    put("name", name)
    put("phone", phoneNumber)
  }
  val newUri = contentResolver.insert(CONTENT_URI, contentValues)
}
Enter fullscreen mode Exit fullscreen mode

Update Existing Contact

The updateContact function updates a contact in the database using the provided contact ID. Here's a concise summary:

Constructs the URI: Builds the URI for the specific contact to be updated, using the base content provider URI and the contact ID.
Creates ContentValues: Prepares a ContentValues object with the new name and phone number from the input fields.
Defines Selection Criteria: Specifies the selection criteria to identify the contact to be updated by its _id.
Executes Update: Calls the update method on the contentResolver, passing the URI, ContentValues, selection criteria, and selection arguments.
Updates the Contact: The database updates the contact with the provided new values, and the number of rows affected is returned and stored in updateCount.

This function ensures that the contact with the given ID has its name and phone number updated in the database using the ContentProvider

private fun updateContact(id: Int) {
    val recordId = id
val CONTENT_URI=Uri.parse("com.example.mycontentprovider/contacts/$recordId")
    val updateValues = ContentValues().apply {
      put("name", binding.name.text.toString())
      put("phone", binding.phoneNumber.text.toString())
    }

    val selection = "_id = ?"
    val selectionArgs = arrayOf(recordId.toString())
    val updateCount = contentResolver.update(CONTENT_URI!!, updateValues, selection, selectionArgs)

  }
Enter fullscreen mode Exit fullscreen mode

Delete contact
The deleteContact function deletes a contact from the database using the provided contact ID.

Constructs the URI: Builds the base URI for the contacts table.
Defines Selection Criteria: Specifies the selection criteria to identify the contact to be deleted by its _id.
Executes Delete: Calls the delete method on the contentResolver, passing the URI, selection criteria, and selection arguments.
Deletes the Contact: The database deletes the contact with the provided ID, and the number of rows affected is returned and stored in count.
This function ensures that the contact with the given ID is deleted from the database using the ContentProvider.

private fun deleteContact(id: Int) {
val CONTENT_URI=Uri.parse("com.example.mycontentprovider/contacts")
  val selection = "_id = ?"
  val selectionArgs = arrayOf(id.toString())
 val count = contentResolver.delete(CONTENT_URI, selection, selectionArgs)
}
Enter fullscreen mode Exit fullscreen mode

Observe changes in contentProvider’s data

private lateinit var contentObserver: ContentObserver

  init  {
      contentObserver = object : ContentObserver(Handler()) {

      override fun onChange(selfChange: Boolean, uri: Uri?) {
        super.onChange(selfChange, uri)
        if (uri != null) {
          // Data change detected.
         // Here also we are using uriMatcher, you need to define first, like we defined in contentProvider's part.
          when (uriMatcher.match(uri)) {
            MYTABLE -> {
              getAllContacts()
            }
            MYTABLE_ID -> {
              getChangedContact(ContentUris.parseId(uri).toString())
            }
          }
        }
      }
    }

    // Register a ContentObserver to listen for changes 
    contentResolver.registerContentObserver(
      CONTENT_URI,
      true,
      contentObserver
     )

}
Enter fullscreen mode Exit fullscreen mode

The provided code initializes a ContentObserver to monitor changes in the content provider's data and take appropriate actions based on the URI of the changed data.

1. Initialize ContentObserver:

contentObserver = object : ContentObserver(Handler()) { ... }: Creates a ContentObserver instance with a Handler to handle callbacks.

2. Override onChange Method:

override fun onChange(selfChange: Boolean, uri: Uri?) { ... }: The onChange method is overridden to handle data changes. If a URI is provided:
when (uriMatcher.match(uri)) { ... }: Checks which URI was changed:
MYTABLE -> getAllContacts(): If the change is for the entire table, it calls getAllContacts() to fetch all contacts.
MYTABLE_ID -> getChangedContact(ContentUris.parseId(uri).toString()): If the change is for a specific record, it calls getChangedContact with the ID of the changed record.

3. Register ContentObserver:

contentResolver.registerContentObserver(CONTENT_URI, true, contentObserver): Registers the ContentObserver to listen for changes to the specified CONTENT_URI and all its descendants (true).

In this article, we’ve explored the roles of ContentProvider and ContentResolver in Android development, learned about Content URIs, and implemented a ContentProvider with SQLite in Kotlin. By understanding these concepts, you can efficiently manage and share data within and across your applications. You can find the full example in this github repository. In this example, we accessed ContentProvider's data within the same app's components. You can try writing ContentResolver code in a different app to access data from the ContentProvider. Make sure both apps implementing ContentProvider and ContentResolver are installed on your device.

Thank you for reading! I hope you found this guide helpful. Stay tuned for more articles in the Epic Android Concepts series. If you have any questions or feedback, feel free to leave a comment below. I would love to hear from you so that I can improve my explanations in future articles in the Epic Android Concepts series.

Happy coding!

Top comments (0)