| // 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 |