天天看點

Android 6.0 運作時權限管理最佳實踐AndPermission特性關于運作時權限哪些權限需要動态申請關于運作時權限的一些建議幾個重要的方法與常量解釋AndPermission使用介紹混淆國産手機适配方案

版權聲明:轉載必須注明本文轉自嚴振傑的部落格: http://blog.yanzhenjie.com

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

由于項目一直在更新,更多新内容請直接看AndPermission開源首頁:

https://github.com/yanzhenjie/AndPermission

AndPermission能解決大部分國産機遇到的權限問題,請參考:國産手機權限适配方案

AndPermission特性

  1. 鍊式調用,一句話申請權限,為你省去複雜的邏輯判斷。
  2. 支援注解回調結果、支援Listener回調結果。
  3. 拒絕一次某權限後,再次申請該權限時可使用

    Rationale

    向使用者說明申請該權限的目的,在使用者同意後再繼續申請,避免使用者勾選不再提示而導緻不能再次申請該權限。
  4. 就算使用者拒絕權限并勾選不再提示,可使用

    SettingDialog

    提示使用者去設定中授權。
  5. RationaleDialog

    SettingDialog

    允許開發者自定義。
  6. AndPermission

    自帶預設對話框除可自定義外,也支援國際化。
  7. 支援在任何地方申請權限,不僅限于

    Activity

    Fragment

    等。

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

  • System Permissions
  • Requesting Permissions at Run Time
  • Permissions Best Practices

關于運作時權限

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

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

特别注意:這個對話框不是開發者調用某個權限的功能時由系統自動彈出,而是需要開發者手動調用,如果你直接調用而沒有去申請權限的話,将會導緻App崩潰。

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

哪些權限需要動态申請

新的權限政策講權限分為兩類,第一類是不涉及使用者隐私的,隻需要在Manifest中聲明即可,比如網絡、藍牙、NFC等;第二類是涉及到使用者隐私資訊的,需要使用者授權後才可使用,比如SD卡讀寫、聯系人、短信讀寫等。

不需要運作時申請的權限

此類權限都是正常保護的權限,隻需要在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

需要運作時申請的權限

所有危險的Android系統權限屬于權限組,如果APP運作在

Android 6.0 (API level 23)

或者更進階别的裝置中,而且

targetSdkVersion>=23

時,系統将會自動采用動态權限管理政策,如果你在涉及到特殊權限操作時沒有申請權限權限而直接調用了相關代碼,你的App可能就崩潰了,綜上所述你需要注意:

  • 此類權限也必須在Manifest中申明,否則申請時不提示使用者,直接回調開發者權限被拒絕。
  • 同一個權限組的任何一個權限被授權了,這個權限組的其他權限也自動被授權。例如一旦

    WRITE_CONTACTS

    被授權了,App也有

    READ_CONTACTS

    GET_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指令同樣可以授權/撤銷某個權限:

關于運作時權限的一些建議

  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-v4

ActivityCompat

中都有,建議使用相容庫中的方法。最後一個方法是使用者授權或者拒絕某個權限組時系統會回調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,權限數組,你需要申請的的權限的數組。
  2. 由于該方法是異步的,是以無傳回值,當使用者處理完授權操作時,會回調Activity或者Fragment的

    onRequestPermissionsResult()

    方法。

對于Activity我們直接調用

requestPermissions(int, String[])

即可,不過這個方法是在api leve 23以上,是以我們為了适配可以是使用相容包提供的方法:

ActivityCompat.requestPermissions(activity, new String[]{Manifest.permission.READ_CONTACTS}, MMM);
           

對于support包的

Fragment

就可以直接調用

requestPermissions(int, String[])

,對于app包的

Fragment

就需要做版本判斷了,這樣就顯得比較麻煩。

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 > 
                && grantResults[] == 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 > 
                && grantResults[] == PackageManager.PERMISSION_GRANTED) {
                // 權限被使用者同意,可以去放肆了。
            } else {
                // 權限被使用者拒絕了,洗洗睡吧。
            }
            return;
        }
    }
}
           

總結

從上面來看,判斷很多,邏輯也很多,這樣就加重了我們開發的負擔,加上很多人回報說國産手機有各種各樣的bug,這樣相容起來就更加麻煩了,那麼下面我就為大家介紹一個開源内褲來解決這一系列問題。

