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)
CALENDARREAD_CALENDAR WRITE_CALENDAR
CAMERACAMERA
CONTACTSREAD_CONTACTS WRITE_CONTACTS GET_ACCOUNTS
LOCATIONACCESS_FINE_LOCATION ACCESS_COARSE_LOCATION
MICROPHONERECORD_AUDIO
PHONEREAD_PHONE_STATE CALL_PHONE READ_CALL_LOG WRITE_CALL_LOG ADD_VOICEMAIL USE_SIP PROCESS_OUTGOING_CALLS
SENSORSBODY_SENSORS
SMSSEND_SMS RECEIVE_SMS READ_SMS RECEIVE_WAP_PUSH RECEIVE_MMS
STORAGEREAD_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与授予权限时没有不同。
    • 拒绝授予权限后,一直调用该方法申请权限,然后一直拒绝,回调方法中的参数与第一次是一样的。

四、申请特殊权限


  1. 通过隐式调用,打开申请特殊权限界面,如图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);
    
  1. 在回调方法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 6.0+ 运行时权限探索

Android 6.0 变更

Android数据存储之Android 6.0运行时权限下文件存储的思考

1.Android6.0运行时权限简介_2.Android6.0权限适配之WRITE_EXTERNAL_STORAGE(SD卡写入)3_.Android 6.0 运行时权限理解

安卓6.0(棉花糖)新特性汇总

Android 6.0 运行时权限管理