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

#include "ash/app_list/app_list_controller_impl.h"

#include <utility>
#include <vector>

#include "ash/app_list/app_list_metrics.h"
#include "ash/app_list/app_list_presenter_delegate_impl.h"
#include "ash/app_list/model/app_list_folder_item.h"
#include "ash/app_list/model/app_list_item.h"
#include "ash/app_list/views/app_list_main_view.h"
#include "ash/app_list/views/app_list_view.h"
#include "ash/app_list/views/apps_container_view.h"
#include "ash/app_list/views/apps_grid_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/ui/assistant_ui_constants.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/home_screen/home_screen_controller.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_config.h"
#include "ash/public/cpp/app_list/app_list_controller_observer.h"
#include "ash/public/cpp/app_list/app_list_features.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/ash_features.h"
#include "ash/public/cpp/ash_pref_names.h"
#include "ash/public/cpp/assistant/controller/assistant_controller.h"
#include "ash/public/cpp/assistant/controller/assistant_ui_controller.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/window_properties.h"
#include "ash/root_window_controller.h"
#include "ash/screen_util.h"
#include "ash/session/session_controller_impl.h"
#include "ash/shell.h"
#include "ash/wallpaper/wallpaper_controller_impl.h"
#include "ash/wm/mru_window_tracker.h"
#include "ash/wm/overview/overview_controller.h"
#include "ash/wm/splitview/split_view_controller.h"
#include "ash/wm/tablet_mode/tablet_mode_controller.h"
#include "ash/wm/window_state.h"
#include "base/callback_helpers.h"
#include "base/logging.h"
#include "base/metrics/histogram_macros.h"
#include "base/metrics/user_metrics.h"
#include "base/strings/utf_string_conversions.h"
#include "components/pref_registry/pref_registry_syncable.h"
#include "components/prefs/pref_change_registrar.h"
#include "components/prefs/pref_registry_simple.h"
#include "components/prefs/pref_service.h"
#include "components/services/app_service/public/cpp/app_registry_cache_wrapper.h"
#include "extensions/common/constants.h"
#include "ui/base/ui_base_features.h"
#include "ui/display/manager/display_manager.h"
#include "ui/display/screen.h"
#include "ui/message_center/message_center.h"
#include "ui/views/controls/textfield/textfield.h"
#include "ui/wm/core/coordinate_conversion.h"
#include "ui/wm/public/activation_client.h"

namespace ash {

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

namespace {

bool IsTabletMode() {
  return Shell::Get()->tablet_mode_controller()->InTabletMode();
}

TabletModeAnimationTransition CalculateAnimationTransitionForMetrics(
    HomeScreenDelegate::AnimationTrigger trigger,
    bool launcher_should_show) {
  switch (trigger) {
    case HomeScreenDelegate::AnimationTrigger::kHideForWindow:
      return TabletModeAnimationTransition::kHideHomeLauncherForWindow;
    case HomeScreenDelegate::AnimationTrigger::kLauncherButton:
      return TabletModeAnimationTransition::kHomeButtonShow;
    case HomeScreenDelegate::AnimationTrigger::kDragRelease:
      return launcher_should_show
                 ? TabletModeAnimationTransition::kDragReleaseShow
                 : TabletModeAnimationTransition::kDragReleaseHide;
    case HomeScreenDelegate::AnimationTrigger::kOverviewModeFade:
      return launcher_should_show
                 ? TabletModeAnimationTransition::kFadeOutOverview
                 : TabletModeAnimationTransition::kFadeInOverview;
  }
}

int GetSuggestedContentInfoShownCount() {
  PrefService* prefs =
      Shell::Get()->session_controller()->GetLastActiveUserPrefService();
  return prefs->GetInteger(prefs::kSuggestedContentInfoShownInLauncher);
}

void SetSuggestedContentInfoShownCount(int count) {
  PrefService* prefs =
      Shell::Get()->session_controller()->GetLastActiveUserPrefService();
  prefs->SetInteger(prefs::kSuggestedContentInfoShownInLauncher, count);
}

bool IsSuggestedContentInfoDismissed() {
  PrefService* prefs =
      Shell::Get()->session_controller()->GetLastActiveUserPrefService();
  return prefs->GetBoolean(prefs::kSuggestedContentInfoDismissedInLauncher);
}

void SetSuggestedContentInfoDismissed() {
  PrefService* prefs =
      Shell::Get()->session_controller()->GetLastActiveUserPrefService();
  prefs->SetBoolean(prefs::kSuggestedContentInfoDismissedInLauncher, true);
}

bool IsSuggestedContentEnabled() {
  PrefService* prefs =
      Shell::Get()->session_controller()->GetLastActiveUserPrefService();
  return prefs->GetBoolean(chromeos::prefs::kSuggestedContentEnabled);
}

int GetOffset(int offset, bool from_touchpad) {
  PrefService* prefs =
      Shell::Get()->session_controller()->GetLastActiveUserPrefService();
  if (from_touchpad)
    return prefs->GetBoolean(prefs::kNaturalScroll) ? -offset : offset;
  return prefs->GetBoolean(prefs::kMouseReverseScroll) ? -offset : offset;
}

// 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<aura::Window*> window_list =
      Shell::Get()->mru_window_tracker()->BuildWindowListIgnoreModal(
          DesksMruType::kActiveDesk);
  for (auto* window : window_list) {
    if (window->TargetVisibility() && !WindowState::Get(window)->IsMinimized())
      return window;
  }
  return nullptr;
}

void LogAppListShowSource(AppListShowSource show_source) {
  UMA_HISTOGRAM_ENUMERATION(kAppListToggleMethodHistogram, show_source);
}

base::Optional<TabletModeAnimationTransition>
GetTransitionFromMetricsAnimationInfo(
    base::Optional<HomeScreenDelegate::AnimationInfo> animation_info) {
  if (!animation_info.has_value())
    return base::nullopt;

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

}  // namespace

AppListControllerImpl::AppListControllerImpl()
    : model_(std::make_unique<AppListModel>()),
      color_provider_(AppListColorProviderImpl()),
      presenter_(std::make_unique<AppListPresenterDelegateImpl>(this)),
      is_notification_indicator_enabled_(
          ::features::IsNotificationIndicatorEnabled()) {
  model_->AddObserver(this);
  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();
  shell->tablet_mode_controller()->AddObserver(this);
  shell->wallpaper_controller()->AddObserver(this);
  shell->AddShellObserver(this);
  shell->overview_controller()->AddObserver(this);
  keyboard::KeyboardUIController::Get()->AddObserver(this);
  AssistantState::Get()->AddObserver(this);
  shell->window_tree_host_manager()->AddObserver(this);
  shell->mru_window_tracker()->AddObserver(this);
  AssistantController::Get()->AddObserver(this);
  AssistantUiController::Get()->GetModel()->AddObserver(this);
  message_center::MessageCenter::Get()->AddObserver(this);
}

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

