Android6.0權(quán)限適配及兼容庫(kù)的實(shí)現(xiàn)

從6.0 MarshMallow開始,Android支持動(dòng)態(tài)權(quán)限管理木柬,即有些權(quán)限需要在使用到的時(shí)候動(dòng)態(tài)申請(qǐng)潮峦,根據(jù)用戶的選擇需要有不同的處理兴喂,具體表現(xiàn)可以看下圖:

權(quán)限申請(qǐng)

本文并不關(guān)心權(quán)限適配的原理,原理可以參考Android權(quán)限管理原理
蒙幻,這里只是針對(duì)6.0中的表現(xiàn)做適配映凳,先思考以下幾個(gè)問題:

  • 為什么6.0權(quán)限需要適配
  • 什么權(quán)限需要?jiǎng)討B(tài)適配
  • 怎樣動(dòng)態(tài)適配權(quán)限
  • 怎么樣實(shí)現(xiàn)第三方庫(kù),簡(jiǎn)化代碼及適配流程 權(quán)限兼容庫(kù) PermissionCompat
  • 對(duì)于國(guó)產(chǎn)ROM的影響

為什么6.0需要權(quán)限適配

6.0之前Android的權(quán)限都是在安裝的時(shí)候授予的邮破,6.0之后诈豌,為了簡(jiǎn)化安裝流程,并且方便用戶控制權(quán)限抒和,Android允許在運(yùn)行的時(shí)候動(dòng)態(tài)控制權(quán)限矫渔。對(duì)于開發(fā)而言就是將targetSdkVersion設(shè)置為23,當(dāng)運(yùn)行在Android 6.0 +的手機(jī)上時(shí)摧莽,就會(huì)調(diào)用6.0相關(guān)的API庙洼,達(dá)到動(dòng)態(tài)控制權(quán)限的目的。但是镊辕,如果僅僅是將targetSdkVersion設(shè)置為23油够,而在代碼層面沒有針對(duì)Android 6.0做適配,就可能在申請(qǐng)系統(tǒng)服務(wù)的時(shí)候征懈,由于權(quán)限不足叠聋,引發(fā)崩潰。

  • targetSDKVersion:該屬性用于通知系統(tǒng)受裹,您已針對(duì)目標(biāo)版本進(jìn)行測(cè)試碌补,標(biāo)識(shí)App能夠適配的系統(tǒng)版本虏束,有些新的API是只有新的系統(tǒng)才有的。

什么權(quán)限需要?jiǎng)討B(tài)適配

并非所有的權(quán)限都需要?jiǎng)討B(tài)申請(qǐng)厦章,Android6.0將權(quán)限分為兩種镇匀,普通權(quán)限跟敏感(危險(xiǎn))權(quán)限,普通權(quán)限是不需要?jiǎng)討B(tài)申請(qǐng)的袜啃,但是敏感權(quán)限需要?jiǎng)討B(tài)申請(qǐng)汗侵。

  • 1、普通權(quán)限(Normal permissions):不會(huì)泄露用戶隱私群发,同時(shí)也不會(huì)導(dǎo)致手機(jī)安全問題晰韵。如網(wǎng)絡(luò)請(qǐng)求權(quán)限、WIFI狀態(tài)等熟妓,這類權(quán)限只需要在Manifest列出來雪猪,之后,系統(tǒng)會(huì)自動(dòng)賦給APP權(quán)限:

    • ACCESS_NETWORK_STATE
    • ACCESS_NOTIFICATION_POLICY
    • ACCESS_WIFI_STATE
    • BLUETOOTH
    • BLUETOOTH_ADMIN
  • 2起愈、敏感權(quán)限(Dangerous permissions):與?普通權(quán)限對(duì)應(yīng)只恨,可能會(huì)影響用戶的隱私,存儲(chǔ)數(shù)據(jù)等抬虽,比如拍照官觅、存儲(chǔ)、通訊錄阐污、地理GPS等休涤,這類權(quán)限需要在Manifest列出來,在需要的的時(shí)候笛辟,顯示的請(qǐng)求用戶準(zhǔn)許滑绒。

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

敏感權(quán)限的請(qǐng)求是按照分組進(jìn)行提醒的,并非僅僅針對(duì)一條隘膘,比如通訊錄的讀取權(quán)限與寫權(quán)限疑故,只要一個(gè)權(quán)限獲到,下次請(qǐng)求權(quán)限的時(shí)候會(huì)自動(dòng)提供弯菊,當(dāng)然也要請(qǐng)求纵势。否則還是有問題。

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

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

