ContentProvider的介绍和使用

ContentProvider

  • 如果我们想要实现跨程序数据共享的功能,我们就可以使用这个ContentProvider

ContentProvider简介

  • ContentProvider主要应用于不同的应用程序之间实现数据的共享,它提供了一整套完整的机制,允许一个程序访问另一个程序当中的数据,同时还能保证被访问数据的安全性.
  • 目前使用ContentProvider是Android实现跨程序共享数据的标准方式
  • 不同于文件存储和SharedPreferences存储中的两种全局可读可写操作模式,ContentProvider可以选择只对那一部分数据进行共享,从而保证我们程序中的隐私数据不会有泄露的风险.

运行时权限

  • Android开发团队在Android6.0系统中引入了运行时权限这个功能,从而可以更好的保护用户的安全和隐私

Android权限机制详解

  • 在学习广播机制的时候,为了要监听开机广播,在AndroidManifest.xml文件当中添加了这样一段声明
<uses-permission android:name="android.intent.action.BOOT_COMPLETED" />
  • 因为监听开机广播涉及用户设备安全问题,所以必须要在AndroidManifest.xml文件当中进行权限声明,否则我们的程序就会崩溃
  • 当我们添加了这段权限声明之后,用户主要在两个方面受到了保护
  • 一方面,如果用户在低于Android6.0系统的设备上安装程序,会在安装界面给出下面这样的提示,这样用户就知晓该程序一共申请了哪些权限,从而决定是否要安装这个程序.

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-B6ih75pk-1671516835598)(C:/Users/zhengbo/%E6%88%91%E7%9A%84%E5%AD%A6%E4%B9%A0/Typora%E5%AD%A6%E4%B9%A0%E7%AC%94%E8%AE%B0/%E5%AE%89%E5%8D%93/image-20221215155041884.png)].

  • 另外一方面,用户可以随时在应用程序管理界面查看任意一个程序权限申请情况,这样程序所申请的权限就尽收眼底了.
  • 而运行时权限的功能就是,用户不需要在安装软件的时候一次性授权所有申请的权限,而是可以在软件使用的过程当中再对某一项权限进行授权,比如一款相机在运行的时候申请了地理位置权限,就算我拒绝了它,也应该可以使用这个应用的其他功能,而不是像之前那样直接无法安装.
  • 当然也并不是说所有的权限都需要在运行的时候进行申请,频繁的授权也会很繁琐,Android现在将常用的权限分为了两个大类,一个是普通权限,一个是危险权限.
  • 普通权限就是那些不会直接威胁到用户的安全和隐私的权限,对于这部分的权限系统会自动帮助我们授权
  • 反之就是危险权限,如获取设备联系人信息,定位设备的地理位置,对于这部分的权限申请,必须由用户手动进行授权才可以,否则程序无法使用相应的程序.
  • Android中一共有上百种权限,危险权限就那么多,除了危险权限之外就是普通权限了,下面的表格当中就列举了Android 10系统到目前为止所有的危险权限,一共是11组30个权限

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-C399jICc-1671516835601)(C:/Users/zhengbo/%E6%88%91%E7%9A%84%E5%AD%A6%E4%B9%A0/Typora%E5%AD%A6%E4%B9%A0%E7%AC%94%E8%AE%B0/%E5%AE%89%E5%8D%93/image-20221215160811566.png)]

  • 表格当中每一个危险权限都属于一个权限组,我们在进行运行时权限处理的时候使用的是权限名,原则上用户一旦同意了某一个权限申请之后,同组的其他权限也会被系统自动授权,但是谨记不要使用此规则来实现任何功能逻辑,因为Android系统随时有可能调整权限分组.

在程序运行时申请权限

  • 创建一个RuntimePermissionTest项目来进行测试
  • 使用CALL_PHONE这个权限来作为示例
  • CALL_PHNOE这个权限是编写拨打电话功能的时候所需要使用到的权限,因为拨打电话涉及手机资费的问题所以该权限被列为了危险权限的系列
  • 修改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">

    <Button
        android:id="@+id/makeCall"
        android:layout_width="match_parent"
        android:layout_height="wrap_content"
        android:text="make call" />

