Android 8.0 執行時許可權策略變化和適配方案

嚴振杰發表於2017-08-14

Android8.0也就是Android O即將要釋出了,有很多新特性,目前我們可以通過AndroidStudio3.0 Canary版本下載Android O最新的系統映像的Developer Preview 4版本,Developer Preview 4是Android O正式版推出前的最後一個預覽版本,所以它是Android O的候選版本,我們可以使用它來完成開發和測試,讓我們的應用平穩過度到Android O。

後期會計劃出一篇Android O行為變化和相容方案的文章,本篇文章主要講Android O行為變化的其中一點——系統執行時許可權的策略變化和適配方案

Android系統的執行時許可權是從Android 6.0(Android M)開始加入的,如果你還不知道Android執行時許可權,你可以看我在掘金的另一篇文章Android 6.0 執行時許可權管理最佳實踐
juejin.im/post/57d5de…

針對執行時許可權管理,有很多開源的管理庫,去年這個時候本人也開源了一個執行許可權管理方案,它最大程度上相容了國產機,當然也相容了Android 8.0:
github.com/yanzhenjie/…

在正式開始之前,先糾正一個問題,在網上看到有專案可以做到自定義申請授權的系統Dialog,首先要糾正就目前來看是絕對不行的,最多在呼叫申請的程式碼之前彈一個自己的Dialog提示使用者要申請授權了。我快速拜讀了下那個專案原始碼,果然如我想象的一樣,在繞了一個圈子後最終還是呼叫了系統申請授權的程式碼。


Android O的執行時許可權策略變化

如果你喜歡看Google官網的文章,你可以看這裡:
developer.android.com/preview/beh…

在 Android O 之前,如果應用在執行時請求許可權並且被授予該許可權,系統會錯誤地將屬於同一許可權組並且在清單中註冊的其他許可權也一起授予應用。

對於針對Android O的應用,此行為已被糾正。系統只會授予應用明確請求的許可權。然而一旦使用者為應用授予某個許可權,則所有後續對該許可權組中許可權的請求都將被自動批准。

例如,假設某個應用在其清單中列出READ_EXTERNAL_STORAGEWRITE_EXTERNAL_STORAGE。應用請求READ_EXTERNAL_STORAGE,並且使用者授予了該許可權,如果該應用針對的是API級別24或更低階別,系統還會同時授予WRITE_EXTERNAL_STORAGE,因為該許可權也屬於STORAGE許可權組並且也在清單中註冊過。如果該應用針對的是Android O,則系統此時僅會授予READ_EXTERNAL_STORAGE,不過在該應用以後申請WRITE_EXTERNAL_STORAGE許可權時,系統會立即授予該許可權,而不會提示使用者。

下面我們還是以READ_EXTERNAL_STORAGEWRITE_EXTERNAL_STORAGE為例來具體分析一下,這對我們現有的程式碼有什麼影響。

正式開始之前,我們先約定兩個方法:

/**
 * 拿到沒有被授權的許可權。
 */
getDeinedPermission(String... permissions);
/**
 * 請求幾個許可權。
 */
requestPermission(String... deinedPermissions);複製程式碼

許可權的常量在Manifest.permission類中,而READ_EXTERNAL_STORAGE許可權是在API 16之後才新增的,所以在在Android M出來後為了適配更低版本的系統,我們一般是這樣申請許可權的(虛擬碼):

// 需要申請的許可權。
String[] permissions = {
    Manifest.permission.WRITE_EXTERNAL_STORAGE,
    Manifest.permission.READ_SMS,
    ...
};

String[] deniedPermissions = getDeinedPermission(permissions);

if(deniedPermissions.length <= 0) {
    // TODO do something...
} else {
    requestPermission(deniedPermissions, callback);
}複製程式碼

邏輯非常簡單清晰,其中的callback是申請許可權的回撥,這裡我們申請了WRITE_EXTERNAL_STORAGE許可權,在Android O之前,我們同時會得到READ_EXTERNAL_STORAGE許可權,我們在其它地方涉及到讀取儲存卡的操作時只需要判斷有WRITE_EXTERNAL_STORAGE許可權就去讀取了。

