2020-09-07

2020-09-07  本文已影响0人  伯尤特夫

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;
    }
  }
}
上一篇下一篇

猜你喜欢

热点阅读