chromium/ash/system/focus_mode/focus_mode_detailed_view.cc

// Copyright 2023 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/system/focus_mode/focus_mode_detailed_view.h"

#include <memory>
#include <utility>

#include "ash/accessibility/accessibility_controller.h"
#include "ash/glanceables/common/glanceables_util.h"
#include "ash/public/cpp/window_properties.h"
#include "ash/resources/vector_icons/vector_icons.h"
#include "ash/shell.h"
#include "ash/strings/grit/ash_strings.h"
#include "ash/style/icon_button.h"
#include "ash/style/pill_button.h"
#include "ash/style/rounded_container.h"
#include "ash/style/switch.h"
#include "ash/style/system_textfield.h"
#include "ash/style/system_textfield_controller.h"
#include "ash/style/typography.h"
#include "ash/system/focus_mode/focus_mode_animations.h"
#include "ash/system/focus_mode/focus_mode_controller.h"
#include "ash/system/focus_mode/focus_mode_countdown_view.h"
#include "ash/system/focus_mode/focus_mode_task_view.h"
#include "ash/system/focus_mode/focus_mode_util.h"
#include "ash/system/focus_mode/sounds/focus_mode_sounds_view.h"
#include "ash/system/model/clock_model.h"
#include "ash/system/model/system_tray_model.h"
#include "ash/system/palette/palette_tray.h"
#include "ash/system/time/time_view_utils.h"
#include "ash/system/tray/hover_highlight_view.h"
#include "ash/system/tray/tri_view.h"
#include "ash/wm/desks/templates/saved_desk_item_view.h"
#include "ash/wm/window_properties.h"
#include "base/check_op.h"
#include "base/i18n/time_formatting.h"
#include "base/strings/string_number_conversions.h"
#include "base/time/time.h"
#include "ui/base/l10n/l10n_util.h"
#include "ui/base/metadata/metadata_impl_macros.h"
#include "ui/base/models/image_model.h"
#include "ui/chromeos/styles/cros_tokens_color_mappings.h"
#include "ui/color/color_id.h"
#include "ui/compositor/layer.h"
#include "ui/gfx/canvas.h"
#include "ui/gfx/geometry/insets.h"
#include "ui/gfx/vector_icon_types.h"
#include "ui/views/accessibility/view_accessibility.h"
#include "ui/views/animation/ink_drop.h"
#include "ui/views/background.h"
#include "ui/views/controls/highlight_path_generator.h"
#include "ui/views/controls/label.h"
#include "ui/views/controls/scroll_view.h"
#include "ui/views/focus/focus_manager.h"
#include "ui/views/layout/box_layout_view.h"
#include "ui/views/view_class_properties.h"
#include "ui/views/view_observer.h"
#include "ui/views/widget/widget.h"
#include "ui/views/widget/widget_delegate.h"

