Android 6.0 執行時許可權管理最佳實踐

嚴振杰發表於2019-03-04

這是一篇遲來的文章,Android M已經發布一年多了(6.0的變化),在Android M中許可權系統被重新設計,發生了顛覆性的變化,很多人把握不好這個變化,一是對這個許可權策略和套路還沒有摸透,二是沒有一個很好的實踐來支撐,在我的技術開發群裡很多人問我關於許可權的問題,往往我都沒有直接回答,因為這個問題不是一兩句說的清楚的,這幾點是今天我寫這篇文章的原因。這裡有一切關於Android執行時許可權你需要知道的,包括如何在程式碼中實現,如果你以前不知道這些東西,現在來看也為時不晚,我將在詳解之後給你一個最佳的實踐方案。

執行時許可權開源庫AndPermission:github.com/yanzhenjie/…

如果你的英文夠好,推薦你閱讀官網的文章:

正開始開始之前來幾張我的例項圖:

  • Activity/Fragment中申請單個許可權
    Activity/Fragment中申請單個許可權

  • Activity/Fragment中同時申請多個許可權
    Activity/Fragment中同時申請多個許可權

  • Activity/Fragment中被使用者拒絕後,下次申請時提醒使用者
    Activity/Fragment中被使用者拒絕後,下次申請時提醒使用者


關於執行時許可權

在舊的許可權管理系統中,許可權僅僅在App安裝時詢問使用者一次,使用者同意了這些許可權App才能被安裝(某些深度定製系統另說),App一旦安裝後就可以偷偷的做一些不為人知的事情了。

在Android6.0開始,App可以直接安裝,App在執行時一個一個詢問使用者授予許可權,系統會彈出一個對話方塊讓使用者選擇是否授權某個許可權給App(這個Dialog不能由開發者定製),當App需要使用者授予不恰當的許可權的時候,使用者可以拒絕,使用者也可以在設定頁面對每個App的許可權進行管理。

特別注意:這個對話方塊不是開發者呼叫某個許可權的功能時由系統自動彈出,而是需要開發者手動呼叫,如果你直接呼叫而沒有去申請許可權的話,將會導致App奔潰。

也許你已經開始慌了,這對於使用者來說是好事,但是對於開發者來說我們不能直接呼叫方法了,我們不得不在每一個需要許可權的地方檢查並請求使用者授權,所以就引出了以下兩個問題。

哪些許可權需要動態申請

新的許可權策略講許可權分為兩類,第一類是不涉及使用者隱私的,只需要在Manifest中宣告即可,比如網路、藍芽、NFC等;第二類是涉及到使用者隱私資訊的,需要使用者授權後才可使用,比如SD卡讀寫、聯絡人、簡訊讀寫等。

Normal Permissions

此類許可權都是正常保護的許可權,只需要在AndroidManifest.xml中簡單宣告這些許可權即可,安裝即授權,不需要每次使用時都檢查許可權,而且使用者不能取消以上授權,除非使用者解除安裝App。

  • ACCESS_LOCATION_EXTRA_COMMANDS
  • ACCESS_NETWORK_STATE
  • ACCESS_NOTIFICATION_POLICY
  • ACCESS_WIFI_STATE
  • BLUETOOTH
  • BLUETOOTH_ADMIN
  • BROADCAST_STICKY
  • CHANGE_NETWORK_STATE
  • CHANGE_WIFI_MULTICAST_STATE
  • CHANGE_WIFI_STATE
  • DISABLE_KEYGUARD
  • EXPAND_STATUS_BAR
  • GET_PACKAGE_SIZE
  • INSTALL_SHORTCUT
  • INTERNET
  • KILL_BACKGROUND_PROCESSES
  • MODIFY_AUDIO_SETTINGS
  • NFC
  • READ_SYNC_SETTINGS
  • READ_SYNC_STATS
  • RECEIVE_BOOT_COMPLETED
  • REORDER_TASKS
  • REQUEST_IGNORE_BATTERY_OPTIMIZATIONS
  • REQUEST_INSTALL_PACKAGES
  • SET_ALARM
  • SET_TIME_ZONE
  • SET_WALLPAPER
  • SET_WALLPAPER_HINTS
  • TRANSMIT_IR
  • UNINSTALL_SHORTCUT
  • USE_FINGERPRINT
  • VIBRATE
  • WAKE_LOCK
  • WRITE_SYNC_SETTINGS

Dangerous Permissions

所有危險的Android系統許可權屬於許可權組,如果APP執行在Android 6.0 (API level 23)或者更高階別的裝置中,而且targetSdkVersion>=23時,系統將會自動採用動態許可權管理策略,如果你在涉及到特殊許可權操作時沒有做動態許可權的申請將會導致App崩潰,因此你需要注意:

  • 此類許可權也必須在Manifest中申明,否則申請時不提使用使用者,直接回撥開發者許可權被拒絕。
  • 同一個許可權組的任何一個許可權被授權了,這個許可權組的其他許可權也自動被授權。例如,一旦WRITE_CONTACTS被授權了,App也有READ_CONTACTSGET_ACCOUNTS了。
  • 申請某一個許可權的時候系統彈出的Dialog是對整個許可權組的說明,而不是單個許可權。例如我申請READ_EXTERNAL_STORAGE,系統會提示"允許xxx訪問裝置上的照片、媒體內容和檔案嗎?"

如果App執行在Android 5.1 (API level 22)或者更迭級別的裝置中,或者targetSdkVersion<=22時(此時裝置可以是Android 6.0 (API level 23)或者更高),在所有系統中仍將採用舊的許可權管理策略,系統會要求使用者在安裝的時候授予許可權。其次,系統就告訴使用者App需要什麼許可權組,而不是個別的某個許可權。

  • CALENDAR(日曆)
    • READ_CALENDAR
    • WRITE_CALENDAR
  • CAMERA(相機)
    • CAMERA
  • CONTACTS(聯絡人)
    • READ_CONTACTS
    • WRITE_CONTACTS
    • GET_ACCOUNTS
  • LOCATION(位置)
    • ACCESS_FINE_LOCATION
    • ACCESS_COARSE_LOCATION
  • MICROPHONE(麥克風)
    • RECORD_AUDIO
  • PHONE(手機)
    • READ_PHONE_STATE
    • CALL_PHONE
    • READ_CALL_LOG
    • WRITE_CALL_LOG
    • ADD_VOICEMAIL
    • USE_SIP
    • PROCESS_OUTGOING_CALLS
  • SENSORS(感測器)
    • BODY_SENSORS
  • SMS(簡訊)
    • SEND_SMS
    • RECEIVE_SMS
    • READ_SMS
    • RECEIVE_WAP_PUSH
    • RECEIVE_MMS
  • STORAGE(儲存卡)
    • READ_EXTERNAL_STORAGE
    • WRITE_EXTERNAL_STORAGE

使用adb命令可以檢視這些需要授權的許可權組:

adb shell pm list permissions -d -g複製程式碼

使用adb命令同樣可以授權/撤銷某個許可權:

adb shell pm [grant|revoke] ...複製程式碼

關於執行時許可權的一些建議

  1. 只請求你需要的許可權,減少請求的次數,或用Intent來代替,讓其他的應用來處理。

    1. 如果你使用Intent,你不需要設計介面,由第三方的應用來完成所有操作。比如打電話、選擇圖片等。
    2. 如果你請求許可權,你可以完全控制使用者體驗,自己定義UI。但是使用者也可以拒絕許可權,就意味著你的應用不能執行這個特殊操作。
  2. 防止一次請求太多的許可權或請求次數太多,使用者可能對你的應用感到厭煩,在應用啟動的時候,最好先請求應用必須的一些許可權,非必須許可權在使用的時候才請求,建議整理並按照上述分類管理自己的許可權:

    1. 普通許可權(Normal PNermissions):只需要在Androidmanifest.xml中宣告相應的許可權,安裝即許可。
    2. 需要執行時申請的許可權(Dangerous Permissions):
    • 必要許可權:最好在應用啟動的時候,進行請求許可的一些許可權(主要是應用中主要功能需要的許可權)。
    • 附帶許可權:不是應用主要功能需要的許可權(如:選擇圖片時,需要讀取SD卡許可權)。
  3. 解釋你的應用為什麼需要這些許可權:在你呼叫requestPermissions()之前,你為什麼需要這個許可權。

    1. 例如,一個攝影的App可能需要使用定位服務,因為它需要用位置標記照片。一般的使用者可能會不理解,他們會困惑為什麼他們的App想要知道他的位置。所以在這種情況下,所以你需要在requestpermissions()之前告訴使用者你為什麼需要這個許可權。
  4. 使用相容庫support-v4中的方法

    ContextCompat.checkSelfPermission()
    ActivityCompat.requestPermissions()
    ActivityCompat.shouldShowRequestPermissionRationale()複製程式碼

幾個重要的方法與常量解釋

  • PackageManager中的兩個常量:

    • PackageManager.PERMISSION_DENIED:該許可權是被拒絕的。
    • PackageManager.PERMISSION_GRANTED:該許可權是被授權的。
  • Activity中或者Fragment都會有以下幾個方法:

    int checkSelfPermission(String)
    void requestPermissions(int, String...)
    boolean shouldShowRequestPermissionRationale(String)
    void onRequestPermissionsResult()複製程式碼

