1. 程式人生 > >Android 靜默安裝(自動安裝)和靜默解除安裝的實現方法

Android 靜默安裝(自動安裝)和靜默解除安裝的實現方法

1 簡單介紹

目前很多應用市場,做了靜默安裝的功能,靜默安裝就是無聲無息的在後檯安裝apk,沒有任何介面提示。智慧安裝就是有安裝介面,但全部是自動的,不需要使用者去點選。

首先強調兩點:靜默安裝必須要root許可權 智慧安裝必須要使用者手動開啟無障礙服務。

2 原理

靜默安裝、解除安裝的原理就是利用pm install命令來安裝apk,pm uninstall 來解除安裝apk. 智慧安裝是利用android系統提供的無障礙服務AccessibilityService,來模擬使用者點選,從而自動安裝.

3 pm命令介紹

(1) pm install

pm install 命令的用法及引數解釋如下:

pm install [-l][-r] [-t] [-i INSTALLER_PACKAGE_NAME] [-s] [-f] PATH

Options:

  -l: install the package with FORWARD_LOCK.

  -r: reinstall an exisiting app, keeping itsdata.

  -t: allow test .apks to be installed.

  -i: specify the installer package name.

  -s: install package on sdcard.

  -f: install package on internal flash.

(2) pm uninstall

pm uninstall 命令的用法及引數解釋如下:

pm uninstall[-k] PACKAGE

Options:

  -k: keep the data and cache directoriesaround.

上面英語很簡單,不解釋了.

4 靜默安裝

為了方便演示,我把愛奇藝的安裝包重新命名為test.apk後放在了sdcard上。你可以自己去愛奇藝官網去下載,也可以自己找一個apk放到sdcard上,但是要知道apk的包名,後面解除安裝的時候要用到。

先上程式碼:

//靜默安裝

    private void installSlient() {

        String cmd = "pm install -r/mnt/sdcard/test.apk";

        Process process = null;

        DataOutputStream os = null;

        BufferedReader successResult = null;

        BufferedReader errorResult = null;

        StringBuilder successMsg = null;

        StringBuilder errorMsg = null;

        try {

            //靜默安裝需要root許可權

            process = Runtime.getRuntime().exec("su");

            os = newDataOutputStream(process.getOutputStream());

            os.write(cmd.getBytes());

            os.writeBytes("\n");

            os.writeBytes("exit\n");

            os.flush();

            //執行命令

            process.waitFor();

            //獲取返回結果

            successMsg = new StringBuilder();

            errorMsg = new StringBuilder();

            successResult = newBufferedReader(new InputStreamReader(process.getInputStream()));

            errorResult = newBufferedReader(new InputStreamReader(process.getErrorStream()));

            String s;

            while ((s =successResult.readLine()) != null) {

                successMsg.append(s);

            }

            while ((s = errorResult.readLine())!= null) {

                errorMsg.append(s);

            }

        } catch (Exception e) {

            e.printStackTrace();

        } finally {

            try {

                if (os != null) {

                    os.close();

                }

                if (process != null) {

                    process.destroy();

                }

                if (successResult != null) {

                    successResult.close();

                }

                if (errorResult != null) {

                    errorResult.close();

                }

            } catch (Exception e) {

                e.printStackTrace();

            }

        }

        //顯示結果

        tvTest.setText("成功訊息:" + successMsg.toString() +"\n" + "錯誤訊息: " + errorMsg.toString());

    }

這段程式碼就是在程式中執行pm命令,和在adb下執行 pm install -r /mnt/sdcard/test.apk 效果是一樣的, 關鍵的程式碼是 Runtime.getRuntime().exec(“su”) ,這段程式碼會要求獲取root許可權,所以你的手機必須root,不想root的話,直接用模擬器也可以。

通過 Runtime.getRuntime().exec(“su”) 獲取到 process 物件後就可以寫入命令了,每寫入一條命令就要換行,寫入‘\n’ 即可,最後寫入exit後離開命令執行的環境.

5 靜默解除安裝

靜默解除安裝和靜默安裝是一樣的,只是命令不同,靜默解除安裝需要用到包名,同樣,靜默解除安裝也需要root許可權

看程式碼:

//愛奇藝apk的包名

private staticfinal String PACKAGE_NAME = "com.qiyi.video";

