談談Android自動安裝技術 應用程序 自動推送 自動安裝

轉載學習,所有權歸原作者所有。如有侵權請聯繫刪文。原文地址:http://www.jianshu.com/p/241b383ba377


2016年5月9日

提起應用自動裝

應用自動裝一開始給我的感覺就是擁有root權限才能做得事情,畢竟各大市場早期的自動裝都需要root權限。而現在不需要root權限的自動裝也不是什麼新鮮產物了,Android在4.2有了AccessibilityService這個類,他的作用主要是幫助有障礙的人使用Android手機的,他可以做到幫助你操作手機。這項技術主要面向應用自動更新、應用市場、應用SDK提供的自動更新。但自動更新已經有了插件化技術,比較好用比如360的DroidPlus等。關於AccessibilityService市場上也有了一些比較好玩的應用,比如搶紅包。不過呢,今天的主題主要是App的一鍵安裝,他的實現原理就是當出現安裝頁面時候幫你點一下安裝那個按鈕而已

技術點

  1. 如何使用AccessibilityService監聽應用Android
  2. 如何只監聽你自己的應用
  3. 最後說一下Root下自動安裝

關於AccessibilityService

首先說說這個類:

這裏不講API,API可以查看這個類的註釋,寫的很詳細

它是一個輔助服務,他可以幫你做點擊、長按等事件(ACTION)。。那麼怎麼完成這個過程呢。根據我們以往的經驗,完成一個事件,首先要明確什麼時候做什麼事,比如onClick監聽,他就表示在這個View被點擊的時候,做了方法裏面描述的事情。AccessibilityService思路也是一樣的,首先你要在AndroidMainfests裏面註冊這個服務並綁定事件,然後這個類的相應方法就做了某些事兒。給個小例子

AndroidMainfests.xml:

<service
    android:name=".MyAccessibilityService"
    android:label="我的自動裝"
    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被激活就會執行這個方法。進而回調AccessibilityService類的onAccessibilityEvent方法。但是不要忘記,你需要在設置-輔助功能開啓你的輔助功能。還算是比較簡單的。如果想理解深刻一點可以查看文章末尾給的Demo

如果只監聽自己的應用(本文重點)

AccessibilityService是一個服務,他會不斷的在後臺運行,監聽所有App或者用戶發起的安裝器請求。如果系統安裝器一啓動,AccessibilityService的onAccessibilityEvent的方法就會回調。那麼,試想象一個情景,你同時裝有兩個有自動裝的App A和B,上面註冊的服務會監聽所有包名爲com.android.packageinstaller的Activity。也就是A和B同時都會監聽com.android.packageinstaller的狀態,當A去發起一個Intent調起它去安裝App的時候,這時候B幫你點了安裝。這種情況比較噁心。在實際情況中表現就是,在豌豆莢安裝一個應用,用戶沒有開啓豌豆莢的應用自動裝,然後被你的自動裝給裝上了。用戶會去罵誰,哈哈哈。

要解決這個問題,首先你需要知道當應用安裝器被調起來的時候正在安裝的是不是你要安裝的應用。他的實現也很簡單,AccessibilityService有一個孿生兄弟類叫AccessibilityNodeInfo。他通過AccessibilityNodeInfo nodeInfo = getRootInActiveWindow();獲取,在裏面保存了View節點的所有信息,只要把所有節點遍歷一下,就知道是不是你要安裝的了。若果不明白,你就親自打開一個安裝包,然後看着那個安裝界面。你就想,不同應用怎麼區分呢。然後你就明白了,因爲你看到整個界面只有App名稱是特有的,剩下都TND一樣。

for (Iterator<String> ite = whiteList.iterator(); ite.hasNext(); ) {
    String appName = ite.next();
    Log.d(TAG, "待安裝/卸載的應用:" + appName);

    List<AccessibilityNodeInfo> nodes = nodeInfo.findAccessibilityNodeInfosByText(appName);
    if (nodes != null && !nodes.isEmpty()) {
        return appName;
    }
}

