從6.0 MarshMallow開始,Android支持動態(tài)權(quán)限管理,即有些權(quán)限需要在使用到的時候動態(tài)申請,根據(jù)用戶的選擇需要有不同的處理,具體表現(xiàn)可以看下圖:
本文并不關(guān)心權(quán)限適配的原理,原理可以參考Android權(quán)限管理原理
,這里只是針對6.0中的表現(xiàn)做適配,先思考以下幾個問題:
- 為什么6.0權(quán)限需要適配
- 什么權(quán)限需要動態(tài)適配
- 怎樣動態(tài)適配權(quán)限
- 怎么樣實現(xiàn)第三方庫,簡化代碼及適配流程 權(quán)限兼容庫 PermissionCompat
- 對于國產(chǎn)ROM的影響
為什么6.0需要權(quán)限適配
6.0之前Android的權(quán)限都是在安裝的時候授予的,6.0之后,為了簡化安裝流程,并且方便用戶控制權(quán)限,Android允許在運行的時候動態(tài)控制權(quán)限。對于開發(fā)而言就是將targetSdkVersion設(shè)置為23,當運行在Android 6.0 +的手機上時,就會調(diào)用6.0相關(guān)的API,達到動態(tài)控制權(quán)限的目的。但是,如果僅僅是將targetSdkVersion設(shè)置為23,而在代碼層面沒有針對Android 6.0做適配,就可能在申請系統(tǒng)服務(wù)的時候,由于權(quán)限不足,引發(fā)崩潰。
- targetSDKVersion:該屬性用于通知系統(tǒng),您已針對目標版本進行測試,標識App能夠適配的系統(tǒng)版本,有些新的API是只有新的系統(tǒng)才有的。
什么權(quán)限需要動態(tài)適配
并非所有的權(quán)限都需要動態(tài)申請,Android6.0將權(quán)限分為兩種,普通權(quán)限跟敏感(危險)權(quán)限,普通權(quán)限是不需要動態(tài)申請的,但是敏感權(quán)限需要動態(tài)申請。
-
1、普通權(quán)限(Normal permissions):不會泄露用戶隱私,同時也不會導(dǎo)致手機安全問題。如網(wǎng)絡(luò)請求權(quán)限、WIFI狀態(tài)等,這類權(quán)限只需要在Manifest列出來,之后,系統(tǒng)會自動賦給APP權(quán)限:
- ACCESS_NETWORK_STATE
- ACCESS_NOTIFICATION_POLICY
- ACCESS_WIFI_STATE
- BLUETOOTH
- BLUETOOTH_ADMIN
-
2、敏感權(quán)限(Dangerous permissions):與?普通權(quán)限對應(yīng),可能會影響用戶的隱私,存儲數(shù)據(jù)等,比如拍照、存儲、通訊錄、地理GPS等,這類權(quán)限需要在Manifest列出來,在需要的的時候,顯示的請求用戶準許。
- CALENDAR
- CAMERA
- CONTACTS
- LOCATION
- PHONE
- SENSORS
- SMS
- STORAGE
敏感權(quán)限的請求是按照分組進行提醒的,并非僅僅針對一條,比如通訊錄的讀取權(quán)限與寫權(quán)限,只要一個權(quán)限獲到,下次請求權(quán)限的時候會自動提供,當然也要請求。否則還是有問題。
-
3、特殊權(quán)限(Special Permissions) --不在本文分析范圍
There are a couple of permissions that don't behave like normal and dangerous permissions. SYSTEM_ALERT_WINDOW and WRITE_SETTINGS
怎樣動態(tài)適配權(quán)限
對于敏感權(quán)限的適配有一個原則,那就是實時檢查,因為權(quán)限隨時可能被回收,比如用戶可以在設(shè)置里面把權(quán)限給取消,但是APP并不一定知道,因此每次都需要檢查,一旦沒有,就需要請求,之后,根據(jù)返回結(jié)果處理后續(xù)邏輯。
實現(xiàn)步驟
-
1、在Manifest中列出來
無論普通權(quán)限還是敏感權(quán)限,都需要在Manifest中列出來,同時也是對6.0之前的版本的一種兼容。
<manifest xmlns:android="http://schemas.android.com/apk/res/android" package="com.snail.labaffinity"> <uses-permission android:name="android.permission.CAMERA"/> <uses-permission android:name="android.permission.CALL_PHONE"/>
2、需要時,顯示的請求
在權(quán)限沒被授予前提下,系統(tǒng)會顯示授權(quán)對話框,讓用戶操作,目前授權(quán)對話框不可定制,不過可以在申請之前添加一些解釋,告訴用戶為什么需要該權(quán)限,但是Google提醒,不要做過多的解釋,可能會使用戶感到厭煩,用法如下:
ActivityCompat.requestPermissions(target.getActivity(), permissions,
· requestCode);
public static void requestPermissions(final @NonNull Activity activity,
final @NonNull String[] permissions, final int requestCode) {
if (Build.VERSION.SDK_INT >= 23) {
ActivityCompatApi23.requestPermissions(activity, permissions, requestCode);
} else if (activity instanceof OnRequestPermissionsResultCallback) {
Handler handler = new Handler(Looper.getMainLooper());
handler.post(new Runnable() {
@Override
public void run() {
final int[] grantResults = new int[permissions.length];
PackageManager packageManager = activity.getPackageManager();
String packageName = activity.getPackageName();
final int permissionCount = permissions.length;
for (int i = 0; i < permissionCount; i++) {
grantResults[i] = packageManager.checkPermission(
permissions[i], packageName);
}
((OnRequestPermissionsResultCallback) activity).onRequestPermissionsResult(
requestCode, permissions, grantResults);
}
});
}
}
-
3、處理授權(quán)回調(diào)
兼容6.0之前的處理:在這里只需要處理獲得權(quán)限即可,因為6.0之前只存在Install權(quán)限,一旦安裝,所有權(quán)限都是默認授予的,雖然國內(nèi)ROM對權(quán)限管理做了自己的一些定制,但基本都是兼容的。
-
需要對6.0的授權(quán)成功、失敗、永不詢問做處理
public void onRequestPermissionsResult(int requestCode, @NonNull String[] permissions, @NonNull int[] grantResults) { super.onRequestPermissionsResult(requestCode, permissions, grantResults); if(this.mOnGrantedListener != null) { <!--6.0之前--> if(PermissionUtils.getTargetSdkVersion(this) < 23 && !PermissionUtils.hasSelfPermissions(this, permissions)) { this.mOnGrantedListener.onGranted(this, permissions); return; } //<!--6.0之后--> 需要根據(jù)結(jié)果進行驗證 if(PermissionUtils.verifyPermissions(grantResults)) { this.mOnGrantedListener.onGranted(this, permissions); } else if(!PermissionUtils.shouldShowRequestPermissionRationale(this, permissions)) { this.mOnGrantedListener.onNeverAsk(this, permissions); } else { this.mOnGrantedListener.onDenied(this, permissions); } } }
具體APP中不同的實現(xiàn)方案
- 1、簡單的封裝回調(diào)
- 2、基于APT,采用注解方式簡化編碼邏輯,自動封封回調(diào)
先看一下直接回調(diào)的方式
采用最直接的回調(diào)
首先在基類Activity或者Fragment中統(tǒng)一設(shè)置授權(quán)回調(diào)監(jiān)聽,這里我們用一個
public class BasePermissionCompatActivity extends AppCompatActivity {
private SparseArray<OnGrantedListener<BasePermissionCompatActivity>> mOnGrantedListeners = new SparseArray<>();
@Override
public void onRequestPermissionsResult(int requestCode, @NonNull String[] permissions, @NonNull int[] grantResults) {
super.onRequestPermissionsResult(requestCode, permissions, grantResults);
OnGrantedListener<BasePermissionCompatActivity> listener = mOnGrantedListeners.get(requestCode);
if (listener == null)
return;
if (PermissionUtils.verifyPermissions(grantResults)) {
listener.onGranted(this, permissions);
} else {
if (PermissionUtils.shouldShowRequestPermissionRationale(this, permissions)) {
listener.onDenied(this, permissions);
} else {
listener.onNeverAsk(this, permissions);
}
}
}
@Override
protected void onDestroy() {
super.onDestroy();
mOnGrantedListeners.clear();
mOnGrantedListeners = null;
}
public void requestPermissions(final @NonNull String[] permissions, OnGrantedListener<BasePermissionCompatActivity> onGrantedListener) {
int requestCode = getNextRequestCode();
ActivityCompat.requestPermissions(this, permissions, requestCode);
mOnGrantedListeners.put(requestCode, onGrantedListener);
}
private static int sNextCode;
private static int getNextRequestCode() {
return sNextCode++;
}
}
之后在需要時候的請求,并根據(jù)結(jié)果處理后續(xù)邏輯即可。
requestPermissions(activity, P_CAMERA, new OnGrantedListener() {
// 根據(jù)permissions自行處理,可合并,可分開
@Override
public void onGranted(SecondActivity target, String[] permissions,int requestCode) {
}
@Override
public void onDenied(SecondActivity target, String[] permissions,int requestCode) {
}
@Override
public void onNeverAsk(SecondActivity target, String[] permissions,int requestCode) {
}
@Override
public void onShowRationale(SecondActivity target, String[] permissions,int requestCode) {
});
上面的方法比較直接,靈活,不過每次都要自己實現(xiàn)回調(diào)監(jiān)聽Listener,接下來看第二種實現(xiàn),基于APT,通過注解的方式,自動添加Listener,這種實現(xiàn)參考了ButterKnife的實現(xiàn)方式。
基于APT與注解,編譯過程中生成代碼,自動添加回調(diào)
- 1、基于APT,定義一系列Annotation,并動態(tài)生成輔助Listener類
- 2、添加Android支持庫,在基類統(tǒng)一處理回調(diào),
- 3、添加工具類,連接綁定Listener與Activity(Fragment)
相應(yīng)的實現(xiàn)分三個庫:
- 注解庫
- APT生成支持庫
- Android支持庫
注解庫:
主要用來定義一些回調(diào)方法注解、及請求實體的類注解
* ActivityPermission
* FragmentPermission
* OnDenied
* OnGranted
* OnGrantedListener
* OnNeverAsk
* OnShowRationale
APT生成支持庫
主要用來在編譯階段,動態(tài)生Listener類
PermissionProcessor.java
部分參考代碼:
@AutoService(Processor.class)
public class PermissionProcessor extends AbstractProcessor {
private Elements elementUtils;
private Set<Class<? extends Annotation>> getSupportedAnnotations() {
Set<Class<? extends Annotation>> annotations = new LinkedHashSet<>();
annotations.add(OnDenied.class);
annotations.add(OnGranted.class);
annotations.add(OnNeverAsk.class);
annotations.add(OnShowRationale.class);
return annotations;
}
@Override
public synchronized void init(ProcessingEnvironment env) {
super.init(env);
elementUtils = env.getElementUtils();
}
@Override
public boolean process(Set<? extends TypeElement> annotations, RoundEnvironment roundEnv) {
if (!checkIntegrity(roundEnv))
return false;
Set<? extends Element> elementActivities = roundEnv.getElementsAnnotatedWith(ActivityPermission.class);
Set<? extends Element> elementFragments = roundEnv.getElementsAnnotatedWith(FragmentPermission.class);
return makeListenerJavaFile(elementActivities) && makeListenerJavaFile(elementFragments);
}
...
Android支持庫
主要會封裝了一些工具類,基類以及對回調(diào)的處理
* BasePermissionCompatActivity.java
* BasePermissionCompatFragment.java
* PermissionCompat.java
* PermissionUtils.java
參考代碼:
public class PermissionCompat {
private static int sNextRequestCode;
static final Map<Class<?>, OnGrantedListener> BINDERS = new LinkedHashMap<>();
// 分批次請求權(quán)限
public static void requestPermission(BasePermissionCompatActivity target, String[] permissions) {
Class<?> targetClass = target.getClass();
try {
// 找到監(jiān)聽Listener類,并實例一個
OnGrantedListener<BasePermissionCompatActivity> listener = findOnGrantedListenerForClass(targetClass, permissions);
if (PermissionUtils.hasSelfPermissions(target, permissions)) {
listener.onGranted(target, permissions);
} else if (PermissionUtils.shouldShowRequestPermissionRationale(target, permissions)) {
// 拒絕過,再次請求的時候,這個函數(shù)是否有必要,不在詢問后,返回false,第一次返回false,
//listener.onShowRationale(target, permissions);
startRequest(target, listener, permissions);
} else {
startRequest(target, listener, permissions);
}
} catch (Exception e) {
throw new RuntimeException("Unable to bind views for " + targetClass.getName(), e);
}
}
private static void startRequest(BasePermissionCompatActivity target, OnGrantedListener listener, final @NonNull String[] permissions) {
target.setOnGrantedListener(listener);
ActivityCompat.requestPermissions(target, permissions, getNextRequestCode());
}
使用
1、Activity繼承BasePermissionCompatActivity
2、用注解寫回調(diào)函數(shù),支持權(quán)限分組,跟單獨處理,但是每個分組都要寫自己的回調(diào)函數(shù)(目前回調(diào)函數(shù),不支持參數(shù))
3、回調(diào)必需配套,也就是一個權(quán)限必須對應(yīng)四個函數(shù),否則編譯不通過
-
4、請求的權(quán)限必須有回調(diào)函數(shù),不然報運行時錯誤--崩潰
@ActivityPermission public class PermssionActivity extends BasePermissionCompatActivity { 。。。 @OnGranted(value = {Manifest.permission.CAMERA}) void granted() { LogUtils.v("granted"); } @OnDenied(value = {Manifest.permission.CAMERA}) void onDenied() { LogUtils.v("onDenied"); } @OnNeverAsk(value = {Manifest.permission.CAMERA}) void OnNeverAsk() { LogUtils.v("OnNeverAsk"); } @OnShowRationale(value = {Manifest.permission.CAMERA}) void OnShowRationale() { LogUtils.v("OnShowRationale"); } <!--何時的時機調(diào)用--> @OnClick(R.id.get) void get() { PermissionCompat.requestPermission(this, new String[]{Manifest.permission.CAMERA}); } }
國產(chǎn)ROM兼容性
6.0之前權(quán)限管理即不是原生功能又沒有制定相應(yīng)標準,每個廠家的實現(xiàn)都是完全不同的,雖然4.3 Google官方試圖推出AppOpsManager來動態(tài)適配權(quán)限管理,但由于不成熟,一直到6.0也沒走向前臺。不過,看6.0之前國內(nèi)ROM的表現(xiàn),基本是在每個服務(wù)內(nèi)部觸發(fā)鑒權(quán)請求,對原生權(quán)限的判斷并沒多大影響,所以兼容沒太大問題。
提醒:記得區(qū)分是不是自己的App要權(quán)限,比如拍照,是調(diào)用系統(tǒng)相機,還是自己open Camera,兩者是有區(qū)別的。
最后附上GitHub Demo及第三方庫鏈接 權(quán)限兼容庫 PermissionCompat
作者:看書的小蝸牛
原文鏈接: Android6.0權(quán)限適配及兼容庫的實現(xiàn)
參考文檔
1、Requesting Permissions at Run Time
2、PermissionDispatcher
3、Android6.0權(quán)限適配之WRITE_EXTERNAL_STORAGE(SD卡寫入)