chromium/ash/auth/views/fingerprint_view.cc

// Copyright 2024 The Chromium Authors
// Use of this source code is governed by a BSD-style license that can be
// found in the LICENSE file.

#include "ash/auth/views/fingerprint_view.h"

#include <memory>
#include <string>
#include <string_view>

#include "ash/auth/views/auth_common.h"
#include "ash/login/resources/grit/login_resources.h"
#include "ash/login/ui/animated_rounded_image_view.h"
#include "ash/login/ui/horizontal_image_sequence_animation_decoder.h"
#include "ash/public/cpp/login_types.h"
#include "ash/resources/vector_icons/vector_icons.h"
#include "ash/strings/grit/ash_strings.h"
#include "ash/style/ash_color_id.h"
#include "base/check.h"
#include "base/check_op.h"
#include "base/functional/bind.h"
#include "base/memory/raw_ptr.h"
#include "base/memory/weak_ptr.h"
#include "base/strings/utf_string_conversions.h"
#include "base/timer/timer.h"
#include "ui/accessibility/ax_node_data.h"
#include "ui/base/l10n/l10n_util.h"
#include "ui/base/metadata/metadata_impl_macros.h"
#include "ui/base/resource/resource_bundle.h"
#include "ui/chromeos/styles/cros_tokens_color_mappings.h"
#include "ui/gfx/geometry/size.h"
#include "ui/views/accessibility/view_accessibility.h"
#include "ui/views/controls/label.h"
#include "ui/views/layout/box_layout.h"
#include "ui/views/layout/layout_types.h"
#include "ui/views/view.h"

namespace ash {

namespace {

// Size of the Fingerprint icon.
constexpr int kFingerprintIconSizeDp = 28;

// Vertical spacing above the fingerprint view.
constexpr int kSpacingTopDp = 28;

// Vertical space between the fingerprint icon and label.
constexpr int kSpacingBetweenFingerprintIconAndLabelDp = 18;

// Number of frames and total duration for the fingerprint failed animation.
constexpr int kFingerprintFailedAnimationNumFrames = 45;
constexpr base::TimeDelta kFingerprintFailedAnimationDuration =
    base::Milliseconds(700);

// Delay after a failed attempt before the icon reverts to its default
// 'available' state.
constexpr base::TimeDelta kResetToDefaultIconDelay = base::Milliseconds(1300);

// Duration for which the label displays a temporary message after a gesture
// event.
constexpr base::TimeDelta kResetToDefaultMessageDelay =
    base::Milliseconds(3000);

// Color id's for the fingerprint icon.
constexpr ui::ColorId kFingerprintIconEnabledColorId =
    cros_tokens::kCrosSysOnSurface;
constexpr ui::ColorId kFingerprintIconDisabledColorId =
    cros_tokens::kCrosSysDisabled;

}  // namespace

//----------------------- FingerprintView Test API ------------------------

FingerprintView::TestApi::TestApi(FingerprintView* view) : view_(view) {
  CHECK(view_);
}

FingerprintView::TestApi::~TestApi() = default;

bool FingerprintView::TestApi::GetEnabled() const {
  return view_->GetEnabled();
}

void FingerprintView::TestApi::SetEnabled(bool enabled) {
  view_->SetEnabled(enabled);
}

views::Label* FingerprintView::TestApi::GetLabel() {
  return view_->label_;
}
AnimatedRoundedImageView* FingerprintView::TestApi::GetIcon() {
  return view_->icon_;
}

void FingerprintView::TestApi::ShowFirstFrame() {
  view_->icon_->SetAnimationPlayback(
      AnimatedRoundedImageView::Playback::kFirstFrameOnly);
}

void FingerprintView::TestApi::ShowLastFrame() {
  view_->icon_->SetAnimationPlayback(
      AnimatedRoundedImageView::Playback::kLastFrameOnly);
}

FingerprintView* FingerprintView::TestApi::GetView() {
  return view_;
}

FingerprintState FingerprintView::TestApi::GetState() const {
  return view_->state_;
}

//----------------------- FingerprintView ------------------------

FingerprintView::FingerprintView() {
  SetVisible(false);

  auto* layout = SetLayoutManager(std::make_unique<views::BoxLayout>(
      views::BoxLayout::Orientation::kVertical, gfx::Insets(),
      kSpacingBetweenFingerprintIconAndLabelDp));
  layout->set_main_axis_alignment(views::BoxLayout::MainAxisAlignment::kCenter);

  icon_ = AddChildView(std::make_unique<AnimatedRoundedImageView>(
      gfx::Size(kFingerprintIconSizeDp, kFingerprintIconSizeDp),
      0 /*corner_radius*/));

  label_ = AddChildView(std::make_unique<views::Label>());
  label_->SetSubpixelRenderingEnabled(false);
  label_->SetAutoColorReadabilityEnabled(false);

  // kTextColorId, kTextFont defined in auth_common.h
  label_->SetEnabledColorId(kTextColorId);
  label_->SetFontList(
      TypographyProvider::Get()->ResolveTypographyToken(kTextFont));

  label_->SetMultiLine(true);
  label_->SetFocusBehavior(FocusBehavior::ACCESSIBLE_ONLY);
  label_->GetViewAccessibility().SetRole(ax::mojom::Role::kStaticText);
}

FingerprintView::~FingerprintView() {
  icon_ = nullptr;
  label_ = nullptr;
}

void FingerprintView::SetState(FingerprintState state) {
  if (state_ == state) {
    return;
  }
  reset_state_.Stop();
  state_ = state;
  DisplayCurrentState();
}

void FingerprintView::SetHasPin(bool has_pin) {
  if (has_pin_ == has_pin) {
    return;
  }

  has_pin_ = has_pin;
  DisplayCurrentState();
}

void FingerprintView::NotifyAuthFailure() {
  SetState(FingerprintState::AVAILABLE_WITH_FAILED_ATTEMPT);
  reset_state_.Start(
      FROM_HERE, kResetToDefaultIconDelay,
      base::BindOnce(&FingerprintView::SetState, base::Unretained(this),
                     FingerprintState::AVAILABLE_DEFAULT));
}

void FingerprintView::OnGestureEvent(ui::GestureEvent* event) {
  if (event->type() != ui::EventType::kGestureTap) {
    return;
  }
  if (state_ == FingerprintState::AVAILABLE_DEFAULT ||
      state_ == FingerprintState::AVAILABLE_WITH_TOUCH_SENSOR_WARNING ||
      state_ == FingerprintState::AVAILABLE_WITH_FAILED_ATTEMPT) {
    SetState(FingerprintState::AVAILABLE_WITH_TOUCH_SENSOR_WARNING);
    reset_state_.Start(
        FROM_HERE, kResetToDefaultMessageDelay,
        base::BindOnce(&FingerprintView::SetState, base::Unretained(this),
                       FingerprintState::AVAILABLE_DEFAULT));
  }
}

void FingerprintView::DisplayCurrentState() {
  if (state_ == FingerprintState::UNAVAILABLE) {
    SetVisible(false);
    return;
  }
  SetVisible(true);
  SetIcon();
  label_->SetText(l10n_util::GetStringUTF16(GetTextIdFromState()));
  label_->GetViewAccessibility().SetName(
      l10n_util::GetStringUTF16(GetA11yTextIdFromState()));
}

void FingerprintView::SetIcon() {
  switch (state_) {
    case FingerprintState::AVAILABLE_DEFAULT:
    case FingerprintState::AVAILABLE_WITH_TOUCH_SENSOR_WARNING:
    case FingerprintState::DISABLED_FROM_TIMEOUT:
      icon_->SetImageModel(ui::ImageModel::FromVectorIcon(
          kLockScreenFingerprintIcon, GetIconColorIdFromState(),
          kFingerprintIconSizeDp));
      break;
    case FingerprintState::DISABLED_FROM_ATTEMPTS:
    case FingerprintState::AVAILABLE_WITH_FAILED_ATTEMPT:
      icon_->SetAnimationDecoder(
          std::make_unique<HorizontalImageSequenceAnimationDecoder>(
              *ui::ResourceBundle::GetSharedInstance().GetImageSkiaNamed(
                  IDR_LOGIN_FINGERPRINT_UNLOCK_SPINNER),
              kFingerprintFailedAnimationDuration,
              kFingerprintFailedAnimationNumFrames),
          AnimatedRoundedImageView::Playback::kSingle);
      break;
    case FingerprintState::UNAVAILABLE:
      NOTREACHED();
  }
}

ui::ColorId FingerprintView::GetIconColorIdFromState() const {
  switch (state_) {
    case FingerprintState::AVAILABLE_DEFAULT:
    case FingerprintState::AVAILABLE_WITH_TOUCH_SENSOR_WARNING:
      return kFingerprintIconEnabledColorId;
    case FingerprintState::DISABLED_FROM_TIMEOUT:
      return kFingerprintIconDisabledColorId;
    case FingerprintState::UNAVAILABLE:
    case FingerprintState::DISABLED_FROM_ATTEMPTS:
    case FingerprintState::AVAILABLE_WITH_FAILED_ATTEMPT:
      NOTREACHED();
  }
}

int FingerprintView::GetTextIdFromState() const {
  switch (state_) {
    case FingerprintState::AVAILABLE_DEFAULT:
    case FingerprintState::AVAILABLE_WITH_FAILED_ATTEMPT:
      return IDS_ASH_IN_SESSION_AUTH_FINGERPRINT_AVAILABLE;
    case FingerprintState::AVAILABLE_WITH_TOUCH_SENSOR_WARNING:
      return IDS_ASH_IN_SESSION_AUTH_FINGERPRINT_TOUCH_SENSOR;
    case FingerprintState::DISABLED_FROM_ATTEMPTS:
      return IDS_ASH_IN_SESSION_AUTH_FINGERPRINT_DISABLED_FROM_ATTEMPTS;
    case FingerprintState::DISABLED_FROM_TIMEOUT:
      if (has_pin_) {
        return IDS_ASH_IN_SESSION_AUTH_FINGERPRINT_PIN_OR_PASSWORD_REQUIRED;
      }
      return IDS_ASH_IN_SESSION_AUTH_FINGERPRINT_PASSWORD_REQUIRED;
    case FingerprintState::UNAVAILABLE:
      NOTREACHED();
  }
}

int FingerprintView::GetA11yTextIdFromState() const {
  switch (state_) {
    case FingerprintState::AVAILABLE_DEFAULT:
    case FingerprintState::AVAILABLE_WITH_FAILED_ATTEMPT:
      return IDS_ASH_IN_SESSION_AUTH_FINGERPRINT_AVAILABLE;
    case FingerprintState::AVAILABLE_WITH_TOUCH_SENSOR_WARNING:
      return IDS_ASH_IN_SESSION_AUTH_FINGERPRINT_TOUCH_SENSOR;
    case FingerprintState::DISABLED_FROM_ATTEMPTS:
      return IDS_ASH_IN_SESSION_AUTH_FINGERPRINT_ACCESSIBLE_DISABLED_FROM_ATTEMPTS;
    case FingerprintState::DISABLED_FROM_TIMEOUT:
      if (has_pin_) {
        return IDS_ASH_IN_SESSION_AUTH_FINGERPRINT_PIN_OR_PASSWORD_REQUIRED;
      }
      return IDS_ASH_IN_SESSION_AUTH_FINGERPRINT_PASSWORD_REQUIRED;
    case FingerprintState::UNAVAILABLE:
      NOTREACHED();
  }
}

gfx::Size FingerprintView::CalculatePreferredSize(
    const views::SizeBounds& available_size) const {
  int preferred_height = 0;
  if (GetVisible()) {
    preferred_height = kSpacingTopDp + kFingerprintIconSizeDp +
                       kSpacingBetweenFingerprintIconAndLabelDp +
                       label_->GetHeightForWidth(kTextLineWidthDp);
  }
  return gfx::Size(kTextLineWidthDp, preferred_height);
}

BEGIN_METADATA(FingerprintView)
END_METADATA

}  // namespace ash