Android6.0權限適配及兼容庫的實現(xiàn)

從6.0 MarshMallow開始,Android支持動態(tài)權限管理,即有些權限需要在使用到的時候動態(tài)申請,根據用戶的選擇需要有不同的處理,具體表現(xiàn)可以看下圖:

權限申請

本文并不關心權限適配的原理,原理可以參考Android權限管理原理
,這里只是針對6.0中的表現(xiàn)做適配,先思考以下幾個問題:

  • 為什么6.0權限需要適配
  • 什么權限需要動態(tài)適配
  • 怎樣動態(tài)適配權限
  • 怎么樣實現(xiàn)第三方庫,簡化代碼及適配流程 權限兼容庫 PermissionCompat
  • 對于國產ROM的影響

為什么6.0需要權限適配

6.0之前Android的權限都是在安裝的時候授予的,6.0之后,為了簡化安裝流程,并且方便用戶控制權限,Android允許在運行的時候動態(tài)控制權限。對于開發(fā)而言就是將targetSdkVersion設置為23,當運行在Android 6.0 +的手機上時,就會調用6.0相關的API,達到動態(tài)控制權限的目的。但是,如果僅僅是將targetSdkVersion設置為23,而在代碼層面沒有針對Android 6.0做適配,就可能在申請系統(tǒng)服務的時候,由于權限不足,引發(fā)崩潰。

  • targetSDKVersion:該屬性用于通知系統(tǒng),您已針對目標版本進行測試,標識App能夠適配的系統(tǒng)版本,有些新的API是只有新的系統(tǒng)才有的。

什么權限需要動態(tài)適配

并非所有的權限都需要動態(tài)申請,Android6.0將權限分為兩種,普通權限跟敏感(危險)權限,普通權限是不需要動態(tài)申請的,但是敏感權限需要動態(tài)申請。

  • 1、普通權限(Normal permissions):不會泄露用戶隱私,同時也不會導致手機安全問題。如網絡請求權限、WIFI狀態(tài)等,這類權限只需要在Manifest列出來,之后,系統(tǒng)會自動賦給APP權限:

    • ACCESS_NETWORK_STATE
    • ACCESS_NOTIFICATION_POLICY
    • ACCESS_WIFI_STATE
    • BLUETOOTH
    • BLUETOOTH_ADMIN
  • 2、敏感權限(Dangerous permissions):與?普通權限對應,可能會影響用戶的隱私,存儲數(shù)據等,比如拍照、存儲、通訊錄、地理GPS等,這類權限需要在Manifest列出來,在需要的的時候,顯示的請求用戶準許。

    • CALENDAR
    • CAMERA
    • CONTACTS
    • LOCATION
    • PHONE
    • SENSORS
    • SMS
    • STORAGE

敏感權限的請求是按照分組進行提醒的,并非僅僅針對一條,比如通訊錄的讀取權限與寫權限,只要一個權限獲到,下次請求權限的時候會自動提供,當然也要請求。否則還是有問題。

  • 3、特殊權限(Special Permissions) --不在本文分析范圍

    There are a couple of permissions that don't behave like normal and dangerous permissions. SYSTEM_ALERT_WINDOW and WRITE_SETTINGS

怎樣動態(tài)適配權限

對于敏感權限的適配有一個原則,那就是實時檢查,因為權限隨時可能被回收,比如用戶可以在設置里面把權限給取消,但是APP并不一定知道,因此每次都需要檢查,一旦沒有,就需要請求,之后,根據返回結果處理后續(xù)邏輯。

實現(xiàn)步驟

  • 1、在Manifest中列出來

    無論普通權限還是敏感權限,都需要在Manifest中列出來,同時也是對6.0之前的版本的一種兼容。

      <manifest xmlns:android="http://schemas.android.com/apk/res/android"
            package="com.snail.labaffinity">
      <uses-permission android:name="android.permission.CAMERA"/>
      <uses-permission android:name="android.permission.CALL_PHONE"/>
    
  • 2、需要時,顯示的請求

在權限沒被授予前提下,系統(tǒng)會顯示授權對話框,讓用戶操作,目前授權對話框不可定制,不過可以在申請之前添加一些解釋,告訴用戶為什么需要該權限,但是Google提醒,不要做過多的解釋,可能會使用戶感到厭煩,用法如下:

    ActivityCompat.requestPermissions(target.getActivity(), permissions, 

· requestCode);