  // 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();
}

// static
void AppListControllerImpl::RegisterProfilePrefs(PrefRegistrySimple* registry) {
  registry->RegisterIntegerPref(
      prefs::kSuggestedContentInfoShownInLauncher, 0,
      user_prefs::PrefRegistrySyncable::SYNCABLE_OS_PREF);
  registry->RegisterBooleanPref(
      prefs::kSuggestedContentInfoDismissedInLauncher, false,
      user_prefs::PrefRegistrySyncable::SYNCABLE_OS_PREF);
}

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

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

AppListModel* AppListControllerImpl::GetModel() {
  return model_.get();
}

SearchModel* AppListControllerImpl::GetSearchModel() {
  return &search_model_;
}

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

void AppListControllerImpl::AddItem(
    std::unique_ptr<AppListItemMetadata> item_data) {
  const std::string folder_id = item_data->folder_id;
  if (folder_id.empty())
    model_->AddItem(CreateAppListItem(std::move(item_data)));
  else
    AddItemToFolder(std::move(item_data), folder_id);
}

void AppListControllerImpl::AddItemToFolder(
    std::unique_ptr<AppListItemMetadata> item_data,
    const std::string& folder_id) {
  // When we're setting a whole model of a profile, each item may have its
  // folder id set properly. However, |AppListModel::AddItemToFolder| requires
  // the item to add is not in the target folder yet, and sets its folder id
  // later. So we should clear the folder id here to avoid breaking checks.
  item_data->folder_id.clear();
  model_->AddItemToFolder(CreateAppListItem(std::move(item_data)), folder_id);
}

void AppListControllerImpl::RemoveItem(const std::string& id) {
  model_->DeleteItem(id);
}

void AppListControllerImpl::RemoveUninstalledItem(const std::string& id) {
  model_->DeleteUninstalledItem(id);
}

void AppListControllerImpl::MoveItemToFolder(const std::string& id,
                                             const std::string& folder_id) {
  AppListItem* item = model_->FindItem(id);
  model_->MoveItemToFolder(item, folder_id);
}

void AppListControllerImpl::SetStatus(AppListModelStatus status) {
  model_->SetStatus(status);
}

void AppListControllerImpl::SetSearchEngineIsGoogle(bool is_google) {
  search_model_.SetSearchEngineIsGoogle(is_google);
}

void AppListControllerImpl::UpdateSearchBox(const base::string16& text,
                                            bool initiated_by_user) {
  search_model_.search_box()->Update(text, initiated_by_user);
}

void AppListControllerImpl::PublishSearchResults(
    std::vector<std::unique_ptr<SearchResultMetadata>> results) {
  std::vector<std::unique_ptr<SearchResult>> new_results;
  for (auto& result_metadata : results) {
    std::unique_ptr<SearchResult> result = std::make_unique<SearchResult>();
    result->SetMetadata(std::move(result_metadata));
    new_results.push_back(std::move(result));
  }
  search_model_.PublishResults(std::move(new_results));
}

void AppListControllerImpl::SetItemMetadata(
    const std::string& id,
    std::unique_ptr<AppListItemMetadata> data) {
  AppListItem* item = model_->FindItem(id);
  if (!item)
    return;

  // data may not contain valid position or icon. Preserve it in this case.
  if (!data->position.IsValid())
    data->position = item->position();

  // Update the item's position and name based on the metadata.
  if (!data->position.Equals(item->position()))
    model_->SetItemPosition(item, data->position);

  if (data->short_name.empty()) {
    if (data->name != item->name()) {
      model_->SetItemName(item, data->name);
    }
  } else {
    if (data->name != item->name() || data->short_name != item->short_name()) {
      model_->SetItemNameAndShortName(item, data->name, data->short_name);
    }
  }

  // Folder icon is generated on ash side and chrome side passes a null
  // icon here. Skip it.
  if (data->icon.isNull())
    data->icon = item->GetIcon(AppListConfigType::kShared);

  item->SetMetadata(std::move(data));
}

void AppListControllerImpl::SetItemIcon(const std::string& id,
                                        const gfx::ImageSkia& icon) {
  AppListItem* item = model_->FindItem(id);
  if (item)
    item->SetIcon(AppListConfigType::kShared, icon);
}

void AppListControllerImpl::SetItemNotificationBadgeColor(const std::string& id,
                                                          const SkColor color) {
  AppListItem* item = model_->FindItem(id);
  if (item)
    item->SetNotificationBadgeColor(color);
}