AndPermission

這個開源庫名叫AndPermission:https://github.com/yanzhenjie/AndPermission,經過我的實踐是完全解決了上述問題,推薦大家使用。

  • Gradle
  • Maven
<dependency>
  <groupId>com.yanzhenjie</groupId>
  <artifactId>permission</artifactId>
  <version>1.0.5</version>
  <type>pom</type>
</dependency>
           
  • Eclipse 請放棄治療

使用介紹

我建議看官去Github下載下傳

Demo

并閱讀本文會幫助你了解。

申請權限

// 在Activity:
AndPermission.with(activity)
    .requestCode()
    .permission(Manifest.permission.WRITE_CONTACTS)
    .rationale(...)
    .callback(...)
    .start();

// 在Fragment:
AndPermission.with(fragment)
    .requestCode()
    .permission(
        // 多個權限,以數組的形式傳入。
        Manifest.permission.WRITE_CONTACTS,
        Manifest.permission.READ_SMS
    )
    .rationale(...)
    .callback(...)
    .start();

// 在其它任何地方:
AndPermission.with(context)
    .requestCode()
    .permission(
        Manifest.permission.WRITE_CONTACTS,
        Manifest.permission.READ_SMS
    )
    .rationale(...)
    .callback(...)
    .start();
           

接受回調結果

接受回調結果目前有兩種方式:一、

Listener

方式,二、注解方式。

方式一:Listener方式回調

callback()

方法傳入

PermissionListener

即可,授權成功或者失敗至少會回調其中一個方法。

AndPermission.with(context)
    ...
    .requestCode()
    .callback(listener)
    .start();

private PermissionListener listener = new PermissionListener() {
    @Override
    public void onSucceed(int requestCode, List<String> grantedPermissions) {
        // 權限申請成功回調。

        // 這裡的requestCode就是申請時設定的requestCode。
        // 和onActivityResult()的requestCode一樣,用來區分多個不同的請求。
        if(requestCode == ) {
            // TODO ...
        }
    }

    @Override
    public void onFailed(int requestCode, List<String> deniedPermissions) {
        // 權限申請失敗回調。
        if(requestCode == ) {
            // TODO ...
        }
    }
};
           

方式二:注解方式回調

callback()

方法傳入你的回調方法所在執行個體的對象即可。

AndPermission.with(context)
    ...
    .requestCode()
    .callback(this)
    .start();

// 成功回調的方法,用注解即可,這裡的300就是請求時的requestCode。
@PermissionYes()
private void getPermissionYes(List<String> grantedPermissions) {
    // TODO 申請權限成功。
}

@PermissionNo()
private void getPermissionNo(List<String> deniedPermissions) {
    // TODO 申請權限失敗。
}
           

如果你會用了,你就可以大刀闊斧的幹了,部落格中講到的各種複雜邏輯,

AndPermission

自動完成。

Rationale能力

Android

運作時權限有一個特點,在拒絕過一次權限後,再此申請該權限,在申請框會多一個[不再提示]的複選框,當使用者勾選了[不再提示]并拒絕了權限後,下次再申請該權限将直接回調申請失敗。

是以

Rationale

功能是在使用者拒絕一次權限後,再次申請時檢測到已經申請過一次該權限了,允許開發者彈窗說明申請權限的目的,擷取使用者的同意後再申請權限,避免使用者勾選不再提示,導緻不能再次申請權限。

方式一:使用AndPermssion預設MD風格對話框

AndPermission.with(this)
    ...
    .requestCode(...)
    .rationale((requestCode, rationale) ->
        // 此對話框可以自定義,調用rationale.resume()就可以繼續申請。
        AndPermission.rationaleDialog(context, rationale).show()
    )
    .start()
           

方式二:自定義對話框

AndPermission.with(this)
    ...
    .requestCode(...)
    .rationale(rationaleListener)
    .start()

/**
 * Rationale支援,這裡自定義對話框。
 */
