8.4--创建自己的 ContentProvider

在上一节当中,我们学习了如何在自己的程序中访问其他应用程序的数据。总体来说思路还是非常简单的,只需要获取到该应用程序的内容URI,然后借助ContentResolver进行CRUD操作就可以了。可是你有没有想过,那些提供外部访问接口的应用程序都是如何实现这种功能的呢?它们又是怎样保证数据的安全性,使得隐私数据不会泄漏出去?学习完本节的知识后,你的疑惑将会被一一解开。

 

8.4.1 创建 ContentProvider 的步骤

如果要实现跨程序共享数据功能,可以通过新建一个类去继承ContentProvider 的方式来实现。ContentProvider 类中有6个抽象方法,我们在使用子类继承它的时候,需要将这6个抽象方法全部重写。

class MyProvider : ContentProvider() {
    override fun insert(uri: Uri, values: ContentValues?): Uri? {
        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 onCreate(): Boolean {
        TODO("Not yet implemented")
    }

    override fun update(
        uri: Uri,
        values: ContentValues?,
        selection: String?,
        selectionArgs: Array<out String>?
    ): Int {
        TODO("Not yet implemented")
    }

    override fun delete(uri: Uri, selection: String?, selectionArgs: Array<out String>?): Int {
        TODO("Not yet implemented")
    }

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

对于这6个方法简单介绍一下把,已经见到很多次了:

(1)onCreate()。 初始化ContentProvider 的时候调用。通常会在这里完成对数据库的创建和升级等操作,返回true 表示初始化成功,返回false 则表示失败。

(2)query()。从ContentProvider 中查询数据。uri 参数用于确定查询哪张表,projection 参数用于确定查询哪些列,selection 和 selectionArgs 参数用于约束查询哪些行,sortOrder参数用于对结果进行排序,查询的结果放在Cursor 对象中返回。

(3)insert()。向内容提供器中添加一条数据。使用uri参数来确定要添加到的表,待添加的数据保存在values参数中。添加完成后,返回一个用于表示这条新记录的URI。

(4)update()。更新内容提供器中已有的数据。使用uri参数来确定更新哪一张表中的数据,新数据保存在values 参数中,selection和selectionArgs参数用于约束更新哪些行,受影响的行数将作为返回值返回。

(5)delete()。从内容提供器中删除数据。使用uri参数来确定删除哪一张表中的数据,selection和selectionArgs参数用于约束删除哪些行,被删除的行数将作为返回值返回。

(6)getType()。根据传入的内容URI来返回相应的MIME类型。

可以看到,几乎每一个方法都会带有Uri这个参数,这个参数也正是调用ContentResolver的增删改查方法时传递过来的。而现在,我们需要对传入的Uri参数进行解析,从中分析出调用方期望访问的表和数据。

回顾一下,一个标准的内容URI写法是这样的:

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

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

这就表示调用方期望访问的是com.example.app这个应用的tablel表中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个参数,可以分别把authority、path和一个自定义代码传进去。这样,当调用UriMatcher的match()方法时,就可以将一个Uri对象传入,返回值是某个能够匹配这个Uri对象所对应的自定义代码,利用这个代码,我们就可以判断出调用方期望访问的是哪张表中的数据了。修改MyProvider中的代码,如下所示:

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)
    }

    override fun insert(uri: Uri, values: ContentValues?): Uri? {
        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 表中的单条数据
            }
        }
        return null
    }

    override fun onCreate(): Boolean {
        TODO("Not yet implemented")
    }

    override fun update(
        uri: Uri,
        values: ContentValues?,
        selection: String?,
        selectionArgs: Array<out String>?
    ): Int {
        TODO("Not yet implemented")
    }

    override fun delete(uri: Uri, selection: String?, selectionArgs: Array<out String>?): Int {
        TODO("Not yet implemented")
    }

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

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

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

