Android ContentProvider学习(二)

1,332 阅读13分钟

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

概述

在上一篇的学习笔记中,我们通过对联系人数据库中的数据进行增删改查的操作,了解了如何通过ContentProvider获取和修改其它应用提供的数据。主要是使用了ContentResolver类中提供的query(),insert(),update(),delete()这四个方法执行相应的操作。

上一篇笔记主要聚焦的是客户端这一类型的角色,我们的应用需要读取其它应用的数据。这边学习笔记我们将会聚焦到服务端这一类型的角色,我们的应用需要向其它应用提供数据并接受其它应用对我们应用数据的修改。

本片学习笔记的内容均来自官方文档中创建内容提供程序,点击链接可以直接阅读官方文档部分。

内容提供程序管理对中央数据存储区的访问。如果要实现内容提供程序,需要将其作为应用中的一个或多个类,其中一个类会继承ContentProvider,即内容提供程序与其它应用之间的接口。同时需要创建清单文件中的<provider>元素。尽管内容提供程序的主要目的是向其它应用提供数据,但是我们应用中的某些Activity则必定允许用户查询和修改提供程序所管理的数据。也就是说,我们的内容提供程序仍然需要实现增删改查的基础功能。

目的

首先我们需要了解在什么情况下我们需要创建内容提供程序.如果我们想要实现下面任意的功能,则可以创建内容提供程序:

  • 我们需要为其他应用提供复杂的数据或文件
  • 我们需要将复杂的数据从我们的应用拷贝到其它应用中
  • 我们想要使用搜索框架提供自定义的搜索建议
  • 我们希望向微件公开应用数据
  • 我们希望实现AbstractThreadedSyncAdapter,CursorAdapter或者CursorLoader类。

如果我们已经确认需要实现内容提供程序,则可以通过如下的步骤实现内容提供程序:

  1. 确认我们的应用本身的数据需要采用何种方式存储数据:

    • 文件数据

      这是通常存储在文件中的数据,例如照片,视频,文本等。将文件存储在私有空间内,我们的应用可以通过内容提供程序向其它应用提供文件句柄。

    • 结构化数据

      通常存储在数据库,数组或者类似的结构中。

  2. 定义ContentProvider的子类及其所需方法的具体实现,此类是数据与Android系统其余部分之间的接口。

  3. 定义提供程序的授权字符串(也就是Uri中的authority),该字符串的内容Uri以及列名称。另外,还需要为访问数据的应用提供必要的权限,可以将这些值定义为常量。最后如果想要提供程序应用处理Intent,则需要定义Intent操作,Extra数据等。

  4. 添加其它可选部分。

设计内容URI

内容URI是用于在提供程序中标识数据的URI,内容URI包含整个提供程序的符号名称(也就是授权)和指向表格或文件的名称(也就是路径)。可选ID部分则指向表格中的单个行。ContentProvider的每个数据访问方法均将内容URI作为参数,可以通过这一点确定要访问的表格或文件。

通过之前对Uri的学习,我们已经了解到:Uri的基本结构如下:

模式名称://模式特定部分
scheme://modelSpecialPart

在这里我们使用的Uri的结果为:

模式名称://授权机构/路径
content://authority/path

设计授权

提供程序通常拥有单一授权,该授权充当其Android内部名称。为避免和其它应用发生冲突,一般建议使用软件包名称来作为授权前缀,例如包名为com.example.project的授权可以为com.example.project.provider.

设计路径

在设计完授权之后,我们通常还会加上路径信息,从而根据权限创建内容URI。比如我们有table1table2两个表格,则可以将这两个表名作为路径添加到内容URI中,结合上面的授权,加上路径之后的内容URI可以为content://com.example.project/table1content://com.example.project/table2

提供内容URI id

按照约定,提供程序会接受末尾拥有行ID的内容URI,通过这个值提供对表内单个行的内容的访问。结合上面两部分,如果我们指定需要访问的数据是table1表中id为10的数据,那么我们可以生成的内容URI为:content://com.example.project/table1/10

实现ContentProvider