void AppListControllerImpl::SetModelData(
    int profile_id,
    std::vector<std::unique_ptr<AppListItemMetadata>> apps,
    bool is_search_engine_google) {
  // Clear old model data.
  model_->DeleteAllItems();
  search_model_.DeleteAllResults();

  profile_id_ = profile_id;

  // Populate new models. First populate folders and then other items to avoid
  // automatically creating folder items in |AddItemToFolder|.
  for (auto& app : apps) {
    if (!app->is_folder)
      continue;
    DCHECK(app->folder_id.empty());
    AddItem(std::move(app));
  }
  for (auto& app : apps) {
    if (!app)
      continue;
    AddItem(std::move(app));
  }
  search_model_.SetSearchEngineIsGoogle(is_search_engine_google);
}

void AppListControllerImpl::SetSearchResultMetadata(
    std::unique_ptr<SearchResultMetadata> metadata) {
  SearchResult* result = search_model_.FindSearchResult(metadata->id);
  if (result)
    result->SetMetadata(std::move(metadata));
}

void AppListControllerImpl::GetIdToAppListIndexMap(
    GetIdToAppListIndexMapCallback callback) {
  base::flat_map<std::string, uint16_t> id_to_app_list_index;
  for (size_t i = 0; i < model_->top_level_item_list()->item_count(); ++i)
    id_to_app_list_index[model_->top_level_item_list()->item_at(i)->id()] = i;
  std::move(callback).Run(id_to_app_list_index);
}

void AppListControllerImpl::FindOrCreateOemFolder(
    const std::string& oem_folder_name,
    const syncer::StringOrdinal& preferred_oem_position,
    FindOrCreateOemFolderCallback callback) {
  AppListFolderItem* oem_folder = model_->FindFolderItem(kOemFolderId);
  if (!oem_folder) {
    std::unique_ptr<AppListFolderItem> new_folder =
        std::make_unique<AppListFolderItem>(kOemFolderId);
    syncer::StringOrdinal oem_position = preferred_oem_position.IsValid()
                                             ? preferred_oem_position
                                             : GetOemFolderPos();
    // Do not create a sync item for the OEM folder here, do it in
    // ResolveFolderPositions() when the item position is finalized.
    oem_folder =
        static_cast<AppListFolderItem*>(model_->AddItem(std::move(new_folder)));
    model_->SetItemPosition(oem_folder, oem_position);
  }
  model_->SetItemName(oem_folder, oem_folder_name);
  std::move(callback).Run();
}

void AppListControllerImpl::ResolveOemFolderPosition(
    const syncer::StringOrdinal& preferred_oem_position,
    ResolveOemFolderPositionCallback callback) {
  // In ash:
  AppListFolderItem* ash_oem_folder = FindFolderItem(kOemFolderId);
  std::unique_ptr<AppListItemMetadata> metadata;
  if (ash_oem_folder) {
    const syncer::StringOrdinal& oem_folder_pos =
        preferred_oem_position.IsValid() ? preferred_oem_position
                                         : GetOemFolderPos();
    model_->SetItemPosition(ash_oem_folder, oem_folder_pos);
    metadata = ash_oem_folder->CloneMetadata();
  }
  std::move(callback).Run(std::move(metadata));
}

void AppListControllerImpl::NotifyProcessSyncChangesFinished() {
  // When there are incompatible apps on different devices under the same
  // user account, it is possible that moving or adding an app on an empty
  // spot on a page of a different type of device (e.g. Device 1) may cause app
  // overflow on another device (e.g. Device 2) since it may have more apps on
  // the same page. See details in http://crbug.com/1098174.
  // When the change is synced to the Device 2, paged view structure may load
  // meta data and detect a full page of apps without a page break item
  // at the end of the overflowed page. Therefore, after the sync service has
  // finished processing sync change, SaveToMetaData should be called to insert
  // page break items if there are any missing at the end of full pages.
  AppListView* const app_list_view = presenter_.GetView();
  if (app_list_view) {
    app_list_view->app_list_main_view()
        ->contents_view()
        ->apps_container_view()
        ->apps_grid_view()
        ->UpdatePagedViewStructure();
  }
}

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

  presenter_.Dismiss(base::TimeTicks());
}

void AppListControllerImpl::GetAppInfoDialogBounds(
    GetAppInfoDialogBoundsCallback callback) {
  AppListView* app_list_view = presenter_.GetView();
  gfx::Rect bounds = gfx::Rect();
  if (app_list_view)
    bounds = app_list_view->GetAppInfoDialogBounds();
  std::move(callback).Run(bounds);
}

void AppListControllerImpl::ShowAppList() {
  presenter_.Show(AppListViewState::kPeeking, GetDisplayIdToShowAppListOn(),
                  base::TimeTicks());
}

aura::Window* AppListControllerImpl::GetWindow() {
  return presenter_.GetWindow();
}

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

////////////////////////////////////////////////////////////////////////////////
// AppListModelObserver:

void AppListControllerImpl::OnAppListItemAdded(AppListItem* item) {
  client_->OnItemAdded(profile_id_, item->CloneMetadata());

  if (is_notification_indicator_enabled_ && cache_ &&
      notification_badging_pref_enabled_.value_or(false)) {
    // Update the notification badge indicator for the newly added app list
    // item.
    cache_->ForOneApp(item->id(), [item](const apps::AppUpdate& update) {
      item->UpdateNotificationBadge(update.HasBadge() ==
                                    apps::mojom::OptionalBool::kTrue);
    });
  }
}

