跨程序共享数据,探究 ContentProvider

1,342 阅读15分钟

文章目录

介绍

ContentProvider 主要用于在不同的应用程序之间实现数据共享的功能,它提供了一套完整的机制,允许一个程序访问另一个程序中的数据,同时还能保证被访问数据的安全性。目前,使用 ContentProvider 是 Android 实现跨程序共享数据的标准方式

不同于 文件存储和 SharedPreferences 存储中的两种全局可读写操作模式,ContentProvider 可以选择只对哪一部分数据进行共享,从而保证我们程序中的隐私数据不会有泄露的风险

ContentProvider 的用法一般有两种:一种是使用现有的 ContentProvider 读取和操作相应程序中的数据;另一种是创建自己的 ContentProvider 给程序的数据提供外部访问接口

ContentResolver 的基本用法

知识储备

对于每一个应用程序来说,想要访问 ContentProvider 中共享的数据,就要借助 ContentResolver 类,可以通过 getContentResolver() 方法来获取该类实例。ContentResolver 中提供了一系列方法对数据进行增删改查:insert()添加数据;update()更新数据;delete()删除数据;query()查询数据

接收一个 Uri 参数,被称为 内容 URI。 内容 URI 给 ContentProvider 中的数据建立了唯一标识符,它由 authoritypath 组成。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 。最后需要在字符串头部加上协议声明。因此内容 URI 最标准的格式为:

content://com.example.app.provider/table1
content://com.example.app.provider/table2

然后需要把它解析成 Uri 对象才可以作为参数传入:

val uri = Uri.parse("content://com.example.app.provider/table1")

现在可以使用这个 Uri 对象查询 table1 表中的数据了:

val cursor = contentResolver.query(
	uri,
	projection,
	selection,
	selectionArgs,
	sortOrder
)

参数说明:
uri:指定查询某个应用程序下的某一张表
projection:指定查询的列名
selection:指定 where 的约束条件
selectionArgs:为 where 中占位符提供具体的值
sortOrder:指定查询结果的排序方式

查询完成后仍是 Cursor 对象,读取方式如下:

while(cursor.moveToNext()){
	val column1 = cursor.getString(cursor.getColumnIndex("column1"))
	val column2 = cursor.getInt(cursor.getColumnIndex("column2"))
}
cursor.cloase()

增加一条数据可以这么写:

val values = contentValuesOf("column1" to "text","column2" to 1)
contentResolver.insert(uri,values)

如果想要更新这条添加的数据,把 column1 的值清空,可以借助 ContentResolver 的 update() 方法实现:

val values = contentValuesOf("column1" to "")
contentResolver.update(uri,values,"column1 = ? and column2 = ?",arrayOf("text","1"))

注意,上述代码使用了 selection 和 selectionArgs 参数来约束,防止所有行都会受到影响

删除这条数据:

contentResolver.delete(uri,"column2 = ?",arrayOf("1))

接下来我们利用目前所学的知识,读取系统通讯录中的联系人信息

读取系统联系人

布局

<?xml version="1.0" encoding="utf-8"?>
<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
    android:layout_width="match_parent"
    android:layout_height="match_parent"
    android:orientation="vertical">

    <ListView
        android:id="@+id/contactsView"
        android:layout_width="match_parent"
        android:layout_height="match_parent"/>

</LinearLayout>

代码

class TestActivity : AppCompatActivity() {
    private val contactsList = ArrayList<String>()
    private lateinit var adapter: ArrayAdapter<String>

    override fun onCreate(savedInstanceState: Bundle?) {
        super.onCreate(savedInstanceState)
        setContentView(R.layout.activity_test)
        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<out 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(getColumnIndex(ContactsContract.CommonDataKinds.Phone.DISPLAY_NAME))
                //获取联系人手机号
                val number =
                    getString(getColumnIndex(ContactsContract.CommonDataKinds.Phone.NUMBER))
                contactsList.add("$displayName\n$number")
            }
            adapter.notifyDataSetChanged()
            close()
        }
    }
}

AndroidManifest中权限

<uses-permission android:name="android.permission.READ_CONTACTS"/>

运行结果(提前在模拟机中增加一些联系人数据)
在这里插入图片描述

创建自己的 ContentProvider

创建 ContentProvider 步骤

想要实现跨程序共享数据的功能,可以新建一个类去继承 ContentProvider 的方式来实现

class MyProvider : ContentProvider() {
    /**
     * 初始化 ContentProvider 的时候调用,通常在这里完成对数据库的创建和升级等操作
     * 返回 true 表示 ContentProvider 初始化成功,false表示失败
     */
    override fun onCreate(): Boolean {
        return false
    }

    /**
     * 从 ContentProvider 中查询数据
     */
    override fun query(
        uri: Uri,
        projection: Array<out String>?,
        selection: String?,
        selectionArgs: Array<out String>?,
        sortOrder: String?
    ): Cursor? {
        TODO("Not yet implemented")
    }

    /**
     * 向 ContentProvider 中添加一条数据
     * uri:确定要添加到的表
     * values:待添加的数据
     * 返回一个用于表示这个新纪录的 URI
     */
    override fun insert(uri: Uri, values: ContentValues?): Uri? {
        TODO("Not yet implemented")
    }

    /**
     * 更新 ContentProvider 中已有数据
     * uri:更新哪个表中的数据
     * values:更新数据保存在这个参数
     * selection\selectionArgs 来约束更新哪些行
     * 返回值:受影响的行
     */
    override fun update(
        uri: Uri,
        values: ContentValues?,
        selection: String?,
        selectionArgs: Array<out String>?
    ): Int {
        TODO("Not yet implemented")
    }

    /**
     * 从 ContentProvider 中删除数据
     * 返回值:被删除的行
     */
    override fun delete(uri: Uri, selection: String?, selectionArgs: Array<out String>?): Int {
        TODO("Not yet implemented")
    }

    /**
     * 根据传入的内容URI返回相应的MIME类型
     */
    override fun getType(uri: Uri): String? {
        TODO("Not yet implemented")
    }
}

回顾一下,一个标准的内容URI写法是这样的:
content://com.example.app.provider/table1这就表示调用方期望访问的是com.example.app这个应用的table1表中的数据

除此之外,我们还可以在这个内容URI的后面加上一个id,如下所示:
content://com.example.app.provider/table1/1,这就表示调用方期望访问的是com.example.app这个应用的table1表中id为 1 的数据

内容URI的格式主要就只有以上两种,以路径结尾就表示期望访问该表中所有的数据,以 id 结尾就表示期望访问该表中拥有相应id的数据。我们可以使用通配符的方式来分别匹配这两种格式的内容URI,规则如下:

*:表示匹配任意长度的任意字符。
#:表示匹配任意长度的数字。

所以,一个能够匹配任意表的内容URI格式就可以写成:
content://com.example.app.provider/*

而一个能够匹配table1表中任意一行数据的内容URI格式就可以写成:
content://com.example.app.provider/table1/#

接着,我们再借助 UriMatcher 这个类就可以轻松地实现匹配内容 URI 的功能。UriMatcher中提供了一个addURI()方法,这个方法接收3个参数,可以分别把authoritypath和一个自定义代码传进去。这样,当调用UriMatchermatch()方法时,就可以将一个Uri对象传入,返回值是某个能够匹配这个Uri对象所对应的自定义代码,利用这个代码,我们就可以判断出调用方期望访问的是哪张表中的数据了。修改MyProvider中的代码,如下所示:

