Android 6.0版本以上運行時權限解析(一)

一、概述
隨着Android 6.0發佈以及普及,作爲開發者所要應對的主要是新版本SDK帶來的一些變化;首先關注的就是權限機制的變化,Android 6.0以後增加了運行時權限(Runtime Permissions)。本篇文章的目的就是對運行時權限處理做一個簡單的介紹,並實現在應用開發過程中對權限管理的封裝。
GitHub地址: 點擊打開鏈接

 二、運行時權限
在Android 6.0以前,在安裝應用的時候,會根據權限聲明產生一個權限列表,用戶只有在同意所有權限以後才能完成app的安裝,這樣就要忍受一些不必要的權限。而在Android 6.0之後,應用可以直接安裝,使用時app需要權限時會給用戶提示,可以選擇同意或者拒絕(當然也可以在系統設置界面對每個app的權限進行查看,以及對單個權限進行授權或者解除授權)。

新的權限機制更好的保護了用戶的隱私,Google 將權限分爲兩類,一類是Normal Permissions,這類權限一般不涉及用戶隱私,不需要用戶進行授權,比如訪問網絡權限;另一類是Dangerous Permissions, 一般涉及到用戶的隱私,需要用戶進行授權,比如SD卡的讀寫權限。

  • Normal Permissions
ACCESS_NETWORK_STATE  
ACCESS_NOTIFICATION_POLICY
ACCESS_WIFI_STATE
BLUETOOTH
CHANGE_NETWORK_STATE
CHANGE_WIFI_MULTICAST_STATE
CHANGE_WIFI_STATE
GET_PACKAGE_SIZE
INTERNET
  • Dangerous Permissions
group:android.permission-group.CONTACTS
      permission:android.permission.WRITE_CONTACTS
      permission:android.permission.GET_ACCOUNTS
      permission:android.permission.READ_CONTACTS
group:android.permission-group.PHONE
      permission:android.permission.READ_CALL_LOG
      permission:android.permission.READ_PHONE_STATE
      permission:android.permission.CALL_PHONE
      permission:android.permission.WRITE_CALL_LOG
      permission:android.permission.USE_SIP
      permission:android.permission.PROCESS_OUTGOING_CALLS
      permission:com.android.voicemail.permission.ADD_VOICEMAIL
Dangerous Permissions 中權限都是分組的,那麼分組對權限機制有什麼影響呢?
如果app運行在Android 6.0以上版本的機器上,如果你申請某個危險的權限,假設你的app早已被用戶授權了同一組的某個危險權限,那麼系統會立即授權,而不需要用戶去點擊授權。比如你的app對READ_CONTACTS已經授權了,當你的app申請WRITE_CONTACTS時,系統會直接授權通過。

三、API的使用

1. 在AndroidManifest文件中添加需要的權限
   這個步驟和之前的開發並沒有什麼變化
2. 檢查權限
以申請打電話的權限爲例:
   
   // ContextCompat.checkSelfPermission()
 // ContextCompat.checkSelfPermission()
   // 方法返回值爲PackageManager.PERMISSION_DENIED或者PackageManager.PERMISSION_GRANTED
   // 當返回GRANTED表示有該權限,DENIED表示沒有該權限
   if(ContextCompat.checkSelfPermission(this,Manifest.permission.CALL_PHONE)!= PackageManager.PERMISSION_GRANTED){
           // 沒有該權限 申請打電話權限
           //  三個參數 第一個參數是 Context , 第二個參數是用戶需要申請的權限字符串數組,第三個參數是請求碼 主要用來處理用戶選擇的返回結果
           
   }else {
           ...
   }

這裏涉及到一個API方法,

ContextCompat.checkSelfPermission 主要用於檢測某個權限是否已經被授權,方法返回值爲`PackageManager.PERMISSION_DENIED` 或者 `PackageManager.PERMISSION_GRANNTED`
3. 申請權限

ActivityCompat.requestPermissions(this,new String[]{"Manifest.permission.CALL_PHONE"},CALL_PHONE_REQUEST_CODE);
這個方法是異步的,第一個參數是Context,第二個參數是需要申請的權限的字符串數組,第三個參數是requestCode,主要用於回調的時候檢查。從第二個參數來看,是支持一次性申請多個權限的,系統會彈出對話框逐一詢問用戶是否授權。
4. 處理回調
如果用戶同意或是拒絕那麼會回調`onRequestPermissionsResult()`

