1

版权声明:本文已授权微信公众号:Android必修课,转载请申明出处

Android6.0以上的系统中,引入了运行时权限检查,运行时权限分为正常权限和危险权限,当我们的App调用了需要危险权限的api时,需要向系统申请权限,系统会弹出一个对话框让用户感知,只有当用户授权以后,App才能正常调用api。

关于危险权限的说明,请参阅官方文档:https://developer.android.goo...

官方权限申请示例:

这里采用googleSamples中的权限申请框架EasyPermissions作为例子:

public class MainActivity extends AppCompatActivity implements EasyPermissions.PermissionCallbacks,EasyPermissions.RationaleCallbacks{
    private static final int RC_CAMERA_PERM = 123;
    private static final int RC_LOCATION_CONTACTS_PERM = 124;

    @AfterPermissionGranted(RC_CAMERA_PERM)
    public void cameraTask() {
        EasyPermissions.requestPermissions(
            this,
            getString(R.string.rationale_camera),
            RC_CAMERA_PERM,
            Manifest.permission.CAMERA);
    }

    @AfterPermissionGranted(RC_LOCATION_CONTACTS_PERM)
    public void locationAndContactsTask() {
        EasyPermissions.requestPermissions(
            this,
            getString(R.string.rationale_location_contacts),
            RC_LOCATION_CONTACTS_PERM,
            LOCATION_AND_CONTACTS);
    }

    @Override
    public void onRequestPermissionsResult(int requestCode, @NonNull String[] permissions, @NonNull int[] grantResults) {
        super.onRequestPermissionsResult(requestCode, permissions, grantResults);
        EasyPermissions.onRequestPermissionsResult(requestCode, permissions, grantResults, this);
    }

    @Override
    public void onPermissionsGranted(int requestCode, @NonNull List<String> perms) {
        Log.d(TAG, "onPermissionsGranted:" + requestCode + ":" + perms.size());
    }

    @Override
    public void onPermissionsDenied(int requestCode, @NonNull List<String> perms) {
        if (EasyPermissions.somePermissionPermanentlyDenied(this, perms)) {
            new AppSettingsDialog.Builder(this).build().show();
        }
    }
}

官方权限申请的例子,代码量相当多,每个涉及危险权限的地方都得写这么一堆代码。


改造

既然官方例子无法满足我们,那只能自己改造了,首先看看我们最后要实现的效果:

GPermisson.with(this)
    .permisson(new String[] {Manifest.permission.ACCESS_FINE_LOCATION, Manifest.permission.CAMERA})
    .callback(new PermissionCallback() {
        @Override
        public void onPermissionGranted() {}

        @Override
        public void shouldShowRational(String permisson) {}

        @Override
        public void onPermissonReject(String permisson) {}
    }).request();
  • onPermissionGranted是权限申请通过回调。
  • shouldShowRational是权限被拒绝,但是没有勾选“不再提醒"。
  • onPermissonReject是权限被拒绝,并且勾选了"不再提醒",即彻底被拒绝

可以看到,相对于官方例子,我们的api简洁了很多,并且流式调用可以让逻辑更容易接受。

怎么实现呢?慢慢看

1.编写权限申请Activity

首先,我们封装一个透明的Activity,在该Activity中进行权限申请

/*
 * 权限申请回调
 */
public interface PermissionCallback {
    void onPermissionGranted();

    void shouldShowRational(String permisson);

    void onPermissonReject(String permisson);
}


public class PermissionActivity extends Activity {
    public static final String KEY_PERMISSIONS = "permissions";
    private static final int RC_REQUEST_PERMISSION = 100;
    private static PermissionCallback CALLBACK;

    /*
     * 添加一个静态方法方便使用
     */
    public static void request(Context context, String[] permissions, PermissionCallback callback) {
        CALLBACK = callback;
        Intent intent = new Intent(context, PermissionActivity.class);
        intent.putExtra(KEY_PERMISSIONS, permissions);
        intent.addFlags(Intent.FLAG_ACTIVITY_NEW_TASK);
        context.startActivity(intent);
    }

