2016年5月9日
提起應(yīng)用自動(dòng)裝
應(yīng)用自動(dòng)裝一開始給我的感覺就是擁有root權(quán)限才能做得事情,畢竟各大市場(chǎng)早期的自動(dòng)裝都需要root權(quán)限。而現(xiàn)在不需要root權(quán)限的自動(dòng)裝也不是什么新鮮產(chǎn)物了,Android在4.2有了AccessibilityService這個(gè)類,他的作用主要是幫助有障礙的人使用Android手機(jī)的,他可以做到幫助你操作手機(jī)。這項(xiàng)技術(shù)主要面向應(yīng)用自動(dòng)更新、應(yīng)用市場(chǎng)、應(yīng)用SDK提供的自動(dòng)更新。但自動(dòng)更新已經(jīng)有了插件化技術(shù),比較好用比如360的DroidPlus等。關(guān)于AccessibilityService市場(chǎng)上也有了一些比較好玩的應(yīng)用,比如搶紅包。不過呢,今天的主題主要是App的一鍵安裝,他的實(shí)現(xiàn)原理就是當(dāng)出現(xiàn)安裝頁面時(shí)候幫你點(diǎn)一下安裝那個(gè)按鈕而已。
技術(shù)點(diǎn)
- 如何使用AccessibilityService監(jiān)聽?wèi)?yīng)用Android
- 如何只監(jiān)聽你自己的應(yīng)用
- 最后說一下Root下自動(dòng)安裝
關(guān)于AccessibilityService
首先說說這個(gè)類:
這里不講API,API可以查看這個(gè)類的注釋,寫的很詳細(xì)
它是一個(gè)輔助服務(wù),他可以幫你做點(diǎn)擊、長按等事件(ACTION)。。那么怎么完成這個(gè)過程呢。根據(jù)我們以往的經(jīng)驗(yàn),完成一個(gè)事件,首先要明確什么時(shí)候做什么事,比如onClick監(jiān)聽,他就表示在這個(gè)View被點(diǎn)擊的時(shí)候,做了方法里面描述的事情。AccessibilityService思路也是一樣的,首先你要在AndroidMainfests里面注冊(cè)這個(gè)服務(wù)并綁定事件,然后這個(gè)類的相應(yīng)方法就做了某些事兒。給個(gè)小例子
AndroidMainfests.xml:
<service
android:name=".MyAccessibilityService"
android:label="我的自動(dòng)裝"
android:permission="android.permission.BIND_ACCESSIBILITY_SERVICE"
>
<intent-filter>
<action android:name="android.accessibilityservice.AccessibilityService"/>
</intent-filter>
<meta-data
android:name="android.accessibilityservice"
android:resource="@xml/accessibility_service"/>
</service>
xml/accessibility_service
<accessibility-service
xmlns:android="http://schemas.android.com/apk/res/android"
android:accessibilityEventTypes="typeAllMask"
android:accessibilityFeedbackType="feedbackGeneric"
android:accessibilityFlags="flagDefault"
android:canRetrieveWindowContent="true"
android:label="@string/title"
android:description="@string/description"
android:packageNames="com.android.packageinstaller"
android:notificationTimeout="100" />
上面綁定是所有類型,如果有com.android.packageinstaller
被激活就會(huì)執(zhí)行這個(gè)方法。進(jìn)而回調(diào)AccessibilityService類的onAccessibilityEvent
方法。但是不要忘記,你需要在設(shè)置-輔助功能
開啟你的輔助功能。還算是比較簡(jiǎn)單的。如果想理解深刻一點(diǎn)可以查看文章末尾給的Demo
如果只監(jiān)聽自己的應(yīng)用(本文重點(diǎn))
AccessibilityService是一個(gè)服務(wù),他會(huì)不斷的在后臺(tái)運(yùn)行,監(jiān)聽所有App或者用戶發(fā)起的安裝器請(qǐng)求。如果系統(tǒng)安裝器一啟動(dòng),AccessibilityService的onAccessibilityEvent
的方法就會(huì)回調(diào)。那么,試想象一個(gè)情景,你同時(shí)裝有兩個(gè)有自動(dòng)裝的App A和B,上面注冊(cè)的服務(wù)會(huì)監(jiān)聽所有包名為com.android.packageinstaller
的Activity。也就是A和B同時(shí)都會(huì)監(jiān)聽com.android.packageinstaller
的狀態(tài),當(dāng)A去發(fā)起一個(gè)Intent調(diào)起它去安裝App的時(shí)候,這時(shí)候B幫你點(diǎn)了安裝。這種情況比較惡心。在實(shí)際情況中表現(xiàn)就是,在豌豆莢安裝一個(gè)應(yīng)用,用戶沒有開啟豌豆莢的應(yīng)用自動(dòng)裝,然后被你的自動(dòng)裝給裝上了。用戶會(huì)去罵誰,哈哈哈。
要解決這個(gè)問題,首先你需要知道當(dāng)應(yīng)用安裝器被調(diào)起來的時(shí)候正在安裝的是不是你要安裝的應(yīng)用。他的實(shí)現(xiàn)也很簡(jiǎn)單,AccessibilityService有一個(gè)孿生兄弟類叫AccessibilityNodeInfo
。他通過AccessibilityNodeInfo nodeInfo = getRootInActiveWindow();
獲取,在里面保存了View節(jié)點(diǎn)的所有信息,只要把所有節(jié)點(diǎn)遍歷一下,就知道是不是你要安裝的了。若果不明白,你就親自打開一個(gè)安裝包,然后看著那個(gè)安裝界面。你就想,不同應(yīng)用怎么區(qū)分呢。然后你就明白了,因?yàn)槟憧吹秸麄€(gè)界面只有App名稱是特有的,剩下都TND一樣。
for (Iterator<String> ite = whiteList.iterator(); ite.hasNext(); ) {
String appName = ite.next();
Log.d(TAG, "待安裝/卸載的應(yīng)用:" + appName);
List<AccessibilityNodeInfo> nodes = nodeInfo.findAccessibilityNodeInfosByText(appName);
if (nodes != null && !nodes.isEmpty()) {
return appName;
}
}
whiteList是一個(gè)HashSet,他臨時(shí)保存了你將要安裝的App的名稱。用這里面的應(yīng)用名稱和nodeInfo的相應(yīng)信息進(jìn)行比對(duì),如果你的HashSet有那么幫它點(diǎn)吧。
然后問題又來了,怎么獲取我要安裝Apk的名稱呢。根據(jù)以往的經(jīng)驗(yàn),在AndroidMainfests中的Application里面有個(gè)label屬性,他一般就是App名稱。
ApplicationInfo info = null;
try {
info = context.getPackageManager().getApplicationInfo(context.getPackageName(),0);
} catch (PackageManager.NameNotFoundException e) {
e.printStackTrace();
}
ApplicationInfo info = packageInfo.applicationInfo;
appName = info.loadLabel(context.getPackageManager()).toString();
的確用它可以獲取到我們自己App的名稱,但是對(duì)于其他的App就無能為了。
那么如果根據(jù)Apk獲取應(yīng)用名稱呢?答案還是ApplicationInfo,只不過通過其他的方式獲取的對(duì)應(yīng)Apk的ApplicationInfo。Android中有這樣一個(gè)類android.content.pm.PackageParser
,他負(fù)責(zé)把a(bǔ)pk中的AndroidMainfests中的信息讀取出來,并存到他自己的內(nèi)部類Package
中,這時(shí)候我希望你去看一下這個(gè)類。在這個(gè)類里面保存著ApplicationInfo以及其他信息。那么我們就通過反射讓目標(biāo)Apk的android.content.pm.PackageParse
,讓其工作起來。這里直接貼代碼,都是反射
private static Object getPackage(String apkPath) throws Exception {
String PATH_PackageParser = "android.content.pm.PackageParser";
Constructor<?> packageParserConstructor = null;
Method parsePackageMethod = null;
Object packageParser = null;
Class<?>[] parsePackageTypeArgs = null;
Object[] parsePackageValueArgs = null;
Class<?> pkgParserCls = Class.forName(PATH_PackageParser);
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.LOLLIPOP) {
packageParserConstructor = pkgParserCls.getConstructor();//PackageParser構(gòu)造器
packageParser = packageParserConstructor.newInstance();//PackageParser對(duì)象實(shí)例
parsePackageTypeArgs = new Class<?>[]{File.class, int.class};
parsePackageValueArgs = new Object[]{new File(apkPath), 0};//parsePackage方法參數(shù)
} else {
Class<?>[] paserTypeArgs = {String.class};
packageParserConstructor = pkgParserCls.getConstructor(paserTypeArgs);//PackageParser構(gòu)造器
Object[] paserValueArgs = {apkPath};
packageParser = packageParserConstructor.newInstance(paserValueArgs);//PackageParser對(duì)象實(shí)例
parsePackageTypeArgs = new Class<?>[]{File.class, String.class,
DisplayMetrics.class, int.class};
DisplayMetrics metrics = new DisplayMetrics();
metrics.setToDefaults();
parsePackageValueArgs = new Object[]{new File(apkPath), apkPath, metrics, 0};//parsePackage方法參數(shù)
}
parsePackageMethod = pkgParserCls.getDeclaredMethod("parsePackage", parsePackageTypeArgs);
// 執(zhí)行pkgParser_parsePackageMtd方法并返回
return parsePackageMethod.invoke(packageParser, parsePackageValueArgs);
}
這么一大段東西,無疑就是做了兩件事,找到PackageParser
對(duì)象,調(diào)用packageParser()
方法獲取Package
對(duì)象。這里面確實(shí)有ApplicationInfo對(duì)象,但是你把它的applicationinfo.loadLabel(pm).toString()
打印出來他是包名,這不是我門想要的。其實(shí)在Resource里面其實(shí)也可以讀到應(yīng)用名稱,我們都知道,Resource要想讀取一個(gè)值必須給他指定Id,這個(gè)Id其實(shí)就存在在ApplicationInfo里面,它叫labelRes
。這時(shí)用resource.getText(applicationinfo.labelRes)
去還是取不到,因?yàn)槟氵@里的Resource是屬于現(xiàn)在這個(gè)應(yīng)用而不是被安裝應(yīng)用的。那應(yīng)該怎么做呢?
做過插件化都知道,如果讀取出來插件apk的資源呢。有一個(gè)類叫AssetManager
,用它的addAssetPath
的方法可以把一個(gè)apk的Resource讀到當(dāng)前Resource對(duì)象中,雖然這個(gè)方法是public的,但是實(shí)際調(diào)用時(shí)候還是失敗,必須用反射獲取。具體看代碼,反射這個(gè)類有點(diǎn)惡心,挺費(fèi)解的。
public static String getAppNameByReflection(Context ctx, String apkPath) {
File apkFile = new File(apkPath);
if (!apkFile.exists()) {//|| !apkPath.toLowerCase().endsWith(".apk")
return null;
}
String PATH_AssetManager = "android.content.res.AssetManager";
try {
Object pkgParserPkg = getPackage(apkPath);
// pkgParserPkg 為Package對(duì)象
if (pkgParserPkg == null) {
return null;
}
Field appInfoFld = pkgParserPkg.getClass().getDeclaredField(
"applicationInfo");
// 從對(duì)象Package對(duì)象得到applicationInfo
if (appInfoFld.get(pkgParserPkg) == null) {
return null;
}
ApplicationInfo info = (ApplicationInfo) appInfoFld.get(pkgParserPkg);
// 反射得到AssetManager
Class<?> assetMagCls = Class.forName(PATH_AssetManager);
Object assetMag = assetMagCls.newInstance();
// 從AssetManager類得到addAssetPath方法
Class[] typeArgs = new Class[1];
typeArgs[0] = String.class;
Method assetMag_addAssetPathMtd = assetMagCls.getDeclaredMethod(
"addAssetPath", typeArgs);
Object[] valueArgs = new Object[1];
valueArgs[0] = apkPath;
// 執(zhí)行addAssetPath方法,加載目標(biāo)apk資源
assetMag_addAssetPathMtd.invoke(assetMag, valueArgs);
// 得到本地Resources對(duì)象并實(shí)例化,有參數(shù)
Resources res = ctx.getResources();
typeArgs = new Class[3];
typeArgs[0] = assetMag.getClass();
typeArgs[1] = res.getDisplayMetrics().getClass();
typeArgs[2] = res.getConfiguration().getClass();
//反射得到目標(biāo)Resource的構(gòu)造器
Constructor resCt = Resources.class
.getConstructor(typeArgs);
valueArgs = new Object[3];
valueArgs[0] = assetMag;
valueArgs[1] = res.getDisplayMetrics();
valueArgs[2] = res.getConfiguration();
//得到組合之后的Resource
res = (Resources) resCt.newInstance(valueArgs);
PackageManager pm = ctx.getPackageManager();
// 讀取apk文件的信息
if (info == null) {
return null;
}
String appName;
if (info.labelRes != 0) {
appName = (String) res.getText(info.labelRes);
} else {
appName = info.loadLabel(pm).toString();
if (TextUtils.isEmpty(appName)) {
appName = apkFile.getName();
}
}
return appName;
} catch (Exception e) {
Log.e(TAG, "Exception", e);
}
return null;
}
這里把思路屢一下,通過反射PackageParser
獲取到Package
對(duì)象,繼續(xù)反射Package
得到ApplicationInfo
,取出ApplicationInfo
里面的labelRes
供Resource使用。接下來是獲取Resource,反射AssetManager
得到把目標(biāo)Resource放到本地Apk的Resource里面。調(diào)用本地Resource獲取應(yīng)用名稱。
好的,費(fèi)很大的勁終于把Apk中的名稱給讀出來,那么把他加到whiteList里面,這樣通過比對(duì)whiteList里面的內(nèi)容是否在應(yīng)用安裝器的界面出現(xiàn)過就可以了。
Root模式怎么做
Root為什么有那么大權(quán)限呢,玩過Shell都懂。當(dāng)你想在比你權(quán)限高或者不屬于你的目錄移動(dòng)活刪除文件或被拒絕,但是Root就不一樣了。Android賦予Root安裝免詢問功能。
他的原理就是一條shell命令pm install
。具體看代碼
//LD_LIBRARY_PATH 指定鏈接庫位置 指定安裝命令
String command = "LD_LIBRARY_PATH=/vendor/lib:/system/lib pm install " +
(pmParams == null ? "" : pmParams) +
" " +
filePath.replace(" ", "\\ ");
//以root模式執(zhí)行
ShellUtils.CommandResult result = ShellUtils.execCommand(command, true, true);
if (result.successMsg != null
&& (result.successMsg.contains("Success") || result.successMsg.contains("success"))) {
Log.i(TAG, "installSilent: success");
}
卸載也是一樣的道理
String command = "LD_LIBRARY_PATH=/vendor/lib:/system/lib pm uninstall" +
(isKeepData ? " -k " : " ") +
packageName.replace(" ", "\\ ");
ShellUtils.CommandResult result = ShellUtils.execCommand(command, true, true);
if (result.successMsg != null
&& (result.successMsg.contains("Success") || result.successMsg.contains("success"))) {
Log.i(TAG, "uninstallSilent: success");
}
Demo地址:https://github.com/liucloo/InstallAppDemo
多謝閱讀