8.1 ContentProvider简介
概述
ContentProvider主要用于在不同的应用程序之间实现数据共享的功能,它提供了一套完整的机制,允许一个程序访问另一个程序中的数据,同时还能保证被访问数据的安全性。
不同于文件存储和SharedPreferences存储中的两种全局可读写操作模式,ContentProvider可以选择只对哪一部分数据进行共享,从而保证我们程序中的隐私数据不会有泄漏的风险。
ContentProvider如何共享数据
ContentProvider通过uri来标识其它应用要访问的数据,通过**ContentResolver**的增、删、改、查方法实现对共享数据的操作。
详解
8.2 运行时权限
安卓权限机制详解
权限分类
 Android常用权限归类,一类是普通权限,一类是危险权限。准确地讲,其实还有一些特殊权限。     
①普通权限:指的是不会直接威胁到用户的安全和隐私的权限,对于这部分权限申请,系统会自动进行授权,不需要用户手动操作。     
②危险权限:表示那些可能会触及用户隐私或者对设备安全性造成影响的权限,如获取设备联系人信息、定位设备的地理位置等,对于这部分权限申请,必须由用户手动授权才可以,否则程序就无法使用相应的功能。
危险权限列表:
| 权限组名 | 权限名称 | 
|---|---|
| CALENDAR(日历) | READ_CALENDAR | 
| WRITE_CALENDAR | |
| CAMERA(相机) | CAMERA | 
| CONTACTS(联系人) | READ_CONTACTS | 
| WRITE_CONTACTS | |
| GET_ACCOUNTS | |
| LOCATION(位置) | ACCESS_FINE_LOCATION | 
| ACCESS_COARSE_LOCATION | |
| MICROPHONE(麦克风) | RECORD_AUDIO | 
| PHONE(手机) | READ_PHONE_STATE | 
| CALL_PHONE | |
| ERAD_CALL_LOG | |
| WRITE_CALL_LOG | |
| ADD_VOICEMAIL | |
| USE_SIP | |
| PROCESS_OUTGOING_CALLS | |
| SENSORS(传感器) | BODY_SENSORS | 
| SMS(短信) | SEND_SMS | 
| RECEIVE_SMS | |
| READ_SMS | |
| RECEIVE_WAP_PUSH | |
| RECEIVE_MMS | |
| STORAGE(存储卡) | READ_EXTERNAL_STORAGE | 
| WRITE_EXTERNAL_STORAGE | 
用户权限保护
(1)用户在低于Android 6.0的设备上安装该程序,会在安装界面给出如图所示的提醒。这样用户可以清楚知晓该程序一共申请了哪些权限,从而决定是否安装这个程序。
(2)用户可以随时在应用程序管理界面查看任意一个程序的权限申请情况,如图所示。
运行时权限
Android 6.0系统中加入了运行时权限功能。也就是说,用户不需要在安装软件的时候一次性授权所有申请的权限,而是可以在软件的使用过程中再对某一项权限申请进行授权。
比如一款相机应用在运行时申请了地理位置定位权限,就算我拒绝了这个权限,也应该可以使用这个应用的其他功能,而不是像之前那样直接无法安装它。
在运行时申请权限
首先创建一个RuntimePermissionTest项目。
修改布局文件
<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"android:layout_width="match_parent"android:layout_height="match_parent"><Buttonandroid:id="@+id/makeCall"android:layout_width="match_parent"android:layout_height="wrap_content"android:text="Make Call" /></LinearLayout>
接着添加权限
在AndroidManifest文件中添加:
<uses-permission android:name="android.permission.CALL_PHONE" />
主要逻辑
package com.example.runtimepermissiontestimport android.Manifestimport android.content.Intentimport android.content.pm.PackageManagerimport android.net.Uriimport androidx.appcompat.app.AppCompatActivityimport android.os.Bundleimport android.widget.Buttonimport android.widget.Toastimport androidx.core.app.ActivityCompatimport androidx.core.content.ContextCompatclass MainActivity : AppCompatActivity() {override fun onCreate(savedInstanceState: Bundle?) {super.onCreate(savedInstanceState)setContentView(R.layout.activity_main)val makeCall: Button = findViewById(R.id.makeCall)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<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:10086")startActivity(intent)} catch (e: SecurityException) {e.printStackTrace()}}}
点击左边三角形即可展开。
call方法
代码比较长,一步一来分析,首先是call()方法:
private fun call() {try {val intent = Intent(Intent.ACTION_CALL)intent.data = Uri.parse("tel:10086")startActivity(intent)} catch (e: SecurityException) {e.printStackTrace()}}
这个就很简单,call方法里面定义的了一个intent,目标是启动一个打电话的界面Intent.ACTION_CALL,给这个intent定义数据内容,通过Uri.prase传递数据,这个数据的内容就是电话号码:10086。最后启动这个intent即可。
由于中途可能会有异常出现,比如没有授权之类的,所以之类使用try-catch对语句进行异常处理。
请求结果处理
如果想转跳到打电话的页面,就需要对其进行授权,因为这里涉及到个人隐私,所以这就需要请求授予权限,onRequestPermissionsResult是对请求权限结果的处理,当用户处理完授权操作时,系统会自动回调该方法:
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) {call()} else {Toast.makeText(this, "You denied the permission",Toast.LENGTH_SHORT).show()}}}}
onRequestPermissionsResult有三个参数:
requestCode:请求码permissions:请求权限的列表grantResults:请求权限之后系统授予的结果的列表- 这个结果一般有两种
 PackageManager.PERMISSION_GRANTED:代表已经授予权限PackageManager.PERMISSION_DENIED:代表没有授予权限
一般情况下,这个方法都需要继承父类的方法:
super.onRequestPermissionsResult(requestCode, permissions, grantResults)
接下里就是根据请求码requestCode,执行相应的程序。这里的请求码是自定义的,这里定义的如果请求码是1,那么进入if判断:
grantResults.isNotEmpty() && grantResults[0] == PackageManager.PERMISSION_GRANTED
这一段的意思就很简单:
- 首先判断授权结果是不是空的,不是空的才说明有授权
 - 其次是判断授权结果列表的第一个(索引为0)结果是不是已经授权
 
上面两个条件都满足的情况下,调用call()方法,否则提示没有授权。
这里就有一个疑问,为什么需要判断授权列表的第一个结果,后面会一起解答。
触发请求权限事件
最后是关于按钮的点击事件:
makeCall.setOnClickListener {if (ContextCompat.checkSelfPermission(this,Manifest.permission.CALL_PHONE) !=PackageManager.PERMISSION_GRANTED) {ActivityCompat.requestPermissions(this,arrayOf(Manifest.permission.CALL_PHONE), 1)} else {call()}}
点击按钮后,首先判断这个Activity也就是this是否有相应的授权:
ContextCompat.checkSelfPermission(this,Manifest.permission.CALL_PHONE) != PackageManager.PERMISSION_GRANTED
checkSelfPermission的参数有两个:
- 第一个参数是上下文,这里就是本
Activity - 第二个参数是需要的权限,这里就是
Manifest.permission.CALL_PHONE,这个权限自己根据需求查询即可。 
如果没有授权,那么就对该权限进行申请:
ActivityCompat.requestPermissions(this,arrayOf(Manifest.permission.CALL_PHONE), 1)
requestPermissions:三个参数分别是:
- 第一个this不解释了
 - 请求获取参数的列表,这里就是
arrayOf(Manifest.permission.CALL_PHONE) - 请求码,这里就是
1 
对权限请求之后,就会执行onRequestPermissionsResult,对请求的结果进行处理。
如果检查到自身Activity已经授权,那就直接执行call方法即可。
请求流程
整体请求流程如下:
最终效果如下:

