**权限机制变更的背景 **
在Android6.0之前,app在安装时会提示用户此app需要使用哪些权限,但用户只能选择同意或拒绝安装,而不能单独对某项权限进行授予或拒绝。只要用户选择了安装,即表示用户接受了app对这些权限的使用,如果用户不希望app获取某些涉及隐私的信息,例如读取通讯录,读取短信,获取地理位置等,只能选择不安装。
在这套权限机制下,用户只能在安装应用和拒绝权限之间二选一,选择拒绝权限就意味着不能使用此应用,这样做的代价太大,和用户下载此应用的初衷相违背,大多数时候用户只能选择妥协,而安装了应用则意味着将个人隐私信息完全暴露给了应用。当用户习惯了这种方式之后,在应用安装时基本都不会再关注提示的权限信息,因此Android的这套权限机制并没有真正的起到权限管理和保护信息的作用。
**新的权限管理机制 **
从Android6.0开始,Android引入了新的权限管理机制,将应用可使用的权限划分成了两类,一类是normal permissions,也就是普通权限,例如访问网络,创建快捷方式,开启闪光灯等 ,这类权限一般不涉及用户隐私,另一类是dangerous permissions,例如拨打电话,读取通讯录,读取短信,获取地理位置等。对normal permissions,仍然和以前一样,开发者只需要在AndroidManifest中配置即可,应用安装时提示用户所需的权限,用户同意安装即表示授权应用使用这些权限。对dangerous permissions这类涉及用户隐私的权限,不仅需要在AndroidManifest中配置,还需要在运行时请求用户授权,用户这时可以单独允许或拒绝某项权限。当用户选择了拒绝某项权限时,应用将无法执行需要对应权限的api。
通过引入这套新的权限管理机制,用户在权限管理上有了更高的自由度,用户不再需要为了限制某项信息不被获取而舍弃整个应用的使用权。对涉及用户隐私的这类操作,用户可以选择拒绝,而应用的其他功能又不受影响。
**运行时权限申请流程 **
dangerous permissions运行时的权限申请主要用到如下几个API。
- Context.checkSelfPermission(String permission) 检查是否被授予了某个权限
- Activity.requestPermissions(String[] permissions, int requestCode) 申请一组权限
- Activity.shouldShowRequestPermissionRationale(String permission) 判断是否需要显示申请此权限的原因,在应用第一次申请某个权限,或者用户对该权限请求授权界面选择了不再显示时此方法返回false,否则返回true。
- Activity.onRequestPermissionsResult(int requestCode, String[] permissions, int[] grantResults) 权限申请结果回调
这四个都是从Android 6.0系统 (API Level 23)才开始有的new API,因此使用前都需要判断当前系统的版本是否是Android 6.0以上。
完整的权限申请流程如下,虚线表示这是一个异步的过程。
官网资料:
https://developer.android.com/intl/zh-cn/training/permissions/requesting.html
https://developer.android.com/reference/android/content/pm/PermissionInfo.html
https://developer.android.com/intl/zh-cn/guide/topics/security/permissions.html#normal-dangerous
其他资料:
http://www.codeceo.com/article/android-6-runtime.html
http://www.jcodecraeer.com/a/anzhuokaifa/androidkaifa/2015/0830/3387.html
运行时权限申请注意事项
-
requestPermissions()的第一个参数是一个数组,可以一次申请多个权限。
-
如果一次申请了多个权限,权限请求对话框的弹出顺序是按照数组的顺序来的,数组前面的权限会先让用户确认。一般来说,将必要的权限放在数组前面,辅助的权限放在数组后面。这样可以增加必要权限申请的成功率。
-
如果一次申请了多个权限,只有所有的权限被用户处理(拒绝或接受)后,onRequestPermissionsResult()才会被回调,不是处理一个回调一次。
-
如果一次申请的权限中,部分权限没有在AndroidManifest.xml中声明,则不会弹出该权限的请求对话框,只会弹出那些在AndroidManifest.xml中声明过的权限,等用户处理完后onRequestPermissionsResult()被回调,那些未在AndroidManifest.xml中声明的权限请求结果一定是PERMISSION_DENIED。特别的,如果一次申请的权限中,所有的权限都没有在AndroidManifest.xml中声明,则不会弹出任何请求对话框,回调onRequestPermissionsResult()会被立刻执行(这里立刻的含义是指整个过程中没有需要用户交互的地方,不是指onRequestPermissionsResult()会在执行requestPermissions()时就被调用,onRequestPermissionsResult()回调仍然是一个异步的过程),且所有权限请求结果都是PERMISSION_DENIED。
-
如果一次申请的权限中,部分权限已经被授予,对已经授予的权限并不是忽略,而是仍然会弹出请求对话框,不同的是没有下次不再提醒的复选框。如果用户此次选择了拒绝,则应用将会失去该权限。所以在申请权限前一定要先判断哪些权限是已经获得的,已经授予的权限不要再次申请。特别是一次申请多个权限的时候,一定要每次都判断哪些权限已经获得了,只申请哪些未被授予的权限。
例如,想要一次性申请READ_PHONE_STATE,READ_EXTERNAL_STORAGE,RECORD_AUDIO,ACCESS_COARSE_LOCATION,GET_ACCOUNTS五个权限,以下是正确的处理方式String [] permissions = {Manifest.permission.READ_PHONE_STATE, Manifest.permission.READ_EXTERNAL_STORAGE, Manifest.permission.RECORD_AUDIO, Manifest.permission.ACCESS_COARSE_LOCATION, Manifest.permission.GET_ACCOUNTS}; // getUngrantedPermissions():遍历permissions,返回其中未被授予的权限,如果所有权限都被授予,则返回空的数组。 String [] unGranted = getUngrantedPermissions(permissions); if (unGranted.length != 0) { requestPermissions(unGranted); }
以下是错误的处理方式
String [] permissions = {Manifest.permission.READ_PHONE_STATE, Manifest.permission.READ_EXTERNAL_STORAGE, Manifest.permission.RECORD_AUDIO, Manifest.permission.ACCESS_COARSE_LOCATION, Manifest.permission.GET_ACCOUNTS}; // checkPermissions()会遍历permissions,如果存在未被授予的权限,返回false,所有权限都被授予,返回true boolean isGranted = checkPermissions(permissions); if (!isGranted) { requestPermissions(permissions); }
-
调用requestPermissions()申请权限后,在onRequestPermissionsResult()收到回调之前不可再次调用requestPermissions(),无论第二次调用requestPermissions()请求的权限是否和第一次相同,都会在onRequestPermissionsResult()中立刻收到第二次requestPermissions()的回调,且onRequestPermissionsResult()的第二个和第三个参数的数组大小都是0。特别的是,如果requestPermissions()是用户操作触发的,如果用户快速点击多次,就可能导致requestPermissions()多次调用的问题,如果不在onRequestPermissionsResult()的时候做特殊处理,将会导致申请的权限被错误的判定为已授予或已拒绝。
-
如果一个app的targetSdkVersion设置为23以下,当这个app在Android 6.0系统上运行时,系统会自动为它授予dangerous的权限。不过用户仍然可以通过系统设置来取消某项权限。在系统设置取消授权时,和targetSdkVersion设置为23的app不同的是,会多出一个警告提示,告知用户取消授权可能会导致应用异常。
-
如果一个app的targetSdkVersion设置为23以下,在Android 6.0系统上执行checkSelfPermission()检查是否有某项权限时,只要在AndroidManifest.xml中声明了该权限,无论当前是否被授予了该权限,返回结果都是PERMISSION_GRANTED。也就是说如果该权限没有在AndroidManifest.xml中声明,则checkSelfPermission()返回PERMISSION_DENIED,如果该权限在AndroidManifest.xml中声明了,即使用户手动禁止了该权限,checkSelfPermission()也会返回PERMISSION_GRANTED。所以,无法通过后checkSelfPermission()来判断用户是否禁止了某项权限。
-
如果一个app的targetSdkVersion设置为23以下,在Android 6.0系统上执行requestPermissions(),结果和targetSdkVersion设置为23的app差不多,唯一不同的对话框中没有下次不再提醒的复选框。这点和targetSdkVersion设置为23时申请已经被授予的权限的效果相同。原因应该也是系统认为所有申请的权限都已经被授予了。
-
如果一个app的targetSdkVersion设置为23以下,在Android 6.0系统上调用一个需要权限的api时,如果这个权限被用户手动取消了,不会抛出异常。但是该api将什么也不做,如果有返回值的话会返回null或者0。
-
requestPermissions()的第二个参数requestCode是一个int类型的整数,用来标识一次请求过程,在onRequestPermissionsResult()中可以通过requestCode来区分此次返回的是哪一次请求的结果。如果在一个Activity类中有多次请求不同权限的操作,则需要区分requestCode,一般来说可以随意取一个整数。需要注意的是,requestCode必须是一个大于等于0的整数。如果传入了一个小于0的整数,虽然不会有异常,但是也不会有任何效果。不会弹出请求对话框,onRequestPermissionsResult()也不会被执行。例如,用0xFF000001作为requestCode是不会有任何效果的。
WRITE_SETTINGS和SYSTEM_ALERT_WINDOW权限申请
从normal permissions (https://developer.android.com/guide/topics/security/normal-permissions.html) 和dangerous permissions (https://developer.android.com/guide/topics/security/permissions.html#normal-dangerous) 列表中可以看到,这两个列表中都没有包含WRITE_SETTINGS和SYSTEM_ALERT_WINDOW这两个权限,也就是说这两个权限既不属于normal permission,也不属于dangerous permission。这是因为Android认为这两个权限非常敏感,已经超出了dangerous permissions的程度,一般app中都不应该使用这两个权限,因此将这两个权限单独分成一类,称为special permissions。
这两个权限在Android6.0系统上同样需要在运行时申请,不过上述针对dangerous permissions的运行时权限申请方法对这两个权限是不适用的,Android提供了额外的api来检查和申请这两个权限。
special permissions运行时的权限申请主要用到如下几个api。
- Settings.System.canWrite(Context context) 检查是否被授予了WRITE_SETTINGS权限
- Settings.canDrawOverlays(Context context) 检查是否被授予了SYSTEM_ALERT_WINDOW权限
- startActivityForResult(Intent intent, in requestCode) 打开用户授权界面
- onActivityResult(int requestCode, int resultCode, Intent data) 权限申请结果回调
此外还用到两个字符串常量
- Settings.ACTION_MANAGE_WRITE_SETTINGS申请WRITE_SETTINGS权限对应的intent action
- Settings.ACTION_MANAGE_OVERLAY_PERMISSION申请SYSTEM_ALERT_WINDOW权限对应的intent action
前两个API和两个字符串常量同样是从Android 6.0系统(API Level 23)才开始有的,因此使用前都需要判断当前系统的版本是否是Android 6.0以上。
申请WRITE_SETTINGS权限示例代码如下。
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.M) {
// 判断是否有WRITE_SETTINGS权限
if(!Settings.System.canWrite(this)) {
// 申请WRITE_SETTINGS权限
Intent intent = new Intent(Settings.ACTION_MANAGE_WRITE_SETTINGS,
Uri.parse("package:" + getPackageName()));
// REQUEST_CODE1是本次申请的请求码
startActivityForResult(intent, REQUEST_CODE1);
} else {
dosomething();
}
} else {
dosomething();
}
判断WRITE_SETTINGS权限申请结果流程示例代码如下。
@Override
protected void onActivityResult(int requestCode, int resultCode, Intent data) {
// 请求码是REQUEST_CODE1,表示本次结果是申请WRITE_SETTINGS权限的结果
if (requestCode == REQUEST_CODE1) {
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.M) {
// 判断是否有WRITE_SETTINGS权限
if (Settings.System.canWrite(this)) {
dosomething();
}
}
}
super.onActivityResult(requestCode, resultCode, data);
}
申请SYSTEM_ALERT_WINDOW权限示例代码如下。
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.M) {
// 判断是否有SYSTEM_ALERT_WINDOW权限
if(!Settings.canDrawOverlays(this)) {
// 申请SYSTEM_ALERT_WINDOW权限
Intent intent = new Intent(Settings.ACTION_MANAGE_OVERLAY_PERMISSION,
Uri.parse("package:" + getPackageName()));
// REQUEST_CODE2是本次申请的请求码
startActivityForResult(intent, REQUEST_CODE2);
} else {
dosomething();
}
} else {
dosomething();
}
判断SYSTEM_ALERT_WINDOW权限申请结果流程示例代码如下。
@Override
protected void onActivityResult(int requestCode, int resultCode, Intent data) {
// 请求码是REQUEST_CODE2,表示本次结果是申请SYSTEM_ALERT_WINDOW权限的结果
if (requestCode == REQUEST_CODE2) {
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.M) {
// 判断是否有SYSTEM_ALERT_WINDOW权限
if (Settings.canDrawOverlays(this)) {
dosomething();
}
}
}
super.onActivityResult(requestCode, resultCode, data);
}
这里同样有几个需要注意的地方
- special permission同样需要先在AndroidManifest中配置,如果未在AndroidManifest中配置,执行startActivityForResult()后仍然会显示用户授权界面,不过文字和按钮都是灰色的,用户无法更改。
- 申请的权限如果已经被授予,执行startActivityForResult()后仍然会显示用户授权界面,用户可以选择取消授权,所以在执行startActivityForResult()前一定要先判断是否已经授予了权限。
- 多次执行startActivityForResult(),会按照startActivityForResult()的执行顺序依次弹出多个用户授权界面。即使申请的是同一个权限,也是如此。
- 和onRequestPermissionsResult()中的第三个参数grantResults的作用不同,onActivityResult()中的resultCode不能用来判断权限申请的结果,无论用户是否授予了权限,resultCode始终为0。
- 对这两个special permission,无论是否已经授予权限,checkSelfPermission()返回的都是PERMISSION_DENIED,如果试图使用requestPermissions()来申请权限,不会弹出任何权限确认的界面,不过onRequestPermissionsResult()回调方法仍然会被执行,grantResults的结果始终都是PERMISSION_DENIED。因此,试图用申请dangerous permission的方法来检查和申请这两个special permission是没有任何效果的。
一些额外的话题
###Android设备的唯一标识问题
很多app都需要获取Android设备的唯一标识(UDID),用来作为临时的身份认证,后台的日志记录,数据统计等。但由于Android版本和机型众多,同时又有数量庞大的各种非官方ROM,通过某个单一特征难以唯一标识一台设备。因此,通常会获取多个特征,然后整合在一起作为该设备标识。设备的IMEI码是其中的一个重要特征,对有电话功能的设备,IMEI码都是唯一的。国内大量的app都会使用IMEI码作为标记用户身份的一个关键信息。
然而获取IMEI码需要READ_PHONE_STATE权限,此权限属于dangerous permission,所以在Android 6.0之后需要在运行时申请。这带来了如下两个问题。
- 获取设备唯一标识通常都在进入应用后立刻执行的,而申请权限则是一个异步的过程,用户可能很长时间后才会处理,这使得所有需要此信息的地方都需要放到onRequestPermissionsResult()回调后才能执行。
- 由于用户可以选择允许和拒绝此权限,如果用户选择了允许此权限,则可以获取到IMEI码,如果用户选择了拒绝,则无法获取。这意味着同一个设备,用户的不同选择会产生两个不同的设备识别码(注意:用户选择后可以随时在系统里面更改是否授予此权限)。如果设备识别码只是用来记录一些用户日志,可能不会有太大问题,只是同一个用户产生了两份用户日志。但是如果将设备识别码作为登陆时的身份认证(例如很多app都有的游客登录功能),则可能会产生一些问题。用户选择允许或拒绝权限就会变成两个不同的用户。
为了避免此类问题,建议调整设备识别码的计算方式,对Android 6.0及以上版本的设备不再将IMEI码作为设备识别码(或设备识别码的一部分)。另一个常用的作为设备识别码的信息是ANDROID ID,这也是Google官方推荐的设备标识。不过在早期的Android版本中,ANDROID ID的设置存在一些bug,此外几年前有些国内手机厂家出厂时会将同一个批次的所有手机用同一个ANDROID ID。这些问题使得ANDROID ID在早期的版本上不是很可靠,不过目前这些问题应该都已得到解决。ANDROID ID的bug Google早已修复,国内几个大厂应该也不会再犯这种错误。因此,对Android 6.0及以上版本使用ANDROID ID已经完全可以唯一标识一个设备。对Android 6.0以下版本,仍然可以使用原先的混合多个信息的方式。
相关文章:
http://technet.weblineindia.com/mobile/getting-unique-device-id-of-an-android-smartphone/
http://developer.android.com/intl/zh-cn/reference/android/provider/Settings.Secure.html#ANDROID_ID
http://stackoverflow.com/questions/2785485/is-there-a-unique-android-device-id?rq=1
第三方SDK的问题
一个功能完整的app通常需要接入多个第三方sdk,如地图,推送,统计,社交,广告,渠道等。目前大量的第三方sdk仍然是在低版本上开发,没有兼容Android 6.0。当sdk中的代码需要使用某个权限时,没有经过权限检查,权限申请的流程。当集成了这些sdk的app在Android 6.0系统上运行时, 如果此时应用没有被授予对应的权限,就会导致程序异常。
对app开发者来说。可以尝试以下几个方法。
- 将targetSDKVersion设置为Android 23以下。由于Android 6.0系统会为targetSDKVersion为23以下的app自动授予dangerous权限,这样就可以避免由于没有权限导致的异常。不过这种方法并不是万能的,部分应用市场对新发布APP的targetSDKVersion有最低版本的要求,如果要将应用发布到这些应用市场, 就不能这样设置了。此外,虽然Android 6.0系统会为targetSDKVersion为23以下的app自动授予dangerous权限,但是用户仍然可以通过系统设置来禁止某项权限。如果用户手动禁止了某项权限,仍然会导致程序异常。
- app代替sdk申请权限。在sdk api调用之前,在app代码中增加权限申请流程。待权限申请通过后再去调用sdk的api。这种方法并不能完全解决问题,sdk中很多代码都是在后台执行的,不是通过某个api,而是通过某些后台事件来触发,没有办法在app中知道何时需要申请该权限,即使是在app启动时就立刻申请权限,也无法保证sdk中代码执行时,用户已经授予了权限。
对sdk开发者来说,应当尽快升级sdk版本,支持Android 6.0的权限机制。
通过intent使用相机的权限问题
通常我们会使用如下代码来使用系统相机,将拍照保存到指定的文件中。
Uri uri = Uri.fromFile(picFile);
Intent intent = new Intent(MediaStore.ACTION_IMAGE_CAPTURE);
intent.putExtra(MediaStore.EXTRA_OUTPUT, uri);
startActivityForResult(intent, REQUEST_CODE);
无论是Android6.0之前的版本,还是Android6.0及之后的版本, 通过这种方式使用相机都不需要在AndroidManifest.xml中声明CAMERA权限(https://developer.android.com/training/permissions/best-practices.html#perms-vs-intents)。也就是说,如果没有在AndroidManifest.xml中声明CAMERA权限,那么这段代码运行是没有问题的。但是反过来,如果在AndroidManifest.xml中声明了CAMERA权限(这可能是其他地方代码或者接入的某些第三方SDK中需要用到CAMERA权限),则这段代码在Android6.0系统上运行时,会检查app此时是否已经被授予了使用相机的权限,如果没有,则会产生SecurityException。
这个设定看起来相当怪异,没有声明权限可以正常运行,声明了权限却有可能导致应用崩溃。但是Google就是这样设计的,只能在代码中做兼容了。
对app开发者来说,如果app的AndroidManifest.xml中没有声明CAMERA权限,则不需要修改。如果app的AndroidManifest.xml中声明了CAMERA权限,则在通过intent启动相机前需要先判断是否已经被授予了CAMERA权限,如果没有,则需要先通过requestPermissions()申请拍照权限,申请到权限后再执行上述代码。
对sdk开发者来说,由于不知道接入sdk的app是否需要CAMERA权限,所以必须先判断AndroidManifest.xml中是否声明了CAMERA权限,如果没有声明,则直接启动相机,如果声明了,则同样是先判断是否已经被授予了CAMERA权限,如果没有授予权限,则需要再通过requestPermissions()申请拍照权限。这个流程是通用的,对app开发者来说也是适用的。建议app开发者也采用这个流程,避免出现刚开始项目中没有用到CAMERA权限,但是后来由于接入第三方sdk等原因加了CAMERA权限,但是却忘记修改之前的调用相机代码的情况。
判断AndroidManifest.xml中是否声明了某项权限的方法如下。
public boolean hasPermissionInManifest(Context context, String permissionName) {
final String packageName = context.getPackageName();
try {
final PackageInfo packageInfo = context.getPackageManager()
.getPackageInfo(packageName, PackageManager.GET_PERMISSIONS);
final String[] declaredPermisisons = packageInfo.requestedPermissions;
if (declaredPermisisons != null && declaredPermisisons.length > 0) {
for (String p : declaredPermisisons) {
if (p.equals(permissionName)) {
return true;
}
}
}
} catch (NameNotFoundException e) {
}
return false;
}
参考:http://stackoverflow.com/questions/32789027/android-m-camera-intent-permission-bug
WIFI和蓝牙扫描问题
Android 6.0增加了对附近设备扫描的权限限制,如下三个API在调用前都需要先获取ACCESS_FINE_LOCATION 或者 ACCESS_COARSE_LOCATION权限。
- WifiManager.getScanResults()
- BluetoothDevice.ACTION_FOUND
- BluetoothLeScanner.startScan()
例如,通过BluetoothAdapter.startDiscovery()来搜索附近的蓝牙设备,在Android 6.0之前只需要在AndroidManifest中声明BLUETOOTH和BLUETOOTH_ADMIN权限即可,从Android 6.0之后还需要在AndroidManifest中声明ACCESS_FINE_LOCATION 或者 ACCESS_COARSE_LOCATION权限。由于这两个权限属于dangerous permission,所以还需要在运行时申请该权限,等用户授权后才可以通过BluetoothAdapter.startDiscovery()来搜索附近的蓝牙设备。如果没有在AndroidManifest中声明ACCESS_FINE_LOCATION 或者 ACCESS_COARSE_LOCATION权限,或者没有得到用户授权就调用BluetoothAdapter.startDiscovery(),那么定义的BroadcastReceiver在Android 6.0系统上中是不会收到任何消息的。
参考:
- https://developer.android.com/about/versions/marshmallow/android-6.0-changes.html#behavior-hardware-id
- http://stackoverflow.com/questions/33052811/since-marshmallow-update-bluetooth-discovery-using-bluetoothadapter-getdefaultad