1 运行时权限
在上几篇文章中,在访问系统的网络状态由于涉及到用户设备的安全性,所以需要使用权限声明,即在 AndroidManifest
中添加 uses-permission
标签,在添加了权限声明后,用户主要在以下两个方面得到保护:
- 如果用户在低于
6.0
系统的设备中安装该程序,会在该程序的安装界面上出现提醒,让用户从而决定是否安装该程序 - 安装程序之后,用户可以在应用程序管理界面中查看任意程序的权限申请情况
但有很多常用的软件普遍存在着滥用权限的情况,即不管该软件是否用到该权限,先声明了再说,但由于该软件常用,用户又不得不安装,举例子说,微信它在安装的提醒界面说需要读取手机的短信,即使不认可,但难道就拒绝安装吗?
所以 Android 在 6.0
系统中加入了 运行时权限功能,也就说用户不需在安装软件的时候一次性授权所有申请的权限,而是可以在软件使用的时候再对某项权限申请进行授权,从而达到即使拒绝了某个权限,还能使用到该程序的其他功能。
并不是所有的权限都是 运动时权限,Android 将所有的权限归为两类,分别是 普通权限和危险权限。
- 普通权限指的是不会直接威胁到用户的安全和隐私的权限,这部分权限申请,系统会自动帮我们进行授权,比如访问系统的网络状态。
- 危险权限表示可能触及到用户隐私或者设备安全性造成影响的权限,如获取设备联系人信息,定位等
所有的危险权限如下,即如果不在这个表中,那么只需在 AndroidManifest.xml
文件中添加权限声明即可:
注:表格中每个危险权限都属于一个权限组,在进行运行时权限处理时使用的是权限名,但用户一旦同意授权,那么该权限所对应的权限组中所有的其他权限也会同时被授权。
1.1 运行时申请权限
在程序运行时申请权限,举 CALL_PHONE
权限例子说明,在 Android 6.0 系统出现之前,拨打电话功能的实现很简单,直接在 AndroidManifest.xml
文件中声明权限,再使用 Intent
对象即可。
但在 Android 6.0 系统之后,系统在使用危险权限就必须进行运行时权限处理了。
所以当在 6.0
系统之后逻辑的步骤就变为:
- 判断用户是否已授权
- 如果已授权,直接执行相关逻辑
- 如果没有授权,向用户申请授权
ContextCompat.checkSelfPermission
方法可以获取就判断用户是否已经授权,该方法接收两个参数,分别是:
Context
实例- 具体的权限名,如
Manifest.permission.CALL_PHONE
判断该方法返回的数值是否与 PackageManager.PERMISSION_GRANTED
相同,如果相同即用户已授权。
ActivityCompat.requestPermission
方法可以向用户申请授权,该方法接受三个参数,分别是:
Activity
实例String
数组,把要申请的权限名放在数组中即可- 申请码,用于在回调函数
onRequestPermissionsResult
中使用,只要是唯一值即可
onRequestPermissionsResult
方法是当用户无论点击同意或拒绝的回调函数,授权的结果会封装在 grantResults
参数中。
例子代码如下:
public class MainActivity extends AppCompatActivity {
@Override
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
setContentView(R.layout.activity_main);
Button button9 = (Button) findViewById(R.id.make_call);
button9.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();
}
}
/**
* 授权回调
* @param requestCode 申请码
* @param permissions 授权
* @param grantResults 封装结果
*/
@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, "没有授权", Toast.LENGTH_LONG).show();
}
break;
default:
break;
}
}
}
当用户完成授权操作之后,在点击相关按钮就不会再弹出对话框寻求权限申请
2 访问其他程序中的数据
如果一个程序通过 内容提供器 对其数据提供了外部访问接口,那么任何其他的应用程序都可以对这部分数据进行访问,如系统自带的电话簿、短信等。
2.1 ContentResolver
应用想要访问内容提供器中共享的数据,就需要借助 ContentResolver
类,通过 Context
中的 getContentResolver
方法可以获取该类的实例,而 ContentResolver
实例提供了一系列方法用于数据进行 CRUD 操作。
ContentResolver
类的增删改查方法不接收表名参数,而是使用一个 Uri
参数代替,这个参数称为 内容 URI,如:
content:top.seiei.appoftest.provider/table1
内容 URI 给内容提供器中的数据建立了唯一标识符,它有三个部分组成,分别是:
- 协议声明:如上的
content://
authority
:用于对不同的应用程序做区分,如上的top.seiei.appoftest
path
:用于对同一个应用程序中不同的表做区分,如上的/table1
内容 URI 的格式主要有两种,分别是:
- 以路径结尾,表示期望访问该表中所有的数据,如:
content://top.seiei.appoftest/table1
- 以
id
结尾,表示期望访问该表中拥有相应id
的数据,如:content://top.seiei.appoftest/table1/1
可以使用通配符 *
和 #
来分别匹配两种格式的 内容 URI:
content://top.seiei.appoftest/*
:能够匹配任意表的 内容 URIcontent://top.seiei.appoftest/table1/#
:能匹配table1
表中的任意数据的 内容 URI
在得到 内容 URI 字符串之后,还需要作为参数传入 Uri.parse
方法解析成 Uri
对象,获取到 Uri
对象之后就可以对相应的表进行操作了。
2.1.1 查询
使用 ContentResolver
类的 query
方法可以查询相应数据 ,query
的参数为:
uri
:Uri
对象,指定了哪个程序中的哪张表projection
:指定查询的列表文本数组selection
:指定筛选条件selectionArgs
:为筛选条件的占位符提供具体的数值sortOrder
:指定结果排序方式
如下例子为查询手机联系人的所有数据(需要请求权限):
private void readContacts() {
Cursor cursor = null;
try {
// ContactsContract.CommonDataKinds.Phone.CONTENT_URI 封装了 uri 对象
cursor = getContentResolver().query(ContactsContract.CommonDataKinds.Phone.CONTENT_URI, null, null, null, null);
if (cursor != null) {
while (cursor.moveToNext()) {
String displayName = cursor.getString(cursor.getColumnIndex(ContactsContract.CommonDataKinds.Phone.DISPLAY_NAME));
String number = cursor.getString(cursor.getColumnIndex(ContactsContract.CommonDataKinds.Phone.NUMBER));
Log.d("联系人信息:" , displayName + " " + number);
}
}
} catch (Exception e) {
e.printStackTrace();
} finally {
if (cursor != null) {
cursor.close();
}
}
}
2.1.2 增删改
如同操作 SQLite
一样,组装数据到 ContentValues
中,再调用 ContentResolver
的 update
、insert
、delete
方法即可,如:
ContentValues values = new ContentValues();
values.put("column1", "test");
getContentResolver().insert(uri, values);
2.2 创建内容提供器
创建内容提供器的推荐方法是:通过新建一个类去继承 ContentProvider
的方式创建一个自己的内容提供器。 而 ContentProvider
类中有六个抽象方法,分别是:
onCreate
:初始化内容提供器时调用,通常会在这里完成对数据库的创建和升级等操作,返回true
表示创建成功query(Uri uri, String[] projection, String selection, String[] selectionArgs, String sortOrder)
:查询数据,使用uri
参数来确定查询哪个表,还有一些参数用于组成 SQL 语句insert(Uri uri, ContentValues values)
:添加数据,返回一个能表示该新记录的Uri
update(Uri uri, ContentValues values, String selection, String[] selectionArgs)
:更新数据,返回受影响的行数delete(Uri uri, String selection, String[] selectionArgs)
:删除数据,返回删除的条数getType(Uri uri)
:根据传入的 内容 URI 来返回相应的 MIME 类型
其中 getType
方法它是所有的内容提供器都必须提供的方法,用于获取 Uri
对象对应的 MIME 类型,一个 内容 URI 所对应的 MIME 字符串主要由三个部分组成:
- 必须以
vnd
开头 - 如果 内容 URI 以路径结尾,则后接
android.cursor.dir/
,如果 内容 URI 以id
结尾,则后接android.cursor.item/
- 最后接上
vnd.<authority>.<path>
2.2.1 UriMatcher
通过重写 ContentProvider
的六个抽象方法,分析规定格式的 uri
对象可以达到数据的安全性,使得隐私数据不会泄漏出去,而 UriMatcher
类能够轻松的实现匹配 内容 URI 的功能。
UriMatcher
提供了 addURI
方法,它接收三个参数,分别是:
authority
:匹配uri
对象的authority
值path
:匹配uri
对象的path
值- 自定义代码:在调用
UriMatcher
的match()
方法,传入的Uri
对象匹配的时候返回这个自定义代码
在使用 addURI
方法添加完对应的信息之后,就可以使用 match
方法检测 uri
对象是否匹配了。
下面使用 Android Studio 创建一个 provider 类,右键项目 New
-> Other
-> Content Provider
即可,例子代码如下:
public class MyProvider extends ContentProvider {
public static final int TABLE1 = 0;
public static final String AUTHORITY = "top.seiei.appoftest.provider";
private static UriMatcher uriMatcher;
private MyDatabaseHelper dbHelper;
// 初始化 UriMatcher
static {
uriMatcher = new UriMatcher(uriMatcher.NO_MATCH);
uriMatcher.addURI(AUTHORITY, "table1", TABLE1);
}
/**
* 初始化数据库
* @return
*/
@Override
public boolean onCreate() {
dbHelper = new MyDatabaseHelper(getContext(), "msgOfStudent.db", null,2);
return false;
}
@Nullable
@Override
public Cursor query(@NonNull Uri uri, @Nullable String[] projection, @Nullable String selection, @Nullable String[] selectionArgs, @Nullable String sortOrder) {
SQLiteDatabase db = dbHelper.getReadableDatabase();
Cursor cursor = null;
switch (uriMatcher.match(uri)) {
case TABLE1:
cursor = db.query("table1", projection, selection, selectionArgs, null, null, sortOrder);
break;
}
return cursor;
}
/**
* 返回 MIME 类型
* @param uri
* @return
*/
@Nullable
@Override
public String getType(@NonNull Uri uri) {
switch (uriMatcher.match(uri)) {
case TABLE1:
return "vnd.android.cursor.dir/vnd.top.seiei.appoftest.provider.table1";
default:
break;
}
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;
}
}
编写完内容提供器一定要在 AndroidManifest.xml
文件中注册才能使用,不过如果使用 Android Studio 的快捷方式创建,这一步就会被自动完成。打开 AndroidManifest.xml
文件的 application
标签会出现一个 provider
标签:
<provider
android:name=".providers.MyContentProvider"
android:authorities="top.seiei.appoftest.provider"
android:enabled="true"
android:exported="true"></provider>