void AppListControllerImpl::OnActiveUserPrefServiceChanged(
    PrefService* pref_service) {
  if (is_notification_indicator_enabled_) {
    pref_change_registrar_ = std::make_unique<PrefChangeRegistrar>();
    pref_change_registrar_->Init(pref_service);

    pref_change_registrar_->Add(
        prefs::kAppNotificationBadgingEnabled,
        base::BindRepeating(
            &AppListControllerImpl::UpdateAppNotificationBadging,
            base::Unretained(this)));

    // Observe AppRegistryCache for the current active account to get
    // notification updates.
    AccountId account_id =
        Shell::Get()->session_controller()->GetActiveAccountId();
    cache_ =
        apps::AppRegistryCacheWrapper::Get().GetAppRegistryCache(account_id);
    Observe(cache_);

    // Resetting the recorded pref forces the next call to
    // UpdateAppNotificationBadging() to update notification badging for every
    // app item.
    notification_badging_pref_enabled_.reset();
    UpdateAppNotificationBadging();
  }

  if (!IsTabletMode()) {
    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 (presenter_.GetView()) {
    presenter_.GetView()->CloseOpenedPage();
    presenter_.GetView()->search_box_view()->ClearSearch();
  }
}

void AppListControllerImpl::OnSessionStateChanged(
    session_manager::SessionState state) {
  if (!IsTabletMode())
    return;

  if (state != session_manager::SessionState::ACTIVE)
    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 (!presenter_.GetTargetVisibility())
    Shell::Get()->home_screen_controller()->Show();

  // 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())
    presenter_.SetViewVisibility(false);
  else
    OnVisibilityChanged(true, last_visible_display_id_);
}

void AppListControllerImpl::OnAppListItemWillBeDeleted(AppListItem* item) {
  if (!client_)
    return;

  if (item->is_folder())
    client_->OnFolderDeleted(profile_id_, item->CloneMetadata());

  if (item->is_page_break())
    client_->OnPageBreakItemDeleted(profile_id_, item->id());
}

void AppListControllerImpl::OnAppListItemUpdated(AppListItem* item) {
  if (client_)
    client_->OnItemUpdated(profile_id_, item->CloneMetadata());
}

void AppListControllerImpl::OnAppListStateChanged(AppListState new_state,
                                                  AppListState old_state) {
  UpdateLauncherContainer();

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

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

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

bool AppListControllerImpl::GetTargetVisibility(
    const base::Optional<int64_t>& display_id) const {
  return last_target_visible_ &&
         (!display_id.has_value() ||
          display_id.value() == last_visible_display_id_);
}

void AppListControllerImpl::Show(int64_t display_id,
                                 base::Optional<AppListShowSource> show_source,
                                 base::TimeTicks event_time_stamp) {
  if (show_source.has_value())
    LogAppListShowSource(show_source.value());

  presenter_.Show(AppListViewState::kPeeking, display_id, event_time_stamp);

  // AppListControllerImpl::Show is called in ash at the first time of showing
  // app list view. So check whether the expand arrow view should be visible.
  UpdateExpandArrowVisibility();
}

void AppListControllerImpl::UpdateYPositionAndOpacity(
    int y_position_in_screen,
    float background_opacity) {
  // Avoid changing app list opacity and position when homecher is enabled.
  if (IsTabletMode())
    return;
  presenter_.UpdateYPositionAndOpacity(y_position_in_screen,
                                       background_opacity);
}

void AppListControllerImpl::EndDragFromShelf(AppListViewState app_list_state) {
  // Avoid dragging app list when homecher is enabled.
  if (IsTabletMode())
    return;
  presenter_.EndDragFromShelf(app_list_state);
}

void AppListControllerImpl::ProcessMouseWheelEvent(
    const ui::MouseWheelEvent& event,
    bool from_touchpad) {
  gfx::Vector2d offset(event.offset().x(),
                       GetOffset(event.offset().y(), from_touchpad));
  presenter_.ProcessMouseWheelOffset(event.location(), offset);
}

ShelfAction AppListControllerImpl::ToggleAppList(
    int64_t display_id,
    AppListShowSource show_source,
    base::TimeTicks event_time_stamp) {
  if (IsTabletMode()) {
    bool handled = Shell::Get()->home_screen_controller()->GoHome(display_id);

    // Perform the "back" action for the app list.
    if (!handled) {
      Back();
      return SHELF_ACTION_APP_LIST_BACK;
    }

    LogAppListShowSource(show_source);
    return SHELF_ACTION_APP_LIST_SHOWN;
  }

  base::AutoReset<bool> auto_reset(&should_dismiss_immediately_,
                                   display_id != last_visible_display_id_);
  ShelfAction action =
      presenter_.ToggleAppList(display_id, show_source, event_time_stamp);
  UpdateExpandArrowVisibility();
  if (action == SHELF_ACTION_APP_LIST_SHOWN)
    LogAppListShowSource(show_source);
  return action;
}

AppListViewState AppListControllerImpl::GetAppListViewState() {
  return model_->state_fullscreen();
}

bool AppListControllerImpl::ShouldHomeLauncherBeVisible() const {
  if (!IsTabletMode())
    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 (!IsTabletMode())
    DismissAppList();
}

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

void AppListControllerImpl::OnOverviewModeStarting() {
  if (IsTabletMode()) {
    const int64_t display_id = last_visible_display_id_;
    OnVisibilityWillChange(false /*shown*/, display_id);
  } else {
    DismissAppList();
  }
}

void AppListControllerImpl::OnOverviewModeStartingAnimationComplete(
    bool canceled) {
  if (!IsTabletMode())
    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) {
  if (!IsTabletMode())
    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 (!IsTabletMode())
    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::OnTabletModeStarted() {
  const AppListView* app_list_view = presenter_.GetView();
  // In tablet mode shelf orientation is always "bottom". Dismiss app list if
  // switching to tablet mode from side shelf app list, to ensure the app list
  // is re-shown and laid out with correct "side shelf" value.
  if (app_list_view && app_list_view->is_side_shelf())
    DismissAppList();

  presenter_.OnTabletModeChanged(true);

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

  // 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::OnTabletModeEnded() {
  aura::Window* window = presenter_.GetWindow();
  base::AutoReset<bool> auto_reset(
      &should_dismiss_immediately_,
      window && RootWindowController::ForWindow(window)
                    ->GetShelfLayoutManager()
                    ->HasVisibleWindow());
  presenter_.OnTabletModeChanged(false);
  UpdateLauncherContainer();

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

void AppListControllerImpl::OnWallpaperColorsChanged() {
  if (IsVisible(last_visible_display_id_))
    presenter_.GetView()->OnWallpaperColorsChanged();
}

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

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

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

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

void AppListControllerImpl::OnDisplayConfigurationChanged() {
  // 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 =
      IsTabletMode() && Shell::Get()->session_controller()->GetSessionState() ==
                            session_manager::SessionState::ACTIVE;

  if (!should_be_shown || should_be_shown == presenter_.GetTargetVisibility())
    return;

  Shell::Get()->home_screen_controller()->Show();
}

void AppListControllerImpl::OnWindowUntracked(aura::Window* untracked_window) {
  UpdateExpandArrowVisibility();
}

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

void AppListControllerImpl::OnUiVisibilityChanged(
    AssistantVisibility new_visibility,
    AssistantVisibility old_visibility,
    base::Optional<AssistantEntryPoint> entry_point,
    base::Optional<AssistantExitPoint> exit_point) {
  switch (new_visibility) {
    case AssistantVisibility::kVisible:
      if (!IsVisible(base::nullopt)) {
        Show(GetDisplayIdToShowAppListOn(), kAssistantEntryPoint,
             base::TimeTicks());
      }

      if (!IsShowingEmbeddedAssistantUI())
        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.
      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 (IsTabletMode()) {
        base::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 tansient 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 (exit_point == AssistantExitPoint::kScreenshot) {
          set_active_state_animation_disabler.emplace(
              presenter_.GetView()->app_list_main_view()->contents_view());
        }

        presenter_.ShowEmbeddedAssistantUI(false);

        if (exit_point != AssistantExitPoint::kBackInLauncher) {
          presenter_.GetView()
              ->search_box_view()
              ->ClearSearchAndDeactivateSearchBox();
        }
      } else if (exit_point != AssistantExitPoint::kBackInLauncher) {
        // Similarly, when taking a screenshot by Assistant in clamshell mode,
        // we do not want to dismiss launcher with animation. Otherwise the
        // screenshot may have tansient state during the animation.
        base::AutoReset<bool> auto_reset(
            &should_dismiss_immediately_,
            exit_point == AssistantExitPoint::kScreenshot);
        DismissAppList();
      }

      break;
  }
}

base::ScopedClosureRunner
AppListControllerImpl::DisableHomeScreenBackgroundBlur() {
  AppListView* const app_list_view = presenter_.GetView();
  if (!app_list_view)
    return base::ScopedClosureRunner(base::DoNothing());
  return app_list_view->app_list_main_view()
      ->contents_view()
      ->apps_container_view()
      ->DisableSuggestionChipsBlur();
}

void AppListControllerImpl::OnHomeLauncherAnimationComplete(
    bool shown,
    int64_t display_id) {
  // Stop disabling background blur in home screen when the home screen
  // transition ends.
  home_screen_blur_disabler_.reset();

  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) {
  // Disable home screen background blur if the home launcher transition is
  // staring - the blur disabler will be reset when the transition ends (in
  // OnHomeLauncherAnimationComplete()).
  if (home_launcher_transition_state_ == HomeLauncherTransitionState::kFinished)
    home_screen_blur_disabler_ = DisableHomeScreenBackgroundBlur();

  const bool mostly_shown = percent_shown >= 50;
  home_launcher_transition_state_ =
      mostly_shown ? HomeLauncherTransitionState::kMostlyShown
                   : HomeLauncherTransitionState::kMostlyHidden;
  OnVisibilityWillChange(mostly_shown, display_id);
}

void AppListControllerImpl::ShowHomeScreenView() {
  DCHECK(IsTabletMode());

  // 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.
  base::Optional<AppListShowSource> show_source;
  if (!GetTopVisibleWindow())
    show_source = kTabletMode;

  Show(GetDisplayIdToShowAppListOn(), show_source, base::TimeTicks());
}

aura::Window* AppListControllerImpl::GetHomeScreenWindow() {
  return presenter_.GetWindow();
}

void AppListControllerImpl::UpdateScaleAndOpacityForHomeLauncher(
    float scale,
    float opacity,
    base::Optional<AnimationInfo> animation_info,
    UpdateAnimationSettingsCallback callback) {
  DCHECK(!animation_info.has_value() || !callback.is_null());

  presenter_.UpdateScaleAndOpacityForHomeLauncher(
      scale, opacity,
      GetTransitionFromMetricsAnimationInfo(std::move(animation_info)),
      std::move(callback));
}

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

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

  keyboard_traversal_engaged_ = engaged;

  views::View* focused_view =
      presenter_.GetView()->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 == presenter_.GetView()->search_box_view()->search_box())
    presenter_.GetView()->search_box_view()->SchedulePaint();
  else
    focused_view->SchedulePaint();
}

bool AppListControllerImpl::IsShowingEmbeddedAssistantUI() const {
  return presenter_.IsShowingEmbeddedAssistantUI();
}

void AppListControllerImpl::UpdateExpandArrowVisibility() {
  presenter_.SetExpandArrowViewVisibility(!IsTabletMode());
}

AppListViewState AppListControllerImpl::CalculateStateAfterShelfDrag(
    const ui::LocatedEvent& event_in_screen,
    float launcher_above_shelf_bottom_amount) const {
  if (presenter_.GetView())
    return presenter_.GetView()->CalculateStateAfterShelfDrag(
        event_in_screen, launcher_above_shelf_bottom_amount);
  return AppListViewState::kClosed;
}

void AppListControllerImpl::SetAppListModelForTest(
    std::unique_ptr<AppListModel> model) {
  model_->RemoveObserver(this);
  model_ = std::move(model);
  model_->AddObserver(this);
}

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()),
      IsTabletMode(), recorded_app_list_visibility_.value_or(last_visible_));
  recorded_app_list_view_state_ = base::nullopt;
  recorded_app_list_visibility_ = base::nullopt;
}

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

