hackajob Insider

All You Need to Know About Content Providers in Android

Written by hackajob Staff | Mar 15, 2022 12:00:00 AM

In this Content Provider tutorial for Android, we're going to cover all your burning questions including: What are content providers? How do they work? What does API use to retrieve, insert, update or delete data? Ready to dig in and find out all things Android? Let's get started!

What is a content provider?

To put it simply: a content provider is a library used to manage access to a central repository of data. It's a part of the Android Framework, which provides user interface (UI) features to work with the data. It's mainly accessed by other applications which use a client object to access the content provider. The combination of provider and provider-client offers a consistent interface to data and also handles the inter-process communication.

The application has to declare a request in your manifest file to access the provider. You'll work with a content provider in one of two ways: either you access the existing content providers from other applications or you create a new content provider for your application to share data with other applications. A content provider coordinates access to the data stored in your application for several different APIs and components. It shares access to your app data with other apps, sends data to a widget, returns custom search suggestions

How can I access a Content Provider?

A ContentResolver object is used to access data in an application’s context using ContentProivider class. It communicates with the provider object as a client, performs the requested action, and returns the results. This object has methods that call identically-named methods in the provider object and gives the basic "CRUD" (create, retrieve, update, and delete) functions of persistent storage.

A common way to access a ContentProvider from your UI is a CursorLoader. It runs an asynchronous query in the background. and the UI calls a CursorLoader to the query, which in turn gets the ContentProvider using the ContentResolver. In this way, the UI continues to be available to the user while the query is running. This pattern involves the interaction of many different objects, as well as the underlying storage mechanism.

What's a User Dictionary?

User Dictionary is a common built-in content provider in Android, which stores the spellings of non-standard words that user wants to retain. You can get a list of the words and their locales from the User Dictionary Provider by calling ContentResolver.query(). The query() method is the ContentProvider’s built-in method for User Dictionary. The following lines of code show a ContentResolver.query() call:

// Queries the user dictionary and returns results
cursor = contentResolver.query(
        UserDictionary.Words.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
)

Tell me about Content URI

A content URI is a Uniform Resource Identifier that recognises data in a provider. It includes the symbolic name of the entire provider (its authority) and a name that points to a table (a path). The calling method you use to access a table in a provider contains the content URI as one of the arguments.

What different kinds of Provider Data Types are there?

Content providers offer different data types. The User Dictionary Provider offers only textual data, but providers can also offer the following formats:

  • integer
  • long integer (long)
  • floating point
  • long floating point (double)

Another data type included in providers is Binary Large Object (BLOB). This data type is implemented as a 64KB byte array. You can see the available data types by looking at the Cursor class "get" methods.

How to retrieve data from the provider

There are two steps to follow to retrieve data from the provider. First, you have to create a request to read access permission for the provider. Then define the code that sends a query to the provider. You can achieve this by using a User Dictionary Provider.

1. Read Access Permission

You have to declare a request to read access permission to retrieve data for a provider. Your application cannot request permission at runtime. This permission should be requested by the <user-permission> element and the exact permission name in your application’s manifest file. By doing so, your app creates a compile-time request for the permission that is granted straight away when the application is getting installed by the user.

2. Constructing the Query

The next step in retrieving data from a provider is to construct a query. The following code snippet defines some variables for accessing the User Dictionary Provider:

// A "projection" defines the columns that will be returned for each row
private val mProjection: Array<String> = arrayOf(
        UserDictionary.Words._ID,    // Contract class constant for the _ID column name
        UserDictionary.Words.WORD,   // Contract class constant for the word column name
        UserDictionary.Words.LOCALE  // Contract class constant for the locale column name
)

// Defines a string to contain the selection clause
private var selectionClause: String? = null

// Declares an array to contain selection arguments
private lateinit var selectionArgs: Array<String>

Let's use ContentResolver.query(), to handle a provider-client, that is similar to an SQL query. It will return a set of columns, a set of selection criteria, and a sort order. If a user does not enter a word, the selection clause goes null, which means that the query returns all the words in the provider. If the user enters a word, the selection clause is set to UserDictionary. Words.WORD + " = ?" and the first element of the selection argument is set to the word user enters.

/*
 * This declares String array to contain the selection arguments.
 */
private lateinit var selectionArgs: Array<String>

// Gets a word from the UI
searchString = searchWord.text.toString()

// Remember to insert code here to check for invalid or malicious input.

