chromium/ash/app_list/app_list_controller_impl.cc

// Copyright 2018 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/app_list_controller_impl.h"

#include <string_view>
#include <utility>
#include <vector>

#include "ash/app_list/app_list_badge_controller.h"
#include "ash/app_list/app_list_bubble_presenter.h"
#include "ash/app_list/app_list_model_provider.h"
#include "ash/app_list/app_list_presenter_impl.h"
#include "ash/app_list/app_list_view_delegate.h"
#include "ash/app_list/apps_collections_controller.h"
#include "ash/app_list/model/search/search_box_model.h"
#include "ash/app_list/quick_app_access_model.h"
#include "ash/app_list/views/app_list_item_view.h"
#include "ash/app_list/views/app_list_main_view.h"
#include "ash/app_list/views/app_list_toast_container_view.h"
#include "ash/app_list/views/app_list_toast_view.h"
#include "ash/app_list/views/app_list_view.h"
#include "ash/app_list/views/contents_view.h"
#include "ash/app_list/views/search_box_view.h"
#include "ash/assistant/assistant_controller_impl.h"
#include "ash/assistant/model/assistant_ui_model.h"
#include "ash/assistant/ui/assistant_view_delegate.h"
#include "ash/assistant/util/assistant_util.h"
#include "ash/assistant/util/deep_link_util.h"
#include "ash/constants/ash_features.h"
#include "ash/constants/ash_pref_names.h"
#include "ash/keyboard/ui/keyboard_ui_controller.h"
#include "ash/public/cpp/app_list/app_list_client.h"
#include "ash/public/cpp/app_list/app_list_controller_observer.h"
#include "ash/public/cpp/app_list/app_list_metrics.h"
#include "ash/public/cpp/app_list/app_list_notifier.h"
#include "ash/public/cpp/app_list/app_list_types.h"
#include "ash/public/cpp/assistant/controller/assistant_controller.h"
#include "ash/public/cpp/assistant/controller/assistant_ui_controller.h"
#include "ash/public/cpp/feature_discovery_duration_reporter.h"
#include "ash/public/cpp/feature_discovery_metric_util.h"
#include "ash/public/cpp/shelf_config.h"
#include "ash/public/cpp/shelf_types.h"
#include "ash/public/cpp/shell_window_ids.h"
#include "ash/public/cpp/wallpaper/wallpaper_controller.h"
#include "ash/root_window_controller.h"
#include "ash/screen_util.h"
#include "ash/session/session_controller_impl.h"
#include "ash/shelf/home_button.h"
#include "ash/shelf/shelf_navigation_widget.h"
#include "ash/shell.h"
#include "ash/user_education/welcome_tour/welcome_tour_metrics.h"
#include "ash/wm/float/float_controller.h"
#include "ash/wm/mru_window_tracker.h"
#include "ash/wm/overview/overview_controller.h"
#include "ash/wm/overview/overview_session.h"
#include "ash/wm/splitview/split_view_controller.h"
#include "ash/wm/window_state.h"
#include "ash/wm/window_util.h"
#include "base/barrier_closure.h"
#include "base/callback_list.h"
#include "base/containers/adapters.h"
#include "base/containers/contains.h"
#include "base/functional/bind.h"
#include "base/functional/callback_helpers.h"
#include "base/logging.h"
#include "base/memory/raw_ptr.h"
#include "base/metrics/histogram_macros.h"
#include "base/metrics/user_metrics.h"
#include "base/trace_event/trace_event.h"
#include "chromeos/ash/services/assistant/public/cpp/assistant_enums.h"
#include "components/pref_registry/pref_registry_syncable.h"
#include "components/prefs/pref_registry_simple.h"
#include "ui/compositor/compositor.h"
#include "ui/compositor/layer.h"
#include "ui/compositor/layer_animation_sequence.h"
#include "ui/display/display_observer.h"
#include "ui/display/manager/display_manager.h"
#include "ui/display/screen.h"
#include "ui/display/util/display_util.h"
#include "ui/views/controls/textfield/textfield.h"
#include "ui/wm/core/scoped_animation_disabler.h"
#include "ui/wm/core/window_animations.h"

namespace ash {

using assistant::AssistantEntryPoint;
using assistant::AssistantExitPoint;

namespace {

constexpr char kHomescreenAnimationHistogram[] =
    "Ash.Homescreen.AnimationSmoothness";

// The target scale to which (or from which) the home launcher will animate when
// overview is being shown (or hidden) using fade transitions while home
// launcher is shown.
constexpr float kOverviewFadeAnimationScale = 0.92f;

// The home launcher animation duration for transitions that accompany overview
// fading transitions.
constexpr base::TimeDelta kOverviewFadeAnimationDuration =
    base::Milliseconds(350);

// The app id for the settings app used for testing quick app access.
constexpr char kOsSettingsAppId[] = "odknhmnlageboeamepcngndbggdpaobj";

// Update layer animation settings for launcher scale and opacity animation that
// runs on overview mode change.
void UpdateOverviewSettings(base::TimeDelta duration,
                            ui::ScopedLayerAnimationSettings* settings) {
  settings->SetTransitionDuration(kOverviewFadeAnimationDuration);
  settings->SetTweenType(gfx::Tween::FAST_OUT_SLOW_IN);
  settings->SetPreemptionStrategy(
      ui::LayerAnimator::IMMEDIATELY_ANIMATE_TO_NEW_TARGET);
}

// Layer animation observer that waits for layer animator to schedule, and
// complete animations. When all animations complete, it fires |callback| and
// deletes itself.
class WindowAnimationsCallback : public ui::LayerAnimationObserver {
 public:
  WindowAnimationsCallback(base::OnceClosure callback,
                           ui::LayerAnimator* animator)
      : callback_(std::move(callback)), animator_(animator) {
    subscription_ = animator_->AddSequenceScheduledCallback(
        base::BindRepeating(&WindowAnimationsCallback::OnSequenceScheduled,
                            base::Unretained(this)));
  }
  WindowAnimationsCallback(const WindowAnimationsCallback&) = delete;
  WindowAnimationsCallback& operator=(const WindowAnimationsCallback&) = delete;
  ~WindowAnimationsCallback() override = default;

  // ui::LayerAnimationObserver:
  void OnLayerAnimationEnded(ui::LayerAnimationSequence* sequence) override {
    FireCallbackIfDone();
  }
  void OnLayerAnimationAborted(ui::LayerAnimationSequence* sequence) override {
    FireCallbackIfDone();
  }
  void OnLayerAnimationScheduled(
      ui::LayerAnimationSequence* sequence) override {}

  void OnDetachedFromSequence(ui::LayerAnimationSequence* sequence) override {
    FireCallbackIfDone();
  }

 private:
  void OnSequenceScheduled(ui::LayerAnimationSequence* sequence) {
    // LayerAnimationSequence::RemoveObserver is called by the ancestor during
    // destruction.
    sequence->AddObserver(this);
  }

  // Fires the callback if all scheduled animations completed (either ended or
  // got aborted).
  void FireCallbackIfDone() {
    if (!callback_ || animator_->is_animating())
      return;
    std::move(callback_).Run();
    delete this;
  }