ContentProvider实例会处理其它应用发送过来的请求,从而管理对结构化数据集的访问。所有形式的访问最终都会调用ContentResolver,后者接着通过调用ContentProvider的具体方法来获取数据。实现ContentProvider类必须实现下面的方法:

  • query()

从提供程序中检索数据,使用参数选择要查询的表,要返回的行和列以及结果的排序顺序,将数据作为Cursor对象返回。

  • insert()

在提供程序中插入新行。使用参数选择目标表并获取要使用的列值。返回新插入的行的内容URI。

  • update()

更新提供程序中的现有行。使用参数选择要更新的表和行,并获取更新后的列值,返回已更新的行数。

  • delete()

从提供程序中删除行。使用参数选择要删除的表和行。返回已删除的行数。

  • getType()

返回内容URI对应的MIME类型。

  • onCreate()

初始化提供程序。创建提供程序后,Android系统会立即调用此方法。创建的时机为:在ContentResolver对象尝试访问提供程序时。

在实现上面的方法的时候,有些事情需要注意:

  1. 所有这些方法,除onCreate()方法外,均可由多个线程同时调用,因此他们必须是线程安全的方法,在实现的时候需要注意多线程的问题。
  2. 避免在onCreate()方法中执行冗长操作,将初始化任务推迟到实际需要时执行。
  3. 尽管我们必须实现上面的方法,但是在具体实现时,我们著需要根据方法签名返回指定类型的数据即可。比如我们并不想别的应用向我们的数据库中插入新的数据,那么在实现insert()方法的时候可以直接返回null

实现onCreate()方法

Android系统会在启动提供程序时调用onCreate(),在这个方法中,我们应该只执行快速运行的初始化任务,并将数据库创建和数据加载延迟到提供程序实际收到数据请求时执行。下面是官方文档中的代码,演示了在onCreate()方法构建数据库对象并获得数据访问对象的句柄:

    // Defines a handle to the Room database
    private lateinit var appDatabase: AppDatabase

    // Defines a Data Access Object to perform the database operations
    private var userDao: UserDao? = null

    override fun onCreate(): Boolean {

        // Creates a new database object.
        appDatabase = Room.databaseBuilder(context, AppDatabase::class.java, DBNAME).build()

        // Gets a Data Access Object to perform the database operations
        userDao = appDatabase.userDao

        return true
    }

实现query()方法

query()方法必须返回Cursor对象,如果失败,系统会抛出Exception,如果我们使用的是SQLite数据库保存对象,那么只需要返回SQLiteDatabase类的某个query()方法返回的Cursor。如果指定的查询操作没有匹配到任何数据,仍然需要返回一个Curosr,只不过这个CursorgetCount()为0.只有当查询出现内部错误的时候,才应该返回null

如果我们保存数据的方式不是使用的SQLite数据库,则需要返回Cursor的某个具体的子类。

实现insert()方法

insert()方法会使用ContentValues中的值,向相应表中添加新行。如果ContentValues中未包含列名称,我们可以在提供程序代码或数据库模式中提供默认值。

此方法应该返回新行的UriSQLiteDatabase中的insert()方法本身就会返回新插入行的id值,我们可以通过ContentUris.withAppendedId()这个方法将新插入的id值设置到Uri中,并将这个Uri作为结果返回。

实现delete()方法

在使用delete()方法的时候,我们无需从数据存储中删除实际的行,在我们将同步适配器和提供程序一起使用的情况下,我们可以考虑为已删除的行添加删除标记,而不是完全移除行。同步适配器可以检查是否存在已删除的行,并将这些行从服务器中移除,然后再将其从提供程序中删除。

实现update()方法

update()方法和insert()方法采用相同的ContentValues参数,并且该方法和query(),delete()方法采用相同的selectionselectionArgs参数,这样,我们就可以再这些方法之间重复使用代码。

实现内容提供程序MIME类型

除了实现上面的增删改查的方法之外,ContentProvider的子类还必须实现getType()方法,这是任何提供程序都必须实现的方法。另外,如果我们的内容提供程序提供的是文件的时候,还需要实现getStreamType()方法。

