[go: nahoru, domu]

blob: 9ceda9f390ecbf3bd892d219d349fa8e2ed20656 [file] [log] [blame]
// Copyright 2014 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/search_result_tile_item_list_view.h"
#include <stddef.h>
#include <algorithm>
#include <memory>
#include <set>
#include <string>
#include "ash/app_list/app_list_util.h"
#include "ash/app_list/app_list_view_delegate.h"
#include "ash/app_list/model/search/search_result.h"
#include "ash/app_list/views/search_result_page_view.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_features.h"
#include "ash/public/cpp/app_list/app_list_notifier.h"
#include "ash/public/cpp/app_list/app_list_types.h"
#include "ash/public/cpp/app_list/internal_app_id_constants.h"
#include "base/bind.h"
#include "base/callback.h"
#include "base/i18n/rtl.h"
#include "base/metrics/histogram_macros.h"
#include "base/metrics/user_metrics.h"
#include "base/stl_util.h"
#include "ui/gfx/color_palette.h"
#include "ui/gfx/geometry/insets.h"
#include "ui/views/background.h"
#include "ui/views/border.h"
#include "ui/views/controls/separator.h"
#include "ui/views/controls/textfield/textfield.h"
#include "ui/views/focus/focus_manager.h"
#include "ui/views/layout/box_layout.h"
#include "ui/views/widget/widget.h"
namespace ash {
namespace {
// Layout constants used when fullscreen app list feature is enabled.
constexpr int kItemListVerticalSpacing = 16;
constexpr int kItemListHorizontalSpacing = 12;
constexpr int kBetweenItemSpacing = 8;
constexpr int kSeparatorLeftRightPadding = 4;
constexpr int kSeparatorHeight = 46;
constexpr int kSeparatorTopPadding = 10;
// The Delay before recording play store app results impression, i.e., if the
// play store results are displayed less than the duration, we assume user
// won't have chance to see them clearly and click on them, and wont' log
// the impression.
constexpr int kPlayStoreImpressionDelayInMs = 1000;
// Returns true if the search result is an installable app.
bool IsResultAnInstallableApp(SearchResult* result) {
SearchResult::ResultType result_type = result->result_type();
return result_type == AppListSearchResultType::kPlayStoreApp ||
result_type == AppListSearchResultType::kPlayStoreReinstallApp ||
result_type == AppListSearchResultType::kInstantApp;
}
bool IsPlayStoreApp(SearchResult* result) {
return result->result_type() == AppListSearchResultType::kPlayStoreApp;
}
} // namespace
SearchResultTileItemListView::SearchResultTileItemListView(
views::Textfield* search_box,
AppListViewDelegate* view_delegate)
: SearchResultContainerView(view_delegate),
search_box_(search_box),
is_app_reinstall_recommendation_enabled_(
app_list_features::IsAppReinstallZeroStateEnabled()),
max_search_result_tiles_(
AppListConfig::instance().max_search_result_tiles()) {
layout_ = SetLayoutManager(std::make_unique<views::BoxLayout>(
views::BoxLayout::Orientation::kHorizontal,
gfx::Insets(kItemListVerticalSpacing, kItemListHorizontalSpacing),
kBetweenItemSpacing));
for (size_t i = 0; i < max_search_result_tiles_; ++i) {
views::Separator* separator =
AddChildView(std::make_unique<views::Separator>());
separator->SetVisible(false);
separator->SetBorder(views::CreateEmptyBorder(
kSeparatorTopPadding, kSeparatorLeftRightPadding,
AppListConfig::instance().search_tile_height() - kSeparatorHeight,
kSeparatorLeftRightPadding));
separator->SetColor(AppListColorProvider::Get()->GetSeparatorColor());
separator_views_.push_back(separator);
layout_->SetFlexForView(separator, 0);
SearchResultTileItemView* tile_item =
AddChildView(std::make_unique<SearchResultTileItemView>(view_delegate));
tile_item->set_index_in_container(i);
tile_item->SetParentBackgroundColor(
AppListColorProvider::Get()->GetSearchBoxCardBackgroundColor());
tile_views_.push_back(tile_item);
AddObservedResultView(tile_item);
}
// Tile items are shown horizontally.
set_horizontally_traversable(true);
}
SearchResultTileItemListView::~SearchResultTileItemListView() = default;
SearchResultTileItemView* SearchResultTileItemListView::GetResultViewAt(
size_t index) {
DCHECK(index >= 0 && index < tile_views_.size());
return tile_views_[index];
}
int SearchResultTileItemListView::DoUpdate() {
if (!GetWidget() || !GetWidget()->IsVisible() || !GetWidget()->IsActive()) {
for (size_t i = 0; i < max_search_result_tiles_; ++i) {
SearchResultBaseView* result_view = GetResultViewAt(i);
result_view->SetResult(nullptr);
result_view->SetVisible(false);
}
return 0;
}
std::vector<SearchResult*> display_results = GetDisplayResults();
std::set<std::string> result_id_removed, result_id_added;
bool is_result_an_installable_app = false;
bool is_previous_result_installable_app = false;
int installed_app_index = -1;
int playstore_app_index = -1;
int reinstall_app_index = -1;
int app_group_index = -1;
bool found_playstore_results = false;
for (size_t i = 0; i < max_search_result_tiles_; ++i) {
// If the current result at i exists, wants to be notified and is a
// different id, notify it that it is being hidden.
SearchResult* current_result = tile_views_[i]->result();
if (current_result != nullptr) {
result_id_removed.insert(current_result->id());
}
if (i >= display_results.size()) {
separator_views_[i]->SetVisible(false);
GetResultViewAt(i)->SetResult(nullptr);
continue;
}
SearchResult* item = display_results[i];
if (IsPlayStoreApp(item)) {
++playstore_app_index;
app_group_index = playstore_app_index;
found_playstore_results = true;
} else if (item->result_type() ==
AppListSearchResultType::kPlayStoreReinstallApp) {
++reinstall_app_index;
app_group_index = playstore_app_index;
} else {
++installed_app_index;
app_group_index = installed_app_index;
}
GetResultViewAt(i)->SetResult(item);
GetResultViewAt(i)->set_group_index_in_container_view(app_group_index);
result_id_added.insert(item->id());
is_result_an_installable_app = IsResultAnInstallableApp(item);
if (i > 0 &&
(is_result_an_installable_app != is_previous_result_installable_app)) {
// Add a separator between installed apps and installable apps.
// This assumes the search results are already separated in groups for
// installed and installable apps.
separator_views_[i]->SetVisible(true);
} else {
separator_views_[i]->SetVisible(false);
}
is_previous_result_installable_app = is_result_an_installable_app;
}
auto* notifier = view_delegate()->GetNotifier();
if (notifier) {
std::vector<AppListNotifier::Result> notifier_results;
for (const auto* result : display_results)
notifier_results.emplace_back(result->id(), result->metrics_type());
notifier->NotifyResultsUpdated(SearchResultDisplayType::kTile,
notifier_results);
}
// Track play store results and start the timer for recording their impression
// UMA metrics.
std::u16string user_typed_query = GetUserTypedQuery();
if (found_playstore_results && user_typed_query != recent_playstore_query_) {
recent_playstore_query_ = user_typed_query;
playstore_impression_timer_.Stop();
playstore_impression_timer_.Start(
FROM_HERE,
base::TimeDelta::FromMilliseconds(kPlayStoreImpressionDelayInMs), this,
&SearchResultTileItemListView::OnPlayStoreImpressionTimer);
// Set the starting time in result view for play store results.
base::TimeTicks result_display_start = base::TimeTicks::Now();
for (size_t i = 0; i < max_search_result_tiles_; ++i) {
SearchResult* result = GetResultViewAt(i)->result();
if (result && IsPlayStoreApp(result)) {
GetResultViewAt(i)->set_result_display_start_time(result_display_start);
}
}
} else if (!found_playstore_results) {
playstore_impression_timer_.Stop();
}
// notify visibility changes, if needed.
std::set<std::string> actual_added_ids =
base::STLSetDifference<std::set<std::string>>(result_id_added,
result_id_removed);
for (const std::string& added_id : actual_added_ids) {
SearchResult* added =
view_delegate()->GetSearchModel()->FindSearchResult(added_id);
if (added != nullptr && added->notify_visibility_change()) {
view_delegate()->OnSearchResultVisibilityChanged(added->id(), shown());
}
}
if (shown() != false) {
std::set<std::string> actual_removed_ids =
base::STLSetDifference<std::set<std::string>>(result_id_removed,
result_id_added);
// we only notify removed items if we're in the middle of showing.
for (const std::string& removed_id : actual_removed_ids) {
SearchResult* removed =
view_delegate()->GetSearchModel()->FindSearchResult(removed_id);
if (removed != nullptr && removed->notify_visibility_change()) {
view_delegate()->OnSearchResultVisibilityChanged(removed->id(),
false /*=shown*/);
}
}
}
set_container_score(
display_results.empty()
? -1.0
: AppListConfig::instance().app_tiles_container_score());
return display_results.size();
}
std::vector<SearchResult*> SearchResultTileItemListView::GetDisplayResults() {
std::u16string raw_query = search_box_->GetText();
std::u16string query;
base::TrimWhitespace(raw_query, base::TRIM_ALL, &query);
// We ask for |max_search_result_tiles_| policy tile results first,
// then add them to their preferred position in the tile list if found.
// Note: Policy tile provides a mechanism to display the result tile at the
// preferred position recommended by display_index() property of the search
// result. This is what policy referred to. It has nothing to do with
// Enterprise policy.
auto policy_tiles_filter =
base::BindRepeating([](const SearchResult& r) -> bool {
return r.display_index() != SearchResultDisplayIndex::kUndefined &&
r.display_type() == SearchResultDisplayType::kTile &&
r.is_recommendation();
});
std::vector<SearchResult*> policy_tiles_results =
is_app_reinstall_recommendation_enabled_ && query.empty()
? SearchModel::FilterSearchResultsByFunction(
results(), policy_tiles_filter, max_search_result_tiles_)
: std::vector<SearchResult*>();
SearchResult::DisplayType display_type = SearchResultDisplayType::kTile;
size_t display_num = max_search_result_tiles_ - policy_tiles_results.size();
// Do not display the repeat reinstall results or continue reading app in the
// search result list.
auto non_policy_tiles_filter = base::BindRepeating(
[](const SearchResult::DisplayType& display_type,
const SearchResult& r) -> bool {
return r.display_type() == display_type &&
r.result_type() !=
AppListSearchResultType::kPlayStoreReinstallApp &&
r.id() != kInternalAppIdContinueReading;
},
display_type);
std::vector<SearchResult*> display_results =
SearchModel::FilterSearchResultsByFunction(
results(), non_policy_tiles_filter, display_num);
// Policy tile results will be appended to the final tiles list
// based on their specified index. If the requested index is out of
// range of the current list, the result will be appended to the back.
std::sort(policy_tiles_results.begin(), policy_tiles_results.end(),
[](const SearchResult* r1, const SearchResult* r2) -> bool {
return r1->display_index() < r2->display_index();
});
const SearchResultDisplayIndex display_results_last_index =
static_cast<SearchResultDisplayIndex>(display_results.size() - 1);
for (auto* result : policy_tiles_results) {
const SearchResultDisplayIndex result_index = result->display_index();
if (result_index > display_results_last_index) {
display_results.emplace_back(result);
} else {
// TODO(newcomer): Remove this check once we determine the root cause for
// https://crbug.com/992344.
CHECK_GE(result_index, SearchResultDisplayIndex::kFirstIndex);
display_results.emplace(display_results.begin() + result_index, result);
}
}
return display_results;
}
std::u16string SearchResultTileItemListView::GetUserTypedQuery() {
std::u16string search_box_text = search_box_->GetText();
gfx::Range range = search_box_->GetSelectedRange();
std::u16string raw_query = range.is_empty()
? search_box_text
: search_box_text.substr(0, range.start());
std::u16string query;
base::TrimWhitespace(raw_query, base::TRIM_ALL, &query);
return query;
}
void SearchResultTileItemListView::OnPlayStoreImpressionTimer() {
size_t playstore_app_num = 0;
for (const auto* tile_view : tile_views_) {
SearchResult* result = tile_view->result();
if (result == nullptr)
continue;
if (IsPlayStoreApp(result))
++playstore_app_num;
}
// Log the UMA metrics of play store impression.
base::RecordAction(
base::UserMetricsAction("AppList_ShowPlayStoreQueryResults"));
DCHECK_LE(playstore_app_num, max_search_result_tiles_);
UMA_HISTOGRAM_EXACT_LINEAR("Apps.AppListPlayStoreSearchAppsDisplayed",
playstore_app_num, max_search_result_tiles_);
}
void SearchResultTileItemListView::CleanUpOnViewHide() {
playstore_impression_timer_.Stop();
recent_playstore_query_.clear();
}
const char* SearchResultTileItemListView::GetClassName() const {
return "SearchResultTileItemListView";
}
void SearchResultTileItemListView::Layout() {
const bool flex = GetContentsBounds().width() < GetPreferredSize().width();
layout_->SetDefaultFlex(flex ? 1 : 0);
layout_->set_between_child_spacing(flex ? 1 : kBetweenItemSpacing);
views::View::Layout();
}
void SearchResultTileItemListView::OnShownChanged() {
SearchResultContainerView::OnShownChanged();
for (const auto* tile_view : tile_views_) {
SearchResult* result = tile_view->result();
if (result == nullptr) {
continue;
}
if (result->notify_visibility_change()) {
view_delegate()->OnSearchResultVisibilityChanged(result->id(), shown());
}
}
}
void SearchResultTileItemListView::VisibilityChanged(View* starting_from,
bool is_visible) {
SearchResultContainerView::VisibilityChanged(starting_from, is_visible);
// We only do this work when is_visible is false, since this is how we
// receive the event. We filter and only run when shown.
if (is_visible && shown()) {
return;
}
CleanUpOnViewHide();
for (const auto* tile_view : tile_views_) {
SearchResult* result = tile_view->result();
if (result == nullptr) {
continue;
}
if (result->notify_visibility_change()) {
view_delegate()->OnSearchResultVisibilityChanged(result->id(),
false /*=visible*/);
}
}
}
} // namespace ash