Android 6.0的运行时权限

 

       Android 6.0,代号棉花糖,自发布伊始,其主要的特征运行时权限就很受关注。因为这一特征不仅改善了用户对于应用的使用体验,还使得应用开发者在实践开发中需要做出改变。
       没有深入了解运行时权限的开发者通常会有很多疑问,比如什么是运行时权限,哪些是运行时的权限,我的应用是不是会在6.0系统上各种崩溃呢,如何才能支持运行时权限机制呢。本文讲尝试回答这一些问题,希望读者阅读完成之后,都能找到较为完美的答案。


权限一刀切


在6.0以前的系统,都是权限一刀切的处理方式,只要用户安装,Manifest申请的权限都会被赋予,并且安装后权限也撤销不了。
这种情况下,当我们从Google Play安装一个应用,在安装之前会得到这样的权限提示信息。



当上述对话框弹出后,用户只有两种选择:

  • 我信任你,即使有敏感权限
  • 你一个**应用,要这个权限干嘛,我还是不安装了。


所以,这种一刀切的处理方式还是有弊端的,我们没有办法只允许某些权限或者拒绝某些权限。


棉花糖运行时权限


从棉花糖开始,Android系统引入了新的权限机制,即本文要讲的运行时权限。
何为运行时权限呢?举个栗子,以某个需要拍照的应用为例,当运行时权限生效时,其Camera权限不是在安装后赋予,而是在应用运行的时候进行请求权限(比如当用户按下”相机拍照“按钮后)看到的效果则是这样的




接下来,对于Camera权限的处理完全权交给用户。是不是有点像苹果系统的处理呢,不要说这是抄袭,暂且称为师夷长技以制夷。


权限的分组


Android中有很多权限,但并非所有的权限都是敏感权限,于是6.0系统就对权限进行了分类,一般为下述几类

  • 正常(Normal Protection)权限
  • 危险(Dangerous)权限
  • 特殊(Particular)权限
  • 其他权限(一般很少用到)


正常权限


正常权限具有如下的几个特点

  • 对用户隐私没有较大影响或者不会打来安全问题。
  • 安装后就赋予这些权限,不需要显示提醒用户,用户也不能取消这些权限。


正常权限列表

  1. ACCESS_LOCATION_EXTRA_COMMANDS
  2. ACCESS_NETWORK_STATE
  3. ACCESS_NOTIFICATION_POLICY
  4. ACCESS_WIFI_STATE
  5. BLUETOOTH
  6. BLUETOOTH_ADMIN
  7. BROADCAST_STICKY
  8. CHANGE_NETWORK_STATE
  9. CHANGE_WIFI_MULTICAST_STATE
  10. CHANGE_WIFI_STATE
  11. DISABLE_KEYGUARD
  12. EXPAND_STATUS_BAR
  13. GET_PACKAGE_SIZE
  14. INTERNET
  15. KILL_BACKGROUND_PROCESSES
  16. MODIFY_AUDIO_SETTINGS
  17. NFC
  18. READ_SYNC_SETTINGS
  19. READ_SYNC_STATS
  20. RECEIVE_BOOT_COMPLETED
  21. REORDER_TASKS
  22. REQUEST_INSTALL_PACKAGES
  23. SET_TIME_ZONE
  24. SET_WALLPAPER
  25. SET_WALLPAPER_HINTS
  26. TRANSMIT_IR
  27. USE_FINGERPRINT
  28. VIBRATE
  29. WAKE_LOCK
  30. WRITE_SYNC_SETTINGS
  31. SET_ALARM
  32. INSTALL_SHORTCUT
  33. UNINSTALL_SHORTCUT


上述的权限基本设计的是关于网络,蓝牙,时区,快捷方式等方面,只要在Manifest指定了这些权限,就会被授予,并且不能撤销。


特殊权限


这里讲特殊权限提前讲一下,因为这个相对来说简单一些。
特殊权限,顾名思义,就是一些特别敏感的权限,在Android系统中,主要由两个

  • SYSTEM_ALERT_WINDOW,设置悬浮窗,进行一些黑科技
  • WRITE_SETTINGS 修改系统设置

关于上面两个特殊权限的授权,做法是使用startActivityForResult启动授权界面来完成。


请求SYSTEM_ALERT_WINDOW

 1 private static final int REQUEST_CODE = 1;
 2 private  void requestAlertWindowPermission() {
 3     Intent intent = new Intent(Settings.ACTION_MANAGE_OVERLAY_PERMISSION);
 4     intent.setData(Uri.parse("package:" + getPackageName()));
 5     startActivityForResult(intent, REQUEST_CODE);
 6 }
 7 
 8 @Override
 9 protected void onActivityResult(int requestCode, int resultCode, Intent data) {
10     super.onActivityResult(requestCode, resultCode, data);
11     if (requestCode == REQUEST_CODE) {
12         if (Settings.canDrawOverlays(this)) {
13           Log.i(LOGTAG, "onActivityResult granted");
14         }
15     }
16 }

 

