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

原文:html

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

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

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

 

1、Marshmallow版本权限简介安全

android的权限系统一直是首要的安全概念,由于这些权限只在安装的时候被询问一次。一旦安装了,app能够在用户绝不知晓的状况下访问权限内的全部东西,并且通常用户安装的时候不多会去仔细看权限列表,更不会去深刻了解这些权限可能带来的相关危害。因此在android 6.0 Marshmallow版本以后,系统不会在软件安装的时候就赋予该app全部其申请的权限,对于一些危险级别的权限,app须要在运行时一个一个询问用户授予权限。app



2、旧版本app兼容问题

  那么问题来了,是否是全部之前发布的app都会出现问题呢?答案是不会,只有那些targetSdkVersion 设置为23和23以上的应用才会出现异常,在使用危险权限的时候系统必需要得到用户的赞成才能使用,要否则应用就会崩溃,出现相似
java.lang.SecurityException: Permission Denial: reading com.android.providers.media.MediaProvideride

的崩溃日志。因此targetSdkVersion若是没有设置为23版本或者以上,系统仍是会使用旧规则:在安装的时候赋予该app所申请的全部权限。因此app固然能够和之前同样正常使用了,可是还有一点须要注意的是6.0的系统里面,用户能够手动将该app的权限关闭,以下图函数

  
  那么问题又来了,若是之前的老应用申请的权限被用户手动关闭了怎么办,应用会崩溃么?咱们来试一试
  这里写图片描述
  好吧,能够庆幸了一下了,不会抛出异常,不会崩溃,只不过调用那些被用户禁止权限的api接口返回值都为null或者0,因此咱们只须要作一下判空操做就能够了,不判空固然仍是会崩溃的喽。


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

测试

  如今对于新版本的权限变动应该有了基本的认识,那么,是否是全部权限都须要去进行特殊处理呢?固然不是,只有那些危险级别的权限才须要。gradle

 

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权限了。

 

4、支持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:
  
  紧接着就须要去处理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();
        }
    }
}
  好了,这样就算是简单初步适配完成了。


5、处理再也不提醒

  若是用户拒绝某受权。下一次弹框,用户会有一个“再也不提醒”的选项的来防止app之后继续请求受权。

  

  若是这个选项在拒绝受权前被用户勾选了。下次为这个权限请求requestPermissions时,对话框就不弹出来了,系统会直接回调onRequestPermissionsResult函数,回调结果为最后一次用户的选择。因此为了应对这种状况,系统提供了一个shouldShowRequestPermissionRationale()函数,这个函数的做用是帮助开发者找到须要向用户额外解释权限的状况,这个函数:
应用安装后第一次访问,直接返回false;第一次请求权限时,用户拒绝了,下一次shouldShowRequestPermissionRationale()返回 true,这时候能够显示一些为何须要这个权限的说明;第二次请求权限时,用户拒绝了,并选择了“再也不提醒”的选项时:shouldShowRequestPermissionRationale()返回 false;设备的系统设置中禁止当前应用获取这个权限的受权,shouldShowRequestPermissionRationale()返回false;  注意:第二次请求权限时,才会有“再也不提醒”的选项,若是用户一直拒绝,并无选择“再也不提醒”的选项,下次请求权限时,会继续有“再也不提醒”的选项,而且shouldShowRequestPermissionRationale()也会一直返回true。
  因此利用这个函数咱们能够进行相应的优化,针对shouldShowRequestPermissionRationale函数返回false的处理有两种方案。第一种方案:若是应用是第一次请求该权限,则直接调用requestPermissions函数去请求权限;若是不是则表明用户勾选了’再也不提醒’,弹出dialog,告诉用户为何你须要该权限,让用户本身手动开启该权限。连接:http://stackoverflow.com/questions/32347532/android-m-permissions-confused-on-the-usage-of-shouldshowrequestpermissionrati 。第二种方案:在onRequestPermissionsResult函数中进行检测,若是返回PERMISSION_DENIED,则去调用shouldShowRequestPermissionRationale函数,若是返回false表明用户已经禁止该权限(上面的3和4两种状况),弹出dialog告诉用户你须要该权限的理由,让用户手动打开。连接:http://stackoverflow.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,提醒用户该权限的重要性:
  
  


6、使用兼容库


  以上的代码在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效果同样。



7、一次请求多个权限


  固然了有时候须要多个权限,能够用上面方法一次请求多个权限。固然最重要的是不要忘了为每一个权限检查“再也不提醒”的设置。
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函数中一个个处理返回结果便可。


8、第三方库简化代码


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


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


  若是APP正在运行中,用户进入设置-应用程序页面去手动撤销该APP权限,会出现什么状况呢?哈哈,系统又会接着弹出权限请求对话框,挺好挺好:
  
  这样就没有问题了吧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 

 

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

 

10、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.一些用户感受困惑的一些权限:如:分享图片,还须要获取位置的权限,这个须要提示用户:为何须要这个权限。