Android PermissionChecker 权限全面详细分析和解决方案

原文:

http://www.2cto.com/kf/201512/455888.html

http://blog.csdn.net/yangqingqo/article/details/48371123

http://inthecheesefactory.com/blog/things-you-need-to-know-about-Android-m-permission-developer-edition/en

一、Marshmallow版本权限简介

android的权限系统一直是首要的安全概念,因为这些权限只在安装的时候被询问一次。一旦安装了,app可以在用户毫不知晓的情况下访问权限内的所有东西,而且一般用户安装的时候很少会去仔细看权限列表,更不会去深入了解这些权限可能带来的相关危害。所以在android 6.0 Marshmallow版本之后,系统不会在软件安装的时候就赋予该app所有其申请的权限,对于一些危险级别的权限,app需要在运行时一个一个询问用户授予权限。

Android PermissionChecker 权限全面详细分析和解决方案

二、旧版本app兼容问题

  那么问题来了,是不是所有以前发布的app都会出现问题呢?答案是不会,只有那些targetSdkVersion 设置为23和23以上的应用才会出现异常,在使用危险权限的时候系统必须要获得用户的同意才能使用,要不然应用就会崩溃,出现类似
java.lang.SecurityException: Permission Denial: reading com.android.providers.media.MediaProvider

的崩溃日志。所以targetSdkVersion如果没有设置为23版本或者以上,系统还是会使用旧规则:在安装的时候赋予该app所申请的所有权限。所以app当然可以和以前一样正常使用了,但是还有一点需要注意的是6.0的系统里面,用户可以手动将该app的权限关闭,如下图

Android PermissionChecker 权限全面详细分析和解决方案

  
  那么问题又来了,如果以前的老应用申请的权限被用户手动关闭了怎么办,应用会崩溃么?我们来试一试
  这里写图片描述
  好吧,可以庆幸了一下了,不会抛出异常,不会崩溃,只不过调用那些被用户禁止权限的api接口返回值都为null或者0,所以我们只需要做一下判空操作就可以了,不判空当然还是会崩溃的喽。
Android PermissionChecker 权限全面详细分析和解决方案

三、普通权限和危险权限列表

  现在对于新版本的权限变更应该有了基本的认识,那么,是不是所有权限都需要去进行特殊处理呢?当然不是,只有那些危险级别的权限才需要。

PROTECTION_NORMAL类权限

当用户安装或更新应用时,系统将授予应用所请求的属于 PROTECTION_NORMAL 的所有权限(安装时授权的一类基本权限)。这类权限包括:

android.permission.ACCESS LOCATIONEXTRA_COMMANDS 
android.permission.ACCESS NETWORKSTATE 
android.permission.ACCESS NOTIFICATIONPOLICY 
android.permission.ACCESS WIFISTATE 
android.permission.ACCESS WIMAXSTATE 
android.permission.BLUETOOTH 
android.permission.BLUETOOTH_ADMIN 
android.permission.BROADCAST_STICKY 
android.permission.CHANGE NETWORKSTATE 
android.permission.CHANGE WIFIMULTICAST_STATE 
android.permission.CHANGE WIFISTATE 
android.permission.CHANGE WIMAXSTATE 
android.permission.DISABLE_KEYGUARD 
android.permission.EXPAND STATUSBAR 
android.permission.FLASHLIGHT 
android.permission.GET_ACCOUNTS 
android.permission.GET PACKAGESIZE 
android.permission.INTERNET 
android.permission.KILL BACKGROUNDPROCESSES 
android.permission.MODIFY AUDIOSETTINGS 
android.permission.NFC 
android.permission.READ SYNCSETTINGS 
android.permission.READ SYNCSTATS 
android.permission.RECEIVE BOOTCOMPLETED 
android.permission.REORDER_TASKS 
android.permission.REQUEST INSTALLPACKAGES 
android.permission.SET TIMEZONE 
android.permission.SET_WALLPAPER 
android.permission.SET WALLPAPERHINTS 
android.permission.SUBSCRIBED FEEDSREAD 
android.permission.TRANSMIT_IR 
android.permission.USE_FINGERPRINT 
android.permission.VIBRATE 
android.permission.WAKE_LOCK 
android.permission.WRITE SYNCSETTINGS 
com.android.alarm.permission.SET_ALARM 
com.android.launcher.permission.INSTALL_SHORTCUT 
com.android.launcher.permission.UNINSTALL_SHORTCUT