</LinearLayout>
  • 在activity_main.xml文件当中编写了一个按钮,点击按钮就去触发拨打按钮的逻辑,接着修改MainActivity当中的代码
class MainActivity : AppCompatActivity() {
    override fun onCreate(savedInstanceState: Bundle?) {
        super.onCreate(savedInstanceState)
        setContentView(R.layout.activity_main)
        makeCall.setOnClickListener {
            try {
                val intent = Intent(Intent.ACTION_CALL)
                intent.data = Uri.parse("tel:10086")
                startActivity(intent)
            } catch (e: SecurityException) {
                e.printStackTrace()
            }
        }
    }
}
  • 在按钮的点击事件当中,我们构建了一个隐私的Intent,Intent的action指定为Intent.ACTION_CALL,这是系统内置的一个打电话的动作,然后再data部分指定了协议tel,号码是10086
  • 接下来修改AndroidManifest.xml文件,在其中声明如下权限

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-VwRbbf86-1671516835601)(C:/Users/zhengbo/%E6%88%91%E7%9A%84%E5%AD%A6%E4%B9%A0/Typora%E5%AD%A6%E4%B9%A0%E7%AC%94%E8%AE%B0/%E5%AE%89%E5%8D%93/image-20221215174121046.png)]

  • 这样我们就将拨打电话的功能实现了,并且在低于Android 6.0系统的手机上都是可以正常运行的.但是在Android 6.0版本及以上点击make call按钮就没有任何效果了,点击按钮我们会看到报错的信息

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-z02zxvfJ-1671516835602)(C:/Users/zhengbo/%E6%88%91%E7%9A%84%E5%AD%A6%E4%B9%A0/Typora%E5%AD%A6%E4%B9%A0%E7%AC%94%E8%AE%B0/%E5%AE%89%E5%8D%93/image-20221215174809025.png)]

  • 在报错信息当中可以看到Permission Denial,这是由于权限被禁止所导致的,因为Android 6.0及以上系统在使用危险权限时必须进行运行时权限处理.
  • 那么我们可以在MainActivity当中修复这个问题
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
    ) {
        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()
        }
    }
}
  • 运行时权限的核心就是在程序运行的过程当中,由用户授权我们去执行某一些危险的操作,程序是不可以擅作主张去执行一些危险操作的
  • 因此我们的第一步就是先判断用户是否给我们进行了授权,借助的是ContextCompat.checkSelfPermission()方法
  • 该方法接收两个参数,第一个参数是Context
  • 第二个参数是具体的权限名,比如拨打电话的权限名就是android.Manifest.permission.CALL_PHONE
  • 然后我们使用该方法的返回值和PackageManager.PERMISSION_GRANTED作比较,相等就说明我们做了授权,不相等就表示我们没有做授权
  • 如果授权了的话就比较简单了,直接点击按钮进行直接拨打电话即可,我们将拨打电话的逻辑封装到call()方法当中
  • 如果没有授权的话,则需要调用ActivityCompat.requestPermissions()方法向用户进行授权,requestPermissions()方法接收三个参数
  • 第一个参数:要求是Activity的实例
  • 第二个参数是String数组,我们要把申请的权限放在数组中即可
  • 第三个参数是请求码,只要是唯一值就可以了,我们在这里传入1
  • 调用完requestPermissions()方法之后,系统弹出一个权限申请的对话框,用户可以选择统一或者拒绝我们的权先申请,不论是那种结果,最终都会回调到onRequestPermissionsResult()方法,然而授权的结果会封装到grantResults参数当中,这里我们只需要判断一下最后的授权结果,如果是同意的话,就调用call()方法,如果用户不同意的话,就提示一段文本即可.

访问其他程序当中数据

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

ContentResolver的基本用法

  • 对于任何一个应用程序来说,如果想要访问ContentProvider中共享的数据,就一定要借助ContentProvider类,可以通过Context中的getContentResolver()方法获取该类的实例.
  • ContentResolver中提供了一系列的方法用于对数据进行增删改查操作,其中inset()方法用于添加数据,update()方法用于更新数据,delete()方法用于对数据进行删除,query()方法用于对数据进行查询操作,只不过就是它们在方法参数上稍微有一些差别.
  • 增删改查方法会用到一个Uri参数,这个参数被称为内容URI.内容URI给ContentProvider中的数据建立为一个标识符,它主要由两个部分组成:anthority和path
  • anthority是用于对不同的程序做区分的,一般为了避免冲突,会采用应用包名的方式进行命名,比如某个应用的包名是com.example.app那么该应用对应的authority就可以命名为com.example.app.provider
  • path则是用于对一个应用程序中不同的表做区分,通常会添加到authority的后面,比如某一个应用程序在数据库中存放了两张表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就可以非常清楚的表大我们想要访问哪个程序中的那张表里的数据,也正是因此,ContentResolver中的增删改查方法才接受Uri对象作为参数,如果使用表名的话,系统将无法的得知我们期望回访的是哪个应用程序当中的表.
  • 在我们得到内容URI的时候,我们还需要将他解析成Uri对象才可以作为参数进行传入,解析方法也相当简单,代码如下所示
val uri = Uri.parse("content://com.example.app.provider/table1")
  • 只需要调用Uri.parse()方法即可将内容URI解析成为Uri对象了
  • 现在我们想要使用这个Uri对象来查询table1表中的数据,代码如下所示
val cursor = contentResolver.query (
    uri,
    projection,
    selection,
    selectionArgs,
    sortOrder
)
  • 下面这个表对query()方法当中的参数做了详细的说明

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-i4AndaHZ-1671516835603)(C:/Users/zhengbo/%E6%88%91%E7%9A%84%E5%AD%A6%E4%B9%A0/Typora%E5%AD%A6%E4%B9%A0%E7%AC%94%E8%AE%B0/%E5%AE%89%E5%8D%93/image-20221216133028457.png)]

  • 查询完成之后,仍然是一个cursor对象,这时我们就可以将数据从cursor对象中逐个读取出来了,读取的思路仍然是通过移动游标的位置遍历cursor的所有行,然后取出每一行中相应列的数据,代码如下
while(cursor.moveToNext()) {
    val column1 = cursor.getString(cursor.getColumnIndex("column1"))
    val column2 = cursor.getString(cursor.getColumnIndex("column2"))
}
cursor.close()
  • 向表中添加一条数据,代码如下所示
//将待添加的数据添加到contentValues当中
val values = contentValuesOf("column1" to "text", "column2" to 1)
//然后调用contentResolver的insert()方法,将uri和ContentValues作为参数传入即可
contentResolver.insert(uri, values)
  • 如果我们想要将新添加到这个数据进行更新将column1的值进行清空,可以借助contentResolver的update()方法实现,代码如下所示
val values = contentValuesOf("column1" to "")
contentResolver.update(uri, values, "column1 = ? and column2 = ?", arrayOf("text", " 1"))
  • 最后可以调用contentResolver.delete()方法将这条数据删除掉,代码如下所示
contentResolver.delete(uri, "column2 = ?", arrayOf("1"))

读取系统联系人信息

  • 创建一个CotentsTest项目,修改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">

    <ListView
        android:id="@+id/contactsView"
        android:layout_width="match_parent"
        android:layout_height="match_parent" />
</LinearLayout>
  • 接着修改MainActivity当中的代码.
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<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, "你没有权限", 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()
        }
    }
}
  • 在onCreate()方法当中,我们首先按照ListView的标准用法对其进行初始化,然后开始调用运行时权限进行逻辑处理,因为READ_CONTACTS权限属于是危险权限了,这里我们在用户进行授权之后,调用readContacts()方法读取系统联系人信息.

  • readContacts()方法,可以看到使用了ContentResolver的query()方法查询系统的联系人数据

  • 不过传入的Uri参数并没有调用Uri.parse()方法去解析一个内容URI字符串,这是因为ContactsContract.CommonDataKinds.Phone类已经帮我们做好了封装,提供了一个CONTENT_URI常量,这个常量就是使用Uri.parse()方法解析出来的结果.

  • 接着我们对query()方法返回的Cursor对象进行遍历,这里使用了?.操作符和apply函数来简化遍历的代码

  • 在apply函数当中将联系人姓名和手机号逐个取出,联系人姓名这一列对应的常量是ContactsContract.CommonDataKinds.Phone.DISPLAY_NAME

  • 联系人电话号码对应的常量是:ContactsContract.CommonDataKinds.Phone.NUMBER

  • 将两个数据取出来之后进行拼接,并且在中间加上换行符号,然后将拼接的数据放到ListView的数据源里面

  • 并且通知刷新一下ListView,最后千万不要忘记了Cursor对象的关闭

  • 最后我们还需要在AndroidManifest.xml文件当中声明读取联系人的权限

  • 所以总的来说在自己的程序当中访问其他程序当中的数据,还是比较简单的,只需要获得该应用程序的内容URI,然后借助ContentResolver进行增删改查就可以

创建自己的ContentProvider

创建ContentProvider的步骤

  • 想要实现一个跨程序数据共享,还可以就是自己写一个类取继承ContentProvider的方式来进行实现
  • ContentProvider类中共有6个抽象方法,我们在使用子类继承它的时候,需要将这六个方法全部进行重写.
class MyProvider : ContentProvider() {
    override fun onCreate(): Boolean {
        TODO("Not yet implemented")
    }

    override fun query(
        uri: Uri,
        projection: Array<out String>?,
        selection: String?,
        selectionArgs: Array<out String>?,
        sortOrder: String?
    ): Cursor? {
        TODO("Not yet implemented")
    }

    override fun getType(uri: Uri): String? {
        TODO("Not yet implemented")
    }

    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")
    }
}
  • 需要的注意的就是标准URI的写法是这样的
content://com.example.app.provider/table1
  • 就表示我们期望访问的是com.example.app这个应用的table1表当中的数据
  • 还可以这样写
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()方法,这个方法接收三个参数,分别把authority,path和一个自定义代码传进去
  • 这样当调用UriMatcher的match()方法时,就可以将一个Uri对象传入,返回值是某个能够匹配这个Uri对象所定义的自定义代码,利用这个代码我们就可以判断出调用方期望我们访问的是哪张表当中的数据了.
  • 修改MyProvider当中的代码,如下所示
class MyProvider : ContentProvider() {
    /**
     * 表示访问table1表中的所有数据
     */
    private val table1Dir = 0

    /**
     * 表示访问table1表中的单条数据
     */
    private val table1Item = 1
    private val table2Dir = 2
    private val table2Item = 3

    /**
     * 在MyProvider实例化的时候就创建UriMatcher实例
     */
    private val uriMatcher = UriMatcher(UriMatcher.NO_MATCH)

    /**
     * 调用addURI()方法,将期望匹配的内容URI格式传递进去
     */
    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 onCreate(): Boolean {
        TODO("Not yet implemented")
    }

    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表当中的所有数据
            }
        }
    }

    override fun getType(uri: Uri): String? {
        TODO("Not yet implemented")
    }

    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")
    }
}
  • 在query()方法被调用的时候,就会通过UriMatcher的match()方法对传入的Uri对象进行匹配,如果发现UriMatcher中某个内容URI格式成功匹配了该Uri对象,则会返回相应的自定义代码,然后我们就可以判断出调用方法期望访问的到底是什么数据了.
  • 其他的一些方法(insert(),update(),delete())这几个方法的实现都是差不多的,它们都会携带uri参数,然后同样利用UriMatcher的match()方法判断出调用方法期望访问的是哪张表,再对该表中的数据进行操作就可以了
  • 在重写的几个方法当中有一个比较陌生的方法,getType()方法,它是所有的ContentProvider都必须提供的一个方法,用于获取Uri对象所对应的MIME类型,一个内容URI所对应的MIME字符串主要有3个部分组成,Android对这三个部分做了如下格式规定
  • 必须以vnd开头
  • 如果内容URI以路径结尾,则后接android.cursor.dir/;如果内容URI以id结尾,则后接android.cursor.item/
  • 最后街上vnd..
  • 所以对于content://com.example.app.provider/table1这个内容URI,它所对应的MIME类型就可以写成:
van.android.cursor.dir/vnd.com.example.app.provider.table1
  • 对于content://com.example.app.provider/table1/1这个内容URI,它所对应的MIME类型就可以写成
van.android.cursor.item/vnd.com.example.app.provider.table1
  • 所以现在就可以实现getType()方法当中的逻辑了,代码如下所示
    override fun getType(uri: Uri) = when (uriMatcher.match(uri)) {
        table1Dir -> "van.android.cursor.dir/vnd.com.example.app.provider.table1"
        table1Item -> "van.android.cursor.item/vnd.com.example.app.provider.table1"
        table2Dir -> "van.android.cursor.dir/vnd.com.example.app.provider.table2"
        table2Item -> "van.android.cursor.item/vnd.com.example.app.provider.table2"
        else -> null
    }
  • 现在我们就创建了一个完整的ContentProvider,现在任何一个应用程序都可以使用ContentProvider访问我们程序当中的数据了
  • 并且得益于ContentProvider的良好机制,能够保证隐私数据不会被泄露出去

实现跨程序数据共享

  • 在DatebaseTest项目当中创建一个DatebaseProvider
  • 修改DatebaseProvider当中的代码
class DatebaseProvider : ContentProvider() {
    private val bookDir = 0
    private val bookItem = 1
    private val categoryDir = 2
    private val categoryItem = 3
    private val authority = "com.zb.databasetest.provider"
    private var dbHelper: MyDatabaseHelper? = null

    /**
     * by lazy 懒加载机制对UriMatcher进行初始化操作
     */
    private val uriMather by lazy {
        val matcher = UriMatcher(UriMatcher.NO_MATCH)
        matcher.addURI(authority, "book", bookDir)
        matcher.addURI(authority, "book/#", bookItem)
        matcher.addURI(authority, "category", categoryDir)
        //代码块的最后一行代码,赋值给uriMather
        matcher
    }

