// Copyright (c) 2012 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/views/app_list_view.h"

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

#include "ash/app_list/app_list_util.h"
#include "ash/app_list/model/app_list_model.h"
#include "ash/app_list/views/app_list_folder_view.h"
#include "ash/app_list/views/app_list_main_view.h"
#include "ash/app_list/views/apps_container_view.h"
#include "ash/app_list/views/contents_view.h"
#include "ash/app_list/views/search_box_view.h"
#include "ash/assistant/ui/assistant_ui_constants.h"
#include "ash/keyboard/ui/keyboard_ui_controller.h"
#include "ash/public/cpp/app_list/app_list_color_provider.h"
#include "ash/public/cpp/app_list/app_list_config.h"
#include "ash/public/cpp/app_list/app_list_config_provider.h"
#include "ash/public/cpp/app_list/app_list_features.h"
#include "ash/public/cpp/app_list/app_list_types.h"
#include "ash/public/cpp/ash_features.h"
#include "ash/public/cpp/metrics_util.h"
#include "ash/public/cpp/shell_window_ids.h"
#include "ash/public/cpp/wallpaper_types.h"
#include "base/macros.h"
#include "base/metrics/histogram_macros.h"
#include "base/metrics/user_metrics.h"
#include "base/strings/string_util.h"
#include "ui/accessibility/aura/aura_window_properties.h"
#include "ui/accessibility/ax_node.h"
#include "ui/accessibility/ax_node_data.h"
#include "ui/aura/window.h"
#include "ui/aura/window_targeter.h"
#include "ui/aura/window_tree_host.h"
#include "ui/base/l10n/l10n_util.h"
#include "ui/base/ui_base_switches.h"
#include "ui/compositor/animation_throughput_reporter.h"
#include "ui/compositor/layer.h"
#include "ui/compositor/layer_animation_element.h"
#include "ui/compositor/layer_animation_observer.h"
#include "ui/compositor/layer_animation_sequence.h"
#include "ui/compositor/layer_animator.h"
#include "ui/compositor/scoped_animation_duration_scale_mode.h"
#include "ui/compositor/scoped_layer_animation_settings.h"
#include "ui/display/display.h"
#include "ui/display/screen.h"
#include "ui/gfx/canvas.h"
#include "ui/gfx/geometry/insets.h"
#include "ui/gfx/image/image_skia.h"
#include "ui/gfx/interpolated_transform.h"
#include "ui/gfx/skia_util.h"
#include "ui/strings/grit/ui_strings.h"
#include "ui/views/accessibility/view_accessibility.h"
#include "ui/views/controls/image_view.h"
#include "ui/views/controls/textfield/textfield.h"
#include "ui/views/layout/fill_layout.h"
#include "ui/views/views_delegate.h"
#include "ui/views/widget/widget.h"
#include "ui/wm/core/coordinate_conversion.h"
#include "ui/wm/core/ime_util_chromeos.h"
#include "ui/wm/core/shadow_types.h"

namespace ash {

namespace {

// The height of the half app list from the bottom of the screen.
constexpr int kHalfAppListHeight = 545;

// The scroll offset in order to transition from PEEKING to FULLSCREEN
constexpr int kAppListMinScrollToSwitchStates = 20;

// The DIP distance from the bezel in which a gesture drag end results in a
// closed app list.
constexpr int kAppListBezelMargin = 50;

// The size of app info dialog in fullscreen app list.
constexpr int kAppInfoDialogWidth = 512;
constexpr int kAppInfoDialogHeight = 384;

// The duration of app list animations when |short_animations_for_testing| are
// enabled.
constexpr int kAppListAnimationDurationImmediateMs = 0;

// Quality of the shield background blur.
constexpr float kAppListBlurQuality = 0.33f;

// Set animation durations to 0 for testing.
// TODO(oshima): Use ui::ScopedAnimationDurationScaleMode instead.
bool short_animations_for_testing;

// Histogram for the app list dragging in clamshell mode.
constexpr char kAppListDragInClamshellHistogram[] =
    "Apps.StateTransition.Drag.PresentationTime.ClamshellMode";
constexpr char kAppListDragInClamshellMaxLatencyHistogram[] =
    "Apps.StateTransition.Drag.PresentationTime.MaxLatency.ClamshellMode";

// The number of minutes that must pass for the current app list page to reset
// to the first page.
constexpr int kAppListPageResetTimeLimitMinutes = 20;

// When true, immdeidately fires the page reset timer upon starting.
bool skip_page_reset_timer_for_testing = false;

// Returns whether AppList's rounded corners should be hidden based on
// the app list view state and app list view bounds.
bool ShouldHideRoundedCorners(ash::AppListViewState app_list_state,
                              const gfx::Rect& bounds) {
  switch (app_list_state) {
    case ash::AppListViewState::kClosed:
      return false;
    case ash::AppListViewState::kFullscreenAllApps:
    case ash::AppListViewState::kFullscreenSearch:
      // Hide rounded corners in fullscreen state.
      return true;

    case ash::AppListViewState::kPeeking:
    case ash::AppListViewState::kHalf:
      // When the virtual keyboard shows, the AppListView is moved upward to
      // avoid the overlapping area with the virtual keyboard. As a result, its
      // bottom side may be on the display edge. Stop showing the rounded
      // corners under this circumstance.
      return bounds.y() == 0;
  }
  NOTREACHED();
  return false;
}

// This view forwards the focus to the search box widget by providing it as a
// FocusTraversable when a focus search is provided.
class SearchBoxFocusHost : public views::View {
 public:
  explicit SearchBoxFocusHost(views::Widget* search_box_widget)
      : search_box_widget_(search_box_widget) {}

  ~SearchBoxFocusHost() override = default;

  views::FocusTraversable* GetFocusTraversable() override {
    if (search_box_widget_->IsVisible())
      return search_box_widget_;
    return nullptr;
  }

  // views::View:
  const char* GetClassName() const override { return "SearchBoxFocusHost"; }

 private:
  views::Widget* search_box_widget_;

  DISALLOW_COPY_AND_ASSIGN(SearchBoxFocusHost);
};

SkColor GetBackgroundShieldColor(const std::vector<SkColor>& colors,
                                 float color_opacity,
                                 bool is_tablet_mode) {
  const U8CPU sk_opacity_value = static_cast<U8CPU>(255 * color_opacity);
  return SkColorSetA(
      AppListColorProvider::Get()->GetAppListBackgroundColor(is_tablet_mode),
      sk_opacity_value);
}

DEFINE_UI_CLASS_PROPERTY_KEY(bool, kExcludeWindowFromEventHandling, false)

// Gets radius for app list background corners when the app list has the
// provided height. The rounded corner should match the current app list height
// (so the rounded corners bottom edge matches the shelf top), until it reaches
// the app list background radius (i.e. background radius in peeking app list
// state).
// |height|: App list view height, relative to the shelf top (i.e. distance
//           between app list top and shelf top edge).
double GetBackgroundRadiusForAppListHeight(double height,
                                           int shelf_background_corner_radius) {
  return std::min(static_cast<double>(shelf_background_corner_radius),
                  std::max(height, 0.));
}

// This targeter prevents routing events to sub-windows, such as
// RenderHostWindow in order to handle events in context of app list.
class AppListEventTargeter : public aura::WindowTargeter {
 public:
  explicit AppListEventTargeter(AppListViewDelegate* delegate)
      : delegate_(delegate) {}
  ~AppListEventTargeter() override = default;

  // aura::WindowTargeter:
  bool SubtreeShouldBeExploredForEvent(aura::Window* window,
                                       const ui::LocatedEvent& event) override {
    if (delegate_ && !delegate_->CanProcessEventsOnApplistViews())
      return false;

    if (window->GetProperty(kExcludeWindowFromEventHandling)) {
      // Allow routing to sub-windows for ET_MOUSE_MOVED event which is used by
      // accessibility to enter the mode of exploration of WebView contents.
      if (event.type() != ui::ET_MOUSE_MOVED)
        return false;
    }

    if (window->GetProperty(ash::assistant::ui::kOnlyAllowMouseClickEvents)) {
      if (event.type() != ui::ET_MOUSE_PRESSED &&
          event.type() != ui::ET_MOUSE_RELEASED) {
        return false;
      }
    }

    return aura::WindowTargeter::SubtreeShouldBeExploredForEvent(window, event);
  }

 private:
  AppListViewDelegate* delegate_;  // Weak. Owned by AppListService.

  DISALLOW_COPY_AND_ASSIGN(AppListEventTargeter);
};

float ComputeSubpixelOffset(const display::Display& display, float value) {
  float pixel_position = std::round(display.device_scale_factor() * value);
  float dp_position = pixel_position / display.device_scale_factor();
  return dp_position - std::floor(value);
}

}  // namespace

////////////////////////////////////////////////////////////////////////////////
// AppListView::StateAnimationMetricsReporter

class AppListView::StateAnimationMetricsReporter {
 public:
  StateAnimationMetricsReporter() = default;
  StateAnimationMetricsReporter(const StateAnimationMetricsReporter&) = delete;
  StateAnimationMetricsReporter& operator=(
      const StateAnimationMetricsReporter&) = delete;
  ~StateAnimationMetricsReporter() = default;

  // Sets target state of the transition for metrics.
  void SetTargetState(AppListViewState target_state) {
    target_state_ = target_state;
  }

  // Sets tablet animation transition type for metrics.
  void SetTabletModeAnimationTransition(
      TabletModeAnimationTransition transition) {
    tablet_transition_ = transition;
  }

  // Resets the target state and animation type for metrics.
  void Reset();

  // Gets a callback to report smoothness.
  metrics_util::SmoothnessCallback GetReportCallback(bool tablet_mode) {
    if (tablet_mode) {
      return base::BindRepeating(
          &StateAnimationMetricsReporter::RecordMetricsInTablet,
          std::move(tablet_transition_));
    }
    return base::BindRepeating(
        &StateAnimationMetricsReporter::RecordMetricsInClamshell,
        std::move(target_state_));
  }

 private:
  static void RecordMetricsInTablet(
      base::Optional<TabletModeAnimationTransition> transition,
      int value);
  static void RecordMetricsInClamshell(
      base::Optional<AppListViewState> target_state,
      int value);

