Android 靜默安裝的幾種方式

乍聽起來女器,靜默安裝是非常流氓的一件事,它讓用戶不知覺的情況下被「收割」浸遗。但是技術(shù)本身是中立的猫胁,我們只談?wù)剬?shí)現(xiàn)靜默安裝這件事兒。

下面我將介紹三種靜默安裝的方案跛锌,每種方案各有利弊弃秆,但是目的是一致的。

  1. 手機(jī)被 Root 后直接靜默安裝
  2. 聲明安裝權(quán)限并進(jìn)行系統(tǒng)簽名來靜默安裝
  3. 使用輔助功能進(jìn)行安裝(稱作「智能安裝」更貼切吧)

1. 手機(jī)被 Root 后直接靜默安裝

眾所周知髓帽,手機(jī)被 Root 后可以做好多奇妙的事情菠赚,比如靜默安裝,直接調(diào)用 pm install 命令就可以實(shí)現(xiàn)郑藏,來看代碼:

    public static boolean silentInstall(String apkPath) {
        boolean result = false;
        DataOutputStream dataOutputStream = null;
        BufferedReader errorStream = null;
        BufferedReader successStream = null;
        Process process = null;
        try {
            // 申請 su 權(quán)限
            process = Runtime.getRuntime().exec("su");
            dataOutputStream = new DataOutputStream(process.getOutputStream());
            // 執(zhí)行 pm install 命令
            String command = "pm install -r " + apkPath + "\n";
            dataOutputStream.write(command.getBytes(Charset.forName("UTF-8")));
            dataOutputStream.writeBytes("exit\n");
            dataOutputStream.flush();
            process.waitFor();
            errorStream = new BufferedReader(new InputStreamReader(process.getErrorStream()));
            StringBuilder errorMsg = new StringBuilder();
            String line;
            while ((line = errorStream.readLine()) != null) {
                errorMsg.append(line);
            }
            log.debug("silent install error message:{}", errorMsg);
            StringBuilder successMsg = new StringBuilder();
            successStream = new BufferedReader(new InputStreamReader(process.getInputStream()));
            // 讀取命令執(zhí)行結(jié)果
            while ((line = successStream.readLine()) != null) {
                successMsg.append(line);
            }
            log.debug("silent install success message:{}", successMsg);
            // 如果執(zhí)行結(jié)果中包含 Failure 字樣就認(rèn)為是操作失敗衡查,否則就認(rèn)為安裝成功
            if (!(errorMsg.toString().contains("Failure") || successMsg.toString().contains("Failure"))) {
                result = true;
            }
        } catch (Exception e) {
            log.error(e);
        } finally {
            try {
                if (process != null) {
                    process.destroy();
                }
                if (dataOutputStream != null) {
                    dataOutputStream.close();
                }
                if (errorStream != null) {
                    errorStream.close();
                }
                if (successStream != null) {
                    successStream.close();
                }
            } catch (Exception e) {
                // ignored
            }
        }
        return result;
    }

    public static boolean isRoot() {
        return new File("/system/bin/su").exists() || new File("/system/xbin/su").exists();
    }

首先申請 Root 權(quán)限,然后執(zhí)行 pm install- r <apk 路徑>命令必盖,-r 參數(shù)表示允許覆蓋安裝拌牲。 process.waitFor() 說明安裝過程是同步的,等待命令執(zhí)行完成歌粥,然后讀取執(zhí)行結(jié)果塌忽。注意:不要在主線程調(diào)用靜默安裝的代碼,安裝過程會比較耗時失驶,導(dǎo)致線程一直等待結(jié)果土居。

結(jié)論:只要手機(jī)被 Root,該方法十分奏效嬉探。但是絕大部分用戶不懂 Root擦耀,即使手機(jī)被 Root了,還需要用戶授權(quán)甲馋,所以該方案局限性非常大埂奈。

2. 聲明安裝權(quán)限并進(jìn)行系統(tǒng)簽名來靜默安裝

當(dāng)我們選擇手動安裝應(yīng)用時,會跳轉(zhuǎn)到應(yīng)用安裝界面定躏,這個界面就是系統(tǒng)的 PackageInstaller 提供账磺,專門用來讓用戶有感知地安裝應(yīng)用芹敌。

    Intent intent = new Intent(Intent.ACTION_VIEW);
    Uri uri = Uri.fromFile(new File("/sdcard/news.apk")));
    intent.setDataAndType(uri, "application/vnd.android.package-archive");
    startActivity(intent);