whiteList是一個HashSet,他臨時保存了你將要安裝的App的名稱。用這裏面的應用名稱和nodeInfo的相應信息進行比對,如果你的HashSet有那麼幫它點吧。
然後問題又來了,怎麼獲取我要安裝Apk的名稱呢。根據以往的經驗,在AndroidMainfests中的Application裏面有個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的名稱,但是對於其他的App就無能爲了。
那麼如果根據Apk獲取應用名稱呢?答案還是ApplicationInfo,只不過通過其他的方式獲取的對應Apk的ApplicationInfo。Android中有這樣一個類android.content.pm.PackageParser,他負責把apk中的AndroidMainfests中的信息讀取出來,並存到他自己的內部類Package中,這時候我希望你去看一下這個類。在這個類裏面保存着ApplicationInfo以及其他信息。那麼我們就通過反射讓目標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構造器
        packageParser = packageParserConstructor.newInstance();//PackageParser對象實例
        parsePackageTypeArgs = new Class<?>[]{File.class, int.class};
        parsePackageValueArgs = new Object[]{new File(apkPath), 0};//parsePackage方法參數
    } else {
        Class<?>[] paserTypeArgs = {String.class};
        packageParserConstructor = pkgParserCls.getConstructor(paserTypeArgs);//PackageParser構造器
        Object[] paserValueArgs = {apkPath};
        packageParser = packageParserConstructor.newInstance(paserValueArgs);//PackageParser對象實例

        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方法參數

    }
    parsePackageMethod = pkgParserCls.getDeclaredMethod("parsePackage", parsePackageTypeArgs);
    // 執行pkgParser_parsePackageMtd方法並返回
    return parsePackageMethod.invoke(packageParser, parsePackageValueArgs);
    }

這麼一大段東西,無疑就是做了兩件事,找到PackageParser對象,調用packageParser()方法獲取Package對象。這裏面確實有ApplicationInfo對象,但是你把它的applicationinfo.loadLabel(pm).toString()打印出來他是包名,這不是我門想要的。其實在Resource裏面其實也可以讀到應用名稱,我們都知道,Resource要想讀取一個值必須給他指定Id,這個Id其實就存在在ApplicationInfo裏面,它叫labelRes。這時用resource.getText(applicationinfo.labelRes)去還是取不到,因爲你這裏的Resource是屬於現在這個應用而不是被安裝應用的。那應該怎麼做呢?
做過插件化都知道,如果讀取出來插件apk的資源呢。有一個類叫AssetManager,用它的addAssetPath的方法可以把一個apk的Resource讀到當前Resource對象中,雖然這個方法是public的,但是實際調用時候還是失敗,必須用反射獲取。具體看代碼,反射這個類有點噁心,挺費解的。

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對象
        if (pkgParserPkg == null) {
            return null;
        }
        Field appInfoFld = pkgParserPkg.getClass().getDeclaredField(
                "applicationInfo");
        // 從對象Package對象得到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;
        // 執行addAssetPath方法,加載目標apk資源
        assetMag_addAssetPathMtd.invoke(assetMag, valueArgs);

        // 得到本地Resources對象並實例化,有參數
        Resources res = ctx.getResources();
        typeArgs = new Class[3];
        typeArgs[0] = assetMag.getClass();
        typeArgs[1] = res.getDisplayMetrics().getClass();
        typeArgs[2] = res.getConfiguration().getClass();
        //反射得到目標Resource的構造器
        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對象,繼續反射Package得到ApplicationInfo,取出ApplicationInfo裏面的labelRes供Resource使用。接下來是獲取Resource,反射AssetManager得到把目標Resource放到本地Apk的Resource裏面。調用本地Resource獲取應用名稱。

好的,費很大的勁終於把Apk中的名稱給讀出來,那麼把他加到whiteList裏面,這樣通過比對whiteList裏面的內容是否在應用安裝器的界面出現過就可以了。

Root模式怎麼做

Root爲什麼有那麼大權限呢,玩過Shell都懂。當你想在比你權限高或者不屬於你的目錄移動活刪除文件或被拒絕,但是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模式執行
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



文/liucloo(簡書作者)
原文鏈接:http://www.jianshu.com/p/241b383ba377
著作權歸作者所有,轉載請聯繫作者獲得授權,並標註“簡書作者”。

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章