  base::OnceClosure callback_;
  raw_ptr<ui::LayerAnimator>
      animator_;  // Owned by the layer that is animating.
  base::CallbackListSubscription subscription_;
};

// Minimizes all windows in |windows| that aren't in the home screen container,
// and are not in |windows_to_ignore|. Done in reverse order to preserve the mru
// ordering.
// Returns true if any windows are minimized.
bool MinimizeAllWindows(const aura::Window::Windows& windows,
                        const aura::Window::Windows& windows_to_ignore) {
  aura::Window* container = Shell::Get()->GetPrimaryRootWindow()->GetChildById(
      kShellWindowId_HomeScreenContainer);
  aura::Window::Windows windows_to_minimize;
  for (aura::Window* window : base::Reversed(windows)) {
    if (!container->Contains(window) &&
        !base::Contains(windows_to_ignore, window) &&
        !WindowState::Get(window)->IsMinimized()) {
      windows_to_minimize.push_back(window);
    }
  }

  window_util::MinimizeAndHideWithoutAnimation(windows_to_minimize);
  return !windows_to_minimize.empty();
}

TabletModeAnimationTransition CalculateAnimationTransitionForMetrics(
    HomeLauncherAnimationTrigger trigger,
    bool launcher_should_show) {
  switch (trigger) {
    case HomeLauncherAnimationTrigger::kHideForWindow:
      return TabletModeAnimationTransition::kHideHomeLauncherForWindow;
    case HomeLauncherAnimationTrigger::kLauncherButton:
      return TabletModeAnimationTransition::kHomeButtonShow;
    case HomeLauncherAnimationTrigger::kOverviewModeFade:
      return launcher_should_show
                 ? TabletModeAnimationTransition::kFadeOutOverview
                 : TabletModeAnimationTransition::kFadeInOverview;
  }
}

PrefService* GetLastActiveUserPrefService() {
  return Shell::Get()->session_controller()->GetLastActiveUserPrefService();
}

// Gets the MRU window shown over the applist when in tablet mode.
// Returns nullptr if no windows are shown over the applist.
aura::Window* GetTopVisibleWindow() {
  std::vector<raw_ptr<aura::Window, VectorExperimental>> window_list =
      Shell::Get()->mru_window_tracker()->BuildWindowListIgnoreModal(
          DesksMruType::kActiveDesk);
  for (aura::Window* window : window_list) {
    if (!window->TargetVisibility() || WindowState::Get(window)->IsMinimized())
      continue;

    // Floated windows can be tucked offscreen in tablet mode. Their target
    // visibility is true but the app list is fully visible under them.
    if (WindowState::Get(window)->IsFloated() &&
        Shell::Get()->float_controller()->IsFloatedWindowTuckedForTablet(
            window)) {
      continue;
    }

    return window;
  }
  return nullptr;
}

void LogAppListShowSource(AppListShowSource show_source, bool app_list_bubble) {
  if (app_list_bubble) {
    UMA_HISTOGRAM_ENUMERATION("Apps.AppListBubbleShowSource", show_source);
    return;
  }
  UMA_HISTOGRAM_ENUMERATION("Apps.AppListShowSource", show_source);
}

std::optional<TabletModeAnimationTransition>
GetTransitionFromMetricsAnimationInfo(
    std::optional<HomeLauncherAnimationInfo> animation_info) {
  if (!animation_info.has_value())
    return std::nullopt;

  return CalculateAnimationTransitionForMetrics(animation_info->trigger,
                                                animation_info->showing);
}

bool IsKioskSession() {
  return Shell::Get()->session_controller()->IsRunningInAppMode();
}

void MaybeLogWelcomeTourInteraction(AppListShowSource show_source) {
  if (features::IsWelcomeTourEnabled() &&
      IsAppListShowSourceUserTriggered(show_source)) {
    welcome_tour_metrics::RecordInteraction(
        GetLastActiveUserPrefService(),
        welcome_tour_metrics::Interaction::kLauncher);
  }
}

bool IsAssistantExitPointScreenshot(
    std::optional<assistant::AssistantExitPoint> exit_point) {
  return exit_point == AssistantExitPoint::kScreenshot;
}

bool IsAssistantExitPointInsideLauncher(
    std::optional<assistant::AssistantExitPoint> exit_point) {
  return exit_point == AssistantExitPoint::kBackInLauncher ||
         exit_point == AssistantExitPoint::kLauncherSearchIphChip;
}

}  // namespace

AppListControllerImpl::AppListControllerImpl()
    : model_provider_(std::make_unique<AppListModelProvider>()),
      fullscreen_presenter_(std::make_unique<AppListPresenterImpl>(this)),
      bubble_presenter_(std::make_unique<AppListBubblePresenter>(this)),
      badge_controller_(std::make_unique<AppListBadgeController>()),
      apps_collections_controller_(
          std::make_unique<AppsCollectionsController>()) {
  SessionControllerImpl* session_controller =
      Shell::Get()->session_controller();
  session_controller->AddObserver(this);

  // In case of crash-and-restart case where session state starts with ACTIVE
  // and does not change to trigger OnSessionStateChanged(), notify the current
  // session state here to ensure that the app list is shown.
  OnSessionStateChanged(session_controller->GetSessionState());

  Shell* shell = Shell::Get();
  WallpaperController::Get()->AddObserver(this);
  shell->AddShellObserver(this);
  shell->overview_controller()->AddObserver(this);
  display::Screen::GetScreen()->AddObserver(this);
  keyboard::KeyboardUIController::Get()->AddObserver(this);
  AssistantState::Get()->AddObserver(this);
  shell->display_manager()->AddDisplayManagerObserver(this);
  AssistantController::Get()->AddObserver(this);
  AssistantUiController::Get()->GetModel()->AddObserver(this);
  FeatureDiscoveryDurationReporter::GetInstance()->AddObserver(this);
}

AppListControllerImpl::~AppListControllerImpl() {
  if (tracked_app_window_) {
    tracked_app_window_->RemoveObserver(this);
    tracked_app_window_ = nullptr;
  }

  if (has_session_started_)
    RecordMetricsOnSessionEnd();

  // If this is being destroyed before the Shell starts shutting down, first
  // remove this from objects it's observing.
  if (!is_shutdown_)
    Shutdown();

  if (client_)
    client_->OnAppListControllerDestroyed();

  // Dismiss the window before `fullscreen_presenter_` is reset, because
  // Dimiss() may call back into this object and access `fullscreen_presenter_`.
  fullscreen_presenter_->Dismiss(base::TimeTicks());
}

// static
void AppListControllerImpl::RegisterProfilePrefs(PrefRegistrySimple* registry) {
  registry->RegisterBooleanPref(
      prefs::kLauncherFeedbackOnContinueSectionSent, false,
      user_prefs::PrefRegistrySyncable::SYNCABLE_OS_PREF);
  registry->RegisterBooleanPref(
      prefs::kLauncherContinueSectionHidden, false,
      user_prefs::PrefRegistrySyncable::SYNCABLE_OS_PREF);
  registry->RegisterTimePref(prefs::kLauncherLastContinueRequestTime,
                             base::Time());
  registry->RegisterBooleanPref(prefs::kLauncherUseLongContinueDelay, false);

  // The prefs for launcher search controls.
  registry->RegisterDictionaryPref(prefs::kLauncherSearchCategoryControlStatus);

  registry->RegisterTimePref(prefs::kLauncherSearchLastFileScanLogTime,
                             base::Time());

  // The prefs for apps collections experiment.
  registry->RegisterIntegerPref(
      prefs::kLauncherAppsCollectionsExperimentArm,
      static_cast<int>(
          AppsCollectionsController::ExperimentalArm::kDefaultValue));
}

void AppListControllerImpl::SetClient(AppListClient* client) {
  client_ = client;
  apps_collections_controller_->SetClient(client);
}

AppListClient* AppListControllerImpl::GetClient() {
  DCHECK(client_);
  return client_;
}

AppListNotifier* AppListControllerImpl::GetNotifier() {
  if (!client_)
    return nullptr;
  return client_->GetNotifier();
}

std::unique_ptr<ScopedIphSession>
AppListControllerImpl::CreateLauncherSearchIphSession() {
  if (!client_) {
    return nullptr;
  }
  return client_->CreateLauncherSearchIphSession();
}

void AppListControllerImpl::SetActiveModel(
    int profile_id,
    AppListModel* model,
    SearchModel* search_model,
    QuickAppAccessModel* quick_app_access_model) {
  profile_id_ = profile_id;
  model_provider_->SetActiveModel(model, search_model, quick_app_access_model);
  UpdateSearchBoxUiVisibilities();
}

void AppListControllerImpl::ClearActiveModel() {
  profile_id_ = kAppListInvalidProfileID;
  model_provider_->ClearActiveModel();
  UpdateSearchBoxUiVisibilities();
}

void AppListControllerImpl::DismissAppList() {
  if (tracked_app_window_) {
    tracked_app_window_->RemoveObserver(this);
    tracked_app_window_ = nullptr;
  }

  // Don't check tablet mode here. This function can be called during tablet
  // mode transitions and we always want to close anyway.
  bubble_presenter_->Dismiss();

  fullscreen_presenter_->Dismiss(base::TimeTicks());
}

void AppListControllerImpl::ShowAppList(AppListShowSource source) {
  if (Shell::Get()->session_controller()->GetSessionState() !=
      session_manager::SessionState::ACTIVE) {
    return;
  }

  last_open_source_ = source;
  Show(GetDisplayIdToShowAppListOn(), source, base::TimeTicks(),
       /*should_record_metrics=*/true);
}

AppListShowSource AppListControllerImpl::LastAppListShowSource() {
  DCHECK(last_open_source_.has_value());
  return last_open_source_.value();
}

aura::Window* AppListControllerImpl::GetWindow() {
  if (IsInTabletMode()) {
    return fullscreen_presenter_->GetWindow();
  }
  return bubble_presenter_->GetWindow();
}

bool AppListControllerImpl::IsVisible(
    const std::optional<int64_t>& display_id) {
  return last_visible_ && (!display_id.has_value() ||
                           display_id.value() == last_visible_display_id_);
}

bool AppListControllerImpl::IsVisible() {
  return IsVisible(std::nullopt);
}

void AppListControllerImpl::OnActiveUserPrefServiceChanged(
    PrefService* pref_service) {
  if (IsKioskSession())
    return;

  if (!IsInTabletMode()) {
    DismissAppList();
    return;
  }

  // The app list is not dismissed before switching user, suggestion chips will
  // not be shown. So reset app list state and trigger an initial search here to
  // update the suggestion results.
  if (fullscreen_presenter_->GetView()) {
    fullscreen_presenter_->GetView()->CloseOpenedPage();
    fullscreen_presenter_->GetView()->search_box_view()->ClearSearch();
  }
}

void AppListControllerImpl::OnSessionStateChanged(
    session_manager::SessionState state) {
  if (state == session_manager::SessionState::ACTIVE)
    has_session_started_ = true;

  const bool in_clamshell = !IsInTabletMode();
  if (state != session_manager::SessionState::ACTIVE || IsKioskSession()) {
    if (in_clamshell)
      DismissAppList();
    return;
  }

  if (base::FeatureList::IsEnabled(features::kQuickAppAccessTestUI)) {
    SetHomeButtonQuickApp(kOsSettingsAppId);
  }

  if (in_clamshell)
    return;

  // Show the app list after signing in in tablet mode. For metrics, the app
  // list is not considered shown since the browser window is shown over app
  // list upon login.
  if (!fullscreen_presenter_->GetTargetVisibility())
    ShowHomeScreen(AppListShowSource::kTabletMode);

  // Hide app list UI initially to prevent app list from flashing in background
  // while the initial app window is being shown.
  if (!last_target_visible_ && !ShouldHomeLauncherBeVisible())
    fullscreen_presenter_->SetViewVisibility(false);
  else
    OnVisibilityChanged(true, last_visible_display_id_);
}

void AppListControllerImpl::OnUserSessionAdded(const AccountId& account_id) {
  if (!client_)
    return;

  ash::ReportPrefSortOrderOnSessionStart(client_->GetPermanentSortingOrder(),
                                         IsInTabletMode());

  auto* prefs =
      Shell::Get()->session_controller()->GetUserPrefServiceForUser(account_id);
  if (features::IsLauncherNudgeSessionResetEnabled()) {
    AppListNudgeController::ResetPrefsForNewUserSession(prefs);
  }
}

////////////////////////////////////////////////////////////////////////////////
// Methods used in Ash

bool AppListControllerImpl::GetTargetVisibility(
    const std::optional<int64_t>& display_id) const {
  return last_target_visible_ &&
         (!display_id.has_value() ||
          display_id.value() == last_target_visible_display_id_);
}

void AppListControllerImpl::Show(int64_t display_id,
                                 AppListShowSource show_source,
                                 base::TimeTicks event_time_stamp,
                                 bool should_record_metrics) {
  if (IsKioskSession())
    return;

  last_open_source_ = show_source;
  if (should_record_metrics)
    LogAppListShowSource(show_source, !IsInTabletMode());

  // Checking `should_record_metrics` is redundant here, since this helper
  // function never logs metrics when the app list was shown by tablet mode.
  MaybeLogWelcomeTourInteraction(show_source);

  if (IsInTabletMode()) {
    fullscreen_presenter_->Show(AppListViewState::kFullscreenAllApps,
                                display_id, event_time_stamp, show_source);
    return;
  }

  bubble_presenter_->Show(display_id);
}

void AppListControllerImpl::UpdateAppListWithNewTemporarySortOrder(
    const std::optional<AppListSortOrder>& new_order,
    bool animate,
    base::OnceClosure update_position_closure) {
  TRACE_EVENT0("ui",
               "AppListControllerImpl::UpdateAppListWithNewTemporarySortOrder");
  if (new_order) {
    RecordAppListSortAction(*new_order, IsInTabletMode());

    FeatureDiscoveryDurationReporter* reporter =
        FeatureDiscoveryDurationReporter::GetInstance();
    reporter->MaybeFinishObservation(feature_discovery::TrackableFeature::
                                         kAppListReorderAfterEducationNudge);
    reporter->MaybeFinishObservation(
        feature_discovery::TrackableFeature::
            kAppListReorderAfterEducationNudgePerTabletMode);
    reporter->MaybeFinishObservation(feature_discovery::TrackableFeature::
                                         kAppListReorderAfterSessionActivation);
  }

  // Adapt the bubble app list to the new sorting order. NOTE: the bubble app
  // list is visible only in clamshell mode. Therefore do not animate in tablet
  // mode.
  const bool is_tablet_mode = IsInTabletMode();
  bubble_presenter_->UpdateForNewSortingOrder(
      new_order, !is_tablet_mode && animate,
      is_tablet_mode ? base::NullCallback()
                     : std::move(update_position_closure));

  // Adapt the fullscreen app list to the new sorting order. NOTE: the full
  // screen app list is visible only in tablet mode. Therefore do not animate in
  // clamshell mode.
  fullscreen_presenter_->UpdateForNewSortingOrder(
      new_order, is_tablet_mode && animate,
      is_tablet_mode ? std::move(update_position_closure)
                     : base::NullCallback());

  // Notify the AppsCollectionsController that there was a reorder.
  apps_collections_controller_->SetAppsReordered();
}

ShelfAction AppListControllerImpl::ToggleAppList(
    int64_t display_id,
    AppListShowSource show_source,
    base::TimeTicks event_time_stamp) {
  if (Shell::Get()->session_controller()->GetSessionState() !=
      session_manager::SessionState::ACTIVE) {
    return SHELF_ACTION_APP_LIST_DISMISSED;
  }

  if (IsKioskSession())
    return SHELF_ACTION_APP_LIST_DISMISSED;

  if (IsInTabletMode()) {
    bool handled = GoHome(display_id);

    // Perform the "back" action for the app list.
    if (!handled) {
      Back();
      return SHELF_ACTION_APP_LIST_BACK;
    }
    LogAppListShowSource(show_source, /*app_list_bubble=*/false);
    last_open_source_ = show_source;

    MaybeLogWelcomeTourInteraction(show_source);

    return SHELF_ACTION_APP_LIST_SHOWN;
  }

  ShelfAction action = bubble_presenter_->Toggle(display_id);
  if (action == SHELF_ACTION_APP_LIST_SHOWN) {
    LogAppListShowSource(show_source, /*app_list_bubble=*/true);
    last_open_source_ = show_source;

    MaybeLogWelcomeTourInteraction(show_source);
  }
  return action;
}

bool AppListControllerImpl::GoHome(int64_t display_id) {
  if (IsKioskSession())
    return false;

  DCHECK(IsInTabletMode());

  if (fullscreen_presenter_->IsShowingEmbeddedAssistantUI()) {
    // OnHomeLauncherAnimationComplete() may not be called if the
    // `foreground_windows` is empty. Call AssistantUiController::CloseUi() here
    // directly.
    AssistantUiController::Get()->CloseUi(AssistantExitPoint::kLauncherClose);
    fullscreen_presenter_->ShowEmbeddedAssistantUI(false);
  }

  SplitViewController* split_view_controller =
      SplitViewController::Get(Shell::GetPrimaryRootWindow());
  const bool split_view_active = split_view_controller->InSplitViewMode();

  // The home screen opens for the current active desk, there's no need to
  // minimize windows in the inactive desks.
  aura::Window::Windows windows =
      Shell::Get()->mru_window_tracker()->BuildWindowForCycleList(kActiveDesk);

  // The foreground window or windows (for split mode) - the windows that will
  // not be minimized without animations (instead they will be animated into the
  // home screen).
  std::vector<raw_ptr<aura::Window, VectorExperimental>> foreground_windows;
  if (split_view_active) {
    foreground_windows = {split_view_controller->primary_window(),
                          split_view_controller->secondary_window()};
    std::erase_if(foreground_windows,
                  [](aura::Window* window) { return !window; });
  } else if (!windows.empty() && !WindowState::Get(windows[0])->IsMinimized()) {
    foreground_windows.push_back(windows[0]);
  }

  OverviewController* overview_controller = Shell::Get()->overview_controller();
  if (split_view_active) {
    // If overview session is active (e.g. on one side of the split view), end
    // it immediately, to prevent overview UI being visible while transitioning
    // to home screen.
    if (overview_controller->InOverviewSession()) {
      overview_controller->EndOverview(OverviewEndAction::kEnterHomeLauncher,
                                       OverviewEnterExitType::kImmediateExit);
    }

    // End split view mode.
    split_view_controller->EndSplitView(
        SplitViewController::EndReason::kHomeLauncherPressed);
  }

  // If overview is active (if overview was active in split view, it exited by
  // this point), just fade it out to home screen.
  if (overview_controller->InOverviewSession()) {
    overview_controller->EndOverview(OverviewEndAction::kEnterHomeLauncher,
                                     OverviewEnterExitType::kFadeOutExit);
    return true;
  }

  // First minimize all inactive windows.
  const bool window_minimized =
      MinimizeAllWindows(windows, foreground_windows /*windows_to_ignore*/);

  if (foreground_windows.empty())
    return window_minimized;

  {
    // Disable window animations before updating home launcher target
    // position. Calling OnHomeLauncherPositionChanged() can cause
    // display work area update, and resulting cross-fade window bounds change
    // animation can interfere with WindowTransformToHomeScreenAnimation
    // visuals.
    //
    // TODO(crbug.com/40656009): This can be removed once transitions
    // between in-app state and home do not cause work area updates.
    std::vector<std::unique_ptr<wm::ScopedAnimationDisabler>>
        animation_disablers;
    for (aura::Window* window : foreground_windows) {
      animation_disablers.push_back(
          std::make_unique<wm::ScopedAnimationDisabler>(window));
    }

    OnHomeLauncherPositionChanged(/*percent_shown=*/100, display_id);
  }

  StartTrackingAnimationSmoothness(display_id);

  base::RepeatingClosure window_transforms_callback = base::BarrierClosure(
      foreground_windows.size(),
      base::BindOnce(&AppListControllerImpl::OnGoHomeWindowAnimationsEnded,
                     weak_ptr_factory_.GetWeakPtr(), display_id));

  // Minimize currently active windows, but this time, using animation.
  // Home screen will show when all the windows are done minimizing.
  for (aura::Window* foreground_window : foreground_windows) {
    if (::wm::WindowAnimationsDisabled(foreground_window)) {
      WindowState::Get(foreground_window)->Minimize();
      window_transforms_callback.Run();
    } else {
      // Create animator observer that will fire |window_transforms_callback|
      // once the window layer stops animating - it deletes itself when
      // animations complete.
      new WindowAnimationsCallback(window_transforms_callback,
                                   foreground_window->layer()->GetAnimator());
      WindowState::Get(foreground_window)->Minimize();
    }
  }

  return true;
}

bool AppListControllerImpl::ShouldHomeLauncherBeVisible() const {
  if (!IsInTabletMode() || IsKioskSession()) {
    return false;
  }

  if (home_launcher_transition_state_ ==
      HomeLauncherTransitionState::kMostlyShown) {
    return true;
  }

  return !Shell::Get()->overview_controller()->InOverviewSession() &&
         !GetTopVisibleWindow();
}

void AppListControllerImpl::OnShelfAlignmentChanged(
    aura::Window* root_window,
    ShelfAlignment old_alignment) {
  if (!IsInTabletMode()) {
    DismissAppList();
  }
}

void AppListControllerImpl::OnShellDestroying() {
  // Stop observing at the beginning of ~Shell to avoid unnecessary work during
  // Shell shutdown.
  Shutdown();
}

void AppListControllerImpl::OnOverviewModeStarting() {
  const OverviewEnterExitType overview_enter_type =
      Shell::Get()
          ->overview_controller()
          ->overview_session()
          ->enter_exit_overview_type();

  const bool animate =
      IsHomeScreenVisible() &&
      overview_enter_type == OverviewEnterExitType::kFadeInEnter;

  UpdateForOverviewModeChange(/*show_home_launcher=*/false, animate);

  if (IsInTabletMode()) {
    const int64_t display_id = last_visible_display_id_;
    OnVisibilityWillChange(false /*shown*/, display_id);
  } else {
    DismissAppList();
  }
}

void AppListControllerImpl::OnOverviewModeStartingAnimationComplete(
    bool canceled) {
  if (!IsInTabletMode()) {
    return;
  }

  // If overview start was canceled, overview end animations are about to start.
  // Preemptively update the target app list visibility.
  if (canceled) {
    OnVisibilityWillChange(!GetTopVisibleWindow(), last_visible_display_id_);
    return;
  }

  OnVisibilityChanged(false /* shown */, last_visible_display_id_);
}

void AppListControllerImpl::OnOverviewModeEnding(OverviewSession* session) {
  // The launcher will be shown after overview mode finishes animating, in
  // OnOverviewModeEndingAnimationComplete(). Overview however is nullptr by
  // the time the animations are finished, so cache the exit type here.
  overview_exit_type_ = std::make_optional(session->enter_exit_overview_type());

  // If the overview is fading out, start the home launcher animation in
  // parallel. Otherwise the transition will be initiated in
  // OnOverviewModeEndingAnimationComplete().
  if (session->enter_exit_overview_type() ==
      OverviewEnterExitType::kFadeOutExit) {
    UpdateForOverviewModeChange(/*show_home_launcher=*/true, /*animate=*/true);

    // Make sure the window visibility is updated, in case it was previously
    // hidden due to overview being shown.
    UpdateHomeScreenVisibility();
  }

  if (!IsInTabletMode()) {
    return;
  }

  // Overview state might end during home launcher transition - if that is the
  // case, respect the final state set by in-progress home launcher transition.
  if (home_launcher_transition_state_ != HomeLauncherTransitionState::kFinished)
    return;

  OnVisibilityWillChange(!GetTopVisibleWindow() /*shown*/,
                         last_visible_display_id_);
}

void AppListControllerImpl::OnOverviewModeEnded() {
  if (!IsInTabletMode()) {
    return;
  }

  // Overview state might end during home launcher transition - if that is the
  // case, respect the final state set by in-progress home launcher transition.
  if (home_launcher_transition_state_ != HomeLauncherTransitionState::kFinished)
    return;
  OnVisibilityChanged(!GetTopVisibleWindow(), last_visible_display_id_);
}

void AppListControllerImpl::OnOverviewModeEndingAnimationComplete(
    bool canceled) {
  DCHECK(overview_exit_type_.has_value());

  // For kFadeOutExit OverviewEnterExitType, the home animation is scheduled in
  // OnOverviewModeEnding(), so there is nothing else to do at this point.
  if (canceled || *overview_exit_type_ == OverviewEnterExitType::kFadeOutExit) {
    overview_exit_type_ = std::nullopt;
    return;
  }

  const bool animate =
      *overview_exit_type_ == OverviewEnterExitType::kFadeOutExit;
  overview_exit_type_ = std::nullopt;

  UpdateForOverviewModeChange(/*show_home_launcher=*/true, animate);

  // Make sure the window visibility is updated, in case it was previously
  // hidden due to overview being shown.
  UpdateHomeScreenVisibility();
}

void AppListControllerImpl::OnSplitViewStateChanged(
    SplitViewController::State previous_state,
    SplitViewController::State state) {
  UpdateHomeScreenVisibility();
}

void AppListControllerImpl::OnChangedToInTabletMode() {
  if (IsKioskSession()) {
    return;
  }

  // Reset the keyboard traversal mode to prevent using the value saved in
  // clamshell mode.
  SetKeyboardTraversalMode(false);

  bubble_presenter_->Dismiss();

  // Show the app list if the tablet mode starts.
  if (Shell::Get()->session_controller()->GetSessionState() ==
      session_manager::SessionState::ACTIVE) {
    ShowHomeScreen(AppListShowSource::kTabletMode);
  }
  UpdateFullscreenLauncherContainer();

  // If the app list is visible before the transition to tablet mode,
  // AppListPresenter relies on the active window change to detect the app list
  // view got hidden behind a window. Though, app list UI moving behind an app
  // window does not always cause an active window change:
  // *   If the app list is still being shown - given that app list takes focus
  //     from the top window only when it's fully shown, the focus will remain
  //     within the app window throughout the tablet mode transition.
  // *   If the assistant UI is visible before the tablet mode transition - the
  //     assistant will keep the focus during transition, even though the app
  //     window will be shown over the app list view.
  // Ensure the app list visibility is properly updated if the app list is
  // hidden behind a window at this point.
  if (last_target_visible_ && !ShouldHomeLauncherBeVisible()) {
    OnVisibilityChanged(false, last_visible_display_id_);
  }
}

void AppListControllerImpl::OnChangedToInClamshellMode() {
  // Reset the keyboard traversal mode to prevent using the value saved last
  // time in tablet mode.
  SetKeyboardTraversalMode(false);

  aura::Window* window = fullscreen_presenter_->GetWindow();
  base::AutoReset<bool> auto_reset(
      &should_dismiss_immediately_,
      window && RootWindowController::ForWindow(window)
                    ->GetShelfLayoutManager()
                    ->HasVisibleWindow());
  UpdateFullscreenLauncherContainer();

  // Dismiss the app list if the tablet mode ends.
  DismissAppList();
}

void AppListControllerImpl::OnDisplayTabletStateChanged(
    display::TabletState state) {
  switch (state) {
    case display::TabletState::kEnteringTabletMode:
    case display::TabletState::kExitingTabletMode:
      // Do nothing when the tablet state is still in the process of transition.
      break;
    case display::TabletState::kInTabletMode:
      OnChangedToInTabletMode();
      break;
    case display::TabletState::kInClamshellMode:
      OnChangedToInClamshellMode();
      break;
  }
}

void AppListControllerImpl::OnWallpaperPreviewStarted() {
  in_wallpaper_preview_ = true;
  UpdateHomeScreenVisibility();
}

void AppListControllerImpl::OnWallpaperPreviewEnded() {
  in_wallpaper_preview_ = false;
  UpdateHomeScreenVisibility();
}

void AppListControllerImpl::OnKeyboardVisibilityChanged(const bool is_visible) {
  onscreen_keyboard_shown_ = is_visible;
  AppListView* app_list_view = fullscreen_presenter_->GetView();
  if (app_list_view)
    app_list_view->OnScreenKeyboardShown(is_visible);
}

void AppListControllerImpl::OnAssistantStatusChanged(
    assistant::AssistantStatus status) {
  UpdateSearchBoxUiVisibilities();
}

void AppListControllerImpl::OnAssistantSettingsEnabled(bool enabled) {
  UpdateSearchBoxUiVisibilities();
}

void AppListControllerImpl::OnAssistantFeatureAllowedChanged(
    assistant::AssistantAllowedState state) {
  UpdateSearchBoxUiVisibilities();
}

void AppListControllerImpl::OnDidApplyDisplayChanges() {
  // Entering tablet mode triggers a display configuration change when we
  // automatically switch to mirror mode. Switching to mirror mode happens
  // asynchronously (see DisplayConfigurationObserver::OnTabletModeStarted()).
  // This may result in the removal of a window tree host, as in the example of
  // switching to tablet mode while Unified Desktop mode is on; the Unified host
  // will be destroyed and the Home Launcher (which was created earlier when we
  // entered tablet mode) will be dismissed.
  // To avoid crashes, we must ensure that the Home Launcher shown status is as
  // expected if it's enabled and we're still in tablet mode.
  // https://crbug.com/900956.
  const bool should_be_shown =
      IsInTabletMode() &&
      Shell::Get()->session_controller()->GetSessionState() ==
          session_manager::SessionState::ACTIVE;

  if (!should_be_shown ||
      should_be_shown == fullscreen_presenter_->GetTargetVisibility()) {
    return;
  }
  ShowHomeScreen(AppListShowSource::kTabletMode);
}

void AppListControllerImpl::OnAssistantReady() {
  UpdateSearchBoxUiVisibilities();
}

void AppListControllerImpl::OnUiVisibilityChanged(
    AssistantVisibility new_visibility,
    AssistantVisibility old_visibility,
    std::optional<AssistantEntryPoint> entry_point,
    std::optional<AssistantExitPoint> exit_point) {
  const bool is_old_visibility_closing =
      (old_visibility == AssistantVisibility::kClosing);

  switch (new_visibility) {
    case AssistantVisibility::kVisible:
      DVLOG(1) << "Assistant becoming visible";
      if (!IsVisible() || is_old_visibility_closing) {
        std::optional<AppListView::ScopedContentsResetDisabler> disabler;
        if (is_old_visibility_closing) {
          // Avoid resetting the contents view when the transition to close the
          // Assistant ui is going to be reversed.
          if (fullscreen_presenter_->GetView())
            disabler.emplace(fullscreen_presenter_->GetView());

          // Reset `close_assistant_ui_runner_` because the Assistant ui is
          // going to show.
          DCHECK(close_assistant_ui_runner_);
          IgnoreResult(close_assistant_ui_runner_.Release());
        }

        Show(GetDisplayIdToShowAppListOn(),
             AppListShowSource::kAssistantEntryPoint, base::TimeTicks(),
             /*should_record_metrics=*/true);
      }
      if (!IsInTabletMode()) {
        bubble_presenter_->ShowEmbeddedAssistantUI();
      } else {
        if (!fullscreen_presenter_->IsShowingEmbeddedAssistantUI() ||
            is_old_visibility_closing) {
          fullscreen_presenter_->ShowEmbeddedAssistantUI(true);
        }

        // Make sure that app list views are visible - they might get hidden
        // during session startup, and the app list visibility might not have
        // yet changed to visible by this point. https://crbug.com/1040751
        fullscreen_presenter_->SetViewVisibility(true);
      }
      break;
    case AssistantVisibility::kClosed:
      if (!IsShowingEmbeddedAssistantUI())
        break;

      // When Launcher is closing, we do not want to call
      // |ShowEmbeddedAssistantUI(false)|, which will show previous state page
      // in Launcher and make the UI flash.
      if (IsInTabletMode()) {
        std::optional<ContentsView::ScopedSetActiveStateAnimationDisabler>
            set_active_state_animation_disabler;
        // When taking a screenshot by Assistant, we do not want to animate to
        // the final state. Otherwise the screenshot may have transient state
        // during the animation. In tablet mode, we want to go back to
        // kStateApps immediately, i.e. skipping the animation in
        // |SetActiveStateInternal|, which are called from
        // |ShowEmbeddedAssistantUI(false)| and
        // |ClearSearchAndDeactivateSearchBox()|.
        if (IsAssistantExitPointScreenshot(exit_point)) {
          set_active_state_animation_disabler.emplace(
              fullscreen_presenter_->GetView()
                  ->app_list_main_view()
                  ->contents_view());
        }

        fullscreen_presenter_->ShowEmbeddedAssistantUI(false);

        if (!IsAssistantExitPointInsideLauncher(exit_point)) {
          fullscreen_presenter_->GetView()
              ->search_box_view()
              ->ClearSearchAndDeactivateSearchBox();
        }
      } else if (!IsAssistantExitPointInsideLauncher(exit_point)) {
        // Similarly, when taking a screenshot by Assistant in clamshell mode,
        // we do not want to dismiss launcher with animation. Otherwise the
        // screenshot may have transient state during the animation.
        base::AutoReset<bool> auto_reset(
            &should_dismiss_immediately_,
            IsAssistantExitPointScreenshot(exit_point));
        DismissAppList();
      }
      break;
    case AssistantVisibility::kClosing:
      break;
  }
}

void AppListControllerImpl::OnHomeLauncherAnimationComplete(
    bool shown,
    int64_t display_id) {
  home_launcher_transition_state_ = HomeLauncherTransitionState::kFinished;

  AssistantUiController::Get()->CloseUi(
      shown ? AssistantExitPoint::kLauncherOpen
            : AssistantExitPoint::kLauncherClose);

  // Animations can be reversed (e.g. in a drag). Let's ensure the target
  // visibility is correct first.
  OnVisibilityChanged(shown, display_id);
}

void AppListControllerImpl::OnHomeLauncherPositionChanged(int percent_shown,
                                                          int64_t display_id) {
  const bool mostly_shown = percent_shown >= 50;
  home_launcher_transition_state_ =
      mostly_shown ? HomeLauncherTransitionState::kMostlyShown
                   : HomeLauncherTransitionState::kMostlyHidden;
  OnVisibilityWillChange(mostly_shown, display_id);
}

aura::Window* AppListControllerImpl::GetHomeScreenWindow() const {
  return fullscreen_presenter_->GetWindow();
}

void AppListControllerImpl::UpdateScaleAndOpacityForHomeLauncher(
    float scale,
    float opacity,
    std::optional<HomeLauncherAnimationInfo> animation_info,
    UpdateAnimationSettingsCallback callback) {
  DCHECK(!animation_info.has_value() || !callback.is_null());

  fullscreen_presenter_->UpdateScaleAndOpacityForHomeLauncher(
      scale, opacity,
      GetTransitionFromMetricsAnimationInfo(std::move(animation_info)),
      std::move(callback));
}

void AppListControllerImpl::Back() {
  fullscreen_presenter_->GetView()->Back();
}

void AppListControllerImpl::SetKeyboardTraversalMode(bool engaged) {
  if (keyboard_traversal_engaged_ == engaged)
    return;

  keyboard_traversal_engaged_ = engaged;
  AssistantUiController::Get()->SetKeyboardTraversalMode(engaged);

  // No need to schedule paint for bubble presenter.
  if (bubble_presenter_->IsShowing())
    return;

  AppListView* app_list_view = fullscreen_presenter_->GetView();
  // May be null in tests of bubble presenter.
  if (!app_list_view)
    return;
  views::View* focused_view =
      app_list_view->GetFocusManager()->GetFocusedView();

  if (!focused_view)
    return;

  // When the search box has focus, it is actually the textfield that has focus.
  // As such, the |SearchBoxView| must be told to repaint directly.
  if (focused_view == app_list_view->search_box_view()->search_box()) {
    app_list_view->search_box_view()->UpdateSearchBoxFocusPaint();
  } else if (AppListToastView::IsToastButton(focused_view)) {
    // Toast button can become focused after app list sorting, so make sure the
    // focus ring appears correctly when updating `keyboard_traversal_engaged_`.
    focused_view->SchedulePaint();
  } else {
    // Ensure that when an app list item's focus ring is triggered by key
    // events, the item is selected.
    // TODO(https://crbug.com/1262236): class name comparision and static cast
    // should be avoided in the production code. Find a better way to guarantee
    // the item's selection status.
    if (std::string_view(focused_view->GetClassName()) ==
        std::string_view(AppListItemView::kViewClassName)) {
      static_cast<AppListItemView*>(focused_view)->EnsureSelected();
    }

    focused_view->SchedulePaint();
  }
}

bool AppListControllerImpl::IsShowingEmbeddedAssistantUI() const {
  return bubble_presenter_->IsShowingEmbeddedAssistantUI() ||
         fullscreen_presenter_->IsShowingEmbeddedAssistantUI();
}

void AppListControllerImpl::SetStateTransitionAnimationCallbackForTesting(
    StateTransitionAnimationCallback callback) {
  state_transition_animation_callback_ = std::move(callback);
}

void AppListControllerImpl::SetHomeLauncherAnimationCallbackForTesting(
    HomeLauncherAnimationCallback callback) {
  home_launcher_animation_callback_ = std::move(callback);
}

void AppListControllerImpl::RecordShelfAppLaunched() {
  RecordAppListAppLaunched(
      AppListLaunchedFrom::kLaunchedFromShelf,
      recorded_app_list_view_state_.value_or(GetAppListViewState()),
      IsInTabletMode(), recorded_app_list_visibility_.value_or(last_visible_));
  recorded_app_list_view_state_ = std::nullopt;
  recorded_app_list_visibility_ = std::nullopt;
}

////////////////////////////////////////////////////////////////////////////////
// Methods of |client_|:

void AppListControllerImpl::StartAssistant(
    assistant::AssistantEntryPoint entry_point) {
  AssistantUiController::Get()->ShowUi(entry_point);
  UpdateSearchBoxUiVisibilities();
}

void AppListControllerImpl::EndAssistant(
    assistant::AssistantExitPoint exit_point) {
  AssistantUiController::Get()->CloseUi(exit_point);
}

std::vector<AppListSearchControlCategory>
AppListControllerImpl::GetToggleableCategories() const {
  if (client_) {
    return client_->GetToggleableCategories();
  }
  return std::vector<AppListSearchControlCategory>();
}

void AppListControllerImpl::StartSearch(const std::u16string& raw_query) {
  if (client_) {
    std::u16string query;
    base::TrimWhitespace(raw_query, base::TRIM_ALL, &query);
    client_->StartSearch(query);
  }
}

void AppListControllerImpl::StartZeroStateSearch(base::OnceClosure callback,
                                                 base::TimeDelta timeout) {
  if (client_)
    client_->StartZeroStateSearch(std::move(callback), timeout);
}

void AppListControllerImpl::OpenSearchResult(const std::string& result_id,
                                             int event_flags,
                                             AppListLaunchedFrom launched_from,
                                             AppListLaunchType launch_type,
                                             int suggestion_index,
                                             bool launch_as_default) {
  SearchModel* search_model = GetSearchModel();
  SearchResult* result = search_model->FindSearchResult(result_id);
  if (!result)
    return;

  if (launch_type == AppListLaunchType::kAppSearchResult) {
    switch (launched_from) {
      case AppListLaunchedFrom::kLaunchedFromSearchBox:
      case AppListLaunchedFrom::kLaunchedFromRecentApps:
        RecordAppLaunched(launched_from);
        break;
      case AppListLaunchedFrom::kLaunchedFromGrid:
      case AppListLaunchedFrom::kLaunchedFromShelf:
      case AppListLaunchedFrom::kLaunchedFromContinueTask:
      case AppListLaunchedFrom::kLaunchedFromQuickAppAccess:
      case AppListLaunchedFrom::kLaunchedFromAppsCollections:
      case AppListLaunchedFrom::kLaunchedFromDiscoveryChip:
        break;
      case AppListLaunchedFrom::DEPRECATED_kLaunchedFromSuggestionChip:
        NOTREACHED();
    }
  }

  const bool is_tablet_mode = IsInTabletMode();
  switch (launched_from) {
    case AppListLaunchedFrom::kLaunchedFromSearchBox:
      switch (launch_type) {
        case AppListLaunchType::kSearchResult:
          RecordLauncherWorkflowMetrics(AppListUserAction::kOpenSearchResult,
                                        is_tablet_mode, last_show_timestamp_);
          break;
        case AppListLaunchType::kAppSearchResult:
          RecordLauncherWorkflowMetrics(AppListUserAction::kOpenAppSearchResult,
                                        is_tablet_mode, last_show_timestamp_);
          break;
        case AppListLaunchType::kApp:
          NOTREACHED();
      }
      break;
    case AppListLaunchedFrom::kLaunchedFromContinueTask:
      RecordLauncherWorkflowMetrics(AppListUserAction::kOpenContinueSectionTask,
                                    is_tablet_mode, last_show_timestamp_);
      break;
    case AppListLaunchedFrom::kLaunchedFromGrid:
    case AppListLaunchedFrom::kLaunchedFromRecentApps:
    case AppListLaunchedFrom::kLaunchedFromShelf:
    case AppListLaunchedFrom::DEPRECATED_kLaunchedFromSuggestionChip:
    case AppListLaunchedFrom::kLaunchedFromQuickAppAccess:
    case AppListLaunchedFrom::kLaunchedFromAppsCollections:
    case AppListLaunchedFrom::kLaunchedFromDiscoveryChip:
      NOTREACHED();
  }

  base::RecordAction(base::UserMetricsAction("AppList_OpenSearchResult"));

  if (client_) {
    client_->OpenSearchResult(profile_id_, result_id, event_flags,
                              launched_from, launch_type, suggestion_index,
                              launch_as_default);
  }

  ResetHomeLauncherIfShown();
}

void AppListControllerImpl::InvokeSearchResultAction(
    const std::string& result_id,
    SearchResultActionType action) {
  if (client_)
    client_->InvokeSearchResultAction(result_id, action);
}

void AppListControllerImpl::ViewShown(int64_t display_id) {
  UpdateSearchBoxUiVisibilities();

  // Note that IsHomeScreenVisible() might still return false at this point, as
  // the home screen visibility takes into account whether the app list view is
  // obscured by an app window, or overview UI. This method gets called when the
  // app list view widget visibility changes (regardless of whether anything is
  // stacked above the home screen).
  aura::Window* window = GetHomeScreenWindow();
  split_view_observation_.Observe(SplitViewController::Get(window));
  UpdateHomeScreenVisibility();

  // Ensure search box starts fresh with no ring each time it opens.
  keyboard_traversal_engaged_ = false;
}

bool AppListControllerImpl::AppListTargetVisibility() const {
  return last_target_visible_;
}

void AppListControllerImpl::ViewClosing() {
  split_view_observation_.Reset();
}

void AppListControllerImpl::ActivateItem(const std::string& id,
                                         int event_flags,
                                         AppListLaunchedFrom launched_from,
                                         bool is_app_above_the_fold) {
  RecordAppLaunched(launched_from);

  const bool is_tablet_mode = IsInTabletMode();
  switch (launched_from) {
    case AppListLaunchedFrom::kLaunchedFromGrid:
      RecordLauncherWorkflowMetrics(AppListUserAction::kAppLaunchFromAppsGrid,
                                    is_tablet_mode, last_show_timestamp_);
      break;
    case AppListLaunchedFrom::kLaunchedFromRecentApps:
      RecordLauncherWorkflowMetrics(AppListUserAction::kAppLaunchFromRecentApps,
                                    is_tablet_mode, last_show_timestamp_);
      break;
    case AppListLaunchedFrom::kLaunchedFromQuickAppAccess:
    // Metrics for quick app launch already recorded at RecordApplaunched().
    case AppListLaunchedFrom::kLaunchedFromAppsCollections:
    // Metrics for apps collections launch recorded by the
    // AppListViewDelegate.
    case AppListLaunchedFrom::kLaunchedFromDiscoveryChip:
      // Metrics for discovery chip already recorded at RecordApplaunched().
      break;
    case AppListLaunchedFrom::kLaunchedFromContinueTask:
    case AppListLaunchedFrom::kLaunchedFromSearchBox:
    case AppListLaunchedFrom::kLaunchedFromShelf:
    case AppListLaunchedFrom::DEPRECATED_kLaunchedFromSuggestionChip:
      NOTREACHED();
  }

  if (client_)
    client_->ActivateItem(profile_id_, id, event_flags, launched_from,
                          is_app_above_the_fold);

  ResetHomeLauncherIfShown();
}

void AppListControllerImpl::GetContextMenuModel(
    const std::string& id,
    AppListItemContext item_context,
    GetContextMenuModelCallback callback) {
  if (client_)
    client_->GetContextMenuModel(profile_id_, id, item_context,
                                 std::move(callback));
}

void AppListControllerImpl::ShowWallpaperContextMenu(
    const gfx::Point& onscreen_location,
    ui::MenuSourceType source_type) {
  Shell::Get()->ShowContextMenu(onscreen_location, source_type);
}

bool AppListControllerImpl::KeyboardTraversalEngaged() {
  return keyboard_traversal_engaged_;
}

bool AppListControllerImpl::CanProcessEventsOnApplistViews() {
  // Do not allow processing events during overview or while overview is
  // finished but still animating out.
  OverviewController* overview_controller = Shell::Get()->overview_controller();
  if (overview_controller->InOverviewSession() ||
      overview_controller->IsCompletingShutdownAnimations()) {
    return false;
  }

  return true;
}

bool AppListControllerImpl::ShouldDismissImmediately() {
  return should_dismiss_immediately_;
}

AssistantViewDelegate* AppListControllerImpl::GetAssistantViewDelegate() {
  return Shell::Get()->assistant_controller()->view_delegate();
}

void AppListControllerImpl::OnSearchResultVisibilityChanged(
    const std::string& id,
    bool visibility) {
  if (client_)
    client_->OnSearchResultVisibilityChanged(id, visibility);
}

bool AppListControllerImpl::IsAssistantAllowedAndEnabled() const {
  if (!Shell::Get()->assistant_controller()->IsAssistantReady())
    return false;

  auto* state = AssistantState::Get();
  return state->settings_enabled().value_or(false) &&
         state->allowed_state() == assistant::AssistantAllowedState::ALLOWED &&
         state->assistant_status() != assistant::AssistantStatus::NOT_READY;
}

void AppListControllerImpl::OnStateTransitionAnimationCompleted(
    AppListViewState state,
    bool was_animation_interrupted) {
  if (!was_animation_interrupted &&
      !state_transition_animation_callback_.is_null()) {
    state_transition_animation_callback_.Run(state);
  }

  MaybeCloseAssistant();
}

void AppListControllerImpl::MaybeCloseAssistant() {
  if (close_assistant_ui_runner_)
    close_assistant_ui_runner_.RunAndReset();
}

AppListViewState AppListControllerImpl::GetAppListViewState() const {
  return app_list_view_state_;
}

void AppListControllerImpl::OnViewStateChanged(AppListViewState state) {
  DVLOG(1) << __PRETTY_FUNCTION__ << " " << state;
  app_list_view_state_ = state;

  for (auto& observer : observers_)
    observer.OnViewStateChanged(state);

  if (state == AppListViewState::kClosed)
    ScheduleCloseAssistant();
}

void AppListControllerImpl::ScheduleCloseAssistant() {
  DVLOG(1) << __PRETTY_FUNCTION__;
  // Close the Assistant in asynchronous way if the app list is going to be
  // closed while the Assistant is visible. If the app list close animation is
  // not reversed, `close_assistant_ui_runner_` runs at the end of the animation
  // to actually close the Assistant.
  const bool is_assistant_ui_visible =
      (AssistantUiController::Get()->GetModel()->visibility() ==
       AssistantVisibility::kVisible);
  if (is_assistant_ui_visible) {
    std::optional<base::ScopedClosureRunner> runner =
        AssistantUiController::Get()->CloseUi(
            AssistantExitPoint::kLauncherClose);
    DCHECK(runner);
    DCHECK(!close_assistant_ui_runner_);
    close_assistant_ui_runner_.ReplaceClosure(runner->Release());
  }
}

void AppListControllerImpl::LoadIcon(const std::string& app_id) {
  if (client_)
    client_->LoadIcon(profile_id_, app_id);
}

bool AppListControllerImpl::HasValidProfile() const {
  return profile_id_ != kAppListInvalidProfileID;
}

bool AppListControllerImpl::ShouldHideContinueSection() const {
  PrefService* prefs = GetLastActiveUserPrefService();
  return prefs->GetBoolean(prefs::kLauncherContinueSectionHidden);
}

void AppListControllerImpl::SetHideContinueSection(bool hide) {
  PrefService* prefs = GetLastActiveUserPrefService();
  bool is_hidden = prefs->GetBoolean(prefs::kLauncherContinueSectionHidden);
  if (hide == is_hidden)
    return;
  prefs->SetBoolean(prefs::kLauncherContinueSectionHidden, hide);
  fullscreen_presenter_->UpdateContinueSectionVisibility();
  bubble_presenter_->UpdateContinueSectionVisibility();
}

bool AppListControllerImpl::IsCategoryEnabled(
    AppListSearchControlCategory category) {
  PrefService* prefs =
      Shell::Get()->session_controller()->GetLastActiveUserPrefService();
  return prefs->GetDict(prefs::kLauncherSearchCategoryControlStatus)
      .FindBool(GetAppListControlCategoryName(category))
      .value_or(true);
}

void AppListControllerImpl::SetCategoryEnabled(
    AppListSearchControlCategory category,
    bool enabled) {
  PrefService* prefs =
      Shell::Get()->session_controller()->GetLastActiveUserPrefService();
  ScopedDictPrefUpdate pref_update(prefs,
                                   prefs::kLauncherSearchCategoryControlStatus);
  pref_update->Set(GetAppListControlCategoryName(category), enabled);
}

void AppListControllerImpl::RecordAppsDefaultVisibility(
    const std::vector<std::string>& apps_above_the_fold,
    const std::vector<std::string>& apps_below_the_fold,
    bool is_apps_collections_page) {
  if (client_) {
    client_->RecordAppsDefaultVisibility(
        apps_above_the_fold, apps_below_the_fold, is_apps_collections_page);
  }
}

void AppListControllerImpl::GetAppLaunchedMetricParams(
    AppLaunchedMetricParams* metric_params) {
  metric_params->app_list_view_state = GetAppListViewState();
  metric_params->is_tablet_mode = IsInTabletMode();
  metric_params->app_list_shown = last_visible_;
  metric_params->launcher_show_timestamp = last_show_timestamp_;
}

gfx::Rect AppListControllerImpl::SnapBoundsToDisplayEdge(
    const gfx::Rect& bounds) {
  AppListView* app_list_view = fullscreen_presenter_->GetView();
  DCHECK(app_list_view && app_list_view->GetWidget());
  aura::Window* window = app_list_view->GetWidget()->GetNativeView();
  return screen_util::SnapBoundsToDisplayEdge(bounds, window);
}

AppListState AppListControllerImpl::GetCurrentAppListPage() const {
  return app_list_page_;
}

void AppListControllerImpl::OnAppListPageChanged(AppListState page) {
  const AppListState old_page = app_list_page_;
  if (old_page == page)
    return;

  app_list_page_ = page;

  if (!fullscreen_presenter_)
    return;

  UpdateFullscreenLauncherContainer();

  if (page == AppListState::kStateEmbeddedAssistant) {
    // ShowUi() will be no-op if the Assistant UI is already visible.
    AssistantUiController::Get()->ShowUi(AssistantEntryPoint::kUnspecified);
    return;
  }

  if (old_page == AppListState::kStateEmbeddedAssistant) {
    // CloseUi() will be no-op if the Assistant UI is already closed.
    AssistantUiController::Get()->CloseUi(AssistantExitPoint::kBackInLauncher);
  }
}

int AppListControllerImpl::GetShelfSize() {
  return ShelfConfig::Get()->GetSystemShelfSizeInTabletMode();
}

int AppListControllerImpl::GetSystemShelfInsetsInTabletMode() {
  return ShelfConfig::Get()->GetTabletModeShelfInsetsAndRecordUMA();
}

bool AppListControllerImpl::IsInTabletMode() const {
  return display::Screen::GetScreen()->InTabletMode();
}

void AppListControllerImpl::RecordAppLaunched(
    AppListLaunchedFrom launched_from) {
  RecordAppListAppLaunched(launched_from, GetAppListViewState(),
                           IsInTabletMode(), last_visible_);
}

void AppListControllerImpl::AddObserver(AppListControllerObserver* observer) {
  observers_.AddObserver(observer);
}

void AppListControllerImpl::RemoveObserver(
    AppListControllerObserver* observer) {
  observers_.RemoveObserver(observer);
}

void AppListControllerImpl::OnVisibilityChanged(bool visible,
                                                int64_t display_id) {
  // In the Kiosk session we should never show the app list.
  CHECK(!visible || !IsKioskSession());

  if (client_) {
    client_->RecalculateWouldTriggerLauncherSearchIph();
  }

  DVLOG(1) << __PRETTY_FUNCTION__ << " visible " << visible << " display_id "
           << display_id;
  // Focus and app visibility changes while finishing home launcher state
  // animation may cause OnVisibilityChanged() to be called before the home
  // launcher state transition finished - delay the visibility change until
  // the home launcher stops animating, so observers do not miss the animation
  // state update.
  if (home_launcher_transition_state_ !=
      HomeLauncherTransitionState::kFinished) {
    OnVisibilityWillChange(visible, display_id);
    return;
  }

  bool real_visibility = visible;
  // HomeLauncher is only visible when no other app windows are visible,
  // unless we are in the process of animating to (or dragging) the home
  // launcher.
  if (IsInTabletMode()) {
    UpdateTrackedAppWindow();

    if (tracked_app_window_)
      real_visibility = false;

    // When transitioning to/from overview, ensure the AppList window is not in
    // the process of being hidden.
    aura::Window* app_list_window = GetWindow();
    real_visibility &= app_list_window && app_list_window->TargetVisibility();
  }

  OnVisibilityWillChange(real_visibility, display_id);

  // Skip adjacent same changes.
  if (last_visible_ == real_visibility &&
      last_visible_display_id_ == display_id) {
    return;
  }

  last_visible_display_id_ = display_id;

  if (visible)
    last_show_timestamp_ = base::TimeTicks::Now();

  AppListView* const app_list_view = fullscreen_presenter_->GetView();
  if (app_list_view) {
    app_list_view->UpdatePageResetTimer(real_visibility);

    if (!real_visibility) {
      app_list_view->search_box_view()->ClearSearchAndDeactivateSearchBox();
      // Reset the app list contents state, so the app list is in initial state
      // when the app list visibility changes again.
      app_list_view->app_list_main_view()->contents_view()->ResetForShow();
    }
  }

  // Notify chrome of visibility changes.
  if (last_visible_ != real_visibility) {
    // When showing the launcher with the virtual keyboard enabled, one
    // feature called "transient blur" (which means that if focus was lost but
    // regained a few seconds later, we would show the virtual keyboard again)
    // may show the virtual keyboard, which is not what we want. So hide the
    // virtual keyboard explicitly when the launcher shows.
    if (real_visibility)
      keyboard::KeyboardUIController::Get()->HideKeyboardExplicitlyBySystem();

    if (client_)
      client_->OnAppListVisibilityChanged(real_visibility);

    last_visible_ = real_visibility;

    // Updates AppsContainerView in `fullscreen_presenter_`.
    if (app_list_view)
      app_list_view->OnAppListVisibilityChanged(real_visibility);

    for (auto& observer : observers_)
      observer.OnAppListVisibilityChanged(real_visibility, display_id);

    // Record whether the continue section is hidden by the user.
    if (real_visibility)
      RecordHideContinueSectionMetric();

    if (!home_launcher_animation_callback_.is_null())
      home_launcher_animation_callback_.Run(real_visibility);
  }
}

void AppListControllerImpl::OnWindowVisibilityChanging(aura::Window* window,
                                                       bool visible) {
  if (visible || window != tracked_app_window_)
    return;

  UpdateTrackedAppWindow();

  if (!tracked_app_window_ && ShouldHomeLauncherBeVisible())
    OnVisibilityChanged(true, last_visible_display_id_);
}

void AppListControllerImpl::OnWindowDestroyed(aura::Window* window) {
  if (window != tracked_app_window_)
    return;

  tracked_app_window_ = nullptr;
}

void AppListControllerImpl::OnVisibilityWillChange(bool visible,
                                                   int64_t display_id) {
  // In the Kiosk session we should never show the app list.
  CHECK(!visible || !IsKioskSession());

  bool real_target_visibility = visible;
  // HomeLauncher is only visible when no other app windows are visible,
  // unless we are in the process of animating to (or dragging) the home
  // launcher.
  if (IsInTabletMode() && home_launcher_transition_state_ ==
                              HomeLauncherTransitionState::kFinished) {
    real_target_visibility &= !GetTopVisibleWindow();
  }

  // Skip adjacent same changes.
  if (last_target_visible_ == real_target_visibility &&
      last_target_visible_display_id_ == display_id) {
    return;
  }

  // Notify chrome of target visibility changes.
  if (last_target_visible_ != real_target_visibility) {
    last_target_visible_ = real_target_visibility;
    last_target_visible_display_id_ = display_id;

    if (real_target_visibility && fullscreen_presenter_->GetView())
      fullscreen_presenter_->SetViewVisibility(true);

    if (client_)
      client_->OnAppListVisibilityWillChange(real_target_visibility);

    for (auto& observer : observers_) {
      observer.OnAppListVisibilityWillChange(real_target_visibility,
                                             display_id);
    }

    // The virtual keyboard should be hidden before the bubble launcher
    // calculating the work area.
    if (real_target_visibility) {
      keyboard::KeyboardUIController::Get()->HideKeyboardExplicitlyBySystem();
    }
  }
}

////////////////////////////////////////////////////////////////////////////////
// Private used only:

AppListModel* AppListControllerImpl::GetModel() {
  return model_provider_->model();
}

SearchModel* AppListControllerImpl::GetSearchModel() {
  return model_provider_->search_model();
}

void AppListControllerImpl::UpdateSearchBoxUiVisibilities() {
  GetSearchModel()->search_box()->SetShowAssistantButton(
      IsAssistantAllowedAndEnabled());

  if (!client_) {
    return;
  }

  client_->RecalculateWouldTriggerLauncherSearchIph();
}

int64_t AppListControllerImpl::GetDisplayIdToShowAppListOn() {
  if (IsInTabletMode() && !Shell::Get()->display_manager()->IsInUnifiedMode()) {
    return display::HasInternalDisplay()
               ? display::Display::InternalDisplayId()
               : display::Screen::GetScreen()->GetPrimaryDisplay().id();
  }

  return display::Screen::GetScreen()
      ->GetDisplayNearestWindow(Shell::GetRootWindowForNewWindows())
      .id();
}

void AppListControllerImpl::ResetHomeLauncherIfShown() {
  if (!IsInTabletMode() || !fullscreen_presenter_->IsVisibleDeprecated()) {
    return;
  }

  auto* const keyboard_controller = keyboard::KeyboardUIController::Get();
  if (keyboard_controller->IsKeyboardVisible())
    keyboard_controller->HideKeyboardByUser();
  fullscreen_presenter_->GetView()->CloseOpenedPage();

  // Refresh the suggestion chips with empty query.
  // TODO(crbug.com/40204937): Switch to client_->StartZeroStateSearch()?
  StartSearch(std::u16string());
}

void AppListControllerImpl::ShowHomeScreen(AppListShowSource show_source) {
  DCHECK(IsInTabletMode());

  if (!Shell::Get()->session_controller()->IsActiveUserSessionStarted() ||
      IsKioskSession())
    return;

  // App list is only considered shown for metrics if there are currently no
  // other visible windows shown over the app list after the tablet
  // transition.
  bool should_record_metrics = !GetTopVisibleWindow();

  Show(GetDisplayIdToShowAppListOn(), show_source, base::TimeTicks(),
       should_record_metrics);
  UpdateHomeScreenVisibility();

  aura::Window* window = GetHomeScreenWindow();
  if (window)
    Shelf::ForWindow(window)->MaybeUpdateShelfBackground();
  last_open_source_ = show_source;
}

void AppListControllerImpl::UpdateHomeScreenVisibility() {
  if (!IsInTabletMode()) {
    return;
  }

  aura::Window* window = GetHomeScreenWindow();
  if (!window)
    return;

  if (ShouldShowHomeScreen())
    window->Show();
  else
    window->Hide();
}

bool AppListControllerImpl::ShouldShowHomeScreen() const {
  if (IsKioskSession() || in_window_dragging_ || in_wallpaper_preview_)
    return false;

  aura::Window* window = GetHomeScreenWindow();
  if (!window)
    return false;

  if (!IsInTabletMode()) {
    return false;
  }
  if (Shell::Get()->overview_controller()->InOverviewSession()) {
    return false;
  }

  return !SplitViewController::Get(window)->InSplitViewMode();
}

void AppListControllerImpl::UpdateForOverviewModeChange(bool show_home_launcher,
                                                        bool animate) {
  // Force the home view into the expected initial state without animation,
  // except when transitioning out from home launcher. Gesture handling for
  // the gesture to move to overview can update the scale before triggering
  // transition to overview - undoing these changes here would make the UI
  // jump during the transition.
  if (animate && show_home_launcher) {
    UpdateScaleAndOpacityForHomeLauncher(kOverviewFadeAnimationScale,
                                         /*opacity=*/0.0f,
                                         /*animation_info=*/std::nullopt,
                                         /*callback=*/base::NullCallback());
  }

  // Hide all transient child windows in the app list (e.g. uninstall dialog)
  // before starting the overview mode transition, and restore them when
  // reshowing the app list.
  aura::Window* app_list_window =
      Shell::Get()->app_list_controller()->GetHomeScreenWindow();
  if (app_list_window) {
    for (aura::Window* child : wm::GetTransientChildren(app_list_window)) {
      if (show_home_launcher)
        child->Show();
      else
        child->Hide();
    }
  }

  std::optional<HomeLauncherAnimationInfo> animation_info =
      animate ? std::make_optional<HomeLauncherAnimationInfo>(
                    HomeLauncherAnimationTrigger::kOverviewModeFade,
                    show_home_launcher)
              : std::nullopt;
  UpdateAnimationSettingsCallback animation_settings_updater =
      animate ? base::BindRepeating(&UpdateOverviewSettings,
                                    kOverviewFadeAnimationDuration)
              : base::NullCallback();
  const float target_scale =
      show_home_launcher ? 1.0f : kOverviewFadeAnimationScale;
  const float target_opacity = show_home_launcher ? 1.0f : 0.0f;
  UpdateScaleAndOpacityForHomeLauncher(target_scale, target_opacity,
                                       std::move(animation_info),
                                       animation_settings_updater);
}

void AppListControllerImpl::UpdateFullscreenLauncherContainer(
    std::optional<int64_t> display_id) {
  aura::Window* window = fullscreen_presenter_->GetWindow();
  if (!window)
    return;

  aura::Window* parent_window =
      GetFullscreenLauncherContainerForDisplayId(display_id);
  if (parent_window && !parent_window->Contains(window)) {
    parent_window->AddChild(window);
    // Release focus if the launcher is moving behind apps, and there is app
    // window showing. Note that the app list can be shown behind apps in
    // tablet mode only.
    if (IsInTabletMode() && !ShouldHomeLauncherBeVisible()) {
      WindowState* const window_state = WindowState::Get(window);
      if (window_state->IsActive())
        window_state->Deactivate();
    }
  }
}

aura::Window* AppListControllerImpl::GetFullscreenLauncherContainerForDisplayId(
    std::optional<int64_t> display_id) {
  aura::Window* root_window = nullptr;
  if (display_id.has_value()) {
    root_window = Shell::GetRootWindowForDisplayId(display_id.value());
  } else if (fullscreen_presenter_->GetWindow()) {
    root_window = fullscreen_presenter_->GetWindow()->GetRootWindow();
  }

  return root_window
             ? root_window->GetChildById(GetFullscreenLauncherContainerId())
             : nullptr;
}

void AppListControllerImpl::Shutdown() {
  DCHECK(!is_shutdown_);
  is_shutdown_ = true;

  // Cancel any pending assistant UI close requests to avoid attempts to update
  // assistant UI state mid shutdown (possibly after assistant has started
  // shutting down).
  IgnoreResult(close_assistant_ui_runner_.Release());

  // Always shutdown the bubble presenter.
  bubble_presenter_->Shutdown();

  Shell* shell = Shell::Get();
  AssistantController::Get()->RemoveObserver(this);
  AssistantUiController::Get()->GetModel()->RemoveObserver(this);
  shell->display_manager()->RemoveDisplayManagerObserver(this);
  AssistantState::Get()->RemoveObserver(this);
  keyboard::KeyboardUIController::Get()->RemoveObserver(this);
  display::Screen::GetScreen()->RemoveObserver(this);
  shell->overview_controller()->RemoveObserver(this);
  shell->RemoveShellObserver(this);
  WallpaperController::Get()->RemoveObserver(this);
  shell->session_controller()->RemoveObserver(this);
  FeatureDiscoveryDurationReporter::GetInstance()->RemoveObserver(this);

  badge_controller_->Shutdown();
}

bool AppListControllerImpl::IsHomeScreenVisible() {
  return IsInTabletMode() && IsVisible();
}

void AppListControllerImpl::OnWindowDragStarted() {
  in_window_dragging_ = true;
  UpdateHomeScreenVisibility();

  // Dismiss Assistant if it's running when a window drag starts.
  if (fullscreen_presenter_->IsShowingEmbeddedAssistantUI())
    fullscreen_presenter_->ShowEmbeddedAssistantUI(false);
}

void AppListControllerImpl::OnWindowDragEnded(bool animate) {
  in_window_dragging_ = false;
  UpdateHomeScreenVisibility();
  if (ShouldShowHomeScreen())
    UpdateForOverviewModeChange(/*show_home_launcher=*/true, animate);
}

void AppListControllerImpl::UpdateTrackedAppWindow() {
  // Do not want to observe new windows or further update
  // |tracked_app_window_| once Shutdown() has been called.
  aura::Window* top_window = !is_shutdown_ ? GetTopVisibleWindow() : nullptr;
  if (tracked_app_window_ == top_window)
    return;

  if (tracked_app_window_)
    tracked_app_window_->RemoveObserver(this);
  tracked_app_window_ = top_window;
  if (tracked_app_window_)
    tracked_app_window_->AddObserver(this);
}

void AppListControllerImpl::RecordAppListState() {
  recorded_app_list_view_state_ = GetAppListViewState();
  recorded_app_list_visibility_ = last_visible_;
}

void AppListControllerImpl::StartTrackingAnimationSmoothness(
    int64_t display_id) {
  auto* root_window = Shell::GetRootWindowForDisplayId(display_id);
  auto* compositor = root_window->layer()->GetCompositor();
  smoothness_tracker_ = compositor->RequestNewThroughputTracker();
  smoothness_tracker_->Start(
      metrics_util::ForSmoothnessV3(base::BindRepeating([](int smoothness) {
        UMA_HISTOGRAM_PERCENTAGE(kHomescreenAnimationHistogram, smoothness);
      })));
}

void AppListControllerImpl::RecordAnimationSmoothness() {
  if (!smoothness_tracker_)
    return;
  smoothness_tracker_->Stop();
  smoothness_tracker_.reset();
}

void AppListControllerImpl::OnGoHomeWindowAnimationsEnded(int64_t display_id) {
  RecordAnimationSmoothness();
  OnHomeLauncherAnimationComplete(/*shown=*/true, display_id);
}

void AppListControllerImpl::OnReporterActivated() {
  FeatureDiscoveryDurationReporter::GetInstance()->MaybeActivateObservation(
      feature_discovery::TrackableFeature::
          kAppListReorderAfterSessionActivation);
}

int AppListControllerImpl::GetFullscreenLauncherContainerId() const {
  const bool should_show_behind_apps =
      app_list_page_ != AppListState::kStateEmbeddedAssistant;
  return should_show_behind_apps ? kShellWindowId_HomeScreenContainer
                                 : kShellWindowId_AppListContainer;
}

int AppListControllerImpl::GetPreferredBubbleWidth(
    aura::Window* root_window) const {
  DCHECK(bubble_presenter_);

  return bubble_presenter_->GetPreferredBubbleWidth(root_window);
}

bool AppListControllerImpl::SetHomeButtonQuickApp(const std::string& app_id) {
  if (!features::IsHomeButtonQuickAppAccessEnabled()) {
    return false;
  }
  return model_provider_->quick_app_access_model()->SetQuickApp(app_id);
}

}  // namespace ash