美文网首页
2020-09-07

2020-09-07

作者: 伯尤特夫 | 来源:发表于2020-09-07 19:28 被阅读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;
        }
      }
    }
    

    相关文章

      网友评论

          本文标题:2020-09-07

          本文链接:https://www.haomeiwen.com/subject/pwvlektx.html