[Journeys] De-dupe labels and show no. of matches in queryless view
Bug: 1303171
Change-Id: I9bfea490eea3d60caab9a1a0de1724695c4a81c3
Reviewed-on: https://chromium-review.googlesource.com/c/chromium/src/+/3778777
Reviewed-by: Sophie Chang <[email protected]>
Commit-Queue: Patrick Noland <[email protected]>
Reviewed-by: Tommy Li <[email protected]>
Cr-Commit-Position: refs/heads/main@{#1026991}
diff --git a/chrome/android/junit/src/org/chromium/chrome/browser/history_clusters/HistoryClustersCoordinatorTest.java b/chrome/android/junit/src/org/chromium/chrome/browser/history_clusters/HistoryClustersCoordinatorTest.java
index bd2746b2..38d7fbb 100644
--- a/chrome/android/junit/src/org/chromium/chrome/browser/history_clusters/HistoryClustersCoordinatorTest.java
+++ b/chrome/android/junit/src/org/chromium/chrome/browser/history_clusters/HistoryClustersCoordinatorTest.java
@@ -25,6 +25,7 @@
import androidx.test.core.app.ActivityScenario;
import com.google.android.material.tabs.TabLayout;
+import com.google.common.collect.ImmutableMap;
import org.hamcrest.Matchers;
import org.junit.After;
@@ -64,6 +65,7 @@
import java.util.Arrays;
import java.util.Collections;
import java.util.HashSet;
+import java.util.LinkedHashMap;
import java.util.List;
import java.util.concurrent.CopyOnWriteArraySet;
@@ -205,7 +207,8 @@
new ArrayList<>(), mGurl2, 123L, new ArrayList<>());
mCluster = new HistoryCluster(Arrays.asList(mVisit1, mVisit2), "\"label\"", "label",
Collections.emptyList(), 123L, Arrays.asList("pugs", "terriers"));
- mClusterResult = new HistoryClustersResult(Arrays.asList(mCluster), "dogs", false, false);
+ mClusterResult = new HistoryClustersResult(Arrays.asList(mCluster),
+ new LinkedHashMap<>(ImmutableMap.of("label", 1)), "dogs", false, false);
mActivityScenario =
ActivityScenario.launch(ChromeTabbedActivity.class).onActivity(activity -> {
diff --git a/chrome/android/junit/src/org/chromium/chrome/browser/history_clusters/HistoryClustersMediatorTest.java b/chrome/android/junit/src/org/chromium/chrome/browser/history_clusters/HistoryClustersMediatorTest.java
index ba0ee34..2b971748 100644
--- a/chrome/android/junit/src/org/chromium/chrome/browser/history_clusters/HistoryClustersMediatorTest.java
+++ b/chrome/android/junit/src/org/chromium/chrome/browser/history_clusters/HistoryClustersMediatorTest.java
@@ -29,6 +29,8 @@
import androidx.recyclerview.widget.LinearLayoutManager;
import androidx.recyclerview.widget.RecyclerView;
+import com.google.common.collect.ImmutableMap;
+
import org.hamcrest.BaseMatcher;
import org.hamcrest.Description;
import org.hamcrest.Matcher;
@@ -71,6 +73,7 @@
import java.util.ArrayList;
import java.util.Arrays;
import java.util.Collections;
+import java.util.LinkedHashMap;
import java.util.List;
import java.util.concurrent.TimeUnit;
@@ -248,12 +251,17 @@
new ArrayList<>(), 123L, Collections.emptyList());
mCluster3 = new HistoryCluster(Arrays.asList(mVisit4), "\"label3\"", "label3",
new ArrayList<>(), 789L, Collections.EMPTY_LIST);
- mHistoryClustersResultWithQuery = new HistoryClustersResult(
- Arrays.asList(mCluster1, mCluster2), "query", true, false);
- mHistoryClustersFollowupResultWithQuery =
- new HistoryClustersResult(Arrays.asList(mCluster3), "query", false, true);
+ mHistoryClustersResultWithQuery =
+ new HistoryClustersResult(Arrays.asList(mCluster1, mCluster2),
+ new LinkedHashMap<>(ImmutableMap.of("label", 1)), "query", true, false);
+ mHistoryClustersFollowupResultWithQuery = new HistoryClustersResult(
+ Arrays.asList(mCluster3),
+ new LinkedHashMap<>(ImmutableMap.of("label", 1, "hostname.com", 1, "label3", 1)),
+ "query", false, true);
mHistoryClustersResultEmptyQuery =
- new HistoryClustersResult(Arrays.asList(mCluster1, mCluster2), "", false, false);
+ new HistoryClustersResult(Arrays.asList(mCluster1, mCluster2),
+ new LinkedHashMap<>(ImmutableMap.of("label", 1, "hostname.com", 1)), "",
+ false, false);
}
@Test
@@ -318,8 +326,9 @@
ListItem item = mModelList.get(3);
PropertyModel model = item.model;
- assertTrue(model.getAllSetProperties().containsAll(Arrays.asList(
- HistoryClustersItemProperties.CLICK_HANDLER, HistoryClustersItemProperties.TITLE)));
+ assertTrue(model.getAllSetProperties().containsAll(
+ Arrays.asList(HistoryClustersItemProperties.CLICK_HANDLER,
+ HistoryClustersItemProperties.TITLE, HistoryClustersItemProperties.LABEL)));
}
@Test
@@ -496,6 +505,7 @@
doReturn(secondPromise).when(mBridge).loadMoreClusters("query");
doReturn(3).when(mLayoutManager).findLastVisibleItemPosition();
+ mMediator.setQueryState(QueryState.forQuery("query", ""));
mMediator.startQuery("query");
fulfillPromise(promise, mHistoryClustersResultWithQuery);
diff --git a/chrome/browser/history_clusters/history_clusters_bridge.cc b/chrome/browser/history_clusters/history_clusters_bridge.cc
index 13bd5c3f..19ae258 100644
--- a/chrome/browser/history_clusters/history_clusters_bridge.cc
+++ b/chrome/browser/history_clusters/history_clusters_bridge.cc
@@ -177,11 +177,23 @@
}
ScopedJavaLocalRef<jclass> cluster_type = base::android::GetClass(
env, "org/chromium/chrome/browser/history_clusters/HistoryCluster");
+ std::vector<std::u16string> unique_raw_labels;
+ std::vector<int> label_counts;
+ if (query_clusters_state_->query().empty()) {
+ for (const auto& label_entry :
+ query_clusters_state_->raw_label_counts_so_far()) {
+ unique_raw_labels.push_back(label_entry.first);
+ label_counts.push_back(label_entry.second);
+ }
+ }
+
const ScopedJavaLocalRef<jobject>& j_result =
Java_HistoryClustersBridge_buildClusterResult(
env,
base::android::ToTypedJavaArrayOfObjects(env, j_clusters,
cluster_type),
+ base::android::ToJavaArrayOfStrings(env, unique_raw_labels),
+ base::android::ToJavaIntArray(env, label_counts),
base::android::ConvertUTF8ToJavaString(env, query), can_load_more,
is_continuation);
base::android::RunObjectCallbackAndroid(j_callback, j_result);
diff --git a/chrome/browser/history_clusters/java/src/org/chromium/chrome/browser/history_clusters/HistoryClustersBridge.java b/chrome/browser/history_clusters/java/src/org/chromium/chrome/browser/history_clusters/HistoryClustersBridge.java
index 6f84036..3e07fb6 100644
--- a/chrome/browser/history_clusters/java/src/org/chromium/chrome/browser/history_clusters/HistoryClustersBridge.java
+++ b/chrome/browser/history_clusters/java/src/org/chromium/chrome/browser/history_clusters/HistoryClustersBridge.java
@@ -17,6 +17,7 @@
import java.util.ArrayList;
import java.util.Arrays;
+import java.util.LinkedHashMap;
import java.util.List;
@JNINamespace("history_clusters")
@@ -69,10 +70,18 @@
}
@CalledByNative
- static HistoryClustersResult buildClusterResult(
- HistoryCluster[] clusters, String query, boolean canLoadMore, boolean isContinuation) {
+ static HistoryClustersResult buildClusterResult(HistoryCluster[] clusters,
+ String[] uniqueRawLabels, int[] labelCounts, String query, boolean canLoadMore,
+ boolean isContinuation) {
+ assert uniqueRawLabels.length == labelCounts.length;
+ LinkedHashMap<String, Integer> labelCountsMap = new LinkedHashMap<>();
+ for (int i = 0; i < uniqueRawLabels.length; i++) {
+ labelCountsMap.put(uniqueRawLabels[i], labelCounts[i]);
+ }
+
List<HistoryCluster> clustersList = Arrays.asList(clusters);
- return new HistoryClustersResult(clustersList, query, canLoadMore, isContinuation);
+ return new HistoryClustersResult(
+ clustersList, labelCountsMap, query, canLoadMore, isContinuation);
}
@CalledByNative
diff --git a/chrome/browser/history_clusters/java/src/org/chromium/chrome/browser/history_clusters/HistoryClustersMediator.java b/chrome/browser/history_clusters/java/src/org/chromium/chrome/browser/history_clusters/HistoryClustersMediator.java
index bb13fbf..e3a202f0 100644
--- a/chrome/browser/history_clusters/java/src/org/chromium/chrome/browser/history_clusters/HistoryClustersMediator.java
+++ b/chrome/browser/history_clusters/java/src/org/chromium/chrome/browser/history_clusters/HistoryClustersMediator.java
@@ -47,7 +47,9 @@
import java.util.ArrayList;
import java.util.Arrays;
+import java.util.LinkedHashMap;
import java.util.List;
+import java.util.Map;
import java.util.concurrent.TimeUnit;
class HistoryClustersMediator extends RecyclerView.OnScrollListener implements SearchDelegate {
@@ -79,6 +81,7 @@
private ListItem mClearBrowsingDataItem;
private QueryState mQueryState = QueryState.forQueryless();
private final HistoryClustersMetricsLogger mMetricsLogger;
+ private Map<String, PropertyModel> mLabelToModelMap = new LinkedHashMap<>();
/**
* Create a new HistoryClustersMediator.
@@ -134,7 +137,7 @@
// SearchDelegate implementation.
@Override
public void onSearchTextChanged(String query) {
- mModelList.clear();
+ resetModel();
startQuery(query);
}
@@ -166,7 +169,7 @@
mQueryState = queryState;
mToolbarModel.set(HistoryClustersToolbarProperties.QUERY_STATE, queryState);
if (!queryState.isSearching()) {
- mModelList.clear();
+ resetModel();
startQuery(mQueryState.getQuery());
}
}
@@ -267,7 +270,7 @@
}
mDelegate.removeMarkedItems();
- mModelList.clear();
+ resetModel();
startQuery(mQueryState.getQuery());
}
@@ -282,6 +285,39 @@
boolean isQueryLess = !mQueryState.isSearching();
if (isQueryLess) {
ensureHeaders();
+ for (Map.Entry<String, Integer> entry : result.getLabelCounts().entrySet()) {
+ // Check if label exists in the model already
+ // If not, create a new entry
+ String rawLabel = entry.getKey();
+ PropertyModel existingModel = mLabelToModelMap.get(rawLabel);
+ if (existingModel == null) {
+ existingModel = new PropertyModel(HistoryClustersItemProperties.ALL_KEYS);
+ mLabelToModelMap.put(rawLabel, existingModel);
+ Drawable journeysDrawable =
+ AppCompatResources.getDrawable(mContext, R.drawable.ic_journeys);
+ existingModel.set(
+ HistoryClustersItemProperties.ICON_DRAWABLE, journeysDrawable);
+ existingModel.set(HistoryClustersItemProperties.DIVIDER_VISIBLE, true);
+ existingModel.set(HistoryClustersItemProperties.TITLE,
+ getQuotedLabelFromRawLabel(rawLabel, result.getClusters()));
+ ListItem clusterItem = new ListItem(ItemType.CLUSTER, existingModel);
+ mModelList.add(clusterItem);
+ existingModel.set(HistoryClustersItemProperties.CLICK_HANDLER,
+ (v)
+ -> setQueryState(QueryState.forQuery(
+ rawLabel, mDelegate.getSearchEmptyString())));
+ existingModel.set(HistoryClustersItemProperties.END_BUTTON_DRAWABLE, null);
+ }
+ existingModel.set(HistoryClustersItemProperties.LABEL,
+ mResources.getQuantityString(R.plurals.history_clusters_n_matches,
+ entry.getValue(), entry.getValue()));
+ }
+
+ if (result.canLoadMore() && !result.isContinuation()) {
+ continueQuery("");
+ }
+
+ return;
}
for (HistoryCluster cluster : result.getClusters()) {
@@ -294,15 +330,6 @@
clusterModel.set(HistoryClustersItemProperties.DIVIDER_VISIBLE, isQueryLess);
ListItem clusterItem = new ListItem(ItemType.CLUSTER, clusterModel);
mModelList.add(clusterItem);
- if (isQueryLess) {
- clusterModel.set(HistoryClustersItemProperties.CLICK_HANDLER,
- (v)
- -> setQueryState(QueryState.forQuery(
- cluster.getRawLabel(), mDelegate.getSearchEmptyString())));
- clusterModel.set(HistoryClustersItemProperties.END_BUTTON_DRAWABLE, null);
- clusterModel.set(HistoryClustersItemProperties.LABEL, null);
- continue;
- }
List<ListItem> visitsAndRelatedSearches =
new ArrayList<>(cluster.getVisits().size() + 1);
@@ -365,6 +392,22 @@
}
}
+ private void resetModel() {
+ mModelList.clear();
+ mLabelToModelMap.clear();
+ }
+
+ private String getQuotedLabelFromRawLabel(String rawLabel, List<HistoryCluster> clusters) {
+ for (HistoryCluster cluster : clusters) {
+ if (cluster.getRawLabel().equals(rawLabel)) {
+ return cluster.getLabel();
+ }
+ }
+
+ // This shouldn't happen, but the unquoted label is a graceful fallback in case it does.
+ return rawLabel;
+ }
+
private void ensureHeaders() {
if (mQueryState.isSearching()) {
return;
diff --git a/chrome/browser/history_clusters/java/src/org/chromium/chrome/browser/history_clusters/HistoryClustersResult.java b/chrome/browser/history_clusters/java/src/org/chromium/chrome/browser/history_clusters/HistoryClustersResult.java
index 06daa27..4a76c13 100644
--- a/chrome/browser/history_clusters/java/src/org/chromium/chrome/browser/history_clusters/HistoryClustersResult.java
+++ b/chrome/browser/history_clusters/java/src/org/chromium/chrome/browser/history_clusters/HistoryClustersResult.java
@@ -7,7 +7,9 @@
import androidx.annotation.VisibleForTesting;
import java.util.Collections;
+import java.util.LinkedHashMap;
import java.util.List;
+import java.util.Map;
@VisibleForTesting(otherwise = VisibleForTesting.PACKAGE_PRIVATE)
/**
@@ -16,18 +18,25 @@
*/
public class HistoryClustersResult {
private final List<HistoryCluster> mClusters;
+ private final LinkedHashMap<String, Integer> mLabelCounts;
private final String mQuery;
private final boolean mCanLoadMore;
private final boolean mIsContinuation;
/** Create a new result with no clusters and an empty query. */
public static HistoryClustersResult emptyResult() {
- return new HistoryClustersResult(Collections.EMPTY_LIST, "", false, false);
+ return new HistoryClustersResult(
+ Collections.EMPTY_LIST, new LinkedHashMap<>(), "", false, false);
}
- HistoryClustersResult(List<HistoryCluster> clusters, String query, boolean canLoadMore,
- boolean isContinuation) {
+ /**
+ * Constructs a new HistoryClustersResult. {@code labelCounts} must be a LinkedHashMap so that
+ * order is stable and preserved.
+ */
+ HistoryClustersResult(List<HistoryCluster> clusters, LinkedHashMap<String, Integer> labelCounts,
+ String query, boolean canLoadMore, boolean isContinuation) {
mClusters = clusters;
+ mLabelCounts = labelCounts;
mQuery = query;
mCanLoadMore = canLoadMore;
mIsContinuation = isContinuation;
@@ -44,4 +53,12 @@
public boolean canLoadMore() {
return mCanLoadMore;
}
+
+ public Map<String, Integer> getLabelCounts() {
+ return mLabelCounts;
+ }
+
+ public boolean isContinuation() {
+ return mIsContinuation;
+ }
}
diff --git a/components/history_clusters/core/query_clusters_state.cc b/components/history_clusters/core/query_clusters_state.cc
index cc4a3ed..3bc58d9 100644
--- a/components/history_clusters/core/query_clusters_state.cc
+++ b/components/history_clusters/core/query_clusters_state.cc
@@ -166,14 +166,19 @@
return;
const auto& raw_label_value = cluster.raw_label.value();
-
- // Subtle code below: If we've NEVER encountered the label before, this []
- // operator initializes the count to 0, and then post-increments it to 1.
- // If it already exists, the post-increment will up the count, but will
- // return false.
- if (raw_label_counts_[raw_label_value]++ == 0) {
- unique_raw_labels_.push_back(raw_label_value);
+ // Warning: N^2 algorithm below. If this ends up scaling poorly, it can be
+ // optimized by adding a map that tracks which labels have been seen
+ // already.
+ auto it = std::find_if(raw_label_counts_so_far_.begin(),
+ raw_label_counts_so_far_.end(),
+ [&raw_label_value](const LabelCount& label_count) {
+ return label_count.first == raw_label_value;
+ });
+ if (it == raw_label_counts_so_far_.end()) {
+ it = raw_label_counts_so_far_.insert(it,
+ std::make_pair(raw_label_value, 0));
}
+ it->second++;
}
}
diff --git a/components/history_clusters/core/query_clusters_state.h b/components/history_clusters/core/query_clusters_state.h
index 7d5c5d6..511c512 100644
--- a/components/history_clusters/core/query_clusters_state.h
+++ b/components/history_clusters/core/query_clusters_state.h
@@ -24,6 +24,8 @@
class HistoryClustersService;
+using LabelCount = std::pair<std::u16string, size_t>;
+
// This object encapsulates the results of a query to HistoryClustersService.
// It manages fetching more pages from the clustering backend as the user
// scrolls down.
@@ -54,15 +56,12 @@
// Used to request another batch of clusters of the same query.
void LoadNextBatchOfClusters(ResultCallback callback);
- // Use these together to iterate through the list of raw labels in the same
- // order as the clusters are ordered. The counts can be fetched by inputting
- // the labels into the map as keys - but note, this only counts the number
- // of label instances seen SO FAR, not necessarily in all of History.
- const std::vector<std::u16string>& unique_raw_labels() {
- return unique_raw_labels_;
- }
- const std::map<std::u16string, size_t>& raw_label_counts() {
- return raw_label_counts_;
+ // The list of raw labels in the same order as the clusters are ordered
+ // alongside the number of occurrences so far. The counts can be fetched by
+ // inputting the labels into the map as keys - but note, this only counts the
+ // number of label instances seen SO FAR, not necessarily in all of History.
+ const std::vector<LabelCount>& raw_label_counts_so_far() {
+ return raw_label_counts_so_far_;
}
private:
@@ -97,16 +96,11 @@
// The string query the user entered into the searchbox.
const std::string query_;
- // The de-duplicated list of raw labels we've seen so far, in the same order
- // as the clusters themselves were provided. This is only computed if `query`
- // is empty. For non-empty `query`, this will be an empty list.
- std::vector<std::u16string> unique_raw_labels_;
- // Counts the number of instances of each raw label we've seen. Note that
- // the value will always be an integer 1 or above. This is also used for our
- // internal uniqueness test. Note: this only counts the number of raw labels
- // of this string seen SO FAR, so unless we iterate through ALL the clusters,
- // this number may be smaller than the total number in History.
- std::map<std::u16string, size_t> raw_label_counts_;
+ // The de-duplicated list of raw labels we've seen so far and their number of
+ // occurrences, in the same order as the clusters themselves were provided.
+ // This is only computed if `query` is empty. For non-empty `query`, this will
+ // be an empty list.
+ std::vector<LabelCount> raw_label_counts_so_far_;
// The continuation params used to track where the last query left off and
// query for the "next page".
diff --git a/components/history_clusters/core/query_clusters_state_unittest.cc b/components/history_clusters/core/query_clusters_state_unittest.cc
index 0941a51c..3b5d1ce 100644
--- a/components/history_clusters/core/query_clusters_state_unittest.cc
+++ b/components/history_clusters/core/query_clusters_state_unittest.cc
@@ -285,20 +285,18 @@
auto result = InjectRawClustersAndAwaitPostProcessing(
&state, {cluster1, cluster2, cluster4}, {});
ASSERT_EQ(result.cluster_batch.size(), 3U);
- EXPECT_THAT(state.unique_raw_labels(),
- ElementsAre(u"rawlabel1", u"rawlabel2"));
- EXPECT_EQ(state.raw_label_counts().at(u"rawlabel1"), 2U);
- EXPECT_EQ(state.raw_label_counts().at(u"rawlabel2"), 1U);
+ EXPECT_THAT(state.raw_label_counts_so_far(),
+ ElementsAre(std::make_pair(u"rawlabel1", 2),
+ std::make_pair(u"rawlabel2", 1)));
// Test updating an existing count, and adding new ones after that.
result =
InjectRawClustersAndAwaitPostProcessing(&state, {cluster5, cluster3}, {});
ASSERT_EQ(result.cluster_batch.size(), 2U);
- EXPECT_THAT(state.unique_raw_labels(),
- ElementsAre(u"rawlabel1", u"rawlabel2", u"rawlabel3"));
- EXPECT_EQ(state.raw_label_counts().at(u"rawlabel1"), 2U);
- EXPECT_EQ(state.raw_label_counts().at(u"rawlabel2"), 2U);
- EXPECT_EQ(state.raw_label_counts().at(u"rawlabel3"), 1U);
+ EXPECT_THAT(state.raw_label_counts_so_far(),
+ ElementsAre(std::make_pair(u"rawlabel1", 2),
+ std::make_pair(u"rawlabel2", 2),
+ std::make_pair(u"rawlabel3", 1)));
}
} // namespace history_clusters
diff --git a/components/history_clusters_strings.grdp b/components/history_clusters_strings.grdp
index 30670a4..fe94d0a 100644
--- a/components/history_clusters_strings.grdp
+++ b/components/history_clusters_strings.grdp
@@ -70,4 +70,8 @@
<message name="IDS_HISTORY_CLUSTERS_SEARCH_YOUR_JOURNEYS" desc="A label for the hint text for the search box in Chrome Journeys." formatter_data="android_java">
Search your Journeys
</message>
+ <message name="IDS_HISTORY_CLUSTERS_N_MATCHES" desc="A label for the number of matching clusters for a given label." formatter_data="android_java">
+ {NUM_MATCHES, plural,
+ =1 {# match} other {# matches}}
+ </message>
</grit-part>
diff --git a/components/history_clusters_strings_grdp/IDS_HISTORY_CLUSTERS_N_MATCHES.png.sha1 b/components/history_clusters_strings_grdp/IDS_HISTORY_CLUSTERS_N_MATCHES.png.sha1
new file mode 100644
index 0000000..160e9ad
--- /dev/null
+++ b/components/history_clusters_strings_grdp/IDS_HISTORY_CLUSTERS_N_MATCHES.png.sha1
@@ -0,0 +1 @@
+e5b8707aa16ed376ba83f2258f78b489b928d069
\ No newline at end of file