private RationaleListener rationaleListener = (requestCode, rationale) -> {
    AlertDialog.newBuilder(this)
        .setTitle("友好提醒")
        .setMessage("你已拒絕過定位權限,沒有定位定位權限無法為你推薦附近的妹子,你看着辦!")
        .setPositiveButton("好,給你", (dialog, which) -> {
            rationale.resume();
        })
        .setNegativeButton("我拒絕", (dialog, which) -> {
            rationale.cancel();
        }).show();
};
           

提示使用者在系統設定中授權

當使用者拒絕權限并勾選了不再提示時,此時再次申請權限時将會直接回調申請失敗,是以

AndPermission

提供了一個供使用者在系統

Setting

中給我們授權的能力。

我們在授權失敗的回調方法中添加如下代碼,以下三種選擇一種即可:

// 是否有不再提示并拒絕的權限。
if (AndPermission.hasAlwaysDeniedPermission(activity, deniedPermissions)) {
    // 第一種:用AndPermission預設的提示語。
    AndPermission.defaultSettingDialog(activity, ).show();

    // 第二種:用自定義的提示語。
    AndPermission.defaultSettingDialog(activity, )
    .setTitle("權限申請失敗")
    .setMessage("您拒絕了我們必要的一些權限,已經沒法愉快的玩耍了,請在設定中授權!")
    .setPositiveButton("好,去設定")
    .show();

    // 第三種:自定義dialog樣式。
    SettingService settingService = AndPermission.defineSettingDialog(activity, );
    ...
    // 你的dialog點選了确定調用:
    settingService.execute();
    // 你的dialog點選了取消調用:
    settingService.cancel();
}
           

如果你是在

Activity/Fragment

中調用的上述代碼,那麼當使用者在系統

Setting

中操作完成後,會回調

Activity/Fragment

中的這個方法:

@Override
protected void onActivityResult(int requestCode, int resultCode, Intent data) {
    switch (requestCode) {
        case : { // 這個400就是你上面傳入的數字。
            // 你可以在這裡檢查你需要的權限是否被允許,并做相應的操作。
            break;
        }
    }
}
           

混淆

  1. 如果使用

    Listener

    接受回調結果,不用任何配置。
  2. 使用注解的方式回調結果,在

    proguard

    中添加如下配置:
-keepclassmembers class ** {
    @com.yanzhenjie.permission.PermissionYes <methods>;
}
-keepclassmembers class ** {
    @com.yanzhenjie.permission.PermissionNo <methods>;
}
           

國産手機适配方案

AndPermission是嚴格按照

Android

系統的

運作時權限

設計的,并最大限度上相容了國産手機,目前發現的國産手機bug及解決方案:

  • 部分中國廠商生産手機(例如小米某型号)的

    Rationale

    功能,在第一次拒絕後,第二次申請時不會傳回

    true

    ,并且會回調申請失敗,也就是說在第一次決絕後預設勾選了

    不再提示

    ,是以建議一定使用

    SettingDialog

    :提示使用者在系統設定中授權。
  • 部分中國廠商生産手機(例如小米、華為某型号)在申請權限時,使用者點選确定授權後,還是回調我們申請失敗,這個時候其實我們是擁有權限的,是以我們可以在失敗的方法中使用

    AppOpsManager

    進行權限判斷,

    AndPermission

    已經封裝好了:
if(AndPermission(context, permission1, permission2)) {
    // 執行操作。
}
           
  • 部分中國廠商生産手機(例如vivo、pppo某型号)在使用者允許權限,并且回調了權限授權陳功的方法,但是實際執行代碼時并沒有這個權限,建議開發者在回調成功的方法中也利用

    AppOpsManager

    判斷下:
if(AndPermission(context, permission1, permission2)) {
    // 執行操作。
} else {
    // 提醒使用者手機問題,請使用者去Setting中授權。這裡可以使用AndPermission的SettingDialog。
}
           
  • 部分開發者回報,在某些手機的

    Setting

    中授權後實際,檢查時還是沒有權限,部分執行代碼也是沒有權限,這種手機真的相容不到了,我也覺得沒必要相容了,建議直接放棄這種平台。
最後希望咱中國Android手機廠商早日修複這些問題,祝你們事業越來越成功,産品越做越好。
版權聲明:轉載必須注明本文轉自嚴振傑的部落格: http://blog.yanzhenjie.com