参考:
系统权限:https://developer.android.com/guide/topics/security/permissions.html#normal-dangerous
在开发 Android
应用时,如果想要获取系统某些特定功能,需要在应用清单上进行权限声明。按照不同的功能需求设置不同的权限;针对不同权限所产生的影响,将权限分为不同级别;Android
系统版本从 Android 6.0(API 级别 23)
开始,需要在运行时动态申请权限
主要内容
权限浅析
Android
系统将每个应用运行在单独的进程沙盒中,即默认各个应用之间并不产生交互。应用在进程沙盒中可实现的功能不需要权限声明,当应用需要系统提供额外的功能,比如访问网络,读写存储等操作时,需要进行权限声明,Andriod
系统才能授予权限声明的功能。
正常权限和危险权限
Android
系统权限可分为几个级别,主要的两个级别是 正常权限 和 危险权限:
正常权限:指需要访问沙盒外部数据或资源,但对用户隐私或者对其他应用操作风险很小的功能。当应用声明了这一类权限,系统将自动授予该权限
危险权限:指需要涉及用户隐私信息的数据或资源,或者对用户存储的数据或其他应用的操作产生影响的功能。对于这一类权限,应用声明后,还需要用户明确向应用授予该权限
正常权限
正常权限包括网络访问的权限,设置时区的权限等等
危险权限
Android
系统将所有的危险权限进行分类,每个危险权限都属于特定的权限组,同一个权限组可包含多个危险权限
Note:权限组中可能同时包含正常权限和危险权限,不过仅有危险权限需要用户在运行时授予,所以可忽略正常权限
具体的危险权限和权限组如下所示:
权限声明与授予
需要在应用清单(AndroidManifest.xml
)上进行权限声明,使用属性 <uses-permission>
进行标记,示例如下:
<?xml version="1.0" encoding="utf-8"?>
<manifest xmlns:android="http://schemas.android.com/apk/res/android"
package="com.zj.permissiondemo">
<uses-permission android:name="android.permission.INTERNET" />
<application
...
</application>
</manifest>
上面的代码实现了对网络访问的权限声明
- 当声明正常权限时,系统会自动授予该权限
- 当声明危险权限时,系统需要用户显式确认该权限
危险权限授予
Android
系统授予危险权限的方式有两种:
- 如果系统版本运行在
Android 5.1 (API 级别 22)
或者更低时,系统会在 应用安装时 向用户请求权限。如果用户 不授予 该权限,则应用无法安装;一旦应用授予该权限,撤销权限 的唯一方式是卸载该应用;如果 应用更新时 增加了新的危险权限,同样需要在更新应用时请求权限。 - 如果系统版本是
Android 6.0(API 级别 23)
或者更高时,应用需要在运行时向用户请求权限,并且用户可通过系统设置对应用权限进行授予或者取消
前面讲到危险权限均有其所属的权限组,同一权限组内可能包含多个危险权限。当应用向用户请求授予危险权限时,并不会描述应用要访问的具体权限,而是描述其所属权限组。而如果同一权限组内的某个危险权限已被授权,那么同组内的其它权限在请求时会自动授予,不再和用户交互。
运行时请求权限
从 Android 6.0(API 级别 23)
开始,应用需要在运行中请求权限
检查权限
在执行危险权限系统功能时,需要检查是否已授予该权限,使用函数 ContextCompat.checkSelfPermission
:
int phonePermission = ContextCompat.checkSelfPermission(this, Manifest.permission.CALL_PHONE);
Log.e(TAG, "onClick: phonePermission = " + phonePermission);
int locationPermission = ContextCompat.checkSelfPermission(this, Manifest.permission.ACCESS_FINE_LOCATION);
Log.e(TAG, "onClick: locationPermission = " + locationPermission);
上面进行了电话危险权限和定位危险权限的查询,如果返回值为 PackageManager.PERMISSION_GRANTED
,表示权限已授予,如果返回为 PackageManager.PERMISSION_DENIED
,需要向用户请求权限
请求权限
如果权限已授予,则继续下面的操作即可;如果没有授予,那么调用函数 ActivityCompat.requestPermissions
请求授予权限:
void requestPermissions (Activity activity,
String[] permissions,
int requestCode)
可以一次性请求多个危险权限,应用会逐个弹出对话框向用户请求
请求权限是一个异步操作,用户授予情况将通过函数 onRequestPermissionsResult
返回:
void onRequestPermissionsResult (int requestCode,
String[] permissions,
int[] grantResults)
当用户没有授予某项权限,应用将无法访问该权限所给予的系统功能。有两种处理方法
第一种就是应用禁止该权限功能的实现
第二种就是在这里弹出一个对话框,向用户解释为什么需要该权限,然后再次申请该权限
辅助请求权限手段
请求说明
在请求权限之前,可以先向用户说明为什么本应用需要该权限,调用函数 ActivityCompat.shouldShowRequestPermissionRationale
判断是否需要进行说明
boolean shouldShowRequestPermissionRationale (Activity activity,
String permission)
如果用户之前已经拒绝过该权限请求,将返回
true
如果是第一次请求,将返回
false
如果在之前的权限请求中,用户拒绝了请求,并在对话框上点击了
Don't ask again
(如果有的话),那么也将返回false
(Android 7.0 MIUI 8.5.2.0 MI5
手机上进行权限请求时的对话框没有Don't ask again
选项,拒绝后再ononRequestPermissionsResult
上调用shouldShowRequestPermissionRationale
也返回false
,同时仅能请求两次,之后就直接拒绝请求)
请求每个危险权限
即使同一权限组中的其它危险权限已被授予,也需要明确向用户请求权限,虽然系统会立即授予该权限,这是为了预防将来的 Android
系统版本中权限组的变化
手动设置权限
当用户在弹出的对话框中拒绝了此权限,并提示系统不在提供该权限,比如,点击 Don't ask again
(如果有的话)。这种情况下,之后的权限请求将自动拒绝,直接跳转到 onRequestPermissionsResult
并返回 PERMISSION_DENIED
,不再与用户进行直接交互
Android 6.0(API 级别 23)
或更高系统需要在运行时授予权限,同时也给予了用户可以手动设置权限请求,打开手机系统设置界面,找到该应用并设置权限
Uri packageURI = Uri.parse("package:" + getPackageName());
Intent intent = new Intent(Settings.ACTION_APPLICATION_DETAILS_SETTINGS, packageURI);
startActivityForResult(intent, ACTION_TO_SETTING_PAGE);
上面这段代码将从应用跳转到系统设置中的该 app
的详细信息页面,返回时在 onActivityResult
方法中再次判断权限
请求 STORAGE
权限
STORAGE
权限
下面演示如何查询,请求和处理权限操作
读取存储文件是常用的操作,权限组 STORAGE
有两个权限:READ_EXTERNAL_STORAGE
和 WRITE_EXTERNAL_STORAGE
读取 内部存储 不需要 读写 权限,读取 外部存储 分别需要读写权限,其中,从 Android 4.4(API 级别 19)
开始,读取使用函数 getExternalFileDir
或 getExternalCacheDir
得到的外部存储路径下的文件不需要读写权限
实现功能:从外部存储中读入一张图片并将该图片重新写入外部存储
声明权限
在应用清单(AndroidManifest.xml
)中写入:
<?xml version="1.0" encoding="utf-8"?>
<manifest xmlns:android="http://schemas.android.com/apk/res/android"
package="com.zj.permissiondemo">
<uses-permission android:name="android.permission.READ_EXTERNAL_STORAGE" />
<uses-permission android:name="android.permission.WRITE_EXTERNAL_STORAGE" />
<application
...
</application>
</manifest>
权限操作
首先检查权限是否已授予:
private boolean checkPermission(String permission) {
int check = PackageManager.PERMISSION_GRANTED;
if (Build.VERSION.SDK_INT >= 23) {
check = checkSelfPermission(Manifest.permission.READ_EXTERNAL_STORAGE);
}
return check == PackageManager.PERMISSION_GRANTED;
}
如果已授予,继续下面操作;否则,需要请求授予权限
请求权限前判断是否需要向用户解释:
private boolean shouldRequestPermission(String permission) {
boolean flag = false;
if (Build.VERSION.SDK_INT >= 23) {
flag = shouldShowRequestPermissionRationale(permission);
}
return flag;
}
如果需要解释,自定义一个对话框,向用户进行说明:
private void showPermissionDialog(final String... permissions) {
AlertDialog.Builder builder = new AlertDialog.Builder(this)
.setTitle("权限请求")
.setMessage("请确认权限,以保证应用相应功能正常使用")
.setPositiveButton("同意", new DialogInterface.OnClickListener() {
@Override
public void onClick(DialogInterface dialogInterface, int i) {
onShowPermissionResult(true, permissions);
}
})
.setNegativeButton("拒绝", new DialogInterface.OnClickListener() {
@Override
public void onClick(DialogInterface dialogInterface, int i) {
onShowPermissionResult(false, permissions);
}
});
builder.show();
}
针对用户点击 同意 或者 决绝 按钮,自定义函数 onShowPermissionResult
进行操作
private void onShowPermissionResult(boolean flag, String... permissions) {
if (flag) {
if (Build.VERSION.SDK_INT >= 23) {
requestPermissions(permissions, REQUEST_PERMISSION);
}
} else {
finish();
}
}
请求结果可在回调函数 onRequestPermissionsResult
中获取,如果权限已授予,则继续下面操作;否则,判断是否需要解释请求权限
如果用户已经点击 Don't ask again
选项,那么,请求权限操作无法再与用户交互,需要用户手动开启权限:
@Override
public void onRequestPermissionsResult(int requestCode, @NonNull String[] permissions, @NonNull int[] grantResults) {
switch (requestCode) {
case REQUEST_PERMISSION:
for (int i = 0; i < permissions.length; i++) {
String permission = permissions[i];
if (permission.equals(readPermission)) {
if (grantResults.length >= i && grantResults[i] == PackageManager.PERMISSION_GRANTED) {
showImage();
}
} else {
if (shouldRequestPermission(readPermission)) {
if (Build.VERSION.SDK_INT >= 23) {
requestPermissions(new String[]{readPermission}, REQUEST_PERMISSION);
}
} else {
showSettingDialog();
}
}
}
break;
default:
super.onRequestPermissionsResult(requestCode, permissions, grantResults);
break;
}
}
showSettingDialog
方法中弹出一个对话框,告诉用户需要手动进行设置:
private void showSettingDialog(final String... permissions) {
AlertDialog.Builder bulilder = new AlertDialog.Builder(this)
.setTitle("权限请求")
.setMessage("请手动开启权限,已保证应用相应功能正常使用")
.setPositiveButton("确认", new DialogInterface.OnClickListener() {
@Override
public void onClick(DialogInterface dialogInterface, int i) {
Uri packageURI = Uri.parse("package:" + getPackageName());
Intent intent = new Intent(Settings.ACTION_APPLICATION_DETAILS_SETTINGS, packageURI);
startActivityForResult(intent, ACTION_TO_SETTING_PAGE);
}
})
.setNegativeButton("拒绝", new DialogInterface.OnClickListener() {
@Override
public void onClick(DialogInterface dialogInterface, int i) {
onShowPermissionResult(false, permissions);
}
});
bulilder.show();
}
点击确认,将跳转到系统设置该 apk
的详细信息页面,再返回时再通过回调函数 onActivityResult
进行权限操作
@Override
protected void onActivityResult(int requestCode, int resultCode, Intent data) {
switch (requestCode) {
case ACTION_TO_SETTING_PAGE:
break;
default:
super.onActivityResult(requestCode, resultCode, data);
break;
}
}
权限操作小结
权限操作其实还是一个比较麻烦的事情,尤其是遇上请求多个权限的问题,经过上面的实践,有一些小小的总结:
- 可以需要的危险权限分类,将本应用必须使用的危险权限归为一类,在启动应用时就统一申请这些权限,这些权限如果没有完全授予就结束应用
- 其它辅助类型的危险权限,可在需要时进行授予,如果没有授予就不实现相应功能,并不完全结束应用