1、ContentProvider的简介
ContentProvider是跨程序共享数据的标准方式,它可以选择某一部分数据对外共享,从而保证隐私数据不会有泄露的风险。ContentProvider的使用方式有两种:
- 1、使用现有的ContentProvider去读取或操作其他程序的数据。
- 2、创建新的ContentProvider为外部程序提供数据访问的接口。
Android系统中自带的通讯录、短信、媒体库等都提供了外部访问的接口,这就使得第三方程序可以访问这些数据,下面看下ContentProvider的使用。
2、ContentResolver的使用
对于每一个程序来说,如果要想访问ContentProvider中共享的数据,就需要使用ContentResolver类,可以通过Context.getContentResolver()
来获取ContentResolver
的实例。ContentResolver类中提供了和SQLiteDatabase类中相似的方法:query用于数据查询、delete删除数据、update更新数据、insert插入数据。
SQLiteDatabase操作数据是需要指定表名,表示操作某张表下的数据,但是对于跨程序操作共享数据的ContentResolver来说,如果只指定表名是不够的还需要指定操作那个程序下的表,所以ContentResolver使用Uri来指定哪个程序下的哪张表,Uri由authority和path组成的
,authority用于区分程序的,为了防止冲突一般使用应用包名指定。比如某个程序的包名是com.example.app
,那么authority就可以命名为com.example.app.provider
。path是为了区分表的,比如应用程序中数据库中有table1和table2两张表,那么path分别命名为/table1
和/table2
。然后把authority和path组合后com.example.app.provider/table1
和com.example.app.provider/table2
。不过目前还无法看出它们是URI,还需要在前面加上协议名。因此URI的标准格式如下:
content://com.example.app.provider/table1
content://com.example.app.provider/table2
可以看到上面的URI可以很清晰的指出我们要操作哪个程序下的哪张表,这样我们就可以使用ContentResolver对数据进行操作了。 由于ContentResolver的增删改查方法和SQLiteDatabase类似,这里我们就简单学习下。
- 1、ContentResolver的查询
public final Cursor query(Uri uri,
String[] projection,
String selection,
String[] selectionArgs,
String sortOrder) {}
参数说明:
Uri uri:指定查询的表
String[] projection:要查询那些列,如column1,column2,column3
String selection:指定where条件,如where column=?
String[] selectionArgs:为where占位符提供具体的值
String sortOrder:指定查询结果进行排序的方式
query()方法依然返回Cursor对象,Cursor的使用在SQLiteDatabase中已经学习过了。
- 2、ContentResolver的插入、删除、修改
//插入
public final Uri insert(Uri url,
ContentValues values) {}
//删除
public final int delete(Uri url, String where,
String[] selectionArgs) {}
//修改
public final int update(Uri uri,
ContentValues values, String where,
String[] selectionArgs) {}
学习了最复杂的查询数据,插入、删除、修改方法比较简单。这里不再赘述。下面就具体学习下如果访问ContentProvider中的具体数据。
3、读取系统联系人
没有什么复杂的逻辑,直接上代码
class QueryContactActivity : AppCompatActivity() {
private val contactList = ArrayList<String>()
private lateinit var adapter: ArrayAdapter<String>
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
setContentView(R.layout.activity_query_contact)
adapter = ArrayAdapter(this, android.R.layout.simple_list_item_1, contactList)
contact_list.adapter = adapter
btn_query_contact.setOnClickListener {
initData()
}
}
private fun initData() {
if (ContextCompat.checkSelfPermission(
this,
Manifest.permission.READ_CONTACTS
) != PackageManager.PERMISSION_GRANTED
) {
ActivityCompat.requestPermissions(this, arrayOf(Manifest.permission.READ_CONTACTS), 1)
} else {
getContactData()
}
}
override fun onRequestPermissionsResult(requestCode: Int, permissions: Array<out String>, grantResults: IntArray) {
super.onRequestPermissionsResult(requestCode, permissions, grantResults)
when (requestCode) {
1 -> {
if (!grantResults.isEmpty() && grantResults[0] == PackageManager.PERMISSION_GRANTED) {
getContactData()
} else {
Toast.makeText(this, "请开启读取联系人权限", Toast.LENGTH_SHORT).show()
}
}
}
}
private fun getContactData() {
val contentResolver = contentResolver
contentResolver.query(ContactsContract.CommonDataKinds.Phone.CONTENT_URI, null, null, null, null)?.apply {
while (moveToNext()) {
val name = getString(getColumnIndex(ContactsContract.CommonDataKinds.Phone.DISPLAY_NAME))
val phonNume = getString(getColumnIndex(ContactsContract.CommonDataKinds.Phone.NUMBER))
contactList.add("$name\n$phonNume")
}
close()
adapter.notifyDataSetChanged()
}
}
}
学习完了访问现有的ContentProvider中的共享数据,下面我们看下如何自定义ContentProvider,为其他程序提供访问数据的接口。
4、创建自己的ContentProvider
下面就学习如何定义ContentProvider,为其他程序提供访问数据的接口并保证私密数据的安全性。
4.1、ContentProvider的创建
创建一个类继承ContentProvider并实现其中的方法
class MyContentProvider : ContentProvider() {
override fun insert(uri: Uri, values: ContentValues?): Uri? {
return null
}
override fun query(
uri: Uri,
projection: Array<String>?,
selection: String?,
selectionArgs: Array<String>?,
sortOrder: String?
): Cursor? {
return null
}
override fun onCreate(): Boolean {
return false
}
override fun update(uri: Uri, values: ContentValues?, selection: String?, selectionArgs: Array<String>?): Int {
return 0
}
override fun delete(uri: Uri, selection: String?, selectionArgs: Array<String>?): Int {
return 0
}
override fun getType(uri: Uri): String? {
return null
}
}
- 1、onCreate():初始化ContentProvider时调用,通常在这里完成对数据库的创建和升级等操作,返回true表示ContentProvider初始化成功,返回false表示初始化失败。
- 2、query():从ContentProvider中查询数据。uri表示从哪张表中查询数据,projection表示查询那些列,selection和selectionArgs参数用于查询那些行的数据,sortOrder表示查询数据的排序方式,查询结果通过Cursor对象返回。
- 3、insert():将要插入的数据通过ContentValues组装,uri表示向哪个表中插入数据,返回表示这条新纪录的URI。
- 4、update():通过Uri指定需要更新的表,更新数据通过ContentValues组装,selection和selectionArgs约束指定更新那些行,返回更新影响的行数。
-5、delete():通过Uri指定哪个表需要删除数据,selection和selectionArgs约束指定删除那些行的数据,返回删除影响的行数。
-6、getType():根据传入的Uri返回支持的MIME类型。
4.2、UriMatcher类
可以看到几乎所有的方法都和Uri相关,Uri的标准格式我们上面说过:content://com.example.app.provider/table1
除此之外我们还可以在Uri后加上id,content://com.example.app.provider/table1/1
表示操作table1表中id为1的数据。内容Uri的格式主要就上面两种,我们可以使用通配符来匹配以上两种方式。
*表示匹配任意长度的任意字符
,所以能够匹配任意表的Uri就可以写成content://com.example.app.provider/*
。#表示匹配任意长度的数字
,所以能够匹配表table1中所有数据的内容Uri可以写成content://com.example.app.provider/table1/#
。
接着我们还可以使用UriMatcher
这个类轻松的实现Uri的功能,该类提供了两个方法:
addURI(String authority, String path, int code)
通过该方法可以组建Uri,code是自定义代码和id无关,用于使用Uri.match(Uri uri)进行Uri的匹配。match(Uri uri)
返回匹配传入Uri对象所对应的code,和addURI()
组建URI传入的code对应。
修改MyProvider中的代码
class MyContentProvider : ContentProvider() {
private val tag = javaClass.name
private val authority = "com.example.abu.listview.runtimepermissiondemo.provider"
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(authority, "table1", table1Dir)
uriMatcher.addURI(authority, "table1/#", table1Item)
uriMatcher.addURI(authority, "table2", table2Dir)
uriMatcher.addURI(authority, "table2/#", table2Item)
}
//...
override fun query(
uri: Uri,
projection: Array<String>?,
selection: String?,
selectionArgs: Array<String>?,
sortOrder: String?
): Cursor? {
when (uriMatcher.match(uri)) {
table1Dir -> Log.e(tag, "查询table1中所有数据")
table1Item -> Log.e(tag, "查询table1中单条数据")
table2Dir -> Log.e(tag, "查询table2中所有数据")
table2Item -> Log.e(tag, "查询table2中单条数据")
}
return null
}
//.....
}
下面看下调用ContentResolver.query()
时,ContentProvider中query()
输出的log。
query_data.setOnClickListener {
contentResolver.query(Uri.parse("content://$authority/table1"),null,null,null,null)
contentResolver.query(Uri.parse("content://$authority/table1/1"),null,null,null,null)
contentResolver.query(Uri.parse("content://$authority/table1/#"),null,null,null,null)
}
//输出结果
MyContentProvider: 查询table1中所有数据
MyContentProvider: 查询table1中单条数据
MyContentProvider: 查询table1中所有数据
对此做如下的分析:
- 1、uriMatcher.addURI(authority, "table1", table1Dir):期望匹配table1表中所有数据,所以在传入URI为
content://com.example.app.provider/table1/#
或content://com.example.app.provider/table1
时UriMatcher.match()
返回自定义code即table1Dir - 2、uriMatcher.addURI(authority, "table1/#", table1Item):期望匹配table1表中某一行的数据,所以当传入的URI为
content://com.example.app.provider/table1/1
时UriMatcher.match()
返回自定义code即table1Item,而当传入的Uri是content://com.example.app.provider/table1/#
时却不匹配,故而UriMatcher.match()
返回-1。
4.3、getType()
除此之外,还有一个比较陌生的就是getType()
方法,返回Uri支持的MIME类型,一个Uri对应的MIME字符串有3部分组成:
- 1、必须以
vnd
开头。 - 2、如果内容URI以路径结尾,则后接
android.cursor.dir/
;如果内容URI以id结尾,则后接android.cursor.item/
。 - 3、最后接上vnd.<authority>.<path>。
所以,对于content://com.example.app.provider/table1/1
这个Uri对应的MIMIE为:
vnd.android.cursor.item/vnd.com.example.app.provider.table1
对于content://com.example.app.provider/table1
这个Uri对应的MIMIE为:
vnd.android.cursor.dir/vnd.com.example.app.provider.table1
现在我们可以修改一下MyContentProvider
的getType()方法
override fun getType(uri: Uri): String? {
when (uriMatcher.match(uri)) {
table1Dir -> return "vnd.android.cursor.dir/vnd.$packageName.table1"
table1Item -> return "vnd.android.cursor.item/vnd.$packageName.table1"
table2Dir -> return "vnd.android.cursor.dir/vnd.$packageName.table2"
table2Item -> "vnd.android.cursor.item/vnd.$packageName.table2"
}
return null
}
4.4、ContentProvider的安全
到这里一个完整的ContentProvider就创建完成了,那么如何解决隐私数据不会泄露呢?其实很简单,上面我们在UriMatcher中添加了一些Uri,只有和我们添加的Uri匹配的才能进行数据的增删改查,只要我们不添加隐私数据的Uri即可解决隐私数据的安全问题。
5、实现跨程序数据共享
创建DatabaseProvider
类继承ContentProvider
,并在AndroidManifest中进行配置,指定authorities
为com.example.abu.listview.databaseproviderdemo.provider,enabled
为true表示可用,exported
指定为true表示可以供外部程序使用。
<provider android:authorities="com.example.abu.listview.databaseproviderdemo.provider"
android:exported="true"
android:enabled="true"
android:name=".DatabaseProvider"/>
下面看下DatabaseProvider的具体实现
class DatabaseProvider : ContentProvider() {
private val bookDir = 0
private val bookItem = 1
private val authority = "com.example.abu.listview.databaseproviderdemo.provider"
private var dbHelper: MyDatabaseHelper? = null
private val uriMatcher by lazy {
val mathcer = UriMatcher(UriMatcher.NO_MATCH)
mathcer.addURI(authority, TABLE_BOOK, bookDir)
mathcer.addURI(authority, "$TABLE_BOOK/#", bookItem)
mathcer
}
override fun insert(uri: Uri, values: ContentValues?): Uri? = dbHelper?.let {
val db = it.readableDatabase
when (uriMatcher.match(uri)) {
bookDir, bookItem -> {
val newBookId = db.insert(TABLE_BOOK, null, values)
Uri.parse("content://$authority/$TABLE_BOOK/$newBookId")
}
else -> null
}
}
override fun query(
uri: Uri,
projection: Array<String>?,
selection: String?,
selectionArgs: Array<String>?,
sortOrder: String?
): Cursor? = dbHelper?.let {
val db = it.readableDatabase
when (uriMatcher.match(uri)) {
bookItem -> {
//uri.getPathSegments()它会将URI权限之后的部分以 / 进行分割,将分割后的结果方法集合中,那这个集合中第0个位置存放的就是path,第一个位置存放的就是id。
val queryId = uri.pathSegments[1]
db.query(TABLE_BOOK, projection, "id=?", arrayOf(queryId), null, null, sortOrder)
}
bookDir -> {
db.query(TABLE_BOOK, projection, selection, selectionArgs, null, null, sortOrder)
}
else -> null
}
}
override fun onCreate(): Boolean = context?.let {
dbHelper = MyDatabaseHelper(it, DB_NAME, version = 1)
true
} ?: false
override fun update(uri: Uri, values: ContentValues?, selection: String?, selectionArgs: Array<String>?) =
dbHelper?.let {
val db = it.readableDatabase
when (uriMatcher.match(uri)) {
bookDir -> db.update(TABLE_BOOK, values, selection, selectionArgs)
bookItem -> {
val updateId = uri.pathSegments[1]
db.update(TABLE_BOOK, values, "id=?", arrayOf(updateId))
}
else -> 0
}
} ?: 0
override fun delete(uri: Uri, selection: String?, selectionArgs: Array<String>?): Int = dbHelper?.let {
val db = it.readableDatabase
when (uriMatcher.match(uri)) {
bookItem -> {
val deleteId = uri.pathSegments[1]
db.delete(TABLE_BOOK, "id=?", arrayOf(deleteId))
}
bookDir -> db.delete(TABLE_BOOK, selection, selectionArgs)
else -> 0
}
} ?: 0
override fun getType(uri: Uri): String? = when (uriMatcher.match(uri)) {
bookDir -> "vnd.android.cursor.dir/vnd.$authority.$TABLE_BOOK"
bookItem -> "vnd.android.cursor.item/vnd.$authority.$TABLE_BOOK"
else -> null
}
}
代码很长,不过并不难理解,一开始定义了两个变量bookDir
和bookItem
用于区分访问Book表的全部数据和单条数据。然后在一个by lazy
代码块中对UriMatcher
进行初始化操作,并将期望匹配的几种URI添加到UriMatcher中。by lazy是Kotlin中提供的一种懒加载技术,代码块中的代码一开始并不会执行,只有当uriMatcher首次被调用时才会执行,并且将代码块中最后一行代码的返回值赋值给uriMatcher。
接下来就是每个抽象方法的实现了,逻辑比较简单这里就不再一一细说了。现在我们的程序就拥有了跨程序共享数据的功能了,下面看下如何跨程序操作这些数据。新建一个项目,在新项目中执行如下代码。
class MainActivity : AppCompatActivity() ,View.OnClickListener{
private lateinit var mContentResolver: ContentResolver
private val authority = "com.example.abu.listview.databaseproviderdemo.provider"
private val tag = javaClass.simpleName
private val TABLE_BOOK = "Book"
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
setContentView(R.layout.activity_main)
mContentResolver = contentResolver
insert_data.setOnClickListener(this)
delete_data.setOnClickListener(this)
update_data.setOnClickListener(this)
query_data.setOnClickListener(this)
}
override fun onClick(v: View?) {
when (v?.id) {
R.id.insert_data -> {
val uri = mContentResolver.insert(
Uri.parse("content://$authority/$TABLE_BOOK/#"),
createValues("name" to "西游记", "pages" to 100000, "author" to "施耐庵")
)
Log.e(tag, "插入的新数据的id:${uri!!.pathSegments[1]}")
}
R.id.delete_data -> {
val deleteLine = mContentResolver.delete(Uri.parse("content://$authority/$TABLE_BOOK/3"), null, null)
Log.e(tag, "删除了几行数据:$deleteLine")
}
R.id.update_data -> {
val updateLine = mContentResolver.update(
Uri.parse("content://$authority/$TABLE_BOOK/4"),
createValues("name" to "三国演义", "pages" to 200000, "author" to "罗贯中"),
null,
null
)
Log.e(tag, "更新了几行数据:$updateLine")
}
R.id.query_data -> {
val cursor =
mContentResolver.query(Uri.parse("content://$authority/$TABLE_BOOK"), null, null, null, null)
cursor?.apply {
while (moveToNext()) {
val name = getString(getColumnIndex("name"))
val pages = getInt(getColumnIndex("pages"))
val author = getString(getColumnIndex("author"))
Log.e(tag, "查询:name:$name,pages:$pages,author:$author")
}
close()
}
}
}
}
}
功能很简单就是使用ContentProvider对数据进行增删改查。到此为止ContentProvider相关的内容基本就介绍完毕了。
网友评论