除此之外,还有一个方法你会比较陌生,即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()方法中的逻辑,代码如下所示:

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,所以这部分数据根本无法被外部程序访问到,安全问题也就不存在了。
好了,创建内容提供器的步骤你也已经清楚了,下面就来实战一下,真正体验一回跨程序数据共享的功能。

 

8.4.2 实战跨程序数据共享

简单起见,我们还是在上一章中DatabaseTest项目的基础上继续开发,通过内容提供器来给它加入外部访问接口。打开DatabaseTest项目,首先将MyDatabaseHelper中使用Toast弹出创建数据库成功的提示去除掉,因为跨程序访问时我们不能直接使用Toast。然后创建一个内容提供器,右击com.example.broadcasttest包→New→Other→Content Provider。

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

接着我们修改DatabaseProvider中的代码,如下所示:

class DatabaseProvider : ContentProvider() {

    private val bookDir = 0
    private val bookItem = 1
    private val categoryDir = 2
    private val categoryItem = 3
    private val authority = "com.example.databasetest.provider"
    private var dbHelper:MyDatabaseHelper? = null
    // 懒加载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
    }
    override fun delete(uri: Uri, selection: String?, selectionArgs: Array<String>?) = dbHelper?.let {
        val db = it.readableDatabase
        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.corsor.dir/vnd.$authority.book"
        bookItem -> "vnd.android.corsor.item/vnd.$authority.book"
        categoryDir -> "vnd.android.corsor.dir/vnd.$authority.category"
        categoryItem -> "vnd.android.corsor.item/vnd.$authority.category"
        else -> null
    }

    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
    }
    // ?. == 非空逻辑   ?: == 空逻辑 2.7章有介绍哦。  还有返回值也可以省略 具体查看2.3.2函数 那节
    override fun onCreate() = context?.let {
        dbHelper = MyDatabaseHelper(it,"BookStore",2)
        true
    }?:false

    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
    }

    override fun update(
        uri: Uri, values: ContentValues?, selection: String?,
        selectionArgs: Array<String>?
    ) = dbHelper?.let {
        val db = it.readableDatabase
        val updateRows = 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
        }
        updateRows
    }?:0
}

代码虽然很长,不过不用担心,这些内容不难理解,因为使用的全都是上一小节中我们学到的知识。首先在类的一开始,同样是定义了4个变量,分别用于表示访问Book 表中的所有数据,访问Book 表中的单条数据,访问Category 表中的所有数据和访问Category 表中的单条数据。最后在一个by lazy 代码块里对UriMatcher(Uri匹配器) 进行了初始化操作,将期望匹配的几种URI 格式添加了进去。by lazy 代码块是Kotlin 提供的一种懒加载技术,代码块中的代码开始并不会执行,只有当uriMatcher 变量首次被调用的时候才会执行。并且将代码块中最后一行代码的返回值 赋给 UriMatcher。

接下来就是每个抽象方法的具体实现了,先来看一下onCreate() 方法。这个方法的代码很短,但是语法可能有些特殊。这里我们综合利用了Getter 方法语法糖 、?. 非空判断操作符、?: 空判断操作符、let 函数以及单行代码函数语法糖。首先调用了getContext() 方法并借助 ?. 操作符和let 函数判断它的返回值是否为空:如果为空就使用?:  操作符返回false,表示ContentProvider 初始化失败;如果不为空就执行let 函数中的代码。在let 函数中创建了一个MyDatabaseHelper 的实例,然后返回true 表示ContentProvider 初始化成功。由于我们借助了多个操作符和标准函数,因此这段逻辑是在一行表达式内完成的,符合单行代码函数的语法糖要求,所以直接使用等号连接返回值即可。其他几个方法的语法结果是类似的,相信各位看官能看得懂。

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

