写在前面:
本篇博文所讨论的内容主要是与大家一起讨论Recovery模式本地化显示文本的原理,以及如何使用谷歌提供的recovery_l10n工具实现定制本地化显示的文本。
首先我们来讨论Recovery模式下本地化文本的显示是如何实现的。
先看两张图,相信很多人都很熟悉,第一张是我们恢复出厂设置操作,关机重启进入recovery模式之后所看到的界面,第二张是通过按键进入recovery模式,带有选项菜单的主界面。一般来说普通用户正常的操作是不会看到第二个界面的,而在第一张图片中我们看到,在绿色小机器人下面有一行字符,这行字符就是本文的关键。
图1:恢复出厂设置-擦除数据
图2:recovery模式主界面-选项菜单
其实上面这行文本内容并不是以字符的形式显示的,而是用图片代替,如下图:
图3:本地化文本图片合成
补充一下,就是说当Recovery模式下需要显示这些文本信息的时候,会根据进入recovery模式前的系统语言来从上面这张图片中截取对应语言的文本信息,也就是说这个信息并不是直接用C语言打印输出到屏幕上的。
在Recovery模式下是不支持系统语言库的,但是recovery中文本信息本地化又是与主系统当前语言环境保持同步的,那么,在recovery模式是如何与主系统进行交互的呢?
主系统与recovery通过command文件中特定的参数进行交互的。
首先来看framework/base/core/java/android/os/RecoverySystem.java中的代码片段:
/** RECOVERY_DIR是用来与recovery系统交互的目录,也就是说主系统与recovery系统是通过文件进行交互的. 详情可了解 bootable/recovery/recovery.c. */
private static File RECOVERY_DIR = new File("/cache/recovery");
private static File COMMAND_FILE = new File(RECOVERY_DIR, "command");
/* 安装指定的更新包并进行重启*/
public static void installPackage(Context context, File packageFile)
throws IOException {
String filename = packageFile.getCanonicalPath()//得到更新包路径
......
final String filenameArg = "--update_package=" + filename;//将更新包路径作为参数传递写入Command文件
final String localeArg = "--locale=" + Locale.getDefault().toString();//本地化参数
bootCommand(context, filenameArg, localeArg);//重启,并将参数写入command文件
}
/*擦除data和cache分区的数据并重启*/
public static void rebootWipeUserData(Context context, boolean shutdown, String reason)
throws IOException {
......
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);
}
/*擦除cache分区的数据并重启*/
public static void rebootWipeCache(Context context, String reason) throws IOException {
......
final String localeArg = "--locale=" + Locale.getDefault().toString();//本地化参数
bootCommand(context, "--wipe_cache", reasonArg, localeArg);
}
/*重启进入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();
/*向command文件中写入指定的参数*/
FileWriter command = new FileWriter(COMMAND_FILE);
try {
for (String arg : args) {
if (!TextUtils.isEmpty(arg)) {
command.write(arg);
command.write("\n");
}
}
} finally {
command.close();
}
// Having written the command file, go ahead and reboot
PowerManager pm=(PowerManager)context.getSystemService(Context.POWER_SERVICE);
pm.reboot(PowerManager.REBOOT_RECOVERY);
throw new IOException("Reboot failed (no permissions?)");
}
从上面代码告诉我们,主系统是通过COMMAND_FILE文件的形式与recovery进行交互,根据不同的命令行参数执行不同的操作,如系统升级、恢复出厂设置等。
BCB主要用来加载和启动系统,并且通过读取flash中MISC分区中主系统和recovery的信息,并做出相应处理。BCB既是Bootloader和Recovery的通信接口,也是Bootloader与主系统的接口,为什么呢?我们来了解下BCB的结构体:
struct bootloader_message {
char command[32];
char status[32];
char recovery[1024];
};
从上面一部分我们了解到当系统进行恢复出厂设置操作时会将主系统当前的语言环境作为参数写入/bootable/recovery/command中,而上面结构体中的command区域就是主系统与recovery交互式所操作的区域,当主系统想要进入recovery模式时,会修改MISC分区中command区域并重启,而Bootloader会根据command区域中的信息来决定是进入主系统还是recovery系统。
/system/core/init/signal_handler.c里的wait_for_one_process函数中有如下代码:
android_reboot(ANDROID_RB_RESTART2, 0, "recovery")->
__reboot(LINUX_REBOOT_MAGIC1, LINUX_REBOOT_MAGIC2,
LINUX_REBOOT_CMD_RESTART2, “recovery”);
因此BCB就成了主系统与recovery进行交互的桥梁。
当BCB加载recovery.img来启动recovery模式时,会在recovery的initrc,kernel启动完成后启动第一个recovery服务,也就是recovery.cpp,位于/bootable/recovery/目录下。
在recovery.c中会首先执行get_args函数(如,get_args(&argc, &argv);),用来获取命令行参数,也就是读取/cache/recovery/command中的命令行参数。get_args不仅会获取到命令行参数也会将获取到的参数写入到MISC分区中,这样一旦在执行升级或者恢复出厂设置过程中失败,重启之后依旧会进入recovery模式重新执行之前失败的动作。
Recovery模式是支持command文件中特定参数的,如OPTIONS[]中的定义:
static const struct option OPTIONS[] = {
{ "send_intent", required_argument, NULL, 's' },//向intent文件中写入数据
{ "update_package", required_argument, NULL, 'u' },//验证ota package路径下更新包文件
{ "wipe_data", no_argument, NULL, 'w' },//擦除data分区
{ "wipe_cache", no_argument, NULL, 'c' },//擦除cache分区
{ "show_text", no_argument, NULL, 't' },//显示主菜单
{ "just_exit", no_argument, NULL, 'x' },//退出并重启
{ "locale", required_argument, NULL, 'l' },//本地化
{ NULL, 0, NULL, 0 },
};
当我们恢复出厂设置重启进入recovery模式时会首先执行recovery.cpp下main函数,recovery系统会将/cache/recovery/command中的内容作为命令行参数传递给main函数:
/bootable/recovery/recovery.cpp
int arg;
while ((arg = getopt_long(argc, argv, "", OPTIONS, NULL)) != -1) {
switch (arg) {
case 'p': previous_runs = atoi(optarg); break;
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 '?':
LOGE("Invalid command argument\n");
continue;
}
}
if (locale == NULL) {
load_locale_from_cache();
}
printf("locale is [%s]\n", locale);
Device* device = make_device();
ui = device->GetUI();
ui->Init();
ui->SetLocale(locale);
ui->SetBackground(RecoveryUI::NONE);
if (show_text) ui->ShowText(true);
上面getopt被用来解析命令行选项参数,而getopt_long支持长选项的命令行解析,第三个参数指的就是命令行选项参数所组成的字符串,如果单个字符后面跟了一个冒号则表示该选项后面必须跟一个参数,而参数的指针会复制给optarg。在上面这段代码中,参数“l”指的就是本地化。Recovery系统会根据这个参数(如--local=en)来决定第一张图中的文字对应的时第三张图的哪一部分的。那么我们恢复出厂设置操作的时,command中所对应的参数是什么样子的呢,看下面:
--wipe_data
--locale=en
这样的话,也就解释了case 'l': locale = optarg; break;中变量为何从optarg中取值了。
然后recovery会通过下面一系列的函数调用来实现对预制png图片中对应的文本信息进行截取,如下:
(bootable/recovery/screen_ui.cpp)ScreenRecoveryUI::Init()-->ScreenRecoveryUI::LoadLocalizedBitmap()-->(bootable/recovery/minui/resources.c)res_create_localized_surface();
int res_create_localized_surface(const char* name, gr_surface* pSurface) {
char resPath[256];
GGLSurface* surface = NULL;
int result = 0;
unsigned char header[8];
png_structp png_ptr = NULL;
png_infop info_ptr = NULL;
*pSurface = NULL;
snprintf(resPath, sizeof(resPath)-1, "/res/images/%s.png", name);
resPath[sizeof(resPath)-1] = '\0';
FILE* fp = fopen(resPath, "rb");
if (fp == NULL) {
result = -1;
goto exit;
}
size_t bytesRead = fread(header, 1, sizeof(header), fp);
if (bytesRead != sizeof(header)) {
result = -2;
goto exit;
}
if (png_sig_cmp(header, 0, sizeof(header))) {
result = -3;
goto exit;
}
png_ptr = png_create_read_struct(PNG_LIBPNG_VER_STRING, NULL, NULL, NULL);
if (!png_ptr) {
result = -4;
goto exit;
}
info_ptr = png_create_info_struct(png_ptr);
if (!info_ptr) {
result = -5;
goto exit;
}
if (setjmp(png_jmpbuf(png_ptr))) {
result = -6;
goto exit;
}
png_init_io(png_ptr, fp);
png_set_sig_bytes(png_ptr, sizeof(header));
png_read_info(png_ptr, info_ptr);
size_t width = info_ptr->width;
size_t height = info_ptr->height;
size_t stride = 4 * width;
int color_type = info_ptr->color_type;
int bit_depth = info_ptr->bit_depth;
int channels = info_ptr->channels;
if (!(bit_depth == 8 &&
(channels == 1 && color_type == PNG_COLOR_TYPE_GRAY))) {
return -7;
goto exit;
}
unsigned char* row = malloc(width);
int y;
for (y = 0; y < height; ++y) {
png_read_row(png_ptr, row, NULL);
int w = (row[1] << 8) | row[0];
int h = (row[3] << 8) | row[2];
int len = row[4];
char* loc = row+5;
if (y+1+h >= height || matches_locale(loc)) {//匹配字符
printf(" %20s: %s (%d x %d @ %d)\n", name, loc, w, h, y);
surface = malloc(sizeof(GGLSurface));
if (surface == NULL) {
result = -8;
goto exit;
}
unsigned char* pData = malloc(w*h);
surface->version = sizeof(GGLSurface);
surface->width = w;
surface->height = h;
surface->stride = w; /* Yes, pixels, not bytes */
surface->data = pData;
surface->format = GGL_PIXEL_FORMAT_A_8;
int i;
for (i = 0; i < h; ++i, ++y) {
png_read_row(png_ptr, row, NULL);
memcpy(pData + i*w, row, w);
}
*pSurface = (gr_surface) surface;
break;
} else {
int i;
for (i = 0; i < h; ++i, ++y) {
png_read_row(png_ptr, row, NULL);
}
}
}
exit:
png_destroy_read_struct(&png_ptr, &info_ptr, NULL);
if (fp != NULL) {
fclose(fp);
}
if (result < 0) {
if (surface) {
free(surface);
}
}
return result;
}