一文搞定大部分安卓权限相关知识
一,什么是安卓权限
程序要调用摄像头进行摄像,要进行定位获得经纬度坐标等等均涉及到安卓权限。
在安卓6.0(也就是Android M,对应的API为23)之前,通过在AndroidManifest.xml文件中声明<uses-permission android:name="android.permission.XXXXXX" />
便可以形成权限列表。在程序第一次启动的时候会提示用户是否允许权限,若用户点击否,那么会退出安装,因此用户为了使用软件只能默默忍受权限。
但是从6.0开始,安卓开始模仿iOS对权限提高了重视,对权限进行了分类。在6.0之后,app是可以直接安装的,AndroidManifest.xml中声明的权限中的部分权限需要编码完成最终权限的授予。
二,安卓6.0后的权限升级
对权限划分了等级:普通权限(Normal Permissions)、危险权限(Dangerous Permissions)、特殊和签名。
普通权限
普通权限覆盖了应用程序需要访问应用程序沙箱外的数据或资源的区域,对用户的隐私或其他应用程序的操作几乎没有风险。不需要用户授权,比如手机震动、访问网络等。
ACCESS_LOCATION_EXTRA_COMMANDS
ACCESS_NETWORK_STATE
ACCESS_NOTIFICATION_POLICY
ACCESS_WIFI_STATE
BLUETOOTH
BLUETOOTH_ADMIN
BROADCAST_STICKY
CHANGE_NETWORK_STATE
CHANGE_WIFI_MULTICAST_STATE
CHANGE_WIFI_STATE
DISABLE_KEYGUARD
EXPAND_STATUS_BAR
GET_PACKAGE_SIZE
INSTALL_SHORTCUT
INTERNET
KILL_BACKGROUND_PROCESSES
MODIFY_AUDIO_SETTINGS
NFC
READ_SYNC_SETTINGS
READ_SYNC_STATS
RECEIVE_BOOT_COMPLETED
REORDER_TASKS
REQUEST_INSTALL_PACKAGES
SET_ALARM
SET_TIME_ZONE
SET_WALLPAPER
SET_WALLPAPER_HINTS
TRANSMIT_IR
UNINSTALL_SHORTCUT
USE_FINGERPRINT
VIBRATE
WAKE_LOCK
WRITE_SYNC_SETTINGS
危险权限
危险权限覆盖应用程序需要数据或资源的区域,这些数据或资源涉及用户的私有信息,或者可能影响用户的存储数据或其他应用程序的操作。例如,读取用户联系人、读取sdcard、访问通讯录。在用户批准权限之前,应用程序不能提供依赖于该权限的功能。
危险权限和权限组
通讯录
group:android.permission-group.CONTACTS
permission:android.permission.WRITE_CONTACTS
permission:android.permission.GET_ACCOUNTS
permission:android.permission.READ_CONTACTS
电话
group:android.permission-group.PHONE
permission:android.permission.READ_CALL_LOG
permission:android.permission.READ_PHONE_STATE
permission:android.permission.CALL_PHONE
permission:android.permission.WRITE_CALL_LOG
permission:android.permission.USE_SIP
permission:android.permission.PROCESS_OUTGOING_CALLS
permission:com.android.voicemail.permission.ADD_VOICEMAIL
日历
group:android.permission-group.CALENDAR
permission:android.permission.READ_CALENDAR
permission:android.permission.WRITE_CALENDAR
摄像头
group:android.permission-group.CAMERA
permission:android.permission.CAMERA
身体传感器
group:android.permission-group.SENSORS
permission:android.permission.BODY_SENSORS
地理位置
group:android.permission-group.LOCATION
permission:android.permission.ACCESS_FINE_LOCATION
permission:android.permission.ACCESS_COARSE_LOCATION
存储空间
group:android.permission-group.STORAGE
permission:android.permission.READ_EXTERNAL_STORAGE
permission:android.permission.WRITE_EXTERNAL_STORAGE
麦克风
group:android.permission-group.MICROPHONE
permission:android.permission.RECORD_AUDIO
短信
group:android.permission-group.SMS
permission:android.permission.READ_SMS
permission:android.permission.RECEIVE_WAP_PUSH
permission:android.permission.RECEIVE_MMS
permission:android.permission.RECEIVE_SMS
permission:android.permission.SEND_SMS
permission:android.permission.READ_CELL_BROADCASTS
危险权限可以通过adb命令adb shell pm list permissions -d -g
查看
危险权限的分组机制对于运行在Android6.0及以上且是使用targetSdkVersion>=23打包的app是有影响的。
如果你申请某个危险的权限,假设你的app早已被用户授权了同一组的某个危险权限,那么系统会立即授权,而不需要用户去点击授权。比如你的app对READ_CONTACTS已经授权了,当你的app申请WRITE_CONTACTS时,系统会直接授权通过。此外,对于申请时弹出的dialog上面的文本说明也是对整个权限组的说明,而不是单个权限(ps:这个dialog是不能进行定制的)。
不过需要注意的是,不要对权限组过多的依赖,尽可能对每个危险权限都进行正常流程的申请,因为在后期的版本中这个权限组可能会产生变化。
特殊权限
SYSTEM_ALERT_WINDOW
和WRITE_SETTINGS
权限比较特别,一般不推荐在app中使用它们。如果非要使用,那么要先在Manifest文件中声明,然后发送Intent来请求用户授权,系统会通过一个详细的管理界面来响应Intent。
注意这两个权限在6.0之前为安装时授权,6.0之后为特殊权限。
下面以WRITE_SETTINGS(允许读写系统设置)为例说明一下在6.0之后如何申请:
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.M) {
if (!Settings.System.canWrite(this)) {
Intent intent = new Intent(Settings.ACTION_MANAGE_WRITE_SETTINGS, Uri.parse("package:" + getPackageName()));
intent.addFlags(Intent.FLAG_ACTIVITY_NEW_TASK);
startActivity(intent);
} else {
//有了权限,你要做什么呢?具体的动作
}
}
签名权限
使用相同证书app之间权限,不常用。
三,权限组
上面介绍危险权限的时候提到了权限组。所有危险的安卓权限都属于权限组。任何权限都可以属于权限组,而不考虑保护级别;但是,只有当权限是危险权限时,权限组才会影响用户体验。
四,权限获取
无论是6.0还是6.0之后,所有的权限都必须先在Manifest中声明。
对于Manifest文件中声明的正常权限,系统会自动把权限授予应用,不会区分6.0系统与否。
对于Manifest文件中声明的危险权限,那么用户必须明确同意授予这些权限,那么6.0之前和之后对于危险权限获取,有何异同,见下面总结:
Android6.0之前获取权限特点(安卓设备系统版本低于6.0,或者app构建时选择的targetSdkVersion低于23)
系统要求用户在安装时授予权限。系统只会告诉用户应用程序需要什么权限组,而不是个*限。例如,当应用程序请求READ_CONTACTS时,安装对话框列出联系人组。当用户接受时,只有READ_CONTACTS权限被授予应用程序。
Android 6.0 (API level 23)获取权限特点(设备运行于6.0之上,并且 app构建的时候选择的targetSdkVersion 高于等于23)
- 如果应用程序当前没有权限组中的任何权限,系统将向用户显示权限请求对话框,向用户描述应用程序想要访问的权限组。该对话框不描述该组中的特定权限。例如,如果一个应用程序请求READ_CONTACTS权限,系统对话框只是说应用程序需要访问设备的联系人。如果用户同意,系统只给应用程序请求的权限。
- 如果应用程序已经获取同一权限组中另一个危险权限,系统立即授予权限而不与用户进行任何交互。例如,如果一个应用程序先前请求并获得了READ_CONTACTS权限,然后它请求READ_CONTACTS,系统立即授予该权限,而不向用户显示权限对话框。
[注意]:权限的申请不要基于用户组(喊6.0前后所有版本),原因如下:
Caution: Future versions of the Android SDK might move a
particular permission from one group to another. Therefore, don't
base your app's logic on the structure of these permission groups.
五,权限申请过程
Android 6.0之前
- AndroidManifest.xml中注册
- 安装时会提示,不接受的话会取消应用的安装
Android 6.0之后
- AndroidManifest.xml中注册
- 写动态申请代码
- 申请时提示
[第一次申请]
只有拒绝、允许两个按钮,点允许则永久获得权限,除非卸载游戏再重新安装(覆盖安装依旧会保留之前的权限)。点拒绝则会拒绝该次的授权
[第二次申请]
第一次申请被拒绝后,才会有第二次申请的情况,这次除了拒绝、允许按钮外,还有个选项“Never ask again”。用户勾选“Never ask again”的话,那么只有拒绝按钮可以点击,这样的话,后面代码运行到这里就不会弹出让你授权的界面,需要用户自己判断,弹出引导设置界面来授权。如果用户不勾选“Never ask again”选项而选择了拒绝按钮,那么这次拒绝授权依旧是单词授权,下次代码运行到此,会继续弹出第三次申请。
六,Android 6.0动态权限申请代码
相关API
- 检查权限
if (ContextCompat.checkSelfPermission(thisActivity,
Manifest.permission.READ_CONTACTS)
!= PackageManager.PERMISSION_GRANTED) {
}else{
//
}
返回值为PackageManager.PERMISSION_DENIED
或者PackageManager.PERMISSION_GRANTED
- 申请授权
ActivityCompat.requestPermissions(thisActivity,
new String[]{Manifest.permission.READ_CONTACTS},
MY_PERMISSIONS_REQUEST_READ_CONTACTS);
该方法是异步的,第一个参数是Context;第二个参数是需要申请的权限的字符串数组;第三个参数为requestCode,主要用于回调的时候检测。可以从方法名requestPermissions以及第二个参数看出,是支持一次性申请多个权限的,系统会通过对话框逐一询问用户是否授权。
- 处理权限申请回调
@Override
public void onRequestPermissionsResult(int requestCode,
String permissions[], int[] grantResults) {
switch (requestCode) {
case MY_PERMISSIONS_REQUEST_READ_CONTACTS: {
// If request is cancelled, the result arrays are empty.
if (grantResults.length > 0
&& grantResults[0] == PackageManager.PERMISSION_GRANTED) {
// permission was granted, yay! Do the
// contacts-related task you need to do.
} else {
// permission denied, boo! Disable the
// functionality that depends on this permission.
}
return;
}
}
}
首先验证requestCode定位到你的申请,permissions对应权限名,grantResults对应权限申请结果。如果你同时申请两个权限,那么grantResults的length就为2,分别记录你两个权限的申请结果。如果申请成功,就可以做你的事情了~
- 是否应该提示
if (ActivityCompat.shouldShowRequestPermissionRationale(thisActivity,
Manifest.permission.READ_CONTACTS))
// Show an expanation to the user *asynchronously* -- don't block
// this thread waiting for the user's response! After the user
// sees the explanation, try again to request the permission.
}
这个API主要用于给用户一个申请权限的解释,该方法只有在用户在上一次已经拒绝过你的这个权限申请。也就是说,用户已经拒绝一次了,你又弹个授权框,你需要给用户一个解释,为什么要授权,则使用该方法。
若用户在过去拒绝了权限请求,并在权限请求对话框选择了Do not ask again选项,那么此方法将返回false。由于安卓碎片化的问题,有些厂商会静止应用具有该权限,该方法也会返回false。
对于其它场景该方法返回值如下:
a.第一次对app请求权限时shouldShowRequestPermissionRationale返回false
b.第一次请求权限被禁止,但未选择[不再询问]。shouldShowRequestPermissionRationale返回true
c.允许某权限后,shouldShowRequestPermissionRationale返回false
d.禁止权限,并选择[不再询问],shouldShowRequestPermissionRationale返回false
- 步骤总结
以上几个API的步骤总结就是:
// Here, thisActivity is the current activity
if (ContextCompat.checkSelfPermission(thisActivity,
Manifest.permission.READ_CONTACTS)
!= PackageManager.PERMISSION_GRANTED) {
// Should we show an explanation?
if (ActivityCompat.shouldShowRequestPermissionRationale(thisActivity,
Manifest.permission.READ_CONTACTS)) {
// Show an expanation to the user *asynchronously* -- don't block
// this thread waiting for the user's response! After the user
// sees the explanation, try again to request the permission.
} else {
// No explanation needed, we can request the permission.
ActivityCompat.requestPermissions(thisActivity,
new String[]{Manifest.permission.READ_CONTACTS},
MY_PERMISSIONS_REQUEST_READ_CONTACTS);
// MY_PERMISSIONS_REQUEST_READ_CONTACTS is an
// app-defined int constant. The callback method gets the
// result of the request.
}
}
shouldShowRequestPermissionRationale的功能价值何在
在此之前先说明下,由于不同的系统厂商定制的结果,
1.有的手机某些权限清单注册了权限就能用,不用动态申请(因为系统会在安装时自动app分配一些权限,具体怎么分配的这里暂不做讨论);
2.有的手机在弹出授权时选择拒绝就默认了不再弹出;
3.有的沿用了原生系统的规则;
4.设置-应用-权限中权限分“允许、询问、拒绝”三个级别,但是有的权限只有“允许、拒绝”两个级别;
这里先统一下名词:
允许 – 权限通过
拒绝–拒绝了但是还允许询问
禁止–拒绝了且不再允许询问(如4中所述的“拒绝”先定义为禁止)
从前面就可以看出来,这个方法大部分情况下是放回false的,只有被用户拒绝了权限,再次获取才会得到true;如果没有申请过,或者禁止了权限,都是返回的false。所以很多人想要通过shouldShowRequestPermissionRationale去判断是否权限被禁止,有时候是并不准确的,真要说怎样会准确的获取到权限被禁止的情况,那就是:
1.在requestPermissions之后在
onRequestPermissionsResult中获取到没给权限,并且shouldShowRequestPermissionRationale是false,此时可以认定该权限被用户禁止了;
2.还有一个点是是在onRequestPermissionsResult的参数值第三个参数grantResults是null,此时权限也是被拒绝的。(权限被拒绝后再次调用requestPermissions,没有返回结果)
shouldShowRequestPermissionRationale,回到最初的解释“应不应该解释下请求这个权限的目的”。
1.都没有请求过这个权限,用户不一定会拒绝你,所以你不用解释,故返回false;
2.请求了但是被拒绝了,此时返回true,意思是你该向用户好好解释下了;
3.请求权限被禁止了,也不给你弹窗提醒了,所以你也不用解释了,故返回fasle;
4.请求被允许了,都给你权限了,还解释个啥,故返回false。
Google的初衷大概就是第一次requestPermissions的时候被拒绝时给你一次解释的机会,所以是让你在请求权限的回调中使用的。
Never ask again拒绝权限后的处理
上面也提到过对于授权时用户选择拒绝并选择Never ask again选项的情况,后面申请权限都会失败,因此需要用户进行设置提示。
一般在处理权限申请回调android.app.Activity#onRequestPermissionsResult
时,结合shouldShowRequestPermissionRationale
返回值特性,可以判定是否为该种情形。
代码如下:
public void onRequestPermissionsResult(int requestCode, @NonNull String[] permissions, @NonNull int[] grantResults) {
super.onRequestPermissionsResult(requestCode, permissions, grantResults);
for (int index = 0; index < permissions.length; index++) {
if (grantResults[index] != PackageManager.PERMISSION_GRANTED
&& !activity.shouldShowRequestPermissionRationale(permissions[index])) {
//用户之前拒绝,并勾选不再提示时,在此引导用户去设置页设置权限
}
}
}
申请权限
/**
* 申请权限
*/
@RequiresApi(api = Build.VERSION_CODES.M)
public static void requestPermissions(@NonNull Activity activity, @NonNull String[] permissions, int requestCode) {
if (!needRequestPermission(activity, permissions)) {
return;
}
List<String> deniedPermissions = findDeniedPermissions(activity, permissions);
if (!deniedPermissions.isEmpty()) {
activity.requestPermissions(permissions, requestCode);
}
}
/**
* @return 未获得的权限
*/
@RequiresApi(api = Build.VERSION_CODES.M)
private static List<String> findDeniedPermissions(Activity activity, String... permission) {
List<String> denyPermissions = new ArrayList<>();
for (String value : permission) {
if (activity.checkSelfPermission(value) != PackageManager.PERMISSION_GRANTED) {
denyPermissions.add(value);
}
}
return denyPermissions;
}
/**
* Android系统6.0 往上
*/
private static boolean versionOverM() {
return Build.VERSION.SDK_INT >= Build.VERSION_CODES.M;
}
/**
* @return true: 需要动态获取的权限未全部获得
*/
@RequiresApi(Build.VERSION_CODES.M)
private static boolean permissionNotGranted(Activity activity, String... permission) {
for (String value : permission) {
if (activity.checkSelfPermission(value) != PackageManager.PERMISSION_GRANTED) {
return true;
}
}
return false;
}
/**
* @return true: 所需要的权限未全部已获取,需申请权限
*/
private static boolean needRequestPermission(Activity activity, String... permission) {
return versionOverM() && permissionNotGranted(activity, permission);
}
处理申请结果回调
@RequiresApi(api = Build.VERSION_CODES.M)
@Override
public void onRequestPermissionsResult(int requestCode, @NonNull String[] permissions, @NonNull int[] grantResults) {
super.onRequestPermissionsResult(requestCode, permissions, grantResults);
PermissionUtils.dispatchPermissionResult(this, requestCode, permissions, grantResults);
switch (requestCode) {
//do what you need
}
}
/**
* 申请权限后,是否所有的权限都申请成功
*
* @return true:所有权限申请成功
*/
public static boolean isAllPermissionsGranted(int... permissions) {
if (permissions == null) {
return true;
}
for (int p : permissions) {
if (p != PackageManager.PERMISSION_GRANTED) {
return false;
}
}
return true;
}
/**
* 权限申请统一处理信息
*/
@RequiresApi(api = Build.VERSION_CODES.M)
public static void dispatchPermissionResult(@NonNull Activity activity, int requestCode, @NonNull String[] permissions, @NonNull int[] grantResults) {
if (permissions.length == 0 || grantResults.length == 0) {
return;
}
for (int index = 0; index < permissions.length; index++) {
if (grantResults[index] != PackageManager.PERMISSION_GRANTED
&& !activity.shouldShowRequestPermissionRationale(permissions[index])) {
//用户之前拒绝,并勾选不再提示时,在此引导用户去设置页设置权限
//jumpToSettingPage(activity);
}
}
}
/**
* 帮跳转到该应用的设置界面,让用户手动授权
*/
private static void jumpToSettingPage(@NonNull Activity activity) {
Intent intent = new Intent(Settings.ACTION_APPLICATION_DETAILS_SETTINGS);
Uri uri = Uri.fromParts("package", activity.getPackageName(), null);
intent.setData(uri);
activity.startActivity(intent);
}
参考文档
推荐阅读