编写一个Android ContentProvider 内容提供器

什么是内容提供器

内容提供器（Content Provider）是一个 Android 系统提供的一个对自身存储的内容进行管理或者对其他应用存储的内容进行访问的功能，即提供内容在不同应用之间共享的功能。

基本协议格式

对于简单传递数据，只需要知道基本的格式就可以，格式：content://org.z5r.ta.provider/test

其中 org.z5r.ta.provider 是你的提供器的路径，test 是提供器内容存储的表的名称。如果你想指定一个库，可以写成 content://org.z5r.ta.provider/test/ 数据库名称

参考：https://developer.android.com/guide/topics/providers/content-provider-creating?hl=zh-cn#content-uri-patterns

如何实现一个 Content Provider

以下所有代码均由 Kotlin 编写

我们在应用中新建一个名为 TestContentProvider 的类并继承 Android 提供的原有的 ContentProvider，随后会提示我们需要实现相应的方法。代码如下：

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

初始化一些要用到的变量和内部辅助类

Copy
class TestContentProvider: ContentProvider() {

companion object {
// 用于打印日志的TAG
const val TAG: String = "TestContentProvider"
// 数据库名称
const val DATABASE_NAME: String = "test.db"
// 数据库版本号（在这里并没什么用）
const val DATABASE_VERSION: Int = 1
// 表名称
const val TEST_TABLE_NAME: String = "test"
// 内容提供器的路径
private const val AUTHORITY: String = "org.z5r.ta.provider"
// 路径匹配，暂时为空，后面用来判断内容路径是否匹配
var sUriMatcher: UriMatcher? = null
// Uri匹配结果编码映射（用于区分操作）
const val TEST: Int = 1
// 同上
const val TEST_ID: Int = 2
// 查询的字段投影，将调用者传入查询的列名映射到数据库字段名
var testProjectionMap: HashMap<String, String>? = null
// 辅助操作数据库
lateinit var dbHelper: DatabaseHelper
// 构造提供器Uri
val CONTENT_URI: Uri = Uri.parse("content://$AUTHORITY/test")
// 内容类型，虽然写什么都行，但还是按照标准来写：vnd.android.cursor.dir/这里自定义
const val CONTENT_TYPE = "vnd.android.cursor.dir/vnd.z5r.test"
// 第一个字段，ID（看情况写，这里就写一个键值对的表）
const val ID = "_id"
// 第二个字段，result
const val RESULT = "result"
}

/**
* 内部类，数据库版本管理辅助类
*/
class DatabaseHelper internal constructor(context: Context?) :
SQLiteOpenHelper(context, DATABASE_NAME, null, DATABASE_VERSION) {
override fun onCreate(db: SQLiteDatabase) {
// 建表
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")
// 删表
db.execSQL("DROP TABLE IF EXISTS $TEST_TABLE_NAME")
onCreate(db)
}
}

// 其他代码 ...
}

实现 onCreate 方法

Copy
override fun onCreate(): Boolean {
// 初始化数据库辅助对象
dbHelper = DatabaseHelper(context)
// 初始化uri匹配结果为不匹配
sUriMatcher = UriMatcher(UriMatcher.NO_MATCH)
// 添加要匹配的Uri
// AUTHORITY：提供器路径
// TEST_TABLE_NAME：表名
// TEST：Uri匹配结果编码映射
sUriMatcher?.addURI(AUTHORITY, TEST_TABLE_NAME, TEST)
sUriMatcher?.addURI(AUTHORITY, "$TEST_TABLE_NAME/#", TEST_ID)
// 初始化数据库字段投影
testProjectionMap = HashMap<String, String>()
testProjectionMap!![ID] = ID
testProjectionMap!![RESULT] = RESULT
// 返回True，要做什么异常处理就自己写吧
return true
}

实现 query 查询方法

Copy
override fun query(
uri: Uri,
projection: Array<out String>?,
selection: String?,
selectionArgs: Array<out String>?,
sortOrder: String?
): Cursor? {
// 拷贝条件字段
var newSelection = selection
// 构建数据库操作对象
val qb = SQLiteQueryBuilder()
// 指定表名
qb.tables = TEST_TABLE_NAME
// 指定字段投影
qb.projectionMap = testProjectionMap
// 根据Uri匹配结果编码映射选择对应的操作
when (sUriMatcher?.match(uri)) {
TEST -> {}
TEST_ID -> {
// 根据ID过滤数据
newSelection = selection + "_id = " + uri.lastPathSegment
}
// 不支持的操作
else -> throw IllegalArgumentException("Unknown URI $uri")
}
// 拷贝数据库辅助类的读取对象
val db = dbHelper.readableDatabase
// 开始查询
val c = qb.query(db, projection, newSelection, selectionArgs, null, null, sortOrder)
// 注册内容变化监听
c.setNotificationUri(context?.contentResolver, uri)
// 返回结果
return c
}

