内容提供其用于在不同程序之前实现数据共享的功能,提供了一整套完整机制,允许一个程序从另一个程序中访问数据。内容提供器可以选择只对部分数据进行共享,保证了数据安全。
运行时权限
要先了解一下这个东西,不仅在这里能用到,以后也常用。
在Android 6.0之前,程序的权限是在AndroidManifest.xml中写好了的,在安装软件的时候会显示出来,安装软件就表示授权了这些权限,但是有很多软件会申请一些并不需要的权限,而你不同意的话又没法使用软件,就很不方便。
因此Android 从6.0以后开始针对这个问题提出了运行时权限的功能,不需要再安装软件的时候授权所有权限,在使用过程中需要什么权限进行申请,这样即便拒绝了这个权限,也只是这个功能用不成,其他功能还是可以使用的。
但是并不是所有权限都需要在运行的时候一个一个授权,因此分为两类,普通权限和危险权限,普通权限是指不会直接威胁用户的安全和隐私的权限,这些权限系统会自动帮我们授权。比如前面用到的BroadcastTest里面的两个权限就是普通权限,危险权限就是指会触及用户隐私或设备安全的权限,比如联系人信息、地理位置信息等等,需要手动授权才可以,否则无法使用相关功能。
Android所有的危险权限一共有九组24个,当权限在这里面的时候,就需要进行运行时权限处理,如果需要的权限不在这里面就只需要在AndroidManifest.xml中注册就行了。
每个危险权限都属于一个权限组,处理的时候用的是权限名但是一旦授权,组内其他权限会一同被授予。
这里举个打电话的栗子,CALL_PHONE
这个权限是打电话的时候需要声明的,理由是打电话要掏钱,很危险。
布局文件还是那个按钮,给按钮设置监听,当点击按钮的时候就打电话,写完代码后她就开始报错了,把权限写一下。
TestButton.setOnClickListener(new View.OnClickListener() {
@Override
public void onClick(View v) {
Intent intent=new Intent(Intent.ACTION_CALL);
intent.setData(Uri.parse("tel:10086"));
startActivity(intent);
}
});
那就写一下权限吧,编辑AndroidManifest.xml,添加CALL_PHONE权限
<manifest xmlns:android="http://schemas.android.com/apk/res/android"
package="com.example.k.androidpractice">
<uses-permission android:name="android.permission.CALL_PHONE"/>
...
此时如果运行程序,点击按钮以后,会报错,因为还是没有给他授权。
稍微做一下修改。
如下,这是运行时权限的所有流程。
首先通过ContextCompat.checkSelfPermission()
判断是否已经授权了该权限,参数第一个是Context,第二个是权限名,如打电话是Manifest.permission.CALL_PHONE
,将返回值和PackageManager.PERMISSION_GRANTED
作比较,如果相等就说明已经授权了,否则没授权。
如果未授权,调用ActivityCompat.requestPermissions()
方法申请权限,三个参数,第一个是Activity实例,第二个是String数组,存放权限名,第三个请求码只要唯一就行。
如果用户授权就可以打电话,否则会弹窗。
TestButton.setOnClickListener(new View.OnClickListener() {
@Override
public void onClick(View v) {
if (ContextCompat.checkSelfPermission(MainActivity.this, Manifest.permission.CALL_PHONE)!= PackageManager.PERMISSION_GRANTED){
ActivityCompat.requestPermissions(MainActivity.this,new String[]{Manifest.permission.CALL_PHONE},1);
}else{
call();
}
}
});
添加两个方法
private void call(){
try{
Intent intent=new Intent(Intent.ACTION_CALL);
intent.setData(Uri.parse("tel:10086"));
startActivity(intent);
}catch (SecurityException e){
e.printStackTrace();
}
}
@Override
public void onRequestPermissionsResult(int requestCode, @NonNull String[] permissions, @NonNull int[] grantResults) {
super.onRequestPermissionsResult(requestCode, permissions, grantResults);
switch (requestCode){
case 1:if (grantResults.length>0 && grantResults[0]==PackageManager.PERMISSION_GRANTED){
call();
}else{
Toast.makeText(this,"Denied the permission",Toast.LENGTH_SHORT).show();
}
break;
}
}
运行程序,点击按钮会有授权提示
如果选择DENY就会返回,选择ALLOW会打电话
内容提供器
用法一般有两种,一种使用现有的内容提供器读取和操作应用程序的数据,另一种是创建自己的内容提供器给自己程序提供外部访问接口。
ContentResolver
应用程序要想访问内容提供器中的数据就必须要借助这个类,通过Context的getContentResolver()方法取得实例,同时还提供了一些CRUD操作方法
- insert
- delete
- update
- query
和数据库的不同,使用的时候不接收表名参数没使用Uri参数代替,由两部分组成,authority和path。前者区分不同应用程序,为了避免冲突一般是包名,后者区分同一应用程序中的不同表。如有table1和table2,则path为/table1和/table2。组合起来就是com.example.k.androidpractice/table1
,再加上协议声明,内容URI标准写法如下
content://com.example.k.androidpractice/table1
Uri uri=Uri.parse(“com.example.k.androidpractice/table1”)
查询
查询代码如下所示,参数分别代表某个应用的某个表、指定字段列、where约束条件、占位符具体数值以及指定排序方式。返回结果仍然是一个Cursor对象。。
Cursor cursor=getContentResolver().query(uri,projection,selection,selectionArgs,sortOrder);
读物数据的思路还是通过移动游标来遍历每一行
if (cursor!=null){
while (cursor.moveToNext()){
String column1=cursor.getString(cursor.getColumnIndex("column1"));
}
cursor.close();
}
添加
和数据库的操作很像,使用ContentValues对象存放数据然后作为参数传入。
ContentValues values=new ContentValues();
values.put("colume1","sss");
getContentResolver.insert(uri,values);
更新
像更新数据,把colume的值清空,调用update()方法
ContentValues values=new ContentValues();
values.put("column1","");
getContentResolver().update(uri,values,"column1= ? and column2= ?",new String[]{"text","1"});
删除
通过delete()方法可以删除数据。
getCntentResolver.delete(uri,"column2=?",new String[]{"1"});
然后ContentResolver的增删改查就完了。
读取联系人
一个例子,读取一下联系人数据。
给avd里面添加几个联系人数据,需要将获取的数据展示出来,用ListView来显示数据,修改acivity_layout.xml,添加一个控件就行。
<ListView
android:layout_width="match_parent"
android:layout_height="match_parent"
android:id="@+id/ShowContact"></ListView>
修改MainActivity.java,获取ListView实例,设置适配器,在使用前进授权询问,如果给权限了就抓紧时间进行处理。
这里没有使用Uri解析,是因为在ContentResolver.CommonDataKinds.Phone
类中已经封装好了,里面有一个CONTENT_URI,这就是Uri.parse()解析后的结果。
然后湖区通讯录数据,表示联系人姓名的常量是ContactsContract.CommonDataKinds.Phone.DISPLAY_NAME
,相当于数据库中查询用到的列名,这里封装好了。进行拼装,添加到ListView数据源中,然后同值ListViw刷新一下,最后关闭cursor。
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
setContentView(R.layout.activity_main);
ShowContactListView=findViewById(R.id.ShowContact);
adapter=new ArrayAdapter<String>(this,R.layout.list_item_1,ContactList);
ShowContactListView.setAdapter(adapter);
if (ContextCompat.checkSelfPermission(this,Manifest.permission.READ_CONTACTS)!=PackageManager.PERMISSION_GRANTED){
ActivityCompat.requestPermissions(this,new String[]{Manifest.permission.READ_CONTACTS},1);
}else{
readContact();
}
}
private void readContact(){
Cursor cursor=null;
try{
cursor=getContentResolver().query(ContactsContract.CommonDataKinds.Phone.CONTENT_URI,null,null,null,null);
if (cursor!=null){
while (cursor.moveToNext()){
String Name=cursor.getString(cursor.getColumnIndex(ContactsContract.CommonDataKinds.Phone.DISPLAY_NAME));
String Number=cursor.getString(cursor.getColumnIndex(ContactsContract.CommonDataKinds.Phone.NUMBER));
ContactList.add(Name+"\n"+Number);
}
adapter.notifyDataSetChanged();
}
}catch(Exception e){
e.printStackTrace();
}finally{
if (cursor!=null)
cursor.close();
}
}
@Override
public void onRequestPermissionsResult(int requestCode, @NonNull String[] permissions, @NonNull int[] grantResults) {
super.onRequestPermissionsResult(requestCode, permissions, grantResults);
switch (requestCode){
case 1:if (grantResults.length>0 && grantResults[0]==PackageManager.PERMISSION_GRANTED){
readContact();
}else{
Toast.makeText(this,"Denied the permission",Toast.LENGTH_SHORT).show();
}
break;
}
}
还需要再AndroidManifest.xml中添加权限
<uses-permission android:name="android.permission.READ_CONTACTS"/>
运行程序,ALLOW授权,可以看到列表了。
ArrayAdapter
这是我看这个代码发现的,泛型设为String,可以存储字符串,参数分别为Context,布局,数据List。
adapter=new ArrayAdapter(this,android.R.layout.simple_list_item_1,ContactList);
布局可以使用安卓自带的android.R.layout.simple_list_item_1
。
然后直接给ListView调用setAdapter()就行了,不用重写那乱七八糟的东西。
自己的内容提供器
可以通过一个继承ContentProvider类的类实现自己的内容提供器,一共有六个抽象类,需要全部重写,新建一个MyProvider类,继承ContentProvider类,在继承以后按alt+insert然后选择implement,自动填充需要实现的方法。
- onCreate():初始化提供器,完成对数据库的创建和升级,成功返回true
- query():查询数据,使用uri确定查询哪张表,projection确定查询的列,selection和selectionArgs约束哪些行,sortOrder对结果排序,结果作为Cursor对象返回
- insert():向提供器添加一条数据,使用uri确定哪个表,添加的数据保存在values中,添加完成后返回一个用于表示新纪录的URI。
- update():更新数据,使用uri表示哪个表,数据在values中,selection和selectionArgs表示约束条件,返回值是影响的行数
- delete():uri指定哪个表,selection和selectionArgs约束删除哪些行,返回值是删除的行数
- getType():根据传入的内容URI返回响应的MIME类型
public class MyProvider extends ContentProvider {
@Override
public boolean onCreate() {
return false;
}
@Nullable
@Override
public Cursor query(@NonNull Uri uri, @Nullable String[] projection, @Nullable String selection, @Nullable String[] selectionArgs, @Nullable String sortOrder) {
return null;
}
@Nullable
@Override
public String getType(@NonNull Uri uri) {
return null;
}
@Nullable
@Override
public Uri insert(@NonNull Uri uri, @Nullable ContentValues values) {
return null;
}
@Override
public int delete(@NonNull Uri uri, @Nullable String selection, @Nullable String[] selectionArgs) {
return 0;
}
@Override
public int update(@NonNull Uri uri, @Nullable ContentValues values, @Nullable String selection, @Nullable String[] selectionArgs) {
return 0;
}
}
标准URI是这样的content://com.example.k.androidpractice/table1
,还可以再添加,content://com.example.k.androidpractice/table1/1
,表示table1表中id为1的数据。
URI只有这两种,一个是指定表,一个是指定id的数据。
还可以使用通配符。
- *:任意长度的任意字符
- #:任意长度的数字
如content://com.example.ss/*
表示匹配任意表的内容,content://com.example.k.androidpractice/table1/#则表示匹配table1表中的任意一行的数据。
使用UriMatcher类可以实现匹配内容URI的功能,有一个addUri()方法,有三个参数,authority(类名)、path(Uri)和一个自定义代码(标识符的功能)。当调用UriMatcher类中的match()方法的时候,可以传入一个Uri对象,返回值是能匹配这个Uri对象的自定义代码,这个代码可以判断出调用方法期望访问哪个表的数据。
修改一下MyProvider代码
public class MyProvider 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;
private static UriMatcher uriMatcher;
static{
uriMatcher=new UriMatcher(UriMatcher.NO_MATCH);
uriMatcher.addURI("com.example.k.androidpractice","table1",TABLE1_DIR);
uriMatcher.addURI("com.example.k.androidpractice","table1/#",TABLE1_ITEM);
uriMatcher.addURI("com.example.k.androidpractice","table2",TABLE2_DIR);
uriMatcher.addURI("com.example.k.androidpractice","table2/#",TABLE2_ITEM);
}
...
public Cursor query(@NonNull Uri uri, @Nullable String[] projection, @Nullable String selection, @Nullable String[] selectionArgs, @Nullable String sortOrder) {
switch (uriMatcher.match(uri)){
case TABLE1_DIR:
//查询table1所有数据
break;
case TABLE1_ITEM:
//查询table1所有数据
break;
case TABLE2_DIR:
break;
case TABLE2_ITEM:
break;
}
return null;
}
...
其他方法也都差不多。
getType()方法,用于获取Uri对象对应的MIME类型,这个字符串主要由三部分组成。
必须以vnd开头,如果内容URI以路径结尾,则接andoid.cursor.dir/
,如果是id结尾,则接android.cursor.item/
,然后再接上vnd..
如对于com.example.k.androidpractice/table1
可以写成vnd.android.cursor.dir/vnd.com.example.k.androidpractice.table1
。
继续完善代码,修改getType()代码,需要做的就是构造对应的MIME字符串,这里我只写了一个,懒得写了,太麻烦了,其他的照着写就行。
public String getType(@NonNull Uri uri) {
switch (uriMatcher.match(uri)) {
case TABLE1_DIR:return "vnd.android.cursor.dir/vnd.com.example.k.androidpractice.table1";break;
}
}
说白了就是相当于在这里实现各种关于数据库的增啥改查操作,然后另一个应用访问接口,间接访问这个应用的数据库。
有个小栗子,太麻烦不写了。