Android ContentProvider学习(一)

1,222 阅读3分钟

这是我参与11月更文挑战的第1天,活动详情查看:2021最后一次更文挑战

本篇学习笔记主要是学习ContentProvider的基础知识,通过操作联系人信息的增删改查,了解如何通过ContentProvider操作提供程序的数据。本片学习笔记的内容都是来源于内容提供程序基础知识,可以直接点击连接访问官方文档。

概述

内容提供程序有助于管理其自身和其它应用所存储数据的访问,并提供与其它应用共享数据的方法。它们回封装数据并提供用于定义数据安全性的机制。

内容提供程序是一种标准接口,可将一个进程中的数据与另一个进程中的代码相连接。通过配置内容提供程序,可以使其它应用安全的访问和修改当前应用的数据。

基础

访问内容提供程序

如果需要访问内容提供程序中的数据,可以在客户端中使用ContextContentResolver对象与提供程序进行通信。提供程序对象从客户端接收数据请求,执行请求的操作并返回结果.ContentResolver对象的某些方法可调用提供程序对象中的同名方法。ContentResolver方法可提供持久性存储空间的基本CRUD(创建,检索,更新和删除)功能。

Android中的通讯录是内置的提供程序之一,用户可以在这里存储通讯录信息。如果我们需要查询当前手机的通讯录列表,则可以通过调用ContentResolver.query()方法获取,此方法会调用ContentProvider中的query()方法,下面的代码展示了获取当前手机通讯录的功能:

    //读取联系人数据
    private fun queryContactList() {
        //请求联系人信息
        val cursor = this.contentResolver.query(
            ContactsContract.Contacts.CONTENT_URI,
            null,
            null,
            null,
            null
        )
        cursor?.let {
            while (it.moveToNext()){
                val rawId = it.getString(it.getColumnIndex(ContactsContract.Contacts.NAME_RAW_CONTACT_ID))
                //根据rawId请求联系人信息
                val dataCursor = this.contentResolver.query(
                    ContactsContract.Data.CONTENT_URI,
                    null,
                    "${ContactsContract.Data.RAW_CONTACT_ID} = ? ",
                    arrayOf(rawId.toString()),
                    null,
                )?.apply {
                    while (this.moveToNext()) {
                        //获取类型信息
                        val mimeType = this.getString(this.getColumnIndex(ContactsContract.Data.MIMETYPE))
                        var name = ""
                        var phone = ""
                        when(mimeType){
                            ContactsContract.CommonDataKinds.StructuredName.CONTENT_ITEM_TYPE -> {
                                name = this.getString(this.getColumnIndex(ContactsContract.CommonDataKinds.StructuredName.DISPLAY_NAME))
                            }
                            ContactsContract.CommonDataKinds.Phone.CONTENT_ITEM_TYPE -> {
                                phone = this.getString(this.getColumnIndex(ContactsContract.CommonDataKinds.Phone.NUMBER))
                            }
                        }
                        Log.i(TAG, "queryContactList: contact name is:$name,phone is:$phone")
                    }
                }
                dataCursor?.close()
            }
        }
        cursor?.close()
    }

上面的代码的功能是从联系人数据库中获取每一个联系人的名称和手机号信息,我们首先通过ContentResolverquery()方法查询出全部的原始联系人的rawId,接着使用这些rawId再去查找具体的信息。

上面我们主要使用query()方法来执行查询操作,它的参数列表和含义如下:

query()参数SELECT关键字的参数备注
UriFROM table_nameUri映射至提供程序中名为table_name的表
projectioncol,col,col...我们需要查询出的列的名称数组
selectionwhere col = ?指定的选择的行的条件
selectionAtgs代替selection中?的值我们需要指定的条件值
sortOrderORDER BY col,col指定返回的Cursor中每一行的显示顺序

内容Uri

内容Uri用来在提供程序中标识数据,内容Uri包含整个提供程序的符号名称(也就是Uri中授权机构authority部分),和指向表的名称(也就是Uri中的path部分)。

在上面的代码中,我们使用ContactsContract.Contacts.CONTENT_URI这个常量来表示我们需要查询的表的路径。ContentResolver对象会解析出Uri的授权,并将该授权与已知提供程序的系统表进行比较,从而解析提供程序。之后,ContentResolver就可以将查询参数分配给正确的提供程序。

ContentProvider使用内容Uri的路径部分表示需要访问的表的名称。通常,提供程序会为其公开的每个表显示一条路径。