实现 getType 方法

Copy
override fun getType(uri: Uri): String {
// 匹配Uri
when (sUriMatcher!!.match(uri)) {
// 如果匹配1操作就返回内容类型
TEST -> return CONTENT_TYPE
else -> throw java.lang.IllegalArgumentException("Unknown URI $uri")
}
}

实现 insert 数据插入

Copy
override fun insert(uri: Uri, values: ContentValues?): Uri {
// 判断Uri是否匹配1操作
if (sUriMatcher!!.match(uri) != TEST) {
throw java.lang.IllegalArgumentException("Unknown URI $uri")
}
// 获取要插入的值，如果为空就返回一个空的Map
val newVal: ContentValues = if (values != null) {
ContentValues(values)
} else {
ContentValues()
}
// 获取数据库辅助操作的写库对象
val db = dbHelper.writableDatabase
// 开始插入数据
// 表名，字段，值
val rowId = db.insert(TEST_TABLE_NAME, RESULT, newVal)
// 判断ID是否大于0（大于0表示成功）
if (rowId > 0) {
// 获取新的数据行的uri
val testUri = ContentUris.withAppendedId(CONTENT_URI, rowId)
// 通知内容获取器某行的内容发生了变化
context!!.contentResolver.notifyChange(testUri, null)
// 返回uri
return testUri
}
// 没插入成功
throw SQLException("Failed to insert row into $uri")
}

实现 delete 数据删除方法

Copy
override fun delete(uri: Uri, selection: String?, selectionArgs: Array<out String>?): Int {
// 获取数据库辅助操作的写库对象
val db = dbHelper.writableDatabase
// 拷贝条件字段
var newSelection = selection
// 匹配Uri操作
when (sUriMatcher?.match(uri)) {
TEST -> {}
TEST_ID -> {
// 根据ID过滤数据
newSelection = selection + "_id = " + uri.lastPathSegment
}
else -> throw IllegalArgumentException("Unknown URI $uri")
}
// 执行删除
val count = db.delete(TEST_TABLE_NAME, newSelection, selectionArgs)
// 通知内容获取器
context?.contentResolver?.notifyChange(uri, null)
// 返回删除的行数
return count
}

实现 update 数据更新

Copy
override fun update(
uri: Uri,
values: ContentValues?,
selection: String?,
selectionArgs: Array<out String>?
): Int {
// 获取数据库辅助操作的写库对象
val db = dbHelper.writableDatabase
// 更新的数量
val count: Int
// 匹配Uri操作
when (sUriMatcher!!.match(uri)) {
// 根据条件更新数据
TEST -> count = db.update(TEST_TABLE_NAME, values, selection, selectionArgs)
else -> throw java.lang.IllegalArgumentException("Unknown URI $uri")
}
// 通知内容更新
context!!.contentResolver.notifyChange(uri, null)
// 返回影响行数
return count
}

注册并定义权限

内容提供器不是写了就可以用，还需要注册和配置权限。

Copy

<application android:name="application的内容保持原有不变">
<provider
android:name=".provider.TestContentProvider（这里是提供器的包路径）"
android:authorities="org.z5r.ta.provider（这里是提供器路径，和上面Kotlin代码部分定义的AUTHORITY保持一致）"
android:permission="android.permission.READ_USER_DICTIONARY（权限，就写这个就行）"
android:exported="true（是否公开，如果不想让其他应用访问就需要改为false）" >
<grant-uri-permission android:path="test（这是授权的路径“/”后面的部分）" />
</provider>
</application>

更多权限需要参考这里：https://developer.android.com/privacy-and-security/security-tips?hl=zh-cn#ContentProviders

使用内容提供器

申请权限

Copy

<manifest xmlns:android="http://schemas.android.com/apk/res/android" xmlns:tools="http://schemas.android.com/tools">

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

<uses-permission android:name="android.permission.QUERY_ALL_PACKAGES" tools:ignore="QueryAllPackagesPermission" />

</manifest>

使用提供器

Copy
val uri = Uri.parse("content://org.z5r.ta.provider/test")
val resObj = contentResolver.query(uri, null, null, null)
// 这里是必须写的，不写的话会抛出异常：android.database.CursorIndexOutOfBoundsException: Index -1 requested, with a size of 1
// 需要指定初始指针位置
resObj?.moveToFirst()
val colIdx = resObj?.getColumnIndex("result")
val res = colIdx?.let { resObj.getStringOrNull(it) }
if (res != null) {
// 这里可以直接处理获取到的数据
}