之前学的数据持续化技术是在当前应用程序中访问。为了实现跨程序数据共享要使用的技术就是内容提供器。
内容提供器简介
主要用于在不同的应用程序之间实现数据的共享。允许一个程序访问另一个程序的数据,还保证了安全性。
学习内容提供器之前先了解Android运行时权限。
运行时权限
Android权限机制详解
在Android 6.0系统后加入了运行时权限功能。用户不需要在安装软件的时候一次性授权所有申请的权限,而是可以在软件的使用过程中再对某一项权限进行授权。
Android将所有的权限归位两类,一类是普通权限,一类是危险权限。
普通权限指的是那些不会直接威胁用户的安全和隐私的权限,对于这部分权限申请,系统会自动帮我们进行授权,而不需要用户再去手动操作。
危险权限指的是那些可能会触及用户隐私或者对设备安全性造成影响的权限,如获取设备联系人信息、定位设备的地理位置等,对于这部分权限申请,必须要由用户手动点击授权才可以,否则程序就无法使用相应的功能。
在程序运行时申请权限
修改androidManifest文件,在其中声明如下权限:
<uses-permission android:name="android.permission.CALL_PHONE" />
把拨打电话的逻辑封装到call()方法当中
private void call() {
try {
Intent intent = new Intent(Intent.ACTION_CALL);
intent.setData(Uri.parse("tel:10010"));
startActivity(intent);
} catch (SecurityException e) {
e.printStackTrace();
}
}
构建了一个隐私Intent,Intent的action指定为Intent.ACTION_CALL,这是一个系统内置的打电话的动作,然后在data部分指定了协议是tel,号码是10086.
这个权限在Android 6.0以上是危险权限。
我们需要修改这个问题,修改MainActivity中的代码
第一步我们需要先判断用户是否给我们授权,借助的是ContextCompat.checkSelfPerfession()方法接收两个参数,第一个是Context,第二个是具体的权限名。
然后我们把返回结果和PackageManager.PERMISSION_GRANTED比较,相等表面授权,不等就是用户没有授权。
如果已经授权就直接执行逻辑,没有授权就需要调用ActivityCompat.requestPermissions()方法向用户申请授权,requestPermission()方法接收三个参数,第一个Activity实例,第二个参数是一个String数组,我们把要申请的权限名放进数组就可以了,第三个是请求码,只要是唯一值就可以,可以传入1.
调用完requestPermissions()之后会弹出一个权限申请对话框,无论同意不同意都会回调到onRequestPermissionsResult()方法中。授权结果封装在grandResults参数中,然后判断授权结果,同意就调用call()打电话。
访问其他程序中的数据
内容提供器的用法一般有两种,一种是使用现有的内容提供器来读取和操作相应程序的数据,另外一种创建自己的内容提供器给我们程序的数据提供外部访问接口。
ContentResolver的基本用法
对于每个应用程序来说,想要访问内容提供器的共享数据就要借助ContentResolver类,可以通过Context的getContentResolver()方法获得该类的实例。ContentResolver类中提供了一系列方法对数据进行CRUD操作。insert()方法用于添加 delete()方法进行删除 update()更新 query()查询。和SQLIteDatabase()很像。
不同于SQLIteDatabase,ContentResolver的增删改查都是不接受表名参数的,使用Uri替代。这个参数被称为内容URI。内容URI给内容提供器中的数据建立做了唯一的标识符,主要由两部分组成,authority和path。authority是对不同的程序区分,path是对同一个程序的中不同的表进行区分。比如某个程序的包名是com.example.app,则应用程序的的authority的就命名为com.example.app.provider。程序中存在两个表,table1,table2。path分别命名为/table1,和/table2。然后进行组合,内容URI就变成了com.example.app.provider/table1,和com.example.app.provider/table2。但是不够区分这是两个URI内容,我们需要在字符串的头部加上协议声明:
content://com.example.app.provider/table1
content://com.example.app.provider/table2
内容URI可以清晰的表达出想要访问哪个应用程序中哪张表的数据。
得到内容URI字符串后,需要将其解析成Uri对象才可以作为参数传入:
Uri uri=Uri.parse("content://com.example.app.provide/table1“)
只需要调用Uri.parse()方法即可
查询query
Cursor cursor = getContentResolver().query(
uri,
projection,
selection,
selectionArgs,
sortOrder);
查询完成后返回的仍然是一个Cursor对象,这时就可以将数据从Cursor对象中逐个读取出来了。读取的思路仍然是通过移动游标的位置来遍历Cursor的所有行,然后再取出每一行中相应列的数据,代码如下所示:
if(cursor!=null)
{
while(cursor.moveToNext())
{
String column1=cursor.getString(cursor.getColumnIndex("column1"));
int column2=cursor.getInt(cursor.getColumnIndex("column2"));
}
cursor.close();
}
向table中添加一条数据
ContentValues values=new ContentValues();
values.put("colunm1","text");
values.put("column2",1);
getContentResolver().insert(uri,values);
添加数据就是把待添加的数据放入ContentValues中,然后调用ContentResolver的insert()方法,将Uri和ContentValues作为参数传入。
可以看到,仍然是将待添加的数据组装到ContentValues中,然后调用ContentResolver的insert()方法,将Uri和ContentValues作为参数传入即可。现在如果我们想要更新这条新添加的数据,把column1的值清空,可以借助ContentResolver的update()方法实现,代码如下所示:
ContentValues values = new ContentValues();
values.put("column1","");
getContentResolver().update(uri,values,"column1 = ? and column2 = ? ",new String[] {"text","1"});
注意上述代码使用了selection和selectionArgs参数来对想要更新的数据进行约束,以防止所有的行都会受影响。最后,可以调用ContentResolver的delete()方法将这条数据删除掉,代码如下所示:
getContentResolver().delete(uri,"column2 = ? ",new String[] {"1"});
创建自己的内容提供器
通过新建一个类去继承ContentProvider的方式来创建自己的内容提供器。
ContentProvider类中有6个抽象方法,必须全部重写。
public class DatabaseProvider extends ContentProvider {
public DatabaseProvider() {
}
@Override
public int delete(Uri uri, String selection, String[] selectionArgs) {
// Implement this to handle requests to delete one or more rows.
throw new UnsupportedOperationException("Not yet implemented");
}
@Override
public String getType(Uri uri) {
// TODO: Implement this to handle requests for the MIME type of the data
// at the given URI.
throw new UnsupportedOperationException("Not yet implemented");
}
@Override
public Uri insert(Uri uri, ContentValues values) {
// TODO: Implement this to handle requests to insert a new row.
throw new UnsupportedOperationException("Not yet implemented");
}
@Override
public boolean onCreate() {
// TODO: Implement this to initialize your content provider on startup.
return false;
}
@Override
public Cursor query(Uri uri, String[] projection, String selection,
String[] selectionArgs, String sortOrder) {
// TODO: Implement this to handle query requests from clients.
throw new UnsupportedOperationException("Not yet implemented");
}
@Override
public int update(Uri uri, ContentValues values, String selection,
String[] selectionArgs) {
// TODO: Implement this to handle requests to update one or more rows.
throw new UnsupportedOperationException("Not yet implemented");
}
}
- onCreate()
初始化内容提供的时候调用。通常会在这里完成对数据库的创建和升级等操作,返回true表示内容提供器初始化成功,返回false则表示失败。只有当存在ContentResolver尝试访问程序中的数据时,内容提供器才会被初始化。
- query()
从内容提供器中查询数据。使用uri参数来确定查询哪张表,projection参数用于确定查询哪些列,selection和selectionArgs参数用于约束查询哪些行,sortOrder参数用于对结果进行排序,查询的结果存放在Cursor对象中返回。
- insert()
向内容提供器中添加一条数据。使用uri参数来确定要添加到的表,待添加的数据保存在values参数中。添加完成后,返回一个用于表示这条新纪录的URI。
- update()
更新内容提供器中已有的数据。使用uri参数来确定更新哪一张表中的数据,新数据保存在values参数中,selection和selectionArgs参数用于约束更新哪些行,受影响的行数将作为返回值返回。
- delete()
从内容提供器删除数据。使用uri参数来确定删除哪一张表中的数据,selection和selectionArgs参数用于约束删除哪些行,被删除的行数将作为返回值返回。
- getType()
根据传入的内容URI来返回相应的MIME类型。
「*」:表示匹配任意长度的任意字符
「#」:表示匹配任意长度的数字
一个能够匹配任意表的内容URI格式:
再借助UriMatcher类就可以实现匹配内容Uri的功能。UriMatcher中提供了一个addURI()方法,这个方法接收3个参数,可以分别把authority、path和一个自定义代码块传进去。当调用UriMatcher的match()方法时,就可以将一个Uri对象传入,返回值是某个能够匹配这个Uri对象所对应的自定义代码,再利用这个代码,就可以判断出调用方法期望访问的是哪张表中的数据。
public class DatabaseProvider extends ContentProvider {
public static final int TABLE1_DIR = 0;
public static final int TABLE1_ITEM = 1;
public static final int TABLE2_DIR = 2;
public static final int TABLE2_ITEM = 3;
public static final String AUTHORITY = "com.cunxie.databasetest.provider";
private static UriMatcher uriMatcher;
static {
uriMatcher = new UriMatcher(UriMatcher.NO_MATCH);
uriMatcher.addURI(AUTHORITY, "table1", TABLE1_DIR);
uriMatcher.addURI(AUTHORITY, "table1/#", TABLE1_ITEM);
uriMatcher.addURI(AUTHORITY, "table2", TABLE2_DIR);
uriMatcher.addURI(AUTHORITY, "table2/#", TABLE2_ITEM);
}
@Override
public Cursor query(Uri uri, String[] projection, String selection,
String[] selectionArgs, String sortOrder) {
switch (uriMatcher.match(uri)) {
case TABLE1_DIR:
//查询table1表中的所有数据
break;
case TABLE1_ITEM:
//查询table1表中的单条数据
break;
case TABLE2_DIR:
//查询table2表中的所有数据
break;
case TABLE2_ITEM:
//查询table2表中的单条数据
break;
default:
break;
}
......
}
}
DatabaseProvider新增了4个整型常量,其中TABLE1_DIR表示访问table1表中的所有数据,TABLE1_ITEM表示访问table1表中的单条数据,TABLE2_DIR表示访问table2表中的所有数据,TABLE2_ITEM表示访问table2表中的单条数据。接着在静态代码块里创建了UriMatcher的实例,并调用addURI()方法,将期望匹配的内容URI格式传递进去,注意这里传入的路径参数是可以使用通配符的。然后当query()方法被调用时,就会通过UriMatcher的match()方法对传入的Uri对象进行匹配,如果发现UriMatcher中某个内容URI格式成功匹配了该Uri对象,则会返回相应的自定义代码,然后就可以判断出调用方期望访问的数据。
getType()方法,是所有内容提供器都必须提供的一个方法,用于获取Uri对象所对应的MIME类型。一个内容URI所对应的MIME字符串主要有3部分组成。Android对这3个部分作出了规定:
- 必须以vnd开头
- 如果内容URI以路径结尾,则后接android.cursor.dir/,如果内容URI以id结尾,则后接android.cursor.item/
- 最后接上vnd..
对于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
@Override
public String getType(Uri uri) {
switch (uriMatcher.match(uri)) {
case TABLE1_DIR:
return "vnd.android.cursor.dir/vnd.com.example.app.provider.table1";
case TABLE1_ITEM:
return "vnd.android.cursor.item/vnd.com.example.app.provider.table1";
case TABLE2_DIR:
return "vnd.android.cursor.dir/vnd.com.example.app.provider.table2";
case TABLE2_ITEM:
return "vnd.android.cursor.item/vnd.com.example.app.provider.table2";
default:
break;
}
return null;
}