提示:此文章仅作为本人记录日常学习使用,若有存在错误或者不严谨得地方欢迎指正。
文章目录
一、运行时权限
ContentProvider主要用于在不同的应用程序之间实现数据共享的功能。它允许一个程序访问另一个程序中的数据,同时还可以保证被访问数据的安全性。不同于文件存储和SharedPreferences存储中全局可读写的模式,ContentProvider可以只对某一部分的数据进行共享,从而保证我们程序中的隐私数据不会被泄露。在开始学习ContentProvider之前我们需要先学习另一个非常重要的知识——Android运行时权限。
1.1 Android运行时权限机制
那什么是“Android运行时权限机制”呢?在Android 6.0之前,在安装App时会要求用户授权程序所需要的全部权限。用户若想使用该App,就必须在安装时同意授权App所要求的全部权限,否则就无法安装该App。为了改变这种情况,从Android 6.0开始,用户不需要在安装软件时一次性授权所有权限,而是可以在应用需要使用到某项权限时再进行申请。就算用户拒绝了App的某个权限,也可以正常使用该App的其他功能。
1.2 普通权限与危险权限
然而,并不是所有的权限都需要在运行时申请,对于用户来说不停的授权也是一件很繁琐的事情。Android将权限大旨归为两类:普通权限和危险权限。普通权限可以直接在AndroidManifest.xml文件中申请即可,而涉及到用户利益的危险权限则需要在App运行时进行申请,这主要是为了更好地保护用户的隐私和数据安全。
Android系统中的危险权限非常多,下图是Android10系统所有的危险权限:
原则上,用户一旦同意了某个权限申请之后,同组的其他权限也会被系统自动授权。
1.3 在程序运行时申请权限
新建一个RuntimePermissionTest项目,我们来实现点击按钮自动拨打电话的功能,这里涉及的危险权限是CALL_PHONE。
CALL_PHONE权限允许应用程序进行拨打电话操作,这种危险权限主要通过动态申请的方式获取,即在程序运行时向用户申请该权限。
在Android系统中,Intent.ACTION_CALL和Intent.ACTION_DIAL是两种不同的操作。前者允许应用程序直接拨打电话,这需要应用程序在清单文件中声明CALL_PHONE权限。而后者则只是打开拨号盘并自动填充电话号码,并不会实际拨打出去,因此并不需要声明权限。
以下内容选自第七篇文章——如何在我们的程序中调用系统拨号盘:
button1.setOnClickListener {
val intent = Intent(Intent.ACTION_DIAL)
intent.data = Uri.parse("tel:10086")
startActivity(intent)
}
在点击Button1按钮时就会调用系统默认的拨号盘并自动填入10086号码。
为了实现自动拨打电话的功能,我们先修改activity_main.xml文件,在界面中添加一个Button:
<?xml version="1.0" encoding="utf-8"?>
<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:tools="http://schemas.android.com/tools"
android:layout_width="match_parent"
android:layout_height="match_parent"
android:gravity="center"
android:orientation="vertical"
tools:context=".MainActivity">
<Button
android:id="@+id/makeCallButton"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:text="Make Call" />
</LinearLayout>
在主界面中我们添加了一个按钮,当用户点击按钮就触发拨打电话的逻辑。接下来我们为按钮添加点击监听器的逻辑,修改MainActivity.kt文件:
class MainActivity : AppCompatActivity() {
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
setContentView(R.layout.activity_main)
//按钮点击逻辑
makeCallButton.setOnClickListener {
try {
//创建拨打电话的intent 并添加要拨打的号码
val intent = Intent(Intent.ACTION_CALL)
intent.data = Uri.parse("tel:10086")
startActivity(intent)
} catch (e: SecurityException) {
e.printStackTrace()
Toast.makeText(this, "SecurityException!", Toast.LENGTH_SHORT).show()
}
}
}
}
我们在按钮的点击事件中创建了一个隐式Intent,并将其action指定为Intent.ACTION_CALL。这个action是一个系统内置的打电话的动作,然后在data部分指定了协议是tel,号码是10086。由于拨打电话操作是需要申请权限的,为了防止程序崩溃,我们将相关操作放到了try-catch异常捕获结构中。
最后一步,我们需要在清单文件中声明CALL_PHONE权限:
<uses-permission android:name="android.permission.CALL_PHONE" />
这样,拨打电话的功能就实现了。在低于Android6.0系统的手机上都是可以正常运行的,但是在Android6.0或者更高版本系统的手机上运行,点击MakeCall按钮就没有任何效果了,并且会打印很多错误信息。错误信息如下图所示:
错误原因也很简单,是由于我们拨打电话的权限被禁止所导致的。为了修复这个问题,我们修改MainActivity.kt中的代码:
class MainActivity : AppCompatActivity() {
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
setContentView(R.layout.activity_main)
//用户是否已给予CALL_PHONE权限
var hasCallPermission = ContextCompat.checkSelfPermission(this, Manifest.permission.CALL_PHONE) == PackageManager.PERMISSION_GRANTED
makeCallButton.setOnClickListener {
if (hasCallPermission) {
//若已经授权,则直接拨打电话
makeCall()
} else {
//若未授权,则调用requestPermissions()弹出权限申请框 requestCode用于识别授权结果的数据来源
ActivityCompat.requestPermissions(this, arrayOf(Manifest.permission.CALL_PHONE), 1)
}
}
}
//权限申请框处理结果(不论用户同意授权还是拒绝授权)回调
override fun onRequestPermissionsResult(requestCode: Int, permissions: Array<out String>, grantResults: IntArray) {
super.onRequestPermissionsResult(requestCode, permissions, grantResults)
//若授权结果来自于CALL_PHONE
when (requestCode) {
1 -> {
//若用户同意授权CALL_PHONE则直接拨打电话
if (grantResults.isNotEmpty() && grantResults[0] == PackageManager.PERMISSION_GRANTED) {
makeCall()
} else {
Toast.makeText(this, "你拒绝了授权!", Toast.LENGTH_SHORT).show()
}
}
}
}
//拨打10086
private fun makeCall() {
try {
//隐式intent action为Intent.ACTION_CALL 协议为tel 号码为10086
val intent = Intent(Intent.ACTION_CALL)
intent.data = Uri.parse("tel:10086")
startActivity(intent)
} catch (e: SecurityException) {
e.printStackTrace()
Toast.makeText(this, "未授予CALL_PHONE权限,无法拨打电话!", Toast.LENGTH_SHORT).show()
}
}
}
第一步:通过ContextCompat.checkSelfPermission( )方法判断用户是否已经给我们授权过CALL_PHONE权限。ContextCompat.checkSelfPermission( )要求传入两个参数:Context和要检查的权限名。如果checkSelfPermission( )方法的返回值等于PackageManager.PERMISSION_GRANTED,说明应用已经获得了该权限。否则,说明应用没有获得该权限。若用户已经授权则直接执行拨打电话的操作,否则就调用ActivityCompat.requestPermissions( )方法向用户弹出权限申请框。
第二步:重写onRequestPermissionsResult( )方法。当调用requestPermissions( )弹出权限申请框后,不论用户点击同意授权还是拒绝授权,最终都会回调onRequestPermissionsResult( )方法,它接收三个参数:
- requestCode:表示请求的权限代码,用于区分不同的权限请求。
- permissions:一个字符串数组,包含了请求的权限列表。
- grantResults:一个Int数组,表示每个权限请求的结果。用户同意户授权的值为PackageManager.PERMISSION_GRANTED,用户不同意授权则为其他值。
在onRequestPermissionsResult( )方法中我们先通过requestCode判断权限请求结果的来源是否来自于CALL_PHONE。然后通过grantResults数组的值判断用户是否同意授权。若同意授权则进行拨打电话的操作,否则就弹出一个Toast来提示用户。
检查grantResults数组是否非空是为了确保在调用grantResults[0]之前,数组中至少有一个元素。如果数组为空,则直接访问grantResults[0]会导致数组越界异常进而导致程序崩溃。
第三步:实现makeCall( )拨打电话逻辑。
现在我们运行以下程序,点击MAKE CALL按钮,会弹出来一个运行时权限申请框:
若我们点击不允许:
若我们点击允许,则直接拨打10086:
2024年2月7日更新
1.4 如何打开应用设置详情界面
在前面的内容中我们学习了如何弹出权限申请框来向用户请求运行时权限,并对用户的授权结果进行处理。Android系统规定:当用户连续两次拒绝某个运行时权限的申请时,就默认用户拒绝授予次权限,后续将不会再弹出权限申请框来向用户请求该运行时权限。在实际工作中可能会遇到用户误操作的情况下连续两次拒绝授权运行时权限的情况,这个时候我们就需要提示用户手动打开应用设置详情界面去完成授权操作。
class BaseActivity : AppCompatActivity() {
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
· · ·
//申请查找蓝牙设备权限
requestBlueTooth()
}
//向用户申请运行时权限
private fun requestBlueTooth() {
if (ContextCompat.checkSelfPermission(this, Manifest.permission.BLUETOOTH_CONNECT) != PackageManager.PERMISSION_GRANTED) {
ActivityCompat.requestPermissions(this, arrayOf(Manifest.permission.BLUETOOTH_CONNECT), 1)
}
}
//运行时权限授权结果处理回调
override fun onRequestPermissionsResult(requestCode: Int, permissions: Array<out String>, grantResults: IntArray) {
when (requestCode) {
//若授权返回结果来自于requestBlueTooth()
1 -> {
if (grantResults.isEmpty() || grantResults[0] != PackageManager.PERMISSION_GRANTED) {
super.onRequestPermissionsResult(requestCode, permissions, grantResults)
//弹出提示框提示用户
val mDialog = AlertDialog.Builder(this)
.setTitle("警告")
.setMessage("此应用程序无法在没有授权所需权限的情况下工作,请在设置中授予权限。")
.setPositiveButton("前往设置") { _, _ ->
//用于打开应用详情界面intent的action和data
val intent = Intent(Settings.ACTION_APPLICATION_DETAILS_SETTINGS)
val uri = Uri.fromParts("package", packageName, null)
intent.data = uri
finish()
//跳转到应用详情界面
startActivity(intent)
}
.setNegativeButton("退出当前应用") { _, _ ->
finish()
}
.create()
mDialog.show()
}
}
}
}
}
二、使用ContentProvider访问其他程序中的数据
上面我们了解并实际体验了Android运行时权限,下面就来学习ContentProvider相关的知识吧。ContentProvider一般有两种用法:
- ①使用现有的ContentProvider,访问其他程序中的数据
- ②创建自己的ContentProvider,给本程序中的数据提供外部访问接口。
如果一个应用程序通过ContentProvider对其数据提供了外部访问接口,那么任何其他的应用程序都可以对这部分数据进行访问。Android系统中自带的通讯录、短信、媒体库等程序都提供了类似的访问接口,这就使得第三方应用程序可以通过ContentProvider访问并利用这些数据进行功能开发。
2.1 ContentProvider的基本用法
对于一个App来说,如果想要访问ContentProvider中共享的数据,就一定要借助ContentResolver类,通过Context的getContentResolver( ) 方法获取该类的实例。ContentResolver中提供了一系列方法对数据进行增删改查操作:
- insert( ):添加数据。
- update( ):更新已存在的数据。
- delete( ):删除数据。
- query( ):查询数据。query( )返回一个Cursor对象,我们可以对这个Cursor对象进行遍历以获取查询结果。
需要注意的是,不同于SQLiteDatabase,ContentResolver中的这些增删改查方法都是不接收表名参数,而是使用一个Uri参数来代替,这个Uri参数被称为“内容Uri”。
内容Uri为ContentProvider中的数据提供了一个唯一标识符,它主要由两部分组成:Authority和Path。
- Authority字段用于区分不同的应用程序,通常采用程序包名.provider的方式进行命名以避免冲突。
- Path字段则用于对同一个应用程序中不同的表进行区分,通常添加在Authority字段后面。
例如某个App的包名是“com.example.app”,该App的数据库中存在table1和table2两张表。那么Authority字段就是“com.example.app.provider”,Path字段就是是“/table1”和“/table2”。将Authority和Path字段进行组合,内容URI就变成了“com.example.app.provider/table1”和“com.example.app.provider/table2”。最后在字符串的的头部加上协议声明就完成了。以下是内容URI的标准格式:
"authority" "path"
content://com.example.app.provider/table1
content://com.example.app.provider/table2
可以看到,内容Uri可以很清楚的表达我们想要访问哪个应用程序中哪张表的数据。在得到内容Uri字符串后,我们还需要将其解析成Uri对象才可以作为参数传入。下面是将内容Uri解析成Uri对象的代码:
val uriObject = Uri.parse("content://com.example.app.provider/table1")
在这个例子中,字符串"content://com.example.app.provider/table1"表示了一个内容URI,它指向了名为"table1"的表,该表位于名为"com.example.app"的应用中。通过调用Uri.parse()方法并传入这个字符串作为参数,可以将其转换为一个Uri对象。
2.1.1 通过query( )方法查询数据
接下来我们通过ContentResolver的query( )方法查询table1表中的数据,代码如下:
val cursor = contentResolver.query(uriObject, projection, selection, selectionArgs, sortOrder)
它的参数说明如下:
query( )方法返回一个Cursor对象,我们可以从Cursor对象中将数据逐个读取出来。读取的思路依然是通过移动游标位置遍历Cursor的所有行,然后取出每一行中相应列的数据,代码如下:
//若下一行还有数据
while(cursor.moveToNext()) {
val column1 = cursor.getString(cursor.getColumnIndex("column1"))
val column2 = cusor.getInt(cursor.getColumnIndex("column2"))
}
cursor.close()
我们通过cursor.moveToNext()方法来判断下一行是否还有数据。如果下一行还有数据则继续执行while循环体内的代码,如果没有数据则跳出循环并关闭Cursor对象。这种方式类似于我们在第18篇文章中通过query()从SQLite数据库中查询数据的操作,下面这段代码取自《【18】应用开发——数据存储与持久化技术》的4.5小节:
class MainActivity : AppCompatActivity() {
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
setContentView(R.layout.activity_main)
//创建MyDatabaseHelper对象 通过构造函数指定数据库名和版本号
val dataBaseHelper = MyDatabaseHelper(this, "BookStore", 2)
· · ·
/*查询数据按钮点击*/
queryDataButton.setOnClickListener {
//SQLiteDatabase对象
val database = dataBaseHelper.writableDatabase
//query()方法返回一个Cursor对象
val cursor = database.query("Book", null, null, null, null, null, null)
//将游标(Cursor)移动到查询结果的第一行
//通常我们会使用这个方法来判断查询是否有结果,如果查询没有结果,这个方法会返回false。
if (cursor.moveToFirst()) {
do {
// 遍历Cursor对象,取出数据并打印
val name = cursor.getString(cursor.getColumnIndex("name"))
val author = cursor.getString(cursor.getColumnIndex("author"))
val pages = cursor.getInt(cursor.getColumnIndex("pages"))
val price = cursor.getDouble(cursor.getColumnIndex("price"))
Log.d("MainActivityTAG", "Book name is $name")
Log.d("MainActivityTAG", "Book author is $author")
Log.d("MainActivityTAG", "Book pages is $pages")
Log.d("MainActivityTAG", "Book price is $price")
} while (cursor.moveToNext())//判断下一行是否还有数据
}
//关闭Cursor 释放系统资源 避免内存泄漏
cursor.close()
}
}
}
在使用完Cursor后将Cursor关闭是十分重要的。关闭Cursor对象的主要原因是释放系统资源,包括内存和数据库连接等。如果不及时释放,可能会导致系统资源浪费、内存泄漏等问题。
2.1.2 通过insert( )方法插入数据
查询操作是最难的,剩下的增加、删除、修改操作就比较容易了。接下来我们看一下如何通过insert( )方法向table1表中插入数据:
val contentValues = contentValuesOf("column1" to "text", "column2" to 1)
contentResolver.insert(uriObject, contentValues)
可以看到我们依旧是将待添加的数据组装到ContentValues中(参考第18篇文章),然后调用ContentResolver的insert( )方法,将Uri对象和ContentValues作为参数传入即可。
column1 | column2 |
---|---|
text | 1 |
2.1.3 通过update( )方法更新数据
如果我们想要更新这条新添加的数据,把column1的值修改为doc,可以借助ContentResolver的update( )方法实现:
val contentValues = contentValuesOf("column1" to "doc")
contentResolver.update(uriObject, contentValues, "column1 = ? and column2 = ?", arrayOf("text","1"))
可以看到我们对column1等于"text"、column2等于"1"的这条数据进行了修改,我们将其column1的值修改为doc。
column1 | column2 |
---|---|
doc | 1 |
2.1.4 通过delete( )方法删除数据
如果我们想要删除所有新添加的数据,可以通过ContentResolver的delete( )方法实现:
contentResolver.delete(uriObject, "column2 = ?", arrayOf("1"))
这段代码的作用是从指定的数据表中删除所有列名为"column2"且值为"1"的记录。
2.2 读取系统联系人
首先编写主界面,我们给主界面添加一个ListView列表用来显示系统通讯录中的联系人。修改activity_main.xml中的代码:
<?xml version="1.0" encoding="utf-8"?>
<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:tools="http://schemas.android.com/tools"
android:layout_width="match_parent"
android:layout_height="match_parent"
android:orientation="vertical"
tools:context=".MainActivity">
<ListView
android:id="@+id/contactsListView"
android:layout_width="match_parent"
android:layout_height="match_parent" />
</LinearLayout>
我们希望实现的功能是运行程序后在主界面会弹出一个授权提示框,当用户授权后会读取系统通讯录中的联系人信息,并将信息填充到ListView列表中。修改MainActivity.kt中的代码:
class MainActivity : AppCompatActivity() {
//通讯录数据列表
private val contactsList = ArrayList<String>()
//通讯录数据列表适配器
private lateinit var contactsAdapter: ArrayAdapter<String>
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
setContentView(R.layout.activity_main)
//ListView的数据适配器
contactsAdapter = ArrayAdapter(this, android.R.layout.simple_list_item_1, contactsList)
//将适配器设置到ListView空间上
contactsListView.adapter = contactsAdapter
//是否拥有访问系统通讯录的权限
var hasContactsPermission = ContextCompat.checkSelfPermission(this, Manifest.permission.READ_CONTACTS) == PackageManager.PERMISSION_GRANTED
if (hasContactsPermission) {
//若有访问权限则直接读取系统通讯录
readContacts()
} else {
//若没有权限则弹出请求授权对话框请求用户授权
ActivityCompat.requestPermissions(this, arrayOf(Manifest.permission.READ_CONTACTS), 1)
}
}
//权限申请框处理结果(不论用户同意授权还是拒绝授权)回调
override fun onRequestPermissionsResult(requestCode: Int, permissions: Array<out String>, grantResults: IntArray) {
super.onRequestPermissionsResult(requestCode, permissions, grantResults)
when (requestCode) {
//若授权结果来自于READ_CONTACTS
1 -> {
//如果用户同意授权
if (grantResults.isNotEmpty() && grantResults[0] == PackageManager.PERMISSION_GRANTED) {
readContacts()
} else {
Toast.makeText(this, "你拒绝了授权!", Toast.LENGTH_SHORT).show()
}
}
}
}
@SuppressLint("Range")
private fun readContacts() {
//查询通讯录中的联系人数据(query()方法返回的是一个Cursor对象)
contentResolver.query(ContactsContract.CommonDataKinds.Phone.CONTENT_URI,null,null,null,null)?.apply {
//通过cursor.moveToNext()方法判断下一行是否还有数据
while (moveToNext()){
//联系人姓名
val contacts_name = getString(getColumnIndex(ContactsContract.CommonDataKinds.Phone.DISPLAY_NAME))
//联系人电话
val contacts_tel = getString(getColumnIndex(ContactsContract.CommonDataKinds.Phone.NUMBER))
//将联系人姓名和电话添加到数据列表中
contactsList.add("$contacts_name\n$contacts_tel")
}
//通知ListView更新数据
contactsAdapter.notifyDataSetChanged()
//将Cursor对象关闭
close()
}
}
}
上面这段代码的ListView使用和运行时权限的实现都和之前的内容基本一致。需要注意的是readContacts()方法里,我们调用了contentResolver.query()查询方法。不同于上面学习的query()方法第一个参数是Uri对象,我们这里是传入了一个"ContactsContract.CommonDataKinds.Phone.CONTENT_URI"参数。这是因为ContactsContract.CommonDataKinds.Phone类已经帮我们做好了封装,提供了一个CONTENT_URI常量,这个常量就是使用Uri.parse()方法解析出来的结果。我们先将联系人的姓名和电话保存在变量中,并将其进行拼接,然后将拼接后的结果添加到数据列表中。最后通知ListView刷新数据并关闭Cursor对象。
当更改了ListView的数据后,需要通知它的Adapter去更新数据。当你添加、删除或修改ListView的数据时,你需要调用Adapter的notifyDataSetChanged()方法来通知ListView数据已经发生了变化。
最后别忘了添加权限声明,修改AndroidManifest.xml文件:
<uses-permission android:name="android.permission.READ_CONTACTS" />
加入了READ_CONTACTS权限,我们的程序就可以访问系统联系人数据了。现在运行程序看看吧!
点击允许后:
可以看到我们已经成功将系统通讯录中的联系人数据填充到主界面的ListView中了!
三、创建自己的ContentProvider
在前面的内容中,我们学习了如何在自己的程序中访问其他应用程序的数据。大致流程就是先获得该应用程序的内容URI,然后借助ContentResolver进行增删改查操作就可以了。下面我们就来学习一下如何将我们程序中的数据共享给其他应用程序。
3.1 创建ContentProvider的步骤
如果我们想要创建一个自己的ContentProvider,可以通过"创建一个新的类并让其继承自ContentProvider"的方式来实现。ContentProvider有6个抽象方法,我们在使用子类继承ContentProvider类后需要将这6个方法全部重写。下面是一个代码示例:
class MyContentProvider : ContentProvider() {
/**
* 初始化ContentProvider时调用
* 通常会在这里完成对数据库的创建和升级等操作,返回true表示ContentProvider初始化成功,反之亦然。
*/
override fun onCreate(): Boolean {
TODO("Not yet implemented")
}
/**
* 从ContentProvider中查询数据(查询结果会放在一个Cursor对象中)
* uri:查询哪张表的数据
* projection:确定查询哪些列
* selection、selectionArgs:约束查询哪些行
* sortOrder:对查询结果进行排序
*/
override fun query(
uri: Uri,
projection: Array<out String>?,
selection: String?,
selectionArgs: Array<out String>?,
sortOrder: String?
): Cursor? {
TODO("Not yet implemented")
}
/**
* 向ContentProvider中添加一条数据(添加完成后,会返回一个用于表示这条新记录的Uri对象)
* uri:将数据添加到哪张表
* values:待添加的数据
*/
override fun insert(uri: Uri, values: ContentValues?): Uri? {
TODO("Not yet implemented")
}
/**
* 更新ContentProvider中已有的数据(被更新的行数将作为返回值返回)
* uri:更新哪张表的数据
* values:新数据
* selection、selectionArgs:约束更新哪些行
*/
override fun update(
uri: Uri,
values: ContentValues?,
selection: String?,
selectionArgs: Array<out String>?
): Int {
TODO("Not yet implemented")
}
/**
* 从ContentProvider中删除数据(被删除的行数将作为返回值返回)
* uri:删除哪张表的数据
* selection、selectionArgs:约束删除哪些行
*/
override fun delete(uri: Uri, selection: String?, selectionArgs: Array<out String>?): Int {
TODO("Not yet implemented")
}
/**
* 根据传入的"内容Uri"返回相应的MIME类型(通常用于指定返回的数据格式)
*/
override fun getType(uri: Uri): String? {
TODO("Not yet implemented")
}
}
可以看到,我们重写的这6个抽象方法中有5个抽象方法都要求传入一个Uri类型的参数。这个Uri类型的参数正是调用ContentResolver的增删改查方法时传入过来的。下面是查询方法的示例:
⬇
val cursor = contentResolver.query(uriObject, projection, selection, selectionArgs, sortOrder)
而我们现在要做的就是对传入进来的Uri类型参数进行解析,从中分析出对方程序想要访问我们应用程序中哪张表的什么数据。
我们之前提到过,标准的"内容URI"写法是:
content://com.example.app.provider/table1
这样就表示对方想要访问的是包名为com.example.app这个应用程序的table1表中的数据。除此以外,我们还可以在"内容URI"后面加上一个id:
content://com.example.app.provider/table1/1
这样就表示我们想要访问com.example.app这个应用程序table1表中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的math()方法时,可以传入一个Uri对象,并返回能够匹配这个Uri对象所对应的自定义代码。通过这个自定义代码我们就能够判断出调用方希望访问的是哪张表中的数据。修改MyContentProvider中的代码:
class MyContentProvider : ContentProvider() {
//访问table1表中所有数据
private val table1_all = 0
//访问table1表中任意一条数据
private val table1_single = 1
private val table2_all = 2
private val table2_single = 3
//初始模式设置为NO_MATCH 表示没有任何匹配的模式
private val uriMatcher = UriMatcher(UriMatcher.NO_MATCH)
init {
//内容Uri:table1表中的所有数据
uriMatcher.addURI("com.example.app.provider", "table1", table1_all)
//内容Uri:table1表中任意一行数据
uriMatcher.addURI("com.example.app.provider", "table1/#", table1_single)
uriMatcher.addURI("com.example.app.provider", "table2", table2_all)
uriMatcher.addURI("com.example.app.provider", "table2/#", table2_single)
}
· · ·
override fun query(
uri: Uri,
projection: Array<out String>?,
selection: String?,
selectionArgs: Array<out String>?,
sortOrder: String?
): Cursor? {
when (uriMatcher.match(uri)) {
table1_all -> {
//查询table1表中的所有数据
}
table1_single -> {
//查询table1表中任意一行的数据
}
table2_all -> {
//查询table2表中的所有数据
}
table2_single -> {
//查询table2表中的任意一行的数据
}
}
· · ·
}
· · ·
}
可以看到,我们给MyContentProvider增加了四个变量用来表示对不同表的不同访问请求。接着我们在MyContentProvider对象初始化的时候创建了UriMatcher实例,并调用addURI( )方法,将期望匹配的内容URI格式传递进去。当query()方法被调用时,UriMathcer会调用match()方法对Uri对象进行匹配。若发现UriMathcer中某个内容URI格式成功匹配了该Uri对象,则会返回相应的自定义代码,然后我们就可以判断出调用方希望访问的到底是什么数据了。这里我们只用了query()方法进行演示,其实insert()、update()、delete()这几个方法都是可以通过类似方法进行实现。因为他们都需要传入一个Uri对象,我们通过UriMatcher判断出调用方希望访问哪张表,然后再对这张表中的数据进行相应操作就可以了。
你可能会有一个疑问,因为MyContentProvider中我们重写了很多方法,除了常见的增删改查方法和onCreate()方法,还多了一个没见过的getType()方法。这个getType()方法是所有ContentProvider都必须要提供的一个方法,用来获取Uri对象所对应的MIME类型。
一个内容Uri所对应的MIME字符串主要由三部分组成:
①必须以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
现在我们来实现MyContentProvider中的getType()方法:
/**
* 根据传入的"内容Uri"返回相应的MIME类型(通常用于指定返回的数据格式)
*/
override fun getType(uri: Uri) = when (uriMatcher.match(uri)) {
table1_all -> "vnd.android.cursor.dir/vnd.com.example.app.provider.table1"
table1_single -> "vnd.android.cursor.item/vnd.com.example.app.provider.table1"
table2_all -> "vnd.android.cursor.dir/vnd.com.example.app.provider.table2"
table2_single -> "vnd.android.cursor.item/vnd.com.example.app.provider.table2"
else -> null
}
到这里,一个完整的自定义ContentProvider就已经创建完毕了。现在,任意一个应用程序都可以通过ContentProvider来访问我们程序中的数据。接下来就实战体验一下跨程序数据共享吧!
3.2 实现跨程序数据共享
简单起见,我们还是在之前的DatabaseTest项目的基础上进行二次开发,通过ContentProvider来给它加入外部访问接口。这次我们让Android Studio自动帮我们创建ContentProvider。右键"包名" —> New —> Other —> Content Provider —> 输入Authority —> 勾选Exported和Enable —> 点击Finish
- Exported属性表示是否允许外部应用程序访问我们的Content Provider
- Enable属性表示是否启用这个Content Provider。
接着我们修改新创建的DatabaseProvider.kt:
class DatabaseProvider : ContentProvider() {
private val book_dir = 0
private val book_item = 1
private val category_dir = 2
private val category_item = 3
//内容Uri的authority
private val authority = "com.example.databasetest.provider"
//MyDatabaseHelper对象
private var databaseHelper: MyDatabaseHelper? = null
private val uriMatcher by lazy {
val matcher = UriMatcher(UriMatcher.NO_MATCH)
matcher.addURI(authority, "book", book_dir)
matcher.addURI(authority, "book/#", book_item)
matcher.addURI(authority, "category", category_dir)
matcher.addURI(authority, "category/#", category_item)
matcher
}
//若onCreate()成功返回true,否则返回false
override fun onCreate() = context?.let {
//创建数据库
databaseHelper = MyDatabaseHelper(it, "BookStore.db", 2)
true
} ?: false
/**
* 向ContentProvider中添加一条数据(添加完成后,会返回一个用于表示这条新记录的Uri对象)
* uri:将数据添加到哪张表
* values:待添加的数据
*/
override fun insert(uri: Uri, values: ContentValues?): Uri? {
val database = databaseHelper?.writableDatabase
val uriReturn = when (uriMatcher.match(uri)) {
book_dir, book_item -> {
val newBookId = database?.insert("Book", null, values)
Uri.parse("content://${authority}/book/${newBookId}")
}
category_dir, category_item -> {
val newCategoryId = database?.insert("Category", null, values)
Uri.parse("content://${authority}/category/${newCategoryId}")
}
else -> null
}
return uriReturn
}
/**
* 从ContentProvider中删除数据(被删除的行数将作为返回值返回)
* uri:删除哪张表的数据
* selection、selectionArgs:约束删除哪些行
*/
override fun delete(uri: Uri, selection: String?, selectionArgs: Array<String>?): Int {
val database = databaseHelper?.writableDatabase
val deleteRows = when (uriMatcher.match(uri)) {
book_dir -> database?.delete("Book", selection, selectionArgs)
book_item -> {
val bookId = uri.pathSegments[1]
database?.delete("Book", "id=?", arrayOf(bookId))
}
category_dir -> database?.delete("Category", selection, selectionArgs)
category_item -> {
val categoryId = uri.pathSegments[1]
database?.delete("Category", "id=?", arrayOf(categoryId))
}
else -> 0
}
return deleteRows ?: 0
}
/**
* 更新ContentProvider中已有的数据(被更新的行数将作为返回值返回)
* uri:更新哪张表的数据
* values:新数据
* selection、selectionArgs:约束更新哪些行
*/
override fun update(uri: Uri, values: ContentValues?, selection: String?, selectionArgs: Array<String>?): Int {
val database = databaseHelper?.writableDatabase
val updatedRows = when (uriMatcher.match(uri)) {
book_dir -> database?.update("Book", values, selection, selectionArgs)
book_item -> {
val bookId = uri.pathSegments[1]
database?.update("Book", values, "id=?", arrayOf(bookId))
}
category_dir -> database?.update("Category", values, selection, selectionArgs)
category_item -> {
val categoryId = uri.pathSegments[1]
database?.update("Category", values, "id = ?", arrayOf(categoryId))
}
else -> 0
}
return updatedRows ?: 0
}
/**
* 从ContentProvider中查询数据(查询结果会放在一个Cursor对象中)
* uri:查询哪张表的数据
* projection:确定查询哪些列
* selection、selectionArgs:约束查询哪些行
* sortOrder:对查询结果进行排序
*/
override fun query(uri: Uri, projection: Array<String>?, selection: String?, selectionArgs: Array<String>?, sortOrder: String?): Cursor? {
//查询数据只需要读取数据库 所以通过只读方式进行创建
val database = databaseHelper?.readableDatabase
//query()方法要返回一个Cursor对象用来储存结果
val cursor = when (uriMatcher.match(uri)) {
book_dir -> {
database?.query("Book", projection, selection, selectionArgs, null, null, sortOrder)
}
book_item -> {
val bookId = uri.pathSegments[1]
database?.query("Book", projection, "id=?", arrayOf(bookId), null, null, sortOrder)
}
category_dir -> {
database?.query("Category", projection, selection, selectionArgs, null, null, sortOrder)
}
category_item -> {
val categoryId = uri.pathSegments[1]
database?.query("Category", projection, "id =?", arrayOf(categoryId), null, null, sortOrder)
}
else -> null
}
return cursor
}
/**
* 根据传入的"内容Uri"返回相应的MIME类型(通常用于指定返回的数据格式)
*/
override fun getType(uri: Uri): String? {
return when (uriMatcher.match(uri)) {
book_dir -> "vnd.android.cursor.dir/vnd.com.example.databasetest.provider.book"
book_item -> "vnd.android.cursor.item/vnd.com.example.databasetest.provider.book"
category_dir -> "vnd.android.cursor.dir/vnd.com.example.databasetest.provider.category"
category_item -> "vnd.android.cursor.item/vnd.com.example.databasetest.provider.category"
else -> null
}
}
}
代码有一点多,我们来一点一点学习:
『by lazy懒加载技术』:
by lazy是Kotlin提供的一种懒加载技术。by lazy代码块中的代码一开始并不会执行,而是在uriMatcher变量第一次被调用时才会执行。并且代码块中最后一行代码会做为返回值赋给uriMatcher, 这样设计可以节省系统资源。
private val uriMatcher by lazy {
没有任何匹配的模式
val matcher = UriMatcher(UriMatcher.NO_MATCH)
matcher.addURI(authority, "table", book_dir)
matcher.addURI(authority, "book/#", book_item)
matcher.addURI(authority, "category", category_dir)
matcher.addURI(authority, "category/#", category_item)
//返回uriMatcher对象
matcher
}
by lazy和lateinit是Kotlin中两种不同的延迟初始化的实现方式,lateinit只能用于变量var,而by lazy只能用于常量val。lateinit只是让编译器忽略对属性未初始化的检查,后续在哪里以及何时初始化还需要开发者自己决定。而by lazy真正做到了声明的同时也指定了延迟初始化时的行为,在属性被第一次被使用的时候能自动初始化。
『by lazy懒加载技术的作用』:
懒加载的目的是为了避免不必要的计算和资源消耗。在许多情况下,我们可能不需要立即使用某个变量的值,而是希望它在需要的时候才被计算出来——这就是所谓的"懒加载"。在这个例子中,uriMatcher的值只有在第一次访问时才会被计算出来,并且只会被计算一次。之后的访问将直接返回已经计算好的结果,而不会再次进行计算。这样可以节省计算资源,提高程序的性能。
『writableDatabase和readableDatabase』:
- SQLiteOpenHelper.writableDatabase:用于获取一个可读写的数据库实例。它允许你对数据库执行写操作,如INSERT、UPDATE和DELETE等操作。
- SQLiteOpenHelper.readableDatabase:用于获取一个只读的数据库实例。它只允许你对数据库执行查询操作(如SELECT),而不允许执行写操作。
『Uri.getPathSegments()方法』:
Uri.getPathSegments()方法会将"内容Uri"中Authority之后的部分以"/"符号进行分割,并把分割后的结果放入一个字符串列表中。这个字符串列表的第0位存放的是内容Uri的"path",字符串列表的第1位存放的是内容Uri的"行id"。
下面是一段getPathSegments()方法的示例代码:
val uri = Uri.parse("content://com.example.app.provider/table1/2")
val pathSegmentsList = uri.getPathSegments()
println(pathSegmentsList)
这条内容Uri被getPathSegments()方法解析后的字符串列表是:
第0位 第1位
["table1", "2"]
字符串列表的第一个元素pathSegmentsList[0]为表名,第二个元素pathSegmentsList[1]是行id。
- insert()方法用于向ContentProvider中插入一条数据。由于需要对数据库进行编辑操作,所以我们是通过writableDatabase方法来获取数据库的。uriReturn用于表示新插入的这条记录的Ur。我们通过uriMatcher的match()方法对insert()方法的uri参数进行匹配。如果另一个应用程序期望向Book表中插入数据,我们就将数据库insert()方法执行成功后的返回值保存在newBookId中,用来表示新插入的这条数据的行id。最后通过Uri.parse()方法将内容Uri字符串转换为一个Uri对象,并将其返回。
override fun insert(uri: Uri, values: ContentValues?): Uri? {
val database = databaseHelper?.writableDatabase
val uriReturn = when (uriMatcher.match(uri)) {
book_dir, book_item -> {
val newBookId = database?.insert("Book", null, values)
Uri.parse("content://${authority}/book/${newBookId}")
}
category_dir, category_item -> {
val newCategoryId = database?.insert("Category", null, values)
Uri.parse("content://${authority}/category/${newCategoryId}")
}
else -> null
}
//返回新添加的数据的Uri对象
return uriReturn
}
- delete()方法中我们需要对表和表中的单条数据进行判断。若其他应用期望访问的是表中某条具体的数据,我们需要将内容Uri字符串中期望访问的行id解析出来。我们通过调用Uri.getPathSegments()方法将内容Uri中"Authority"之后的部分以"/"符号进行分割,并把分割后的结果放入一个字符串列表中。这个列表的第0位存放的就是"Path",列表的第1位存放的就是id了。所以,我们通过uri.pathSegments[1]就可以轻松获得内容Uri的id了。
override fun delete(uri: Uri, selection: String?, selectionArgs: Array<String>?): Int {
val database = databaseHelper?.writableDatabase
val deleteRows = when (uriMatcher.match(uri)) {
book_dir -> database?.delete("Book", selection, selectionArgs)
book_item -> {
//获得内容Uri的行id
val bookId = uri.pathSegments[1]
database?.delete("Book", "id=?", arrayOf(bookId))
}
category_dir -> database?.delete("Category", selection, selectionArgs)
category_item -> {
val categoryId = uri.pathSegments[1]
database?.delete("Category", "id=?", arrayOf(categoryId))
}
else -> 0
}
return deleteRows ?: 0
}
需要注意的是,ContentProvider必须要在AndroidManifest.xml清单文件中注册才可以使用。
Android的四大组件——Activity、Service、BroadcastReceiver和ContentProvider都需要在AndroidManifest.xml文件中注册才能使用。
由于我们是通过Android Studio来创建ContentProvider的,因此无需手动注册。打开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">
· · ·
</activity>
</application>
</manifest>
可以看到,在< application >标签内出现了一个 < provider>标签,我们使用它对DatabaseProvider进行注册。 到这里,我们的DatabaseTest程序就已经拥有了跨程序共享数据的功能了!接下来我们创建一个新项目ProviderTest用来访问DatabaseTest程序中的数据。修改activity_main.xml中的代码:
<?xml version="1.0" encoding="utf-8"?>
<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
android:layout_width="match_parent"
android:layout_height="match_parent"
android:layout_margin="20dp"
android:orientation="vertical">
<Button
android:id="@+id/addDataButton"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:text="向Book表中添加数据" />
<Button
android:id="@+id/queryDataButton"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:text="从Book表中查询数据" />
<Button
android:id="@+id/updateDataButton"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:text="更新Book表中的数据" />
<Button
android:id="@+id/deleteDataButton"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:text="从Book表中删除数据" />
</LinearLayout>
接下来实现按钮的点击逻辑,修改MainActivity.kt中的代码:
class MainActivity : AppCompatActivity() {
//新插入的数据的Uri
var bookId: String? = null
@SuppressLint("Range")
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
setContentView(R.layout.activity_main)
//向Book表中添加数据按钮
addDataButton.setOnClickListener {
//将内容Uri转为Uri对象
val uri = Uri.parse("content://com.example.databasetest.provider/book")
//要添加的数据
val values = contentValuesOf(
"name" to "A Clash of King",
"author" to "George Martin",
"pages" to 1024,
"price" to 22.85
)
//新插入的数据的Uri
val newUri = contentResolver.insert(uri, values)
//获取newUri的行id
bookId = newUri?.pathSegments?.get(1)
}
//从Book表中查询数据按钮
queryDataButton.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("MainActivityTAG", "name is :${name},")
Log.d("MainActivityTAG", "author is :${author},")
Log.d("MainActivityTAG", "pages is :${pages},")
Log.d("MainActivityTAG", "price is :${price},")
}
//关闭Cursor对象
close()
}
}
//更新Book表中的数据按钮
updateDataButton.setOnClickListener {
bookId?.let {
//修改刚才新添加的数据
val uri = Uri.parse("content://com.example.databasetest.provider/book/${it}")
val values = contentValuesOf(
"name" to "A Storm of Swords",
"pages" to "1216",
"price" to 24.05
)
contentResolver.update(uri, values, null, null)
}
}
//从Book表中删除数据按钮
deleteDataButton.setOnClickListener {
bookId?.let {
//删除刚才新添加的数据
val uri = Uri.parse("content://com.example.databasetest.provider/book/${it}")
contentResolver.delete(uri, null, null)
}
}
}
}
如果你build.gradle(:app)文件中的项目编译版本和目标SDK版本>=SDK30,那么你还需要在AndroidManifest.xml清单文件中添加READ、WRITE权限和< queries>标签。
<?xml version="1.0" encoding="utf-8"?>
<manifest xmlns:android="http://schemas.android.com/apk/res/android"
package="com.example.providertest">
<!--获取provider的读写权限-->
<uses-permission android:name="DatabaseProvider._READ_PERMISSION" />
<uses-permission android:name="DatabaseProvider._WRITE_PERMISSION" />
<!--声明provider要访问的程序的包名-->
<queries>
<package android:name="com.example.databasetest" />
</queries>
<application>
· · ·
</application>
</manifest>
否则点击按钮会导致应用闪退并报如下错误:
java.lang.IllegalArgumentException: Unknown URL content://com.example.datebasetest.provider/book/
如果你的编译版本和目标SDK版本为29或者更小,则不用设置READ、WRITE权限和< queries>标签,因为这是自Android 11版本后新增加的特性。运行程序,点击"向Book表中添加数据"按钮后点击"从Book表中查询数据"按钮,可以看到Android Studio已经将我们刚才添加的新数据查询出来了。
我们点击"更新Book表中的数据"按钮然后再查询,可以看到刚才新增加册那条数据已经被更新了:
最后我们点击删除数据按钮后再查询,会发现查询不到任何数据。到这里,与ContentProvider相关的主要内容就已经学习完毕了!