void AppListControllerImpl::StartAssistant() {
  AssistantUiController::Get()->ShowUi(
      AssistantEntryPoint::kLauncherSearchBoxIcon);
}

void AppListControllerImpl::StartSearch(const base::string16& raw_query) {
  if (client_) {
    base::string16 query;
    base::TrimWhitespace(raw_query, base::TRIM_ALL, &query);
    client_->StartSearch(query);
    auto* notifier = GetNotifier();
    if (notifier)
      notifier->NotifySearchQueryChanged(raw_query);
  }
}

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

  if (launch_type == AppListLaunchType::kAppSearchResult) {
    switch (launched_from) {
      case AppListLaunchedFrom::kLaunchedFromSearchBox:
      case AppListLaunchedFrom::kLaunchedFromSuggestionChip:
        RecordAppLaunched(launched_from);
        break;
      case AppListLaunchedFrom::kLaunchedFromGrid:
      case AppListLaunchedFrom::kLaunchedFromShelf:
        break;
    }
  }

  UMA_HISTOGRAM_ENUMERATION(kSearchResultOpenDisplayTypeHistogram,
                            result->display_type(),
                            SearchResultDisplayType::kLast);

  // Suggestion chips are not represented to the user as search results, so do
  // not record search result metrics for them.
  if (launched_from != AppListLaunchedFrom::kLaunchedFromSuggestionChip) {
    base::RecordAction(base::UserMetricsAction("AppList_OpenSearchResult"));

    UMA_HISTOGRAM_COUNTS_100(kSearchQueryLength, GetLastQueryLength());
    if (IsTabletMode()) {
      UMA_HISTOGRAM_COUNTS_100(kSearchQueryLengthInTablet,
                               GetLastQueryLength());
    } else {
      UMA_HISTOGRAM_COUNTS_100(kSearchQueryLengthInClamshell,
                               GetLastQueryLength());
    }
  }

  auto* notifier = GetNotifier();
  if (notifier) {
    // Special-case chip results, because the display type of app results
    // doesn't account for whether it's being displayed in the suggestion chips
    // or app tiles.
    AppListNotifier::Result notifier_result(result->id(),
                                            result->metrics_type());
    if (launched_from == AppListLaunchedFrom::kLaunchedFromSuggestionChip) {
      notifier->NotifyLaunched(SearchResultDisplayType::kChip, notifier_result);
    } else {
      notifier->NotifyLaunched(result->display_type(), notifier_result);
    }
  }

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

  ResetHomeLauncherIfShown();
}

