Android關於應用升級

很多時候開發的app運行在定製過的設備上,不需要適配各種各樣的系統版本,但是往往沒有外網連接,應用作爲系統的桌面,一直保持運行。這時應用通常選擇本機安裝和遠程升級,以下主要分析用到的關鍵技術點。

1.靜默安裝(系統ROOT的情況下)

  • 接收到升級包後可以進行靜默安裝
/**
     * install slient
     *
     * @param context
     * @param filePath
     * @return 0 means normal, 1 means file not exist, 2 means other exception error
     */
    public static int installSlient(Context context, String filePath) {
        File file = new File(filePath);
        if (filePath == null || filePath.length() == 0 || (file = new File(filePath)) == null || file.length() <= 0
                || !file.exists() || !file.isFile()) {
            return 1;
        }

        String[] args = {"pm", "install", "-r", filePath};
        ProcessBuilder processBuilder = new ProcessBuilder(args);

        Process process = null;
        BufferedReader successResult = null;
        BufferedReader errorResult = null;
        StringBuilder successMsg = new StringBuilder();
        StringBuilder errorMsg = new StringBuilder();
        int result;
        try {
            process = processBuilder.start();
            successResult = new BufferedReader(new InputStreamReader(process.getInputStream()));
            errorResult = new BufferedReader(new InputStreamReader(process.getErrorStream()));
            String s;

            while ((s = successResult.readLine()) != null) {
                successMsg.append(s);
            }

            while ((s = errorResult.readLine()) != null) {
                errorMsg.append(s);
            }
        } catch (IOException e) {
            e.printStackTrace();
            result = 2;
        } catch (Exception e) {
            e.printStackTrace();
            result = 2;
        } finally {
            try {
                if (successResult != null) {
                    successResult.close();
                }
                if (errorResult != null) {
                    errorResult.close();
                }
            } catch (IOException e) {
                e.printStackTrace();
            }
            if (process != null) {
                process.destroy();
            }
        }

        // TODO should add memory is not enough here
        if (successMsg.toString().contains("Success") || successMsg.toString().contains("success")) {
            result = 0;
        } else {
            result = 2;
        }
        Log.d("installSlient", "successMsg:" + successMsg + ", ErrorMsg:" + errorMsg);
        return result;
    }
  • 安裝之後保證新的應用自動啓動起來
    註冊系統廣播監聽安裝完成並啓動:
public class MyReceiver extends BroadcastReceiver {

    private static final String PACKAGE_ID = "mi.com.demo";

    @Override
    public void onReceive(Context context, Intent intent) {

        if (intent.getAction() == null){
            return;
        }

        if (intent.getAction().equals(Intent.ACTION_PACKAGE_REMOVED)) {
            if (intent.getData() == null){
                return;
            }
            String packageName = intent.getData().getSchemeSpecificPart();
            Log.e("MyReceiver","卸載成功"+packageName);

        }
        if (intent.getAction().equals(Intent.ACTION_PACKAGE_REPLACED)) {
            String packageName = intent.getData().getSchemeSpecificPart();
            Log.e("MyReceiver","替換成功"+packageName);

            if (packageName.equals(PACKAGE_ID)){

                Intent newIntent;
                PackageManager packageManager = context.getPackageManager();
                newIntent = packageManager.getLaunchIntentForPackage(packageName);
                if (newIntent == null){
                    return;
                }
                newIntent.setFlags(Intent.FLAG_ACTIVITY_NEW_TASK|Intent.FLAG_ACTIVITY_RESET_TASK_IF_NEEDED | Intent.FLAG_ACTIVITY_CLEAR_TOP) ;
                context.startActivity(newIntent);
                Log.e("MyReceiver","start success !");
            }
        }

    }
}
  • 但是應用在升級的過程中把原來的應用進程完全刪了,所以不會收到系統廣播,這時做法是:把接收廣播的程序放到一個單獨的應用中,並且在每次升級前檢查此應用是否啓動運行

