文章目录
一、权限简介
Android安全体系结构的核心设计点是:默认情况下,任何应用程序都无权执行任何会对 其他应用程序、操作系统 或 用户 产生 负面影响 的操作。例如:读取或写入用户的私人数据(如联系人或电子邮件),读取或写入其他应用程序的文件,执行网络访问,保持设备唤醒等等
为了保护Android用户的隐私,避免上述负面影响,Android对权限进行管理:Android应用在需要使用某些功能时必须进行权限的申请,而根据权限的保护级别,系统可能会自动授予权限,或者提示用户批准请求
二、保护级别
-
正常权限 :应用在AndroidManifest.xml声明,则系统自动授权给应用
从Android 9(API级别28)开始,以下权限分类为 正常权限
-
危险权限 :应用在AndroidManifest.xml声明,还需要用户明确的授权给应用
注意:
- 请求一个危险权限时,若此时APP还没有该危险权限组中的任何其它权限,则系统会向用户显示一个对话框,描述应用要访问的权限组。对话框不描述该组内的具体权限。如果用户批准,系统将向应用授予其请求的权限。
- 请求一个危险权限时,若此时APP已获得该危险权限组中的任何一个权限,则系统会立即授予该权限,而无需与用户进行任何交互。
- 即使用户已在同一组中授予了其他权限,应用仍需要明确请求其所需的每个权限。
-
签名权限:
-
特殊权限:
- SYSTEM_ALERT_WINDOW
- WRITE_SETTINGS
对于大部分的Android开发者来说,一般只用关心危险权限的申请,主要是6.0运行时权限,这也是我们能够在APP里通过代码处理直接申请到的权限;而签名权限和特殊权限则属于系统权限,只能跳转到响应的设置页面,让用户去手动开启。
本文主要讲运行时权限的申请,下面先简单介绍一点签名权限和特殊权限的申请,其他详细的大家自行了解:
BIND_ACCESSIBILITY_SERVICE 无障碍权限
- 参考:Android 一文学会无障碍服务(AccessibilityService)
- 无障碍权限,APP进程被杀死后重新进入需要重新开启
SYSTEM_ALERT_WINDOW 悬浮窗权限
// 别忘记在清单文件添加申请权限声明
@TargetApi(Build.VERSION_CODES.M)
public void requestAlertWindowPermission() {
if (!Settings.canDrawOverlays(MainActivity.this)) {
// 没有权限,请求
Intent intent = new Intent(Settings.ACTION_MANAGE_OVERLAY_PERMISSION, Uri.parse("package:" + MainActivity.this.getPackageName()));
startActivityForResult(intent, 3344);
} else {
// 有权限,直接操作
}
}
@TargetApi(Build.VERSION_CODES.M)
@Override
protected void onActivityResult(int requestCode, int resultCode, Intent data) {
if (requestCode == 3344) {
if (!Settings.canDrawOverlays(this)) {
// 获得权限
} else {
// 未获得权限
}
}
}
WRITE_SETTINGS 修改系统设置
// 别忘记在清单文件添加申请权限声明
private void requestWriteSettings() {
if (!Settings.System.canWrite(this)) {
// 没有权限,请求
Intent intent = new Intent(Settings.ACTION_MANAGE_WRITE_SETTINGS);
intent.setData(Uri.parse("package:" + getPackageName()));
startActivityForResult(intent, 1314 );
}else{
// 已经有权限,直接操作
}
}
@Override
protected void onActivityResult(int requestCode, int resultCode, Intent data) {
super.onActivityResult(requestCode, resultCode, data);
if (requestCode == 1314) {
// 检查是否可以修改系统设置
if (Settings.System.canWrite(this)) {
// 申请成功
}else{
// 申请失败
}
}
}
参考:官方文档-保护级别
三、使用原则
使用 Android 权限时,建议遵循以下原则:
- 仅使用应用正常工作所需的权限
- 注意库所需的权限
- 公开透明
- 让系统以显式方式访问
在访问敏感功能(例如,摄像头或麦克风)时提供连续指示,让用户知道APP在收集数据,避免让他们认为APP在偷偷地收集数据。
四、6.0之前:安装时权限
对于开发者:
将所有应用需要(或不需要的)的权限在AndroidManifest.xml中声明,应用程序安装后即获取到了所有声明的权限,可以随心所欲
对于用户:
在安装时,就要授予所有开发者声明的权限,否则无法安装使用;用户一旦安装应用,撤销权限的唯一方式是卸载应用
- APP运行在Android系统6.0以前版本:不管targetSdkVersion是多少,都是安装时授权
- 开发者很方便,用户的隐私受到了很大的威胁!很多用户不懂或者说没办法,在安装时直接授予了开发者请求的所有权限,一些不法分子则有机可乘,轻而易举获取用户的隐私信息。
申请权限:
<manifest xmlns:android = “http://schemas.android.com/apk/res/android” package = “com.example.snazzyapp” >
<!-- 申请发送短信权限(危险权限) -->
<uses-permission android:name = “android.permission.SEND_SMS” />
<application ... >
...
</ application>
</ manifest>
五、6.0开始:运行时权限
对于开发者:
同样需要将权限在AndroidManifest.xml中声明申请,但是对于危险权限,开发者要代码动态获取该权限,给出用户提示,用户同意才能使用该权限
对于用户:
在安装时,不用一次性授权所有权限,正常情况不授权也能安装使用有限功能,授权后也可以随时撤销授权(特殊应用特殊权限,开发者做了特殊处理除外,例如:相机APP,必须使用相机权限,如果不授权,开发者一般会代码处理为退出应用)
APP运行在Android系统6.0及更高版本:targetSdkVersion < 23是安装时授权;targetSdkVersion >= 23是运行时授权。但是在Android系统6.0及以上版本上,用户可以自主管理权限取消授权,会引起程序崩溃,所以应该尽早适配。
targetSdkVersion < 23 运行在6.0及更高版本手机上checkSelfPermission会失效,都是已授权状态的问题,可以看这篇文章
开发者需要做更多的工作,但用户的隐私得到了很大程度上的保障,也对应用的功能可以有了更多的控制
申请权限:
以在Activity界面拨打电话为例
<manifest xmlns:android = “http://schemas.android.com/apk/res/android” package = “com.example.snazzyapp” >
<!-- 声明要申请打电话权限 -->
<uses-permission android:name = “android.permission.CALL_PHONE” />
<application ... >
...
</ application>
</ manifest>
if (android.os.Build.VERSION.SDK_INT < M) { // 6.0以下
// 直接调用相关API拨打电话
} else { // 6.0开始
// 1. 检查权限
if (ContextCompat.checkSelfPermission(this, Manifest.permission.CALL_PHONE) != PackageManager.PERMISSION_GRANTED) {
// 2. 没有被授权,是否应该解释该权限作用
if (ActivityCompat.shouldShowRequestPermissionRationale(this,Manifest.permission.CALL_PHONE)) {
// 弹窗说明权限作用,点击确定,则去请求权限
} else{
// 3. 请求权限:无权限,也无需解释,直接请求权限
ActivityCompat.requestPermissions(this, new String[]{Manifest.permission.CALL_PHONE}, PERMISSION_CODE_CALL);
}
} else {
// 有权限,继续拨打电话操作
}
}
// 4. 获取权限请求结果
@Override
public void onRequestPermissionsResult(int requestCode, @NonNull String[] permissions, @NonNull int[] grantResults) {
super.onRequestPermissionsResult(requestCode, permissions, grantResults);
switch (requestCode) {
case PERMISSION_CODE_CALL: // 根据请求码判断
if (grantResults.length > 0 && grantResults[0] == PackageManager.PERMISSION_GRANTED) {
// 请求成功,继续拨打电话操作
} else {
// 请求失败
Toast.makeText(this, "本次拨打电话授权失败,请手动去设置页打开权限,或者重试授权权限", Toast.LENGTH_SHORT).show();
}
break;
default:
break;
}
}
补充说明:
-
ContextCompat.checkSelfPermission( Context context, String permission)
检查APP是否已获得某个权限如果应用具有此权限,方法将返回
PackageManager.PERMISSION_GRANTED
,并且应用可以继续操作。如果应用不具有此权限,方法将返回PackageManager.PERMISSION_DENIED
,且应用必须明确向用户要求权限。 -
ActivityCompat.shouldShowRequestPermissionRationale( Activity activity, String permission)
是否应该显示权限请求合理性说明 (Fragment中也有该方法)如果应用之前请求过此权限但用户拒绝了请求,此方法将返回 true。
如果用户在过去拒绝了权限请求,并在权限请求系统对话框中选择了 Don’t ask again 选项,此方法将返回 false。如果设备规范禁止应用具有该权限,此方法也会返回 false。
-
ActivityCompat.requestPermissions( final Activity activity, final String[] permissions, final int requestCode)
请求权限 (Fragment中也有该方法) -
onRequestPermissionsResult(int requestCode, String[] permissions, int[] grantResults)
权限请求结果回调 (在FragmentActivity和Fragment中都有该方法)
参考:在运行时请求权限
六、变更记录
- 8.0
在 Android 8.0 之前,如果应用在运行时请求权限并且被授予该权限,系统会错误地将属于同一权限组并且在清单中注册的其他权限也一起授予应用。
对于针对 Android 8.0 的应用,此行为已被纠正。系统只会授予应用明确请求的权限。然而,一旦用户为应用授予某个权限,则所有后续对该权限组中权限的请求都将被自动批准。
例如,假设某个应用在其清单中列出 READ_EXTERNAL_STORAGE 和 WRITE_EXTERNAL_STORAGE。应用请求 READ_EXTERNAL_STORAGE,并且用户授予了该权限。如果该应用针对的是 API 级别 24 或更低级别,系统还会同时授予 WRITE_EXTERNAL_STORAGE,因为该权限也属于同一 STORAGE 权限组并且也在清单中注册过。如果该应用针对的是 Android 8.0,则系统此时仅会授予 READ_EXTERNAL_STORAGE;不过,如果该应用后来又请求 WRITE_EXTERNAL_STORAGE,则系统会立即授予该权限,而不会提示用户。
七、轮子
1. SoulPermission
特点:
- 解耦Activity和Fragment、不再需要Context、不再需要onPermissionResult
- 内部涵盖版本判断,一行代码解决权限相关操作,无需在调用业务方写权限适配代码,继而实现真正的运行时权限
- 支持多项权限同时请求
- 支持特殊权限(Notification[通知]、SystemAlert[应用悬浮窗]、UnknownAppSource[未知来源应用安装])的检查与请求
- 支持系统权限页面跳转
- 支持debug模式
效果:
2. RxPermissions
这个库更简洁,使用Rxjava处理运行时权限