Google Android 6.0 权限完全解析

注:本文只针对Google原生Android系统有效, 小米魅族等手机有自己的权限机制, 可能不适用

一、运行时权限的变化及特点

新的权限机制更好的保护了用户的隐私,Google将权限分为两类,一类是Normal Permissions,这类权限一般不涉及用户隐私,是不需要用户进行授权的,比如手机震动、访问网络等;另一类是Dangerous Permission,一般是涉及到用户隐私的,需要用户进行授权,比如读取sdcard、访问通讯录等。

运行时权限的特点是,实时性,用户可以随时取消授权,所以每次调用运行时权限的方法都需要判断或者请求一次运行时权限。

If the device is running Android 5.1 (API level 22) or lower, or the app's targetSdkVersion is 22 or lower, the system asks the user to grant the permissions at install time. And the system just tells the user what permission groups the app needs, not the individual(个别的) permissions.

If the device is running Android 6.0 (API level 23) and the app's targetSdkVersion is 23 or higher, the following system behavior applies when your app requests a dangerous permission:

  • If an app requests a dangerous permission listed in its manifest, and the app does not currently have any permissions in the permission group, the system shows a dialog box to the user describing the permission group that the app wants access to. The dialog box does not describe the specific permission within that group. For example, if an app requests the READ_CONTACTS permission, the system dialog box just says the app needs access to the device's contacts. If the user grants approval, the system gives the app just the permission it requested.
  • If an app requests a dangerous permission listed in its manifest, and the app already has another dangerous permission in the same permission group, the system immediately grants the permission without any interaction with the user. For example, if an app had previously requested and been granted the READ_CONTACTS permission, and it then requests WRITE_CONTACTS, the system immediately grants that permission.

Permission groups

dangerous permissions被分入到9个权限组:同一组的任何一个权限被授权了,其他权限也自动被授权。例如,一旦WRITE_CONTACTS被授权了,app也有READ_CONTACTS和GET_ACCOUNTS了。
其实所有权限(普通权限、运行时权限、用户自定义权限)都属于特定的权限组,但是只有运行时权限的权限组才会影响用户体验。

Table 1. Permissions and permission groups.

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

二、相关API

API的讲解就跟着申请权限步骤一起了:

1. 在AndroidManifest文件中添加需要的权限。这个步骤和我们之前的开发并没有什么变化,试图去申请一个没有声明的权限可能会导致程序崩溃。

2.  检查权限

  1. if (ContextCompat.checkSelfPermission(thisActivity,
    Manifest.permission.READ_CONTACTS)
    != PackageManager.PERMISSION_GRANTED) {
    }else{
    //
    }
     
这里涉及到一个API,ContextCompat.checkSelfPermission,主要用于检测某个权限是否已经被授予,方法返回值为PackageManager.PERMISSION_DENIED或者PackageManager.PERMISSION_GRANTED。当返回DENIED就需要进行申请授权了。
3.申请授权
 ActivityCompat.requestPermissions(thisActivity,
new String[]{Manifest.permission.READ_CONTACTS},
MY_PERMISSIONS_REQUEST_READ_CONTACTS);

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

4. 处理权限申请回调

@Override
public void onRequestPermissionsResult(int requestCode,
String permissions[], int[] grantResults) {
switch (requestCode) {
case MY_PERMISSIONS_REQUEST_READ_CONTACTS: {
// If request is cancelled, the result arrays are empty.
if (grantResults.length > 0
&& grantResults[0] == PackageManager.PERMISSION_GRANTED) { // permission was granted, yay! Do the
// contacts-related task you need to do. } else { // permission denied, boo! Disable the
// functionality that depends on this permission.
}
return;
}
}
}

ok,对于权限的申请结果,首先验证requestCode定位到你的申请,然后验证grantResults对应于申请的结果,这里的数组对应于申请时的第二个权限字符串数组。

如果你同时申请两个权限,那么grantResults的length就为2,分别记录你两个权限的申请结果。如果申请成功,就可以做你的事情了~

5.当然,到此我们的权限申请的不走,基本介绍就如上述。不过还有个API值得提一下:

// Should we show an explanation?
if (ActivityCompat.shouldShowRequestPermissionRationale(thisActivity,
Manifest.permission.READ_CONTACTS))
// Show an expanation to the user *asynchronously* -- don't block
// this thread waiting for the user's response! After the user
// sees the explanation, try again to request the permission. }

