問題引入
由于應(yīng)用的支付、登錄和分享的場景需要,需要去根據(jù)應(yīng)用是否安裝微信、QQ等應(yīng)用,去做相應(yīng)的處理。但是經(jīng)常有線上用戶反饋安裝了微信、QQ,但是沒有相應(yīng)的支付方式。猜測是用戶禁止了用戶讀取已安裝應(yīng)用列表的權(quán)限。于是針對此問題進(jìn)行了調(diào)研和驗證。
已安裝應(yīng)用列表權(quán)限
Android原生系統(tǒng)是沒有這個權(quán)限的,該權(quán)限不需要也沒辦法向系統(tǒng)進(jìn)行注冊申請。但是(重點來了),國內(nèi)ROM(EMUI,MIUI,F(xiàn)untouch OS等)把這個權(quán)限暴露給用戶,在應(yīng)用要讀取已安裝應(yīng)用列表的時候會彈出彈窗供用戶選擇,用戶如果拒絕以后,應(yīng)用將無法讀取到已安裝應(yīng)用列表。
在驗證demo的時候發(fā)現(xiàn)一個有意思的現(xiàn)象,在華為手機上,如果應(yīng)用在Manifest文件中未申請任何權(quán)限,在權(quán)限管理“讀取已安裝應(yīng)用列表”中是看不到demo應(yīng)用的,應(yīng)用也會顯示未申請任何權(quán)限,也就無法禁止應(yīng)用讀取已安裝應(yīng)用列表權(quán)限。

獲取某應(yīng)用是否已安裝
通過Application拿到PackageManager,然后通過PackageManager的getInstalledPackages(int flags)獲取到安裝應(yīng)用列表的PackageInfo,遍歷看是否有某應(yīng)用包名來判斷是否安裝了某應(yīng)用。
public boolean isWxInstall() {
PackageManager packageManager = MyApplication.getInstance().getPackageManager();
List<PackageInfo> pinfo = packageManager.getInstalledPackages(0);
if (pinfo != null) {
for (int i = 0; i < pinfo.size(); i++) {
String pn = pinfo.get(i).packageName;
if (pn.equals("com.tencent.mm")) {
return true;
}
}
}
return false;
}
如果禁止讀取獲取已安裝應(yīng)用列表權(quán)限后,上述方法可能會導(dǎo)致實際的結(jié)果不正確,于是分別找了vivo、華為和小米手機進(jìn)行了驗證。
驗證
-
vivo x9 Funtouch OS_3.1 android 7.1.2
不禁止權(quán)限,通過getInstalledPackages()方法拿到的List<PackageInfo>的size是204,包含了系統(tǒng)應(yīng)用、用戶安裝的應(yīng)用等所有的應(yīng)用。
1.png
禁止讀取已安裝應(yīng)用列表的權(quán)限以后,通過getInstalledPackages()方法拿到的List<PackageInfo>的size僅僅為15。
2.png
查看此時數(shù)據(jù),發(fā)現(xiàn)除了一部分系統(tǒng)應(yīng)用能被讀取到以外,微博、微信和QQ的PackageInfo信息也能讀取到,這里猜測vivo的系統(tǒng)針對該權(quán)限被禁止以后,可能設(shè)置有白名單,使得無論是否禁止讀取已安裝應(yīng)用都能拿到相應(yīng)的PackageInfo。