这类权限只需要在AndroidManifest.xml中简单声明这些权限就好,安装时就授权。不需要每次使用时都检查权限,而且用户不能取消以上授权。

危险权限

Permission Group Permissions
android.permission-group.CALENDAR
  • android.permission.READ_CALENDAR
  • android.permission.WRITE_CALENDAR
android.permission-group.CAMERA
  • android.permission.CAMERA
android.permission-group.CONTACTS
  • android.permission.READ_CONTACTS
  • android.permission.WRITE_CONTACTS
  • android.permission.GET_ACCOUNTS
android.permission-group.LOCATION
  • android.permission.ACCESS_FINE_LOCATION
  • android.permission.ACCESS_COARSE_LOCATION
android.permission-group.MICROPHONE
  • android.permission.RECORD_AUDIO
android.permission-group.PHONE
  • android.permission.READ_PHONE_STATE
  • android.permission.CALL_PHONE
  • android.permission.READ_CALL_LOG
  • android.permission.WRITE_CALL_LOG
  • com.android.voicemail.permission.ADD_VOICEMAIL
  • android.permission.USE_SIP
  • android.permission.PROCESS_OUTGOING_CALLS
android.permission-group.SENSORS
  • android.permission.BODY_SENSORS
android.permission-group.SMS
  • android.permission.SEND_SMS
  • android.permission.RECEIVE_SMS
  • android.permission.READ_SMS
  • android.permission.RECEIVE_WAP_PUSH
  • android.permission.RECEIVE_MMS
  • android.permission.READ_CELL_BROADCASTS
android.permission-group.STORAGE
  • android.permission.READ_EXTERNAL_STORAGE
  • android.permission.WRITE_EXTERNAL_STORAGE

  android开发者官网也有相关描述:
  http://developer.android.com/training/permissions/requesting.html
  http://developer.android.com/guide/topics/security/permissions.html

  所以仔细去看看自己的app,对照列表,如果有需要申请其中的一个权限,就需要进行特殊操作。还有一个比较人性的地方就是如果同一组的任何一个权限被授权了,其他权限也自动被授权。例如,一旦WRITE_EXTERNAL_STORAGE被授权了,app也有READ_EXTERNAL_STORAGE权限了。

四、支持Marshmallow新版本权限机制

关于权限控制主要使用到

PermissionChecker类的checkSelfPermission();

ActivityCompat类的

public static boolean shouldShowRequestPermissionRationale(@NonNull Activity activity,
            @NonNull String permission)

Fragment类的

public boolean shouldShowRequestPermissionRationale(@NonNull String permission)

ActivityCompat类的

public static void requestPermissions(final @NonNull Activity activity,
            final @NonNull String[] permissions, final int requestCode)

Fragment类的

public final void requestPermissions(@NonNull String[] permissions, int requestCode)

终于要开始支持android 6.0版本了,最先一步当然就是修改build.gradle文件中的tragetSdkVersion和compileSdkVersion成23版本,同时使用compile ‘com.android.support:appcompat-v7:23.1.1’最新v7包。

