Android優雅地申請動態許可權
Android6.0以上的系統中,引入了執行時許可權檢查,執行時許可權分為正常許可權和危險許可權,當我們的App呼叫了需要危險許可權的api時,需要向系統申請許可權,系統會彈出一個對話方塊讓使用者感知,只有當用戶授權以後,App才能正常呼叫api。
關於危險許可權的說明,請參閱官方文件: ofollow,noindex">https://developer.android.google.cn/guide/topics/security/permissions#normal-dangerous
官方許可權申請示例:
這裡採用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依賴,依賴方式見上一篇:
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); } } });
可能不是那麼優雅,如果有好的方式,請留言告知,讓大家學習學習……感謝。
想解鎖更多姿勢,請關注微信公眾號:Android必修課