  base::Optional<AppListViewState> target_state_;
  base::Optional<TabletModeAnimationTransition> tablet_transition_;
};

void AppListView::StateAnimationMetricsReporter::Reset() {
  tablet_transition_.reset();
  target_state_.reset();
}

// static
void AppListView::StateAnimationMetricsReporter::RecordMetricsInTablet(
    base::Optional<TabletModeAnimationTransition> tablet_transition,
    int value) {
  UMA_HISTOGRAM_PERCENTAGE("Apps.StateTransition.AnimationSmoothness", value);

  // It can't ensure the target transition is properly set. Simply give up
  // reporting per-state metrics in that case. See https://crbug.com/954907.
  if (!tablet_transition)
    return;
  switch (*tablet_transition) {
    case TabletModeAnimationTransition::kDragReleaseShow:
      UMA_HISTOGRAM_PERCENTAGE(
          "Apps.HomeLauncherTransition.AnimationSmoothness.DragReleaseShow",
          value);
      break;
    case TabletModeAnimationTransition::kDragReleaseHide:
      UMA_HISTOGRAM_PERCENTAGE(
          "Apps.HomeLauncherTransition.AnimationSmoothness."
          "DragReleaseHide",
          value);
      break;
    case TabletModeAnimationTransition::kHomeButtonShow:
      UMA_HISTOGRAM_PERCENTAGE(
          "Apps.HomeLauncherTransition.AnimationSmoothness."
          "PressAppListButtonShow",
          value);
      break;
    case TabletModeAnimationTransition::kHideHomeLauncherForWindow:
      UMA_HISTOGRAM_PERCENTAGE(
          "Apps.HomeLauncherTransition.AnimationSmoothness."
          "HideLauncherForWindow",
          value);
      break;
    case TabletModeAnimationTransition::kEnterFullscreenAllApps:
      UMA_HISTOGRAM_PERCENTAGE(
          "Apps.HomeLauncherTransition.AnimationSmoothness."
          "EnterFullscreenAllApps",
          value);
      break;
    case TabletModeAnimationTransition::kEnterFullscreenSearch:
      UMA_HISTOGRAM_PERCENTAGE(
          "Apps.HomeLauncherTransition.AnimationSmoothness."
          "EnterFullscreenSearch",
          value);
      break;
    case TabletModeAnimationTransition::kFadeInOverview:
      UMA_HISTOGRAM_PERCENTAGE(
          "Apps.HomeLauncherTransition.AnimationSmoothness.FadeInOverview",
          value);
      break;
    case TabletModeAnimationTransition::kFadeOutOverview:
      UMA_HISTOGRAM_PERCENTAGE(
          "Apps.HomeLauncherTransition.AnimationSmoothness.FadeOutOverview",
          value);
      break;
  }
}

// static
void AppListView::StateAnimationMetricsReporter::RecordMetricsInClamshell(
    base::Optional<AppListViewState> target_state,
    int value) {
  UMA_HISTOGRAM_PERCENTAGE("Apps.StateTransition.AnimationSmoothness", value);

  // It can't ensure the target transition is properly set. Simply give up
  // reporting per-state metrics in that case. See https://crbug.com/954907.
  if (!target_state)
    return;

  switch (*target_state) {
    case AppListViewState::kClosed:
      UMA_HISTOGRAM_PERCENTAGE(
          "Apps.StateTransition.AnimationSmoothness.Close.ClamshellMode",
          value);
      break;
    case AppListViewState::kPeeking:
      UMA_HISTOGRAM_PERCENTAGE(
          "Apps.StateTransition.AnimationSmoothness.Peeking.ClamshellMode",
          value);
      break;
    case AppListViewState::kHalf:
      UMA_HISTOGRAM_PERCENTAGE(
          "Apps.StateTransition.AnimationSmoothness.Half.ClamshellMode", value);
      break;
    case AppListViewState::kFullscreenAllApps:
      UMA_HISTOGRAM_PERCENTAGE(
          "Apps.StateTransition.AnimationSmoothness.FullscreenAllApps."
          "ClamshellMode",
          value);
      break;
    case AppListViewState::kFullscreenSearch:
      UMA_HISTOGRAM_PERCENTAGE(
          "Apps.StateTransition.AnimationSmoothness.FullscreenSearch."
          "ClamshellMode",
          value);
      break;
  }
}

////////////////////////////////////////////////////////////////////////////////
// An animation observer to notify AppListView when animations for an app list
// view state transition complete. The observer goes through the following
// states:
// 1. kIdle
// 2. kReady, once `Reset()` has been called, and target app list state has been
//    set.
// 3. kActive, once `Activate()` has been called.
// 4. kTransitionDone, once `SetTransitionDone()` has been called.
//    *   `SetTransitionDone()` gets called when observed implicit animation
//        complete, but can be called directly if the app list view state is
//        updated without animation.
// 5. kIdle, once the app list view has been notified that the transition has
//    complete.
//
// Note that 3. and 4. may happen out of order - app list view will only be
// notified of transition completion when both steps are complete. The goal is
// to ensure that state transition notification is not sent out prematurely,
// before the internal app list view state is updated.
class StateTransitionNotifier : public ui::ImplicitAnimationObserver {
 public:
  explicit StateTransitionNotifier(AppListView* view) : view_(view) {}

  ~StateTransitionNotifier() override = default;

  // Resets the notifier, and set a new target app list state.
  void Reset(AppListViewState target_app_list_state) {
    StopObservingImplicitAnimations();

    state_ = State::kReady;
    target_app_list_view_state_ = target_app_list_state;
  }

  // Activates the notifier - moves the notifier in the state where it can
  // notify the app list view of state transition completion.
  // NOTE: If the app list state transition has already completed, the app list
  // view will get notified immediately.
  void Activate() {
    DCHECK(target_app_list_view_state_.has_value());

    if (state_ == State::kTransitionDone) {
      NotifyTransitionCompleted();
      return;
    }

    DCHECK_EQ(state_, State::kReady);
    state_ = State::kActive;
  }

  // Marks the app list view state transition as completed. If the notifier is
  // active, it will notify the app list view of the transition completion.
  // NOTE: This should be called directly only if the notifier is not added as a
  // transition animation observer. If the notifier is observing the animation,
  // this method gets called on the animation completion.
  void SetTransitionDone() {
    DCHECK_NE(state_, State::kTransitionDone);
    DCHECK_NE(state_, State::kIdle);

    const bool can_notify = state_ == State::kActive;
    state_ = State::kTransitionDone;

    if (can_notify)
      NotifyTransitionCompleted();
  }

 private:
  enum class State { kIdle, kReady, kActive, kTransitionDone };

  // Overridden from ui::ImplicitAnimationObserver:
  void OnImplicitAnimationsCompleted() override {
    StopObservingImplicitAnimations();

    TRACE_EVENT_NESTABLE_ASYNC_END1("ui", "AppList::StateTransitionAnimations",
                                    this, "state",
                                    target_app_list_view_state_.value());
    SetTransitionDone();
  }

  void NotifyTransitionCompleted() {
    DCHECK_EQ(state_, State::kTransitionDone);

    state_ = State::kIdle;

    AppListViewState app_list_state = *target_app_list_view_state_;
    target_app_list_view_state_ = base::nullopt;
    view_->OnBoundsAnimationCompleted(app_list_state);
  }

  State state_ = State::kIdle;
  AppListView* const view_;
  base::Optional<AppListViewState> target_app_list_view_state_;

  DISALLOW_COPY_AND_ASSIGN(StateTransitionNotifier);
};

// The view for the app list background shield which changes color and radius.
class AppListBackgroundShieldView : public views::View {
 public:
  explicit AppListBackgroundShieldView(int shelf_background_corner_radius,
                                       bool is_tablet_mode)
      : color_(AppListColorProvider::Get()->GetAppListBackgroundColor(
            is_tablet_mode)),
        shelf_background_corner_radius_(shelf_background_corner_radius) {
    SetPaintToLayer(ui::LAYER_SOLID_COLOR);
    layer()->SetFillsBoundsOpaquely(false);
    SetBackgroundRadius(shelf_background_corner_radius_);
    layer()->SetColor(color_);
    layer()->SetName("launcher/BackgroundShield");
  }

  ~AppListBackgroundShieldView() override = default;

  void UpdateBackground(bool use_blur) {
    if (blur_value_ == use_blur)
      return;
    blur_value_ = use_blur;

    if (use_blur) {
      layer()->SetBackgroundBlur(AppListConfig::instance().blur_radius());
      layer()->SetBackdropFilterQuality(kAppListBlurQuality);
    } else {
      layer()->SetBackgroundBlur(0);
    }
  }

  void UpdateBackgroundRadius(
      AppListViewState state,
      bool shelf_has_rounded_corners,
      base::Optional<base::TimeTicks> animation_end_timestamp) {
    const double target_corner_radius =
        (state == AppListViewState::kClosed && !shelf_has_rounded_corners)
            ? 0
            : shelf_background_corner_radius_;
    if (radius_ == target_corner_radius)
      return;

    layer()->GetAnimator()->StopAnimatingProperty(
        ui::LayerAnimationElement::ROUNDED_CORNERS);

    std::unique_ptr<ui::ScopedLayerAnimationSettings> settings;
    if (animation_end_timestamp.has_value()) {
      settings = std::make_unique<ui::ScopedLayerAnimationSettings>(
          layer()->GetAnimator());
      settings->SetTransitionDuration((*animation_end_timestamp) -
                                      base::TimeTicks::Now());
      settings->SetTweenType(gfx::Tween::EASE_OUT);
      settings->SetPreemptionStrategy(
          ui::LayerAnimator::IMMEDIATELY_SET_NEW_TARGET);
    }
    SetBackgroundRadius(target_corner_radius);
  }

  void SetBackgroundRadius(double radius) {
    radius_ = radius;
    layer()->SetRoundedCornerRadius({radius, radius, 0, 0});
  }

  void UpdateColor(SkColor color) {
    if (color_ == color)
      return;

    color_ = color;
    layer()->SetColor(color);
  }

  void UpdateBounds(const gfx::Rect& bounds) {
    // Inset bottom by 2 * the background radius to account for the rounded
    // corners on the top and bottom of the |app_list_background_shield_|. Only
    // add the inset to the bottom to keep padding at the top of the AppList the
    // same.
    gfx::Rect new_bounds = bounds;
    new_bounds.Inset(0, 0, 0, -shelf_background_corner_radius_ * 2);
    SetBoundsRect(new_bounds);
  }

  SkColor GetColorForTest() const { return color_; }

  const char* GetClassName() const override {
    return "AppListBackgroundShieldView";
  }

 private:
  // Whether the background blur has been set on the background shield.
  bool blur_value_ = false;

  double radius_ = 0.f;

  SkColor color_;

  int shelf_background_corner_radius_ = 0;

