| /* Copyright 2020 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. |
| * |
| * User interfaces for developer and recovery mode menus. |
| */ |
| |
| #include "2api.h" |
| #include "2common.h" |
| #include "2misc.h" |
| #include "2nvstorage.h" |
| #include "2return_codes.h" |
| #include "2ui.h" |
| #include "2ui_private.h" |
| #include "vboot_api.h" /* For VB_SHUTDOWN_REQUEST_POWER_BUTTON */ |
| #include "vboot_kernel.h" |
| |
| /*****************************************************************************/ |
| /* Utility functions */ |
| |
| /** |
| * Check GBB flags against VbExIsShutdownRequested() shutdown request, |
| * and check for VB_BUTTON_POWER_SHORT_PRESS key, to determine if a |
| * shutdown is required. |
| * |
| * @param ui UI context pointer |
| * @return VB2_REQUEST_SHUTDOWN if shutdown needed, or VB2_REQUEST_UI_CONTINUE |
| */ |
| vb2_error_t check_shutdown_request(struct vb2_ui_context *ui) |
| { |
| struct vb2_gbb_header *gbb = vb2_get_gbb(ui->ctx); |
| uint32_t shutdown_request = VbExIsShutdownRequested(); |
| |
| /* |
| * Ignore power button push until after we have seen it released. |
| * This avoids shutting down immediately if the power button is still |
| * being held on startup. After we've recognized a valid power button |
| * push then don't report the event until after the button is released. |
| */ |
| if (shutdown_request & VB_SHUTDOWN_REQUEST_POWER_BUTTON) { |
| shutdown_request &= ~VB_SHUTDOWN_REQUEST_POWER_BUTTON; |
| if (ui->power_button == VB2_POWER_BUTTON_RELEASED) |
| ui->power_button = VB2_POWER_BUTTON_PRESSED; |
| } else { |
| if (ui->power_button == VB2_POWER_BUTTON_PRESSED) |
| shutdown_request |= VB_SHUTDOWN_REQUEST_POWER_BUTTON; |
| ui->power_button = VB2_POWER_BUTTON_RELEASED; |
| } |
| |
| if (ui->key == VB_BUTTON_POWER_SHORT_PRESS) |
| shutdown_request |= VB_SHUTDOWN_REQUEST_POWER_BUTTON; |
| |
| /* If desired, ignore shutdown request due to lid closure. */ |
| if (gbb->flags & VB2_GBB_FLAG_DISABLE_LID_SHUTDOWN) |
| shutdown_request &= ~VB_SHUTDOWN_REQUEST_LID_CLOSED; |
| |
| /* |
| * In detachables, disable shutdown due to power button. |
| * It is used for menu selection instead. |
| */ |
| if (DETACHABLE) |
| shutdown_request &= ~VB_SHUTDOWN_REQUEST_POWER_BUTTON; |
| |
| if (shutdown_request) |
| return VB2_REQUEST_SHUTDOWN; |
| |
| return VB2_REQUEST_UI_CONTINUE; |
| } |
| |
| /*****************************************************************************/ |
| /* Error action functions */ |
| |
| vb2_error_t error_exit_action(struct vb2_ui_context *ui) |
| { |
| /* |
| * If an error message is currently shown on the screen, any |
| * key press clears that error. Unset the key so that it is |
| * not processed by other action functions. |
| */ |
| if (ui->key && ui->error_code) { |
| ui->error_code = VB2_UI_ERROR_NONE; |
| ui->key = 0; |
| } |
| return VB2_REQUEST_UI_CONTINUE; |
| } |
| |
| /*****************************************************************************/ |
| /* Menu navigation functions */ |
| |
| const struct vb2_menu *get_menu(struct vb2_ui_context *ui) |
| { |
| const struct vb2_menu *menu; |
| static const struct vb2_menu empty_menu = { |
| .num_items = 0, |
| .items = NULL, |
| }; |
| if (ui->state->screen->get_menu) { |
| menu = ui->state->screen->get_menu(ui); |
| return menu ? menu : &empty_menu; |
| } else { |
| return &ui->state->screen->menu; |
| } |
| } |
| |
| vb2_error_t menu_navigation_action(struct vb2_ui_context *ui) |
| { |
| uint32_t key = ui->key; |
| |
| /* Map detachable button presses for simplicity. */ |
| if (DETACHABLE) { |
| if (key == VB_BUTTON_VOL_UP_SHORT_PRESS) |
| key = VB_KEY_UP; |
| else if (key == VB_BUTTON_VOL_DOWN_SHORT_PRESS) |
| key = VB_KEY_DOWN; |
| else if (key == VB_BUTTON_POWER_SHORT_PRESS) |
| key = VB_KEY_ENTER; |
| } |
| |
| switch (key) { |
| case VB_KEY_UP: |
| return vb2_ui_menu_prev(ui); |
| case VB_KEY_DOWN: |
| return vb2_ui_menu_next(ui); |
| case VB_KEY_ENTER: |
| return vb2_ui_menu_select(ui); |
| case VB_KEY_ESC: |
| return vb2_ui_screen_back(ui); |
| default: |
| if (key != 0) |
| VB2_DEBUG("Pressed key %#x, trusted? %d\n", |
| ui->key, ui->key_trusted); |
| } |
| |
| return VB2_REQUEST_UI_CONTINUE; |
| } |
| |
| vb2_error_t vb2_ui_menu_prev(struct vb2_ui_context *ui) |
| { |
| int item; |
| |
| if (!DETACHABLE && ui->key == VB_BUTTON_VOL_UP_SHORT_PRESS) |
| return VB2_REQUEST_UI_CONTINUE; |
| |
| item = ui->state->selected_item - 1; |
| while (item >= 0 && VB2_GET_BIT(ui->state->hidden_item_mask, item)) |
| item--; |
| /* Only update if item is valid */ |
| if (item >= 0) |
| ui->state->selected_item = item; |
| |
| return VB2_REQUEST_UI_CONTINUE; |
| } |
| |
| vb2_error_t vb2_ui_menu_next(struct vb2_ui_context *ui) |
| { |
| int item; |
| const struct vb2_menu *menu; |
| |
| if (!DETACHABLE && ui->key == VB_BUTTON_VOL_DOWN_SHORT_PRESS) |
| return VB2_REQUEST_UI_CONTINUE; |
| |
| menu = get_menu(ui); |
| item = ui->state->selected_item + 1; |
| while (item < menu->num_items && |
| VB2_GET_BIT(ui->state->hidden_item_mask, item)) |
| item++; |
| /* Only update if item is valid */ |
| if (item < menu->num_items) |
| ui->state->selected_item = item; |
| |
| return VB2_REQUEST_UI_CONTINUE; |
| } |
| |
| vb2_error_t vb2_ui_menu_select(struct vb2_ui_context *ui) |
| { |
| const struct vb2_menu *menu; |
| const struct vb2_menu_item *menu_item; |
| |
| if (!DETACHABLE && ui->key == VB_BUTTON_POWER_SHORT_PRESS) |
| return VB2_REQUEST_UI_CONTINUE; |
| |
| menu = get_menu(ui); |
| if (menu->num_items == 0) |
| return VB2_REQUEST_UI_CONTINUE; |
| |
| menu_item = &menu->items[ui->state->selected_item]; |
| |
| /* Cannot select a disabled menu item */ |
| if (VB2_GET_BIT(ui->state->disabled_item_mask, |
| ui->state->selected_item)) { |
| VB2_DEBUG("Menu item <%s> disabled; ignoring\n", |
| menu_item->text); |
| return VB2_REQUEST_UI_CONTINUE; |
| } |
| |
| if (menu_item->action) { |
| VB2_DEBUG("Menu item <%s> run action\n", menu_item->text); |
| return menu_item->action(ui); |
| } else if (menu_item->target) { |
| VB2_DEBUG("Menu item <%s> to target screen %#x\n", |
| menu_item->text, menu_item->target); |
| return vb2_ui_screen_change(ui, menu_item->target); |
| } |
| |
| VB2_DEBUG("Menu item <%s> no action or target screen\n", |
| menu_item->text); |
| return VB2_REQUEST_UI_CONTINUE; |
| } |
| |
| /*****************************************************************************/ |
| /* Screen navigation functions */ |
| |
| vb2_error_t vb2_ui_screen_back(struct vb2_ui_context *ui) |
| { |
| struct vb2_screen_state *tmp; |
| |
| if (ui->state && ui->state->prev) { |
| tmp = ui->state->prev; |
| free(ui->state); |
| ui->state = tmp; |
| if (ui->state->screen->reinit) |
| return ui->state->screen->reinit(ui); |
| } else { |
| VB2_DEBUG("ERROR: No previous screen; ignoring\n"); |
| } |
| |
| return VB2_REQUEST_UI_CONTINUE; |
| } |
| |
| static vb2_error_t default_screen_init(struct vb2_ui_context *ui) |
| { |
| const struct vb2_menu *menu = get_menu(ui); |
| ui->state->selected_item = 0; |
| if (menu->num_items > 1 && menu->items[0].is_language_select) |
| ui->state->selected_item = 1; |
| return VB2_REQUEST_UI_CONTINUE; |
| } |
| |
| vb2_error_t vb2_ui_screen_change(struct vb2_ui_context *ui, enum vb2_screen id) |
| { |
| const struct vb2_screen_info *new_screen_info; |
| struct vb2_screen_state *cur_state; |
| int state_exists = 0; |
| |
| new_screen_info = vb2_get_screen_info(id); |
| if (new_screen_info == NULL) { |
| VB2_DEBUG("ERROR: Screen entry %#x not found; ignoring\n", id); |
| return VB2_REQUEST_UI_CONTINUE; |
| } |
| |
| /* Check to see if the screen state already exists in our stack. */ |
| cur_state = ui->state; |
| while (cur_state != NULL) { |
| if (cur_state->screen->id == id) { |
| state_exists = 1; |
| break; |
| } |
| cur_state = cur_state->prev; |
| } |
| |
| if (state_exists) { |
| /* Pop until the requested screen is at the top of stack. */ |
| while (ui->state->screen->id != id) { |
| cur_state = ui->state; |
| ui->state = cur_state->prev; |
| free(cur_state); |
| } |
| if (ui->state->screen->reinit) |
| return ui->state->screen->reinit(ui); |
| } else { |
| /* Allocate the requested screen on top of the stack. */ |
| cur_state = malloc(sizeof(*ui->state)); |
| memset(cur_state, 0, sizeof(*ui->state)); |
| if (cur_state == NULL) { |
| VB2_DEBUG("WARNING: malloc failed; ignoring\n"); |
| return VB2_REQUEST_UI_CONTINUE; |
| } |
| cur_state->prev = ui->state; |
| cur_state->screen = new_screen_info; |
| ui->state = cur_state; |
| if (ui->state->screen->init) |
| return ui->state->screen->init(ui); |
| else |
| return default_screen_init(ui); |
| } |
| |
| return VB2_REQUEST_UI_CONTINUE; |
| } |
| |
| /*****************************************************************************/ |
| /* Core UI loop */ |
| |
| vb2_error_t ui_loop(struct vb2_context *ctx, enum vb2_screen root_screen_id, |
| vb2_error_t (*global_action)(struct vb2_ui_context *ui)) |
| { |
| struct vb2_ui_context ui; |
| struct vb2_screen_state prev_state; |
| int prev_disable_timer; |
| enum vb2_ui_error prev_error_code; |
| const struct vb2_menu *menu; |
| const struct vb2_screen_info *root_info; |
| uint32_t key_flags; |
| uint32_t start_time_ms, elapsed_ms; |
| vb2_error_t rv; |
| |
| memset(&ui, 0, sizeof(ui)); |
| ui.ctx = ctx; |
| root_info = vb2_get_screen_info(root_screen_id); |
| if (root_info == NULL) |
| VB2_DIE("Root screen not found.\n"); |
| ui.locale_id = vb2_nv_get(ctx, VB2_NV_LOCALIZATION_INDEX); |
| rv = vb2_ui_screen_change(&ui, root_screen_id); |
| if (rv != VB2_REQUEST_UI_CONTINUE) |
| return rv; |
| memset(&prev_state, 0, sizeof(prev_state)); |
| prev_disable_timer = 0; |
| prev_error_code = VB2_UI_ERROR_NONE; |
| |
| while (1) { |
| start_time_ms = vb2ex_mtime(); |
| |
| /* Draw if there are state changes. */ |
| if (memcmp(&prev_state, ui.state, sizeof(*ui.state)) || |
| /* Redraw when timer is disabled. */ |
| prev_disable_timer != ui.disable_timer || |
| /* Redraw/beep on a transition. */ |
| prev_error_code != ui.error_code || |
| /* Beep. */ |
| ui.error_beep != 0 || |
| /* Redraw on a screen request to refresh. */ |
| ui.force_display) { |
| |
| menu = get_menu(&ui); |
| VB2_DEBUG("<%s> menu item <%s>\n", |
| ui.state->screen->name, |
| menu->num_items ? |
| menu->items[ui.state->selected_item].text : |
| "null"); |
| vb2ex_display_ui(ui.state->screen->id, ui.locale_id, |
| ui.state->selected_item, |
| ui.state->disabled_item_mask, |
| ui.state->hidden_item_mask, |
| ui.disable_timer, |
| ui.state->current_page, |
| ui.error_code); |
| if (ui.error_beep || |
| (ui.error_code && |
| prev_error_code != ui.error_code)) { |
| vb2ex_beep(250, 400); |
| ui.error_beep = 0; |
| } |
| |
| /* Reset refresh flag. */ |
| ui.force_display = 0; |
| |
| /* Update prev variables. */ |
| memcpy(&prev_state, ui.state, sizeof(*ui.state)); |
| prev_disable_timer = ui.disable_timer; |
| prev_error_code = ui.error_code; |
| } |
| |
| /* Grab new keyboard input. */ |
| ui.key = VbExKeyboardReadWithFlags(&key_flags); |
| ui.key_trusted = !!(key_flags & VB_KEY_FLAG_TRUSTED_KEYBOARD); |
| |
| /* Check for shutdown request. */ |
| rv = check_shutdown_request(&ui); |
| if (rv != VB2_REQUEST_UI_CONTINUE) { |
| VB2_DEBUG("Shutdown requested!\n"); |
| return rv; |
| } |
| |
| /* Check if we need to exit an error box. */ |
| rv = error_exit_action(&ui); |
| if (rv != VB2_REQUEST_UI_CONTINUE) |
| return rv; |
| |
| /* Run screen action. */ |
| if (ui.state->screen->action) { |
| rv = ui.state->screen->action(&ui); |
| if (rv != VB2_REQUEST_UI_CONTINUE) |
| return rv; |
| } |
| |
| /* Run menu navigation action. */ |
| rv = menu_navigation_action(&ui); |
| if (rv != VB2_REQUEST_UI_CONTINUE) |
| return rv; |
| |
| /* Run global action function if available. */ |
| if (global_action) { |
| rv = global_action(&ui); |
| if (rv != VB2_REQUEST_UI_CONTINUE) |
| return rv; |
| } |
| |
| /* Delay. */ |
| elapsed_ms = vb2ex_mtime() - start_time_ms; |
| if (elapsed_ms < KEY_DELAY_MS) |
| vb2ex_msleep(KEY_DELAY_MS - elapsed_ms); |
| } |
| |
| return VB2_SUCCESS; |
| } |
| |
| /*****************************************************************************/ |
| /* Developer mode */ |
| |
| vb2_error_t vb2_developer_menu(struct vb2_context *ctx) |
| { |
| return ui_loop(ctx, VB2_SCREEN_DEVELOPER_MODE, developer_action); |
| } |
| |
| vb2_error_t developer_action(struct vb2_ui_context *ui) |
| { |
| /* Developer mode keyboard shortcuts */ |
| if (ui->key == VB_KEY_CTRL('S')) |
| return vb2_ui_screen_change(ui, VB2_SCREEN_DEVELOPER_TO_NORM); |
| if (ui->key == VB_KEY_CTRL('U') || |
| (DETACHABLE && ui->key == VB_BUTTON_VOL_UP_LONG_PRESS)) |
| return vb2_ui_developer_mode_boot_external_action(ui); |
| if (ui->key == VB_KEY_CTRL('D') || |
| (DETACHABLE && ui->key == VB_BUTTON_VOL_DOWN_LONG_PRESS)) |
| return vb2_ui_developer_mode_boot_internal_action(ui); |
| if (ui->key == VB_KEY_CTRL('L')) |
| return vb2_ui_developer_mode_boot_alternate_action(ui); |
| if (ui->key == '\t') |
| return vb2_ui_screen_change(ui, VB2_SCREEN_DEBUG_INFO); |
| |
| return VB2_REQUEST_UI_CONTINUE; |
| } |
| |
| /*****************************************************************************/ |
| /* Broken recovery */ |
| |
| vb2_error_t vb2_broken_recovery_menu(struct vb2_context *ctx) |
| { |
| return ui_loop(ctx, VB2_SCREEN_RECOVERY_BROKEN, broken_recovery_action); |
| } |
| |
| vb2_error_t broken_recovery_action(struct vb2_ui_context *ui) |
| { |
| /* Broken recovery keyboard shortcuts */ |
| if (ui->key == '\t') |
| return vb2_ui_screen_change(ui, VB2_SCREEN_DEBUG_INFO); |
| |
| return VB2_REQUEST_UI_CONTINUE; |
| } |
| |
| /*****************************************************************************/ |
| /* Manual recovery */ |
| |
| vb2_error_t vb2_manual_recovery_menu(struct vb2_context *ctx) |
| { |
| return ui_loop(ctx, VB2_SCREEN_RECOVERY_SELECT, manual_recovery_action); |
| } |
| |
| vb2_error_t manual_recovery_action(struct vb2_ui_context *ui) |
| { |
| /* See if we have a recovery kernel available yet. */ |
| vb2_error_t rv = VbTryLoadKernel(ui->ctx, VB_DISK_FLAG_REMOVABLE); |
| if (rv == VB2_SUCCESS) |
| return rv; |
| |
| /* If disk validity state changed, switch to appropriate screen. */ |
| if (ui->recovery_rv != rv) { |
| VB2_DEBUG("Recovery VbTryLoadKernel %#x --> %#x\n", |
| ui->recovery_rv, rv); |
| ui->recovery_rv = rv; |
| return vb2_ui_screen_change(ui, |
| rv == VB2_ERROR_LK_NO_DISK_FOUND ? |
| VB2_SCREEN_RECOVERY_SELECT : |
| VB2_SCREEN_RECOVERY_INVALID); |
| } |
| |
| /* Manual recovery keyboard shortcuts */ |
| if (ui->key == VB_KEY_CTRL('D') || |
| (DETACHABLE && ui->key == VB_BUTTON_VOL_UP_DOWN_COMBO_PRESS)) |
| return vb2_ui_screen_change(ui, VB2_SCREEN_RECOVERY_TO_DEV); |
| |
| if (ui->key == '\t') |
| return vb2_ui_screen_change(ui, VB2_SCREEN_DEBUG_INFO); |
| |
| return VB2_REQUEST_UI_CONTINUE; |
| } |
| |
| /*****************************************************************************/ |
| /* Diagnostics */ |
| |
| vb2_error_t vb2_diagnostic_menu(struct vb2_context *ctx) |
| { |
| return ui_loop(ctx, VB2_SCREEN_DIAGNOSTICS, NULL); |
| } |