chromium/ash/app_list/views/continue_section_view.cc

// Copyright 2021 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/app_list/views/continue_section_view.h"

#include <algorithm>
#include <memory>
#include <string>
#include <utility>
#include <vector>

#include "ash/app_list/app_list_controller_impl.h"
#include "ash/app_list/app_list_model_provider.h"
#include "ash/app_list/app_list_util.h"
#include "ash/app_list/app_list_view_delegate.h"
#include "ash/app_list/model/search/search_model.h"
#include "ash/app_list/views/app_list_nudge_controller.h"
#include "ash/app_list/views/app_list_toast_view.h"
#include "ash/app_list/views/app_list_view_util.h"
#include "ash/app_list/views/continue_task_view.h"
#include "ash/public/cpp/resources/grit/ash_public_unscaled_resources.h"
#include "ash/resources/vector_icons/vector_icons.h"
#include "ash/session/session_controller_impl.h"
#include "ash/shell.h"
#include "ash/strings/grit/ash_strings.h"
#include "ash/wm/desks/templates/saved_desk_controller.h"
#include "base/check.h"
#include "base/strings/string_util.h"
#include "extensions/common/constants.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/compositor/layer.h"
#include "ui/views/animation/animation_builder.h"
#include "ui/views/layout/flex_layout.h"
#include "ui/views/widget/widget.h"

