第8章 跨程序共享数据:ContentProvider
8.1 什么是ContentProvider
ContentProvider用于在不同的应用程序之间实现数据共享。不同于文件存储和SharedPreferences存储中的两种全局可读写操作模式,ContentProvider可以选择只对哪一份部分数据进行共享。
8.2 运行时权限
首先需要了解一下运行时权限。
8.2.1 Android权限机制详解
常用权限大致分为两类:普通权限和危险权限。前者系统会自动帮我们进行授权,不需要用户手动操作;后者表示那些可能会触及用户隐私或者对设备安全性造成影响的权限,必须由用户手动授权。P321的表8.1罗列了所有的危险权限。注意:原则上用户一旦同意某个权限申请后,同组的其他权限也会被系统自动授权。
8.2.2 在程序运行时申请权限
以拨打电话为例。首先在AndroidManifest.xml中添加以下代码以申请拨打电话的权限:
<uses-permission android:name="android.permission.CALL_PHONE" />
接下来修改MainActivity中的代码:
class MainActivity : AppCompatActivity() {
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
val binding = ActivityMainBinding.inflate(layoutInflater)
setContentView(binding.root)
binding.makeCall.setOnClickListener {
if (ContextCompat.checkSelfPermission(this, android.Manifest.permission.CALL_PHONE) != PackageManager.PERMISSION_GRANTED) {
ActivityCompat.requestPermissions(this, arrayOf(android.Manifest.permission.CALL_PHONE), 1)
} else {
call()
}
}
}
override fun onRequestPermissionsResult(
requestCode: Int,
permissions: Array<out String>,
grantResults: IntArray
) {
super.onRequestPermissionsResult(requestCode, permissions, grantResults)
when (requestCode) {
1 -> {
if (grantResults.isNotEmpty() && grantResults[0] == PackageManager.PERMISSION_GRANTED) {
call()
} else {
Toast.makeText(this, "You denied the permission", Toast.LENGTH_SHORT).show()
}
}
}
}
private fun call() {
try {
val intent = Intent(Intent.ACTION_CALL)
intent.data = Uri.parse("tel:18017690109")
startActivity(intent)
} catch (e: SecurityException) {
e.printStackTrace()
}
}
}
第一步通过ContextCompat.checkSelfPermission()
方法判断用户是否给我们授权。该方法接受两个参数:Context和具体的权限名。
书上具体的权限名是
Manifest.permission.CALL_PHONE
,但是在Android Studio中编译不通过。AI告诉我需要使用完全限定的类名(即包括包名),因此需要改成android.Manifest.permission.CALL_PHONE
才行。
如果没有授权,则调用ActivityCompat.requestPermissions()
方法申请授权。该方法接受三个参数
- Activity实例;
- 一个String数组,存放要申请的权限名;
- 请求码:只要是唯一值就可以。
8.3 访问其他程序中的数据
ContentProvider一般有两种用法:
- 使用现有的ContentProvider读取和操作相应程序中的数据;
- 创建自己的ContentProvider给陈晓古的数据提供外部访问接口。
如果一个程序通过ContentProvider对其数据提供了外部访问接口,那么其他任何应用都可以对这部分数据进行访问。
8.3.1 ContentResolver的基本用法
如果想访问ContentProvider中共享的数据,就一定要借助ContentResolver
类。ContentResolver
同样提供insert()
、update()
、delete()
和query()
方法对数据增删改查。
不同的是,ContentResolver
中不接收表名参数,而是使用一个Uri参数(内容URI)代替。它又两部分组成:authority(区分不同应用程序)和path(区分同一程序内不同的表),最后在字符串头部加上协议声明,就构成了内容URI最标准的格式:
content://com.example.app.provider/table1
content://com.example.app.provider/table2
其中,content
是协议声明;com.example.app.provider
是authority;table1
是path。
之后,只需要调用Uri.parse()
方法可以解析它,就可以将其作为参数传入了。
val uri = Uri.parse("content://com.example.app.provider/table1")
val cursor = contentResolver.query(
uri,
projection, // 指定查询的列名: SELECT column1, column2
selection, // 指定where的约束条件: WHERE column = value
selectionArgs, // 为where中的占位符提供具体的值
sortOrder) // 指定查询结果的排序方式: ORDER BY column1, column2
// 查:遍历所有行,将数据从Cursor对象中逐个读取,取出每一行中相应列的数据
while (cursor.moveToNext()) {
val col1 = cursor.getString(cursor.getColumnIndex("column1"))
val col2 = cursor.getInt(cursor.getColumnIndex("column2"))
}
cursor.close()
// 增:
val values = contentValuesOf("columns1" to "text", "columns2" to 1)
ContentResolver.insert(uri, values)
// 删:
ContentResolver.delete(uri, "columns2 = ?", arryOf("1"))
// 改:清空column1的数据
val values = contentValuesOf("columns1" to "")
ContentResolver.update(uri, values, "column1 = ? and column2 = ?", arrayOf("text", "1"))
8.4 创建自己的ContentProvider
8.4.1 创建ContentProvider的步骤
想要实现跨程序共享数据,需要通过新建一个类去继承ContentProvider的方式去实现。ContentProvider有6种抽象方法,使用时需要将6个方法全部重写:
onCreate()
:初始化。通常在这里完成创建和升级数据库的操作;返回true表示初始化成功,否则失败;query()
:查询数据。uri参数表示查哪张表、projection参数确定查询哪几列、selection和selectionArgs确定查哪几行、sortOrder用于对结果排序,查询的结果存放在Cursor对象中返回;insert()
:uri参数、values参数存储待添加的数据。添加完成后返回一个用于表示这条心记录的URI;update()
:uri、values、selection和selectionArgs。返回受影响的行数;delete()
:uri、values、selection和selectionArgs。返回被删除的行数;getType()
:根据传入的uri,返回相应的MIME类型。
MIME (Multipurpose Internet Mail Extensions) 是描述消息内容类型的标准,用来表示文档、文件或字节流的性质和格式。
标准的内容URI以路径结尾,代表期望访问该表中所有数据。我们可以在内容URI后面加一个id,例如:
content://com.example.app/provider/table/1
它以id结尾,代表期望访问该表中具有相应id的数据。
可以使用通配符分别匹配这两种格式的内容URI,规则为:
*
表示匹配任意长度的任意字符;#
表示匹配任意长度的数字。
getType()
方法:一个内容Uri对应的MIME字符串由三部分组成:
- 必须以
vnd
开头; - 如果内容Uri以路径结尾,则接
android.cursor.dir/
;如果内容Uri以id结尾,则接android.cursor.item/
; - 最后接上
vnd.<authority>.<path>
.
借助UriMatcher就可以实现匹配内容Uri的功能了,详见下节的代码。
8.4.2 实现跨程序数据共享
超长代码预警。。。
class DatabaseProvider : ContentProvider() {
private val bookDir = 0 // 访问Book表中所有数据
private val bookItem = 1 // 访问Book表中单条数据
private val categoryDir = 2 // 访问Category表中所有数据
private val categoryItem = 3 // 访问Category表中单条数据
private val authority = "com.example.broadcastbestpractice.provider"
private var dbHelper: MyDatabaseHelper? = null
// 懒加载:只有当变量首次被调用时才被执行,并将最后一行代码的返回值赋给uriMatcher
private val uriMatcher by lazy {
val matcher = UriMatcher(UriMatcher.NO_MATCH)
matcher.addURI(authority, "book", bookDir)
matcher.addURI(authority, "book/#", bookItem)
matcher.addURI(authority, "category", categoryDir)
matcher.addURI(authority, "category/#", categoryItem)
matcher
}
override fun delete(uri: Uri, selection: String?, selectionArgs: Array<String>?)= dbHelper?.let {
val db = it.writableDatabase
val deleteRows = when (uriMatcher.match(uri)) {
bookDir -> db.delete("Book", selection, selectionArgs)
bookItem -> {
val bookId = uri.pathSegments[1]
db.delete("Book", "id = ?", arrayOf(bookId))
}
categoryDir -> db.delete("Category", selection, selectionArgs)
categoryItem -> {
val categoryId = uri.pathSegments[1]
db.delete("Category", "id = ?", arrayOf(categoryId))
}
else -> 0
}
deleteRows
} ?: 0
override fun getType(uri: Uri) = when (uriMatcher.match(uri)) {
bookDir -> "vnd.android.cursor.dir/vnd.com.example.broadcastbestpractice.provider.book"
bookItem -> "vnd.android.cursor.item/vnd.com.example.broadcastbestpractice.provider.book"
categoryDir -> "vnd.android.cursor.dir/vnd.com.example.broadcastbestpractice.provider.category"
categoryItem -> "vnd.android.cursor.item/vnd.com.example.broadcastbestpractice.provider.category"
else -> null
}
override fun insert(uri: Uri, values: ContentValues?) = dbHelper?.let {
// 添加数据
val db = it.writableDatabase
val uriReturn = when (uriMatcher.match(uri)) {
bookDir, bookItem -> {
val newBookId = db.insert("Book", null, values)
Uri.parse("content://$authority/book/$newBookId")
}
categoryDir, categoryItem -> {
val newCategoryId = db.insert("Category", null, values)
Uri.parse("content://$authority/category/$newCategoryId")
}
else -> null
}
uriReturn
}
override fun onCreate(): Boolean = context?.let {
dbHelper = MyDatabaseHelper(it, "BookStore.db", 2)
true
} ?: false
override fun query(
uri: Uri, projection: Array<String>?, selection: String?,
selectionArgs: Array<String>?, sortOrder: String?
) = dbHelper?.let {
// 查询数据
val db = it.readableDatabase
val cursor = when ( uriMatcher.match(uri)) {
bookDir -> db.query("Book", projection, selection, selectionArgs, null, null, sortOrder)
bookItem -> {
val bookId = uri.pathSegments[1] // 获得id(第0个位置存储的是路径)
db.query("Book", projection, "id = ?", arrayOf(bookId), null, null, sortOrder)
}
categoryDir -> db.query("Category", projection, selection, selectionArgs, null, null, sortOrder)
categoryItem -> {
val categoryId = uri.pathSegments[1]
db.query("Category", projection, "id = ?", arrayOf(categoryId), null, null, sortOrder)
}
else -> null
}
cursor
}
override fun update(
uri: Uri, values: ContentValues?, selection: String?,
selectionArgs: Array<String>?
)= dbHelper?.let {
val db = it.writableDatabase
val updateRows = when (uriMatcher.match(uri)) {
bookDir -> db.update("Book", values, selection, selectionArgs)
bookItem -> {
val bookId = uri.pathSegments[1]
db.update("Book", values, "id = ?", arrayOf(bookId))
}
categoryDir -> db.update("Category", values, selection, selectionArgs)
categoryItem -> {
val categoryId = uri.pathSegments[1]
db.update("Category", values, "id = ?", arrayOf(categoryId))
}
else -> 0
}
updateRows
} ?: 0
}
之后关闭该程序,新建一个新的程序用来查询该程序的数据:
class MainActivity : AppCompatActivity() {
private lateinit var binding: ActivityMainBinding
private var bookId: String? = null
@SuppressLint("Range")
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
binding = ActivityMainBinding.inflate(layoutInflater)
val view = binding.root
setContentView(view)
binding.addData.setOnClickListener {
val uri = Uri.parse("content://com.example.broadcastbestpractice.provider/book")
val values = contentValuesOf("name" to "A Clash of Kings", "author" to "George Martin", "pages" to 1040, "price" to 22.85)
val newUri = contentResolver.insert(uri, values)
if (newUri != null) {
bookId = newUri.pathSegments?.get(1)
Log.d("MainActivity1", "Data Added")
} else {
Log.e("MainActivity1", "Insertion failed: newUri is null")
}
bookId = newUri?.pathSegments?.get(1)
}
binding.queryData.setOnClickListener {
val uri = Uri.parse("content://com.example.broadcastbestpractice.provider/book")
contentResolver.query(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"))
Log.d("MainActivity1", "name is $name")
Log.d("MainActivity1", "author is $author")
Log.d("MainActivity1", "pages is $pages")
Log.d("MainActivity1", "price is $price")
}
close()
}
}
binding.updateData.setOnClickListener {
bookId?.let {
val uri = Uri.parse("content://com.example.broadcastbestpractice.provider/book/$it")
val values = contentValuesOf("name" to "A Storm of Swords", "pages" to 1216, "price" to 24.05)
contentResolver.update(uri, values, null, null)
}
Log.d("MainActivity1", "Data Updated")
}
binding.deleteData.setOnClickListener {
bookId?.let {
val uri = Uri.parse("content://com.example.broadcastbestpractice.provider/book/$it")
contentResolver.delete(uri, null, null)
}
Log.d("MainActivity1", "Data Deleted")
}
}
}
但是一开始运行是失败的,后来查看日志发现是因为之前写database的时候,数据库的version被我改成了4,日志提醒我version不能downgrade from 4 to 2 虽然不知道哪里是2但是把4改成2就能运行了。