@Override
public void onRequestPermissionsResult(int requestCode, String[] permissions, int[] grantResults) {
        if(requestCode == CALL_PHONE_REQUEST_CODE){
            if (grantResults !=null&&grantResults[0] == PackageManager.PERMISSION_GRANTED) {
                // permission was granted, yay! Do the
                // contacts-related task you need to do.  
                ...
            } else {
                // permission denied, boo! Disable the
                // functionality that depends on this permission.  
                
            }
        }
}
對於權限的申請結果,首先驗證requestCode定位到你的申請,然後驗證grantResults對應於申請的結果,這裏的數組對應於申請時的第二個權限字符串數組。如果你同時申請兩個權限,那麼grantResults的length就爲2,分別記錄你兩個權限的申請結果。如果申請成功,就可以做你的事情了。

不過還有一個API方法值得注意,就是`ActivityCompat.shouldShowRequestPermissionRationale`


// Should we show an explanation?
if (ActivityCompat.shouldShowRequestPermissionRationale(thisActivity,
        Manifest.permission.READ_CONTACTS)) 
    // Show an expanation to the user *asynchronously* -- don't block
    // this thread waiting for the user's response! After the user
    // sees the explanation, try again to request the permission.

}
這個API主要用於給用戶一個申請權限的解釋,該方法只有在用戶在上一次已經拒絕過你的這個權限申請。也就是說,用戶已經拒絕一次了,又彈個授權框,需要給用戶一個解釋,爲什麼要授權,則使用該方法。


四、簡單的例子
這裏寫一個簡單的例子,針對於運行時權限。看看直接撥打電話在Android 6.x的設備上權限需要如何處理。
public class MainActivity extends AppCompatActivity {
    // 打電話權限申請的請求碼
    private static final int REQUEST_PERMISSION_CALL_CODE = 0x0011;

    @Override
    protected void onCreate(Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);
        setContentView(R.layout.activity_main);
    }

    public void phoneClick(View view){
        if(ContextCompat.checkSelfPermission(this, Manifest.permission.CALL_PHONE)
                != PackageManager.PERMISSION_GRANTED){
            Toast.makeText(this, "申請權限", Toast.LENGTH_SHORT).show();
            ActivityCompat.requestPermissions(this,
                    new String[]{"Manifest.permission.CALL_PHONE"}, REQUEST_PERMISSION_CALL_CODE);
        }else {
            callPhone();
        }
    }

    /**
    * 撥打電話
    **/
    private void callPhone() {
        Intent intent = new Intent(Intent.ACTION_CALL);
        Uri data = Uri.parse("tel:13111111111");
        intent.setData(data);
        startActivity(intent);
    }

    @Override
    public void onRequestPermissionsResult(int requestCode, String[] permissions, int[] grantResults) {
        super.onRequestPermissionsResult(requestCode, permissions, grantResults);
        if(requestCode == REQUEST_PERMISSION_CALL_CODE){
            if (grantResults !=null&&grantResults[0] == PackageManager.PERMISSION_GRANTED) {
                // Permission Granted
                callPhone();
            } else {
                // Permission Denied
                Toast.makeText(this,"權限被拒絕了",Toast.LENGTH_SHORT).show();
            }
        }
    }
}