上述四個方法中,前三個方法在support-v4ActivityCompat中都有,建議使用相容庫中的方法。最後一個方法是使用者授權或者拒絕某個許可權組時系統會回撥Activity或者Fragment中的方法。

checkSelfPermission() 檢查許可權

  1. 檢查某一個許可權的當前狀態,你應該在請求某個許可權時檢查這個許可權是否已經被使用者授權,已經授權的許可權重複申請可能會讓使用者產生厭煩。
  2. 該方法有一個引數是許可權名稱,有一個int的返回值,用這個值與上面提到的兩個常量做比較可判斷檢查的許可權當前的狀態。
    if (ContextCompat.checkSelfPermission(context, Manifest.permission.READ_CONTACTS)
         != PackageManager.PERMISSION_GRANTED) {
     // 沒有許可權,申請許可權。
    }else{
     // 有許可權了,去放肆吧。
    }複製程式碼

requestPermissions() 申請許可權

  1. 請求使用者授權幾個許可權,呼叫後系統會顯示一個請求使用者授權的提示對話方塊,App不能配置和修改這個對話方塊,如果需要提示使用者這個許可權相關的資訊或說明,需要在呼叫 requestPermissions() 之前處理,該方法有兩個引數:
  • int requestCode,會在回撥onRequestPermissionsResult()時返回,用來判斷是哪個授權申請的回撥。
  • String[] permissions,許可權陣列,你需要申請的的許可權的陣列。
  1. 由於該方法是非同步的,所以無返回值,當使用者處理完授權操作時,會回撥Activity或者Fragment的onRequestPermissionsResult()方法。
    ActivityCompat.requestPermissions(activity, new String[]{Manifest.permission.READ_CONTACTS}, MMM);複製程式碼

onRequestPermissionsResult() 處理許可權結果回撥

  1. 該方法在Activity/Fragment中應該被重寫,當使用者處理完授權操作時,系統會自動回撥該方法,該方法有三個引數:
  • int requestCode,在呼叫requestPermissions()時的第一個引數。
  • String[] permissions,許可權陣列,在呼叫requestPermissions()時的第二個引數。
  • int[] grantResults,授權結果陣列,對應permissions,具體值和上方提到的PackageManager中的兩個常量做比較。
    @Override
    public void onRequestPermissionsResult(int requestCode, String permissions[], int[] grantResults) {
      switch (requestCode) {
          case MMM: {
              if (grantResults.length > 0
                  && grantResults[0] == PackageManager.PERMISSION_GRANTED) {
                  // 許可權被使用者同意,可以去放肆了。
              } else {
                  // 許可權被使用者拒絕了,洗洗睡吧。
              }
              return;
          }
      }
    }複製程式碼

shouldShowRequestPermissionRationale()

  1. 望文生義,是否應該顯示請求許可權的說明。
  2. 第一次請求許可權時,使用者拒絕了,呼叫shouldShowRequestPermissionRationale()後返回true,應該顯示一些為什麼需要這個許可權的說明。
  3. 使用者在第一次拒絕某個許可權後,下次再次申請時,授權的dialog中將會出現“不再提醒”選項,一旦選中勾選了,那麼下次申請將不會提示使用者。
  4. 第二次請求許可權時,使用者拒絕了,並選擇了“不在提醒”的選項,呼叫shouldShowRequestPermissionRationale()後返回false。
  5. 裝置的策略禁止當前應用獲取這個許可權的授權:shouldShowRequestPermissionRationale()返回false 。
  6. 加這個提醒的好處在於,使用者拒絕過一次許可權後我們再次申請時可以提醒該許可權的重要性,面得再次申請時使用者勾選“不再提醒”並決絕,導致下次申請許可權直接失敗。

綜上所述,整合程式碼後:

if (ContextCompat.checkSelfPermission(this, Manifest.permission.READ_CONTACTS) != PackageManager.PERMISSION_GRANTED) {// 沒有許可權。
    if (ActivityCompat.shouldShowRequestPermissionRationale(this, Manifest.permission.READ_CONTACTS)) {
            // 使用者拒絕過這個許可權了,應該提示使用者,為什麼需要這個許可權。
    } else {
        // 申請授權。
        ActivityCompat.requestPermissions(thisActivity, new String[]{Manifest.permission.READ_CONTACTS}, MMM);
    }
}

...

@Override
public void onRequestPermissionsResult(int requestCode, String permissions[], int[] grantResults) {
    switch (requestCode) {
        case MMM: {
            if (grantResults.length > 0
                && grantResults[0] == PackageManager.PERMISSION_GRANTED) {
                // 許可權被使用者同意,可以去放肆了。
            } else {
                // 許可權被使用者拒絕了,洗洗睡吧。
            }
            return;
        }
    }
}複製程式碼

