public void onClick(View view) {
UtilsWithPermission.makeCall(getActivity(), “10086”);
}
});
其中完全拒绝以后,SoulPermission 提供了跳转到系统权限设置页的方法,我们再来看看效果:
很多时候,其实绿色部分(shouldShowRequestPermissionRationale)其实并不一定必要,反复的弹框用户可能会厌烦,大多数情况,我们这么封装就好:
public abstract class CheckPermissionAdapter implements CheckRequestPermissionListener {
@Override
public void onPermissionDenied(Permission permission) {
//SoulPermission提供栈顶Activity
Activity activity = SoulPermission.getInstance().getTopActivity();
if (null == activity) {
return;
}
String permissionDesc = permission.getPermissionNameDesc();
new AlertDialog.Builder(activity)
.setTitle(“提示”)
.setMessage(permissionDesc + “异常,请前往设置->权限管理,打开” + permissionDesc + “。”)
.setPositiveButton(“去设置”, new DialogInterface.OnClickListener() {
@Override
public void onClick(DialogInterface dialogInterface, int i) {
//去设置页
SoulPermission.getInstance().goPermissionSettings();
}
}).create().show();
}
}
我们再写一个选择联系人的方法:
/**
- 选择联系人
*/
public static void chooseContact(final Activity activity, final int requestCode) {
SoulPermission.getInstance().checkAndRequestPermission(Manifest.permission.READ_CONTACTS,
new CheckPermissionAdapter() {
@Override
public void onPermissionOk(Permission permission) {
activity.startActivityForResult(new Intent(Intent.ACTION_PICK, ContactsContract.Contacts.CONTENT_URI), requestCode);
}
});
}
在Activity中也是一行解决问题:
findViewById(R.id.bt_choose_contact).setOnClickListener(new View.OnClickListener() {
@Override
public void onClick(View view) {
UtilsWithPermission.chooseContact(AfterActivity.this, REQUEST_CODE_CONTACT);
}
});
代码细节请参考demo,我们再来看看效果:
主要源码分析
1、优雅的避掉onPermissionResult
适配权限最大的痛点在于:项目业务页面繁多,如果你想实现“真运行时权限”的话就需要在业务的Activity或者Fragment中去重写权限请求回调方法。
斟酌一番并且在参考了下RxPermission中对权限请求的处理,我决定用同样的方式—用一个没有界面的Fragment去完成我们权限请求的操作,下面贴上部分代码:
首先定义一个接口,用于封装权限请求的结果
public interface RequestPermissionListener {
/**
-
得到权限检查结果
-
@param permissions 封装权限的数组
*/
void onPermissionResult(Permission[] permissions);
}
然后是我们的Fragment:
public class PermissionSupportFragment extends Fragment implements IPermissionActions {
/**
- 内部维护requestCode
*/
private static final int REQUEST_CODE = 11;
/**
- 传入的回调
*/
private RequestPermissionListener listener;
@Override
public void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
//当状态发生改变,比如设备旋转时候,Fragment不会被销毁
setRetainInstance(true);
}
/**
-
外部请求的最终调用方法
-
@param permissions 权限
-
@param listener 回调
*/
@TargetApi(M)
@Override
public void requestPermissions(String[] permissions, RequestPermissionListener listener) {
requestPermissions(permissions, REQUEST_CODE);
this.listener = listener;
}
@TargetApi(M)
@Override
public void onRequestPermissionsResult(int requestCode, String[] permissions, int[] grantResults) {
super.onRequestPermissionsResult(requestCode, permissions, grantResults);
Permission[] permissionResults = new Permission[permissions.length];
//拿到授权结果以后对结果做一些封装
if (requestCode == REQUEST_CODE) {
for (int i = 0; i < permissions.length; ++i) {
Permission permission = new Permission(permissions[i], grantResults[i], this.shouldShowRequestPermissionRationale(permissions[i]));
permissionResults[i] = permission;
}
}
if (listener != null && getActivity() != null && !getActivity().isDestroyed()) {
listener.onPermissionResult(permissionResults);
}
}
}
其中Permission是我们的权限名称、授予结果、是否需要给用于一个解释的包装类:
public class Permission {
private static final String TAG = Permission.class.getSimpleName();
/**
- 权限名称
*/
public String permissionName;
/**
- 授予结果
*/
public int grantResult;
/**
- 是否需要给用户一个解释
*/
public boolean shouldRationale;
/**
- 权限是否已经被授予
*/
public boolean isGranted() {
return grantResult == PackageManager.PERMISSION_GRANTED;
}
//。。。
}
至此,我们已经利用自己实现的一个没有界面的Fragment封装了运行时权限相关的请求、RequestCode的维护、以及onPermissionResult的回调、在我们真正调用的时候代码是这样的:
/**
-
@param activity 栈顶 Activity
-
@param permissionsToRequest 待请求的权限
-
@param listener 回调
*/
private void requestRuntimePermission(final Activity activity, final Permission[] permissionsToRequest, final CheckRequestPermissionsListener listener) {
new PermissionRequester(activity)
.withPermission(permissionsToRequest)
.request(new RequestPermissionListener() {
@Override
public void onPermissionResult(Permission[] permissions) {
List refusedListAfterRequest = new LinkedList<>();
for (Permission requestResult : permissions) {
if (!requestResult.isGranted()) {
refusedListAfterRequest.add(requestResult);
}
}
if (refusedListAfterRequest.size() == 0) {
listener.onAllPermissionOk(permissionsToRequest);
} else {
listener.onPermissionDenied(PermissionTools.convert(refusedListAfterRequest));
}
}
});
}
其中PermissionRequester也就是一个简单的构建者模式,其中包含了对Activity的类型判断,根据Activity类型去确定Fragment的实现:
如果是FragmentActivity的实例,则使用Support包中的Fragment,否则用默认的Fragment,这样就兼容了有些应用的项目的基类不是AppComponentActivity(FragmentActivity)的情形,当然,原则上最低支持4.0,即默认Fragment的支持版本。
class PermissionFragmentFactory {
private static final String FRAGMENT_TAG = “permission_fragment_tag”;
static IPermissionActions create(Activity activity) {
IPermissionActions action;
if (activity instanceof FragmentActivity) {
FragmentManager supportFragmentManager = ((FragmentActivity) activity).getSupportFragmentManager();
PermissionSupportFragment permissionSupportFragment = (PermissionSupportFragment) supportFragmentManager.findFragmentByTag(FRAGMENT_TAG);
if (null == permissionSupportFragment) {
permissionSupportFragment = new PermissionSupportFragment();
supportFragmentManager.beginTransaction()
.add(permissionSupportFragment, FRAGMENT_TAG)
.commitNowAllowingStateLoss();
}
action = permissionSupportFragment;
} else {
android.app.FragmentManager fragmentManager = activity.getFragmentManager();
PermissionFragment permissionFragment = (PermissionFragment) fragmentManager.findFragmentByTag(FRAGMENT_TAG);
if (null == permissionFragment) {
permissionFragment = new PermissionFragment();
activity.getFragmentManager().beginTransaction()
.add(permissionFragment, FRAGMENT_TAG)
.commitAllowingStateLoss();
}
action = permissionFragment;
}
return action;
}
}
至此,整个请求链已经很像最外层暴露的CheckAndRequestPermission方法了,就差一个Activity了,那么参数Activity怎么来呢?
我们继续想办法。
2、再舍去Activity
当然是使用Application中的ActivityLifecycleCallbacks,使用它的registerActivityLifecycleCallbacks,感知Activity声明周期变化,获取到当前应用栈顶的Activity,这样我们就不需要自己手动传入了。
public class PermissionActivityLifecycle implements Application.ActivityLifecycleCallbacks {
WeakReference topActWeakReference;
@Override
public void onActivityCreated(Activity activity, Bundle savedInstanceState) {
//原则上只需要onResume,兼容如果在onCreate的时候做权限申请保证此时有Activity对象
topActWeakReference = new WeakReference<>(activity);
}
//…
@Override
public void onActivityResumed(Activity activity) {
topActWeakReference = new WeakReference<>(activity);
}
//…
}
注册它仅仅需要一个Application:
/**
- @param context Application
*/
private void registerLifecycle(Application context) {
if (null != lifecycle) {
context.unregisterActivityLifecycleCallbacks(lifecycle);
}
lifecycle = new PermissionActivityLifecycle();
context.registerActivityLifecycleCallbacks(lifecycle);
}
这样一来,只要调用了初始化方法registerLifecycle,我们就能提供提供栈顶Activity了
/**
-
获取栈顶Activity
-
@return 当前应用栈顶Activity
-
@throws InitException 初始化失败
-
@throws ContainerStatusException Activity状态异常
*/
private Activity getContainer() {
// may auto init failed
if (null == lifecycle || null == lifecycle.topActWeakReference) {
throw new InitException();
}
// activity status error
if (null == lifecycle.topActWeakReference.get() || lifecycle.topActWeakReference.get().isFinishing()) {
throw new ContainerStatusException();
}
return lifecycle.topActWeakReference.get();
}
结合起来回到我们之前申请权限的方法(省略了日志打印和线程的判断,如果需要再细看源码):
private void requestPermissions(final Permissions permissions, final CheckRequestPermissionsListener listener) {
//check container status
final Activity activity;
try {
activity = getContainer();
} catch (Exception e) {
//activity status error do not request
return;
}
//…
//finally request
requestRuntimePermission(activity, permissions.getPermissions(), listener);
}
至此,我们已经能脱离Activity和Fragment,也无需重写onPermissionResult了,只需要一个ApplicationContext初始化即可。
3、能否更简便一点?
最后避掉Application(免初始化):
我们可以自定义ContentProvider来完成库的初始化,我们可以参考Lifecycle组件的初始化:
http://chaosleong.github.io/2017/05/27/How-Lifecycle-aware-Components-actually-works/
//lifeCycle定义的初始化Provider
public class LifecycleRuntimeTrojanProvider extends ContentProvider {
@Override
public boolean onCreate() {
LifecycleDispatcher.init(getContext());
ProcessLifecycleOwner.init(getContext());
return true;
}
}
和它的Manifest文件:
<provider
android:name=“android.arch.lifecycle.LifecycleRuntimeTrojanProvider”
android:authorities=“${applicationId}.lifecycle-trojan”
android:exported=“false”
android:multiprocess=“true” />
参照它的实现给我们提供了一个很好的思路,我们可以自定义Provider去初始化一些库或者其他的内容,现在我们写一个自己的initContentProvider:
public class InitProvider extends ContentProvider {
@Override
public boolean onCreate() {
//初始化我们的库
SoulPermission.getInstance().autoInit((Application) getContext());
return true;
}
//…
}
在库的AndroidManifest文件中声明:
<provider android:authorities=“${applicationId}.permission.provider”
android:name=“.permission.InitProvider”
android:multiprocess=“true”
android:exported=“false”/>
至于为什么这个Context就是Application,我们可以参考ActivityThread中的对ContentProvider的初始化:
public void handleInstallProvider(ProviderInfo info) {
//即我们的应用的Application
installContentProviders(mInitialApplication, Arrays.asList(info));
}
至此,我们权限申请流程就跟Activity、Fragment、乃至Context都没有关系了。
4. 去除if&else、涵盖版本判断:
虽然我们完成了对运行时权限的申请流程,但是毕竟只针对6.0以上机型,如果上面流程还想一句话完成的话,那我们还得兼容老的机型,so,我们需要做在方法内做一个版本判断:
首先判断系统版本
public static boolean isOldPermissionSystem(Context context) {
int targetSdkVersion = context.getApplicationInfo().targetSdkVersion;
return android.os.Build.VERSION.SDK_INT < M || targetSdkVersion < M;
}
然后是检查权限:
6.0以上当然是走系统Api:
class RunTimePermissionChecker implements PermissionChecker {
private String permission;
private Context context;
RunTimePermissionChecker(Context context, String permission) {
this.permission = permission;
this.context = context;
}
@TargetApi(M)
@Override
public boolean check() {
int checkResult = ContextCompat.checkSelfPermission(context, permission);
return checkResult == PackageManager.PERMISSION_GRANTED;
}
}
6.0以下、4.4以上通过AppOps反射获取(为了保证一致性,把权限名称参数在check方法中做了映射,把权限的String参数映射成checkOp的整形参数):
class AppOpsChecker implements PermissionChecker {
private Context context;
private String permission;
AppOpsChecker(Context context, String permission) {
this.context = context;
this.permission = permission;
}
/**
-
老的通過反射方式檢查權限狀態
-
结果可能不准确,如果返回false一定未授予
-
按需在里面添加
-
如果没匹配上或者异常都默认权限授予
-
@return 检查结果
*/
@Override
public boolean check() {
if (null == permission) {
return true;
}
switch (permission) {
case Manifest.permission.READ_CONTACTS:
return checkOp(4);
case Manifest.permission.WRITE_CONTACTS:
return checkOp(5);
case Manifest.permission.CALL_PHONE:
return checkOp(13);
…
default:
break;
}
return true;
}
boolean checkOp(int op) {
if (Build.VERSION.SDK_INT < KITKAT) {
return true;
}
try {
AppOpsManager manager = (AppOpsManager) context.getSystemService(Context.APP_OPS_SERVICE);
Method method = AppOpsManager.class.getDeclaredMethod(“checkOp”, int.class, int.class, String.class);
return 0 == (int) method.invoke(manager, op, Binder.getCallingUid(), context.getPackageName());
} catch (Exception e) {
e.printStackTrace();
}
return true;
}
}
和版本判断起来就是这样:
public static PermissionChecker create(Context context, String permission) {
if (PermissionTools.isOldPermissionSystem(context)) {
return new AppOpsChecker(context, permission);
} else {
return new RunTimePermissionChecker(context, permission);
}
}
再到我们最终调用的权限检测方法:
private boolean checkPermission(Context context, String permission) {
return CheckerFactory.create(context, permission).check();
}
最终我们权限库一行代码从权限检测、权限请求联合起来的操作就是这样:
/**
-
多个权限的检查与申请
-
在敏感操作前,先检查权限和请求权限,当完成操作后可做后续的事情
-
@param permissions 多个权限的申请 Permissions.build(Manifest.permission.CALL_PHONE,Manifest.permission.CAMERA)
-
@param listener 请求之后的回调
*/
public void checkAndRequestPermissions(@NonNull Permissions permissions, @NonNull final CheckRequestPermissionsListener listener) {
//首先检查权限
Permission[] checkResult = checkPermissions(permissions.getPermissionsString());
//得到有多少权限被拒绝了
final Permission[] refusedPermissionList = filterRefusedPermissions(checkResult);
if (refusedPermissionList.length > 0) {
//是否可以请求运行时权限,即6.0以上
if (canRequestRunTimePermission()) {
//请求权限,并把listener传下去,也就是我们一开始看请求流程分析中的那个方法
requestPermissions(Permissions.build(refusedPermissionList), listener);
} else {
//无法请求权限,本次操作失败
listener.onPermissionDenied(refusedPermissionList);
}
} else {
//没有权限被拒绝,认为所有权限都ok,回调成功
listener.onAllPermissionOk(checkResult);
}
}
至此,我们的三个主要需求的源码分析基本完成,如果有啥疑问和细节上的实现,可以自行阅读源码即可。
总结:
SoulPerission很好的适配了真运行时权限、除了上述三个个主要功能以外还提供以下功能:
-
支持多项权限同时请求
-
支持检查通知权限
-
支持系统权限页面跳转
-
支持debug模式
https://github.com/soulqw/SoulPermission
Android开发资料+面试架构资料 免费分享 点击链接 即可领取
《Android架构师必备学习资源免费领取(架构视频+面试专题文档+学习笔记)》
最后
文章不易,如果大家喜欢这篇文章,或者对你有帮助希望大家多多点赞转发关注哦。文章会持续更新的。绝对干货!!!
- Android进阶学习全套手册
关于实战,我想每一个做开发的都有话要说,对于小白而言,缺乏实战经验是通病,那么除了在实际工作过程当中,我们如何去更了解实战方面的内容呢?实际上,我们很有必要去看一些实战相关的电子书。目前,我手头上整理到的电子书还算比较全面,HTTP、自定义view、c++、MVP、Android源码设计模式、Android开发艺术探索、Java并发编程的艺术、Android基于Glide的二次封装、Android内存优化——常见内存泄露及优化方案、.Java编程思想 (第4版)等高级技术都囊括其中。
-
Android高级架构师进阶知识体系图
关于视频这块,我也是自己搜集了一些,都按照Android学习路线做了一个分类。按照Android学习路线一共有八个模块,其中视频都有对应,就是为了帮助大家系统的学习。接下来看一下导图和对应系统视频吧!!!
-
Android对标阿里P7学习视频
- BATJ大厂Android高频面试题
这个题库内容是比较多的,除了一些流行的热门技术面试题,如Kotlin,数据库,Java虚拟机面试题,数组,Framework ,混合跨平台开发,等
《Android学习笔记总结+移动架构视频+大厂面试真题+项目实战源码》,点击传送门,即可获取!
供以下功能:
-
支持多项权限同时请求
-
支持检查通知权限
-
支持系统权限页面跳转
-
支持debug模式
https://github.com/soulqw/SoulPermission
Android开发资料+面试架构资料 免费分享 点击链接 即可领取
《Android架构师必备学习资源免费领取(架构视频+面试专题文档+学习笔记)》
最后
文章不易,如果大家喜欢这篇文章,或者对你有帮助希望大家多多点赞转发关注哦。文章会持续更新的。绝对干货!!!
- Android进阶学习全套手册
关于实战,我想每一个做开发的都有话要说,对于小白而言,缺乏实战经验是通病,那么除了在实际工作过程当中,我们如何去更了解实战方面的内容呢?实际上,我们很有必要去看一些实战相关的电子书。目前,我手头上整理到的电子书还算比较全面,HTTP、自定义view、c++、MVP、Android源码设计模式、Android开发艺术探索、Java并发编程的艺术、Android基于Glide的二次封装、Android内存优化——常见内存泄露及优化方案、.Java编程思想 (第4版)等高级技术都囊括其中。
[外链图片转存中…(img-VfFMPShv-1715415734447)]
-
Android高级架构师进阶知识体系图
关于视频这块,我也是自己搜集了一些,都按照Android学习路线做了一个分类。按照Android学习路线一共有八个模块,其中视频都有对应,就是为了帮助大家系统的学习。接下来看一下导图和对应系统视频吧!!!
[外链图片转存中…(img-P2PbJVeS-1715415734448)] -
Android对标阿里P7学习视频
[外链图片转存中…(img-ha1JbWBW-1715415734448)]
- BATJ大厂Android高频面试题
这个题库内容是比较多的,除了一些流行的热门技术面试题,如Kotlin,数据库,Java虚拟机面试题,数组,Framework ,混合跨平台开发,等
[外链图片转存中…(img-8hcgmnfB-1715415734449)]
《Android学习笔记总结+移动架构视频+大厂面试真题+项目实战源码》,点击传送门,即可获取!