Chapters

Hide chapters

Saving Data on Android

First Edition · Android 10 · Kotlin 1.3 · AS 3.5

Before You Begin

Section 0: 3 chapters
Show chapters Hide chapters

Using Firebase

Section 3: 11 chapters
Show chapters Hide chapters

4. ContentProvider
Written by Jenn Bailey

Heads up... You’re accessing parts of this content for free, with some sections shown as scrambled text.

Heads up... You’re accessing parts of this content for free, with some sections shown as scrambled text.

Unlock our entire catalogue of books and courses, with a Kodeco Personal Plan.

Unlock now

Being able to persist structured data in a SQLite database is an excellent feature included in the Android SDK. However, this data is only accessible by the app that created it. What if an app would like to share data with another app? ContentProvider is the tool that allows apps to share persisted files and data with one another.

Content providers sit between the app’s data source and provide a means to manage this data. This can be a helpful organizational tool in an app, even if the app is not intended to share its data externally with other apps. Content providers provide a standardized interface that can connect data in one process with code running in another process. They encapsulate the data and provide mechanisms for defining data security at a granular level. A content provider can be used to aggregate multiple data sources and abstract away the details.

Although it can be a good idea to use a content provider to better organize and manage the data in an app, this is not a requirement if the app is not going to share its data.

One of the simplest use cases of a content provider is to gain access to the Contacts of a device via a content provider. Another common built-in provider in the Android platform is the user dictionary. The user dictionary holds spellings of non-standard words specific to the user.

Understanding content provider basics

In order to get data from a content provider, you use a mechanism called ContentResolver. The content resolver provides methods to query(), update(), insert() and delete() data from a content provider. A request is made to a content resolver by passing a URI to one of the SQL-like methods. These methods return a Cursor.

Note: Cursor is defined and discussed in more detail in the previous SQLite chapter. A cursor is essentially a pointer to a row in a table of structured data that was returned by the query.

To interact with a content provider via a content resolver there are two basic steps:

  1. Request permission from the provider by adding a permission in the manifest.
  2. Construct a query with an appropriate content URI and send the query to the provider via a content resolver object.

Understanding Content URIs

To find the data within the provider, use a content URI. The content URI is essentially the address of where to find the data within the provider. A content URI always starts with content:// and then includes the authority of a provider which is the provider’s symbolic name. It can also include the names of tables or other specific information relating the query. An example content URI for the user dictionary looks like:

content://user_dictionary/words

Requesting permission to use a content provider

The application will need read access permission for the specific provider. Utilize the <uses-permission> element and the exact permission that was defined by the provider. The provider’s application can specify which permissions that requesting applications must have in order to access the data. Users can see the requested permissions when they install the application. The code to request read permission of the user dictionary is:

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

Permission types

The types of permission that can be granted by a content provider include:

Constructing the query

The statement to perform a query on the user dictionary database looks like this:

cursor = contentResolver.query(
    // 1
    UserDictionary.Words.CONTENT_URI,   
    // 2
    projection,                       
    // 3
    selectionClause,                   
    // 4
    selectionArgs,      
    // 5
    sortOrder                          
)

Inserting, updating and deleting data

The insert, update and delete operations look very similar to the query operation. In each case, a function is called on the content resolver object and the appropriate parameters passed in.

Inserting data

Below is a statement to insert a record:

newUri = contentResolver.insert(
        UserDictionary.Words.CONTENT_URI,   
        newValues                         
)

Updating data

To update data, call update on the content resolver object and pass in content values that include key-values for the columns being updated in the corresponding row. Arguments should also be included for selection criteria and arguments to identify the correct records to update. When populating the content values, you only have to include columns that you’re updating, and including column keys with a null value will clear out the data for that column. One important consideration when updating data is to sanitize user input. The developer guide to protecting against malicious data has been included in the Where to go from here section below. An integer is returned from update that contains the count of how many rows were updated.

Deleting data

Deleting data is very similar to the other operations. Call delete on the content resolver object passing in arguments for the selection clause and selection arguments to identify the group of records to delete. A value is returned with an integer count of how many rows were deleted.

Adding a contract class

A contract class is a place to define constants used to assemble the content URIs. This can include constants to contain the authority, table names and column names along with assembled URIs. This class must be created and shared by the developer creating the provider. It can make it easier for other developers to understand and utilize the content provider in their application.

MIME types

Content providers can return standard MIME types like those used by media, or custom MIME type strings, or both. MIME types take the format type/subtype an example being text/html. Custom MIME types or vendor-specific MIME types are more complicated and come in the form of: vnd.android.cursor.dir for multiple rows. They come in the form of vnd.android.cursor.item for single rows.

Getting Started

Locate this chapter’s folder in the provided materials, named content-provider, and open up the projects folder. Next, open the ContentProviderToDo app under the starter folder. Allow the project to sync, download dependencies, and setup the workplace environment. For now, ignore the errors in the code.

Adding the provider package

It is a good idea to keep the provider classes in their own package. You will also include the contract class in this package. Right click on the com.raywenderlich.contentprovidertodo.Controller folder and select new > package. A dialog pops up prompting for the name of the new package, type in provider and click OK.

