之前公司一个项目,项目需求为软件在后台自动更新,有新版本发布则自动下载并安装新版本。通过查阅了大量资料,了解了要想完成这件事情途径有两:
1, app需要拥有系统级别的身份。这就需要在系统源码中获取到系统签名,然后对生成的app进行签名,完了之后才能安装运行在系统上执行静默操作;
2, 在已root系统上app获取到系统root权限,即可执行静默安装的操作。
由于公司的嵌入式设备已root,那么这里我直接选择了后者。
实现获取系统root权限并且进行静默安装的过程其实还是比较简单的,网上也有很多这方面的教程,我这里就简单的说下过程:
1, 检测设备是否root:
public boolean hasRooted() {
if (hasRooted == null) {
for (String path : Constants.SU_BINARY_DIRS) {
File su = new File(path + "/su");
if (su.exists()) {
hasRooted = true;
break;
} else {
hasRooted = false;
}
}
}
return hasRooted;
}
不同的设备su所在地址可能不一样,为了尽量适配所有的设备这里把所有可能的地址放在一个数组里面:
public static final String[] SU_BINARY_DIRS = {
"/system/bin", "/system/sbin", "/system/xbin",
"/vendor/bin", "/sbin"
};
2, 接着获取系统的root权限,将会弹出对话框让用户选择是否授予此应用root权限
public boolean grantPermission() {
if (!hasGivenPermission) {
hasGivenPermission = accessRoot();
lastPermissionCheck = System.currentTimeMillis();
} else {
if (lastPermissionCheck < 0
|| System.currentTimeMillis() - lastPermissionCheck > Constants.PERMISSION_EXPIRE_TIME) {
hasGivenPermission = accessRoot();
lastPermissionCheck = System.currentTimeMillis();
}
}
return hasGivenPermission;
}
3, 接下来执行异步安装即可
runAsyncTask(new AsyncTask<Void, Void, Result>() {
@Override
protected void onPreExecute() {
updateLog("Installing package " + apkPath + " ...........");
super.onPreExecute();
}
@Override
protected Result doInBackground(Void... params) {
return RootManager.getInstance().installPackage(apkPath);
}
@Override
protected void onPostExecute(Result result) {
updateLog("Install " + apkPath + " " + result.getResult()
+ " with the message " + result.getMessage());
super.onPostExecute(result);
}
});
主要方法:
public Result installPackage(String apkPath, String installLocation) {
RootUtils.checkUIThread();//如果是UIthread抛出异常
final ResultBuilder builder = Result.newBuilder(); //运行结果集
if (TextUtils.isEmpty(apkPath)) {
return builder.setFailed().build();
}
String command = Constants.COMMAND_INSTALL;
if (RootUtils.isNeedPathSDK()) { //4.0版本以上必须在命令前加上patch
command = Constants.COMMAND_INSTALL_PATCH + command;
}
command = command + apkPath;
if (TextUtils.isEmpty(installLocation)) {
if (installLocation.equalsIgnoreCase("ex")) { //安装至外置内存
command = command + Constants.COMMAND_INSTALL_LOCATION_EXTERNAL;
} else if (installLocation.equalsIgnoreCase("in")) { //安装至内置内存
command = command + Constants.COMMAND_INSTALL_LOCATION_INTERNAL;
}
}
final StringBuilder infoSb = new StringBuilder();
Command commandImpl = new Command(command) {//装载命令
@Override
public void onUpdate(int id, String message) {
infoSb.append(message + "\n");
}
@Override
public void onFinished(int id) {
String finalInfo = infoSb.toString();
if (TextUtils.isEmpty(finalInfo)) {
builder.setInstallFailed();
} else {
if (finalInfo.contains("success") || finalInfo.contains("Success")) {
builder.setInstallSuccess();
} else if (finalInfo.contains("failed") || finalInfo.contains("FAILED")) {
if (finalInfo.contains("FAILED_INSUFFICIENT_STORAGE")) {
builder.setInsallFailedNoSpace();
} else if (finalInfo.contains("FAILED_INCONSISTENT_CERTIFICATES")) {
builder.setInstallFailedWrongCer();
} else if (finalInfo.contains("FAILED_CONTAINER_ERROR")) {
builder.setInstallFailedWrongCer();
} else {
builder.setInstallFailed();
}
} else {
builder.setInstallFailed();
}
}
}
};
try {
Shell.startRootShell().add(commandImpl).waitForFinish(); //执行
} catch (InterruptedException e) {
e.printStackTrace();
builder.setCommandFailedInterrupted();
} catch (IOException e) {
e.printStackTrace();
builder.setCommandFailed();
} catch (TimeoutException e) {
e.printStackTrace();
builder.setCommandFailedTimeout();
} catch (PermissionException e) {
e.printStackTrace();
builder.setCommandFailedDenied();
}
return builder.build();
}
其实获取了系统的root不仅能执行静默安装,还能执行其他有趣的命令,以下是我收集总结的一些命令:
pm install –r 静默安装
-s 安装APP到SD-CARD
-f 安装APP至phone RAM
pm uninstall 静默卸载
"rm '" + apkPath +"'" 卸载系统app
screencap 系统截屏
ps 进程是否运行
"pidof "+进程名 通过进程名杀死一个进程
"kill "+进程ID 通过进程ID杀死一个进程
"reboot -p" 关机
"reboot" 重启
"reboot recovery" 重启进入Recovery模式
执行命令方法:
public Result runCommand(String command) {
final ResultBuilder builder = Result.newBuilder();
if (TextUtils.isEmpty(command)) {
return builder.setFailed().build();
}
final StringBuilder infoSb = new StringBuilder();
Command commandImpl = new Command(command) {
@Override
public void onUpdate(int id, String message) {
infoSb.append(message + "\n");
}
@Override
public void onFinished(int id) {
builder.setCustomMessage(infoSb.toString());
}
};
try {
Shell.startRootShell().add(commandImpl).waitForFinish();
} catch (InterruptedException e) {
e.printStackTrace();
builder.setCommandFailedInterrupted();
} catch (IOException e) {
e.printStackTrace();
builder.setCommandFailed();
} catch (TimeoutException e) {
e.printStackTrace();
builder.setCommandFailedTimeout();
} catch (PermissionException e) {
e.printStackTrace();
builder.setCommandFailedDenied();
}
return builder.build();
}
继续回到主题《静默安装》,你以为这样就完了,其实还没,这样做是能执行安装的操作,但是安装的不是本身,而是其他app,就是说静默安装执行者的执行对象不能是本身,既然如此,那就必须得借助外部的力量才能对自己完成更新,so,便引入了主、从APP的概念,这里我们把这个需要更新的app作为主APP,然后再添加一个从APP,让从APP对主APP执行一个安装更新的操作就行了,思略良久,一个大致的流程就想出来了:
首先主APP首次安装即把携带的从APP静默安装至系统,并启动,让其在后台运行,当主APP接收到服务器发来的更新指令,则下载新版主APP,然后启动从APP,若启动过程中发现系统中的从APP被误删或者第一次未安装成功则再次执行安装,安装完成后就启动从APP,然后主APP通知从APP执行静默安装操作,由于是两个APP之间的通信,这里就采用了常规的通信方式——广播,通过发送一条安装广播,从APP接收到就对预先下载好的apk进去静默安装,如此一来,主APP就完成了软件自动更新的操作。
//首次安装拷贝install_quite.apk到sdCard根目录,并安装启动运行,这里把从APP放在了主APP的Assets目录下面
if(copyAssetsToFile("install_quite.apk", Environment.getExternalStorageDirectory().getPath() + "/install_quite.apk")){
runAsyncTask(new AsyncTask<Void, Void, Result>() {
@Override
protected void onPreExecute() {
super.onPreExecute();
}
@Override
protected Result doInBackground(Void... params) {
return RootManager.getInstance().installPackage(
Environment.getExternalStorageDirectory().getPath() + "/install_quite.apk");//执行后台安装
}
@Override
protected void onPostExecute(Result result) {
<span style="white-space:pre"> </span> Intent intent = new Intent();
<span style="white-space:pre"> </span> ComponentName cn = new ComponentName("com.yph.install_quite","com.yph.install_quite.MainActivity");
<span style="white-space:pre"> </span> intent.setComponent(cn);
<span style="white-space:pre"> </span> intent.setAction("android.intent.action.MAIN");
try {
startActivity(intent);//启动从APP
} catch (Exception e) {
}
super.onPostExecute(result);
}
});
}
当接收到服务器软件更新的指令,把新版本的apk下载至指定文件夹,然后执行以下启动从APP,发送安装广播等操作:
<span style="white-space:pre"> </span>final Intent intent1 = new Intent();
ComponentName cn = new ComponentName("com.yph.install_quite","com.yph.install_quite.MainActivity");
intent1.setComponent(cn);
intent1.setAction("android.intent.action.MAIN");
try {
startActivity(intent1); //启动 静默安装从app
} catch (Exception e) { //如果系统没安装此 静默安装从app,则会进入catch
//把Assets里的apk拷贝到sdCard,并安装开启运行
if(copyAssetsToFile("install_quite.apk", Environment.getExternalStorageDirectory().getPath() + "/install_quite.apk")){
runAsyncTask(new AsyncTask<Void, Void, Result>() {
@Override
protected void onPreExecute() {
super.onPreExecute();
}
@Override
protected Result doInBackground(Void... params) {
return RootManager.getInstance().installPackage(
Environment.getExternalStorageDirectory().getPath() + "/install_quite.apk");
}
@Override
protected void onPostExecute(Result result) {
Toast.makeText(MainMenu.this, result.getMessage(),Toast.LENGTH_SHORT).show();
try {
startActivity(intent1);
sendBroadcast(new Intent("INSTALL_NEW_PACKAGE"));
} catch (Exception e) {
}
super.onPostExecute(result);
}
});
}
else
Toast.makeText(this, "<span style="font-family: Arial, Helvetica, sans-serif;">Assets</span><span style="font-family: Arial, Helvetica, sans-serif;">内未找到install_quite.apk", Toast.LENGTH_LONG).show();</span>
}
//发广播则告诉它要对主app进行更新了
sendBroadcast(new Intent("INSTALL_NEW_PACKAGE"));
从APP比较简单,主要就一个接收广播和执行操作的service:
public class MainService extends Service {
private BroadcastReceiver myBroadCast = new BroadcastReceiver() {
@Override
public void onReceive(Context context, Intent intent) {
String action = intent.getAction();
if (action.equals("INSTALL_NEW_PACKAGE")) {
Toast.makeText(context, "接收到了一条广播为" + "INSTALL_NEW_PACKAGE",Toast.LENGTH_LONG).show();
runAsyncTask(new AsyncTask<Void, Void, Result>() {
@Override
protected void onPreExecute() {
super.onPreExecute();
}
@Override
protected Result doInBackground(Void... params) {
return RootManager.getInstance().installPackage(
"/sdcard/aaa.apk");
}
@Override
protected void onPostExecute(Result result) {
Toast.makeText(getApplication(), result.getMessage(),Toast.LENGTH_SHORT).show();
startAPP("android_serialport_api.sample");
super.onPostExecute(result);
}
});
}
}
};
@Override
public void onCreate() {
super.onCreate();
}
@Override
public void onDestroy() {
this.unregisterReceiver(myBroadCast);
super.onDestroy();
}
@Override
public int onStartCommand(Intent intent, int flags, int startId) {
//注册广播
IntentFilter myFilter = new IntentFilter();
myFilter.addAction("INSTALL_NEW_PACKAGE");
this.registerReceiver(myBroadCast, myFilter);
flags = START_STICKY;
Toast.makeText(getApplication(), "已经打开service", Toast.LENGTH_LONG).show();
return super.onStartCommand(intent, flags, startId);
}
/*
* 启动一个app
*/
private void startAPP(String packageName) {
try {
Intent intent = this.getPackageManager().getLaunchIntentForPackage(packageName);
startActivity(intent);
} catch (Exception e) {
Toast.makeText(getApplication(), "没有安装", Toast.LENGTH_LONG).show();
}
}
private static final <T> void runAsyncTask(AsyncTask<T, ?, ?> asyncTask,T... params) {
asyncTask.execute(params);
}
@Override
public IBinder onBind(Intent intent) {
return null;
}
}
由于担心客户在不明情况的情况下会误删该从APP,需要对其图标进行隐藏,那么怎么隐藏其图标呢,其实很简单,只要在表单文件中注释category即可
<intent-filter>
<action android:name="android.intent.action.MAIN" />
<!-- <category android:name="android.intent.category.LAUNCHER" /> -->
</intent-filter>
这里还有另外一种利用代码动态隐藏图标的方式供大家参考:
private void setComponentEnabled(Class<?> clazz, boolean enabled) {
final ComponentName c = new ComponentName(this, clazz.getName());
getPackageManager().setComponentEnabledSetting(c,enabled?PackageManager.COMPONENT_ENABLED_STATE_ENABLED:PackageManager.COMPONENT_E<span style="white-space:pre"> </span>NABLED_STATE_DISABLED,PackageManager.DONT_KILL_APP);
}
细心的朋友可能发现了我上面用了两种启动APP方式:即如下两种
1, Intent intent = new Intent();
ComponentName cn = new ComponentName("com.yph.install_quite","com.yph.install_quite.MainActivity");
intent.setComponent(cn);
intent.setAction("android.intent.action.MAIN");
try {
startActivity(intent);
} catch (Exception e) {
Toast.makeText(this, "没有该从APP,请下载安装",Toast.LENGTH_SHORT).show();
}
2,
/**
* 启动一个app
* @author yph
*/
<span style="white-space:pre"> </span>private void startAPP(String packageName) {
try {
Intent intent = this.getPackageManager().getLaunchIntentForPackage(packageName);
startActivity(intent);
} catch (Exception e) {
}
}
这里写说下他们的区别,显然第二种比较简单,只需要传入包名即可,但是其缺陷在于不能启动没有设置category的app,即是不能启动隐藏了图标的app。
OK,以上便是全部内容。