分析 PackageInstaller 的源碼,我們發(fā)現(xiàn)它會通過 PackageManager 調(diào)用 installPackage 方法垮抗,這是個隱藏的抽象方法氏捞,實(shí)現(xiàn)類是 ApplicationPackageManager。主要看一下四個參數(shù):packageURI 就是 apk 的路徑冒版;observer 是安裝的監(jiān)聽器液茎,應(yīng)用安裝完成時會被回調(diào),不能為 null辞嗡;flags 是標(biāo)志位捆等,指定安裝的參數(shù);installersPackageName 表示可選的安裝來源续室,比如應(yīng)用寶之類的栋烤。

    public abstract void installPackage(
            Uri packageURI, PackageInstallObserver observer,
            int flags, String installerPackageName);

ApplicationPackageManager 里面 mPM 是一個 IPackageManager 類型的對象,它會執(zhí)行具體的安裝任務(wù)挺狰。

        try {
            mPM.installPackage(originPath, observer.getBinder(), flags, installerPackageName,
                    verificationParams, null);
        } catch (RemoteException ignored) {
        }

ContextImpl 的 getPackageManager 方法明郭,通過 ActivityThread 獲取 IPackageManager 對象用來構(gòu)造 ApplicationPackageManager,然后返回 ApplicationPackageManager丰泊。

    public PackageManager getPackageManager() {
        if (mPackageManager != null) {
            return mPackageManager;
        }

        IPackageManager pm = ActivityThread.getPackageManager();
        if (pm != null) {
            // Doesn't matter if we make more than one instance.
            return (mPackageManager = new ApplicationPackageManager(this, pm));
        }
        return null;
    }

ActivityThread 的 getPackageManager 方法薯定,其實(shí)就是獲取系統(tǒng)服務(wù)的過程。

    public static IPackageManager getPackageManager() {
        if (sPackageManager != null) {
            //Slog.v("PackageManager", "returning cur default = " + sPackageManager);
            return sPackageManager;
        }
        IBinder b = ServiceManager.getService("package");
        //Slog.v("PackageManager", "default service binder = " + b);
        sPackageManager = IPackageManager.Stub.asInterface(b);
        //Slog.v("PackageManager", "default service = " + sPackageManager);
        return sPackageManager;
    }

通過以上分析瞳购,我們通過 PackageManager 調(diào)用 installPackage 方法就行了话侄,下面看代碼:

   public static boolean silentInstall(PackageManager packageManager, String apkPath) {
        Class<?> pmClz = packageManager.getClass();
        try {
            if (Build.VERSION.SDK_INT >= 21) {
                Class<?> aClass = Class.forName("android.app.PackageInstallObserver");
                Constructor<?> constructor = aClass.getDeclaredConstructor();
                constructor.setAccessible(true);
                Object installObserver = constructor.newInstance();
                Method method = pmClz.getDeclaredMethod("installPackage", Uri.class, aClass, int.class, String.class);
                method.setAccessible(true);
                method.invoke(packageManager, Uri.fromFile(new File(apkPath)), installObserver, 2, null);
            } else {
                Method method = pmClz.getDeclaredMethod("installPackage", Uri.class, Class.forName("android.content.pm.IPackageInstallObserver"), int.class, String.class);
                method.setAccessible(true);
                method.invoke(packageManager, Uri.fromFile(new File(apkPath)), null, 2, null);
            }
            return true;
        } catch (Exception e) {
            log.error(e);
        }
        return false;
    }

由于 PackageManager 在不同版本上的 installPackage 方法參數(shù)不一致,所以我們根據(jù)編譯版本做了處理学赛。在 API 21 及以上满葛,需要傳遞一個非 null 的 PackageInstallObserver,這個類是不可見 的罢屈,我們就用反射創(chuàng)建一個 observer 對象,flags 指定 INSTALL_REPLACE_EXISTING篇亭,用常量表示就是 2缠捌。在 API 21 以下,observer 類型是IPackageInstallObserver译蒂,同樣使用反射處理即可曼月。

最后聲明權(quán)限 <uses-permission android:name="android.permission.INSTALL_PACKAGES"/>,還要使用系統(tǒng)簽名柔昼,這個非常關(guān)鍵哑芹,要不然就會出現(xiàn)異常: java.lang.SecurityException: Neither user 10052 nor current process has android.permission.INSTALL_PACKAGES.

結(jié)論:通過調(diào)用系統(tǒng) API 靜默安裝捕透,終于可以堂堂正正地搞事情了聪姿!雖然這是官方提供的接口碴萧,但是為了不讓你為所欲為,強(qiáng)制使用系統(tǒng)簽名末购,所以對于第三方應(yīng)用采用的可能性是零破喻。

