banner
ZetoHkr

ZetoHkr

摸🐟从未停止,努力从未开始
github

Write an Android ContentProvider

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

Reference: https://developer.android.com/guide/topics/providers/content-provider-creating?hl=en#content-uri-patterns

How to Implement a Content Provider#

All the following code is written in Kotlin

  1. 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")
		}
	}
  1. 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 ...
	}
  1. 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
		}
  1. 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
	}
  1. 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")
			}
		}
  1. 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")
		}
  1. 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
		}
  1. 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#

  1. 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>
  1. 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
			}
Loading...
Ownership of this post data is guaranteed by blockchain and smart contracts to the creator alone.