-
HUAWEI Mate10 Pro EMUI 8.0.0 android 8.0.0
不禁止讀取已安裝應(yīng)用列表的權(quán)限,通過getInstalledPackages()方法拿到的List<PackageInfo>的size是236,包含了所有系統(tǒng)安裝的應(yīng)用。
4.png
但是禁止該權(quán)限以后,通過getInstalledPackages()方法僅僅能讀取到3個應(yīng)用的PackageInfo,包括應(yīng)用本身、華為云文件夾(com.huawei.hifolder)和一個系統(tǒng)內(nèi)置應(yīng)用(com.android.cts.priv.ctsshim)。由此可見,華為對該權(quán)限限制的太嚴(yán)重了,一旦禁止該權(quán)限,連System級別的應(yīng)用的PackageInfo都無法獲取到了。
5.png -
MI 5 MIUI 9.5.2.0 android 7.0
不禁止讀取應(yīng)用列表權(quán)限之前,通過getInstalledPackages()方法拿到的List<PackageInfo>的size是286,可以拿到所有應(yīng)用的PackageInfo。
9.png
但是禁止讀取應(yīng)用列表權(quán)限后,通過getInstalledPackages()方法拿到的List<PackageInfo>的size是188,包含了所有Android System級別和MIUI系統(tǒng)自帶的應(yīng)用,但是第三方應(yīng)用的PackageInfo無法獲取到。

由此可以看出,國內(nèi)廠商的這個讀取已安裝應(yīng)用列表的權(quán)限在被禁了以后,第三方應(yīng)用的PackageInfo用getInstalledPackages()方法是獲取不到的。
另一種方案
public boolean isWxInstall2() {
PackageManager packageManager = MyApplication.getInstance().getPackageManager();
String packageName = "com.tencent.mm";
boolean hasInstallWx;
try {
PackageInfo packageInfo = packageManager.getPackageInfo(packageName,PackageManager.GET_GIDS);
hasInstallWx = packageInfo != null;
} catch (PackageManager.NameNotFoundException e) {
hasInstallWx = false;
e.printStackTrace();
}
return hasInstallWx;
}
這種方案用的是PackageManager里的getPackageInfo()方法,該方法不能獲得已安裝應(yīng)用的列表,但是可以獲得指定包名的PackageInfo,當(dāng)指定包名的PackageInfo不存在的時候,系統(tǒng)會拋出PackageManager.NameNotFoundException異常,可以以此為依據(jù)來進(jìn)行判斷系統(tǒng)是否安裝某應(yīng)用。
同樣,Android原生系統(tǒng)用上述方法是肯定能夠獲得正確結(jié)果的,但不知道國內(nèi)手機廠商的ROM是否對該方法也進(jìn)行了修改,于是用上述三部手機分別進(jìn)行了驗證,得到的結(jié)果是一樣的。在禁止讀取已安裝列表權(quán)限以后,能夠獲得正確的結(jié)果:

所以,就目前手機廠商的ROM來說,用上述方案是能夠正確獲得手機是否安裝某應(yīng)用的。
查看微信SDK、QQSDK里封裝的判斷應(yīng)用是否安裝了應(yīng)用的原理是一樣的
微信:
public final boolean isWXAppInstalled() {
if(this.detached) {
throw new IllegalStateException("isWXAppInstalled fail, WXMsgImpl has been detached");
} else {
try {
PackageInfo var1;
return (var1 = this.context.getPackageManager().getPackageInfo("com.tencent.mm", 64)) == null?false:WXApiImplComm.validateAppSignature(this.context, var1.signatures, this.checkSignature);
} catch (NameNotFoundException var2) {
return false;
}
}
}
QQ:
public static boolean checkMobileQQ(Context var0) {
PackageManager var1 = var0.getPackageManager();
PackageInfo var2 = null;
try {
var2 = var1.getPackageInfo("com.tencent.mobileqq", 0);
} catch (NameNotFoundException var7) {
Log.d("checkMobileQQ", "error");
var7.printStackTrace();
}
if(var2 != null) {
String var3 = var2.versionName;
try {
Log.d("MobileQQ verson", var3);
String[] var4 = var3.split("\\.");
int var5 = Integer.parseInt(var4[0]);
int var6 = Integer.parseInt(var4[1]);
return var5 > 4 || var5 == 4 && var6 >= 1;
} catch (Exception var8) {
var8.printStackTrace();
return false;
}
} else {
return false;
}
}
PMS的源碼太多了,PackageManager.getPackageInfo()到底做了什么暫時不做分析,感興趣的同學(xué)可以自行去查看下源碼。




