diff options
Diffstat (limited to 'recovery_ui/screen_ui.cpp')
-rw-r--r-- | recovery_ui/screen_ui.cpp | 1334 |
1 files changed, 1334 insertions, 0 deletions
diff --git a/recovery_ui/screen_ui.cpp b/recovery_ui/screen_ui.cpp new file mode 100644 index 000000000..870db621c --- /dev/null +++ b/recovery_ui/screen_ui.cpp @@ -0,0 +1,1334 @@ +/* + * 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 <dirent.h> +#include <errno.h> +#include <fcntl.h> +#include <stdarg.h> +#include <stdio.h> +#include <stdlib.h> +#include <string.h> +#include <sys/stat.h> +#include <sys/time.h> +#include <sys/types.h> +#include <time.h> +#include <unistd.h> + +#include <algorithm> +#include <chrono> +#include <memory> +#include <string> +#include <thread> +#include <unordered_map> +#include <vector> + +#include <android-base/logging.h> +#include <android-base/properties.h> +#include <android-base/stringprintf.h> +#include <android-base/strings.h> + +#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<std::string>& headers, const std::vector<std::string>& 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<size_t>(std::numeric_limits<int>::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<std::string>& 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<size_t>(std::numeric_limits<int>::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<size_t>(sel) < menu_start_) { + menu_start_--; + } else if (static_cast<size_t>(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<const GRSurface*>& 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<size_t>(std::numeric_limits<int>::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<const GRSurface*>& 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<float>(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(); + 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<int>(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<std::string>& locales_entries, + size_t sel) { + SetLocale(locales_entries[sel]); + std::vector<std::string> text_name = { "erasing_text", "error_text", "installing_text", + "installing_security_text", "no_command_text" }; + std::unordered_map<std::string, std::unique_ptr<GRSurface>> 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<std::mutex> 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<std::string> 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<std::string> 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<int>(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<std::string>& 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<std::string>& 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<std::string>& lines) { + title_lines_ = lines; +} + +// 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(); + + // clang-format off + static std::vector<std::string> REGULAR_HELP{ + "Use volume up/down and power.", + }; + static std::vector<std::string> LONG_PRESS_HELP{ + "Any button cycles highlight.", + "Long-press activates.", + }; + // clang-format on + draw_menu_and_text_buffer_locked(HasThreeButtons() ? REGULAR_HELP : LONG_PRESS_HELP); +} + +// 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<std::string>& 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<std::mutex> 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<useconds_t>(delay * 1000000)); + } +} + +std::unique_ptr<GRSurface> 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<GRSurface>(surface); +} + +std::unique_ptr<GRSurface> ScreenRecoveryUI::LoadLocalizedBitmap(const std::string& filename) { + GRSurface* surface; + if (auto result = res_create_localized_alpha_surface(filename.c_str(), locale_.c_str(), &surface); + result < 0) { + LOG(ERROR) << "Failed to load bitmap " << filename << " (error " << result << ")"; + return nullptr; + } + return std::unique_ptr<GRSurface>(surface); +} + +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; +} + +bool ScreenRecoveryUI::Init(const std::string& locale) { + RecoveryUI::Init(locale); + + if (gr_init() == -1) { + 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, decltype(&closedir)> dir(opendir(Paths::Get().resource_dir().c_str()), + closedir); + dirent* de; + std::vector<std::string> intro_frame_names; + std::vector<std::string> 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<std::mutex> lg(updateMutex); + + current_icon_ = icon; + update_screen_locked(); +} + +void ScreenRecoveryUI::SetProgressType(ProgressType type) { + std::lock_guard<std::mutex> 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<std::mutex> 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<std::mutex> 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<std::mutex> 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<std::mutex> 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'; + update_screen_locked(); + } +} + +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<std::mutex> 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<std::mutex> 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<off_t> 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<int>(100 * (double(ftello(fp)) / double(sb.st_size))), + static_cast<int>(sb.st_size)); + Redraw(); + while (show_prompt) { + show_prompt = false; + int key = WaitKey(); + if (key == static_cast<int>(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<FILE, decltype(&fclose)> 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<Menu> ScreenRecoveryUI::CreateMenu( + const GRSurface* graphic_header, const std::vector<const GRSurface*>& graphic_items, + const std::vector<std::string>& text_headers, const std::vector<std::string>& 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<GraphicMenu>(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<Menu> ScreenRecoveryUI::CreateMenu(const std::vector<std::string>& text_headers, + const std::vector<std::string>& text_items, + size_t initial_selection) const { + if (text_rows_ > 0 && text_cols_ > 1) { + return std::make_unique<TextMenu>(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<std::mutex> 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>&& menu, bool menu_only, + const std::function<int(int, bool)>& 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<size_t>(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<int>(KeyError::INTERRUPTED)) { // WaitKey() was interrupted. + return static_cast<size_t>(KeyError::INTERRUPTED); + } + if (key == static_cast<int>(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<size_t>(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<std::string>& headers, + const std::vector<std::string>& items, size_t initial_selection, + bool menu_only, + const std::function<int(int, bool)>& key_handler) { + auto menu = CreateMenu(headers, items, initial_selection); + if (menu == nullptr) { + return initial_selection; + } + + return ShowMenu(CreateMenu(headers, items, initial_selection), menu_only, key_handler); +} + +size_t ScreenRecoveryUI::ShowPromptWipeDataMenu(const std::vector<std::string>& backup_headers, + const std::vector<std::string>& backup_items, + const std::function<int(int, bool)>& 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<std::string>& backup_headers, const std::vector<std::string>& backup_items, + const std::function<int(int, bool)>& 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<std::mutex> lg(updateMutex); + int visible = show_text; + return visible; +} + +bool ScreenRecoveryUI::WasTextEverVisible() { + std::lock_guard<std::mutex> lg(updateMutex); + int ever_visible = show_text_ever; + return ever_visible; +} + +void ScreenRecoveryUI::ShowText(bool visible) { + std::lock_guard<std::mutex> lg(updateMutex); + show_text = visible; + if (show_text) show_text_ever = true; + update_screen_locked(); +} + +void ScreenRecoveryUI::Redraw() { + std::lock_guard<std::mutex> 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; + } + } +} |