android {
    compileSdkVersion 23
    ...
 
    defaultConfig {
        ...
        targetSdkVersion 23
        ...
    }
}
...
dependencies {
...
compile 'com.android.support:appcompat-v7:23.1.1'
  修改完后,感兴趣的朋友可以直接打包在手机上测试一下,看看是不是会出现类似于上面我说的那些崩溃日志。
  接着下一步当然就是要修改代码了,最原始代码,无任何处理:

private void startGetImageThread(){
....
    Uri uri = MediaStore.Images.Media.EXTERNAL_CONTENT_URI;
    ContentResolver contentResolver = getContentResolver();
    //获取jpeg和png格式的文件,并且按照时间进行倒序
    Cursor cursor = contentResolver.query(uri, null, MediaStore.Images.Media.MIME_TYPE + "=\"image/jpeg\" or " +
    MediaStore.Images.Media.MIME_TYPE + "=\"image/png\"", null, MediaStore.Images.Media.DATE_MODIFIED+" desc");
    ....
}
  这段代码需要访问外部存储(相册图片),属于危险级别的权限,直接使用会造成应用崩溃,所以在这段代码执行之前我们需要进行特殊处理:

int hasWriteContactsPermission = checkSelfPermission(Manifest.permission.WRITE_EXTERNAL_STORAGE);

if (hasWriteContactsPermission != PackageManager.PERMISSION_GRANTED) {

Activity activty=this;

ActivityCompat.requestPermissions(activty,new String[] {Manifest.permission.WRITE_EXTERNAL_STORAGE},
                CODE_FOR_WRITE_PERMISSION);
    return;
}
  写完这段代码之后,就会出现如下系统dialog:
  Android PermissionChecker 权限全面详细分析和解决方案
  紧接着就需要去处理DENY和ALLOW的回调了,重写 Activity activity的ActivityCompat.OnRequestPermissionsResultCallback函数:

@Override
public void onRequestPermissionsResult(int requestCode, String[] permissions, int[] grantResults) {
    if (requestCode == CODE_FOR_WRITE_PERMISSION){
        if (permissions[0].equals(Manifest.permission.WRITE_EXTERNAL_STORAGE)
            &&grantResults[0] == PackageManager.PERMISSION_GRANTED){
            //用户同意使用write
            startGetImageThread();
        }else{
            //用户不同意,自行处理即可
            finish();
        }
    }
}
  好了,这样就算是简单初步适配完成了。

五、处理不再提醒

  如果用户拒绝某授权。下一次弹框,用户会有一个“不再提醒”的选项的来防止app以后继续请求授权。

  Android PermissionChecker 权限全面详细分析和解决方案

  如果这个选项在拒绝授权前被用户勾选了。下次为这个权限请求requestPermissions时,对话框就不弹出来了,系统会直接回调onRequestPermissionsResult函数,回调结果为最后一次用户的选择。所以为了应对这种情况,系统提供了一个shouldShowRequestPermissionRationale()函数,这个函数的作用是帮助开发者找到需要向用户额外解释权限的情况,这个函数:
应用安装后第一次访问,直接返回false;第一次请求权限时,用户拒绝了,下一次shouldShowRequestPermissionRationale()返回 true,这时候可以显示一些为什么需要这个权限的说明;第二次请求权限时,用户拒绝了,并选择了“不再提醒”的选项时:shouldShowRequestPermissionRationale()返回 false;设备的系统设置中禁止当前应用获取这个权限的授权,shouldShowRequestPermissionRationale()返回false;  注意:第二次请求权限时,才会有“不再提醒”的选项,如果用户一直拒绝,并没有选择“不再提醒”的选项,下次请求权限时,会继续有“不再提醒”的选项,并且shouldShowRequestPermissionRationale()也会一直返回true。
  所以利用这个函数我们可以进行相应的优化,针对shouldShowRequestPermissionRationale函数返回false的处理有两种方案。第一种方案:如果应用是第一次请求该权限,则直接调用requestPermissions函数去请求权限;如果不是则代表用户勾选了’不再提醒’,弹出dialog,告诉用户为什么你需要该权限,让用户自己手动开启该权限。链接:http://*.com/questions/32347532/android-m-permissions-confused-on-the-usage-of-shouldshowrequestpermissionrati 。第二种方案:在onRequestPermissionsResult函数中进行检测,如果返回PERMISSION_DENIED,则去调用shouldShowRequestPermissionRationale函数,如果返回false代表用户已经禁止该权限(上面的3和4两种情况),弹出dialog告诉用户你需要该权限的理由,让用户手动打开。链接:http://*.com/questions/30719047/android-m-check-runtime-permission-how-to-determine-if-the-user-checked-nev  处理方法已经有了,修改一下代码,我这里就以第二种方案来处理了:
 
@Override
public void onRequestPermissionsResult(int requestCode, String[] permissions, int[] grantResults) {
    if (requestCode == CODE_FOR_WRITE_PERMISSION){
        if (permissions[0].equals(Manifest.permission.WRITE_EXTERNAL_STORAGE)
            &&grantResults[0] == PackageManager.PERMISSION_GRANTED){
            //用户同意使用write
            startGetImageThread();
        }else{
            //用户不同意,向用户展示该权限作用
            if (!ActivityCompat.shouldShowRequestPermissionRationale(this, Manifest.permission.WRITE_EXTERNAL_STORAGE)) {
                AlertDialog dialog = new AlertDialog.Builder(this)
                        .setMessage("该相册需要赋予访问存储的权限,不开启将无法正常工作!")
                        .setPositiveButton("确定", new DialogInterface.OnClickListener() {
                            @Override
                            public void onClick(DialogInterface dialog, int which) {
                                finish();
                            }
                        })
                        .setNegativeButton("取消", new DialogInterface.OnClickListener() {
                            @Override
                            public void onClick(DialogInterface dialog, int which) {
                                finish();
                            }
                        }).create();
                dialog.show();
                return;
            }
            finish();
        }
    }
}
  当勾选不再提醒,并且拒绝之后,弹出dialog,提醒用户该权限的重要性:
  
  Android PermissionChecker 权限全面详细分析和解决方案

六、使用兼容库

  以上的代码在6.0版本上使用没有问题,但是在之前就有问题了,最简单粗暴的解决方法可能就是利用Build.VERSION.SDK_INT >= 23这个判断语句来判断了,方便的是SDK 23的v4包加入了专门类进行相关的处理:

ContextCompat.checkSelfPermission()被授权函数返回PERMISSION_GRANTED,否则返回PERMISSION_DENIED ,在所有版本都是如此。ActivityCompat.requestPermissions()这个方法在6.0之前版本调用,OnRequestPermissionsResultCallback 直接被调用,带着正确的 PERMISSION_GRANTED或者PERMISSION_DENIED。ActivityCompat.shouldShowRequestPermissionRationale()在6.0之前版本调用,永远返回false。  用v4包的这三方法,完美兼容所有版本!下面是代码:
//使用兼容库就无需判断系统版本
int hasWriteContactsPermission = ContextCompat.checkSelfPermission(getApplication(), Manifest.permission.WRITE_EXTERNAL_STORAGE);
if (hasWriteContactsPermission == PackageManager.PERMISSION_GRANTED) {
    startGetImageThread();
}
//需要弹出dialog让用户手动赋予权限
else{
    ActivityCompat.requestPermissions(PickOrTakeImageActivity.this, new String[]{Manifest.permission.WRITE_EXTERNAL_STORAGE}, CODE_FOR_WRITE_PERMISSION);

}

  onRequestPermissionsResult函数不变。后两个方法,我们也可以在Fragment中使用,用v13兼容包:FragmentCompat.requestPermissions() and FragmentCompat.shouldShowRequestPermissionRationale()和activity效果一样。

七、一次请求多个权限

  当然了有时候需要多个权限,可以用上面方法一次请求多个权限。当然最重要的是不要忘了为每个权限检查“不再提醒”的设置。
List<string> permissionsNeeded = new ArrayList<string>();
permissionsNeeded.add(Manifest.permission.ACCESS_FINE_LOCATION);
permissionsNeeded.add(Manifest.permission.READ_CONTACTS);
permissionsNeeded.add(Manifest.permission.WRITE_CONTACTS);
requestPermissions(permissionsNeeded.toArray(new String[permissionsList.size()]), CODE_FOR_MULTIPLE_PERMISSION);</string></string>
  最后在onRequestPermissionsResult函数中一个个处理返回结果即可。

八、第三方库简化代码

  当然早就有第三方库来帮忙做这些事情了:
  Github上的开源项目 PermissionHelper和hotchemi’s PermissionsDispatcher

九、APP处于运行状态下,被撤销权限

  如果APP正在运行中,用户进入设置-应用程序页面去手动撤销该APP权限,会出现什么情况呢?哈哈,系统又会接着弹出权限请求对话框,挺好挺好:
  Android PermissionChecker 权限全面详细分析和解决方案
  这样就没有问题了吧O(∩_∩)O~
  上面的测试环境为genymotion6.0模拟器,有朋友跟我反映在6.0nexus 6p真机上会直接退出应用,所以这个应该还和测试环境有关。

使用兼容库support-v4中的方法

The v4 support library also contains the PermissionChecker class, which provides several static utility methods for apps that use IPC to provide services for other apps. For example,PermissionChecker.checkCallingPermission() checks whether an IPC made by a particular package has a specified permission.



 requestPermissions() 的一些说明:

Note: When your app calls the framework's requestPermissions() method, the system shows a standard dialog box to the user.
 Your app cannot configure or alter that dialog box. If you need to provide any information or explanation to the user, 
you should do that before you call requestPermissions(), as described in Explain why the app needs permissions.

当调用 requestPermissions() 时,系统会显示一个获取权限的提示对话框,当前应用不能配置和修改这个对话框,
如果需要提示用户一些这个权限相关的信息或说明,需要在调用 requestPermissions() 之前处理。



To help find the situations where you need to provide extra explanation, the system provides theshouldShowRequestPermissionRationale() method.

This method returns true if the app has requested this permission previously and the user denied the request.

That indicates that you should probably explain to the user why you need the permission.

If the user turned down the permission request in the past and chose the Don't ask again option in the permission request system dialog, this method returns false.

The method also returns false if the device policy prohibits the app from having that permission.

1. 第一次请求权限时,用户拒绝了,下一次:shouldShowRequestPermissionRationale()  返回 true,应该显示一些为什么需要这个权限的说明

2.第二次请求权限时,用户拒绝了,并选择了“不在提醒”的选项时:shouldShowRequestPermissionRationale()  返回 false

3. 设备的策略禁止当前应用获取这个权限的授权:shouldShowRequestPermissionRationale()  返回 false

注意:上面的:第二次请求权限时,才会有“不在提醒”的选项,如果用户一直拒绝,并没有选择“不在提醒”的选项,下次请求权限时,会继续有“不在提醒”的选项

十、shouldShowRequestPermissionRationale() 的方法说明:

Gets whether you should show UI with rationale for requesting a permission.

You should do this only if you do not have the permission and the context in which the permission is requested does not clearly communicate to the user what would be the benefit from granting this permission.

For example, if you write a camera app, requesting the camera permission would be expected by the user and no rationale for why it is requested is needed. If however, the app needs location for tagging photos then a non-tech savvy user may wonder how location is related to taking photos. In this case you may choose to show UI with rationale of requesting this permission.

根据方法说明:
显示权限说明:是根据你的应用中使用的权限分类来的:
1.用户容易知道应用需要获取的权限:如一个拍照应用,需要摄像头的权限,是很正常,不用提示。
2.一些用户感觉困惑的一些权限:如:分享图片,还需要获取位置的权限,这个需要提示用户:为什么需要这个权限。

上一篇:android 08 AndroidManifest.xml


下一篇:Android 签名(5)用命令签名和用IDE签名