简介
第一次接手FOTA ,记录下app 端的实现过程,和一些基本概念,不对的地方,还望指正.
Android FOTA 广义是Android 实现设备系统无线升级的全部过程和手段。如果一个设备无线升级的系统是完备的话,则一个FOTA过程主要包含以下工作:
1. 制作FOTA升级包:升级包一种可供设备实现系统更新升级的一个压缩包文件,解压缩后本质是各种img文件、被执行文件和一些配置信息等。Android升级时
会有对应的程序解析这个文件。
2. 测试FOTA升级包:在发布这个版本之后,需要测试设备是否可以进行无线升级,是否会出现问题。
3. 发布升级包:将FOTA升级包发给客户或者上传客户服务器
其实和app 升级是一个道理,下载-安装-重启生效
升级
google 已经为我们集成了升级方式,AB 升级和Recovery 升级,相对来说recovery 会进入系统recovery mode ,影响用户使用,而AB 升级是无感升级,两个slot 重启切换,不影响用户使用,体验更好.
目前android 11 默认是开启 virtural AB,对于上层来说其实没啥区别,都是调用updata_engine就可以了,参考了一部分文章,两者主要是super 分区的差异
bootloader_a |
bootloader_b |
boot_a |
boot_b |
vendor_boot_a |
vendor_boot_b |
dtbo_a |
dtbo_b |
vbmeta_a |
vbmeta_b |
super |
virtualAB 通过快照方式写入 super 分区,只保留一份
system | product | vendor |
AB 传统分区,两个slot
system_a | product_a | vendor_a |
system_b | product_b | vendor_b |
app 实现
第一次接触的,可以参考packages/apps/Car/SystemUpdater android 系统为我们提供的demo
好了直接上代码,我把安装流程集成到service 里面.actvity 等要是想要进度条,实现 ResultReceiver,传递过来就可以了
/**
安装软件的服务
*/
public class InstallService extends IntentService {
private static final String TAG = "InstallService";
private final UpdateEngine mUpdateEngine = new UpdateEngine();
private final SimUpdateEngineCallback mSimUpdateEngineCallback = new SimUpdateEngineCallback();
public static final String EXTRA_PARAM_RESULT_RECEIVER = "result-receiver";
public InstallService() {
super("InstallService");
}
WeakReference<ResultReceiver> resultCallbackWeak;
@Override
protected void onHandleIntent(Intent intent) {
Log.d(TAG, "On handle intent is called");
spec = new PayloadSpec();
ResultReceiver resultCallback = (ResultReceiver)intent.getParcelableExtra(EXTRA_PARAM_RESULT_RECEIVER);
this.resultCallbackWeak = new WeakReference<ResultReceiver>(resultCallback);
Log.d(TAG, "resultCallback = "+resultCallback);
File otaFile = new File(CommonUtils.INSTALL_PATH);
if(otaFile.exists()){
execute(otaFile);
}
}
PayloadSpec spec;
@Override
public void onCreate() {
super.onCreate();
}
@Override
public void onStart(Intent intent, int startId) {
super.onStart(intent, startId);
}
@Override
public android.os.IBinder onBind(Intent intent) {
return null;
}
android.os.Handler mHandler = new android.os.Handler() {
@Override
public void handleMessage(android.os.Message msg) {
}
};
/**
* Starts InstallService.
*
* @param context application context
* @param resultCallback callback that will be called when the update is ready to be installed
*/
public static void startService(Context context,
ResultReceiver resultCallback) {
Log.d(TAG, "Starting InstallService");
Intent intent = new Intent(context, InstallService.class);
intent.putExtra(EXTRA_PARAM_RESULT_RECEIVER, resultCallback);
context.startService(intent);
}
private void execute(File file){
// Preconditions.checkArgument(files.length > 0, "No file specified");
try {
UpdateParser.ParsedUpdate result = UpdateParser.parse(file);
if (result == null) {
spec.resultCode = MessageContant.FOTA_FILE_INVAlID;
spec.description = "Failed verification";
Log.e(TAG, String.format("Failed verification"));
updateResult(spec);
return;
}
if (!result.isValid()) {
spec.resultCode = MessageContant.FOTA_FILE_INVAlID;
spec.description = "file invalid";
Log.e(TAG, String.format("Failed verification %s", result));
updateResult(spec);
return;
}
if (Log.isLoggable(TAG, Log.INFO)) {
Log.i(TAG, result.toString());
}
mUpdateEngine.bind(mSimUpdateEngineCallback, handler);
mUpdateEngine.applyPayload(
result.mUrl, result.mOffset, result.mSize, result.mProps);
} catch (IOException e) {
Log.e(TAG, String.format("For file %s", file), e);
spec.resultCode = MessageContant.FOTA_FILE_INVAlID;
spec.description = "IOException";
updateResult(spec);
}
}
/** Handles events from the UpdateEngine. */
public class SimUpdateEngineCallback extends UpdateEngineCallback {
@Override
public void onStatusUpdate(int status, float percent) {
Log.d(TAG, String.format("onStatusUpdate %d, Percent %.2f", status, percent));
switch (status) {
case UpdateEngine.UpdateStatusConstants.IDLE:
//todo ,Update status code: update engine is in idle state.
break;
case UpdateEngine.UpdateStatusConstants.CHECKING_FOR_UPDATE:
//todo ,Update status code: update engine is checking for update.
break;
case 11:
//todo ,i do not know why
break;
case UpdateEngine.UpdateStatusConstants.UPDATE_AVAILABLE:
//todo ,Update status code: an update is available.
break;
case UpdateEngine.UpdateStatusConstants.DOWNLOADING:
Log.d(TAG, "UpdateEngine.UpdateStatusConstants.DOWNLOADING progress = "+percent);
int progress = (int) (percent * 100);
spec.resultCode = MessageContant.FOTA_UPDATE_PROGRESS;
spec.progress = progress;
updateResult(spec);
break;
case UpdateEngine.UpdateStatusConstants.VERIFYING:
//todo ,Update status code: update engine is verifying an update.
break;
case UpdateEngine.UpdateStatusConstants.FINALIZING :
//todo Update status code: update engine is finalizing an update.
break;
case UpdateEngine.UpdateStatusConstants.UPDATED_NEED_REBOOT:
spec.resultCode = MessageContant.FOTA_FINISH_REBOOT;
spec.description = "更新成功";
updateResult(spec);
break;
case UpdateEngine.UpdateStatusConstants.REPORTING_ERROR_EVENT:
//出错
// spec.resultCode = MessageContant.FOTA_FILE_INVAlID;
// spec.description = "系统更新失败";
// updateResult(spec);
break;
case UpdateEngine.UpdateStatusConstants.ATTEMPTING_ROLLBACK:
//系统回滚
// spec.resultCode = MessageContant.FOTA_FILE_INVAlID;
// spec.description = "系统更新失败";
// updateResult(spec);
break;
case UpdateEngine.UpdateStatusConstants.DISABLED:
//update engine disable
break;
default:
// spec.resultCode = MessageContant.FOTA_FILE_INVAlID;
// spec.description = "系统更新失败";
// updateResult(spec);
break;
// noop
}
}
@Override
public void onPayloadApplicationComplete(int errorCode) {
Log.w(TAG, String.format("ErrorCodeConstants onPayloadApplicationComplete %d", errorCode));
mUpdateEngine.unbind();
switch(errorCode){
case UpdateEngine.ErrorCodeConstants.SUCCESS:
//todo 升级成功
break;
case UpdateEngine.ErrorCodeConstants.ERROR:
spec.resultCode = MessageContant.FOTA_FILE_INVAlID;
spec.description = "ERROR";
updateResult(spec);
break;
case UpdateEngine.ErrorCodeConstants.NOT_ENOUGH_SPACE:
spec.resultCode = MessageContant.FOTA_NOT_ENOUGH_SPACE;
spec.description = "存储空间不足";
updateResult(spec);
break;
case UpdateEngine.ErrorCodeConstants.PAYLOAD_TIMESTAMP_ERROR:
spec.resultCode = MessageContant.FOTA_FILE_INVAlID;
spec.description = "时间戳不对";
updateResult(spec);
break;
case UpdateEngine.ErrorCodeConstants.PAYLOAD_HASH_MISMATCH_ERROR:
spec.resultCode = MessageContant.FOTA_FILE_INVAlID;
spec.description = "hash值不对";
updateResult(spec);
break;
case UpdateEngine.ErrorCodeConstants.PAYLOAD_SIZE_MISMATCH_ERROR:
spec.resultCode = MessageContant.FOTA_FILE_INVAlID;
spec.description = "大小不对";
updateResult(spec);
break;
case UpdateEngine.ErrorCodeConstants.DOWNLOAD_PAYLOAD_VERIFICATION_ERROR:
spec.resultCode = MessageContant.FOTA_FILE_INVAlID;
spec.description = "签名不对";
updateResult(spec);
break;
case UpdateEngine.ErrorCodeConstants.PAYLOAD_MISMATCHED_TYPE_ERROR:
spec.resultCode = MessageContant.FOTA_FILE_INVAlID;
spec.description = "类型不匹配";
updateResult(spec);
break;
default:
//升级失败
spec.resultCode = MessageContant.FOTA_UPGRADE_FAIL;
spec.description = "系统更新失败";
updateResult(spec);
break;
}
}
}
private Handler handler=new Handler(){
@Override
public void handleMessage(Message msg) {
// switch (msg.what){
// case MessageContant.FOTA_START_UPDATE:
// Log.e(TAG,"FOTA_START_UPDATE");
// break;
// case MessageContant.FOTA_UPDATE_PROGRESS:
// Log.e(TAG,"FOTA_UPDATE_PROGRESS");
// //通过 msg.obj 获取 ProgressBar 的进度,然后显示进度值
// int process = (int) msg.obj;
// break;
// }
super.handleMessage(msg);
}
};
Bundle resultData = new Bundle();
/**
return to UI
*/
private void updateResult(PayloadSpec spec){
//非法安装包,则统一删除
if(spec.description != null){
Log.e(TAG, "spec.description = "+spec.description);
}
if(spec.resultCode == MessageContant.FOTA_FILE_INVAlID){
Log.e(TAG, "parse file error");
UpdateUtil.showUpdateResult(getApplicationContext());
SharedPreferences sp = PreferenceManager.getDefaultSharedPreferences(getApplicationContext());
SharedPreferences.Editor pEdits = sp.edit();
pEdits.putInt ("INSTALL",0);
pEdits.putInt ("DOWNLOADING",0);
pEdits.commit();
}
if(null != resultCallbackWeak.get()){
Log.i(TAG,"updateResult");
resultData.putSerializable("result",spec);
resultCallbackWeak.get().send(0, resultData);
}
}
}
PayloadSpec.java
public class PayloadSpec implements Serializable {
private static final long serialVersionUID = 41043L;
/**
返回结果码
*/
public int resultCode;
/**
描述
*/
public String description;
/**
进度条
*/
public int progress;
}
只需调用
/**
mResultReceiver 为ResultReceiver 实例
*/
InstallService.startService(mContext,mResultReceiver);
我们主要关注几个状态码
case UpdateEngine.UpdateStatusConstants.UPDATED_NEED_REBOOT:
升级成功,提示您是否重启
public void onPayloadApplicationComplete(int errorCode)
中的
case UpdateEngine.ErrorCodeConstants.SUCCESS:
提示升级完成,
升级失败的,主要监听这里面的,这个是最终状态.
OTA 包
整包升级
make otapackage
out/target/product/(device)/(device)ota-eng.xss.zip
编译出整包,优点是只保留最新版本就可以了,缺点是下载包太大.
差分包
差分包顾名思义,就是两个版本的差异,通过以下命令.
./build/make/tools/releasetools/ota_from_target_files.py -v -p ./out/host/linux-x86/ -i old-target-files.zip new-target-files.zip fota.zip
old-target-files.zip
new-target-files.zip 这两个target 包,整编后位置在out/disk 下面
或者out/target/product/(device)/obj/PACKAGING/target_files_intermediates
差分包升级缺点是每次都得备份target 包,优点是差分包比较小,节约流量;
命令升级
为了验证差分包是否ok, 可以先通过命令升级,看看是否ok.
制作完差分包后 adb shell ,输入下面命令
update_engine_client --payload=file:///sdcard/payload.bin --update --headers="
FILE_HASH=osa/esLUKk6WPwoZkIehvz4JB/PVO7j15DEBaHKhr/s=
FILE_SIZE=192242848
METADATA_HASH=pdqbMcHlZpKSPll7XRMUz8wN/demqmgRy3fOIiPpQ50=
METADATA_SIZE=95795"
注意换行,标注红色部分的,需要手动修改对应差分包里面的信息,解压差分包得到下面信息
打开payload_properties.txt 就能看到上面需要的参数信息了,对应填上就可以了,
注意把payload.bin push 到设置升级的位置,我这边选择的是file:///sdcard/payload.bin
错误分析
1,针对http 下载的文件,一定要主要文件权限问题,我这边下载到/data/ota_package/ 后发现文件权限变成,-rw------- 1 system system ,这样升级就会遇到权限问题,其他分组的就没有读写权限.
修改方式:修改下载文件的权限,注意一点要下载之前,就修改,下载后修改就不行了.
public static void changemode(File destFile){
try{
if(!destFile.exists()){
destFile.createNewFile();
}
/* file.createNewFile();*/
destFile.setWritable(Boolean.TRUE);
destFile.setReadable (Boolean.TRUE);
String command = "chmod 666 " + destFile.getAbsolutePath();
Log.i(TAG, "command = " + command);
Runtime runtime = Runtime.getRuntime();
Process proc = runtime.exec(command);
} catch (IOException e) {
Log.i(TAG,"chmod fail!!!!");
e.printStackTrace();
}
}
2,刚开始升级会抛出状态码
onStatusUpdate
2 11 3
其中11 ,我查了值是大小不匹配,我以为是异常情况,认为安装包不对,直接删除了,导致后面更新进行不下去了,莫名其妙
所以onStatusUpdate 中的状态码,我们主要关注reboot 的,即可,
onPayloadApplicationComplete 中返回的,才是真正是否升级成功状态.
参考system/update_engine/common/error_code.h 中状态码
注意事项
1,android 默认升级文件是放在/data/ota_package/,建议保留,但是data 分区随着时间的推移,空间可能不够了,所以,也可以创建一个单独的分区放升级文件 eg:/fota/
2,测试的时候一定要把selinux 关掉,adb shell setenforce 0,可以专注升级流程是否ok,最后通过dmesg ,把权限都加上.