关于运行时的权限不用多说,这个概念已经好久,近期工信部在强推TargetSDK26,我这边作了一些适配工做,其中有一项就是运行时权限,今天将对运行时权限提供一个更优雅的解决方案,若是你还不了解运行时权限,请移步:Android运行时权限浅谈html
首先咱们项目中可能会有这么一个方法:java
/** * 拨打指定电话 */
public static void makeCall(Context context, String phoneNumber) {
Intent intent = new Intent(Intent.ACTION_CALL);
Uri data = Uri.parse("tel:" + phoneNumber);
intent.setData(data);
if (!(context instanceof Activity)) {
intent.addFlags(Intent.FLAG_ACTIVITY_NEW_TASK);
}
context.startActivity(intent);
}
复制代码
那么在适配动态权限之前,在咱们任意用到打电话的业务页面咱们可能就是这么用:android
public void makeCall() {
Utils.makeCall(BeforeActivity.this, "10086");
}
复制代码
因而乎,某一天,咱们应用要适配targetSdk 26,首先咱们要适配的就是动态权限,因此下面的代码就会变成这样:git
public void makeCall() {
//6.0如下 直接便可拨打
if (android.os.Build.VERSION.SDK_INT < M) {
Utils.makeCall(BeforeActivity.this, "10086");
} else {
//6.0以上
if (ContextCompat.checkSelfPermission(BeforeActivity.this, Manifest.permission.CALL_PHONE) != PackageManager.PERMISSION_GRANTED) {
ActivityCompat.requestPermissions(BeforeActivity.this, new String[]{Manifest.permission.CALL_PHONE},
REQUEST_CODE_CALL);
} else {
Utils.makeCall(BeforeActivity.this, "10086");
}
}
}
@Override
public void onRequestPermissionsResult(int requestCode, @NonNull String[] permissions, @NonNull int[] grantResults) {
super.onRequestPermissionsResult(requestCode, permissions, grantResults);
if (requestCode == REQUEST_CODE_CALL) {
if (grantResults[0] != PackageManager.PERMISSION_GRANTED) {
Toast.makeText(BeforeActivity.this, "本次拨打电话受权失败,请手动去设置页打开权限,或者重试受权权限", Toast.LENGTH_SHORT).show();
} else {
Utils.makeCall(BeforeActivity.this, "10086");
}
}
}
复制代码
以上就是拨打电话功能新老权限版本的基本实现(还不包括shouldShowRequestPermissionRationale的部分)。 目前也有一些知名的开源库,如PermissionsDispatcher,RXPermission等。虽然也能实现咱们的功能,但不管本身适配仍是现有开源库方案大致上都会或多或少有如下几个问题:github
基于第一个业务繁琐的问题,不少应用选择适配权限的时候,把所用到的敏感权限放在一个特定的页面去申请,好比欢迎页(某知名音乐播放器等),若是受权不成功,则会直接没法进入应用,这样虽然省事,可是用户体验很差,我在应用一打开,提示须要电话权限,用户会很疑惑。这样其实就违背了“运行时受权”的初衷,谷歌但愿咱们在真正调用的该功能的时候去请求,这样权限请求和用户的目的是一致的,也更容易授予权限成功。数组
那么能不能作到以下几个点呢?app
带着上述几个问题,咱们今天的主角:SoulPermission应运而生。ide
当使用了SoulPermission之后,最直观上看,咱们上面的代码就变成了这样:源码分析
public void makeCall() {
SoulPermission.getInstance()
.checkAndRequestPermission(Manifest.permission.CALL_PHONE, new CheckRequestPermissionListener() {
@Override
public void onPermissionOk(Permission permission) {
Utils.makeCall(AfterActivity.this, "10086");
}
@Override
public void onPermissionDenied(Permission permission) {
Toast.makeText(AfterActivity.this, "本次拨打电话受权失败,请手动去设置页打开权限,或者重试受权权限", Toast.LENGTH_SHORT).show();
}
});
}
复制代码
若是我以在Android手机上要作一件事(doSomeThing),那么我最终能够有两个结果:gradle
基于上述两种结果,那么SoulPermission的大体工做流程以下:
从开始到结束展现了咱们上述打电话的流程,A即直接拨打,B即toast提示用户,没法继续后续操做,绿色部分流程便可选部分,即对shouldShowRequestPermissionRationale的处理,那么完整权限流程下来,咱们拨打电话的代码就是这么写:
public void makeCall() {
SoulPermission.getInstance()
.checkAndRequestPermission(Manifest.permission.CALL_PHONE, new CheckRequestPermissionListener() {
@Override
public void onPermissionOk(Permission permission) {
Utils.makeCall(AfterActivity.this, "10086");
}
@Override
public void onPermissionDenied(Permission permission) {
//绿色框中的流程
//用户第一次拒绝了权限且没有勾选"再也不提示"的状况下这个值为true,此时告诉用户为何须要这个权限。
if (permission.shouldRationale()) {
new AlertDialog.Builder(AfterActivity.this)
.setTitle("提示")
.setMessage("若是你拒绝了权限,你将没法拨打电话,请点击授予权限")
.setPositiveButton("授予", new DialogInterface.OnClickListener() {
@Override
public void onClick(DialogInterface dialogInterface, int i) {
//用户肯定之后,从新执行请求原始流程
makeCall();
}
}).create().show();
} else {
Toast.makeText(AfterActivity.this, "本次拨打电话受权失败,请手动去设置页打开权限,或者重试受权权限", Toast.LENGTH_SHORT).show();
}
}
});
}
复制代码
上述即是其在知足运行时权限下的完整工做流程。那么关于版本兼容呢? 针对部分手机6.0如下手机,SoulPermission也作了兼容,能够经过AppOps 检查权限,内部将权限名称作了相应的映射,它的大致流程就是下图: (这个检查结果不必定准确,可是即便不许确,也默认成功(A),保证咱们回调能往下走,不会阻塞流程,有些在6.0如下本身实现了权限系统的手机(如vivo,魅族)等也是走此A的回调,最终会走到它们本身的权限申请流程)
基于对于代码中对新老系统版本作了控制,而在权限拒绝里面不少处理也是又能够提取的部分,咱们能够把回调再次封装一下,进一步减小重复代码:
public abstract class CheckPermissionWithRationaleAdapter implements CheckRequestPermissionListener {
private String rationaleMessage;
private Runnable retryRunnable;
/** * @param rationaleMessage 当用户首次拒绝弹框时候,根据权限不一样给用户不一样的文案解释 * @param retryRunnable 用户点从新受权的runnable 即从新执行原方法 */
public CheckPermissionWithRationaleAdapter(String rationaleMessage, Runnable retryRunnable) {
this.rationaleMessage = rationaleMessage;
this.retryRunnable = retryRunnable;
}
@Override
public void onPermissionDenied(Permission permission) {
Activity activity = SoulPermission.getInstance().getTopActivity();
if (null == activity) {
return;
}
//绿色框中的流程
//用户第一次拒绝了权限、而且没有勾选"再也不提示"这个值为true,此时告诉用户为何须要这个权限。
if (permission.shouldRationale()) {
new AlertDialog.Builder(activity)
.setTitle("提示")
.setMessage(rationaleMessage)
.setPositiveButton("授予", new DialogInterface.OnClickListener() {
@Override
public void onClick(DialogInterface dialogInterface, int i) {
//用户肯定之后,从新执行请求原始流程
retryRunnable.run();
}
}).create().show();
} else {
//此时请求权限会直接报未授予,须要用户手动去权限设置页,因此弹框引导用户跳转去设置页
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();
}
}
}
复制代码
而后咱们在App全部打电话的入口处作一次调用:
/** * 拨打指定电话 */
public static void makeCall(final Context context, final String phoneNumber) {
SoulPermission.getInstance().checkAndRequestPermission(Manifest.permission.CALL_PHONE,
new CheckPermissionWithRationaleAdapter("若是你拒绝了权限,你将没法拨打电话,请点击授予权限",
new Runnable() {
@Override
public void run() {
//retry
makeCall(context, phoneNumber);
}
}) {
@Override
public void onPermissionOk(Permission permission) {
Intent intent = new Intent(Intent.ACTION_CALL);
Uri data = Uri.parse("tel:" + phoneNumber);
intent.setData(data);
if (!(context instanceof Activity)) {
intent.addFlags(Intent.FLAG_ACTIVITY_NEW_TASK);
}
context.startActivity(intent);
}
});
}
复制代码
那么这样下来,在Activity和任何业务页面的调用就只有一行代码了:
findViewById(R.id.bt_call).setOnClickListener(new View.OnClickListener() {
@Override
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,咱们再来看看效果:
适配权限最大的痛点在于:项目业务页面繁多,若是你想实现“真运行时权限”的话就须要在业务的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<Permission> 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怎么来呢?咱们继续想办法。
固然是使用Application中的ActivityLifecycleCallbacks,使用它的 registerActivityLifecycleCallbacks,感知Activity声明周期变化,获取到当前应用栈顶的Activity,这样咱们就不须要本身手动传入了。
public class PermissionActivityLifecycle implements Application.ActivityLifecycleCallbacks {
WeakReference<Activity> 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初始化便可。
可否更简便一点?
咱们能够自定义ContentProvider来完成库的初始化,咱们能够参考Lifecycle组件的初始化:
//lifeCycle定义的初始化Provider
public class LifecycleRuntimeTrojanProvider extends ContentProvider {
@Override
public boolean onCreate() {
LifecycleDispatcher.init(getContext());
ProcessLifecycleOwner.init(getContext());
return true;
}
}
复制代码
和它的Manifest文件:
<application>
<provider android:name="android.arch.lifecycle.LifecycleRuntimeTrojanProvider" android:authorities="${applicationId}.lifecycle-trojan" android:exported="false" android:multiprocess="true" />
</application>
复制代码
参照它的实现给咱们提供了一个很好的思路,咱们能够自定义Provider去初始化一些库或者其余的内容,如今咱们写一个本身的initContentProvider:
public class InitProvider extends ContentProvider {
@Override
public boolean onCreate() {
//初始化咱们的库
SoulPermission.getInstance().autoInit((Application) getContext());
return true;
}
//......
}
复制代码
在库的AndroidManifest文件中声明:
<application>
<provider android:authorities="${applicationId}.permission.provider" android:name=".permission.InitProvider" android:multiprocess="true" android:exported="false"/>
</application>
复制代码
至于为何这个Context就是Application,咱们能够参考ActivityThread中的对ContentProvider的初始化:
public void handleInstallProvider(ProviderInfo info) {
//即咱们的应用的Application
installContentProviders(mInitialApplication, Arrays.asList(info));
}
复制代码
至此,咱们权限申请流程就跟Activity、Fragment、乃至Context都没有关系了。
虽然咱们完成了对运行时权限的申请流程,可是毕竟只针对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);
case Manifest.permission.READ_PHONE_STATE:
return checkOp(51);
case Manifest.permission.CAMERA:
return checkOp(26);
case Manifest.permission.READ_EXTERNAL_STORAGE:
return checkOp(59);
case Manifest.permission.WRITE_EXTERNAL_STORAGE:
return checkOp(60);
case Manifest.permission.ACCESS_FINE_LOCATION:
case Manifest.permission.ACCESS_COARSE_LOCATION:
return checkOp(2);
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);
}
}
复制代码
至此,咱们的三个主要需求的源码分析基本完成,若是有啥疑问和细节上的实现,能够自行阅读源码便可。
SoulPermission很好的适配了真运行时权限、除了上述三个个主要功能之外还提供如下功能: