Chapters

Hide chapters

Saving Data on Android

Second Edition · Android 11 · Kotlin 1.5 · Android Studio 4.2

Using Firebase

Section 3: 11 chapters
Show chapters Hide chapters

4. ContentProvider
Written by Fuad Kamal

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 isn’t a requirement if the app isn’t 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. It provides methods to query, update, insert and delete data from a content provider. A request to a content resolver contains an URI to one of the SQL-like methods. These methods return a Cursor instance.

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

There are two basic steps to interact with a content provider via content resolver:

  1. Request permission from the provider by adding a permission in the manifest file.
  2. Construct a query with an appropriate content URI and send the query to the provider via 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

Oftentimes, providers allow you to append an ID value to the end of the URI to find a specific record, or a string such as count to denote that you want to run a query that counts the number of records. You must refer to the provider documentation to figure out what a specific content provider exposes.

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 requesting applications must have in order to access the data. Users can see the requested permissions when they install the application. For example, the code to request read permission of the user dictionary is:

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

If you want to use the user dictionary, the above permission should be added inside the manifest tag in AndroidManifest.xml.

Permission types

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

  • Single read-write provider-level permission – This permission controls both read and write access to the whole provider. It’s one permission to rule them all. :]
  • Separate read and write provider-level permission – Read and write permissions can be set separately for the whole provider.
  • Path-level permission – Read, write or read/write permissions can be applied to each content URI individually.
  • Temporary permission – Temporary access can be granted to an application even if it doesn’t have the permissions that would otherwise be required. This means that only applications that need permanent permissions for your providers are apps that continually access your data.

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                          
)

contentResolver that is part of a context is utilized to call the query function and a list of arguments can be passed if necessary. The only required argument is the content URI. Below is an explanation of each of the parameters.

  1. The content URI of the provider including the desired table.
  2. The columns definitions to return for each row, this is String array.
  3. The selection clause, similar to the WHERE clause only excluding the where. A ? is used in place of arguments.
  4. The arguments to be utilized for the selection clause that fill in the ?.
  5. The sort order such as "ASC" or "DESC".

Note: Allowing raw SQL statements from external sources can lead to malicious input from SQL injection attempts. Using the selection clause with ? representing a replaceable parameter and an array of selection arguments instead can prevent the user from making these attempts.

A content provider not only allows data to be read by an outside application, the data can also be updated, added to or deleted.

Inserting, updating and deleting data

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

Inserting data

Below is a statement to insert a record:

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

newValues is a ContentValues object which is populated with key-value pairs containing the column and value of the data to be inserted into the new row. newURI contains the content URI of the new record in the specific form — content://user_dictionary/words/<id_value>.

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. update() returns an Integer value 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. delete() returns an Integer value which represents the 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, and vnd.android.cursor.item for single rows.

The vnd stands for vendor and isn’t part of the package name of the app. The subtype of a custom MIME type is provider-specific and is generally defined in the contract class for the provider.

getType() of the provider returns a String in MIME format. If the provider returns a specific type of data, getType() returns the common MIME type for that data. If the content URI points to a row or a table of data, getType() returns a vendor specific formatted MIME type including the authority and the table name, e.g. vnd.android.cursor.dir/vnd.com.raywenderlich.android.contentprovidertodo.provider.todoitems. This MIME type is for multiple rows in the todoitems table with the authority com.raywenderlich.android.contentprovidertodo.provider.

A content URI can also perform pattern matches using content URIs that include wildcard characters.

  • * – Matches a String object of any valid characters of any length.
  • # – Matches a String object of numeric characters of any length.

Now that you’ve learned a little bit about content providers, it’s time to create one of your own.

Getting started

Locate the content-provider folder and open up the projects folder inside of it. Next, open the ContentProviderToDo app under the starter folder. Allow the project to sync, download dependencies, and setup the workplace environment. Build and run to see the main screen.

Add a Content Provider dialog.
Add a Content Provider dialog.

You have the shell for a basic todo list app, but if you try to add anything to the todo list now, nothing gets added.

The provider package

It’s a good idea to keep the provider classes in their own package. If you look inside the controller package, you’ll find the provider package. It contains the file with a ToDoContract class.

The contract class

Now, open ToDoContract.kt and examine the contents:

// 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.android.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.android.contentprovidertodo.provider.todoitems"

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

  // 10
  // Table Constants
  object ToDoTable {
    // 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" 
    }
  }
}
  1. The ALL_ITEMS constant is used for the URI when query() returns all the items in the database.
  2. count is the suffix used on the URI when the count of items in the table is requested.
  3. AUTHORITY is the prefix of the URI that serves as the symbolic name for the provider.
  4. CONTENT_PATH corresponds to the name of the todoitems table.
  5. CONTENT_URI is created by concatenating the authority, or name of the provider with the path, or the name of the table. Then it’s parsed into a URI used to get all the records from the provider.
  6. ROW_COUNT_URI is a second URI that utilizes the same authority but has the count content type. It’s used to retrieve the number of records in the table.
  7. SINGLE_RECORD_MIME_TYPE is the complete, custom MIME type for URIs that return a single record.
  8. MULTIPLE_RECORD_MIME_TYPE is the custom MIME type for URIs that will return multiple records. Notice the use of dir instead of itemitem is used for a single record MIME type.
  9. ToDoTable is an inner object that contains the name of the main table and definitions for the columns in the database.

ToDoContract is the Contract class that contains all the constant definitions you need for your content provider. This class can be distributed to client apps that would like to use the provider and will provide insight into what this provider has to provide.

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? :]

Right click the provider package and select New > Other > Content Provider. Provide the class name ToDoContentProvider and the URI authority com.raywenderlich.android.contentprovidertodo.provider. Also check the boxes for exported and enabled. Ensure that the source language is Kotlin, and press Finish.

Add a Content Provider dialog.
Add a Content Provider dialog.

The class name field provides the name of the new class that will be created in the provider package. The URI Authorities field can contain a semicolon-separated list of one to many URI authorities that identify the data under the purview of the content provider. Checking the exported field means that the component can be used by components of other applications. This automatically adds a <Provider> element into AndroidManifest.xml. Checking the enabled field allows the component to be instantiated by the system.

Now that you have added the template for your content provider, open AndroidManifest.xml to see the provider tag that has been added.

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

Notice how the location, authority and permissions are included in this tag as we specified through the dialog. Next, you need to implement the methods of the content provider.

Implementing the content provider methods

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.

Open ToDoContentProvider.kt. Notice how the class inherits from ContentProvider in the Android SDK. Before implementing the methods, add the following declarations inside the class above all the overridden method stubs:

  // 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
  1. Add an instance of the database handler as the content provider sits between the database and the rest of the program, also create a URI matcher to help construct the URIs.
  2. Create initializeURIMathching() and add each URI that this content provider can match with. This provider will accommodate a URI to retrieve a single record with an id hence the #, a URI to get all the records, and a URI to get a count of the number of records. Each URI includes the authority, the content path, any arguments represented by special characters, and a unique code.
  3. Declare some constants that represent the numeric code for the URI. The contract class gives descriptive names to the corresponding values these codes will match with. These codes are unique and chosen by the developer.

The next step is to start implementing the stub methods one by one. The template doesn’t always put them in the most logical order by default. You can reorder them if you like.

Initializing the database and Uris

Insert the code below into onCreate() stub replacing TODO:

context?.let {
	db = ToDoDatabaseHandler(it)
  // intialize the URIs
  initializeUriMatching()
}
return true

onCreate() prepares the content provider by instantiating the database handler object, calling initializeUriMatching() to initialize the URI matcher, and returns true to signal that this content provider was created successfully.

Resolving the MIME type

Next, implement getType() 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
}

This function accepts Uri and matches it with the URI code. Then it returns the correct MIME type. These constants are defined in the contract that has made the code for this function more self-descriptive.

Now that the trivial details are out of the way, you can jump into implementing the CRUD operations.

Querying the database

query() queries the database and returns the results. It 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 -> { 
  	uri.lastPathSegment?.let {
    	cursor = db.query(it.toInt())
    } 
  }
  URI_COUNT_CODE -> { cursor = db.count()}
  UriMatcher.NO_MATCH -> { /*error handling goes here*/ }
  else -> { /*unexpected problem*/ }
}
return cursor

Here you declare a Cursor object and assign it to null. Based on the provided uri, the URI matcher returns the corresponding code. Then, the WHEN statement calls the correct function on the database handler object to retrieve the results, assigns them to the cursor and returns cursor .

Modifying the adapter

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

  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"

These declarations provide the parameters you’ll need to pass to the contentResolver methods to query, update, insert and delete from the database.

Add the following lines to bindViews():