3. 使用輔助功能進(jìn)行安裝

現(xiàn)在大多數(shù)應(yīng)用采取的是這種辦法,讓用戶主動打開輔助功能盟榴,然后模擬點(diǎn)擊用戶操作曹质,進(jìn)行自動安裝。核心就是 AccessibilityService擎场,我們來實(shí)現(xiàn)這一功能羽德。

  1. 創(chuàng)建 AccessibilityService 配置文件
    在 res 目錄下創(chuàng)建 xml 目錄,然后在 xml 目錄下創(chuàng)建 accessibility_service_config.xml 文件迅办,內(nèi)容如下
<accessibility-service xmlns:android="http://schemas.android.com/apk/res/android"
                       android:accessibilityEventTypes="typeAllMask"
                       android:accessibilityFeedbackType="feedbackGeneric"
                       android:accessibilityFlags="flagDefault"
                       android:canRetrieveWindowContent="true"
                       android:description="@string/accessibility_service_description"
                       android:packageNames="com.android.packageinstaller"
    />

accessibilityEventTypes: 指定我們在監(jiān)聽窗口中可以模擬哪些事件宅静,typeAllMask 表示所有的事件都能模擬。
accessibilityFeedbackType: 指定無障礙服務(wù)的反饋方式礼饱。
canRetrieveWindowContent: 指定是否允許我們的程序讀取窗口中的節(jié)點(diǎn)和內(nèi)容坏为,當(dāng)然是 true。
description: 當(dāng)用戶手動配置服務(wù)時镊绪,顯示給用戶看的說明信息匀伏。
packageNames: 指定要監(jiān)聽哪個應(yīng)用程序下的窗口活動,這里寫 com.android.packageinstaller 表示監(jiān)聽 Android 系統(tǒng)的安裝界面蝴韭。
配置里面描述的內(nèi)容

<resources>  
    <string name="app_name">InstallTest</string>  
    <string name="accessibility_service_description">智能安裝服務(wù)够颠,無需用戶的任何操作就可以自動安裝程序。</string>  
</resources>  
  1. 創(chuàng)建 AccessibilityService 服務(wù)
    創(chuàng)建一個類繼承自 AccessibilityService 榄鉴,然后重寫 onAccessibilityEvent 方法履磨。
public class MyAccessibilityService extends AccessibilityService {
    private static final String TAG = "[TAG]";
    private Map<Integer, Boolean> handleMap = new HashMap<>();

    @Override
    public void onAccessibilityEvent(AccessibilityEvent event) {
        AccessibilityNodeInfo nodeInfo = event.getSource();
        if (nodeInfo != null) {
            int eventType = event.getEventType();
            if (eventType == AccessibilityEvent.TYPE_WINDOW_CONTENT_CHANGED || eventType == AccessibilityEvent.TYPE_WINDOW_STATE_CHANGED) {
                if (handleMap.get(event.getWindowId()) == null) {
                    boolean handled = iterateNodesAndHandle(nodeInfo);
                    if (handled) {
                        handleMap.put(event.getWindowId(), true);
                    }
                }
            }
        }
    }

    @Override
    public void onInterrupt() {
    }
    // 遍歷節(jié)點(diǎn),模擬點(diǎn)擊安裝按鈕
    private boolean iterateNodesAndHandle(AccessibilityNodeInfo nodeInfo) {
        if (nodeInfo != null) {
            int childCount = nodeInfo.getChildCount();
            if ("android.widget.Button".equals(nodeInfo.getClassName())) {
                String nodeCotent = nodeInfo.getText().toString();
                Log.d(TAG, "content is: " + nodeCotent);
                if ("安裝".equals(nodeCotent) || "完成".equals(nodeCotent) || "確定".equals(nodeCotent)) {
                    nodeInfo.performAction(AccessibilityNodeInfo.ACTION_CLICK);
                    return true;
                }
            }
            // 遇到 ScrollView 的時候模擬滑動一下
            else if ("android.widget.ScrollView".equals(nodeInfo.getClassName())) {
                nodeInfo.performAction(AccessibilityNodeInfo.ACTION_SCROLL_FORWARD);
            }
            for (int i = 0; i < childCount; i++) {
                AccessibilityNodeInfo childNodeInfo = nodeInfo.getChild(i);
                if (iterateNodesAndHandle(childNodeInfo)) {
                    return true;
                }
            }
        }
        return false;
    }
}

