ContentProvider是四大组件之一,常用来实现进程间通信,提供供其他应用调用的数据接口等。直接调用ContentResolve,可以获取系统音乐,联系人等。
Uri和UriMatch
在学习ContentProvider之前,先了解一下Uri。Uri(Universal Resource Identifier)通用资源标识符,代表要操作的数据。在ContentProvider中,用来表示要操作的ContentProvider和具体的表。
UriMatch用来过滤Uri匹配出具体的操作表。
Uri由authority、path和协议声明组成,authority用于对不同的应用作区分,一般采用包名进行命名。如包名为com.example.app,那么对应的authority可以命名为com.example.app.provider。path用于对同一应用中的不同表作区分,通常加到authority后面,如com.example.app.provider/table1。Uri还需要在字符串头部加上协议声明content。如下,表示包名为“com.example.app“的包下的ContentProvider,其所对应的表table1和table2。
content://com.example.app.provider/table1
content://com.example.app.provider/table2
调用Uri.parse(String)就可以将Uri字符串包装成Uri对象。
ContentProvider的创建和注册
可以通过右键直接创建一个ContentProvider,也可以通过继承ContentProvider实现。需要重写onCreate()、getType(Uri)和增删改查等方法。
一般在onCreate()中实现数据库的初始化,其返回值表示数据库初始化是否成功。
getType(Uri)传入一个Uri参数,根据这个参数返回mimeType字符串(这个mimeType就是Intent过滤器中的mineType属性)。ContentProvider的mimeType有固定的格式,如果表示整个表,头部是“vnd.android.cursor.dir/vnd“,如果是单条记录,以”vnd.android.cursor.item/vnd“开头。后面接上authority和表名。
增删改查方法会传入对应的属性,对数据库进行相应的操作,返回操作结果即可。
一个ContentProvider可以处理多个表,也可以处理表的某一条记录。所以一般还会创建一个UriMatcher进行Uri的过滤,以确定操作目标。
示例代码如下:
class SpendProvider : ContentProvider() {
private val spendDir = 0
private val spendItem = 1
private val authority = "com.example.bookkeeping.provider"
private lateinit var db: SQLiteDatabase
private val uriMatcher by lazy {
UriMatcher(UriMatcher.NO_MATCH).apply {
addURI(authority, "spend", spendDir)
addURI(authority, "spend/#", spendItem)
}
}
override fun delete(uri: Uri, selection: String?, selectionArgs: Array<String>?): Int =
when (uriMatcher.match(uri)) {
spendDir -> db.delete("Spend", selection, selectionArgs)
spendItem -> {
val spendId = uri.pathSegments[1]
db.delete("Spend", "id=?", arrayOf(spendId))
}
else -> 0
}
override fun getType(uri: Uri): String? = when (uriMatcher.match(uri)) {
spendDir -> "vnd.android.cursor.dir/vnd.$authority.spend"
spendItem -> "vnd.android.cursor.item/vnd.$authority.spend"
else -> null
}
override fun insert(uri: Uri, values: ContentValues?): Uri? =
when (uriMatcher.match(uri)) {
spendDir, spendItem -> {
val spendId = db.insert("Spend", null, values)
Uri.parse("content://$authority/spend/$spendId")
}
else -> null
}
override fun onCreate(): Boolean = context?.let {
LitePal.initialize(it)
db = LitePal.getDatabase()
true
} ?: false
override fun query(
uri: Uri, projection: Array<String>?, selection: String?,
selectionArgs: Array<String>?, sortOrder: String?
): Cursor? = when (uriMatcher.match(uri)) {
spendDir -> {
db.query("Spend", projection, selection, selectionArgs, null, null, sortOrder)
}
spendItem -> {
val spendId = uri.pathSegments[1]
db.query("Spend", projection, "id=?", arrayOf(spendId), null, null, sortOrder)
}
else -> null
}
override fun update(
uri: Uri, values: ContentValues?, selection: String?,
selectionArgs: Array<String>?
): Int =
when (uriMatcher.match(uri)) {
spendDir -> {
db.update("Spend", values, selection, selectionArgs)
}
spendItem -> {
val spendId = uri.pathSegments[1]
db.update("Spend", values, "id=?", arrayOf(spendId))
}
else -> 0
}
}
四大组件都需要在Manifest中进行注册,ContentProvider也不例外。在<provider>标签中,name属性填写组件名,authorities属性填写该组件的标识,即Uri的authority部分,exported默认为true,才能和其他应用进行交互。
注意,在Android11以上使用ContentProvider需要添加< queries>来指定需要和哪些应用进行交互
<manifest xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:tools="http://schemas.android.com/tools"
package="com.example.bookkeeping">
<uses-permission android:name="android.permission.READ_EXTERNAL_STORAGE" />
<uses-permission android:name="android.permission.WRITE_EXTERNAL_STORAGE" />
<queries>
<package android:name="com.example.contactstest"/>
</queries>
<application ...>
<provider
android:name=".provider.SpendProvider"
android:authorities="com.example.bookkeeping.provider"
android:enabled="true"
android:exported="true" />
</application>
</manifest>
ContentProvider的使用
当获取了目标ContentProvider的Uri信息后,可以通过context.contentResolver进行交互。
调用contentResolver的增删改查方法,传入Uri和对应的参数,即可实现进程间通信。
class MainActivity : BaseActivity<ActivityMainBinding>() {
override fun onCreateViewBinding(layoutInflater: LayoutInflater): ActivityMainBinding =
ActivityMainBinding.inflate(layoutInflater)
override fun initViews() {
viewBinding.run {
arrayOf(
btnAdd,
btnDelete,
btnQuery,
btnUpdate
).forEach { it.setOnClickListener(::onViewClick) }
}
}
private var bookId: String? = null
private val uri = "content://com.example.contactstest.provider/book"
private fun onViewClick(v: View) {
when (v.id) {
R.id.btn_add -> {
val value = contentValuesOf(
"name" to "A Clash of Kings", "author" to "George Martin",
"pages" to 1040, "price" to 22.85
)
val newUri = contentResolver.insert(Uri.parse(uri), value)
bookId = newUri?.pathSegments?.get(1)
}
R.id.btn_delete -> {
bookId?.let {
contentResolver.delete(Uri.parse("$uri/$it"), null, null)
}
}
R.id.btn_query -> {
contentResolver.query(Uri.parse(uri), null, null, null, null)?.apply {
while (moveToNext()) {
val name = getString(getColumnIndex("name"))
val author = getString(getColumnIndex("author"))
val pages = getInt(getColumnIndex("pages"))
val price = getDouble(getColumnIndex("price"))
LogUtils.d(
TAG,
"book name:$name\nauthor:$author\npages:$pages\nprice:$price"
)
}
}
}
R.id.btn_update -> {
bookId?.let {
val values = contentValuesOf(
"name" to "A Storm of Swords",
"pages" to 1216, "price" to 24.05
)
contentResolver.update(Uri.parse("$uri/$it"), values, null, null)
}
}
}
}
}