1. 程式人生 > >Activity原始碼之Android 6.0許可權相關完全解析

Activity原始碼之Android 6.0許可權相關完全解析

我們都知道Android6.0以前許可權的申請非常簡單,只需要在mainfest宣告所需的許可權即可。而6.0以後,Android將許可權的管理進一步嚴格化,它要求使用者在使用某些敏感許可權時,必須在mainfest中先宣告之後再動態申請。在一定程度上約束了應用對許可權的索取,保證了使用者的資料隱私;當然,反過來也增加了開發者的難度。

想必也都知道,6.0將許可權粗分成了兩種Normal和Dangerous。所謂的Normal就是一些不需要使用者授權就直接擁有的許可權,這類許可權往往不涉及使用者隱私資料,比如訪問網路/wifi等,你從來沒有見過一個應用彈出對話方塊讓使用者授權訪問網路的吧;另外一種就是Dangerous,可以想象,這種許可權一般都會訪問到使用者的私人資料,比如聯絡人、儲存卡等;至於具體許可權的分類所屬,請檢視相關文件

,這裡不再贅述。

每一個開發者在初次接觸許可權問題時,一定頭大過,也一定都會思考我什麼時間申請、我需不需要申請、怎麼申請、申請結果怎麼處理諸如此類問題。在Activity長達7000多行的原始碼中,關於許可權的部分並不是很多。除去兩個分發許可權結果的私有函式dispatchRequestPermissionsResult和dispatchRequestPermissionsResultToFragment之外,真正與我們直接相關的api其實只有3個,在加上ContextCompat中有一個檢查許可權的函式,一共是4個。許可權問題,歸根結底就是這四個函式如何使用的問題,巧合的是,這四個函式也正好解決了前面所說的幾個問題。

  • 如何檢查許可權?
    許可權的檢查很簡單,ContextCompat(它的子類ActivityCompat中也有)中提供了一個靜態函式checkSelfPermission(context, permission),它會返回當前Activity是否擁有相關許可權,通過比對PackageManager相關常量,就可以做出判定;舉個例子,儲存卡許可權就可以這樣寫:
