Android 9.0 recovery下触屏代码分析
查看了Android 7和Android 8的recovery代码,都没有实现recovery下触屏功能,为什么要有这个功能呢?因为当机器无法正常进入系统之后,会进入recovery模式,让用户选择恢复出厂设置,但是如果机器没有home键和音量键的话就无法进行选择,所以就需要实现recovery下触屏这个功能了。
Android 9.0已经帮我们实现了该功能,我们只需要将/bootable/recovery/ui.cpp中类RecoveryUI的构造函数中把成员变量touch_screen_allowed_设置为true(默认是false),recovery下就能使能TP功能。下面我们就具体来分析一下其运行原理,及实现过程。
首先看到/bootable/recovery/recovery.cpp代码中如下两段代码
片段1
else if (should_prompt_and_wipe_data) {
ui->ShowText(true);
ui->SetBackground(RecoveryUI::ERROR);
if (!prompt_and_wipe_data(device)) {
status = INSTALL_ERROR;
}
ui->ShowText(false);
}
片段2
if ((status == INSTALL_NONE && !sideload_auto_reboot) || ui->IsTextVisible()) {
Device::BuiltinAction temp = prompt_and_wait(device, status);
if (temp != Device::NO_ACTION) {
after = temp;
}
}
这两段代码分别会去调用prompt_and_wipe_data(device)和prompt_and_wait(device, status)接口,这两个接口内部都会去调用get_menu_selection(/* args */)这个接口,从而在recovery下实现选项菜单界面,供用户进行选择。我们先看prompt_and_wipe_data接口中的实现:
static bool prompt_and_wipe_data(Device* device) {
// headers中的描述将会显示在Recovery界面上,用于提示用户
// Use a single string and let ScreenRecoveryUI handles the wrapping.
const char* const headers[] = {
"Can't load Android system. Your data may be corrupt. "
"If you continue to get this message, you may need to "
"perform a factory data reset and erase all user data "
"stored on this device.",
nullptr
};
// items中有两个元素,即后面Recovery界面中呈现出来的菜单选项就只有Try
//again和Factory data reset这两个选项
const char* const items[] = {
"Try again",
"Factory data reset",
NULL
};
// 这里是一个死循环
for (;;) {
// 初始化选择菜单界面,返回值为1就退出,重启系统
int chosen_item = get_menu_selection(headers, items, true, 0, device);
if (chosen_item != 1) {
return true; // Just reboot, no wipe; not a failure, user asked for it
}
// 返回值是其它就进入ask_to_wipe_data函数中,该函数中绘制"yes"、"no"这两
// 个菜单界面,选择"yes"则返回true,否则返回false
if (ask_to_wipe_data(device)) {
// 调用wipe_data函数进行格式化处理
return wipe_data(device);
}
}
}
紧接着我们看get_menu_selection(headers, items, true, 0, device);这里传入5个参数,headers和items用于界面渲染;第三个参数true表示为单选;第四个参数0表示光标的初始位置为第一个菜单选项;第五个参数传入一个Device指针。返回值为选择的菜单序号,或者超时返回-1。如下为函数完整定义/bootable/recovery/recovery.cpp:
// Display a menu with the specified 'headers' and 'items'. Device specific HandleMenuKey() may
// return a positive number beyond the given range. Caller sets 'menu_only' to true to ensure only
// a menu item gets selected. 'initial_selection' controls the initial cursor location. Returns the
// (non-negative) chosen item number, or -1 if timed out waiting for input.
static int get_menu_selection(const char* const* headers, const char* const* items, bool menu_only, int initial_selection, Device* device) {
// Throw away keys pressed previously, so user doesn't accidentally trigger menu items.
// 清空用于存放用户触屏操作的数组,设置其有效长度为0
ui->FlushKeys();
// 开始渲染菜单
ui->StartMenu(headers, items, initial_selection);
int selected = initial_selection;
int chosen_item = -1;
while (chosen_item < 0) {
// 等待用户触屏手势输入,如果返回-1,则为超时未收到用户操作
int key = ui->WaitKey();
if (key == -1) { // WaitKey() timed out.
// 超时之后,如果界面还可见,继续等待用户操作
if (ui->WasTextEverVisible()) {
continue;
} else {
LOG(INFO) << "Timed out waiting for key input; rebooting.";
// 退出菜单,返回-1;
ui->EndMenu();
return -1;
}
}
// 获取log是否输出到屏幕上标志
bool visible = ui->IsTextVisible();
// 处理用户输入的Key是上下选择还是确认选项
int action = device->HandleMenuKey(key, visible);
if (action < 0) {
switch (action) {
// 如果是向上键(向上滑动),则将光标向上移动
case Device::kHighlightUp:
selected = ui->SelectMenu(--selected);
break;
// 如果是向下键(向下滑动),则光标向下移动
case Device::kHighlightDown:
selected = ui->SelectMenu(++selected);
break;
// 如果是按的home键(左右滑动),表示确认,返回当前光标序号
case Device::kInvokeItem:
chosen_item = selected;
break;
case Device::kNoAction:
break;
}
} else if (!menu_only) {
chosen_item = action;
}
}
ui->EndMenu();
return chosen_item;
}
移动光标的代码
int ScreenRecoveryUI::SelectMenu(int sel) {
pthread_mutex_lock(&updateMutex);
if (show_menu) {
int old_sel = menu_sel;
menu_sel = sel;
// Wrap at top and bottom.
if (menu_sel < 0) menu_sel = menu_items - 1;
if (menu_sel >= menu_items) menu_sel = 0;
sel = menu_sel;
if (menu_sel != old_sel) update_screen_locked();
}
pthread_mutex_unlock(&updateMutex);
return sel;
}
结束菜单选项代码
void ScreenRecoveryUI::EndMenu() {
pthread_mutex_lock(&updateMutex);
if (show_menu && text_rows_ > 0 && text_cols_ > 0) {
show_menu = false;
update_screen_locked();
}
pthread_mutex_unlock(&updateMutex);
}
我们看到ui->StartMenu(headers, items, initial_selection);这个接口的定义在/bootable/recovery/screen_ui.cpp中。该接口就将三个参数内容赋值给ScreenRecoveryUI的成员变量,然后调用update_screen_locked()函数将菜单界面绘制出来。注:Recovery界面由一个单独的线程在循环绘制的,所以别的线程在需要更新界面的时候,需要pthread_mutex_lock(&updateMutex)加锁操作,以保证线程同步问题。
void ScreenRecoveryUI::StartMenu(const char* const* headers, const char* const* items, int initial_selection) {
pthread_mutex_lock(&updateMutex);
if (text_rows_ > 0 && text_cols_ > 0) {
menu_headers_ = headers;
menu_.clear();
for (size_t i = 0; i < text_rows_ && items[i] != nullptr; ++i) {
menu_.emplace_back(std::string(items[i], strnlen(items[i], text_cols_ - 1)));
}
menu_items = static_cast<int>(menu_.size());
show_menu = true;
menu_sel = initial_selection;
// 调用该接口实现界面的绘制
update_screen_locked();
}
pthread_mutex_unlock(&updateMutex);
}
函数update_screen_locked这里不讲,后面继续将recovery界面的时候再聊该接口,现在只需要知道,修改增加了界面上的东西之后,调用该接口让界面更新。现在我们说 ui->WaitKey();接口。接口定义在/bootable/recovery/ui.cpp中,完整代码如下:
int RecoveryUI::WaitKey() {
pthread_mutex_lock(&key_queue_mutex);
// 这个UI_WAIT_KEY_TIMEOUT_SEC值为120,即2分钟超时时间
// Time out after UI_WAIT_KEY_TIMEOUT_SEC, unless a USB cable is
// plugged in.
do {
struct timeval now;
struct timespec timeout;
gettimeofday(&now, nullptr);
timeout.tv_sec = now.tv_sec;
timeout.tv_nsec = now.tv_usec * 1000;
timeout.tv_sec += UI_WAIT_KEY_TIMEOUT_SEC;
int rc = 0;
// 这个key_queue_len表示用户的输入key的个数,没有输入则为0
// ETIMEDOUT定义在errno.h中,为pthread_cond_timedwait函数超时未满足条
// 件的返回值。即当无用户输入时并且没有超时时,进入while循环。
while (key_queue_len == 0 && rc != ETIMEDOUT) {
// 这里在等待条件变量key_queue_cond被唤醒,唤醒操作在
// RecoveryUI::EnqueueKey(int key_code)函数中,即当用户有输入时,发出
// 唤醒信号
rc = pthread_cond_timedwait(&key_queue_cond, &key_queue_mutex, &timeout);
}
// 如下这段if嵌套语句表示,当用户没有输入时,没超时2分钟,屏幕亮度逐渐变
// 暗,从正常的normal,到灰色dimmed,再到灭屏off。如果灭屏期间收到用户
// 输入则屏幕亮灭状态由off变为normal,点亮屏幕。向节点
// "/sys/class/leds/lcd-backlight/brightness"写值可以控制屏幕亮灭状态
if (screensaver_state_ != ScreensaverState::DISABLED) {
if (rc == ETIMEDOUT) {
// Lower the brightness level: NORMAL -> DIMMED; DIMMED -> OFF.
if (screensaver_state_ == ScreensaverState::NORMAL) {
if (android::base::WriteStringToFile(std::to_string(brightness_dimmed_value_),
brightness_file_)) {
LOG(INFO) << "Brightness: " << brightness_dimmed_value_ << " (" << brightness_dimmed_
<< "%)";
screensaver_state_ = ScreensaverState::DIMMED;
}
} else if (screensaver_state_ == ScreensaverState::DIMMED) {
if (android::base::WriteStringToFile("0", brightness_file_)) {
LOG(INFO) << "Brightness: 0 (off)";
screensaver_state_ = ScreensaverState::OFF;
}
}
} else if (screensaver_state_ != ScreensaverState::NORMAL) {
// Drop the first key if it's changing from OFF to NORMAL.
if (screensaver_state_ == ScreensaverState::OFF) {
if (key_queue_len > 0) {
memcpy(&key_queue[0], &key_queue[1], sizeof(int) * --key_queue_len);
}
}
// Reset the brightness to normal.
if (android::base::WriteStringToFile(std::to_string(brightness_normal_value_),
brightness_file_)) {
screensaver_state_ = ScreensaverState::NORMAL;
LOG(INFO) << "Brightness: " << brightness_normal_value_ << " (" << brightness_normal_
<< "%)";
}
}
}
// 循环结束条件是,当USB断开或者用户有输入操作
} while (IsUsbConnected() && key_queue_len == 0);
// 将用户输入的key返回回去,或者返回-1
int key = -1;
if (key_queue_len > 0) {
key = key_queue[0];
memcpy(&key_queue[0], &key_queue[1], sizeof(int) * --key_queue_len);
}
pthread_mutex_unlock(&key_queue_mutex);
return key;
}
发送收到用户输入通知的代码
void RecoveryUI::EnqueueKey(int key_code) {
pthread_mutex_lock(&key_queue_mutex);
const int queue_max = sizeof(key_queue) / sizeof(key_queue[0]);
if (key_queue_len < queue_max) {
key_queue[key_queue_len++] = key_code;
pthread_cond_signal(&key_queue_cond);
}
pthread_mutex_unlock(&key_queue_mutex);
}
判断USB是否连接的代码
bool RecoveryUI::IsUsbConnected() {
int fd = open("/sys/class/android_usb/android0/state", O_RDONLY);
if (fd < 0) {
printf("failed to open /sys/class/android_usb/android0/state: %s\n", strerror(errno));
return 0;
}
char buf;
// USB is connected if android_usb state is CONNECTED or CONFIGURED.
int connected = (TEMP_FAILURE_RETRY(read(fd, &buf, 1)) == 1) && (buf == 'C');
if (close(fd) < 0) {
printf("failed to close /sys/class/android_usb/android0/state: %s\n", strerror(errno));
}
return connected;
}
再get_menu_selection接口分析完之后,继续看到prompt_and_wipe_data接口中ask_to_wipe_data函数,功能是显示两行提示语,已经显示yes和no菜单,然后调用get_menu_selection实现该界面。完整代码如下:
static bool ask_to_wipe_data(Device* device) {
return yes_no(device, "Wipe all user data?", " THIS CAN NOT BE UNDONE!");
}
static bool yes_no(Device* device, const char* question1, const char* question2) {
const char* headers[] = { question1, question2, NULL };
const char* items[] = { " No", " Yes", NULL };
int chosen_item = get_menu_selection(headers, items, true, 0, device);
return (chosen_item == 1);
}
当用户选择了yes之后,就调用wipe_data(device);进行格式化系统(这里不进行讨论如何格式化),如果用户选择no则直接返回。至此片段1中if (!prompt_and_wipe_data(device))分析结束。
我们继续看片段2中prompt_and_wait(device, status);接口完整定义如下:
// Returns REBOOT, SHUTDOWN, or REBOOT_BOOTLOADER. Returning NO_ACTION means to take the default,
// which is to reboot or shutdown depending on if the --shutdown_after flag was passed to recovery.
static Device::BuiltinAction prompt_and_wait(Device* device, int status) {
for (;;) {
finish_recovery(nullptr);
switch (status) {
case INSTALL_SUCCESS:
case INSTALL_NONE:
ui->SetBackground(RecoveryUI::NO_COMMAND);
break;
case INSTALL_ERROR:
case INSTALL_CORRUPT:
ui->SetBackground(RecoveryUI::ERROR);
break;
}
ui->SetProgressType(RecoveryUI::EMPTY);
int chosen_item = get_menu_selection(nullptr, device->GetMenuItems(), false, 0, device);
// Device-specific code may take some action here. It may return one of the core actions
// handled in the switch statement below.
Device::BuiltinAction chosen_action =
(chosen_item == -1) ? Device::REBOOT : device->InvokeMenuItem(chosen_item);
bool should_wipe_cache = false;
switch (chosen_action) {
case Device::NO_ACTION:
break;
case Device::REBOOT:
case Device::SHUTDOWN:
case Device::REBOOT_BOOTLOADER:
return chosen_action;
case Device::WIPE_DATA:
if (ui->IsTextVisible()) {
if (ask_to_wipe_data(device)) {
wipe_data(device);
}
} else {
wipe_data(device);
return Device::NO_ACTION;
}
break;
case Device::WIPE_CACHE:
wipe_cache(ui->IsTextVisible(), device);
if (!ui->IsTextVisible()) return Device::NO_ACTION;
break;
case Device::APPLY_ADB_SIDELOAD:
case Device::APPLY_SDCARD:
{
bool adb = (chosen_action == Device::APPLY_ADB_SIDELOAD);
if (adb) {
status = apply_from_adb(&should_wipe_cache, TEMPORARY_INSTALL_FILE);
} else {
status = apply_from_sdcard(device, &should_wipe_cache);
}
if (status == INSTALL_SUCCESS && should_wipe_cache) {
if (!wipe_cache(false, device)) {
status = INSTALL_ERROR;
}
}
if (status != INSTALL_SUCCESS) {
ui->SetBackground(RecoveryUI::ERROR);
ui->Print("Installation aborted.\n");
copy_logs();
} else if (!ui->IsTextVisible()) {
return Device::NO_ACTION; // reboot if logs aren't visible
} else {
ui->Print("\nInstall from %s complete.\n", adb ? "ADB" : "SD card");
}
}
break;
case Device::VIEW_RECOVERY_LOGS:
choose_recovery_file(device);
break;
case Device::RUN_GRAPHICS_TEST:
run_graphics_test();
break;
case Device::RUN_LOCALE_TEST: {
ScreenRecoveryUI* screen_ui = static_cast<ScreenRecoveryUI*>(ui);
screen_ui->CheckBackgroundTextImages(locale);
break;
}
case Device::MOUNT_SYSTEM:
// For a system image built with the root directory (i.e. system_root_image == "true"), we
// mount it to /system_root, and symlink /system to /system_root/system to make adb shell
// work (the symlink is created through the build system). (Bug: 22855115)
if (android::base::GetBoolProperty("ro.build.system_root_image", false)) {
if (ensure_path_mounted_at("/", "/system_root") != -1) {
ui->Print("Mounted /system.\n");
}
} else {
if (ensure_path_mounted("/system") != -1) {
ui->Print("Mounted /system.\n");
}
}
break;
}
}
}
网友评论