    /**
     * 删除数据
     *
     * @param uri Uri
     * @param selection String?
     * @param selectionArgs Array<String>?
     */
    override fun delete(uri: Uri, selection: String?, selectionArgs: Array<String>?) =
        dbHelper?.let {
            //删除数据
            val db = it.writableDatabase
            val deletedRows = when (uriMather.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 (uriMather.match(uri)) {
        bookDir -> "vnd.android.cursor.dir/vnd.com.zb.databasetest.provider.book"
        bookItem -> "vnd.android.cursor.item/vnd.com.zb.databasetest.provider.book"
        categoryDir -> "vnd.android.cursor.dir/vnd.com.zb.databasetest.provider.category"
        categoryItem -> "vnd.android.cursor.item/vnd.com.zb.databasetest.provider.category"
        else -> null
    }

    /**
     * 添加数据
     * @param uri Uri
     * @param values ContentValues?
     * @return Uri?
     */
    override fun insert(uri: Uri, values: ContentValues?) = dbHelper?.let {
        //添加数据
        val db = it.writableDatabase
        val uriReturn = when (uriMather.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() = context?.let {
        dbHelper = MyDatabaseHelper(it, "BookStore.db", 2)
        true
    } ?: false

    /**
     * 查询数据
     * @param uri Uri
     * @param projection Array<String>?
     * @param selection String?
     * @param selectionArgs Array<String>?
     * @param sortOrder String?
     * @return Cursor?
     */
    override fun query(
        uri: Uri, projection: Array<String>?, selection: String?,
        selectionArgs: Array<String>?, sortOrder: String?
    ) = dbHelper?.let {
        //查询数据
        val db = it.readableDatabase
        val cursor = when (uriMather.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
    }

    /**
     * 更新数据
     *
     * @param uri Uri
     * @param values ContentValues?
     * @param selection String?
     * @param selectionArgs Array<String>?
     */
    override fun update(
        uri: Uri, values: ContentValues?, selection: String?,
        selectionArgs: Array<String>?
    ) = dbHelper?.let {
        //更新数据
        val db = it.writableDatabase
        val updateRows = when (uriMather.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
}
  • 在类的一开始定义了四个变量,分别用于表示访问Book表中所有的数据,访问Book表中的单条数据,访问Category表中的所有/单挑数据
  • 然后在一个by lazy代码块里对UriMatcher进行了初始化操作,将希望匹配的几种URI格式添加进去
  • by lazy代码是Kotlin当中的一种懒加载技术,代码块中的代码一开始并不会执行,只有当uriMather变量首次被调用的时候才会执行,并且会将代码块中最后一行代码作为返回值赋给uriMather
  • 在onCreate()方法当中,调用了getContext()方法借助?.操作符和let函数判断它的返回值是否为空:如果为空就使用?:操作符返回fasle,表示ContentProvider初始化失败;如果不为空就执行let函数当中的代码.
  • 在let函数当中创建了一个MyDatebaseHelper的实例,然后返回true表示ContentProvider初始化成功.
  • 在query()方法当中先获取了SQLiteDatabase的实例,然后根据传入的Uri参数判断用户想要访问那种表,再调用SQLiteDatabase的query()进行查询,并将Cursor对象进行返回.
  • 然后就是insert()方法,它先是获取了SQLiteDatabase的实例,然后根据传入的Uri参数判断用户想要往那张表当中添加数据,再调用SQLiteDatabase的insert()方法进行添加就可以了.
  • 需要注意的就是insert方法需要返回一个能够表示这条新增数据的URI,所以我们还需要调用Uri.parse()方法,将一个内容URI解析成为Uri对象,当然这个内容URI是以新增数据的id结尾的.
  • 接下来就是update()方法,在这个方法当中也是先获取SQLiteDatabase的实例,然后根据传入的uri参数判断用户想要更新那张表当中的数据,再调用update()方法进行更新即可,受影响的行数将被作为返回值进行返回.
  • delete()方法,仍然是先获取SQLiteDatabase的实例,然后根据传入的uri参数判断用户想要删除那张表当中的数据,再调用SQLiteDatabase的delete()方法进行删除就可以啦,被删除的行数被作为返回值进行返回.
  • 还有一点需要注意的就是,ContentProvider一定要在AndroidManifest.xml文件中注册才可以使用,但是使用Android Studio的快捷方式注册的ContentProvider会自动帮我们进行注册,注册的形式如下所示

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-LUJBZgEL-1671516835604)(C:/Users/zhengbo/%E6%88%91%E7%9A%84%E5%AD%A6%E4%B9%A0/Typora%E5%AD%A6%E4%B9%A0%E7%AC%94%E8%AE%B0/%E5%AE%89%E5%8D%93/image-20221219215915047.png)]

  • 创建一个ProviderTest进行测试
  • 首先修改activity_main.xml当中的代码
<?xml version="1.0" encoding="utf-8"?>
<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>
  • 定义四个按钮
  • 在MainActivity当中编写按钮的点击事件,分别对应的就是增删改查的逻辑
class MainActivity : AppCompatActivity() {
    var bookId: String? = null

    @SuppressLint("Range")
    override fun onCreate(savedInstanceState: Bundle?) {
        super.onCreate(savedInstanceState)
        setContentView(R.layout.activity_main)
        addData.setOnClickListener {
            //添加数据
            val uri = Uri.parse("content://com.zb.databasetest.provider/book")
            val values = contentValuesOf(
                "name" to "A Clash of Kings",
                "author" to "George Martin", "pages" to 1040, "price" to 22.5
            )
            val newUri = contentResolver.insert(uri, values)
            bookId = newUri?.pathSegments?.get(1)
        }
        queryData.setOnClickListener {
            //查询数据
            val uri = Uri.parse("content://com.zb.database")
            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 {
            //更新数据
            bookId?.let {
                val uri = Uri.parse("content://com.zb.database.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 {
            //删除数据
            bookId?.let { 
                val uri = Uri.parse("content://com.zb.databasetest.provider/book/$it")
                contentResolver.delete(uri, null, null)
            }
        }
    }
}
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值