概要
本文讨论Android在recovery模式如何支持多语言的文本显示。涉及如下几个要点:
- 应用层如何传入locale信息;
- recovery各个模块如何共享locale信息;
- 如何根据locale从预置全locale图片中提取特定的文本图片;
- 预置png图片的格式。
问题
手机进入Recovery模式之后,会有安装的小动画、以及文本提示信息,如:
而这个信息并不是直接用C语言输出的字符串。因为在recovery模式下,Android并没有加载语言库。那么Android如何实现多语言的文本显示呢?
总体思路
事实上,Android预先绘制了包括各种语言(严格来讲并不完备)的文本图片,做成一个png文件。然后在recovery初始化的时候,根据当前“语言”而从图片文件中提取出对应该语言的提示信息。
比如,“正在安装系统更新...”就是\bootable\recovery\res\images\installing_text.png中的一部分:
接下来就分析如何从一个完整的png文件中生成“当前语言”所需要的那部分。
Locale
首先谈语言的问题。
在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);
注意其中的locale变量,它就是用于做本地化处理的。——可以简单地认为就是“当前语言”。这个locale是需要从Android应用层作为参数传进来,如“--locale=en_GB”。据此,UI部分根据这个locale从png中加载对应的部分。
UI对应的部分如下,即LoadLocalizedBitmap():
// \bootable\recovery\screen_ui.cpp void ScreenRecoveryUI::LoadLocalizedBitmap(const char* filename, gr_surface* surface) { int result = res_create_localized_surface(filename, surface); if (result < 0) { LOGE("missing bitmap %s\n(Code %d)\n", filename, result); } } void ScreenRecoveryUI::Init() { gr_init(); text_col = text_row = 0; text_rows = gr_fb_height() / CHAR_HEIGHT; if (text_rows > kMaxRows) text_rows = kMaxRows; text_top = 1; text_cols = gr_fb_width() / CHAR_WIDTH; if (text_cols > kMaxCols - 1) text_cols = kMaxCols - 1; LoadBitmap("icon_installing", &backgroundIcon[INSTALLING_UPDATE]); backgroundIcon[ERASING] = backgroundIcon[INSTALLING_UPDATE]; LoadBitmap("icon_error", &backgroundIcon[ERROR]); backgroundIcon[NO_COMMAND] = backgroundIcon[ERROR]; LoadBitmap("progress_empty", &progressBarEmpty); LoadBitmap("progress_fill", &progressBarFill); LoadLocalizedBitmap("installing_text", &backgroundText[INSTALLING_UPDATE]); LoadLocalizedBitmap("erasing_text", &backgroundText[ERASING]); LoadLocalizedBitmap("no_command_text", &backgroundText[NO_COMMAND]); LoadLocalizedBitmap("error_text", &backgroundText[ERROR]);
进一步调用:
// \bootable\recovery\minui\resources.c 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; }
其中有这么一句:
if (y+1+h >= height || matches_locale(loc)) {
这里是读取预置的png图片中的每一部分,然后判断该部分的locale是否和当前locale一致。matches_locale()函数就在刚才res_create_localized_surface()函数的前面,的代码如下:
// \bootable\recovery\minui\resources.c static int matches_locale(const char* loc) { if (locale == NULL) return 0; if (strcmp(loc, locale) == 0) return 1; // if loc does *not* have an underscore, and it matches the start // of locale, and the next character in locale *is* an underscore, // that‘s a match. For instance, loc == "en" matches locale == // "en_US". int i; for (i = 0; loc[i] != 0 && loc[i] != ‘_‘; ++i); if (loc[i] == ‘_‘) return 0; return (strncmp(locale, loc, i) == 0 && locale[i] == ‘_‘); }
在匹配的时候,会比较字符串。需要说明的是,“语言”是一个概念,但国家/地区是另外一个概念,正如English是英语,但又区分美国英语、英国英语,等等。为此Locale包括了getLanguage()、getCountry()等方法,也包括toString()方法。——后者就是language和country的合一。
在Android预置的图片中,每一个小部分图片数据都包括了locale信息(后面会提到),其中一些locale的字符串只有语言部分,有些就是语言+国家代码,从而区分同一语言的不同国家。再比如一个简单的例子,同为中文,却有简体和繁体之分。
这同时也说明了,在Android应用层让手机进入Recovery模式的时候,传入locale要用“语言+国家”的形式,及Locale.toString()方法;否则recovery时显示的字符串可能就不是预期的效果。
在描述了这些概念之后,可以注意以上代码中locale变量的定义&声明方式。该变量在recovery.cpp中定义为全局变量:
char* locale = NULL;
在resources.c中,通过extern声明来使用它:
extern char* locale;
而在screen_ui.cpp中,则局部化了:
void SetLocale(const char* locale);
recovery.cpp中,通过该成员函数来设置locale:
Device* device = make_device(); ui = device->GetUI(); ui->Init(); ui->SetLocale(locale);
提取特定Locale的文本图片
接下来再分析如何从Android预置的包括了“各种”locale的字符串中提取出当前locale的字符串。这就是前面提到的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; }
这段代码看起来很长,实际上一半的代码都是用于png的图片的通常的调用方法。——这在另外一篇文章中介绍。
简单来讲,分为3个部分:
- 通过signature验证是合法的png文件;
- 读取IHDR部分;
- 读取IDAT部分,它包括了所有的locale的图片。
前面两种可以在另外一篇文章中找到相关信息,这里单说IDAT。通过代码可以看到,除了IDAT的Type和CRC部分,剩下的就是每个Locale的子信息。而每个Local的构成又分成两部分:
- 图片基本信息:width, height, length, locale
- 图片数据
其中,width和height是图片数据对应的图片的宽度和高度(pixel),而length是指后面locale字符串的长度。可以看到代码中并没有真正用到len这个信息,因为实际的locale字符串很短,通常最多就5个字符串(再加一个\0结束符)。因此,width, height, length, locale共4种信息放在一个row中绰绰有余。
代码通过循环,遍历每一种locale,直到找到匹配项。如果都不匹配,即查找失败,此时recovery就不会显示对应的文本,即pSurface=NULL。——为此只要注意到函数一开始即设置为NULL即可。
int res_create_localized_surface(const char* name, gr_surface* pSurface) {