執行時許可權最佳實踐的套路

總體下來我們應該對執行時許可權有一個系統的認識,我總結出了一些套路:

  1. 需要區分各種Normal Permissioin和Dangerous Permissions。
  2. 判斷多個許可權授權回撥時需要判斷每一個許可權是否全都是被授權了,否則操作不能繼續。
  3. 需要請求多個許可權時需要挨個檢查是否已經被授權過,沒授權的才去請求,還要檢查這些許可權是否需要提示使用者,如果多個許可權都需要提示,該如何處理。
  4. 上述1 2 3如果在需要在多個頁面 實現,程式碼重複。
  5. ...

其實問題遠遠不止這些,認真看過文章的人應該會發現,實現程式碼比較簡單,但是程式碼重複加上需要我們考慮和注意的細節太多了,那麼下面我就為大家介紹一個開源內褲來解決這一系列問題。

AndPermission

這個開源庫名叫AndPermission:github.com/yanzhenjie/…,經過我的實踐是完全解決了上述問題,推薦大家使用,有興趣的朋友可以去star下。

  • AndroidStudio使用方法,gradle一句話遠端依賴

    compile 'com.yanzhenjie:permission:1.0.0'複製程式碼

    Or Maven:

    <dependency>
    <groupId>com.yanzhenjie</groupId>
    <artifactId>permission</artifactId>
    <version>1.0.0</version>
    <type>pom</type>
    </dependency>複製程式碼
  • Eclipse 下載jar包,或者下載原始碼

使用介紹

1、申請許可權就是這麼簡單

AndPermission.with(this)
    .requestCode(101)
    .permission(Manifest.permission.WRITE_CONTACTS,
        Manifest.permission.READ_SMS,
        Manifest.permission.WRITE_EXTERNAL_STORAGE)
    .send();複製程式碼

只需要在Activity中或者Fragment中直接呼叫即可,AndPermission自動為你打理好後宮。

2、接受許可權回撥更簡單 只需要重寫Activity/Fragment的一個方法,然後提供一個授權時回撥的方法即可:

@Override
public void onRequestPermissionsResult(int requestCode, @NonNull String[] permissions, @NonNull int[] grantResults) {
    // 只需要呼叫這一句,剩下的AndPermission自動完成。
    AndPermission.onRequestPermissionsResult(this, requestCode, permissions, grantResults);
}

// 成功回撥的方法,用註解即可,裡面的數字是請求時的requestCode。
@PermissionYes(100)
private void getLocationYes() {
    // 申請許可權成功,可以去做點什麼了。
    Toast.makeText(this, "獲取定位許可權成功", Toast.LENGTH_SHORT).show();
}

// 失敗回撥的方法,用註解即可,裡面的數字是請求時的requestCode。
@PermissionNo(100)
private void getLocationNo() {
    // 申請許可權失敗,可以提醒一下使用者。
    Toast.makeText(this, "獲取定位許可權失敗", Toast.LENGTH_SHORT).show();
}複製程式碼

只需要上面這麼幾句話即可,你就可以大刀闊斧的幹了,在總結中提到的各種判斷、複雜的情況AndPermission自動完成。

3、如果你需要在使用者多次拒絕許可權後提示使用者

AndPermission.with(this)
    .requestCode(101)
    .permission(Manifest.permission.WRITE_CONTACTS,
        Manifest.permission.READ_SMS,
        Manifest.permission.WRITE_EXTERNAL_STORAGE)
    .rationale(mRationaleListener)
    .send();

private RationaleListener mRationaleListener = new RationaleListener() {
    @Override
    public void showRequestPermissionRationale(int requestCode, final Rationale rationale) {
        new AlertDialog.Builder(RationalePermissionActivity.this)
            .setTitle("友好提醒")
            .setMessage("沒有定位許可權將不能為您推薦附近妹子,請把定位許可權賜給我吧!")
            .setPositiveButton("好,給你", new DialogInterface.OnClickListener() {
                @Override
                public void onClick(DialogInterface dialog, int which) {
                    dialog.cancel();
                    rationale.resume();// 使用者同意繼續申請。
                }
            })
            .setNegativeButton("我拒絕", new DialogInterface.OnClickListener() {
                @Override
                public void onClick(DialogInterface dialog, int which) {
                    dialog.cancel();
                    rationale.cancel(); // 使用者拒絕申請。
                }
        }).show();
    }
};複製程式碼

這麼做的好處請看上面shouldShowRequestPermissionRationale()方法的介紹。

基本上就到這裡啦,大家有疑問可以關注我的微信,微信公眾號搜尋:嚴振杰,即可關注我。

相關文章