談?wù)凙ndroid自動(dòng)安裝技術(shù)

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)

  1. 如何使用AccessibilityService監(jiān)聽?wèi)?yīng)用Android
  2. 如何只監(jiān)聽你自己的應(yīng)用
  3. 最后說一下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
多謝閱讀

最后編輯于
?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請(qǐng)聯(lián)系作者
平臺(tái)聲明:文章內(nèi)容(如有圖片或視頻亦包括在內(nèi))由作者上傳并發(fā)布,文章內(nèi)容僅代表作者本人觀點(diǎn),簡(jiǎn)書系信息發(fā)布平臺(tái),僅提供信息存儲(chǔ)服務(wù)。

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

  • Android 自定義View的各種姿勢(shì)1 Activity的顯示之ViewRootImpl詳解 Activity...
    passiontim閱讀 172,813評(píng)論 25 708
  • Spring Cloud為開發(fā)人員提供了快速構(gòu)建分布式系統(tǒng)中一些常見模式的工具(例如配置管理,服務(wù)發(fā)現(xiàn),斷路器,智...
    卡卡羅2017閱讀 134,837評(píng)論 18 139
  • 終于讓太陽將自已曬在床上了,渾身難受,躺了大半天了,不能再睡下去,環(huán)視屋子被我的小桌子吸引住,笑著對(duì)自已說“還是畫...
    綠肥紅瘦_4066閱讀 270評(píng)論 0 0
  • 文/小輝兒 一輩子和黑土地打交道的母親,耳提面命地提醒我:“一定要做一個(gè)體面的人!”我問她:“什么是體面?”母親說...
    子矜老師閱讀 758評(píng)論 0 0
  • 暮年老人望霞煙, 輕威嘴角勾回憶, 執(zhí)戩戰(zhàn)沙場(chǎng),妻兒在身旁, 老母在后方。 豈敢棄戩躲一方。 他日走過醉生樓, 與...
    RY周周閱讀 435評(píng)論 2 2