安卓6.0許可權處理在專案中的實踐
前言
最近公司的app說裝在安卓6.0的系統上程式直接崩潰了,然後呢crash日 志也沒有捕獲到,感覺好煩人因為公司壓根就沒有安卓6.0的測試機,最後呢我還是用genymotion來搞,由於用到了so庫以前下載的那個jar也不知怎的5.0就執行不通過了,然後今天那到處弄Genymotion-ARM-Translation.zip,最後終於還是可以了。
由於公司是使用eclipse開發的也是醉了,然後呢網上對安卓6.0的許可權有了還幾個開源框架
相關開源專案
使用eclipse和grant開源庫許可權管理
1. 更新api到23
這裡我們選擇使用grant來進行專案的演示,因為是使用eclipse來開發,所以還是和android studio裡面的還是有很多的區別,我們genymotion的6.0開啟 ,因為是6.0系統的開發,所以呢我們的api的target 必須是23,如果不是呢那就麻煩你去update下。
2. 獲得v4包
不多扯了,去你的sdk目錄下拷貝出v4包放在你工程目錄的libs下,不要問為什麼因為在接下來的 專案裡面需要用到。如下圖目錄圖:
當然我們選擇23.1.1下面的android-support-v4.jar,放入你專案的libs下面:
哈哈為什麼要這樣幹呢?因為在裡面我們會用到
這樣做的話,程式碼會比較複雜,而在support library v4中,已經為我們準備好了更簡單的方法,我們只需要採用library包中的方法來替換上面的方法即可:
- ContextCompat.checkSelfPermission()
不管當前執行的Android版本是多少,該方法都可以正確的方法許可權的許可情況,允許或者拒絕.
- ActivityCompat.requestPermissions()
當該方法在6.0之前被呼叫時,onRequestPermissionsResult回撥方法會立即被呼叫,並返回給你正確的PERMISSION_GRANTED 或者
PERMISSION_DENIED結果.
- ActivityCompat.shouldShowRequestPermissionRationale()
在6.0之前呼叫該方法,將總會返回false.
切記,你應該總是使用這些方法來取代Activity自身的checkSelfPermission,requestPermissions和shouldShowRequestPermissionsRationale方法.這樣,在不同的Android版本中,你的程式碼將總會完美的執行.
ContextCompat.checkSelfPermission();
ActivityCompat.requestPermissions();上面的方法是v4包下的方法,然後呢低版本的v4包還不具有這樣的方法,所以as的專案放在eclipse也是相當的蛋疼。
接下我們就實戰專案了,
3. 專案實戰開發
不多扯,來看程式碼:
package com.richerpay.ryshop.permissions;
/**
* Enum class to handle the different states
* of permissions since the PackageManager only
* has a granted and denied state.
*/
enum Permissions {
GRANTED,
DENIED,
NOT_FOUND
}
上面呢就 是許可權的型別了,簡單翻譯下注釋就是 用列舉對應3種狀態:
已授權,授權失敗,未發現的許可權。
再看下我們的PermissionsManager.java
package com.richerpay.ryshop.permissions;
import android.Manifest;
import android.app.Activity;
import android.content.Context;
import android.content.pm.PackageInfo;
import android.content.pm.PackageManager;
import android.os.Build;
import android.support.annotation.NonNull;
import android.support.annotation.Nullable;
import android.support.v4.app.ActivityCompat;
import android.support.v4.app.Fragment;
import android.util.Log;
import java.lang.ref.WeakReference;
import java.lang.reflect.Field;
import java.util.ArrayList;
import java.util.HashSet;
import java.util.Iterator;
import java.util.List;
import java.util.Set;
/**
* 許可權管理類
*/
public class PermissionsManager {
private static final String TAG = PermissionsManager.class.getSimpleName();
private final Set<String> mPendingRequests = new HashSet<String>(1);
private final Set<String> mPermissions = new HashSet<String>(1);
private final List<WeakReference<PermissionsResultAction>> mPendingActions = new ArrayList<WeakReference<PermissionsResultAction>>(1);
private static PermissionsManager mInstance = null;
public static PermissionsManager getInstance() {
if (mInstance == null) {
mInstance = new PermissionsManager();
}
return mInstance;
}
private PermissionsManager() {
initializePermissionsMap();
}
/**
* 此方法使用反射來讀取清單類中的所有許可權。
*因為一些許可權不存在於舊版本的安卓系統中,
*因為他們不存在,他們將被拒絕時,你檢查是否有許可權
*因為一個新的許可權,往往是補充,那裡沒有以前的
*所需的許可。我們初始化一組可用的許可權,並檢查組
*檢查是否有許可權,當我們被拒絕時許可權仍然不存在
*
*/
private synchronized void initializePermissionsMap() {
Field[] fields = Manifest.permission.class.getFields();
for (Field field : fields) {
String name = null;
try {
name = (String) field.get("");
} catch (IllegalAccessException e) {
Log.e(TAG, "Could not access field", e);
}
mPermissions.add(name);
}
}
/**
* 此方法檢索在應用程式清單中宣告的所有許可權。
* 它返回一個非空陣列,可以宣告的許可權。
*
* @param 檢查我們需要哪些許可權
* @return 返回在應用程式清單中宣告的非空陣列
*/
@NonNull
private synchronized String[] getManifestPermissions(@NonNull final Activity activity) {
PackageInfo packageInfo = null;
List<String> list = new ArrayList<String>(1);
try {
Log.d(TAG, activity.getPackageName());
packageInfo = activity.getPackageManager().getPackageInfo(activity.getPackageName(), PackageManager.GET_PERMISSIONS);
} catch (PackageManager.NameNotFoundException e) {
Log.e(TAG, "A problem occurred when retrieving permissions", e);
}
if (packageInfo != null) {
String[] permissions = packageInfo.requestedPermissions;
if (permissions != null) {
for (String perm : permissions) {
Log.d(TAG, "Manifest contained permission: " + perm);
list.add(perm);
}
}
}
return list.toArray(new String[list.size()]);
}
/**
* 在permissionsresultaction物件,它將變更通知這些許可權
* @param 許可權所需的操作的許可權。
* @param 將 動作新增到當前正在執行的動作列表中。
*/
private synchronized void addPendingAction(@NonNull String[] permissions, @Nullable PermissionsResultAction action) {
if (action == null) {
return;
}
action.registerPermissions(permissions);
mPendingActions.add(new WeakReference<PermissionsResultAction>(action));
}
/**
* 刪除從佇列中等待的動作和執行該動作
* @param 移除動作
*/
private synchronized void removePendingAction(@Nullable PermissionsResultAction action) {
for (Iterator<WeakReference<PermissionsResultAction>> iterator = mPendingActions.iterator();
iterator.hasNext(); ) {
WeakReference<PermissionsResultAction> weakRef = iterator.next();
if (weakRef.get() == action || weakRef.get() == null) {
iterator.remove();
}
}
}
/**
*這個靜態方法可以用來檢查你是否有一個特定的許可權。
*
* @param 檢查許可權的上下文物件
* @param 要檢查的許可權
* @return 返回是否授權了此許可權
*/
public synchronized boolean hasPermission(@Nullable Context context, @NonNull String permission) {
return context != null && (ActivityCompat.checkSelfPermission(context, permission)
== PackageManager.PERMISSION_GRANTED || !mPermissions.contains(permission));
}
/**
* 這個靜態方法可以用來檢查你是否有幾個特定的許可權。
* 為每一個許可權,並將簡單地返回一個布林值是否你有所有的許可權
* @param 需要檢查 許可權的上下文
* @param 許可權 陣列
* @return 返回你是否有所有的許可權
*/
public synchronized boolean hasAllPermissions(@Nullable Context context, @NonNull String[] permissions) {
if (context == null) {
return false;
}
boolean hasAllPermissions = true;
for (String perm : permissions) {
hasAllPermissions &= hasPermission(context, perm);
}
return hasAllPermissions;
}
/**
*這種方法的是獲取清單裡面的所有許可權。permissionsresultaction 用於通知允許使用者允許或拒絕每一個許可權。
*
* @param 需要檢查許可權的activity
* @param permissionsresultaction用於許可權接受通知你
*/
public synchronized void requestAllManifestPermissionsIfNecessary(final @Nullable Activity activity,
final @Nullable PermissionsResultAction action) {
if (activity == null) {
return;
}
String[] perms = getManifestPermissions(activity);
requestPermissionsIfNecessaryForResult(activity, perms, action);
}
/**
*該方法將請求的許可權,如果
*他們需要被要求(即我們沒有許可),並會增加
* permissionsresultaction到佇列被通知的許可權被授予或
*否認。在預Android棉花糖的情況下,將立即授予許可權。
*活動變數為空,但如果它是無效的,不能執行的方法。
*這是唯一可作為一種禮貌片段,getactivity()可能產量空
*如果該片段沒有新增到其父活動中
* @param 請求許可權的活動
* @param 許可權列表
* @param permissionsresultaction通知當權限授予或拒絕。
*/
public synchronized void requestPermissionsIfNecessaryForResult(@Nullable Activity activity,
@NonNull String[] permissions,
@Nullable PermissionsResultAction action) {
if (activity == null) {
return;
}
addPendingAction(permissions, action);
if (Build.VERSION.SDK_INT < Build.VERSION_CODES.M) {
doPermissionWorkBeforeAndroidM(activity, permissions, action);
} else {
List<String> permList = getPermissionsListToRequest(activity, permissions, action);
if (permList.isEmpty()) {
//if there is no permission to request, there is no reason to keep the action int the list
removePendingAction(action);
} else {
String[] permsToRequest = permList.toArray(new String[permList.size()]);
mPendingRequests.addAll(permList);
ActivityCompat.requestPermissions(activity, permsToRequest, 1);
}
}
}
/**
* 該方法將請求的許可權,如果
*他們需要被要求(即我們沒有許可),並會增加
* permissionsresultaction到佇列被通知的許可權被授予或
*否認。在預Android棉花糖(6.0)的情況下,將立即授予許可權。
*但如果 getactivity()返回null,這方法
*將無法工作作為活動引用,必須檢查許可權。
* @param 需要檢查 許可權的fragmnet
* @param 需要請求的許可權列表
* @param permissionsresultaction通知當權限授予或拒絕。
*/
public synchronized void requestPermissionsIfNecessaryForResult(@NonNull Fragment fragment,@NonNull String[] permissions,
@Nullable PermissionsResultAction action) {
Activity activity = fragment.getActivity();
if (activity == null) {
return;
}
addPendingAction(permissions, action);
if (Build.VERSION.SDK_INT < Build.VERSION_CODES.M) {
doPermissionWorkBeforeAndroidM(activity, permissions, action);
} else {
List<String> permList = getPermissionsListToRequest(activity, permissions, action);
if (permList.isEmpty()) {
//if there is no permission to request, there is no reason to keep the action int the list
removePendingAction(action);
} else {
String[] permsToRequest = permList.toArray(new String[permList.size()]);
mPendingRequests.addAll(permList);
fragment.requestPermissions(permsToRequest, 1);
}
}
}
/**
* 這個方法通知permissionsmanager,許可權已經改變。如果你正在做
*使用活動的許可權請求,則應呼叫此方法,將通知所有懸而未決 的,permissionsresultaction當前物件
*在佇列中,並將從掛起的請求列表中刪除許可權請求。
* @param 已更改的許可權。
* @param 每個許可權的值
*/
public synchronized void notifyPermissionsChange(@NonNull String[] permissions, @NonNull int[] results) {
int size = permissions.length;
if (results.length < size) {
size = results.length;
}
Iterator<WeakReference<PermissionsResultAction>> iterator = mPendingActions.iterator();
while (iterator.hasNext()) {
PermissionsResultAction action = iterator.next().get();
for (int n = 0; n < size; n++) {
if (action == null || action.onResult(permissions[n], results[n])) {
iterator.remove();
break;
}
}
}
for (int n = 0; n < size; n++) {
mPendingRequests.remove(permissions[n]);
}
}
/**
* 在安卓裝置前要求許可權(安卓6,api23),根據許可權狀態,直接進行或拒絕工作
* @param 檢測許可權的activity
* @param 許可權陣列
* @param 我們執行某項操作後許可權檢查
*/
private void doPermissionWorkBeforeAndroidM(@NonNull Activity activity, @NonNull String[] permissions, @Nullable PermissionsResultAction action) {
for (String perm : permissions) {
if (action != null) {
if (!mPermissions.contains(perm)) {
action.onResult(perm, Permissions.NOT_FOUND);
} else if (ActivityCompat.checkSelfPermission(activity, perm)
!= PackageManager.PERMISSION_GRANTED) {
action.onResult(perm, Permissions.DENIED);
} else {
action.onResult(perm, Permissions.GRANTED);
}
}
}
}
/**
* @param 檢查許可權的activity
* @param 所有許可權的名字
* @param 我們執行某項操作後許可權檢查
* @return 尚未授予的許可權名稱列表
*/
@NonNull
private List<String> getPermissionsListToRequest(@NonNull Activity activity,
@NonNull String[] permissions,@Nullable PermissionsResultAction action) {
List<String> permList = new ArrayList<String>(permissions.length);
for (String perm : permissions) {
if (!mPermissions.contains(perm)) {
if (action != null) {
action.onResult(perm, Permissions.NOT_FOUND);
}
} else if (ActivityCompat.checkSelfPermission(activity, perm) != PackageManager.PERMISSION_GRANTED) {
if (!mPendingRequests.contains(perm)) {
permList.add(perm);
}
} else {
if (action != null) {
action.onResult(perm,Permissions.GRANTED);
}
}
}
return permList;
}
}
接下來我們來看PermissionsResultAction.java
package com.xxxx.xxxx.permissions;
import android.content.pm.PackageManager;
import android.os.Handler;
import android.os.Looper;
import android.support.annotation.CallSuper;
import android.support.annotation.NonNull;
import android.util.Log;
import java.util.Collections;
import java.util.HashSet;
import java.util.Set;
/**
*將例項傳遞給 requestpermissionsifnecessaryforresult方法。結 *果將被送回你無論是ongranted(所有的許可權已被授予),或ondenied(所需*的許可權被拒絕)。
*你把你的ongranted方法和通知功能,使用者什麼都在ondenied方法不工作。
*/
public abstract class PermissionsResultAction {
private static final String TAG = PermissionsResultAction.class.getSimpleName();
private final Set<String> mPermissions = new HashSet<String>(1);
private Looper mLooper = Looper.getMainLooper();
/**
* Default Constructor
*/
public PermissionsResultAction() {}
/**
*如果您是在後臺執行緒中使用許可權請求,則希望
*回撥是在UI執行緒中,通過looper重新整理ui
*/
public PermissionsResultAction(@NonNull Looper looper) {mLooper = looper;}
/**
*當所有許可權已被使用者授予,把所有你的許可權敏感的程式碼,可以只需執行所 需許可權
*/
public abstract void onGranted();
/**
*當一個許可權被拒絕的時候呼叫
*
* @param 被拒絕的許可權
*/
public abstract void onDenied(String permission);
/**
* 忽視未發現的許可權
*/
@SuppressWarnings({})
public synchronized boolean shouldIgnorePermissionNotFound(String permission) {
Log.d(TAG, "Permission not found: " + permission);
return true;
}
/*
*返回授權 結果
*/
@CallSuper
protected synchronized final boolean onResult(final @NonNull String permission, int result) {
if (result == PackageManager.PERMISSION_GRANTED) {
return onResult(permission, Permissions.GRANTED);
} else {
return onResult(permission, Permissions.DENIED);
}
}
/**
*當一個特定的許可權已更改。
*此方法將呼叫所有許可權,所以該方法確定
*如果授權影響到該狀態,是否可以繼續進行
*/
@CallSuper
protected synchronized final boolean onResult(final @NonNull String permission, Permissions result) {
//先從許可權列表裡移除當前許可權
mPermissions.remove(permission);
if (result == Permissions.GRANTED) {
if (mPermissions.isEmpty()) {
new Handler(mLooper).post(new Runnable() {
@Override
public void run() {
onGranted();
}
});
return true;
}
} else if (result == Permissions.DENIED) {//許可權被拒
new Handler(mLooper).post(new Runnable() {
@Override
public void run() {
onDenied(permission);
}
});
return true;
} else if (result == Permissions.NOT_FOUND) {
if (shouldIgnorePermissionNotFound(permission)) {
if (mPermissions.isEmpty()) {//許可權為空
new Handler(mLooper).post(new Runnable() {
@Override
public void run() {
onGranted();//去授權
}
});
return true;
}
} else {
new Handler(mLooper).post(new Runnable() {
@Override
public void run() {
//拒絕許可權
onDenied(permission);
}
});
return true;
}
}
return false;
}
/**
*註冊指定的許可權物件的permissionsresultaction
*讓它知道哪些許可權來查詢更改。
*
* @param permissions名單
*/
@CallSuper
protected synchronized final void registerPermissions(@NonNull String[] perms) {
//把名單加到許可權集合裡
Collections.addAll(mPermissions, perms);
}
}
好了,最後我們就來使用許可權管理:
// 先對app獲取所有需要的授權(6.0需要去獲取許可權)
grantPermissions();
/***
* 獲取清單檔案中的所有許可權
*/
private void grantPermissions() {
@Override public void onGranted() {
Toast.makeText(MainActivity.this,
Toast.LENGTH_SHORT).show();
}
@Override public void onDenied(String permission) {
String message = String.format(
Locale.getDefault(),"Permission \"%1$s\" has been denied", permission); Toast.makeText(MainActivity.this, message,
Toast.LENGTH_SHORT).show();
}});
}
上面呢 我們在程式一開始呢就去授權所有的清單裡面的許可權,你會看到和ios一樣的許可權dialog提示,這裡呢我們直接來圖片展示下。
如下圖所示:
接下來我們要去通知許可權授權的改變
@Override
public void onRequestPermissionsResult(intrequestCode,@NonNull String[] permissions, @NonNull int[] grantResults) {
PermissionsManager.getInstance().notifyPermissionsChange(permissions,grantResults);
}
這裡呢我們以百度定位的例子來使用:
/**
* 定位資訊
*/
private void initLocation() {
LocationClientOption option = new LocationClientOption();
// 可選,預設高精度,設定定位模式,高精度(LocationMode.Hight_Accuracy),
// 低功耗(LocationMode.Battery_Saving),
// 僅裝置(LocationMode.Device_Sensors)
option.setLocationMode(LocationMode.Hight_Accuracy);
option.setCoorType("bd09ll");// 可選,預設gcj02,設定返回的定位結果座標系,
// tempcoor="gcj02";//國家測繪局標準"bd09"//百度墨卡託標準"bd09ll"//百度經緯度標準
// int span=1000;
option.setScanSpan(0);// 可選,預設0,即僅定位一次,設定發起定位請求的間隔需要大於等於1000ms才是有效的
option.setIsNeedAddress(true);// 可選,設定是否需要地址資訊,預設不需要
option.setOpenGps(true);// 可選,預設false,設定是否使用gps
option.setLocationNotify(true);// 可選,預設false,設定是否當gps有效時按照1S1次頻率輸出GPS結果
option.setIgnoreKillProcess(false);// 可選,預設true,定位SDK內部是一個SERVICE,並放到了獨立程序,設定是否在stop的時候殺死這個程序,預設不殺死
mLocationClient.setLocOption(option);
}
/**
*頁面 停止狀態下關閉定位
*/
@Override
protected void onStop() {
if (mLocationClient != null) {
mLocationClient.stop();// 介面消失後取消定位
mLocationClient = null;
}
super.onStop();
}
我們先來判斷定位許可權是否已授權成功:
// 判斷是否授權定位許可權
boolean hasPermission = PermissionsManager.getInstance().hasPermission(
this, Manifest.permission.ACCESS_FINE_LOCATION);
接下來我們根據是否授權成功來進行相關的操作:
if (hasPermission == false) {//沒有授權成功
grantLoactionPermissons();//去授權
}else{//授權成功去定位
mLocationClient = ((RYApplication) getApplication()).mLocationClient;
initLocation();// 定位
mLocationClient.start();// 定位SDK
// start之後會預設發起一次定位請求,開發者無須判斷isstart並主動呼叫request
mLocationClient.requestLocation();
}
根據定位需要的許可權去授權
/**
* 授權定位許可權
*/
private void grantLoactionPermissons() { PermissionsManager.getInstance().requestPermissionsIfNeces saryForResult(this,new String[] {
Manifest.permission.ACCESS_COARSE_LOCATION,
Manifest.permission.ACCESS_FINE_LOCATION,
Manifest.permission.WRITE_EXTERNAL_STORAGE },new PermissionsResultAction() { @Override public void onGranted() {
mLocationClient = ((RYApplication) getApplication()).mLocationClient;
initLocation();// 定位
mLocationClient.start();// 定位SDK // start之後會預設發起一次定位請求,開發者無須判斷isstart並主動呼叫
requestmLocationClient.requestLocation();
}
@Override
public void onDenied(String permission) {
}
});
}
當然還有一些許可權你無須去判斷:
預設許可許可權(無需授權)
還有一些許可權是在安裝的時候預設許可並且不可撤銷的,我們把它們叫做普通許可權,這些許可權如下:
android.permission.ACCESS_LOCATION_EXTRA_COMMANDS
android.permission.ACCESS_NETWORK_STATE
android.permission.ACCESS_NOTIFICATION_POLICY
android.permission.ACCESS_WIFI_STATE
android.permission.ACCESS_WIMAX_STATE
android.permission.BLUETOOTH
android.permission.BLUETOOTH_ADMIN
android.permission.BROADCAST_STICKY
android.permission.CHANGE_NETWORK_STATE
android.permission.CHANGE_WIFI_MULTICAST_STATE
android.permission.CHANGE_WIFI_STATE
android.permission.CHANGE_WIMAX_STATE
android.permission.DISABLE_KEYGUARD
android.permission.EXPAND_STATUS_BAR
android.permission.FLASHLIGHT
android.permission.GET_ACCOUNTS
android.permission.GET_PACKAGE_SIZE
android.permission.INTERNET
android.permission.KILL_BACKGROUND_PROCESSES
android.permission.MODIFY_AUDIO_SETTINGS
android.permission.NFC
android.permission.READ_SYNC_SETTINGS
android.permission.READ_SYNC_STATS
android.permission.RECEIVE_BOOT_COMPLETED
android.permission.REORDER_TASKS
android.permission.REQUEST_INSTALL_PACKAGES
android.permission.SET_TIME_ZONE
android.permission.SET_WALLPAPER
android.permission.SET_WALLPAPER_HINTS
android.permission.SUBSCRIBED_FEEDS_READ
android.permission.TRANSMIT_IR
android.permission.USE_FINGERPRINT
android.permission.VIBRATE
android.permission.WAKE_LOCK
android.permission.WRITE_SYNC_SETTINGS
com.android.alarm.permission.SET_ALARM
com.android.launcher.permission.INSTALL_SHORTCUT
com.android.launcher.permission.UNINSTALL_SHORTCUT
這些許可權和以前一樣使用,你無需檢查它們是否被撤銷了
總結和建議
現在你應該很清楚整個新的許可權系統了,同時你也應該知道我們面臨的問題了.在Android6.0中動態許可權系統已經取代了原有的許可權系統,我們別無選擇,退無可退,唯一能做的就是去做好適配工作.
好訊息是需要進行動態申請的許可權是少量的,大部分常用的許可權是預設獲得許可,並不需要你處理的.總而言之,你只需要修改一小部分程式碼來完成適配工作.
在這裡給出兩個建議:
1.優先對重要的部分進行適配,確保不會出現崩潰問題.
2.在完成適配工作前,不要將應用的targetSdkVersion設為23,尤其是在使用AndroidStudio建立新工程的時候,記得手動修改一下工程的targetSdkVersion,因為AndroidStudio會預設使用最新的SDK版本.說到修改程式碼,我必須承認工作量很大.如果之前的架構設計不合理的話,你可能需要花費一些時間來重新設計了,就像之前說的,我們別無選擇,除了去做好它.
我建議你列出應用的功能所依賴的所有的許可權,然後考慮如果許可權被拒絕,怎樣使你的應用的功能儘可能可用,並考慮好如果部分許可權被拒絕的情況下,你應該怎麼做.恩,這也很複雜,最好能記錄好各種情況.
好累啊,一不小心到12點,睡覺,加油吧。第一次使用markDown感覺好辛苦,寫個東西都