Android新特性之6.0运行时权限
Posted 王梵
tags:
篇首语:本文由小常识网(cha138.com)小编为大家整理,主要介绍了Android新特性之6.0运行时权限相关的知识,希望对你有一定的参考价值。
友情提示:只想看实现代码,可直接看第3部分:代码申请危险权限的步骤
一、简述
为了保证系统完整和保护用户隐私,每个app都运行在一个访问受限的沙盒中。app想要使用沙盒外的资源或信息,必须非常明确地请求适当权限。有的权限,系统会自动地给予,而有一些权限则必须用户主动授权。
从6.0开始,用户授予app权限的时机,从安装时改为了运行时。
这个改变不仅简化了app安装过程,在安装或更新时用户不再需要授予权限。同时也给予了用户更多的权力,用来控制app的功能。
比如,对于一个照相机类app,用户觉得照相机的权限可以给,但定位的权限不需要给,就可以不给这个权限。而且,用户还可以在任意时刻撤回已授予的权限,只需要进入Settings中去设置即可。即便app的target API小于6.0,只要手机系统版本大于等于6.0,那么就可以进入设置界面去拒绝权限。
如此一来,app需要保证在任何API水平下,失去了需要权限时,都能正常运行。
二、权限分级
在5.1或比5.1低的系统版本中,安装app时,会弹窗向用户展示声明的权限组列表(并不是某个具体的权限)。如果用户不授权,安装就会终止。
在6.0及以上版本,除了必须在清单文件中声明权限外,对于危险权限,必须在运行时进行申请。如果用户拒绝了权限申请,app就失去了相关功能,比如说读取手机联系人的功能。
非敏感权限,系统可以自动授权给app,敏感权限则必须要app主动申请。比如,打开闪光灯的权限,只要在清单文件中声明,系统会直接授权。但是,如果想要读取用户的联系人,app必须主动申请,让用户同意之后才会授予该权限。
在5.1或更低的版本中,系统会直接授予权限;在6.0或更高的版本,在运行时需要用户授予权限。
如果app需要当前应用不能产生的信息或资源,又或者是影响其它app或设备的操作时,是需要申请权限的。比如,联网操作,摄像头,打开/关闭wifi,这些行为都需要适当的权限。
对于其它app主动提供的信息,我们不需要申请权限。比如如果我们读取用户的联系人,需要申请READ_CONTACTS权限,但是如果我们通过intent的方式从联系人app中请求数据,则不需要任何权限。但是联系人app需要那些权限。
系统权限主要分2种,normal和dangerous
Normal permissions
Normal permissions,这类权限对用户隐私和其他app的操作的影响风险比较小,例如设置时区。这个级别的权限,只需在清单文件中声明就会自动被授予。
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_IGNORE_BATTERY_OPTIMIZATIONS |
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 |
Dangerous permissions
Dangerous permissions,与Normal级别的权限相比,Dangerous级别的权限,则可能会影响用户或其他app的存储数据。比如,读取用户的联系人列表就是一个非常危险的权限。如果app需要一个app权限,用户必须明确地授予该权限给app。
权限组
系统的所有危险权限都属于各个权限组,如果运行app的设备在6.0或者app的targetSdkVersion为23或者更高,当申请危险权限时,系统会表现出如下行为。
在申请权限组中的某个危险权限时,系统会弹窗提醒用户,弹窗内容并不会描述所申请的权限的信息,而是描述权限组的信息。比如,申请一个READ_CONTACTS权限,弹窗只会提醒用户,app需要获取设备的联系人权限。如果用户同意授权,系统只会给予刚才申请的权限,而不是权限组的所有权限。
在申请一个危险权限时,如果之前已经申请过和它同在一个权限组的危险权限,并且用户授予了时,那么系统会立刻授权这个权限,儿不会在弹窗提醒用户。比如之前已经获取到了READ_CONTACTS权限,在申请WRITE_CONTACTS权限时,系统会立刻授权此权限。
任何权限都能属于一个权限组,包括Normal级权限和自定义权限。不过,权限组只有包含危险权限时,才会影响用户体验,所以可以正常权限的权限组不予理会。
权限组(Permission Group) | 权限(Permissions) |
---|---|
CALENDAR | READ_CALENDAR WRITE_CALENDAR |
CAMERA | CAMERA |
CONTACTS | READ_CONTACTS WRITE_CONTACTS GET_ACCOUNTS |
LOCATION | ACCESS_FINE_LOCATION ACCESS_COARSE_LOCATION |
MICROPHONE | RECORD_AUDIO |
PHONE | READ_PHONE_STATE CALL_PHONE READ_CALL_LOG WRITE_CALL_LOG ADD_VOICEMAIL USE_SIP PROCESS_OUTGOING_CALLS |
SENSORS | BODY_SENSORS |
SMS | SEND_SMS RECEIVE_SMS READ_SMS RECEIVE_WAP_PUSH RECEIVE_MMS |
STORAGE | READ_EXTERNAL_STORAGE WRITE_EXTERNAL_STORAGE |
Special Permissions
特殊权限在文档中只有2个
- SYSTEM_ALERT_WINDOW
- WRITE_SETTINGS
它们的申请与危险权限有些不同,详见下面的代码。
三、代码申请危险权限的步骤
第1步:清单文件中声明权限
无论是哪个版本的系统系统,想要某种权限,都需要在清单文件中声明,normal和dangerous权限都是如此。
<manifest xmlns:android="http://schemas.android.com/apk/res/android"
package="com.wisely">
<uses-permission android:name="android.permission.READ_SMS"/>
<application ...>
...
</application>
</manifest>
第2步: 检查权限
由于用户可以在设置界面,主动拒绝权限,所以每次进行与危险权限有关的操作时,都必须要检查是否拥有该权限。
//this就是当前activity
int permissionCheck = ContextCompat.checkSelfPermission(this, Manifest.permission.WRITE_CALENDAR);
//PackageManager.PERMISSION_GRANTED:已经拥有了权限
//PackageManager.PERMISSION_DENIED:已经拒绝了权限
if(permissionCheck == PackageManager.PERMISSION_GRANTED)
else if(permissionCheck == PackageManager.PERMISSION_DENIED)
如果检查权限的返回值为PackageManager.PERMISSION_GRANTED,app就可以进行与这个权限有关的操作。如果返回值为PackageManager.PERMISSION_DENIED,app必须再次申请权限。
第3步:申请权限
一些情况下,需要告知用户,为什么app需要某个权限。比如一个摄影app,用户对于需要摄像头权限肯定不会奇怪,但需要定位或者联系人权限,用户就会不理解,这时候就需要我们给用户解释一下。
当用户已经拒绝了一次权限申请时,我们可以向用户解释一下为什么需要这个权限。
Google提供了一个工具方法shouldShowRequestPermissionRationale(),如果之前已经申请过权限,而且用户拒绝了申请,这个方法就会返回true。
如果用户拒绝了权限申请,并且选择了Don`t ask again选项,该方法的返回值为false。如果设备策略阻止app获取权限,这个方法的返回值一直都是false。
public final int MY_PERMISSIONS_REQUEST_READ_CALENDAR = 1;
//this就是当前activity
int permissionCheck = ContextCompat.checkSelfPermission(this, Manifest.permission.WRITE_CALENDAR);
if(permissionCheck != PackageManager.PERMISSION_GRANTED)
//是否向用户解释
if(ActivityCompat.shouldShowRequestPermissionRationale(this,Manifest.permission.WRITE_CALENDAR))
//异步向用户解释-不阻塞线程。用户看到了解释之后,再次尝试申请权限
else
//不需要解
ActivityCompat.requestPermissions(this,new String[]Manifest.permission.WRITE_CALENDAR,MY_PERMISSIONS_REQUEST_READ_CALENDAR);
else
调用方法requestPermissions()时,系统会向用户弹出一个标准弹窗。这个弹窗是不能定制的。如果想要向用户展示一些解释信息,必须在requestPermissions()之前。
第4步:处理权限请求响应
用户响应了申请权限的系统弹窗后,系统会调用onRequestPermissionsResult() 方法来传递用户的响应。可以通过请求码(request code)来区分用户响应了哪个权限的申请。
//grantResults的数组长度与申请权限时传入的一样
public void onRequestPermissionsResult(int requestCode, String[] permissions, int[] grantResults)
switch (requestCode)
case MY_PERMISSIONS_REQUEST_READ_CALENDAR:
//如果请求被cancell了,grantResults是空的
if(grantResults.length > 0 && grantResults[0] == PackageManager.PERMISSION_GRANTED)
//获取到了WRITE_CALENDAR权限,可以在这里进行与WRITE_CALENDAR权限有关的操作
else
//权限申请被拒绝,依赖该权限的功能不能再使用了。
break;
系统弹窗展示的是权限组信息,并不是申请的某个权限的信息。比如,申请 READ_CONTACTS权限,系统弹窗只会说需要访问设备联系人权限。对于每一个权限组,用户只需要授权一次,同组内的其它权限就会自动被授权。
app需要申请每一个需要的危险权限,即便用户已经授权了权限组中的某一个权限,因为在未来的release版本中,对于权限的分组可能会有改动。
比如,已经在清单文件中声明了 READ_CONTACTS和WRITE_CONTACTS权限,申请 READ_CONTACTS权限,用户已经授权。当再次申请WRITE_CONTACTS权限时,系统会立即授权,而不会弹框提示用户。
总结
shouldShowRequestPermissionRationale()方法
- 第一次申请权限时,方法返回值为false。
- 第2次申请权限时,返回值为true,如果一直拒绝授予权限,该返回值一直为true。当授予权限后该值为false。
- 当选择了Don`t ask me,拒绝授予权限后,该方法返回值为false。
requestPermissions()方法
- 当选择了Don`t ask me,并拒绝授予权限后,调用该方法就不再弹出系统权限框,但回调onRequestPermissionsResult()会运行,与拒绝时的参数是一样的。
- 当授予权限后,弹窗不出现,仍然会运行回调onRequestPermissionsResult(),参数grantResults与授予权限时没有不同。
- 拒绝授予权限后,一直调用该方法申请权限,然后一直拒绝,回调方法中的参数与第一次是一样的。
四、申请特殊权限
- 通过隐式调用,打开申请特殊权限界面,如图3。
public final int REQUEST_PARTICULAR_PERMISSION_CODE = 2;
private void requestParticularPermission()
//Settings.ACTION_MANAGE_WRITE_SETTINGS对应android.permission.WRITE_SETTINGS权限
//Settings.ACTION_MANAGE_OVERLAY_PERMISSION对应android.permission.SYSTEM_ALERT_WINDOW权限
Intent intent = new Intent(Settings.ACTION_MANAGE_OVERLAY_PERMISSION);
intent.setData(Uri.parse("package:"+getPackageName()));
startActivityForResult(intent,REQUEST_PARTICULAR_PERMISSION_CODE);
- 在回调方法onActivityResult()中判断是否已经获取了权限
protected void onActivityResult(int requestCode, int resultCode, Intent data)
switch (requestCode)
case REQUEST_PARTICULAR_PERMISSION_CODE:
if(Build.VERSION.SDK_INT > Build.VERSION_CODES.M)
if (Settings.System.canWrite(this))
Logger.d("已经获取了悬浮窗权限");
else
Logger.d("拒绝了悬浮窗权限");
break;
与申请危险权限不同,申请特殊权限是通过隐式启动打开Settings界面,相当于打开了另一个app。所以说,即便你已经获取到了权限,那么只要你还通过intent打开Settings界面,仍旧是能打开的。
如果获取到了权限,那么打开Settings界面时,权限的选择开关是默认打开的。反之则是默认没有打开。
回调方法中加上版本的检查,是因为Settings.System.canWrite(this)是从API 23开始的。
五、运行时权限踩过的坑
001号坑:需求是获取到IMEI号,其它手机还可以,但在小米手机上出现了bug。
用户授予app权限,然后在设置中拒绝权限,那么在调用checkSelfPermission()方法时,返回值为true,但获取不到IEMI号码。
002号坑:特殊权限中的SYSTEM_ALERT_WINDOW,无法获取到该权限。
通过隐式启动设置界面,switch开关打开后返回,在onActivityResult()中调用Settings.System.canWrite(this)方法的返回值一直是false,也就是一直拒绝授予此权限。
另一方面,如果上次选择授予了该权限,那么下次进入后,switch开关是打开的,这点正常。另一个特殊权限WRITE_SETTINGS则可以正常申请,没有异常。
有一篇博客是讲这个的,Android M WRITE_SETTINGS权限的一个BUG
以上是关于Android新特性之6.0运行时权限的主要内容,如果未能解决你的问题,请参考以下文章
Android数据存储之Android 6.0运行时权限下文件存储的思考
1.Android6.0运行时权限简介_2.Android6.0权限适配之WRITE_EXTERNAL_STORAGE(SD卡写入)3_.Android 6.0 运行时权限理解