public static void requestPermissions(final @NonNull Activity activity,
        final @NonNull String[] permissions, final int requestCode) {
        if (Build.VERSION.SDK_INT >= 23) {
                   ActivityCompatApi23.requestPermissions(activity, permissions, requestCode);
      } else if (activity instanceof OnRequestPermissionsResultCallback) {
        Handler handler = new Handler(Looper.getMainLooper());
        handler.post(new Runnable() {
            @Override
            public void run() {
                final int[] grantResults = new int[permissions.length];

                PackageManager packageManager = activity.getPackageManager();
                String packageName = activity.getPackageName();

                final int permissionCount = permissions.length;
                for (int i = 0; i < permissionCount; i++) {
                    grantResults[i] = packageManager.checkPermission(
                            permissions[i], packageName);
                }

                ((OnRequestPermissionsResultCallback) activity).onRequestPermissionsResult(
                        requestCode, permissions, grantResults);
            }
        });
    }
}
  • 3、處理授權回調

    • 兼容6.0之前的處理:在這里只需要處理獲得權限即可,因為6.0之前只存在Install權限,一旦安裝,所有權限都是默認授予的,雖然國內ROM對權限管理做了自己的一些定制,但基本都是兼容的。

    • 需要對6.0的授權成功、失敗、永不詢問做處理

         public void onRequestPermissionsResult(int requestCode, @NonNull String[] permissions, @NonNull int[] grantResults) {
          super.onRequestPermissionsResult(requestCode, permissions, grantResults);
          if(this.mOnGrantedListener != null) {
          <!--6.0之前-->
              if(PermissionUtils.getTargetSdkVersion(this) < 23 && !PermissionUtils.hasSelfPermissions(this, permissions)) {
                  this.mOnGrantedListener.onGranted(this, permissions);
                  return;
              }            
              //<!--6.0之后-->  需要根據結果進行驗證
              if(PermissionUtils.verifyPermissions(grantResults)) {
                  this.mOnGrantedListener.onGranted(this, permissions);
              } else if(!PermissionUtils.shouldShowRequestPermissionRationale(this, permissions)) {
                  this.mOnGrantedListener.onNeverAsk(this, permissions);
              } else {
                  this.mOnGrantedListener.onDenied(this, permissions);
              }
          }
        }
      

具體APP中不同的實現(xiàn)方案

  • 1、簡單的封裝回調
  • 2、基于APT,采用注解方式簡化編碼邏輯,自動封封回調

先看一下直接回調的方式

采用最直接的回調

首先在基類Activity或者Fragment中統(tǒng)一設置授權回調監(jiān)聽,這里我們用一個

 public class BasePermissionCompatActivity extends AppCompatActivity {

    private SparseArray<OnGrantedListener<BasePermissionCompatActivity>> mOnGrantedListeners = new SparseArray<>();

    @Override
    public void onRequestPermissionsResult(int requestCode, @NonNull String[] permissions, @NonNull int[] grantResults) {
        super.onRequestPermissionsResult(requestCode, permissions, grantResults);
        OnGrantedListener<BasePermissionCompatActivity> listener = mOnGrantedListeners.get(requestCode);
        if (listener == null)
            return;
        if (PermissionUtils.verifyPermissions(grantResults)) {
            listener.onGranted(this, permissions);
        } else {
            if (PermissionUtils.shouldShowRequestPermissionRationale(this, permissions)) {
                listener.onDenied(this, permissions);
            } else {
                listener.onNeverAsk(this, permissions);
            }
        }
    }
    
    @Override
    protected void onDestroy() {
        super.onDestroy();
        mOnGrantedListeners.clear();
        mOnGrantedListeners = null;
    }

    public void requestPermissions(final @NonNull String[] permissions, OnGrantedListener<BasePermissionCompatActivity> onGrantedListener) {
        int requestCode = getNextRequestCode();
        ActivityCompat.requestPermissions(this, permissions, requestCode);
        mOnGrantedListeners.put(requestCode, onGrantedListener);
    }

    private static int sNextCode;

    private static int getNextRequestCode() {
        return sNextCode++;
    }
}

