| // 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 |