Android的数据持久化技术,包括文件存储、SharedPreferences存储以及数据库存储。使用这些持久化技术所保存的数据只能在当前应用程序中访问。虽然文件存储和SharedPreferences存储中提供了MODE_WORLD和MODE_WORLD_WRITEABLE这两种操作模式,用于供给其他应用程序访问当前应用的数据,但这两种模式在Android4.2版本中都已被废弃了。因为Android官方推荐更加安全可靠的ContentProvider技术。
可能你会问为什么要将我们程序中的数据共享给其他程序呢?当然,这个要视情况而定的,比如账号和密码这样的隐私数据显然是不能共享给其他程序的,不过一些可以让其他程序进行二次开发的数据是可以共享的。例如系统的通讯录程序,它的数据库中保存了很多联系人信息,如果这些数据都不允许第三方程序进行访问的话,恐怕很多应用的功能就要大大折扣了。除了通讯录之外,还有短信、媒体库等程序都实现了跨程序数据共享的功能,而使用的技术当然就是ContentProvider。
ContentProvider简介
ContentProvider主要用于在不同的应用程序之间实现数据共享的功能,它提供了一套完整的机制,允许一个程序访问另一个程序中的数据,同时还能保证被访问数据的安全性。目前,使用ContentProvider是Android实现跨程序共享数据的标准方式。
不同于文件存储和SharedPreferences存储中的两种全局可读可写操作模式,ContentProvider可以选择只对哪一部分数据进行共享,从而保证我们程序中的隐私数据不会有泄露的风险。
运行时权限
Android的权限机制并不是什么新鲜事物,从系统的第一个版本开始就已经存在了。但其实之前Android的权限机制在保护用户安全和隐私等方面起到的作用比较有限,尤其是一些大家都离不开的常用软件,非常容易“店大欺客”。为此,Android开发团队在Android6.0系统引入了运行时权限这个功能,从而更好地保护了用户的安全和隐私。
Android权限机制详解
Android6.0系统中加入了运行时权限功能。也就是说,用户不需要在安装软件的时候一次性授权所有申请的权限,而是可以在软件的使用过程中再对某一项权限申请进行授权。比如,一款相机应用再运行时申请了地理位置定位权限,就算我拒绝了这个权限,也应该可以使用这个应用的其他功能,而不是像之前那样直接无法安装它。
当然,并不是所有权限都需要在运行时申请,对于用户来说,不停地授权也很烦琐。Android现在将常用的权限大致归成了两类,一类是普通权限,一类是危险权限。准确地说,其实还有一些特殊权限,不过这些特殊权限使用得相对较少,因此不在本书得讨论范围之内。普通权限指的是哪些不会直接威胁到用户得安全和隐私得权限,对于这部分权限申请,系统会自动帮我们进行授权,不需要用户手动操作。危险权限则表示哪些可能会触及用户隐私或者对设备安全性造成影响的权限。Android系统的危险权限如下表所示。
每当我们开发程序需要使用一个权限时,可以先查一下这张表的权限。如果是这张表里面的权限,就需要进行运行时权限处理,否则,只需要在AndroidManifest.xml文件中添加一下权限的声明就可以了。另外注意,表格中每个危险权限都属于一个权限组,我们在进行运行时权限处理时使用的时权限名。原则上,用户一旦同意了某个权限申请之后,同组的其他权限也会被系统自动授权。但是请记住,不要基于此规则来实现任何功能逻辑,因为Android系统随时有可能调整权限的分组。
- 运行时权限案例
package com.example.myapplication
import android.Manifest
import android.content.Intent
import android.content.pm.PackageManager
import android.net.Uri
import androidx.appcompat.app.AppCompatActivity
import android.os.Bundle
import android.widget.Toast
import androidx.core.app.ActivityCompat
import androidx.core.content.ContextCompat
import kotlinx.android.synthetic.main.activity_test.*
class TestActivity : AppCompatActivity() {
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
setContentView(R.layout.activity_test)
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(){
val intent = Intent(Intent.ACTION_CALL)
intent.data = Uri.parse("tel:10086")
startActivity(intent)
}
}
<?xml version="1.0" encoding="utf-8"?>
<manifest xmlns:android="http://schemas.android.com/apk/res/android"
package="com.example.myapplication">
<uses-permission android:name="android.permission.CALL_PHONE"/>
<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">
<activity android:name=".MainActivity"></activity>
<activity android:name=".TestActivity">
<intent-filter>
<action android:name="android.intent.action.MAIN" />
<category android:name="android.intent.category.LAUNCHER" />
</intent-filter>
</activity>
</application>
</manifest>
由于用户已经完成了授权操作,之后再点击“Make Call”按钮不会再弹出权限申请对话框,而是直接拨打电话。那可能你会担心,万一以后后悔了怎么办?没关系,可以在手机的“设置”应用程序中程序的权限关闭。
访问其他程序中的数据
ContentProvider的用法一般有两种:一种时使用现有的ContentProvider读取和操作相应程序中的数据;另一种是创建自己的ContentProvider,给程序的数据提供外部访问接口。首先从使用现有的ContentProvider开始。
ContentResolver的基本用法
对于每一个应用程序来说,如果想要访问ContentProvider中共享的数据,就一定要借助ContentResolver类,可以通过Context中的getContentResolver()方法来获取该类的实例。ContentResolver中提供了一系列的方法用于对数据进行增删改查的操作,其中insert()方法用于添加数据,update()方法用于更新数据,delete()方法用于删除数据,query()方法用于查询数据。
不同于SQLiteDatabase,ContentResolver中的增删改查方法都不是接收表名参数的,而是使用一个Uri参数代替的,这个参数被称为内容URI。内容URI给ContentProvider中的数据建立了唯一标识符,它主要由两部分组成:authority和path。authority是用于对于不同的应用程序做区分的,一般为了避免冲突,会采用应用包名的方式进行命名。比如某个应用的包名是com.example.app,那么该应用对应的authority就可以命名为com.example.app.provider。path则是用于对同一应用程序中不同的表做区分的,通常会添加到authority的后面。比如某个应用的数据库里存在两张表table1和table2,这时就可以将path分别命名为table1和table2,然后把authority和path进行组合,内容URI就变成了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中的增删改查方法才都接收Uri对象作为参数。如果使用表名的话,系统将无法得知我们期望访问的是哪个应用程序里的表。
在得知了内容URI字符串之后,我们还需要将它解析成Uri对象才可以作为参数传入。解析的方法也相当简单,代码如下所示:
val url = Uri.parse("content://com.example.app.provider/table1")
只需要调用Uri.parse()方法就可以将内容URI字符串解析成Uri对象。
内容URI的格式主要有两种:以路径结尾表示期望访问该表中所有的数据,以id结尾表示期望访问该表中拥有的相应id的数据。我们可以使用通配符分别匹配这两种格式的内容uri,规则如下:
* 表示匹配任意长度的任意字符
# 表示匹配任意长度的数字
所以,一个能够匹配任意表的内容uri格式就可以写成:
content://com.example.app.provider/*
一个能够匹配table1表中任意一行数据的内容uri格式可以写成:
content://com.example.app.provider/table1/#
- 示例代码如下:
package com.example.myapplication
import android.Manifest
import android.content.pm.PackageManager
import androidx.appcompat.app.AppCompatActivity
import android.os.Bundle
import android.provider.ContactsContract
import android.widget.ArrayAdapter
import android.widget.Toast
import androidx.core.app.ActivityCompat
import androidx.core.content.ContextCompat
import kotlinx.android.synthetic.main.activity_content_resolver_test.*
class ContentResolverTestActivity : AppCompatActivity() {
private val contactsList = ArrayList<String>()
private lateinit var adapter:ArrayAdapter<String>
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
setContentView(R.layout.activity_content_resolver_test)
adapter = ArrayAdapter(this,android.R.layout.simple_list_item_1,contactsList)
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(getColumnIndex(ContactsContract.CommonDataKinds.Phone.DISPLAY_NAME))
val number = getString(getColumnIndex(ContactsContract.CommonDataKinds.Phone.NUMBER))
contactsList.add("$displayName\n$number")
}
adapter.notifyDataSetChanged()
close()
}
}
}
<?xml version="1.0" encoding="utf-8"?>
<manifest xmlns:android="http://schemas.android.com/apk/res/android"
package="com.example.myapplication">
<uses-permission android:name="android.permission.READ_CONTACTS"/>
<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">
<activity android:name=".ContentResolverTestActivity">
<intent-filter>
<action android:name="android.intent.action.MAIN" />
<category android:name="android.intent.category.LAUNCHER" />
</intent-filter>
</activity>
</application>
</manifest>
创建自己的ContentProvider
- 示例代码如下:
package com.example.myapplication
import android.content.ContentProvider
import android.content.ContentValues
import android.content.UriMatcher
import android.database.Cursor
import android.net.Uri
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 insert(uri: Uri, values: ContentValues?): Uri? = dbHelper?.let {
//向ContentProvider中添加一条数据。uri参数用于确定要添加到的表,待添加的数据保存在values参数中。添加完成后,返回一个用于表达这条新纪录的uri
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 query(
uri: Uri,
projection: Array<out String>?,
selection: String?,
selectionArgs: Array<out String>?,
sortOrder: String?
): Cursor? = dbHelper?.let {
//从ContentProvider中查询数据。uri参数用于确定查询哪张表,projection参数用于确定查询哪些列,
// selection和selectionArgs参数用于约束查询哪些行,sortOrder参数用于对结果进行排序,查询的结果存放在Cursor对象中返回
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 onCreate(): Boolean = context?.let {
//初始化ContentProvider的时候调用。通常会在这里完成对数据库的创建和升级等操作
// 返回true表示ContentProvider初始化成功,返回false则表示失败
dbHelper = MyDatabaseHelper(it,"BookStore.db",2)
true
} ?:false
override fun update(
uri: Uri,
values: ContentValues?,
selection: String?,
selectionArgs: Array<out String>?
): Int =dbHelper?.let{
//更新ContentProvider中已有的数据。uri参数用于确定更新哪一张表中的数据,更新数据保存在values参数中,
// selection和selectionArgs参数用于约束更新哪些行,被影响的行数将作为返回值返回
val db = it.writableDatabase
val updateRows = when(uriMatcher.match(uri)){
bookDir -> db.update("Book",values,selection,selectionArgs)
bookItem -> {
val bookId = uri.pathSegments[1]
db.delete("Book","id=?", arrayOf(bookId))
}
categoryDir -> db.update("Category",values,selection,selectionArgs)
categoryItem -> {
val categoryId = uri.pathSegments[1]
db.delete("Category","id=?", arrayOf(categoryId))
}
else -> 0
}
updateRows
} ?:0
override fun delete(uri: Uri, selection: String?, selectionArgs: Array<out String>?): Int = dbHelper?.let {
//从ContentProvider中删除数据。uri参数用于确定删除哪一张表中的数据,
// selection和selectionArgs参数用于约束删除哪些行,被删除的行数将作为返回值返回
//删除数据
var db = it.writableDatabase
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): String? = when (uriMatcher.match(uri)){
//根据传入的内容uri返回相应的mime类型
//一个内容URI所对应的mime字符串主要由3部分组成,Android对这3个部分做了如下格式规定
//1.必须以vnd开头
//2.如果内容URI以路径结尾,则后接android.cursor.dir/;如果内容uri以id结尾,则后接android.cursor.item/
//3.最后街上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
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
}
}
package com.example.myapplication
import android.net.Uri
import androidx.appcompat.app.AppCompatActivity
import android.os.Bundle
import androidx.core.content.contentValuesOf
import kotlinx.android.synthetic.main.activity_content_provider.*
class ContentProviderActivity : AppCompatActivity() {
var bookId:String?= null
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
setContentView(R.layout.activity_content_provider)
addData.setOnClickListener {
//添加数据
val uri = Uri.parse("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("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"))
//......
}
}
}
updateData.setOnClickListener {
bookId?.let {
val uri = Uri.parse("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)
}
}
deleteData.setOnClickListener {
bookId?.let {
val uri = Uri.parse("com.example.databasetest.provider/book/$it")
contentResolver.delete(uri,null,null)
}
}
}
}
<?xml version="1.0" encoding="utf-8"?>
<manifest xmlns:android="http://schemas.android.com/apk/res/android"
package="com.example.myapplication">
<uses-permission android:name="android.permission.READ_CONTACTS" />
<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">
<activity android:name=".ContentProviderActivity">
<intent-filter>
<action android:name="android.intent.action.MAIN" />
<category android:name="android.intent.category.LAUNCHER" />
</intent-filter>
</activity> <!-- enabled和exported属性为true表示允许DatabaseProvider被其他程序访问 -->
<provider
android:name=".DatabaseProvider"
android:authorities="com.example.databasetest.provider"
android:enabled="true"
android:exported="true"></provider>
</application>
</manifest>