An Android Content Provider Tutorial

As outlined in the previous chapter, content providers provide a mechanism through which the data stored by one Android application can be made accessible to other applications. Having provided a theoretical overview of content providers, this chapter will continue the coverage of content providers by extending the SQLDemo project created in the chapter entitled An Android SQLite Database Tutorial to implement content provider-based access to the database.

Copying the SQLDemo Project

To keep the original SQLDemo project intact, we will make a backup copy of the project before modifying it to implement content provider support for the application. If the SQLDemo project is currently open within Android Studio, close it using the File -> Close Project menu option.

Using the file system explorer for your operating system type, navigate to the directory containing your Android Studio projects and copy the SQLDemo project folder to a new folder named SQLDemo_provider.

Within the Android Studio welcome screen, open the SQLDemo_provider project so that it loads into the main window.

Adding the Content Provider Package

The next step is to add a new package to the SQLDemo project to contain the content provider class. Add this new package by navigating within the Project tool window to app -> kotlin+java, right-clicking on it, and selecting the New -> Package menu option. When the Choose Destination Directory dialog appears, select the ..\app\src\main\java option from the Directory Structure panel and click on OK.

 

You are reading a sample chapter from an old edition of the Android Studio Essentials – Kotlin Edition book.

Purchase the fully updated Android Studio Iguana Kotlin Edition of this publication in eBook or Print format.

The full book contains 99 chapters and over 842 pages of in-depth information.

Learn more.

Preview  Buy eBook  Buy Print

 

In the New Package dialog, enter the following package name into the name field before pressing enter:

com.ebookfrenzy.sqldemo.providerCode language: plaintext (plaintext)

The new package should now be listed within the Project tool window, as illustrated in Figure 38-1:

Figure 72-1

Creating the Content Provider Class

As discussed in Understanding Android Content Providers, content providers are created by subclassing the android.content.ContentProvider class. Consequently, the next step is to add a class to the new provider package to serve as the content provider for this application. Locate the new package in the Project tool window, rightclick on it and select the New -> Other -> Content Provider menu option. In the Configure Component dialog, enter MyContentProvider into the Class Name field and the following into the URI Authorities field:

com.ebookfrenzy.sqldemo.provider.MyContentProviderCode language: plaintext (plaintext)

Ensure that the new content provider class is exported and enabled before clicking on Finish to create the new class.

Once the new class has been created, the MyContentProvider.kt file should be listed beneath the provider package in the Project tool window and automatically loaded into the editor, where it will appear as outlined in the following listing:

 

You are reading a sample chapter from an old edition of the Android Studio Essentials – Kotlin Edition book.

Purchase the fully updated Android Studio Iguana Kotlin Edition of this publication in eBook or Print format.

The full book contains 99 chapters and over 842 pages of in-depth information.

Learn more.

Preview  Buy eBook  Buy Print

 

package com.ebookfrenzy.sqldemo.provider

import android.content.ContentProvider
import android.content.ContentValues
import android.database.Cursor
import android.net.Uri

class MyContentProvider : ContentProvider() {

    override fun delete(uri: Uri, selection: String?, 
                selectionArgs: Array<String>?): Int {
        TODO("Implement this to handle requests to delete one or more rows")
    }

    override fun getType(uri: Uri): String? {
        TODO(
            "Implement this to handle requests for the MIME type of the data" +
                    "at the given URI"
        )
    }

    override fun insert(uri: Uri, values: ContentValues?): Uri? {
        TODO("Implement this to handle requests to insert a new row.")
    }

    override fun onCreate(): Boolean {
        TODO("Implement this to initialize your content provider on startup.")
    }

    override fun query(
        uri: Uri, projection: Array<String>?, selection: String?,
        selectionArgs: Array<String>?, sortOrder: String?
    ): Cursor? {
        TODO("Implement this to handle query requests from clients.")
    }

    override fun update(
        uri: Uri, values: ContentValues?, selection: String?,
        selectionArgs: Array<String>?
    ): Int {
        TODO("Implement this to handle requests to update one or more rows.")
    }
}Code language: Kotlin (kotlin)

As is evident from a quick review of the code in this file, Android Studio has already populated the class with stubs for each of the methods that a subclass of ContentProvider is required to implement. It will soon be necessary to begin implementing these methods, but first some constants relating to the provider’s content authority and URI need to be declared.

Constructing the Authority and Content URI

As outlined in the previous chapter, all content providers must have associated with them an authority and a content uri. In practice, the authority is typically the full package name of the content provider class itself, in this case com.ebookfrenzy.sqldemo.provider.MyContentProvider as declared when the new Content Provider class was created in the previous section.

