最近得手一个NEXUS,那么就顺带将项目适配到android6.0吧,免得以后有客户投诉,被老大找茬。6.0最重要一个变化就是权限管理,任何未被授予权限的逻辑都可能引起后续运行的崩溃。
这么说的有点太直白,还是说的具体一点:当我们要使用SD卡的读写权限的时候,在6.0之前我们直接在AndroidManifest.xml文件下配置相应的< uses-permission >即可,然后我们就可以几乎为所欲为的掌控了整个手机,尽管用户可能不希望我们如此这样。尽管国内第三方rom在很早之前就做了一些权限的控制,但是并没有暴露相关的api去告诉我们开发者如何控制。谷歌现在意识到这个问题的所在,在给予用户充分的权利的同时,也给了我们开发者api去操作这个流程
我们通过截图来看下吧
我们在使用api操作SD卡的读写时,除了仍然需要在AndroidManifest里面配置相应的必要声明,还需要在操作前动态获取SD卡的读写权限。如果什么都不处理的话,在你的logcat里面就有sdk给出的一段提示The permissoin is required - android.permission.WRITE_EXTERNAL_STORAGE
。申请权限之后,系统就会弹出图中所示的选择对话框,让用户去选择拒绝还是允许。如果选择允许的话,app就可以继续按照正常流程执行,反之app就将没有该权限以至于无法正常完成后续的业务逻辑,此时比较通用的方式就是告诉用户你不给我权限我就做不了什么事情,以免用户不清楚为什么这个功能丧失。
当然,你可以选择回避这个问题。这些问题仅仅是在targetSdkVersion 23
的情况下才会发生,如果你选择低于这个版本,那么在android6.0上发生的一切依然跟早期版本相同。但是用户依然可以在系统设置中选择禁止app获取某种权限,所以不做适配并不是一个长远的打算
通过adb shell pm list permissions -d -g命令进行查看到底系统有多少权限需要手动去申请
group:com.google.android.gms.permission.CAR_INFORMATION
permission:com.google.android.gms.permission.CAR_VENDOR_EXTENSION
permission:com.google.android.gms.permission.CAR_MILEAGE
permission:com.google.android.gms.permission.CAR_FUEL
group:android.permission-group.CONTACTS
permission:android.permission.WRITE_CONTACTS
permission:android.permission.GET_ACCOUNTS
permission:android.permission.READ_CONTACTS
group:android.permission-group.PHONE
permission:android.permission.READ_CALL_LOG
permission:android.permission.READ_PHONE_STATE
permission:android.permission.CALL_PHONE
permission:android.permission.WRITE_CALL_LOG
permission:android.permission.USE_SIP
permission:android.permission.PROCESS_OUTGOING_CALLS
permission:com.android.voicemail.permission.ADD_VOICEMAIL
group:android.permission-group.CALENDAR
permission:android.permission.READ_CALENDAR
permission:android.permission.WRITE_CALENDAR
group:android.permission-group.CAMERA
permission:android.permission.CAMERA
group:android.permission-group.SENSORS
permission:android.permission.BODY_SENSORS
group:com.taobao.permission-group.ACCOUNT
group:android.permission-group.LOCATION
permission:android.permission.ACCESS_FINE_LOCATION
permission:com.google.android.gms.permission.CAR_SPEED
permission:android.permission.ACCESS_COARSE_LOCATION
group:android.permission-group.STORAGE
permission:android.permission.READ_EXTERNAL_STORAGE
permission:android.permission.WRITE_EXTERNAL_STORAGE
group:android.permission-group.MICROPHONE
permission:android.permission.RECORD_AUDIO
group:android.permission-group.SMS
permission:android.permission.READ_SMS
permission:android.permission.RECEIVE_WAP_PUSH
permission:android.permission.RECEIVE_MMS
permission:android.permission.RECEIVE_SMS
permission:android.permission.SEND_SMS
permission:android.permission.READ_CELL_BROADCASTS
这里大致包括地理定位、短信息、电话、通讯录、麦克风、传感器、相机等,而且权限都是成组出现,一般情况下,我们也是按组对权限进行申请。只要允许了某组中的某一个权限,该组中的其他权限均被授予通过
主要的api其实不是很多,我们一个一个介绍
public static int checkSelfPermission(@NonNull Context context, @NonNull String permission)
这个是在ContextCompat类中用来判断是否app已经获取到某一个权限的使用权。如果返回android.content.pm.PackageManager#PERMISSION_GRANTED
,则说明app被授予权限,如果返回android.content.pm.PackageManager#PERMISSION_DENIED
则说明权限被禁止授予
public static boolean shouldShowRequestPermissionRationale(@NonNull Activity activity, @NonNull String permission)
这个是在ActivityCompat类以及FragmentCompat类中用来判断是否显示权限询问对话框。在权限申请的过程中,如果用户选择允许权限使用
或者在拒绝的同时又勾选了不再询问对话框
,这2种情况下该api就会返回false,表示不再弹出权限询问对话框
public static void requestPermissions(final @NonNull Activity activity, final @NonNull String[] permissions, final int requestCode)
这个是在ActivityCompat类以及FragmentCompat类中用来申请权限的方法,这个类似于startActivityForResult,通过回调方法public void onRequestPermissionsResult(int requestCode, @NonNull String[] permissions, @NonNull int[] grantResults)
来判断到底哪个权限被授予允许还是拒绝
OK就这么简单,没有什么特别复杂的地方,下面我们来实战一下
/**
* 判断是否缺少权限
* @param context
* @param permissions
* @return
*/
public static boolean lacksPermissions(Context context, String... permissions) {
for (String permission : permissions) {
if (ContextCompat.checkSelfPermission(context, permission)== PackageManager.PERMISSION_DENIED) {
return true;
}
}
return false;
}
通过这段代码进行审核,如果没有丢失需要申请的权限,则开始申请权限操作。有一个地方需要强烈注意下:由于使用的是Compat包,所以这段代码在低于6.0以下也是可以正常运行的,并且在6.0以下就算你在系统权限中把他关闭,通过api获取到的一样是授权通过状态
/**
* 用户关闭并不再提醒所有权限提示
* @param activity
* @param permissions
* @return
*/
public static boolean hasDelayAllPermissions(Activity activity, String... permissions) {
int count=0;
for (String permission : permissions) {
if (!ActivityCompat.shouldShowRequestPermissionRationale(activity, permission) && ContextCompat.checkSelfPermission(activity, permission)== PackageManager.PERMISSION_DENIED) {
count++;
}
}
if (count==permissions.length) {
return true;
}
return false;
}
这边我是想单独区分一下用户手动拒绝还是永久性拒绝这2种情况。因为就算你不加上这句判断,在回调过来的时候,依然可以通过grantResults
这个数组的返回情况去判断权限是否都被授予。
/**
* 申请权限
* @param context
* @param permissions
*/
public static void requestPermissions(Context context, String... permissions) {
ActivityCompat.requestPermissions((Activity) context, permissions, ParamUtils.RESULT_PERMISSION);
}
这段没什么好说的,就是申请你要的所有权限。
String delayDesp;
public interface OnPermissionCheckedListener {
void checked(boolean flag);
void grant();
void delay();
}
@Override
protected void onResume() {
super.onResume();
if (isCheckAgain) {
isCheckAgain=false;
checkPermission();
}
}
@Override
public void onRequestPermissionsResult(int requestCode, @NonNull String[] permissions, @NonNull int[] grantResults) {
super.onRequestPermissionsResult(requestCode, permissions, grantResults);
boolean isGrant=true;
for (int grantResult : grantResults) {
if (grantResult== PackageManager.PERMISSION_DENIED) {
isGrant=false;
break;
}
}
if (listener!=null) {
listener.checked(isGrant);
if (isGrant) {
if (listener!=null) {
listener.grant();
}
}
else {
new AlertDialog.Builder(BaseActivity.this).setTitle("提示")
.setMessage(delayDesp)
.setPositiveButton("确定", new DialogInterface.OnClickListener() {
@Override
public void onClick(DialogInterface dialog, int which) {
Intent intent = new Intent(Settings.ACTION_APPLICATION_DETAILS_SETTINGS);
intent.setData(Uri.parse("package:" + getPackageName()));
startActivity(intent);
isCheckAgain=true;
}
}).setNegativeButton("取消", new DialogInterface.OnClickListener() {
@Override
public void onClick(DialogInterface dialog, int which) {
if (listener!=null) {
listener.delay();
}
}
}).show();
}
}
}
这边我是在baseActivity里面进行逻辑处理,介绍一下流程,首先我定义一个接口用来实现真正授权逻辑,其次在授权回调方法中进行权限通过判断。如果权限被运行,则按照允许的逻辑进行,如果被拒绝,弹出对话框,将告诉用户不授予这个权限会发生什么情况,引导用户去系统设置里面再次开启,如果用户开启了,将通过onResume再次去尝试判断
在使用的时候,我们就按照这个流程去执行就行了
public void checkPermission() {
if (permission.size()==0) {
return;
}
String[] permissions=new String[permission.size()];
for (int i = 0; i < permission.size(); i++) {
permissions[i]=permission.get(i);
}
if (PermissionsUtils.lacksPermissions(this, permissions)) {
if (PermissionsUtils.hasDelayAllPermissions(this, permissions)) {
if (listener!=null) {
new AlertDialog.Builder(BaseActivity.this).setTitle("提示")
.setMessage(delayDesp)
.setPositiveButton("确定", new DialogInterface.OnClickListener() {
@Override
public void onClick(DialogInterface dialog, int which) {
Intent intent = new Intent(Settings.ACTION_APPLICATION_DETAILS_SETTINGS);
intent.setData(Uri.parse("package:" + getPackageName()));
startActivity(intent);
isCheckAgain=true;
}
}).setNegativeButton("取消", new DialogInterface.OnClickListener() {
@Override
public void onClick(DialogInterface dialog, int which) {
if (listener!=null) {
listener.delay();
}
}
}).show();
}
}
else {
PermissionsUtils.requestPermissions(this, permissions);
}
}
else {
if (listener!=null) {
listener.grant();
}
}
}
先去判断权限是否都被授予,如果有没有授予的情况,就去判断是否被用户永久禁止提示。有的话,直接引导用户开启而需要申请,反之则去申请,系统自动弹出授权对话框
String[] permissions={Manifest.permission.WRITE_EXTERNAL_STORAGE, Manifest.permission.READ_EXTERNAL_STORAGE, Manifest.permission.CAMERA};
setPermission(permissions, "为了您可以正常选择头像,\n请点击\"设置\"-\"权限\"-打开 \"存储空间\"与\"相机\" 权限。\n最后点击两次后退按钮,即可返回。");
setOnPermissionCheckedListener(new OnPermissionCheckedListener() {
@Override
public void checked(boolean flag) {
}
@Override
public void grant() {
}
@Override
public void delay() {
}
});
只要定义好所需权限并且设置好接口处理事件
Android M 新的运行时权限开发者需要知道的一切
Android6.0中的运行时请求权限
Android6.0权限管理
Android 6.0: 动态权限管理的解决方案