2.(智能安裝)系統在未ROOT的情況下

  • 準確來說系統在未ROOT的情況下實現的不是真正意義上的靜默安裝,而是自動安裝
  • 安裝方法:
        String apkPath = "";
        Uri uri = Uri.fromFile(new File(apkPath));
        Intent localIntent = new Intent(Intent.ACTION_VIEW);
        localIntent.setDataAndType(uri, "application/vnd.android.package-archive");
        startActivity(localIntent);

執行後就出現了下面界面:



無法自動安裝

  • 使用輔助服務AccessibilityService可以模擬操作
public class MyInstallAccessibilityService extends AccessibilityService {

    Map<Integer, Boolean> handledMap = new HashMap<>();

    public static boolean isStop = false;

    @Override
    public void onAccessibilityEvent(AccessibilityEvent event) {
        Log.e("TAG","onAccessibilityEvent");
        if (isStop){
            return;
        }
        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 (handledMap.get(event.getWindowId()) == null) {
                    boolean handled = iterateNodesAndHandle(nodeInfo);
                    if (handled) {
                        handledMap.put(event.getWindowId(), true);
                    }
                }
            }
        }
    }

    private boolean iterateNodesAndHandle(AccessibilityNodeInfo nodeInfo) {
        if (nodeInfo != null) {
            int childCount = nodeInfo.getChildCount();
            if ("android.widget.Button".equals(nodeInfo.getClassName())) {
                String nodeContent = nodeInfo.getText().toString();
                Log.d("TAG", "content is " + nodeContent);
                if ("安裝".equals(nodeContent)
                        || "繼續安裝".equals(nodeContent)
                        || "打開".equals(nodeContent)
                        || "完成".equals(nodeContent)
                        || "確定".equals(nodeContent)) {
                    nodeInfo.performAction(AccessibilityNodeInfo.ACTION_CLICK);
                    return true;
                }
            } 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;
    }

    @Override
    protected void onServiceConnected() {
        super.onServiceConnected();
        Log.e("TAG","onServiceConnected");
    }
}
  • 這個輔助服務同樣要放在一個獨立的應用內,否則自動安裝好後把原來應用清除了,無法再執行打開新應用的操作
  • 由於輔助服務手動打開才能用,所以檢測到未開啓的情況下提示用戶打開,這就要兩個應用之間可以相互通信,推薦使用AIDL進行應用間通信,每次升級前確保AccessibilityService是開啓狀態

3.關於U盤安裝

  • 由於應用會作爲系統的桌面,使用USB進行應用升級時要回到系統桌面找到文件瀏覽器讀取安裝包,這樣的體驗不太好,較好的辦法:應用監聽系統U盤掛載的廣播
<receiver android:name=".common.MyReceiver">
            <intent-filter android:priority="1000">
                <category android:name="android.intent.category.LAUNCHER" />

                <action android:name="android.intent.action.MEDIA_MOUNTED" />
                <action android:name="android.intent.action.MEDIA_UNMOUNTED" />
                <action android:name="android.intent.action.MEDIA_REMOVED" />

                <data android:scheme="file" />
            </intent-filter>
</receiver>
  • 收到已掛載的廣播後,彈出顯示文件瀏覽的界面,選擇安裝即可

4.其他升級方法

  • web升級app,app作爲服務端給給前端上傳網頁
  • pc升級app,好處可以使用廣播查詢app,不用進行IP輸入,但通信交互比web升級稍微複雜

5.總結

  • 靜默升級應用最好在系統root,或能用系統簽名打包,再或者提供sdk支持靜默安裝時使用,否則最好不用AccessibilityService。因爲像華爲,小米等定製過的系統,當清除後臺時,輔助服務被關閉了,總會提示用戶打開,體驗不好,在一些原生系統測試(Android5.1)輔助服務開啓後一直保持開啓,開關機不受影響,除非應用卸載重裝。
  • 至於選哪種升級方法,要根據實際情況進行選則,比如項目中已經有一套web後臺配置系統,這時沒必要再寫一個別的單獨軟件進行升級。
發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章