写这篇文章的时候,安卓已经推出了9.0,然而公司项目的targetSdkVersion还停留在6.0之前,虽然谷歌对之前版本实现了兼容,但是作为一名开发者,与时俱进是必须的,况且“腾讯开发平台”计划在明年五月后不再上架targetSdkVersion低于26,也就是8.0的应用。所以,对公司应用进行了targetSdkVersion = 26的适配,项目相关,主要适配了6.0动态权限以及8.0的一些新特性。
动态权限
在6.0之前,当用户安装应用时,系统会提示用户应用需要的所有权限,用户允许所有权限后才可以安装并使用。这样的话,即使我们开发者在应用里加了用户不愿意接受的权限,用户也得同意授权才可以使用应用。谷歌也在考略这方面的优化,因此在6.0进行了相关的更新。更新后,权限可分为Normal Permissions,这类权限一般不涉及用户隐私,是不需要用户进行授权的,比如手机震动、访问网络等,以及Dangerous Permission,一般是涉及到用户隐私的,需要用户进行授权,比如读取sdcard、访问通讯录等,在用户不授权该类权限的情况下,应用也可以使用,只是用到相关权限对应的需求的时候,那么我们开发者就需要引导用户去进行对应的授权,以正常使用对应的应用功能。
首先,我们来掌握官方提供的对应API进行动态权限的适配。
- 在AndroidManifest文件中添加需要的权限。
- 检测权限
if (ContextCompat.checkSelfPermission(thisActivity,Manifest.permission.READ_CONTACTS)
!= PackageManager.PERMISSION_GRANTED) {
}else{
//
}
- 当我们的权限没有被授权时,那么就需要申请权限
ActivityCompat.requestPermissions(thisActivity,
new String[]{Manifest.permission.READ_CONTACTS},
MY_PERMISSIONS_REQUEST_READ_CONTACTS);
该方法是异步的,第一个参数是Context;第二个参数是需要申请的权限的字符串数组;第三个参数为requestCode,主要用于回调的时候检测。可以从方法名requestPermissions以及第二个参数看出,是支持一次性申请多个权限的,系统会通过对话框逐一询问用户是否授权。
- 权限申请后的回调
@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;
}
}
}
首先验证requestCode定位到你的申请,然后验证grantResults对应于申请的结果,这里的数组对应于申请时的第二个权限字符串数组。如果你同时申请两个权限,那么grantResults的length就为2,分别记录你两个权限的申请结果。
- 拒绝后再次申请
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.
}
上面的判断为true,那么表示用户已经拒绝过该权限的申请,那么开发者可以针对该权限进行一定的处理,比如向用户解释之类的。
ok,通过上面的步骤,我们就可以进行动态权限的适配了。通常情况下,我们会申请多个权限,如果单纯的通过上面的API的话,会显得代码很累赘,这样的情况下就会通过对API的封装来实现,GitHub上关于该API的封装很多,下面将对XXPermissions进行学习。
8.0适配
-
权限组改变
在 Android 8.0 之前,如果应用在运行时请求权限并且被授予该权限,系统会错误地将属于同一权限组并且在清单中注册的其他权限也一起授予应用。
对于针对 Android 8.0 的应用,此行为已被纠正。系统只会授予应用明确请求的权限。然而,一旦用户为应用授予某个权限,则所有后续对该权限组中权限的请求都将被自动批准。
例如,假设某个应用在其清单中列出 READ_EXTERNAL_STORAGE 和 WRITE_EXTERNAL_STORAGE。应用请求 READ_EXTERNAL_STORAGE,并且用户授予了该权限。如果该应用针对的是 API 级别 24 或更低级别,系统还会同时授予 WRITE_EXTERNAL_STORAGE,因为该权限也属于同一 STORAGE 权限组并且也在清单中注册过。如果该应用针对的是 Android 8.0,则系统此时仅会授予 READ_EXTERNAL_STORAGE;不过,如果该应用后来又请求 WRITE_EXTERNAL_STORAGE,则系统会立即授予该权限,而不会提示用户。 -
未知来源APK安装
Android 8.0去除了“允许未知来源”选项,所以如果我们的App有安装App的功能(检查更新之类的),那么会无法正常安装。当然,国内的很多手机厂商的系统进行了相关的适配,也能正常安装,但还是适配 上最好。
首先在AndroidManifest文件中添加安装未知来源应用的权限:
<uses-permission android:name="android.permission.REQUEST_INSTALL_PACKAGES"/>
这样系统会自动询问用户完成授权。当然你也可以先使用 canRequestPackageInstalls()查询是否有此权限,如果没有的话使用Settings.ACTION_MANAGE_UNKNOWN_APP_SOURCES这个action将用户引导至安装未知应用权限界面去授权。
private static final int REQUEST_CODE_UNKNOWN_APP = 100;
private void installAPK(){
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O) {
boolean hasInstallPermission = getPackageManager().canRequestPackageInstalls();
if (hasInstallPermission) {
//安装应用
} else {
//跳转至“安装未知应用”权限界面,引导用户开启权限
Uri selfPackageUri = Uri.parse("package:" + this.getPackageName());
Intent intent = new Intent(Settings.ACTION_MANAGE_UNKNOWN_APP_SOURCES, selfPackageUri);
startActivityForResult(intent, REQUEST_CODE_UNKNOWN_APP);
}
}else {
//安装应用
}
}
//接收“安装未知应用”权限的开启结果
@Override
protected void onActivityResult(int requestCode, int resultCode, Intent data) {
super.onActivityResult(requestCode, resultCode, data);
if (requestCode == REQUEST_CODE_UNKNOWN_APP) {
installAPK();
}
}
- 悬浮窗
使用 SYSTEM_ALERT_WINDOW 权限的应用无法再使用以下窗口类型来在其他应用和系统窗口上方显示提醒窗口:
TYPE_PHONE
TYPE_PRIORITY_PHONE
TYPE_SYSTEM_ALERT
TYPE_SYSTEM_OVERLAY
TYPE_SYSTEM_ERROR
相反,应用必须使用名为 TYPE_APPLICATION_OVERLAY 的新窗口类型。
先添加对应的权限:
<uses-permission android:name="android.permission.SYSTEM_ALERT_WINDOW"/>
<uses-permission android:name="android.permission.SYSTEM_OVERLAY_WINDOW" />
需要在之前的基础上判断一下:
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O) {
mWindowParams.type = WindowManager.LayoutParams.TYPE_APPLICATION_OVERLAY
}else {
mWindowParams.type = WindowManager.LayoutParams.TYPE_SYSTEM_ALERT
}
XXPermissions解析
在GitHub上关于权限适配的开源库很多,但是万变不离其宗,都是以上面提到的系统API来进行封装。
项目地址:https://github.com/getActivity/XXPermissions
XXPermissions的基本用法:
List<String> arrayList = new ArrayList<>();
XXPermissions.with(this)
.permission(arrayList)//故意放一个空的list,该方法判断为空后会获取AndroidManifest.xml里申明的所有权限
.request(new OnPermission() {
@Override
public void hasPermission(List<String> granted, boolean isAll) {
if (isAll) {
Toast.makeText(ActivityLaunch.this, "获取权限成功", Toast.LENGTH_SHORT).show();
}else {
Toast.makeText(ActivityLaunch.this, "获取权限成功,部分权限未正常授予", Toast.LENGTH_SHORT).show();
}
}
@Override
public void noPermission(List<String> denied, boolean quick) {
if(quick) {
Toast.makeText(ActivityLaunch.this, "被永久拒绝授权,请手动授予权限", Toast.LENGTH_SHORT).show();
//如果是被永久拒绝就跳转到应用权限系统设置页面
XXPermissions.gotoPermissionSettings(ActivityLaunch.this);
}else {
Toast.makeText(ActivityLaunch.this, "获取权限失败", Toast.LENGTH_SHORT).show();
}
}
});
代码就不详细解释了,通过注释就了然了。几行代码搞定有没有,感谢开源!作者定义了OnPermission接口,供权限申请回调后使用:
public interface OnPermission {
/**
* 有权限被授予时回调
*
* @param granted 请求成功的权限组
* @param isAll 是否全部授予了
*/
void hasPermission(List<String> granted, boolean isAll);
/**
* 有权限被拒绝授予时回调
*
* @param denied 请求失败的权限组
* @param quick 是否有某个权限被永久拒绝了
*/
void noPermission(List<String> denied, boolean quick);
}
看一下权限申请的主要流程:
当我们调用request方法后,开始权限的申请,
public void request(OnPermission call) {
//如果没有指定请求的权限,就使用清单注册的权限进行请求
if (mPermissions == null || mPermissions.size() == 0) mPermissions = PermissionUtils.getManifestPermissions(mActivity);
if (mPermissions == null || mPermissions.size() == 0) throw new IllegalArgumentException("The requested permission cannot be empty");
//使用isFinishing方法Activity在熄屏状态下会导致崩溃
//if (mActivity == null || mActivity.isFinishing()) throw new IllegalArgumentException("Illegal Activity was passed in");
if (mActivity == null) throw new IllegalArgumentException("The activity is empty");
if (call == null) throw new IllegalArgumentException("The permission request callback interface must be implemented");
ArrayList<String> failPermissions = PermissionUtils.getFailPermissions(mActivity, mPermissions);
if (failPermissions == null || failPermissions.size() == 0) {
//证明权限已经全部授予过
call.hasPermission(mPermissions, true);
} else {
//检测权限有没有在清单文件中注册
PermissionUtils.checkPermissions(mActivity, mPermissions);
//申请没有授予过的权限
PermissionFragment.newInstant((new ArrayList<>(mPermissions)), mConstant).prepareRequest(mActivity, call);
}
}
首先检测用户是否指定了申请权限,如果没有则去获取申请的所有权限。获取到需要申请的权限后,判断这些权限种是否有未授权,若全部已授权则回调hasPermission方法,反之通过PermissionFragment类开启权限的正式申请。
在PermissionFragment类中,首先通过newInstant方法进行初始化:
public static PermissionFragment newInstant(ArrayList<String> permissions, boolean constant) {
PermissionFragment fragment = new PermissionFragment();
Bundle bundle = new Bundle();
int requestCode;
//请求码随机生成,避免随机产生之前的请求码,必须进行循环判断
do {
//requestCode = new Random().nextInt(65535);//Studio编译的APK请求码必须小于65536
requestCode = new Random().nextInt(255);//Eclipse编译的APK请求码必须小于256
} while (sContainer.get(requestCode) != null);
bundle.putInt(REQUEST_CODE, requestCode);
bundle.putStringArrayList(PERMISSION_GROUP, permissions);
bundle.putBoolean(REQUEST_CONSTANT, constant);
fragment.setArguments(bundle);
return fragment;
}
prepareRequest方法将当前的fragment放到任务中,
public void prepareRequest(Activity activity, OnPermission call) {
//将当前的请求码和对象添加到集合中
sContainer.put(getArguments().getInt(REQUEST_CODE), call);
activity.getFragmentManager().beginTransaction().add(this, activity.getClass().getName()).commit();
}
然后requestPermission方法开始真正的权限申请:
public void requestPermission() {
if (PermissionUtils.isOverMarshmallow()) {
ArrayList<String> permissions = getArguments().getStringArrayList(PERMISSION_GROUP);
requestPermissions(permissions.toArray(new String[permissions.size() - 1]), getArguments().getInt(REQUEST_CODE));
}
}
到这里权限申请就ok了,接下来就是处理申请回调的结果。
@Override
public void onRequestPermissionsResult(int requestCode, String[] permissions, int[] grantResults) {
OnPermission call = sContainer.get(requestCode);
//根据请求码取出的对象为空,就直接返回不处理
if (call == null) return;
//获取授予权限
List<String> succeedPermissions = PermissionUtils.getSucceedPermissions(permissions, grantResults);
//如果请求成功的权限集合大小和请求的数组一样大时证明权限已经全部授予
if (succeedPermissions.size() == permissions.length) {
//代表申请的所有的权限都授予了
call.hasPermission(succeedPermissions, true);
} else {
//获取拒绝权限
List<String> failPermissions = PermissionUtils.getFailPermissions(permissions, grantResults);
//检查是否开启了继续申请模式,如果是则检查没有授予的权限是否还能继续申请
if (getArguments().getBoolean(REQUEST_CONSTANT)
&& PermissionUtils.isRequestDeniedPermission(getActivity(), failPermissions)) {
//如果有的话就继续申请权限,直到用户授权或者永久拒绝
requestPermission();
return;
}
//代表申请的权限中有不同意授予的,如果有某个权限被永久拒绝就返回true给开发人员,让开发者引导用户去设置界面开启权限
call.noPermission(failPermissions, PermissionUtils.checkMorePermissionPermanentDenied(getActivity(), failPermissions));
//证明还有一部分权限被成功授予,回调成功接口
if (!succeedPermissions.isEmpty()) {
call.hasPermission(succeedPermissions, false);
}
}
//权限回调结束后要删除集合中的对象,避免重复请求
sContainer.remove(requestCode);
getFragmentManager().beginTransaction().remove(this).commit();
}
作者在代码里注释还是很清楚了,整体逻辑还是比较清楚的。在该开源库中,还涉及到8.0系统的“未知来源安装”以及“悬浮窗”的权限申请,具体代码没有贴出来,感兴趣的话可以去看看源码。一般情况下,我们在应用的入口就检测权限,并引导用户进行权限的申请,对于一些比较重要的权限,在用户拒绝后可以提示用户该权限的重要性,也可以不断的弹窗直到用户授权后方可进入应用。
ok,关于6.0的权限适配以及8.0相关更新的介绍就到这里了,再次感谢开源!