高仿京东实现6.0权限封装框架
适配android 6.0权限适配看代码log是18年11月份的事情了,
今天把当年的笔记搬出来
当时为什么要适配?
适配2.png 适配1.png参考现在的轮子
android 6.0权限适配 当时是参考京东app 做的,用京东app测试了一下,各流程走了一下如下图
京东app申请电话权限.png附上当时的一个gif 录屏
京东申请电话权限流程.mp4.gif测试过程中的一些细节如下:
0 。当上图中的对话框弹出后点击页面空白区域或物理返回按钮是无效的,并不会关闭对话框。
1 。测试的过程中发现进入的是京东的 【存储页面】(就是可以删除数据或清空缓存的页面),点击返回进入应用的信息页面,上图为简化起见直接用[应用的信息页]代替。
2。如果用户授权了京东的电话权限,在使用的过程中比如用户在京东的商品详情页,按home键手动打开京东的[应用的信息页]关闭京东的电话权限,并使京东的 app 切换回前台, 这时京东app页面还是会弹出上图 [禁止&不再询问下]文字下方的对话框的 。也就是说京东的每个页面在切换回前台时都会去检查是否具有 电话权限!!
这个功能应该是在BaseActivity中的onResule中做的处理,
但个人在开发过程中,发现 当关闭 app中的某个权限回到app后,app会 重启, 可以根 启动页设置的flag有关,这个问题,回头有时间在研究。
4 ,开发过程中 申请权限的对话框应该看成是一个activity页面,因为对话框每次打开和关闭都会调用上个activity中的onResule 和 onpaule 方法
3 。京东权限的分类:
a. 启动权限 app 必须拥有的权限,没有该权限,app 不让使用。
b. 非启动权限 ,app 不是必须拥有的权限,等使用时再去申请权限 比如摄像头权限,使用时去申请,如果没有该权限,不能使用app 摄像头功能.
如下开始正式介绍android中的6.0权限
android 6.0权限分为如下4组
正常(Normal Protection)权限
简介
1.对用户隐私没有较大影响或者不会带来安全问题。
2, android manifest 文件中需要显示声明
3.安装后就赋予这些权限,不需要显示提醒用户,用户也不能取消这些权限。
危险(Dangerous)权限
简介
危险权限实际上才是运行时权限主要处理的对象,这些权限可能引起隐私问题或者影响其他程序运行 这种权限也是我们需要适配的重点区域,所有的危险权限都是在运行时(需要时)才会申请。
分组
需要注意的是,权限进行了分组,每一组中只要有一个权限被授予了,那么组内其它权限也会被授予。例如,一旦WRITE_CONTENTS被授权了,APP也有READ_CONTACTS和GET_ACCOUNTS了。
危险权限可以归为以下几个分组:
CALENDAR(日历) READ_CALENDAR , WRITE_CALENDAR
CAMERA(照相机) CAMERA
CONTACTS(联系人) READ_CONTACTS , WRITE_CONTACTS , GET_ACCOUNTS
LOCATION(位置) ACCESS_FINE_LOCATION (访问精细的位置), ACCESS_COARSE_LOCATION(访问粗略的位置)
MICROPHONE(麦克风) RECORD_AUDIO(录音)
PHONE(手机) READ_PHONE_STATE , CALL_PHONE , READ_CALL_LOG , WRITE_CALL_LOG ,
ADD_VOICEMAIL(添加语音信箱) , USE_SIP(使用SIP协议 , PROCESS_OUTGOING_CALLS(程序拨出电话)
SENSORS(传感器) BODY_SENSORS
SMS SEND_SMS , RECEIVE_SMS , READ_SMS , RECEIVE_WAP_PUSH , RECEIVE_MMS
TORAGE(存储) READ_EXTERNAL_STORAGE ,WRITE_EXTERNAL_STORAGE
必须要适配运行时权限吗
如果各应用市场没有限制,你可以不适配,但适配了可以开启新系统的各种特性,用户可以更强精确,灵活地控制权限 .这些特性也会使app 更加安全,稳定,和符合android的规范。
不适配运行时权限会崩溃么
如果你代码中设置的targetSdkVersion >=23 , 那么说明你app已经启用了该平台的新特性,安装在小于6.0系统的手机上是没有问题的,但如果在安装在>=6.0系统的手机上时你的代码中就必须进行应用的更改来适配该平台的特性,否则会出现崩溃,比如如下代码:
TelephonyManager telephonyManager = (TelephonyManager)getSystemService(Context.TELEPHONY_SERVICE);
String deviceId = telephonyManager.getDeviceId();
if (deviceId.equals(mLastDeviceId)) {//This may cause NPE
//do something
}
该行代码如果运行在android6.0的系统上,你没有提前申请到相应权限时就会出现崩溃,因为获取DeviceId需要 获得手机状态权限,不能越权做事情。
如何申请权限
当执行的代码,需要用到相应权限时,应该先申请后使用
动态权限的申请流程是什么?
请参考上图【京东app申请电话权限】流程,其它权限申请流程于其一致。
动态权限的申请用到哪些api 及如何使用?
检测权限api
/**
* 检测某个权限是否授予
* @param context Context对象
* @param permission 需要检测的权限
*/
ContextCompat.checkSelfPermission(Context context, String permission);
//又或者使用子类的方法
ActivityCompat.checkSelfPermission(Context context, String permission);
//minSdkVersion >= 23 可以直接使用
activity.checkSelfPermission(String permission);
/**
* 授权了
*/
public static final int PERMISSION_GRANTED = 0;
/**
* 拒绝了
*/
public static final int PERMISSION_DENIED = -1;
//检测某个权限是否授予
if (ContextCompat.checkSelfPermission(context, permission) == PackageManager.PERMISSION_GRANTED){
}
申请权限相关api
/**
* 申请相关权限
* @param activity Activity对象
* @param permissions 请求的权限组
* @param requestCode 本次请求码
申请相关权限。调用这个方法后会弹出一个系统对话框来向用户申请权限,APP不能自定义这个对话框的内容,这也就增加了上面提到的解释说明的必要性。这里还有一点也需要交代一下。从上面危险权限列表中也可以看出,这些权限都是有分组的。如,READ_EXTERNAL_STORAGE 和 WRITE_EXTERNAL_STORAGE权限就是属于STORAGE组的。分门别类不仅仅是为了方便容易阅读,组内权限在申请上也是有关联的
在申请组内某个权限时,弹出的系统对话框会显示组名,而不是指明所申请的权限名。如,申请READ_EXTERNAL_STORAGE权限时,系统对话框提示请求“访问sd卡”权限,但不会说明是请求的sd卡读权限
申请权限时,在使用每一条权限时都必须(不是应该)调用requestPermissions()方法来申请权限。如,在已经获取了READ_EXTERNAL_STORAGE权限的情况下,使用WRITE_EXTERNAL_STORAGE权限时依然需要调用requestPermissions()方法来申请,否则就会因为权限问题导致写sd卡失败
经过一定的测试,得到以下结论
1第一次安装后请求权限:没有不再询问的选项
2被拒绝后再次请求权限,会有不再询问的选项
被拒绝权限且不再询问,后面再请求是不会再弹框!!!!
*/
ActivityCompat.requestPermissions(Activity activity, String[] permissions, int requestCode);
//minSdkVersion >= 23 可以直接使用
activity.requestPermissions(String[] permissions, int requestCode);
处理权限回调结果api
/**
* Activity处理权限结果回调
* @param requestCode 权限请求码
* @param permissions 请求的权限组
* @param grantResults 请求的结果
该方法在Activity或Fragment中应该被重写,当用户处理完授权操作时,系统会自动回调该方法
int requestCode: 权限请求码,和requestPermissions的同名参数对应
String[] permissions: 请求权限组,和requestPermissions的同名参数对应
int[] grantResults: 授权结果数组,用于区分上一个参数permissions中的权限有没有被授予,permissions和grantResults两个数组大小是一样的,具体值和上方提到的PackageManager中的两个常量做比较
举个栗子,如何判断请求的这些权限有没有被全部授予
for (int i = 0; i < grantResults.length ; i++) {
if (grantResults[i] == PackageManager.PERMISSION_DENIED) {
return false;
}
}
return true;
*/
@Override
public void onRequestPermissionsResult(int requestCode, @NonNull String[] permissions, @NonNull int[] grantResults) {}
是否需要向用户解释api
/**
* 是否需要向用户解释
* @param activity Activity对象
* @param permission 需要检测的权限
判断是否需要向用户解释,为什么需要这些权限。有时候用户会不理解应用程序为什么需要这些权限。如,相机应用申请摄像头使用权限用户容易理解,但是相机应用申请地理位置使用权限可能会让用户产生疑惑,因为用户很有能不知道相机需要保存每张照片的拍摄地点。这时候我们就需要做适当的解释说明了。这个方法只有在APP请求过某一权限且用户禁止APP使用该权限的时候返回true。在用户授权了权限和禁止权限时勾选了“Don't ask again”选项的情况下都会返回false。Android官方开发指导还提到一点,为避免给用户带来糟糕的用户体验,这里的解释说明应该是异步的,不要阻塞用户的操作。时下很多适配了6.0的APP在这点上处理的都不尽如人意,有的根本没有解释说明,有的是弹出对话框,用户体验都不是很好
为了帮助查找用户可能需要解释的情形,Android 提供了一个实用程序方法,即 shouldShowRequestPermissionRationale()。如果应用之前请求过此权限但用户拒绝了请求,此方法将返回 true
如果用户在过去拒绝了权限请求,并在权限请求系统对话框中选择了 Don’t ask again 选项,此方法将返回 false。如果设备规范禁止应用具有该权限,此方法也会返回 false
《下面是不同应用场景调用的结果,已经过一定的测试》
之前没有拒绝过此权限的申请(第一次安装后请求权限前调用):false
曾经被拒绝过权限后再调用:true
曾经被拒绝过权限且不再询问后再调用:false
系统不允许任何程序获取该权限:false
查看源码得知安卓6.0以下返回:false
总是允许权限后再次调用:false
由此可以得出一个结论,只有曾经拒绝过才需要向用户解释,这句代码应该在Activity的onRequestPermissionsResult中调用比较合适,调用之前应该需要先判断是否为6.0以上设备
*/
ActivityCompat.shouldShowRequestPermissionRationale(Activity activity, String permission);
//minSdkVersion >= 23 可以直接使用
activity.shouldShowRequestPermissionRationale(String permission);
申请过的权限可以撤销吗?
一个权限被用户允许后,可以被撤销,撤销权限的用户操作一共有两种:
1.在应用信息-权限设置页面,进行取消
2.直接删除所有数据
3. 对于需要权限的操作,在使用时每次都需要判断是否已经授权,因为用户可以随时收回权限。
特殊(Particular)权限
1 ,看权限名就知道特殊权限比危险权限更危险,特殊权限需要在manifest中申请并且通过发送Intent让用户在设置界面进行勾。
2这些权限,在Android系统中,主要由两个
SYSTEM_ALERT_WINDOW,设置悬浮窗
WRITE_SETTINGS 修改系统设置
关于上面两个特殊权限的授权的用法是使用startActivityForResult启动授权界面来完成 如下 ,具体的处理
请求SYSTEM_ALERT_WINDOW
private static final int REQUEST_CODE = 1;
private void requestAlertWindowPermission() {
Intent intent = new Intent(Settings.ACTION_MANAGE_OVERLAY_PERMISSION);
intent.setData(Uri.parse("package:" + getPackageName()));
startActivityForResult(intent, REQUEST_CODE);
}
@Override
protected void onActivityResult(int requestCode, int resultCode, Intent data) {
super.onActivityResult(requestCode, resultCode, data);
if (requestCode == REQUEST_CODE) {
if (Settings.canDrawOverlays(this)) {
Log.i(LOGTAG, "onActivityResult granted");
}
}
}
上述代码需要注意的是
使用Action Settings.ACTION_MANAGE_OVERLAY_PERMISSION启动隐式Intent
使用"package:" + getPackageName()携带App的包名信息
使用Settings.canDrawOverlays方法判断授权结果
但这并非可靠的方式 ,更为可靠的方式请参靠如下链接
请求WRITE_SETTINGS
注意:
在android 6.0及以后,WRITE_SETTINGS权限的保护等级已经由原来的dangerous升级为signature,这意味着我们的APP需要用系统签名或者成为系统预装软件才能够申请此权限,并且还需要提示用户跳转到修改系统的设置界面去授予此权限 ,参考如下:
安卓6.0系统权限问题android.permission.WRITE_SETTINGS
private static final int REQUEST_CODE_WRITE_SETTINGS = 2;
private void requestWriteSettings() {
Intent intent = new Intent(Settings.ACTION_MANAGE_WRITE_SETTINGS);
intent.setData(Uri.parse("package:" + getPackageName()));
startActivityForResult(intent, REQUEST_CODE_WRITE_SETTINGS );
}
@Override
protected void onActivityResult(int requestCode, int resultCode, Intent data) {
super.onActivityResult(requestCode, resultCode, data);
if (requestCode == REQUEST_CODE_WRITE_SETTINGS) {
if (Settings.System.canWrite(this)) {
Log.i(LOGTAG, "onActivityResult write settings granted" );
}
}
}
测试过程中发现的问题
activity 分别调用 startActivity 或 startActivityForResult方法打开应用详情页 对 activity 页面生命周期的影响
/**
* 跳转到应用详情页面
*
* @param context
*/
public static void goToAppDetailSettingIntent(@NonNull Activity context) {
Intent localIntent = new Intent();
localIntent.addFlags(Intent.FLAG_ACTIVITY_NEW_TASK);
if (Build.VERSION.SDK_INT >= 9) {
localIntent.setAction(Settings.ACTION_APPLICATION_DETAILS_SETTINGS);
localIntent.setData(Uri.fromParts("package", context.getPackageName(), null));
} else if (Build.VERSION.SDK_INT <= 8) {
localIntent.setAction(Intent.ACTION_VIEW);
localIntent.setClassName("com.android.settings", "com.android.settings.InstalledAppDetails");
localIntent.putExtra("com.android.settings.ApplicationPkgName", context.getPackageName());
}
if (context != null){
//context.startActivityForResult(localIntent,1000);
context.startActivity(localIntent);
}
使用 startActivity 跳转到应用详情页面 Log 如下:
UUY: onPause mObtainPermissionFlag : false
其中 UUY 为 TAG , onPause 为 activity执行 onPause 方法 mObtainPermissionFlag 为页面的一个标志,请忽略。
使用 startActivityForResult 打开应用详情页面 Log 如下:
UUY: onPause mObtainPermissionFlag : false
onActivityResult requestCode : 1000 resultCode : 0
UUY: onResume mObtainPermissionFlag : false
UUY: onPause mObtainPermissionFlag : false
UUY 为 TAG ,mObtainPermissionFlag 为页面的一个标志,请忽略。
第一行Log很好理解,由于打开了应用详情,当前页面会执行 onPause 方法
疑惑点:
1 .2~4行Log就不理解了,当通过activity 的 startActivityForResult 打开应用信息页面 为什么会执行 activity 页面的 onActivityResult 方法呢?
2 . activity 为什么又会执行 一次 onResume 和 onPause 方法呢?
当用户勾选了不再询问时并且 禁用应用的存储权限后,杀掉进程启动app到Logo activity页面时 Log 如下,
UUY: onResume mObtainPermissionFlag : true
UUY: requestPermissions
UUY: onPause mObtainPermissionFlag : false
UUY: onRequestPermissionsResult
UUY: onResume mObtainPermissionFlag : false
第1行Log : //执行onResume方法 ,执行获取权限方法 , 并将mObtainPermissionFlag 置为false
第2行Log : //执行获取权限方法
第3行Log : //打开申请权限(permissionActivity)页面,当前activity 执行onPause 方法, mObtainPermissionFlag 标志为false
第4行Log : //由于之前用户已经勾选了不再询问并且禁用应用的存储权限,所以 onRequestPermissionsResult
方法会立即执行并关闭当前权限页面,申请对应权限状态为 PERMISSION_DENIED ,并且 经过ActivityCompat.shouldShowRequestPermissionRationale 方法判断会返回 false;
第5行Log : // 由于权限页面关闭,activity需要重新执行 onResume 方法
由于禁用列表集合disableList 不为空,所以弹“前往应用权限设置打开权限页面”对话框,当用户点击对话框中的[去打开]按钮后 调用 goToAppDetailSettingIntent 方法跳转到应用详情页面, 用户在该页面单击返回,由于这是一个强制用户开启的权限,如果不开启该权限将无法使用app ,所以正常情况下activity界面应再次弹框,告诉用户 用应用设置页打开相应权限。
参考:
Fragment之setRetainInstance详解
项目代码及封装原理
项目 会上传到我github里
封装原理:
创建一个空的,配置改变时(setRetainInstance)不会被重新创建的 Fragment来做中传和统一处理,降低代码耦合度 Fragment 中去requestPermissions ,然后回调Fragment的onRequestPermissionsResult方法来处理授权情况。
项目代码