之后在需要時候的請求,并根據結果處理后續(xù)邏輯即可。

   requestPermissions(activity, P_CAMERA, new OnGrantedListener() {

        // 根據permissions自行處理,可合并,可分開
        @Override
        public void onGranted(SecondActivity target, String[] permissions,int requestCode) {
        }
        @Override
        public void onDenied(SecondActivity target, String[] permissions,int requestCode) {
        }
        @Override
        public void onNeverAsk(SecondActivity target, String[] permissions,int requestCode) {
        }
        @Override
        public void onShowRationale(SecondActivity target, String[] permissions,int requestCode) {
    });

上面的方法比較直接,靈活,不過每次都要自己實現(xiàn)回調監(jiān)聽Listener,接下來看第二種實現(xiàn),基于APT,通過注解的方式,自動添加Listener,這種實現(xiàn)參考了ButterKnife的實現(xiàn)方式。

基于APT與注解,編譯過程中生成代碼,自動添加回調

  • 1、基于APT,定義一系列Annotation,并動態(tài)生成輔助Listener類
  • 2、添加Android支持庫,在基類統(tǒng)一處理回調,
  • 3、添加工具類,連接綁定Listener與Activity(Fragment)

相應的實現(xiàn)分三個庫:

  • 注解庫
  • APT生成支持庫
  • Android支持庫

注解庫:

注解庫

主要用來定義一些回調方法注解、及請求實體的類注解

* ActivityPermission
* FragmentPermission
* OnDenied
* OnGranted
* OnGrantedListener
* OnNeverAsk
* OnShowRationale

APT生成支持庫

主要用來在編譯階段,動態(tài)生Listener類

PermissionProcessor.java

部分參考代碼:

@AutoService(Processor.class)
public class PermissionProcessor extends AbstractProcessor {

    private Elements elementUtils;

    private Set<Class<? extends Annotation>> getSupportedAnnotations() {
        Set<Class<? extends Annotation>> annotations = new LinkedHashSet<>();
        annotations.add(OnDenied.class);
        annotations.add(OnGranted.class);
        annotations.add(OnNeverAsk.class);
        annotations.add(OnShowRationale.class);
        return annotations;
    }

    @Override
    public synchronized void init(ProcessingEnvironment env) {
        super.init(env);
        elementUtils = env.getElementUtils();
    }

    @Override
    public boolean process(Set<? extends TypeElement> annotations, RoundEnvironment roundEnv) {
        if (!checkIntegrity(roundEnv))
            return false;
        Set<? extends Element> elementActivities = roundEnv.getElementsAnnotatedWith(ActivityPermission.class);
        Set<? extends Element> elementFragments = roundEnv.getElementsAnnotatedWith(FragmentPermission.class);
        return makeListenerJavaFile(elementActivities) && makeListenerJavaFile(elementFragments);
    }
   ...

Android支持庫

主要會封裝了一些工具類,基類以及對回調的處理

* BasePermissionCompatActivity.java
* BasePermissionCompatFragment.java
* PermissionCompat.java
* PermissionUtils.java

參考代碼:

public class PermissionCompat {

    private static int sNextRequestCode;
    static final Map<Class<?>, OnGrantedListener> BINDERS = new LinkedHashMap<>();

    // 分批次請求權限
    public static void requestPermission(BasePermissionCompatActivity target, String[] permissions) {

        Class<?> targetClass = target.getClass();
        try {
           // 找到監(jiān)聽Listener類,并實例一個
            OnGrantedListener<BasePermissionCompatActivity> listener = findOnGrantedListenerForClass(targetClass, permissions);
            if (PermissionUtils.hasSelfPermissions(target, permissions)) {
                listener.onGranted(target, permissions);
            } else if (PermissionUtils.shouldShowRequestPermissionRationale(target, permissions)) {
                // 拒絕過,再次請求的時候,這個函數(shù)是否有必要,不在詢問后,返回false,第一次返回false,
                //listener.onShowRationale(target, permissions);
                startRequest(target, listener, permissions);
            } else {
                startRequest(target, listener, permissions);
            }

        } catch (Exception e) {
            throw new RuntimeException("Unable to bind views for " + targetClass.getName(), e);
        }
    }

    private static void startRequest(BasePermissionCompatActivity target, OnGrantedListener listener, final @NonNull String[] permissions) {
        target.setOnGrantedListener(listener);
        ActivityCompat.requestPermissions(target, permissions, getNextRequestCode());
    }

使用

  • 1、Activity繼承BasePermissionCompatActivity

  • 2、用注解寫回調函數(shù),支持權限分組,跟單獨處理,但是每個分組都要寫自己的回調函數(shù)(目前回調函數(shù),不支持參數(shù))

  • 3、回調必需配套,也就是一個權限必須對應四個函數(shù),否則編譯不通過

  • 4、請求的權限必須有回調函數(shù),不然報運行時錯誤--崩潰

      @ActivityPermission
      public class PermssionActivity extends BasePermissionCompatActivity {
      
          。。。
       
          @OnGranted(value = {Manifest.permission.CAMERA})
          void granted() {
              LogUtils.v("granted");
          }
      
          @OnDenied(value = {Manifest.permission.CAMERA})
          void onDenied() {
              LogUtils.v("onDenied");
          }
      
          @OnNeverAsk(value = {Manifest.permission.CAMERA})
          void OnNeverAsk() {
              LogUtils.v("OnNeverAsk");
    
          }
          @OnShowRationale(value = {Manifest.permission.CAMERA})
          void OnShowRationale() {
              LogUtils.v("OnShowRationale");
          }    
          <!--何時的時機調用-->
          
          @OnClick(R.id.get)
          void get() {
              PermissionCompat.requestPermission(this, new String[]{Manifest.permission.CAMERA});
          }
      }
    

國產ROM兼容性

6.0之前權限管理即不是原生功能又沒有制定相應標準,每個廠家的實現(xiàn)都是完全不同的,雖然4.3 Google官方試圖推出AppOpsManager來動態(tài)適配權限管理,但由于不成熟,一直到6.0也沒走向前臺。不過,看6.0之前國內ROM的表現(xiàn),基本是在每個服務內部觸發(fā)鑒權請求,對原生權限的判斷并沒多大影響,所以兼容沒太大問題。

提醒:記得區(qū)分是不是自己的App要權限,比如拍照,是調用系統(tǒng)相機,還是自己open Camera,兩者是有區(qū)別的。

最后附上GitHub Demo及第三方庫鏈接 權限兼容庫 PermissionCompat

作者:看書的小蝸牛
原文鏈接: Android6.0權限適配及兼容庫的實現(xiàn)

參考文檔

1、Requesting Permissions at Run Time
2、PermissionDispatcher
3、Android6.0權限適配之WRITE_EXTERNAL_STORAGE(SD卡寫入)

最后編輯于
?著作權歸作者所有,轉載或內容合作請聯(lián)系作者
【社區(qū)內容提示】社區(qū)部分內容疑似由AI輔助生成,瀏覽時請結合常識與多方信息審慎甄別。
平臺聲明:文章內容(如有圖片或視頻亦包括在內)由作者上傳并發(fā)布,文章內容僅代表作者本人觀點,簡書系信息發(fā)布平臺,僅提供信息存儲服務。

相關閱讀更多精彩內容

  • Android 自定義View的各種姿勢1 Activity的顯示之ViewRootImpl詳解 Activity...
    passiontim閱讀 179,351評論 25 708
  • 簡述 Android是一個權限分離的操作系統(tǒng),每一個應用程序運行時都會有一個明確地系統(tǒng)身份標識(Linux的use...
    Kisson閱讀 6,681評論 1 37
  • 剛剛看到一則新聞:海鷗啄食鯨魚事件。小編我吃驚的一塌糊涂,有道是大魚吃小魚的自然規(guī)律,瞬間被逆襲。 常言道:大魚吃...
    格小主閱讀 306評論 0 0
  • 這個問題無他,因為我發(fā)現(xiàn)小秘最近越來越放肆了,上完班就走,下班微信找她也不回復,卻發(fā)現(xiàn)她發(fā)了朋友圈。 然后就想問一...
    吾聊職場閱讀 625評論 0 1
  • 長大后發(fā)現(xiàn)流淚不只是因為難過 還有有委屈 有愛 有心疼 有思念 有沒有你的日子自己給自己的安慰
    聰明的狗閱讀 186評論 0 0

友情鏈接更多精彩內容