在上面的代码中,ContactsContract.Contacts.CONTENT_URI的完整路径是:

content://com.android.contacts/contacts

同时,许多应用程序支持将id添加到Uri的末尾从未指定要查询具体的某一行,例如在上面的代码中,我们只希望查询rawId为2对应的联系人的信息,则可以将Uri设置为:

val cursor = this.contentResolver.query(
    ContentUris.withAppendedId(ContactsContract.Contacts.CONTENT_URI,2),
    null,
    null,
    null,
    null
)

当我们检索到很多行并且想要更新或者删除某一行的时候可以使用ID值。

插入,更新和删除数据

与从提供程序检索数据类似,我们也可以通过提供程序客户端和提供程序的ContentProvider之间的交互修改数据。我们可以使用传递至ContentProvider对应方法的参数调用ContentResolver的方法,提供程序和提供程序客户端会自动处理安全性和进程间通信。

插入数据

如果需要将数据插入到提供程序中,我们可以通过调用ContentResolver.insert()方法执行相应的操作。这个方法会在提供程序中插入新行,并为该行返回内容Uri,下面的代码演示了如何将一个新的联系人插入到通讯录中:

    /**
     * 在添加联系人的时候,我们需要指定原始联系人信息,我们一般情况下可以指定这个联系人所在的账户信息,也可以不指定
     */
    private fun addRawContact(accountName: String, accountType: String) {
        //插入原始联系人之后返回的id信息
        var id = -1L
        //创建插入数据时的ContentValues
        val value = ContentValues()
        value.put(ContactsContract.RawContacts.ACCOUNT_NAME, accountName)
        value.put(ContactsContract.RawContacts.ACCOUNT_TYPE, accountType)
        val uri = this.contentResolver.insert(
            ContactsContract.RawContacts.CONTENT_URI,
            value
        )
        if (uri != null) {
            id = ContentUris.parseId(uri)
        }
        //判断能否添加联系人
        if (id == -1L) {
            showInfo(R.string.can_not_create_account)
        } else {
            addContact(id)
        }

    }

    //向联系人数据库中添加数据
    private fun addContact(rawId: Long) {
        val name = mBinding.etContactName.text.toString()
        val phone = mBinding.etContactPhoneNumber.text.toString()

        //有了账户id之后,我们需要将这个联系人的信息存储进来,我们不会判断联系人信息是否重复,直接插入数据
        //首先插入名称信息
        val nameContentValues = ContentValues()
        nameContentValues.put(ContactsContract.Data.RAW_CONTACT_ID, rawId)
        nameContentValues.put(
            ContactsContract.Data.MIMETYPE,
            ContactsContract.CommonDataKinds.StructuredName.CONTENT_ITEM_TYPE
        )
        nameContentValues.put(ContactsContract.CommonDataKinds.StructuredName.DISPLAY_NAME, name)

        //接着插入手机号信息
        val phoneContentValues = ContentValues()
        phoneContentValues.put(ContactsContract.Data.RAW_CONTACT_ID, rawId)
        phoneContentValues.put(
            ContactsContract.Data.MIMETYPE,
            ContactsContract.CommonDataKinds.Phone.CONTENT_ITEM_TYPE
        )
        phoneContentValues.put(ContactsContract.CommonDataKinds.Phone.NUMBER, phone)

        //执行插入操作
        this.contentResolver.insert(ContactsContract.Data.CONTENT_URI, nameContentValues)
        this.contentResolver.insert(ContactsContract.Data.CONTENT_URI, phoneContentValues)
    }

上面的代码演示了向通讯录中插入一个新的联系人的操作。首先我们在ContactsContract.RawContacts.CONTENT_URI指向的原始联系人表中添加了一个新的原始联系人记录,插入这个数据之后,我们会获得这个记录的Uri,之后我们分离出其中的id信息,获取到这个id之后,我们就可以针对这个id设置这个联系人的姓名和手机号的信息。

执行完上面的操作之后,我们就可以在通讯录应用中找到刚才插入的这个联系人的信息。

ContentProviderOperation

上面的代码,我们在添加联系人的时候,是向ContactsContract.Data.CONTENT_URI所指向的表中添加了两行数据,一行是联系人名称的数据,一行是联系人手机号的数据。我们执行了两次插入操作。