怎樣動(dòng)態(tài)適配權(quán)限

對(duì)于敏感權(quán)限的適配有一個(gè)原則钦铁,那就是實(shí)時(shí)檢查,因?yàn)闄?quán)限隨時(shí)可能被回收才漆,比如用戶可以在設(shè)置里面把權(quán)限給取消牛曹,但是APP并不一定知道,因此每次都需要檢查醇滥,一旦沒有黎比,就需要請(qǐng)求超营,之后,根據(jù)返回結(jié)果處理后續(xù)邏輯阅虫。

實(shí)現(xiàn)步驟

  • 1演闭、在Manifest中列出來

    無論普通權(quán)限還是敏感權(quán)限,都需要在Manifest中列出來颓帝,同時(shí)也是對(duì)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、需要時(shí)购城,顯示的請(qǐng)求

在權(quán)限沒被授予前提下吕座,系統(tǒng)會(huì)顯示授權(quán)對(duì)話框,讓用戶操作瘪板,目前授權(quán)對(duì)話框不可定制吴趴,不過可以在申請(qǐng)之前添加一些解釋,告訴用戶為什么需要該權(quán)限篷帅,但是Google提醒,不要做過多的解釋拴泌,可能會(huì)使用戶感到厭煩魏身,用法如下:

    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、處理授權(quán)回調(diào)

    • 兼容6.0之前的處理:在這里只需要處理獲得權(quán)限即可蚪腐,因?yàn)?.0之前只存在Install權(quán)限箭昵,一旦安裝,所有權(quán)限都是默認(rèn)授予的回季,雖然國(guó)內(nèi)ROM對(duì)權(quán)限管理做了自己的一些定制家制,但基本都是兼容的。

    • 需要對(duì)6.0的授權(quán)成功泡一、失敗颤殴、永不詢問做處理

         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之后-->  需要根據(jù)結(jié)果進(jìn)行驗(yàn)證
              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中不同的實(shí)現(xiàn)方案

  • 1、簡(jiǎn)單的封裝回調(diào)
  • 2鼻忠、基于APT涵但,采用注解方式簡(jiǎn)化編碼邏輯,自動(dòng)封封回調(diào)

先看一下直接回調(diào)的方式

采用最直接的回調(diào)

首先在基類Activity或者Fragment中統(tǒng)一設(shè)置授權(quán)回調(diào)監(jiān)聽帖蔓,這里我們用一個(gè)

 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++;
    }
}

之后在需要時(shí)候的請(qǐng)求矮瘟,并根據(jù)結(jié)果處理后續(xù)邏輯即可。

   requestPermissions(activity, P_CAMERA, new OnGrantedListener() {

        // 根據(jù)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) {
    });

上面的方法比較直接,靈活埋酬,不過每次都要自己實(shí)現(xiàn)回調(diào)監(jiān)聽Listener哨啃,接下來看第二種實(shí)現(xiàn)烧栋,基于APT,通過注解的方式棘催,自動(dòng)添加Listener劲弦,這種實(shí)現(xiàn)參考了ButterKnife的實(shí)現(xiàn)方式。

基于APT與注解醇坝,編譯過程中生成代碼邑跪,自動(dòng)添加回調(diào)

  • 1、基于APT呼猪,定義一系列Annotation画畅,并動(dòng)態(tài)生成輔助Listener類
  • 2、添加Android支持庫(kù)宋距,在基類統(tǒng)一處理回調(diào)轴踱,
  • 3、添加工具類谚赎,連接綁定Listener與Activity(Fragment)

相應(yīng)的實(shí)現(xiàn)分三個(gè)庫(kù):

  • 注解庫(kù)
  • APT生成支持庫(kù)
  • Android支持庫(kù)

注解庫(kù):

注解庫(kù)

主要用來定義一些回調(diào)方法注解淫僻、及請(qǐng)求實(shí)體的類注解

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

APT生成支持庫(kù)