void AppListControllerImpl::LogSearchAbandonHistogram() {
  RecordSearchAbandonWithQueryLengthHistogram(GetLastQueryLength());
}

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

void AppListControllerImpl::GetSearchResultContextMenuModel(
    const std::string& result_id,
    GetContextMenuModelCallback callback) {
  if (client_)
    client_->GetSearchResultContextMenuModel(result_id, std::move(callback));
}

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

  if (client_)
    client_->ViewShown(display_id);

  Shell::Get()->home_screen_controller()->OnAppListViewShown();

  // 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() {
  if (presenter_.GetView()->search_box_view()->is_search_box_active()) {
    // Close the virtual keyboard before the app list view is dismissed.
    // Otherwise if the browser is behind the app list view, after the latter is
    // closed, IME is updated because of the changed focus. Consequently,
    // the virtual keyboard is hidden for the wrong IME instance, which may
    // bring troubles when restoring the virtual keyboard (see
    // https://crbug.com/944233).
    keyboard::KeyboardUIController::Get()->HideKeyboardExplicitlyBySystem();
  }

  AssistantUiController::Get()->CloseUi(AssistantExitPoint::kLauncherClose);

  if (client_)
    client_->ViewClosing();

  if (Shell::Get()->home_screen_controller())
    Shell::Get()->home_screen_controller()->OnAppListViewClosing();
}

const std::vector<SkColor>&
AppListControllerImpl::GetWallpaperProminentColors() {
  return Shell::Get()->wallpaper_controller()->GetWallpaperColors();
}

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

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

  ResetHomeLauncherIfShown();
}

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

ui::ImplicitAnimationObserver* AppListControllerImpl::GetAnimationObserver(
    AppListViewState target_state) {
  // |presenter_| observes the close animation only.
  if (target_state == AppListViewState::kClosed)
    return &presenter_;
  return nullptr;
}

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() {
  if (should_dismiss_immediately_)
    return true;

  DCHECK(Shell::HasInstance());
  const int ideal_shelf_y =
      Shelf::ForWindow(presenter_.GetView()->GetWidget()->GetNativeView())
          ->GetIdealBounds()
          .y();

  const int current_y =
      presenter_.GetView()->GetWidget()->GetNativeWindow()->bounds().y();
  return current_y > ideal_shelf_y;
}

int AppListControllerImpl::GetTargetYForAppListHide(aura::Window* root_window) {
  DCHECK(Shell::HasInstance());
  gfx::Point top_center =
      Shelf::ForWindow(root_window)->GetShelfBoundsInScreen().top_center();
  wm::ConvertPointFromScreen(root_window, &top_center);
  return top_center.y();
}

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);
}

