版權聲明:本文已授權微信公眾號:Android必修課,轉載請申明出處
Android6.0以上的系統中,引入了運行時權限檢查,運行時權限分為正常權限和危險權限,當我們的App調用了需要危險權限的api時,需要向系統申請權限,系統會彈出一個對話框讓用戶感知,只有當用戶授權以后,App才能正常調用api。
關于危險權限的說明,請參閱官方文檔: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);
}
}
});
可能不是那么優雅,如果有好的方式,請留言告知,讓大家學習學習……感謝。
源碼地址:(https://github.com/DevBraon/GPermission)
想解鎖更多姿勢,請關注微信公眾號:Android必修課