五、封裝
雖然權限處理並不複雜,但是需要編寫很多重複的代碼,所以目前也有很多庫對用法進行了封裝(可以在github首頁搜索)。本節將要講解的是使用註解和反射的方式封裝,封裝的代碼很簡單,接下來看看如何實現。
(1) 權限申請
從上面的例子中可以看到,如果申請其他權限,申請的邏輯是類似的,區別就在於參數不同,我們需要三個參數: `Activity\Fragment``permissions數組``requestCode申請碼`, 也就是說只需要寫個方法,接收這幾個參數即可,然後邏輯就是統一的。
@TargetApi(Build.VERSION_CODES.M)
    private void requestPermissions(Object obj, int requestCode, String[] permissions) {
        if (!PermissionUtil.isOverMarshmallow()) {
            //如果運行在Android 6.0以下的版本上,直接執行方法
            executeSuccess(obj, requestCode);
            return;
        }
        //尋找是否有未被允許的權限
        List<String> deniedPermissions = PermissionUtil.findDeniedPermissions(getActivity(obj), permissions);
        if (deniedPermissions.size() > 0) {
            //存在未允許的權限,需要申請權限
            if (obj instanceof Activity) {
                ((Activity) obj).requestPermissions(deniedPermissions.toArray(new String[deniedPermissions.size()]), requestCode);
            } else if (obj instanceof Fragment) {
                ((Fragment) obj).requestPermissions(deniedPermissions.toArray(new String[deniedPermissions.size()]), requestCode);
            } else {
                throw new IllegalArgumentException(object.getClass().getName() + " is not supported");
            }
        } else {
            // 權限都已經被允許,直接執行方法
            executeSuccess(obj, requestCode);
        }
}


`PermissionUtil.findDeniedPermissions` 就是檢查 沒有授權的權限.
 @TargetApi(Build.VERSION_CODES.M)
    public static List<String> findDeniedPermissions(Activity activity, String... permissions) {
        List<String> deniedPermissions = new ArrayList<>();

        for (String permission : permissions) {
            if (activity.checkSelfPermission(permission) != PackageManager.PERMISSION_GRANTED) {
                deniedPermissions.add(permission);
            }
        }

        return deniedPermissions;
    }


那麼上述的邏輯就很清晰了,需要的3種參數傳入,先去除已經申請的權限,然後開始申請權限。


(2) 回調處理

回調主要做的事情:
   1. 瞭解是否授權
   2. 根據授權情況進行回調

對於第二條,會涉及到兩個分支,每個分支執行不同的方法,這裏利用註解去確定需要執行的方法,存在兩個註解:
@PermissionSuccess(requestCode = 0x01)
@PermissionFail(requestCode = 0x01)
利用反射根據授權情況+requestCode即可找到註解標註的方法,然後直接執行即可。

大致代碼如下:
 public static void onRequestPermissionsResult(Object obj, int requestCode, String[] permissions, int[] grantResults) {
        List<String> deniedPermissions = new ArrayList<>();
        for (int i = 0; i < grantResults.length; i++) {
            if (grantResults[i] != PackageManager.PERMISSION_GRANTED) {
                deniedPermissions.add(permissions[i]);
            }
        }

        if (deniedPermissions.size() > 0) {
            executeFail(obj, requestCode);
        } else {
            executeSuccess(obj, requestCode);
        }
}


首先根據grantResults進行判斷成功還是失敗,對於成功則執行成功的回調:
    private static void executeSuccess(Object obj, int requestCode) {
        Method succMethod = PermissionUtil.findMethodWithRequestCode(obj.getClass(), PermissionSuccess.class, requestCode);
        executeMethod(obj, succMethod);
    }


`PermissionUtil.findMethodWithRequestCode` 根據註解和requestCode找到方法,然後反射執行即可,代碼如下:

public static <T extends Annotation> Method findMethodWithRequestCode(Class clazz, Class<T> annotation, int requestCode) {
        for (Method method : clazz.getDeclaredMethods()) {
            if (method.isAnnotationPresent(annotation)) {
                if (isEqualRequestCodeFromAnnotation(method, annotation, requestCode)) {
                    return method;
                }
            }
        }
        return null;
}

對於失敗則執行失敗的回調,代碼如下:

private static void executeFail(Object obj, int requestCode) {
        Method failMethod = PermissionUtil.findMethodWithRequestCode(obj.getClass(), PermissionFail.class, requestCode);
        executeMethod(obj, failMethod);
 }
到此,對於權限處理的封裝基本介紹完了。

六、總結

以上講解的封裝方式,採用了註解和反射,對於效率的影響還是存在的;計劃在上述代碼的基礎上將運行時註解修改成`Annotation Processor`的方式,即編譯時註解,這樣就會避免因反射影響到效率的問題了。

參考資料

http://blog.csdn.net/lmj623565791/article/details/50709663
https://github.com/lovedise/PermissionGen




發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章