在上一节当中,我们学习了如何在自己的程序中访问其他应用程序的数据。总体来说思路还是非常简单的,只需要获取到该应用程序的内容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项目 从上到下依次点击按钮来试验结果吧。
呼呼,这一章终于写完了,好累,即使是站在巨人的肩膀上学习也觉得累,更何况巨人是怎么学习的呢?各位看官们,爱拼才会赢,加油吧大家。