The content URI will vary depending on application requirements, but for this example it will comprise the authority with the name of the database table appended at the end. Within the MyContentProvider.kt file, make the following modifications:

package com.ebookfrenzy.sqldemo.provider
.
.
class MyContentProvider : ContentProvider() {
.
.
   companion object {
        val AUTHORITY = "com.ebookfrenzy.sqldemo.provider.MyContentProvider"
        private val CUSTOMERS_TABLE = "customers"
        val CONTENT_URI : Uri = Uri.parse("content://" + AUTHORITY + "/" +
                CUSTOMERS_TABLE)
    }
}Code language: Kotlin (kotlin)

The above statements begin by creating a new String object named AUTHORITY and assigning the authority string to it. Similarly, a second String object named CUSTOMERS_TABLE is created and initialized with the name of our database table (customers).

Finally, these two string elements are combined, prefixed with content:// and converted to a Uri object using the parse() method of the Uri class. The result is assigned to a variable named CONTENT_URI.

 

You are reading a sample chapter from an old edition of the Android Studio Essentials – Kotlin Edition book.

Purchase the fully updated Android Studio Iguana Kotlin Edition of this publication in eBook or Print format.

The full book contains 99 chapters and over 842 pages of in-depth information.

Learn more.

Preview  Buy eBook  Buy Print

 

Implementing URI Matching in the Content Provider

When the methods of the content provider are called, they will be passed as an argument a URI indicating the data on which the operation is to be performed. This URI may take the form of a reference to a specific row in a specific table. It is also possible that the URI will be more general, for example specifying only the database table. It is the responsibility of each method to identify the Uri type and to act accordingly. This task can be eased considerably by making use of a UriMatcher instance. Once a UriMatcher instance has been created, it can be configured to return a specific integer value corresponding to the type of URI it detects when asked to do so. For this tutorial, we will be configuring our UriMatcher instance to return a value of 1 when the URI references the entire customers table, and a value of 2 when the URI references the ID of a specific row in the customers table. Before working on creating the URIMatcher instance, we will first create two integer variables to represent the two URI types:

package com.ebookfrenzy.sqldemo.provider

import android.content.ContentProvider
import android.content.ContentValues
import android.database.Cursor
import android.net.Uri
import android.content.UriMatcher
import android.database.sqlite.SQLiteDatabase
import android.database.sqlite.SQLiteQueryBuilder
import android.text.TextUtils

import com.ebookfrenzy.sqldemo.MyDBHandler

class MyContentProvider : ContentProvider() {

    private val CUSTOMERS = 1
    private val CUSTOMER_ID = 2
.
.Code language: Kotlin (kotlin)

With the Uri type variables declared, it is now time to add code to create a UriMatcher instance and configure it to return the appropriate variables:

class MyContentProvider : ContentProvider() {

    private var myDB: MyDBHandler? = null
    private val CUSTOMERS = 1
    private val CUSTOMER_ID = 2

    private val sURIMatcher = UriMatcher(UriMatcher.NO_MATCH)

    init {
        sURIMatcher.addURI(AUTHORITY, CUSTOMERS_TABLE, CUSTOMERS)
        sURIMatcher.addURI(AUTHORITY, CUSTOMERS_TABLE + "/#",
            CUSTOMER_ID)
    }
.
.Code language: Kotlin (kotlin)

The UriMatcher instance (named sURIMatcher) is now primed to return the value of CUSTOMER when just the customers table is referenced in a URI, and CUSTOMER_ID when the URI includes the ID of a specific row in the table.

Implementing the Content Provider onCreate() Method

When the content provider class is created and initialized, a call will be made to the onCreate() method of the class. It is within this method that any initialization tasks for the class need to be performed. For this example, all that needs to be performed is for an instance of the MyDBHandler class implemented in “An Android SQLite Database Tutorial” to be created. Once this instance has been created, it will need to be accessible from the other methods in the class, so a declaration for the database handler also needs to be declared, resulting in the following code changes to the MyContentProvider.kt file:

class MyContentProvider : ContentProvider() {
.
.
    override fun onCreate(): Boolean {
        myDB = context?.let { MyDBHandler(it, null, null, 1) }
        return false
    }
.
.Code language: Kotlin (kotlin)

Implementing the Content Provider insert() Method

When a client application or activity requests that data be inserted into the underlying database, the insert() method of the content provider class will be called. At this point, however, all that exists in the MyContentProvider. kt file of the project is a stub method, which reads as follows:

 

You are reading a sample chapter from an old edition of the Android Studio Essentials – Kotlin Edition book.

Purchase the fully updated Android Studio Iguana Kotlin Edition of this publication in eBook or Print format.

The full book contains 99 chapters and over 842 pages of in-depth information.

Learn more.

Preview  Buy eBook  Buy Print

 

override fun insert(uri: Uri, values: ContentValues?): Uri? {
    TODO("Implement this to handle requests to insert a new row.")
}Code language: Kotlin (kotlin)

Passed as arguments to the method are a URI specifying the destination of the insertion and a ContentValues object containing the data to be inserted.

This method now needs to be modified to perform the following tasks:

  • Use the sUriMatcher object to identify the URI type.
  • Throw an exception if the URI is not valid.
  • Obtain a reference to a writable instance of the underlying SQLite database.
  • Perform a SQL insert operation to insert the data into the database table.
  • Notify the corresponding content resolver that the database has been modified.
  • Return the URI of the newly added table row.

Bringing these requirements together results in a modified insert() method, which reads as follows:

override fun insert(uri: Uri, values: ContentValues?): Uri? {
    val uriType = sURIMatcher.match(uri)
    val sqlDB = myDB!!.writableDatabase
    val id: Long
    when (uriType) {
        CUSTOMERS -> id = sqlDB.insert(MyDBHandler.TABLE_CUSTOMERS, null, values)
        else -> throw IllegalArgumentException("Unknown URI: " + uri)
    }
    context?.contentResolver?.notifyChange(uri, null)
    return Uri.parse(CUSTOMERS_TABLE + "/" + id)
}Code language: Kotlin (kotlin)

Implementing the Content Provider query() Method

When a content provider is called upon to return data, the query() method of the provider class will be called. When called, this method is passed some or all of the following arguments:

  • URI – The URI specifying the data source on which the query is to be performed. This can take the form of a general query with multiple results, or a specific query targeting the ID of a single table row.
  • Projection – A row within a database table can comprise multiple columns of data. In the case of this application, for example, these correspond to the ID, customer name and phone number. The projection argument is simply a String array containing the name for each of the columns that is to be returned in the result data set.
  • Selection – The “where” element of the selection to be performed as part of the query. This argument controls which rows are selected from the specified database. For example, if the query was required to select only customers named “John Andrews” then the selection string passed to the query() method would read customername = “John Andrews”.
  • Selection Args – Any additional arguments that need to be passed to the SQL query operation to perform the selection.
  • Sort Order – The sort order for the selected rows.

When called, the query() method is required to perform the following operations:

 

You are reading a sample chapter from an old edition of the Android Studio Essentials – Kotlin Edition book.

Purchase the fully updated Android Studio Iguana Kotlin Edition of this publication in eBook or Print format.

The full book contains 99 chapters and over 842 pages of in-depth information.

Learn more.

Preview  Buy eBook  Buy Print

 

  • Use the sUriMatcher to identify the Uri type.
  • Throw an exception if the URI is not valid.
  • Construct a SQL query based on the criteria passed to the method. For convenience, the SQLiteQueryBuilder class can be used to construct the query.
  • Execute the query operation on the database.
  • Notify the content resolver of the operation.
  • Return a Cursor object containing the results of the query.

With these requirements in mind, the code for the query() method in the MyContentProvider.kt file should now read as outlined in the following listing:

override fun query(
    uri: Uri, projection: Array<String>?, selection: String?,
    selectionArgs: Array<String>?, sortOrder: String?
): Cursor? {

    val queryBuilder = SQLiteQueryBuilder()
    queryBuilder.tables = MyDBHandler.TABLE_CUSTOMERS
    val uriType = sURIMatcher.match(uri)
    when (uriType) {
        CUSTOMER_ID -> queryBuilder.appendWhere(MyDBHandler.COLUMN_ID + "="
                + uri.lastPathSegment)
        CUSTOMERS -> {
        }
        else -> throw IllegalArgumentException("Unknown URI")
    }
    val cursor = queryBuilder.query(myDB?.readableDatabase,
        projection, selection, selectionArgs, null, null,
        sortOrder)
    cursor.setNotificationUri(context?.contentResolver, uri)
    return cursor
}Code language: Kotlin (kotlin)

Implementing the Content Provider update() Method

The update() method of the content provider is called when changes are being requested to existing database table rows. The method is passed a URI with the new values in the form of a ContentValues object and the usual selection argument strings.

When called, the update() method would typically perform the following steps:

