【Android入门】6、ContentProvider:跨程序的数据共享:访问其他 App、被其他 App 访问
八、跨程序数据共享 ContentProvider
应用场景: 例如通讯录/媒体库共享给其他APP, 拿到了其他APP的数据, 就可以提供更好的内容, 有更好的用户体验
- Android曾用
SharedPreferences的MODE_WORLD_READABLE和MODE_WORLD_WORLD_WRITABLE
这两种操作模式, 供给其他APP访问当前APP的数据 - Android目前更推荐用
ContentProvider
, 其更安全可靠, 是标准方式 ContentProvider
是一种机制, 允许访问其他APP的数据, 且保证安全性(例如可只共享一部分数据)
8.1 运行时权限
在AndroidManifest.xml
中添加权限声明, 即可得到下图效果
<manifest xmlns:android="http://schemas.android.com/apk/res/android"
package="com.example.broadcasttest">
<uses-permission android:name="android.permission.RECEIVE_BOOT_COMPLETED" />
...
</manifest>
早期Android必须要求APP一次性申请所有权限(如位置/相机等), 若用户不同意则压根无法安装, 容易让厂商店大欺客
现在Android已经可以在运行时申请权限(如需要定位时才访问位置), 即使用户不同意也不影响使用
权限分为危险权限和普通权限, 前者共11组30个
8.2 打电话权限案例
效果如下
布局如下
<?xml version="1.0" encoding="utf-8"?>
<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:tools="http://schemas.android.com/tools"
android:layout_width="match_parent"
android:layout_height="match_parent"
tools:context=".MainActivity">
<Button
android:id="@+id/makeCall"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:text="Make Call" />
</LinearLayout>
权限文件如下
<uses-permission android:name="android.permission.CALL_PHONE" />
代码如下
package com.example.contentprovidertest
import android.Manifest
import android.content.Intent
import android.content.pm.PackageManager
import android.net.Uri
import androidx.appcompat.app.AppCompatActivity
import android.os.Bundle
import android.widget.Toast
import androidx.core.app.ActivityCompat
import androidx.core.content.ContextCompat
import kotlinx.android.synthetic.main.activity_main.*
class MainActivity : AppCompatActivity() {
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
setContentView(R.layout.activity_main)
makeCall.setOnClickListener {
if (ContextCompat.checkSelfPermission(
this,
Manifest.permission.CALL_PHONE
) != PackageManager.PERMISSION_GRANTED
) {
// 未授权, 准备申请授权, 第二个参数是需要申请的权限范围, 第三个参数传一个唯一值即可
ActivityCompat.requestPermissions(this, arrayOf(Manifest.permission.CALL_PHONE), 1)
} else {
call() // 已授权, 直接拨号
}
}
}
override fun onRequestPermissionsResult(
requestCode: Int,
permissions: Array<out String>,
grantResults: IntArray
) {
// Android系统弹窗, 请求用户的授权
super.onRequestPermissionsResult(requestCode, permissions, grantResults)
when (requestCode) {
1 -> {
// 授权的结果封装在grantResults()方法中
if (grantResults.isNotEmpty() && grantResults[0] == PackageManager.PERMISSION_GRANTED) {
call() // 用户授权通过, 直接拨号, 同时也会记录在Android系统中, 下次则不会弹窗请求用户的权限
} 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:10086")
startActivity(intent)
} catch (e: SecurityException) {
e.printStackTrace()
}
}
}
用户授权后, 也可在Settings->App->Permission Manager
手动管理授权
8.3 ContentProvider 访问其他 App 的数据
若一个APP通过ContentProvider
对外部提供了访问接口, 则其他APP都可访问此数据, 例如Android的通讯录, 短信, 媒体库都提供了类似接口
可通过getContentResolver()
获取实例, 其提供了一系列增删改查方法
通过uri
表示操作的是什么内容, 一般约定为如下示例, 即协议://app/表
content://com.example.app.provider/table1 // 查整个表
content://com.example.app.provider/table2/3 // 查表中id=3的行
// *表示匹配任意长度的任意字符
// #表示匹配任意长度的数字
content://com.example.app.provider/* // 匹配任意表
content://com.example.app.provider/table1/# // 匹配表的任意一行
拿到字符串uri后, 可通过下例访问
val uri = Uri.parse("content://com.example.app.provider/table1") // 得到uri对象
val cursor = contentResolver.query(uri, projection, selection, selectionArgs, sortOrder);
// query
while (cursor.moveToNext()) {
val column1 = cursor.getString(cursor.getColumnIndex("column1"))
val column2 = cursor.getInt(cursor.getColumnIndex("column2"))
}
cursor.close()
// insert
val values = contentValuesOf("column1" to "text", "column2" to 1)
contentResolver.insert(uri, values)
// update
val values = contentValuesOf("column1" to "")
contentResolver.update(uri, values, "column1 = ? and column2 = ?", arrayOf("text", "1"))
// delete
contentResolver.delete(uri, "column2 = ?", arrayOf("1"))
- 下文以读取系统联系人案例演示
首先提前申请权限
<uses-permission android:name="android.permission.READ_CONTACTS" />
设计布局
<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:tools="http://schemas.android.com/tools"
android:layout_width="match_parent"
android:layout_height="match_parent"
tools:context=".MainActivity">
<ListView
android:id="@+id/contactsView"
android:layout_width="match_parent"
android:layout_height="match_parent"
/>
</LinearLayout>
业务逻辑
class MainActivity : AppCompatActivity() {
private val contactsList = ArrayList<String>()
private lateinit var adapter: ArrayAdapter<String>
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
setContentView(R.layout.activity_main)
adapter = ArrayAdapter(this, android.R.layout.simple_list_item_1, contactsList)
contactsView.adapter = adapter
if (ContextCompat.checkSelfPermission(this, Manifest.permission.READ_CONTACTS)
!= PackageManager.PERMISSION_GRANTED
) {
ActivityCompat.requestPermissions(
this,
arrayOf(Manifest.permission.READ_CONTACTS), 1
)
} else {
readContacts()
}
}
override fun onRequestPermissionsResult(
requestCode: Int, permissions: Array<String>,
grantResults: IntArray
) {
super.onRequestPermissionsResult(requestCode, permissions, grantResults)
when (requestCode) {
1 -> {
if (grantResults.isNotEmpty()
&& grantResults[0] == PackageManager.PERMISSION_GRANTED
) {
readContacts()
} else {
Toast.makeText(
this, "You denied the permission",
Toast.LENGTH_SHORT
).show()
}
}
}
}
@SuppressLint("Range")
private fun readContacts() {
// 查询联系人数据
contentResolver.query(
ContactsContract.CommonDataKinds.Phone.CONTENT_URI,
null, null, null, null
)?.apply {
while (moveToNext()) {
// 获取联系人姓名
val displayName = getString(
getColumnIndex(
ContactsContract.CommonDataKinds.Phone.DISPLAY_NAME
)
)
// 获取联系人手机号
val number = getString(
getColumnIndex(
ContactsContract.CommonDataKinds.Phone.NUMBER
)
)
contactsList.add("$displayName\n$number")
}
adapter.notifyDataSetChanged()
close()
}
}
}
运行效果如下
8.4 创建 ContentProvider 供其他 APP 访问
需要新建一个类, 继承ContentProvider类
, 需要override
如下方法
package com.example.contentprovidertest
import android.content.ContentProvider
import android.content.ContentValues
import android.content.UriMatcher
import android.database.Cursor
import android.net.Uri
class MyProvider : ContentProvider() {
private val table1Dir = 0
private val table1Item = 1
private val table2Dir = 2
private val table2Item = 3
private val uriMatcher = UriMatcher(UriMatcher.NO_MATCH)
init {
uriMatcher.addURI("com.example.app.provider", "table1", table1Dir) // 访问table1表中的所有数据
uriMatcher.addURI("com.example.app.provider ", "table1/#", table1Item) // 访问table1表中的单条数据
uriMatcher.addURI("com.example.app.provider ", "table2", table2Dir) // 访问table2表中的所有数据
uriMatcher.addURI("com.example.app.provider ", "table2/#", table2Item) // 访问table2表中的单条数据
}
// 初始化ContentProvider的时候调用。通常会在这里完成对数据库的创建和
// 升级等操作,返回true表示ContentProvider初始化成功,返回false则表示失败
override fun onCreate(): Boolean {
TODO("Not yet implemented")
}
override fun query(
uri: Uri, projection: Array<String>?, selection: String?,
selectionArgs: Array<String>?, sortOrder: String?
): Cursor? {
when (uriMatcher.match(uri)) {
table1Dir -> {
// 查询table1表中的所有数据
}
table1Item -> {
// 查询table1表中的单条数据
}
table2Dir -> {
// 查询table2表中的所有数据
}
table2Item -> {
// 查询table2表中的单条数据
}
}
}
// 根据传入的URI返回MIME类型
override fun getType(uri: Uri) = when (uriMatcher.match(uri)) {
table1Dir -> "vnd.android.cursor.dir/vnd.com.example.app.provider.table1"
table1Item -> "vnd.android.cursor.item/vnd.com.example.app.provider.table1"
table2Dir -> "vnd.android.cursor.dir/vnd.com.example.app.provider.table2"
table2Item -> "vnd.android.cursor.item/vnd.com.example.app.provider.table2"
else -> null
}
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")
}
}
-
其中
MIME字符串
由3部分构成- 必须以vdn开头
- 如果内容URI
- 以路径结尾,则后接
android.cursor.dir/
- 以id结尾,则后接
android.cursor.item/
- 以路径结尾,则后接
- 最后接上
vnd.<authority>.<path>
-
所以
content://com.example.app.provider/table1
的URI对应的MIME是vnd.android.cursor.dir/vnd.com.example.app.provider.table1
-
所以
content://com.example.app.provider/table1/1
的URI对应的MIME是vnd.android.cursor.item/vnd.com.example.app.provider.table1
因为访问者APP
必须通过提供者APP
暴露的指定URI
访问, 所以没有安全问题 -
案例如下, 首先在DatabaseTest提供一个
ContentProvider
New->Other->ContentProvider
填写如下信息
上图操作后, AndroidStudio
即会在AndroidManifest.xml
中自动添加如下代码
<provider
android:name=".DatabaseProvider"
android:authorities="com.example.databasetest.provider"
android:enabled="true"
android:exported="true"></provider>
修改DatabaseProvider
的代码如下
package com.example.databasetest
import android.content.ContentProvider
import android.content.ContentValues
import android.database.Cursor
import android.net.Uri
import android.content.UriMatcher
import android.os.CancellationSignal
import android.database.sqlite.SQLiteDatabase
class DatabaseProvider : ContentProvider() {
private val bookDir = 0
private val bookItem = 1
private val categoryDir = 2
private val categoryItem = 3
private val authority = "com.example.databasetest.provider"
private var dbHelper: MyDatabaseHelper? = null
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 onCreate() = 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]
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 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 update(uri: Uri, values: ContentValues?, selection: String?, selectionArgs: Array<String>?) =
dbHelper?.let {
// 更新数据
val db = it.writableDatabase
val updatedRows = 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
}
updatedRows
} ?: 0
override fun delete(uri: Uri, selection: String?, selectionArgs: Array<String>?) = dbHelper?.let {
// 删除数据
val db = it.writableDatabase
val deletedRows = 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
}
deletedRows
} ?: 0
override fun getType(uri: Uri) = when (uriMatcher.match(uri)) {
bookDir -> "vnd.android.cursor.dir/vnd.com.example.databasetest.provider.book"
bookItem -> "vnd.android.cursor.item/vnd.com.example.databasetest.provider.book"
categoryDir -> "vnd.android.cursor.dir/vnd.com.example.databasetest.provider.category"
categoryItem -> "vnd.android.cursor.item/vnd.com.example.databasetest.provider.category"
else -> null
}
}
- 其次, 新建一个名为
ProviderTest项目
,其layout如下
<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
android:orientation="vertical"
android:layout_width="match_parent"
android:layout_height="match_parent" >
<Button
android:id="@+id/addData"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:text="Add To Book" />
<Button
android:id="@+id/queryData"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:text="Query From Book" />
<Button
android:id="@+id/updateData"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:text="Update Book" />
<Button
android:id="@+id/deleteData"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:text="Delete From Book" />
</LinearLayout>
逻辑如下
package com.example.providertest
import android.annotation.SuppressLint
import android.net.Uri
import android.os.Bundle
import android.util.Log
import androidx.appcompat.app.AppCompatActivity
import androidx.core.content.contentValuesOf
import kotlinx.android.synthetic.main.activity_main.*
class MainActivity : AppCompatActivity() {
var bookId: String? = null
@SuppressLint("Range")
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
setContentView(R.layout.activity_main)
addData.setOnClickListener {
// 添加数据
//首先调用了Uri.parse()方法将一个内容URI解析成Uri对象,然后把要添加的数据都存放到
//ContentValues对象中,接着调用ContentResolver的insert()方法执行添加操作就可以
//了。注意,insert()方法会返回一个Uri对象,这个对象中包含了新增数据的id,我们通过
//getPathSegments()方法将这个id取出,稍后会用到它。
val uri = Uri.parse("content://com.example.databasetest.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)
bookId = newUri?.pathSegments?.get(1)
}
queryData.setOnClickListener {
// 查询数据
//调用了Uri.parse()方法将一个内容URI解析成Uri对象,然后调用
//ContentResolver的query()方法查询数据,查询的结果当然还是存放在Cursor对象中。之
//后对Cursor进行遍历,从中取出查询结果,并一一打印出来
val uri = Uri.parse("content://com.example.databasetest.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("MainActivity", "book name is $name")
Log.d("MainActivity", "book author is $author")
Log.d("MainActivity", "book pages is $pages")
Log.d("MainActivity", "book price is $price")
}
close()
}
}
updateData.setOnClickListener {
// 更新数据
//为了不想让Book表中的其他行受到影响,在调用Uri.parse()方法时,
//给内容URI的尾部增加了一个id,而这个id正是添加数据时所返回的。这就表示我们只希望更新
//刚刚添加的那条数据,Book表中的其他行都不会受影响
bookId?.let {
val uri = Uri.parse(
"content://com.example.databasetest.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)
}
}
deleteData.setOnClickListener {
// 删除数据
//在内容URI里指定了一个id,因此只会删掉拥有相应id的那行数据,Book表中的其他数据都不会受影响
bookId?.let {
val uri = Uri.parse(
"content://com.example.databasetest.provider/book/$it"
)
contentResolver.delete(uri, null, null)
}
}
}
}
点击添加书籍时, 会添加到DatabaseTest程序的数据库中
点击查询书籍时, 会打印如下日志
2022-08-17 14:12:52.362 7413-7413/com.example.providertest D/MainActivity: book name is Game of Thrones
2022-08-17 14:12:52.363 7413-7413/com.example.providertest D/MainActivity: book author is George Martin
2022-08-17 14:12:52.363 7413-7413/com.example.providertest D/MainActivity: book pages is 720
2022-08-17 14:12:52.363 7413-7413/com.example.providertest D/MainActivity: book price is 20.85
2022-08-17 14:12:52.363 7413-7413/com.example.providertest D/MainActivity: book name is The Da Vinci Code
2022-08-17 14:12:52.363 7413-7413/com.example.providertest D/MainActivity: book author is Dan Brown
2022-08-17 14:12:52.363 7413-7413/com.example.providertest D/MainActivity: book pages is 454
2022-08-17 14:12:52.363 7413-7413/com.example.providertest D/MainActivity: book price is 16.96
2022-08-17 14:12:52.363 7413-7413/com.example.providertest D/MainActivity: book name is The Lost Symbol
2022-08-17 14:12:52.363 7413-7413/com.example.providertest D/MainActivity: book author is Dan Brown
2022-08-17 14:12:52.363 7413-7413/com.example.providertest D/MainActivity: book pages is 510
2022-08-17 14:12:52.363 7413-7413/com.example.providertest D/MainActivity: book price is 19.95
点击更新书籍, 再点击查询书籍, 同样会打印在日志中