package com.example.myapplication

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)
        uriMatcher.addURI("com.example.app.provider","table1/#",table1Item)
        uriMatcher.addURI("com.example.app.provider","table2",table2Dir)
        uriMatcher.addURI("com.example.app.provider","table2/#",table2Item)
    }
    /**
     * 初始化 ContentProvider 的时候调用,通常在这里完成对数据库的创建和升级等操作
     * 返回 true 表示 ContentProvider 初始化成功,false表示失败
     */
    override fun onCreate(): Boolean {
        return false
    }

    /**
     * 从 ContentProvider 中查询数据
     */
    override fun query(
        uri: Uri,
        projection: Array<out String>?,
        selection: String?,
        selectionArgs: Array<out String>?,
        sortOrder: String?
    ): Cursor? {
        when(uriMatcher.match(uri)){
            table1Dir -> {
                // 查询table1 表中的所有数据
            }
            table1Item -> {
                // 查询table1 表中的单条数据
            }
            table2Dir -> {
                // 查询table2 表中的所有数据
            }
            table2Item -> {
                // 查询table2 表中的单条数据
            }
        }
        return null
    }

    /**
     * 向 ContentProvider 中添加一条数据
     * uri:确定要添加到的表
     * values:待添加的数据
     * 返回一个用于表示这个新纪录的 URI
     */
    override fun insert(uri: Uri, values: ContentValues?): Uri? {
        TODO("Not yet implemented")
    }

    /**
     * 更新 ContentProvider 中已有数据
     * uri:更新哪个表中的数据
     * values:更新数据保存在这个参数
     * selection\selectionArgs 来约束更新哪些行
     * 返回值:受影响的行
     */
    override fun update(
        uri: Uri,
        values: ContentValues?,
        selection: String?,
        selectionArgs: Array<out String>?
    ): Int {
        TODO("Not yet implemented")
    }

    /**
     * 从 ContentProvider 中删除数据
     * 返回值:被删除的行
     */
    override fun delete(uri: Uri, selection: String?, selectionArgs: Array<out String>?): Int {
        TODO("Not yet implemented")
    }

    /**
     * 根据传入的内容URI返回相应的MIME类型
     */
    override fun getType(uri: Uri): String? {
        TODO("Not yet implemented")
    }
}

可以看到,MyProvider中新增了4个整型常量,其中 table1Dir 表示访问table1 表中的所有数据,table1Item 表示访问 table1 表中的单条数据,table2 也是同样的。接着在静态代码块里我们创建了UriMatcher的实例,并调用addURI()方法,将期望匹配的内容 URI 格式传递进去,注意这里传入的路径参数是可以使用通配符的。然后当query()方法被调用的时候,就会通过UriMatchermatch()方法对传入的 Uri 对象进行匹配,如果发现UriMatcher中某个内容 URI 格式成功匹配了该 Uri 对象,则会返回相应的自定义代码,然后我们就可以判断出调用方期望访问的到底是什么数据了

上述代码只是以query()方法为例做了个示范,其实insert()update()delete()这几个方法的实现也是差不多的,它们都会携带 Uri 这个参数,然后同样利用UriMatchermatch()方法判断出调用方期望访问的是哪张表,再对该表中的数据进行相应的操作就可以了

除此之外,还有一个方法你会比较陌生,即getType()方法。它是所有的内容提供器都必须提供的一个方法,用于获取 Uri 对象所对应的 MIME 类型。一个内容 URI 所对应的 MIME 字符串主要由3部分组成,Android对这3个部分做了如下格式规定

  • 必须以vnd开头
  • 如果内容 URI 以路径结尾,则后接android.cursor.dir/;,如果内容 URI 以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

现在我们可以继续完善 MyProvider 中的内容了,这次来实现getType()方法中的逻辑,代码如下所示:

    /**
     * 根据传入的内容URI返回相应的MIME类型
     */
    override fun getType(uri: Uri): String? = 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
    }

到这里,一个完整的内容提供器就创建完成了,现在任何一个应用程序都可以使用ContentResolver来访问我们程序中的数据。那么前面所提到的,如何才能保证隐私数据不会泄漏出去呢?其实多亏了内容提供器的良好机制,这个问题在不知不觉中已经被解决了。因为所有的 CRUD 操作都一定要匹配到相应的内容 URI 格式才能进行的,而我们当然不可能向 UriMatcher 中添加隐私数据的URI,所以这部分数据根本无法被外部程序访问到,安全问题也就不存在了