8.3 访问其他程序中的数据
ContentProvider的用法一般有两种:一种是使用现有的ContentProvider读取和操作相应程序中的数据;另一种是创建自己的ContentProvider,给程序的数据提供外部访问接口。
ContentResolver的基本用法
 对于每一个应用程序来说,如果想要访问ContentProvider中共享的数据,一定要借助**ContentResolver**类,可以通过Context中的getContentResolver()方法获取该类的实例。ContentResolver中提供了一系列的方法用于对数据进行增删改查操作,其中:
insert()方法:用于添加数据update()方法:用于更新数据delete()方法:用于删除数据- 
insert
val values = contentValuesOf("column1" to "text", "column2" to 1)contentResolver.insert(uri, values)
这里两个参数分别是:
 **Uri**- 
update
val values = contentValuesOf("column1" to "")contentResolver.update(uri, values, "column1 = ? and column2 = ?", arrayOf("text", "1"))
这里四个参数分别是:
 **Uri****projection****selection**- 
delete
contentResolver.delete(uri, "column2 = ?", arrayOf("1"))
这里三个参数分别是:
 **Uri****selection**- 
query
contentResolver.query(ContactsContract.CommonDataKinds.Phone.CONTENT_URI,null, null, null, null)
这个方法有若干个参数:
 第一个参数:
**Uri**- 也叫
内容URI,类似于数据库查询的表名称(注意这里不是URL) - 这里的
Uri参数就是:ContactsContract.CommonDataKinds.Phone.CONTENT_URI 
- 也叫
 - 第二个参数:
**projection**- 这个参数告诉内容提供者要返回的内容(列Column),也就是类似数据库中需要返回的字段。
 
 - 第三个参数:
**selection**- 这个参数相当于SQL语句中的
Where子句,对返回的内容进行筛选。 
 - 这个参数相当于SQL语句中的
 - 第四个参数:
**selectionArgs**- 这个参数需要配合第三个参数进行使用
 - 里面的数据会按照次序替换掉第三个参数当中的
? 
 - 第五个参数:
**sortOrder**- 这个参数相当于SQL语句里的
Order by子句,就是给查询结果进行排序。 
 - 这个参数相当于SQL语句里的
 
这五个参数详细介绍参见:
ContentResolver.query详解_一朵桃花压海棠的博客-CSDN博客_contentresolver
uri详解
内容URI给ContentProvider(内容提供者)中的数据建立了唯一标识符,主要由两部分组成:**authority**和**path**。
**authority**:用于对不同的应用程序做区分,一般为了避免冲突,会采用应用包名的方式进行命名。比如某个应用的包名是com.example.app,那么该应用对应的authority就可以命名为com.example.app.provider。**path**:是用于对同一应用程序中不同的表做区分的,通常会添加到authority的后面。比如某个应用的数据库里存在两张表table1和table2,这时就可以将path分别命名为/table1和/table2,然后把authority和path进行组合,内容URI就变成了com.example.app.provider/table1和com.example.app.provider/table2。读取系统联系人
下面通过实例来讲解ContentResolver的用法:我们需要通过ContentResolver来查询到联系人的数据,所以在此之前需要先在联系人列表创建相应的记录。

