blob: 90b346df7bb72eeaad7df260a5923be6bbfb73d1 [file] [log] [blame]
// Copyright 2017 The Chromium 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 "ash/login/ui/lock_contents_view.h"
#include <algorithm>
#include <memory>
#include <utility>
#include "ash/detachable_base/detachable_base_pairing_status.h"
#include "ash/focus_cycler.h"
#include "ash/ime/ime_controller.h"
#include "ash/keyboard/keyboard_observer_register.h"
#include "ash/login/login_screen_controller.h"
#include "ash/login/ui/layout_util.h"
#include "ash/login/ui/lock_screen.h"
#include "ash/login/ui/login_auth_user_view.h"
#include "ash/login/ui/login_bubble.h"
#include "ash/login/ui/login_detachable_base_model.h"
#include "ash/login/ui/login_user_view.h"
#include "ash/login/ui/non_accessible_view.h"
#include "ash/login/ui/note_action_launch_button.h"
#include "ash/login/ui/scrollable_users_list_view.h"
#include "ash/root_window_controller.h"
#include "ash/shelf/shelf.h"
#include "ash/shelf/shelf_widget.h"
#include "ash/shell.h"
#include "ash/strings/grit/ash_strings.h"
#include "ash/system/status_area_widget.h"
#include "ash/system/status_area_widget_delegate.h"
#include "ash/system/tray/system_tray_notifier.h"
#include "base/strings/string16.h"
#include "base/strings/utf_string_conversions.h"
#include "mojo/common/values_struct_traits.h"
#include "ui/accessibility/ax_node_data.h"
#include "ui/base/l10n/l10n_util.h"
#include "ui/display/display.h"
#include "ui/display/manager/display_manager.h"
#include "ui/display/manager/managed_display_info.h"
#include "ui/gfx/geometry/insets.h"
#include "ui/gfx/geometry/rect.h"
#include "ui/gfx/geometry/size.h"
#include "ui/gfx/geometry/vector2d.h"
#include "ui/views/accessibility/ax_aura_obj_cache.h"
#include "ui/views/background.h"
#include "ui/views/controls/label.h"
#include "ui/views/controls/scroll_view.h"
#include "ui/views/controls/styled_label.h"
#include "ui/views/focus/focus_search.h"
#include "ui/views/layout/box_layout.h"
#include "ui/views/layout/fill_layout.h"
#include "ui/views/style/typography.h"
#include "ui/views/view.h"
namespace ash {
namespace {
// Any non-zero value used for separator height. Makes debugging easier; this
// should not affect visual appearance.
constexpr int kNonEmptyHeightDp = 30;
// Horizontal distance between two users in the low density layout.
constexpr int kLowDensityDistanceBetweenUsersInLandscapeDp = 118;
constexpr int kLowDensityDistanceBetweenUsersInPortraitDp = 32;
// Margin left of the auth user in the medium density layout.
constexpr int kMediumDensityMarginLeftOfAuthUserLandscapeDp = 98;
constexpr int kMediumDensityMarginLeftOfAuthUserPortraitDp = 0;
// Horizontal distance between the auth user and the medium density user row.
constexpr int kMediumDensityDistanceBetweenAuthUserAndUsersLandscapeDp = 220;
constexpr int kMediumDensityDistanceBetweenAuthUserAndUsersPortraitDp = 84;
constexpr const char kLockContentsViewName[] = "LockContentsView";
// A view which stores two preferred sizes. The embedder can control which one
// is used.
class MultiSizedView : public views::View {
public:
MultiSizedView(const gfx::Size& a, const gfx::Size& b) : a_(a), b_(b) {}
~MultiSizedView() override = default;
void SwapPreferredSizeTo(bool use_a) {
if (use_a)
SetPreferredSize(a_);
else
SetPreferredSize(b_);
}
private:
gfx::Size a_;
gfx::Size b_;
DISALLOW_COPY_AND_ASSIGN(MultiSizedView);
};
// Returns the first or last focusable child of |root|. If |reverse| is false,
// this returns the first focusable child. If |reverse| is true, this returns
// the last focusable child.
views::View* FindFirstOrLastFocusableChild(views::View* root, bool reverse) {
views::FocusSearch search(root, reverse /*cycle*/,
false /*accessibility_mode*/);
views::FocusTraversable* dummy_focus_traversable;
views::View* dummy_focus_traversable_view;
return search.FindNextFocusableView(
root, reverse, views::FocusSearch::DOWN, false /*check_starting_view*/,
&dummy_focus_traversable, &dummy_focus_traversable_view);
}
// Make a section of the text bold.
// |label|: The label to apply mixed styles.
// |text|: The message to display.
// |bold_start|: The position in |text| to start bolding.
// |bold_length|: The length of bold text.
void MakeSectionBold(views::StyledLabel* label,
const base::string16& text,
const base::Optional<int>& bold_start,
int bold_length) {
auto create_style = [&](bool is_bold) {
views::StyledLabel::RangeStyleInfo style;
if (is_bold) {
style.custom_font = label->GetDefaultFontList().Derive(
0, gfx::Font::FontStyle::NORMAL, gfx::Font::Weight::BOLD);
}
style.override_color = SK_ColorWHITE;
return style;
};
auto add_style = [&](const views::StyledLabel::RangeStyleInfo& style,
int start, int end) {
if (start >= end)
return;
label->AddStyleRange(gfx::Range(start, end), style);
};
views::StyledLabel::RangeStyleInfo regular_style =
create_style(false /*is_bold*/);
views::StyledLabel::RangeStyleInfo bold_style =
create_style(true /*is_bold*/);
if (!bold_start || bold_length == 0) {
add_style(regular_style, 0, text.length());
return;
}
add_style(regular_style, 0, *bold_start - 1);
add_style(bold_style, *bold_start, *bold_start + bold_length);
add_style(regular_style, *bold_start + bold_length + 1, text.length());
}
// Helper function to create a label for the dev channel info view.
views::Label* CreateInfoLabel() {
views::Label* label = new views::Label();
label->SetAutoColorReadabilityEnabled(false);
label->SetEnabledColor(SK_ColorWHITE);
label->SetFontList(views::Label::GetDefaultFontList().Derive(
-1, gfx::Font::FontStyle::NORMAL, gfx::Font::Weight::NORMAL));
label->SetSubpixelRenderingEnabled(false);
return label;
}
keyboard::KeyboardController* GetKeyboardControllerForWidget(
const views::Widget* widget) {
keyboard::KeyboardController* keyboard_controller =
keyboard::KeyboardController::GetInstance();
if (!keyboard_controller)
return nullptr;
aura::Window* keyboard_window =
keyboard_controller->GetContainerWindow()->GetRootWindow();
aura::Window* this_window = widget->GetNativeWindow()->GetRootWindow();
return keyboard_window == this_window ? keyboard_controller : nullptr;
}
} // namespace
LockContentsView::TestApi::TestApi(LockContentsView* view) : view_(view) {}
LockContentsView::TestApi::~TestApi() = default;
LoginAuthUserView* LockContentsView::TestApi::primary_auth() const {
return view_->primary_auth_;
}
LoginAuthUserView* LockContentsView::TestApi::opt_secondary_auth() const {
return view_->opt_secondary_auth_;
}
ScrollableUsersListView* LockContentsView::TestApi::users_list() const {
return view_->users_list_;
}
views::View* LockContentsView::TestApi::note_action() const {
return view_->note_action_;
}
LoginBubble* LockContentsView::TestApi::tooltip_bubble() const {
return view_->tooltip_bubble_.get();
}
LoginBubble* LockContentsView::TestApi::auth_error_bubble() const {
return view_->auth_error_bubble_.get();
}
LoginBubble* LockContentsView::TestApi::detachable_base_error_bubble() const {
return view_->detachable_base_error_bubble_.get();
}
views::View* LockContentsView::TestApi::dev_channel_info() const {
return view_->dev_channel_info_;
}
LockContentsView::UserState::UserState(AccountId account_id)
: account_id(account_id) {}
LockContentsView::UserState::UserState(UserState&&) = default;
LockContentsView::UserState::~UserState() = default;
LockContentsView::LockContentsView(
mojom::TrayActionState initial_note_action_state,
LoginDataDispatcher* data_dispatcher,
std::unique_ptr<LoginDetachableBaseModel> detachable_base_model)
: NonAccessibleView(kLockContentsViewName),
data_dispatcher_(data_dispatcher),
detachable_base_model_(std::move(detachable_base_model)),
display_observer_(this),
session_observer_(this),
keyboard_observer_(this) {
data_dispatcher_->AddObserver(this);
display_observer_.Add(display::Screen::GetScreen());
Shell::Get()->login_screen_controller()->AddLockScreenAppsFocusObserver(this);
Shell::Get()->system_tray_notifier()->AddSystemTrayFocusObserver(this);
auth_error_bubble_ = std::make_unique<LoginBubble>();
detachable_base_error_bubble_ = std::make_unique<LoginBubble>();
tooltip_bubble_ = std::make_unique<LoginBubble>();
// We reuse the focusable state on this view as a signal that focus should
// switch to the system tray. LockContentsView should otherwise not be
// focusable.
SetFocusBehavior(FocusBehavior::ALWAYS);
SetLayoutManager(std::make_unique<views::FillLayout>());
main_view_ = new NonAccessibleView();
AddChildView(main_view_);
// The top header view.
top_header_ = new views::View();
auto top_header_layout =
std::make_unique<views::BoxLayout>(views::BoxLayout::kHorizontal);
top_header_layout->set_main_axis_alignment(
views::BoxLayout::MAIN_AXIS_ALIGNMENT_END);
top_header_->SetLayoutManager(std::move(top_header_layout));
AddChildView(top_header_);
dev_channel_info_ = new views::View();
auto dev_channel_info_layout = std::make_unique<views::BoxLayout>(
views::BoxLayout::kVertical, gfx::Insets(5, 8));
dev_channel_info_layout->set_cross_axis_alignment(
views::BoxLayout::CROSS_AXIS_ALIGNMENT_END);
dev_channel_info_->SetLayoutManager(std::move(dev_channel_info_layout));
dev_channel_info_->SetVisible(false);
top_header_->AddChildView(dev_channel_info_);
note_action_ = new NoteActionLaunchButton(initial_note_action_state);
top_header_->AddChildView(note_action_);
OnLockScreenNoteStateChanged(initial_note_action_state);
Shell::Get()->AddShellObserver(this);
}
LockContentsView::~LockContentsView() {
data_dispatcher_->RemoveObserver(this);
Shell::Get()->login_screen_controller()->RemoveLockScreenAppsFocusObserver(
this);
Shell::Get()->system_tray_notifier()->RemoveSystemTrayFocusObserver(this);
if (unlock_attempt_ > 0) {
// Times a password was incorrectly entered until user gives up (sign out
// current session or shutdown the device). For a successful unlock,
// unlock_attempt_ should already be reset by OnLockStateChanged.
Shell::Get()->metrics()->login_metrics_recorder()->RecordNumLoginAttempts(
unlock_attempt_, false /*success*/);
}
Shell::Get()->RemoveShellObserver(this);
keyboard_observer_.RemoveAll();
}
void LockContentsView::Layout() {
View::Layout();
LayoutTopHeader();
if (users_list_)
users_list_->Layout();
}
void LockContentsView::AddedToWidget() {
// Register keyboard observer after view has been added to the widget. If
// virtual keyboard is activated before displaying lock screen we do not
// receive OnVirtualKeyboardStateChanged() callback and we need to register
// keyboard observer here.
keyboard::KeyboardController* keyboard_controller = GetKeyboardController();
if (keyboard_controller)
keyboard_observer_.Add(keyboard_controller);
DoLayout();
// Focus the primary user when showing the UI. This will focus the password.
if (primary_auth_)
primary_auth_->RequestFocus();
}
void LockContentsView::OnFocus() {
// If LockContentsView somehow gains focus (ie, a test, but it should not
// under typical circumstances), immediately forward the focus to the
// primary_auth_ since LockContentsView has no real focusable content by
// itself.
if (primary_auth_)
primary_auth_->RequestFocus();
}
void LockContentsView::AboutToRequestFocusFromTabTraversal(bool reverse) {
// The LockContentsView itself doesn't have anything to focus. If it gets
// focused we should change the currently focused widget (ie, to the shelf or
// status area, or lock screen apps, if they are active).
if (reverse && lock_screen_apps_active_) {
Shell::Get()->login_screen_controller()->FocusLockScreenApps(reverse);
return;
}
FocusNextWidget(reverse);
}
void LockContentsView::GetAccessibleNodeData(ui::AXNodeData* node_data) {
Shelf* shelf = Shelf::ForWindow(GetWidget()->GetNativeWindow());
ShelfWidget* shelf_widget = shelf->shelf_widget();
int next_id = views::AXAuraObjCache::GetInstance()->GetID(shelf_widget);
node_data->AddIntAttribute(ax::mojom::IntAttribute::kNextFocusId, next_id);
int previous_id =
views::AXAuraObjCache::GetInstance()->GetID(shelf->GetStatusAreaWidget());
node_data->AddIntAttribute(ax::mojom::IntAttribute::kPreviousFocusId,
previous_id);
node_data->SetNameExplicitlyEmpty();
}
void LockContentsView::OnUsersChanged(
const std::vector<mojom::LoginUserInfoPtr>& users) {
// The debug view will potentially call this method many times. Make sure to
// invalidate any child references.
main_view_->RemoveAllChildViews(true /*delete_children*/);
opt_secondary_auth_ = nullptr;
users_list_ = nullptr;
rotation_actions_.clear();
users_.clear();
// If there are no users we have no UI to build.
if (users.empty()) {
LOG(ERROR) << "Empty user list received";
return;
}
// Build user state list.
for (const mojom::LoginUserInfoPtr& user : users)
users_.push_back(UserState{user->basic_user_info->account_id});
auto box_layout =
std::make_unique<views::BoxLayout>(views::BoxLayout::kHorizontal);
main_layout_ = box_layout.get();
main_layout_->set_main_axis_alignment(
views::BoxLayout::MAIN_AXIS_ALIGNMENT_CENTER);
main_layout_->set_cross_axis_alignment(
views::BoxLayout::CROSS_AXIS_ALIGNMENT_CENTER);
main_view_->SetLayoutManager(std::move(box_layout));
// Add auth user.
primary_auth_ = AllocateLoginAuthUserView(users[0], true /*is_primary*/);
main_view_->AddChildView(primary_auth_);
// Build layout for additional users.
if (users.size() == 2)
CreateLowDensityLayout(users);
else if (users.size() >= 3 && users.size() <= 6)
CreateMediumDensityLayout(users);
else if (users.size() >= 7)
CreateHighDensityLayout(users);
LayoutAuth(primary_auth_, opt_secondary_auth_, false /*animate*/);
// Auth user may be the same if we already built lock screen.
OnAuthUserChanged();
// Force layout.
PreferredSizeChanged();
Layout();
}
void LockContentsView::OnPinEnabledForUserChanged(const AccountId& user,
bool enabled) {
LockContentsView::UserState* state = FindStateForUser(user);
if (!state) {
LOG(ERROR) << "Unable to find user when changing PIN state to " << enabled;
return;
}
state->show_pin = enabled;
LoginAuthUserView* auth_user =
TryToFindAuthUser(user, true /*require_auth_active*/);
if (auth_user)
LayoutAuth(auth_user, nullptr /*opt_to_hide*/, true /*animate*/);
}
void LockContentsView::OnClickToUnlockEnabledForUserChanged(
const AccountId& user,
bool enabled) {
LockContentsView::UserState* state = FindStateForUser(user);
if (!state) {
LOG(ERROR) << "Unable to find user enabling click to auth";
return;
}
state->enable_tap_auth = enabled;
LoginAuthUserView* auth_user =
TryToFindAuthUser(user, true /*require_auth_active*/);
if (auth_user)
LayoutAuth(auth_user, nullptr /*opt_to_hide*/, true /*animate*/);
}
void LockContentsView::OnShowEasyUnlockIcon(
const AccountId& user,
const mojom::EasyUnlockIconOptionsPtr& icon) {
UserState* state = FindStateForUser(user);
if (!state)
return;
state->easy_unlock_state = icon->Clone();
UpdateEasyUnlockIconForUser(user);
// Show tooltip only if the user is actively showing auth.
auto* auth_user = TryToFindAuthUser(user, true /*require_auth_active*/);
if (auth_user) {
tooltip_bubble_->Close();
if (icon->autoshow_tooltip) {
tooltip_bubble_->ShowTooltip(
icon->tooltip,
CurrentAuthUserView()->password_view() /*anchor_view*/);
}
}
}
void LockContentsView::OnLockScreenNoteStateChanged(
mojom::TrayActionState state) {
bool old_lock_screen_apps_active = lock_screen_apps_active_;
lock_screen_apps_active_ = state == mojom::TrayActionState::kActive;
note_action_->UpdateVisibility(state);
LayoutTopHeader();
// If lock screen apps just got deactivated - request focus for primary auth,
// which should focus the password field.
if (old_lock_screen_apps_active && !lock_screen_apps_active_ && primary_auth_)
primary_auth_->RequestFocus();
}
void LockContentsView::OnDevChannelInfoChanged(
const std::string& os_version_label_text,
const std::string& enterprise_info_text,
const std::string& bluetooth_name) {
DCHECK(!os_version_label_text.empty() || !enterprise_info_text.empty() ||
!bluetooth_name.empty());
if (!dev_channel_info_->visible()) {
// Initialize the dev channel info view.
dev_channel_info_->SetVisible(true);
for (int i = 0; i < 3; ++i)
dev_channel_info_->AddChildView(CreateInfoLabel());
}
views::Label* version_label =
static_cast<views::Label*>(dev_channel_info_->child_at(0));
version_label->SetVisible(!os_version_label_text.empty());
version_label->SetText(base::UTF8ToUTF16(os_version_label_text));
views::Label* enterprise_label =
static_cast<views::Label*>(dev_channel_info_->child_at(1));
enterprise_label->SetVisible(!enterprise_info_text.empty());
enterprise_label->SetText(base::UTF8ToUTF16(enterprise_info_text));
views::Label* bluetooth_label =
static_cast<views::Label*>(dev_channel_info_->child_at(2));
bluetooth_label->SetVisible(!bluetooth_name.empty());
bluetooth_label->SetText(base::UTF8ToUTF16(bluetooth_name));
LayoutTopHeader();
}
void LockContentsView::OnPublicSessionDisplayNameChanged(
const AccountId& account_id,
const std::string& display_name) {
NOTIMPLEMENTED();
}
void LockContentsView::OnPublicSessionLocalesChanged(
const AccountId& account_id,
const base::ListValue& locales,
const std::string& default_locale,
bool show_advanced_view) {
NOTIMPLEMENTED();
}
void LockContentsView::OnDetachableBasePairingStatusChanged(
DetachableBasePairingStatus pairing_status) {
const mojom::UserInfoPtr& user_info =
CurrentAuthUserView()->current_user()->basic_user_info;
// If the base is not paired, or the paired base matches the last used by the
// current user, the detachable base error bubble should be hidden. Otherwise,
// the bubble should be shown.
if (pairing_status == DetachableBasePairingStatus::kNone ||
(pairing_status == DetachableBasePairingStatus::kAuthenticated &&
detachable_base_model_->PairedBaseMatchesLastUsedByUser(*user_info))) {
detachable_base_error_bubble_->Close();
return;
}
auth_error_bubble_->Close();
base::string16 error_text =
l10n_util::GetStringUTF16(IDS_ASH_LOGIN_ERROR_DETACHABLE_BASE_CHANGED);
views::Label* label =
new views::Label(error_text, views::style::CONTEXT_MESSAGE_BOX_BODY_TEXT,
views::style::STYLE_PRIMARY);
label->SetMultiLine(true);
label->SetAutoColorReadabilityEnabled(false);
label->SetHorizontalAlignment(gfx::ALIGN_LEFT);
label->SetEnabledColor(SK_ColorWHITE);
detachable_base_error_bubble_->ShowErrorBubble(
label, CurrentAuthUserView()->password_view() /*anchor_view*/,
LoginBubble::kFlagPersistent);
// Remove the focus from the password field, to make user less likely to enter
// the password without seeing the warning about detachable base change.
if (GetWidget()->IsActive())
GetWidget()->GetFocusManager()->ClearFocus();
}
void LockContentsView::OnFocusLeavingLockScreenApps(bool reverse) {
if (!reverse || lock_screen_apps_active_)
FocusNextWidget(reverse);
else
FindFirstOrLastFocusableChild(this, reverse)->RequestFocus();
}
void LockContentsView::OnFocusLeavingSystemTray(bool reverse) {
// This function is called when the system tray is losing focus. We want to
// focus the first or last child in this view, or a lock screen app window if
// one is active (in which case lock contents should not have focus). In the
// later case, still focus lock screen first, to synchronously take focus away
// from the system shelf (or tray) - lock shelf view expect the focus to be
// taken when it passes it to lock screen view, and can misbehave in case the
// focus is kept in it.
FindFirstOrLastFocusableChild(this, reverse)->RequestFocus();
if (lock_screen_apps_active_) {
Shell::Get()->login_screen_controller()->FocusLockScreenApps(reverse);
return;
}
}
void LockContentsView::OnDisplayMetricsChanged(const display::Display& display,
uint32_t changed_metrics) {
// Ignore all metric changes except rotation.
if ((changed_metrics & DISPLAY_METRIC_ROTATION) == 0)
return;
DoLayout();
}
void LockContentsView::OnLockStateChanged(bool locked) {
if (!locked) {
// Successfully unlock the screen.
Shell::Get()->metrics()->login_metrics_recorder()->RecordNumLoginAttempts(
unlock_attempt_, true /*success*/);
unlock_attempt_ = 0;
}
}
void LockContentsView::OnVirtualKeyboardStateChanged(
bool activated,
aura::Window* root_window) {
const views::Widget* widget = GetWidget();
if (widget) {
UpdateKeyboardObserverFromStateChanged(
activated, root_window, widget->GetNativeWindow()->GetRootWindow(),
&keyboard_observer_);
}
}
void LockContentsView::OnStateChanged(
const keyboard::KeyboardControllerState state) {
if (state == keyboard::KeyboardControllerState::SHOWN ||
state == keyboard::KeyboardControllerState::HIDDEN) {
LayoutAuth(primary_auth_, opt_secondary_auth_, false /*animate*/);
}
}
void LockContentsView::FocusNextWidget(bool reverse) {
Shelf* shelf = Shelf::ForWindow(GetWidget()->GetNativeWindow());
// Tell the focus direction to the status area or the shelf so they can focus
// the correct child view.
if (reverse) {
shelf->GetStatusAreaWidget()
->status_area_widget_delegate()
->set_default_last_focusable_child(reverse);
Shell::Get()->focus_cycler()->FocusWidget(shelf->GetStatusAreaWidget());
} else {
shelf->shelf_widget()->set_default_last_focusable_child(reverse);
Shell::Get()->focus_cycler()->FocusWidget(shelf->shelf_widget());
}
}
void LockContentsView::CreateLowDensityLayout(
const std::vector<mojom::LoginUserInfoPtr>& users) {
DCHECK_EQ(users.size(), 2u);
// Space between auth user and alternative user.
main_view_->AddChildView(MakeOrientationViewWithWidths(
kLowDensityDistanceBetweenUsersInLandscapeDp,
kLowDensityDistanceBetweenUsersInPortraitDp));
// Build auth user.
opt_secondary_auth_ =
AllocateLoginAuthUserView(users[1], false /*is_primary*/);
opt_secondary_auth_->SetAuthMethods(LoginAuthUserView::AUTH_NONE);
main_view_->AddChildView(opt_secondary_auth_);
}
void LockContentsView::CreateMediumDensityLayout(
const std::vector<mojom::LoginUserInfoPtr>& users) {
// Insert spacing before (left of) auth.
main_view_->AddChildViewAt(MakeOrientationViewWithWidths(
kMediumDensityMarginLeftOfAuthUserLandscapeDp,
kMediumDensityMarginLeftOfAuthUserPortraitDp),
0);
// Insert spacing between auth and user list.
main_view_->AddChildView(MakeOrientationViewWithWidths(
kMediumDensityDistanceBetweenAuthUserAndUsersLandscapeDp,
kMediumDensityDistanceBetweenAuthUserAndUsersPortraitDp));
users_list_ = BuildScrollableUsersListView(users, LoginDisplayStyle::kSmall);
main_view_->AddChildView(users_list_);
// Insert dynamic spacing on left/right of the content which changes based on
// screen rotation and display size.
auto* left = new NonAccessibleView();
main_view_->AddChildViewAt(left, 0);
auto* right = new NonAccessibleView();
main_view_->AddChildView(right);
AddRotationAction(base::BindRepeating(
[](views::BoxLayout* layout, views::View* left, views::View* right,
bool landscape) {
if (landscape) {
layout->SetFlexForView(left, 1);
layout->SetFlexForView(right, 1);
} else {
layout->SetFlexForView(left, 2);
layout->SetFlexForView(right, 1);
}
},
main_layout_, left, right));
}
void LockContentsView::CreateHighDensityLayout(
const std::vector<mojom::LoginUserInfoPtr>& users) {
// Insert spacing before and after the auth view.
auto* fill = new NonAccessibleView();
main_view_->AddChildViewAt(fill, 0);
main_layout_->SetFlexForView(fill, 1);
fill = new NonAccessibleView();
main_view_->AddChildView(fill);
main_layout_->SetFlexForView(fill, 1);
users_list_ =
BuildScrollableUsersListView(users, LoginDisplayStyle::kExtraSmall);
main_view_->AddChildView(users_list_);
}
void LockContentsView::DoLayout() {
bool landscape = login_layout_util::ShouldShowLandscape(GetWidget());
for (auto& action : rotation_actions_)
action.Run(landscape);
const display::Display& display =
display::Screen::GetScreen()->GetDisplayNearestWindow(
GetWidget()->GetNativeWindow());
SetPreferredSize(display.size());
SizeToPreferredSize();
Layout();
}
void LockContentsView::LayoutTopHeader() {
int preferred_width = dev_channel_info_->GetPreferredSize().width() +
note_action_->GetPreferredSize().width();
int preferred_height =
std::max(dev_channel_info_->GetPreferredSize().height(),
note_action_->GetPreferredSize().height());
top_header_->SetPreferredSize(gfx::Size(preferred_width, preferred_height));
top_header_->SizeToPreferredSize();
top_header_->Layout();
// Position the top header - the origin is offset to the left from the top
// right corner of the entire view by the width of this top header view.
top_header_->SetPosition(GetLocalBounds().top_right() -
gfx::Vector2d(preferred_width, 0));
}
views::View* LockContentsView::MakeOrientationViewWithWidths(int landscape,
int portrait) {
auto* view = new MultiSizedView(gfx::Size(landscape, kNonEmptyHeightDp),
gfx::Size(portrait, kNonEmptyHeightDp));
AddRotationAction(base::BindRepeating(&MultiSizedView::SwapPreferredSizeTo,
base::Unretained(view)));
return view;
}
void LockContentsView::AddRotationAction(const OnRotate& on_rotate) {
on_rotate.Run(login_layout_util::ShouldShowLandscape(GetWidget()));
rotation_actions_.push_back(on_rotate);
}
void LockContentsView::SwapActiveAuthBetweenPrimaryAndSecondary(
bool is_primary) {
if (is_primary &&
primary_auth_->auth_methods() == LoginAuthUserView::AUTH_NONE) {
LayoutAuth(primary_auth_, opt_secondary_auth_, true /*animate*/);
OnAuthUserChanged();
} else if (!is_primary && opt_secondary_auth_ &&
opt_secondary_auth_->auth_methods() ==
LoginAuthUserView::AUTH_NONE) {
LayoutAuth(opt_secondary_auth_, primary_auth_, true /*animate*/);
OnAuthUserChanged();
}
}
void LockContentsView::OnAuthenticate(bool auth_success) {
if (auth_success) {
auth_error_bubble_->Close();
detachable_base_error_bubble_->Close();
// Now that the user has been authenticated, update the user's last used
// detachable base (if one is attached). This will prevent further
// detachable base change notifications from appearing for this base (until
// the user uses another detachable base).
if (detachable_base_model_->GetPairingStatus() ==
DetachableBasePairingStatus::kAuthenticated) {
detachable_base_model_->SetPairedBaseAsLastUsedByUser(
*CurrentAuthUserView()->current_user()->basic_user_info);
}
} else {
ShowAuthErrorMessage();
++unlock_attempt_;
}
}
LockContentsView::UserState* LockContentsView::FindStateForUser(
const AccountId& user) {
for (UserState& state : users_) {
if (state.account_id == user)
return &state;
}
return nullptr;
}
void LockContentsView::LayoutAuth(LoginAuthUserView* to_update,
LoginAuthUserView* opt_to_hide,
bool animate) {
// Capture animation metadata before we changing state.
if (animate) {
to_update->CaptureStateForAnimationPreLayout();
if (opt_to_hide)
opt_to_hide->CaptureStateForAnimationPreLayout();
}
// Update auth methods for |to_update|. Disable auth on |opt_to_hide|.
uint32_t to_update_auth = LoginAuthUserView::AUTH_PASSWORD;
UserState* state =
FindStateForUser(to_update->current_user()->basic_user_info->account_id);
keyboard::KeyboardController* keyboard_controller = GetKeyboardController();
bool keyboard_visible =
keyboard_controller ? keyboard_controller->keyboard_visible() : false;
if (state->show_pin && !keyboard_visible)
to_update_auth |= LoginAuthUserView::AUTH_PIN;
if (state->enable_tap_auth)
to_update_auth |= LoginAuthUserView::AUTH_TAP;
to_update->SetAuthMethods(to_update_auth);
if (opt_to_hide)
opt_to_hide->SetAuthMethods(LoginAuthUserView::AUTH_NONE);
Layout();
// Apply animations.
if (animate) {
to_update->ApplyAnimationPostLayout();
if (opt_to_hide)
opt_to_hide->ApplyAnimationPostLayout();
}
}
void LockContentsView::SwapToAuthUser(int user_index) {
DCHECK(users_list_);
auto* view = users_list_->GetUserViewAtIndex(user_index);
DCHECK(view);
mojom::LoginUserInfoPtr previous_auth_user =
primary_auth_->current_user()->Clone();
mojom::LoginUserInfoPtr new_auth_user = view->current_user()->Clone();
view->UpdateForUser(previous_auth_user, true /*animate*/);
primary_auth_->UpdateForUser(new_auth_user);
LayoutAuth(primary_auth_, nullptr, true /*animate*/);
OnAuthUserChanged();
}
void LockContentsView::OnAuthUserChanged() {
const AccountId new_auth_user =
CurrentAuthUserView()->current_user()->basic_user_info->account_id;
Shell::Get()->login_screen_controller()->OnFocusPod(new_auth_user);
UpdateEasyUnlockIconForUser(new_auth_user);
if (unlock_attempt_ > 0) {
// Times a password was incorrectly entered until user gives up (change
// user pod).
Shell::Get()->metrics()->login_metrics_recorder()->RecordNumLoginAttempts(
unlock_attempt_, false /*success*/);
// Reset unlock attempt when the auth user changes.
unlock_attempt_ = 0;
}
// The new auth user might have different last used detachable base - make
// sure the detachable base pairing error is updated if needed.
OnDetachableBasePairingStatusChanged(
detachable_base_model_->GetPairingStatus());
}
void LockContentsView::UpdateEasyUnlockIconForUser(const AccountId& user) {
// Try to find an auth view for |user|. If there is none, there is no state to
// update.
LoginAuthUserView* auth_view =
TryToFindAuthUser(user, false /*require_auth_active*/);
if (!auth_view)
return;
UserState* state = FindStateForUser(user);
DCHECK(state);
// Hide easy unlock icon if there is no data is available.
if (!state->easy_unlock_state) {
auth_view->SetEasyUnlockIcon(mojom::EasyUnlockIconId::NONE,
base::string16());
return;
}
// TODO(jdufault): Make easy unlock backend always send aria_label, right now
// it is only sent if there is no tooltip.
base::string16 accessibility_label = state->easy_unlock_state->aria_label;
if (accessibility_label.empty())
accessibility_label = state->easy_unlock_state->tooltip;
auth_view->SetEasyUnlockIcon(state->easy_unlock_state->icon,
accessibility_label);
}
LoginAuthUserView* LockContentsView::CurrentAuthUserView() {
if (opt_secondary_auth_ &&
opt_secondary_auth_->auth_methods() != LoginAuthUserView::AUTH_NONE) {
DCHECK(primary_auth_->auth_methods() == LoginAuthUserView::AUTH_NONE);
return opt_secondary_auth_;
}
return primary_auth_;
}
void LockContentsView::ShowAuthErrorMessage() {
base::string16 error_text = l10n_util::GetStringUTF16(
unlock_attempt_ ? IDS_ASH_LOGIN_ERROR_AUTHENTICATING_2ND_TIME
: IDS_ASH_LOGIN_ERROR_AUTHENTICATING);
ImeController* ime_controller = Shell::Get()->ime_controller();
if (ime_controller->IsCapsLockEnabled()) {
error_text += base::ASCIIToUTF16(" ") +
l10n_util::GetStringUTF16(IDS_ASH_LOGIN_ERROR_CAPS_LOCK_HINT);
}
base::Optional<int> bold_start;
int bold_length = 0;
// Display a hint to switch keyboards if there are other active input
// methods.
if (ime_controller->available_imes().size() > 1) {
error_text += base::ASCIIToUTF16(" ");
bold_start = error_text.length();
base::string16 shortcut =
l10n_util::GetStringUTF16(IDS_ASH_LOGIN_KEYBOARD_SWITCH_SHORTCUT);
bold_length = shortcut.length();
size_t shortcut_offset_in_string;
error_text +=
l10n_util::GetStringFUTF16(IDS_ASH_LOGIN_ERROR_KEYBOARD_SWITCH_HINT,
shortcut, &shortcut_offset_in_string);
*bold_start += shortcut_offset_in_string;
}
views::StyledLabel* label = new views::StyledLabel(error_text, this);
MakeSectionBold(label, error_text, bold_start, bold_length);
label->set_auto_color_readability_enabled(false);
auth_error_bubble_->ShowErrorBubble(
label, CurrentAuthUserView()->password_view() /*anchor_view*/,
LoginBubble::kFlagsNone);
}
void LockContentsView::OnEasyUnlockIconHovered() {
UserState* state = FindStateForUser(
CurrentAuthUserView()->current_user()->basic_user_info->account_id);
DCHECK(state);
mojom::EasyUnlockIconOptionsPtr& easy_unlock_state = state->easy_unlock_state;
DCHECK(easy_unlock_state);
if (!easy_unlock_state->tooltip.empty()) {
tooltip_bubble_->ShowTooltip(
easy_unlock_state->tooltip,
CurrentAuthUserView()->password_view() /*anchor_view*/);
}
}
void LockContentsView::OnEasyUnlockIconTapped() {
UserState* state = FindStateForUser(
CurrentAuthUserView()->current_user()->basic_user_info->account_id);
DCHECK(state);
mojom::EasyUnlockIconOptionsPtr& easy_unlock_state = state->easy_unlock_state;
DCHECK(easy_unlock_state);
if (easy_unlock_state->hardlock_on_click) {
AccountId user =
CurrentAuthUserView()->current_user()->basic_user_info->account_id;
Shell::Get()->login_screen_controller()->HardlockPod(user);
// TODO(jdufault): This should get called as a result of HardlockPod.
OnClickToUnlockEnabledForUserChanged(user, false /*enabled*/);
}
}
keyboard::KeyboardController* LockContentsView::GetKeyboardController() const {
return GetWidget() ? GetKeyboardControllerForWidget(GetWidget()) : nullptr;
}
LoginAuthUserView* LockContentsView::AllocateLoginAuthUserView(
const mojom::LoginUserInfoPtr& user,
bool is_primary) {
return new LoginAuthUserView(
user,
base::Bind(&LockContentsView::OnAuthenticate, base::Unretained(this)),
base::Bind(&LockContentsView::SwapActiveAuthBetweenPrimaryAndSecondary,
base::Unretained(this), is_primary),
base::Bind(&LockContentsView::OnEasyUnlockIconHovered,
base::Unretained(this)),
base::Bind(&LockContentsView::OnEasyUnlockIconTapped,
base::Unretained(this)));
}
LoginAuthUserView* LockContentsView::TryToFindAuthUser(
const AccountId& user,
bool require_auth_active) {
LoginAuthUserView* view = nullptr;
// Find auth instance.
if (primary_auth_->current_user()->basic_user_info->account_id == user) {
view = primary_auth_;
} else if (opt_secondary_auth_ &&
opt_secondary_auth_->current_user()->basic_user_info->account_id ==
user) {
view = opt_secondary_auth_;
}
// Make sure auth instance is active if required.
if (require_auth_active && view &&
view->auth_methods() == LoginAuthUserView::AUTH_NONE) {
view = nullptr;
}
return view;
}
ScrollableUsersListView* LockContentsView::BuildScrollableUsersListView(
const std::vector<mojom::LoginUserInfoPtr>& users,
LoginDisplayStyle display_style) {
auto* view = new ScrollableUsersListView(
users,
base::BindRepeating(&LockContentsView::SwapToAuthUser,
base::Unretained(this)),
display_style);
view->ClipHeightTo(view->contents()->size().height(), size().height());
return view;
}
} // namespace ash