with(binding) {
  	// 1
    txtToDoName.text = toDo.toDoName 
    chkToDoCompleted.isChecked = toDo.isCompleted
  	// 2
  	imgDelete.setOnClickListener(this@ViewHolder)
    imgEdit.setOnClickListener(this@ViewHolder)
  	// 3
    chkToDoCompleted.setOnCheckedChangeListener { compoundButton, _ ->
      toDo.isCompleted = compoundButton.isChecked
      val values = ContentValues().apply {
        put(KEY_TODO_IS_COMPLETED, toDo.isCompleted)
        put(KEY_TODO_ID, toDo.toDoId)
        put(KEY_TODO_NAME, toDo.toDoName)
      }
      selectionArgs = arrayOf(toDo.toDoId.toString())
      context.contentResolver.update(
        Uri.parse(queryUri),
        values,
        selectionClause,
        selectionArgs
      )
    }
 }

In this code you:

  1. Fill up the RecyclerView item with the toDo item data — you provide its name and the flag that tells if the item is completed or not.
  2. Set setOnClickListener for both imgDelete and imgEdit.
  3. Apply setOnCheckedChangeListener for chkToDoCompleted in which you change the completed state for the toDo item and update contentResolver with appropriate ContentValues, arguments and URI.

Then, add this block to onClick():

val cursor = context.contentResolver.query(
  Uri.parse(queryUri),
  projection,
  selectionClause, 
  selectionArgs, 
  sortOrder
)

if (cursor != null) {
  if (cursor.moveToPosition(bindingAdapterPosition)) {
    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
    cursor.close()
    val toDo = ToDo(toDoId, toDoName, toDoCompleted)
    when (imgButton?.id) {
    	binding.imgDelete.id -> {
      	deleteToDo(toDo.toDoId)
      }
      binding.imgEdit.id -> {
      	editToDo(toDo)
       }	
    }
	}
}

With this code you set an action that will happen once the user click ons the delete or update button — deleteToDo() and editToDo() . Both of these methods are currently empty, you’ll implement them in a bit. To know which item to update or delete, you use query() that returns a cursor containing. quieried to-do item’s data . You use that data to create a ToDo item so you can pass it to deleteToDo() and its id to editToDo() as parameters.

Next, you’ll utilize the content provider by calling the content resolver’s functions.

The adapter needs to know how many rows there are to properly configure RecyclerView. Locate getItemCount() and insert this code above return:

// 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()) {
    val itemCount = cursor.getInt(0)
    cursor.close()
    return itemCount
  }
}

The query above utilizes the query String queryCountURI, which is appended with /count to query the provider for the count of records instead of returning all the records or a single item. Now wouldn’t it be neat if another app could utilize these queries as well?

Now, find TODO in onBindViewHolder() and replace it wiith this:

// 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
    cursor.close()
    val toDo = ToDo(toDoId, toDoName, toDoCompleted)
    holder.bindViews(toDo)
  }
}

In this code you:

  1. Call query() on the content resolver returns cursor that allows the app to iterate through all the records in the table, create a to-do item and bind the fields of the RecyclerView’s row to the fields of that to-do item.
  2. Create a ToDo object from the fields in the query and bind it to the view.

Lastly, add the code to insert a record. Then, you can test the basic functionality of the content provider.

Inserting items

Open ToDoContentProvider.kt and locate insert(). Replace its TODO in with:

values?.let {
	val id = db.insert(it.getAsString(KEY_TODO_NAME))
  	return Uri.parse("$CONTENT_URI/$id")
  }
return null

When inserting a to-do item, the only relevant information that is provided is the name of that item. Completed is false by default and the id field is provided by insert() itself. The database handler returns the id, and the content provider uses this id to return a URI where this provider can access the new record.

Next, open ToDoAdapter.kt and find insertToDo(). Replace TODO with:

// 1
val values = ContentValues()
values.put(KEY_TODO_NAME, toDoName)
// 2
context.contentResolver.insert(CONTENT_URI, values)
notifyDataSetChanged()

Here you:

  1. Create and populate a content values object.
  2. Run the insert query on the content resolver to insert the record. Then, notify adapter that change happened.

After all your hard work, it’s time to run the app and add a couple items! Build and run the app. Click the button in the lower left corner and add some items to your TODO list. They will be displayed in the list as you add them. Nice work! :]

Data appeared in the list.
Data appeared in the list.

The update and delete buttons will not actually do anything, yet. You’ll add the functionality to update (edit) an item, next.

Updating items

Open ToDoContentProvider.kt and copy the code below into the body of update():

values?.let {
  val toDo = ToDo(
    it.getAsLong(KEY_TODO_ID),
    it.getAsString(KEY_TODO_NAME),
    it.getAsBoolean(KEY_TODO_IS_COMPLETED)
  )
  return db.update(toDo)
}
return 0

First, you create a ToDo item by extracting the values utilizing the key values defined in the contract. Then pass this toDo to db to update the database.