设置布局文件
<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"android:orientation="vertical"android:layout_width="match_parent"android:layout_height="match_parent" ><ListViewandroid:id="@+id/contactsView"android:layout_width="match_parent"android:layout_height="match_parent" ></ListView></LinearLayout>
这里使用的是ListView,忘记得可以会看这个地址。
添加权限
在AndroidManifest里面添加如下权限:
<uses-permission android:name="android.permission.READ_CONTACTS" />
主要逻辑
package com.example.contactstestimport android.Manifestimport android.content.pm.PackageManagerimport androidx.appcompat.app.AppCompatActivityimport android.os.Bundleimport android.provider.ContactsContractimport android.widget.ArrayAdapterimport android.widget.ListViewimport android.widget.Toastimport androidx.core.app.ActivityCompatimport androidx.core.content.ContextCompatclass 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)val contactsView: ListView = findViewById(R.id.contactsView)contactsView.adapter = adapterif(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()}}}}private fun readContacts() { // 查询联系人数据contentResolver.query(ContactsContract.CommonDataKinds.Phone.CONTENT_URI,null, null, null, null)?.apply {while (moveToNext()) { // 获取联系人姓名val displayName = getString(getColumnIndexOrThrow(ContactsContract.CommonDataKinds.Phone.DISPLAY_NAME)) // 获取联系人手机号val number = getString(getColumnIndexOrThrow(ContactsContract.CommonDataKinds.Phone.NUMBER))contactsList.add("$displayName\n$number")}adapter.notifyDataSetChanged()close()}}}
太长了就不展示了,点击右边三角形即可查看。
全局变量设置
首先是两个全局变量:
private val contactsList = ArrayList<String>()private lateinit var adapter: ArrayAdapter<String>
一个是需要读取之后用于存储的联系人列表contactsList,另一个是延迟初始化的接收器。
读取联系人
接着看查询联系人的方法:readContacts方法
private fun readContacts() { // 查询联系人数据contentResolver.query(ContactsContract.CommonDataKinds.Phone.CONTENT_URI,null, null, null, null)?.apply {while (moveToNext()) { // 获取联系人姓名val displayName = getString(getColumnIndexOrThrow(ContactsContract.CommonDataKinds.Phone.DISPLAY_NAME)) // 获取联系人手机号val number = getString(getColumnIndexOrThrow(ContactsContract.CommonDataKinds.Phone.NUMBER))contactsList.add("$displayName\n$number")}adapter.notifyDataSetChanged()close()}}
这里用到的查询方法就是:contentResolver.query,通过contentResolver进行查询。
这里因为我们只需要把数据绑定到ListView上面,所以后面四个参数就不需要了。
query查询方法最终的返回结果是一个**Cursor**对象,那么什么是**Cursor**对象?
简单的来讲就是数据库中每一行的集合。
详细介绍可以参见:
Android笔记——关于Cursor类的介绍 - 竹林幽径 - 博客园
所以当我们查询到我们需要的数据之后需要遍历Cursor对象,以获取每一个具体的值。遍历的方法就是使用**apply**对数据进行组装。
看一下apply里面的具体内容:
while (moveToNext()) {// 获取联系人姓名val displayName = getString(getColumnIndexOrThrow(ContactsContract.CommonDataKinds.Phone.DISPLAY_NAME))// 获取联系人手机号val number = getString(getColumnIndexOrThrow(ContactsContract.CommonDataKinds.Phone.NUMBER))contactsList.add("$displayName\n$number")}adapter.notifyDataSetChanged()close()
moveToNext()的作用就是每次循环之后就会扫描到下一条数据。getColumnIndexOrThrow()的作用就是根据列名获取相应的值。ContactsContract.CommonDataKinds.Phone.DISPLAY_NAME就是一个列名,只是比较复杂而已,需要的时候查询即可。
getString()就是将获取的值转换为字符串。notifyDataSetChanged表示刷新适配器**adapter**里面的内容。- 最后注意一下要对
Cursor对象进行关闭close。 
返回结果处理
这个就跟上面讲的差不多了,可以回看一下。
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()}}}}
触发权限请求事件
首先是一段对适配器的配置:
adapter = ArrayAdapter(this, android.R.layout.simple_list_item_1, contactsList)val contactsView: ListView = findViewById(R.id.contactsView)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()}
流程同上。
8.4 创建自己的ContentProvider
在上面的例子中,我们通过ContentResolver获取到了联系人应用的数据。 这个应用相当于一个写好的ContentProvider
创建ContentProvider的步骤
要想实现跨程序共享数据的功能,可以通过新建一个类去继承ContentProvider的方式来实现。ContentProvider类中有6个抽象方法,在使用子类继承它的时候,需要将这6个方法全部重写。
重写方法
class MyProvider : ContentProvider() {override fun onCreate(): Boolean {return false}override fun query(uri: Uri, projection: Array<String>?, selection: String?,selectionArgs: Array<String>?, sortOrder: String?): Cursor? {return null}override fun insert(uri: Uri, values: ContentValues?): Uri? {return null}override fun update(uri: Uri, values: ContentValues?, selection: String?,selectionArgs: Array<String>?): Int {return 0}override fun delete(uri: Uri, selection: String?, selectionArgs: Array<String>?): Int {return 0}override fun getType(uri: Uri): String? {return null}}
这六个方法很好记,就是创建方法+增删改查四个方法+获取类型方法。方法的参数在上面介绍过了,这里就不过多赘述。OnCreate()方法:初始化ContentProvider时调用,完成对数据库的创建和升级等操作,返回true表示ContentProvider初始化成功,返回false则表示失败。
Uri
上面六个方法都用到了Uri参数,这里主要有两种格式:
- 以路径结尾表示期望访问该表中所有的数据:
- content://com.example.app.provider/table1
 - 表示访问这个应用的