上述代码需要注意的是

  • 使用Action Settings.ACTION_MANAGE_OVERLAY_PERMISSION启动隐式Intent
  • 使用"package:" + getPackageName()携带App的包名信息
  • 使用Settings.canDrawOverlays方法判断授权结果


   请求WRITE_SETTINGS

 1 private static final int REQUEST_CODE_WRITE_SETTINGS = 2;
 2 private void requestWriteSettings() {
 3     Intent intent = new Intent(Settings.ACTION_MANAGE_WRITE_SETTINGS);
 4     intent.setData(Uri.parse("package:" + getPackageName()));
 5     startActivityForResult(intent, REQUEST_CODE_WRITE_SETTINGS );
 6 }
 7 @Override
 8 protected void onActivityResult(int requestCode, int resultCode, Intent data) {
 9     super.onActivityResult(requestCode, resultCode, data);
10     if (requestCode == REQUEST_CODE_WRITE_SETTINGS) {
11         if (Settings.System.canWrite(this)) {
12             Log.i(LOGTAG, "onActivityResult write settings granted" );
13         }
14     }
15 }

 

上述代码需要注意的是

  • 使用Action Settings.ACTION_MANAGE_WRITE_SETTINGS 启动隐式Intent
  • 使用"package:" + getPackageName()携带App的包名信息
  • 使用Settings.System.canWrite方法检测授权结果

注意:关于这两个特殊权限,一般不建议应用申请。

危险权限


危险权限实际上才是运行时权限主要处理的对象,这些权限可能引起隐私问题或者影响其他程序运行。Android中的危险权限可以归为以下几个分组:

  • CALENDAR
  • CAMERA
  • CONTACTS
  • LOCATION
  • MICROPHONE
  • PHONE
  • SENSORS
  • SMS
  • STORAGE

各个权限分组与其具体的权限,可以参考:

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

 

 

必须要支持运行时权限么


目前应用实际上是可以不需要支持运行时权限的,但是最终肯定还是需要支持的,只是时间问题而已。
想要不支持运行时权限机制很简单,只需要将targetSdkVersion设置低于23就可以了,意思是告诉系统,我还没有完全在API 23(6.0)上完全搞定,不要给我启动新的特性。


不支持运行时权限会崩溃么


可能会,但不是那种一上来就噼里啪啦崩溃不断的那种。
如果你的应用将targetSdkVersion设置低于23,那么在6.0的系统上不会为这个应用开启运行时权限机制,即按照以前的一刀切方式处理。

 


然而有点糟糕的是

6.0系统提供了一个应用权限管理界面,界面长得是这样的

 


既然是可以管理,用户就能取消权限,当一个不支持运行时权限的应用某项权限被取消时

 

 

系统会弹出一个对话框提醒撤销的危害,如果用户执意撤销,会带来如下的反应

  • 如果你的程序正在运行,则会被杀掉。
  • 当你的应用再次运行时,可能出现崩溃

为什么会可能崩溃的,比如下面这段代码

1 TelephonyManager telephonyManager = (TelephonyManager)getSystemService(Context.TELEPHONY_SERVICE);
2 String deviceId = telephonyManager.getDeviceId();
3 if (deviceId.equals(mLastDeviceId)) {//This may cause NPE
4   //do something
5 }

 

如果用户撤消了获取DeviceId的权限,那么再次运行时,deviceId就是null,如果程序后续处理不当,就会出现崩溃。


该来的还得来


6.0的运行时权限,我们最终都是要支持的,通常我们需要使用如下的API

  • int checkSelfPermission(String permission) 用来检测应用是否已经具有权限
  • void requestPermissions(String[] permissions, int requestCode) 进行请求单个或多个权限
  • void onRequestPermissionsResult(int requestCode, String[] permissions, int[] grantResults) 用户对请求作出响应后的回调

以一个请求电话权限为例

  第一步:核实权限 int checkSelfPermission(String permission)

1 if (ContextCompat.checkSelfPermission(this, Manifest.permission.CALL_PHONE)
2                 != PackageManager.PERMISSION_GRANTED) {
3     .......
4 }

 如果还没有获取到,则返回值为PackageManager.PERMISSION_DENIED,  如若已经获取到权限,则返回值是PackageManager.PERMISSION_GRANTED

 

 第二步: 请求权限 void requestPermissions(String[] permissions, int requestCode)

                第一步如果核实结果是未授权,则可以执行此步。