Now, open ToDoAdapter.kt and replace TODO in editToDo() with the following:

 // 1
val values = ContentValues().apply {
  put(KEY_TODO_NAME, dialogToDoItemBinding.edtToDoName.text.toString())
  put(KEY_TODO_ID, toDo.toDoId)
  put(KEY_TODO_IS_COMPLETED, toDo.isCompleted)
}
// 2
context.contentResolver.update(
  Uri.parse(queryUri), 
  values, 
  selectionClause,
  selectionArgs
)
// 3
notifyDataSetChanged()

In this code block you:

  1. Create and populate the ContentValues object.
  2. Run the update query to update the record.
  3. Notify the adapter that the dataset has changed so RecyclerView updates.

Run the app and use the pencil icon to update an item that you added previously.

Updating item successful.
Updating item successful.

The app can now update items. But what about deleting them? You will add the ability to delete items in the next steps.

Deleting items

Deleting a record is simple. Open ToDoContentProvider.kt and replace TODO of delete():

selectionArgs?.get(0)?.let {
	return db.delete(parseLong(it))
}
return 0

This gets the id for the record to delete out of the selectionArgs and pass that value to the database handler to delete the record from the underlying database. The number of rows that are deleted is returned.

Next, open ToDoAdapter.kt and replace TODO of deleteToDo() with:

// 1
selectionArgs = arrayOf(id.toString())
// 2
context.contentResolver.delete(Uri.parse(queryUri), selectionClause, selectionArgs)
// 3
notifyDataSetChanged()
Toast.makeText(context, "Item deleted.", LENGTH_LONG).show()

Here you:

  1. Populate selectionArgs with the id of the record to delete.
  2. Run the query to delete the record.
  3. Notify the adapter that the dataset has changed and show a Toast message.

Run the app, you are now able to delete items from the list. Great!

Deleting item successful.
Deleting item successful.

Now you have created your very own content provider and content resolver in one. Even though it’s not required to utilize a content provider when outside apps aren’t accessing the data, sometimes it can provide an organizational layer of abstraction that can improve an app’s overall architecture. Now, if you’d like, you can tackle an additional challenge and create a client app that will utilize the content provider.

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’s 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:

  1. Locate your final contentprovider app in the file system and make a copy of the project folder with name final_client.. Append the word client to the name of the folder so you can tell the difference between the two apps.
  2. Open the app in Android Studio by going to File > Open… and select the new folder and click OK.
  3. Rename the package in the project to com.raywenderlich.android.contentprovidertodoclient by right clicking on the package and selecting Refactor > Rename…. A dialog emerges asking if you’d like to rename the directory or the package. Click Rename Package. You are then prompted for the new name so type that in and click Refactor. The last step is to click the Do Refactor button at the bottom of the editor.
    Do Refactor button to rename the package
    Do Refactor button to rename the package
  4. Open the build.gradle app module and change the app id to com.raywenderlich.android.contentprovidertodoclient.
  5. Open strings.xml and change the name element value to Content Provider To Do List Client.
  6. Open AndroidManifest.xml and add the following permission just inside the <manifest> tag.
<uses-permission android:name = "com.raywenderlich.android.contentprovidertodo.provider.PERMISSION"/>

This allows the client to use whatever permissions the provider set.

  1. Delete the <provider> tag and its contents. This app does not provide data, it simply relies on the data shared by the other app.
  2. Remove the files model/ToDoDbSchema.kt, controller/provider/ToDoContentProvider.kt and controller/ToDoDatabaseHandler.kt by right clicking them, selecting Refactor > Safe Delete…. Uncheck any boxes that would search for usages and hit OK. If the Usages Detected dialog pops up simply press Delete Anyway. Allow the editor to sync the gradle dependencies if prompted to.
  3. Once those files are removed, run the app. Any items you’ve added in the previous app will show in the new app. But how is this possible? You just deleted the database handler class as well as the schema and the content provider. If you create, update or delete any to-do items and then run the previous app that contains the content provider, those changes will reflect in that app as well. The two apps are sharing data through the same content provider.

Congratulate yourself! You just created two apps that are sharing the same data, think of the possibilities!

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’s 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.

Learn more specifics about content provider permissions here: https://developer.android.com/guide/topics/providers/content-provider-basics#Permissions.

Learn more about how to protect against malicious data from this guide found here: https://developer.android.com/guide/topics/providers/content-provider-basics#Injection

Find out more about using content providers with Storage Access Framework here: https://developer.android.com/guide/topics/providers/document-provider.

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.