namespace ash {
namespace {
// Whether the files section has been shown.
bool g_continue_section_files_shown = false;

// Suggested tasks layout constants.
constexpr size_t kMinFilesForContinueSectionClamshellMode = 3;
constexpr size_t kMinFilesForContinueSectionTabletMode = 2;

// Privacy toast icon size.
constexpr size_t kPrivacyIconSizeClamshell = 60;
constexpr size_t kPrivacyIconSizeTablet = 48;

// Privacy toast interior margin
constexpr auto kPrivacyToastInteriorMarginClamshell =
    gfx::Insets::TLBR(12, 12, 12, 16);

// Delay before marking the privacy notice as swhon.
const base::TimeDelta kPrivacyNoticeShownDelay = base::Seconds(6);

// Animation constants.
constexpr base::TimeDelta kDismissToastAnimationDuration =
    base::Milliseconds(200);
constexpr base::TimeDelta kShowSuggestionsAnimationDuration =
    base::Milliseconds(300);

// The vertical inside padding between this view and its parent. Taken from
// AppListBubbleAppsPage.
constexpr int kVerticalPaddingFromParent = 16;

void CleanupLayer(views::View* view) {
  view->DestroyLayer();
}

}  // namespace

ContinueSectionView::ContinueSectionView(AppListViewDelegate* view_delegate,
                                         int columns,
                                         bool tablet_mode)
    : view_delegate_(view_delegate), tablet_mode_(tablet_mode) {
  DCHECK(view_delegate_);

  AppListModelProvider::Get()->AddObserver(this);

  if (Shell::HasInstance())
    Shell::Get()->app_list_controller()->AddObserver(this);

  SetLayoutManager(std::make_unique<views::FlexLayout>())
      ->SetMainAxisAlignment(views::LayoutAlignment::kCenter)
      .SetCrossAxisAlignment(views::LayoutAlignment::kCenter)
      .SetOrientation(views::LayoutOrientation::kVertical)
      .SetDefault(views::kFlexBehaviorKey,
                  views::FlexSpecification(
                      views::MinimumFlexSizeRule::kScaleToMinimumSnapToZero,
                      views::MaximumFlexSizeRule::kUnbounded));

  suggestions_container_ =
      AddChildView(std::make_unique<ContinueTaskContainerView>(
          view_delegate, columns,
          base::BindRepeating(
              &ContinueSectionView::OnSearchResultContainerResultsChanged,
              base::Unretained(this)),
          tablet_mode));
  suggestions_container_->SetVisible(false);
}

ContinueSectionView::~ContinueSectionView() {
  AppListModelProvider::Get()->RemoveObserver(this);
  if (Shell::HasInstance() && Shell::Get()->app_list_controller())
    Shell::Get()->app_list_controller()->RemoveObserver(this);
}

size_t ContinueSectionView::GetTasksSuggestionsCount() const {
  return suggestions_container_->num_results();
}

void ContinueSectionView::DisableFocusForShowingActiveFolder(bool disabled) {
  if (privacy_toast_)
    privacy_toast_->toast_button()->SetEnabled(!disabled);

  suggestions_container_->DisableFocusForShowingActiveFolder(disabled);

  // Prevent items from being accessed by ChromeVox.
  SetViewIgnoredForAccessibility(this, disabled);
}

ContinueTaskView* ContinueSectionView::GetTaskViewAtForTesting(
    size_t index) const {
  DCHECK_GT(GetTasksSuggestionsCount(), index);
  return static_cast<ContinueTaskView*>(
      suggestions_container_->children()[index]);
}

// static
bool ContinueSectionView::EnableContinueSectionFileRemovalMetrics() {
  return g_continue_section_files_shown;
}

// static
void ContinueSectionView::
    ResetContinueSectionFileRemovalMetricEnabledForTest() {
  g_continue_section_files_shown = false;
}

void ContinueSectionView::UpdateSuggestionTasks() {
  suggestions_container_->SetResults(
      AppListModelProvider::Get()->search_model()->results());
}

void ContinueSectionView::OnSearchResultContainerResultsChanged() {
  MaybeCreatePrivacyNotice();
  MaybeAnimateOutPrivacyNotice();
}

bool ContinueSectionView::HasMinimumFilesToShow() const {
  return suggestions_container_->num_file_results() >=
         (tablet_mode_ ? kMinFilesForContinueSectionTabletMode
                       : kMinFilesForContinueSectionClamshellMode);
}

bool ContinueSectionView::HasDesksAdminTemplates() const {
  return suggestions_container_->num_desks_admin_template_results() > 0;
}

bool ContinueSectionView::ShouldShowPrivacyNotice() const {
  if (!nudge_controller_)
    return false;

  // Don't show the privacy notice if the reorder nudge is showing.
  if (nudge_controller_->current_nudge() ==
      AppListNudgeController::NudgeType::kReorderNudge) {
    return false;
  }

  return (HasDesksAdminTemplates() || HasMinimumFilesToShow()) &&
         !(nudge_controller_->IsPrivacyNoticeAccepted() ||
           nudge_controller_->WasPrivacyNoticeShown());
}

bool ContinueSectionView::ShouldShowFilesSection() const {
  return (HasDesksAdminTemplates() || HasMinimumFilesToShow()) &&
         (nudge_controller_->IsPrivacyNoticeAccepted() ||
          nudge_controller_->WasPrivacyNoticeShown()) &&
         !privacy_toast_;
}

void ContinueSectionView::SetShownInBackground(bool shown_in_background) {
  // If the privacy notice becomes inactive when it is shown in background, stop
  // the privacy notice shown timer to restart the count on the next show.
  if (shown_in_background && privacy_notice_shown_timer_.IsRunning()) {
    privacy_notice_shown_timer_.AbandonAndStop();
    return;
  }

  // If the privacy notice becomes active again, restart the
  // `privacy_notice_shown_timer_`.
  if (!shown_in_background && !nudge_controller_->WasPrivacyNoticeShown() &&
      privacy_toast_) {
    privacy_notice_shown_timer_.Start(
        FROM_HERE, kPrivacyNoticeShownDelay,
        base::BindOnce(&ContinueSectionView::OnPrivacyNoticeShowTimerDone,
                       base::Unretained(this)));
  }
}

void ContinueSectionView::SetNudgeController(
    AppListNudgeController* nudge_controller) {
  nudge_controller_ = nudge_controller;
}

void ContinueSectionView::OnPrivacyToastAcknowledged() {
  if (nudge_controller_) {
    nudge_controller_->SetPrivacyNoticeAcceptedPref(true);
    nudge_controller_->SetPrivacyNoticeShown(false);
  }
  AnimateDismissToast(
      base::BindRepeating(&ContinueSectionView::AnimateShowContinueSection,
                          weak_ptr_factory_.GetWeakPtr()));
}

void ContinueSectionView::AnimateDismissToast(base::RepeatingClosure callback) {
  // Prevents setting up new animation if the toast is already hiding.
  // https://crbug.com/1326237.
  DCHECK(privacy_toast_);
  if (privacy_toast_->layer() &&
      privacy_toast_->layer()->GetTargetOpacity() == 0.f) {
    return;
  }

  PrepareForLayerAnimation(privacy_toast_);

  views::AnimationBuilder animation_builder;
  animation_builder.OnEnded(callback);
  animation_builder.OnAborted(callback);

  animation_builder
      .SetPreemptionStrategy(
          ui::LayerAnimator::IMMEDIATELY_ANIMATE_TO_NEW_TARGET)
      .Once()
      .SetOpacity(privacy_toast_, 0, gfx::Tween::LINEAR)
      .SetDuration(kDismissToastAnimationDuration);
}

void ContinueSectionView::AnimateSlideLauncherContent(int vertical_offset) {
  RemovePrivacyNotice();
  // The sibling views from this should slide down the total of the height
  // difference to make room for the continue section suggestions and title.
  for (views::View* view : parent()->children()) {
    if (view == this)
      continue;
    const bool create_layer = PrepareForLayerAnimation(view);
    auto cleanup = create_layer ? base::BindRepeating(&CleanupLayer, view)
                                : base::DoNothing();
    StartSlideInAnimation(view, vertical_offset, base::Milliseconds(300),
                          gfx::Tween::ACCEL_40_DECEL_100_3, cleanup);
  }
}

void ContinueSectionView::AnimateShowContinueSection() {
  int height_difference = privacy_toast_->GetPreferredSize().height() -
                          suggestions_container_->GetPreferredSize().height();

  const gfx::Tween::Type animation_tween = gfx::Tween::ACCEL_40_DECEL_100_3;

  // The initial position for the launcher continue section should be right
  // below the search divider.
  gfx::Transform initial_transform;
  initial_transform.Translate(0, -kVerticalPaddingFromParent);

  PrepareForLayerAnimation(suggestions_container_);
  suggestions_container_->layer()->SetTransform(initial_transform);

  privacy_toast_->DestroyLayer();
  privacy_toast_->SetVisible(false);

  suggestions_container_->AnimateSlideInSuggestions(
      height_difference + kVerticalPaddingFromParent,
      kShowSuggestionsAnimationDuration, animation_tween);

  AnimateSlideLauncherContent(height_difference);

  views::AnimationBuilder()
      .SetPreemptionStrategy(
          ui::LayerAnimator::IMMEDIATELY_ANIMATE_TO_NEW_TARGET)
      .Once()
      .SetTransform(suggestions_container_, gfx::Transform(), animation_tween)
      .SetDuration(kShowSuggestionsAnimationDuration);
}

void ContinueSectionView::RemovePrivacyNotice() {
  if (privacy_toast_) {
    RemoveChildViewT(privacy_toast_.get());
    privacy_toast_ = nullptr;
  }
  UpdateElementsVisibility();
  PreferredSizeChanged();
}

void ContinueSectionView::OnPrivacyNoticeShowTimerDone() {
  if (!nudge_controller_)
    return;

  nudge_controller_->SetPrivacyNoticeShownPref(true);
}

void ContinueSectionView::MaybeCreatePrivacyNotice() {
  DCHECK(nudge_controller_);

  if (!ShouldShowPrivacyNotice()) {
    // Reset the nudge controller state if privacy notice shouldn't be showing.
    if (nudge_controller_->current_nudge() ==
        AppListNudgeController::NudgeType::kPrivacyNotice) {
      nudge_controller_->SetPrivacyNoticeShown(false);
    }

    return;
  }

  nudge_controller_->SetPrivacyNoticeShown(true);

  if (privacy_toast_)
    return;

  // When creating the privacy notice and after a delay, mark the nudge as
  // shown, which will keep the user from seeing the nudge again.
  privacy_notice_shown_timer_.Start(
      FROM_HERE, kPrivacyNoticeShownDelay,
      base::BindOnce(&ContinueSectionView::OnPrivacyNoticeShowTimerDone,
                     base::Unretained(this)));

  const int privacy_toast_text_id =
      tablet_mode_ ? IDS_ASH_LAUNCHER_CONTINUE_SECTION_PRIVACY_TEXT_TABLET
                   : IDS_ASH_LAUNCHER_CONTINUE_SECTION_PRIVACY_TEXT;

  privacy_toast_ = AddChildView(
      AppListToastView::Builder(
          l10n_util::GetStringUTF16(privacy_toast_text_id))
          .SetButton(l10n_util::GetStringUTF16(
                         IDS_ASH_LAUNCHER_CONTINUE_SECTION_PRIVACY_BUTTON),
                     base::BindRepeating(
                         &ContinueSectionView::OnPrivacyToastAcknowledged,
                         base::Unretained(this)))
          .SetStyleForTabletMode(tablet_mode_)
          .SetIcon(
              ui::ResourceBundle::GetSharedInstance().GetThemedLottieImageNamed(
                  IDR_APP_LIST_CONTINUE_SECTION_NOTICE_IMAGE))
          .SetIconSize(tablet_mode_ ? kPrivacyIconSizeTablet
                                    : kPrivacyIconSizeClamshell)
          .SetIconBackground(true)
          .Build());
  privacy_toast_->SetProperty(
      views::kFlexBehaviorKey,
      views::FlexSpecification(views::MinimumFlexSizeRule::kScaleToMinimum,
                               views::MaximumFlexSizeRule::kScaleToMaximum));
  if (available_width_) {
    privacy_toast_->SetAvailableWidth(*available_width_);
  }

  if (!tablet_mode_)
    privacy_toast_->UpdateInteriorMargins(kPrivacyToastInteriorMarginClamshell);
}

bool ContinueSectionView::FirePrivacyNoticeShownTimerForTest() {
  if (!privacy_notice_shown_timer_.IsRunning())
    return false;

  privacy_notice_shown_timer_.FireNow();
  return true;
}

void ContinueSectionView::MaybeAnimateOutPrivacyNotice() {
  if (Shell::HasInstance() && Shell::Get()->app_list_controller() &&
      Shell::Get()->app_list_controller()->IsVisible() && !tablet_mode_ &&
      privacy_toast_ && !ShouldShowPrivacyNotice()) {
    AnimateDismissToast(
        base::BindRepeating(&ContinueSectionView::AnimateSlideLauncherContent,
                            weak_ptr_factory_.GetWeakPtr(),
                            privacy_toast_->GetPreferredSize().height()));
    return;
  }
  UpdateElementsVisibility();
}

void ContinueSectionView::UpdateElementsVisibility() {
  // If the user chose to hide the section, set visibility false and reset the
  // privacy toast.
  if (view_delegate_->ShouldHideContinueSection()) {
    SetVisible(false);
    if (privacy_toast_) {
      RemoveChildViewT(privacy_toast_.get());
      privacy_toast_ = nullptr;
      nudge_controller_->SetPrivacyNoticeShown(false);
      privacy_notice_shown_timer_.AbandonAndStop();
    }
    return;
  }
  const bool show_files_section = ShouldShowFilesSection();
  const bool show_privacy_notice = ShouldShowPrivacyNotice();

  SetVisible(show_files_section || show_privacy_notice);

  if (show_files_section)
    g_continue_section_files_shown = true;

  const bool suggestions_visibility_changed =
      show_files_section != suggestions_container_->GetVisible();
  suggestions_container_->SetVisible(show_files_section);

  if (suggestions_visibility_changed)
    PreferredSizeChanged();
}

void ContinueSectionView::AddedToWidget() {
  GetFocusManager()->AddFocusChangeListener(this);
}

void ContinueSectionView::RemovedFromWidget() {
  GetFocusManager()->RemoveFocusChangeListener(this);
}

void ContinueSectionView::OnDidChangeFocus(views::View* focused_before,
                                           views::View* focused_now) {
  // Tablet mode does not have a scrollable container or continue label.
  if (tablet_mode_)
    return;
  // Nothing to do if views are losing focus.
  if (!focused_now)
    return;
  // If a child of the privacy toast gained focus (e.g. the OK button) then
  // ensure the whole toast is visible.
  if (privacy_toast_ && privacy_toast_->Contains(focused_now)) {
    // The parent view owns the continue label, which provides more context
    // for the privacy notice. Ensure the label is visible.
    parent()->ScrollViewToVisible();
    return;
  }
  // If a suggested task gained focus then ensure the continue label is visible
  // so the user knows what this section is.
  if (suggestions_container_->Contains(focused_now)) {
    // The parent view owns the continue label, so ensure label visibility.
    parent()->ScrollViewToVisible();
  }
}

void ContinueSectionView::OnActiveAppListModelsChanged(
    AppListModel* model,
    SearchModel* search_model) {
  UpdateSuggestionTasks();
}

void ContinueSectionView::OnAppListVisibilityChanged(bool shown,
                                                     int64_t display_id) {
  if (!shown && privacy_toast_) {
    // Abort any in-progress privacy toast animation. This may delete the toast
    // view. Explicitly aborting the animation avoids a double-delete in
    // RemoveChildViewT() below. https://crbug.com/1357434
    if (privacy_toast_->layer())
      privacy_toast_->layer()->GetAnimator()->AbortAllAnimations();

    if (privacy_toast_) {
      RemoveChildViewT(privacy_toast_.get());
      privacy_toast_ = nullptr;
    }
  }

  // Update the nudge type in nudge controller if the privacy notice is
  // considered shown and will not be shown again.
  if (!shown && nudge_controller_->WasPrivacyNoticeShown() &&
      nudge_controller_->current_nudge() ==
          AppListNudgeController::NudgeType::kPrivacyNotice) {
    nudge_controller_->SetPrivacyNoticeShown(false);
  }

  // When hiding the launcher, stop the privacy notice shown timer to restart
  // the count on the next show.
  if (!shown && privacy_notice_shown_timer_.IsRunning())
    privacy_notice_shown_timer_.AbandonAndStop();

  if (shown)
    MaybeCreatePrivacyNotice();
  UpdateElementsVisibility();

  if (privacy_toast_)
    PreferredSizeChanged();
}

void ContinueSectionView::ConfigureLayoutForAvailableWidth(
    int available_width) {
  available_width_ = available_width;

  if (privacy_toast_) {
    privacy_toast_->SetAvailableWidth(available_width);
  }
}

BEGIN_METADATA(ContinueSectionView)
END_METADATA

}  // namespace ash