if(ContextCompat.checkPermission(this,Manifest.permission.WRITE_EXTERNAL_STORAGE)!=PackageManager.PERMISSION_GRANTED) {
  //沒有許可權,需要在此處申請了
} else { //已經擁有許可權 }
  • 什麼時間申請?
    這個很好解決,就在你需要這個許可權的時候申請。比如說我的應用要支援語音訊息,很顯然需要麥克風許可權。一般的做法是,當你在Activty/Fragment傳送語音時(而不是進入Activty/Fragment時),檢查是否擁有麥克風許可權,沒有的話必須申請,否則不能正常使用。

  • 如何申請?
    申請的關鍵是對requestPermissions(Activity activity,String[] permissions, int requestCode)的使用,三個引數分別是申請的activity、申請的許可權陣列、請求碼(必須>=0)。雖然Activity(Fragment)和ActivityCompat中都提供了這個函式,但前者是final型的,後者卻是static型的,應用較多的是ActivityCompat中的這個(兩個沒什麼區別,static型的用著更方便封裝)。配合著上 main的許可權檢查,程式碼就變成了這樣:

int requestCode = 1if(ContextCompat.checkPermission(this,Manifest.permission.WRITE_EXTERNAL_STORAGE)!=PackageManager.PERMISSION_GRANTED) {
    ActivityCompat.requestPermission(this, new String[{Manifest.permission.WRITE_EXTERNAL_STORAGE}, requestCode);
} else {
    //已經擁有許可權
}
  • 申請結果怎麼處理?
    許可權的申請已經發起,但是否授予卻掌握在使用者手裡。應用必須要妥善處理使用者可能的兩種操作:同意、拒絕。好在強大的Android已經幫我們封裝好了,Activity(Fragment)中提供了onRequestPermissionsResult(int requestCode, String[] permissions,int[] grantResults)這樣一個回撥函式來處理使用者的授權結果。3個引數分表表示請求碼(就是requestPermission中傳進去的那個)、一次申請的多個許可權、對應的授予結果可以這樣寫:
@Override
public void onRequestPermissionsResult(int requestCode, @NonNull String[] permissions, @NonNull int[] grantResults) {
    if (requestCode == 1) {
       if (grantResults[0] == 
           PackageManager.PERMISSION_GRANTED) {
           Log.d(TAG, "onRequestPermissionsResult: 已經有許可權");
       } else {
           Log.d(TAG, "onRequestPermissionsResult: 去申請");  
       }
    }
}

許可權的申請及結果處理都有了,但是我們只用了3個函式,還有一個shouldShowRequestPermissionRationale(String permission)
,api中說它是檢查是否該給使用者一個提示,來解釋一下為什麼我需要這個許可權。所以大多數許可權相關的部落格(包括鴻洋大神還有)中都這樣使用它:

@Override
public void onRequestPermissionsResult(int requestCode, @NonNull String[] permissions,
@NonNull int[] grantResults) {
     if (requestCode == 0) {
        if (grantResults[0] ==    
            PackageManager.PERMISSION_GRANTED) {
            Log.d(TAG, "onRequestPermissionsResult: 已經有許可權");
        } else {     
           if(shouldShowRequestPermissionRationale(permissions[0]){
              //彈框給使用者一個解釋
           } else {
              //接著申請
              requestPermission(this,
                          new String[{Manifest.permission.WRITE_EXTERNAL_STORAGE}, 0);
           }

           //if(shouldShowRequestPermissionRationale(permissions[0])) {
           //   requestPermission(this, 
           //            new String[]{Manifest.permission.WRITE_EXTERNAL_STORAGE}, 0);  
           //} else {
           //
           //}                                                   
        }
    }
}

上面提供了兩種寫法:註釋掉的和未註釋的。遺憾的是,這兩種寫法都讓人崩潰。註釋掉的寫法,顯然是用錯了shouldShowRequestPermissionRationale,造成的後果是,如果使用者如果不勾選“不再詢問”,許可權申請的彈框會不斷彈出,讓人抓狂。沒註釋的寫法也是錯的,分析之前先來看兩張圖。
這裡寫圖片描述
圖1 許可權彈窗第一次出現的樣子
這裡寫圖片描述
圖2 第一次拒絕之後許可權彈窗再次出現時的樣子

測試發現,不管是圖1還是圖2,只要只點擊deny,shouldShowRequestPermissionRationale返回的都是true;如果圖2中勾選了“don’t ask again”,並點選了deny時shouldShowRequestPermissionRationale才會返回false。

由上面的結論再來看未註釋的程式碼,發現如果勾選了“don’t ask again”,並且deny了,shouldShowRequestPermissionRationale此時返回的是false。程式會進入else分支重新requestPermission,而因為勾選了“don’t ask again”導致許可權彈窗不會再出現,造成的後果是許可權直接獲取失敗,不幸的是shouldShowRequestPermissionRationale一直返回false,又會進行下一次requestPermission。就這樣,不斷地迴圈,一直到crash。

事實上,個人覺得這個函式的設計比較雞肋,它名義上是來判斷是否需要給使用者一個許可權申請的說明。可實際情況上,當我們動態地向用戶申請某個許可權時,只要使用者拒絕,不管shouldShowRequestPermissionRationale返回什麼,我們都必須要給出一個提醒(一般是來告訴使用者為什麼要開這個許可權,怎樣開);如果不提醒使用者,使用者很可能不知道自己曾經拒絕了某個許可權,進而導致無法正常使用app;大多數應用包括微信等,都是採用的這種策略。那程式碼該如何展示呢?

public class Permission extends AppCompatActivity {
    private static final String TAG = "MainActivity";

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

        if (checkPermission(this, Manifest.permission.READ_CONTACTS) != 
            PackageManager.PERMISSION_GRANTED) {
           requestPermission(this, new String[]{Manifest.permission.READ_CONTACTS}, 0);
        }
    }

    private int checkPermission(Context context, String permission) {
        return ContextCompat.checkSelfPermission(context, permission);
    }

    private void requestPermission(Context context, String[] permissions, int 
        requestCode) {
        ActivityCompat.requestPermissions((Activity) context, permissions, 
        requestCode);
    }

    @Override
    public void onRequestPermissionsResult(int requestCode, @NonNull String[]
        permissions,@NonNull int[] grantResults) {
        if (requestCode == 0) {
            if (grantResults[0] == PackageManager.PERMISSION_GRANTED) {
                Log.d(TAG, "onRequestPermissionsResult: 已經有許可權");
            } else {
                createDialog();
            }
        }
    }

    private void createDialog() {
        final TextView hint = new TextView(this);
        hint.setText("你可以在設定中開啟聯絡人許可權");
        AlertDialog.Builder builder = new AlertDialog.Builder(this);
        builder.setTitle("許可權申
           請").setIcon(android.R.drawable.ic_dialog_info)
               .setView(hint)
               .setNegativeButton("取消", null);
        builder.setPositiveButton("確定", (dialog, which) -> {
            Log.d(TAG, "createDialog: 去設定");
            //一般是開啟手機設定
        });
        builder.show();
    }
}

當然,AndroidMainfest中的許可權宣告也不能少。這裡只是給了個示例,完全可以按照自己的喜好來進行相關封裝;或者使用註解等方法來讓程式碼變得更簡單。最後,強力吐槽一下這個程式碼編輯器,已經盡力把它做到最美觀了……

關於許可權處理,這裡總結幾點:
1 許可權+系統api,這兩個任意一個都比較棘手;
2 需要多測試。不同手機可能對系統api進行了定製,導致手機的表現可能不盡相同,比如華為和小米。
3 許可權彈窗是系統定製的,不可更改,基本每個品牌一個風格。。
4 一些重要許可權,比如儲存卡,一般使用了儲存的應用都要滿足“如果沒有儲存卡許可權,應用就退出”的原則(微信就是這麼幹的),最好放在基類的onResume裡申請(因為儲存許可權使用的地方太多了,不可能在每一處都動態申請,也是為什麼“沒有儲存許可權就退出”的原因)。別問為什麼在onResume裡面,因為使用者可能會用的好好的,手癢去把許可權給關了。。。
5 通過inetnt的方式開啟手機上的某些應用,比如開啟通訊錄頁面,不需要你自己去申請許可權;因為你只是開啟通訊錄並沒有讀取通訊錄資料;真正需要申請許可權的是通訊錄這個系統應用;
6 多動手試試,不要盲從。

有問題,歡迎留言.如果可能,將第一時間為你解答。