霸特,此時應用如果安裝在Android O的系統中我們會發現,判斷了有WRITE_EXTERNAL_STORAGE許可權後去讀取儲存卡內容時應用崩潰了,原因就是我們沒有申請READ_EXTERNAL_STORAGE許可權。

對Android O執行時許可權策略變化的應對方案

針對Android O的執行時許可權策略的特點,為了適配各個版本的系統,我們的程式碼會變成如下方式(虛擬碼):

// 需要申請的許可權。
String[] permissions = {
    Manifest.permission.WRITE_EXTERNAL_STORAGE,
    Manifest.permission.READ_EXTERNAL_STORAGE,
    Manifest.permission.READ_SMS,
    ...
};

String[] deniedPermissions = getDeinedPermission(permissions);

if(deniedPermissions.length <= 0) {
    // TODO do something...
} else {
    requestPermission(deniedPermissions, callback);
}複製程式碼

但是這樣會存在兩個問題,一是有的許可權組許可權比較多,開發者難易全部記住;二是READ_EXTERNAL_STORAGE這個許可權常量是在API 16時才被新增到SDK中,類似這樣的許可權常量還有好幾個,有的甚至在Android M時才被新增到SDK中。如果我們強制寫了,當APP執行在低版本的系統中時,還是會崩潰。有人就說了,我們在申請之前判斷系統版本不就好啦?當然,如果你不嫌麻煩,這是完全可以的。

升級方案

因此我們總結出一個更優的方案,歸根結底就是申請許可權時要申請許可權組,而不是單一的某個許可權。所以我們按照系統許可權組分類,把一個組的常量放到一個陣列中,並根據系統版本為這個陣列賦值,於是乎產生了這樣一個類:

public final class Permission {

    public static final String[] CALENDAR;   // 讀寫日曆。
    public static final String[] CAMERA;     // 相機。
    public static final String[] CONTACTS;   // 讀寫聯絡人。
    public static final String[] LOCATION;   // 讀位置資訊。
    public static final String[] MICROPHONE; // 使用麥克風。
    public static final String[] PHONE;      // 讀電話狀態、打電話、讀寫電話記錄。
    public static final String[] SENSORS;    // 感測器。
    public static final String[] SMS;        // 讀寫簡訊、收發簡訊。
    public static final String[] STORAGE;    // 讀寫儲存卡。

    static {
        if (Build.VERSION.SDK_INT < Build.VERSION_CODES.M) {
            CALENDAR = new String[]{};
            CAMERA = new String[]{};
            CONTACTS = new String[]{};
            LOCATION = new String[]{};
            MICROPHONE = new String[]{};
            PHONE = new String[]{};
            SENSORS = new String[]{};
            SMS = new String[]{};
            STORAGE = new String[]{};
        } else {
            CALENDAR = new String[]{
                    Manifest.permission.READ_CALENDAR,
                    Manifest.permission.WRITE_CALENDAR};

            CAMERA = new String[]{
                    Manifest.permission.CAMERA};

            CONTACTS = new String[]{
                    Manifest.permission.READ_CONTACTS,
                    Manifest.permission.WRITE_CONTACTS,
                    Manifest.permission.GET_ACCOUNTS};

            LOCATION = new String[]{
                    Manifest.permission.ACCESS_FINE_LOCATION,
                    Manifest.permission.ACCESS_COARSE_LOCATION};

            MICROPHONE = new String[]{
                    Manifest.permission.RECORD_AUDIO};

            PHONE = new String[]{
                    Manifest.permission.READ_PHONE_STATE,
                    Manifest.permission.CALL_PHONE,
                    Manifest.permission.READ_CALL_LOG,
                    Manifest.permission.WRITE_CALL_LOG,
                    Manifest.permission.USE_SIP,
                    Manifest.permission.PROCESS_OUTGOING_CALLS};

            SENSORS = new String[]{
                    Manifest.permission.BODY_SENSORS};

            SMS = new String[]{
                    Manifest.permission.SEND_SMS,
                    Manifest.permission.RECEIVE_SMS,
                    Manifest.permission.READ_SMS,
                    Manifest.permission.RECEIVE_WAP_PUSH,
                    Manifest.permission.RECEIVE_MMS};

            STORAGE = new String[]{
                    Manifest.permission.READ_EXTERNAL_STORAGE,
                    Manifest.permission.WRITE_EXTERNAL_STORAGE};
        }
    }

}複製程式碼

