blob: 2e43ef299ac2dd2bc94164dd42b1b75939c137cc [file] [log] [blame]
// Copyright 2019 The Chromium Authors. All rights reserved.
// Use of this source code is governed by a BSD-style license that can be
// found in the LICENSE file.
#include "ash/app_list/app_list_controller_impl.h"
#include "ash/app_list/app_list_metrics.h"
#include "ash/app_list/views/app_list_main_view.h"
#include "ash/app_list/views/app_list_view.h"
#include "ash/app_list/views/contents_view.h"
#include "ash/app_list/views/expand_arrow_view.h"
#include "ash/app_list/views/search_box_view.h"
#include "ash/home_screen/home_launcher_gesture_handler.h"
#include "ash/home_screen/home_screen_controller.h"
#include "ash/keyboard/ash_keyboard_controller.h"
#include "ash/public/cpp/presentation_time_recorder.h"
#include "ash/shelf/shelf.h"
#include "ash/shelf/shelf_widget.h"
#include "ash/shell.h"
#include "ash/test/ash_test_base.h"
#include "ash/wm/tablet_mode/tablet_mode_controller.h"
#include "ash/wm/window_state.h"
#include "ash/wm/window_util.h"
#include "base/strings/string16.h"
#include "base/strings/utf_string_conversions.h"
#include "base/test/metrics/histogram_tester.h"
#include "ui/events/test/event_generator.h"
namespace ash {
namespace {
using ::app_list::kAppListResultLaunchIndexAndQueryLength;
using ::app_list::kAppListTileLaunchIndexAndQueryLength;
using ::app_list::SearchResultLaunchLocation;
bool IsTabletMode() {
return Shell::Get()
->tablet_mode_controller()
->IsTabletModeWindowManagerEnabled();
}
app_list::AppListView* GetAppListView() {
return Shell::Get()->app_list_controller()->presenter()->GetView();
}
bool GetExpandArrowViewVisibility() {
return GetAppListView()
->app_list_main_view()
->contents_view()
->expand_arrow_view()
->GetVisible();
}
app_list::SearchBoxView* GetSearchBoxView() {
return GetAppListView()
->app_list_main_view()
->contents_view()
->GetSearchBoxView();
}
aura::Window* GetVirtualKeyboardWindow() {
return Shell::Get()
->ash_keyboard_controller()
->keyboard_controller()
->GetKeyboardWindow();
}
void ShowAppListNow() {
Shell::Get()->app_list_controller()->presenter()->Show(
display::Screen::GetScreen()->GetPrimaryDisplay().id(),
base::TimeTicks::Now());
}
void DismissAppListNow() {
Shell::Get()->app_list_controller()->presenter()->Dismiss(
base::TimeTicks::Now());
}
aura::Window* GetAppListViewNativeWindow() {
return GetAppListView()->get_fullscreen_widget_for_test()->GetNativeView();
}
void SetSearchText(AppListControllerImpl* controller, const std::string& text) {
controller->GetSearchModel()->search_box()->Update(base::ASCIIToUTF16(text),
false);
}
} // namespace
class AppListControllerImplTest : public AshTestBase {
public:
AppListControllerImplTest() = default;
~AppListControllerImplTest() override = default;
std::unique_ptr<aura::Window> CreateTestWindow() {
return AshTestBase::CreateTestWindow(gfx::Rect(0, 0, 400, 400));
}
private:
DISALLOW_COPY_AND_ASSIGN(AppListControllerImplTest);
};
// Hide the expand arrow view in tablet mode when there is no activatable window
// (see https://crbug.com/923089).
TEST_F(AppListControllerImplTest, UpdateExpandArrowViewVisibility) {
// Turn on the tablet mode.
Shell::Get()->tablet_mode_controller()->EnableTabletModeWindowManager(true);
EXPECT_TRUE(IsTabletMode());
// No activatable windows. So hide the expand arrow view.
EXPECT_FALSE(GetExpandArrowViewVisibility());
std::unique_ptr<aura::Window> w1(CreateTestWindow());
std::unique_ptr<aura::Window> w2(CreateTestWindow());
// Activate w1 then press home launcher button. Expand arrow view should show
// because w1 still exists.
wm::ActivateWindow(w1.get());
Shell::Get()
->home_screen_controller()
->home_launcher_gesture_handler()
->ShowHomeLauncher(display::Screen::GetScreen()->GetPrimaryDisplay());
EXPECT_EQ(WindowStateType::kMinimized,
wm::GetWindowState(w1.get())->GetStateType());
EXPECT_TRUE(GetExpandArrowViewVisibility());
// Activate w2 then close w1. w2 still exists so expand arrow view shows.
wm::ActivateWindow(w2.get());
w1.reset();
EXPECT_TRUE(GetExpandArrowViewVisibility());
// No activatable windows. Hide the expand arrow view.
w2.reset();
EXPECT_FALSE(GetExpandArrowViewVisibility());
}
// In clamshell mode, when the AppListView's bottom is on the display edge
// and app list state is HALF, the rounded corners should be hidden
// (https://crbug.com/942084).
TEST_F(AppListControllerImplTest, HideRoundingCorners) {
Shell::Get()->ash_keyboard_controller()->SetEnableFlag(
keyboard::mojom::KeyboardEnableFlag::kShelfEnabled);
// Show the app list view and click on the search box with mouse. So the
// VirtualKeyboard is shown.
ShowAppListNow();
GetSearchBoxView()->SetSearchBoxActive(true, ui::ET_MOUSE_PRESSED);
// Wait until the virtual keyboard shows on the screen.
base::RunLoop().RunUntilIdle();
EXPECT_TRUE(GetVirtualKeyboardWindow()->IsVisible());
// Test the following things:
// (1) AppListView is at the top of the screen.
// (2) AppListView's state is HALF.
// (3) AppListBackgroundShield is translated to hide the rounded corners.
aura::Window* native_window =
GetAppListView()->get_fullscreen_widget_for_test()->GetNativeView();
gfx::Rect app_list_screen_bounds = native_window->GetBoundsInScreen();
EXPECT_EQ(0, app_list_screen_bounds.y());
EXPECT_EQ(ash::mojom::AppListViewState::kHalf,
GetAppListView()->app_list_state());
gfx::Transform expected_transform;
expected_transform.Translate(0, -app_list::kAppListBackgroundRadius);
EXPECT_EQ(
expected_transform,
GetAppListView()->GetAppListBackgroundShieldForTest()->GetTransform());
// Set the search box inactive and wait until the virtual keyboard is hidden.
GetSearchBoxView()->SetSearchBoxActive(false, ui::ET_MOUSE_PRESSED);
base::RunLoop().RunUntilIdle();
EXPECT_EQ(nullptr, GetVirtualKeyboardWindow());
// Test that the rounded corners should show again.
expected_transform = gfx::Transform();
EXPECT_EQ(
expected_transform,
GetAppListView()->GetAppListBackgroundShieldForTest()->GetTransform());
}
// Verifies that in clamshell mode the bounds of AppListView are correct when
// the AppListView is in PEEKING state and the virtual keyboard is enabled (see
// https://crbug.com/944233).
TEST_F(AppListControllerImplTest, CheckAppListViewBoundsWhenVKeyboardEnabled) {
Shell::Get()->ash_keyboard_controller()->SetEnableFlag(
keyboard::mojom::KeyboardEnableFlag::kShelfEnabled);
// Show the AppListView and click on the search box with mouse. So the
// VirtualKeyboard is shown. Wait until the virtual keyboard shows.
ShowAppListNow();
GetSearchBoxView()->SetSearchBoxActive(true, ui::ET_MOUSE_PRESSED);
base::RunLoop().RunUntilIdle();
EXPECT_TRUE(GetVirtualKeyboardWindow()->IsVisible());
// Hide the AppListView. Wait until the virtual keyboard is hidden as well.
DismissAppListNow();
base::RunLoop().RunUntilIdle();
EXPECT_EQ(nullptr, GetVirtualKeyboardWindow());
// Show the AppListView again. Check the following things:
// (1) Virtual keyboard does not show.
// (2) AppListView is in PEEKING state.
// (3) AppListView's bounds are the same as the preferred bounds for
// the PEEKING state.
ShowAppListNow();
base::RunLoop().RunUntilIdle();
EXPECT_EQ(ash::mojom::AppListViewState::kPeeking,
GetAppListView()->app_list_state());
EXPECT_EQ(nullptr, GetVirtualKeyboardWindow());
EXPECT_EQ(GetAppListView()->GetPreferredWidgetBoundsForState(
ash::mojom::AppListViewState::kPeeking),
GetAppListViewNativeWindow()->bounds());
}
// Verifies that in tablet mode, the AppListView has correct bounds when the
// virtual keyboard is dismissed (see https://crbug.com/944133).
TEST_F(AppListControllerImplTest, CheckAppListViewBoundsWhenDismissVKeyboard) {
Shell::Get()->ash_keyboard_controller()->SetEnableFlag(
keyboard::mojom::KeyboardEnableFlag::kShelfEnabled);
// Show the AppListView and click on the search box with mouse so the
// VirtualKeyboard is shown. Wait until the virtual keyboard shows.
ShowAppListNow();
GetSearchBoxView()->SetSearchBoxActive(true, ui::ET_MOUSE_PRESSED);
base::RunLoop().RunUntilIdle();
EXPECT_TRUE(GetVirtualKeyboardWindow()->IsVisible());
// Turn on the tablet mode. The virtual keyboard should still show.
Shell::Get()->tablet_mode_controller()->EnableTabletModeWindowManager(true);
EXPECT_TRUE(IsTabletMode());
EXPECT_TRUE(GetVirtualKeyboardWindow()->IsVisible());
// Close the virtual keyboard. Wait until it is hidden.
Shell::Get()->ash_keyboard_controller()->HideKeyboard(
mojom::HideReason::kUser);
base::RunLoop().RunUntilIdle();
EXPECT_EQ(nullptr, GetVirtualKeyboardWindow());
// Check the following things:
// (1) AppListView's state is FULLSCREEN_SEARCH
// (2) AppListView's bounds are the same as the preferred bounds for
// the FULLSCREEN_SEARCH state.
EXPECT_EQ(ash::mojom::AppListViewState::kFullscreenSearch,
GetAppListView()->app_list_state());
EXPECT_EQ(GetAppListView()->GetPreferredWidgetBoundsForState(
ash::mojom::AppListViewState::kFullscreenSearch),
GetAppListViewNativeWindow()->bounds());
}
class AppListControllerImplMetricsTest : public AshTestBase {
public:
AppListControllerImplMetricsTest() = default;
~AppListControllerImplMetricsTest() override = default;
void SetUp() override {
AshTestBase::SetUp();
controller_ = ash::Shell::Get()->app_list_controller();
ash::PresentationTimeRecorder::SetReportPresentationTimeImmediatelyForTest(
true);
}
void TearDown() override {
ash::PresentationTimeRecorder::SetReportPresentationTimeImmediatelyForTest(
false);
AshTestBase::TearDown();
}
AppListControllerImpl* controller_;
const base::HistogramTester histogram_tester_;
private:
DISALLOW_COPY_AND_ASSIGN(AppListControllerImplMetricsTest);
};
TEST_F(AppListControllerImplMetricsTest, LogSingleResultListClick) {
histogram_tester_.ExpectTotalCount(kAppListResultLaunchIndexAndQueryLength,
0);
SetSearchText(controller_, "");
controller_->LogResultLaunchHistogram(SearchResultLaunchLocation::kResultList,
4);
histogram_tester_.ExpectUniqueSample(kAppListResultLaunchIndexAndQueryLength,
4, 1);
}
TEST_F(AppListControllerImplMetricsTest, LogSingleTileListClick) {
histogram_tester_.ExpectTotalCount(kAppListTileLaunchIndexAndQueryLength, 0);
SetSearchText(controller_, "aaaa");
controller_->LogResultLaunchHistogram(SearchResultLaunchLocation::kTileList,
4);
histogram_tester_.ExpectUniqueSample(kAppListTileLaunchIndexAndQueryLength,
32, 1);
}
TEST_F(AppListControllerImplMetricsTest, LogOneClickInEveryBucket) {
histogram_tester_.ExpectTotalCount(kAppListResultLaunchIndexAndQueryLength,
0);
for (int query_length = 0; query_length < 11; ++query_length) {
const std::string query(query_length, 'a');
for (int click_index = 0; click_index < 7; ++click_index) {
SetSearchText(controller_, query);
controller_->LogResultLaunchHistogram(
SearchResultLaunchLocation::kResultList, click_index);
}
}
histogram_tester_.ExpectTotalCount(kAppListResultLaunchIndexAndQueryLength,
77);
for (int query_length = 0; query_length < 11; ++query_length) {
for (int click_index = 0; click_index < 7; ++click_index) {
histogram_tester_.ExpectBucketCount(
kAppListResultLaunchIndexAndQueryLength,
7 * query_length + click_index, 1);
}
}
}
TEST_F(AppListControllerImplMetricsTest, LogManyClicksInOneBucket) {
histogram_tester_.ExpectTotalCount(kAppListTileLaunchIndexAndQueryLength, 0);
SetSearchText(controller_, "aaaa");
for (int i = 0; i < 50; ++i)
controller_->LogResultLaunchHistogram(SearchResultLaunchLocation::kTileList,
4);
histogram_tester_.ExpectUniqueSample(kAppListTileLaunchIndexAndQueryLength,
32, 50);
}
// Verifies that the PresentationTimeRecorder works correctly for the home
// launcher gesture drag in tablet mode (https://crbug.com/947105).
TEST_F(AppListControllerImplMetricsTest,
PresentationTimeRecordedForDragInTabletMode) {
// Wait until the construction of TabletModeController finishes.
base::RunLoop().RunUntilIdle();
// Turn on the tablet mode.
Shell::Get()->tablet_mode_controller()->EnableTabletModeWindowManager(true);
EXPECT_TRUE(IsTabletMode());
// Create a window then press the home launcher button. Expect that |w| is
// hidden.
std::unique_ptr<aura::Window> w(
AshTestBase::CreateTestWindow(gfx::Rect(0, 0, 400, 400)));
Shell::Get()
->home_screen_controller()
->home_launcher_gesture_handler()
->ShowHomeLauncher(display::Screen::GetScreen()->GetPrimaryDisplay());
EXPECT_FALSE(w->IsVisible());
EXPECT_EQ(mojom::AppListViewState::kFullscreenAllApps,
GetAppListView()->app_list_state());
int delta_y = 1;
gfx::Point start = GetAppListView()
->get_fullscreen_widget_for_test()
->GetWindowBoundsInScreen()
.top_right();
base::TimeTicks timestamp = base::TimeTicks::Now();
// Emulate to drag the launcher downward.
// Send SCROLL_START event. Check the presentation metrics values.
ui::GestureEvent start_event = ui::GestureEvent(
start.x(), start.y(), ui::EF_NONE, timestamp,
ui::GestureEventDetails(ui::ET_GESTURE_SCROLL_BEGIN, 0, delta_y));
GetAppListView()->OnGestureEvent(&start_event);
histogram_tester_.ExpectTotalCount(
"Apps.StateTransition.Drag.PresentationTime.TabletMode", 0);
histogram_tester_.ExpectTotalCount(
"Apps.StateTransition.Drag.PresentationTime.MaxLatency.TabletMode", 0);
// Send SCROLL_UPDATE event. Check the presentation metrics values.
timestamp += base::TimeDelta::FromMilliseconds(25);
delta_y += 20;
start.Offset(0, 1);
ui::GestureEvent update_event = ui::GestureEvent(
start.x(), start.y(), ui::EF_NONE, timestamp,
ui::GestureEventDetails(ui::ET_GESTURE_SCROLL_UPDATE, 0, delta_y));
GetAppListView()->OnGestureEvent(&update_event);
histogram_tester_.ExpectTotalCount(
"Apps.StateTransition.Drag.PresentationTime.TabletMode", 1);
histogram_tester_.ExpectTotalCount(
"Apps.StateTransition.Drag.PresentationTime.MaxLatency.TabletMode", 0);
// Send SCROLL_END event. Check the presentation metrics values.
timestamp += base::TimeDelta::FromMilliseconds(25);
start.Offset(0, 1);
ui::GestureEvent end_event =
ui::GestureEvent(start.x(), start.y() + delta_y, ui::EF_NONE, timestamp,
ui::GestureEventDetails(ui::ET_GESTURE_END));
GetAppListView()->OnGestureEvent(&end_event);
histogram_tester_.ExpectTotalCount(
"Apps.StateTransition.Drag.PresentationTime.TabletMode", 1);
histogram_tester_.ExpectTotalCount(
"Apps.StateTransition.Drag.PresentationTime.MaxLatency.TabletMode", 1);
// After the gesture scroll event ends, the window shows.
EXPECT_TRUE(w->IsVisible());
ASSERT_TRUE(IsTabletMode());
}
// One edge case may do harm to the presentation metrics reporter for tablet
// mode: the user may keep pressing on launcher while exiting the tablet mode by
// rotating the lid. In this situation, OnHomeLauncherDragEnd is not triggered.
// It is handled correctly now because the AppListView is always closed after
// exiting the tablet mode. But it still has potential risk to break in future.
// Write this test case for precaution (https://crbug.com/947105).
TEST_F(AppListControllerImplMetricsTest,
PresentationMetricsForTabletNotRecordedInClamshell) {
// Wait until the construction of TabletModeController finishes.
base::RunLoop().RunUntilIdle();
// Turn on the tablet mode.
Shell::Get()->tablet_mode_controller()->EnableTabletModeWindowManager(true);
EXPECT_TRUE(IsTabletMode());
// Create a window then press the home launcher button. Expect that |w| is
// hidden.
std::unique_ptr<aura::Window> w(
AshTestBase::CreateTestWindow(gfx::Rect(0, 0, 400, 400)));
Shell::Get()
->home_screen_controller()
->home_launcher_gesture_handler()
->ShowHomeLauncher(display::Screen::GetScreen()->GetPrimaryDisplay());
EXPECT_FALSE(w->IsVisible());
EXPECT_EQ(mojom::AppListViewState::kFullscreenAllApps,
GetAppListView()->app_list_state());
gfx::Point start = GetAppListView()
->get_fullscreen_widget_for_test()
->GetWindowBoundsInScreen()
.top_right();
base::TimeTicks timestamp = base::TimeTicks::Now();
// Emulate to drag the launcher downward.
// Send SCROLL_START event. Check the presentation metrics values.
ui::GestureEvent start_event = ui::GestureEvent(
start.x(), start.y(), ui::EF_NONE, timestamp,
ui::GestureEventDetails(ui::ET_GESTURE_SCROLL_BEGIN, 0, 1));
GetAppListView()->OnGestureEvent(&start_event);
// Turn off the tablet mode before scrolling is finished.
Shell::Get()->tablet_mode_controller()->EnableTabletModeWindowManager(false);
EXPECT_FALSE(IsTabletMode());
base::RunLoop().RunUntilIdle();
EXPECT_FALSE(GetAppListView());
// Check metrics initial values.
histogram_tester_.ExpectTotalCount(
"Apps.StateTransition.Drag.PresentationTime.TabletMode", 0);
histogram_tester_.ExpectTotalCount(
"Apps.StateTransition.Drag.PresentationTime.MaxLatency.TabletMode", 0);
// Emulate to drag launcher from shelf. Then verifies the following things:
// (1) Metrics values for tablet mode are not recorded.
// (2) Metrics values for clamshell mode are recorded correctly.
gfx::Rect shelf_bounds =
GetPrimaryShelf()->shelf_widget()->GetWindowBoundsInScreen();
shelf_bounds.Intersect(
display::Screen::GetScreen()->GetPrimaryDisplay().bounds());
gfx::Point shelf_center = shelf_bounds.CenterPoint();
gfx::Point target_point =
display::Screen::GetScreen()->GetPrimaryDisplay().bounds().CenterPoint();
ui::test::EventGenerator* generator = GetEventGenerator();
generator->GestureScrollSequence(shelf_center, target_point,
base::TimeDelta::FromMicroseconds(500), 1);
base::RunLoop().RunUntilIdle();
EXPECT_EQ(mojom::AppListViewState::kFullscreenAllApps,
GetAppListView()->app_list_state());
histogram_tester_.ExpectTotalCount(
"Apps.StateTransition.Drag.PresentationTime.TabletMode", 0);
histogram_tester_.ExpectTotalCount(
"Apps.StateTransition.Drag.PresentationTime.MaxLatency.TabletMode", 0);
// AppListView::UpdateYPositionAndOpacity is triggered by
// ShelfLayoutManager::StartGestureDrag and
// ShelfLayoutManager::UpdateGestureDrag. Note that scrolling step of event
// generator is 1. So the expected value is 2.
histogram_tester_.ExpectTotalCount(
"Apps.StateTransition.Drag.PresentationTime.ClamshellMode", 2);
histogram_tester_.ExpectTotalCount(
"Apps.StateTransition.Drag.PresentationTime.MaxLatency.ClamshellMode", 1);
}
} // namespace ash