國產 Android 許可權申請最佳適配方案 —— permissions4m

揪克發表於2017-09-11

* 本篇文章已授權微信公眾號 guolin_blog (郭霖)獨家釋出

前言


permissions4m 最初的設計是僅僅做成一個編譯器註解框架,在1.0.0版本時,它純粹地實現了原生 Android 請求流程,關於它的設計思路可以檢視這篇如何打造一個 Android 編譯時註解框架。但是當投入筆者自己專案中使用的時候,筆者發現國產手機有許多適配缺陷,例如:

1.ActivityCompat.shouldShowRequestPermissionRationale(Activity, String) 無法彈出許可權申請對話方塊
2. 明明使用者點選拒絕授權,卻回撥的是許可權申請成功方法
3.只能有一次許可權是否授予選擇,拒絕後就無法再有提示

相信做過許可權適配的小夥伴們都知道適配國產 Android 機的許可權會有多少坑,而國內也並沒有任何許可權申請框架解決這些問題,於是筆者在 1.0.2 版本中增強了許可權申請功能的適配,現在對於這三個問題 permissions4m 都有良好的解決:

1.許可權申請必定彈出對話方塊
2.拒絕授權時回撥的就是授權失敗方法,接受授權時回撥的就是授權成功方法,讓它一定回撥正確的方法
3.當系統許可權申請對話方塊不再彈出時,函式可返回一個 Intent,跳轉到系統設定頁面或者手機管家介面

情景再現

Boss: "mmp,為什麼展示聯絡人這塊在小米手機顯示不出來?"
Programmer: "boss,其他手機都沒問題,我這塊做了許可權申請的,但是小米就是不彈出許可權申請對話方塊,與此同時小米預設授權失敗,所以不能讀取通訊錄。"
Boss: "mmp,那這塊呢,明明說了讀取日曆許可權成功了,為什麼還是沒讀取到?"
Programmer: "boss,其他手機都沒問題,我這塊做了許可權申請的,但是小米就是不彈出許可權申請對話方塊,與此同時小米預設授權成功,但是實際上是授權失敗的。"
Boss: "mmp,那這塊呢,明明我拒絕授予許可權,為什麼你提示我授權成功?"
Programmer: "boss,其他手機都沒問題,我這塊做了許可權申請的,小米彈出許可權申請對話方塊,與此同時你點了拒絕,但是小米做了手腳,實際上呼叫了授權成功的方法。"
Boss: "你有個毛用?測試機我都給你買好了,還這麼菜,收拾收拾滾蛋吧。"
Programmer: "f**k 小米!"

原生 Android 請求方式在小米等國內機型上適配的情形,相信有部分讀者已經有過經歷,這裡就不做原生測試了,拿出國內一個比較有名的許可權申請框架我們來看看:

小米申請地理位置:

小米申請地理位置
小米申請地理位置

小米申請聯絡人:

小米申請聯絡人
小米申請聯絡人

小米申請手機狀態:

小米申請手機狀態
小米申請手機狀態

其實不僅僅是小米,國內其它手機也會有一樣的問題,筆者再做了一份 oppo a57 的截圖:

這裡寫圖片描述
這裡寫圖片描述

可以看到,在申請過程中並沒有任何彈窗彈出,並且提示授權成功,而實際上我們到許可權管理介面可以看到並未得到許可權。

下圖是使用 permissions4m 的效果:

permissions4m 申請地理位置(小米):

permissions4m申請地理位置
permissions4m申請地理位置

permissions4m 申請聯絡人(小米):

這裡寫圖片描述
這裡寫圖片描述

permissions4m 申請手機狀態(小米):

這裡寫圖片描述
這裡寫圖片描述

permissions4m 申請簡訊、日曆(OPPO A57)

這裡寫圖片描述
這裡寫圖片描述

我們可以看到彈出了許可權申請對話方塊,而且授予許可權的情況下確實獲得了許可權。

生活中,無論是作為開發者還是普通使用者,應該都有接觸到過 5.0+ 的小米/魅族手機,使用過這些手機的讀者們應該還有些許印象——部分國產手機早在 android 6.0 之前,也就是在 google 推出動態許可權之前就有了許可權申請,而國產的 5.0 許可權申請使用 6.0 的許可權申請程式碼是行不通的,理由很簡單——在5.0的系統原始碼裡沒有6.0許可權申請的原始碼,這個問題在 permissions4m 2.0.0 版本中已經迎刃而解了,這意味著從 2.0.0 版本開始, permissions4m 開始支援國產手機 5.0 許可權申請了。

permissions4m 簡介

簡介中只是節選了部分內容,更詳細完整的請移至專案:github.com/jokermonn/p…

注:截止筆者釋出部落格為止,permissions4m 最新版本為 2.0.0