  • Use the sUriMatcher to identify the URI type.
  • Throw an exception if the URI is not valid.
  • Obtain a reference to a writable instance of the underlying SQLite database.
  • Perform the appropriate update operation on the database, depending on the selection criteria and the URI type.
  • Notify the content resolver of the database change.
  • Return a count of the number of rows that were changed due to the update operation.

A general-purpose update() method, and the one we will use for this project, would read as follows:

override fun update(
    uri: Uri, values: ContentValues?, selection: String?,
    selectionArgs: Array<String>?
): Int {
    val uriType = sURIMatcher.match(uri)
    val sqlDB: SQLiteDatabase = myDB!!.writableDatabase
    val rowsUpdated: Int
    when (uriType) {
        CUSTOMERS -> rowsUpdated = sqlDB.update(MyDBHandler.TABLE_CUSTOMERS,
            values,
            selection,
            selectionArgs)
        CUSTOMER_ID -> {
            val id = uri.lastPathSegment
            rowsUpdated = if (TextUtils.isEmpty(selection)) {
                sqlDB.update(MyDBHandler.TABLE_CUSTOMERS,
                    values,
                    MyDBHandler.COLUMN_ID + "=" + id, null)
            } else {
                sqlDB.update(MyDBHandler.TABLE_CUSTOMERS,
                    values,
                    MyDBHandler.COLUMN_ID + "=" + id
                            + " and "
                            + selection,
                    selectionArgs)
            }
        }
        else -> throw IllegalArgumentException("Unknown URI: " + uri)
    }
    context?.contentResolver?.notifyChange(uri, null)
    return rowsUpdated
}Code language: Kotlin (kotlin)

Implementing the Content Provider delete() Method

In common with several other content provider methods, the delete() method is passed a URI, a selection string, and an optional set of selection arguments. A typical delete() method will also perform the following, and by now largely familiar, tasks when called:

 

You are reading a sample chapter from an old edition of the Android Studio Essentials – Kotlin Edition book.

Purchase the fully updated Android Studio Iguana Kotlin Edition of this publication in eBook or Print format.

The full book contains 99 chapters and over 842 pages of in-depth information.

Learn more.

Preview  Buy eBook  Buy Print

 

  • Use the sUriMatcher to identify the URI type.
  • Throw an exception if the URI is not valid.
  • Obtain a reference to a writable instance of the underlying SQLite database.
  • Perform the appropriate delete operation on the database depending on the selection criteria and the Uri type.
  • Notify the content resolver of the database change.
  • Return the number of rows deleted as a result of the operation.

A typical delete() method is in many ways similar to the update() method and may be implemented as follows:

override fun delete(uri: Uri, selection: String?, selectionArgs: Array<String>?): Int {
        val uriType = sURIMatcher.match(uri)
        val sqlDB = myDB!!.writableDatabase
        val rowsDeleted: Int
        when (uriType) {
            CUSTOMERS -> rowsDeleted = sqlDB.delete(MyDBHandler.TABLE_CUSTOMERS,
                selection,
                selectionArgs)
            CUSTOMER_ID -> {
                val id = uri.lastPathSegment
                rowsDeleted = if (TextUtils.isEmpty(selection)) {
                    sqlDB.delete(MyDBHandler.TABLE_CUSTOMERS,
                        MyDBHandler.COLUMN_ID + "=" + id,
                        null)
                } else {
                    sqlDB.delete(MyDBHandler.TABLE_CUSTOMERS,
                        MyDBHandler.COLUMN_ID + "=" + id
                                + " and " + selection,
                        selectionArgs)
                }
            }
            else -> throw IllegalArgumentException("Unknown URI: " + uri)
        }
        context?.contentResolver?.notifyChange(uri, null)
        return rowsDeleted
}Code language: Kotlin (kotlin)

With these methods implemented, the content provider class, in terms of the requirements for this example, is complete. The next step is to ensure that the content provider is declared in the project manifest file to be visible to any content resolvers seeking access.

Declaring the Content Provider in the Manifest File

Unless a content provider is declared in the manifest file of the application to which it belongs, it will not be possible for a content resolver to locate and access it. As outlined, content providers are declared using the <provider> tag and the manifest entry must correctly reference the content provider authority and content URI.

For this project, therefore, locate the manifests -> AndroidManifest.xml file within the Project tool window and double-click on it to load it into the editing panel. Within the editing panel, make sure that the content provider declaration has already been added by Android Studio when the MyContentProvider class was added to the project:

<?xml version="1.0" encoding="utf-8"?>
<manifest xmlns:android="http://schemas.android.com/apk/res/android"
    xmlns:tools="http://schemas.android.com/tools">