实现跨程序数据共享

接下来的操作在 【SQLite数据库存储】创建、升级数据库 这篇文章的项目上继续开发,通过 ContentProvider 来给它加入外部访问接口。打开项目,首先将 MyDatabaseHelper 中使用 Toast 弹出创建数据库成功的提示去除掉,因为跨程序访问时我们不能直接使用Toast。然后创建一个内容提供
器,右击com.example.myapplication 包→New→Other→Content Provider

在这里插入图片描述
这里我们将内容提供器命名为 DatabaseProvider,authority 指定为com.example.myapplication.provider,Exported 属性表示是否允许外部程序访问我们的内容提供器,Enabled 属性表示是否启用这个内容提供器。将两个属性都勾中,点击Finish完成创建

代码如下:

class DatabaseProvider : ContentProvider() {
    private val bookDir = 0
    private val bookItem = 1
    private val categoryDir = 2
    private val categoryItem = 3
    private val authority = "com.example.myapplication.provider"
    private var dbHelper: MyDatabaseHelper? = null

    //by lazy 代码块是Kotlin 提供的一种懒加载技术,码块中的代码开始并不会执行,
    //只有当 uriMatcher 变量首次被调用的时候才会执行,并且将代码块中最后一行代码的返回值 赋给 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
    }

    //delete()方法这里仍然是先获取SQLiteDatabase的实例
    //然后根据传入的uri参数判断用户想要删除哪张表里的数据
    //再调用 SQLiteDatabase的delete()方法进行删除就好了
    // 被删除的行数将作为返回值返回
    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
    }

    //insert()方法,先获取了SQLiteDatabase的实例
    //然后根据传入的Uri参数判断用户想要往哪张表里添加数据
    //再调用SQLiteDatabase的insert()方法进行添加就可以了
    //注意,insert()方法要求返回一个能够表示这条新增数据的URI
    //所以我们还需要调用Uri.parse()方法,将一个内容URI解析成Uri对象
    //当然这个内容URI是以新增数据的id结尾的
    override fun insert(uri: Uri, values: ContentValues?) = dbHelper?.let {
        // 添加数据
        val db = it.readableDatabase
        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
    }

    //首先调用了getContext() 方法并借助 ?. 操作符和let 函数判断它的返回值是否为空
    //如果为空就使用?:操作符返回false,表示ContentProvider 初始化失败
    //如果不为空就执行let 函数中的代码
    //在let函数中创建了一个 MyDatabaseHelper的实例,然后返回 true表示ContentProvider初始化成功
    override fun onCreate() = context?.let {
        dbHelper = MyDatabaseHelper(it, "BookStore", 2)
        true
    } ?: false


    //先获取了SQLiteDatabase的实例,然后根据传入的Uri参数判断用户想要访问哪张表
    //再调用 SQLiteDatabase的query()进行查询,并将Cursor对象返回就好了
    //注意,当访问单条数据的时候,调用了Uri对象的getPathSegments()方法
    //它会将内容URI权限之后的部分以“/”符号进行分割,并把分割后的结果放入一个字符串列表中
    //那这个列表的第0个位置存放的就是路径
    //第1个位置存放的就是id了
    //得到了id之后,再通过selection和selectionArgs参数进行约束,就实现了查询单条数据的功能
    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
    }

    //update()方法也是先获取SQLiteDatabase的实例
    //然后根据传入的Uri参数判断出用户想要更新哪张表里的数据
    //再调用SQLiteDatabase的update()方法进行更新就好了
    //受影响的行数将作为返回值返回
    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
}

ContentProvider一定要在 AndroidManifest.xml 文件中注册才可以使用。由于我们是使用 Android Studio 的快捷方式创建的 ContentProvider,因此注册这一步已经自动完成了。打开 AndroidManifest.xml