对于这种批量访问提供程序的操作,例如插入大量行或者通过同一个方法调用在多个表中插入行,或者通常以事务(原子操作)的形式跨进程边界执行一组操作,通常可以使用ContentProviderOperation来执行,如下修改了添加联系人中的方法:

        //使用ContentProviderOperation进行批量操作
        val operationList = arrayListOf<ContentProviderOperation>()

        //插入联系人姓名
        operationList.add(ContentProviderOperation.newInsert(ContactsContract.Data.CONTENT_URI)
            .withValue(ContactsContract.Data.RAW_CONTACT_ID,rawId)
            .withValue(ContactsContract.Data.MIMETYPE,
                ContactsContract.CommonDataKinds.StructuredName.CONTENT_ITEM_TYPE)
            .withValue(ContactsContract.CommonDataKinds.StructuredName.DISPLAY_NAME, name)
            .build()
        )
        //插入联系人手机号
        operationList.add(
            ContentProviderOperation.newInsert(ContactsContract.Data.CONTENT_URI)
                .withValue(ContactsContract.Data.RAW_CONTACT_ID, rawId)
                .withValue(ContactsContract.Data.MIMETYPE,
                    ContactsContract.CommonDataKinds.Phone.CONTENT_ITEM_TYPE)
                .withValue(ContactsContract.CommonDataKinds.Phone.NUMBER, phone)
                .build()
        )
        //执行插入操作
        val resultArray = this.contentResolver.applyBatch(ContactsContract.AUTHORITY,operationList)

通过上面的操作,也可以在通讯录中插入一条联系人信息。

更新数据

和插入数据一样,我们可以通过ContentResolver.update()方法更新相应的数据,唯一需要注意的是,在更新数据的时候我们需要指定条件。下面的代码演示了使用ContentProviderOpration更新某一个联系人的信息。

    //更新联系人信息
    private fun updateContact(){
        val operation = arrayListOf<ContentProviderOperation>()
        val name = mBinding.etContactName.text.toString()
        val phone = mBinding.etContactPhoneNumber.text.toString()
        //设置名称信息
        operation.add(ContentProviderOperation.newUpdate(ContactsContract.Data.CONTENT_URI)
            .withValue(ContactsContract.Data.MIMETYPE,ContactsContract.CommonDataKinds.StructuredName.CONTENT_ITEM_TYPE)
            .withValue(ContactsContract.Data.RAW_CONTACT_ID,mContactEntity!!.rawId)
            .withValue(ContactsContract.CommonDataKinds.StructuredName.DISPLAY_NAME,name)
            .withSelection("${ContactsContract.Data._ID} = ? ", arrayOf(mContactEntity!!.nameId.toString()))
            .build()
        )
        //设置手机号信息
        operation.add(ContentProviderOperation.newUpdate(ContactsContract.Data.CONTENT_URI)
            .withValue(ContactsContract.Data.MIMETYPE,ContactsContract.CommonDataKinds.Phone.CONTENT_ITEM_TYPE)
            .withValue(ContactsContract.Data.RAW_CONTACT_ID,mContactEntity!!.rawId)
            .withValue(ContactsContract.CommonDataKinds.Phone.NUMBER,phone)
            .withSelection("${ContactsContract.Data._ID} = ? ", arrayOf(mContactEntity!!.phoneId.toString()))
            .build()
        )
        //执行插入操作
        val resultArray = this.contentResolver.applyBatch(ContactsContract.AUTHORITY,operation)
        for(result in resultArray){
            Log.i(TAG, "addContact: result is:${result.uri}")
        }
        //执行完操作之后退出页面
        onBackPressed()
    }

上面的代码设置了根据当前的id更新对应的联系人信息。最终会将一个联系人的信息成功更新为新的数据。

删除数据

删除行与检索行数据类似,为需要删除的行指定选择条件,客户端方法会返回已删除的行数,下面代码会删除联系人表中指定的联系人:

    //删除一个联系人信息
    private fun deleteContact(){
        mContactEntity?.let {
            val count = this.contentResolver.delete(
                ContactsContract.RawContacts.CONTENT_URI,
                "${ContactsContract.RawContacts._ID} = ?",
                arrayOf("${it.rawId}")
            )
            Log.i(TAG, "deleteContact: count is:$count")

            //删除完成后退出页面
            onBackPressed()
        }
    }

在上面的代码中,我们指定了要删除的联系人的rawId,通过这个id删除了ContactsContract.RawContacts表中的数据,返回之后能够看到指定的联系人已经不存在了。

至此,我们就对ContentProvider中如何访问和修改提供程序的数据学习完了。

本片学习笔记中的代码位于ContentProvider学习