Adding the contract class

Now add the ToDoContract.kt class. Right click the new provider package and select New > Kotlin File/Class. In the resulting dialog enter the name ToDoContract, for the “Kind” dropdown select “File” and press OK. A Kotlin file will be created in the provider directory. Insert the following declarations into the Contract.kt file beneath the package declaration:

// The ToDoContract class
object ToDoContract {
  // 1
  // The URI Code for All items
  const val ALL_ITEMS = -2

  // 2
  //The URI suffix for counting records
  const val COUNT = "count"

  // 3
  //The URI Authority
  const val AUTHORITY = "com.raywenderlich.contentprovidertodo.provider"

  // 4
  // Only one public table.
  const val CONTENT_PATH = "todoitems"

  // 5
  // Content URI for this table. Returns all items.
  val CONTENT_URI = Uri.parse("content://$AUTHORITY/$CONTENT_PATH")

  // 6
  // URI to get the number of entries.
  val ROW_COUNT_URI = Uri.parse("content://$AUTHORITY/$CONTENT_PATH/$COUNT")

  // 7
  // Single record mime type
  const val SINGLE_RECORD_MIME_TYPE = "vnd.android.cursor.item/vnd.com.raywenderlich.contentprovidertodo.provider.todoitems"

  // 8
  // Multiple Record MIME type
  const val MULTIPLE_RECORDS_MIME_TYPE = "vnd.android.cursor.item/vnd.com.raywenderlich.contentprovidertodo.provider.todoitems"

  // 9
  // Database name
  const val DATABASE_NAME: String = "todoitems.db"

  // 10
  // Table Constants
  object ToDoTable {
    // The table name
    const val TABLE_NAME: String = "todoitems"

    // The constants for the table columns
    object Columns {
      //The unique ID column
      const val KEY_TODO_ID: String = "todoid" 
      //The ToDo's Name
      const val KEY_TODO_NAME: String = "todoname" 
      //The ToDo's category
      const val KEY_TODO_IS_COMPLETED: String = "iscompleted" 
    }
  }
}

Adding the content provider

Android Studio has a neat feature to automatically add content classes. A content provider class extends ContentProvider from the Android SDK and implements all the required methods. By using the automated method of adding the content provider class, many of these method stubs will be provided for you. It will be your job to fill in the functions in the content provider one by one. Ready to get started? :]

Add a Content Provider dialog
Esl o Yoygesq Vyolizuh biijuq

  <provider
    android:name=".Controller.provider.ToDoContentProvider"
    android:authorities="com.raywenderlich.contentprovidertodo"
    android:enabled="true"
    android:exported="true">
  </provider>

Implementing the methods in the content provider

Note: As you add code to the method stubs in the provider, be sure to replace the TODO comments with the new code. Also, you may need to press alt + enter and import libraries as you go along. If given the choice between constants defined in the ToDoDbSchema or the new ToDoContract, choose ToDoContract. The goal is to have the content provider depending on the contract so that it serves as an abstract layer above the database handler. This design allows for the data source to be swapped out as long as it meets the same specifications as the previous data source, and no other code that is dependent on the database will be affected, in this app or other apps that utilize the contract.

  // 1
  // This is the content provider that will
  // provide access to the database
  private lateinit var db : ToDoDatabaseHandler
  private lateinit var sUriMatcher : UriMatcher

  // 2
  // Add the URI's that can be matched on
  // this content provider
  private fun initializeUriMatching() {

    sUriMatcher = UriMatcher(UriMatcher.NO_MATCH)
    sUriMatcher.addURI(AUTHORITY,CONTENT_PATH, URI_ALL_ITEMS_CODE)
    sUriMatcher.addURI(AUTHORITY, CONTENT_PATH + "/#", URI_ONE_ITEM_CODE)
    sUriMatcher.addURI(AUTHORITY, CONTENT_PATH + "/" + COUNT, 
                       URI_COUNT_CODE)
  }

  // 3
  // The URI Codes
  private val URI_ALL_ITEMS_CODE = 10
  private val URI_ONE_ITEM_CODE = 20
  private val URI_COUNT_CODE = 30

Implementing onCreate

Insert the code below into the onCreate method stub replacing the TODO marker:

db = ToDoDatabaseHandler(context)	
initializeUriMatching()
return true

Implementing getType

Next, implement the getType function by replacing the entire stub with the code below:

override fun getType(uri: Uri) : String? = when(sUriMatcher.match(uri)) {
  URI_ALL_ITEMS_CODE -> MULTIPLE_RECORDS_MIME_TYPE
  URI_ONE_ITEM_CODE -> SINGLE_RECORD_MIME_TYPE
  else -> null
}

Implementing query

The query function queries the database and returns the results. This function has been designed so that it can perform multiple types of queries, depending on the URI. Insert the code below into the body of the function:

var cursor : Cursor? = null
when(sUriMatcher.match(uri)) {
  URI_ALL_ITEMS_CODE -> { cursor = db.query(ALL_ITEMS)}
  URI_ONE_ITEM_CODE -> { cursor = db.query(uri.lastPathSegment.toInt()) }
  URI_COUNT_CODE -> { cursor = db.count()}
  UriMatcher.NO_MATCH -> { /*error handling goes here*/ }
  else -> { /*unexpected problem*/ }
}
return cursor

Modifying the adapter

To test the content provider you just created, open ToDoAdapter.kt and add the following code inside the class before the onCreateViewHolder method:

  private val queryUri = CONTENT_URI.toString() // base uri
  private val queryCountUri = ROW_COUNT_URI.toString()
  private val projection = arrayOf(CONTENT_PATH) //table
  private var selectionClause: String? = null
  private var selectionArgs: Array<String>? = null
  private val sortOrder = "ASC"
// Get the number of records from the Content Resolver
val cursor = context.contentResolver.query(Uri.parse(queryCountUri),
  projection, selectionClause,
  selectionArgs,sortOrder)
// Return the count of records
if(cursor != null) {
  if(cursor.moveToFirst()) {
    return cursor.getInt(0)
  }
}
// 1
val cursor = context.contentResolver.query(Uri.parse("$queryUri"),
  projection,
  selectionClause,
  selectionArgs, sortOrder)

// 2
if(cursor != null) {
  if(cursor.moveToPosition(position)) {
    val toDoId = cursor.getLong(cursor.getColumnIndex(KEY_TODO_ID))
    val toDoName = cursor.getString(cursor.getColumnIndex(KEY_TODO_NAME))
    val toDoCompleted= cursor.getInt(cursor.getColumnIndex(KEY_TODO_IS_COMPLETED)) > 0
    val toDo= ToDo(toDoId, toDoName, toDoCompleted)
    holder.bindViews(toDo)
  }
}

Implementing insert

Open ToDoContentProvider.kt and replace the TODO in the body of the insert method with the following code:

val id = db.insert(values!!.getAsString(KEY_TODO_NAME))
return Uri.parse("$CONTENT_URI/$id")
import com.raywenderlich.contentprovidertodo.Controller.provider.ToDoContract.ToDoTable.Columns.KEY_TODO_NAME
// 1
var values = ContentValues()
values.put(KEY_TODO_NAME, toDoName)
// 2
context.contentResolver.insert(CONTENT_URI, values)

Implementing update

To implement the update function, open ToDoContentProvider.kt and copy the code below into the body of the update function:

var toDo = ToDo(values!!.getAsLong(KEY_TODO_ID),values!!
                .getAsString(KEY_TODO_NAME), values!!
                .getAsBoolean(KEY_TODO_IS_COMPLETED))
return db.update(toDo)
// 1
var values = ContentValues()
values.put(KEY_TODO_NAME,view.edtToDoName.text.toString())
values.put(KEY_TODO_ID, toDo.toDoId)
values.put(KEY_TODO_IS_COMPLETED, toDo.isCompleted)
// 2
context.contentResolver.update(Uri.parse(queryUri), values, selectionClause,
    selectionArgs)
// 3
notifyDataSetChanged()

Implementing delete

Deleting a record is simple. Open ToDoContentProvider.kt and copy the following code into the body of delete replacing the TODO statement:

return db.delete(parseLong(selectionArgs?.get(0)))
// 1
selectionArgs = arrayOf(id.toString())
// 2
context.contentResolver.delete(Uri.parse(queryUri), selectionClause, selectionArgs)
// 3
notifyDataSetChanged()

Challenge: Creating a client

For an additional, interesting challenge, make a copy of the app you just created that creates a content provider and see if you can remove the database and transform it into a client that utilizes the content provider.

Challenge Solution: Creating a client

It is easy to create a client app that utilizes the provider you just created in the previous steps. You can achieve this by making a copy of the provider app and deleting the database and content provider from it. This proves the content provider is shared with an external app. The steps are as follows:

<uses-permission android:name = "com.raywenderlich.contentprovidertodo.provider.PERMISSION"/>

Key points

  • Content providers sit just above the data source of the app, providing an additional level of abstraction from the data repository.
  • A content provider allows for the sharing of data between apps.
  • A content provider can be a useful way of making a single data provider if the data is housed in multiple repositories for the app.
  • It is not necessary to use a content provider if the app is not intended to share data with other apps.
  • The content resolver is utilized to run queries on the content provider.
  • Using a content provider can allow granular permissions to be set on the data.
  • Use best practices such as selection clauses and selection arguments to prevent SQL Injection when utilizing a content provider, or any raw query data mechanism.
  • Data in a content provider can be accessed via exposed URIs that are matched by the content provider.

Where to go from here?

See Google’s documentation on content provider here: https://developer.android.com/guide/topics/providers/content-providers.

Have a technical question? Want to report a bug? You can ask questions and report bugs to the book authors in our official book forum here.
© 2024 Kodeco Inc.

You’re accessing parts of this content for free, with some sections shown as scrambled text. Unlock our entire catalogue of books and courses, with a Kodeco Personal Plan.

Unlock now