上一篇谈了Android 隐藏的权限管理机制 AppOps,本文将就 Android M 6.0 之后引入的动态权限管理 Android Runtime Permission 展开讨论,Android 运行时权限管理也是基于 AppOps 开发的。
Android M 之前,应用的权限请求是在安装时提示,确认后权限就会拥有。但从 Android M 6.0(API level 23)开始,用户对应用权限进行授权是发生在应用运行时,而不是在安装时。这样可以让用户在安装时节省时间,而且可以更方便的控制应用的权限(至少权限管理不需要 root 了)。用户可以按照对应用的需求来控制应用的权限,比如百度地图的联系人权限。同时用户也可以在应用程序设置中撤销对应用的权限授权。
Android 系统中的权限被划分为两类:普通权限和敏感权限。
普通权限不会涉及到用户隐私,如果应用在 manifest 文件中直接声明了普通权限,系统会自动授予权限给应用。比如:网络INTERNET、蓝牙 BLUETOOTH、震动 VIBRATE 等权限。
敏感权限则要获取到一些用户私密的信息。如果你的应用需要获取敏感权限,首先需要获取用户的授权。比如:相机 CAMERA、联系人 CONTACTS、存储设备 STORAGE。
Android 详细的权限信息可以在此处查找 https://developer.android.com/reference/android/Manifest.permission.html,每个权限里面可能会有 Protection level,标记着是 dangerous 还是 normal。
在 Android 中普通权限和敏感权限,都需要在 manifest 文件中进行权限声明。然而,在不同版本的操作系统或不同的 target SDK level 中的结果是不同的。如果设备运行 Android 5.1 或者更低版本的操作系统,或者你的目标 SDK 版本号小于或等于 22,当你在 manifest 文件中请求了一些权限,用户必须在安装过程时授予全部权限,否则应用不能正常安装。如果设备运行在 Android 6.0 或者更高版本,并且目标 SDK 版本号大于或等于23,应用程序必须要在 manifest 文件中声明需要的权限,当程序运行时,它必须向用户请求授权每个所需的敏感权限。用户可以允许或拒绝授权请求,此时程序可以依赖用户已经授权的权限继续运行。例如:假设你的 app 需要定位和拍照权限,在请求权限时用户只授予了定位权限,那么当前程序可以正常运行并获取定位信息,但是无法进行拍照。
Protection level: normal类权限
当用户安装或更新应用时,系统将授予应用所请求的属于 PROTECTION_NORMAL 的所有权限(安装时授权的一类基本权限)。此类权限包括:
android.permission.ACCESS_LOCATION_EXTRA_COMMANDS // 允许程序访问额外的位置提供命令
android.permission.ACCESS_NETWORK_STATE // 允许程序访问网络信息状态,如当前的网络连接是否有效
android.permission.ACCESS_NOTIFICATION_POLICY // 允许程序访问APP通知方式
android.permission.ACCESS_WIFI_STATE // 允许程序访问WiFi网络状态信息
android.permission.BLUETOOTH // 允许程序连接到已配对的蓝牙设备
android.permission.BLUETOOTH_ADMIN // 允许程序发现和配对蓝牙设备
android.permission.BROADCAST_STICKY // 允许程序广播常用intents
android.permission.CHANGE_NETWORK_STATE // 改变网络状态如是否能联网
android.permission.CHANGE_WIFI_MULTICAST_STATE // 改变WiFi多播状态,控制WIFI热点
android.permission.CHANGE_WIFI_STATE // 允许程序改变WiFi连接状态
android.permission.DISABLE_KEYGUARD // 允许程序禁用键盘锁
android.permission.EXPAND_STATUS_BAR // 允许程序扩展收缩在状态栏
android.permission.GET_PACKAGE_SIZE // 允许程序获取任何package占用空间容量
android.permission.INTERNET // 访问网络连接,可能产生GPRS流量
android.permission.KILL_BACKGROUND_PROCESSES // 允许调用killBackgroundProcesses方法结束后台进程
android.permission.MODIFY_AUDIO_SETTINGS // 修改声音设置信息
android.permission.NFC // 允许程序执行NFC近距离通讯操作,用于移动支持
android.permission.READ_SYNC_SETTINGS // 读取同步设置,读取Google在线同步设置
android.permission.READ_SYNC_STATS // 读取同步状态,获得Google在线同步状态
android.permission.WRITE_SYNC_SETTINGS // 允许程序写入同步设置
android.permission.RECEIVE_BOOT_COMPLETED // 允许程序开机自动运行
android.permission.REORDER_TASKS // 允许程序改变Z轴排列任务
android.permission.REQUEST_INSTALL_PACKAGES // 允许程序请求安装应用程序
android.permission.SET_TIME_ZONE // 设置系统时区
android.permission.SET_WALLPAPER // 允许程序设置壁纸
android.permission.SET_WALLPAPER_HINTS // 允许程序设置壁纸hits
android.permission.TRANSMIT_IR // 红外发射
android.permission.USE_FINGERPRINT // 指纹识别
android.permission.VIBRATE // 允许振动
android.permission.WAKE_LOCK // 允许程序在手机屏幕关闭后后台进程仍然运行
com.android.alarm.permission.SET_ALARM // 设置闹铃提醒
com.android.launcher.permission.INSTALL_SHORTCUT // 创建快捷方式
com.android.launcher.permission.UNINSTALL_SHORTCUT // 删除快捷方式
只需要在AndroidManifest.xml中简单声明这些权限就好,安装时就授权。不需要每次使用时都检查权限,而且用户不能取消以上授权。
Protection level: dangerous类权限
这类权限需要在需要的时候,需要我们动态申请,比如:当我们需要打开相机拍摄照片的时候需要我们通过代码的方式在需要的地方去申请权限。Android6.0中权限问题中我们需要注意的是:
特殊权限组:
android.permission-group.CALENDAR
android.permission-group.CAMERA
android.permission-group.CONTACTS
android.permission-group.LOCATION
android.permission-group.MICROPHONE
android.permission-group.PHONE
android.permission-group.SENSORS
android.permission-group.SMS
android.permission-group.STORAGE
具体的分组如下:
动态权限的申请流程,请参考下面的请求拨打电话权限 Manifest.permission.CALL_PHONE 的代码:
// Request Permission type
public static final int PERMISSIONS_REQUEST_CALL_PHONE = 0;
public String mPhoneNum = "";
private void callUp(final String phoneNum) {
Intent intent = new Intent();
intent.setAction(Intent.ACTION_CALL);
intent.setData(Uri.parse("tel:" + phoneNum));
startActivity(intent);
}
private void satartCall(final Activity activity, final String phoneNum) {
mPhoneNum = phoneNum;
if (ContextCompat.checkSelfPermission(activity, Manifest.permission.CALL_PHONE) == PackageManager.PERMISSION_GRANTED) {
callUp(phoneNum);
} else {
if (ActivityCompat.shouldShowRequestPermissionRationale(activity, Manifest.permission.CALL_PHONE)) {
AlertDialog.Builder builder = new AlertDialog.Builder(activity);
builder.setTitle("Request Permission").setMessage("Request permission to make calls")
.setCancelable(false)
.setPositiveButton("Ok", new DialogInterface.OnClickListener() {
@Override
public void onClick(DialogInterface dialog, int which) {
dialog.dismiss();
ActivityCompat.requestPermissions(activity,
new String[]{Manifest.permission.CALL_PHONE},
PERMISSIONS_REQUEST_CALL_PHONE);
}
}).setNegativeButton("Cancle", new DialogInterface.OnClickListener() {
@Override
public void onClick(DialogInterface dialog, int which) {
dialog.dismiss();
}
}).create().show();
} else {
ActivityCompat.requestPermissions(activity,
new String[]{Manifest.permission.CALL_PHONE},
PERMISSIONS_REQUEST_CALL_PHONE);
}
}
}
@Override
public void onRequestPermissionsResult(int requestCode, @NonNull String[] permissions, @NonNull int[] grantResults) {
switch (requestCode) {
case PERMISSIONS_REQUEST_CALL_PHONE:
if (grantResults.length > 0 && PackageManager.PERMISSION_GRANTED == grantResults[0]) {
callUp(mPhoneNum);
} else {
// Permission Denied
Toast.makeText(MainActivity.this, "Permission Denied", Toast.LENGTH_SHORT).show();
}
return;
default:
break;
}
super.onRequestPermissionsResult(requestCode, permissions, grantResults);
}
上面代码使用了V4兼容库,若不用V4兼容库就需要自己对SDK版本做兼容:
判定是否被授权方法,授权返回 PERMISSION_GRANTED,否则返回 PERMISSION_DENIED ,在所有版本都是如此。
请求权限授权方法。
在弹出权限选择的对话框前给用户show一个dialog,用于引导用户进行选择。在M之前版本调用,永远返回false。
另外,对于Fragment可以用 Android Support v13 兼容包中的方法FragmentCompat.requestPermissions() 和 FragmentCompat.shouldShowRequestPermissionRationale(),与 activity 效果一样。
小结:
为了适应越来越严格的安全性需要,建议你的代码最好是适配Android动态权限管理。好在大部分权限都是normal类权限,只需在 AndroidManifest.xml 中配置即可,只有少数的dangerous类权限需要动态申请。如果你实在是不想支持动态权限,就一定不要设置targetSdkVersion 23 及以上。