/* * Copyright (C) 2011 The Android Open Source Project * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. * You may obtain a copy of the License at * * http://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, software * distributed under the License is distributed on an "AS IS" BASIS, * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. * See the License for the specific language governing permissions and * limitations under the License. */ #include "recovery_ui/screen_ui.h" #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include "minui/minui.h" #include "otautil/paths.h" #include "recovery_ui/device.h" #include "recovery_ui/ui.h" // Return the current time as a double (including fractions of a second). static double now() { struct timeval tv; gettimeofday(&tv, nullptr); return tv.tv_sec + tv.tv_usec / 1000000.0; } Menu::Menu(size_t initial_selection, const DrawInterface& draw_func) : selection_(initial_selection), draw_funcs_(draw_func) {} size_t Menu::selection() const { return selection_; } TextMenu::TextMenu(bool scrollable, size_t max_items, size_t max_length, const std::vector& headers, const std::vector& items, size_t initial_selection, int char_height, const DrawInterface& draw_funcs) : Menu(initial_selection, draw_funcs), scrollable_(scrollable), max_display_items_(max_items), max_item_length_(max_length), text_headers_(headers), menu_start_(0), char_height_(char_height) { CHECK_LE(max_items, static_cast(std::numeric_limits::max())); // It's fine to have more entries than text_rows_ if scrollable menu is supported. size_t items_count = scrollable_ ? items.size() : std::min(items.size(), max_display_items_); for (size_t i = 0; i < items_count; ++i) { text_items_.emplace_back(items[i].substr(0, max_item_length_)); } CHECK(!text_items_.empty()); } const std::vector& TextMenu::text_headers() const { return text_headers_; } std::string TextMenu::TextItem(size_t index) const { CHECK_LT(index, text_items_.size()); return text_items_[index]; } size_t TextMenu::MenuStart() const { return menu_start_; } size_t TextMenu::MenuEnd() const { return std::min(ItemsCount(), menu_start_ + max_display_items_); } size_t TextMenu::ItemsCount() const { return text_items_.size(); } bool TextMenu::ItemsOverflow(std::string* cur_selection_str) const { if (!scrollable_ || ItemsCount() <= max_display_items_) { return false; } *cur_selection_str = android::base::StringPrintf("Current item: %zu/%zu", selection_ + 1, ItemsCount()); return true; } // TODO(xunchang) modify the function parameters to button up & down. int TextMenu::Select(int sel) { CHECK_LE(ItemsCount(), static_cast(std::numeric_limits::max())); int count = ItemsCount(); // Wraps the selection at boundary if the menu is not scrollable. if (!scrollable_) { if (sel < 0) { selection_ = count - 1; } else if (sel >= count) { selection_ = 0; } else { selection_ = sel; } return selection_; } if (sel < 0) { selection_ = 0; } else if (sel >= count) { selection_ = count - 1; } else { if (static_cast(sel) < menu_start_) { menu_start_--; } else if (static_cast(sel) >= MenuEnd()) { menu_start_++; } selection_ = sel; } return selection_; } int TextMenu::DrawHeader(int x, int y) const { int offset = 0; draw_funcs_.SetColor(UIElement::HEADER); if (!scrollable()) { offset += draw_funcs_.DrawWrappedTextLines(x, y + offset, text_headers()); } else { offset += draw_funcs_.DrawTextLines(x, y + offset, text_headers()); // Show the current menu item number in relation to total number if items don't fit on the // screen. std::string cur_selection_str; if (ItemsOverflow(&cur_selection_str)) { offset += draw_funcs_.DrawTextLine(x, y + offset, cur_selection_str, true); } } return offset; } int TextMenu::DrawItems(int x, int y, int screen_width, bool long_press) const { int offset = 0; draw_funcs_.SetColor(UIElement::MENU); // Do not draw the horizontal rule for wear devices. if (!scrollable()) { offset += draw_funcs_.DrawHorizontalRule(y + offset) + 4; } for (size_t i = MenuStart(); i < MenuEnd(); ++i) { bool bold = false; if (i == selection()) { // Draw the highlight bar. draw_funcs_.SetColor(long_press ? UIElement::MENU_SEL_BG_ACTIVE : UIElement::MENU_SEL_BG); int bar_height = char_height_ + 4; draw_funcs_.DrawHighlightBar(0, y + offset - 2, screen_width, bar_height); // Bold white text for the selected item. draw_funcs_.SetColor(UIElement::MENU_SEL_FG); bold = true; } offset += draw_funcs_.DrawTextLine(x, y + offset, TextItem(i), bold); draw_funcs_.SetColor(UIElement::MENU); } offset += draw_funcs_.DrawHorizontalRule(y + offset); return offset; } GraphicMenu::GraphicMenu(const GRSurface* graphic_headers, const std::vector& graphic_items, size_t initial_selection, const DrawInterface& draw_funcs) : Menu(initial_selection, draw_funcs) { graphic_headers_ = graphic_headers->Clone(); graphic_items_.reserve(graphic_items.size()); for (const auto& item : graphic_items) { graphic_items_.emplace_back(item->Clone()); } } int GraphicMenu::Select(int sel) { CHECK_LE(graphic_items_.size(), static_cast(std::numeric_limits::max())); int count = graphic_items_.size(); // Wraps the selection at boundary if the menu is not scrollable. if (sel < 0) { selection_ = count - 1; } else if (sel >= count) { selection_ = 0; } else { selection_ = sel; } return selection_; } int GraphicMenu::DrawHeader(int x, int y) const { draw_funcs_.SetColor(UIElement::HEADER); draw_funcs_.DrawTextIcon(x, y, graphic_headers_.get()); return graphic_headers_->height; } int GraphicMenu::DrawItems(int x, int y, int screen_width, bool long_press) const { int offset = 0; draw_funcs_.SetColor(UIElement::MENU); offset += draw_funcs_.DrawHorizontalRule(y + offset) + 4; for (size_t i = 0; i < graphic_items_.size(); i++) { auto& item = graphic_items_[i]; if (i == selection_) { draw_funcs_.SetColor(long_press ? UIElement::MENU_SEL_BG_ACTIVE : UIElement::MENU_SEL_BG); int bar_height = item->height + 4; draw_funcs_.DrawHighlightBar(0, y + offset - 2, screen_width, bar_height); // Bold white text for the selected item. draw_funcs_.SetColor(UIElement::MENU_SEL_FG); } draw_funcs_.DrawTextIcon(x, y + offset, item.get()); offset += item->height; draw_funcs_.SetColor(UIElement::MENU); } offset += draw_funcs_.DrawHorizontalRule(y + offset); return offset; } bool GraphicMenu::Validate(size_t max_width, size_t max_height, const GRSurface* graphic_headers, const std::vector& graphic_items) { int offset = 0; if (!ValidateGraphicSurface(max_width, max_height, offset, graphic_headers)) { return false; } offset += graphic_headers->height; for (const auto& item : graphic_items) { if (!ValidateGraphicSurface(max_width, max_height, offset, item)) { return false; } offset += item->height; } return true; } bool GraphicMenu::ValidateGraphicSurface(size_t max_width, size_t max_height, int y, const GRSurface* surface) { if (!surface) { fprintf(stderr, "Graphic surface can not be null\n"); return false; } if (surface->pixel_bytes != 1 || surface->width != surface->row_bytes) { fprintf(stderr, "Invalid graphic surface, pixel bytes: %zu, width: %zu row_bytes: %zu\n", surface->pixel_bytes, surface->width, surface->row_bytes); return false; } if (surface->width > max_width || surface->height > max_height - y) { fprintf(stderr, "Graphic surface doesn't fit into the screen. width: %zu, height: %zu, max_width: %zu," " max_height: %zu, vertical offset: %d\n", surface->width, surface->height, max_width, max_height, y); return false; } return true; } ScreenRecoveryUI::ScreenRecoveryUI() : ScreenRecoveryUI(false) {} constexpr int kDefaultMarginHeight = 0; constexpr int kDefaultMarginWidth = 0; constexpr int kDefaultAnimationFps = 30; ScreenRecoveryUI::ScreenRecoveryUI(bool scrollable_menu) : margin_width_( android::base::GetIntProperty("ro.recovery.ui.margin_width", kDefaultMarginWidth)), margin_height_( android::base::GetIntProperty("ro.recovery.ui.margin_height", kDefaultMarginHeight)), animation_fps_( android::base::GetIntProperty("ro.recovery.ui.animation_fps", kDefaultAnimationFps)), density_(static_cast(android::base::GetIntProperty("ro.sf.lcd_density", 160)) / 160.f), current_icon_(NONE), current_frame_(0), intro_done_(false), progressBarType(EMPTY), progressScopeStart(0), progressScopeSize(0), progress(0), pagesIdentical(false), text_cols_(0), text_rows_(0), text_(nullptr), text_col_(0), text_row_(0), show_text(false), show_text_ever(false), scrollable_menu_(scrollable_menu), file_viewer_text_(nullptr), stage(-1), max_stage(-1), locale_(""), rtl_locale_(false) {} ScreenRecoveryUI::~ScreenRecoveryUI() { progress_thread_stopped_ = true; if (progress_thread_.joinable()) { progress_thread_.join(); } // No-op if gr_init() (via Init()) was not called or had failed. gr_exit(); } const GRSurface* ScreenRecoveryUI::GetCurrentFrame() const { if (current_icon_ == INSTALLING_UPDATE || current_icon_ == ERASING) { return intro_done_ ? loop_frames_[current_frame_].get() : intro_frames_[current_frame_].get(); } return error_icon_.get(); } const GRSurface* ScreenRecoveryUI::GetCurrentText() const { switch (current_icon_) { case ERASING: return erasing_text_.get(); case ERROR: return error_text_.get(); case INSTALLING_UPDATE: return installing_text_.get(); case NO_COMMAND: return no_command_text_.get(); case NONE: abort(); } } int ScreenRecoveryUI::PixelsFromDp(int dp) const { return dp * density_; } // Here's the intended layout: // | portrait large landscape large // ---------+------------------------------------------------- // gap | // icon | (200dp) // gap | 68dp 68dp 56dp 112dp // text | (14sp) // gap | 32dp 32dp 26dp 52dp // progress | (2dp) // gap | // Note that "baseline" is actually the *top* of each icon (because that's how our drawing routines // work), so that's the more useful measurement for calling code. We use even top and bottom gaps. enum Layout { PORTRAIT = 0, PORTRAIT_LARGE = 1, LANDSCAPE = 2, LANDSCAPE_LARGE = 3, LAYOUT_MAX }; enum Dimension { TEXT = 0, ICON = 1, DIMENSION_MAX }; static constexpr int kLayouts[LAYOUT_MAX][DIMENSION_MAX] = { { 32, 68 }, // PORTRAIT { 32, 68 }, // PORTRAIT_LARGE { 26, 56 }, // LANDSCAPE { 52, 112 }, // LANDSCAPE_LARGE }; int ScreenRecoveryUI::GetAnimationBaseline() const { return GetTextBaseline() - PixelsFromDp(kLayouts[layout_][ICON]) - gr_get_height(loop_frames_[0].get()); } int ScreenRecoveryUI::GetTextBaseline() const { return GetProgressBaseline() - PixelsFromDp(kLayouts[layout_][TEXT]) - gr_get_height(installing_text_.get()); } int ScreenRecoveryUI::GetProgressBaseline() const { int elements_sum = gr_get_height(loop_frames_[0].get()) + PixelsFromDp(kLayouts[layout_][ICON]) + gr_get_height(installing_text_.get()) + PixelsFromDp(kLayouts[layout_][TEXT]) + gr_get_height(progress_bar_fill_.get()); int bottom_gap = (ScreenHeight() - elements_sum) / 2; return ScreenHeight() - bottom_gap - gr_get_height(progress_bar_fill_.get()); } // Clear the screen and draw the currently selected background icon (if any). // Should only be called with updateMutex locked. void ScreenRecoveryUI::draw_background_locked() { pagesIdentical = false; gr_color(0, 0, 0, 255); gr_clear(); if (current_icon_ != NONE) { if (max_stage != -1) { int stage_height = gr_get_height(stage_marker_empty_.get()); int stage_width = gr_get_width(stage_marker_empty_.get()); int x = (ScreenWidth() - max_stage * gr_get_width(stage_marker_empty_.get())) / 2; int y = ScreenHeight() - stage_height - margin_height_; for (int i = 0; i < max_stage; ++i) { const auto& stage_surface = (i < stage) ? stage_marker_fill_ : stage_marker_empty_; DrawSurface(stage_surface.get(), 0, 0, stage_width, stage_height, x, y); x += stage_width; } } const auto& text_surface = GetCurrentText(); int text_x = (ScreenWidth() - gr_get_width(text_surface)) / 2; int text_y = GetTextBaseline(); gr_color(255, 255, 255, 255); DrawTextIcon(text_x, text_y, text_surface); } } // Draws the animation and progress bar (if any) on the screen. Does not flip pages. Should only be // called with updateMutex locked. void ScreenRecoveryUI::draw_foreground_locked() { if (current_icon_ != NONE) { const auto& frame = GetCurrentFrame(); int frame_width = gr_get_width(frame); int frame_height = gr_get_height(frame); int frame_x = (ScreenWidth() - frame_width) / 2; int frame_y = GetAnimationBaseline(); if (frame_x >= 0 && frame_y >= 0 && (frame_x + frame_width) < ScreenWidth() && (frame_y + frame_height) < ScreenHeight()) DrawSurface(frame, 0, 0, frame_width, frame_height, frame_x, frame_y); } if (progressBarType != EMPTY) { int width = gr_get_width(progress_bar_empty_.get()); int height = gr_get_height(progress_bar_empty_.get()); int progress_x = (ScreenWidth() - width) / 2; int progress_y = GetProgressBaseline(); // Erase behind the progress bar (in case this was a progress-only update) gr_color(0, 0, 0, 255); DrawFill(progress_x, progress_y, width, height); if (progressBarType == DETERMINATE) { float p = progressScopeStart + progress * progressScopeSize; int pos = static_cast(p * width); if (rtl_locale_) { // Fill the progress bar from right to left. if (pos > 0) { DrawSurface(progress_bar_fill_.get(), width - pos, 0, pos, height, progress_x + width - pos, progress_y); } if (pos < width - 1) { DrawSurface(progress_bar_empty_.get(), 0, 0, width - pos, height, progress_x, progress_y); } } else { // Fill the progress bar from left to right. if (pos > 0) { DrawSurface(progress_bar_fill_.get(), 0, 0, pos, height, progress_x, progress_y); } if (pos < width - 1) { DrawSurface(progress_bar_empty_.get(), pos, 0, width - pos, height, progress_x + pos, progress_y); } } } } } void ScreenRecoveryUI::SetColor(UIElement e) const { switch (e) { case UIElement::INFO: gr_color(249, 194, 0, 255); break; case UIElement::HEADER: gr_color(247, 0, 6, 255); break; case UIElement::MENU: case UIElement::MENU_SEL_BG: gr_color(0, 106, 157, 255); break; case UIElement::MENU_SEL_BG_ACTIVE: gr_color(0, 156, 100, 255); break; case UIElement::MENU_SEL_FG: gr_color(255, 255, 255, 255); break; case UIElement::LOG: gr_color(196, 196, 196, 255); break; case UIElement::TEXT_FILL: gr_color(0, 0, 0, 160); break; default: gr_color(255, 255, 255, 255); break; } } void ScreenRecoveryUI::SelectAndShowBackgroundText(const std::vector& locales_entries, size_t sel) { SetLocale(locales_entries[sel]); std::vector text_name = { "erasing_text", "error_text", "installing_text", "installing_security_text", "no_command_text" }; std::unordered_map> surfaces; for (const auto& name : text_name) { auto text_image = LoadLocalizedBitmap(name); if (!text_image) { Print("Failed to load %s\n", name.c_str()); return; } surfaces.emplace(name, std::move(text_image)); } std::lock_guard lg(updateMutex); gr_color(0, 0, 0, 255); gr_clear(); int text_y = margin_height_; int text_x = margin_width_; int line_spacing = gr_sys_font()->char_height; // Put some extra space between images. // Write the header and descriptive texts. SetColor(UIElement::INFO); std::string header = "Show background text image"; text_y += DrawTextLine(text_x, text_y, header, true); std::string locale_selection = android::base::StringPrintf( "Current locale: %s, %zu/%zu", locales_entries[sel].c_str(), sel + 1, locales_entries.size()); // clang-format off std::vector instruction = { locale_selection, "Use volume up/down to switch locales and power to exit." }; // clang-format on text_y += DrawWrappedTextLines(text_x, text_y, instruction); // Iterate through the text images and display them in order for the current locale. for (const auto& p : surfaces) { text_y += line_spacing; SetColor(UIElement::LOG); text_y += DrawTextLine(text_x, text_y, p.first, false); gr_color(255, 255, 255, 255); gr_texticon(text_x, text_y, p.second.get()); text_y += gr_get_height(p.second.get()); } // Update the whole screen. gr_flip(); } void ScreenRecoveryUI::CheckBackgroundTextImages() { // Load a list of locales embedded in one of the resource files. std::vector locales_entries = get_locales_in_png("installing_text"); if (locales_entries.empty()) { Print("Failed to load locales from the resource files\n"); return; } std::string saved_locale = locale_; size_t selected = 0; SelectAndShowBackgroundText(locales_entries, selected); FlushKeys(); while (true) { int key = WaitKey(); if (key == static_cast(KeyError::INTERRUPTED)) break; if (key == KEY_POWER || key == KEY_ENTER) { break; } else if (key == KEY_UP || key == KEY_VOLUMEUP) { selected = (selected == 0) ? locales_entries.size() - 1 : selected - 1; SelectAndShowBackgroundText(locales_entries, selected); } else if (key == KEY_DOWN || key == KEY_VOLUMEDOWN) { selected = (selected == locales_entries.size() - 1) ? 0 : selected + 1; SelectAndShowBackgroundText(locales_entries, selected); } } SetLocale(saved_locale); } int ScreenRecoveryUI::ScreenWidth() const { return gr_fb_width(); } int ScreenRecoveryUI::ScreenHeight() const { return gr_fb_height(); } void ScreenRecoveryUI::DrawSurface(const GRSurface* surface, int sx, int sy, int w, int h, int dx, int dy) const { gr_blit(surface, sx, sy, w, h, dx, dy); } int ScreenRecoveryUI::DrawHorizontalRule(int y) const { gr_fill(0, y + 4, ScreenWidth(), y + 6); return 8; } void ScreenRecoveryUI::DrawHighlightBar(int x, int y, int width, int height) const { gr_fill(x, y, x + width, y + height); } void ScreenRecoveryUI::DrawFill(int x, int y, int w, int h) const { gr_fill(x, y, w, h); } void ScreenRecoveryUI::DrawTextIcon(int x, int y, const GRSurface* surface) const { gr_texticon(x, y, surface); } int ScreenRecoveryUI::DrawTextLine(int x, int y, const std::string& line, bool bold) const { gr_text(gr_sys_font(), x, y, line.c_str(), bold); return char_height_ + 4; } int ScreenRecoveryUI::DrawTextLines(int x, int y, const std::vector& lines) const { int offset = 0; for (const auto& line : lines) { offset += DrawTextLine(x, y + offset, line, false); } return offset; } int ScreenRecoveryUI::DrawWrappedTextLines(int x, int y, const std::vector& lines) const { // Keep symmetrical margins based on the given offset (i.e. x). size_t text_cols = (ScreenWidth() - x * 2) / char_width_; int offset = 0; for (const auto& line : lines) { size_t next_start = 0; while (next_start < line.size()) { std::string sub = line.substr(next_start, text_cols + 1); if (sub.size() <= text_cols) { next_start += sub.size(); } else { // Line too long and must be wrapped to text_cols columns. size_t last_space = sub.find_last_of(" \t\n"); if (last_space == std::string::npos) { // No space found, just draw as much as we can. sub.resize(text_cols); next_start += text_cols; } else { sub.resize(last_space); next_start += last_space + 1; } } offset += DrawTextLine(x, y + offset, sub, false); } } return offset; } void ScreenRecoveryUI::SetTitle(const std::vector& lines) { title_lines_ = lines; } std::vector ScreenRecoveryUI::GetMenuHelpMessage() const { // clang-format off static std::vector REGULAR_HELP{ "Use volume up/down and power.", }; static std::vector LONG_PRESS_HELP{ "Any button cycles highlight.", "Long-press activates.", }; // clang-format on return HasThreeButtons() ? REGULAR_HELP : LONG_PRESS_HELP; } // Redraws everything on the screen. Does not flip pages. Should only be called with updateMutex // locked. void ScreenRecoveryUI::draw_screen_locked() { if (!show_text) { draw_background_locked(); draw_foreground_locked(); return; } gr_color(0, 0, 0, 255); gr_clear(); draw_menu_and_text_buffer_locked(GetMenuHelpMessage()); } // Draws the menu and text buffer on the screen. Should only be called with updateMutex locked. void ScreenRecoveryUI::draw_menu_and_text_buffer_locked( const std::vector& help_message) { int y = margin_height_; if (fastbootd_logo_ && fastbootd_logo_enabled_) { // Try to get this centered on screen. auto width = gr_get_width(fastbootd_logo_.get()); auto height = gr_get_height(fastbootd_logo_.get()); auto centered_x = ScreenWidth() / 2 - width / 2; DrawSurface(fastbootd_logo_.get(), 0, 0, width, height, centered_x, y); y += height; } if (menu_) { int x = margin_width_ + kMenuIndent; SetColor(UIElement::INFO); for (size_t i = 0; i < title_lines_.size(); i++) { y += DrawTextLine(x, y, title_lines_[i], i == 0); } y += DrawTextLines(x, y, help_message); y += menu_->DrawHeader(x, y); y += menu_->DrawItems(x, y, ScreenWidth(), IsLongPress()); } // Display from the bottom up, until we hit the top of the screen, the bottom of the menu, or // we've displayed the entire text buffer. SetColor(UIElement::LOG); int row = text_row_; size_t count = 0; for (int ty = ScreenHeight() - margin_height_ - char_height_; ty >= y && count < text_rows_; ty -= char_height_, ++count) { DrawTextLine(margin_width_, ty, text_[row], false); --row; if (row < 0) row = text_rows_ - 1; } } // Redraw everything on the screen and flip the screen (make it visible). // Should only be called with updateMutex locked. void ScreenRecoveryUI::update_screen_locked() { draw_screen_locked(); gr_flip(); } // Updates only the progress bar, if possible, otherwise redraws the screen. // Should only be called with updateMutex locked. void ScreenRecoveryUI::update_progress_locked() { if (show_text || !pagesIdentical) { draw_screen_locked(); // Must redraw the whole screen pagesIdentical = true; } else { draw_foreground_locked(); // Draw only the progress bar and overlays } gr_flip(); } void ScreenRecoveryUI::ProgressThreadLoop() { double interval = 1.0 / animation_fps_; while (!progress_thread_stopped_) { double start = now(); bool redraw = false; { std::lock_guard lg(updateMutex); // update the installation animation, if active // skip this if we have a text overlay (too expensive to update) if ((current_icon_ == INSTALLING_UPDATE || current_icon_ == ERASING) && !show_text) { if (!intro_done_) { if (current_frame_ == intro_frames_.size() - 1) { intro_done_ = true; current_frame_ = 0; } else { ++current_frame_; } } else { current_frame_ = (current_frame_ + 1) % loop_frames_.size(); } redraw = true; } // move the progress bar forward on timed intervals, if configured int duration = progressScopeDuration; if (progressBarType == DETERMINATE && duration > 0) { double elapsed = now() - progressScopeTime; float p = 1.0 * elapsed / duration; if (p > 1.0) p = 1.0; if (p > progress) { progress = p; redraw = true; } } if (redraw) update_progress_locked(); } double end = now(); // minimum of 20ms delay between frames double delay = interval - (end - start); if (delay < 0.02) delay = 0.02; usleep(static_cast(delay * 1000000)); } } std::unique_ptr ScreenRecoveryUI::LoadBitmap(const std::string& filename) { GRSurface* surface; if (auto result = res_create_display_surface(filename.c_str(), &surface); result < 0) { LOG(ERROR) << "Failed to load bitmap " << filename << " (error " << result << ")"; return nullptr; } return std::unique_ptr(surface); } std::unique_ptr ScreenRecoveryUI::LoadLocalizedBitmap(const std::string& filename) { GRSurface* surface; auto result = res_create_localized_alpha_surface(filename.c_str(), locale_.c_str(), &surface); if (result == 0) { return std::unique_ptr(surface); } // TODO(xunchang) create a error code enum to refine the retry condition. LOG(WARNING) << "Failed to load bitmap " << filename << " for locale " << locale_ << " (error " << result << "). Falling back to use default locale."; result = res_create_localized_alpha_surface(filename.c_str(), DEFAULT_LOCALE, &surface); if (result == 0) { return std::unique_ptr(surface); } LOG(ERROR) << "Failed to load bitmap " << filename << " for locale " << DEFAULT_LOCALE << " (error " << result << ")"; return nullptr; } static char** Alloc2d(size_t rows, size_t cols) { char** result = new char*[rows]; for (size_t i = 0; i < rows; ++i) { result[i] = new char[cols]; memset(result[i], 0, cols); } return result; } // Choose the right background string to display during update. void ScreenRecoveryUI::SetSystemUpdateText(bool security_update) { if (security_update) { installing_text_ = LoadLocalizedBitmap("installing_security_text"); } else { installing_text_ = LoadLocalizedBitmap("installing_text"); } Redraw(); } bool ScreenRecoveryUI::InitTextParams() { // gr_init() would return successfully on font initialization failure. if (gr_sys_font() == nullptr) { return false; } gr_font_size(gr_sys_font(), &char_width_, &char_height_); text_rows_ = (ScreenHeight() - margin_height_ * 2) / char_height_; text_cols_ = (ScreenWidth() - margin_width_ * 2) / char_width_; return true; } bool ScreenRecoveryUI::LoadWipeDataMenuText() { // Ignores the errors since the member variables will stay as nullptr. cancel_wipe_data_text_ = LoadLocalizedBitmap("cancel_wipe_data_text"); factory_data_reset_text_ = LoadLocalizedBitmap("factory_data_reset_text"); try_again_text_ = LoadLocalizedBitmap("try_again_text"); wipe_data_confirmation_text_ = LoadLocalizedBitmap("wipe_data_confirmation_text"); wipe_data_menu_header_text_ = LoadLocalizedBitmap("wipe_data_menu_header_text"); return true; } static bool InitGraphics() { // Timeout is same as init wait for file default of 5 seconds and is arbitrary const unsigned timeout = 500; // 10ms increments for (auto retry = timeout; retry > 0; --retry) { if (gr_init() == 0) { if (retry < timeout) { // Log message like init wait for file completion log for consistency. LOG(WARNING) << "wait for 'graphics' took " << ((timeout - retry) * 10) << "ms"; } return true; } std::this_thread::sleep_for(10ms); } // Log message like init wait for file timeout log for consistency. LOG(ERROR) << "timeout wait for 'graphics' took " << (timeout * 10) << "ms"; return false; } bool ScreenRecoveryUI::Init(const std::string& locale) { RecoveryUI::Init(locale); if (!InitGraphics()) { return false; } if (!InitTextParams()) { return false; } // Are we portrait or landscape? layout_ = (gr_fb_width() > gr_fb_height()) ? LANDSCAPE : PORTRAIT; // Are we the large variant of our base layout? if (gr_fb_height() > PixelsFromDp(800)) ++layout_; text_ = Alloc2d(text_rows_, text_cols_ + 1); file_viewer_text_ = Alloc2d(text_rows_, text_cols_ + 1); text_col_ = text_row_ = 0; // Set up the locale info. SetLocale(locale); error_icon_ = LoadBitmap("icon_error"); progress_bar_empty_ = LoadBitmap("progress_empty"); progress_bar_fill_ = LoadBitmap("progress_fill"); stage_marker_empty_ = LoadBitmap("stage_empty"); stage_marker_fill_ = LoadBitmap("stage_fill"); erasing_text_ = LoadLocalizedBitmap("erasing_text"); no_command_text_ = LoadLocalizedBitmap("no_command_text"); error_text_ = LoadLocalizedBitmap("error_text"); if (android::base::GetBoolProperty("ro.boot.dynamic_partitions", false)) { fastbootd_logo_ = LoadBitmap("fastbootd"); } // Background text for "installing_update" could be "installing update" or // "installing security update". It will be set after Init() according to the commands in BCB. installing_text_.reset(); LoadWipeDataMenuText(); LoadAnimation(); // Keep the progress bar updated, even when the process is otherwise busy. progress_thread_ = std::thread(&ScreenRecoveryUI::ProgressThreadLoop, this); return true; } std::string ScreenRecoveryUI::GetLocale() const { return locale_; } void ScreenRecoveryUI::LoadAnimation() { std::unique_ptr dir(opendir(Paths::Get().resource_dir().c_str()), closedir); dirent* de; std::vector intro_frame_names; std::vector loop_frame_names; while ((de = readdir(dir.get())) != nullptr) { int value, num_chars; if (sscanf(de->d_name, "intro%d%n.png", &value, &num_chars) == 1) { intro_frame_names.emplace_back(de->d_name, num_chars); } else if (sscanf(de->d_name, "loop%d%n.png", &value, &num_chars) == 1) { loop_frame_names.emplace_back(de->d_name, num_chars); } } size_t intro_frames = intro_frame_names.size(); size_t loop_frames = loop_frame_names.size(); // It's okay to not have an intro. if (intro_frames == 0) intro_done_ = true; // But you must have an animation. if (loop_frames == 0) abort(); std::sort(intro_frame_names.begin(), intro_frame_names.end()); std::sort(loop_frame_names.begin(), loop_frame_names.end()); intro_frames_.clear(); intro_frames_.reserve(intro_frames); for (const auto& frame_name : intro_frame_names) { intro_frames_.emplace_back(LoadBitmap(frame_name)); } loop_frames_.clear(); loop_frames_.reserve(loop_frames); for (const auto& frame_name : loop_frame_names) { loop_frames_.emplace_back(LoadBitmap(frame_name)); } } void ScreenRecoveryUI::SetBackground(Icon icon) { std::lock_guard lg(updateMutex); current_icon_ = icon; update_screen_locked(); } void ScreenRecoveryUI::SetProgressType(ProgressType type) { std::lock_guard lg(updateMutex); if (progressBarType != type) { progressBarType = type; } progressScopeStart = 0; progressScopeSize = 0; progress = 0; update_progress_locked(); } void ScreenRecoveryUI::ShowProgress(float portion, float seconds) { std::lock_guard lg(updateMutex); progressBarType = DETERMINATE; progressScopeStart += progressScopeSize; progressScopeSize = portion; progressScopeTime = now(); progressScopeDuration = seconds; progress = 0; update_progress_locked(); } void ScreenRecoveryUI::SetProgress(float fraction) { std::lock_guard lg(updateMutex); if (fraction < 0.0) fraction = 0.0; if (fraction > 1.0) fraction = 1.0; if (progressBarType == DETERMINATE && fraction > progress) { // Skip updates that aren't visibly different. int width = gr_get_width(progress_bar_empty_.get()); float scale = width * progressScopeSize; if ((int)(progress * scale) != (int)(fraction * scale)) { progress = fraction; update_progress_locked(); } } } void ScreenRecoveryUI::SetStage(int current, int max) { std::lock_guard lg(updateMutex); stage = current; max_stage = max; } void ScreenRecoveryUI::PrintV(const char* fmt, bool copy_to_stdout, va_list ap) { std::string str; android::base::StringAppendV(&str, fmt, ap); if (copy_to_stdout) { fputs(str.c_str(), stdout); } std::lock_guard lg(updateMutex); if (text_rows_ > 0 && text_cols_ > 0) { for (const char* ptr = str.c_str(); *ptr != '\0'; ++ptr) { if (*ptr == '\n' || text_col_ >= text_cols_) { text_[text_row_][text_col_] = '\0'; text_col_ = 0; text_row_ = (text_row_ + 1) % text_rows_; } if (*ptr != '\n') text_[text_row_][text_col_++] = *ptr; } text_[text_row_][text_col_] = '\0'; // HUANGLONG begin //recovery, fix the upgrade animation jitters if (show_text) { // HUANGLONG end update_screen_locked(); // HUANGLONG begin } // HUANGLONG end } } void ScreenRecoveryUI::Print(const char* fmt, ...) { va_list ap; va_start(ap, fmt); PrintV(fmt, true, ap); va_end(ap); } void ScreenRecoveryUI::PrintOnScreenOnly(const char* fmt, ...) { va_list ap; va_start(ap, fmt); PrintV(fmt, false, ap); va_end(ap); } void ScreenRecoveryUI::PutChar(char ch) { std::lock_guard lg(updateMutex); if (ch != '\n') text_[text_row_][text_col_++] = ch; if (ch == '\n' || text_col_ >= text_cols_) { text_col_ = 0; ++text_row_; } } void ScreenRecoveryUI::ClearText() { std::lock_guard lg(updateMutex); text_col_ = 0; text_row_ = 0; for (size_t i = 0; i < text_rows_; ++i) { memset(text_[i], 0, text_cols_ + 1); } } void ScreenRecoveryUI::ShowFile(FILE* fp) { std::vector offsets; offsets.push_back(ftello(fp)); ClearText(); struct stat sb; fstat(fileno(fp), &sb); bool show_prompt = false; while (true) { if (show_prompt) { PrintOnScreenOnly("--(%d%% of %d bytes)--", static_cast(100 * (double(ftello(fp)) / double(sb.st_size))), static_cast(sb.st_size)); Redraw(); while (show_prompt) { show_prompt = false; int key = WaitKey(); if (key == static_cast(KeyError::INTERRUPTED)) return; if (key == KEY_POWER || key == KEY_ENTER) { return; } else if (key == KEY_UP || key == KEY_VOLUMEUP) { if (offsets.size() <= 1) { show_prompt = true; } else { offsets.pop_back(); fseek(fp, offsets.back(), SEEK_SET); } } else { if (feof(fp)) { return; } offsets.push_back(ftello(fp)); } } ClearText(); } int ch = getc(fp); if (ch == EOF) { while (text_row_ < text_rows_ - 1) PutChar('\n'); show_prompt = true; } else { PutChar(ch); if (text_col_ == 0 && text_row_ >= text_rows_ - 1) { show_prompt = true; } } } } void ScreenRecoveryUI::ShowFile(const std::string& filename) { std::unique_ptr fp(fopen(filename.c_str(), "re"), fclose); if (!fp) { Print(" Unable to open %s: %s\n", filename.c_str(), strerror(errno)); return; } char** old_text = text_; size_t old_text_col = text_col_; size_t old_text_row = text_row_; // Swap in the alternate screen and clear it. text_ = file_viewer_text_; ClearText(); ShowFile(fp.get()); text_ = old_text; text_col_ = old_text_col; text_row_ = old_text_row; } std::unique_ptr ScreenRecoveryUI::CreateMenu( const GRSurface* graphic_header, const std::vector& graphic_items, const std::vector& text_headers, const std::vector& text_items, size_t initial_selection) const { // horizontal unusable area: margin width + menu indent size_t max_width = ScreenWidth() - margin_width_ - kMenuIndent; // vertical unusable area: margin height + title lines + helper message + high light bar. // It is safe to reserve more space. size_t max_height = ScreenHeight() - margin_height_ - char_height_ * (title_lines_.size() + 3); if (GraphicMenu::Validate(max_width, max_height, graphic_header, graphic_items)) { return std::make_unique(graphic_header, graphic_items, initial_selection, *this); } fprintf(stderr, "Failed to initialize graphic menu, falling back to use the text menu.\n"); return CreateMenu(text_headers, text_items, initial_selection); } std::unique_ptr ScreenRecoveryUI::CreateMenu(const std::vector& text_headers, const std::vector& text_items, size_t initial_selection) const { if (text_rows_ > 0 && text_cols_ > 1) { return std::make_unique(scrollable_menu_, text_rows_, text_cols_ - 1, text_headers, text_items, initial_selection, char_height_, *this); } fprintf(stderr, "Failed to create text menu, text_rows %zu, text_cols %zu.\n", text_rows_, text_cols_); return nullptr; } int ScreenRecoveryUI::SelectMenu(int sel) { std::lock_guard lg(updateMutex); if (menu_) { int old_sel = menu_->selection(); sel = menu_->Select(sel); if (sel != old_sel) { update_screen_locked(); } } return sel; } size_t ScreenRecoveryUI::ShowMenu(std::unique_ptr&& menu, bool menu_only, const std::function& key_handler) { // Throw away keys pressed previously, so user doesn't accidentally trigger menu items. FlushKeys(); // If there is a key interrupt in progress, return KeyError::INTERRUPTED without starting the // menu. if (IsKeyInterrupted()) return static_cast(KeyError::INTERRUPTED); CHECK(menu != nullptr); // Starts and displays the menu menu_ = std::move(menu); Redraw(); int selected = menu_->selection(); int chosen_item = -1; while (chosen_item < 0) { int key = WaitKey(); if (key == static_cast(KeyError::INTERRUPTED)) { // WaitKey() was interrupted. return static_cast(KeyError::INTERRUPTED); } if (key == static_cast(KeyError::TIMED_OUT)) { // WaitKey() timed out. if (WasTextEverVisible()) { continue; } else { LOG(INFO) << "Timed out waiting for key input; rebooting."; menu_.reset(); Redraw(); return static_cast(KeyError::TIMED_OUT); } } bool visible = IsTextVisible(); int action = key_handler(key, visible); if (action < 0) { switch (action) { case Device::kHighlightUp: selected = SelectMenu(--selected); break; case Device::kHighlightDown: selected = SelectMenu(++selected); break; case Device::kInvokeItem: chosen_item = selected; break; case Device::kNoAction: break; } } else if (!menu_only) { chosen_item = action; } } menu_.reset(); Redraw(); return chosen_item; } size_t ScreenRecoveryUI::ShowMenu(const std::vector& headers, const std::vector& items, size_t initial_selection, bool menu_only, const std::function& key_handler) { auto menu = CreateMenu(headers, items, initial_selection); if (menu == nullptr) { return initial_selection; } return ShowMenu(std::move(menu), menu_only, key_handler); } size_t ScreenRecoveryUI::ShowPromptWipeDataMenu(const std::vector& backup_headers, const std::vector& backup_items, const std::function& key_handler) { auto wipe_data_menu = CreateMenu(wipe_data_menu_header_text_.get(), { try_again_text_.get(), factory_data_reset_text_.get() }, backup_headers, backup_items, 0); if (wipe_data_menu == nullptr) { return 0; } return ShowMenu(std::move(wipe_data_menu), true, key_handler); } size_t ScreenRecoveryUI::ShowPromptWipeDataConfirmationMenu( const std::vector& backup_headers, const std::vector& backup_items, const std::function& key_handler) { auto confirmation_menu = CreateMenu(wipe_data_confirmation_text_.get(), { cancel_wipe_data_text_.get(), factory_data_reset_text_.get() }, backup_headers, backup_items, 0); if (confirmation_menu == nullptr) { return 0; } return ShowMenu(std::move(confirmation_menu), true, key_handler); } bool ScreenRecoveryUI::IsTextVisible() { std::lock_guard lg(updateMutex); int visible = show_text; return visible; } bool ScreenRecoveryUI::WasTextEverVisible() { std::lock_guard lg(updateMutex); int ever_visible = show_text_ever; return ever_visible; } void ScreenRecoveryUI::ShowText(bool visible) { std::lock_guard lg(updateMutex); show_text = visible; if (show_text) show_text_ever = true; update_screen_locked(); } void ScreenRecoveryUI::Redraw() { std::lock_guard lg(updateMutex); update_screen_locked(); } void ScreenRecoveryUI::KeyLongPress(int) { // Redraw so that if we're in the menu, the highlight // will change color to indicate a successful long press. Redraw(); } void ScreenRecoveryUI::SetLocale(const std::string& new_locale) { locale_ = new_locale; rtl_locale_ = false; if (!new_locale.empty()) { size_t separator = new_locale.find('-'); // lang has the language prefix prior to the separator, or full string if none exists. std::string lang = new_locale.substr(0, separator); // A bit cheesy: keep an explicit list of supported RTL languages. if (lang == "ar" || // Arabic lang == "fa" || // Persian (Farsi) lang == "he" || // Hebrew (new language code) lang == "iw" || // Hebrew (old language code) lang == "ur") { // Urdu rtl_locale_ = true; } } }