在Android6.0之前,app安装时会提示用户此app需要使用哪些权限,但用户不能单独对某项权限进行授权或拒绝,只要用户选择了安装,即表示用户接受了app对这些权限的使用,如果用户不希望app获取某些涉及隐私的信息,例如读取通讯录,读取短信,获取地理位置等,只能选择不安装。
在这套权限机制下,用户只能在安装应用和拒绝权限之间二选一,选择拒绝权限就意味着不能使用此应用,这样做的代价太大,和用户下载此应用的初衷相违背,大多数时候用户只能选择妥协,而安装了应用则意味着将个人隐私信息完全暴露给了应用。当用户习惯了这种方式之后,在应用安装时基本都不会再关注提示的权限信息,因此Android的这套权限机制并没有真正的起到权限管理和保护信息的作用。
从Android6.0开始,Android引入了新的权限管理机制,将应用可使用的权限划分成了两类,一类是normal permissions,也就是普通权限,例如访问网络,创建快捷方式,开启闪光灯等 ,这类权限一般不涉及用户隐私,另一类是dangerous permissions,例如拨打电话,读取通讯录,读取短信,获取地理位置等。对normal permissions,仍然和以前一样,开发者只需要在AndroidManifest中配置即可,应用安装时提示用户所需的权限,用户同意安装即表示授权应用使用这些权限。对dangerous permissions这类涉及用户隐私的权限,不仅需要在AndroidManifest中配置,还需要在运行时请求用户授权,用户这时可以单独允许或拒绝某项权限。当用户选择了拒绝某项权限时,应用将无法执行该权限对应的api。
通过引入这套新的权限管理机制,用户在权限管理上有了更高的自由度,用户不再需要为了限制某项信息不被获取而舍弃整个应用的使用权。对涉及用户隐私的这类操作,用户可以选择拒绝,而应用的其他功能又不受影响。
dangerous permissions运行时的权限申请主要用到如下几个API。
这四个都是从Android 6.0系统 (API Level 21)才开始有的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
如果一次申请的权限中,部分权限已经被授予,对已经授予的权限并不是忽略,而是仍然会弹出请求对话框,不同的是没有下次不再提醒的复选框。如果用户此次选择了拒绝,则应用将会失去该权限。所以在申请权限前一定要先判断哪些权限是已经获得的,已经授予的权限不要再次申请。特别是一次申请多个权限的时候,一定要每次都判断哪些权限已经获得了,只申请哪些未被授予的权限。
以下是正确的处理方式
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);
}
如果一个app的targetSdkVersion设置为23以下,当这个app在Android 6.0系统上运行时,系统会自动为它授予dangerous的权限。不过用户仍然可以通过系统设置来取消某项权限。在取消授权时,和targetSdkVersion设置为23的app不同的是,会多出一个警告提示,告知用户取消授权可能会导致应用异常。
从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单独制作了一个activity作为这两个权限的用户授权界面,必须通过指定intent,然后通过startActivity(intent)的方式来申请。
special permissions运行时的权限申请主要用到如下几个api。
此外还用到两个字符串常量
前两个API和两个字符串常量同样是从Android 6.0系统(API Level 21)才开始有的,因此使用前都需要判断当前系统的版本是否是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);
}
这里同样有几个需要注意的地方
很多app都需要获取Android设备的唯一标识(UDID),用来作为临时的身份认证,后台的日志记录,数据统计等。但由于Android版本和机型众多,同时又有数量庞大的各种非官方ROM,通过某个单一特征难以唯一标识一台设备。因此,通常会获取多个特征,然后整合在一起作为该设备标识。设备的IMEI码是其中的一个重要特征,对有电话功能的设备,IMEI码都是唯一的。国内大量的app都会使用IMEI码作为标记用户身份的一个关键信息。
然而获取IMEI码需要READ_PHONE_STATE权限,此权限被Android定义为dangerous的权限,所以在Android 6.0之后需要在运行时申请。这带来了如下两个问题。
1. 获取设备唯一标识通常都在进入应用后立刻执行的,而申请权限则是一个异步的过程,用户可能几个小时后才会处理,这使得所有需要此信息的地方都需要放到onRequestPermissionsResult()回调后才能执行。
2. 由于用户可以选择允许和拒绝此权限,如果用户选择了允许此权限,则可以获取到IMEI码,如果用户选择了拒绝,则无法获取。这意味着同一个设备,用户的不同选择会产生两个不同的设备识别码(注意:用户选择后可以随时在系统里面更改是否授予此权限)。如果设备识别码只是用来记录一些用户日志,可能不会有太大问题,只是同一个用户产生了两份用户日志。但是如果将设备识别码作为登陆时的身份认证,则可能会产生一些问题。用户选择允许或拒绝权限就会变成两个不同的用户。
为了避免此类问题,建议调整设备识别码的计算方式,对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
一个功能完整的app通常需要接入多个第三方sdk,如地图,推送,统计,社交,广告,渠道等。目前国内大量的第三方sdk仍然是在低版本上开发,没有兼容Android 6.0。当sdk中的代码需要使用某个权限时,没有经过权限检查,权限申请的流程。当集成了这些sdk的app在Android 6.0系统上运行时, 如果此时应用没有被授予对应的权限,就会导致程序异常。
对app开发者来说。可以尝试以下几个方法。
1. 将targetSDKVersion设置为Android 23以下。由于Android 6.0系统会为targetSDKVersion为23以下的app自动授予dangerous权限,这样就可以避免由于没有权限导致的异常。不过这种方法并不是万能的,Google Play现在强制要求所有新提交的apk的targetSDKVersion必须为23。所以如果要将应用发布到Google Play, 就不能这样设置了。此外,虽然Android 6.0系统会为targetSDKVersion为23以下的app自动授予dangerous权限,但是用户仍然可以通过系统设置来禁止某项权限。如果用户手动禁止了某项权限,仍然会导致程序异常。
2. app代替sdk申请权限。在sdk api调用之前,在app代码中增加权限申请流程。待权限申请通过后再去调用sdk的api。这种方法并不能完全解决问题,sdk中很多代码都是在后台执行的,不是通过某个api,而是通过某些事件来触发,没有办法在app中知道何时该申请权限,即使是在app启动时就立刻申请权限,也无法保证sdk中代码执行时,用户已经授予了权限。
对sdk开发者来说,应当尽快升级sdk版本,支持Android 6.0的权限机制。
通常我们会使用如下代码来使用系统相机,将拍照保存到指定的文件中。
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
Android 6.0增加了对附近设备扫描的权限限制,如下三个API在调用前都需要先获取ACCESS_FINE_LOCATION 或者 ACCESS_COARSE_LOCATION权限。
1. WifiManager.getScanResults()
2. BluetoothDevice.ACTION_FOUND
3. 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系统上中是不会收到任何消息的。
参考:
1. https://developer.android.com/about/versions/marshmallow/android-6.0-changes.html#behavior-hardware-id
2. http://stackoverflow.com/questions/33052811/since-marshmallow-update-bluetooth-discovery-using-bluetoothadapter-getdefaultad