//靜默解除安裝

    private void uninstallSlient() {

        String cmd = "pm uninstall "+ PACKAGE_NAME;

        Process process = null;

        DataOutputStream os = null;

        BufferedReader successResult = null;

        BufferedReader errorResult = null;

        StringBuilder successMsg = null;

        StringBuilder errorMsg = null;

        try {

            //解除安裝也需要root許可權

            process =Runtime.getRuntime().exec("su");

            os = newDataOutputStream(process.getOutputStream());

            os.write(cmd.getBytes());

            os.writeBytes("\n");

            os.writeBytes("exit\n");

            os.flush();

            //執行命令

            process.waitFor();

            //獲取返回結果

            successMsg = new StringBuilder();

            errorMsg = new StringBuilder();

            successResult = newBufferedReader(new InputStreamReader(process.getInputStream()));

            errorResult = newBufferedReader(new InputStreamReader(process.getErrorStream()));

            String s;

            while ((s =successResult.readLine()) != null) {

                successMsg.append(s);

            }

            while ((s = errorResult.readLine())!= null) {

                errorMsg.append(s);

            }

        } catch (Exception e) {

            e.printStackTrace();

        } finally {

            try {

                if (os != null) {

                    os.close();

                }

                if (process != null) {

                    process.destroy();

                }

                if (successResult != null) {

                    successResult.close();

                }

                if (errorResult != null) {

                    errorResult.close();

                }

            } catch (Exception e) {

                e.printStackTrace();

            }

        }

        //顯示結果

        tvTest.setText("成功訊息:" + successMsg.toString() +"\n" + "錯誤訊息: " + errorMsg.toString());

    }

和靜默安裝一樣的程式碼就不解釋了。還有,如果你不知道一個apk的包名,那麼請反編譯後去看AndroidManifest.xml檔案,如果這個檔案開啟全是亂碼,說明是被混淆過的,那麼直接安裝它,然後到/data/data下面去找它的包,當然,手機得root才能進/data/data目錄。

6 智慧安裝

智慧安裝就稍微麻煩點了,原理是用到了android提供的AccessibilityService服務,這個服務可以獲取螢幕上的節點,一個節點也就是一個view,我們寫的xml檔案中每個標籤就是一個節點,然後可以模擬使用者的操作,對這些節點進行點選、滑動等操作。我們就是利用這個原理,來自動點選安裝按鈕的,當然使用這個服務必須使用者手動開啟無障礙服務。下面我們來看具體的實現方法。

(1) 建立AccessibilityService配置檔案

在res目錄下建立xml目錄,然後在xml目錄下建立一個accessibility_service_config.xml檔案,內容如下

res/xml/accessibility_service_config.xml:

accessibilityEventTypes:指定我們在監聽視窗中可以模擬哪些事件,typeAllMask表示所有的事件都能模擬.

accessibilityFeedbackType:指定無障礙服務的反饋方式.

canRetrieveWindowContent:指定是否允許我們的程式讀取視窗中的節點和內容,當然是true.

description: 當用戶手動配置服務時,會顯示給使用者看.

packageNames: 指定我們要監聽哪個應用程式下的視窗活動,這裡寫com.android.packageinstaller表示監聽Android系統的安裝介面。

其餘引數照寫即可。

res/strings.xml:

    SlientInstallTest

    智慧安裝app功能演示

(2) 建立AccessibilityService服務

public classMyAccessibilityService extends AccessibilityService {

    private static final String TAG ="[TAG]";

    private Map handleMap = newHashMap<>();

    @Override

    public voidonAccessibilityEvent(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() {

    }

    //遍歷節點,模擬點選安裝按鈕

    private booleaniterateNodesAndHandle(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++) {

                AccessibilityNodeInfochildNodeInfo = nodeInfo.getChild(i);

                if(iterateNodesAndHandle(childNodeInfo)) {

                    return true;

                }

            }

        }

        return false;

    }

}

當進入apk安裝介面就會回撥onAccessibilityEvent()這個方法,我們只關心TYPE_WINDOW_CONTENT_CHANGED和TYPE_WINDOW_STATE_CHANGED兩個事件,為了防止重複處理事件,用一個map來過濾事件,後面遞迴遍歷節點,找到’安裝’ ‘完成’ ‘確定’ 的按鈕,就點選,由於安裝介面需要滾動一下才能出現安裝按鈕,所以遇到ScrollView的時候就滾動一下.

(3) 在AndroidManifest中配置服務

重點是後面的service標籤:

android:label:這個就是使用者看到的無障礙服務的名稱

android:permission:需要用到BIND_ACCESSIBILITY_SERVICE這個許可權.

action:android.accessibilityservice.AccessibilityService 有了這個action,使用者才能在設定裡面看到我們的服務,否則使用者無法開啟我們寫的服務,也就不能進到我們寫的MyAccessibilityService裡面了.所以,注意不要寫錯了,如果你發現無障礙服務裡面沒有我們寫的服務,請檢查這裡.

(4) 呼叫智慧安裝程式碼

