Android9 ab系统OTA升级总结
OTA升级介绍 官方介绍 https://source.android.google.cn/devices/tech/ota/tools#multiple-skus
1.OTA升级包的制作
OTA升级有两种方式,全包升级和差分升级总体升级操作步骤类似
首先需要对代码做一些改动:如下在/build/core/Makefile中添加如下代码:
/build/core/Makefile
2850 $(hide) PATH=$(foreach p,$(INTERNAL_USERIMAGES_BINARY_PATHS),$(p):)$$PATH MKBOOTIMG=$(MKBOOTIMG) \
2851 build/make/tools/releasetools/add_img_to_target_files -a -v -p $(HOST_OUT) $(zip_root)
2852 @# Zip everything up, preserving symlinks and placing META/ files first to
2853 @# help early validation of the .zip file while uploading it.
2854 $(hide) find $(zip_root)/META | sort >$@.list
2855 $(hide) find $(zip_root) -path $(zip_root)/META -prune -o -print | sort >>$@.list
2856 $(hide) $(SOONG_ZIP) -d -o $@ -C $(zip_root) -l $@.list
++++ $(hide) ./build/tools/releasetools/replace_img_from_target_files.py $@ $(PRODUCT_OUT)
2858 .PHONY: target-files-package
2859 target-files-package: $(BUILT_TARGET_FILES_PACKAGE)
因为replace_img_from_target_files.py在源码中不存在,所以需要在build/tools/releasetools/下新建replace_img_from_target_files.py文件内容如下:
"""
Given a target-files zipfile that does contain images (ie, does
have an IMAGES/ top-level subdirectory), replace the images to
the output dir.
Usage: replace_img_from_target_files target_files output
"""
import sys
if sys.hexversion < 0x02070000:
print >> sys.stderr, "Python 2.7 or newer is required."
sys.exit(1)
import errno
import os
import re
import shutil
import subprocess
import tempfile
import zipfile
image_replace_list = ["boot.img","system.img"]
if not hasattr(os, "SEEK_SET"):
os.SEEK_SET = 0
def main(argv):
if len(argv) != 2:
sys.exit(1)
if not os.path.exists(argv[0]):
print "Target file:%s is invalid" % argv[0]
sys.exit(1)
if not os.path.exists(argv[1]):
print "Output dir:%s is invalid" % argv[1]
sys.exit(1)
zf = zipfile.ZipFile(argv[0], 'r')
for img in zf.namelist():
if img.find("IMAGES/") != -1:
if img.find(".img") != -1:
data = zf.read(img)
name = img.replace("IMAGES/", '')
if name in image_replace_list:
print "Replace %s" % name
name = '/'.join((argv[1], name))
file = open(name, "w")
file.write(data)
file.close()
if __name__ == '__main__':
main(sys.argv[1:])
至于为什么要加上上面的代码,我们后面会去介绍
参考 Android OTA差分包升级失败 img sha 验证问题
(1)编译基础版本的系统 在代码库的根目录执行如下指令
source build/envsetup.sh && make dist DIST_DIR=dist_output_v1
上述命令执行成功后会在dist_output_v1 生成如下文件:
其中xxx-ota-eng.yuwei.zip是这个初始版本的全包升级包,你可以用这个包将其他版本升级到此版本.
xxx-target_files-eng.yuwei.zip是生成OTA升级包的资源包。我们生成差分包也是基于这个资源包生成的。
同时请将out/target目录下的系统镜像保存下来,这个是基础版本的镜像例如 boot.img mdtp.img vendor.img system.img persist.img userdata.img
(2)修改系统比如预置一个系统app 接着按照上面的方式生成第二个版本的资源包
make dist DIST_DIR=dist_output_v2
这时你可以在dist_output_v2中看到上面一样的文件。此时,如果你需要全包升级那么需要导出dist_output_v2/xxx-ota-eng.yuwei.zip 即可进行升级,但是如果需要差分包升级那么还需要制作差分包见(3),不需要差分包升级的可以跳过第三步
(3)制作差分包 在代码根目录下执行如下指令即可制作差分包
? ./build/tools/releasetools/ota_from_target_files -v -i dist_output_v1/xxx-target_files-eng.yuwei.zip dist_output_v2/xxx-target_files-eng.yuwei.zip OTA/v1_v2_update.zip
上面这个命令使用的是./build/tools/releasetools/ota_from_target_files这个文件去生成差分包的,-v会把生成差分包过程中的log打印到shell上,-i (–incrementral_from)则是生成差分包的必要选项.总结下来格式就是
ota_from_target_files -v -i 原始版本的target_files.zip 新版本的target_files.zip 要生成的差分包的update.zip
2.制作升级demo app
ab系统的升级需要调用UpdateEngine的applyPayload函数,而这个api是系统api,所以还需要系统权限。
准备工作:
1.系统签名
要想使用Android Studio来调试apk,那么需要将系统签名导入到Android Studio中,步骤如下:
(1) 在代码库根目录新建文件夹 mkdir key
(2) 将系统签名拷贝过去 系统签名一般存放在/build/target/product/security/ 将其中的platform.pk8和platform.x509.pem拷贝到之前创建的目录下
(3)将系统签名的key转换成Android Studio中的keystore;
在拷贝了系统签名的目录下执行如下指令
openssl pkcs8 -in platform.pk8 -inform DER -outform PEM -out shared.priv.pem -nocrypt
openssl pkcs12 -export -in platform.x509.pem -inkey shared.priv.pem -out shared.pk12 -name androiddebugkey
密码都是:android
keytool -importkeystore -deststorepass android -destkeypass android -destkeystore platform.keystore -srckeystore shared.pk12 -srcstoretype PKCS12 -srcstorepass android -alias androiddebugkey
这时会生成一个platform.keystore,把它拷贝到windows系统中
接着时配置AS的环境
(4)在模块级别的build.gradle中添加如下代码
android {
++ signingConfigs {
++ platform {
++ storeFile file('D:\\UbuntuShared\\platform.keystore')
++ storePassword 'android'
++ keyAlias 'androiddebugkey'
++ keyPassword 'android'
++ }
++ }
compileSdk 31
defaultConfig {
applicationId "com.example.otaupgradedemo"
minSdk 28
targetSdk 31
versionCode 1
versionName "1.0"
testInstrumentationRunner "androidx.test.runner.AndroidJUnitRunner"
signingConfig signingConfigs.platform
}
++ buildTypes {
++ release {
++ minifyEnabled false
++ proguardFiles getDefaultProguardFile('proguard-android-optimize.txt'), 'proguard-rules.pro'
++ }
++ debug {
++ signingConfig signingConfigs.platform
++ }
++ }
此时你就可以直接将你的apk当作系统apk来调试
参考 Apk 使用系统签名
2.系统接口库
由于demo apk需要使用到系统的接口,但是sdk是不提供这些接口,所以需要将系统的framework.jar导入到Android Studio中
(1)将代码编译的framework.jar拷贝到windows系统,framework.jar的路径如下:
out/target/common/obj/JAVA_LIBRARIES/framework_intermediates/classes.jar
接着在AndroidStudio中将app切换成project模式,之后将上面的class.jar拷贝到项目的libs目录接着右键点击选择 add as library即可将库导入你的app中
参考 Android 引入framework.jar Demo 源代码如下:
AndroidManifest.xml
<?xml version="1.0" encoding="utf-8"?>
<manifest xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:tools="http://schemas.android.com/tools"
package="com.example.otaupgradedemo"
android:sharedUserId="android.uid.system">
<uses-permission android:name="android.permission.INTERNET"/>
<uses-permission android:name="android.permission.REBOOT" />
<uses-permission android:name="android.permission.RECOVERY" />
<uses-permission android:name="android.permission.WRITE_EXTERNAL_STORAGE" />
<uses-permission android:name="android.permission.READ_EXTERNAL_STORAGE" />
<application
android:allowBackup="true"
android:icon="@mipmap/ic_launcher"
android:label="@string/app_name"
android:supportsRtl="true"
android:theme="@style/Theme.AppCompat">
<activity
android:name=".MainActivity"
android:exported="true">
<intent-filter>
<action android:name="android.intent.action.MAIN" />
<category android:name="android.intent.category.LAUNCHER" />
</intent-filter>
</activity>
</application>
</manifest>
activity_main.xml
<?xml version="1.0" encoding="utf-8"?>
<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:app="http://schemas.android.com/apk/res-auto"
xmlns:tools="http://schemas.android.com/tools"
android:layout_width="match_parent"
android:layout_height="match_parent"
android:orientation="vertical"
tools:context=".MainActivity">
<Button
android:id="@+id/upgrade"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:text="升级"
android:onClick="update"
android:enabled="true" />
<Button
android:id="@+id/verify"
android:onClick="verify"
android:text="绑定UpdateEngine服务"
android:layout_width="wrap_content"
android:layout_height="wrap_content" />
<Button
android:onClick="prase"
android:text="解析zip文件"
android:layout_width="wrap_content"
android:layout_height="wrap_content" />
</LinearLayout>
MainActivity.java
package com.example.otaupgradedemo;
import androidx.appcompat.app.AppCompatActivity;
import android.app.ProgressDialog;
import android.content.Context;
import android.content.DialogInterface;
import android.os.PowerManager;
import android.os.UpdateEngine;
import android.os.UpdateEngineCallback;
import android.os.Build;
import android.os.Bundle;
import android.os.Environment;
import android.os.RecoverySystem;
import android.util.Log;
import android.view.View;
import android.widget.Toast;
import java.io.File;
import java.io.IOException;
import java.net.MalformedURLException;
import java.security.GeneralSecurityException;
import java.text.DecimalFormat;
public class MainActivity extends AppCompatActivity {
public static final String TAG = "YW_OTA";
private PowerManager powerManager;
UpdateEngine mUpdateEngine = new UpdateEngine();
public String OTA_PACKAGE = "/data/ota_package"+File.separator+"update.zip";
UpdateEngineCallback mUpdateEngineCallback = new UpdateEngineCallback() {
@Override
public void onStatusUpdate(int status, float percent) {
Log.d(TAG, "onStatusUpdate status: " + status);
switch (status) {
case UpdateEngine.UpdateStatusConstants.UPDATED_NEED_REBOOT:
progressDialog.dismiss();
rebootNow();
break;
case UpdateEngine.UpdateStatusConstants.DOWNLOADING:// 回调状态,升级进度
progressDialog.setProgress((int) (percent * 100));
DecimalFormat df = new DecimalFormat("#");
String progress = df.format(percent * 100);
Log.d(TAG, "update progress: " + progress);
break;
default:
}
}
@Override
public void onPayloadApplicationComplete(int errorCode) {
progressDialog.cancel();
Log.d(TAG, "onPayloadApplicationComplete errorCode=" + errorCode);
if (errorCode == UpdateEngine.ErrorCodeConstants.SUCCESS) {
Log.d(TAG, "UPDATE SUCCESS!");
}
}
};
File upgradePackage;
UpdateParser.ParsedUpdate parsedUpdate;
ProgressDialog progressDialog;
@Override
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
setContentView(R.layout.activity_main);
String packagePath =OTA_PACKAGE;
Log.d("YW","patch = "+packagePath);
upgradePackage = new File(packagePath);
powerManager = (PowerManager) getSystemService(Context.POWER_SERVICE);
}
public void update(View view) {
if(parsedUpdate == null){
Toast.makeText(this, "please click prase button first", Toast.LENGTH_SHORT).show();
return;
}
progressDialog = ProgressDialog.show(this,"update system","updating ...");
progressDialog.setMax(100);
mUpdateEngine.applyPayload(
parsedUpdate.mUrl, parsedUpdate.mOffset, parsedUpdate.mSize, parsedUpdate.mProps);
}
public void verify(View view) {
boolean success = mUpdateEngine.bind(mUpdateEngineCallback);
Toast.makeText(this, "bindService "+(success?" success !!":"fail !!"), Toast.LENGTH_SHORT).show();
}
private void rebootNow() {
if(upgradePackage.exists()){
upgradePackage.delete();
}
Log.e("YW", "rebootNow");
powerManager.reboot("systemUpdate");
}
public void prase(View view) {
if(!upgradePackage.exists()){
Log.d("YW","No such file or dir "+upgradePackage.getAbsolutePath());
Toast.makeText(this, "No such file or dir "+upgradePackage.getAbsolutePath(), Toast.LENGTH_SHORT).show();
return;
}
new Thread(new Runnable() {
@Override
public void run() {
try {
parsedUpdate = UpdateParser.parse(upgradePackage);
} catch (IOException e) {
e.printStackTrace();
}
}
}).start();
}
}
解析升级包的内容类UpdateParser.java 参考 /packages/apps/Car/SystemUpdater/src/com/android/car/systemupdater/UpdateParser.java
package com.example.otaupgradedemo;
import android.util.Log;
import java.io.BufferedReader;
import java.io.File;
import java.io.IOException;
import java.io.InputStreamReader;
import java.util.Arrays;
import java.util.Enumeration;
import java.util.Locale;
import java.util.zip.ZipEntry;
import java.util.zip.ZipFile;
class UpdateParser {
private static final String TAG = "UpdateLayoutFragment";
private static final String PAYLOAD_BIN_FILE = "payload.bin";
private static final String PAYLOAD_PROPERTIES = "payload_properties.txt";
private static final String FILE_URL_PREFIX = "file://";
private static final int ZIP_FILE_HEADER = 30;
private UpdateParser() {
}
static ParsedUpdate parse(File file) throws IOException {
long payloadOffset = 0;
long payloadSize = 0;
boolean payloadFound = false;
String[] props = null;
try (ZipFile zipFile = new ZipFile(file)) {
Enumeration<? extends ZipEntry> entries = zipFile.entries();
while (entries.hasMoreElements()) {
ZipEntry entry = entries.nextElement();
long fileSize = entry.getCompressedSize();
if (!payloadFound) {
payloadOffset += ZIP_FILE_HEADER + entry.getName().length();
if (entry.getExtra() != null) {
payloadOffset += entry.getExtra().length;
}
}
if (entry.isDirectory()) {
continue;
} else if (entry.getName().equals(PAYLOAD_BIN_FILE)) {
payloadSize = fileSize;
payloadFound = true;
} else if (entry.getName().equals(PAYLOAD_PROPERTIES)) {
try (BufferedReader buffer = new BufferedReader(
new InputStreamReader(zipFile.getInputStream(entry)))) {
props = buffer.lines().toArray(String[]::new);
}
}
if (!payloadFound) {
payloadOffset += fileSize;
}
if (Log.isLoggable(TAG, Log.DEBUG)) {
Log.d(TAG, String.format("Entry %s", entry.getName()));
}
}
}
return new ParsedUpdate(file, payloadOffset, payloadSize, props);
}
static class ParsedUpdate {
final String mUrl;
final long mOffset;
final long mSize;
final String[] mProps;
ParsedUpdate(File file, long offset, long size, String[] props) {
mUrl = FILE_URL_PREFIX + file.getAbsolutePath();
mOffset = offset;
mSize = size;
mProps = props;
}
boolean isValid() {
return mOffset >= 0 && mSize > 0 && mProps != null;
}
@Override
public String toString() {
return String.format(Locale.getDefault(),
"ParsedUpdate: URL=%s, offset=%d, size=%s, props=%s",
mUrl, mOffset, mSize, Arrays.toString(mProps));
}
}
}
apk的升级demo也可以参考Google的官方demo:https://cs.android.com/android/platform/superproject/+/master:bootable/recovery/updater_sample/
上面的链接需要VPN才能访问
3.升级验证:
1.先将设备烧入基础版本(没有预置三方apk的版本)
2.将升级包push 到data/ota_package目录下
adb root && adb push update.zip data/ota_package
3.关闭系统的selinux权限(在正式版本中需要解决selinux的权限问题)
adb root && adb shell setenforce 0
4.启动demo app,界面如下
先点击解析zip文件,接着点击绑定服务,最后点击升级会出现一个弹框,等待重启,即可升级成功
4.遇到的问题
1.修改了系统app(例如:settings)的app的名字发现升级前后没有改变
将apk反编译后会发现改动是生效的,但是由于launcher的缓存导致升级后改动没有生效,需要将/data/data/com.android.launcher3/databases数据库删除后重启即可看到相应改动
2.在升级的过程中报错
The hash of the source data on disk for this operation doesn’t match the expected value. This could mean that the delta update payload was targeted for another version, or that the source partition was modified after it was installed, for example, by mounting a filesystem.
参考https://blog.csdn.net/qq_27061049/article/details/107388136
大概意思是make dist也会重新打包image导致生成差分包时的源文件和PRODUCT_OUT目录下的image hash值发生了差别继而导致hash值的验证失败
解决办法:本文开头需要添加的代码就是这个问题的解决方法,思路是在生成target_files.zip包时将其中的image替换成PRODUCT_OUT目录下的image。
3.编译过程中的缓存
在编译过程中一定要保证你的前后两个版本时有差异的,必要情况下可以先执行make clean清除out目录下的所有文件再编译新的版本
4.权限问题,最终确定将ota的升级包统一拷贝到data/ota_package目录下最后再交给update_engine去升级这就需要升级应用具有data/ota_package的读写权限
|