ContentProvider 主要用于在不同的程序应用之间实现数据共享的功能,它提供了一套完整的机制,允许一个程序访问另一个程序中的数据,同时还能保证被访问数据的安全性。
它可以选择只对哪一部分数据进行共享,从而保证我们程序中的隐私数据不会有泄露的风险。
首先我们需要掌握一个非常重要的知识——Android运行时权限,因为会经常用到运行时权限的功能。
权限组名 | 权限名称 |
---|---|
CALENDAR(日历) | READ_CALENDAR |
WRITE_CALENDAR | |
CAMERA(相机) | CAMERA |
CONTACTS(联系人) | READ_CONTACTS |
WRITE_CONTACTS | |
GET_ACCOUNTS | |
LOCATION(位置) | ACCESS_FINE_LOCATION |
ACCESS_BACKGROUND_LOCATION ACCESS_COARSE_LOCATION | |
MICROPHONE(麦克风) | RECORD_AUDIO |
PHONE(手机) | READ_PHONE_STATE |
CALL_PHONE | |
ERAD_CALL_LOG | |
WRITE_CALL_LOG | |
ADD_VOICEMAIL | |
USE_SIP | |
PROCESS_OUTGOING_CALLS | |
SENSORS(传感器) | BODY_SENSORS |
SMS(短信) | SEND_SMS |
RECEIVE_SMS | |
READ_SMS | |
RECEIVE_WAP_PUSH | |
RECEIVE_MMS | |
STORAGE(存储卡) | READ_EXTERNAL_STORAGE |
WRITE_EXTERNAL_STORAGE | |
ACCESS_MEDIA_LOCATION | |
CALL_LOG | READ_CALL_LOG |
WRITE_CALL_LOG | |
PROCESS_OUTGOING_CALLS | |
ACITIVITY_RECOGNITION | ACITIVITY_RECOGNITION |
这张表格列出了Android10系统为止所有的危险权限,一共是11组30个权限。
新建一个RuntimePermissionTest项目。这里我们使用CALL_PHONE这个权限作为示例。
在其布局文件中定义一个makeCall按钮,点击按钮触发拨打电话的逻辑。在MAinActivity中的onCreate()方法中添加如下代码:
val makeCall:Button=findViewById(R.id.makeCall)
makeCall.setOnClickListener {
try {
val intent=Intent(Intent.ACTION_CALL)
intent.data= Uri.parse("tel:10086")
startActivity(intent)
}
catch (e:SecurityException){
e.printStackTrace()
}
}
}
修改AndroidManifest.xml文件,在其声明中加入权限:
<uses-permission android:name="android.permission.CALL_PHONE"/>
这时我们拨打电话的功能就成功实现了,并且在低于Android6.0系统的手机上都是可以运行的,但是在高版本手机上运行就会没有效果,并会在Logcat中显示错误。这是由于系统在使用危险权限时必须进行运行时权限处理。
接下来修复这个问题,修改MAinActivity的代码:
class MainActivity : AppCompatActivity() {
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
setContentView(R.layout.activity_main)
val makeCall: Button = findViewById(R.id.makeCall)
makeCall.setOnClickListener {
if (ContextCompat.checkSelfPermission(
this,
Manifest.permission.CALL_PHONE
) != PackageManager.PERMISSION_GRANTED
) {
ActivityCompat.requestPermissions(
this,
arrayOf(Manifest.permission.CALL_PHONE), 1
)
} else {
call()
}
}
}
override fun onRequestPermissionsResult(requestCode: Int,
permissions: Array<out String>, grantResults: IntArray) {
super.onRequestPermissionsResult(requestCode, permissions, grantResults)
when(requestCode){
1->{
if (grantResults.isNotEmpty()&&
grantResults[0]==PackageManager.PERMISSION_GRANTED){
call()
}else{
Toast.makeText(this,"you denied the permission",Toast.LENGTH_SHORT).show()
}
}
}
}
private fun call() {
try {
val intent = Intent(Intent.ACTION_CALL)
intent.data = Uri.parse("tel:10086")
startActivity(intent)
} catch (e: SecurityException) {
e.printStackTrace()
}
}
}
运行时权限的核心就是在程序运行时由用户授权我们去执行某些危险操作,程序是不可以擅自做主去执行这些危险操作的。因此第一步就是判断用户是否给我们授权了,如果已经授权直接拨打电话,没有的话则调用方法向用户申请授权。
ContentProvider有两种用法:一种是使用现有的ContentProvider读取和操作相应程序中的数据;另一种是创建自己的ContentProvider,给程序的数据提供外部访问接口。
如果想要访问ContentProvider中的共享数据,就一定要借助ContentResolver类,可以通过Context中的getContentResolver()方法获取该类的实例。
ContentResolver中的增删改查方法是使用一个Uri参数代替,称为内容URI,主要由两部分组成,authority是用于对不同的应用程序作区分的,会采用应用包名的方式进行命名;path则是对同一应用程序中不同的表作区分的,一般加在authority的后面。
在得到内容URI字符串后我们还需要将他解析成Uri对象才能作为参数传入。
查询完成后返回一个Cursor对象,这时就可以将数据从Cursor对象中读取出来了;添加数据的话则是将待添加的数据组装到ContentValues中,然后调用ContentResolver的insert()方法,将Uri和ContentValues作为参数传入即可。更新和删除则分别调用ContentResolver的update()和delete()方法。
新建ContactsTest项目,首先编辑布局文件,只在里面放置一个ListView,命名contactsView,修改MainActivity:
class MainActivity : AppCompatActivity() {
private val contactsList=ArrayList<String>()
private lateinit var adapter: ArrayAdapter<String>
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
setContentView(R.layout.activity_main)
adapter=ArrayAdapter(this,android.R.layout.simple_list_item_1,contactsList)
val contactsView:ListView=findViewById(R.id.contactsView)
contactsView.adapter=adapter
if (ContextCompat.checkSelfPermission(this,Manifest.permission.READ_CONTACTS)
!=PackageManager.PERMISSION_GRANTED){
ActivityCompat.requestPermissions(this, arrayOf(Manifest.permission.READ_CONTACTS),1)
}else{
readContacts()
}
}
override fun onRequestPermissionsResult(requestCode: Int, permissions: Array<out String>,
grantResults: IntArray) {
super.onRequestPermissionsResult(requestCode, permissions, grantResults)
when(requestCode){
1->{
if (grantResults.isNotEmpty()&&grantResults[0]==PackageManager.PERMISSION_GRANTED){
readContacts()
}else {
Toast.makeText(this,"you denied the permission",Toast.LENGTH_SHORT).show()
}
}
}
}
private fun readContacts() {//查询联系人数据
contentResolver.query(ContactsContract.CommonDataKinds.Phone.CONTENT_URI,null,null,null,null)?.apply {
while (moveToNext()){
//获取联系人姓名
val displayName=getString(getColumnIndexOrThrow(ContactsContract.CommonDataKinds.Phone.DISPLAY_NAME))
//获取联系人手机号
val number=getString(getColumnIndexOrThrow(ContactsContract.CommonDataKinds.Phone.NUMBER))
contactsList.add("$displayName is $number")
}
adapter.notifyDataSetChanged()
close()
}
}
}
向AndroidManifest.xml中添加权限代码:
<manifest xmlns:android="http://schemas.android.com/apk/res/android"
package="com.example.contacttest">
<uses-permission android:name="android.permission.READ_CONTACTS"/
...
</manifest>
这下刚创建的联系人信息都出现了。
创建自己的ContentProvider
如果想实现跨程序共享数据的功能,可以 实现新建一个类去继承ContentProvider的方式来实现。ContentProvider类中有6个抽象方法,在使用子类继承时需要将这6个方法重写。
再借助UriMatcher这个类就可以轻松的实现匹配内容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 onCreate(): Boolean {
return false
}
override fun query(uri: Uri, p1: Array<out String>?, p2: String?,
p3: Array<out String>?, p4: String?): Cursor? {
when(uriMatcher.match(uri)){
table1Dir->{}//查询table1表中的所有数据
table1Item->{}//查询table1表中的单条数据
table2Dir->{}//查询table2表中的所有数据
table2Item->{}//查询table2表中的单条数据
}
return null
}
override fun update(p0: Uri, p1: ContentValues?, p2: String?, p3: Array<out String>?): Int {
return 0
}
override fun getType(uri: Uri): String? {
return null
}
override fun insert(p0: Uri, p1: ContentValues?): Uri? {
return null
}
override fun delete(p0: Uri, p1: String?, p2: Array<out String>?): Int {
return 0
}
}
上述代码以query()方法做了个示范,其他方法的实现也都是差不多的。
getType()方法比较陌生,它是ContentProvider都必须提供的一个方法,用于获取Uri对象所对应的MIME类型。
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"
}
return null
}
到这里一个完整的ContentProvider就创建完成了,现在任一程序都可使用ContentResolver访问我们程序中的数据。
实现跨程序数据共享
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
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 onCreate()= context?.let{
dbHelper=MyDatabaseHelper(it,"BookStore.db",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 insert(uri: Uri, values: ContentValues?) = dbHelper?.let{//添加数据
val db=it.writableDatabase
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/book/$newCategoryId")
}
else->null
}
uriReturn
}
override fun update(uri: Uri, values: ContentValues?, selection: String?,
selectionArgs: Array<String>?) = dbHelper?.let{//更新数据
val db=it.writableDatabase
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->null
}
updateRows
}?:0
override fun delete(uri: Uri, selection: String?, selectionArgs: Array<String>?)
=dbHelper?.let {//删除数据
val db=it.writableDatabase
val deleteRows=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->null
}
deleteRows
}?:0
override fun getType(uri: Uri)=when(uriMatcher.match(uri)){
bookDir->"vnd.android.cursor.dir/vnd.com.example.databasetest.provider.book"
bookItem->"vnd.android.cursor.item/vnd.com.example.databasetest.provider.book"
categoryDir->"vnd.android.cursor.dir/vnd.com.example.databasetest.provider.category"
categoryItem->"vnd.android.cursor.item/vnd.com.example.databasetest.provider.category"
else->null
}
}
在主页面的布局代码中添加四个按钮,分别用于添加、查询、更新和删除数据。
修改MainActivity:
class MainActivity : AppCompatActivity() {
var bookId:String?=null
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
setContentView(R.layout.activity_main)
val addDate:Button=findViewById(R.id.addData)
addDate.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)
}
val queryData:Button=findViewById(R.id.queryData)
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(getColumnIndexOrThrow("name"))
val author=getString(getColumnIndexOrThrow("author"))
val pages=getString(getColumnIndexOrThrow("pages"))
val price=getString(getColumnIndexOrThrow("price"))
Log.d("MainActivity","book name is $name")
Log.d("MainActivity","book author is $author")
Log.d("MainActivity","book pages is $pages")
Log.d("MainActivity","book price is $price")
}
close()
}
}
val updateData:Button=findViewById(R.id.updateData)
updateData.setOnClickListener { //更新数据
bookId?.let {
val uri=Uri.parse("content://com.example.databasetest.provider/book")
val values= contentValuesOf("name" to "A Storm of Swords",
"pages" to 1216,"price" to 24.05)
contentResolver.update(uri,values,null,null)
}
}
val deleteData:Button=findViewById(R.id.deleteData)
deleteData.setOnClickListener { //删除数据
bookId?.let {
val uri=Uri.parse("content://com.example.databasetest.provider/book/$it")
contentResolver.delete(uri,null,null)
}
}
}
}
现在跨程序共享数据功能以及成功实现了。