Commit 0d329767 authored by Rob Schonberger's avatar Rob Schonberger Committed by Commit Bot

Add result_type kPlayStoreReinstallApp and use to improve Reinstall UX.

Adds a result_type, kPlayeStoreReinstallApp to both the ash/app_list
code, and use this as the result_type of ArcAppReinstallAppResult.

In SearchModel, add a method to filter with a lambda not just a list
of app IDs not to show.

In SearchResultTileItemListView, pull the list of SearchResult and use
the new method in SearchModel to create a list of apps with the
reinstall app as the last items.

In Chip View, use the same filter to ensure that reinstall
recommendations are not shown.

In SearchResultTileItemListView and SearchResultTileItemView track the
value of whether or not zero state is enabled, and make similar
modifications to the UI as the play store changes do.

Modify SearchResultTileItemListViewTest to test the new behavior, and
parameterize that test by both flags.

Bug: 911427
Change-Id: If9d8509835300024d656c89cf127980ae831d4ab
Reviewed-on: https://chromium-review.googlesource.com/c/1449963
Commit-Queue: Rob Schonberger <robsc@chromium.org>
Reviewed-by: default avatarJenny Zhang <jennyz@chromium.org>
Reviewed-by: default avatarXiyuan Xia <xiyuan@chromium.org>
Reviewed-by: default avatarDominick Ng <dominickn@chromium.org>
Cr-Commit-Position: refs/heads/master@{#629841}
parent caeb4fa4
......@@ -8,6 +8,8 @@
#include <string>
#include <utility>
#include "base/bind.h"
namespace app_list {
SearchModel::SearchModel()
......@@ -29,11 +31,27 @@ std::vector<SearchResult*> SearchModel::FilterSearchResultsByDisplayType(
SearchResult::DisplayType display_type,
const std::set<std::string>& excludes,
size_t max_results) {
base::RepeatingCallback<bool(const SearchResult&)> filter_function =
base::BindRepeating(
[](const SearchResult::DisplayType& display_type,
const std::set<std::string>& excludes,
const SearchResult& r) -> bool {
return excludes.count(r.id()) == 0 &&
display_type == r.display_type();
},
display_type, excludes);
return SearchModel::FilterSearchResultsByFunction(results, filter_function,
max_results);
}
std::vector<SearchResult*> SearchModel::FilterSearchResultsByFunction(
SearchResults* results,
const base::RepeatingCallback<bool(const SearchResult&)>& result_filter,
size_t max_results) {
std::vector<SearchResult*> matches;
for (size_t i = 0; i < results->item_count(); ++i) {
SearchResult* item = results->GetItemAt(i);
if (item->display_type() == display_type &&
excludes.count(item->id()) == 0) {
if (result_filter.Run(*item)) {
matches.push_back(item);
if (matches.size() == max_results)
break;
......
......@@ -13,6 +13,7 @@
#include "ash/app_list/model/app_list_model_export.h"
#include "ash/app_list/model/search/search_box_model.h"
#include "ash/app_list/model/search/search_result.h"
#include "base/callback.h"
#include "ui/base/models/list_model.h"
namespace app_list {
......@@ -47,6 +48,13 @@ class APP_LIST_MODEL_EXPORT SearchModel {
const std::set<std::string>& excludes,
size_t max_results);
// Filter the given |results| by those which |result_filter| returns true for.
// The returned list is truncated to |max_results|.
static std::vector<SearchResult*> FilterSearchResultsByFunction(
SearchResults* results,
const base::RepeatingCallback<bool(const SearchResult&)>& result_filter,
size_t max_results);
SearchBoxModel* search_box() { return search_box_.get(); }
SearchResults* results() { return results_.get(); }
......
......@@ -6,6 +6,7 @@
#include <stddef.h>
#include <algorithm>
#include <memory>
#include "ash/app_list/app_list_util.h"
......@@ -16,6 +17,8 @@
#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/internal_app_id_constants.h"
#include "base/bind.h"
#include "base/callback.h"
#include "base/i18n/rtl.h"
#include "ui/gfx/color_palette.h"
#include "ui/gfx/geometry/insets.h"
......@@ -50,13 +53,16 @@ SearchResultTileItemListView::SearchResultTileItemListView(
: search_result_page_view_(search_result_page_view),
search_box_(search_box),
is_play_store_app_search_enabled_(
app_list_features::IsPlayStoreAppSearchEnabled()) {
app_list_features::IsPlayStoreAppSearchEnabled()),
is_app_reinstall_recommendation_enabled_(
app_list_features::IsAppReinstallZeroStateEnabled()) {
SetLayoutManager(std::make_unique<views::BoxLayout>(
views::BoxLayout::kHorizontal,
gfx::Insets(kItemListVerticalSpacing, kItemListHorizontalSpacing),
kBetweenItemSpacing));
for (size_t i = 0; i < kMaxNumSearchResultTiles; ++i) {
if (is_play_store_app_search_enabled_) {
if (is_app_reinstall_recommendation_enabled_ ||
is_play_store_app_search_enabled_) {
views::Separator* separator = new views::Separator;
separator->SetVisible(false);
separator->SetBorder(views::CreateEmptyBorder(
......@@ -97,23 +103,12 @@ SearchResultBaseView* SearchResultTileItemListView::GetFirstResultView() {
}
int SearchResultTileItemListView::DoUpdate() {
base::string16 raw_query = search_box_->text();
base::string16 query;
base::TrimWhitespace(raw_query, base::TRIM_ALL, &query);
SearchResult::DisplayType display_type =
app_list_features::IsZeroStateSuggestionsEnabled()
? (query.empty() ? ash::SearchResultDisplayType::kRecommendation
: ash::SearchResultDisplayType::kTile)
: ash::SearchResultDisplayType::kTile;
// Do not display the continue reading app in the search result list.
std::vector<SearchResult*> display_results =
SearchModel::FilterSearchResultsByDisplayType(
results(), display_type,
/*excludes=*/{app_list::kInternalAppIdContinueReading},
kMaxNumSearchResultTiles);
std::vector<SearchResult*> display_results = GetDisplayResults();
SearchResult::ResultType previous_type = ash::SearchResultType::kUnknown;
ash::SearchResultDisplayType previous_display_type =
ash::SearchResultDisplayType::kNone;
for (size_t i = 0; i < kMaxNumSearchResultTiles; ++i) {
if (i >= display_results.size()) {
if (is_play_store_app_search_enabled_)
......@@ -125,8 +120,10 @@ int SearchResultTileItemListView::DoUpdate() {
SearchResult* item = display_results[i];
tile_views_[i]->SetResult(item);
if (is_play_store_app_search_enabled_) {
if (i > 0 && item->result_type() != previous_type) {
if (is_play_store_app_search_enabled_ ||
is_app_reinstall_recommendation_enabled_) {
if (i > 0 && (item->result_type() != previous_type ||
item->display_type() != previous_display_type)) {
// Add a separator to separate search results of different types.
// The strategy here is to only add a separator only if current search
// result type is different from the previous one. The strategy is
......@@ -139,6 +136,7 @@ int SearchResultTileItemListView::DoUpdate() {
}
previous_type = item->result_type();
previous_display_type = item->display_type();
}
set_container_score(
......@@ -147,6 +145,53 @@ int SearchResultTileItemListView::DoUpdate() {
return display_results.size();
}
std::vector<SearchResult*> SearchResultTileItemListView::GetDisplayResults() {
base::string16 raw_query = search_box_->text();
base::string16 query;
base::TrimWhitespace(raw_query, base::TRIM_ALL, &query);
// We ask for kMaxNumSearchResultTiles total results, and we prefer reinstall
// candidates if appropriate. we fetch |reinstall_results| first, and
// front-fill the rest from the regular result types.
auto reinstall_filter =
base::BindRepeating([](const SearchResult& r) -> bool {
return r.display_type() ==
ash::SearchResultDisplayType::kRecommendation &&
r.result_type() == ash::SearchResultType::kPlayStoreReinstallApp;
});
std::vector<SearchResult*> reinstall_results =
is_app_reinstall_recommendation_enabled_ && query.empty()
? SearchModel::FilterSearchResultsByFunction(
results(), reinstall_filter, kMaxNumSearchResultTiles)
: std::vector<SearchResult*>();
SearchResult::DisplayType display_type =
app_list_features::IsZeroStateSuggestionsEnabled()
? (query.empty() ? ash::SearchResultDisplayType::kRecommendation
: ash::SearchResultDisplayType::kTile)
: ash::SearchResultDisplayType::kTile;
size_t display_num = kMaxNumSearchResultTiles - reinstall_results.size();
// Do not display the continue reading app in the search result list.
auto non_reinstall_filter = base::BindRepeating(
[](const SearchResult::DisplayType& display_type,
const SearchResult& r) -> bool {
return r.display_type() == display_type &&
r.result_type() !=
ash::SearchResultType::kPlayStoreReinstallApp &&
r.id() != app_list::kInternalAppIdContinueReading;
},
display_type);
std::vector<SearchResult*> display_results =
SearchModel::FilterSearchResultsByFunction(
results(), non_reinstall_filter, display_num);
// Append the reinstalls to the display results.
display_results.insert(display_results.end(), reinstall_results.begin(),
reinstall_results.end());
return display_results;
}
bool SearchResultTileItemListView::OnKeyPressed(const ui::KeyEvent& event) {
// Let the FocusManager handle Left/Right keys.
if (!IsUnhandledUpDownKeyEvent(event))
......
......@@ -47,6 +47,8 @@ class APP_LIST_EXPORT SearchResultTileItemListView
// Overridden from SearchResultContainerView:
int DoUpdate() override;
std::vector<SearchResult*> GetDisplayResults();
std::vector<SearchResultTileItemView*> tile_views_;
std::vector<views::Separator*> separator_views_;
......@@ -57,6 +59,8 @@ class APP_LIST_EXPORT SearchResultTileItemListView
const bool is_play_store_app_search_enabled_;
const bool is_app_reinstall_recommendation_enabled_;
DISALLOW_COPY_AND_ASSIGN(SearchResultTileItemListView);
};
......
......@@ -4,6 +4,7 @@
#include "ash/app_list/views/search_result_tile_item_list_view.h"
#include <algorithm>
#include <memory>
#include <utility>
......@@ -28,34 +29,47 @@ namespace {
constexpr int kMaxNumSearchResultTiles = 6;
constexpr int kInstalledApps = 4;
constexpr int kPlayStoreApps = 2;
constexpr int kRecommendedApps = 1;
} // namespace
class SearchResultTileItemListViewTest
: public views::ViewsTestBase,
public ::testing::WithParamInterface<bool> {
public ::testing::WithParamInterface<std::pair<bool, bool>> {
public:
SearchResultTileItemListViewTest() = default;
~SearchResultTileItemListViewTest() override = default;
protected:
void CreateSearchResultTileItemListView() {
std::vector<base::Feature> enabled_features, disabled_features;
// Enable fullscreen app list for parameterized Play Store app search
// feature.
// Zero State affects the UI behavior significantly. This test tests the
// UI behavior with zero state being disable.
// TODO(crbug.com/925195): Write new test cases for zero state.
if (IsPlayStoreAppSearchEnabled()) {
scoped_feature_list_.InitWithFeatures(
{app_list_features::kEnablePlayStoreAppSearch},
{app_list_features::kEnableZeroStateSuggestions});
enabled_features.push_back(app_list_features::kEnablePlayStoreAppSearch);
} else {
scoped_feature_list_.InitWithFeatures(
{}, {app_list_features::kEnablePlayStoreAppSearch,
app_list_features::kEnableZeroStateSuggestions});
disabled_features.push_back(app_list_features::kEnablePlayStoreAppSearch);
}
if (IsReinstallAppRecommendationEnabled()) {
enabled_features.push_back(
app_list_features::kEnableAppReinstallZeroState);
} else {
disabled_features.push_back(
app_list_features::kEnableAppReinstallZeroState);
}
disabled_features.push_back(app_list_features::kEnableZeroStateSuggestions);
scoped_feature_list_.InitWithFeatures(enabled_features, disabled_features);
ASSERT_EQ(IsPlayStoreAppSearchEnabled(),
app_list_features::IsPlayStoreAppSearchEnabled());
ASSERT_EQ(IsReinstallAppRecommendationEnabled(),
app_list_features::IsAppReinstallZeroStateEnabled());
// Sets up the views.
textfield_ = std::make_unique<views::Textfield>();
view_ = std::make_unique<SearchResultTileItemListView>(
......@@ -63,7 +77,9 @@ class SearchResultTileItemListViewTest
view_->SetResults(view_delegate_.GetSearchModel()->results());
}
bool IsPlayStoreAppSearchEnabled() const { return GetParam(); }
bool IsPlayStoreAppSearchEnabled() const { return GetParam().first; }
bool IsReinstallAppRecommendationEnabled() const { return GetParam().second; }
SearchResultTileItemListView* view() { return view_.get(); }
......@@ -103,6 +119,20 @@ class SearchResultTileItemListViewTest
}
}
if (IsReinstallAppRecommendationEnabled()) {
for (int i = 0; i < kRecommendedApps; ++i) {
std::unique_ptr<TestSearchResult> result =
std::make_unique<TestSearchResult>();
result->set_result_id(base::StringPrintf("RecommendedApp %d", i));
result->set_display_type(ash::SearchResultDisplayType::kRecommendation);
result->set_result_type(ash::SearchResultType::kPlayStoreReinstallApp);
result->set_title(
base::UTF8ToUTF16(base::StringPrintf("RecommendedApp %d", i)));
result->SetRating(1 + i);
results->Add(std::move(result));
}
}
// Adding results calls SearchResultContainerView::ScheduleUpdate().
// It will post a delayed task to update the results and relayout.
RunPendingMessages();
......@@ -138,20 +168,30 @@ TEST_P(SearchResultTileItemListViewTest, Basic) {
SetUpSearchResults();
const int results = GetResultCount();
const int expected_results = IsPlayStoreAppSearchEnabled()
? kInstalledApps + kPlayStoreApps
: kInstalledApps;
int expected_results = kInstalledApps;
if (IsPlayStoreAppSearchEnabled()) {
expected_results += kPlayStoreApps;
}
if (IsReinstallAppRecommendationEnabled()) {
expected_results += kRecommendedApps;
}
expected_results = std::min(kMaxNumSearchResultTiles, expected_results);
EXPECT_EQ(expected_results, results);
// When the Play Store app search feature is enabled, for each results,
// we added a separator for result type grouping.
const int expected_child_count = IsPlayStoreAppSearchEnabled()
const bool separators_enabled =
IsPlayStoreAppSearchEnabled() || IsReinstallAppRecommendationEnabled();
// When the Play Store app search feature or app reinstallation feature is
// enabled, for each result, we added a separator for result type grouping.
const int expected_child_count = separators_enabled
? kMaxNumSearchResultTiles * 2
: kMaxNumSearchResultTiles;
EXPECT_EQ(expected_child_count, view()->child_count());
/// Test accessibility descriptions of tile views.
const int first_child = IsPlayStoreAppSearchEnabled() ? 1 : 0;
const int child_step = IsPlayStoreAppSearchEnabled() ? 2 : 1;
const int first_child = separators_enabled ? 1 : 0;
const int child_step = separators_enabled ? 2 : 1;
for (int i = 0; i < kInstalledApps; ++i) {
ui::AXNodeData node_data;
......@@ -163,7 +203,12 @@ TEST_P(SearchResultTileItemListViewTest, Basic) {
node_data.GetStringAttribute(ax::mojom::StringAttribute::kName));
}
for (int i = kInstalledApps; i < expected_results; ++i) {
const int expected_install_apps =
expected_results -
(IsReinstallAppRecommendationEnabled() ? kRecommendedApps : 0) -
kInstalledApps;
for (int i = kInstalledApps; i < (kInstalledApps + expected_install_apps);
++i) {
ui::AXNodeData node_data;
view()
->child_at(first_child + i * child_step)
......@@ -175,15 +220,42 @@ TEST_P(SearchResultTileItemListViewTest, Basic) {
node_data.GetStringAttribute(ax::mojom::StringAttribute::kName));
}
// Recommendations.
const int start_index = kInstalledApps + expected_install_apps;
for (int i = kInstalledApps + expected_install_apps; i < expected_results;
++i) {
ui::AXNodeData node_data;
view()
->child_at(first_child + i * child_step)
->GetAccessibleNodeData(&node_data);
EXPECT_EQ(ax::mojom::Role::kButton, node_data.role);
EXPECT_EQ(base::StringPrintf("RecommendedApp %d, Star rating %d.0",
i - start_index, i + 1 - start_index),
node_data.GetStringAttribute(ax::mojom::StringAttribute::kName));
}
ResetOpenResultCount();
for (int i = 0; i < results; ++i) {
ui::KeyEvent event(ui::ET_KEY_PRESSED, ui::VKEY_RETURN, ui::EF_NONE);
for (int j = 0; j <= i; ++j)
view()->tile_views_for_test()[i]->OnKeyEvent(&event);
EXPECT_EQ(i + 1, GetOpenResultCount(i));
// When both app reinstalls and play store apps are enabled, we actually
// instantiate 7 results, but only show 6. So we have to look, for exactly 1
// result, a "skip" ahead for the reinstall result.
if (IsReinstallAppRecommendationEnabled() &&
IsPlayStoreAppSearchEnabled() && i == (results - 1)) {
EXPECT_EQ(i + 1, GetOpenResultCount(i + 1));
} else {
EXPECT_EQ(i + 1, GetOpenResultCount(i));
}
}
}
INSTANTIATE_TEST_CASE_P(, SearchResultTileItemListViewTest, testing::Bool());
INSTANTIATE_TEST_CASE_P(,
SearchResultTileItemListViewTest,
testing::ValuesIn({std::make_pair(false, false),
std::make_pair(false, true),
std::make_pair(true, false),
std::make_pair(true, true)}));
} // namespace app_list
......@@ -72,6 +72,8 @@ SearchResultTileItemView::SearchResultTileItemView(
pagination_model_(pagination_model),
is_play_store_app_search_enabled_(
app_list_features::IsPlayStoreAppSearchEnabled()),
is_app_reinstall_recommendation_enabled_(
app_list_features::IsAppReinstallZeroStateEnabled()),
show_in_apps_page_(show_in_apps_page),
weak_ptr_factory_(this) {
SetFocusBehavior(FocusBehavior::ALWAYS);
......@@ -87,7 +89,8 @@ SearchResultTileItemView::SearchResultTileItemView(
AddChildView(icon_);
if (is_play_store_app_search_enabled_ ||
app_list_features::IsAppShortcutSearchEnabled()) {
app_list_features::IsAppShortcutSearchEnabled() ||
is_app_reinstall_recommendation_enabled_) {
badge_ = new views::ImageView;
badge_->set_can_process_events_within_subtree(false);
badge_->SetVerticalAlignment(views::ImageView::LEADING);
......@@ -104,7 +107,8 @@ SearchResultTileItemView::SearchResultTileItemView(
title_->SetAllowCharacterBreak(true);
AddChildView(title_);
if (is_play_store_app_search_enabled_) {
if (is_play_store_app_search_enabled_ ||
is_app_reinstall_recommendation_enabled_) {
rating_ = new views::Label;
rating_->SetEnabledColor(kSearchAppRatingColor);
rating_->SetLineHeight(kTileTextLineHeight);
......@@ -263,7 +267,6 @@ bool SearchResultTileItemView::OnKeyPressed(const ui::KeyEvent& event) {
if (event.key_code() == ui::VKEY_RETURN) {
if (IsSuggestedAppTile())
LogAppLaunch();
RecordSearchResultOpenSource(result(), view_delegate_->GetModel(),
view_delegate_->GetSearchModel());
view_delegate_->OpenSearchResult(result()->id(), event.flags());
......
......@@ -115,6 +115,7 @@ class APP_LIST_EXPORT SearchResultTileItemView
SkColor parent_background_color_ = SK_ColorTRANSPARENT;
const bool is_play_store_app_search_enabled_;
const bool is_app_reinstall_recommendation_enabled_;
const bool show_in_apps_page_; // True if shown in app list's apps page.
std::unique_ptr<AppListMenuModelAdapter> context_menu_;
......
......@@ -13,6 +13,8 @@
#include "ash/app_list/views/search_result_suggestion_chip_view.h"
#include "ash/public/cpp/app_list/app_list_config.h"
#include "ash/public/cpp/app_list/app_list_features.h"
#include "base/bind.h"
#include "base/callback.h"
#include "ui/views/accessibility/view_accessibility.h"
#include "ui/views/controls/textfield/textfield.h"
#include "ui/views/focus/focus_manager.h"
......@@ -61,10 +63,14 @@ int SuggestionChipContainerView::DoUpdate() {
if (IgnoreUpdateAndLayout())
return num_results();
auto exclude_reinstall_filter = [](const SearchResult& r) -> bool {
return r.display_type() == ash::SearchResultDisplayType::kRecommendation &&
r.result_type() != ash::SearchResultType::kPlayStoreReinstallApp;
};
std::vector<SearchResult*> display_results =
SearchModel::FilterSearchResultsByDisplayType(
results(), ash::SearchResultDisplayType::kRecommendation,
/*excludes=*/{}, AppListConfig::instance().num_start_page_tiles());
SearchModel::FilterSearchResultsByFunction(
results(), base::BindRepeating(exclude_reinstall_filter),
AppListConfig::instance().num_start_page_tiles());
// Update search results here, but wait until layout to add them as child
// views when we know this view's bounds.
......
......@@ -113,6 +113,8 @@ struct EnumTraits<ash::mojom::SearchResultType, ash::SearchResultType> {
return ash::mojom::SearchResultType::kLauncher;
case ash::SearchResultType::kAnswerCard:
return ash::mojom::SearchResultType::kAnswerCard;
case ash::SearchResultType::kPlayStoreReinstallApp:
return ash::mojom::SearchResultType::kPlayStoreReinstallApp;
case ash::SearchResultType::kUnknown:
break;
}
......@@ -150,6 +152,9 @@ struct EnumTraits<ash::mojom::SearchResultType, ash::SearchResultType> {
case ash::mojom::SearchResultType::kAnswerCard:
*out = ash::SearchResultType::kAnswerCard;
return true;
case ash::mojom::SearchResultType::kPlayStoreReinstallApp:
*out = ash::SearchResultType::kPlayStoreReinstallApp;
return true;
}
NOTREACHED();
return false;
......
......@@ -47,6 +47,7 @@ enum class SearchResultType {
kOmnibox, // Results from Omnibox.
kLauncher, // Results from launcher search (currently only from Files).
kAnswerCard, // WebContents based answer card.
kPlayStoreReinstallApp, // Reinstall recommendations from PlayStore.
// Add new values here.
};
......
......@@ -119,6 +119,7 @@ enum SearchResultType {
kOmnibox, // Results from Omninbox.
kLauncher, // Results from launcher search (currently only from Files).
kAnswerCard, // WebContents based answer card.
kPlayStoreReinstallApp, // Reinstall recommendations from PlayStore.
// Add new values here.
};
......
......@@ -25,16 +25,14 @@ constexpr float kAppReinstallRelevance = 0.7;
ArcAppReinstallAppResult::ArcAppReinstallAppResult(
const arc::mojom::AppReinstallCandidatePtr& mojom_data,
const gfx::ImageSkia& skia_icon,
bool is_recommendation) {
const gfx::ImageSkia& skia_icon) {
ash::mojom::SearchResultMetadataPtr metadata = {base::in_place};
set_id(kPlayStoreAppUrlPrefix + mojom_data->package_name);
SetResultType(ash::SearchResultType::kPlayStoreApp);
SetResultType(ash::SearchResultType::kPlayStoreReinstallApp);
SetTitle(base::UTF8ToUTF16(mojom_data->title));
SetDetails(base::UTF8ToUTF16(metadata->id));
SetDisplayType(is_recommendation
? ash::SearchResultDisplayType::kRecommendation
: ash::SearchResultDisplayType::kTile);
SetDisplayType(ash::SearchResultDisplayType::kRecommendation);
set_relevance(kAppReinstallRelevance);
SetIcon(skia_icon);
......
......@@ -19,8 +19,7 @@ class ArcAppReinstallAppResult : public ChromeSearchResult {
public:
ArcAppReinstallAppResult(
const arc::mojom::AppReinstallCandidatePtr& mojom_data,
const gfx::ImageSkia& skia_icon,
bool is_recommendation);
const gfx::ImageSkia& skia_icon);
~ArcAppReinstallAppResult() override;
// ArcAppResult:
......
......@@ -182,7 +182,7 @@ void ArcAppReinstallSearchProvider::UpdateResults() {
} else if (icon_it != icon_urls_.end()) {
// Icon is loaded, add it to the results.
new_results.emplace_back(std::make_unique<ArcAppReinstallAppResult>(
loaded_value_[i], icon_it->second, /*is_recommendation=*/true));
loaded_value_[i], icon_it->second));
}
}
......
Markdown is supported
0%
or
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment