導(dǎo)語(yǔ)
android碎片化相信是每一個(gè)android開(kāi)發(fā)者的痛。機(jī)型適配也是難以繞過(guò)去的坎。這其中Android動(dòng)態(tài)權(quán)限檢測(cè)適配,相信對(duì)于很多開(kāi)發(fā)者來(lái)說(shuō),都是被按在地上摩擦摩擦。本文就針對(duì)Android權(quán)限動(dòng)態(tài)檢測(cè)提出一種解決方案。
一、Android權(quán)限介紹
談起Android權(quán)限機(jī)制,很多人都會(huì)想到Google在Android 6.0 提出運(yùn)行時(shí)權(quán)限管理機(jī)制(Android Runtime Permission)。針對(duì)運(yùn)行時(shí)權(quán)限管理機(jī)制在這里我們不做過(guò)多描述。相信對(duì)于大多數(shù)開(kāi)發(fā)者而言,真正的痛點(diǎn)在于建立在6.0之前的權(quán)限機(jī)制的權(quán)限檢測(cè)。之所以被認(rèn)為是痛點(diǎn),我們會(huì)在之后給大家介紹。不同于Android Runtime Permission,6.0之前的權(quán)限機(jī)制,所申請(qǐng)只需要在AndroidManifest.xml列舉出來(lái),這無(wú)疑給惡意軟件提供了可趁之機(jī)。
二、動(dòng)態(tài)權(quán)限檢測(cè)
針對(duì)權(quán)限檢測(cè),我們根據(jù)權(quán)限機(jī)制的不同,劃分為以下兩類:
(1) targetSdkVersion >= 23
當(dāng)設(shè)備處于6.0或6.0以上時(shí)采用的運(yùn)行時(shí)權(quán)限機(jī)制,權(quán)限檢測(cè)變得很簡(jiǎn)單,只需要調(diào)用ContextCompat::checkSelfPermission(),瀟瀟灑灑一行代碼即可檢測(cè)對(duì)應(yīng)的權(quán)限是否被賦予,簡(jiǎn)直不要太簡(jiǎn)單。
(2) tagetSdkVersion <23
當(dāng)設(shè)備是6.0以下系統(tǒng)時(shí),使用的老的權(quán)限機(jī)制。不像大家想象的,應(yīng)用所需要的權(quán)限只需要在清單文件里面列出就可以一勞永逸。如果真有這么簡(jiǎn)單,那恐怕連一點(diǎn)裝逼的空間都沒(méi)有啦。事實(shí)上,android設(shè)備提供應(yīng)用權(quán)限開(kāi)關(guān),達(dá)到用戶控制整個(gè)應(yīng)用的權(quán)限的賦予。對(duì)于應(yīng)用本身而言,應(yīng)用的很多功能是建立在擁有某一或者某些權(quán)限的基礎(chǔ)上。一旦用戶收回某一功能的具體權(quán)限,對(duì)于該功能來(lái)說(shuō)無(wú)疑是一種災(zāi)難。開(kāi)發(fā)者建立了良好的容災(zāi)機(jī)制那么只是會(huì)使某一功能異常,影響用戶體驗(yàn),然而對(duì)于那些沒(méi)有
建立起好的容災(zāi)處理的代碼而言,嚴(yán)重的可能導(dǎo)致應(yīng)用閃退。到了線上飄紅的地步,領(lǐng)導(dǎo)請(qǐng)喝茶恐怕是跑不了。
針對(duì)建立在老的權(quán)限機(jī)制基礎(chǔ)上的動(dòng)態(tài)權(quán)限檢測(cè)的方案,很多人第一時(shí)間就會(huì)想到我們是否可以像6.0以上設(shè)備一樣使用ContextCompat::checkSelfPermission()。對(duì)此我們特地做了測(cè)試,然而在我們測(cè)試過(guò)程發(fā)現(xiàn)只有清單文件里面申明了該權(quán)限,那么不管用戶是否關(guān)閉或者開(kāi)啟這一權(quán)限對(duì)應(yīng)的開(kāi)關(guān),該方法都會(huì)返回true。這無(wú)疑宣告了該方案的根本無(wú)法滿足我們的需求。為了弄明白該方法為何會(huì)出現(xiàn)這一情況,我們從源碼入手,去驗(yàn)證對(duì)應(yīng)的邏輯。

在圖中可以看到ActivityCompat::requestPermissions()方法體里面會(huì)對(duì)當(dāng)前api版本中做判斷,當(dāng)api<23時(shí),會(huì)調(diào)用PackageManager.checkPermission(),為了進(jìn)一步了解我們接著去看PackageManager.checkPermission()方法體內(nèi)有什么邏輯。