这个API主要用于给用户一个申请权限的解释,该方法只有在用户在上一次已经拒绝过你的这个权限申请时才用得到。也就是说,用户已经拒绝一次了,你又弹个授权框,你需要给用户一个解释,为什么要授权,则使用该方法。

shouldShowRequestPermissionRationale()返回值 的说明:

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

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

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

处理 “不再提醒”

如果用户拒绝某授权, 下一次弹框,用户会有一个“不再提醒”的选项的来防止app以后继续请求授权。如果这个选项在拒绝授权前被用户勾选了, 下次为这个权限请求requestPermissions时,对话框就不弹出来了,结果就是,app啥都不干。这将是很差的用户体验,用户做了操作却得不到响应, 这种情况需要好好处理一下。在请求requestPermissions前,我们需要检查是否需要展示请求权限的提示.

那么将上述几个步骤结合到一起就是:

// Here, thisActivity is the current activity
if (ContextCompat.checkSelfPermission(thisActivity,
Manifest.permission.READ_CONTACTS)
!= PackageManager.PERMISSION_GRANTED) { // Should we show an explanation?
if (ActivityCompat.shouldShowRequestPermissionRationale(thisActivity,
Manifest.permission.READ_CONTACTS)) { // Show an expanation to the user *asynchronously* -- don't block
// this thread waiting for the user's response! After the user
// sees the explanation, try again to request the permission. } else { // No explanation needed, we can request the permission. ActivityCompat.requestPermissions(thisActivity,
new String[]{Manifest.permission.READ_CONTACTS},
MY_PERMISSIONS_REQUEST_READ_CONTACTS); // MY_PERMISSIONS_REQUEST_READ_CONTACTS is an
// app-defined int constant. The callback method gets the
// result of the request.
}
}

三、Fragment中运行时权限的特殊处理

  • 在Fragment中申请权限,不要使用ActivityCompat.requestPermissions, 直接使用Fragment的requestPermissions方法,否则会回调到Activity的 onRequestPermissionsResult
  • 如果在Fragment中嵌套Fragment,在子Fragment中使用requestPermissions方 法,onRequestPermissionsResult不会回调回来,建议使用 getParentFragment().requestPermissions方法,这个方法会回调到父Fragment中的onRequestPermissionsResult,加入以下代码可以把回调透传到子Fragment
      @Override
    public void onRequestPermissionsResult(int requestCode, String[] permissions, int[] grantResults) {
    super.onRequestPermissionsResult(requestCode, permissions, grantResults);
    List<Fragment> fragments = getChildFragmentManager().getFragments();
    if (fragments != null) {
    for (Fragment fragment : fragments) {
    if (fragment != null) {
    fragment.onRequestPermissionsResult(requestCode,permissions,grantResults);
    }
    }
    }
    }

四、简单的例子

这里写一个简单的例子,针对于运行时权限。相信大家在最开始接触Android的时候,都利用Intent实验了打电话、发短信等功能。

我们看看直接拨打电话在Android 6.x的设备上权限需要如何处理。当然代码非常简单:

package com.wytiger.permissiondemo;

import android.Manifest;
import android.app.Activity;
import android.content.Intent;
import android.content.pm.PackageManager;
import android.net.Uri;
import android.os.Bundle;
import android.support.v4.app.ActivityCompat;
import android.support.v4.content.ContextCompat;
import android.util.Log;
import android.view.View;
import android.view.View.OnClickListener;
import android.widget.Toast;

public class MainActivity extends Activity implements OnClickListener {
private static final int REQUEST_CALL_PHONE = 100;
private static final String TAG = "testPermission";

@Override
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
setContentView(R.layout.activity_main);

findViewById(R.id.btnCall).setOnClickListener(this);
}

@Override
public void onClick(View v) {
testPermission();
}

private void testPermission() {
// 检查权限是否允许
if (ContextCompat.checkSelfPermission(this, Manifest.permission.CALL_PHONE) != PackageManager.PERMISSION_GRANTED) {
Log.e(TAG, "没有权限");
// 没有权限,考虑是否需要解释为什么需要这个权限
/*申请权限的解释,该方法在用户上一次已经拒绝过你的这个权限申请时使用。
* 也就是说,用户已经拒绝一次了,你又弹个授权框,你需要给用户一个解释,为什么要授权*/
if (ActivityCompat.shouldShowRequestPermissionRationale(this, Manifest.permission.CALL_PHONE)) {
Log.e(TAG, "没有权限,用户上次已经拒绝该权限,解释为什么需要这个权限");
// Show an expanation to the user *asynchronously* -- don't block this thread
//waiting for the user's response! After the user
// sees the explanation, try again to request the permission.
Toast.makeText(MainActivity.this, "我需要权限才能拨打电话", Toast.LENGTH_SHORT).show();

} else {
Log.e(TAG, "没有权限,申请权限");
// 申请权限
ActivityCompat.requestPermissions(this, new String[] { Manifest.permission.CALL_PHONE },
REQUEST_CALL_PHONE);
}

} else {
Log.e(TAG, "有权限,执行相关操作");
// 有权限,执行相关操作
callPhone();
}
}

