什麼是內容提供器#
內容提供器(Content Provider)是 Android 系統提供的一個對自身存儲的內容進行管理或者對其他應用存儲的內容進行訪問的功能,即提供內容在不同應用之間共享的功能。
基本協議格式#
對於簡單傳遞數據,只需要知道基本的格式就可以,格式:content://org.z5r.ta.provider/test
其中 org.z5r.ta.provider 是你的提供器的路徑,test 是提供器內容存儲的表的名稱。如果你想指定一個庫,可以寫成 content://org.z5r.ta.provider/test/ 資料庫名稱
如何實現一個 Content Provider#
以下所有代碼均由 Kotlin 編寫
- 我們在應用中新建一個名為 TestContentProvider 的類並繼承 Android 提供的原有的 ContentProvider,隨後會提示我們需要實現相應的方法。代碼如下:
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")
}
}
- 初始化一些要用到的變量和內部輔助類
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 方法
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 查詢方法
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 方法
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 數據插入
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 數據刪除方法
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 數據更新
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
}
註冊並定義權限#
內容提供器不是寫了就可以用,還需要註冊和配置權限。
<!-- AndroidManifest.xml -->
<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
使用內容提供器#
- 申請權限
<!-- AndroidManifest.xml -->
<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" />
<!-- 用於獲取數據,不寫會什麼都訪問不到(找不到提供器 Failed to find provider info for xxxx) -->
<uses-permission android:name="android.permission.QUERY_ALL_PACKAGES" tools:ignore="QueryAllPackagesPermission" />
<!-- 其他代碼 -->
</manifest>
- 使用提供器
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) {
// 這裡可以直接處理獲取到的數據
}