引入依賴

Gradle 依賴

project 中的 build.gradle

buildscript {
  // ...
}

allprojects {
  repositories {
    // 請新增如下一行
    maven { url 'https://jitpack.io' }
  }
}複製程式碼

app 中的 build.gradle

dependencies {
  compile 'com.github.jokermonn:permissions4m:2.0.0-lib'
  annotationProcessor 'com.github.jokermonn:permissions4m:2.0.0-processor'
}複製程式碼

註解回撥

在需要許可權申請的地方呼叫

Permissions4M.get(MainActivity.this)
            // 是否強制彈出許可權申請對話方塊,建議為 true
            .requestForce(true)
            // 許可權
            .requestPermission(Manifest.permission.RECORD_AUDIO)
            // 許可權碼
            .requestCode(AUDIO_CODE)
            // 如果需要使用 @PermissionNonRationale 註解的話,建議新增如下一行
            // 返回的 intent 是跳轉至**系統設定頁面**
            // .requestPageType(Permissions4M.PageType.MANAGER_PAGE)
            // 返回的 intent 是跳轉至**手機管家頁面**
            // .requestPageType(Permissions4M.PageType.ANDROID_SETTING_PAGE)
            .request();複製程式碼

如:

button.setOnClickListener(new View.OnClickListener() {
    @Override
    public void onClick(View v) {
        Permissions4M.get(MainActivity.this)
                .requestForce(true)
                .requestPageType(Permissions4M.PageType.MANAGER_PAGE)
                .requestPermission(Manifest.permission.RECORD_AUDIO)
                .requestCode(AUDIO_CODE)
                .request();
    }
});複製程式碼

然後將會回撥相應的 @PermissionsGranted@PermissionsDenied@PermissionsRationale/PermissionsCustomRationale@PermissionsNonRationale 所修飾的方法

@PermissionsGranted

授權成功時回撥,註解中需要傳入引數,分為兩種情況:

  • 單引數:@PermissionsGranted(LOCATION_CODE),被修飾函式無需傳入引數,例:

      @PermissionsGranted(LOCATION_CODE)
      public void granted() {
          ToastUtil.show("地理位置授權成功");
      }複製程式碼
  • 多引數:@PermissionsGranted({LOCATION_CODE, SENSORS_CODE, CALENDAR_CODE}),被修飾函式需要傳入一個 int 引數,例:

      @PermissionsGranted({LOCATION_CODE, SENSORS_CODE, CALENDAR_CODE})
      public void granted(int code) {
          switch (code) {
              case LOCATION_CODE:
                  ToastUtil.show("地理位置許可權授權成功");
                  break;
              case SENSORS_CODE:
                  ToastUtil.show("感測器許可權授權成功");
                  break;
              case CALENDAR_CODE:
                  ToastUtil.show("讀取日曆許可權授權成功");
                  break;
              default:
                  break;
          }
      }複製程式碼

@PermissionsDenied

授權失敗時回撥,註解中需要傳入引數,分為兩種情況:

  • 單引數:@PermissionsDenied(LOCATION_CODE),被修飾函式無需傳入引數,例:

    @PermissionsDenied(LOCATION_CODE)

      public void denied() {
      }複製程式碼
  • 多引數:@PermissionsDenied({LOCATION_CODE, SENSORS_CODE, CALENDAR_CODE}),被修飾函式需要傳入一個 int 引數,例:

      @PermissionsDenied({LOCATION_CODE, SENSORS_CODE, CALENDAR_CODE})
      public void denied(int code) {
          switch (code) {
              case LOCATION_CODE:
                  ToastUtil.show("地理位置許可權授權失敗");
                  break;
              case SENSORS_CODE:
                  ToastUtil.show("感測器許可權授權失敗");
                  break;
              case CALENDAR_CODE:
                  ToastUtil.show("讀取日曆許可權授權失敗");
                  break;
              default:
                  break;
          }
      }複製程式碼

@PermissionsRationale