1 ActivityCompat.requestPermissions(this,
2                     new String[]{Manifest.permission.CALL_PHONE},
3                     MY_PERMISSIONS_REQUEST_CALL_PHONE);

该方法是异步的,第一个参数是Context;第二个参数是需要申请的权限的字符串数组;第三个参数为requestCode,主要用于回调的时候检测。可以从方法名requestPermissions以及第二个参数看出,是支持一次性申请多个权限的,系统会通过对话框逐一询问用户是否授权。

 

 ActivityCompat类中的代码如下, 如果API 版本达到了23

1 public static void requestPermissions(final @NonNull Activity activity,
2             final @NonNull String[] permissions, final @IntRange(from = 0) int requestCode) {
3         if (Build.VERSION.SDK_INT >= 23) {
4             ActivityCompatApi23.requestPermissions(activity, permissions, requestCode);
5         } else if (activity instanceof OnRequestPermissionsResultCallback) {
6            ......
7         }
8     }

 

ActivityCompatApi23类的代码如下,验证完之后直接调用 Activity 的requestPermissions方法。
1 public static void requestPermissions(Activity activity, String[] permissions,
2             int requestCode) {
3         if (activity instanceof RequestPermissionsRequestCodeValidator) {
4             ((RequestPermissionsRequestCodeValidator) activity)
5                     .validateRequestPermissionsRequestCode(requestCode);
6         }
7         activity.requestPermissions(permissions, requestCode);
8     }

 

Activity.java 类的代码如下:
 1 public final void requestPermissions(@NonNull String[] permissions, int requestCode) {
 2         if (mHasCurrentPermissionsRequest) {
 3             Log.w(TAG, "Can reqeust only one set of permissions at a time");
 4             // Dispatch the callback with empty arrays which means a cancellation.
 5             onRequestPermissionsResult(requestCode, new String[0], new int[0]);
 6             return;
 7         }
 8         Intent intent = getPackageManager().buildRequestPermissionsIntent(permissions);
 9         startActivityForResult(REQUEST_PERMISSIONS_WHO_PREFIX, intent, requestCode, null);
10         mHasCurrentPermissionsRequest = true;
11     }

 

其中Intent的创建是依赖PackageManager类,代码如下:

1  public Intent buildRequestPermissionsIntent(@NonNull String[] permissions) {
2         if (ArrayUtils.isEmpty(permissions)) {
3            throw new IllegalArgumentException("permission cannot be null or empty");
4         }
5         Intent intent = new Intent(ACTION_REQUEST_PERMISSIONS);
6         intent.putExtra(EXTRA_REQUEST_PERMISSIONS_NAMES, permissions);
7         intent.setPackage(getPermissionControllerPackageName());
8         return intent;
9     }

intent中指定了action,permissions,包名等等,所以如果我们想自定义权限弹框,可以在此做出修改。

 

按照安卓6.0的设计,以上Intent是发送到了 PackageInstaller 应用 或者GooglePackageInstaller应用,在此收到Intent后,弹出权限请求框,如下图。




当用户选择允许,我们就可以在onRequestPermissionsResult方法中进行响应的处理;

当用户拒绝,你的应用可能就开始危险了;
当我们再次尝试申请权限时,弹出的对话框和之前有点不一样了,主要表现为多了一个checkbox。
如下图



当用户勾选了”不再询问“拒绝后,你的程序基本这个权限就Game Over了。
不过,你还有一丝希望,那就是再出现上述的对话框之前做一些说明信息,比如你使用这个权限的目的(一定要坦白)。
shouldShowRequestPermissionRationale这个API可以帮我们判断接下来的对话框是否包含”不再询问“选择框。

 

第三步:用户对请求作出响应后的回调  void onRequestPermissionsResult(int requestCode, String[] permissions, int[] grantResults)

             用户操作之后,需要对操作结果进行处理,反馈给对应的应用,应用再针对结果做出相应的处理。

 1  private void dispatchRequestPermissionsResult(int requestCode, Intent data) {
 2         mHasCurrentPermissionsRequest = false;
 3         // If the package installer crashed we may have not data - best effort.
 4         String[] permissions = (data != null) ? data.getStringArrayExtra(
 5                 PackageManager.EXTRA_REQUEST_PERMISSIONS_NAMES) : new String[0];
 6         final int[] grantResults = (data != null) ? data.getIntArrayExtra(
 7                 PackageManager.EXTRA_REQUEST_PERMISSIONS_RESULTS) : new int[0];
 8         onRequestPermissionsResult(requestCode, permissions, grantResults);
 9     }