table1表中的所有数据 
 - 以id结尾表示期望访问该表中拥有相应id的数据:
- content://com.example.app.provider/table1/1
 - 表示访问这个应用的
table1表中id为1的数据。 
 
还可以使用通配符匹配这两种格式的Uri:
*表示匹配任意长度的任意字符。- 例如
content://com.example.app.provider/* - 这里的
*其实表示的就是一个**表名** 
- 例如
 #表示匹配任意长度的数字。- 例如
content://com.example.app.provider/table1/# - 这里的
#其实表示的就是一个**id** 
- 例如
 
实例
class MyProvider : ContentProvider() {// 相应uri匹配成功之后的返回值private val table1Dir = 0private val table1Item = 1private val table2Dir = 2private val table2Item = 3// 初始化uriMatcher// 常量UriMatcher.NO_MATCH表示不匹配任何路径的返回码private val uriMatcher = UriMatcher(UriMatcher.NO_MATCH)init {uriMatcher.addURI("com.example.app.provider", "table1", table1Dir)uriMatcher.addURI("com.example.app.provider ", "table1/#", table1Item)uriMatcher.addURI("com.example.app.provider ", "table2", table2Dir)uriMatcher.addURI("com.example.app.provider ", "table2/#", table2Item)}...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匹配成功之后返回:
private val table1Dir = 0private val table1Item = 1private val table2Dir = 2private val table2Item = 3
接着是初始化uriMatcher,这里的UriMatcher.NO_MATCH表示不匹配任何路径的返回码:
private val uriMatcher = UriMatcher(UriMatcher.NO_MATCH)
接着就是通过uriMatcher对uri进行初始化:
init {# uri:com.example.app.provider/table1 返回值是0uriMatcher.addURI("com.example.app.provider", "table1", table1Dir)# uri:com.example.app.provider/table1/#1 返回值是1uriMatcher.addURI("com.example.app.provider ", "table1/#", table1Item)# uri:com.example.app.provider/table2 返回值是2uriMatcher.addURI("com.example.app.provider ", "table2", table2Dir)# uri:com.example.app.provider/table2/# 返回值是3uriMatcher.addURI("com.example.app.provider ", "table2/#", table2Item)}
后面的查询方法就没什么好介绍的了。<br />上面只是举例了**查询方法**,其余**增加删除修改**的方法类似,就不多赘述了。<br />此外,getType()方法是所有的`ContentProvider`都必须提供的一个方法,用于获取`Uri对象`所对应的[**MIME类型**](https://developer.mozilla.org/zh-CN/docs/Web/HTTP/Basics_of_HTTP/MIME_types)。<br />一个内容`URI`所对应的MIME字符串主要由**3部分**组成,Android对这3个部分做了如下格式规定:<br />① **必须**以`vnd`开头。<br />② 如果内容`URI`以**路径结尾**,则后接`android.cursor.dir/`<br />③ 如果内容`URI`以**id结尾**,则后接`android.cursor.item/`<br />④ 最后接上`vnd.<authority>.<path>`
举个例子:
- 对于
content://com.example.app.provider/table1- MME类型:
vnd.android.cursor.dir/vnd.com.example.app.provider.table1 
 - MME类型:
 - 对于
content://com.example.app.provider/table1/1- MME类型:
vnd.android.cursor.item/vnd.com.example.app.provider.table1 
 - MME类型:
 
所以最后的getType()方法就可以补充为:
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}
实现跨程序数据共享
首先我们打开上一章中的DatabaseTest,在此基础上进行修改,通过ContentProvider加入外部访问接口。
先将MyDatabaseHelper中使用**Toast**弹出创建数据库成功的提示去除,因为跨程序访问时不能直接使用Toast。
创建ContentProvider
创建流程如下:
将ContentProvider 命 名 为DatabaseProvider, 将authority指 定 为com.example.databasetest.provider,Exported属性表示是否允许外部程序访问我们的ContentProvider,Enabled属性表示是否启用这个ContentProvider。将两个属性都勾中,点击“Finish”完成创建。
修改DatabaseProvider中的代码
package com.example.databasetest.providerimport android.content.ContentProviderimport android.content.ContentValuesimport android.content.UriMatcherimport android.net.Uriimport com.example.databasetest.databasehelper.MyDatabaseHelperclass DatabaseProvider : ContentProvider() {private val bookDir = 0private val bookItem = 1private val categoryDir = 2private val categoryItem = 3private val authority = "com.example.databasetest.provider"private var dbHelper: MyDatabaseHelper? = nullprivate 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} ?: falseoverride fun query(uri: Uri, projection: Array<String>?, selection: String?,selectionArgs: Array<String>?, sortOrder: String?) = dbHelper?.let { // 查询数据val db = it.readableDatabaseval 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.writableDatabaseval 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.writableDatabaseval 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} ?: 0override fun delete(uri: Uri, selection: String?, selectionArgs: Array<String>?)= dbHelper?.let { // 删除数据val db = it.writableDatabaseval 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} ?: 0override 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}}
点击左边三角形即可展开
看着很复杂,其实还行,一步一步来分析。
基础变量赋值
private val bookDir = 0private val bookItem = 1private val categoryDir = 2private val categoryItem = 3private val authority = "com.example.databasetest.provider"private var dbHelper: MyDatabaseHelper? = nullprivate 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}
前面六个这就没什么好说的,四个返回值和`authority`以及一个数据库的**帮助类**。<br />`uriMatcher`通过`by lazy`实现**懒加载**。详情可以参见:<br />[Kotlin中 lateinit和by lazy区别:](https://www.jianshu.com/p/24a69a980421)<br />[Kotlin中by lazy是如何实现懒加载的_pgaofeng的博客-CSDN博客_kotlin lazy](https://blog.csdn.net/zip_tts/article/details/112349732)<br />这里只要知道`uriMatcher = matcher`,而且这里的`matcher`已经将四个`uri`组装好了。
let函数
后面几个方法都用到了let,这里简单讲一下。
一般情况下的使用:
object.let{it.todo()//在函数体内使用it替代object对象去访问其公有的属性和方法...}//另一种用途 判断object为null的操作object?.let{//表示object不为null的条件下,才会去执行let函数体it.todo()}
简单的来讲let函数就是可以使用it代替前面对象,进行一些操作。
详细参见:
Kotlin系列之let、with、run、apply、also函数的使用_熊喵先生的博客-CSDN博客_kotlin let
创建方法
override fun onCreate() = context?.let {dbHelper = MyDatabaseHelper(it, "BookStore.db", 2)true} ?: false
在`context`不为空的时候,执行let函数里面的操作,相当于更新了数据库(版本从1到2)。<br />最后一个true的意思就是令相当于返回值为`**true**`。<br />当然由于`dbHelper`不一定能成功赋值,所以后面的语句就不会执行,那么`context`的返回值就有可能是空值,所以使用`?: false`**防止空指针异常**。意思如果`context`是空值,就给`context`赋值为`false`。
增删改查方法
大部分都比较类似,这里就拿一个查询举例子:
override fun query(uri: Uri, projection: Array<String>?, selection: String?,selectionArgs: Array<String>?, sortOrder: String?) = dbHelper?.let { // 查询数据val db = it.readableDatabaseval 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}
简单的来讲就是三个操作:
- 初始化一个数据库
readableDatabase:可读数据库writeableDatabase:可写数据库
 - 使用
when方法,根据匹配**uri**的返回值对数据库进行查询 - 返回查询对象
**cursor** 
insert方法同理:
override fun insert(uri: Uri, values: ContentValues?) = dbHelper?.let { // 添加数据val db = it.writableDatabaseval 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}
注意这里返回的是一个`uri`。
更新和删除方法有一点不同:
override fun update(uri: Uri, values: ContentValues?, selection: String?,selectionArgs: Array<String>?) = dbHelper?.let { // 更新数据val db = it.writableDatabaseval 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} ?: 0override fun delete(uri: Uri, selection: String?, selectionArgs: Array<String>?)= dbHelper?.let { // 删除数据val db = it.writableDatabaseval 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
主要这两个方法的返回值是Int,所以不能为空,需要使用? : 0进行判空处理。
获取MME类型
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}
这个就比较简单了,没什么好说的。
注册ContentProvider
修改activity_main.xml
<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"android:orientation="vertical"android:layout_width="match_parent"android:layout_height="match_parent" ><Buttonandroid:id="@+id/addData"android:layout_width="match_parent"android:layout_height="wrap_content"android:text="Add To Book" /><Buttonandroid:id="@+id/queryData"android:layout_width="match_parent"android:layout_height="wrap_content"android:text="Query From Book" /><Buttonandroid:id="@+id/updateData"android:layout_width="match_parent"android:layout_height="wrap_content"android:text="Update Book" /><Buttonandroid:id="@+id/deleteData"android:layout_width="match_parent"android:layout_height="wrap_content"android:text="Delete From Book" /></LinearLayout>
添加申明
Android 11 中更改了应用之间的交互方式,需要在 Manifest中使用 
<queries><package android:name="com.example.databasetest" /></queries>
修改MainActivity
①添加数据时,首先调用Uri.parse()方法将一个内容URI解析成Uri对象,然后把要添加的数据都存放到ContentValues对象中,接着调用ContentResolver的insert()方法执行添加操作。    注意,insert()方法会返回一个Uri对象,这个对象中包含了新增数据的id,通过getPathSegments()方法将这个id取出,稍后会用到它。
②查询数据时,同样调用Uri.parse()方法将一个内容URI解析成Uri对象,然后调用ContentResolver的query()方法查询数据,查询的结果当然还是存放在Cursor对象中。之后对Cursor进行遍历,从中取出查询结果,并一一打印出来。
③更新数据时,先将内容URI解析成Uri对象,然后把想要更新的数据存放到ContentValues对象中,再调用ContentResolver的update()方法执行更新操作。    注意,为了不让Book表中的其他行受到影响,在调用Uri.parse()方法时,给内容URI的尾部增加一个id,而这个id正是添加数据时所返回的。表示我们只希望更新刚刚添加的那条数据,Book表中的其他行都不会受影响。
④删除数据时,也是使用同样的方法解析了一个以id结尾的内容URI,然后调用ContentResolver的delete()方法执行删除操作。由于我们在内容URI里指定了一个id,因此只会删掉拥有相应id的那行数据,Book表中的其他数据都不会受影响。