void AppListControllerImpl::NotifySearchResultsForLogging(
    const base::string16& raw_query,
    const SearchResultIdWithPositionIndices& results,
    int position_index) {
  if (client_) {
    base::string16 query;
    base::TrimWhitespace(raw_query, base::TRIM_ALL, &query);
    client_->NotifySearchResultsForLogging(query, results, position_index);
  }
}

void AppListControllerImpl::MaybeIncreaseSuggestedContentInfoShownCount() {
  if (ShouldShowSuggestedContentInfo()) {
    const int count = GetSuggestedContentInfoShownCount();
    SetSuggestedContentInfoShownCount(count + 1);
  }
}

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() ==
             chromeos::assistant::AssistantAllowedState::ALLOWED &&
         state->assistant_status() !=
             chromeos::assistant::AssistantStatus::NOT_READY;
}

bool AppListControllerImpl::ShouldShowSuggestedContentInfo() const {
  if (!IsSuggestedContentEnabled()) {
    // Don't show if user has interacted with the setting already.
    SetSuggestedContentInfoDismissed();
    return false;
  }

  if (IsSuggestedContentInfoDismissed()) {
    return false;
  }

  const int count = GetSuggestedContentInfoShownCount();
  constexpr int kThresholdToShow = 3;
  return count >= 0 && count <= kThresholdToShow;
}

void AppListControllerImpl::MarkSuggestedContentInfoDismissed() {
  // User dismissed the privacy info view. Will not show the view again.
  SetSuggestedContentInfoDismissed();
}

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

void AppListControllerImpl::OnViewStateChanged(AppListViewState state) {
  auto* notifier = GetNotifier();
  if (notifier)
    notifier->NotifyUIStateChanged(state);
}

void AppListControllerImpl::GetAppLaunchedMetricParams(
    AppLaunchedMetricParams* metric_params) {
  metric_params->app_list_view_state = GetAppListViewState();
  metric_params->is_tablet_mode = IsTabletMode();
  metric_params->home_launcher_shown = last_visible_;
}

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

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

bool AppListControllerImpl::IsInTabletMode() {
  return Shell::Get()->tablet_mode_controller()->InTabletMode();
}

AppListColorProviderImpl* AppListControllerImpl::GetColorProvider() {
  return &color_provider_;
}