二次授權時回撥,用於解釋為何需要此許可權,註解中需要傳入引數,分為兩種情況:

  • 單引數:@PermissionsRationale(LOCATION_CODE),被修飾函式無需傳入引數,例:

      @PermissionsRationale(LOCATION_CODE)
      public void rationale() {
          ToastUtil.show("請開啟讀取地理位置許可權");
      }複製程式碼
  • 多引數:@PermissionsRationale({LOCATION_CODE, SENSORS_CODE, CALENDAR_CODE}),被修飾函式需要傳入一個 int 引數,例:

      @PermissionsRationale({LOCATION_CODE, SENSORS_CODE, CALENDAR_CODE})
      public void rationale(int code) {
          switch (code) {
              case LOCATION_CODE:
                  ToastUtil.show("請開啟地理位置許可權授權");
                  break;
              case SENSORS_CODE:
                  ToastUtil.show("請開啟感測器許可權授權");
                  break;
              case CALENDAR_CODE:
                  ToastUtil.show("請開啟讀取日曆許可權授權");
                  break;
              default:
                  break;
          }複製程式碼

注:系統彈出許可權申請 dialog 與 toast 提示是非同步操作,所以如果存在希望自行彈出一個 dialog 後(或其他同步需求)再彈出系統對話方塊,那麼請使用 @PermissionsCustomRationale

@PermissionsCustomRationale

二次授權時回撥,用於解釋為何需要此許可權,註解中需要傳入引數,分為兩種情況:

  • 單引數:@PermissionsCustomRationale(LOCATION_CODE),被修飾函式無需傳入引數,例:
    @PermissionsCustomRationale(LOCATION_CODE)
    public void cusRationale() {
        new AlertDialog.Builder(this)
                        .setMessage("讀取地理位置許可權申請:\n我們需要您開啟讀取地理位置許可權(in activity with annotation)")
                        .setPositiveButton("確定", new DialogInterface.OnClickListener() {
                            @Override
                            public void onClick(DialogInterface dialog, int which) {
                                Permissions4M.get(MainActivity.this)
                                        // 注意新增 .requestOnRationale()
                                        .requestOnRationale()
                                        .requestPermission(Manifest.permission.READ_SMS)
                                        .requestCode(SMS_CODE)
                                        .request();
                            }
                        })
                        .show();
    }複製程式碼
  • 多引數:@PermissionsCustomRationale({LOCATION_CODE, SENSORS_CODE, CALENDAR_CODE}),被修飾函式需要傳入一個 int 引數,例:
    @PermissionsCustomRationale({SMS_CODE, AUDIO_CODE})
    public void cusRationale(int code) {
        switch (code) {
            case SMS_CODE:
                new AlertDialog.Builder(this)
                        .setMessage("簡訊許可權申請:\n我們需要您開啟簡訊許可權(in activity with annotation)")
                        .setPositiveButton("確定", new DialogInterface.OnClickListener() {
                            @Override
                            public void onClick(DialogInterface dialog, int which) {
                                Permissions4M.get(MainActivity.this)
                                        // 注意新增 .requestOnRationale()
                                        .requestOnRationale()
                                        .requestPermission(Manifest.permission.READ_SMS)
                                        .requestCode(SMS_CODE)
                                        .request();
                            }
                        })
                        .show();
                break;
            case AUDIO_CODE:
                new AlertDialog.Builder(this)
                        .setMessage("錄音許可權申請:\n我們需要您開啟錄音許可權(in activity with annotation)")
                        .setPositiveButton("確定", new DialogInterface.OnClickListener() {
                            @Override
                            public void onClick(DialogInterface dialog, int which) {
                                Permissions4M.get(MainActivity.this)
                                        // 注意新增 .requestOnRationale()
                                        .requestOnRationale()
                                        .requestPermission(Manifest.permission.RECORD_AUDIO)
                                        .requestCode(AUDIO_CODE)
                                        .request();
                            }
                        })
                        .show();
                break;
            default:
                break;
        }複製程式碼

注:除上述以外的 dialog,開發者可以自定義其他展示效果,呼叫許可權申請時請使用,否則會陷入無限呼叫自定義 Rationale 迴圈中:

Permissions4M.get(MainActivity.this)
        // 務必新增下列一行
      .requestOnRationale()
      .requestPermission(Manifest.permission.RECORD_AUDIO)
      .requestCode(AUDIO_CODE)
      .request();複製程式碼

@PermissionsNonRationale

當使用者點選拒絕許可權不再提示國產畸形許可權適配擴充套件)情況下呼叫,此時意味著無論是 @PermissionsCustomRationale 或者 @PermissionsRationale 都不會被呼叫,無法給予使用者提示,此時該註解修飾的函式被呼叫,註解中需要傳入引數,分為兩種情況:

  • 單引數:@PermissionsNonRationale(LOCATION_CODE),被修飾函式只需傳入 Intent 引數,例:

      @PermissionsNonRationale({LOCATION_CODE})
      public void nonRationale(Intent intent) {
          startActivity(intent);
      }複製程式碼
  • 多引數:@PermissionsNonRationale(AUDIO_CODE, CALL_LOG_CODE),被修飾函式需傳入 int 引數和 Intent 引數,例:

      @PermissionsNonRationale({AUDIO_CODE, CALL_LOG_CODE})
      public void nonRationale(int code, final Intent intent) {
          switch (code) {
              case AUDIO_CODE:
                  new AlertDialog.Builder(MainActivity.this)
                          .setMessage("讀取錄音許可權申請:\n我們需要您開啟讀取錄音許可權")
                          .setPositiveButton("確定", new DialogInterface.OnClickListener() {
                              @Override
                              public void onClick(DialogInterface dialog, int which) {
                                  startActivity(intent);
                              }
                          })
                          .setNegativeButton("取消", new DialogInterface.OnClickListener() {
                              @Override
                              public void onClick(DialogInterface dialog, int which) {
                                  dialog.cancel();
                              }
                          })
                          .show();
                  break;
              case CALL_LOG_CODE:
                  new AlertDialog.Builder(MainActivity.this)
                          .setMessage("讀取通話記錄許可權申請:\n我們需要您開啟讀取通話記錄許可權")
                          .setPositiveButton("確定", new DialogInterface.OnClickListener() {
                              @Override
                              public void onClick(DialogInterface dialog, int which) {
                                  startActivity(intent);
                              }
                          })
                          .setNegativeButton("取消", new DialogInterface.OnClickListener() {
                              @Override
                              public void onClick(DialogInterface dialog, int which) {
                                  dialog.cancel();
                              }
                          })
                          .show();
                  break;
              default:
                  break;
          }
      }複製程式碼

Intent 型別為兩種,一種是跳轉至系統設定頁面,另一種是跳至手機管家頁面,而具體的設定方法請參考 註解回撥.requestPageType(int) 設定方法。

Listener 回撥

例:

Permissions4M.get(MainActivity.this)
    // 是否強制彈出許可權申請對話方塊
    .requestForce(true)
    // 許可權
    .requestPermission(Manifest.permission.READ_CONTACTS)
    // 許可權碼
    .requestCode(READ_CONTACTS_CODE)
    // 許可權請求結果
    .requestCallback(new Wrapper.PermissionRequestListener() {
           @Override
        public void permissionGranted() {
            ToastUtil.show("讀取通訊錄許可權成功 in activity with listener");
        }

        @Override
        public void permissionDenied() {
            ToastUtil.show("讀取通訊錄權失敗 in activity with listener");
        }

        @Override
        public void permissionRationale() {
            ToastUtil.show("請開啟讀取通訊錄許可權 in activity with listener");
        }
    })
    // 許可權完全被禁時回撥函式中返回 intent 型別(手機管家介面)
    .requestPageType(Permissions4M.PageType.MANAGER_PAGE)
    // 許可權完全被禁時回撥函式中返回 intent 型別(系統設定介面)
    //.requestPageType(Permissions4M.PageType.ANDROID_SETTING_PAGE)
    // 許可權完全被禁時回撥,介面函式中的引數 Intent 是由上一行決定的
    .requestPage(new Wrapper.PermissionPageListener() {
        @Override
        public void pageIntent(final Intent intent) {
            new AlertDialog.Builder(MainActivity.this)
            .setMessage("讀取通訊錄許可權申請:\n我們需要您開啟讀取通訊錄許可權(in activity with listener)")
            .setPositiveButton("確定", new DialogInterface.OnClickListener() {
                @Override
                public void onClick(DialogInterface dialog, int which) {
                    startActivity(intent);
                }
            })
            .setNegativeButton("取消", new DialogInterface.OnClickListener() {
                @Override
                public void onClick(DialogInterface dialog, int which) {
                    dialog.cancel();
                }
            })
            .show();
        }
    })
    .request();複製程式碼

同步申請

  • 使用 @PermissionsRequestSync 修飾 Activity 或 Fragment

  • 傳入兩組引數

    • value 陣列:請求碼
    • permission 陣列:請求許可權
  • 使用 Permissions4M.get(MainActivity.this).requestSync(); 進行同步許可權申請

例:參考 sample 中 MainActivity 上的設定 ——

@PermissionsRequestSync(
    permission = {Manifest.permission.BODY_SENSORS, 
                    Manifest.permission.ACCESS_FINE_LOCATION, 
                        Manifest.permission.READ_CALENDAR},
    value = {SENSORS_CODE, 
                LOCATION_CODE, 
                    CALENDAR_CODE})
public class MainActivity extends AppCompatActivity複製程式碼

後記


permissions4m 的目標是適配儘可能多的國產機型,包括但不限於小米、魅族、OPPO、VIVO、華為等機型,不僅是6.0+版本,後期也會支援到小米、魅族等低版本也有許可權申請的手機。但是由於筆者個人能力有限,所以希望儘可能多的開發者參與到此專案的開發當中,更多詳情請移至 permissions4m

求職


筆者目前剛剛大四,想找一份關於 android 的實習,上海/杭州或其他地區都可,如果貴司正在招實習,望告知 jokerzoc.cn@gmail.com,謝謝。

相關文章