// If the word is the empty string, gets everything
selectionArgs = searchString?.takeIf { it.isNotEmpty() }?.let {
    selectionClause = "${UserDictionary.Words.WORD} = ?"
    arrayOf(it)
} ?: run {
    selectionClause = null
    emptyArray<String>()
}

// Does a query against the table and returns a Cursor object
mCursor = contentResolver.query(
        UserDictionary.Words.CONTENT_URI,  // The content URI of the words table
        Projection, 		// The columns to return for each row
        selectionClause,      // Either null, or the word the user entered
        selectionArgs,        // Either empty, or the string the user entered
        sortOrder             // The sort order for the returned rows
)

// Some providers return null if an error occurs, others throw an exception
when (mCursor?.count) {
    null -> {
        /*
         * Insert code here to handle the error. Be sure not to use the cursor!
         * You may want to call android.util.Log.e() to log this error.
         *
         */
    }
    0 -> {
        /*
         * Insert code here to notify the user that the search was unsuccessful. This isn't
         * necessarily an error. You may want to offer the user the option to insert a new
         * row, or re-type the search term.
         */
    }
    else -> {
        // Your code to handle the result returned
    }
}
Insert the code above into your own project

Here is the example of a query with a selection clause:

SELECT _ID, word, locale FROM words WHERE word = <userinput> ORDER BY word ASC;

How to Protect Against Malicious Input

If the data managed by the content provider is present in the SQL database or the external untrusted data, it may lead to SQL injection. To avoid this situation, use a selection clause with “?” as a replaceable parameter and a separate array of selection arguments. Because it is the result of concatenation, so it is not SQL and is directly bound with the query. It won't create malicious SQL results.

// Selection clause by concatenating the user's input to the column name
var selectionClause = "var = $mUserInput"

// Selection clause with a replaceable parameter
var selectionClause = "var = ?"

Now setup the array of selection arguments like:

// Defines a mutable list to contain the selection arguments
var selectionArgs: MutableList<String> = mutableListOf()

How to Display Query Results

The ContentResolver.query() client method always returns a Cursor. This cursor contains the column specified by the query projection. The cursor object provides read access to rows and columns. You can iterate over the rows, determine the data type in each column match by the query criteria, get data out of a column and examine the result returned by the query using Cursor methods.

If no rows match the selection criteria, the provider returns a cursor object for which Cursor.getCount() is 0 (empty cursor).

Since a Cursor returns a "list" of rows, it is always better to display the contents of a Cursor to a ListView via a SimpleCursorAdapter. The following code creates a SimpleCursorAdapter object containing the Cursor from the query, and sets this object to be the adapter for a ListView:

// Defines a list of columns to retrieve from the Cursor and load into an output row
val wordListColumns : Array<String> = arrayOf(
        UserDictionary.Words.WORD,      // Contract class constant containing the word column name
        UserDictionary.Words.LOCALE     // Contract class constant containing the locale column name
)

// Defines a list of View IDs that will receive the Cursor columns for each row
val wordListItems = intArrayOf(R.id.dictWord, R.id.locale)

// Creates a new SimpleCursorAdapter
cursorAdapter = SimpleCursorAdapter(
        applicationContext,       // The application's Context object
        R.layout.wordlistrow,     // A layout in XML for one row in the ListView
        mCursor,                  // The result from the query
        wordListColumns,          // A string array of column names in the cursor
        wordListItems,            // An integer array of view IDs in the row layout
        0                         // Flags (usually none are needed)
)

// Sets the adapter for the ListView
wordList.setAdapter(cursorAdapter)

How to Get Data from Query Results

Instead of simply getting the data, you can use them for other purposes as well. You can retrieve different spellings from the user dictionary and look up to them in other providers. To achieve this, you need  to iterate over the rows in the cursor like this:

/*
* Only executes if the cursor is valid. The User Dictionary Provider returns null if
* an internal error occurs. Other providers may throw an Exception instead of returning null.
*/
mCursor?.apply {
    // Determine the column index of the column named "word"
    val index: Int = getColumnIndex(UserDictionary.Words.WORD)

    /*
     * Moves to the next row in the cursor. Before the first movement in the cursor, the
     * "row pointer" is -1, and if you try to retrieve data at that position you will get an
     * exception.
     */
    while (moveToNext()) {
        // Gets the value from the column.
        newWord = getString(index)

        // Insert code here to process the retrieved word.

        ...

        // end of while loop
    }
}

This implementation contains several “get” methods for retrieving different types of data from the object. For example, the above code snippet uses getString(). They also have a getType() method that returns a value indicating the data type of the column.

What about Content Provider Permissions?

A content provider application can specify permission that other applications have, to provide them access to the provider’s data. These permissions ensure that the user knows what kind of data an application is trying to access. The user sees the permissions requested at the application installation. If no permission is specified, then no application has access to the provider’s data, unless it is exported. On the other hand, providers have full read and write access regardless of the specific permissions.

The User Dictionary Provider requires android.permission.READ_USER_DICTIONARY permission to retrieve data from it. It has only read permission. To insert, update, or delete data, the provider has a separate android.permission.WRITE_USER_DICTIONARY permission.

An application requests permission with the <uses-permission> element in its manifest file to get the permissions needed to access a provider. When the Android Package Manager installs the application using an APK file, a user must approve all of the permissions the application requests. If the user approves all of them, Package Manager continues the installation; if the user doesn't approve them, Package Manager aborts the installation. The following <uses-permission> element requests read access to the User Dictionary Provider:

<uses-permission android:name="android.permission.READ_USER_DICTIONARY">

How to Insert, Update and Delete Data using a Content Provider

You can modify the provider’s data using the interaction between a ContentProvider and a client, in the same way, that you use to retrieve data from a provider. You call a method of ContentResolver with arguments that are passed to the corresponding method of ContentProvider. The provider and provider-client automatically handle security and inter-process communication.

How to Insert Data

You can insert data into a provider by calling the ContentResolver.insert() method. It inserts a new row and returns a content URI for that row. Here is the code snippet that shows how to insert a new word into the User Dictionary Provider:

// Defines a new Uri object that receives the result of the insertion
lateinit var newUri: Uri

...

// Defines an object to contain the new values to insert
val newValues = ContentValues().apply {
    /*
     * Sets the values of each column and inserts the word. The arguments to the "put"
     * method is "column name" and "value"
     */
    put(UserDictionary.Words.APP_ID, "example.user")
    put(UserDictionary.Words.LOCALE, "en_US")
    put(UserDictionary.Words.WORD, "insert")
    put(UserDictionary.Words.FREQUENCY, "100")

}

newUri = contentResolver.insert(
        UserDictionary.Words.CONTENT_URI,   // the user dictionary content URI
        newValues                          // the values to insert
)

How to Update Data

You can update a row using a ContentValues object with the updated values just like insertion, and selection criteria are the same as a query. You only need ContentResolver.update() to add values to the ContentValues object for columns you are updating. The following snippet changes all the rows whose locale has the language "en" to have a locale of null. The return value is the number of rows that were updated:

// Defines a new Uri object that receives the result of the insertion
lateinit var newUri: Uri

...
// Defines an object to contain the updated values
val updateValues = ContentValues().apply {
    /*
     * Sets the updated value and updates the selected words.
     */
    putNull(UserDictionary.Words.LOCALE)
}

// Defines selection criteria for the rows you want to update
val selectionClause: String = UserDictionary.Words.LOCALE + "LIKE ?"
val selectionArgs: Array<String> = arrayOf("en_%")

// Defines a variable to contain the number of updated rows
var rowsUpdated: Int = 0

...

rowsUpdated = contentResolver.update(
        UserDictionary.Words.CONTENT_URI,   // the user dictionary content URI
        updateValues,                      // the columns to update
        selectionClause,                   // the column to select on
        selectionArgs                      // the value to compare to
)

How to Delete Data

You can similarly delete a row to retrieve a row data: just specify selection criteria for the rows you want to delete and the client method returns the number of deleted rows. The following snippet deletes rows whose applied matches "user". The method returns the number of deleted rows.

// Defines selection criteria for the rows you want to delete
val selectionClause = "${UserDictionary.Words.LOCALE} LIKE ?"
val selectionArgs: Array<String> = arrayOf("user")

// Defines a variable to contain the number of rows deleted
var rowsDeleted: Int = 0

...

// Deletes the words that match the selection criteria
rowsDeleted = contentResolver.delete(
        UserDictionary.Words.CONTENT_URI,   // the user dictionary content URI
        selectionClause,                   // the column to select on
        selectionArgs                      // the value to compare to
)

So...what next?

Well we want to know what you'd like to see next!

Like what you've read or want more like this? Let us know! Email us here or DM us: Twitter, LinkedIn, Facebook, we'd love to hear from you.