void AppListControllerImpl::RecordAppLaunched(
    AppListLaunchedFrom launched_from) {
  RecordAppListAppLaunched(launched_from, GetAppListViewState(), IsTabletMode(),
                           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) {
  // 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 (IsTabletMode()) {
    UpdateTrackedAppWindow();

    if (tracked_app_window_)
      real_visibility = false;
  }

  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;

  AppListView* const app_list_view = presenter_.GetView();
  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;

    // We could make Assistant sub-controllers an AppListControllerObserver, but
    // we do not want to introduce new dependency of AppListController to
    // Assistant.
    GetAssistantViewDelegate()->OnHostViewVisibilityChanged(real_visibility);
    for (auto& observer : observers_)
      observer.OnAppListVisibilityChanged(real_visibility, display_id);

    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) {
  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 (IsTabletMode() && 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 && IsTabletMode()) {
      // Update the arrow visibility when starting to show the home screen
      // (presumably, the visibility has already been updated if home is being
      // hidden).
      UpdateExpandArrowVisibility();
    }

    if (real_target_visibility && presenter_.GetView())
      presenter_.SetViewVisibility(true);

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

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

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

syncer::StringOrdinal AppListControllerImpl::GetOemFolderPos() {
  // Place the OEM folder just after the web store, which should always be
  // followed by a pre-installed app (e.g. Search), so the poosition should be
  // stable. TODO(stevenjb): consider explicitly setting the OEM folder location
  // along with the name in ServicesCustomizationDocument::SetOemFolderName().
  AppListItemList* item_list = model_->top_level_item_list();
  if (!item_list->item_count()) {
    LOG(ERROR) << "No top level item was found. "
               << "Placing OEM folder at the beginning.";
    return syncer::StringOrdinal::CreateInitialOrdinal();
  }

  size_t web_store_app_index;
  if (!item_list->FindItemIndex(extensions::kWebStoreAppId,
                                &web_store_app_index)) {
    LOG(ERROR) << "Web store position is not found it top items. "
               << "Placing OEM folder at the end.";
    return item_list->item_at(item_list->item_count() - 1)
        ->position()
        .CreateAfter();
  }

  // Skip items with the same position.
  const AppListItem* web_store_app_item =
      item_list->item_at(web_store_app_index);
  for (size_t j = web_store_app_index + 1; j < item_list->item_count(); ++j) {
    const AppListItem* next_item = item_list->item_at(j);
    DCHECK(next_item->position().IsValid());
    if (!next_item->position().Equals(web_store_app_item->position())) {
      const syncer::StringOrdinal oem_ordinal =
          web_store_app_item->position().CreateBetween(next_item->position());
      VLOG(1) << "Placing OEM Folder at: " << j
              << " position: " << oem_ordinal.ToDebugString();
      return oem_ordinal;
    }
  }

  const syncer::StringOrdinal oem_ordinal =
      web_store_app_item->position().CreateAfter();
  VLOG(1) << "Placing OEM Folder at: " << item_list->item_count()
          << " position: " << oem_ordinal.ToDebugString();
  return oem_ordinal;
}

std::unique_ptr<AppListItem> AppListControllerImpl::CreateAppListItem(
    std::unique_ptr<AppListItemMetadata> metadata) {
  std::unique_ptr<AppListItem> app_list_item =
      metadata->is_folder ? std::make_unique<AppListFolderItem>(metadata->id)
                          : std::make_unique<AppListItem>(metadata->id);
  app_list_item->SetMetadata(std::move(metadata));
  return app_list_item;
}

AppListFolderItem* AppListControllerImpl::FindFolderItem(
    const std::string& folder_id) {
  return model_->FindFolderItem(folder_id);
}

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

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

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

void AppListControllerImpl::ResetHomeLauncherIfShown() {
  if (!IsTabletMode() || !presenter_.IsVisibleDeprecated())
    return;

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

  // Refresh the suggestion chips with empty query.
  StartSearch(base::string16());
}

void AppListControllerImpl::UpdateLauncherContainer(
    base::Optional<int64_t> display_id) {
  aura::Window* window = presenter_.GetWindow();
  if (!window)
    return;

  aura::Window* parent_window = GetContainerForDisplayId(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 (IsTabletMode() && !ShouldHomeLauncherBeVisible()) {
      WindowState* const window_state = WindowState::Get(window);
      if (window_state->IsActive())
        window_state->Deactivate();
    }
  }
}

int AppListControllerImpl::GetContainerId() const {
  return ShouldLauncherShowBehindApps() ? kShellWindowId_HomeScreenContainer
                                        : kShellWindowId_AppListContainer;
}

aura::Window* AppListControllerImpl::GetContainerForDisplayId(
    base::Optional<int64_t> display_id) {
  aura::Window* root_window = nullptr;
  if (display_id.has_value()) {
    root_window = Shell::GetRootWindowForDisplayId(display_id.value());
  } else if (presenter_.GetWindow()) {
    root_window = presenter_.GetWindow()->GetRootWindow();
  }

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

bool AppListControllerImpl::ShouldLauncherShowBehindApps() const {
  return IsTabletMode() &&
         model_->state() != AppListState::kStateEmbeddedAssistant;
}

int AppListControllerImpl::GetLastQueryLength() {
  base::string16 query;
  base::TrimWhitespace(search_model_.search_box()->text(), base::TRIM_ALL,
                       &query);
  return query.length();
}

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

  Shell* shell = Shell::Get();
  message_center::MessageCenter::Get()->RemoveObserver(this);
  AssistantController::Get()->RemoveObserver(this);
  AssistantUiController::Get()->GetModel()->RemoveObserver(this);
  shell->mru_window_tracker()->RemoveObserver(this);
  shell->window_tree_host_manager()->RemoveObserver(this);
  AssistantState::Get()->RemoveObserver(this);
  keyboard::KeyboardUIController::Get()->RemoveObserver(this);
  shell->overview_controller()->RemoveObserver(this);
  shell->RemoveShellObserver(this);
  shell->wallpaper_controller()->RemoveObserver(this);
  shell->tablet_mode_controller()->RemoveObserver(this);
  shell->session_controller()->RemoveObserver(this);
  model_->RemoveObserver(this);
}

bool AppListControllerImpl::IsHomeScreenVisible() {
  return IsTabletMode() && IsVisible(base::nullopt);
}

gfx::Rect AppListControllerImpl::GetInitialAppListItemScreenBoundsForWindow(
    aura::Window* window) {
  if (!presenter_.GetView())
    return gfx::Rect();
  std::string* app_id = window->GetProperty(kAppIDKey);
  return presenter_.GetView()->GetItemScreenBoundsInFirstGridPage(
      app_id ? *app_id : std::string());
}

void AppListControllerImpl::OnAppUpdate(const apps::AppUpdate& update) {
  if (update.HasBadgeChanged() &&
      notification_badging_pref_enabled_.value_or(false) &&
      !quiet_mode_enabled_.value_or(false)) {
    UpdateItemNotificationBadge(update.AppId(), update.HasBadge());
  }
}

void AppListControllerImpl::OnAppRegistryCacheWillBeDestroyed(
    apps::AppRegistryCache* cache) {
  Observe(nullptr);
}

void AppListControllerImpl::OnQuietModeChanged(bool in_quiet_mode) {
  UpdateAppNotificationBadging();
}

void AppListControllerImpl::UpdateTrackedAppWindow() {
  aura::Window* top_window = GetTopVisibleWindow();
  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::UpdateItemNotificationBadge(
    const std::string& app_id,
    apps::mojom::OptionalBool has_badge) {
  AppListItem* item = model_->FindItem(app_id);
  if (item) {
    item->UpdateNotificationBadge(has_badge ==
                                  apps::mojom::OptionalBool::kTrue);
  }
}

void AppListControllerImpl::UpdateAppNotificationBadging() {
  bool new_badging_enabled = pref_change_registrar_
                                 ? pref_change_registrar_->prefs()->GetBoolean(
                                       prefs::kAppNotificationBadgingEnabled)
                                 : false;
  bool new_quiet_mode_enabled =
      message_center::MessageCenter::Get()->IsQuietMode();

  if (notification_badging_pref_enabled_.has_value() &&
      notification_badging_pref_enabled_.value() == new_badging_enabled &&
      quiet_mode_enabled_.has_value() &&
      quiet_mode_enabled_.value() == new_quiet_mode_enabled) {
    return;
  }
  notification_badging_pref_enabled_ = new_badging_enabled;
  quiet_mode_enabled_ = new_quiet_mode_enabled;

  if (cache_) {
    cache_->ForEachApp([this](const apps::AppUpdate& update) {
      // Set the app notification badge hidden when the pref is disabled.
      apps::mojom::OptionalBool has_badge =
          notification_badging_pref_enabled_.value() &&
                  !quiet_mode_enabled_.value() &&
                  (update.HasBadge() == apps::mojom::OptionalBool::kTrue)
              ? apps::mojom::OptionalBool::kTrue
              : apps::mojom::OptionalBool::kFalse;
      UpdateItemNotificationBadge(update.AppId(), has_badge);
    });
  }
}

}  // namespace ash