namespace ash {

namespace {

constexpr auto kViewportMargins = gfx::Insets::VH(8, 0);

// Margins between containers in the detailed view if the container is connected
// to the container above it.
constexpr auto kConnectedContainerMargins = gfx::Insets::TLBR(2, 0, 0, 0);

// Margins between containers in the detailed view if the container is not
// connected to the container above it.
constexpr auto kDisconnectedContainerMargins = gfx::Insets::TLBR(8, 0, 0, 0);

// Insets for items within the `toggle_view_`'s `TriView` container.
constexpr auto kToggleViewInsets = gfx::Insets::VH(5, 24);

constexpr auto kToggleViewHeight = 64;

// Timer view constants.
constexpr auto kTimerViewBorderInsets = gfx::Insets::TLBR(4, 0, 8, 8);
constexpr auto kTimerViewHeaderInsets = gfx::Insets::TLBR(14, 24, 10, 24);
constexpr auto kTimerSettingViewInsets = gfx::Insets::TLBR(0, 16, 12, 16);
constexpr int kTimerSettingViewMaxCharacters = 3;
constexpr int kTimerSettingViewTextHeight = 32;
constexpr int kTimerSettingViewBetweenChildSpacing = 8;
constexpr auto kTimerAdjustmentButtonSize = gfx::Size(63, 36);
constexpr auto kTimerCountdownViewInsets = gfx::Insets::TLBR(0, 24, 12, 16);
constexpr int kTimerTextfieldCornerRadius = 8;

// Task view constants.
constexpr auto kTaskViewContainerInsets = gfx::Insets::TLBR(4, 24, 22, 24);
constexpr auto kTaskViewHeaderInsets = gfx::Insets::VH(18, 0);

constexpr int kToggleButtonLeftPadding = 8;

// Gives us the amount of time by which we should increment or decrement the
// current session duration. A negative result indicates a decrement, and
// positive an increment.
int GetDurationDelta(int duration, bool decrement) {
  const int direction = decrement ? -1 : 1;

  // If the duration is at 5 or below, we can decrement by 1. But we can only
  // increment by 1 if the duration is below 5.
  if ((!decrement && duration < 5) || (decrement && duration <= 5)) {
    return direction;
  }

  // Likewise, if the duration is at 60 or below, we decrement to the nearest
  // multiple of 5. But we can only increment to the nearest multiple of 5 if
  // the duration is under 60.
  if ((!decrement && duration < 60) || (decrement && duration <= 60)) {
    const int duration_remainder = duration % 5;

    if (duration_remainder == 0) {
      return direction * 5;
    }

    return direction *
           (decrement ? duration_remainder : 5 - duration_remainder);
  }

  // Everything else increments to the nearest multiple of 15.
  const int duration_remainder = duration % 15;

  if (duration_remainder == 0) {
    return direction * 15;
  }

  return direction * (decrement ? duration_remainder : 15 - duration_remainder);
}

// Creates an `IconButton` with the formatting needed for the
// `timer_setting_view_`'s timer adjustment buttons.
std::unique_ptr<IconButton> CreateTimerAdjustmentButton(
    views::Button::PressedCallback callback,
    bool decrement) {
  std::unique_ptr<IconButton> timer_adjustment_button =
      std::make_unique<IconButton>(
          std::move(callback), IconButton::Type::kLarge,
          decrement ? &kChevronDownIcon : &kChevronUpIcon,
          /*is_togglable=*/false, /*has_border=*/false);
  timer_adjustment_button->SetImageHorizontalAlignment(
      views::ImageButton::HorizontalAlignment::ALIGN_CENTER);
  timer_adjustment_button->SetImageVerticalAlignment(
      views::ImageButton::VerticalAlignment::ALIGN_MIDDLE);
  timer_adjustment_button->SetPreferredSize(kTimerAdjustmentButtonSize);
  timer_adjustment_button->SetIconColor(cros_tokens::kCrosSysOnSurface);
  timer_adjustment_button->SetBackgroundColor(
      cros_tokens::kCrosSysHighlightShape);

  return timer_adjustment_button;
}

// Tells us what the current session duration would be after an increment or
// decrement.
base::TimeDelta CalculateSessionDurationAfterAdjustment(int duration,
                                                        bool decrement) {
  duration += GetDurationDelta(duration, decrement);
  return std::clamp(base::Minutes(duration), focus_mode_util::kMinimumDuration,
                    focus_mode_util::kMaximumDuration);
}

// Returns true if `contents` is constructed by a number with no more than 3
// digits or by an empty string.
bool IsValidTimeNumber(const std::u16string& contents) {
  const int length = contents.length();
  if (length > kTimerSettingViewMaxCharacters) {
    return false;
  }

  // Check the character from the tail, because each time when the user inserts
  // a new char, `ContentsChanged` will be called.
  for (int i = contents.length() - 1; i >= 0; --i) {
    if (!std::isdigit(contents[i])) {
      return false;
    }
  }
  return true;
}

}  // namespace

// Handles input validation and events for the textfield in
// `timer_setting_view_`.
class FocusModeDetailedView::TimerTextfieldController
    : public SystemTextfieldController,
      public views::ViewObserver {
 public:
  TimerTextfieldController(SystemTextfield* textfield,
                           FocusModeDetailedView* owner)
      : SystemTextfieldController(textfield),
        textfield_(textfield),
        owner_(owner) {
    textfield_->AddObserver(this);
  }
  TimerTextfieldController(const TimerTextfieldController&) = delete;
  TimerTextfieldController& operator=(const TimerTextfieldController&) = delete;
  ~TimerTextfieldController() override { textfield_->RemoveObserver(this); }

  // SystemTextfieldController:
  void ContentsChanged(views::Textfield* sender,
                       const std::u16string& new_contents) override {
    // If there are more than 3 chars inserted from the virtual keyboard, or if
    // a non-numeric character is inserted from the virtual keyboard, set the
    // `textfield_` with the last known valid contents before `new_contents`
    // came in.
    if (!IsValidTimeNumber(new_contents)) {
      textfield_->SetText(valid_new_contents_);
      return;
    }

    valid_new_contents_ = new_contents;
    RefreshTextfieldSize(new_contents);
  }

  bool HandleKeyEvent(views::Textfield* sender,
                      const ui::KeyEvent& key_event) override {
    if (key_event.type() != ui::EventType::kKeyPressed) {
      return false;
    }

    if (key_event.key_code() == ui::VKEY_PROCESSKEY) {
      // Default handling for keyboard events that are not generated by physical
      // key press. This can happen, for example, when virtual keyboard button
      // is pressed.
      return false;
    }

    views::FocusManager* focus_manager = sender->GetWidget()->GetFocusManager();

    if (key_event.key_code() == ui::VKEY_RETURN) {
      if (sender->GetText().length() == 0) {
        textfield_->RestoreText();
        // `RestoreText()`, uses `SetText()`, which does not invoke
        // `ContentsChanged()`. Call `ContentsChanged()` directly, so the text
        // change gets handled by controller overrides.
        ContentsChanged(sender, sender->GetText());
        textfield_->SetActive(false);
      }

      owner_->SetInactiveSessionDuration(base::Minutes(
          focus_mode_util::GetTimerTextfieldInputInMinutes(textfield_)));

      focus_manager->ClearFocus();

      // Avoid having the focus restored to the same view when the parent view
      // is refocused.
      focus_manager->SetStoredFocusView(nullptr);
      return true;
    }

    // Make sure that we set the timer adjustment buttons' enabled states before
    // moving focus to avoid recursively focusing.
    if (key_event.key_code() == ui::VKEY_TAB) {
      owner_->SetInactiveSessionDuration(base::Minutes(
          focus_mode_util::GetTimerTextfieldInputInMinutes(textfield_)));
    }

    if (SystemTextfieldController::HandleKeyEvent(sender, key_event)) {
      if (key_event.key_code() == ui::VKEY_ESCAPE) {
        focus_manager->ClearFocus();
      }

      return true;
    }

    // Skip non-numeric characters.
    const char16_t character = key_event.GetCharacter();
    if (std::isprint(character) && !std::isdigit(character)) {
      return true;
    }

    // We check selected range because if it is not empty then the user is
    // highlighting text that will be replaced with the input character.
    if (std::isdigit(character) &&
        sender->GetText().length() == kTimerSettingViewMaxCharacters &&
        sender->GetSelectedRange().is_empty()) {
      return true;
    }

    return false;
  }

  // views::ViewObserver:
  void OnViewFocused(views::View* view) override {
    valid_new_contents_ = textfield_->GetText();
  }

  void OnViewBlurred(views::View* view) override {
    valid_new_contents_.clear();
  }

  // Recalculates and sets the size of the textfield to fit the input contents.
  void RefreshTextfieldSize(const std::u16string& contents) {
    int width = 0;
    int height = 0;
    gfx::Canvas::SizeStringInt(contents, textfield_->GetFontList(), &width,
                               &height, 0, gfx::Canvas::NO_ELLIPSIS);
    textfield_->SetPreferredSize(
        gfx::Size(width + textfield_->GetCaretBounds().width() +
                      textfield_->GetInsets().width(),
                  kTimerSettingViewTextHeight));
  }

 private:
  const raw_ptr<SystemTextfield> textfield_ = nullptr;

  // The valid contents (only digits and no more than 3 chars) showing in the
  // textfield while the `textfield_` in an editing state.
  std::u16string valid_new_contents_ = std::u16string();

  // The owning `FocusModeDetailedView`.
  const raw_ptr<FocusModeDetailedView> owner_ = nullptr;
};

FocusModeDetailedView::FocusModeDetailedView(DetailedViewDelegate* delegate)
    : TrayDetailedView(delegate) {
  CreateTitleRow(IDS_ASH_STATUS_TRAY_FOCUS_MODE);
  CreateScrollableList();
  // Margins for the scroll viewport to make the focus ring not clipped. See
  // b/360178403.
  scroller()->SetPreferredViewportMargins(kViewportMargins);

  CreateToggleView();

  CreateTimerView();

  const bool is_network_connected = glanceables_util::IsNetworkConnected();

  CreateTaskView(is_network_connected);

  FocusModeController* focus_mode_controller = FocusModeController::Get();

  const base::flat_set<focus_mode_util::SoundType>& sound_sections =
      focus_mode_controller->focus_mode_sounds_controller()->sound_sections();
  focus_mode_sounds_view_ =
      scroll_content()->AddChildView(std::make_unique<FocusModeSoundsView>(
          sound_sections, is_network_connected));
  focus_mode_sounds_view_->SetID(ViewId::kSoundView);

  const bool in_focus_session = focus_mode_controller->in_focus_session();

  CreateDoNotDisturbContainer();
  do_not_disturb_view_->SetVisible(!in_focus_session);

  scroll_content()->SizeToPreferredSize();
  if (!in_focus_session) {
    StartClockTimer();
  }

  focus_mode_controller->AddObserver(this);
  task_view_container_->AddObserver(this);
  Shell::Get()->system_tray_model()->clock()->AddObserver(this);
}

FocusModeDetailedView::~FocusModeDetailedView() {
  Shell::Get()->system_tray_model()->clock()->RemoveObserver(this);
  task_view_container_->RemoveObserver(this);
  FocusModeController::Get()->RemoveObserver(this);
}

void FocusModeDetailedView::OnViewBoundsChanged(views::View* observed_view) {
  DCHECK_EQ(task_view_container_, observed_view);

  const int old_height = task_view_container_height_;
  task_view_container_height_ = task_view_container_->bounds().height();
  // Skip the animations during the first time the user opens the
  // `FocusModeDetailedView`.
  const int shift_height = old_height - task_view_container_height_;
  if (old_height == 0) {
    return;
  }
  PerformTaskContainerViewResizeAnimation(task_view_container_->layer(),
                                          old_height);
  OnTaskViewAnimate(shift_height);
}

void FocusModeDetailedView::OnDateFormatChanged() {
  UpdateEndTimeLabel();
}

void FocusModeDetailedView::OnSystemClockTimeUpdated() {
  UpdateEndTimeLabel();
}

void FocusModeDetailedView::OnSystemClockCanSetTimeChanged(bool can_set_time) {
  UpdateEndTimeLabel();
}

void FocusModeDetailedView::Refresh() {
  UpdateEndTimeLabel();
}

void FocusModeDetailedView::AddedToWidget() {
  // The `TrayBubbleView` is not normally activatable. To make the textfield in
  // this view activatable, we need to tell the bubble that it can be activated.
  // The `TrayBubbleView` may not exist in unit tests.
  if (views::WidgetDelegate* bubble_view = GetWidget()->widget_delegate()) {
    bubble_view->SetCanActivate(true);
    if (auto* window = GetWidget()->GetNativeWindow()) {
      // Set window properties to stay in the overview mode when the focus panel
      // gained the focus.
      window->SetProperty(kStayInOverviewOnActivationKey, true);

      // We should set a property to stay in overview mode when the focus panel
      // was destroyed. For example, we click `Start` toggle button to start a
      // focus session, we will close the bubble view.
      window->SetProperty(kIgnoreWindowActivationKey, true);
    }
  }
}

void FocusModeDetailedView::OnFocusModeChanged(bool in_focus_session) {
  if (in_focus_session) {
    // The system tray bubble is closed by the `FocusModeController` whenever
    // we toggle focus mode on, so do nothing here.
    return;
  }

  toggle_view_->text_label()->SetText(
      l10n_util::GetStringUTF16(IDS_ASH_STATUS_TRAY_FOCUS_MODE));
  if (toggle_view_->sub_text_label()) {
    toggle_view_->sub_text_label()->SetVisible(false);
  }
  views::AsViewClass<PillButton>(toggle_view_->right_view())
      ->SetText(l10n_util::GetStringUTF16(
          IDS_ASH_STATUS_TRAY_FOCUS_MODE_TOGGLE_START_BUTTON));
  UpdateToggleButtonAccessibility(in_focus_session);

  UpdateTimerView(false);

  StartClockTimer();
  do_not_disturb_view_->SetVisible(true);
}

void FocusModeDetailedView::OnTimerTick(
    const FocusModeSession::Snapshot& session_snapshot) {
  timer_countdown_view_->UpdateUI(session_snapshot);
}

void FocusModeDetailedView::OnActiveSessionDurationChanged(
    const FocusModeSession::Snapshot& session_snapshot) {
  if (session_snapshot.state != FocusModeSession::State::kOn) {
    return;
  }

  toggle_view_->SetSubText(focus_mode_util::GetFormattedEndTimeString(
      FocusModeController::Get()->GetActualEndTime()));
  timer_countdown_view_->UpdateUI(session_snapshot);
  UpdateToggleButtonAccessibility(/*in_focus_session=*/true);
}

void FocusModeDetailedView::CreateToggleView() {
  RoundedContainer* toggle_container =
      scroll_content()->AddChildView(std::make_unique<RoundedContainer>(
          RoundedContainer::Behavior::kTopRounded));

  // `RoundedContainer` adds extra insets, so we need to remove those.
  toggle_container->SetBorderInsets(gfx::Insets());
  toggle_view_ = toggle_container->AddChildView(
      std::make_unique<HoverHighlightView>(/*listener=*/this));
  toggle_view_->SetPreferredSize(gfx::Size(0, kToggleViewHeight));
  views::InkDrop::Get(toggle_view_)
      ->SetMode(views::InkDropHost::InkDropMode::OFF);

  FocusModeController* focus_mode_controller = FocusModeController::Get();
  const bool in_focus_session = focus_mode_controller->in_focus_session();
  toggle_view_->AddIconAndLabel(
      ui::ImageModel::FromVectorIcon(kFocusModeLampIcon,
                                     cros_tokens::kCrosSysOnSurface),
      l10n_util::GetStringUTF16(
          in_focus_session ? IDS_ASH_STATUS_TRAY_FOCUS_MODE_TOGGLE_ACTIVE_LABEL
                           : IDS_ASH_STATUS_TRAY_FOCUS_MODE));
  toggle_view_->text_label()->SetEnabledColorId(cros_tokens::kCrosSysOnSurface);
  TypographyProvider::Get()->StyleLabel(ash::TypographyToken::kCrosButton1,
                                        *toggle_view_->text_label());

  // As part of the first time user flow, if the user has never started a
  // session before, we want to provide description text.
  if (!focus_mode_controller->HasStartedSessionBefore()) {
    toggle_view_->SetSubText(l10n_util::GetStringUTF16(
        IDS_ASH_STATUS_TRAY_FOCUS_MODE_FIRST_TIME_SUBLABEL));
  }

  if (in_focus_session) {
    toggle_view_->SetSubText(focus_mode_util::GetFormattedEndTimeString(
        focus_mode_controller->GetActualEndTime()));
    toggle_view_->sub_text_label()->SetEnabledColorId(
        cros_tokens::kCrosSysSecondary);
    TypographyProvider::Get()->StyleLabel(
        ash::TypographyToken::kCrosAnnotation1,
        *toggle_view_->sub_text_label());
  }

  auto toggle_button = std::make_unique<PillButton>(
      base::BindRepeating(
          &FocusModeController::ToggleFocusMode,
          base::Unretained(focus_mode_controller),
          focus_mode_histogram_names::ToggleSource::kFocusPanel),
      l10n_util::GetStringUTF16(
          in_focus_session
              ? IDS_ASH_STATUS_TRAY_FOCUS_MODE_TOGGLE_END_BUTTON_LABEL
              : IDS_ASH_STATUS_TRAY_FOCUS_MODE_TOGGLE_START_BUTTON),
      PillButton::Type::kPrimaryLargeWithoutIcon, /*icon=*/nullptr);
  toggle_button->SetUseLabelAsDefaultTooltip(false);
  toggle_button->SetID(ViewId::kToggleFocusButton);
  toggle_view_->AddRightView(toggle_button.release());
  UpdateToggleButtonAccessibility(in_focus_session);

  toggle_view_->SetFocusBehavior(View::FocusBehavior::NEVER);
  toggle_view_->tri_view()->SetInsets(kToggleViewInsets);
  views::BoxLayout* toggle_view_tri_view_layout =
      toggle_view_->tri_view()->box_layout();
  toggle_view_tri_view_layout->set_cross_axis_alignment(
      views::BoxLayout::CrossAxisAlignment::kCenter);

  // We need to reset the accessible name for `toggle_view_`, so that ChromeVox
  // will not announce the a11y name when tabbing on its `right_view()`.
  views::ViewAccessibility& view_accessibility =
      toggle_view_->GetViewAccessibility();
  view_accessibility.SetName(std::u16string());
  // Set a valid role for this view to avoid announcing the role for
  // `toggle_view_`.
  view_accessibility.SetRole(ax::mojom::Role::kRow);
}

void FocusModeDetailedView::UpdateToggleButtonAccessibility(
    bool in_focus_session) {
  auto* toggle_button =
      views::AsViewClass<PillButton>(toggle_view_->right_view());

  if (!in_focus_session) {
    const std::u16string duration_string = focus_mode_util::GetDurationString(
        FocusModeController::Get()->session_duration(),
        /*digital_format=*/false);
    toggle_button->GetViewAccessibility().SetName(l10n_util::GetStringFUTF16(
        IDS_ASH_STATUS_TRAY_FOCUS_MODE_TOGGLE_BUTTON_INACTIVE,
        duration_string));
    return;
  }

  toggle_button->GetViewAccessibility().SetName(l10n_util::GetStringUTF16(
      IDS_ASH_STATUS_TRAY_FOCUS_MODE_TOGGLE_END_BUTTON_ACCESSIBLE_NAME));
}

void FocusModeDetailedView::UpdateTimerAdjustmentButtonAccessibility() {
  auto update_timer_adjustment_button = [this](bool decrement) {
    // `GetDurationDelta` will return a negative number for a decrement, so we
    // take the absolute value to indicate a positive number of minutes to
    // decrement by.
    IconButton* button =
        decrement ? timer_decrement_button_ : timer_increment_button_;
    const int id = decrement
                       ? IDS_ASH_STATUS_TRAY_FOCUS_MODE_TIMER_DECREMENT_BUTTON
                       : IDS_ASH_STATUS_TRAY_FOCUS_MODE_TIMER_INCREMENT_BUTTON;
    const int delta = std::abs(GetDurationDelta(
        FocusModeController::Get()->session_duration().InMinutes(), decrement));
    const std::u16string accessible_name = l10n_util::GetStringFUTF16(
        id, focus_mode_util::GetDurationString(base::Minutes(delta),
                                               /*digital_format=*/false));
    button->GetViewAccessibility().SetName(accessible_name);
    button->SetTooltipText(accessible_name);
  };

  update_timer_adjustment_button(/*decrement=*/false);
  update_timer_adjustment_button(/*decrement=*/true);
}

void FocusModeDetailedView::CreateTimerView() {
  // Create the timer view container.
  timer_view_container_ =
      scroll_content()->AddChildView(std::make_unique<RoundedContainer>(
          RoundedContainer::Behavior::kBottomRounded));
  timer_view_container_->SetID(ViewId::kTimerView);
  timer_view_container_->SetProperty(views::kMarginsKey,
                                     kConnectedContainerMargins);
  timer_view_container_->SetBorderInsets(kTimerViewBorderInsets);

  // Create the timer header.
  auto timer_view_header = std::make_unique<views::Label>();
  timer_view_header->SetText(l10n_util::GetStringUTF16(
      IDS_ASH_STATUS_TRAY_FOCUS_MODE_TIMER_SUBHEADER));
  timer_view_header->SetHorizontalAlignment(
      gfx::HorizontalAlignment::ALIGN_LEFT);
  timer_view_header->SetBorder(
      views::CreateEmptyBorder(kTimerViewHeaderInsets));
  timer_view_header->SetEnabledColorId(cros_tokens::kCrosSysOnSurfaceVariant);
  TypographyProvider::Get()->StyleLabel(TypographyToken::kCrosBody2,
                                        *timer_view_header);
  timer_view_container_->AddChildView(std::move(timer_view_header));

  // Create the countdown view.
  timer_countdown_view_ = timer_view_container_->AddChildView(
      std::make_unique<FocusModeCountdownView>(/*include_end_button=*/false));
  timer_countdown_view_->SetBorder(
      views::CreateEmptyBorder(kTimerCountdownViewInsets));

  // Create the timer setting view.
  timer_setting_view_ = timer_view_container_->AddChildView(
      std::make_unique<views::BoxLayoutView>());
  timer_setting_view_->SetOrientation(
      views::BoxLayout::Orientation::kHorizontal);
  timer_setting_view_->SetCrossAxisAlignment(
      views::BoxLayout::CrossAxisAlignment::kCenter);
  timer_setting_view_->SetInsideBorderInsets(kTimerSettingViewInsets);
  timer_setting_view_->SetBetweenChildSpacing(
      kTimerSettingViewBetweenChildSpacing);

  // Add a container for the textfield, the minutes label, and the "Until"
  // label.
  auto* end_time_container = timer_setting_view_->AddChildView(
      std::make_unique<views::BoxLayoutView>());
  end_time_container->SetOrientation(views::BoxLayout::Orientation::kVertical);
  end_time_container->SetCrossAxisAlignment(
      views::BoxLayout::CrossAxisAlignment::kStart);

  // Add a container for the timer textfield and the minutes label.
  auto* textfield_container = end_time_container->AddChildView(
      std::make_unique<views::BoxLayoutView>());
  textfield_container->SetOrientation(
      views::BoxLayout::Orientation::kHorizontal);
  textfield_container->SetCrossAxisAlignment(
      views::BoxLayout::CrossAxisAlignment::kCenter);
  textfield_container->SetBetweenChildSpacing(
      kTimerSettingViewBetweenChildSpacing);

  // `SystemTextfield` does not currently confirm text when the user clicks
  // outside of the textfield but within the textfield's parent. See
  // b/302038651.
  timer_textfield_ = textfield_container->AddChildView(
      std::make_unique<SystemTextfield>(SystemTextfield::Type::kLarge));
  timer_textfield_->SetID(ViewId::kTimerTextfield);
  timer_textfield_->SetHorizontalAlignment(
      gfx::HorizontalAlignment::ALIGN_CENTER);
  timer_textfield_->SetFontList(
      TypographyProvider::Get()->ResolveTypographyToken(
          TypographyToken::kCrosDisplay6Regular));
  timer_textfield_controller_ =
      std::make_unique<TimerTextfieldController>(timer_textfield_, this);
  timer_textfield_->SetActiveStateChangedCallback(base::BindRepeating(
      &FocusModeDetailedView::HandleTextfieldActivationChange,
      weak_factory_.GetWeakPtr()));
  auto* focus_ring = views::FocusRing::Get(timer_textfield_);
  DCHECK(focus_ring);
  // Override the default focus ring gap of `SystemTextfield` to let it not
  // intersect with `end_time_label_`.
  focus_ring->SetHaloInset(0);
  // Override the rounded highlight path set in `SystemTextfield` to keep it the
  // same as the corner radius for the task textfield.
  views::InstallRoundRectHighlightPathGenerator(timer_textfield_, gfx::Insets(),
                                                kTimerTextfieldCornerRadius);

  auto* controller = FocusModeController::Get();
  minutes_label_ = textfield_container->AddChildView(
      std::make_unique<views::Label>(l10n_util::GetPluralStringFUTF16(
          IDS_ASH_STATUS_TRAY_FOCUS_MODE_MINUTES_LABEL,
          controller->session_duration().InMinutes())));
  minutes_label_->SetHorizontalAlignment(gfx::HorizontalAlignment::ALIGN_LEFT);
  TypographyProvider::Get()->StyleLabel(TypographyToken::kCrosDisplay6Regular,
                                        *minutes_label_);
  timer_setting_view_->SetFlexForView(end_time_container, 1);

  // The minutes label ignores the between child spacing on its left side so
  // that it can be directly next to the textfield.
  minutes_label_->SetProperty(
      views::kMarginsKey,
      gfx::Insets::TLBR(0, -1 * kTimerSettingViewBetweenChildSpacing, 0, 0));

  end_time_label_ =
      end_time_container->AddChildView(std::make_unique<views::Label>());
  end_time_label_->SetHorizontalAlignment(gfx::HorizontalAlignment::ALIGN_LEFT);
  TypographyProvider::Get()->StyleLabel(TypographyToken::kCrosAnnotation1,
                                        *end_time_label_);
  end_time_label_->SetEnabledColorId(cros_tokens::kCrosSysSecondary);
  end_time_label_->SetBorder(views::CreateEmptyBorder(
      gfx::Insets::VH(0, kTimerSettingViewBetweenChildSpacing)));

  timer_decrement_button_ =
      timer_setting_view_->AddChildView(CreateTimerAdjustmentButton(
          base::BindRepeating(
              &FocusModeDetailedView::AdjustInactiveSessionDuration,
              base::Unretained(this),
              /*decrement=*/true),
          /*decrement=*/true));
  views::InstallRoundRectHighlightPathGenerator(
      timer_decrement_button_, gfx::Insets(),
      kTimerAdjustmentButtonSize.height() / 2);
  views::InkDrop::Get(timer_decrement_button_)
      ->SetMode(views::InkDropHost::InkDropMode::OFF);

  timer_increment_button_ =
      timer_setting_view_->AddChildView(CreateTimerAdjustmentButton(
          base::BindRepeating(
              &FocusModeDetailedView::AdjustInactiveSessionDuration,
              base::Unretained(this),
              /*decrement=*/false),
          /*decrement=*/false));
  views::InstallRoundRectHighlightPathGenerator(
      timer_increment_button_, gfx::Insets(),
      kTimerAdjustmentButtonSize.height() / 2);
  views::InkDrop::Get(timer_increment_button_)
      ->SetMode(views::InkDropHost::InkDropMode::OFF);

  UpdateTimerView(controller->in_focus_session());
}

void FocusModeDetailedView::UpdateTimerView(bool in_focus_session) {
  CHECK(timer_setting_view_ && timer_countdown_view_);
  timer_setting_view_->SetVisible(!in_focus_session);
  timer_countdown_view_->SetVisible(in_focus_session);

  if (in_focus_session) {
    timer_countdown_view_->UpdateUI(
        FocusModeController::Get()->current_session()->GetSnapshot(
            base::Time::Now()));
  } else {
    UpdateTimerSettingViewUI();
  }
}

void FocusModeDetailedView::HandleTextfieldActivationChange() {
  if (timer_textfield_->IsActive()) {
    return;
  }

  if (timer_textfield_->HasFocus()) {
    auto* focus_manager = timer_textfield_->GetWidget()->GetFocusManager();
    focus_manager->ClearFocus();
    focus_manager->SetStoredFocusView(nullptr);

    // TODO(b/322863087): Remove the call of `UpdateBackground` for
    // timer_textfield_ after the bug resolved. The reason for calling it can be
    // found from the description of the bug.
    timer_textfield_->UpdateBackground();
  }

  // Once we clear the focus for the `timer_textfield_`, we need to call the
  // function below manually to update the UI according to the latest session
  // duration, since the `OnViewBlurred` for the textfield controller doesn't
  // automatically call it to avoid the bug b/315358227.
  SetInactiveSessionDuration(base::Minutes(
      focus_mode_util::GetTimerTextfieldInputInMinutes(timer_textfield_)));
}

void FocusModeDetailedView::CreateTaskView(bool is_network_connected) {
  task_view_container_ =
      scroll_content()->AddChildView(std::make_unique<RoundedContainer>(
          RoundedContainer::Behavior::kAllRounded));
  task_view_container_->SetID(ViewId::kTaskView);
  task_view_container_->SetProperty(views::kMarginsKey,
                                    kDisconnectedContainerMargins);
  task_view_container_->SetBorderInsets(kTaskViewContainerInsets);
  task_view_container_->SetPaintToLayer();
  task_view_container_->layer()->SetFillsBoundsOpaquely(false);

  // Create the task header.
  auto* task_view_header =
      task_view_container_->AddChildView(std::make_unique<views::Label>());
  task_view_header->SetText(
      l10n_util::GetStringUTF16(IDS_ASH_STATUS_TRAY_FOCUS_MODE_TASK_SUBHEADER));
  task_view_header->SetHorizontalAlignment(gfx::ALIGN_LEFT);
  task_view_header->SetBorder(views::CreateEmptyBorder(kTaskViewHeaderInsets));
  task_view_header->SetEnabledColorId(cros_tokens::kCrosSysOnSurfaceVariant);
  TypographyProvider::Get()->StyleLabel(TypographyToken::kCrosBody2,
                                        *task_view_header);

  // Create the focus mode task view.
  focus_mode_task_view_ = task_view_container_->AddChildView(
      std::make_unique<FocusModeTaskView>(is_network_connected));
}

void FocusModeDetailedView::OnTaskViewAnimate(const int shift_height) {
  std::vector<views::View*> animatable_views;

  // Add the views that show up below the tasks view container into
  // `animatable_views`.
  if (focus_mode_sounds_view_->GetVisible()) {
    animatable_views.push_back(focus_mode_sounds_view_);
  }

  if (do_not_disturb_view_->GetVisible()) {
    animatable_views.push_back(do_not_disturb_view_);
  }

  if (animatable_views.empty()) {
    return;
  }
  PerformViewsVerticalShitfAnimation(animatable_views, shift_height);
}

void FocusModeDetailedView::CreateDoNotDisturbContainer() {
  do_not_disturb_view_ =
      scroll_content()->AddChildView(std::make_unique<RoundedContainer>(
          RoundedContainer::Behavior::kAllRounded));
  do_not_disturb_view_->SetProperty(views::kMarginsKey,
                                    kDisconnectedContainerMargins);
  // `RoundedContainer` adds extra insets, so we need to remove those.
  do_not_disturb_view_->SetBorderInsets(gfx::Insets());

  HoverHighlightView* toggle_row = do_not_disturb_view_->AddChildView(
      std::make_unique<HoverHighlightView>(/*listener=*/this));
  toggle_row->SetFocusBehavior(View::FocusBehavior::NEVER);
  toggle_row->SetPreferredSize(gfx::Size(0, kToggleViewHeight));
  views::InkDrop::Get(toggle_row)
      ->SetMode(views::InkDropHost::InkDropMode::OFF);

  // Create the do not disturb icon and its label.
  auto icon = std::make_unique<views::ImageView>();
  icon->SetImage(ui::ImageModel::FromVectorIcon(
      kSystemTrayDoNotDisturbIcon, cros_tokens::kCrosSysOnSurface));
  toggle_row->AddViewAndLabel(
      std::move(icon),
      l10n_util::GetStringUTF16(IDS_ASH_STATUS_TRAY_FOCUS_MODE_DO_NOT_DISTURB));
  toggle_row->text_label()->SetEnabledColorId(cros_tokens::kCrosSysOnSurface);
  TypographyProvider::Get()->StyleLabel(TypographyToken::kCrosButton1,
                                        *toggle_row->text_label());

  // Create the toggle button for do not disturb.
  auto toggle = std::make_unique<Switch>(
      base::BindRepeating(&FocusModeDetailedView::OnDoNotDisturbToggleClicked,
                          weak_factory_.GetWeakPtr()));
  auto* controller = FocusModeController::Get();
  const bool do_not_disturb_enabled = controller->turn_on_do_not_disturb();
  toggle->GetViewAccessibility().SetName(l10n_util::GetStringUTF16(
      IDS_ASH_STATUS_TRAY_FOCUS_MODE_DO_NOT_DISTURB_ACCESSIBLE_NAME));
  toggle->SetIsOn(do_not_disturb_enabled);
  do_not_disturb_toggle_button_ = toggle.get();
  toggle_row->AddRightView(toggle.release(),
                           views::CreateEmptyBorder(gfx::Insets::TLBR(
                               0, kToggleButtonLeftPadding, 0, 0)));

  toggle_row->SetExpandable(true);
  toggle_row->tri_view()->SetInsets(kToggleViewInsets);
  views::BoxLayout* toggle_view_tri_view_layout =
      toggle_row->tri_view()->box_layout();
  toggle_view_tri_view_layout->set_cross_axis_alignment(
      views::BoxLayout::CrossAxisAlignment::kCenter);
}

void FocusModeDetailedView::OnDoNotDisturbToggleClicked() {
  auto* controller = FocusModeController::Get();
  CHECK(!controller->in_focus_session());

  controller->set_turn_on_do_not_disturb(
      do_not_disturb_toggle_button_->GetIsOn());
}

void FocusModeDetailedView::OnClockMinutePassed() {
  if (FocusModeController::Get()->in_focus_session()) {
    UpdateToggleButtonAccessibility(/*in_focus_session=*/true);
    return;
  }

  StartClockTimer();

  // If the user is still setting the timer, we should not always update the UI
  // (for example, if a clock minute passes while the textfield is focused).
  if (timer_textfield_->HasFocus()) {
    return;
  }

  // When a clock minute passes outside of a focus session, we want to update
  // `end_time_label_` to display the correct session end time and restart the
  // clock timer. If we are in a focus session, then
  // `FocusModeController::GetEndTime()` will tell us the time at which the
  // session will end.
  UpdateEndTimeLabel();
}

void FocusModeDetailedView::StartClockTimer() {
  clock_timer_.Start(
      FROM_HERE,
      time_view_utils::GetTimeRemainingToNextMinute(base::Time::Now()), this,
      &FocusModeDetailedView::OnClockMinutePassed);
}

void FocusModeDetailedView::AdjustInactiveSessionDuration(bool decrement) {
  FocusModeController* focus_mode_controller = FocusModeController::Get();
  CHECK(!focus_mode_controller->in_focus_session());
  const base::TimeDelta adjusted_duration =
      CalculateSessionDurationAfterAdjustment(
          focus_mode_util::GetTimerTextfieldInputInMinutes(timer_textfield_),
          decrement);

  SetInactiveSessionDuration(adjusted_duration);

  // Read the session duration after it is adjusted.
  Shell::Get()
      ->accessibility_controller()
      ->TriggerAccessibilityAlertWithMessage(
          base::UTF16ToUTF8(focus_mode_util::GetDurationString(
              focus_mode_controller->session_duration(),
              /*digital_format=*/false)));
}

void FocusModeDetailedView::UpdateTimerSettingViewUI() {
  // We always directly fetch `session_duration` here since the timer setting
  // view doesn't care about the durations that are adjusted during a focus
  // session.
  const base::TimeDelta session_duration =
      FocusModeController::Get()->session_duration();
  end_time_label_->SetText(focus_mode_util::GetFormattedEndTimeString(
      session_duration + base::Time::Now()));
  std::u16string new_session_duration_string =
      base::NumberToString16(session_duration.InMinutes());
  timer_textfield_->SetText(new_session_duration_string);
  timer_textfield_->GetViewAccessibility().SetName(l10n_util::GetStringFUTF16(
      IDS_ASH_STATUS_TRAY_FOCUS_MODE_TIMER_TEXTFIELD,
      focus_mode_util::GetDurationString(session_duration,
                                         /*digital_format=*/false)));
  timer_textfield_controller_->RefreshTextfieldSize(
      new_session_duration_string);
  minutes_label_->SetText(l10n_util::GetPluralStringFUTF16(
      IDS_ASH_STATUS_TRAY_FOCUS_MODE_MINUTES_LABEL,
      session_duration.InMinutes()));

  timer_decrement_button_->SetEnabled(session_duration >
                                      focus_mode_util::kMinimumDuration);
  timer_increment_button_->SetEnabled(session_duration <
                                      focus_mode_util::kMaximumDuration);

  UpdateTimerAdjustmentButtonAccessibility();
}

void FocusModeDetailedView::SetInactiveSessionDuration(
    base::TimeDelta duration) {
  FocusModeController::Get()->SetInactiveSessionDuration(duration);
  UpdateTimerSettingViewUI();
}

void FocusModeDetailedView::UpdateEndTimeLabel() {
  FocusModeController* focus_mode_controller = FocusModeController::Get();
  if (focus_mode_controller->in_focus_session()) {
    toggle_view_->SetSubText(focus_mode_util::GetFormattedEndTimeString(
        focus_mode_controller->GetActualEndTime()));
  } else {
    end_time_label_->SetText(focus_mode_util::GetFormattedEndTimeString(
        focus_mode_controller->session_duration() + base::Time::Now()));
  }
}

BEGIN_METADATA(FocusModeDetailedView)
END_METADATA

}  // namespace ash