表的MIME类型

getType()需要返回的是MIME格式的String,后者描述内容URI参数返回的数据类型。URI参数可以是模式,而非特定的URI。在这种情况下,我们应该返回与匹配该模式的内容URI相关联的数据类型。

对于文本,HTML或者JPEG等常见数据类型,getType()应该为该数据返回标准的MIME类型,这些标准类型信息,可以在MIME TYPE 列表网站上获得。

对于指向一行,或多行表数据的内容URI,getType()则应该以Android供应商特有的MIME格式返回相应的类型:

类型部分: vnd

子类型部分: 如果URI模式用于单个行,则使用android.cursor.item/,如果URI模式用于多个行,则使用android.cursor.dir/

提供程序特有部分:vnd.<name>.<type>.其中name应该具有全局唯一性,type应该在对应的URI模式中具有唯一性。适合使用软件包名作为name,适合选择URI关联表的标识字符串作为type

如果提供程序的授权是com.example.project.provider,并且它公开了名为table1的表,则table1中多个行的MIME类型为:vnd.android.cursor.dir/vnd.com.example.project.table1,而单行的MIME类型为vnd.android.cursor.item/vnd.com.example.project.table1.

文件的MIME类型

如果我们的提供程序提供文件,则需要实现getStreamTypes()方法,对于提供程序可以为给定内容URI返回的文件,该方法会为其返回MIME类型的String数组。我们应该通过MIME类型过滤器参数过滤我们提供的MIME类型,一边仅返回客户端想处理的MIME类型。

加入提供程序可以提供.jpg,.png,.gif格式的图像文件,如果客户端应用在调用时使用ContentResolver.getStreams()时使用过滤器字符串image/*(任何图像内容),则ContentProvider.getStreamTypes()方法应返回数组:

{"image/jpeg","image/png","image/gif"}

如果客户端应用只希望获得.jpg格式的文件,则可以在调用ContentResolver.getStreamTypes()时使用过滤器字符串*\/jpg,此时ContentProvider.getStreamTypes()应该返回:

{"image/jpeg"}

如果应用程序中未提供任何过滤器字符串中请求的MIME类型,则getStreamTyps()应该返回null

实现内容提供程序的权限

即使我们的应用数据为底层数据,所有程序仍可读取提供程序的数据或者向其写入数据,这是因为我们的内容提供程序尚未添加任何权限。我们可以使用属性或者<provider>元素的子元素,在清单文件中为提供程序设置权限。我们可以设置适用于整个提供程序,特定表甚至特定记录的权限,或者同时设置适用于这三者的权限。

我们可以使用清单文件中的一个或多个<permission>元素,为提供程序定义权限,例如下面的代码定义了读取和写入提供程序的权限:

    <!--定义访问MyContentProvider需要的权限-->
    <permission
        android:name="${applicationId}.provider.permission.READ_PERMISSION"
        android:protectionLevel="normal"
        />
    <permission
        android:name="${applicationId}.provider.permission.WRITE_PERMISSION"
        android:protectionLevel="normal"
        />

一般情况下我们只需要指定读取写入权限,或者分开指定读取和写入权限即可,如下所示:

  • 指定读取写入权限
android:permission="权限名称"

通过指定上面的权限,外部应用就可以通过这一个权限拥有向提供程序写入和读取数据的能力。

  • 分开指定读取和写入的权限
android:readPermission="权限名称"
android:writePermission="权限名称"

通过分开指定的读取和写入的权限,外部应用可以根据自己的需要声明对应的权限。

例子

下面是一个简单的ContentProvider的例子,主要功能是外部应用修改当前应用中Student表的数据。

创建表

首先我们需要创建一个Student数据表,相关表的内容如下:

//Student数据表
const val TABLE_STUDENT = "Student"
//id
const val TABLE_STUDENT_ID = "id"
//学号
const val TABLE_STUDENT_NO = "no"
//名称
const val TABLE_STUDENT_NAME = "name"
//性别 0-男 1-女
const val TABLE_STUDENT_GENDER = "gender"
//年级
const val TABLE_STUDENT_GRADE = "grade"
//班级
const val TABLE_STUDENT_CLASS = "studentClass"

//创建学生数据表的命令
const val CREATE_TABLE_STUDENT = """
    CREATE TABLE IF NOT EXISTS $TABLE_STUDENT(
        $TABLE_STUDENT_ID INTEGER PRIMARY KEY,
        $TABLE_STUDENT_NO INTEGER PRIMARY KEY,
        $TABLE_STUDENT_NAME TEXT NOT NULL,
        $TABLE_STUDENT_GENDER INTEGER NOT NULL,
        $TABLE_STUDENT_GRADE INTEGER NOT NULL,
        $TABLE_STUDENT_CLASS INTEGER NOT NULL,
        UNIQUE($TABLE_STUDENT_NO)
    );
"""

其次,我们需要在创建数据库的时候创建这个表:

    override fun onCreate(db: SQLiteDatabase?) {
        Log.i(tag,"will create table")
        //数据库创建时调用这个方法
        db?.execSQL(CREATE_TABLE_STUDENT)
    }

创建一个DBDao类用于对学生表进行操作

/**
 * 用于操作学生表的单例
 */
object StudentDBDao {

    //执行数据库相关操作的Helper类
    private var mDBHelper: MySqliteHelper? = null

    //用于读写的数据库连接
    private val mDataBase by lazy {
        mDBHelper!!.writableDatabase
    }

    //在需要的时候创建数据库
    fun openDB(context: Context) {
        if(mDBHelper != null){
            return
        }
        mDBHelper = MySqliteHelper(context, DB_NAME, DB_VERSION)
    }
    
    /**
     * 执行查询操作,如果指定id,那么就将id拼接到筛选参数中去
     */
    fun query(
        projection: Array<out String>?,
        selection: String?,
        selectionArgs: Array<out String>?,
        sortOrder: String?
    ): Cursor {
        return mDataBase.query(
            TABLE_STUDENT,
            projection,
            selection,
            selectionArgs,
            null,
            null,
            sortOrder
        )
    }

    
    //执行插入数据的操作
    fun insert(values: ContentValues): Long{
        if(values.size() == 0){
            //没有需要插入的新数据
            return -1
        }
        return mDataBase.insert(TABLE_STUDENT,null,values)
    }
    
    //执行删除数据的操作
    fun delete(selection: String?, selectionArgs: Array<out String>?): Int{
        return mDataBase.delete(TABLE_STUDENT,selection,selectionArgs)
    }
    
    //执行更新数据的操作
    fun update(selection: String?,selectionArgs: Array<out String>?,contentValues: ContentValues): Int{
        if(contentValues.size() == 0){
            return -1;
        }
        return mDataBase.update(TABLE_STUDENT,contentValues,selection,selectionArgs)
    }

}

在这个类中我们分别定义好了增删改查的方法,这样我们只需要在ContentProvider中调用这里定义好的方法即可。

创建ContentProvider

class StudentProvider : ContentProvider() {

    //授权
    private val AUTHORITY = "com.project.mystudyproject.provider.StudentProvider"

    //数据表
    private val PATH = "/$TABLE_STUDENT"

    override fun onCreate(): Boolean {
        if (context == null) {
            return false
        }
        //准备操作数据库
        StudentDBDao.openDB(context!!)
        return true
    }

    override fun query(
        uri: Uri,
        projection: Array<out String>?,
        selection: String?,
        selectionArgs: Array<out String>?,
        sortOrder: String?
    ): Cursor {
        return StudentDBDao.query(
            projection,
            selection,
            selectionArgs,
            sortOrder
        )
    }

    override fun getType(uri: Uri): String? {
        return "vnd.android.cursor.dir/vnd.example.project.provider.$TABLE_STUDENT"
    }

    override fun insert(uri: Uri, values: ContentValues?): Uri? {
        if (values == null) {
            return null
        }
        return ContentUris.withAppendedId(
            Uri.parse("content://$AUTHORITY/$PATH"),
            StudentDBDao.insert(values)
        )
    }
    override fun delete(uri: Uri, selection: String?, selectionArgs: Array<out String>?): Int {
        return StudentDBDao.delete(selection, selectionArgs)
    }

