很多时候开发的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 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盘挂载的广播
- 收到已挂载的广播后,弹出显示文件浏览的界面,选择安装即可
4.其他升级方法
- web升级app,app作为服务端给给前端上传网页
- pc升级app,好处可以使用广播查询app,不用进行IP输入,但通信交互比web升级稍微复杂
5.总结
- 静默升级应用最好在系统root,或能用系统签名打包,再或者提供sdk支持静默安装时使用,否则最好不用AccessibilityService。因为像华为,小米等定制过的系统,当清除后台时,辅助服务被关闭了,总会提示用户打开,体验不好,在一些原生系统测试(Android5.1)辅助服务开启后一直保持开启,开关机不受影响,除非应用卸载重装。
- 至于选哪种升级方法,要根据实际情况进行选则,比如项目中已经有一套web后台配置系统,这时没必要再写一个别的单独软件进行升级。