再往后就是insert()方法,同样它也是先获取到了SQLiteDatabase的实例,然后根据传入的Uri参数判断出用户想要往哪张表里添加数据,再调用SQLiteDatabase的insert()方法进行添加就可以了。注意insert()方法要求返回一个能够表示这条新增数据的URI,所以我们还需要调用Uri.parse()方法来将一个内容URI解析成Uri对象,当然这个内容URI是以新增数据的id结尾的。

接下来就是update()方法了,相信这个方法中的代码已经完全难不倒你了。也是先获取SQLiteDatabase的实例,然后根据传入的Uri参数判断出用户想要更新哪张表里的数据,再调用SQLiteDatabase的update()方法进行更新就好了,受影响的行数将作为返回值返回。

下面是delete()方法,是不是感觉越到后面越轻松了?因为你已经渐入佳境,真正地找到窍门了。这里仍然是先获取到SQLiteDatabase的实例,然后根据传入的Uri参数判断出用户想要删除哪张表里的数据,再调用SQLiteDatabase的delete()方法进行删除就好了,被删除的行数将作为返回值返回。

最后是getType()方法,这个方法中的代码完全是按照上一节中介绍的格式规则编写的,相信已经没有什么解释的必要了。这样我们就将内容提供器中的代码全部编写完了。
 

另外还有一点需要注意,内容提供器一定要在AndroidManifest.xml文件中注册才可以使用。不过幸运的是,由于我们是使用Android Studio的快捷方式创建的内容提供器,因此注册这一步已经被自动完成了。打开AndroidManifest.xml文件瞧一瞧,代码如下所示:

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

    <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/AppTheme">
        <provider
            android:name=".DatabaseProvider"
            android:authorities="com.example.databasetest.provider"
            android:enabled="true"
            android:exported="true"></provider>

        <activity android:name=".MainActivity">
            <intent-filter>
                <action android:name="android.intent.action.MAIN" />

                <category android:name="android.intent.category.LAUNCHER" />
            </intent-filter>
        </activity>
    </application>

</manifest>

 

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

现在DatabaseTest这个项目就已经拥有了跨程序共享数据的功能了,我们赶快来尝试一下。
首先需要将DatabaseTest程序从模拟器中删除掉,以防止上一章中产生的遗留数据对我们造成干扰。然后运行一下项目,将DatabaseTest程序重新安装在模拟器上了。接着关闭掉 DatabaseTest这个项目,并创建一个新项目ProviderTest,我们就将通过这个程序去访问DatabaseTest中的数据。

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

class MainActivity : AppCompatActivity() {

    private var bookId:String? = null
    override fun onCreate(savedInstanceState: Bundle?) {
        super.onCreate(savedInstanceState)
        setContentView(R.layout.activity_main)
        addData.setOnClickListener {
            // 添加数据
            val uri = Uri.parse("content://com.example.databasetest.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)
        }

        queryData.setOnClickListener {
            // 查询数据
            val uri = Uri.parse("content://com.example.databasetest.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 name is $author")
                    Log.d("MainActivity","book name is $pages")
                    Log.d("MainActivity","book name is $price")
                }
                close()
            }
        }

        updateData.setOnClickListener {
            // 更新数据   更新我们刚刚添加进去的数据
            bookId?.let {
                val uri = Uri.parse("content://com.example.databasetest.provider/book/$it")
                val values = contentValuesOf(
                    "name" to "A Store 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.example.databasetest.provider/book/$it")
                contentResolver.delete(uri,null,null)
            }
        }

    }
}

可以看到,我们分别在这4个按钮的点击事件里面处理了增删改查的逻辑。添加数据的时候,首先调用了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表中的其他数据都不会受影响。

现在运行一下ProviderTest项目 从上到下依次点击按钮来试验结果吧。

呼呼,这一章终于写完了,好累,即使是站在巨人的肩膀上学习也觉得累,更何况巨人是怎么学习的呢?各位看官们,爱拼才会赢,加油吧大家。

  • 1
    点赞
  • 1
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值