1. 程式人生 > >談談Android自動安裝技術 應用程式 自動推送 自動安裝

談談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");
}


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