主要用來在編譯階段,動(dòng)態(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支持庫(kù)

主要會(huì)封裝了一些工具類壶唤,基類以及對(duì)回調(diào)的處理

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

參考代碼:

public class PermissionCompat {

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

    // 分批次請(qǐng)求權(quán)限
    public static void requestPermission(BasePermissionCompatActivity target, String[] permissions) {

        Class<?> targetClass = target.getClass();
        try {
           // 找到監(jiān)聽Listener類雳灵,并實(shí)例一個(gè)
            OnGrantedListener<BasePermissionCompatActivity> listener = findOnGrantedListenerForClass(targetClass, permissions);
            if (PermissionUtils.hasSelfPermissions(target, permissions)) {
                listener.onGranted(target, permissions);
            } else if (PermissionUtils.shouldShowRequestPermissionRationale(target, permissions)) {
                // 拒絕過,再次請(qǐng)求的時(shí)候,這個(gè)函數(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、用注解寫回調(diào)函數(shù)击费,支持權(quán)限分組拢蛋,跟單獨(dú)處理,但是每個(gè)分組都要寫自己的回調(diào)函數(shù)(目前回調(diào)函數(shù)蔫巩,不支持參數(shù))

  • 3瓤狐、回調(diào)必需配套,也就是一個(gè)權(quán)限必須對(duì)應(yīng)四個(gè)函數(shù)批幌,否則編譯不通過

  • 4础锐、請(qǐng)求的權(quán)限必須有回調(diào)函數(shù),不然報(bào)運(yùn)行時(shí)錯(cuò)誤--崩潰

      @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");
          }    
          <!--何時(shí)的時(shí)機(jī)調(diào)用-->
          
          @OnClick(R.id.get)
          void get() {
              PermissionCompat.requestPermission(this, new String[]{Manifest.permission.CAMERA});
          }
      }
    

國(guó)產(chǎn)ROM兼容性

6.0之前權(quán)限管理即不是原生功能又沒有制定相應(yīng)標(biāo)準(zhǔn)昂利,每個(gè)廠家的實(shí)現(xiàn)都是完全不同的疫稿,雖然4.3 Google官方試圖推出AppOpsManager來動(dòng)態(tài)適配權(quán)限管理张弛,但由于不成熟,一直到6.0也沒走向前臺(tái)意推。不過豆瘫,看6.0之前國(guó)內(nèi)ROM的表現(xiàn),基本是在每個(gè)服務(wù)內(nèi)部觸發(fā)鑒權(quán)請(qǐng)求菊值,對(duì)原生權(quán)限的判斷并沒多大影響外驱,所以兼容沒太大問題。

提醒:記得區(qū)分是不是自己的App要權(quán)限腻窒,比如拍照昵宇,是調(diào)用系統(tǒng)相機(jī),還是自己open Camera儿子,兩者是有區(qū)別的瓦哎。

最后附上GitHub Demo及第三方庫(kù)鏈接 權(quán)限兼容庫(kù) PermissionCompat

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

參考文檔

1、Requesting Permissions at Run Time
2柔逼、PermissionDispatcher
3蒋譬、Android6.0權(quán)限適配之WRITE_EXTERNAL_STORAGE(SD卡寫入)

最后編輯于
?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請(qǐng)聯(lián)系作者
  • 序言:七十年代末,一起剝皮案震驚了整個(gè)濱河市愉适,隨后出現(xiàn)的幾起案子犯助,更是在濱河造成了極大的恐慌,老刑警劉巖儡毕,帶你破解...
    沈念sama閱讀 218,122評(píng)論 6 505
  • 序言:濱河連續(xù)發(fā)生了三起死亡事件也切,死亡現(xiàn)場(chǎng)離奇詭異扑媚,居然都是意外死亡腰湾,警方通過查閱死者的電腦和手機(jī),發(fā)現(xiàn)死者居然都...
    沈念sama閱讀 93,070評(píng)論 3 395
  • 文/潘曉璐 我一進(jìn)店門疆股,熙熙樓的掌柜王于貴愁眉苦臉地迎上來费坊,“玉大人,你說我怎么就攤上這事旬痹「骄” “怎么了?”我有些...
    開封第一講書人閱讀 164,491評(píng)論 0 354
  • 文/不壞的土叔 我叫張陵两残,是天一觀的道長(zhǎng)永毅。 經(jīng)常有香客問我,道長(zhǎng)人弓,這世上最難降的妖魔是什么沼死? 我笑而不...
    開封第一講書人閱讀 58,636評(píng)論 1 293
  • 正文 為了忘掉前任,我火速辦了婚禮崔赌,結(jié)果婚禮上意蛀,老公的妹妹穿的比我還像新娘耸别。我一直安慰自己,他們只是感情好县钥,可當(dāng)我...
    茶點(diǎn)故事閱讀 67,676評(píng)論 6 392
  • 文/花漫 我一把揭開白布秀姐。 她就那樣靜靜地躺著,像睡著了一般若贮。 火紅的嫁衣襯著肌膚如雪省有。 梳的紋絲不亂的頭發(fā)上,一...
    開封第一講書人閱讀 51,541評(píng)論 1 305
  • 那天兜看,我揣著相機(jī)與錄音锥咸,去河邊找鬼。 笑死细移,一個(gè)胖子當(dāng)著我的面吹牛搏予,可吹牛的內(nèi)容都是我干的。 我是一名探鬼主播弧轧,決...
    沈念sama閱讀 40,292評(píng)論 3 418
  • 文/蒼蘭香墨 我猛地睜開眼雪侥,長(zhǎng)吁一口氣:“原來是場(chǎng)噩夢(mèng)啊……” “哼!你這毒婦竟也來了精绎?” 一聲冷哼從身側(cè)響起速缨,我...
    開封第一講書人閱讀 39,211評(píng)論 0 276
  • 序言:老撾萬榮一對(duì)情侶失蹤,失蹤者是張志新(化名)和其女友劉穎代乃,沒想到半個(gè)月后旬牲,有當(dāng)?shù)厝嗽跇淞掷锇l(fā)現(xiàn)了一具尸體,經(jīng)...
    沈念sama閱讀 45,655評(píng)論 1 314
  • 正文 獨(dú)居荒郊野嶺守林人離奇死亡搁吓,尸身上長(zhǎng)有42處帶血的膿包…… 初始之章·張勛 以下內(nèi)容為張勛視角 年9月15日...
    茶點(diǎn)故事閱讀 37,846評(píng)論 3 336
  • 正文 我和宋清朗相戀三年原茅,在試婚紗的時(shí)候發(fā)現(xiàn)自己被綠了。 大學(xué)時(shí)的朋友給我發(fā)了我未婚夫和他白月光在一起吃飯的照片堕仔。...
    茶點(diǎn)故事閱讀 39,965評(píng)論 1 348
  • 序言:一個(gè)原本活蹦亂跳的男人離奇死亡擂橘,死狀恐怖,靈堂內(nèi)的尸體忽然破棺而出摩骨,到底是詐尸還是另有隱情通贞,我是刑警寧澤,帶...
    沈念sama閱讀 35,684評(píng)論 5 347
  • 正文 年R本政府宣布恼五,位于F島的核電站昌罩,受9級(jí)特大地震影響,放射性物質(zhì)發(fā)生泄漏灾馒。R本人自食惡果不足惜茎用,卻給世界環(huán)境...
    茶點(diǎn)故事閱讀 41,295評(píng)論 3 329
  • 文/蒙蒙 一、第九天 我趴在偏房一處隱蔽的房頂上張望。 院中可真熱鬧绘搞,春花似錦彤避、人聲如沸。這莊子的主人今日做“春日...
    開封第一講書人閱讀 31,894評(píng)論 0 22
  • 文/蒼蘭香墨 我抬頭看了看天上的太陽。三九已至蒿褂,卻和暖如春圆米,著一層夾襖步出監(jiān)牢的瞬間,已是汗流浹背啄栓。 一陣腳步聲響...
    開封第一講書人閱讀 33,012評(píng)論 1 269
  • 我被黑心中介騙來泰國(guó)打工娄帖, 沒想到剛下飛機(jī)就差點(diǎn)兒被人妖公主榨干…… 1. 我叫王不留,地道東北人昙楚。 一個(gè)月前我還...
    沈念sama閱讀 48,126評(píng)論 3 370
  • 正文 我出身青樓近速,卻偏偏與公主長(zhǎng)得像,于是被迫代替她去往敵國(guó)和親堪旧。 傳聞我的和親對(duì)象是個(gè)殘疾皇子削葱,可洞房花燭夜當(dāng)晚...
    茶點(diǎn)故事閱讀 44,914評(píng)論 2 355

推薦閱讀更多精彩內(nèi)容