<?xml version="1.0" encoding="utf-8"?>
<manifest xmlns:android="http://schemas.android.com/apk/res/android"
    package="com.example.myapplication">

    ......

    <application
        android:allowBackup="true"
        android:icon="@mipmap/ic_launcher"
        android:label="@string/app_name"
        android:roundIcon="@mipmap/ic_launcher_round"
        android:supportsRtl="true"
        android:theme="@style/Theme.MyApplication">
        <provider
            android:name=".DatabaseProvider"
            android:authorities="com.example.myapplication.provider"
            android:enabled="true"
            android:exported="true"></provider>
		......
    </application>

</manifest>

<application>标签内出现了一个新的标签<provider>,我
们使用它来对 DatabaseProvider 进行注册。android:name属性指定了
DatabaseProvider 的类名,android:authorities属性指定了
DatabaseProvider的authority,而enabledexported属性则是根据我
们刚才勾选的状态自动生成的,这里表示允许 DatabaseProvider 被其他应
用程序访问

现在这个项目就已经拥有了跨程序共享数据的功能了,我们来试一下。首先需要将程序从模拟器中删除,以防止上一章中产生的遗留数据对我们造成干扰。然后运行一下项目,将程序重新安装在模拟器上

接着关闭这个项目,并创建一个新项目 ProviderTest,我们将通过这个程序去访问 刚才项目中的数据,编写布局文件,修改activity_main.xml中的代码:

<?xml version="1.0" encoding="utf-8"?>
<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
    android:layout_width="match_parent"
    android:layout_height="match_parent"
    android:orientation="vertical">
    <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>

布局文件很简单,里面放置了4个按钮,分别用于添加、查询、更新和删除
数据。然后修改 MainActivity 中的代码,如下所示:

class MainActivity : AppCompatActivity() {
    var bookId: String? = null
    override fun onCreate(savedInstanceState: Bundle?) {
        super.onCreate(savedInstanceState)
        setContentView(R.layout.activity_main)
        //添加数据的时候,首先调用了Uri.parse()方法将一个内容URI解析成Uri对象
        //然后把要添加的数据都存放到ContentValues对象中
        //接着调用ContentResolver的insert()方法执行添加操作就可以了
        //注意,insert()方法会返回一个Uri对象,这个对象中包含了新增数据的id
        //我们通过getPathSegments()方法将这个id取出,稍后会用到它。
        addData.setOnClickListener {
            // 添加数据
            val uri = Uri.parse("content://com.example.myapplication.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)
        }
        //查询数据的时候,同样是调用了Uri.parse()方法将一个内容URI解析成Uri对象
        //然后调用ContentResolver的query()方法查询数据
        //查询的结果当然还是存放在Cursor对象中
        //之后对Cursor进行遍历,从中取出查询结果,并一一打印出来。
        queryData.setOnClickListener {
            // 查询数据
            val uri = Uri.parse("content://com.example.myapplication.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()
            }
        }
        //更新数据的时候,也是先将内容URI解析成Uri对象,然后把想要更新的数据存放到ContentValues对象中
        //再调用ContentResolver的update()方法执行更新操作就可以了
        //这里我们为了不想让Book表中的其他行受到影响,在调用Uri.parse()方法时,给内容URI的尾部增加了id
        //而这个id正是添加数据时所返回的。这就表示我们只希望更新刚刚添加的那条数据,Book表中的其他行都不会受影响
        updateData.setOnClickListener {
            // 更新数据
            bookId?.let {
                val uri =
                    Uri.parse("content://com.example.myapplication.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)
            }
        }
        //删除数据的时候,也是使用同样的方法解析了一个以id结尾的内容URI
        //然后调用ContentResolver的delete()方法执行删除操作就可以了
        //由于我们在内容URI里指定了一个id,因此只会删掉拥有相应id的那行数据,Book表中的其他数据都不会受影响

        deleteData.setOnClickListener {
            // 删除数据
            bookId?.let {
                val uri =
                    Uri.parse("content://com.example.myapplication.provider/book/$it")
                contentResolver.delete(uri, null, null)
            }
        }
    }
}