    @Override
    protected void onCreate(@Nullable Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);
        Intent intent = getIntent();
        if (!intent.hasExtra(KEY_PERMISSIONS)) {
            return;
        }
        // 当api大于23时,才进行权限申请
        String[] permissions = getIntent().getStringArrayExtra(KEY_PERMISSIONS);
        if (Build.VERSION.SDK_INT >= 23) {
            requestPermissions(permissions, RC_REQUEST_PERMISSION);
        }
    }

    @TargetApi(23)
    @Override
    public void onRequestPermissionsResult(int requestCode, @NonNull String[] permissions, @NonNull int[] grantResults) {
        if (requestCode != RC_REQUEST_PERMISSION) {
            return;
        }
        // 处理申请结果
        boolean[] shouldShowRequestPermissionRationale = new boolean[permissions.length];
        for (int i = 0; i < permissions.length; ++i) {
            shouldShowRequestPermissionRationale[i] = shouldShowRequestPermissionRationale(permissions[i]);
        }
        this.onRequestPermissionsResult(permissions, grantResults, shouldShowRequestPermissionRationale);
    }


    @TargetApi(23)
    void onRequestPermissionsResult(String[] permissions, int[] grantResults, boolean[] shouldShowRequestPermissionRationale) {
        int length = permissions.length;
        int granted = 0;
        for (int i = 0; i < length; i++) {
            if (grantResults[i] != PackageManager.PERMISSION_GRANTED) {
                if (shouldShowRequestPermissionRationale[i] == true){
                    CALLBACK.shouldShowRational(permissions[i]);
                } else {
                    CALLBACK.onPermissonReject(permissions[i]);
                }
            } else {
                granted++;
            }
        }
        if (granted == length) {
            CALLBACK.onPermissionGranted();
        }
        finish();
    }
}

添加一个透明的主题:

<style name="Translucent">
    <item name="android:windowIsTranslucent">true</item>
    <item name="android:windowBackground">@android:color/transparent</item>
    <item name="android:windowContentOverlay">@null</item>
    <item name="android:windowIsFloating">true</item>
    <item name="android:backgroundDimEnabled">false</item>
    <item name="android:windowActionBar">false</item>
    <item name="android:windowNoTitle">true</item>
    <item name="windowNoTitle">true</item>
    <item name="windowActionBar">false</item>
</style>

2.封装一个门面类,提供api调用

public class GPermisson {
    // 权限申请回调
    private PermissionCallback callback;
    // 需要申请的权限
    private String[] permissions;
    private Context context;

    public GPermisson(Context context) {
        this.context = context;
    }

    public static GPermisson with(Context context) {
        GPermisson permisson = new GPermisson(context);
        return permisson;
    }

    public GPermisson permisson(String[] permissons) {
        this.permissions = permissons;
        return this;
    }

    public GPermisson callback(PermissionCallback callback) {
        this.callback = callback;
        return this;
    }

    public void request() {
        if (permissions == null || permissions.length <= 0) {
            return;
        }
        PermissionActivity.request(context, permissions, callback);
    }
}

至此,我们就简单封装好了一个权限请求库,达到上述效果。

等等,这种方式足够优雅了吗?

想想,每个涉及权限的地方,我们还是需要写一段权限请求代码,还能简化吗?

上一篇我们通过AOP封装了按钮点击的优雅实现,这里一样可以用AOP来简化我们的权限请求。

我们希望一个注解完成权限申请,例如:

@Permission(permissions = {Manifest.permission.ACCESS_FINE_LOCATION, Manifest.permission.READ_CONTACTS})
private void initView() {}

这样比上面的方法又简化了很多,但是,有个问题:

大家知道,权限申请是会被拒绝的,甚至是会被勾选上“不再提示”,然后再拒绝。这样被拒绝后再次申请权限是不会弹框提醒的。因此,我们需要处理:

  • 用户点击拒绝,但不勾选“不再提示”,下次请求权限时,系统弹窗依然会出现,而且shouldShowRequestPermissionRationale(permission)为true,意思是,用户拒绝了你,你应该显示一段文字或者其他信息,来说服用户允许你的权限申请。
  • 用户点击拒绝,并勾选“不再提示”,下次请求权限时,系统弹窗不会再出现,而且shouldShowRequestPermissionRationale(permission)为false,此时你的权限申请被用户彻底拒绝,需要跳转到系统设置页手动允许权限。

ok,我们知道了@Permission注解里,只有一个权限数组是不够的,我们还需要有一个rationale信息和被彻底拒绝后让用户跳转到设置页的信息。

升级

1.定义注解

/** 注意,@Retention需要为RUNTIME,否则运行时时没有这个注解的 */
@Retention(RetentionPolicy.RUNTIME)
@Target(ElementType.METHOD)
public @interface Permission {
    /* Permissions */
    String[] permissions();
    /* Rationales */
    int[] rationales() default {};
    /* Rejects */
    int[] rejects() default {};
}

使用int[]而不使用String[],是因为String[]传入的字串无法适配多语言。

2.改写GPermission

public class GPermisson {
    private static PermissionGlobalConfigCallback globalConfigCallback;
    private PermissionCallback callback;
    private String[] permissions;
    private Context context;

    public GPermisson(Context context) {
        this.context = context;
    }

    public static void init(PermissionGlobalConfigCallback callback) {
        globalConfigCallback = callback;
    }

    static PermissionGlobalConfigCallback getGlobalConfigCallback() {
        return globalConfigCallback;
    }

    public static GPermisson with(Context context) {
        GPermisson permisson = new GPermisson(context);
        return permisson;
    }

    public GPermisson permisson(String[] permissons) {
        this.permissions = permissons;
        return this;
    }

    public GPermisson callback(PermissionCallback callback) {
        this.callback = callback;
        return this;
    }

    public void request() {
        if (permissions == null || permissions.length <= 0) {
            return;
        }
        PermissionActivity.request(context, permissions, callback);
    }

    /**
     * 写一个接口,将申请被拒绝的上述两种情况交给调用者自行处理,框架内不处理
     */
    public abstract class PermissionGlobalConfigCallback {
        abstract public void shouldShowRational(String permission, int ration);
        abstract public void onPermissonReject(String permission, int reject);
    }
}

3.Aspect切面处理类

@Aspect
public class PermissionAspect {
    @Around("execution(@me.baron.gpermission.Permission * *(..))")
    public void aroundJoinPoint(final ProceedingJoinPoint joinPoint) {
        try {
            // 获取方法注解
            MethodSignature methodSignature = (MethodSignature) joinPoint.getSignature();
            Method method = methodSignature.getMethod();
            Permission annotation = method.getAnnotation(Permission.class);
            // 获取注解参数,这里我们有3个参数需要获取
            final String[] permissions = annotation.permissions();
            final int[] rationales = annotation.rationales();
            final int[] rejects = annotation.rejects();
            final List<String> permissionList = Arrays.asList(permissions);

            // 获取上下文
            Object object = joinPoint.getThis();
            Context context = null;
            if (object instanceof FragmentActivity) {
                context = (FragmentActivity) object;
            } else if (object instanceof Fragment) {
                context = ((Fragment) object).getContext();
            } else if (object instanceof Service) {
                context = (Service) object;
            }

            // 申请权限
            GPermisson.with(context)
                    .permisson(permissions)
                    .callback(new PermissionCallback() {
                        @Override
                        public void onPermissionGranted() {
                            try {
                                // 权限申请通过,执行原方法
                                joinPoint.proceed();
                            } catch (Throwable throwable) {
                                throwable.printStackTrace();
                            }
                        }

                        @Override
                        public void shouldShowRational(String permisson) {
                            // 申请被拒绝,但没有勾选“不再提醒”,这里我们让外部自行处理
                            int index = permissionList.indexOf(permisson);
                            int rationale = -1;
                            if (rationales.length > index) {
                                rationale = rationales[index];
                            }
                            GPermisson.getGlobalConfigCallback().shouldShowRational(permisson, rationale);
                        }

                        @Override
                        public void onPermissonReject(String permisson) {
                            // 申请被拒绝,且勾选“不再提醒”,这里我们让外部自行处理
                            int index = permissionList.indexOf(permisson);
                            int reject = -1;
                            if (rejects.length > index) {
                                reject = rejects[index];
                            }
                            GPermisson.getGlobalConfigCallback().onPermissonReject(permisson, reject);
                        }
                    }).request();
        } catch (Exception e) {
            e.printStackTrace();
        }
    }
}

使用

1.引入Aspectj依赖,依赖方式见上一篇:

Android优雅地处理按钮重复点击

2.设置全局权限请求结果监听

GPermisson.init(new PermissionGlobalConfigCallback() {
    @Override
    public void shouldShowRational(String permission, int ration) {
        showRationaleDialog(ration);
    }

    @Override
    public void onPermissonReject(String permission, int reject) {
        showRejectDialog(reject);
    }
});

private void showRationaleDialog(int ration) {
    new AlertDialog.Builder(MainActivity.this)
            .setTitle("权限申请")
            .setMessage(getString(ration))
            .show();
}

private void showRejectDialog(int reject) {
    new AlertDialog.Builder(MainActivity.this)
            .setTitle("权限申请")
            .setMessage(getString(reject))
            .setPositiveButton("跳转到设置页", new DialogInterface.OnClickListener() {
                @Override
                public void onClick(DialogInterface dialog, int which) {
                    // 本人魅族手机,其他品牌的设置页跳转逻辑不同,请百度解决
                    Intent intent = new Intent("com.meizu.safe.security.SHOW_APPSEC");
                    intent.addCategory(Intent.CATEGORY_DEFAULT);
                    intent.putExtra("packageName", BuildConfig.APPLICATION_ID);
                    startActivity(intent);
                    dialog.dismiss();
                }
            })
            .setNegativeButton("取消", null)
            .show();
}

3.在需要权限的地方添加注解:

@Permission(permissions = {Manifest.permission.ACCESS_FINE_LOCATION, Manifest.permission.READ_CONTACTS},
        rationales = {R.string.location_rationale, R.string.contact_rationale},
        rejects = {R.string.location_reject, R.string.contact_reject})
private void initView() {}

一旦权限申请被拒绝,将会回调到全局监听中,这里我们只弹窗提醒,若需要其他形式的提醒,自行实现ui即可。运行效果:

注意

如果你们有过组件化开发,就应该马上了解到,我们在上面使用@Permission注解传入的rationale和reject的字符串id,在Module中是会报错的,原因是Module中的R.string.xxx不是final常量,而注解值需要final常量值。

@Permission(permissions = {Manifest.permission.ACCESS_FINE_LOCATION, Manifest.permission.READ_CONTACTS},
        rationales = {R.string.location_rationale, R.string.contact_rationale},
        rejects = {R.string.location_reject, R.string.contact_reject})
private void initView() {}

那么,如何处理在Module中的情况呢,这里我想到了一个思路:

既然R.string.xxx不是常量,我们就给注解值传入我们自定义的常量:

public class Permissions {
    public static final int LOCATION_RATIONALE = 100;
    public static final int LOCATION_REJECT= 101;
    public static final int CONTACT_RATIONALE= 102;
    public static final int CONTACT_REJECT= 103;
}
@Permission(permissions = {Manifest.permission.ACCESS_FINE_LOCATION, Manifest.permission.READ_CONTACTS},
        rationales = {Permissions.LOCATION_RATIONALE, Permissions.CONTACT_RATIONALE},
        rejects = {Permissions.LOCATION_REJECT, Permissions.CONTACT_REJECT})
private void initView() {}

然后在全局的监听中修改:

GPermisson.init(new PermissionGlobalConfigCallback() {
    @Override
    public void shouldShowRational(String permission, int ration) {
        if (ration == Permissions.LOCATION_RATIONALE) {
            showRationaleDialog(R.string.location_rationale);
        } else if (ration == Permissions.CONTACT_RATIONALE) {
            showRationaleDialog(R.string.contact_rationale);
        } else {
            showRationaleDialog(ration);
        }
    }

    @Override
    public void onPermissonReject(String permission, int reject) {
        if (reject == Permissions.LOCATION_RATIONALE) {
            showRejectDialog(R.string.location_reject);
        } else if (reject == Permissions.CONTACT_RATIONALE) {
            showRejectDialog(R.string.contact_reject);
        } else {
            showRejectDialog(reject);
        }
    }
});

可能不是那么优雅,如果有好的方式,请留言告知,让大家学习学习……感谢。
源码地址:(https://github.com/DevBraon/G...)

想解锁更多姿势,请关注微信公众号:Android必修课


XBaron
15 声望0 粉丝