前面準備工作完畢後,現在要用了,呼叫智慧安裝的程式碼如下:

 //智慧安裝

    private void smartInstall() {

        Uri uri = Uri.fromFile(newFile("/mnt/sdcard/test.apk"));

        Intent localIntent = newIntent(Intent.ACTION_VIEW);

        localIntent.setDataAndType(uri,"application/vnd.android.package-archive");

        startActivity(localIntent);

    }

(5) 手動配置智慧安裝服務

程式碼執行之後,還要使用者選擇開啟智慧安裝服務,讓使用者自己去找是不明智的,因此,我們要主動跳到配置介面,程式碼如下:

//跳轉到開啟智慧安裝服務的介面

Intent intent =new Intent(Settings.ACTION_ACCESSIBILITY_SETTINGS);

startActivity(intent);

配置如下圖:

 

看到了嗎,上面顯示的就是Service裡面的label的值,如果你沒有上面的選項,請檢查AndroidManifest裡面Service的配置.

點選’智慧安裝App’,開啟服務,如下圖:

 

其中的提示文字就是我們在res/xml/accessibility_service_config.xml檔案中配置的description屬性

7 只能我們寫的app可以自動安裝

這樣寫完程式碼可以執行,點選按鈕自動安裝sdcard上的test.apk.但是你會發現,所有apk都會自動安裝,這就不符合我們的要求了,我們要求只能通過我們寫的app來自動安裝,其他apk還是要使用者手動去點。怎麼解決這個問題呢?

思路是:在MainActivity中建立一個public static boolean flag,在MyAccessibilityService的onAccessibilityEvent()中加一個flag判斷,然後呼叫智慧安裝前flag設為true,建立apk安裝事件的廣播接收器,當apk安裝完成後,設定falg為false,這樣其他apk就不能自動安裝了,就解決了這個問題

下面上完整程式碼.

8 完整程式碼

app/MainActivity.java:

public classMainActivity extends AppCompatActivity implements View.OnClickListener {

    private static final String TAG ="[TAG][MainActivity]";

    private static final String PACKAGE_NAME ="com.qiyi.video";

    private String apkPath ="/mnt/sdcard/test.apk";

    public static boolean flag = false;//控制只能自己的app才能執行智慧安裝

    private TextView tvTest;

    private MyInstallReceiver receiver;

    @Override

    protected void onCreate(BundlesavedInstanceState) {

        super.onCreate(savedInstanceState);

        setContentView(R.layout.activity_main);

        tvTest = (TextView)findViewById(R.id.tv_test);

       findViewById(R.id.btn_install).setOnClickListener(this);

       findViewById(R.id.btn_uninstall).setOnClickListener(this);

        findViewById(R.id.btn_set).setOnClickListener(this);

       findViewById(R.id.btn_smart_install).setOnClickListener(this);

        //註冊apk安裝監聽

        receiver = new MyInstallReceiver();

        IntentFilter filter = newIntentFilter();

       filter.addAction("android.intent.action.PACKAGE_ADDED");

       filter.addAction("android.intent.action.PACKAGE_REMOVED");

       filter.addDataScheme("package");

        this.registerReceiver(receiver,filter);

    }

    @Override

    public void onClick(View v) {

        switch (v.getId()) {

            //靜默安裝

            case R.id.btn_install:

                installSlient();

                break;

            //靜默解除安裝

            case R.id.btn_uninstall:

                uninstallSlient();

                break;

            //設定無障礙服務

            case R.id.btn_set:

                //跳轉到開啟無障礙服務的介面

                Intent intent = newIntent(Settings.ACTION_ACCESSIBILITY_SETTINGS);

                startActivity(intent);

                break;

            //智慧安裝

            case R.id.btn_smart_install:

                //控制只能自己的app才能智慧安裝

                flag = true;

                smartInstall();

                break;

        }

    }

    //靜默安裝

    private void installSlient() {

        String cmd = "pm install -r/mnt/sdcard/test.apk";

        Process process = null;

        DataOutputStream os = null;

        BufferedReader successResult = null;

        BufferedReader errorResult = null;

        StringBuilder successMsg = null;

        StringBuilder errorMsg = null;

        try {

            //靜默安裝需要root許可權

            process =Runtime.getRuntime().exec("su");

            os = newDataOutputStream(process.getOutputStream());

            os.write(cmd.getBytes());

            os.writeBytes("\n");

            os.writeBytes("exit\n");

            os.flush();

            //執行命令

            process.waitFor();

            //獲取返回結果

            successMsg = new StringBuilder();

            errorMsg = new StringBuilder();

            successResult = newBufferedReader(new InputStreamReader(process.getInputStream()));

            errorResult = newBufferedReader(new InputStreamReader(process.getErrorStream()));

            String s;

            while ((s = successResult.readLine())!= null) {

                successMsg.append(s);

            }

            while ((s = errorResult.readLine())!= null) {

                errorMsg.append(s);

            }

        } catch (Exception e) {

            e.printStackTrace();

        } finally {

            try {

                if (os != null) {

                    os.close();

                }

                if (process != null) {

                    process.destroy();

                }

                if (successResult != null) {

                    successResult.close();

                }

                if (errorResult != null) {

                    errorResult.close();

                }

            } catch (Exception e) {

                e.printStackTrace();

            }

        }

        //顯示結果

        tvTest.setText("成功訊息:" + successMsg.toString() +"\n" + "錯誤訊息: " + errorMsg.toString());

    }

    //靜默解除安裝

    private void uninstallSlient() {

        String cmd = "pm uninstall "+ PACKAGE_NAME;

        Process process = null;

        DataOutputStream os = null;

        BufferedReader successResult = null;

        BufferedReader errorResult = null;

        StringBuilder successMsg = null;

        StringBuilder errorMsg = null;

        try {

            //解除安裝也需要root許可權

            process =Runtime.getRuntime().exec("su");

            os = newDataOutputStream(process.getOutputStream());

            os.write(cmd.getBytes());

            os.writeBytes("\n");

            os.writeBytes("exit\n");

            os.flush();

            //執行命令

            process.waitFor();

            //獲取返回結果

            successMsg = new StringBuilder();

            errorMsg = new StringBuilder();

            successResult = newBufferedReader(new InputStreamReader(process.getInputStream()));

            errorResult = newBufferedReader(new InputStreamReader(process.getErrorStream()));

            String s;

            while ((s =successResult.readLine()) != null) {

                successMsg.append(s);

            }

            while ((s = errorResult.readLine())!= null) {

                errorMsg.append(s);

            }

        } catch (Exception e) {

            e.printStackTrace();

        } finally {

            try {

                if (os != null) {

                    os.close();

                }

                if (process != null) {

                    process.destroy();

                }

                if (successResult != null) {

                    successResult.close();

                }

                if (errorResult != null) {

                    errorResult.close();

                }

            } catch (Exception e) {

                e.printStackTrace();

            }

        }

        //顯示結果

        tvTest.setText("成功訊息:" + successMsg.toString() +"\n" + "錯誤訊息: " + errorMsg.toString());

    }

    //智慧安裝

    private void smartInstall() {

        Uri uri = Uri.fromFile(newFile(apkPath));

        Intent localIntent = newIntent(Intent.ACTION_VIEW);

        localIntent.setDataAndType(uri,"application/vnd.android.package-archive");

        startActivity(localIntent);

    }

    //監聽apk安裝

    private class MyInstallReceiver extendsBroadcastReceiver {

        @Override

        public void onReceive(Context context,Intent intent) {

            if(intent.getAction().equals("android.intent.action.PACKAGE_ADDED")){     // install

                String packageName =intent.getDataString();

                Log.i(TAG, "安裝了 :" + packageName);

                //安裝完畢,設定flag,從而使得其餘的apk不能自動安裝

                flag = false;

            }

            if(intent.getAction().equals("android.intent.action.PACKAGE_REMOVED")){   // uninstall

                String packageName =intent.getDataString();

                Log.i(TAG, "解除安裝了 :" + packageName);

            }

        }

    }

    @Override

    protected void onDestroy() {

        super.onDestroy();

        if (receiver != null) {

            unregisterReceiver(receiver);

        }

    }

}

介面上就三個按鈕

res/layout/activity_main.xml:

服務配置檔案

res/xml/accessibility_service_config.xml

智慧安裝服務

app/MyAccessibilityService.java:

public classMyAccessibilityService extends AccessibilityService {

    private static final String TAG ="[TAG]";

    private Map handleMap = newHashMap<>();

    @Override

    public voidonAccessibilityEvent(AccessibilityEvent event) {

        AccessibilityNodeInfo nodeInfo =event.getSource();

        if (nodeInfo != null &&MainActivity.flag) {

            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() {

    }

    //遍歷節點,模擬點選安裝按鈕

    private booleaniterateNodesAndHandle(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++) {

                AccessibilityNodeInfochildNodeInfo = nodeInfo.getChild(i);

                if(iterateNodesAndHandle(childNodeInfo)) {

                    return true;

                }

            }

        }

        return false;

    }

}

注意:請把自己要安裝的apk放到sdcard上,並且修改程式碼中的apk路徑和包名

9 執行效果

 

10 總結

Android智慧安裝的原理就是利用了類似鉤子的服務,這個服務還可以用於微信搶紅包的開發,怎麼樣,是不是比ios好玩兒的多呢.