recovery 流程学习总结(二) 博客分类: Android Recovery android
程序员文章站
2024-03-02 10:21:04
...
1引言
1.1目的
对学习的Android Recovery模式及OTA升级过程进行总结,为加深理解和防止以后遗忘,所以写这篇文档进行一个总结和梳理,以便日后查阅回顾。文档主要包括两部分,第一部分为恢复出厂设置过程,第二部分为Recovery模式下OTA升级包安装过程的分析以及遇到的问题总结。
1.2适用范围
1.3参考文献
内容主要来自自己的总结,知识库中的经验案例和网络上的一些公开资源。
2.进入recovery的方式
我公司手机一般正确手动进入recovery模式的方式为:power+volume up+volume down
手机开机后,硬件系统上电,完成一系列的初始化工作:CPU、串口、终端、timer、DDR等硬件设备,然后加载bootloader,为后面内核加载做准备工作。在系统启动初始化完成后系统检测进入哪一种工作模式,这一部分代码的源文件在\bootable\bootloader\lk\app\aboot\aboot.c文件的aboot_init()函数中:
检测用户关机方式,如果是强制关机,则进入normal_boot模式
if (is_user_force_reset())
goto normal_boot;
检测音量上下键的按键状态,判断进入何种模式:
if (keys_get_state(KEY_VOLUMEUP) && keys_get_state(KEY_VOLUMEDOWN))
{
dprintf(ALWAYS,"dload mode key sequence detected\n");
if (set_download_mode(EMERGENCY_DLOAD))
{
dprintf(CRITICAL,"dload mode not supported by target\n");
}
else
{
reboot_device(DLOAD);
dprintf(CRITICAL,"Failed to reboot into dload mode\n");
}
boot_into_fastboot = true;
}
if (!boot_into_fastboot)
{
if (keys_get_state(KEY_HOME) || keys_get_state(KEY_VOLUMEUP))
boot_into_recovery = 1;
if (!boot_into_recovery &&
(keys_get_state(KEY_BACK) || keys_get_state(KEY_VOLUMEDOWN)))
boot_into_fastboot = true;
}
根据以上代码,开机过程中按home键或者音量上键会进入recovery模式,按back键或者音量下键会进入fastboot模式。
如果没有组合键(代码中称为magic key)按下,则会检测SMEM(在后头会介绍SMEM的的来源) 中的reboot_mode 变量值。
reboot_mode = check_reboot_mode();
hard_reboot_mode = check_hard_reboot_mode();
if (reboot_mode == RECOVERY_MODE ||
hard_reboot_mode == RECOVERY_HARD_RESET_MODE) {
boot_into_recovery = 1;
} else if(reboot_mode == FASTBOOT_MODE ||
hard_reboot_mode == FASTBOOT_HARD_RESET_MODE) {
boot_into_fastboot = true;
} else if(reboot_mode == ALARM_BOOT ||
hard_reboot_mode == RTC_HARD_RESET_MODE) {
boot_reason_alarm = true;
}
reboot_mode 可取的值宏定义为:
#define FASTBOOT_MODE 0x77665500
#define RECOVERY_MODE 0x77665502
而check_reboot_mode 函数定义在\bootable\bootloader\lk\target\ msm8916\init.c 中,函数先读取restart_reason_addr 处的数值,定义为0x2A05F65C,没有采取宏定义,属于不规范的表达。读取完该值之后,在该地址写入0x00,即擦除其内容,以防下次启动又进入recovery 模式。
unsigned check_reboot_mode(void)
{
uint32_t restart_reason = 0;
/* Read reboot reason and scrub it */
restart_reason = readl(RESTART_REASON_ADDR);
writel(0x00, RESTART_REASON_ADDR);
return restart_reason;
}
如果reboot_mode 的值没有定义,则读取MISC 分区的BCB 进行判断,调用函数为recovery_init(),其实现在\bootable\bootloader\lk\app\aboot\recovery.c 中,函数先通过调用get_recovery_message()把BCB 读到recovery_message 结构体中,再读取其command 字段。如果字段是“boot-recovery”,则进入recovery 模式;如果是“update-radio”,则进入固件升级流程。
系统启动流程分析图
如果以上条件皆不满足,则进入正常启动序列,系统会加载boot.img文件,然后加载kernel,在内核加载完成之后,会根据内核的传递参数寻找android的第一个用户进程,即init进程,该进程根据init.rc以及init.$(hardware).rc脚本文件来启动android的必要的服务,直到完成android系统的启动。
当进入recovery模式时,系统加载的是recovery.img文件,该文件内容与boot.img类似,也包含了标准的内核和根文件系统。但是recovery.img为了具有恢复系统的能力,比普通的boot.img目录结构中:
1、多了/res/images目录,在这个目录下的图片是恢复时我们看到的背景画面。
2、多了/sbin/recovery二进制程序,这个就是恢复用的程序。
3、/sbin/adbd不一样,recovery模式下的adbd不支持shell。
4、初始化程序(init)和初始化配置文件(init.rc)都不一样。这就是系统没有进入图形界面而进入了类似文本界面,并可以通过简单的组合键进行恢复的原因。
与正常启动系统类似,也是启动内核,然后启动文件系统。在进入文件系统后会执行/init,init的配置文件就是 /init.rc。这个配置文件位于bootable/recovery/etc/init.rc。查看这个文件我们可以看到它做的事情很简单:
1) 设置环境变量。
2) 建立etc连接。
3) 新建目录,备用。
4) 挂载文件系统。
5) 启动recovery(/sbin/recovery)服务。
6) 启动adbd服务(用于调试)。
上文所提到的fastboot模式,即命令或SD卡烧写模式,不加载内核及文件系统,此处可以进行工厂模式的烧写。
综上所述,有三种进入recovery模式的方法,分别是开机时按组合键,写SMEM中的reboot_mode变量值,以及写位于MISC分区的BCB中的command字段。
除了上述方式还可以通过命令进入recovery模式:adb reboot recovery
3.恢复出厂设置的过程
在setting-->备份与重置--->恢复出厂设置--->重置手机--->手机关机--->开机--->进行恢复出厂的操作--->开机流程。
恢复出厂设置其实是擦除用户数据分区,同时删除缓存区。在系统设置中选择“恢复出厂设置”选项后, APK 会发出一个广播:“android.intent.action.MASTER_CLEAR”,接收者是MasterClearReceiver 类,在收到广播之后会开启一个新线程:
public void onReceive(final Context context, final Intent intent) {
……
// The reboot call is blocking, so we need to do it on another thread.
Thread thr = new Thread("Reboot") {
@Override
public void run() {
try {
RecoverySystem.rebootWipeUserData(context, shutdown, reason);
Log.wtf(TAG, "Still running after master clear?!");
} catch (IOException e) {
Slog.e(TAG, "Can't perform master clear/factory reset", e);
} catch (SecurityException e) {
Slog.e(TAG, "Can't perform master clear/factory reset", e);
}
}
};
thr.start();
}
线程中调用了\frameworks\base\core\java\android\os\RecoverySystem.java类的rebootWipeUserData方法:
public static void rebootWipeUserData(Context context, boolean shutdown, String reason) throws IOException {
UserManager um = (UserManager) context.getSystemService(Context.USER_SERVICE);
if (um.hasUserRestriction(UserManager.DISALLOW_FACTORY_RESET)) {
throw new SecurityException("Wiping data is not allowed for this user.");
}
final ConditionVariable condition = new ConditionVariable();
Intent intent = new Intent("android.intent.action.MASTER_CLEAR_NOTIFICATION");
intent.addFlags(Intent.FLAG_RECEIVER_FOREGROUND);
context.sendOrderedBroadcastAsUser(intent, UserHandle.OWNER,
android.Manifest.permission.MASTER_CLEAR,
new BroadcastReceiver() {
@Override
public void onReceive(Context context, Intent intent) {
condition.open();
}
}, null, 0, null, null);
// Block until the ordered broadcast has completed.
condition.block();
String shutdownArg = null;
if (shutdown) {
shutdownArg = "--shutdown_after";
}
String reasonArg = null;
if (!TextUtils.isEmpty(reason)) {
reasonArg = "--reason=" + sanitizeArg(reason);
}
final String localeArg = "--locale=" + Locale.getDefault().toString();
bootCommand(context, shutdownArg, "--wipe_data", reasonArg, localeArg);
}
最终调用bootCommand(context, shutdownArg, "--wipe_data", reasonArg, localeArg);完成擦除数据的操作
bootCommand()函数分析:
/**重启进入recovery系统同时传入提供的参数*/
private static void bootCommand(Context context, String... args) throws IOException {
RECOVERY_DIR.mkdirs(); // In case we need it
COMMAND_FILE.delete(); // In case it's not writable
LOG_FILE.delete();
FileWriter command = new FileWriter(COMMAND_FILE);
//将参数逐行写入command文件中
try {
for (String arg : args) {
if (!TextUtils.isEmpty(arg)) {
command.write(arg);
command.write("\n");
}
}
} finally {
command.close();
}
// 写入cache/recovery/command文件中后,继续执行reboot的后续操作
PowerManager pm = (PowerManager) context.getSystemService(Context.POWER_SERVICE);
//这里执行reboot操作进入recovery模式
pm.reboot(PowerManager.REBOOT_RECOVERY);
throw new IOException("Reboot failed (no permissions?)");
}
这里进入recovery模式的入口在于: frameworks\base\core\java\android\os\PowerManager.java
public void reboot(String reason) {
try {
mService.reboot(false, reason, true);
} catch (RemoteException e) {
}
}
IPowerManager.aidl文件中的方法如下:
void reboot(boolean confirm, String reason, boolean wait);
其中对应的实现位于PowerManagerService.java中的内部类BinderService中:
private final class BinderService extends IPowerManager.Stub {
……
@Override // Binder call
public void reboot(boolean confirm, String reason, boolean wait) {
mContext.enforceCallingOrSelfPermission(android.Manifest.permission.REBOOT, null);
if (PowerManager.REBOOT_RECOVERY.equals(reason)) {
mContext.enforceCallingOrSelfPermission(android.Manifest.permission.RECOVERY, null);
}
final long ident = Binder.clearCallingIdentity();
try {
shutdownOrRebootInternal(false, confirm, reason, wait);
} finally {
Binder.restoreCallingIdentity(ident);
}
}
……
}
bootCommand()经过如下调用后最终到调用到本地JNI 接口rebootNative(),就是\frameworks\base\core\jni\android_os_Power.cpp 中的android_os_Power_reboot 函数。
具体的调用流程如下:
该函数继续调用\system\core\libcutils\android_reboot.c 中的int android_reboot(int cmd, int flags UNUSED, char *arg),该函数根据第一个参数的不同走向不同的分支,
switch (cmd) {
case ANDROID_RB_RESTART:
ret = reboot(RB_AUTOBOOT);
break;
case ANDROID_RB_POWEROFF:
ret = reboot(RB_POWER_OFF);
break;
case ANDROID_RB_RESTART2:
ret = syscall(__NR_reboot,LINUX_REBOOT_MAGIC1, LINUX_REBOOT_MAGIC2, LINUX_REBOOT_CMD_RESTART2, arg);
break;
default:
ret = -1;
}
相关宏定义见\kernel\include\linux\reboot.h。在reboot.h中定义的函数实现在kernel/kernel/sys.c中的函数 SYSCALL_DEFINE4():为了避免误操作而进入 recovery模式,该函数首先检测所谓的魔法参数,之后判断启动命令,如果是 LINUX_REBOOT_CMD_RESTART2,表示用所给的命令字符串重启系统。调用关系如下:
可以看出,虽然经过了层层调用,但始终有一个内容为“recovery”的字符串参数没有被丢弃过,最后传给了arch_reset(mode, cmd),最终在这里使用:
可以看到,当参数为“recovery”时,会给restart_reason 地址处写入0x77665502。
在代码里有:
45: #define RESTART_REASON_ADDR 0x65C
247: restart_reason = MSM_IMEM_BASE + RESTART_REASON_ADDR;
在\kernel\arch\arm\mach-msm\include\mach\msm_iomap-8x60.h 中有
所以restart_reason 的实际物理地址是0x2A05F000+0x65C =0x2A05F65C,在之前讲到手机启动时, check_reboot_mode 函数会检测SMEM 中的reboot_mode 变量值以判断进入何种工作方式,该函数中读取内存地址0x2A05F65C,这正是arch_reset 函数写入restart_reason 的地址。这样调用pm.reboot(“recovery”) 函数后,最终实现了重启后进入recovery 模式。
4.OTA工作过程
升级所需要的update.zip包来源有两种,一是OTA在线下载(一般下载到/CACHE分区),二是手动拷贝到SD卡中。不论是哪种方式获得update.zip包,在进入Recovery模式前,都未对这个zip包做处理。只是在重启之前将zip包的路径告诉了Recovery服务。
当选择升级后,调用RecoverySystem类的installPackage()方法。这个函数首先根据传过来的包文件,获取这个包文件的绝对路径filename,然后将其拼成arg = “--update_package=” + filename,最终会被写入到BCB中,这个就是重启进入Recovery模式后,Recovery服务要进行的操作。它被传递到函数bootCommand(context,arg),在这个函数中才是Main System在重启前真正做的准备。
Recovery模式主要的执行过程在bootable/recovery/recovery.cpp中,这里从main()函数开始分析
Int main(int argc,char **argv){
…… ……
}
具体执行流程如下:
1、在函数的开始会对argc和argv进行检验:
if (argc == 2 && strcmp(argv[1], "--adbd") == 0) {
adb_main();//进入adb_main()过程
return 0;
}
2、 Load and parse volume data from /etc/recovery.fstab.
void load_volume_table();该函数在bootable/recovery/roots.cpp中,主要功能根据“etc/recovery.fstab”加载和解析分区;
3、确保参数传入的分区是被成功mounted,成功时返回0
ensure_path_mounted(LAST_LOG_FILE);// LAST_LOG_FILE =”/cache/recovery/last_log”存放的是最近一次的recovery过程的日志文件;
4、重命名日志, Rename last_log -> last_log.1 -> last_log.2 -> ... -> last_log.$max
rotate_last_logs(KEEP_LOG_COUNT);// KEEP_LOG_COUNT不超过10个
5、获取命令参数,get_args(&argc, &argv);
如果参数未提供,则从BCB(Bootloader Control Block)查找,如果BCB中也没有则尝试在命令文件中查找相应的命令,将最终获得的参数写进BCB中,直到finish_recovery被调用完,再从BCB中清除。
6、根据命令参数给控制变量赋值,具体过程见如下的循环:
while ((arg = getopt_long(argc, argv, "", OPTIONS, NULL)) != -1) {
switch (arg) {
case 's': send_intent = optarg; break;
case 'u': update_package = optarg; break;
case 'w': wipe_data = wipe_cache = 1; break;
case 'c': wipe_cache = 1; break;
case 't': show_text = 1; break;
case 'x': just_exit = true; break;
case 'l': locale = optarg; break;
case 'g': {
if (stage == NULL || *stage == '\0') {
char buffer[20] = "1/";
strncat(buffer, optarg, sizeof(buffer)-3);
stage = strdup(buffer);
}
break;
}
case 'p': shutdown_after = true; break;
case 'r': reason = optarg; break;
case '?':
LOGE("Invalid command argument\n");
continue;
}
}
其中的OPTIONS定义如下:
static const struct option OPTIONS[] = {
{ "send_intent", required_argument, NULL, 's' },
{ "update_package", required_argument, NULL, 'u' },
{ "wipe_data", no_argument, NULL, 'w' },
{ "wipe_cache", no_argument, NULL, 'c' },
{ "show_text", no_argument, NULL, 't' },
{ "just_exit", no_argument, NULL, 'x' },
{ "locale", required_argument, NULL, 'l' },
{ "stages", required_argument, NULL, 'g' },
{ "shutdown_after", no_argument, NULL, 'p' },
{ "reason", required_argument, NULL, 'r' },
{ NULL, 0, NULL, 0 },
};
此处这样做的原因我认为是Java中swich case中智能使用单个字符,例如’c’,’x’等,通过转换获得其参数命令后给对应的控制变量赋值。
从以上的参数可以判断locale值,从cache中加载现场
if (locale == NULL) {
load_locale_from_cache();
//从cache/recovery/last_locale文件中获得
}
7、初始化UI界面:Recovery 服务使用了一个基于framebuffer 的miniui 系统,ui_init 函数对其进行了简单的初始化。在Recovery 服务的过程中主要用于显示一个背景图片(正在安装或安装失败)和一个进度条(用于显示进度)。另外还启动了两个线程,一个用于处理进度条的显示(progress_thread),另一个用于响应用户的按键(input_thread)。
获得recovery过程使用的device :Device* device = make_device();
进一步获得Recovery_Ui对象:ui=device->GetUI();并对ui进行初始化和现场设置。
在Init()之后调用SetStage(),显示一个状态指示
设置背景SetBackground(RecoveryUI::NONE);
调用显示内容,ShowText(true),内部调用update_screen_locked(),用来重新绘制更新界面。
到这为止做的准备工作基本完成:参数已经被解析、初始化完成、UI被捕获以及绘制,接着进行的recovery核心部分,调用StartRecovery()。
8、执行recovery操作
if (update_package != NULL) {
status = install_package(update_package, &wipe_cache, TEMPORARY_INSTALL_FILE, true);
if (status == INSTALL_SUCCESS && wipe_cache) {
if (erase_volume("/cache")) {
LOGE("Cache wipe (requested by package) failed.");
}
}
if (status != INSTALL_SUCCESS) {
ui->Print("Installation aborted.\n");
// If this is an eng or userdebug build, then automatically
// turn the text display on if the script fails so the error
// message is visible.
char buffer[PROPERTY_VALUE_MAX+1];
property_get("ro.build.fingerprint", buffer, "");
if (strstr(buffer, ":userdebug/") || strstr(buffer, ":eng/")) {
ui->ShowText(true);
}
}
} else if (wipe_data) {
if (device->WipeData()) status = INSTALL_ERROR;
if (erase_volume("/data")) status = INSTALL_ERROR;
if (wipe_cache && erase_volume("/cache")) status = INSTALL_ERROR;
if (status != INSTALL_SUCCESS) ui->Print("Data wipe failed.\n");
} else if (wipe_cache) {
if (wipe_cache && erase_volume("/cache")) status = INSTALL_ERROR;
if (status != INSTALL_SUCCESS) ui->Print("Cache wipe failed.\n");
} else if (!just_exit) {
status = INSTALL_NONE; // No command specified
ui->SetBackground(RecoveryUI::NO_COMMAND);
}
判断update_package是否存在,若存在则调用install_package()进行升级包更新,在此过程如果返回INSTALL_SUCCESS且wipe_cache is true,则执行wipe cache partition;
install_package(update_package, &wipe_cache, TEMPORARY_INSTALL_FILE, true)
如果update_package为NULL,wipe_data为true则进行device->WipeData()、erase_volume(“/data”)、erase_volume(“/cache”)操作;
如果wipe_cache为true,则执行erase_volume(“/cache”);
9、根据6过程中中shutdown_after的赋值以及8过程中返回的更新结果返回的状态status更新后续操作:
Device::BuiltinAction after = shutdown_after ? Device::SHUTDOWN : Device::REBOOT;
if (status != INSTALL_SUCCESS || ui->IsTextVisible()) {
ui->ShowText(true);
Device::BuiltinAction temp = prompt_and_wait(device, status);
if (temp != Device::NO_ACTION) after = temp;
}
10、执行recovery后续工作:清除recovery相关的命令,准备启动系统,并copy日志文件到cache、记录intent跟main system沟通、删除命令文件、卸载cache分区。
finish_recovery(send_intent);
(1) 将intent(字符串)的内容作为参数传进finish_recovery 中。如果有intent 需要告知Main System,则将其写入/cache/recovery/intent 中。
(2) 将内存文件系统中的Recovery 服务的日志(/tmp/recovery.log)拷贝到cache 分区中的 /cache/recovery/log 文件,以便告知重启后的Main System 发生过什么。
(3) 擦除MISC 分区中的BCB 数据块的内容,以便系统重启后不再进入Recovery 模式,而是进入更新后的主系统。
(4) 删除/cache/recovery/command 文件。这一步也是很重要的,因为重启后bootloader 会自动检索这个文件,如果未删除的话又会进入Recovery 模式。
11、根据在9步设置的after的值对手机的后续操作进行设定
switch (after) {
case Device::SHUTDOWN:
ui->Print("Shutting down...\n");
property_set(ANDROID_RB_PROPERTY, "shutdown,");
break;
case Device::REBOOT_BOOTLOADER:
ui->Print("Rebooting to bootloader...\n");
property_set(ANDROID_RB_PROPERTY, "reboot,bootloader");
break;
default:
ui->Print("Rebooting...\n");
property_set(ANDROID_RB_PROPERTY, "reboot,");
break;
}
最后手机进行关机或者重启或者reboot bootloader模式
5.OTA升级过程遇到的问题
在Y72-921QPA机型上有一个OTA升级过程不显示提示语的BUG,问题描述:通过OTA应用检查更新最新的安装包,点击“立即下载”,完成后不点击“立即更新”,直接手动重启手机,此时手机进入recovery模式,但是现在手机界面上小机器人下面不显示“正在系统更新……”的提示语。
分析思路:提示语的种类、有无与locale参数密切的关系。
1、查看OTA升级过程中的日志,看是否有跟语言提示相关的日志信息:
日志路径:Cache/recovery/last_log,搜索关键字“locale”
locale is [(null)]
stage is []
reason is [(null)]
该日志的输出位置位于recovery.cpp中的main方法中:
说明locale参数没有值
2、Locale参数的获取途径有两种:一种是从command文件中通过调用get_args(&argc,
&argv); 获得;一种是通过调用load_locale_from_cache()获得;第二种途径中会从cache/recovery/last_locale文件中获得,通过查看手机刚烧的版本,并无这个文件,得出结论在command文件中并未提供locale参数,问题出现在command文件中。
3、 OTA应用在下载OTA包结束后,会调用系统中recoverysystem.java中的installPackage
接口,在此方法中会通过将包路径和locale参数传入bootCommand()中:
经过多次验证此处逻辑正常,不会出现在command文件中不存在locale参数的问题。
4、 通过与OTA应用的工作人员沟通,我们公司会根据手机中的配置文件是否调用另外
一个接口installPackageForce(),如果手机中存在强制升级标志文件:system/bin/setmisc_recovery,则会在下载OTA包完成后先调用一次installPackageForce接口生成command命令文件,当用户点击立即更新后会再此调用instalPackage接口重新生成command命令文件。
5、 在Y72-921中system/bin下有setmisc_recovery文件,所以OTA应用在OTA包下载完成
后调用installPackageForce接口,生成command文件,当手动重启后进入recovery模式,此时从command文件中没有locale参数,说明调用的installPackageForce接口有问题。在systeminterface.jar中找installPackageForce接口的代码逻辑如下:
public void installPackageForce(String path) throws RemoteException
{
if (checkCallingPermission("yulong.permission.ACCESS_YLPARAMS") != 0) {
throw new SecurityException("getSecrecyState Requires permission yulong.permission.ACCESS_YLPARAMS");
}
try
{
File pkg = new File(path);
String filename = pkg.getCanonicalPath();
Log.i("SystemInterfaceImpl", "!!! REBOOTING TO INSTALL " + filename + " !!!");
String arg;
String arg;
if ("/storage/sdcard0/update.zip".equalsIgnoreCase(filename)) {
filename = "/sdcard/update.zip";
arg = "--update_package=" + filename + "\n--locale=" + Locale.getDefault().toString();
}
else
{
String arg;
if ((Feature.PLATFORM == 7) && (filename.startsWith("/mnt/sdcard/")))
{
filename = "/udisk/" + filename.substring(12);
arg = "--update_package=" + filename; (需要传入locale参数)
} else {
arg = "--update_package=" + filename;(需要传入locale参数)
}
}
RECOVERY_DIR.mkdirs();
COMMAND_FILE.delete();
LOG_FILE.delete();
FileWriter command = new FileWriter(COMMAND_FILE);
try {
command.write(arg);
command.write("\n");
}
finally {
command.close();
}
} catch (IOException e) {
Log.e("SystemInterfaceImpl", "reboot to recovery mode updating may failed", e);
}
Log.d("SystemInterfaceImpl", " CommandFile is OK! ");
String recoveryExec = "/system/bin/setmisc_recovery";
File file = new File(recoveryExec);
if (file.exists()) {
SystemProperties.set("ctl.start", "recovery_service");
SystemProperties.set("yulong.ota.update.force", "1");
} else {
Log.d("SystemInterfaceImpl", "OsInterface " + recoveryExec + " not Exists, Can't reboot to recovery!");
return;
}
}
代码中有两处逻辑没有传入locale参数,导致最终生成command文件中无locale参数。当下载完OTA包时,由于先调用了installPackageForce接口,此时生成的command文件中无locale参数,所以此时不点击“立即更新”再次调用installPackage接口重新生成command文件,直接重启手机后会进入recovery模式,而此时没有locale参数,也无法从last_locale文件中获得,故不显示提示语。
解决办法:在需要修改的地方添加locale参数,保证调用接口时写入完整的command信息,重新集成SystemInterface.jar。
1.1目的
对学习的Android Recovery模式及OTA升级过程进行总结,为加深理解和防止以后遗忘,所以写这篇文档进行一个总结和梳理,以便日后查阅回顾。文档主要包括两部分,第一部分为恢复出厂设置过程,第二部分为Recovery模式下OTA升级包安装过程的分析以及遇到的问题总结。
1.2适用范围
1.3参考文献
内容主要来自自己的总结,知识库中的经验案例和网络上的一些公开资源。
2.进入recovery的方式
我公司手机一般正确手动进入recovery模式的方式为:power+volume up+volume down
手机开机后,硬件系统上电,完成一系列的初始化工作:CPU、串口、终端、timer、DDR等硬件设备,然后加载bootloader,为后面内核加载做准备工作。在系统启动初始化完成后系统检测进入哪一种工作模式,这一部分代码的源文件在\bootable\bootloader\lk\app\aboot\aboot.c文件的aboot_init()函数中:
检测用户关机方式,如果是强制关机,则进入normal_boot模式
if (is_user_force_reset())
goto normal_boot;
检测音量上下键的按键状态,判断进入何种模式:
if (keys_get_state(KEY_VOLUMEUP) && keys_get_state(KEY_VOLUMEDOWN))
{
dprintf(ALWAYS,"dload mode key sequence detected\n");
if (set_download_mode(EMERGENCY_DLOAD))
{
dprintf(CRITICAL,"dload mode not supported by target\n");
}
else
{
reboot_device(DLOAD);
dprintf(CRITICAL,"Failed to reboot into dload mode\n");
}
boot_into_fastboot = true;
}
if (!boot_into_fastboot)
{
if (keys_get_state(KEY_HOME) || keys_get_state(KEY_VOLUMEUP))
boot_into_recovery = 1;
if (!boot_into_recovery &&
(keys_get_state(KEY_BACK) || keys_get_state(KEY_VOLUMEDOWN)))
boot_into_fastboot = true;
}
根据以上代码,开机过程中按home键或者音量上键会进入recovery模式,按back键或者音量下键会进入fastboot模式。
如果没有组合键(代码中称为magic key)按下,则会检测SMEM(在后头会介绍SMEM的的来源) 中的reboot_mode 变量值。
reboot_mode = check_reboot_mode();
hard_reboot_mode = check_hard_reboot_mode();
if (reboot_mode == RECOVERY_MODE ||
hard_reboot_mode == RECOVERY_HARD_RESET_MODE) {
boot_into_recovery = 1;
} else if(reboot_mode == FASTBOOT_MODE ||
hard_reboot_mode == FASTBOOT_HARD_RESET_MODE) {
boot_into_fastboot = true;
} else if(reboot_mode == ALARM_BOOT ||
hard_reboot_mode == RTC_HARD_RESET_MODE) {
boot_reason_alarm = true;
}
reboot_mode 可取的值宏定义为:
#define FASTBOOT_MODE 0x77665500
#define RECOVERY_MODE 0x77665502
而check_reboot_mode 函数定义在\bootable\bootloader\lk\target\ msm8916\init.c 中,函数先读取restart_reason_addr 处的数值,定义为0x2A05F65C,没有采取宏定义,属于不规范的表达。读取完该值之后,在该地址写入0x00,即擦除其内容,以防下次启动又进入recovery 模式。
unsigned check_reboot_mode(void)
{
uint32_t restart_reason = 0;
/* Read reboot reason and scrub it */
restart_reason = readl(RESTART_REASON_ADDR);
writel(0x00, RESTART_REASON_ADDR);
return restart_reason;
}
如果reboot_mode 的值没有定义,则读取MISC 分区的BCB 进行判断,调用函数为recovery_init(),其实现在\bootable\bootloader\lk\app\aboot\recovery.c 中,函数先通过调用get_recovery_message()把BCB 读到recovery_message 结构体中,再读取其command 字段。如果字段是“boot-recovery”,则进入recovery 模式;如果是“update-radio”,则进入固件升级流程。
系统启动流程分析图
如果以上条件皆不满足,则进入正常启动序列,系统会加载boot.img文件,然后加载kernel,在内核加载完成之后,会根据内核的传递参数寻找android的第一个用户进程,即init进程,该进程根据init.rc以及init.$(hardware).rc脚本文件来启动android的必要的服务,直到完成android系统的启动。
当进入recovery模式时,系统加载的是recovery.img文件,该文件内容与boot.img类似,也包含了标准的内核和根文件系统。但是recovery.img为了具有恢复系统的能力,比普通的boot.img目录结构中:
1、多了/res/images目录,在这个目录下的图片是恢复时我们看到的背景画面。
2、多了/sbin/recovery二进制程序,这个就是恢复用的程序。
3、/sbin/adbd不一样,recovery模式下的adbd不支持shell。
4、初始化程序(init)和初始化配置文件(init.rc)都不一样。这就是系统没有进入图形界面而进入了类似文本界面,并可以通过简单的组合键进行恢复的原因。
与正常启动系统类似,也是启动内核,然后启动文件系统。在进入文件系统后会执行/init,init的配置文件就是 /init.rc。这个配置文件位于bootable/recovery/etc/init.rc。查看这个文件我们可以看到它做的事情很简单:
1) 设置环境变量。
2) 建立etc连接。
3) 新建目录,备用。
4) 挂载文件系统。
5) 启动recovery(/sbin/recovery)服务。
6) 启动adbd服务(用于调试)。
上文所提到的fastboot模式,即命令或SD卡烧写模式,不加载内核及文件系统,此处可以进行工厂模式的烧写。
综上所述,有三种进入recovery模式的方法,分别是开机时按组合键,写SMEM中的reboot_mode变量值,以及写位于MISC分区的BCB中的command字段。
除了上述方式还可以通过命令进入recovery模式:adb reboot recovery
3.恢复出厂设置的过程
在setting-->备份与重置--->恢复出厂设置--->重置手机--->手机关机--->开机--->进行恢复出厂的操作--->开机流程。
恢复出厂设置其实是擦除用户数据分区,同时删除缓存区。在系统设置中选择“恢复出厂设置”选项后, APK 会发出一个广播:“android.intent.action.MASTER_CLEAR”,接收者是MasterClearReceiver 类,在收到广播之后会开启一个新线程:
public void onReceive(final Context context, final Intent intent) {
……
// The reboot call is blocking, so we need to do it on another thread.
Thread thr = new Thread("Reboot") {
@Override
public void run() {
try {
RecoverySystem.rebootWipeUserData(context, shutdown, reason);
Log.wtf(TAG, "Still running after master clear?!");
} catch (IOException e) {
Slog.e(TAG, "Can't perform master clear/factory reset", e);
} catch (SecurityException e) {
Slog.e(TAG, "Can't perform master clear/factory reset", e);
}
}
};
thr.start();
}
线程中调用了\frameworks\base\core\java\android\os\RecoverySystem.java类的rebootWipeUserData方法:
public static void rebootWipeUserData(Context context, boolean shutdown, String reason) throws IOException {
UserManager um = (UserManager) context.getSystemService(Context.USER_SERVICE);
if (um.hasUserRestriction(UserManager.DISALLOW_FACTORY_RESET)) {
throw new SecurityException("Wiping data is not allowed for this user.");
}
final ConditionVariable condition = new ConditionVariable();
Intent intent = new Intent("android.intent.action.MASTER_CLEAR_NOTIFICATION");
intent.addFlags(Intent.FLAG_RECEIVER_FOREGROUND);
context.sendOrderedBroadcastAsUser(intent, UserHandle.OWNER,
android.Manifest.permission.MASTER_CLEAR,
new BroadcastReceiver() {
@Override
public void onReceive(Context context, Intent intent) {
condition.open();
}
}, null, 0, null, null);
// Block until the ordered broadcast has completed.
condition.block();
String shutdownArg = null;
if (shutdown) {
shutdownArg = "--shutdown_after";
}
String reasonArg = null;
if (!TextUtils.isEmpty(reason)) {
reasonArg = "--reason=" + sanitizeArg(reason);
}
final String localeArg = "--locale=" + Locale.getDefault().toString();
bootCommand(context, shutdownArg, "--wipe_data", reasonArg, localeArg);
}
最终调用bootCommand(context, shutdownArg, "--wipe_data", reasonArg, localeArg);完成擦除数据的操作
bootCommand()函数分析:
/**重启进入recovery系统同时传入提供的参数*/
private static void bootCommand(Context context, String... args) throws IOException {
RECOVERY_DIR.mkdirs(); // In case we need it
COMMAND_FILE.delete(); // In case it's not writable
LOG_FILE.delete();
FileWriter command = new FileWriter(COMMAND_FILE);
//将参数逐行写入command文件中
try {
for (String arg : args) {
if (!TextUtils.isEmpty(arg)) {
command.write(arg);
command.write("\n");
}
}
} finally {
command.close();
}
// 写入cache/recovery/command文件中后,继续执行reboot的后续操作
PowerManager pm = (PowerManager) context.getSystemService(Context.POWER_SERVICE);
//这里执行reboot操作进入recovery模式
pm.reboot(PowerManager.REBOOT_RECOVERY);
throw new IOException("Reboot failed (no permissions?)");
}
这里进入recovery模式的入口在于: frameworks\base\core\java\android\os\PowerManager.java
public void reboot(String reason) {
try {
mService.reboot(false, reason, true);
} catch (RemoteException e) {
}
}
IPowerManager.aidl文件中的方法如下:
void reboot(boolean confirm, String reason, boolean wait);
其中对应的实现位于PowerManagerService.java中的内部类BinderService中:
private final class BinderService extends IPowerManager.Stub {
……
@Override // Binder call
public void reboot(boolean confirm, String reason, boolean wait) {
mContext.enforceCallingOrSelfPermission(android.Manifest.permission.REBOOT, null);
if (PowerManager.REBOOT_RECOVERY.equals(reason)) {
mContext.enforceCallingOrSelfPermission(android.Manifest.permission.RECOVERY, null);
}
final long ident = Binder.clearCallingIdentity();
try {
shutdownOrRebootInternal(false, confirm, reason, wait);
} finally {
Binder.restoreCallingIdentity(ident);
}
}
……
}
bootCommand()经过如下调用后最终到调用到本地JNI 接口rebootNative(),就是\frameworks\base\core\jni\android_os_Power.cpp 中的android_os_Power_reboot 函数。
具体的调用流程如下:
该函数继续调用\system\core\libcutils\android_reboot.c 中的int android_reboot(int cmd, int flags UNUSED, char *arg),该函数根据第一个参数的不同走向不同的分支,
switch (cmd) {
case ANDROID_RB_RESTART:
ret = reboot(RB_AUTOBOOT);
break;
case ANDROID_RB_POWEROFF:
ret = reboot(RB_POWER_OFF);
break;
case ANDROID_RB_RESTART2:
ret = syscall(__NR_reboot,LINUX_REBOOT_MAGIC1, LINUX_REBOOT_MAGIC2, LINUX_REBOOT_CMD_RESTART2, arg);
break;
default:
ret = -1;
}
相关宏定义见\kernel\include\linux\reboot.h。在reboot.h中定义的函数实现在kernel/kernel/sys.c中的函数 SYSCALL_DEFINE4():为了避免误操作而进入 recovery模式,该函数首先检测所谓的魔法参数,之后判断启动命令,如果是 LINUX_REBOOT_CMD_RESTART2,表示用所给的命令字符串重启系统。调用关系如下:
可以看出,虽然经过了层层调用,但始终有一个内容为“recovery”的字符串参数没有被丢弃过,最后传给了arch_reset(mode, cmd),最终在这里使用:
可以看到,当参数为“recovery”时,会给restart_reason 地址处写入0x77665502。
在代码里有:
45: #define RESTART_REASON_ADDR 0x65C
247: restart_reason = MSM_IMEM_BASE + RESTART_REASON_ADDR;
在\kernel\arch\arm\mach-msm\include\mach\msm_iomap-8x60.h 中有
所以restart_reason 的实际物理地址是0x2A05F000+0x65C =0x2A05F65C,在之前讲到手机启动时, check_reboot_mode 函数会检测SMEM 中的reboot_mode 变量值以判断进入何种工作方式,该函数中读取内存地址0x2A05F65C,这正是arch_reset 函数写入restart_reason 的地址。这样调用pm.reboot(“recovery”) 函数后,最终实现了重启后进入recovery 模式。
4.OTA工作过程
升级所需要的update.zip包来源有两种,一是OTA在线下载(一般下载到/CACHE分区),二是手动拷贝到SD卡中。不论是哪种方式获得update.zip包,在进入Recovery模式前,都未对这个zip包做处理。只是在重启之前将zip包的路径告诉了Recovery服务。
当选择升级后,调用RecoverySystem类的installPackage()方法。这个函数首先根据传过来的包文件,获取这个包文件的绝对路径filename,然后将其拼成arg = “--update_package=” + filename,最终会被写入到BCB中,这个就是重启进入Recovery模式后,Recovery服务要进行的操作。它被传递到函数bootCommand(context,arg),在这个函数中才是Main System在重启前真正做的准备。
Recovery模式主要的执行过程在bootable/recovery/recovery.cpp中,这里从main()函数开始分析
Int main(int argc,char **argv){
…… ……
}
具体执行流程如下:
1、在函数的开始会对argc和argv进行检验:
if (argc == 2 && strcmp(argv[1], "--adbd") == 0) {
adb_main();//进入adb_main()过程
return 0;
}
2、 Load and parse volume data from /etc/recovery.fstab.
void load_volume_table();该函数在bootable/recovery/roots.cpp中,主要功能根据“etc/recovery.fstab”加载和解析分区;
3、确保参数传入的分区是被成功mounted,成功时返回0
ensure_path_mounted(LAST_LOG_FILE);// LAST_LOG_FILE =”/cache/recovery/last_log”存放的是最近一次的recovery过程的日志文件;
4、重命名日志, Rename last_log -> last_log.1 -> last_log.2 -> ... -> last_log.$max
rotate_last_logs(KEEP_LOG_COUNT);// KEEP_LOG_COUNT不超过10个
5、获取命令参数,get_args(&argc, &argv);
如果参数未提供,则从BCB(Bootloader Control Block)查找,如果BCB中也没有则尝试在命令文件中查找相应的命令,将最终获得的参数写进BCB中,直到finish_recovery被调用完,再从BCB中清除。
6、根据命令参数给控制变量赋值,具体过程见如下的循环:
while ((arg = getopt_long(argc, argv, "", OPTIONS, NULL)) != -1) {
switch (arg) {
case 's': send_intent = optarg; break;
case 'u': update_package = optarg; break;
case 'w': wipe_data = wipe_cache = 1; break;
case 'c': wipe_cache = 1; break;
case 't': show_text = 1; break;
case 'x': just_exit = true; break;
case 'l': locale = optarg; break;
case 'g': {
if (stage == NULL || *stage == '\0') {
char buffer[20] = "1/";
strncat(buffer, optarg, sizeof(buffer)-3);
stage = strdup(buffer);
}
break;
}
case 'p': shutdown_after = true; break;
case 'r': reason = optarg; break;
case '?':
LOGE("Invalid command argument\n");
continue;
}
}
其中的OPTIONS定义如下:
static const struct option OPTIONS[] = {
{ "send_intent", required_argument, NULL, 's' },
{ "update_package", required_argument, NULL, 'u' },
{ "wipe_data", no_argument, NULL, 'w' },
{ "wipe_cache", no_argument, NULL, 'c' },
{ "show_text", no_argument, NULL, 't' },
{ "just_exit", no_argument, NULL, 'x' },
{ "locale", required_argument, NULL, 'l' },
{ "stages", required_argument, NULL, 'g' },
{ "shutdown_after", no_argument, NULL, 'p' },
{ "reason", required_argument, NULL, 'r' },
{ NULL, 0, NULL, 0 },
};
此处这样做的原因我认为是Java中swich case中智能使用单个字符,例如’c’,’x’等,通过转换获得其参数命令后给对应的控制变量赋值。
从以上的参数可以判断locale值,从cache中加载现场
if (locale == NULL) {
load_locale_from_cache();
//从cache/recovery/last_locale文件中获得
}
7、初始化UI界面:Recovery 服务使用了一个基于framebuffer 的miniui 系统,ui_init 函数对其进行了简单的初始化。在Recovery 服务的过程中主要用于显示一个背景图片(正在安装或安装失败)和一个进度条(用于显示进度)。另外还启动了两个线程,一个用于处理进度条的显示(progress_thread),另一个用于响应用户的按键(input_thread)。
获得recovery过程使用的device :Device* device = make_device();
进一步获得Recovery_Ui对象:ui=device->GetUI();并对ui进行初始化和现场设置。
在Init()之后调用SetStage(),显示一个状态指示
设置背景SetBackground(RecoveryUI::NONE);
调用显示内容,ShowText(true),内部调用update_screen_locked(),用来重新绘制更新界面。
到这为止做的准备工作基本完成:参数已经被解析、初始化完成、UI被捕获以及绘制,接着进行的recovery核心部分,调用StartRecovery()。
8、执行recovery操作
if (update_package != NULL) {
status = install_package(update_package, &wipe_cache, TEMPORARY_INSTALL_FILE, true);
if (status == INSTALL_SUCCESS && wipe_cache) {
if (erase_volume("/cache")) {
LOGE("Cache wipe (requested by package) failed.");
}
}
if (status != INSTALL_SUCCESS) {
ui->Print("Installation aborted.\n");
// If this is an eng or userdebug build, then automatically
// turn the text display on if the script fails so the error
// message is visible.
char buffer[PROPERTY_VALUE_MAX+1];
property_get("ro.build.fingerprint", buffer, "");
if (strstr(buffer, ":userdebug/") || strstr(buffer, ":eng/")) {
ui->ShowText(true);
}
}
} else if (wipe_data) {
if (device->WipeData()) status = INSTALL_ERROR;
if (erase_volume("/data")) status = INSTALL_ERROR;
if (wipe_cache && erase_volume("/cache")) status = INSTALL_ERROR;
if (status != INSTALL_SUCCESS) ui->Print("Data wipe failed.\n");
} else if (wipe_cache) {
if (wipe_cache && erase_volume("/cache")) status = INSTALL_ERROR;
if (status != INSTALL_SUCCESS) ui->Print("Cache wipe failed.\n");
} else if (!just_exit) {
status = INSTALL_NONE; // No command specified
ui->SetBackground(RecoveryUI::NO_COMMAND);
}
判断update_package是否存在,若存在则调用install_package()进行升级包更新,在此过程如果返回INSTALL_SUCCESS且wipe_cache is true,则执行wipe cache partition;
install_package(update_package, &wipe_cache, TEMPORARY_INSTALL_FILE, true)
如果update_package为NULL,wipe_data为true则进行device->WipeData()、erase_volume(“/data”)、erase_volume(“/cache”)操作;
如果wipe_cache为true,则执行erase_volume(“/cache”);
9、根据6过程中中shutdown_after的赋值以及8过程中返回的更新结果返回的状态status更新后续操作:
Device::BuiltinAction after = shutdown_after ? Device::SHUTDOWN : Device::REBOOT;
if (status != INSTALL_SUCCESS || ui->IsTextVisible()) {
ui->ShowText(true);
Device::BuiltinAction temp = prompt_and_wait(device, status);
if (temp != Device::NO_ACTION) after = temp;
}
10、执行recovery后续工作:清除recovery相关的命令,准备启动系统,并copy日志文件到cache、记录intent跟main system沟通、删除命令文件、卸载cache分区。
finish_recovery(send_intent);
(1) 将intent(字符串)的内容作为参数传进finish_recovery 中。如果有intent 需要告知Main System,则将其写入/cache/recovery/intent 中。
(2) 将内存文件系统中的Recovery 服务的日志(/tmp/recovery.log)拷贝到cache 分区中的 /cache/recovery/log 文件,以便告知重启后的Main System 发生过什么。
(3) 擦除MISC 分区中的BCB 数据块的内容,以便系统重启后不再进入Recovery 模式,而是进入更新后的主系统。
(4) 删除/cache/recovery/command 文件。这一步也是很重要的,因为重启后bootloader 会自动检索这个文件,如果未删除的话又会进入Recovery 模式。
11、根据在9步设置的after的值对手机的后续操作进行设定
switch (after) {
case Device::SHUTDOWN:
ui->Print("Shutting down...\n");
property_set(ANDROID_RB_PROPERTY, "shutdown,");
break;
case Device::REBOOT_BOOTLOADER:
ui->Print("Rebooting to bootloader...\n");
property_set(ANDROID_RB_PROPERTY, "reboot,bootloader");
break;
default:
ui->Print("Rebooting...\n");
property_set(ANDROID_RB_PROPERTY, "reboot,");
break;
}
最后手机进行关机或者重启或者reboot bootloader模式
5.OTA升级过程遇到的问题
在Y72-921QPA机型上有一个OTA升级过程不显示提示语的BUG,问题描述:通过OTA应用检查更新最新的安装包,点击“立即下载”,完成后不点击“立即更新”,直接手动重启手机,此时手机进入recovery模式,但是现在手机界面上小机器人下面不显示“正在系统更新……”的提示语。
分析思路:提示语的种类、有无与locale参数密切的关系。
1、查看OTA升级过程中的日志,看是否有跟语言提示相关的日志信息:
日志路径:Cache/recovery/last_log,搜索关键字“locale”
locale is [(null)]
stage is []
reason is [(null)]
该日志的输出位置位于recovery.cpp中的main方法中:
说明locale参数没有值
2、Locale参数的获取途径有两种:一种是从command文件中通过调用get_args(&argc,
&argv); 获得;一种是通过调用load_locale_from_cache()获得;第二种途径中会从cache/recovery/last_locale文件中获得,通过查看手机刚烧的版本,并无这个文件,得出结论在command文件中并未提供locale参数,问题出现在command文件中。
3、 OTA应用在下载OTA包结束后,会调用系统中recoverysystem.java中的installPackage
接口,在此方法中会通过将包路径和locale参数传入bootCommand()中:
经过多次验证此处逻辑正常,不会出现在command文件中不存在locale参数的问题。
4、 通过与OTA应用的工作人员沟通,我们公司会根据手机中的配置文件是否调用另外
一个接口installPackageForce(),如果手机中存在强制升级标志文件:system/bin/setmisc_recovery,则会在下载OTA包完成后先调用一次installPackageForce接口生成command命令文件,当用户点击立即更新后会再此调用instalPackage接口重新生成command命令文件。
5、 在Y72-921中system/bin下有setmisc_recovery文件,所以OTA应用在OTA包下载完成
后调用installPackageForce接口,生成command文件,当手动重启后进入recovery模式,此时从command文件中没有locale参数,说明调用的installPackageForce接口有问题。在systeminterface.jar中找installPackageForce接口的代码逻辑如下:
public void installPackageForce(String path) throws RemoteException
{
if (checkCallingPermission("yulong.permission.ACCESS_YLPARAMS") != 0) {
throw new SecurityException("getSecrecyState Requires permission yulong.permission.ACCESS_YLPARAMS");
}
try
{
File pkg = new File(path);
String filename = pkg.getCanonicalPath();
Log.i("SystemInterfaceImpl", "!!! REBOOTING TO INSTALL " + filename + " !!!");
String arg;
String arg;
if ("/storage/sdcard0/update.zip".equalsIgnoreCase(filename)) {
filename = "/sdcard/update.zip";
arg = "--update_package=" + filename + "\n--locale=" + Locale.getDefault().toString();
}
else
{
String arg;
if ((Feature.PLATFORM == 7) && (filename.startsWith("/mnt/sdcard/")))
{
filename = "/udisk/" + filename.substring(12);
arg = "--update_package=" + filename; (需要传入locale参数)
} else {
arg = "--update_package=" + filename;(需要传入locale参数)
}
}
RECOVERY_DIR.mkdirs();
COMMAND_FILE.delete();
LOG_FILE.delete();
FileWriter command = new FileWriter(COMMAND_FILE);
try {
command.write(arg);
command.write("\n");
}
finally {
command.close();
}
} catch (IOException e) {
Log.e("SystemInterfaceImpl", "reboot to recovery mode updating may failed", e);
}
Log.d("SystemInterfaceImpl", " CommandFile is OK! ");
String recoveryExec = "/system/bin/setmisc_recovery";
File file = new File(recoveryExec);
if (file.exists()) {
SystemProperties.set("ctl.start", "recovery_service");
SystemProperties.set("yulong.ota.update.force", "1");
} else {
Log.d("SystemInterfaceImpl", "OsInterface " + recoveryExec + " not Exists, Can't reboot to recovery!");
return;
}
}
代码中有两处逻辑没有传入locale参数,导致最终生成command文件中无locale参数。当下载完OTA包时,由于先调用了installPackageForce接口,此时生成的command文件中无locale参数,所以此时不点击“立即更新”再次调用installPackage接口重新生成command文件,直接重启手机后会进入recovery模式,而此时没有locale参数,也无法从last_locale文件中获得,故不显示提示语。
解决办法:在需要修改的地方添加locale参数,保证调用接口时写入完整的command信息,重新集成SystemInterface.jar。