從圖上框出的文字中可以了解到判斷權(quán)限是否賦予是去判斷對(duì)應(yīng)的package是否含有該權(quán)限,換句更容易理解的話來(lái)說(shuō)就是如果該權(quán)限在清單文件中聲明了,即代表該權(quán)限被賦予。由此綜合前面的代碼片段來(lái)看當(dāng)api<23時(shí),使用
ContextCompat::checkSelfPermission()是無(wú)法滿足我們的需求。那么除了使用
ContextCompat::checkSelfPermission()還有什么辦法可以滿足我們的代碼需求。這里我們就不得不提起我們要介紹給大家的解決方案——AppOpsManager。
三、權(quán)限檢測(cè)解決方案——AppOpsManager
AppOpsManager究竟是什么呢?Google在Android開(kāi)發(fā)者手冊(cè)對(duì)AppPosManager的描述為
API for interacting with "application operation" tracking.
這玩意說(shuō)白了就是應(yīng)用程序操作管理。AppOpsManager是在api 19引入,即Android 4.3。


通過(guò)查看Android開(kāi)發(fā)者手冊(cè),以及查看AppOpsManager的源碼我們會(huì)發(fā)現(xiàn)checkOp()的多態(tài)方法,其中checkOp(String op,int uid,String packageName)主要在api>23可以使用,而checkOp(int op,int uid,String packageName)則沒(méi)有這一限制。既然有了思路,那么就來(lái)介紹一下AppOpsManager針對(duì)權(quán)限控制應(yīng)該主要那些東西。查看AppOpsManager的文檔,你會(huì)看到這樣一段文字:
This API is not generally intended for third party application developers; most features are only available to system applications.
這么一段文字扯了啥呢,其實(shí)主要表達(dá)的是AppOpsManager在設(shè)計(jì)之初并不是面向開(kāi)發(fā)者。這一點(diǎn)其實(shí)在這個(gè)類的很多地方就體現(xiàn)出來(lái)了,同時(shí)也是我們需要注意的。在這個(gè)類中,有很多權(quán)限對(duì)應(yīng)的常量被隱藏起來(lái),如下圖所示:

與此同時(shí),checkOp(int op,int uid,String packageName),這個(gè)方法本身也是hide,也就說(shuō)我們無(wú)法通過(guò)AppOpsManager這個(gè)類正常調(diào)用該方法,以及訪問(wèn)該屬性。走到這一步,然后再告訴大家,這個(gè)方法也走不通的話,我估計(jì)拍磚的肯定不少。那么處理這一問(wèn)題,對(duì)于我們來(lái)說(shuō),這個(gè)時(shí)候當(dāng)然少不了反射。你敢隱藏,只要你存在,我都要把你挖出來(lái)。以下為相關(guān)的代碼片段:
private boolean isPermissionGranted(String permissionCode) {
try {
Object object = getSystemService(Context.APP_OPS_SERVICE);
if (object == null) {
return false;
}
Class localClass = object.getClass();
Class[] arrayOfClass = new Class[3];
arrayOfClass[0] = Integer.TYPE;
arrayOfClass[1] = Integer.TYPE;
arrayOfClass[2] = String.class;
Method method = localClass.getMethod("checkOp", arrayOfClass);
if (method == null) {
return false;
}
Object[] arrayOfObject = new Object[3];
arrayOfObject[0] = Integer.valueOf(permissionCode);
arrayOfObject[1] = Integer.valueOf(Binder.getCallingUid());
arrayOfObject[2] = getPackageName();
int m = ((Integer) method.invoke(object, arrayOfObject)).intValue();
return m == AppOpsManager.MODE_ALLOWED;
} catch (Exception e) {
e.printStackTrace();
}
return false;
}
結(jié)語(yǔ)
權(quán)限動(dòng)態(tài)檢測(cè)有多種解決方案,AppOpsManager只是其中一種維護(hù)成本相對(duì)較少的方案。其中在做權(quán)限動(dòng)態(tài)檢測(cè)時(shí),看到了大家提供了很多針對(duì)某一權(quán)限的腦洞極大的方案,不得不感慨這得被虐了死去活來(lái)才能得出的經(jīng)驗(yàn)教訓(xùn)。同時(shí)有部分反應(yīng)AppOpsManager這一解決方案在某些機(jī)型上依舊不能適配的問(wèn)題,只能表示,

最后,AppOpsManager這個(gè)類還有潛在的彩蛋,通過(guò)這個(gè)工具可以打造一些裝逼利器,大家有興趣可以去挖掘。