Content Provider & Content Resolver in Android | Jetpack Compose | Kotlin

Dear learners, you must have listen about these two terms when you started your development journey in Android. Today we will learn about the two confusing terms – Content Provider & Content Resolver. In this article we learn about following topics:

  1. Difference B/w URI & URL & Uri
  2. What is SQlite Database
  3. Difference B/w Content Provider & Content Resolver
  4. How to create Content Provider

The main role of content provider in your app is to provide a central repository of data which is accessible by other apps as well as solves its own requirement. For example consider our Contacts app which uses Content Provider to provides contacts to other apps along with it uses for its own functioning. A contact app stores its data in SQlite database which maintain the records of all the contacts in your device. Also when other apps need to fetch contacts of the device then they use the content provider of the Contacts app and fetches all the contacts. Similarly other examples of app which use Content Providers are Calls App, SMS app, Notes app etc.

Before start learning about Content Resolver and Content Provider, we need to understand some other important terms for more clarification like URI, URL & Uri etc.

Understanding URI, URL, and Uri

URI (Uniform Resource Identifier), URL (Uniform Resource Locator), and Uri (Android-specific class) are all related to identifying resources, but they serve different purposes and are used in different contexts. Let’s break them down.

1. URI (Uniform Resource Identifier)

A URI is a general term that identifies a resource either by location, name, or both. It provides a way to reference a resource in a uniform way but doesn’t necessarily provide access to the resource.

A URI has two types:

  1. URN (Uniform Resource Name): Identifies a resource by name, independent of its location.
    • Example: urn:isbn:9780134685991 (An identifier for a book by ISBN number).
  2. URL (Uniform Resource Locator): A subset of URI that specifies both where a resource is located and how to access it.

Examples:

  • http://example.com/resource (This is a URL, which is also a URI.)
  • ftp://example.com/file (An FTP location, also a URI.)
  • urn:isbn:9780134685991 (A name-based identifier, not a URL.)

2. URL (Uniform Resource Locator)

Definition:

A URL is a specific type of URI that not only identifies a resource but also provides the means to access it, usually by specifying the protocol (e.g., HTTP, FTP).

  • Always contains a scheme (protocol) and location information.
  • Indicates how to access the resource (e.g., via HTTP, HTTPS, FTP).

A URL typically includes:

  1. Scheme: Specifies the protocol (e.g., http, https, ftp).
  2. Host: The domain or IP address of the server (e.g., example.com).
  3. Path: The specific resource path on the server (e.g., /index.html).
  4. Query Parameters (optional): Extra data sent to the server (e.g., ?id=123).

Examples:

  • https://www.example.com/index.html (A webpage accessed via HTTPS.)
  • ftp://files.example.com/download.zip (A file accessed via FTP.)

3. Uri (Android-specific Class)

Definition:

In Android, Uri is a Kotlin/Java class that represents a URI reference. It is used extensively to handle resource identifiers in the Android system, such as content URIs (content://), file paths (file://), or web URLs.

  • Designed specifically for Android.
  • Makes it easier to manipulate and parse URIs in Android apps.

Common Use Cases:

  1. Content URIs: Access data from content providers.
    • Example: content://com.example.provider/users
  2. File URIs: Access files on the device.
    • Example: file:///storage/emulated/0/DCIM/image.jpg
  3. Network URLs: Access web resources.
    • Example: https://example.com/api/dat

Use Cases in Android

  1. Access Content Providers:
val uri: Uri = Uri.parse("content://com.example.provider/users")
val cursor = contentResolver.query(uri, null, null, null, null)

2. Access Files:

val uri: Uri = Uri.parse("file:///storage/emulated/0/Download/example.txt")

3. Parse URLs:

val uri: Uri = Uri.parse("https://www.example.com")
println(uri.scheme) // "https"
println(uri.host)   // "www.example.com"

So, this is all about the URI, URL and Uri. Now we will learn about how android stores data behind the scenes.

How android Stores data- SQLite Database

Android uses SQlite data which is a Relational Database and uses SQL for data manipulations. Android has these all types of storage where it stores all the types based on type of data, size of data, accessibility of data etc. But when comes to storing structured data android uses SQLite database which is a offline first and very fast and easy to manage database and a lite version of MySQL database.

Storage TypeUse CaseAccessExample
Internal StoragePrivate app dataOnly appSettings, temp files
External StoragePublic/shared filesApp + othersPhotos, videos
SharedPreferencesLightweight key-value pairsOnly appUser preferences
SQLite/RoomStructured relational dataOnly appTo-do list, contacts
CacheTemporary dataOnly appImage cache
MediaStoreShared media filesApp + othersPublic images, videos
Cloud StorageData sync/backupApp + cloudFirebase Storage, Google Drive

Relational Database

A relational database is a type of database that organizes data into rows and columns, which collectively form a table where the data points are related to each other. Data is typically structured across multiple tables, which can be joined together via a primary key or a foreign key.

For example, imagine your company maintains a database table with customer information, which contains company data at the account level. There may also be a different table, which describes all the individual transactions that align to that account. Together, these tables can provide information about the different industries that purchase a specific software product.

Relational Database vs. Relational Database management system

While a relational database organizes data based off a relational data model, a relational database management system (RDBMS) is a more specific reference to the underlying database software that enables users to maintain it. These programs allow users to create, update, insert, or delete data in the system. Examples of popular RDBMS systems include MySQL, PostgreSQL

What is SQL?

Structured Query Language (SQL) is the standard programming language for interacting with relational database management systems, allowing database administrator to add, update, or delete rows of data easily. Originally known as SEQUEL, it was simplified to SQL due to a trademark issue. SQL queries also allows users to retrieve data from databases using only a few lines of code. Given this relationship, it’s easy to see why relational databases are also referred to as “SQL databases” at times.

Why Use SQL?

  1. Data Storage: Store and organize data in tables.
  2. Data Retrieval: Query and fetch specific data.
  3. Data Manipulation: Add, update, or delete data.
  4. Data Analysis: Aggregate and filter data for reporting.
  5. Database Management: Create and modify database structures.

Basic SQL Operations

Here’s how you can use SQL:

1. Create a Database
sqlCopy codeCREATE DATABASE School;
2. Create a Table
sqlCopy codeCREATE TABLE Students (
ID INT PRIMARY KEY,
Name VARCHAR(50),
Age INT,
Grade VARCHAR(5)
);
3. Insert Data into a Table
sqlCopy codeINSERT INTO Students (ID, Name, Age, Grade) 
VALUES (1, 'Alisha', 20, 'A');
4. Retrieve Data (SELECT Query)
  • Fetch all records:sqlCopy codeSELECT * FROM Students;
  • Fetch specific columns:sqlCopy codeSELECT Name, Grade FROM Students;
  • Add a condition:sqlCopy codeSELECT * FROM Students WHERE Grade = 'A';
5. Update Data
sqlCopy codeUPDATE Students 
SET Grade = 'B'
WHERE ID = 1;
6. Delete Data
sqlCopy codeDELETE FROM Students WHERE ID = 1;
7. Aggregate Functions
  • Count records:sqlCopy codeSELECT COUNT(*) FROM Students;
  • Find the average age:sqlCopy codeSELECT AVG(Age) FROM Students;
8. Join Tables
sqlCopy codeSELECT Students.Name, Courses.CourseName 
FROM Students 
JOIN Courses 
ON Students.ID = Courses.StudentID;

So, till now have a basic idea about what are these all terms and their use cases, now we learn how to android use them for its own data management and what relation does content resolver and content provider have with all these.

Difference b/w Content Provider & Content Resolver

What is Content Resolver

The Content Resolver is the single, global instance in your application that provides access to your (and other applications’) content providers. The Content Resolver behaves exactly as its name implies: it accepts requests from clients, and resolves these requests by directing them to the content provider with a distinct authority. To do this, the Content Resolver stores a mapping from authorities to Content Providers. This design is important, as it allows a simple and secure means of accessing other applications’ Content Providers.

The Content Resolver includes the CRUD (create, read, update, delete) methods corresponding to the abstract methods (insert, query, update, delete) in the Content Provider class. The Content Resolver does not know the implementation of the Content Providers it is interacting with (nor does it need to know); each method is passed an URI that specifies the Content Provider to interact with.

What is Content Provider

Whereas the Content Resolver provides an abstraction from the application’s Content Providers, Content Providers provide an abstraction from the underlying data source (i.e. a SQLite database). They provide mechanisms for defining data security (i.e. by enforcing read/write permissions) and offer a standard interface that connects data in one process with code running in another process.

Content Providers provide an interface for publishing and consuming data, based around a simple URI addressing model using the content:// schema. They enable you to decouple your application layers from the underlying data layers, making your application data-source agnostic by abstracting the underlying data source.

Understand these two with a simple analogy

Suppose you go to groceries stores to buy some items for you home need. In the shop you interact with Counter person and told him your requirements of items, then he tells the helper to brought your items from the store, then he brought items one by one as you have asked and give it to the Counter person and then the counter person will give that to you. So here the Counter Person will be work as Content Resolver and the helper will work as Content Provider as Content Resolver(Counter person) resolves your query by asking the required item(Data) from the Content Provider(Helper) in the store(App).

How to create our custom Content Provider

Now lets begin our journey to create our own content provider which will provide simple user data which we will insert or other apps inserts in the app. As you already know that we need to make a SQLite database in our app which will stores our apps data that in turn we will provide to other apps.

In this app will make a simple database which contains only one table which stores id and name field. So to make any class a Provider class we need to extends it with ContentProvider() and you need to override some function which helps you to manipulate data in the content provider. When you make a call to content resolvers then he will call these function as required.

class class UserProvider:ContentProvider() {
    override fun onCreate(): Boolean {
        TODO("Not yet implemented")
    }

    override fun query(
        p0: Uri,
        p1: Array<out String>?,
        p2: String?,
        p3: Array<out String>?,
        p4: String?
    ): Cursor? {
        TODO("Not yet implemented")
    }

    override fun getType(p0: Uri): String? {
        TODO("Not yet implemented")
    }

    override fun insert(p0: Uri, p1: ContentValues?): Uri? {
        TODO("Not yet implemented")
    }

    override fun delete(p0: Uri, p1: String?, p2: Array<out String>?): Int {
        TODO("Not yet implemented")
    }

    override fun update(p0: Uri, p1: ContentValues?, p2: String?, p3: Array<out String>?): Int {
        TODO("Not yet implemented")
    }
}

Now we need to define some variables and constants which we need in our Content Provider class. lets define these in companion object.

companion object {
        // defining authority so that other application can access it
        const val PROVIDER_NAME = "com.example.learngit"

        // defining content URI
        const val URL = "content://$PROVIDER_NAME/users"

        // parsing the content URI
        val CONTENT_URI = Uri.parse(URL)
        const val id = "id"
        const val name = "name"
        const val uriCode = 1
        var uriMatcher: UriMatcher? = null
        private val values: HashMap<String, String>? = null

        // declaring name of the database
        const val DATABASE_NAME = "UserDB"

        // declaring table name of the database
        const val TABLE_NAME = "Users"

        // declaring version of the database
        const val DATABASE_VERSION = 1

        // sql query to create the table
        const val CREATE_DB_TABLE =
            (" CREATE TABLE " + TABLE_NAME
                    + " (id INTEGER PRIMARY KEY AUTOINCREMENT, "
                    + " name TEXT NOT NULL);")

        init {
            // to match the content URI
            // every time user access table under content provider
            uriMatcher = UriMatcher(UriMatcher.NO_MATCH)

            // to access whole table
            uriMatcher!!.addURI(
                PROVIDER_NAME,
                "users",
                uriCode
            )

            // to access a particular row
            // of the table
            uriMatcher!!.addURI(
                PROVIDER_NAME,
                "users/*",
                uriCode
            )
        }
    }

Now lets make a custom class which will make our database. This class will extends SLiteOpenHelper helper class which provides methods to create or update databases. here you need to override two function namely onCreate and onUpgrade.

// creating object of database
// to perform query
private var db: SQLiteDatabase? = null

// creating a database
private class DatabaseHelper  // defining a constructor
    (context: Context?) : SQLiteOpenHelper(
    context,
    DATABASE_NAME,
    null,
    DATABASE_VERSION
) {
    // creating a table in the database
    override fun onCreate(db: SQLiteDatabase) {
        db.execSQL(CREATE_DB_TABLE)
    }

    override fun onUpgrade(
        db: SQLiteDatabase,
        oldVersion: Int,
        newVersion: Int
    ) {

        // sql query to drop a table
        // having similar name
        db.execSQL("DROP TABLE IF EXISTS $TABLE_NAME")
        onCreate(db)
    }
}

After this we will write code for each override function and understand with more clarity. Our first override function is onCreate in which we only initialize our database.

// creating the database
override fun onCreate(): Boolean {
    val context = context
    val dbHelper = DatabaseHelper(context)
    db = dbHelper.writableDatabase
    return if (db != null) {
        true
    } else false
}

now comes to our 2nd and main query function which will take some arguments and return a Cursor object which contains you queried data. our query function takes following arguments

// Queries the UserDictionary and returns results
cursor = contentResolver.query(
        CONTENT_URI,                        // The content URI of the words table
        projection,                        // The columns to return for each row
        selectionClause,                   // Selection criteria
        selectionArgs.toTypedArray(),      // Selection criteria
        sortOrder                          // The sort order for the returned rows
)

Table 2 shows how the arguments to query(Uri,projection,selection,selectionArgs,sortOrder) match a SQL SELECT statement:

query() argumentSELECT keyword/parameterNotes
UriFROM table_nameUri maps to the table in the provider named table_name.
projectioncol,col,col,...projection is an array of columns that is included for each row retrieved.
selectionWHERE col = valueselection specifies the criteria for selecting rows.
selectionArgsNo exact equivalent. Selection arguments replace ? placeholders in the selection clause.
sortOrderORDER BY col,col,...sortOrder specifies the order in which rows appear in the returned Cursor.

Content URIs

content URI is a URI that identifies data in a provider. Content URIs include the symbolic name of the entire provider—its authority—and a name that points to a table—a path. When you call a client method to access a table in a provider, the content URI for the table is one of the arguments.

In the preceding lines of code, the constant CONTENT_URI contains the content URI of the User Data Provider’s Users table. The Content Resolver object parses out the URI’s authority and uses it to resolve the provider by comparing the authority to a system table of known providers. The Content Resolver can then dispatch the query arguments to the correct provider.

The Content Provider uses the path part of the content URI to choose the table to access. A provider usually has a path for each table it exposes.

In the previous lines of code, the full URI for the Users table is:

content://com.example.learngit/users
  • The content:// string is the scheme, which is always present and identifies this as a content URI.
  • The com.example.learngit string is the provider’s authority(normally the package name of the provide app).
  • The users string is the table’s path.

Many providers let you access a single row in a table by appending an ID value to the end of the URI. For example, to retrieve a row whose _ID is 4 from the User Dictionary Provider, you can use this content URI:

val singleUri: Uri = ContentUris.withAppendedId(CONTENT_URI, 4)

Now write the actual implementation of our query function to fetch data from the data base.

override fun query(
        uri: Uri, projection: Array<String>?, selection: String?,
        selectionArgs: Array<String>?, sortOrder: String?
    ): Cursor? {
        var sortOrder = sortOrder
        val qb = SQLiteQueryBuilder()
        qb.tables = TABLE_NAME
        when (uriMatcher!!.match(uri)) {
            uriCode -> qb.projectionMap = values
            else -> throw IllegalArgumentException("Unknown URI $uri")
        }
        if (sortOrder == null || sortOrder === "") {
            sortOrder = id
        }
        val c = qb.query(
            db, projection, selection, selectionArgs, null,
            null, sortOrder
        )
        c.setNotificationUri(context!!.contentResolver, uri)
        return c
    }

Now similarly write code for other 3 functions namely insert, update and delete

// adding data to the database
override fun insert(uri: Uri, values: ContentValues?): Uri? {
    val rowID = db!!.insert(TABLE_NAME, "", values)
    if (rowID > 0) {
        val _uri = ContentUris.withAppendedId(CONTENT_URI, rowID)
        context!!.contentResolver.notifyChange(_uri, null)
        return _uri
    }
    throw SQLiteException("Failed to add a record into $uri")
}

override fun update(
    uri: Uri, values: ContentValues?, selection: String?,
    selectionArgs: Array<String>?
): Int {
    var count = 0
    count = when (uriMatcher!!.match(uri)) {
        uriCode -> db!!.update(TABLE_NAME, values, selection, selectionArgs)
        else -> throw IllegalArgumentException("Unknown URI $uri")
    }
    context!!.contentResolver.notifyChange(uri, null)
    return count
}

override fun delete(
    uri: Uri,
    selection: String?,
    selectionArgs: Array<String>?
): Int {
    var count = 0
    count = when (uriMatcher!!.match(uri)) {
        uriCode -> db!!.delete(TABLE_NAME, selection, selectionArgs)
        else -> throw IllegalArgumentException("Unknown URI $uri")
    }
    context!!.contentResolver.notifyChange(uri, null)
    return count
}

Now one last function left named getType

MIME types for tables

The getType() method returns a String in MIME format that describes the type of data returned by the content URI argument. The Uri argument can be a pattern rather than a specific URI. In this case, return the type of data associated with content URIs that match the pattern. For common types of data such as text, HTML, or JPEG, getType() returns the standard MIME type for that data.

For content URIs that point to a row or rows of table data, getType() returns a MIME type in Android’s vendor-specific MIME format:

  • Type part: vnd
  • Subtype part:
    • If the URI pattern is for a single row: android.cursor.item/
    • If the URI pattern is for more than one row: android.cursor.dir/
  • Provider-specific part: vnd.<name>.<type>You supply the <name> and <type>. The <name> value is globally unique, and the <type> value is unique to the corresponding URI pattern. A good choice for <name> is your company’s name or some part of your application’s Android package name. A good choice for the <type> is a string that identifies the table associated with the URI.

For example, if a provider’s authority is com.example.app.provider, and it exposes a table named table1, the MIME type for multiple rows in table1 is:

vnd.android.cursor.dir/vnd.com.example.provider.table1

For a single row of table1, the MIME type is:

vnd.android.cursor.item/vnd.com.example.provider.table1
override fun getType(uri: Uri): String? {
        return when (uriMatcher!!.match(uri)) {
            uriCode -> "vnd.android.cursor.dir/users"
            else -> throw IllegalArgumentException("Unsupported URI: $uri")
        }
    }

After this don’t forget to add the provider in the app’s manifest file

<provider
   android:name=".phase2.MyContentProvider"
   android:authorities="com.example.learngit"
   android:enabled="true"
   android:exported="true"
   android:permission="com.example.learngit.ACCESS_USERS"/>

If you want to add permission to access you content provider then define it in manifest file

<permission
        android:name="com.example.learngit.ACCESS_USERS"
        android:protectionLevel="normal" />

Time to access our Provider in other apps

We have successfully created our content provider to manage user data in our app. now we will access the data in other app as we usually do in our apps like access contacts from the content provider of Contacts app.

First thing first define the same permission in our app to access the data of provider app. The <queries> element is required to explicitly declare the packages or components your app needs to interact with. Starting from Android 11 (API 30), Android introduced package visibility restrictions to enhance privacy. This means:

Adding a <queries> declaration informs the system that your app will interact with a specific app or its content provider.

Apps can no longer query other apps or their components without declaring their intent in the manifest.

<uses-permission android:name="com.example.learngit.ACCESS_USERS" />

<queries>
    <package android:name="com.example.learngit"/>
</queries>

Now simply user this UI in compose to display data from the content Provider.

@Composable
fun UserListScreen(contentResolver: ContentResolver) {

    // defining authority so that other application can access it
    val PROVIDER_NAME = "com.example.learngit"

    // defining content URI
    val URL = "content://$PROVIDER_NAME/users"

    // parsing the content URI
    val CONTENT_URI = Uri.parse(URL)

    // State to hold list of users
    val users = remember { mutableStateListOf<Map<String, String>>() }

    // States for input fields
    var name by remember { mutableStateOf("") }
    var userIdToUpdate by remember { mutableStateOf<Int?>(null) }

    // Function to fetch all users
    fun fetchUsers() {
        val cursor = contentResolver.query(
            CONTENT_URI,
            null, // Projection
            null, // Selection
            null, // Selection args
            null // Sort order
        )
        users.clear()
        cursor?.use {
            while (it.moveToNext()) {
                val id = it.getInt(it.getColumnIndexOrThrow("id"))
                val name = it.getString(it.getColumnIndexOrThrow("name"))
                users.add(mapOf("id" to id.toString(), "name" to name))
            }
        }
    }

    // Insert or Update function
    fun saveUser() {
        if (name.isNotBlank()) {
            val values = ContentValues().apply {
                put("name", name)
            }

            if (userIdToUpdate != null) {
                val selection = "id = ?"
                val selectionArgs = arrayOf(userIdToUpdate.toString())
                contentResolver.update(
                    CONTENT_URI,
                    values, selection, selectionArgs
                )
                userIdToUpdate = null
            } else {
                contentResolver.insert(CONTENT_URI, values)
            }

            name = ""
            fetchUsers()
        }
    }

    // Delete function
    fun deleteUser(id: Int) {
        val selection = "id = ?"
        val selectionArgs = arrayOf(id.toString())
        contentResolver.delete(CONTENT_URI, selection, selectionArgs)
        fetchUsers()
    }

    // Initial data fetch
    LaunchedEffect(Unit) {
        fetchUsers()
    }

    // UI
    Column(modifier = Modifier.fillMaxSize().padding(16.dp)) {
        // Input fields
        TextField(
            value = name,
            onValueChange = { name = it },
            label = { Text("Name") },
            modifier = Modifier.fillMaxWidth()
        )
        Spacer(modifier = Modifier.height(8.dp))
        Button(
            onClick = { saveUser() },
            modifier = Modifier.fillMaxWidth()
        ) {
            Text(if (userIdToUpdate != null) "Update User" else "Add User")
        }

        Spacer(modifier = Modifier.height(16.dp))

        // User list
        LazyColumn {
            items(users) { user ->
                Row(
                    modifier = Modifier
                        .fillMaxWidth()
                        .padding(vertical = 8.dp),
                    horizontalArrangement = Arrangement.SpaceBetween
                ) {
                    Column {
                        Text("ID: ${user["id"]}")
                        Text("Name: ${user["name"]}")
                    }
                    Row {
                        IconButton(onClick = {
                            name = user["name"].orEmpty()
                            userIdToUpdate = user["id"]?.toInt()
                        }) {
                            Icon(
                                imageVector = Icons.Default.Edit,
                                contentDescription = "Edit"
                            )
                        }
                        IconButton(onClick = {
                            deleteUser(user["id"]!!.toInt())
                        }) {
                            Icon(
                                imageVector = Icons.Default.Delete,
                                contentDescription = "Delete"
                            )
                        }
                    }
                }
            }
        }
    }
}

GitHub Gist link:- here

2 thoughts on “Content Provider & Content Resolver in Android | Jetpack Compose | Kotlin”

  1. Pingback: Read & Write Call Logs in Android using Jetpack compose | Content Providers | Android Storage – decodeandroid.com

  2. Pingback: Read & Write Contacts in Android using Jetpack Compose – decodeandroid.com

Leave a Comment

Your email address will not be published. Required fields are marked *

Scroll to Top