// Copyright 2016 The Chromium Authors
// Use of this source code is governed by a BSD-style license that can be
// found in the LICENSE file.
#include "ash/app_list/app_list_presenter_impl.h"
#include <optional>
#include <utility>
#include "ash/app_list/app_list_controller_impl.h"
#include "ash/app_list/app_list_metrics.h"
#include "ash/app_list/app_list_presenter_event_filter.h"
#include "ash/app_list/app_list_util.h"
#include "ash/app_list/app_list_view_delegate.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/keyboard/ui/keyboard_ui_controller.h"
#include "ash/public/cpp/app_list/app_list_types.h"
#include "ash/public/cpp/assistant/controller/assistant_ui_controller.h"
#include "ash/public/cpp/metrics_util.h"
#include "ash/public/cpp/shell_window_ids.h"
#include "ash/session/session_controller_impl.h"
#include "ash/shell.h"
#include "ash/wm/container_finder.h"
#include "base/containers/contains.h"
#include "base/functional/bind.h"
#include "base/functional/callback_forward.h"
#include "base/functional/callback_helpers.h"
#include "base/memory/raw_ptr.h"
#include "base/metrics/histogram_macros.h"
#include "base/metrics/user_metrics.h"
#include "chromeos/ash/services/assistant/public/cpp/assistant_enums.h"
#include "ui/aura/client/focus_client.h"
#include "ui/aura/window.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_observer.h"
#include "ui/compositor/scoped_layer_animation_settings.h"
#include "ui/display/screen.h"
#include "ui/display/types/display_constants.h"
#include "ui/gfx/geometry/transform.h"
#include "ui/gfx/geometry/transform_util.h"
#include "ui/views/controls/textfield/textfield.h"
#include "ui/views/widget/widget.h"
#include "ui/wm/core/transient_window_manager.h"
#include "ui/wm/public/activation_client.h"
namespace ash {
namespace {
using assistant::AssistantExitPoint;
// The target scale to which (or from which) the fullscreen launcher will
// animate between tablet <-> clamshell mode transition.
constexpr float kFullscreenLauncherFadeAnimationScale = 0.92f;
// The fade in/out animation duration for tablet <-> clamshell mode transition.
constexpr base::TimeDelta kFullscreenLauncherTransitionDuration =
base::Milliseconds(350);
// Callback from the compositor when it presented a valid frame. Used to
// record UMA of input latency.
void DidPresentCompositorFrame(base::TimeTicks event_time_stamp,
bool is_showing,
const viz::FrameTimingDetails& details) {
base::TimeTicks presentation_timestamp =
details.presentation_feedback.timestamp;
if (presentation_timestamp.is_null() || event_time_stamp.is_null() ||
presentation_timestamp < event_time_stamp) {
return;
}
const base::TimeDelta input_latency =
presentation_timestamp - event_time_stamp;
if (is_showing) {
UMA_HISTOGRAM_TIMES("Apps.AppListShow.InputLatency", input_latency);
} else {
UMA_HISTOGRAM_TIMES("Apps.AppListHide.InputLatency", input_latency);
}
}
// Invokes `complete_callback_` at the end of animation.
class FullscreenLauncherAnimationObserver
: public ui::ImplicitAnimationObserver,
public ui::LayerObserver {
public:
// Invoked with `true` if animation was aborted.
using AnimationCompleteCallback = base::OnceCallback<void(bool)>;
FullscreenLauncherAnimationObserver(
ui::Layer* layer,
AnimationCompleteCallback complete_callback)
: layer_(layer), complete_callback_(std::move(complete_callback)) {
DCHECK(layer_);
layer_->AddObserver(this);
}
FullscreenLauncherAnimationObserver(
const FullscreenLauncherAnimationObserver& other) = delete;
FullscreenLauncherAnimationObserver& operator=(
const FullscreenLauncherAnimationObserver& other) = delete;
~FullscreenLauncherAnimationObserver() override {
StopObservingImplicitAnimations();
layer_->RemoveObserver(this);
}
// ui::ImplicitAnimationObserver:
void OnImplicitAnimationsCompleted() override {
const bool aborted =
WasAnimationAbortedForProperty(
ui::LayerAnimationElement::AnimatableProperty::TRANSFORM) ||
WasAnimationAbortedForProperty(
ui::LayerAnimationElement::AnimatableProperty::OPACITY);
std::move(complete_callback_).Run(aborted);
delete this;
}
// ui::LayerObserver overrides:
void LayerDestroyed(ui::Layer* layer) override {
// Old `AppListView`'s layer can be cloned and then destroyed by
// `ScreenRotationAnimator`. In this case run `complete_callback_` and
// destroy `this`.
std::move(complete_callback_).Run(false);
delete this;
}
private:
const raw_ptr<ui::Layer> layer_;
AnimationCompleteCallback complete_callback_;
};
void UpdateTabletModeTransitionAnimationSettings(
FullscreenLauncherAnimationObserver* animation_observer,
ui::ScopedLayerAnimationSettings* settings) {
settings->SetTransitionDuration(kFullscreenLauncherTransitionDuration);
settings->SetTweenType(gfx::Tween::FAST_OUT_SLOW_IN);
settings->SetPreemptionStrategy(
ui::LayerAnimator::IMMEDIATELY_ANIMATE_TO_NEW_TARGET);
if (animation_observer)
settings->AddObserver(animation_observer);
}
// Implicit animation observer that runs a scoped closure runner, and deletes
// itself when the observed implicit animations complete.
class CallbackRunnerLayerAnimationObserver
: public ui::ImplicitAnimationObserver {
public:
explicit CallbackRunnerLayerAnimationObserver(
base::ScopedClosureRunner closure_runner)
: closure_runner_(std::move(closure_runner)) {}
~CallbackRunnerLayerAnimationObserver() override = default;
// ui::ImplicitAnimationObserver:
void OnImplicitAnimationsCompleted() override {
closure_runner_.RunAndReset();
delete this;
}
private:
base::ScopedClosureRunner closure_runner_;
};
} // namespace
constexpr std::array<int, 8>
AppListPresenterImpl::kIdsOfContainersThatWontHideAppList;
AppListPresenterImpl::AppListPresenterImpl(AppListControllerImpl* controller)
: controller_(controller) {
DCHECK(controller_);
}
AppListPresenterImpl::~AppListPresenterImpl() {
if (view_ && view_->GetWidget()) {
view_->GetWidget()->CloseNow();
}
CHECK(!views::WidgetObserver::IsInObserverList());
}
aura::Window* AppListPresenterImpl::GetWindow() const {
return is_target_visibility_show_ && view_
? view_->GetWidget()->GetNativeWindow()
: nullptr;
}
void AppListPresenterImpl::Show(AppListViewState preferred_state,
int64_t display_id,
base::TimeTicks event_time_stamp,
std::optional<AppListShowSource> show_source) {
if (is_target_visibility_show_)
return;
if (!Shell::Get()->GetRootWindowForDisplayId(display_id)) {
LOG(ERROR) << "Root window does not exist for display: " << display_id;
return;
}
// TODO(https://crbug.com/1307871): Remove this when the linked crash gets
// diagnosed - the crash is possible if app list gets dismissed while being
// shown. `showing_app_list_` in intended to catch this case.
showing_app_list_ = true;
is_target_visibility_show_ = true;
RequestPresentationTime(display_id, event_time_stamp);
if (!view_) {
AppListView* view = new AppListView(controller_);
view->InitView(
controller_->GetFullscreenLauncherContainerForDisplayId(display_id));
SetView(view);
view_->GetWidget()->GetNativeWindow()->TrackOcclusionState();
}
OnVisibilityWillChange(GetTargetVisibility(), display_id);
controller_->UpdateFullscreenLauncherContainer(display_id);
// App list needs to know the new shelf layout in order to calculate its
// UI layout when AppListView visibility changes.
Shelf* shelf =
Shelf::ForWindow(view_->GetWidget()->GetNativeView()->GetRootWindow());
shelf->shelf_layout_manager()->UpdateAutoHideState();
// If presenter is observing a shelf instance different than `shelf`, it's
// because the app list view on the associated display is closing. It's safe
// to remove this observation (given that shelf background changes should not
// affect appearance of a closing app list view).
shelf_observer_.Reset();
shelf_observer_.Observe(shelf);
// By setting us as a drag-and-drop recipient, the app list knows that we can
// handle items. Do this on every show because |view_| can be reused after a
// monitor is disconnected but that monitor's ShelfView and
// ScrollableShelfView are deleted. https://crbug.com/1163332
view_->SetDragAndDropHostOfCurrentAppList(
shelf->shelf_widget()->GetDragAndDropHostForAppList());
std::unique_ptr<AppListView::ScopedAccessibilityAnnouncementLock>
scoped_accessibility_lock;
// App list view state accessibility alerts should be suppressed when the app
// list view is shown by the assistant. The assistant UI should handle its
// own accessibility notifications.
if (show_source && *show_source == AppListShowSource::kAssistantEntryPoint) {
scoped_accessibility_lock =
std::make_unique<AppListView::ScopedAccessibilityAnnouncementLock>(
view_);
}
auto* layer = view_->GetWidget()->GetNativeWindow()->layer();
bool has_aborted_animation = false;
if (layer->GetAnimator()->is_animating()) {
layer->GetAnimator()->AbortAllAnimations();
// Mark that animation was aborted in order to keep initial opacity and
// scale values in sync.
has_aborted_animation = true;
}
// `0.01f` prevents a DCHECK error (widgets cannot be shown when visible and
// fully transparent at the same time).
const float initial_opacity =
layer->opacity() == 0.0f ? 0.01f : layer->opacity();
layer->SetOpacity(initial_opacity);
view_->Show(preferred_state);
// If there was no aborted dismiss animation before - set the initial value,
// otherwise smoothly continue where it was aborted.
if (!has_aborted_animation) {
layer->SetTransform(
gfx::GetScaleTransform(gfx::Rect(layer->size()).CenterPoint(),
kFullscreenLauncherFadeAnimationScale));
}
FullscreenLauncherAnimationObserver::AnimationCompleteCallback
animation_complete_callback = base::BindOnce(
&AppListPresenterImpl::OnTabletToClamshellTransitionAnimationDone,
weak_ptr_factory_.GetWeakPtr(), /*target_visibility=*/true);
auto* animation_observer = new FullscreenLauncherAnimationObserver(
layer, std::move(animation_complete_callback));
UpdateScaleAndOpacityForHomeLauncher(
1.0f, 1.0f, std::nullopt,
base::BindRepeating(&UpdateTabletModeTransitionAnimationSettings,
animation_observer));
SnapAppListBoundsToDisplayEdge();
event_filter_ =
std::make_unique<AppListPresenterEventFilter>(controller_, this, view_);
controller_->ViewShown(display_id);
showing_app_list_ = false;
OnVisibilityChanged(GetTargetVisibility(), display_id);
}
void AppListPresenterImpl::Dismiss(base::TimeTicks event_time_stamp) {
if (!is_target_visibility_show_)
return;
// If the app list target visibility is shown, there should be an existing
// view.
DCHECK(view_);
// TODO(https://crbug.com/1307871): Remove this when the linked crash gets
// diagnosed.
CHECK(!showing_app_list_);
is_target_visibility_show_ = false;
RequestPresentationTime(GetDisplayId(), event_time_stamp);
// Hide the active window if it is a transient descendant of |view_|'s widget.
aura::Window* window = view_->GetWidget()->GetNativeWindow();
aura::Window* active_window =
::wm::GetActivationClient(window->GetRootWindow())->GetActiveWindow();
if (active_window) {
aura::Window* transient_parent =
::wm::TransientWindowManager::GetOrCreate(active_window)
->transient_parent();
while (transient_parent) {
if (window == transient_parent) {
active_window->Hide();
break;
}
transient_parent =
::wm::TransientWindowManager::GetOrCreate(transient_parent)
->transient_parent();
}
}
// The dismissal may have occurred in response to the app list losing
// activation. Otherwise, our widget is currently active. When the animation
// completes we'll hide the widget, changing activation. If a menu is shown
// before the animation completes then the activation change triggers the menu
// to close. By deactivating now we ensure there is no activation change when
// the animation completes and any menus stay open.
if (view_->GetWidget()->IsActive())
view_->GetWidget()->Deactivate();
event_filter_.reset();
if (view_->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();
}
controller_->ViewClosing();
OnVisibilityWillChange(GetTargetVisibility(), GetDisplayId());
if (!view_->GetWidget()->GetNativeWindow()->is_destroying()) {
auto* const layer = view_->GetWidget()->GetNativeWindow()->layer();
FullscreenLauncherAnimationObserver::AnimationCompleteCallback
animation_complete_callback = base::BindOnce(
&AppListPresenterImpl::OnTabletToClamshellTransitionAnimationDone,
weak_ptr_factory_.GetWeakPtr(), /*target_visibility=*/false);
auto* animation_observer = new FullscreenLauncherAnimationObserver(
layer, std::move(animation_complete_callback));
// Aborts show animation (if it's running, noop otherwise). This helps to
// run dismiss animation smoothly from the aborted scale/opacity points.
layer->GetAnimator()->AbortAllAnimations();
UpdateScaleAndOpacityForHomeLauncher(
kFullscreenLauncherFadeAnimationScale, 0.0f, std::nullopt,
base::BindRepeating(&UpdateTabletModeTransitionAnimationSettings,
animation_observer));
view_->SetState(AppListViewState::kClosed);
}
view_->SetDragAndDropHostOfCurrentAppList(nullptr);
base::RecordAction(base::UserMetricsAction("Launcher_Dismiss"));
}
void AppListPresenterImpl::SetViewVisibility(bool visible) {
if (!view_)
return;
view_->OnAppListVisibilityWillChange(visible);
view_->SetVisible(visible);
}
bool AppListPresenterImpl::HandleCloseOpenFolder() {
return is_target_visibility_show_ && view_ && view_->HandleCloseOpenFolder();
}
void AppListPresenterImpl::UpdateForNewSortingOrder(
const std::optional<AppListSortOrder>& new_order,
bool animate,
base::OnceClosure update_position_closure) {
if (!view_)
return;
base::OnceClosure done_closure;
if (animate) {
// The search box should ignore a11y events during the reorder animation
// so that the announcement of app list reorder is made before that of
// focus change.
SetViewIgnoredForAccessibility(view_->search_box_view(), true);
// Focus on the search box before starting the reorder animation to prevent
// focus moving through app list items as they're being hidden for order
// update animation.
view_->search_box_view()->search_box()->RequestFocus();
done_closure =
base::BindOnce(&AppListPresenterImpl::OnAppListReorderAnimationDone,
weak_ptr_factory_.GetWeakPtr());
}
view_->app_list_main_view()
->contents_view()
->apps_container_view()
->UpdateForNewSortingOrder(new_order, animate,
std::move(update_position_closure),
std::move(done_closure));
}
void AppListPresenterImpl::UpdateContinueSectionVisibility() {
if (!view_)
return;
view_->app_list_main_view()
->contents_view()
->apps_container_view()
->UpdateContinueSectionVisibility();
}
bool AppListPresenterImpl::IsVisibleDeprecated() const {
return controller_->IsVisible(GetDisplayId());
}
bool AppListPresenterImpl::IsAtLeastPartiallyVisible() const {
const auto* window = GetWindow();
return window &&
window->GetOcclusionState() == aura::Window::OcclusionState::VISIBLE;
}
bool AppListPresenterImpl::GetTargetVisibility() const {
return is_target_visibility_show_;
}
void AppListPresenterImpl::UpdateScaleAndOpacityForHomeLauncher(
float scale,
float opacity,
std::optional<TabletModeAnimationTransition> transition,
UpdateHomeLauncherAnimationSettingsCallback callback) {
// Exiting from overview in clamshell mode should not affect the hidden
// fullscreen launcher.
if (!view_ || view_->app_list_state() == AppListViewState::kClosed)
return;
ui::Layer* layer = view_->GetWidget()->GetNativeWindow()->layer();
if (layer->GetAnimator()->is_animating()) {
layer->GetAnimator()->StopAnimating();
// Reset the animation metrics reporter when the animation is interrupted.
view_->ResetTransitionMetricsReporter();
}
std::optional<ui::ScopedLayerAnimationSettings> settings;
if (!callback.is_null()) {
settings.emplace(layer->GetAnimator());
callback.Run(&settings.value());
}
// The animation metrics reporter will run for opacity and transform
// animations separately - to avoid reporting duplicated values, add the
// reported for transform animation only.
layer->SetOpacity(opacity);
std::optional<ui::AnimationThroughputReporter> reporter;
if (settings.has_value() && transition.has_value()) {
view_->OnTabletModeAnimationTransitionNotified(*transition);
reporter.emplace(settings->GetAnimator(),
metrics_util::ForSmoothnessV3(
view_->GetStateTransitionMetricsReportCallback()));
}
gfx::Transform transform =
gfx::GetScaleTransform(gfx::Rect(layer->size()).CenterPoint(), scale);
layer->SetTransform(transform);
}
void AppListPresenterImpl::ShowEmbeddedAssistantUI(bool show) {
if (view_)
view_->app_list_main_view()->contents_view()->ShowEmbeddedAssistantUI(show);
}
bool AppListPresenterImpl::IsShowingEmbeddedAssistantUI() const {
if (view_) {
return view_->app_list_main_view()
->contents_view()
->IsShowingEmbeddedAssistantUI();
}
return false;
}
////////////////////////////////////////////////////////////////////////////////
// AppListPresenterImpl, private:
void AppListPresenterImpl::SetView(AppListView* view) {
DCHECK(view_ == nullptr);
DCHECK(is_target_visibility_show_);
view_ = view;
views::Widget* widget = view_->GetWidget();
widget->AddObserver(this);
aura::client::GetFocusClient(widget->GetNativeView())->AddObserver(this);
// Sync the |onscreen_keyboard_shown_| in case |view_| is not initiated when
// the on-screen is shown.
view_->set_onscreen_keyboard_shown(controller_->onscreen_keyboard_shown());
}
void AppListPresenterImpl::ResetView() {
if (!view_)
return;
views::Widget* widget = view_->GetWidget();
widget->RemoveObserver(this);
aura::client::GetFocusClient(widget->GetNativeView())->RemoveObserver(this);
view_ = nullptr;
}
int64_t AppListPresenterImpl::GetDisplayId() const {
views::Widget* widget = view_ ? view_->GetWidget() : nullptr;
if (!widget)
return display::kInvalidDisplayId;
return display::Screen::GetScreen()
->GetDisplayNearestView(widget->GetNativeView())
.id();
}
void AppListPresenterImpl::OnVisibilityChanged(bool visible,
int64_t display_id) {
controller_->OnVisibilityChanged(visible, display_id);
}
void AppListPresenterImpl::OnVisibilityWillChange(bool visible,
int64_t display_id) {
controller_->OnVisibilityWillChange(visible, display_id);
}
void AppListPresenterImpl::OnClosed() {
if (!is_target_visibility_show_)
shelf_observer_.Reset();
}
////////////////////////////////////////////////////////////////////////////////
// AppListPresenterImpl, aura::client::FocusChangeObserver implementation:
void AppListPresenterImpl::OnWindowFocused(aura::Window* gained_focus,
aura::Window* lost_focus) {
// Do not focus app list window in the Kiosk mode.
if (Shell::Get()->session_controller()->IsRunningInAppMode())
return;
if (!view_ || !is_target_visibility_show_)
return;
int gained_focus_container_id = kShellWindowId_Invalid;
if (gained_focus) {
gained_focus_container_id = gained_focus->GetId();
const aura::Window* container = ash::GetContainerForWindow(gained_focus);
if (container)
gained_focus_container_id = container->GetId();
}
aura::Window* applist_window = view_->GetWidget()->GetNativeView();
const aura::Window* applist_container = applist_window->parent();
// An AppList dialog window, or a child window of the system tray, may
// take focus from the AppList window. Don't consider this a visibility
// change since the app list is still visible for the most part.
const bool gained_focus_hides_app_list =
gained_focus_container_id != kShellWindowId_Invalid &&
!base::Contains(kIdsOfContainersThatWontHideAppList,
gained_focus_container_id);
const bool app_list_gained_focus = applist_window->Contains(gained_focus) ||
applist_container->Contains(gained_focus);
const bool app_list_lost_focus =
gained_focus ? gained_focus_hides_app_list
: (lost_focus && applist_container->Contains(lost_focus));
// Either the app list has just gained focus, in which case it is already
// visible or will very soon be, or it has neither gained nor lost focus
// and it might still be partially visible just because the focused window
// doesn't occlude it completely.
const bool visible = app_list_gained_focus ||
(IsAtLeastPartiallyVisible() && !app_list_lost_focus);
if (visible != controller_->IsVisible(GetDisplayId())) {
if (app_list_gained_focus)
view_->OnHomeLauncherGainingFocusWithoutAnimation();
OnVisibilityChanged(visible, GetDisplayId());
} else {
// In tablet mode, when Assistant UI lost focus after other new App window
// opened, we should reset the view.
if (app_list_lost_focus && IsShowingEmbeddedAssistantUI())
view_->Back();
}
if (app_list_gained_focus)
base::RecordAction(base::UserMetricsAction("AppList_WindowFocused"));
}
////////////////////////////////////////////////////////////////////////////////
// AppListPresenterImpl, views::WidgetObserver implementation:
void AppListPresenterImpl::OnWidgetDestroying(views::Widget* widget) {
DCHECK_EQ(view_->GetWidget(), widget);
if (is_target_visibility_show_)
Dismiss(base::TimeTicks());
ResetView();
}
void AppListPresenterImpl::OnWidgetDestroyed(views::Widget* widget) {
OnClosed();
}
void AppListPresenterImpl::OnWidgetVisibilityChanged(views::Widget* widget,
bool visible) {
DCHECK_EQ(view_->GetWidget(), widget);
OnVisibilityChanged(visible, GetDisplayId());
}
void AppListPresenterImpl::RequestPresentationTime(
int64_t display_id,
base::TimeTicks event_time_stamp) {
if (event_time_stamp.is_null())
return;
aura::Window* root_window =
Shell::Get()->GetRootWindowForDisplayId(display_id);
if (!root_window)
return;
ui::Compositor* compositor = root_window->layer()->GetCompositor();
if (!compositor)
return;
compositor->RequestSuccessfulPresentationTimeForNextFrame(
base::BindOnce(&DidPresentCompositorFrame, event_time_stamp,
is_target_visibility_show_));
}
////////////////////////////////////////////////////////////////////////////////
// display::DisplayObserver implementation:
void AppListPresenterImpl::OnDisplayMetricsChanged(
const display::Display& display,
uint32_t changed_metrics) {
if (!GetWindow())
return;
view_->OnParentWindowBoundsChanged();
SnapAppListBoundsToDisplayEdge();
}
void AppListPresenterImpl::OnShelfShuttingDown() {
shelf_observer_.Reset();
if (view_)
view_->SetDragAndDropHostOfCurrentAppList(nullptr);
}
void AppListPresenterImpl::SnapAppListBoundsToDisplayEdge() {
CHECK(view_ && view_->GetWidget());
aura::Window* window = view_->GetWidget()->GetNativeView();
const gfx::Rect bounds =
controller_->SnapBoundsToDisplayEdge(window->bounds());
window->SetBounds(bounds);
}
void AppListPresenterImpl::OnAppListReorderAnimationDone() {
if (!view_)
return;
// Re-enable the search box to handle a11y events.
SetViewIgnoredForAccessibility(view_->search_box_view(), false);
}
void AppListPresenterImpl::OnTabletToClamshellTransitionAnimationDone(
bool target_visibility,
bool aborted) {
if (!view_)
return;
auto* window = view_->GetWidget()->GetNativeWindow();
if (!aborted) {
if (target_visibility) {
view_->DeprecatedLayoutImmediately();
} else if (!target_visibility && !window->is_destroying()) {
window->Hide();
OnClosed();
}
}
controller_->OnStateTransitionAnimationCompleted(view_->app_list_state(),
aborted);
}
} // namespace ash