    <application
        android:allowBackup="true"
        android:dataExtractionRules="@xml/data_extraction_rules"
        android:fullBackupContent="@xml/backup_rules"
        android:icon="@mipmap/ic_launcher"
        android:label="@string/app_name"
        android:roundIcon="@mipmap/ic_launcher_round"
        android:supportsRtl="true"
        android:theme="@style/Theme.SQLDemo"
        tools:targetApi="31">
        <provider
            android:name=".provider.MyContentProvider"
            android:authorities="com.ebookfrenzy.sqldemo.provider.MyContentProvider"
            android:enabled="true"
            android:exported="true"></provider>

        <activity
            android:name=".MainActivity"
            android:exported="true">
            <intent-filter>
                <action android:name="android.intent.action.MAIN" />

                <category android:name="android.intent.category.LAUNCHER" />
            </intent-filter>
        </activity>
    </application>

</manifest>Code language: HTML, XML (xml)

All that remains before testing the application is to modify the database handler class to use the content provider instead of directly accessing the database.

 

You are reading a sample chapter from an old edition of the Android Studio Essentials – Kotlin Edition book.

Purchase the fully updated Android Studio Iguana Kotlin Edition of this publication in eBook or Print format.

The full book contains 99 chapters and over 842 pages of in-depth information.

Learn more.

Preview  Buy eBook  Buy Print

 

Modifying the Database Handler

When this application was originally created, it was designed to use a database handler to access the underlying database directly. Now that a content provider has been implemented, the database handler needs to be modified to perform all database operations using the content provider via a content resolver.

The first step is to modify the MyDBHandler.kt class so that it obtains a reference to a ContentResolver instance. This can be achieved in the constructor method of the class:

.
.
import android.content.ContentResolver
.
.
class DBHandler(context: Context, name: String?,
                factory: SQLiteDatabase.CursorFactory?, version: Int) :
    SQLiteOpenHelper(context, DATABASE_NAME,
        factory, DATABASE_VERSION) {

    private val myCR: ContentResolver

    init {
        myCR = context.contentResolver
    }
.
.
Code language: Kotlin (kotlin)

Next, the addCustomer(), findCustomer(), and removeCustomer() methods need to be rewritten to use the content resolver and content provider for data management purposes:

fun addCustomer(customer: Customer) {
    val values = ContentValues()
    values.put(COLUMN_CUSTOMERNAME, customer.customerName)
    values.put(COLUMN_CUSTOMERPHONE, customer.customerPhone)
    myCR.insert(MyContentProvider.CONTENT_URI, values)
}

fun findCustomer(customername: String): Customer? {
    val projection = arrayOf(COLUMN_ID, COLUMN_CUSTOMERNAME, 
            COLUMN_CUSTOMERPHONE)
    val selection = "customername = \"" + customername + "\""

    val cursor = myCR.query(MyContentProvider.CONTENT_URI,
        projection, selection, null, null)
    var customer: Customer? = null
    if (cursor != null) {
        if (cursor.moveToFirst()) {
            cursor.moveToFirst()
            val id = Integer.parseInt(cursor.getString(0))
            val customerName = cursor.getString(1)
            val customerPhone = cursor.getString(2)
            customer = Customer(id, customername, customerPhone)
            cursor.close()
        }
    }
    return customer
}

fun deleteCustomer(customername: String): Boolean {
    var result = false
    val selection = "customername = \"" + customername + "\""
    val rowsDeleted = myCR.delete(MyContentProvider.CONTENT_URI,
        selection, null)
    if (rowsDeleted > 0)
        result = true
    return result
}Code language: Kotlin (kotlin)

With the database handler class updated to use a content resolver and content provider, the application is now ready to be tested. Compile and run the application and perform operations to add, find, and remove customer entries. In terms of operation and functionality, the application should behave exactly as it did when directly accessing the database, except it now uses the content provider.

As we will see in the next chapter, with the content provider now implemented and declared in the manifest file, any other applications can potentially access that data (since no permissions were declared, the default full access is in effect). The only information the other applications need to know to gain access is the content URI and the names of the columns in the customers table.

 

You are reading a sample chapter from an old edition of the Android Studio Essentials – Kotlin Edition book.

Purchase the fully updated Android Studio Iguana Kotlin Edition of this publication in eBook or Print format.

The full book contains 99 chapters and over 842 pages of in-depth information.

Learn more.

Preview  Buy eBook  Buy Print

 

Summary

The goal of this chapter was to provide a more detailed overview of the exact steps involved in implementing an Android content provider with a particular emphasis on the structure and implementation of the query, insert, delete, and update methods of the content provider class. Practical use of the content resolver class to access data in the content provider was also covered, and the Database project was modified to use both a content provider and a content resolver.