blob: f1ade9e493ef195ead2d93a16fe51c10937e2d63 [file] [log] [blame]
// Copyright 2021 The Chromium OS Authors. All rights reserved.
// Use of this source code is governed by a BSD-style license that can be
// found in the LICENSE file.
#include "minios/screens.h"
#include <base/json/json_reader.h>
#include <base/strings/string_number_conversions.h>
#include <base/values.h>
namespace screens {
const char kScreens[] = "etc/screens";
// Colors.
const char kMenuDropdownFrameNavy[] = "0x435066";
const char kMenuDropdownBackgroundBlack[] = "0x2D2E30";
// Key values.
const int kKeyUp = 103;
const int kKeyDown = 108;
const int kKeyEnter = 28;
const int kKeyVolUp = 115;
const int kKeyVolDown = 114;
const int kKeyPower = 116;
// Key state parameters.
const int kFdsMax = 10;
const int kKeyMax = 200;
namespace {
constexpr int kCanvasSize = 1080;
// Buttons Spacing
constexpr int kTitleY = (-kCanvasSize / 2) + 238;
constexpr int kBtnYStep = 40;
} // namespace
bool Screens::Init() {
CheckDetachable();
CheckRightToLeft();
GetVpdRegion();
ReadHardwareId();
screens_path_ = root_.Append(kScreens);
// TODO(vyshu): Change constants.sh and lang_constants.sh to simple text file.
ReadDimensionConstants();
ReadLangConstants();
std::vector<int> wait_keys;
if (!is_detachable_)
wait_keys = {kKeyUp, kKeyDown, kKeyEnter};
else
wait_keys = {kKeyVolDown, kKeyVolUp, kKeyPower};
if (!key_reader_.Init(wait_keys)) {
LOG(ERROR) << "Could not initialize key reader. Unable to continue. ";
return false;
}
return true;
}
bool Screens::InitForTest() {
screens_path_ = root_.Append(kScreens);
ReadDimensionConstants();
return true;
}
void Screens::StartMiniOsFlow() {
index_ = 1;
return ShowMiniOsWelcomeScreen();
}
void Screens::ShowLanguageDropdown() {
constexpr int kItemHeight = 40;
constexpr int kItemPerPage = (kCanvasSize - 260) / kItemHeight;
// Pick begin index such that the selected index is centered on the screen if
// possible.
int begin_index =
std::clamp(index_ - kItemPerPage / 2, 0,
static_cast<int>(supported_locales_.size()) - kItemPerPage);
int offset_y = -kCanvasSize / 2 + 88;
constexpr int kBackgroundX = -kCanvasSize / 2 + 360;
for (int i = begin_index;
i < (begin_index + kItemPerPage) && i < supported_locales_.size(); i++) {
// Get placement for the language image.
int language_width;
if (!GetLangConstants(supported_locales_[i], &language_width)) {
language_width = 95;
LOG(WARNING) << "Could not get width for " << supported_locales_[i]
<< ". Defaulting to " << language_width;
}
int lang_x = -kCanvasSize / 2 + language_width / 2 + 40;
// This is the currently selected language. Show in blue.
if (index_ == i) {
ShowBox(kBackgroundX, offset_y, 720, 40, kMenuBlue);
ShowImage(screens_path_.Append(supported_locales_[i])
.Append("language_focused.png"),
lang_x, offset_y);
} else {
ShowBox(kBackgroundX, offset_y, 720, 40, kMenuDropdownFrameNavy);
ShowBox(kBackgroundX, offset_y, 718, 38, kMenuDropdownBackgroundBlack);
ShowImage(
screens_path_.Append(supported_locales_[i]).Append("language.png"),
lang_x, offset_y);
}
offset_y += kItemHeight;
}
}
void Screens::LanguageMenuOnSelect() {
ShowLanguageMenu(false);
// Find index of current locale to show in the dropdown.
index_ = std::distance(
supported_locales_.begin(),
std::find(supported_locales_.begin(), supported_locales_.end(), locale_));
if (index_ == supported_locales_.size()) {
// Default to en-US.
index_ = 9;
LOG(WARNING) << " Could not find an index to match current locale "
<< locale_ << ". Defaulting to index " << index_ << " for "
<< supported_locales_[index_];
}
ShowLanguageDropdown();
}
void Screens::ShowLanguageMenu(bool is_selected) {
constexpr int kOffsetY = -kCanvasSize / 2 + 40;
constexpr int kBgX = -kCanvasSize / 2 + 145;
constexpr int kGlobeX = -kCanvasSize / 2 + 20;
constexpr int kArrowX = -kCanvasSize / 2 + 268;
int language_width;
if (!GetLangConstants(locale_, &language_width)) {
language_width = 100;
LOG(WARNING) << "Could not get language width for " << locale_
<< ". Defaulting to 100.";
}
const int kTextX = -kCanvasSize / 2 + 40 + language_width / 2;
base::FilePath menu_background =
is_selected ? screens_path_.Append("language_menu_bg_focused.png")
: screens_path_.Append("language_menu_bg.png");
ShowImage(menu_background, kBgX, kOffsetY);
ShowImage(screens_path_.Append("ic_language-globe.png"), kGlobeX, kOffsetY);
ShowImage(screens_path_.Append("ic_dropdown.png"), kArrowX, kOffsetY);
ShowMessage("language_folded", kTextX, kOffsetY);
}
void Screens::ShowFooter() {
constexpr int kQrCodeSize = 86;
constexpr int kQrCodeX = (-kCanvasSize / 2) + (kQrCodeSize / 2);
constexpr int kQrCodeY = (kCanvasSize / 2) - (kQrCodeSize / 2) - 56;
constexpr int kSeparatorX = 410 - (kCanvasSize / 2);
constexpr int kSeparatorY = kQrCodeY;
constexpr int kFooterLineHeight = 18;
constexpr int kFooterY = (kCanvasSize / 2) - kQrCodeSize + 9 - 56;
const int kFooterLeftX =
kQrCodeX + (kQrCodeSize / 2) + 16 + (kDefaultMessageWidth / 2);
const int kFooterRightX = kSeparatorX + 32 + (kDefaultMessageWidth / 2);
ShowMessage("footer_left_1", kFooterLeftX, kFooterY);
ShowMessage("footer_left_2", kFooterLeftX,
kFooterY + kFooterLineHeight * 2 + 14);
ShowMessage("footer_left_3", kFooterLeftX,
kFooterY + kFooterLineHeight * 3 + 14);
constexpr int kNavButtonHeight = 24;
constexpr int kNavButtonY = (kCanvasSize / 2) - (kNavButtonHeight / 2) - 56;
int nav_btn_x = kSeparatorX + 32;
// Navigation key icons.
const std::string kFooterType = is_detachable_ ? "tablet" : "clamshell";
const std::string kNavKeyEnter =
is_detachable_ ? "button_power" : "key_enter";
const std::string kNavKeyUp = is_detachable_ ? "button_volume_up" : "key_up";
const std::string kNavKeyDown =
is_detachable_ ? "button_volume_down" : "key_down";
constexpr int kUpDownIconWidth = 24;
constexpr int kIconPadding = 8;
const int kEnterIconWidth = is_detachable_ ? 40 : 66;
ShowMessage("footer_right_1_" + kFooterType, kFooterRightX, kFooterY);
ShowMessage("footer_right_2_" + kFooterType, kFooterRightX,
kFooterY + kFooterLineHeight + 8);
nav_btn_x += kEnterIconWidth / 2;
ShowImage(screens_path_.Append("nav-" + kNavKeyEnter + ".png"), nav_btn_x,
kNavButtonY);
nav_btn_x += kEnterIconWidth / 2 + kIconPadding + kUpDownIconWidth / 2;
ShowImage(screens_path_.Append("nav-" + kNavKeyUp + ".png"), nav_btn_x,
kNavButtonY);
nav_btn_x += kIconPadding + kUpDownIconWidth;
ShowImage(screens_path_.Append("nav-" + kNavKeyDown + ".png"), nav_btn_x,
kNavButtonY);
ShowImage(screens_path_.Append("qr_code.png"), kQrCodeX, kQrCodeY);
int hwid_len = hwid_.size();
int hwid_x = kQrCodeX + (kQrCodeSize / 2) + 16 + 5;
const int kHwidY = kFooterY + kFooterLineHeight;
if (right_to_left_) {
hwid_x = -hwid_x - kMonospaceGlyphWidth * (hwid_len - 2);
}
ShowText(hwid_, hwid_x, kHwidY, "grey");
ShowBox(kSeparatorX, kSeparatorY, 1, kQrCodeSize, kMenuGrey);
}
void Screens::MessageBaseScreen() {
ClearMainArea();
ShowLanguageMenu(false);
ShowFooter();
}
void Screens::ShowMiniOsWelcomeScreen() {
MessageBaseScreen();
ShowInstructionsWithTitle("MiniOS_welcome");
ShowStepper({"1", "2", "3"});
ShowLanguageMenu(index_ == 0);
constexpr int kBtnY = kTitleY + 80 + kBtnYStep * 2;
ShowButton("btn_next", kBtnY, (index_ == 1), default_button_width_, false);
ShowButton("btn_back", kBtnY + kBtnYStep, (index_ == 2),
default_button_width_, false);
}
void Screens::ShowMiniOsDropdownScreen() {
MessageBaseScreen();
ShowInstructions("title_MiniOS_dropdown");
ShowStepper({"1-done", "2", "3"});
ShowLanguageMenu(index_ == 0);
ShowCollapsedItemMenu((index_ == 1));
ShowButton("btn_back", kTitleY + 58 + (4 * kBtnYStep), (index_ == 2),
default_button_width_, false);
}
void Screens::ExpandItemDropdown() {
SetItems();
ShowLanguageMenu(false);
ShowCollapsedItemMenu(true);
ShowItemDropdown();
}
void Screens::ShowMiniOsGetPasswordScreen() {
MessageBaseScreen();
ShowInstructionsWithTitle("MiniOS_password");
ShowStepper({"done", "2-done", "3"});
ShowLanguageMenu(index_ == 0);
constexpr int kBtnY = kTitleY + 58 + kBtnYStep * 2;
ShowButton("Enter your password", kBtnY, false, default_button_width_ * 4,
true);
ShowButton("btn_back", kBtnY + kBtnYStep, (index_ == 2),
default_button_width_, false);
}
void Screens::GetPassword() {
std::string keyboard_layout;
if (!MapRegionToKeyboard(&keyboard_layout)) {
LOG(WARNING)
<< "Could not find xkb layout for given region. Defaulting to US.";
keyboard_layout = "us";
}
key_reader::KeyReader password_key_reader =
key_reader::KeyReader(/*include_usb=*/true, keyboard_layout);
password_key_reader.InputSetUp();
constexpr int kBtnY = kTitleY + 58 + kBtnYStep * 2;
ShowButton("", kBtnY, false, default_button_width_ * 4, true);
bool enter = false;
bool show_password = false;
std::string input;
std::string plain_text_password;
do {
if (!password_key_reader.GetUserInput(&enter, &show_password, &input))
continue;
plain_text_password = input;
if (!show_password) {
input = std::string(input.size(), '*');
}
ShowButton(input, kBtnY, false, default_button_width_ * 4, true);
} while (!enter);
// TODO(vyshu) : Logging password for development purposes only. Remove.
LOG(INFO) << "User password is: " << plain_text_password;
}
void Screens::ShowMiniOsDownloadingScreen() {
MessageBaseScreen();
ShowInstructionsWithTitle("MiniOS_downloading");
ShowStepper({"done", "done", "3-done"});
ShowLanguageMenu(false);
ShowProgressBar(10);
ShowMiniOsCompleteScreen();
}
void Screens::ShowMiniOsCompleteScreen() {
MessageBaseScreen();
ShowInstructions("title_MiniOS_complete");
ShowStepper({"done", "done", "done"});
ShowLanguageMenu(false);
ShowProgressBar(5);
// TODO(vyshu): Automatically reboot after timeout or on button selection.
ShowButton("Reboot", -100, false, default_button_width_, true);
}
void Screens::UpdateButtons(int menu_count, int key, bool* enter) {
int starting_index = index_;
// Make sure index is in range, if not reset to 0.
if (starting_index < 0 || starting_index >= menu_count)
starting_index = 0;
// Modify selected index and enter state based on user key input.
if (key == kKeyUp || key == kKeyVolUp) {
if (starting_index > 0) {
starting_index--;
}
} else if (key == kKeyDown || key == kKeyVolDown) {
if (starting_index < (menu_count - 1)) {
starting_index++;
}
} else if (key == kKeyEnter || key == kKeyPower) {
*enter = true;
} else {
LOG(ERROR) << "Unknown key value: " << key;
}
index_ = starting_index;
}
void Screens::ReadLangConstants() {
lang_constants_.clear();
supported_locales_.clear();
// Read language widths from lang_constants.sh into memory.
auto lang_constants_path = screens_path_.Append("lang_constants.sh");
std::string const_values;
if (!ReadFileToString(lang_constants_path, &const_values)) {
LOG(ERROR) << "Could not read lang constants file " << lang_constants_path;
return;
}
if (!base::SplitStringIntoKeyValuePairs(const_values, '=', '\n',
&lang_constants_)) {
LOG(ERROR) << "Unable to parse language width information.";
return;
}
for (const auto& pair : lang_constants_) {
if (pair.first == "SUPPORTED_LOCALES") {
// Parse list of supported locales and store separately.
std::string locale_list;
if (!base::RemoveChars(pair.second, "\"", &locale_list))
LOG(WARNING) << "Unable to remove surrounding quotes from locale list.";
supported_locales_ = base::SplitString(
locale_list, " ", base::TRIM_WHITESPACE, base::SPLIT_WANT_NONEMPTY);
}
}
// Add size of language dropdown menu using the number of locales.
menu_count_[static_cast<int>(ScreenType::kLanguageDropDownScreen)] =
supported_locales_.size();
if (supported_locales_.empty()) {
LOG(WARNING) << "Unable to get supported locales. Will not be able to "
"change locale.";
}
}
bool Screens::GetLangConstants(const std::string& locale, int* lang_width) {
if (lang_constants_.empty()) {
LOG(ERROR) << "No language widths available.";
return false;
}
// Lang_consts uses '_' while supported locale list uses '-'.
std::string token;
base::ReplaceChars(locale, "-", "_", &token);
token = "LANGUAGE_" + token + "_WIDTH";
// Find the width for the token.
for (const auto& width_token : lang_constants_) {
if (width_token.first == token) {
if (!base::StringToInt(width_token.second, lang_width)) {
LOG(ERROR) << "Could not convert " << width_token.second
<< " to a number.";
return false;
}
return true;
}
}
return false;
}
void Screens::OnLocaleChange() {
// Change locale and update constants.
locale_ = supported_locales_[index_];
CheckRightToLeft();
ReadDimensionConstants();
ClearScreen();
ShowFooter();
// Reset index state to go back to the MiniOs flow.
index_ = 1;
}
void Screens::ShowCollapsedItemMenu(bool is_selected) {
constexpr int kOffsetY = -kCanvasSize / 2 + 350;
constexpr int kBgX = -kCanvasSize / 2 + 145;
constexpr int kGlobeX = -kCanvasSize / 2 + 20;
constexpr int kArrowX = -kCanvasSize / 2 + 268;
constexpr int kTextX = -kCanvasSize / 2 + 100;
// Currently using language and globe icons as placeholders.
base::FilePath menu_background =
is_selected ? screens_path_.Append("language_menu_bg_focused.png")
: screens_path_.Append("language_menu_bg.png");
ShowImage(menu_background, kBgX, kOffsetY);
ShowImage(screens_path_.Append("ic_language-globe.png"), kGlobeX, kOffsetY);
ShowImage(screens_path_.Append("ic_dropdown.png"), kArrowX, kOffsetY);
ShowMessage("btn_MiniOS_display_options", kTextX, kOffsetY);
}
void Screens::ShowItemDropdown() {
constexpr int kItemPerPage = 10;
// Pick begin index such that the selected index is centered on the screen.
int begin_index =
std::clamp(index_ - kItemPerPage / 2, 0,
static_cast<int>(supported_locales_.size()) - kItemPerPage);
int offset_y = -kCanvasSize / 2 + 350 + 40;
constexpr int kBackgroundX = -kCanvasSize / 2 + 360;
constexpr int kOffsetX = -kCanvasSize / 2 + 60;
constexpr int kItemHeight = 40;
for (int i = begin_index;
i < (begin_index + kItemPerPage) && i < item_list_.size(); i++) {
if (index_ == i) {
ShowBox(kBackgroundX, offset_y, 720, 40, kMenuBlue);
ShowText(item_list_[i], kOffsetX, offset_y, "black");
} else {
ShowBox(kBackgroundX, offset_y, 720, 40, kMenuDropdownFrameNavy);
ShowBox(kBackgroundX, offset_y, 718, 38, kMenuDropdownBackgroundBlack);
ShowText(item_list_[i], kOffsetX, offset_y, "grey");
}
offset_y += kItemHeight;
}
}
void Screens::SetItems() {
// TODO(vyshu): temporary item names, replace with shill information.
item_list_ = {" item 1", "item2_public", "testing ! 1 2 ",
"32_char_is_the_longest_item_name"};
// Change the menu count for the Expanded dropdown menu based on number of
// items.
menu_count_[static_cast<int>(ScreenType::kExpandedDropDownScreen)] =
item_list_.size();
}
void Screens::CheckRightToLeft() {
// TODO(vyshu): Create an unblocked_terms.txt to allow "he" for Hebrew.
right_to_left_ = (locale_ == "ar" || locale_ == "fa" || locale_ == "he");
}
void Screens::CheckDetachable() {
is_detachable_ =
base::PathExists(root_.Append("etc/cros-initramfs/is_detachable"));
}
void Screens::GetVpdRegion() {
if (ReadFileToString(root_.Append("sys/firmware/vpd/ro/region"),
&vpd_region_)) {
return;
}
LOG(WARNING) << "Could not read vpd region from file. Trying commandline.";
int exit_code = 0;
std::string error;
if (!process_manager_->RunCommandWithOutput(
{"/bin/vpd", "-g", "region"}, &exit_code, &vpd_region_, &error) ||
exit_code) {
vpd_region_ = "us";
PLOG(WARNING) << "Error getting vpd -g region. Exit code " << exit_code
<< " with error " << error << ". Defaulting to 'us'. ";
return;
}
return;
}
void Screens::ReadHardwareId() {
int exit_code = 0;
std::string output, error;
if (!process_manager_->RunCommandWithOutput({"/bin/crossystem", "hwid"},
&exit_code, &output, &error) ||
exit_code) {
hwid_ = "CHROMEBOOK";
PLOG(WARNING)
<< "Could not get hwid from crossystem. Exited with exit code "
<< exit_code << " and error " << error
<< ". Defaulting to 'CHROMEBOOK'.";
return;
}
// Truncate HWID.
std::vector<std::string> hwid_parts = base::SplitString(
output, " ", base::TRIM_WHITESPACE, base::SPLIT_WANT_NONEMPTY);
hwid_ = hwid_parts[0];
return;
}
bool Screens::MapRegionToKeyboard(std::string* xkb_keyboard_layout) {
std::string cros_region_json;
if (!ReadFileToString(root_.Append("usr/share/misc/cros-regions.json"),
&cros_region_json)) {
PLOG(ERROR) << "Could not read JSON mapping from cros-regions.json.";
return false;
}
base::JSONReader::ValueWithError json_output =
base::JSONReader::ReadAndReturnValueWithError(cros_region_json);
if (!json_output.value || !json_output.value->is_dict()) {
LOG(ERROR) << "Could not read json. " << json_output.error_message;
return false;
}
// Look up mapping between vpd region and xkb keyboard layout.
const base::Value* kRegionInfo = json_output.value->FindDictKey(vpd_region_);
if (!kRegionInfo) {
LOG(ERROR) << "Region " << vpd_region_ << " not found.";
return false;
}
const base::Value* kKeyboard = kRegionInfo->FindListKey("keyboards");
if (!kKeyboard || kKeyboard->GetList().empty()) {
LOG(ERROR) << "Could not retrieve keyboards for given region "
<< vpd_region_
<< ". Available region information: " << *kRegionInfo;
return false;
}
// Always use the first keyboard in the list.
std::vector<std::string> keyboard_parts =
base::SplitString(kKeyboard->GetList()[0].GetString(), ":",
base::TRIM_WHITESPACE, base::SPLIT_WANT_NONEMPTY);
if (keyboard_parts.size() < 2) {
LOG(ERROR) << "Could not parse keyboard information for region "
<< vpd_region_;
return false;
}
*xkb_keyboard_layout = keyboard_parts[1];
return true;
}
void Screens::OnKeyPress(int fd_index, int key_changed, bool key_released) {
// Make sure you have seen a key press for this key before ending on key
// event release.
if (fd_index < 0 || key_changed < 0 || fd_index >= key_states_.size() ||
key_changed >= key_states_[0].size()) {
LOG(ERROR) << "Fd index or key code out of range. Index: " << fd_index
<< ". Key code: " << key_changed;
return;
}
if (key_released && key_states_[fd_index][key_changed]) {
key_states_[fd_index][key_changed] = false;
bool enter = false;
UpdateButtons(menu_count_[static_cast<int>(current_screen_)], key_changed,
&enter);
SwitchScreen(enter);
return;
} else if (!key_released) {
key_states_[fd_index][key_changed] = true;
}
}
void Screens::SwitchScreen(bool enter) {
if (enter && index_ == 0 &&
current_screen_ != ScreenType::kLanguageDropDownScreen &&
current_screen_ != ScreenType::kExpandedDropDownScreen &&
current_screen_ != ScreenType::kDoneWithFlow) {
previous_screen_ = current_screen_;
current_screen_ = ScreenType::kLanguageDropDownScreen;
LanguageMenuOnSelect();
return;
}
if (!enter) {
ShowNewScreen();
return;
}
switch (current_screen_) {
case ScreenType::kWelcomeScreen:
if (index_ == 1) {
current_screen_ = ScreenType::kDropDownScreen;
}
index_ = 1;
break;
case ScreenType::kDropDownScreen:
if (index_ == 1) {
index_ = 0;
current_screen_ = ScreenType::kExpandedDropDownScreen;
} else {
index_ = 1;
current_screen_ = ScreenType::kWelcomeScreen;
}
break;
case ScreenType::kExpandedDropDownScreen:
index_ = 1;
current_screen_ = ScreenType::kPasswordScreen;
break;
case ScreenType::kPasswordScreen:
if (index_ == 1) {
GetPassword();
current_screen_ = ScreenType::kDoneWithFlow;
} else {
index_ = 1;
current_screen_ = ScreenType::kDropDownScreen;
}
break;
case ScreenType::kLanguageDropDownScreen:
if (enter) {
current_screen_ = previous_screen_;
OnLocaleChange();
SwitchScreen(false);
return;
}
break;
case ScreenType::kDoneWithFlow:
return;
}
ShowNewScreen();
return;
}
void Screens::ShowNewScreen() {
switch (current_screen_) {
case ScreenType::kWelcomeScreen:
ShowMiniOsWelcomeScreen();
break;
case ScreenType::kDropDownScreen:
ShowMiniOsDropdownScreen();
break;
case ScreenType::kExpandedDropDownScreen:
ExpandItemDropdown();
break;
case ScreenType::kPasswordScreen:
ShowMiniOsGetPasswordScreen();
break;
case ScreenType::kLanguageDropDownScreen:
ShowLanguageDropdown();
break;
case ScreenType::kDoneWithFlow:
ShowMiniOsDownloadingScreen();
}
}
} // namespace screens