10 
11     private void dispatchRequestPermissionsResultToFragment(int requestCode, Intent data,
12             Fragment fragment) {
13         // If the package installer crashed we may have not data - best effort.
14         String[] permissions = (data != null) ? data.getStringArrayExtra(
15                 PackageManager.EXTRA_REQUEST_PERMISSIONS_NAMES) : new String[0];
16         final int[] grantResults = (data != null) ? data.getIntArrayExtra(
17                 PackageManager.EXTRA_REQUEST_PERMISSIONS_RESULTS) : new int[0];
18         fragment.onRequestPermissionsResult(requestCode, permissions, grantResults);
19     }

以上两个方法,就是用户操作之后,执行回调的位置,返回用户操作的结果。

 

应用的Activity可以重写onRequestPermissionsResult方法,根绝请求码以及操作结果,判断是否请求成功。

 1     @Override
 2     public void onRequestPermissionsResult(int requestCode, String[] permissions, int[] grantResults){
 3 
 4         if (requestCode == MY_PERMISSIONS_REQUEST_CALL_PHONE) {
 5             if (grantResults[0] == PackageManager.PERMISSION_GRANTED){
 6                 callPhone();
 7             }else{
 8                 // Permission Denied
 9                 Toast.makeText(MainActivity.this, "Permission Denied", Toast.LENGTH_SHORT).show();
10             }
11             return;
12         }
13         super.onRequestPermissionsResult(requestCode, permissions, grantResults);
14     }

 


如何批量申请


批量申请权限很简单,只需要字符串数组放置多个权限即可。如请求代码

1 private static final int REQUEST_CODE = 1;
2 private void requestMultiplePermissions() {
3     String[] permissions = {Manifest.permission.WRITE_EXTERNAL_STORAGE, Manifest.permission.READ_PHONE_STATE};
4     requestPermissions(permissions, REQUEST_CODE);
5 }

 

对应的界面效果是



注意:间隔较短的多个权限申请建议设置成单次多个权限申请形式,避免弹出多个对话框,造成不太好的视觉效果。


申请这么多权限岂不是很累


其实你不需要每个权限都去显式申请,举一个例子,如果你的应用授权了读取联系人的权限,那么你的应用也是被赋予了写入联系人的权限。

因为读取联系人和写入联系人这两个权限都属于联系人权限分组,所以一旦组内某个权限被允许,该组的其他权限也是被允许的。


注意事项


API问题
由于checkSelfPermission和requestPermissions从API 23才加入,低于23版本,需要在运行时判断 或者使用Support Library v4中提供的方法

  • ContextCompat.checkSelfPermission
  • ActivityCompat.requestPermissions
  • ActivityCompat.shouldShowRequestPermissionRationale

多系统问题
当我们支持了6.0必须也要支持4.4,5.0这些系统,所以需要在很多情况下,需要有两套处理。比如Camera权限

1 if (isMarshmallow()) {
2     requestPermission();//然后在回调中处理
3 } else {
4     useCamera();//低于6.0直接使用Camera
5 }

 


两个权限


运行时权限对于应用影响比较大的权限有两个,他们分别是

  • READ_PHONE_STATE
  • WRITE_EXTERNAL_STORAGE/READ_EXTERNAL_STORAGE

其中READ_PHONE_STATE用来获取deviceID,即IMEI号码。这是很多统计依赖计算设备唯一ID的参考。如果新的权限导致读取 不到,避免导致统计的异常。建议在完全支持运行时权限之前,将对应的值写入到App本地数据中,对于新安装的,可以采取其他策略减少对统计的影响。
WRITE_EXTERNAL_STORAGE/READ_EXTERNAL_STORAGE这两个权限和外置存储(即sdcard)有关,对于下载相关的应用这一点还是比较重要的,我们应该尽可能的说明和引导用户授予该权限。


些许建议

 

  • 不要使用多余的权限,新增权限时要慎重
  • 使用Intent来替代某些权限,如拨打电话(和你的产品经理PK去吧)
  • 对于使用权限获取的某些值,比如deviceId,尽量本地存储,下次访问直接使用本地的数据值
  • 注意,由于用户可以撤销某些权限,所以不要使用应用本地的标志位来记录是否获取到某权限。

注意


即使支持了运行时权限,也要在Manifest声明,因为市场应用会根据这个信息和硬件设备进行匹配,决定你的应用是否在该设备上显示。


是否支持运行时权限


个人觉得Marshmallow的运行时权限对于用户来说绝对是一个好东西,但是目前想要支持需要做的事情还是比较多的。
对于一个有很多依赖的宿主应用,想要做到支持还是有一些工作量的,因为你的权限申请受制于依赖。
建议在短期内暂时可以不考虑支持该运行时权限机制,等时机成熟或者简单易用的第三方库完善之后再支持也未尝不可。

 

posted @ 2017-07-04 17:45  浪里小白龙呼呼呼  阅读(794)  评论(0编辑  收藏  举报