@Override
public void onRequestPermissionsResult(int requestCode, String[] permissions, int[] grantResults) {
super.onRequestPermissionsResult(requestCode, permissions, grantResults);
if (requestCode == REQUEST_CALL_PHONE) {
if (grantResults.length > 0 && grantResults[0] == PackageManager.PERMISSION_GRANTED) {
Log.e(TAG, "权限同意");
callPhone();
} else {
Log.e(TAG, "权限拒绝");
Toast.makeText(this, "权限拒绝", Toast.LENGTH_SHORT).show();
}
return;
}
}

private void callPhone() {
Intent intent = new Intent(Intent.ACTION_CALL);
Uri data = Uri.parse("tel:" + "10086");
intent.setData(data);
startActivity(intent);
}
}

在Android 6.x上运行是,点击testCall,即会弹出授权窗口,如何你Allow则直接拨打电话,如果Denied则Toast弹出”权限拒绝”。

例子很简单,不过需要注意的是,对于Intent这种方式,很多情况下是不需要授权的甚至权限都不需要的,比如:你是到拨号界面而不是直接拨打电话,就不需要去申请权限;打开系统图库去选择照片;调用系统相机app去拍照等。当然,上例也说明了并非所有的通过Intent的方式都不需要申请权限。一般情况下,你是通过Intent打开另一个app,让用户通过该app去做一些事情,你只关注结果(onActivityResult),那么权限是不需要你处理的,而是由打开的app去处理。

更多请参考Consider Using an Intent

旧版本app兼容问题

  那么问题来了,是不是所有以前发布的app都会出现问题呢?答案是不会,只有那些targetSdkVersion 设置为23和23以上的应用才会出现异常,在使用危险权限的时候系统必须要获得用户的同意才能使用,要不然应用就会崩溃,出现类似java.lang.SecurityException: Permission Denial: reading com.android.providers.media.MediaProvider的崩溃日志。所以targetSdkVersion如果没有设置为23版本或者以上,系统还是会使用旧规则:在安装的时候赋予该app所申请的所有权限, 所以app当然可以和以前一样正常使用了. 但是还有一点需要注意的是6.0的系统里面,用户可以手动将该app的权限关闭,如下图

  Google Android 6.0 权限完全解析 
  那么问题又来了,虽然会弹出提示,但是如果以前的老应用申请的权限被用户不管提示强行手动关闭了怎么办,应用会崩溃么?

我们来试一试

  Google Android 6.0 权限完全解析 
  好吧,可以庆幸了一下了,不会抛出异常,不会崩溃(只针对原生android系统),只不过调用那些被用户禁止权限的api接口返回值都为null或者0,所以我们只需要做一下判空操作就可以了,不判空当然还是会崩溃的喽。有些权限被关闭, 是会抛异常的(小米魅族等定制rom).

注意事项:

1,权限判断在某些机型上不可靠,会出现明明拒绝了权限,但是打印出来的还是同意,比如小米5. 本文只针对原生android系统有效

2,特殊权限:Special Permissions

There are a couple of permissions that don't behave like normal and dangerous permissions. SYSTEM_ALERT_WINDOW andWRITE_SETTINGS are particularly sensitive, so most apps should not use them. If an app needs one of these permissions, it must declare the permission in the manifest, and send an intent requesting the user's authorization. The system responds to the intent by showing a detailed management screen to the user.

For details on how to request these permissions, see theSYSTEM_ALERT_WINDOW andWRITE_SETTINGS reference entries.

参考:

android permission权限与安全机制解析(上)

android permission权限与安全机制解析(下)

Android API23 M权限管理机制:Runtime Permission简介

Android API19 K原生权限管理:AppOps 

android 6.0 权限管理的学习资料和使用例子

Android 6.0 运行时权限处理完全解析

Android M 新的运行时权限开发者需要知道的一切

上一篇:多线程(Thread),其实很简单!


下一篇:Android 6.0权限管理