很多時候開發的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後臺配置系統,這時沒必要再寫一個別的單獨軟件進行升級。