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

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

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 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">

The above permission is added inside the manifest tag in the AndroidManifest.xml file.

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 is one permission to rule them all :].
  • Separate read and write provider-level permission – Read and write permission can be set separately for the whole provider.
  • Path-level permission – Read, write or read/write permission 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                          
)

The contentResolver object 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 a string array.
  3. The selection clause, similar to a 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, 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                         
)

newValues is a ContentValues 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 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. 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.

The vnd stands for vendor and is not 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.

The getType method of the provider returns a String in MIME format. If the provider returns a specific type of data, the common MIME type for that data should be returned. If the content URI points to a row or a table of data, a vendor specific formatted MIME type including the authority and the table name should be returned such as vnd.android.cursor.dir/vnd.com.raywenderlich.contentprovidertodo.provider.todoitems. This MIME type is for multiple rows in the todoitems table of an app with the authority com.raywenderlich.contentprovidertodo.provider. A content URI can also perform pattern matches using content URIs that include wildcard characters.

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

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

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" 
    }
  }
}
  1. The constant ALL_ITEMS is the code used for the URI when the query method should return 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. This is then parsed into a URI that is used to get all the records from the provider.

  6. ROW_COUNT_URI A second URI that utilizes the same authority but has the count content type. This URI is 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 item, item is used for a single record mime type.

  9. DATABASE_NAME contains the name of the database.

  10. ToDoTable is an inner object that contains the name of the main table and definitions for the columns in the database.

The Contract class 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.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 enabled allows the component to be instantiated by the system.

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

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

Notice how the location, the authority and the permissions are included in this tag as we specified through the dialog. Now the methods must be implemented in the content 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.

Open up the file you just created in the provider package called ToDoContentProvider.kt. There are six TODO tags, one for each method that is required to implement. Notice how the class inherits from the ContentProvider class 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 URI strings.
  2. Create a function called initializeURIMathching. In this function, 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.

Implementing onCreate

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

db = ToDoDatabaseHandler(context)	
initializeUriMatching()
return true

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

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
}

The getType function accepts a 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, the CRUD operations of the content provider can be implemented.

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

Here you declare a cursor object and assign it to null. Based on the provided uri the URI matcher returns the corresponding code and the when statement calls the correct function on the database handler object to retrieve the results and assign them to the curser. The cursor is then returned.

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"

These declarations provide the parameters you will need to pass to the contentResolver methods to query, update, insert and delete from the database. Next you will utilize the content provider by calling the content resolver’s functions.

The adapter needs to know how many rows there are to properly configure the recyclerview. Locate getItemCount and insert the code below into the body of the function, directly above the return statement:

// 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)
  }
}

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 the comment that states // TODO: Add query here to get all items in onBindViewHolder. Replace the comment with this code:

// 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)
  }
}
  1. Calling query on the content resolver returns a cursor that will allow the app to iterate through all the records in the table, create a to-do item and bind the fields of the recycler view’s row to the fields of that to-do item.
  2. Create a ToDo item 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.

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")

If you have multiple import options for KEY_TODO_NAME, use

import com.raywenderlich.contentprovidertodo.Controller.provider.ToDoContract.ToDoTable.Columns.KEY_TODO_NAME

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 the insert method 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 the comment in the body with:

// 1
var values = ContentValues()
values.put(KEY_TODO_NAME, toDoName)
// 2
context.contentResolver.insert(CONTENT_URI, values)
  1. Create and populate a content values object.
  2. Run the insert query on the content resolver to insert the record.

After all your hard work it is 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! :] Note that “update” and “delete” buttons will not actually do anything, yet. You’ll add the functionality to update (edit) an item, next.

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)

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

Now open ToDoAdapter.kt and replace the TODO comment in the editToDo function with the following:

// 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()
  1. Create and populate the content values object.
  2. Run the update query to update the record.
  3. Notify the adapter that the dataset has changed so the recyclerview will update.

Run the app and use the pencil icon to update an item that you added previously. The app can now display, insert and update items. But what about deleting them? You will add the ability to delete items in the next steps.

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)))

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 add this code to the body of deleteToDo replacing the comment:

// 1
selectionArgs = arrayOf(id.toString())
// 2
context.contentResolver.delete(Uri.parse(queryUri), selectionClause, selectionArgs)
// 3
notifyDataSetChanged()
  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 so the recyclerview will update.

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

Now you have created your very own content provider and content resolver in one. Even though it is 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 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:

  1. Locate your finished contentprovider app in the file system and make a copy of the project folder. Append the word client to the name of the folder so you can tell the difference between the two apps. For example copy the starter folder and call the resulting copy starter_client.
  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.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.contentprovidertodoclient.
  5. Open the strings.xml file and change the name element to Content Provider To Do List Client.
  6. Open the manifest and add the following permission just inside the <manifest> tag. This allows the client to use whatever permissions the provider set.
<uses-permission android:name = "com.raywenderlich.contentprovidertodo.provider.PERMISSION"/>
  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.
  4. 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 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.

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

Learn more about different MIME types here: https://en.wikipedia.org/wiki/Media_type#mime.types.

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.
© 2026 Kodeco Inc.