    override fun update(
        uri: Uri,
        values: ContentValues?,
        selection: String?,
        selectionArgs: Array<out String>?
    ): Int {
        if(values == null){
            return 0;
        }
        return StudentDBDao.update(selection,selectionArgs,values)
    }
}

上面的代码中我们定义了一个StudentProvider,用于向外部提供学生信息。

设置<provider>

    <!--定义访问学生信息的权限-->
    <permission
        android:name="${applicationId}.provider.permission.READ_STUDENT"
        android:protectionLevel="normal"
        />
    
    <permission
        android:name="${applicationId}.provider.permission.WRITE_STUDENT"
        android:protectionLevel="normal"
        />
    
    <provider
    android:name=".uri.content_provider.provider.StudentProvider"
    android:authorities="${applicationId}.provider"
    android:exported="true"
    android:enabled="true"
    android:readPermission="${applicationId}.provider.permission.READ_STUDENT"
    android:writePermission="${applicationId}.provider.permission.WRITE_PERMISSION"
    />

至此,我们就定义好了一个内容提供程序,我们可以创建另一个APP测试是否可以读写这个内容提供者的数据。

创建另一个应用查询数据

设置权限

当我们创建了一个新的APP之后,我们可以首先将需要的权限设置进去:

    <uses-permission android:name="com.project.mystudyproject.provider.permission.READ_STUDENT"/>
    <uses-permission android:name="com.project.mystudyproject.provider.permission.WRITE_PERMISSION"/>

查询数据

获取到权限之后,我们就可以执行查询数据的操作了:

    //读取数据
    private fun queryData() {
        mHandler.post {
            try {
                val cursor = this.contentResolver.query(
                    Uri.withAppendedPath(
                        Uri.parse("content://com.project.mystudyproject.provider.StudentProvider"),
                        "Student"
                    ),
                    arrayOf("id", "name"),
                    null,
                    null,
                    "no"
                )?.apply {
                    //读取数据
                    val list = mutableListOf<StudentBean>()
                    while (this.moveToNext()) {
                        val id = this.getInt(this.getColumnIndex("id"))
                        val name = this.getString(this.getColumnIndex("name"))
                        list.add(StudentBean(id, null, name, null, null, null))
                    }
                    mAdapter.clear()
                    mAdapter.addStudentList(list)
                }

                cursor?.close()
            } catch (e: Exception) {
                Log.e("zyf", "queryData: exception is ${Log.getStackTraceString(e)}")
            }
        }
    }

上面的代码中将查询到的语句设置到了一个RecyclerView中。

添加数据

我们可以通过下面的方法向内容提供程序中添加数据

    //添加一个学生信息
    private fun addStudent(){
        val values = ContentValues()
        values.put("no",Integer.parseInt(noEdit.text.toString()))
        values.put("name",nameEdit.text.toString())
        values.put("gender",if(genderGroup.checkedRadioButtonId == R.id.rb_boy) 0 else 1)
        values.put("grade",Integer.parseInt(gradeEdit.text.toString()))
        values.put("studentClass",Integer.parseInt(classEdit.text.toString()))
        val result = this.contentResolver.insert(Uri.parse("content://com.project.mystudyproject.provider.StudentProvider/Student"),values)
    }

执行上面的方法就可以在内容提供程序中添加一条数据。下面是通过执行上面的方法添加进去的数据

sqlite> select * from Student;
id          no          name        gende  grade  stude
----------  ----------  ----------  -----  -----  -----
1           123         张三          0      1      2
2           124         李四          0      1      2
3           125         王五          0      1      2
4           126         巴拉          1      1      2
5           127         芭比          1      1      2
6           128         叮当          0      1      2

本篇笔记的代码地址:[学习ContentProvider--创建ContentProvider](source/app/src/main/java/com/project/mystudyproject/uri/content_provider/provider · 张一凡/AndroidStudyProject - 码云 - 开源中国 (gitee.com))