[go: nahoru, domu]

blob: 653ef26e909d4ecb807eed423d34d6d2a841c3b9 [file] [log] [blame]
// Copyright 2021 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 <algorithm>
#include "ash/login/ui/login_auth_factors_view.h"
#include "ash/login/ui/animated_auth_factors_label_wrapper.h"
#include "ash/login/ui/arrow_button_view.h"
#include "ash/login/ui/auth_icon_view.h"
#include "ash/login/ui/lock_screen.h"
#include "ash/resources/vector_icons/vector_icons.h"
#include "ash/strings/grit/ash_strings.h"
#include "ash/style/ash_color_provider.h"
#include "base/callback.h"
#include "base/logging.h"
#include "base/time/time.h"
#include "ui/accessibility/ax_enums.mojom.h"
#include "ui/base/l10n/l10n_util.h"
#include "ui/compositor/layer.h"
#include "ui/compositor/layer_animation_sequence.h"
#include "ui/compositor/layer_animator.h"
#include "ui/gfx/paint_vector_icon.h"
#include "ui/views/border.h"
#include "ui/views/controls/highlight_path_generator.h"
#include "ui/views/controls/label.h"
#include "ui/views/layout/animating_layout_manager.h"
#include "ui/views/layout/box_layout.h"
#include "ui/views/layout/flex_layout.h"
namespace ash {
namespace {
using AuthFactorState = AuthFactorModel::AuthFactorState;
constexpr int kAuthFactorsViewWidthDp = 280;
constexpr int kSpacingBetweenIconsAndLabelDp = 8;
constexpr int kIconTopSpacingDp = 10;
constexpr int kArrowButtonSizeDp = 32;
constexpr base::TimeDelta kErrorTimeout = base::Seconds(3);
// The values of this enum should be nearly the same as the values of
// AuthFactorState, except instead of kErrorTemporary and kErrorPermanent, we
// have kErrorForeground and kErrorBackground.
//
// Foreground/background here refers to whether or not the error has already
// been displayed to the user. Permanent errors, which can't be recovered from,
// start in the foreground and then transition to the background after having
// been displayed. Temporary errors, on the other hand, start in the foreground
// and then transition to a non-error state after display.
//
// The idea is to provide separation of concerns: AuthFactorModel is concerned
// with the type of error being shown, but LoginAuthFactorsView is concerned
// with how to show the error. When deciding how to prioritize which states to
// show, what matters is whether the error is currently in the foreground or
// background, not whether the underlying error state is temporary or permanent.
//
// DO NOT change the relative ordering of these enum values. The values
// assigned here correspond to the priority of these states. For example, if
// LoginAuthFactorsView has one auth factor in the kClickRequired state and
// one auth factor in the kReady state, then it will prioritize showing the
// kClickRequired state since it's assigned a higher priority.
enum class PrioritizedAuthFactorViewState {
// All auth factors are unavailable, and LoginAuthFactorsView should not be
// visible.
kUnavailable = 0,
// All auth factors are either unavailable or have permanent errors that have
// already been displayed.
kErrorBackground = 1,
// There is at least one auth factor that is available, but it requires extra
// steps to authenticate.
kAvailable = 2,
// There is at least one auth factor that is ready to authenticate.
kReady = 3,
// An auth factor has an error message to display.
kErrorForeground = 4,
// An auth factor requires a tap or click as the last step in its
// authentication flow.
kClickRequired = 5,
// Authentication is complete.
kAuthenticated = 6,
};
PrioritizedAuthFactorViewState GetPrioritizedAuthFactorViewState(
const AuthFactorModel& auth_factor) {
switch (auth_factor.GetAuthFactorState()) {
case AuthFactorState::kUnavailable:
return PrioritizedAuthFactorViewState::kUnavailable;
case AuthFactorState::kErrorPermanent:
if (auth_factor.has_permanent_error_display_timed_out())
return PrioritizedAuthFactorViewState::kErrorBackground;
return PrioritizedAuthFactorViewState::kErrorForeground;
case AuthFactorState::kAvailable:
return PrioritizedAuthFactorViewState::kAvailable;
case AuthFactorState::kReady:
return PrioritizedAuthFactorViewState::kReady;
case AuthFactorState::kErrorTemporary:
return PrioritizedAuthFactorViewState::kErrorForeground;
case AuthFactorState::kClickRequired:
return PrioritizedAuthFactorViewState::kClickRequired;
case AuthFactorState::kAuthenticated:
return PrioritizedAuthFactorViewState::kAuthenticated;
}
}
// Return the AuthFactorModel whose state has the highest priority. "Priority"
// here roughly corresponds with how close the given state is to completing the
// auth flow. E.g. kAvailable < kReady because there are fewer steps to complete
// authentication for a Ready auth factor. The highest priority auth factor's
// state determines the behavior of LoginAuthFactorsView.
AuthFactorModel* GetHighestPriorityAuthFactor(
const std::vector<std::unique_ptr<AuthFactorModel>>& auth_factors) {
if (auth_factors.empty())
return nullptr;
// PrioritizedAuthFactorViewState enum values are assigned so that the
// highest numerical value corresponds to the highest priority.
auto compare_by_priority = [](const std::unique_ptr<AuthFactorModel>& a,
const std::unique_ptr<AuthFactorModel>& b) {
return static_cast<int>(GetPrioritizedAuthFactorViewState(*a)) <
static_cast<int>(GetPrioritizedAuthFactorViewState(*b));
};
auto& max = *std::max_element(auth_factors.begin(), auth_factors.end(),
compare_by_priority);
return max.get();
}
} // namespace
LoginAuthFactorsView::TestApi::TestApi(LoginAuthFactorsView* view)
: view_(view) {}
LoginAuthFactorsView::TestApi::~TestApi() = default;
void LoginAuthFactorsView::TestApi::UpdateState() {
return view_->UpdateState();
}
std::vector<std::unique_ptr<AuthFactorModel>>&
LoginAuthFactorsView::TestApi::auth_factors() {
return view_->auth_factors_;
}
views::Label* LoginAuthFactorsView::TestApi::label() {
return view_->label_wrapper_->label();
}
views::View* LoginAuthFactorsView::TestApi::auth_factor_icon_row() {
return view_->auth_factor_icon_row_;
}
ArrowButtonView* LoginAuthFactorsView::TestApi::arrow_button() {
return view_->arrow_button_;
}
AuthIconView* LoginAuthFactorsView::TestApi::arrow_nudge_animation() {
return view_->arrow_nudge_animation_;
}
AuthIconView* LoginAuthFactorsView::TestApi::checkmark_icon() {
return view_->checkmark_icon_;
}
LoginAuthFactorsView::LoginAuthFactorsView(
base::RepeatingClosure on_click_to_enter_callback,
base::RepeatingCallback<void(bool)>
on_auth_factor_is_hiding_password_changed_callback)
: on_click_to_enter_callback_(on_click_to_enter_callback),
on_auth_factor_is_hiding_password_changed_callback_(
on_auth_factor_is_hiding_password_changed_callback) {
DCHECK(on_click_to_enter_callback);
DCHECK(on_auth_factor_is_hiding_password_changed_callback);
SetPaintToLayer();
layer()->SetFillsBoundsOpaquely(false);
SetBorder(
views::CreateEmptyBorder(gfx::Insets::TLBR(kIconTopSpacingDp, 0, 0, 0)));
auto* layout = SetLayoutManager(std::make_unique<views::FlexLayout>());
layout->SetOrientation(views::LayoutOrientation::kVertical);
layout->SetMainAxisAlignment(views::LayoutAlignment::kCenter);
layout->SetCrossAxisAlignment(views::LayoutAlignment::kCenter);
auth_factor_icon_row_ = AddChildView(std::make_unique<views::View>());
auto* animating_layout = auth_factor_icon_row_->SetLayoutManager(
std::make_unique<views::AnimatingLayoutManager>());
animating_layout->SetBoundsAnimationMode(
views::AnimatingLayoutManager::BoundsAnimationMode::kAnimateMainAxis);
animating_layout->SetTargetLayoutManager(
std::make_unique<views::FlexLayout>());
arrow_icon_container_ = AddChildView(std::make_unique<views::View>());
arrow_icon_container_->SetUseDefaultFillLayout(true);
arrow_button_container_ =
arrow_icon_container_->AddChildView(std::make_unique<views::View>());
SetBoxLayout(arrow_button_container_);
arrow_button_ =
arrow_button_container_->AddChildView(std::make_unique<ArrowButtonView>(
base::BindRepeating(&LoginAuthFactorsView::ArrowButtonPressed,
base::Unretained(this)),
kArrowButtonSizeDp));
arrow_button_->SetInstallFocusRingOnFocus(true);
views::InstallCircleHighlightPathGenerator(arrow_button_);
arrow_button_->SetAccessibleName(
l10n_util::GetStringUTF16(IDS_AUTH_FACTOR_LABEL_CLICK_TO_ENTER));
arrow_nudge_animation_ =
arrow_icon_container_->AddChildView(std::make_unique<AuthIconView>());
arrow_nudge_animation_->SetCircleImage(
kArrowButtonSizeDp / 2, AshColorProvider::Get()->GetControlsLayerColor(
AshColorProvider::ControlsLayerType::
kHairlineBorderColor));
arrow_nudge_animation_->set_on_tap_or_click_callback(base::BindRepeating(
&LoginAuthFactorsView::RelayArrowButtonPressed, base::Unretained(this)));
SetArrowVisibility(false);
// TODO(crbug.com/1233614): Rename kLockScreenFingerprintSuccessIcon once the
// feature flag is removed and FingerprintView no longer needs this.
checkmark_icon_ = AddChildView(std::make_unique<AuthIconView>());
checkmark_icon_->SetIcon(kLockScreenFingerprintSuccessIcon,
AuthIconView::Color::kPositive);
checkmark_icon_->SetVisible(false);
label_wrapper_ =
AddChildView(std::make_unique<AnimatedAuthFactorsLabelWrapper>());
label_wrapper_->SetProperty(
views::kMarginsKey,
gfx::Insets::TLBR(kSpacingBetweenIconsAndLabelDp, 0, 0, 0));
}
LoginAuthFactorsView::~LoginAuthFactorsView() = default;
void LoginAuthFactorsView::AddAuthFactor(
std::unique_ptr<AuthFactorModel> auth_factor) {
auto* icon =
auth_factor_icon_row_->AddChildView(std::make_unique<AuthIconView>());
auth_factor->Init(
icon,
/*update_state_callback=*/base::BindRepeating(
&LoginAuthFactorsView::UpdateState, base::Unretained(this)));
icon->set_on_tap_or_click_callback(base::BindRepeating(
&AuthFactorModel::HandleTapOrClick, base::Unretained(auth_factor.get())));
auth_factors_.push_back(std::move(auth_factor));
UpdateState();
}
void LoginAuthFactorsView::SetCanUsePin(bool can_use_pin) {
if (can_use_pin == AuthFactorModel::can_use_pin())
return;
AuthFactorModel::set_can_use_pin(can_use_pin);
UpdateState();
}
bool LoginAuthFactorsView::ShouldHidePasswordField() {
return should_hide_password_field_;
}
void LoginAuthFactorsView::UpdateState() {
AuthFactorModel* active_auth_factor =
GetHighestPriorityAuthFactor(auth_factors_);
if (!active_auth_factor) {
return;
}
PrioritizedAuthFactorViewState state =
GetPrioritizedAuthFactorViewState(*active_auth_factor);
if (state == PrioritizedAuthFactorViewState::kUnavailable) {
return;
}
if (state != PrioritizedAuthFactorViewState::kErrorForeground) {
error_timer_.Stop();
}
UpdateShouldHidePasswordField(*active_auth_factor);
int ready_label_id;
size_t num_factors_in_error_background_state;
switch (state) {
case PrioritizedAuthFactorViewState::kAuthenticated:
// An auth factor has successfully authenticated. Show a green checkmark.
ShowCheckmark();
if (LockScreen::Get()->screen_type() == LockScreen::ScreenType::kLogin) {
label_wrapper_->SetLabelTextAndAccessibleName(
IDS_AUTH_FACTOR_LABEL_SIGNED_IN, IDS_AUTH_FACTOR_LABEL_SIGNED_IN);
} else {
label_wrapper_->SetLabelTextAndAccessibleName(
IDS_AUTH_FACTOR_LABEL_UNLOCKED, IDS_AUTH_FACTOR_LABEL_UNLOCKED);
}
// Clear focus so that the focus on arrow button does not jump to another
// element after the view transitions.
GetFocusManager()->ClearFocus();
return;
case PrioritizedAuthFactorViewState::kClickRequired:
// An auth factor requires a click to enter. Show arrow button.
ShowArrowButton();
label_wrapper_->SetLabelTextAndAccessibleName(
IDS_AUTH_FACTOR_LABEL_CLICK_TO_ENTER,
IDS_AUTH_FACTOR_LABEL_CLICK_TO_ENTER);
FireAlert();
// Dismiss any errors in the background.
OnErrorTimeout();
return;
case PrioritizedAuthFactorViewState::kReady:
// One or more auth factors is in the Ready state. Show the ready auth
// factors.
ShowReadyAndDisabledAuthFactors();
ready_label_id = GetReadyLabelId();
label_wrapper_->SetLabelTextAndAccessibleName(ready_label_id,
ready_label_id,
/*animate=*/true);
// TODO(crbug.com/1233614): Should FireAlert() be called here?
FireAlert();
return;
case PrioritizedAuthFactorViewState::kAvailable:
// At least one auth factor is available, but none are ready. Show first
// available auth factor.
ShowSingleAuthFactor(active_auth_factor);
label_wrapper_->SetLabelTextAndAccessibleName(
active_auth_factor->GetLabelId(),
active_auth_factor->GetAccessibleNameId(), /*animate=*/true);
if (active_auth_factor->ShouldAnnounceLabel()) {
FireAlert();
}
return;
case PrioritizedAuthFactorViewState::kErrorForeground:
// An auth factor has either a temporary or permanent error to show. Show
// the error for a period of time.
// Do not replace the current error if an error is already showing.
if (error_timer_.IsRunning())
return;
error_timer_.Start(FROM_HERE, kErrorTimeout,
base::BindOnce(&LoginAuthFactorsView::OnErrorTimeout,
base::Unretained(this)));
ShowSingleAuthFactor(active_auth_factor);
label_wrapper_->SetLabelTextAndAccessibleName(
active_auth_factor->GetLabelId(),
active_auth_factor->GetAccessibleNameId());
if (active_auth_factor->ShouldAnnounceLabel()) {
FireAlert();
}
return;
case PrioritizedAuthFactorViewState::kErrorBackground:
// Any auth factors that were available have errors that cannot be
// resolved, and those errors have already been displayed in the
// foreground. Show the "disabled" icons and instruct the user to enter
// their password.
ShowReadyAndDisabledAuthFactors();
num_factors_in_error_background_state = std::count_if(
auth_factors_.begin(), auth_factors_.end(), [](const auto& factor) {
return GetPrioritizedAuthFactorViewState(*factor) ==
PrioritizedAuthFactorViewState::kErrorBackground;
});
if (num_factors_in_error_background_state == 1) {
label_wrapper_->SetLabelTextAndAccessibleName(
active_auth_factor->GetLabelId(),
active_auth_factor->GetAccessibleNameId());
} else {
label_wrapper_->SetLabelTextAndAccessibleName(GetDefaultLabelId(),
GetDefaultLabelId());
}
return;
case PrioritizedAuthFactorViewState::kUnavailable:
NOTREACHED();
return;
}
}
void LoginAuthFactorsView::ShowArrowButton() {
auth_factor_icon_row_->SetVisible(false);
checkmark_icon_->SetVisible(false);
SetArrowVisibility(true);
}
void LoginAuthFactorsView::ShowSingleAuthFactor(AuthFactorModel* auth_factor) {
auth_factor_icon_row_->SetVisible(true);
checkmark_icon_->SetVisible(false);
SetArrowVisibility(false);
for (auto& factor : auth_factors_) {
factor->SetVisible(factor.get() == auth_factor);
}
}
void LoginAuthFactorsView::ShowReadyAndDisabledAuthFactors() {
auth_factor_icon_row_->SetVisible(true);
checkmark_icon_->SetVisible(false);
SetArrowVisibility(false);
for (auto& factor : auth_factors_) {
PrioritizedAuthFactorViewState state =
GetPrioritizedAuthFactorViewState(*factor);
factor->SetVisible(state == PrioritizedAuthFactorViewState::kReady ||
state ==
PrioritizedAuthFactorViewState::kErrorBackground);
}
}
void LoginAuthFactorsView::ShowCheckmark() {
auth_factor_icon_row_->SetVisible(false);
checkmark_icon_->SetVisible(true);
SetArrowVisibility(false);
// TODO(crbug.com/1233614): If transitioning from Click Required state, show
// animation.
}
int LoginAuthFactorsView::GetReadyLabelId() const {
AuthFactorTypeBits ready_factors = 0;
unsigned ready_factor_count = 0u;
AuthFactorModel* ready_factor = nullptr;
for (const auto& factor : auth_factors_) {
if (factor->GetAuthFactorState() == AuthFactorState::kReady) {
ready_factors = ready_factors | factor->GetType();
ready_factor_count++;
ready_factor = factor.get();
}
}
if (ready_factor_count == 0u) {
LOG(ERROR) << "GetReadyLabelId() called without any ready auth factors.";
NOTREACHED();
return GetDefaultLabelId();
}
if (ready_factor_count == 1u)
return ready_factor->GetLabelId();
// Multiple auth factors are ready.
switch (ready_factors) {
case AuthFactorType::kSmartLock | AuthFactorType::kFingerprint:
return IDS_AUTH_FACTOR_LABEL_UNLOCK_METHOD_SELECTION;
}
NOTREACHED();
return GetDefaultLabelId();
}
int LoginAuthFactorsView::GetDefaultLabelId() const {
return AuthFactorModel::can_use_pin()
? IDS_AUTH_FACTOR_LABEL_PASSWORD_OR_PIN_REQUIRED
: IDS_AUTH_FACTOR_LABEL_PASSWORD_REQUIRED;
}
// views::View:
gfx::Size LoginAuthFactorsView::CalculatePreferredSize() const {
gfx::Size size = views::View::CalculatePreferredSize();
size.set_width(kAuthFactorsViewWidthDp);
return size;
}
// views::View:
void LoginAuthFactorsView::OnThemeChanged() {
views::View::OnThemeChanged();
for (const auto& factor : auth_factors_) {
factor->OnThemeChanged();
}
}
void LoginAuthFactorsView::FireAlert() {
label_wrapper_->label()->NotifyAccessibilityEvent(ax::mojom::Event::kAlert,
/*send_native_event=*/true);
}
void LoginAuthFactorsView::ArrowButtonPressed(const ui::Event& event) {
arrow_nudge_animation_->SetVisible(false);
arrow_nudge_animation_->StopAnimating();
arrow_button_->StopAnimating();
if (on_click_to_enter_callback_) {
arrow_button_->EnableLoadingAnimation(true);
on_click_to_enter_callback_.Run();
}
}
void LoginAuthFactorsView::RelayArrowButtonPressed() {
if (arrow_button_) {
ArrowButtonPressed(ui::MouseEvent(ui::ET_MOUSE_PRESSED, gfx::Point(),
gfx::Point(), base::TimeTicks::Now(), 0,
0));
}
}
void LoginAuthFactorsView::OnErrorTimeout() {
for (auto& factor : auth_factors_) {
// If additional errors occur during the error timeout, then mark all
// errors timed out instead of trying to queue them. The user can still get
// the error messages by clicking on the icons.
if (GetPrioritizedAuthFactorViewState(*factor) ==
PrioritizedAuthFactorViewState::kErrorForeground) {
factor->HandleErrorTimeout();
}
}
}
void LoginAuthFactorsView::SetBoxLayout(views::View* parent_view) {
auto* layout =
parent_view->SetLayoutManager(std::make_unique<views::BoxLayout>(
views::BoxLayout::Orientation::kVertical, gfx::Insets(),
kSpacingBetweenIconsAndLabelDp));
layout->set_main_axis_alignment(views::BoxLayout::MainAxisAlignment::kCenter);
layout->set_cross_axis_alignment(
views::BoxLayout::CrossAxisAlignment::kCenter);
}
void LoginAuthFactorsView::SetArrowVisibility(bool is_visible) {
arrow_icon_container_->SetVisible(is_visible);
arrow_button_->SetVisible(is_visible);
arrow_nudge_animation_->SetVisible(is_visible);
if (is_visible) {
arrow_button_->EnableLoadingAnimation(false);
arrow_button_->RunTransformAnimation();
arrow_nudge_animation_->RunNudgeAnimation();
arrow_button_->RequestFocus();
} else {
arrow_nudge_animation_->StopAnimating();
arrow_button_->StopAnimating();
}
}
void LoginAuthFactorsView::UpdateShouldHidePasswordField(
const AuthFactorModel& active_auth_factor) {
PrioritizedAuthFactorViewState state =
GetPrioritizedAuthFactorViewState(active_auth_factor);
// At the moment, Smart Lock is the only auth factor which needs to hide
// the password field, and it does so only during states kClickRequired
// and kAuthenticated.
bool should_hide_password_field =
active_auth_factor.GetType() == AuthFactorType::kSmartLock &&
(state == PrioritizedAuthFactorViewState::kClickRequired ||
state == PrioritizedAuthFactorViewState::kAuthenticated);
if (should_hide_password_field == should_hide_password_field_) {
return;
}
should_hide_password_field_ = should_hide_password_field;
on_auth_factor_is_hiding_password_changed_callback_.Run(
should_hide_password_field);
}
} // namespace ash