ContentProvider
Android数据持久化技术,包括文件存储、SharedPreferences存储以及数据库存储,这些持久化技术所保存的数据只能在当前应用程序中访问,ContentProvider 可以解决这个问题
ContentProvider 主要用于在不同的应用程序之间实现数据共享的功能,它提供了一套完整的机制,允许一个程序访问另一个程序中的数据,同时还能保证被访问数据的安全性。ContentProvider可以选择只对哪一部分数据进行共享,从而保证我们程序中的隐私数据不会有泄漏的风险
Android运行时权限
Android现在将常用的权限大致归成了两类,一类是普通权限,一类是危险权限
普通权限指的是那些不会直接威胁到用户的安全和隐私的权限,对于这部分权限申请,系统会自动帮我们进行授权,不需要用户手动操作
危险权限则表示那些可能会触及用户隐私或者对设备安全性造成影响的权限,如获取设备联系人信息、定位设备的地理位置等,对于这部分权限申请,必须由用户手动授权才可以,否则程序就无法使用相应的功能,下面是到Android 10系统为止所有的危险权限
每个危险权限都属于一个权限组,我们在进行运行时权限处理时使用的是权限名。原则上,用户一旦同意了某个权限申请之后,同组的其他权限也会被系统自动授权
每当要使用一个权限时,如果是这张表中的权限,就需要进行运行时权限处理,否则,只需要在 AndroidManifest.xml 文件中添加一下权限声明
运行时申请权限
下面是直接打电话,并进行 运行时申请权限 的示例
在 AndroidManifest.xml 中声明权限
<uses-permission android:name="android.permission.CALL_PHONE" />
代码
class MainActivity : AppCompatActivity() {
companion object {
private const val TAG = "MainActivity"
private const val CALL_PHONE_REQUEST_CODE = 6
}
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
setContentView(R.layout.activity_contentprovider_main)
make_call.setOnClickListener {
// 检查权限
if (ContextCompat.checkSelfPermission(
applicationContext,
Manifest.permission.CALL_PHONE
) == PackageManager.PERMISSION_DENIED
) {
// 没有权限,请求权限
ActivityCompat.requestPermissions(
this,
arrayOf(Manifest.permission.CALL_PHONE),
CALL_PHONE_REQUEST_CODE
)
} else {
call()
}
}
}
override fun onRequestPermissionsResult(
requestCode: Int,
permissions: Array<out String>,
grantResults: IntArray
) {
super.onRequestPermissionsResult(requestCode, permissions, grantResults)
when (requestCode) {
CALL_PHONE_REQUEST_CODE -> {
if (grantResults.all { it == PackageManager.PERMISSION_GRANTED }) {
// 被授权了,直接打电话
call()
} else {
Toast.makeText(this, "you denied the permission", Toast.LENGTH_SHORT).show()
}
}
}
}
private fun call() {
val intent = Intent(Intent.ACTION_CALL)
intent.data = Uri.parse("tel:10086")
startActivity(intent)
}
}
访问其他程序中的数据
ContentProvider的用法一般有两种:一种是使用现有的ContentProvider读取和操作相应程序中的数据;另一种是创建自己的ContentProvider,给程序的数据提供外部访问接口
如果一个应用程序通过ContentProvider对其数据提供了外部访问接口,那么任何其他的应用程序都可以对这部分数据进行访问。Android系统中自带的通讯录、短信、媒体库等程序都提供了类似的访问接口
ContentResolver的基本用法
想要访问ContentProvider中共享的数据,就一定要借助ContentResolver类,可以通过Context中的getContentResolver()方法获取该类的实例。
ContentResolver中提供了一系列的方法用于对数据进行增删改查操作:insert()、update()、delete()、query()
查询数据
ContentResolver中的增删改查方法都是不接收表名参数的,而是使用一个Uri参数代替,这个参数被称为内容URI。内容URI给ContentProvider中的数据建立了唯一标识符,它主要由两部分组成:authority和path。authority是用于对不同的应用程序做区分的,一般为了避免冲突,会使用应用包名,例如:com.example.app.provider。path则是用于对同一应用程序中不同的表做区分的。再在头部加上协议声明 content://,就是一个URI字符串了。例如:
content://com.example.app.provider/table1
content://com.example.app.provider/table2
在得到了内容URI字符串之后,我们还需要将它解析成Uri对象才可以作为参数传入进行查询
val uri = Uri.parse("content://com.example.app.provider/table1")
contentResolver.query(uri,null, null, null, null)
下面是query方法的参数说明
查询完成后返回的仍然是一个Cursor对象,这时我们就可以将数据从Cursor对象中逐个读取出来了
while (cursor.moveToNext()) {
val column1 = cursor.getString(cursor.getColumnIndex("column1"))
val column2 = cursor.getString(cursor.getColumnIndex("column2"))
}
cursor.close()
添加数据
val values = contentValuesOf("column1" to "text", "column2" to 1)
contentResolver.insert(uri,values)
更新数据
val values = contentValuesOf("column1" to "text", "column2" to 1)
contentResolver.update(uri, values, "column1= ? and column2 = ?", arrayOf("text", "1"))
删除数据
contentResolver.delete(uri, "column2 = ?", arrayOf("1"))
读取系统联系人
在 AndroidManifest.xml 中声明权限
<uses-permission android:name="android.permission.READ_CONTACTS" />
代码
class MainActivity : AppCompatActivity() {
companion object {
private const val TAG = "MainActivity"
private const val READ_CONTACTS_REQUEST_CODE = 7
}
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
setContentView(R.layout.activity_contentprovider_main)
read_contacts.setOnClickListener {
// 检查权限
if (ContextCompat.checkSelfPermission(
applicationContext,
Manifest.permission.READ_CONTACTS
) == PackageManager.PERMISSION_DENIED
) {
// 没有权限,请求权限
ActivityCompat.requestPermissions(
this,
arrayOf(Manifest.permission.READ_CONTACTS),
CALL_PHONE_REQUEST_CODE
)
} else {
readContacts()
}
}
}
/**
* 读取联系人
*/
private fun readContacts() {
contentResolver.query(
ContactsContract.CommonDataKinds.Phone.CONTENT_URI,
null,
null,
null,
null
)?.apply {
while (moveToNext()) {
val name =
getString(getColumnIndex(ContactsContract.CommonDataKinds.Phone.DISPLAY_NAME))
val number =
getString(getColumnIndex(ContactsContract.CommonDataKinds.Phone.NUMBER))
Log.d(TAG, "readContacts: name=$name number=$number")
}
close()
}
}
override fun onRequestPermissionsResult(
requestCode: Int,
permissions: Array<out String>,
grantResults: IntArray
) {
super.onRequestPermissionsResult(requestCode, permissions, grantResults)
when (requestCode) {
READ_CONTACTS_REQUEST_CODE -> {
if (grantResults.all { it == PackageManager.PERMISSION_GRANTED }) {
// 被授权了,读取联系人
readContacts()
} else {
Toast.makeText(this, "you denied the permission", Toast.LENGTH_SHORT).show()
}
}
}
}
}
创建自己的ContentProvider
如果想要实现跨程序共享数据的功能,可以通过新建一个类去继承ContentProvider的方式来实现
class MyProvider : ContentProvider() {
/**
* 初始化ContentProvider的时候调用
* 通常会在这里完成对数据库的创建和升级等操作
* 返回true表示ContentProvider初始化成功,返回false则表示失败
* @return Boolean
*/
override fun onCreate(): Boolean {
}
/**
* 从ContentProvider中查询数据
*
* @param uri 参数用于确定查询哪张表
* @param projection 参数用于确定查询哪些列
* @param selection 用于约束查询哪些行
* @param selectionArgs 用于约束查询哪些行
* @param sortOrder 参数用于对结果进行排序,
* @return 查询的结果存放在Cursor对象中返回
*/
override fun query(
uri: Uri,
projection: Array<out String>?,
selection: String?,
selectionArgs: Array<out String>?,
sortOrder: String?
): Cursor? {
}
/**
* 根据传入的内容URI返回相应的MIME类型
* @param uri Uri
* @return String?
*/
override fun getType(uri: Uri): String? {
}
/**
* 向ContentProvider中添加一条数据
*
* @param uri 用于确定要添加到的表
* @param values 待添加的数据保存在values参数中
* @return 返回一个用于表示这条新记录的URI
*/
override fun insert(uri: Uri, values: ContentValues?): Uri? {
}
/**
* 从ContentProvider中删除数据
*
* @param uri 用于确定删除哪一张表中的数据
* @param selection 用于约束删除哪些行
* @param selectionArgs 用于约束删除哪些行
* @return Int 被删除的行数将作为返回值返回
*/
override fun delete(uri: Uri, selection: String?, selectionArgs: Array<out String>?): Int {
}
/**
* 更新ContentProvider中已有的数据
*
* @param uri 用于确定更新哪一张表中的数据
* @param values 新数据保存在values参数中
* @param selection 用于约束更新哪些行
* @param selectionArgs 用于约束更新哪些行
* @return Int 受影响的行数将作为返回值返回
*/
override fun update(
uri: Uri,
values: ContentValues?,
selection: String?,
selectionArgs: Array<out String>?
): Int {
}
}
URI格式还可以加上一个id
// 表示table1表中id为1的数据
content://com.example.app.provider/table1/1
以路径结尾表示期望访问该表中所有的数据,以id结尾表示期望访问该表中拥有相应id的数据,可以使用通配符分别匹配这两种格式的内容URI
- * 表示匹配任意长度的任意字符
- # 表示匹配任意长度的数字
个能够匹配任意表的内容URI格式
content://com.example.app.provider/*
一个能够匹配table1表中任意一行数据的内容URI格式
content://com.example.app.provider/table1/#
借助UriMatcher这个类就可以轻松地实现匹配内容URI的功能。UriMatcher中提供了一个addURI()方法,这个方法接收3个参数,可以分别把authority、path和一个自定义代码传进去。这样,当调用UriMatcher的match()方法时,就可以将一个Uri对象传入,返回值是某个能够匹配这个Uri对象所对应的自定义代码,利用这个代码,我们就可以判断出调用方期望访问的是哪张表中的数据了
class MyProvider : ContentProvider() {
companion object {
private const val TAG = "MyProvider"
private const val TABLE_BOOK_DIR = 0
private const val TABLE_BOOK_ITEM = 1
private const val TABLE_CATEGORY_DIR = 2
private const val TABLE_CATEGORY_ITEM = 3
private const val AUTHORITY = "com.example.androidstudy.contentprovider_study"
}
private val uriMatcher by lazy {
UriMatcher(UriMatcher.NO_MATCH).apply {
addURI(AUTHORITY, "Book", TABLE_BOOK_DIR)
addURI(AUTHORITY, "Book/#", TABLE_BOOK_ITEM)
addURI(AUTHORITY, "Category", TABLE_CATEGORY_DIR)
addURI(AUTHORITY, "Category/#", TABLE_CATEGORY_ITEM)
}
}
/**
* 初始化ContentProvider的时候调用
* 通常会在这里完成对数据库的创建和升级等操作
* 返回true表示ContentProvider初始化成功,返回false则表示失败
* @return Boolean
*/
override fun onCreate(): Boolean {
}
/**
* 从ContentProvider中查询数据
*
* @param uri 参数用于确定查询哪张表
* @param projection 参数用于确定查询哪些列
* @param selection 用于约束查询哪些行
* @param selectionArgs 用于约束查询哪些行
* @param sortOrder 参数用于对结果进行排序,
* @return 查询的结果存放在Cursor对象中返回
*/
override fun query(
uri: Uri,
projection: Array<out String>?,
selection: String?,
selectionArgs: Array<out String>?,
sortOrder: String?
): Cursor? {
when (uriMatcher.match(uri)) {
TABLE_BOOK_DIR -> {
//查询table1的所有数据
}
TABLE_BOOK_ITEM -> {
//查询table1的单条数据
}
TABLE_CATEGORY_DIR -> {
//查询tabl2e1的所有数据
}
TABLE_CATEGORY_ITEM -> {
//查询table2的单条数据
}
}
}
/**
* 根据传入的内容URI返回相应的MIME类型
* @param uri Uri
* @return String?
*/
override fun getType(uri: Uri): String? {
return when (uriMatcher.match(uri)) {
TABLE_BOOK_DIR -> {
"vnd.android.cursor.dir/$AUTHORITY.Book"
}
TABLE_BOOK_ITEM -> {
"vnd.android.cursor.item/$AUTHORITY.Book"
}
TABLE_CATEGORY_DIR -> {
"vnd.android.cursor.dir/$AUTHORITY.Category"
}
TABLE_CATEGORY_ITEM -> {
"vnd.android.cursor.item/$AUTHORITY.Category"
}
else -> null
}
}
/**
* 向ContentProvider中添加一条数据
*
* @param uri 用于确定要添加到的表
* @param values 待添加的数据保存在values参数中
* @return 返回一个用于表示这条新记录的URI
*/
override fun insert(uri: Uri, values: ContentValues?): Uri? {
}
/**
* 从ContentProvider中删除数据
*
* @param uri 用于确定删除哪一张表中的数据
* @param selection 用于约束删除哪些行
* @param selectionArgs 用于约束删除哪些行
* @return Int 被删除的行数将作为返回值返回
*/
override fun delete(uri: Uri, selection: String?, selectionArgs: Array<out String>?): Int {
}
/**
* 更新ContentProvider中已有的数据
*
* @param uri 用于确定更新哪一张表中的数据
* @param values 新数据保存在values参数中
* @param selection 用于约束更新哪些行
* @param selectionArgs 用于约束更新哪些行
* @return Int 受影响的行数将作为返回值返回
*/
override fun update(
uri: Uri,
values: ContentValues?,
selection: String?,
selectionArgs: Array<out String>?
): Int {
}
}
insert()、update()、delete()这几个方法的实现是差不多的,它们都会携带uri这个参数,然后同样利用UriMatcher的match()方法判断出调用方期望访问的是哪张表,再对该表中的数据进行相应的操作就可以了
getType()方法是所有的ContentProvider都必须提供的一个方法,用于获取Uri对象所对应的MIME类型。一个内容URI所对应的MIME字符串主要由3部分组成
- 如果内容URI以路径结尾,则开头为vnd.android.cursor.dir/;如果内容URI以id结尾,则开头为vnd.android.cursor.item/
- 然后接上vnd.<authority>.<path>
示例
内容URI | MIME类型 |
---|---|
content://com.example.app.provider/table1 | vnd.android.cursor.dir/vnd.com.example.app.provider.table1 |
content://com.example.app.provider/table1/1 | vnd.android.cursor.item/vnd.com.example.app.provider.table1 |
因此,MyProvider 的 getType 方法可改为如下
override fun getType(uri: Uri): String? {
return when (uriMatcher.match(uri)) {
TABLE_1_DIR -> {
"vnd.android.cursor.dir/vnd.com.example.androidstudy.table1"
}
TABLE_1_ITEM -> {
"vnd.android.cursor.item/vnd.com.example.androidstudy.table1"
}
TABLE_2_DIR -> {
"vnd.android.cursor.dir/vnd.com.example.androidstudy.table2"
}
TABLE_2_ITEM -> {
"vnd.android.cursor.item/vnd.com.example.androidstudy.table2"
}
else -> null
}
}
因为所有的增删改查操作都一定要匹配到相应的内容URI格式才能进行,而我们当然不可能向UriMatcher中添加隐私数据的URI,所以这部分数据根本无法被外部程序访问,安全问题也就不存在了
实现跨程序数据共享
在 AndroidManifest.xml 文件中配置 provider
<provider
android:name=".contentprovider_study.MyProvider"
android:authorities="com.example.androidstudy.contentprovider_study.provider"
android:enabled="true"
android:exported="true">
<!--android:name属性指定了DatabaseProvider的类名
android:authorities属性指定了DatabaseProvider的authority
Exported属性表示是否允许外部程序访问我们的ContentProvider,
Enabled属性表示是否启用这个ContentProvider-->
</provider>
MyProvider 最终实现
class MyProvider : ContentProvider() {
companion object {
private const val TAG = "MyProvider"
private const val TABLE_BOOK_DIR = 0
private const val TABLE_BOOK_ITEM = 1
private const val TABLE_CATEGORY_DIR = 2
private const val TABLE_CATEGORY_ITEM = 3
private const val AUTHORITY = "com.example.androidstudy.contentprovider_study.provider"
}
private val uriMatcher by lazy {
UriMatcher(UriMatcher.NO_MATCH).apply {
addURI(AUTHORITY, "Book", TABLE_BOOK_DIR)
addURI(AUTHORITY, "Book/#", TABLE_BOOK_ITEM)
addURI(AUTHORITY, "Category", TABLE_CATEGORY_DIR)
addURI(AUTHORITY, "Category/#", TABLE_CATEGORY_ITEM)
}
}
private lateinit var dbHelper: MyDataBaseHelper
/**
* 初始化ContentProvider的时候调用
* 通常会在这里完成对数据库的创建和升级等操作
* 返回true表示ContentProvider初始化成功,返回false则表示失败
* @return Boolean
*/
override fun onCreate(): Boolean {
return context?.let {
dbHelper = MyDataBaseHelper(it, "BookStore.db", 2)
true
} ?: false
}
/**
* 从ContentProvider中查询数据
*
* @param uri 参数用于确定查询哪张表
* @param projection 参数用于确定查询哪些列
* @param selection 用于约束查询哪些行
* @param selectionArgs 用于约束查询哪些行
* @param sortOrder 参数用于对结果进行排序,
* @return 查询的结果存放在Cursor对象中返回
*/
override fun query(
uri: Uri,
projection: Array<out String>?,
selection: String?,
selectionArgs: Array<out String>?,
sortOrder: String?
): Cursor? {
val db = dbHelper.readableDatabase
return when (uriMatcher.match(uri)) {
TABLE_BOOK_DIR -> {
// 查询Book的所有数据
db.query("Book", projection, selection, selectionArgs, null, null, sortOrder)
}
TABLE_BOOK_ITEM -> {
// 查询Book的单条数据
// 当访问单条数据的时候,调用了Uri对象的getPathSegments()方法
// 它会将内容URI权限之后的部分以“/”符号进行分割,并把分割后的结果放入一个字符串列表中
// 那这个列表的第0个位置存放的就是路径,第1个位置存放的就是id了
val bookId = uri.pathSegments[1]
for (pathSegment in uri.pathSegments) {
Log.d(TAG, "query: pathSegment=$pathSegment")
}
db.query("Book", projection, "id = ?", arrayOf(bookId), null, null, sortOrder)
}
TABLE_CATEGORY_DIR -> {
// 查询Category的所有数据
db.query("Category", projection, selection, selectionArgs, null, null, sortOrder)
}
TABLE_CATEGORY_ITEM -> {
// 查询Category的单条数据
val categoryId = uri.pathSegments[1]
for (pathSegment in uri.pathSegments) {
Log.d(TAG, "query: pathSegment=$pathSegment")
}
db.query(
"Category",
projection,
"id = ?",
arrayOf(categoryId),
null,
null,
sortOrder
)
}
else -> null
}
}
/**
* 根据传入的内容URI返回相应的MIME类型
* @param uri Uri
* @return String?
*/
override fun getType(uri: Uri): String? {
return when (uriMatcher.match(uri)) {
TABLE_BOOK_DIR -> {
"vnd.android.cursor.dir/$AUTHORITY.Book"
}
TABLE_BOOK_ITEM -> {
"vnd.android.cursor.item/$AUTHORITY.Book"
}
TABLE_CATEGORY_DIR -> {
"vnd.android.cursor.dir/$AUTHORITY.Category"
}
TABLE_CATEGORY_ITEM -> {
"vnd.android.cursor.item/$AUTHORITY.Category"
}
else -> null
}
}
/**
* 向ContentProvider中添加一条数据
*
* @param uri 用于确定要添加到的表
* @param values 待添加的数据保存在values参数中
* @return 返回一个用于表示这条新记录的URI
*/
override fun insert(uri: Uri, values: ContentValues?): Uri? {
val db = dbHelper.writableDatabase
return when (uriMatcher.match(uri)) {
TABLE_BOOK_DIR, TABLE_BOOK_ITEM -> {
val newBookId = db.insert("Book", null, values)
Uri.parse("content://$AUTHORITY/Book/$newBookId")
}
TABLE_CATEGORY_DIR, TABLE_CATEGORY_ITEM -> {
val newCategoryId = db.insert("Category", null, values)
Uri.parse("content://$AUTHORITY/Category/$newCategoryId")
}
else -> null
}
}
/**
* 从ContentProvider中删除数据
*
* @param uri 用于确定删除哪一张表中的数据
* @param selection 用于约束删除哪些行
* @param selectionArgs 用于约束删除哪些行
* @return Int 被删除的行数将作为返回值返回
*/
override fun delete(uri: Uri, selection: String?, selectionArgs: Array<out String>?): Int {
val db = dbHelper.writableDatabase
return when (uriMatcher.match(uri)) {
TABLE_BOOK_DIR -> {
db.delete("Book", selection, selectionArgs)
}
TABLE_BOOK_ITEM -> {
val bookId = uri.pathSegments[1]
db.delete("Book", "id = ?", arrayOf(bookId))
}
TABLE_CATEGORY_DIR -> {
db.delete("Category", selection, selectionArgs)
}
TABLE_CATEGORY_ITEM -> {
val categoryId = uri.pathSegments[1]
db.delete("Category", "id = ?", arrayOf(categoryId))
}
else -> 0
}
}
/**
* 更新ContentProvider中已有的数据
*
* @param uri 用于确定更新哪一张表中的数据
* @param values 新数据保存在values参数中
* @param selection 用于约束更新哪些行
* @param selectionArgs 用于约束更新哪些行
* @return Int 受影响的行数将作为返回值返回
*/
override fun update(
uri: Uri,
values: ContentValues?,
selection: String?,
selectionArgs: Array<out String>?
): Int {
val db = dbHelper.writableDatabase
return when (uriMatcher.match(uri)) {
TABLE_BOOK_DIR -> {
db.update("Book", values, selection, selectionArgs)
}
TABLE_BOOK_ITEM -> {
val bookId = uri.pathSegments[1]
db.update("Book", values, "id = ?", arrayOf(bookId))
}
TABLE_CATEGORY_DIR -> {
db.update("Category", values, selection, selectionArgs)
}
TABLE_CATEGORY_ITEM -> {
val categoryId = uri.pathSegments[1]
db.update("Category", values, "id = ?", arrayOf(categoryId))
}
else -> 0
}
}
}
在另一个项目中读取共享的数据
class MainActivity : AppCompatActivity() {
companion object {
private const val TAG = "MainActivity"
}
var bookId: String? = null
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
setContentView(R.layout.activity_main)
add_data.setOnClickListener {
val uri =
Uri.parse("content://com.example.androidstudy.contentprovider_study.provider/Book")
val values =
contentValuesOf("name" to "Java", "author" to "小明", "pages" to 453, "price" to 43.2)
val newUri = contentResolver.insert(uri, values)
bookId = newUri.pathSegments[1]
}
update_data.setOnClickListener {
val uri =
Uri.parse("content://com.example.androidstudy.contentprovider_study.provider/Book/$bookId")
val values = ContentValues().apply {
put("author", "小红")
}
contentResolver.update(uri, values, null, null)
}
delete_data.setOnClickListener {
val uri =
Uri.parse("content://com.example.androidstudy.contentprovider_study.provider/Book/$bookId")
contentResolver.delete(uri, null, null)
}
query_data.setOnClickListener {
val uri =
Uri.parse("content://com.example.androidstudy.contentprovider_study.provider/Book")
val cursor = contentResolver.query(uri, null, null, null, null)
while (cursor.moveToNext()) {
val name = cursor.getString(cursor.getColumnIndex("name"))
val author = cursor.getString(cursor.getColumnIndex("author"))
val pages = cursor.getString(cursor.getColumnIndex("pages"))
val price = cursor.getString(cursor.getColumnIndex("price"))
Log.d(TAG, "onCreate: name=$name author=$author pages=$pages price=$price ")
}
cursor.close()
}
}
}