當(dāng)進(jìn)入安裝界面就會回調(diào) onAccessibilityEvent() 這個方法庆尘,我們只處理 TYPE_WINDOW_CONTENT_CHANGED 和 TYPE_WINDOW_STATE_CHANGED 兩個事件剃诅。為了防止重復(fù)處理事件,用 map 來過濾事件驶忌,然后遞歸遍歷節(jié)點(diǎn)矛辕,找到「安裝」、「完成」付魔、「缺點(diǎn)」的按鈕就模擬點(diǎn)擊聊品。由于安裝界面需要用戶看完權(quán)限才出現(xiàn)按鈕,所以遇到 ScrollView 的時候就模擬滾動几苍,直到出現(xiàn)安裝按鈕翻屈。

  1. 在 AndroidManifest 中配置服務(wù)
        <service
            android:name=".MyAccessibilityService"
            android:label="智能安裝應(yī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_config"
                />
        </service>

android:label:這個就是用戶看到的無障礙服務(wù)的名稱。
android:permission: 需要用到 BIND_ACCESSIBILITY_SERVICE 這個權(quán)限妻坝。
action: android.accessibilityservice.AccessibilityService 有了這個 action伸眶,用戶才能在設(shè)置里面看到我們的服務(wù)惊窖,否則用戶無法開啟我們的 AccessibilityService,也就不能進(jìn)到 MyAccessibilityService 里面了赚抡。

  1. 調(diào)用智能安裝代碼
    private void smartInstall() {
        Uri uri = Uri.fromFile(new File("/sdcard/test.apk"));
        Intent localIntent = new Intent(Intent.ACTION_VIEW);
        localIntent.setDataAndType(uri, "application/vnd.android.package-archive");
        startActivity(localIntent);
    }
  1. 手動配置智能安裝服務(wù)
    跳轉(zhuǎn)輔助功能的配置界面爬坑,引導(dǎo)用戶開啟智能安裝服務(wù)。
Intent intent = new Intent(Settings.ACTION_ACCESSIBILITY_SETTINGS);
startActivity(intent);

結(jié)論:智能安裝是一種妥協(xié)的方案涂臣,在沒有 Root 和安裝權(quán)限的情況下盾计,確實(shí)解放了用戶的拇指×抟牛看看市面上的應(yīng)用署辉,大部分都采用了這種方法。應(yīng)用市場使用智能安裝可以理解岩四,視頻瀏覽器工具一類不相干的應(yīng)用也要開啟哭尝?我真是呵呵了。


帶有智能安裝的應(yīng)用

目前靜默安裝的方法就這么多剖煌,歡迎大家留言交流~

參考文章:
最后編輯于
?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請聯(lián)系作者
  • 序言:七十年代末,一起剝皮案震驚了整個濱河市耕姊,隨后出現(xiàn)的幾起案子桶唐,更是在濱河造成了極大的恐慌,老刑警劉巖茉兰,帶你破解...
    沈念sama閱讀 212,383評論 6 493
  • 序言:濱河連續(xù)發(fā)生了三起死亡事件尤泽,死亡現(xiàn)場離奇詭異,居然都是意外死亡规脸,警方通過查閱死者的電腦和手機(jī)坯约,發(fā)現(xiàn)死者居然都...
    沈念sama閱讀 90,522評論 3 385
  • 文/潘曉璐 我一進(jìn)店門,熙熙樓的掌柜王于貴愁眉苦臉地迎上來莫鸭,“玉大人闹丐,你說我怎么就攤上這事”灰颍” “怎么了妇智?”我有些...
    開封第一講書人閱讀 157,852評論 0 348
  • 文/不壞的土叔 我叫張陵,是天一觀的道長氏身。 經(jīng)常有香客問我,道長惑畴,這世上最難降的妖魔是什么蛋欣? 我笑而不...
    開封第一講書人閱讀 56,621評論 1 284
  • 正文 為了忘掉前任,我火速辦了婚禮如贷,結(jié)果婚禮上陷虎,老公的妹妹穿的比我還像新娘到踏。我一直安慰自己,他們只是感情好尚猿,可當(dāng)我...
    茶點(diǎn)故事閱讀 65,741評論 6 386
  • 文/花漫 我一把揭開白布窝稿。 她就那樣靜靜地躺著,像睡著了一般凿掂。 火紅的嫁衣襯著肌膚如雪伴榔。 梳的紋絲不亂的頭發(fā)上,一...
    開封第一講書人閱讀 49,929評論 1 290
  • 那天庄萎,我揣著相機(jī)與錄音踪少,去河邊找鬼。 笑死糠涛,一個胖子當(dāng)著我的面吹牛援奢,可吹牛的內(nèi)容都是我干的。 我是一名探鬼主播忍捡,決...
    沈念sama閱讀 39,076評論 3 410
  • 文/蒼蘭香墨 我猛地睜開眼集漾,長吁一口氣:“原來是場噩夢啊……” “哼靡羡!你這毒婦竟也來了崩侠?” 一聲冷哼從身側(cè)響起狰晚,我...
    開封第一講書人閱讀 37,803評論 0 268
  • 序言:老撾萬榮一對情侶失蹤芙粱,失蹤者是張志新(化名)和其女友劉穎恩够,沒想到半個月后渊跋,有當(dāng)?shù)厝嗽跇淞掷锇l(fā)現(xiàn)了一具尸體翩伪,經(jīng)...
    沈念sama閱讀 44,265評論 1 303
  • 正文 獨(dú)居荒郊野嶺守林人離奇死亡凡辱,尸身上長有42處帶血的膿包…… 初始之章·張勛 以下內(nèi)容為張勛視角 年9月15日...
    茶點(diǎn)故事閱讀 36,582評論 2 327
  • 正文 我和宋清朗相戀三年侨舆,在試婚紗的時候發(fā)現(xiàn)自己被綠了秒紧。 大學(xué)時的朋友給我發(fā)了我未婚夫和他白月光在一起吃飯的照片。...
    茶點(diǎn)故事閱讀 38,716評論 1 341
  • 序言:一個原本活蹦亂跳的男人離奇死亡挨下,死狀恐怖熔恢,靈堂內(nèi)的尸體忽然破棺而出,到底是詐尸還是另有隱情臭笆,我是刑警寧澤叙淌,帶...
    沈念sama閱讀 34,395評論 4 333
  • 正文 年R本政府宣布,位于F島的核電站愁铺,受9級特大地震影響鹰霍,放射性物質(zhì)發(fā)生泄漏。R本人自食惡果不足惜茵乱,卻給世界環(huán)境...
    茶點(diǎn)故事閱讀 40,039評論 3 316
  • 文/蒙蒙 一茂洒、第九天 我趴在偏房一處隱蔽的房頂上張望。 院中可真熱鬧瓶竭,春花似錦督勺、人聲如沸渠羞。這莊子的主人今日做“春日...
    開封第一講書人閱讀 30,798評論 0 21
  • 文/蒼蘭香墨 我抬頭看了看天上的太陽次询。三九已至,卻和暖如春瓷叫,著一層夾襖步出監(jiān)牢的瞬間屯吊,已是汗流浹背。 一陣腳步聲響...
    開封第一講書人閱讀 32,027評論 1 266
  • 我被黑心中介騙來泰國打工赞辩, 沒想到剛下飛機(jī)就差點(diǎn)兒被人妖公主榨干…… 1. 我叫王不留雌芽,地道東北人。 一個月前我還...
    沈念sama閱讀 46,488評論 2 361
  • 正文 我出身青樓辨嗽,卻偏偏與公主長得像世落,于是被迫代替她去往敵國和親。 傳聞我的和親對象是個殘疾皇子糟需,可洞房花燭夜當(dāng)晚...
    茶點(diǎn)故事閱讀 43,612評論 2 350

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

  • Android 自定義View的各種姿勢1 Activity的顯示之ViewRootImpl詳解 Activity...
    passiontim閱讀 171,838評論 25 707
  • ¥開啟¥ 【iAPP實(shí)現(xiàn)進(jìn)入界面執(zhí)行逐一顯】 〖2017-08-25 15:22:14〗 《//首先開一個線程屉佳,因...
    小菜c閱讀 6,373評論 0 17
  • 9月15中秋節(jié)了,團(tuán)圓洲押,月餅武花,賞月,這都是中秋的節(jié)目杈帐。 臺風(fēng)也在這中秋佳節(jié)來湊熱鬧了体箕,突降的暴雨,濃濃的寒意襲來挑童。...
    _非閱讀 121評論 0 0
  • 眾籌-資源共享累铅、資源整合 每個眾籌項目的中心思想都在以上兩點(diǎn),方可以順利進(jìn)行下去站叼,眾籌一個高大上的咖啡館大家共享娃兽、...
    ZSZ朱閱讀 189評論 0 0
  • 我們好好努力,認(rèn)真工作尽楔,專注用心各自生活投储,然后,有時間阔馋,我們一起去旅行吧B贶瘛!呕寝!
    骨朵昀閱讀 177評論 0 0