What is a Content Provider#
A Content Provider is a feature provided by the Android system that manages content stored in its own storage or accesses content stored by other applications, allowing content to be shared between different applications.
Basic Protocol Format#
For simple data transmission, you only need to know the basic format, which is: content://org.z5r.ta.provider/test
Here, org.z5r.ta.provider is the path of your provider, and test is the name of the table where the provider's content is stored. If you want to specify a database, you can write it as content://org.z5r.ta.provider/test/database_name
How to Implement a Content Provider#
All the following code is written in Kotlin
- Create a new class named TestContentProvider in the application and inherit from the existing ContentProvider provided by Android. You will then be prompted to implement the corresponding methods. The code is as follows:
package org.z5r.ta.provider
import android.content.ContentProvider
import android.content.ContentValues
import android.database.Cursor
import android.net.Uri
class TestContentProvider: ContentProvider() {
override fun onCreate(): Boolean {
TODO("Not yet implemented")
}
override fun query(
uri: Uri,
projection: Array<out String>?,
selection: String?,
selectionArgs: Array<out String>?,
sortOrder: String?
): Cursor? {
TODO("Not yet implemented")
}
override fun getType(uri: Uri): String? {
TODO("Not yet implemented")
}
override fun insert(uri: Uri, values: ContentValues?): Uri? {
TODO("Not yet implemented")
}
override fun delete(uri: Uri, selection: String?, selectionArgs: Array<out String>?): Int {
TODO("Not yet implemented")
}
override fun update(
uri: Uri,
values: ContentValues?,
selection: String?,
selectionArgs: Array<out String>?
): Int {
TODO("Not yet implemented")
}
}
- Initialize some variables and internal helper classes that will be used
class TestContentProvider: ContentProvider() {
companion object {
// TAG for logging
const val TAG: String = "TestContentProvider"
// Database name
const val DATABASE_NAME: String = "test.db"
// Database version number (not much use here)
const val DATABASE_VERSION: Int = 1
// Table name
const val TEST_TABLE_NAME: String = "test"
// Path of the content provider
private const val AUTHORITY: String = "org.z5r.ta.provider"
// Path matching, temporarily empty, used later to determine if the content path matches
var sUriMatcher: UriMatcher? = null
// Uri matching result code mapping (used to distinguish operations)
const val TEST: Int = 1
// Same as above
const val TEST_ID: Int = 2
// Query field projection, maps the column names passed by the caller to database field names
var testProjectionMap: HashMap<String, String>? = null
// Helper for database operations
lateinit var dbHelper: DatabaseHelper
// Construct provider Uri
val CONTENT_URI: Uri = Uri.parse("content://$AUTHORITY/test")
// Content type, although you can write anything, it's still better to follow the standard: vnd.android.cursor.dir/your_custom_here
const val CONTENT_TYPE = "vnd.android.cursor.dir/vnd.z5r.test"
// First field, ID (write as needed, here just a key-value pair table)
const val ID = "_id"
// Second field, result
const val RESULT = "result"
}
/**
* Inner class, database version management helper class
*/
class DatabaseHelper internal constructor(context: Context?) :
SQLiteOpenHelper(context, DATABASE_NAME, null, DATABASE_VERSION) {
override fun onCreate(db: SQLiteDatabase) {
// Create table
db.execSQL(((((("CREATE TABLE $TEST_TABLE_NAME").toString() + " (" + ID)
+ " LONG PRIMARY KEY," + RESULT) + " VARCHAR(255));")))
}
override fun onUpgrade(db: SQLiteDatabase, oldVersion: Int, newVersion: Int) {
Log.w(TAG, "Upgrading database from version $oldVersion to $newVersion, which will destroy all old data")
// Drop table
db.execSQL("DROP TABLE IF EXISTS $TEST_TABLE_NAME")
onCreate(db)
}
}
// Other code ...
}
- Implement the onCreate method
override fun onCreate(): Boolean {
// Initialize database helper object
dbHelper = DatabaseHelper(context)
// Initialize uri match result as not matched
sUriMatcher = UriMatcher(UriMatcher.NO_MATCH)
// Add the Uri to match
// AUTHORITY: provider path
// TEST_TABLE_NAME: table name
// TEST: Uri match result code mapping
sUriMatcher?.addURI(AUTHORITY, TEST_TABLE_NAME, TEST)
sUriMatcher?.addURI(AUTHORITY, "$TEST_TABLE_NAME/#", TEST_ID)
// Initialize database field projection
testProjectionMap = HashMap<String, String>()
testProjectionMap!![ID] = ID
testProjectionMap!![RESULT] = RESULT
// Return True, handle any exceptions as needed
return true
}
- Implement the query method
override fun query(
uri: Uri,
projection: Array<out String>?,
selection: String?,
selectionArgs: Array<out String>?,
sortOrder: String?
): Cursor? {
// Copy condition fields
var newSelection = selection
// Build database operation object
val qb = SQLiteQueryBuilder()
// Specify table name
qb.tables = TEST_TABLE_NAME
// Specify field projection
qb.projectionMap = testProjectionMap
// Select corresponding operation based on Uri match result code mapping
when (sUriMatcher?.match(uri)) {
TEST -> {}
TEST_ID -> {
// Filter data by ID
newSelection = selection + "_id = " + uri.lastPathSegment
}
// Unsupported operation
else -> throw IllegalArgumentException("Unknown URI $uri")
}
// Copy the readable database object from the database helper
val db = dbHelper.readableDatabase
// Start querying
val c = qb.query(db, projection, newSelection, selectionArgs, null, null, sortOrder)
// Register content change listener
c.setNotificationUri(context?.contentResolver, uri)
// Return result
return c
}
- Implement the getType method
override fun getType(uri: Uri): String {
// Match Uri
when (sUriMatcher!!.match(uri)) {
// If matching operation 1, return content type
TEST -> return CONTENT_TYPE
else -> throw java.lang.IllegalArgumentException("Unknown URI $uri")
}
}
- Implement the insert method
override fun insert(uri: Uri, values: ContentValues?): Uri {
// Check if Uri matches operation 1
if (sUriMatcher!!.match(uri) != TEST) {
throw java.lang.IllegalArgumentException("Unknown URI $uri")
}
// Get the values to insert, return an empty Map if null
val newVal: ContentValues = if (values != null) {
ContentValues(values)
} else {
ContentValues()
}
// Get the writable database object from the database helper
val db = dbHelper.writableDatabase
// Start inserting data
// Table name, field, value
val rowId = db.insert(TEST_TABLE_NAME, RESULT, newVal)
// Check if ID is greater than 0 (greater than 0 indicates success)
if (rowId > 0) {
// Get the new data row's uri
val testUri = ContentUris.withAppendedId(CONTENT_URI, rowId)
// Notify the content resolver that a row's content has changed
context!!.contentResolver.notifyChange(testUri, null)
// Return uri
return testUri
}
// Insertion failed
throw SQLException("Failed to insert row into $uri")
}
- Implement the delete method
override fun delete(uri: Uri, selection: String?, selectionArgs: Array<out String>?): Int {
// Get the writable database object from the database helper
val db = dbHelper.writableDatabase
// Copy condition fields
var newSelection = selection
// Match Uri operation
when (sUriMatcher?.match(uri)) {
TEST -> {}
TEST_ID -> {
// Filter data by ID
newSelection = selection + "_id = " + uri.lastPathSegment
}
else -> throw IllegalArgumentException("Unknown URI $uri")
}
// Execute deletion
val count = db.delete(TEST_TABLE_NAME, newSelection, selectionArgs)
// Notify content resolver
context?.contentResolver?.notifyChange(uri, null)
// Return the number of rows deleted
return count
}
- Implement the update method
override fun update(
uri: Uri,
values: ContentValues?,
selection: String?,
selectionArgs: Array<out String>?
): Int {
// Get the writable database object from the database helper
val db = dbHelper.writableDatabase
// Number of updates
val count: Int
// Match Uri operation
when (sUriMatcher!!.match(uri)) {
// Update data based on conditions
TEST -> count = db.update(TEST_TABLE_NAME, values, selection, selectionArgs)
else -> throw java.lang.IllegalArgumentException("Unknown URI $uri")
}
// Notify content update
context!!.contentResolver.notifyChange(uri, null)
// Return the number of affected rows
return count
}
Register and Define Permissions#
A content provider cannot be used just by writing it; it also needs to be registered and configured with permissions.
<!-- AndroidManifest.xml -->
<application android:name="application's content remains unchanged">
<provider
android:name=".provider.TestContentProvider (this is the package path of the provider)"
android:authorities="org.z5r.ta.provider (this is the provider path, consistent with the AUTHORITY defined in the above Kotlin code)"
android:permission="android.permission.READ_USER_DICTIONARY (permission, just write this)"
android:exported="true (whether to expose, change to false if you don't want other applications to access)" >
<grant-uri-permission android:path="test (this is the authorized path after the '/')" />
</provider>
</application>
For more permissions, refer to: https://developer.android.com/privacy-and-security/security-tips?hl=en#ContentProviders
Using the Content Provider#
- Request permissions
<!-- AndroidManifest.xml -->
<manifest xmlns:android="http://schemas.android.com/apk/res/android" xmlns:tools="http://schemas.android.com/tools">
<!-- Required to access data, if not written, access will be denied -->
<uses-permission android:name="android.permission.READ_USER_DICTIONARY" />
<!-- Required to access data, if not written, nothing can be accessed (cannot find provider Failed to find provider info for xxxx) -->
<uses-permission android:name="android.permission.QUERY_ALL_PACKAGES" tools:ignore="QueryAllPackagesPermission" />
<!-- Other code -->
</manifest>
- Use the provider
val uri = Uri.parse("content://org.z5r.ta.provider/test")
val resObj = contentResolver.query(uri, null, null, null)
// This is mandatory; if not written, an exception will be thrown: android.database.CursorIndexOutOfBoundsException: Index -1 requested, with a size of 1
// You need to specify the initial pointer position
resObj?.moveToFirst()
val colIdx = resObj?.getColumnIndex("result")
val res = colIdx?.let { resObj.getStringOrNull(it) }
if (res != null) {
// Here you can directly process the retrieved data
}