在Android M以前使用某許可權是不需要使用者授權的,只要在Manifest中註冊即可,在Android M之後需要註冊並申請使用者授權,所以我們根據系統版本在Android M以前用一個空陣列作為許可權組,在Android M以後用真實陣列許可權。

因為要傳入多個許可權組,所以我們約定的兩個方法就不夠用了,所以我們加兩個方法:

/**
 * 拿到沒有被授權的許可權。
 */
String[] getDeinedPermission(String... permissions);
/**
 * 請求幾個許可權。
 */
void requestPermission(String... deinedPermissions);
/**
 * 拿到沒有被授權的許可權。
 */
String[] getDeinedPermission(String[]... permissions);
/**
 * 請求幾個許可權。
 */
void requestPermission(String[]... deinedPermissions);複製程式碼

於是我們申請許可權的程式碼就簡化成這樣了:

// 這方法裡面判斷版本,返回空陣列或者沒有許可權的陣列。
String[] deniedPermissions = getDeinedPermission(Permission.STORAGE, Permission.SMS);

if(deniedPermissions.length <= 0) {
    // TODO do something...
} else {
    requestPermission(deniedPermissions, callback);
}複製程式碼

當然這不是最簡化的,但是已經足以相容到Android O的許可權策略的變化了。

如果是AndPermission如何做到最簡

這裡只是介紹下AndPermisison也相容了Android O的許可權變化,如果你覺得這個專案不適合你,你可以自行封裝一個,我比較鼓勵開發者自己動手,下面是開源地址:
github.com/yanzhenjie/…

它的一些簡單的特點:

  1. 鏈式呼叫,一句話申請許可權,省去複雜的邏輯判斷。
  2. 支援註解回撥結果、支援Listener回撥結果。
  3. 拒絕一次某許可權後,再次申請該許可權時可使用Rationale向使用者說明申請該許可權的目的,在使用者同意後再繼續申請,避免使用者勾選不再提示而導致不能再次申請該許可權。
  4. 就算使用者拒絕許可權並勾選不再提示,可使用SettingDialog提示使用者去設定中授權。
  5. RationaleDialog和SettingDialog允許開發者自定義。
  6. AndPermission自帶預設對話方塊除可自定義外,也支援國際化。
  7. 支援在任何地方申請許可權,不僅限於Activity和Fragment等。
  8. 支援申請許可權組、相容Android8.0。

申請多個許可權組示例:

AndPermission.with(this)
    .permission(Permission.CAMERA, Permission.SMS) // 多個許可權組。
    .callback(new PermissionListener() {
        @Override
        public void onSucceed(int i, @NonNull List<String> list) {
            // TODO do something...
        }

        @Override
        public void onFailed(int i, @NonNull List<String> list) {
            // TODO 使用者沒有同意授權,一般彈出Dialog讓使用者去Setting中授權。
        }
    })
    .start();複製程式碼

申請單個或者某幾個許可權示例,因為Android O的出現,現在不鼓勵這樣使用了,但是在Android O正式釋出前沒有問題:

AndPermission.with(this)
    .permission(
        // 多個不同許可權組許可權,現在不鼓勵這樣使用了,但是在Android O正式釋出前沒有問題。
        Manifest.permission.WRITE_EXTERNAL_STORAGE,
        Manifest.permission.READ_SMS
    ) 
    .callback(new PermissionListener() {
        @Override
        public void onSucceed(int i, @NonNull List<String> list) {
            // TODO do something...
        }

        @Override
        public void onFailed(int i, @NonNull List<String> list) {
            // TODO 使用者沒有同意授權,一般彈出Dialog讓使用者去Setting中授權。
        }
    })
    .start();複製程式碼

關於Android O的執行時許可權策略變化和應對方案的介紹到這裡就結束了,如果還不理解的可以在文章下方留言。

相關文章