我们在前面初步学习了Android的Activity和BroadcastReceiver组件,接下来要继续学习下一个组件内容提供器 -- Content Provider。
一、内容提供器简介
内容提供器(ContentProvider)主要用于在不同的应用程序之间实现数据共享的功能,它提供了一套完整的机制,允许一个程序访问另一个程序中的数据,同时还能保证被访数据的安全性。目前, 使用内容提供器是 Android实现跨程序共享数据的标准方式。
不同于文件存储和 SharedPreferences存储中的两种全局可读写操作模式,内容提供器可以选
择只对哪一部分数据进行共享,从而保证我们程序中的隐私数据不会有泄漏的风险。
不过在正式开始学习内容提供器之前,我们需要先掌握另外一个非常重要的知识
Android 运行时权限,因为待会的内容提供器示例中会使用到运行时权限的功能。
二、运行时权限
Android开发团队在Android 6.0系统中引入了运行时权限这个功能,用以加强保护用户的安全隐私。
1、Android权限机制
在学习广播的时候,当时为了要访问系统的网络状态,我们在AndroidManifest中添加过如下的声明:
<manifest xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:tools="http://schemas.android.com/tools"
package="com.johnhao.listviewdemo">
...
<uses-permission android:name="android.permission.ACCESS_NETWORK_STATE" />
...
</manifest>
这是因为访问系统的网络状态涉及到了用户设备的安全性,因此必须在AndroidManifest中添加声明,否则程序就会出现大问题。再添加过相应的声明后,用户安装该app时,就会在安装界面提示用户该app申请使用了哪些权限。如果用户不认可某些权限,就可以选择不安装。
但是市场上有很多我们常用的软件存在着滥用权限的情况,但是我们又不得不用,一瞬间有种店大欺客的感觉。Android团队也认识到这个问题,于是在6.0系统中加入了运行时权限。用户不需要在安装软件的时候一次性授权所有的申请权限,可以在使用过程中再对某一项全项进行授权。比如一款拍照软件,要访问通讯录权限,就算用户拒绝了这个权限,该应用的其他功能还是能正常使用,不会说不给通讯录权限就拍不了照了。
Android将现有的权限分成了两类,一类是普通权限,一类是危险权限。普通权限指的是那些不会直接威胁到用户的安全和隐私的权限,这部分权限系统自动帮我们进行授权。危险权限则表示非常有可能会触及用户隐私或对设备造成安全影响,对于这部分权限申请,必须要由用户手动点击授权才可以,否则程序无法使用相应的功能。
图中所示的为Android的危险权限,涉及9组24个权限。每一个危险权限都属于一个权限组,当用户授权了权限组中任意一个权限是,改组的其他权限也会同时被授权。
2、在程序运行时申请权限
简单的介绍完Android的权限机制后,下面我们用打电话的例子来体验一下。我们从上面的表中看到PHONE权限组是危险权限,因为拨打电话会设计用户的手机资费问题,因为被列入了危险权限。新建一个Activity,增加一个Button用来拨打电话:
public class ContentProviderMakeCallActivity extends BaseActivity {
private Button make_call;
@Override
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
setContentView(R.layout.activity_content_provider_make_call);
setTitle("运行时权限Demo -- 拨打电话");
make_call = findViewById(R.id.btn_content_call);
make_call.setOnClickListener(new View.OnClickListener() {
@Override
public void onClick(View v) {
// 运行时权限检测
if (ContextCompat.checkSelfPermission(ContentProviderMakeCallActivity.this, Manifest.permission.CALL_PHONE) != PackageManager.PERMISSION_GRANTED) {
ActivityCompat.requestPermissions(ContentProviderMakeCallActivity.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, String[] permissions, 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, "You denied the permission", Toast.LENGTH_SHORT).show();
}
break;
default:
}
}
}
首先我们定义了一个call()方法,把拨打电话的逻辑写在了里面。构建一个隐式Intent,Intent的action指定为Intent.ACTION_CALL,这是系统内置的打电话动作,然后在data部分指定了协议是tel,号码为10086。为了防止应用程序的崩溃,我们把操作都放在异常捕获代码块中。
回到onCreate()方法,我们在执行call()方法拨打电话前,需要先确认用户是不是已经给过授权。这里用的是ContextCompat.checkSelfPermission()方法,该方法接收两个参数,第一个参数是Context,第二个是具体的权限名,例如拨打电话就是Manifest.permission.CALL_PHONE。然后我们把返回值和PackageManager.PERMISSION_GRANTED做比较,相等说明用户已经授权,可以调用call()方法进行拨打电话操作;不等表示没有授权,则需要进一步调用ActivityCompat.requestPermissions()方法来向用户申请授权。requestPermissions()方法接收3个参数,第一个参数是Context,第二个参数是一个String数组,把权限名放在数组中即可,第三个参数是requestCode,只要是唯一值就行。
调用完requestPermissions()方法后,会在界面上弹出一个授权弹窗,无论用户点击授权或拒绝,最终都会回调onRequestPermissionsResult()方法,并将结果封装在grantResults中。这里,我们只需要根据对应的requestCode,判断下grantResults结果。如果授权了,就调用call()方法拨打电话, 如果没有授权就算了。
三、访问其他程序中的数据
如果一个应用程序通过内容提供器对其数据提供了外部访问接口,那么任何其他的应用程序都可以对这部分数据进行访问。Android系统中自带的电话簿、短信、媒体库等程序都提供了类似的访问接口。我们接下来就看看,如何读取联系人信息。在设备中添加一些测试的电话号码,一会儿我们会读取这些信息。
public class ContentProviderContactActivity extends BaseActivity {
private ListView lv;
private List<String> list = new ArrayList<>();
private ArrayAdapter adapter;
@Override
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
setContentView(R.layout.activity_content_provider_contact);
setTitle("Contact练习");
lv = findViewById(R.id.listview_content_provider_contacts);
adapter = new ArrayAdapter<String>(this, android.R.layout.simple_list_item_1, list);
lv.setAdapter(adapter);
if (ContextCompat.checkSelfPermission(this, Manifest.permission.READ_CONTACTS) != PackageManager.PERMISSION_GRANTED) {
ActivityCompat.requestPermissions(this, new String[]{Manifest.permission.READ_CONTACTS}, 1);
} else {
readContacts();
}
}
private void readContacts() {
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));
list.add(name + "\n" + number);
}
adapter.notifyDataSetChanged();
}
} catch (Exception e) {
e.printStackTrace();
}
finally {
if (cursor != null) {
cursor.close();
}
}
}
@Override
public void onRequestPermissionsResult(int requestCode, String[] permissions, int[] grantResults) {
super.onRequestPermissionsResult(requestCode, permissions, grantResults);
switch (requestCode) {
case 1:
if (grantResults.length > 0 && grantResults[0] == PackageManager.PERMISSION_GRANTED) {
readContacts();
} else {
Toast.makeText(this, "You denied the permission", Toast.LENGTH_LONG).show();
}
break;
default:
}
}
}
我们准备了一个ListView用于存放联系人信息,然后使用ArrayAdapter适配器,这里因为读取联系人是危险权限,因此需要添加处理运行时权限的逻辑,在上面一节中已经掌握了。下面重点看一下readContacts()方法。
这里我们用了ContentResolver的query()方法来查询系统的联系人数据。对于每个应用程序来说,如果想要访问内容提供器中共享的数据,就一定要借助ContentResolver类,我们可以通过Context中的getContentResolver()方法获取到该类的实例。ContentResolver中提供了一系列的方法用于对数据进行CRUD操作。其中insert()方法用于添加数据,update()方法用于更新数据,delete()方法用于删除数据,query()方法用于查询数据。是不是跟SQLite的操作感觉很类似,但不同的地方在于ContentResolver中的CRUD操作都不接收表名参数,而是使用了一个Uri参数代替,这个参数被称为内容URI。内容URI给Content Provider中的数据建立了唯一标识符,主要由两部分组成:authority和path。authority用于对不同的应用程序做区分,通常以包名的形式命名。path则用于对同一个应用程序中不同的表做区分。通过内容URI,可以非常清晰的表达出我们想要访问那个程序的哪张表的数据。
这里我们使用的是ContactsContract.CommonDataKinds.Phone封装好的CONTENT_URI常量,这个常量就是使用Uri.parse()方法解析出来的结果。接着就遍历Cursor对象,将联系人的姓名和电话取出,并添加到List数据中,然后通知刷新一下ListView,一定不能忘记的事情是将Cursor关掉。
最后,在AndroidManifest中添加读取联系人的权限声明。重新运行一下程序:
关于Content Provider,还有些分知识没有学习,例如如何创建自己的内容提供器,大家可以自学一下。
关注获取更多内容
网友评论