  DISALLOW_COPY_AND_ASSIGN(AppListBackgroundShieldView);
};

////////////////////////////////////////////////////////////////////////////////
// AppListView::TestApi

AppListView::TestApi::TestApi(AppListView* view) : view_(view) {
  DCHECK(view_);
}

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

AppsGridView* AppListView::TestApi::GetRootAppsGridView() {
  return view_->GetRootAppsGridView();
}

////////////////////////////////////////////////////////////////////////////////
// AppListView:

AppListView::AppListView(AppListViewDelegate* delegate)
    : delegate_(delegate),
      model_(delegate->GetModel()),
      search_model_(delegate->GetSearchModel()),
      is_background_blur_enabled_(features::IsBackgroundBlurEnabled()),
      state_transition_notifier_(
          std::make_unique<StateTransitionNotifier>(this)),
      state_animation_metrics_reporter_(
          std::make_unique<StateAnimationMetricsReporter>()) {
  CHECK(delegate);
}

AppListView::~AppListView() {
  // Remove child views first to ensure no remaining dependencies on delegate_.
  RemoveAllChildViews(true);
}

// static
void AppListView::ExcludeWindowFromEventHandling(aura::Window* window) {
  DCHECK(window);
  window->SetProperty(kExcludeWindowFromEventHandling, true);
}

// static
float AppListView::GetTransitionProgressForState(AppListViewState state) {
  switch (state) {
    case AppListViewState::kClosed:
      return 0.0f;
    case AppListViewState::kPeeking:
    case AppListViewState::kHalf:
      return 1.0f;
    case AppListViewState::kFullscreenAllApps:
    case AppListViewState::kFullscreenSearch:
      return 2.0f;
  }
  NOTREACHED();
  return 0.0f;
}

// static
void AppListView::SetShortAnimationForTesting(bool enabled) {
  short_animations_for_testing = enabled;
}

// static
bool AppListView::ShortAnimationsForTesting() {
  return short_animations_for_testing;
}

// static
void AppListView::SetSkipPageResetTimerForTesting(bool enabled) {
  skip_page_reset_timer_for_testing = enabled;
}

void AppListView::InitView(gfx::NativeView parent) {
  base::AutoReset<bool> auto_reset(&is_building_, true);
  time_shown_ = base::Time::Now();
  UpdateAppListConfig(parent);
  InitContents();
  InitWidget(parent);
  InitChildWidget();
}

void AppListView::InitContents() {
  DCHECK(!app_list_background_shield_);
  DCHECK(!app_list_main_view_);
  DCHECK(!search_box_view_);
  DCHECK(!announcement_view_);

  auto app_list_background_shield =
      std::make_unique<AppListBackgroundShieldView>(
          delegate_->GetShelfSize() / 2, delegate_->IsInTabletMode());
  app_list_background_shield->UpdateBackground(
      /*use_blur*/ !delegate_->IsInTabletMode() && is_background_blur_enabled_);
  app_list_background_shield_ =
      AddChildView(std::move(app_list_background_shield));

  auto app_list_main_view = std::make_unique<AppListMainView>(delegate_, this);
  search_box_view_ =
      new SearchBoxView(app_list_main_view.get(), delegate_, this);
  search_box_view_->Init(delegate_->IsInTabletMode());

  // Assign |app_list_main_view_| here since it is accessed during Init().
  app_list_main_view_ = app_list_main_view.get();
  app_list_main_view->Init(0, search_box_view_);
  AddChildView(std::move(app_list_main_view));
  announcement_view_ = AddChildView(std::make_unique<views::View>());
}

void AppListView::InitWidget(gfx::NativeView parent) {
  DCHECK(!GetWidget());
  views::Widget::InitParams params(
      views::Widget::InitParams::TYPE_WINDOW_FRAMELESS);
  params.name = "AppList";
  params.parent = parent;
  params.delegate = this;
  params.opacity = views::Widget::InitParams::WindowOpacity::kTranslucent;
  params.layer_type = ui::LAYER_NOT_DRAWN;

  views::Widget* widget = new views::Widget;
  widget->Init(std::move(params));
  DCHECK_EQ(widget, GetWidget());
  widget->GetNativeWindow()->SetEventTargeter(
      std::make_unique<AppListEventTargeter>(delegate_));

  // Enable arrow key. Arrow left/right and up/down triggers the same focus
  // movement as tab/shift+tab.
  SetEnableArrowKeyTraversal(true);

  widget->GetNativeView()->AddObserver(this);

  // Directs A11y focus ring from search box view to AppListView's descendants
  // (like ExpandArrowView) without focusing on the whole app list window when
  // using search + arrow button.
  search_box_view_->GetViewAccessibility().OverrideNextFocus(GetWidget());
  search_box_view_->GetViewAccessibility().OverridePreviousFocus(GetWidget());
}

void AppListView::InitChildWidget() {
  // Create a widget for the SearchBoxView to live in. This allows the
  // SearchBoxView to be on top of the custom launcher page's WebContents
  // (otherwise the search box events will be captured by the WebContents).
  views::Widget::InitParams search_box_widget_params(
      views::Widget::InitParams::TYPE_CONTROL);
  search_box_widget_params.parent = GetWidget()->GetNativeView();
  search_box_widget_params.opacity =
      views::Widget::InitParams::WindowOpacity::kTranslucent;
  search_box_widget_params.name = "SearchBoxView";
  search_box_widget_params.delegate = search_box_view_;

  views::Widget* search_box_widget = new views::Widget;
  search_box_widget->Init(std::move(search_box_widget_params));
  DCHECK_EQ(search_box_widget, search_box_view_->GetWidget());

  // Assign an accessibility role to the native window of |search_box_widget|,
  // so that hitting search+right could move ChromeVox focus across search box
  // to other elements in app list view.
  search_box_widget->GetNativeWindow()->SetProperty(
      ui::kAXRoleOverride,
      static_cast<ax::mojom::Role>(ax::mojom::Role::kGroup));

  // The search box will not naturally receive focus by itself (because it is in
  // a separate widget). Create this SearchBoxFocusHost in the main widget to
  // forward the focus search into to the search box.
  SearchBoxFocusHost* search_box_focus_host =
      new SearchBoxFocusHost(search_box_widget);
  AddChildView(search_box_focus_host);
  search_box_widget->SetFocusTraversableParentView(search_box_focus_host);
  search_box_widget->SetFocusTraversableParent(
      GetWidget()->GetFocusTraversable());

  // Directs A11y focus ring from AppListView's descendants (like
  // ExpandArrowView) to search box view without focusing on the whole app list
  // window when using search + arrow button.
  GetViewAccessibility().OverrideNextFocus(search_box_widget);
  GetViewAccessibility().OverridePreviousFocus(search_box_widget);
}

void AppListView::Show(AppListViewState preferred_state, bool is_side_shelf) {
  if (!time_shown_.has_value())
    time_shown_ = base::Time::Now();
  // The opacity of the AppListView may have been manipulated by overview mode,
  // so reset it before it is shown.
  GetWidget()->GetLayer()->SetOpacity(1.0f);
  is_side_shelf_ = is_side_shelf;

  AddAccelerator(ui::Accelerator(ui::VKEY_ESCAPE, ui::EF_NONE));
  AddAccelerator(ui::Accelerator(ui::VKEY_BROWSER_BACK, ui::EF_NONE));

  UpdateWidget();

  app_list_main_view_->contents_view()->ResetForShow();
  if (!delegate_->IsInTabletMode())
    SelectInitialAppsPage();

  SetState(preferred_state);

  // Ensures that the launcher won't open underneath the a11y keyboard.
  CloseKeyboardIfVisible();

  OnTabletModeChanged(delegate_->IsInTabletMode());
  app_list_main_view_->ShowAppListWhenReady();

  UMA_HISTOGRAM_TIMES(kAppListCreationTimeHistogram,
                      base::Time::Now() - time_shown_.value());
  time_shown_ = base::nullopt;
  RecordFolderMetrics();
}

void AppListView::SetDragAndDropHostOfCurrentAppList(
    ApplicationDragAndDropHost* drag_and_drop_host) {
  app_list_main_view_->SetDragAndDropHostOfCurrentAppList(drag_and_drop_host);
}

void AppListView::Dismiss() {
  CloseKeyboardIfVisible();
  delegate_->DismissAppList();
}

void AppListView::CloseOpenedPage() {
  if (HandleCloseOpenFolder())
    return;

  HandleCloseOpenSearchBox();
}

bool AppListView::HandleCloseOpenFolder() {
  if (GetAppsContainerView()->IsInFolderView()) {
    GetAppsContainerView()->app_list_folder_view()->CloseFolderPage();
    return true;
  }
  return false;
}

bool AppListView::HandleCloseOpenSearchBox() {
  if (app_list_main_view_ &&
      app_list_main_view_->contents_view()->IsShowingSearchResults()) {
    return Back();
  }
  return false;
}

bool AppListView::Back() {
  if (app_list_main_view_)
    return app_list_main_view_->contents_view()->Back();

  return false;
}

void AppListView::OnPaint(gfx::Canvas* canvas) {
  views::WidgetDelegateView::OnPaint(canvas);
}

const char* AppListView::GetClassName() const {
  return "AppListView";
}

bool AppListView::AcceleratorPressed(const ui::Accelerator& accelerator) {
  switch (accelerator.key_code()) {
    case ui::VKEY_ESCAPE:
    case ui::VKEY_BROWSER_BACK:
      // If the ContentsView does not handle the back action, then this is the
      // top level, so we close the app list.
      if (!Back() && !delegate_->IsInTabletMode())
        Dismiss();
      break;
    default:
      NOTREACHED();
      return false;
  }

  // Don't let DialogClientView handle the accelerator.
  return true;
}

void AppListView::Layout() {
  // Avoid layout while building the view.
  if (is_building_)
    return;

  // Avoid layout during animations.
  if (GetWidget()->GetLayer()->GetAnimator() &&
      GetWidget()->GetLayer()->GetAnimator()->is_animating()) {
    return;
  }

  const gfx::Rect contents_bounds = GetContentsBounds();

  // Exclude the shelf size from the contents bounds to avoid apps grid from
  // overlapping with shelf.
  gfx::Rect main_bounds = contents_bounds;
  main_bounds.Inset(GetMainViewInsetsForShelf());

  app_list_main_view_->SetBoundsRect(main_bounds);

  app_list_background_shield_->UpdateBounds(contents_bounds);

  UpdateAppListBackgroundYPosition(target_app_list_state_);
}

void AppListView::OnThemeChanged() {
  views::View::OnThemeChanged();
  SetBackgroundShieldColor();
}

ax::mojom::Role AppListView::GetAccessibleWindowRole() {
  // Default role of root view is ax::mojom::Role::kWindow which traps ChromeVox
  // focus within the root view. Assign ax::mojom::Role::kGroup here to allow
  // the focus to move from elements in app list view to search box.
  return ax::mojom::Role::kGroup;
}

const AppListConfig& AppListView::GetAppListConfig() const {
  return *app_list_config_;
}

views::View* AppListView::GetAppListBackgroundShieldForTest() {
  return app_list_background_shield_;
}

SkColor AppListView::GetAppListBackgroundShieldColorForTest() {
  return app_list_background_shield_->GetColorForTest();
}

bool AppListView::IsShowingEmbeddedAssistantUI() const {
  return app_list_main_view()->contents_view()->IsShowingEmbeddedAssistantUI();
}

bool AppListView::IsFolderBeingRenamed() {
  return GetAppsContainerView()
      ->app_list_folder_view()
      ->folder_header_view()
      ->HasTextFocus();
}

void AppListView::UpdatePageResetTimer(bool app_list_visibility) {
  if (app_list_visibility || !delegate_->IsInTabletMode()) {
    page_reset_timer_.Stop();
    return;
  }
  page_reset_timer_.Start(
      FROM_HERE,
      base::TimeDelta::FromMinutes(kAppListPageResetTimeLimitMinutes), this,
      &AppListView::SelectInitialAppsPage);

  if (skip_page_reset_timer_for_testing)
    page_reset_timer_.FireNow();
}

gfx::Insets AppListView::GetMainViewInsetsForShelf() const {
  if (is_side_shelf()) {
    // Set both horizontal insets so the app list remains centered on the
    // screen.
    return gfx::Insets(0, delegate_->GetShelfSize());
  }
  return gfx::Insets(0, 0, delegate_->GetShelfSize(), 0);
}

void AppListView::UpdateAppListConfig(aura::Window* parent_window) {
  // Create the app list configuration override if it's needed for the current
  // display bounds and the available apps grid size.
  std::unique_ptr<AppListConfig> new_config =
      AppListConfigProvider::Get().CreateForAppListWidget(
          display::Screen::GetScreen()
              ->GetDisplayNearestView(parent_window)
              .work_area()
              .size(),
          GetMainViewInsetsForShelf(), app_list_config_.get());

  if (!new_config)
    return;

  const bool is_initial_config = !app_list_config_.get();
  app_list_config_ = std::move(new_config);

  // Initial config should be set before the app list main view is initialized.
  DCHECK(!is_initial_config || !app_list_main_view_);

  // If the config changed, notify apps container the config has changed, so
  // root and folder apps grids are updated for the new config.
  if (!is_initial_config)
    GetAppsContainerView()->OnAppListConfigUpdated();
}

void AppListView::UpdateWidget() {
  // The widget's initial position will be off the bottom of the display.
  // Set native view's bounds directly to avoid screen position controller
  // setting bounds in the display where the widget has the largest
  // intersection.
  GetWidget()->GetNativeView()->SetBounds(
      GetPreferredWidgetBoundsForState(AppListViewState::kClosed));
  ResetSubpixelPositionOffset(GetWidget()->GetNativeView()->layer());
}

void AppListView::HandleClickOrTap(ui::LocatedEvent* event) {
  // If the virtual keyboard is visible, dismiss the keyboard. If there is some
  // text in the search box or the embedded assistant UI is shown, return early
  // so they don't get closed.
  if (CloseKeyboardIfVisible()) {
    search_box_view_->NotifyGestureEvent();
    if (search_box_view_->HasSearch() || IsShowingEmbeddedAssistantUI())
      return;
  }

  // Close embedded Assistant UI if it is shown.
  if (IsShowingEmbeddedAssistantUI()) {
    Back();
    search_box_view_->ClearSearchAndDeactivateSearchBox();
    return;
  }

  // Clear focus if the located event is not handled by any child view.
  GetFocusManager()->ClearFocus();

  if (GetAppsContainerView()->IsInFolderView()) {
    // Close the folder if it is opened.
    GetAppsContainerView()->app_list_folder_view()->CloseFolderPage();
    return;
  }

  if ((event->IsGestureEvent() &&
       (event->AsGestureEvent()->type() == ui::ET_GESTURE_LONG_PRESS ||
        event->AsGestureEvent()->type() == ui::ET_GESTURE_LONG_TAP ||
        event->AsGestureEvent()->type() == ui::ET_GESTURE_TWO_FINGER_TAP)) ||
      (event->IsMouseEvent() &&
       event->AsMouseEvent()->IsOnlyRightMouseButton())) {
    // Don't show menus on empty areas of the AppListView in clamshell mode.
    if (!delegate_->IsInTabletMode())
      return;

    // Home launcher is shown on top of wallpaper with transparent background.
    // So trigger the wallpaper context menu for the same events.
    gfx::Point onscreen_location(event->location());
    ConvertPointToScreen(this, &onscreen_location);
    delegate_->ShowWallpaperContextMenu(
        onscreen_location, event->IsGestureEvent() ? ui::MENU_SOURCE_TOUCH
                                                   : ui::MENU_SOURCE_MOUSE);
    return;
  }

  if (!search_box_view_->is_search_box_active() &&
      model_->state() != AppListState::kStateEmbeddedAssistant) {
    if (!delegate_->IsInTabletMode())
      Dismiss();
    return;
  }

  search_box_view_->ClearSearchAndDeactivateSearchBox();
}

void AppListView::StartDrag(const gfx::PointF& location_in_root) {
  initial_drag_point_ = location_in_root;

  drag_offset_ =
      initial_drag_point_.y() - GetWidget()->GetNativeWindow()->bounds().y();
}

void AppListView::UpdateDrag(const gfx::PointF& location_in_root) {
  float new_y_position_in_root = location_in_root.y() - drag_offset_;

  UpdateYPositionAndOpacity(new_y_position_in_root,
                            GetAppListBackgroundOpacityDuringDragging());
}

void AppListView::EndDrag(const gfx::PointF& location_in_root) {
  // |is_in_drag_| might have been cleared if the app list was dismissed while
  // drag was still in progress. Nothing to do here in that case.
  if (!is_in_drag_) {
    DCHECK_EQ(AppListViewState::kClosed, app_list_state_);
    return;
  }

  // Remember the last fling velocity, as the value gets reset in SetIsInDrag.
  const int last_fling_velocity = last_fling_velocity_;
  SetIsInDrag(false);

  // Change the app list state based on where the drag ended. If fling velocity
  // was over the threshold, snap to the next state in the direction of the
  // fling.
  if (std::abs(last_fling_velocity) >= kDragVelocityThreshold) {
    // If the user releases drag with velocity over the threshold, snap to
    // the next state, ignoring the drag release position.

    if (last_fling_velocity > 0) {
      switch (app_list_state_) {
        case AppListViewState::kPeeking:
        case AppListViewState::kHalf:
        case AppListViewState::kFullscreenSearch:
        case AppListViewState::kFullscreenAllApps:
          Dismiss();
          break;
        case AppListViewState::kClosed:
          NOTREACHED();
          break;
      }
    } else {
      switch (app_list_state_) {
        case AppListViewState::kFullscreenAllApps:
        case AppListViewState::kFullscreenSearch:
          SetState(app_list_state_);
          break;
        case AppListViewState::kHalf:
          SetState(AppListViewState::kFullscreenSearch);
          break;
        case AppListViewState::kPeeking:
          UMA_HISTOGRAM_ENUMERATION(kAppListPeekingToFullscreenHistogram,
                                    kSwipe, kMaxPeekingToFullscreen);
          SetState(AppListViewState::kFullscreenAllApps);
          break;
        case AppListViewState::kClosed:
          NOTREACHED();
          break;
      }
    }
  } else {
    const int fullscreen_height = GetFullscreenStateHeight();
    int app_list_height = 0;
    switch (app_list_state_) {
      case AppListViewState::kFullscreenAllApps:
      case AppListViewState::kFullscreenSearch:
        app_list_height = fullscreen_height;
        break;
      case AppListViewState::kHalf:
        app_list_height = std::min(fullscreen_height, kHalfAppListHeight);
        break;
      case AppListViewState::kPeeking:
        app_list_height = AppListConfig::instance().peeking_app_list_height();
        break;
      case AppListViewState::kClosed:
        NOTREACHED();
        break;
    }

    const int app_list_threshold =
        app_list_height / kAppListThresholdDenominator;
    const int drag_delta = initial_drag_point_.y() - location_in_root.y();
    int display_bottom_in_root = GetDisplayNearestView().bounds().height();
    // If the drag ended near the bezel, close the app list.
    if (location_in_root.y() >=
        (display_bottom_in_root - kAppListBezelMargin)) {
      Dismiss();
    } else {
      switch (app_list_state_) {
        case AppListViewState::kFullscreenAllApps:
          if (drag_delta < -app_list_threshold) {
            if (delegate_->IsInTabletMode() || is_side_shelf_)
              Dismiss();
            else
              SetState(AppListViewState::kPeeking);
          } else {
            SetState(app_list_state_);
          }
          break;
        case AppListViewState::kFullscreenSearch:
          if (drag_delta < -app_list_threshold)
            Dismiss();
          else
            SetState(app_list_state_);
          break;
        case AppListViewState::kHalf:
          if (drag_delta > app_list_threshold)
            SetState(AppListViewState::kFullscreenSearch);
          else if (drag_delta < -app_list_threshold)
            Dismiss();
          else
            SetState(app_list_state_);
          break;
        case AppListViewState::kPeeking:
          if (drag_delta > app_list_threshold) {
            SetState(AppListViewState::kFullscreenAllApps);
            UMA_HISTOGRAM_ENUMERATION(kAppListPeekingToFullscreenHistogram,
                                      kSwipe, kMaxPeekingToFullscreen);
          } else if (drag_delta < -app_list_threshold) {
            Dismiss();
          } else {
            SetState(app_list_state_);
          }
          break;
        case AppListViewState::kClosed:
          NOTREACHED();
          break;
      }
    }
  }
  initial_drag_point_ = gfx::PointF();
}

void AppListView::SetChildViewsForStateTransition(
    AppListViewState target_state) {
  if (target_state == AppListViewState::kHalf ||
      target_state == AppListViewState::kFullscreenSearch) {
    return;
  }

  if (GetAppsContainerView()->IsInFolderView())
    GetAppsContainerView()->ResetForShowApps();

  // Do not update the contents view state on closing.
  if (target_state != AppListViewState::kClosed) {
    app_list_main_view_->contents_view()->SetActiveState(
        AppListState::kStateApps, !is_side_shelf_);
  }

  // Set the apps to the initial page when PEEKING.
  if (target_state == AppListViewState::kPeeking)
    SelectInitialAppsPage();

  if (target_state == AppListViewState::kClosed && is_side_shelf_) {
    // Reset the search box to be shown again. This is done after the animation
    // is complete normally, but there is no animation when |is_side_shelf_|.
    search_box_view_->ClearSearchAndDeactivateSearchBox();
  }
}

void AppListView::ConvertAppListStateToFullscreenEquivalent(
    AppListViewState* state) {
  if (!(is_side_shelf_ || delegate_->IsInTabletMode()))
    return;

  // If side shelf or tablet mode are active, all transitions should be
  // made to the tablet mode/side shelf friendly versions.
  if (*state == AppListViewState::kHalf) {
    *state = AppListViewState::kFullscreenSearch;
  } else if (*state == AppListViewState::kPeeking) {
    // FULLSCREEN_ALL_APPS->PEEKING in tablet/side shelf mode should close
    // instead of going to PEEKING.
    *state = app_list_state_ == AppListViewState::kFullscreenAllApps
                 ? AppListViewState::kClosed
                 : AppListViewState::kFullscreenAllApps;
  }
}

void AppListView::MaybeIncreasePrivacyInfoRowShownCounts(
    AppListViewState new_state) {
  AppListStateTransitionSource transition =
      GetAppListStateTransitionSource(new_state);
  switch (transition) {
    case kPeekingToHalf:
    case kFullscreenAllAppsToFullscreenSearch:
      if (app_list_main_view()->contents_view()->IsShowingSearchResults())
        delegate_->MaybeIncreaseSuggestedContentInfoShownCount();
      break;
    default:
      break;
  }
}

void AppListView::RecordStateTransitionForUma(AppListViewState new_state) {
  AppListStateTransitionSource transition =
      GetAppListStateTransitionSource(new_state);
  // kMaxAppListStateTransition denotes a transition we are not interested in
  // recording (ie. PEEKING->PEEKING).
  if (transition == kMaxAppListStateTransition)
    return;

  UMA_HISTOGRAM_ENUMERATION(kAppListStateTransitionSourceHistogram, transition,
                            kMaxAppListStateTransition);

  switch (transition) {
    case kPeekingToFullscreenAllApps:
    case KHalfToFullscreenSearch:
      base::RecordAction(base::UserMetricsAction("AppList_PeekingToFull"));
      break;

    case kFullscreenAllAppsToPeeking:
      base::RecordAction(base::UserMetricsAction("AppList_FullToPeeking"));
      break;

    default:
      break;
  }
}

void AppListView::MaybeCreateAccessibilityEvent(AppListViewState new_state) {
  if (new_state == app_list_state_)
    return;

  if ((new_state != AppListViewState::kPeeking &&
       new_state != AppListViewState::kFullscreenAllApps)) {
    return;
  }

  if (!delegate_->AppListTargetVisibility())
    return;

  base::string16 state_announcement;

  if (new_state == AppListViewState::kPeeking) {
    state_announcement = l10n_util::GetStringUTF16(
        IDS_APP_LIST_SUGGESTED_APPS_ACCESSIBILITY_ANNOUNCEMENT);
  } else {
    state_announcement = l10n_util::GetStringUTF16(
        IDS_APP_LIST_ALL_APPS_ACCESSIBILITY_ANNOUNCEMENT);
  }
  announcement_view_->GetViewAccessibility().OverrideName(state_announcement);
  announcement_view_->NotifyAccessibilityEvent(ax::mojom::Event::kAlert, true);
}

void AppListView::EnsureWidgetBoundsMatchCurrentState() {
  const gfx::Rect new_target_bounds =
      GetPreferredWidgetBoundsForState(target_app_list_state_);
  aura::Window* window = GetWidget()->GetNativeView();
  if (new_target_bounds == window->GetTargetBounds())
    return;

  // Update the app list config to match the new window bounds - do this before
  // updating the bounds, so app list config is updated before apps container is
  // laid out (to avoid separate layouts for window bounds change and app list
  // config change).
  UpdateAppListConfig(GetWidget()->GetNativeView());

  // Set the widget size to fit the new display metrics.
  GetWidget()->GetNativeView()->SetBounds(new_target_bounds);
  ResetSubpixelPositionOffset(GetWidget()->GetNativeView()->layer());

  // Update the widget bounds to accommodate the new work
  // area.
  SetState(target_app_list_state_);
}

int AppListView::GetRemainingBoundsAnimationDistance() const {
  return GetWidget()->GetLayer()->transform().To2dTranslation().y();
}

display::Display AppListView::GetDisplayNearestView() const {
  return display::Screen::GetScreen()->GetDisplayNearestView(
      GetWidget()->GetNativeWindow()->parent());
}

AppsContainerView* AppListView::GetAppsContainerView() {
  return app_list_main_view_->contents_view()->apps_container_view();
}

AppsGridView* AppListView::GetRootAppsGridView() {
  return GetAppsContainerView()->apps_grid_view();
}

AppsGridView* AppListView::GetFolderAppsGridView() {
  return GetAppsContainerView()->app_list_folder_view()->items_grid_view();
}

AppListStateTransitionSource AppListView::GetAppListStateTransitionSource(
    AppListViewState target_state) const {
  switch (app_list_state_) {
    case AppListViewState::kClosed:
      // CLOSED->X transitions are not useful for UMA.
      return kMaxAppListStateTransition;
    case AppListViewState::kPeeking:
      switch (target_state) {
        case AppListViewState::kClosed:
          return kPeekingToClosed;
        case AppListViewState::kHalf:
          return kPeekingToHalf;
        case AppListViewState::kFullscreenAllApps:
          return kPeekingToFullscreenAllApps;
        case AppListViewState::kPeeking:
          // PEEKING->PEEKING is used when resetting the widget position after a
          // failed state transition. Not useful for UMA.
          return kMaxAppListStateTransition;
        case AppListViewState::kFullscreenSearch:
          // PEEKING->FULLSCREEN_SEARCH is not a valid transition.
          NOTREACHED();
          return kMaxAppListStateTransition;
      }
    case AppListViewState::kHalf:
      switch (target_state) {
        case AppListViewState::kClosed:
          return kHalfToClosed;
        case AppListViewState::kPeeking:
          return kHalfToPeeking;
        case AppListViewState::kFullscreenSearch:
          return KHalfToFullscreenSearch;
        case AppListViewState::kHalf:
          // HALF->HALF is used when resetting the widget position after a
          // failed state transition. Not useful for UMA.
          return kMaxAppListStateTransition;
        case AppListViewState::kFullscreenAllApps:
          // HALF->FULLSCREEN_ALL_APPS is not a valid transition.
          NOTREACHED();
          return kMaxAppListStateTransition;
      }

    case AppListViewState::kFullscreenAllApps:
      switch (target_state) {
        case AppListViewState::kClosed:
          return kFullscreenAllAppsToClosed;
        case AppListViewState::kPeeking:
          return kFullscreenAllAppsToPeeking;
        case AppListViewState::kFullscreenSearch:
          return kFullscreenAllAppsToFullscreenSearch;
        case AppListViewState::kHalf:
          // FULLSCREEN_ALL_APPS->HALF is not a valid transition.
          NOTREACHED();
          return kMaxAppListStateTransition;
        case AppListViewState::kFullscreenAllApps:
          // FULLSCREEN_ALL_APPS->FULLSCREEN_ALL_APPS is used when resetting the
          // widget positon after a failed state transition. Not useful for UMA.
          return kMaxAppListStateTransition;
      }
    case AppListViewState::kFullscreenSearch:
      switch (target_state) {
        case AppListViewState::kClosed:
          return kFullscreenSearchToClosed;
        case AppListViewState::kFullscreenAllApps:
          return kFullscreenSearchToFullscreenAllApps;
        case AppListViewState::kFullscreenSearch:
          // FULLSCREEN_SEARCH->FULLSCREEN_SEARCH is used when resetting the
          // widget position after a failed state transition. Not useful for
          // UMA.
          return kMaxAppListStateTransition;
        case AppListViewState::kPeeking:
          // FULLSCREEN_SEARCH->PEEKING is not a valid transition.
          NOTREACHED();
          return kMaxAppListStateTransition;
        case AppListViewState::kHalf:
          // FULLSCREEN_SEARCH->HALF is not a valid transition.
          NOTREACHED();
          return kMaxAppListStateTransition;
      }
  }
}

views::View* AppListView::GetInitiallyFocusedView() {
  views::View* initial_view;
  if (IsShowingEmbeddedAssistantUI()) {
    // Assistant page will redirect focus to its subviews.
    auto* content = app_list_main_view_->contents_view();
    initial_view = content->GetPageView(content->GetActivePageIndex());
  } else {
    initial_view = app_list_main_view_->search_box_view()->search_box();
  }
  return initial_view;
}

void AppListView::OnScrollEvent(ui::ScrollEvent* event) {
  if (!HandleScroll(event->location(),
                    gfx::Vector2d(event->x_offset(), event->y_offset()),
                    event->type())) {
    return;
  }

  event->SetHandled();
  event->StopPropagation();
}

void AppListView::OnMouseEvent(ui::MouseEvent* event) {
  // Ignore events if the app list is closing or closed.
  if (app_list_state_ == AppListViewState::kClosed)
    return;

  switch (event->type()) {
    case ui::ET_MOUSE_PRESSED:
      event->SetHandled();
      if (is_in_drag_)
        return;
      initial_mouse_drag_point_ = event->root_location_f();
      break;
    case ui::ET_MOUSE_DRAGGED:
      event->SetHandled();
      if (is_side_shelf_ || delegate_->IsInTabletMode())
        return;
      if (!is_in_drag_ && event->IsOnlyLeftMouseButton()) {
        // Calculate the mouse drag offset to determine whether AppListView is
        // in drag.
        gfx::Vector2dF drag_distance =
            event->root_location_f() - initial_mouse_drag_point_;
        if (abs(drag_distance.y()) < kMouseDragThreshold)
          return;

        StartDrag(initial_mouse_drag_point_);
        SetIsInDrag(true);
        app_list_main_view_->contents_view()->UpdateYPositionAndOpacity();
      }

      if (!is_in_drag_)
        return;
      UpdateDrag(event->root_location_f());
      break;
    case ui::ET_MOUSE_RELEASED:
      event->SetHandled();
      initial_mouse_drag_point_ = gfx::PointF();
      if (!is_in_drag_) {
        HandleClickOrTap(event);
        return;
      }
      EndDrag(event->root_location_f());
      CloseKeyboardIfVisible();
      SetIsInDrag(false);
      break;
    case ui::ET_MOUSEWHEEL:
      if (HandleScroll(event->location(), event->AsMouseWheelEvent()->offset(),
                       ui::ET_MOUSEWHEEL))
        event->SetHandled();
      break;
    default:
      break;
  }
}

void AppListView::OnGestureEvent(ui::GestureEvent* event) {
  // Ignore events if the app list is closing or closed.
  if (app_list_state_ == AppListViewState::kClosed)
    return;

  switch (event->type()) {
    case ui::ET_GESTURE_TAP:
    case ui::ET_GESTURE_LONG_PRESS:
    case ui::ET_GESTURE_LONG_TAP:
    case ui::ET_GESTURE_TWO_FINGER_TAP:
      SetIsInDrag(false);
      event->SetHandled();
      HandleClickOrTap(event);
      break;
    case ui::ET_SCROLL_FLING_START:
    case ui::ET_GESTURE_SCROLL_BEGIN: {
      // If the search box is active when we start our drag, let it know.
      if (search_box_view_->is_search_box_active())
        search_box_view_->NotifyGestureEvent();

      // Avoid scrolling events for the app list in tablet mode.
      if (is_side_shelf_ || delegate_->IsInTabletMode())
        return;
      // There may be multiple scroll begin events in one drag because the
      // relative location of the finger and widget is almost unchanged and
      // scroll begin event occurs when the relative location changes beyond a
      // threshold. So avoid resetting the initial drag point in drag.
      if (!is_in_drag_)
        StartDrag(event->root_location_f());
      SetIsInDrag(true);
      event->SetHandled();
      break;
    }
    case ui::ET_GESTURE_SCROLL_UPDATE: {
      // Avoid scrolling events for the app list in tablet mode.
      if (is_side_shelf_ || delegate_->IsInTabletMode())
        return;
      SetIsInDrag(true);
      last_fling_velocity_ = event->details().scroll_y();
      UpdateDrag(event->root_location_f());
      event->SetHandled();
      break;
    }
    case ui::ET_GESTURE_END: {
      if (!is_in_drag_)
        break;
      // Avoid scrolling events for the app list in tablet mode.
      if (is_side_shelf_ || delegate_->IsInTabletMode())
        return;
      EndDrag(event->root_location_f());
      event->SetHandled();
      break;
    }
    case ui::ET_MOUSEWHEEL: {
      if (HandleScroll(event->location(), event->AsMouseWheelEvent()->offset(),
                       ui::ET_MOUSEWHEEL))
        event->SetHandled();
      break;
    }
    default:
      break;
  }
}

void AppListView::OnKeyEvent(ui::KeyEvent* event) {
  RedirectKeyEventToSearchBox(event);
}

void AppListView::OnTabletModeChanged(bool started) {
  search_box_view_->OnTabletModeChanged(started);
  search_model_->SetTabletMode(started);
  GetAppsContainerView()->OnTabletModeChanged(started);

  if (is_in_drag_) {
    SetIsInDrag(false);
    UpdateChildViewsYPositionAndOpacity();
  }

  // Refresh the state if the view is not in a fullscreen state.
  if (started && !is_fullscreen())
    SetState(app_list_state_);

  app_list_background_shield_->UpdateBackground(
      /*use_blur*/ is_background_blur_enabled_ && !started);

  // Update background color opacity.
  SetBackgroundShieldColor();
}

void AppListView::OnWallpaperColorsChanged() {
  SetBackgroundShieldColor();
  search_box_view_->OnWallpaperColorsChanged();
}

bool AppListView::HandleScroll(const gfx::Point& location,
                               const gfx::Vector2d& offset,
                               ui::EventType type) {
  // Ignore 0-offset events to prevent spurious dismissal, see crbug.com/806338
  // The system generates 0-offset ET_SCROLL_FLING_CANCEL events during simple
  // touchpad mouse moves. Those may be passed via mojo APIs and handled here.
  if ((offset.y() == 0 && offset.x() == 0) || is_in_drag() ||
      ShouldIgnoreScrollEvents()) {
    return false;
  }

  if (app_list_state_ != AppListViewState::kPeeking &&
      app_list_state_ != AppListViewState::kFullscreenAllApps) {
    return false;
  }

  // Let the Apps grid view handle the event first in FULLSCREEN_ALL_APPS.
  if (app_list_state_ == AppListViewState::kFullscreenAllApps) {
    AppsGridView* apps_grid_view = GetAppsContainerView()->IsInFolderView()
                                       ? GetFolderAppsGridView()
                                       : GetRootAppsGridView();
    gfx::Point apps_grid_location(location);
    views::View::ConvertPointToTarget(this, apps_grid_view,
                                      &apps_grid_location);

    if (apps_grid_view->HandleScrollFromAppListView(apps_grid_location, offset,
                                                    type)) {
      return true;
    }
  }

  // The AppList should not be dismissed with scroll in tablet mode.
  if (delegate_->IsInTabletMode())
    return true;

  // If the event is a mousewheel event, the offset is always large enough,
  // otherwise the offset must be larger than the scroll threshold.
  if (type == ui::ET_MOUSEWHEEL ||
      abs(offset.y()) > kAppListMinScrollToSwitchStates) {
    if (app_list_state_ == AppListViewState::kFullscreenAllApps) {
      if (offset.y() > 0)
        Dismiss();
      return true;
    }

    DCHECK_EQ(AppListViewState::kPeeking, app_list_state_);
    // For upward touchpad or mousewheel scrolling, expand to full screen. For
    // downward, dismiss the peeking launcher.
    if (offset.y() > 0) {
      SetState(AppListViewState::kFullscreenAllApps);
      const AppListPeekingToFullscreenSource source =
          type == ui::ET_MOUSEWHEEL ? kMousewheelScroll : kMousepadScroll;
      UMA_HISTOGRAM_ENUMERATION(kAppListPeekingToFullscreenHistogram, source,
                                kMaxPeekingToFullscreen);
    } else {
      Dismiss();
    }
  }
  return true;
}

void AppListView::SetState(AppListViewState new_state) {
  AppListViewState new_state_override = new_state;
  ConvertAppListStateToFullscreenEquivalent(&new_state_override);

  target_app_list_state_ = new_state_override;

  // Update the contents view state to match the app list view state.
  // Updating the contents view state may cause a nested `SetState()` call.
  // Bind the current state update to a weak ptr that gets invalidated when
  // `SetState()` gets called again to detect whether `SetState()` got called
  // again.
  set_state_weak_factory_.InvalidateWeakPtrs();
  base::WeakPtr<AppListView> set_state_request =
      set_state_weak_factory_.GetWeakPtr();

  // Clear the drag state before closing the view.
  if (new_state_override == AppListViewState::kClosed)
    SetIsInDrag(false);

  SetChildViewsForStateTransition(new_state_override);

  // Bail out if `SetChildViewForStateTransition()` caused another call to
  // `SetState()`.
  if (!set_state_request)
    return;

  MaybeCreateAccessibilityEvent(new_state_override);

  // Prepare state transition notifier for the new state transition.
  state_transition_notifier_->Reset(new_state_override);

  StartAnimationForState(new_state_override);
  MaybeIncreasePrivacyInfoRowShownCounts(new_state_override);
  RecordStateTransitionForUma(new_state_override);
  model_->SetStateFullscreen(new_state_override);
  if (delegate_)
    delegate_->OnViewStateChanged(new_state_override);
  app_list_state_ = new_state_override;

  if (is_in_drag_ && app_list_state_ != AppListViewState::kClosed)
    app_list_main_view_->contents_view()->UpdateYPositionAndOpacity();

  if (GetWidget()->IsActive()) {
    // Reset the focus to initially focused view. This should be
    // done before updating visibility of views, because setting
    // focused view invisible automatically moves focus to next
    // focusable view, which potentially causes bugs.
    GetInitiallyFocusedView()->RequestFocus();
  }

  UpdateWindowTitle();

  // Activate state transition notifier after the app list state has been
  // updated, to ensure any observers that handle app list view state
  // transitions don't end up updating app list state while another state
  // transition is in progress (in case the transition animations complete
  // synchronously).
  state_transition_notifier_->Activate();

  // Updates the visibility of app list items according to the change of
  // |app_list_state_|.
  GetAppsContainerView()->UpdateControlVisibility(app_list_state_, is_in_drag_);
}

void AppListView::UpdateWindowTitle() {
  if (!GetWidget())
    return;
  gfx::NativeView window = GetWidget()->GetNativeView();
  AppListState contents_view_state = app_list_main_view_->model()->state();
  if (window) {
    if (contents_view_state == AppListState::kStateSearchResults ||
        contents_view_state == AppListState::kStateEmbeddedAssistant) {
      window->SetTitle(l10n_util::GetStringUTF16(
          IDS_APP_LIST_LAUNCHER_ACCESSIBILITY_ANNOUNCEMENT));
      return;
    }
    switch (target_app_list_state_) {
      case AppListViewState::kPeeking:
        window->SetTitle(l10n_util::GetStringUTF16(
            IDS_APP_LIST_SUGGESTED_APPS_ACCESSIBILITY_ANNOUNCEMENT));
        break;
      case AppListViewState::kFullscreenAllApps:
        window->SetTitle(l10n_util::GetStringUTF16(
            IDS_APP_LIST_ALL_APPS_ACCESSIBILITY_ANNOUNCEMENT));
        break;
      case AppListViewState::kClosed:
      case AppListViewState::kHalf:
      case AppListViewState::kFullscreenSearch:
        break;
    }
  }
}

base::TimeDelta AppListView::GetStateTransitionAnimationDuration(
    AppListViewState target_state) {
  if (ShortAnimationsForTesting() || is_side_shelf_ ||
      (target_state == AppListViewState::kClosed &&
       delegate_->ShouldDismissImmediately())) {
    return base::TimeDelta::FromMilliseconds(
        kAppListAnimationDurationImmediateMs);
  }

  if (is_fullscreen() || target_state == AppListViewState::kFullscreenAllApps ||
      target_state == AppListViewState::kFullscreenSearch) {
    // Animate over more time to or from a fullscreen state, to maintain a
    // similar speed.
    return base::TimeDelta::FromMilliseconds(
        kAppListAnimationDurationFromFullscreenMs);
  }

  return base::TimeDelta::FromMilliseconds(kAppListAnimationDurationMs);
}

void AppListView::StartAnimationForState(AppListViewState target_state) {
  base::TimeDelta animation_duration =
      GetStateTransitionAnimationDuration(target_state);

  ApplyBoundsAnimation(target_state, animation_duration);
  app_list_main_view_->contents_view()->OnAppListViewTargetStateChanged(
      target_state);
  if (!is_in_drag_) {
    app_list_main_view_->contents_view()->AnimateToViewState(
        target_state, animation_duration);
  }
}

void AppListView::ApplyBoundsAnimation(AppListViewState target_state,
                                       base::TimeDelta duration_ms) {
  if (is_side_shelf_ || is_in_drag_) {
    // There is no animation in side shelf.
    UpdateAppListBackgroundYPosition(target_state);
    // Mark the state transition as complete directly, as no animations that
    // for `state_transition_notifier_` to observe are run in this case.
    state_transition_notifier_->SetTransitionDone();
    return;
  }

  gfx::Rect target_bounds = GetPreferredWidgetBoundsForState(target_state);

  // When closing the view should animate to the shelf bounds. The workspace
  // area will not reflect an autohidden shelf so ask for the proper bounds.
  const int y_for_closed_state = delegate_->GetTargetYForAppListHide(
      GetWidget()->GetNativeView()->GetRootWindow());
  if (target_state == AppListViewState::kClosed) {
    target_bounds.set_y(y_for_closed_state);
  }

  // Record the current transform before removing it because this bounds
  // animation could be pre-empting another bounds animation.
  ui::Layer* layer = GetWidget()->GetLayer();

  // Adjust the closed state y to account for auto-hidden shelf.
  const int current_bounds_y = app_list_state_ == AppListViewState::kClosed
                                   ? y_for_closed_state
                                   : layer->bounds().y();
  const int current_y_with_transform =
      current_bounds_y + GetRemainingBoundsAnimationDistance();

  const gfx::Transform current_shield_transform =
      app_list_background_shield_->layer()->transform();

  // Only report animation throughput for full state transitions - i.e. when the
  // starting app list view position matches the expected position for the
  // current app list state. The goal is to reduce noise introduced by partial
  // state transitions - for example
  // *   When interrupting another state transition half-way, in which case the
  //     layer has non-identity ransform.
  // *   Starting an animation after drag gesture, in which case bounds may not
  //     match the expected app list bounds in the current state.
  bool report_animation_throughput =
      layer->transform() == gfx::Transform() &&
      layer->bounds() == GetPreferredWidgetBoundsForState(app_list_state_);

  // Schedule the animation; set to the target bounds, and make the transform
  // to make this appear in the original location. Then set an empty transform
  // with the animation.
  layer->SetBounds(target_bounds);
  ResetSubpixelPositionOffset(layer);

  gfx::Transform transform;
  const int y_offset = current_y_with_transform - target_bounds.y();
  transform.Translate(0, y_offset);
  layer->SetTransform(transform);
  animation_end_timestamp_ = base::TimeTicks::Now() + duration_ms;

  // Reset animation metrics reporter when animation is started.
  ResetTransitionMetricsReporter();

  if (delegate_->IsInTabletMode() &&
      target_state != AppListViewState::kClosed) {
    DCHECK(target_state == AppListViewState::kFullscreenAllApps ||
           target_state == AppListViewState::kFullscreenSearch);
    TabletModeAnimationTransition transition_type =
        target_state == AppListViewState::kFullscreenAllApps
            ? TabletModeAnimationTransition::kEnterFullscreenAllApps
            : TabletModeAnimationTransition::kEnterFullscreenSearch;
    state_animation_metrics_reporter_->SetTabletModeAnimationTransition(
        transition_type);
  } else {
    state_animation_metrics_reporter_->SetTargetState(target_state);
  }

  ui::ScopedLayerAnimationSettings animation(layer->GetAnimator());
  animation.SetPreemptionStrategy(
      ui::LayerAnimator::IMMEDIATELY_SET_NEW_TARGET);
  base::Optional<ui::AnimationThroughputReporter> reporter;
  if (report_animation_throughput) {
    reporter.emplace(
        animation.GetAnimator(),
        metrics_util::ForSmoothness(GetStateTransitionMetricsReportCallback()));
  }
  TRACE_EVENT_NESTABLE_ASYNC_BEGIN0("ui", "AppList::StateTransitionAnimations",
                                    state_transition_notifier_.get());
  animation.AddObserver(state_transition_notifier_.get());

  // In fullscreen state, or peeking state with restricted vertical space, the
  // background shield is translated upwards to ensure background radius is not
  // visible.
  // NOTE: layer->SetBounds() changes shield transform, so reset the transform
  // to the value before the |layer| bounds are set before starting the
  // animation.
  app_list_background_shield_->SetTransform(current_shield_transform);
  ui::ScopedLayerAnimationSettings shield_animation(
      app_list_background_shield_->layer()->GetAnimator());
  shield_animation.SetTransitionDuration(duration_ms);
  shield_animation.SetTweenType(gfx::Tween::EASE_OUT);
  shield_animation.SetPreemptionStrategy(
      ui::LayerAnimator::IMMEDIATELY_SET_NEW_TARGET);

  gfx::Transform shield_transform;
  if (ShouldHideRoundedCorners(target_state, target_bounds)) {
    shield_transform.Translate(0, -(delegate_->GetShelfSize() / 2));
  }
  app_list_background_shield_->SetTransform(shield_transform);

  animation.SetTransitionDuration(duration_ms);
  animation.SetTweenType(gfx::Tween::EASE_OUT);
  layer->SetTransform(gfx::Transform());

  // Schedule animations of the rounded corners. When running on linux
  // workstation, the rounded corner animation sometimes looks out-of-sync. This
  // does not happen on actual devices.

  // TODO(mukai): fix the out-of-sync problem.
  app_list_background_shield_->UpdateBackgroundRadius(
      target_state, shelf_has_rounded_corners_, animation_end_timestamp_);
}

void AppListView::SetStateFromSearchBoxView(bool search_box_is_empty,
                                            bool triggered_by_contents_change) {
  switch (target_app_list_state_) {
    case AppListViewState::kPeeking:
      if (!search_box_is_empty || search_box_view()->is_search_box_active())
        SetState(AppListViewState::kHalf);
      break;
    case AppListViewState::kHalf:
      if (search_box_is_empty && !triggered_by_contents_change)
        SetState(AppListViewState::kPeeking);
      break;
    case AppListViewState::kFullscreenSearch:
      if (search_box_is_empty && !triggered_by_contents_change)
        SetState(AppListViewState::kFullscreenAllApps);
      break;
    case AppListViewState::kFullscreenAllApps:
      if (!search_box_is_empty ||
          (search_box_is_empty && triggered_by_contents_change)) {
        SetState(AppListViewState::kFullscreenSearch);
      }
      break;
    case AppListViewState::kClosed:
      // We clean search on app list close.
      break;
  }
}

void AppListView::UpdateYPositionAndOpacity(float y_position_in_root,
                                            float background_opacity) {
  DCHECK(!is_side_shelf_);
  if (app_list_state_ == AppListViewState::kClosed)
    return;

  if (GetWidget()->GetLayer()->GetAnimator()->IsAnimatingProperty(
          ui::LayerAnimationElement::TRANSFORM)) {
    GetWidget()->GetLayer()->GetAnimator()->StopAnimatingProperty(
        ui::LayerAnimationElement::TRANSFORM);
  }

  SetIsInDrag(true);

  presentation_time_recorder_->RequestNext();

  background_opacity_in_drag_ = background_opacity;
  gfx::Rect new_window_bounds = GetWidget()->GetNativeWindow()->bounds();
  display::Display display = GetDisplayNearestView();
  float app_list_y_position_in_root = std::min(
      std::max(y_position_in_root,
               static_cast<float>(display.GetWorkAreaInsets().top())),
      static_cast<float>(display.size().height() - delegate_->GetShelfSize()));

  gfx::NativeView native_view = GetWidget()->GetNativeView();
  new_window_bounds.set_y(static_cast<int>(app_list_y_position_in_root));
  native_view->SetBounds(new_window_bounds);
  native_view->layer()->SetSubpixelPositionOffset(gfx::Vector2dF(
      ComputeSubpixelOffset(display, new_window_bounds.x()),
      ComputeSubpixelOffset(display, app_list_y_position_in_root)));

  UpdateChildViewsYPositionAndOpacity();
}

void AppListView::OffsetYPositionOfAppList(int offset) {
  gfx::NativeView native_view = GetWidget()->GetNativeView();
  gfx::Transform transform;
  transform.Translate(0, offset);
  native_view->SetTransform(transform);
}

PaginationModel* AppListView::GetAppsPaginationModel() {
  return GetRootAppsGridView()->pagination_model();
}

gfx::Rect AppListView::GetAppInfoDialogBounds() const {
  gfx::Rect app_info_bounds(GetDisplayNearestView().work_area());
  app_info_bounds.ClampToCenteredSize(
      gfx::Size(kAppInfoDialogWidth, kAppInfoDialogHeight));
  return app_info_bounds;
}

void AppListView::SetIsInDrag(bool is_in_drag) {
  if (!is_in_drag && !delegate_->IsInTabletMode())
    presentation_time_recorder_.reset();

  if (is_in_drag == is_in_drag_)
    return;

  // Reset |last_fling_velocity_| if it was set during the drag.
  if (!is_in_drag)
    last_fling_velocity_ = 0;

  // Don't allow dragging to interrupt the close animation, it probably is not
  // intentional.
  if (app_list_state_ == AppListViewState::kClosed)
    return;

  is_in_drag_ = is_in_drag;

  if (is_in_drag && !delegate_->IsInTabletMode()) {
    presentation_time_recorder_.reset();
    presentation_time_recorder_ = CreatePresentationTimeHistogramRecorder(
        GetWidget()->GetCompositor(), kAppListDragInClamshellHistogram,
        kAppListDragInClamshellMaxLatencyHistogram);
  }

  GetAppsContainerView()->UpdateControlVisibility(target_app_list_state_,
                                                  is_in_drag_);
}

void AppListView::OnHomeLauncherGainingFocusWithoutAnimation() {
  if (GetFocusManager()->GetFocusedView() != GetInitiallyFocusedView())
    GetInitiallyFocusedView()->RequestFocus();
}

void AppListView::SelectInitialAppsPage() {
  if (GetAppsPaginationModel()->total_pages() > 0 &&
      GetAppsPaginationModel()->selected_page() != 0) {
    GetAppsPaginationModel()->SelectPage(0, false /* animate */);
  }
}
int AppListView::GetScreenBottom() const {
  return GetDisplayNearestView().bounds().bottom();
}

int AppListView::GetCurrentAppListHeight() const {
  if (!GetWidget())
    return delegate_->GetShelfSize();
  return GetScreenBottom() - GetWidget()->GetWindowBoundsInScreen().y();
}

float AppListView::GetAppListTransitionProgress(int flags) const {
  // During transition between home and overview in tablet mode, the app list
  // widget gets scaled down from full screen state - if this is the case,
  // the app list layout should match the current app list state, so return
  // the progress for the current app list state.
  const gfx::Transform transform = GetWidget()->GetLayer()->transform();
  if (delegate_->IsInTabletMode() && transform.IsScaleOrTranslation() &&
      !transform.IsIdentityOrTranslation()) {
    return GetTransitionProgressForState(app_list_state_);
  }

  int current_height = GetCurrentAppListHeight();
  if (flags & kProgressFlagWithTransform) {
    current_height -=
        GetWidget()->GetLayer()->transform().To2dTranslation().y();
  }

  const int fullscreen_height = GetFullscreenStateHeight();
  const int baseline_height =
      std::min(fullscreen_height,
               (flags & kProgressFlagSearchResults)
                   ? kHalfAppListHeight
                   : AppListConfig::instance().peeking_app_list_height());

  // If vertical space is limited, the baseline and fullscreen height might be
  // the same. To handle this case, if the height has reached the
  // baseline/fullscreen height, return either 1.0 or 2.0 progress, depending on
  // the current target state.
  if (baseline_height == fullscreen_height &&
      current_height >= fullscreen_height) {
    return GetTransitionProgressForState(app_list_state_);
  }

  if (current_height <= baseline_height) {
    // Currently transition progress is between closed and peeking state.
    // Calculate the progress of this transition.
    const float shelf_height =
        GetScreenBottom() - GetDisplayNearestView().work_area().bottom();

    // When screen is rotated, the current height might be smaller than shelf
    // height for just one moment, which results in negative progress. So force
    // the progress to be non-negative.
    return std::max(0.0f, (current_height - shelf_height) /
                              (baseline_height - shelf_height));
  }

  // Currently transition progress is between peeking and fullscreen state.
  // Calculate the progress of this transition.
  const float fullscreen_height_above_baseline =
      fullscreen_height - baseline_height;
  const float current_height_above_baseline = current_height - baseline_height;
  DCHECK_GT(fullscreen_height_above_baseline, 0);
  DCHECK_LE(current_height_above_baseline, fullscreen_height_above_baseline);
  return 1 + current_height_above_baseline / fullscreen_height_above_baseline;
}

int AppListView::GetFullscreenStateHeight() const {
  const display::Display display = GetDisplayNearestView();
  const gfx::Rect display_bounds = display.bounds();
  return display_bounds.height() - display.work_area().y() + display_bounds.y();
}

AppListViewState AppListView::CalculateStateAfterShelfDrag(
    const ui::LocatedEvent& event_in_screen,
    float launcher_above_shelf_bottom_amount) const {
  AppListViewState app_list_state = AppListViewState::kPeeking;
  if (event_in_screen.type() == ui::ET_SCROLL_FLING_START &&
      fabs(event_in_screen.AsGestureEvent()->details().velocity_y()) >
          kDragVelocityFromShelfThreshold) {
    // If the scroll sequence terminates with a fling, show the fullscreen app
    // list if the fling was fast enough and in the correct direction, otherwise
    // close it.
    app_list_state =
        event_in_screen.AsGestureEvent()->details().velocity_y() < 0
            ? AppListViewState::kFullscreenAllApps
            : AppListViewState::kClosed;
  } else {
    // Snap the app list to corresponding state according to the snapping
    // thresholds.
    if (delegate_->IsInTabletMode()) {
      app_list_state =
          launcher_above_shelf_bottom_amount > kDragSnapToFullscreenThreshold
              ? AppListViewState::kFullscreenAllApps
              : AppListViewState::kClosed;
    } else {
      if (launcher_above_shelf_bottom_amount <= kDragSnapToClosedThreshold) {
        app_list_state = AppListViewState::kClosed;
      } else if (launcher_above_shelf_bottom_amount <=
                 kDragSnapToPeekingThreshold) {
        app_list_state = AppListViewState::kPeeking;
      } else {
        app_list_state = AppListViewState::kFullscreenAllApps;
      }
    }
  }

  // Deal with the situation of dragging app list from shelf while typing in
  // the search box.
  if (app_list_state == AppListViewState::kFullscreenAllApps) {
    AppListState active_state =
        app_list_main_view_->contents_view()->GetActiveState();
    if (active_state == AppListState::kStateSearchResults)
      app_list_state = AppListViewState::kFullscreenSearch;
  }

  return app_list_state;
}

metrics_util::SmoothnessCallback
AppListView::GetStateTransitionMetricsReportCallback() {
  return state_animation_metrics_reporter_->GetReportCallback(
      delegate_->IsInTabletMode());
}

void AppListView::ResetTransitionMetricsReporter() {
  state_animation_metrics_reporter_->Reset();
}

void AppListView::OnWindowDestroying(aura::Window* window) {
  DCHECK_EQ(GetWidget()->GetNativeView(), window);
  window->RemoveObserver(this);
}

void AppListView::OnWindowBoundsChanged(aura::Window* window,
                                        const gfx::Rect& old_bounds,
                                        const gfx::Rect& new_bounds,
                                        ui::PropertyChangeReason reason) {
  DCHECK_EQ(GetWidget()->GetNativeView(), window);

  UpdateAppListConfig(window);

  gfx::Transform transform;
  if (ShouldHideRoundedCorners(target_app_list_state_, new_bounds))
    transform.Translate(0, -(delegate_->GetShelfSize() / 2));

  // Avoid setting new transform if the shield is animating to (or already has)
  // the target value.
  if (app_list_background_shield_->layer()->GetTargetTransform() != transform) {
    app_list_background_shield_->SetTransform(transform);
    app_list_background_shield_->SchedulePaint();
  }
}

void AppListView::OnBoundsAnimationCompleted(AppListViewState target_state) {
  const bool was_animation_interrupted =
      GetRemainingBoundsAnimationDistance() != 0;

  if (target_state == AppListViewState::kClosed) {
    // Close embedded Assistant UI if it is open, to reset the
    // |assistant_page_view| bounds and AppListState.
    auto* contents_view = app_list_main_view()->contents_view();
    if (contents_view->IsShowingEmbeddedAssistantUI())
      contents_view->ShowEmbeddedAssistantUI(false);
  }

  ui::ImplicitAnimationObserver* animation_observer =
      delegate_->GetAnimationObserver(target_state);
  if (animation_observer)
    animation_observer->OnImplicitAnimationsCompleted();

  // Layout if the animation was completed.
  if (!was_animation_interrupted) {
    Layout();

    // NOTE: `target_state` may not match `app_list_state_` if
    // `OnBoundsAnimationCompleted()` gets called synchronously - for example,
    // for state changes during drag, and with side shelf.
    delegate_->OnStateTransitionAnimationCompleted(target_state);
  }
}

gfx::Rect AppListView::GetItemScreenBoundsInFirstGridPage(
    const std::string& id) const {
  const AppsGridView* apps_grid_view = app_list_main_view_->contents_view()
                                           ->apps_container_view()
                                           ->apps_grid_view();
  gfx::Rect item_bounds = apps_grid_view->GetExpectedItemBoundsInFirstPage(id);
  ConvertRectToScreen(apps_grid_view, &item_bounds);
  return item_bounds;
}

void AppListView::SetShelfHasRoundedCorners(bool shelf_has_rounded_corners) {
  if (shelf_has_rounded_corners_ == shelf_has_rounded_corners)
    return;
  shelf_has_rounded_corners_ = shelf_has_rounded_corners;
  base::Optional<base::TimeTicks> animation_end_timestamp;
  if (GetWidget() && GetWidget()->GetLayer()->GetAnimator()->is_animating()) {
    animation_end_timestamp = animation_end_timestamp_;
  }
  app_list_background_shield_->UpdateBackgroundRadius(
      target_app_list_state_, shelf_has_rounded_corners_,
      animation_end_timestamp);
}

void AppListView::UpdateChildViewsYPositionAndOpacity() {
  if (target_app_list_state_ == AppListViewState::kClosed)
    return;

  UpdateAppListBackgroundYPosition(target_app_list_state_);

  // Update the opacity of the background shield.
  SetBackgroundShieldColor();

  app_list_main_view_->contents_view()->UpdateYPositionAndOpacity();
}

void AppListView::RedirectKeyEventToSearchBox(ui::KeyEvent* event) {
  if (event->handled())
    return;

  // Allow text input inside the Assistant page.
  if (IsShowingEmbeddedAssistantUI())
    return;

  views::Textfield* search_box = search_box_view_->search_box();
  const bool is_search_box_focused = search_box->HasFocus();

  // Do not redirect the key event to the |search_box_| when focus is on a
  // text field.
  if (is_search_box_focused || IsFolderBeingRenamed())
    return;

  // Do not redirect the arrow keys in app list as they are are used for focus
  // traversal and app movement.
  if (IsArrowKeyEvent(*event) && !search_box_view_->is_search_box_active())
    return;

  // Redirect key event to |search_box_|.
  search_box->OnKeyEvent(event);
  if (event->handled()) {
    // Set search box focused if the key event is consumed.
    search_box->RequestFocus();
    return;
  }

  // Insert it into search box if the key event is a character. Released
  // key should not be handled to prevent inserting duplicate character.
  if (event->type() == ui::ET_KEY_PRESSED)
    search_box->InsertChar(*event);
}

void AppListView::OnScreenKeyboardShown(bool shown) {
  if (onscreen_keyboard_shown_ == shown)
    return;

  onscreen_keyboard_shown_ = shown;
  if (shown && GetAppsContainerView()->IsInFolderView()) {
    // Move the app list up to prevent folders being blocked by the
    // on-screen keyboard.
    const int folder_offset =
        GetAppsContainerView()->app_list_folder_view()->GetYOffsetForFolder();
    if (folder_offset != 0) {
      OffsetYPositionOfAppList(folder_offset);
      offset_to_show_folder_with_onscreen_keyboard_ = true;
    }
  } else if (offset_to_show_folder_with_onscreen_keyboard_) {
    // If the keyboard is closing or a folder isn't being shown, reset
    // the app list's position
    OffsetYPositionOfAppList(0);
    offset_to_show_folder_with_onscreen_keyboard_ = false;
  }

  if (!shown) {
    // When the virtual keyboard is hidden, it will attempt to restore the app
    // list bounds from when the keyboard was first shown - this might misplace
    // the app list view if its intended bounds changed in the mean time. To
    // avoid that, clear saved "restore bounds", and call SetState() to make
    // sure app list bounds match the current app list view state.
    GetWidget()->GetNativeView()->ClearProperty(
        wm::kVirtualKeyboardRestoreBoundsKey);
    EnsureWidgetBoundsMatchCurrentState();
  }
}

bool AppListView::CloseKeyboardIfVisible() {
  // TODO(ginko) abstract this function to be in
  // |keyboard::KeyboardUIController*|
  if (!keyboard::KeyboardUIController::HasInstance())
    return false;
  auto* const keyboard_controller = keyboard::KeyboardUIController::Get();
  if (keyboard_controller->IsKeyboardVisible()) {
    keyboard_controller->HideKeyboardByUser();
    return true;
  }
  return false;
}

void AppListView::OnParentWindowBoundsChanged() {
  EnsureWidgetBoundsMatchCurrentState();
}

float AppListView::GetAppListBackgroundOpacityDuringDragging() {
  float top_of_applist = GetWidget()->GetWindowBoundsInScreen().y();
  const int shelf_height = delegate_->GetShelfSize();
  float dragging_height =
      std::max((GetScreenBottom() - shelf_height - top_of_applist), 0.f);
  float coefficient =
      std::min(dragging_height / (kNumOfShelfSize * shelf_height), 1.0f);
  float shield_opacity =
      is_background_blur_enabled_ ? kAppListOpacityWithBlur : kAppListOpacity;
  // Assume shelf is opaque when start to drag down the launcher.
  const float shelf_opacity = 1.0f;
  return coefficient * shield_opacity + (1 - coefficient) * shelf_opacity;
}

void AppListView::SetBackgroundShieldColor() {
  // There is a chance when AppListView::OnWallpaperColorsChanged is called
  // from AppListViewDelegate, the |app_list_background_shield_| is not
  // initialized.
  if (!app_list_background_shield_)
    return;

  // Opacity is set on the color instead of the layer because changing opacity
  // of the layer changes opacity of the blur effect, which is not desired.
  float color_opacity = kAppListOpacity;

  if (delegate_->IsInTabletMode()) {
    // The Homecher background should have an opacity of 0.
    color_opacity = 0;
  } else if (is_in_drag_) {
    // Allow a custom opacity while the AppListView is dragging to show a
    // gradual opacity change when dragging from the shelf.
    color_opacity = background_opacity_in_drag_;
  } else if (is_background_blur_enabled_) {
    color_opacity = kAppListOpacityWithBlur;
  }

  app_list_background_shield_->UpdateColor(
      GetBackgroundShieldColor(delegate_->GetWallpaperProminentColors(),
                               color_opacity, delegate_->IsInTabletMode()));
}

void AppListView::RecordFolderMetrics() {
  int number_of_apps_in_folders = 0;
  int number_of_folders = 0;
  AppListItemList* item_list =
      app_list_main_view_->model()->top_level_item_list();
  for (size_t i = 0; i < item_list->item_count(); ++i) {
    AppListItem* item = item_list->item_at(i);
    if (item->GetItemType() != AppListFolderItem::kItemType)
      continue;
    ++number_of_folders;
    AppListFolderItem* folder = static_cast<AppListFolderItem*>(item);
    if (folder->folder_type() == AppListFolderItem::FOLDER_TYPE_OEM)
      continue;  // Don't count items in OEM folders.
    number_of_apps_in_folders += folder->item_list()->item_count();
  }
  UMA_HISTOGRAM_COUNTS_100(kNumberOfFoldersHistogram, number_of_folders);
  UMA_HISTOGRAM_COUNTS_100(kNumberOfAppsInFoldersHistogram,
                           number_of_apps_in_folders);
}

bool AppListView::ShouldIgnoreScrollEvents() {
  // When the app list is doing state change animation or the apps grid view is
  // in transition, ignore the scroll events to prevent triggering extra state
  // changes or transtions.
  return GetWidget()->GetLayer()->GetAnimator()->is_animating() ||
         GetRootAppsGridView()->pagination_model()->has_transition();
}

int AppListView::GetPreferredWidgetYForState(AppListViewState state) const {
  // Note that app list container fills the screen, so we can treat the
  // container's y as the top of display.
  const display::Display display = GetDisplayNearestView();
  const gfx::Rect work_area_bounds = display.work_area();

  // The ChromeVox panel as well as the Docked Magnifier viewport affect the
  // workarea of the display. We need to account for that when applist is in
  // fullscreen to avoid being shown below them.
  const int fullscreen_height = work_area_bounds.y() - display.bounds().y();

  // Force fullscreen height if onscreen keyboard is shown to match the UI state
  // that's set by default when the onscreen keyboard is first shown.
  if (onscreen_keyboard_shown_ && state != AppListViewState::kClosed)
    return fullscreen_height;

  switch (state) {
    case AppListViewState::kPeeking:
      return display.bounds().height() -
             AppListConfig::instance().peeking_app_list_height();
    case AppListViewState::kHalf:
      return std::max(work_area_bounds.y() - display.bounds().y(),
                      display.bounds().height() - kHalfAppListHeight);
    case AppListViewState::kFullscreenAllApps:
    case AppListViewState::kFullscreenSearch:
      return fullscreen_height;
    case AppListViewState::kClosed:
      // Align the widget y with shelf y to avoid flicker in show animation. In
      // side shelf mode, the widget y is the top of work area because the
      // widget does not animate.
      return (is_side_shelf_ ? work_area_bounds.y()
                             : work_area_bounds.bottom()) -
             display.bounds().y();
  }
}

gfx::Rect AppListView::GetPreferredWidgetBoundsForState(
    AppListViewState state) {
  // Use parent's width instead of display width to avoid 1 px gap (See
  // https://crbug.com/884889).
  CHECK(GetWidget());
  aura::Window* parent = GetWidget()->GetNativeView()->parent();
  CHECK(parent);
  return delegate_->SnapBoundsToDisplayEdge(
      gfx::Rect(0, GetPreferredWidgetYForState(state), parent->bounds().width(),
                GetFullscreenStateHeight()));
}

void AppListView::UpdateAppListBackgroundYPosition(AppListViewState state) {
  const int app_list_background_corner_radius = delegate_->GetShelfSize() / 2;

  // Update the y position of the background shield.
  gfx::Transform transform;
  if (is_in_drag_) {
    // For the purpose of determining background shield offset, use progress
    // with kHalf baseline so the background shield does not start translating
    // up before it reaches kHalf height (which is larger than kPeeking height).
    // If the shield transform started at kPeeking height, the app list view
    // background would jump up when starting drag from the kHalf state.
    float app_list_transition_progress =
        GetAppListTransitionProgress(kProgressFlagSearchResults);
    if (app_list_transition_progress < 1 && !shelf_has_rounded_corners()) {
      const float shelf_height =
          GetScreenBottom() - GetDisplayNearestView().work_area().bottom();
      app_list_background_shield_->SetBackgroundRadius(
          GetBackgroundRadiusForAppListHeight(
              GetCurrentAppListHeight() - shelf_height,
              app_list_background_corner_radius));
    } else if (app_list_transition_progress >= 1 &&
               app_list_transition_progress <= 2) {
      // Translate background shield so that it ends drag at a y position
      // according to the background radius in peeking and fullscreen.
      transform.Translate(0, -app_list_background_corner_radius *
                                 (app_list_transition_progress - 1));
    }
  } else if (ShouldHideRoundedCorners(state, GetBoundsInScreen())) {
    transform.Translate(0, -app_list_background_corner_radius);
  }

  // Avoid setting new transform if the shield is animating to (or already has)
  // the target value.
  if (app_list_background_shield_->layer()->GetTargetTransform() != transform)
    app_list_background_shield_->SetTransform(transform);
}

void AppListView::OnTabletModeAnimationTransitionNotified(
    TabletModeAnimationTransition animation_transition) {
  state_animation_metrics_reporter_->SetTabletModeAnimationTransition(
      animation_transition);
}

void AppListView::EndDragFromShelf(AppListViewState app_list_state) {
  SetIsInDrag(false);

  if (app_list_state == AppListViewState::kClosed ||
      target_app_list_state_ == AppListViewState::kClosed) {
    Dismiss();
  } else {
    SetState(app_list_state);
  }
  UpdateChildViewsYPositionAndOpacity();
}

void AppListView::ResetSubpixelPositionOffset(ui::Layer* layer) {
  const display::Display display = GetDisplayNearestView();
  const gfx::Rect& bounds = layer->bounds();
  layer->SetSubpixelPositionOffset(
      gfx::Vector2dF(ComputeSubpixelOffset(display, bounds.x()),
                     ComputeSubpixelOffset(display, bounds.y())));
}

}  // namespace ash
