本文是引用自国外Android开发者写的一篇关于Android 6.0运行时权限的文章。个人认为写的是相当详细,基本这一篇文章,就可以解决掉6.0权限兼容的问题,如果设计到侵权,请联系本人删除。原文链接:
Everything every Android Developer must know about new Android’s Runtime Permission
Android开发者应该知道,自从Android 6.0推出后,我们仍在开发中的应用,在测试的时候,会因为一些文件的读写,读取手机联系人,获取手机信息等操作,而发生程序异常。这就是这篇文章所要介绍的Android 6.0 运行时权限的动态申请。
[Toc]
新的运行时权限
安卓权限系统一直备受关注,应用一旦被安装,应用将会获取应用所需要的权限,而不需要用户的同意。毫无疑问,总有一些人或者开发者利用这个权限漏洞去获取私人数据,降低手机的安全性。在 Android 6.0 棉花糖,应用权限被重新设计,应用将不会在安装时被默认授权这些运行时权限,而是在程序运行时,会一个一个的对权限征询用户的同意。
值得注意的是,第二张图的出现需要开发者去编写代码来实现,才能出现上图弹框,在6.0上如果没有相关的运行时权限,则会出现如下图所示的应用程序异常
除此之外,6.0手机用户还可以在任何时候通过手机的设置->应用程序的权限设置取消这些权限的授权。
看到上面的介绍,你或许已经有点冷风扑面的感觉…,如果你是一位安卓开发者,你会感觉到应用程序的逻辑完全需要改变,当你调用一个需要权限的方法的时候,必须要去检查权限,否则你的应用程序将出现崩溃。
6.0 授权方式对于用户来说是好事,但是对于开发者,我们必须通过编码去兼容我们将来会发布在6.0以上设备的应用程序,否则应用会在短时间或者将来出现一定的问题。
值得注意的是,当我们设置应用程序的targetSdkVersion=23或者大于23时的时候,运行时权限,必须要代码去实现权限的申请,否则应用程序将会异常或者崩溃,当设置targetSdkVersion小于23时,如果应用程序运行在6.0设备上,应用程序会出现权限申请的弹框,此事如果你拒绝了权限的申请,应用程序不会崩溃,只是相应的功能因为权限的问题而不能实现。
如果应用程序已经发布,会因为权限发生什么呢?
如果我们的应用是三年请发布的,那么它安装在6.0的手机上会不会出现崩溃呢?并不用担心,Android 开发团队已经帮我们考虑到这个问题,官方的描述:If the application’s targetSdkVersion is set to less than 23. It will be assumed that application is not tested with new permission system yet and will switch to the same old behavior: user has to accept every single permission at install time and they will be all granted once installed !,也就是说如果我们的应用设置的targetSdkVersion小于23的时候,应用程序在安装的时候会默认授权这些权限,和6.0以前的行为一致。
正因为这样,你已经发布的应用就能完美的运行在6.0设备上,但值得注意的是,用户仍然可以在任意时候通过手机的设置去取消这些权限,当用户点击取消的时候,Android系统会发出一个提示,如下图:
当点击了拒绝后,在targetSdkVersion<23的时候,应用程序在涉及到权限的方法的时候是不会崩溃的,但是此时,方法的功能无法实现。
为了完美的解决这些权限问题,我们最好要兼容我们的应用程序新的权限系统。
当应用程序还在开发中,且未对权限进行兼容,建议将targetSdkVersion设置小于23,
注意:现在我们用Android Studio创建项目时,会自动指定targetSdkVersion=23或者23以上,如果我们还没准备要让自己的应用程序去兼容运行时权限系统,此时需要我们手动设置targetSdkVersion<23
系统自动授权的权限
下面的列举的是应用程序在安装时就自动授权的权限,且用户不能取消,我们称之为Normal Permission,下面是所有这些权限的列表:
android.permission.ACCESS_LOCATION_EXTRA_COMMANDS
android.permission.ACCESS_NETWORK_STATE
android.permission.ACCESS_NOTIFICATION_POLICY
android.permission.ACCESS_WIFI_STATE
android.permission.ACCESS_WIMAX_STATE
android.permission.BLUETOOTH
android.permission.BLUETOOTH_ADMIN
android.permission.BROADCAST_STICKY
android.permission.CHANGE_NETWORK_STATE
android.permission.CHANGE_WIFI_MULTICAST_STATE
android.permission.CHANGE_WIFI_STATE
android.permission.CHANGE_WIMAX_STATE
android.permission.DISABLE_KEYGUARD
android.permission.EXPAND_STATUS_BAR
android.permission.FLASHLIGHT
android.permission.GET_ACCOUNTS
android.permission.GET_PACKAGE_SIZE
android.permission.INTERNET
android.permission.KILL_BACKGROUND_PROCESSES
android.permission.MODIFY_AUDIO_SETTINGS
android.permission.NFC
android.permission.READ_SYNC_SETTINGS
android.permission.READ_SYNC_STATS
android.permission.RECEIVE_BOOT_COMPLETED
android.permission.REORDER_TASKS
android.permission.REQUEST_INSTALL_PACKAGES
android.permission.SET_TIME_ZONE
android.permission.SET_WALLPAPER
android.permission.SET_WALLPAPER_HINTS
android.permission.SUBSCRIBED_FEEDS_READ
android.permission.TRANSMIT_IR
android.permission.USE_FINGERPRINT
android.permission.VIBRATE
android.permission.WAKE_LOCK
android.permission.WRITE_SYNC_SETTINGS
com.android.alarm.permission.SET_ALARM
com.android.launcher.permission.INSTALL_SHORTCUT
com.android.launcher.permission.UNINSTALL_SHORTCUT</font>
当应用程序使用到这些权限时,仅仅需要在AndroidManifest.xml文件中声明权限即可。
让你的应用兼容运行时权限
在module的build.gradle中配置targetSdkVersion>=23,然后设置你的编译语言版本compileSdkVersion=23
在下面的示例中,实现一个向手机通讯录插入一个联系人的功能(该功能的实现需要相应的运行时权限),我将原作者的示例换了一种方式写入。
private void insertContact(String name, String phoneNumber) {
ContentValues values = new ContentValues();
// 插入一个空的联系人,获取成成
Uri rawContactUri = getContentResolver().insert(ContactsContract.RawContacts.CONTENT_URI, values);
long rawContactId = ContentUris.parseId(rawContactUri);
values.clear();
//添加联系人名字
values.put(ContactsContract.Data.RAW_CONTACT_ID, rawContactId);
values.put(ContactsContract.Data.MIMETYPE, ContactsContract.CommonDataKinds.StructuredName.CONTENT_ITEM_TYPE);
values.put(ContactsContract.CommonDataKinds.StructuredName.GIVEN_NAME, name);
getContentResolver().insert(ContactsContract.Data.CONTENT_URI, values);
values.clear();
//给该联系人添加电话
values.put(ContactsContract.Data.RAW_CONTACT_ID, rawContactId);
values.put(ContactsContract.Data.MIMETYPE, ContactsContract.CommonDataKinds.Phone.CONTENT_ITEM_TYPE);
values.put(ContactsContract.CommonDataKinds.Phone.NUMBER, phoneNumber);
values.put(ContactsContract.CommonDataKinds.Phone.TYPE, ContactsContract.CommonDataKinds.Phone.TYPE_MOBILE);
getContentResolver().insert(ContactsContract.Data.CONTENT_URI, values);
values.clear();
}
同时,我们在AndroidManifest.xml中配置写入联系人权限
<uses-permission android:name="android.permission.WRITE_CONTACTS"/>
这段程序直接运行在6.0的手机上,且targetSdkversion大于23的时候会直接崩溃,提示 Permission Denial。接下来我们就去解决这个问题,也就是适配新的运行时权限系统。下面列出了一些权限组,当我们对权限组授权,即可获得权限组下面的所有权限。列表如下
系统源码里面检查权限以及申请权限的方法分别为Activity的方法checkSelfPermission和requestPermissions。这两个方法在API 23(6.0)及其以上版本才有。
final private int REQUEST_CODE = 101;
final private String WRITE_CONTACTS = Manifest.permission.WRITE_CONTACTS;
private void insertContactWrapper() {
if (Build.VERSION.SDK_INT > Build.VERSION_CODES.M) {//API 23以上 这里用的minSdkVersion=14
int checkSelfPermission = checkSelfPermission(WRITE_CONTACTS);
if (checkSelfPermission != PackageManager.PERMISSION_GRANTED) {//如果没有权限
requestPermissions(new String[]{WRITE_CONTACTS}, REQUEST_CODE);
return;
}
}
insertContact("Minion", "18888887777");
}
/**
* 当体统弹出权限申请的对话框后 点击拒绝或者确定的回调
* permissions 和 grantResults是一一对应的,顺序当然也是
*
* @param requestCode 对应权限请求码
* @param permissions 权限集合
* @param grantResults 授权结果
*/
@Override
public void onRequestPermissionsResult(int requestCode, String[] permissions, int[] grantResults) {
switch (requestCode) {
case REQUEST_CODE:
if (grantResults[0] == PackageManager.PERMISSION_GRANTED) {
// 权限拒绝
insertContact("Minion", "18888887777");
} else {
// 权限拒绝
Toast.makeText(MainActivity.this, "写入联系人权限被拒绝", Toast.LENGTH_SHORT)
.show();
}
break;
default:
super.onRequestPermissionsResult(requestCode, permissions, grantResults);
}
}
再次调用程序的时候,会出现如下图所示的界面(用的华为6.0系统手机测试的截图):
这里面点击的选择有三种情况:1、单选禁止,2、单选始终允许,3、勾选了禁止后不再询问,然后选择禁止。
1、单选禁止:该情况下,插入联系人失败,直接走权限拒绝的回调,退出应用程序再次进入,依然出现申请权限的Dialog。这种情况最简单直接,就不做过多说明。
2、单选始终允许,那么走授权成功的回调,联系人写入成功。退出应用程序再次进入,就不会出现权限申请的Dialog。
3、勾选了禁止后不再询问,然后选择禁止。直接走权限拒绝的回调,退出应用程序再次进入,不会出现权限申请的dialog,直接走权限拒绝的回调。
针对上面的三种情况1,2两种情况,上面的示例代码已经足够去解决这个问题了,但是对于情况3,可能你已经意识到,用户拒绝权限且选择不再询问,那我们的应用的谢联系人功能永远不可用,显然那是不合理的。那如何解决这个问题呢?其实系统已经提供了方法,让我们来解决这个问题。只需要在上面的insertContactWrapper()方法稍微改动即可实现,代码如下:
private void insertContactWrapper() {
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.M) {//API 23以上 这里用的minSdkVersion=14
int checkSelfPermission = checkSelfPermission(WRITE_CONTACTS);
if (checkSelfPermission != PackageManager.PERMISSION_GRANTED) {//如果没有权限
//判断用户是否针对写联系人权限勾选了在不再询问选项,也就是不弹框申请权限
if (!shouldShowRequestPermissionRationale(WRITE_CONTACTS)) {
requestPermissions(new String[]{Manifest.permission.WRITE_CONTACTS},
REQUEST_CODE);
return;
}
requestPermissions(new String[]{WRITE_CONTACTS}, REQUEST_CODE);
}
}
insertContact("Minion", "18888887777");
}
通过以上方法,就完全实现了应用运行在6.0以上的运行时单个权限兼容问题。
多个权限申请
上面将的是单个权限申请,但是一般应用都需要很多的权限,下面就多个权限的申请讲解,其实只需要在上面的代码中稍微修改,增加循环即可。代码示例如下:
private void insertContactWrapper() {
//权限是没有授权的集合
List<String> permissionsList = new ArrayList<>();
//权限是拒绝了且勾选了不再提示的集合
List<String> permissionsNeeded = new ArrayList<>();
if (!addPermission(permissionsList, WRITE_CONTACTS))
permissionsNeeded.add(WRITE_CONTACTS);
if (!addPermission(permissionsList, READ_CONTACTS))
permissionsNeeded.add(READ_CONTACTS);
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.M) {//API 23以上 这里用的minSdkVersion=14
if (permissionsList.size() > 0) {
if (permissionsNeeded.size() > 0) {
requestPermissions(permissionsNeeded.toArray(new String[permissionsNeeded.size()]),
REQUEST_CODE);
return;
}
requestPermissions(permissionsList.toArray(new String[permissionsList.size()]),
REQUEST_CODE);
return;
}
}
insertContact("Minion", "18888887777");
}
/**
* 筛选出拒绝的权限,且选择了不再询问的权限
*
* @param permissionsList
* @param permission
* @return
*/
private boolean addPermission(List<String> permissionsList, String permission) {
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.M) {//API 23以上 这里用的minSdkVersion=14
if (checkSelfPermission(permission) != PackageManager.PERMISSION_GRANTED) {
permissionsList.add(permission);
if (!shouldShowRequestPermissionRationale(permission))
return false;
}
}
return true;
}
当权限集合分别被授权时, 都会在回调 onRequestPermissionsResult
方法里面进行回调,示例如下,可以根据需求灵活判断:
@Override
public void onRequestPermissionsResult(int requestCode, String[] permissions, int[] grantResults) {
switch (requestCode) {
case REQUEST_CODE:
if (grantResults[0] == PackageManager.PERMISSION_GRANTED
&& grantResults[1] == PackageManager.PERMISSION_GRANTED) {
// 权限允许
insertContact("Minion", "18888887777");
} else {
// 权限拒绝
Toast.makeText(Main2Activity.this, "读写联系人权限被拒绝", Toast.LENGTH_SHORT)
.show();
}
break;
default:
super.onRequestPermissionsResult(requestCode, permissions, grantResults);
}
}
使用支持库让代码向上兼容
细心的你可能已经发现了,上面的某个代码片段使得if的缩进很迷
//API 23以上 这里用的minSdkVersion=14
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.M) {
}
有没有兼容包来代替这个判断呢,答案是肯定的。
ContextCompat.checkSelfPermission()
ActivityCompat.requestPermissions()
ActivityCompat.shouldShowRequestPermissionRationale()
以上三个方法都引用了Support Library v4 的兼容类,相应的该方法增加了传递context或者activity参数在方法里,就可以很好的去掉if判断,那么上面的多权限动态申请权限的代码可以优化为如下
private void insertContactWrapper() {
//权限是没有授权的集合
List<String> permissionsList = new ArrayList<>();
//权限是拒绝了且勾选了不再提示的集合
List<String> permissionsNeeded = new ArrayList<>();
if (!addPermission(permissionsList, WRITE_CONTACTS))
permissionsNeeded.add(WRITE_CONTACTS);
if (!addPermission(permissionsList, READ_CONTACTS))
permissionsNeeded.add(READ_CONTACTS);
if (permissionsList.size() > 0) {
if (permissionsNeeded.size() > 0) {
ActivityCompat.requestPermissions(Main2Activity.this, permissionsNeeded.toArray(new String[permissionsNeeded.size()]),
REQUEST_CODE);
return;
}
ActivityCompat.requestPermissions(Main2Activity.this, permissionsList.toArray(new String[permissionsList.size()]),
REQUEST_CODE);
return;
}
insertContact("Minion", "18888887777");
}
private boolean addPermission(List<String> permissionsList, String permission) {
if (ContextCompat.checkSelfPermission(getApplicationContext(), permission) != PackageManager.PERMISSION_GRANTED) {
permissionsList.add(permission);
if (!ActivityCompat.shouldShowRequestPermissionRationale(Main2Activity.this, permission))
return false;
}
return true;
}
用开源的第三方来减少代码量
你应该注意到了,为了一个权限的申请,代码还是有点复杂。不用担忧的是,有一些相当优秀的第三方库去帮助我们解决这个问题,这里面原作者推荐了了一个经过比较很多库且让他满意的第三方库,就是hotchemi的PermissionsDispatcher
这个库提供了较为简短和方便的代码来实现权限的申请功能,你可以尝试是否能用在你的应用程序上面,如果不行的话,可以尝试上面原生的适配方案。
当应用程序打开时,如果权限被撤销,会发生什么?
我们知道,当应用程序在使用的过程中,可能会出现用户去手动在设置里面撤销相应的权限,那么需要权限的相应功能将不能在该应用已经打开的情况下使用,对于这种情况目前没有一个更好的解决方案,只能是在需要该功能的时候,就去申请权限
总结和建议
我相信经过上面介绍,你已经对新的权限系统有了一定的认识,同时肯定也知道了新的权限系统的问题所在。
然而,你没有选择,在Android5.0以上,已经在运行新的权限系统,我们没有后退的余地。唯一能做的就是立马让你的应用兼容新的权限系统。
好消息是,新的权限系统下,仅有少量权限需要运行是申请,大部分的常用权限,比如INTERNET,属于一般权限,在程序运行时自动授权不需要去申请。因此,你仅仅需要修改部分的代码.
有两点建议给你:
1)因为运行时权限,处理需要紧急兼容的功能部分;
2)如果你的代码还不支持运行时权限,不要设置targetSdkVersion为23。尤其是当你在Android Studio创建一个新项目的时候,不要忘记去查看一下module的build.gradle 文件的targetSdkVersion设置的是多少。
谈到源码修改,必须承认的是它是一个大的工作量,如果你的代码结构设计的没那么优秀,你将会话费大量的时间去重构兼容运行时权限,或者说至少你的源码需要去修改。正如上面所说,我们不得不做…
如果你的代码正在编写中,请把运行时权限的兼容作为一个紧急的TODO问题
希望这篇文章能对你有所帮助并快乐的编程
关于更多的运行时权限可以查看官方文档