Commit 5051ad04 authored by Wei-Yin Chen (陳威尹)'s avatar Wei-Yin Chen (陳威尹) Committed by Commit Bot

Add search term chip in Grid tab switcher (2/2)

Show a chip on the tab card if that tab has search result page in its
navigation stack. The chip contains the search term. When clicked,
the tab would visit the search result page again.

This is behind a Finch parameter "enable_search_term_chip" under
TabGridLayoutAndroid.

The CL is split into two, and this is part two, which can be verified
to be behind the flag isSearchTermChipEnabled() by the Formal
Equivalence Checker in http://crrev.com/c/1934235/4.

See also: part one (http://crrev.com/c/2038458).

Bug: 1048255
Change-Id: I03d61d97a99964168ebcd758b772231185d9223f
Reviewed-on: https://chromium-review.googlesource.com/c/chromium/src/+/2036513Reviewed-by: default avatarTommy Li <tommycli@chromium.org>
Reviewed-by: default avatarJesse Doherty <jwd@chromium.org>
Reviewed-by: default avatarMei Liang <meiliang@chromium.org>
Reviewed-by: default avatarYue Zhang <yuezhanggg@chromium.org>
Reviewed-by: default avatarMatthew Jones <mdjones@chromium.org>
Commit-Queue: Wei-Yin Chen (陳威尹) <wychen@chromium.org>
Cr-Commit-Position: refs/heads/master@{#739230}
parent 382d3674
...@@ -16,6 +16,7 @@ import static android.support.test.espresso.matcher.ViewMatchers.withId; ...@@ -16,6 +16,7 @@ import static android.support.test.espresso.matcher.ViewMatchers.withId;
import static android.support.test.espresso.matcher.ViewMatchers.withParent; import static android.support.test.espresso.matcher.ViewMatchers.withParent;
import static android.support.test.espresso.matcher.ViewMatchers.withText; import static android.support.test.espresso.matcher.ViewMatchers.withText;
import static org.hamcrest.CoreMatchers.not;
import static org.hamcrest.core.AllOf.allOf; import static org.hamcrest.core.AllOf.allOf;
import static org.junit.Assert.assertEquals; import static org.junit.Assert.assertEquals;
import static org.junit.Assert.assertNotEquals; import static org.junit.Assert.assertNotEquals;
...@@ -73,6 +74,7 @@ import org.chromium.chrome.browser.compositor.layouts.Layout; ...@@ -73,6 +74,7 @@ import org.chromium.chrome.browser.compositor.layouts.Layout;
import org.chromium.chrome.browser.compositor.layouts.content.TabContentManager; import org.chromium.chrome.browser.compositor.layouts.content.TabContentManager;
import org.chromium.chrome.browser.flags.CachedFeatureFlags; import org.chromium.chrome.browser.flags.CachedFeatureFlags;
import org.chromium.chrome.browser.flags.ChromeFeatureList; import org.chromium.chrome.browser.flags.ChromeFeatureList;
import org.chromium.chrome.browser.search_engines.TemplateUrlServiceFactory;
import org.chromium.chrome.browser.tab.Tab; import org.chromium.chrome.browser.tab.Tab;
import org.chromium.chrome.browser.tab.TabFeatureUtilities; import org.chromium.chrome.browser.tab.TabFeatureUtilities;
import org.chromium.chrome.browser.tabmodel.TabModel; import org.chromium.chrome.browser.tabmodel.TabModel;
...@@ -91,6 +93,7 @@ import org.chromium.chrome.test.util.ChromeTabUtils; ...@@ -91,6 +93,7 @@ import org.chromium.chrome.test.util.ChromeTabUtils;
import org.chromium.chrome.test.util.MenuUtils; import org.chromium.chrome.test.util.MenuUtils;
import org.chromium.chrome.test.util.OverviewModeBehaviorWatcher; import org.chromium.chrome.test.util.OverviewModeBehaviorWatcher;
import org.chromium.chrome.test.util.browser.Features; import org.chromium.chrome.test.util.browser.Features;
import org.chromium.content_public.browser.LoadUrlParams;
import org.chromium.content_public.browser.test.util.Criteria; import org.chromium.content_public.browser.test.util.Criteria;
import org.chromium.content_public.browser.test.util.CriteriaHelper; import org.chromium.content_public.browser.test.util.CriteriaHelper;
import org.chromium.content_public.browser.test.util.TestThreadUtils; import org.chromium.content_public.browser.test.util.TestThreadUtils;
...@@ -107,6 +110,7 @@ import java.lang.ref.WeakReference; ...@@ -107,6 +110,7 @@ import java.lang.ref.WeakReference;
import java.util.LinkedList; import java.util.LinkedList;
import java.util.List; import java.util.List;
import java.util.Locale; import java.util.Locale;
import java.util.concurrent.atomic.AtomicReference;
// clang-format off // clang-format off
/** Tests for the {@link StartSurfaceLayout} */ /** Tests for the {@link StartSurfaceLayout} */
...@@ -1387,6 +1391,90 @@ public class StartSurfaceLayoutTest { ...@@ -1387,6 +1391,90 @@ public class StartSurfaceLayoutTest {
} }
} }
@Test
@MediumTest
@CommandLineFlags.Add({BASE_PARAMS + "/enable_search_term_chip/true"})
public void testSearchTermChip_noChip() throws InterruptedException {
prepareTabs(1, 0, mUrl);
enterGTSWithThumbnailChecking();
onView(withId(R.id.tab_list_view)).check(TabCountAssertion.havingTabCount(1));
onView(withId(R.id.search_button)).check(matches(not(isDisplayed())));
}
@Test
@MediumTest
@CommandLineFlags.Add({BASE_PARAMS + "/enable_search_term_chip/true"})
public void testSearchTermChip_withChip() throws InterruptedException {
// Make sure we support RTL and CJKV languages.
String searchTermWithSpecialCodePoints = "a\n ئۇيغۇرچە\u200E漢字";
// Special code points like new line (\n) and left-to-right marker (‎‎‎\u200E) should
// be stripped out. See TabAttributeCache#removeEscapedCodePoints for more details.
String expectedTerm = "a ئۇيغۇرچە漢字";
String anotherTerm = "hello world";
// Do search, and verify the chip is still not shown.
AtomicReference<String> searchUrl = new AtomicReference<>();
ChromeTabbedActivity cta = mActivityTestRule.getActivity();
TestThreadUtils.runOnUiThreadBlocking(() -> {
TemplateUrlServiceFactory.get().setSearchEngine("google.com");
searchUrl.set(TemplateUrlServiceFactory.get().getUrlForSearchQuery(
searchTermWithSpecialCodePoints));
cta.getTabModelSelector().getCurrentTab().loadUrl(new LoadUrlParams(searchUrl.get()));
});
enterGTSWithThumbnailChecking();
onView(withId(R.id.tab_list_view)).check(TabCountAssertion.havingTabCount(1));
onView(withId(R.id.search_button)).check(matches(not(isDisplayed())));
leaveGTSAndVerifyThumbnailsAreReleased();
// Navigate, and verify the chip is shown.
mActivityTestRule.loadUrl(mUrl);
enterGTSWithThumbnailChecking();
onView(withId(R.id.tab_list_view)).check(TabCountAssertion.havingTabCount(1));
onView(allOf(withParent(withId(R.id.search_button)), withText(expectedTerm)))
.check(matches(isDisplayed()));
leaveGTSAndVerifyThumbnailsAreReleased();
// Do another search, and verify the chip is gone.
AtomicReference<String> searchUrl2 = new AtomicReference<>();
TestThreadUtils.runOnUiThreadBlocking(() -> {
TemplateUrlServiceFactory.get().setSearchEngine("google.com");
searchUrl2.set(TemplateUrlServiceFactory.get().getUrlForSearchQuery(anotherTerm));
cta.getTabModelSelector().getCurrentTab().loadUrl(new LoadUrlParams(searchUrl2.get()));
});
enterGTSWithThumbnailChecking();
onView(withId(R.id.tab_list_view)).check(TabCountAssertion.havingTabCount(1));
onView(withId(R.id.search_button)).check(matches(not(isDisplayed())));
leaveGTSAndVerifyThumbnailsAreReleased();
// Back to previous page, and verify the chip is back.
Espresso.pressBack();
enterGTSWithThumbnailChecking();
onView(withId(R.id.tab_list_view)).check(TabCountAssertion.havingTabCount(1));
onView(allOf(withParent(withId(R.id.search_button)), withText(expectedTerm)))
.check(matches(isDisplayed()));
// Click the chip and check the tab navigates back to the search result page.
assertEquals(mUrl, cta.getTabModelSelector().getCurrentTab().getUrl());
OverviewModeBehaviorWatcher hideWatcher = TabUiTestHelper.createOverviewHideWatcher(cta);
onView(allOf(withParent(withId(R.id.search_button)), withText(expectedTerm)))
.perform(click());
hideWatcher.waitForBehavior();
CriteriaHelper.pollUiThread(Criteria.equals(
searchUrl.get(), () -> cta.getTabModelSelector().getCurrentTab().getUrl()));
// Verify the chip is gone.
enterGTSWithThumbnailChecking();
onView(withId(R.id.tab_list_view)).check(TabCountAssertion.havingTabCount(1));
onView(withId(R.id.search_button)).check(matches(not(isDisplayed())));
}
private void switchTabModel(boolean isIncognito) { private void switchTabModel(boolean isIncognito) {
assertTrue(isIncognito != assertTrue(isIncognito !=
mActivityTestRule.getActivity().getTabModelSelector().isIncognitoSelected()); mActivityTestRule.getActivity().getTabModelSelector().isIncognitoSelected());
......
...@@ -133,6 +133,7 @@ android_library("java") { ...@@ -133,6 +133,7 @@ android_library("java") {
"//components/embedder_support/android:web_contents_delegate_java", "//components/embedder_support/android:web_contents_delegate_java",
"//components/feature_engagement:feature_engagement_java", "//components/feature_engagement:feature_engagement_java",
"//components/policy/android:policy_java", "//components/policy/android:policy_java",
"//components/search_engines/android:java",
"//content/public/android:content_java", "//content/public/android:content_java",
"//content/public/android:content_java_resources", "//content/public/android:content_java_resources",
"//third_party/android_deps:android_arch_lifecycle_common_java", "//third_party/android_deps:android_arch_lifecycle_common_java",
......
...@@ -5,6 +5,7 @@ include_rules = [ ...@@ -5,6 +5,7 @@ include_rules = [
"+components/browser_ui/styles/android", "+components/browser_ui/styles/android",
"+components/browser_ui/widget/android", "+components/browser_ui/widget/android",
"+components/feature_engagement/public/android/java/src/org/chromium/components/feature_engagement", "+components/feature_engagement/public/android/java/src/org/chromium/components/feature_engagement",
"+components/search_engines/android/java/src/org/chromium/components/search_engines",
"+components/module_installer", "+components/module_installer",
"+content/public/android/java/src/org/chromium/content_public/browser", "+content/public/android/java/src/org/chromium/content_public/browser",
......
...@@ -6,9 +6,13 @@ package org.chromium.chrome.browser.tasks.pseudotab; ...@@ -6,9 +6,13 @@ package org.chromium.chrome.browser.tasks.pseudotab;
import android.content.Context; import android.content.Context;
import android.content.SharedPreferences; import android.content.SharedPreferences;
import android.text.TextUtils;
import androidx.annotation.Nullable;
import androidx.annotation.VisibleForTesting;
import org.chromium.base.ContextUtils; import org.chromium.base.ContextUtils;
import org.chromium.base.LifetimeAssert; import org.chromium.chrome.browser.search_engines.TemplateUrlServiceFactory;
import org.chromium.chrome.browser.tab.Tab; import org.chromium.chrome.browser.tab.Tab;
import org.chromium.chrome.browser.tab.TabImpl; import org.chromium.chrome.browser.tab.TabImpl;
import org.chromium.chrome.browser.tabmodel.EmptyTabModelObserver; import org.chromium.chrome.browser.tabmodel.EmptyTabModelObserver;
...@@ -18,6 +22,10 @@ import org.chromium.chrome.browser.tabmodel.TabModelObserver; ...@@ -18,6 +22,10 @@ import org.chromium.chrome.browser.tabmodel.TabModelObserver;
import org.chromium.chrome.browser.tabmodel.TabModelSelector; import org.chromium.chrome.browser.tabmodel.TabModelSelector;
import org.chromium.chrome.browser.tabmodel.TabModelSelectorObserver; import org.chromium.chrome.browser.tabmodel.TabModelSelectorObserver;
import org.chromium.chrome.browser.tabmodel.TabModelSelectorTabObserver; import org.chromium.chrome.browser.tabmodel.TabModelSelectorTabObserver;
import org.chromium.components.search_engines.TemplateUrlService;
import org.chromium.content_public.browser.NavigationController;
import org.chromium.content_public.browser.NavigationHandle;
import org.chromium.content_public.browser.NavigationHistory;
/** /**
* Cache for attributes of {@link PseudoTab} to be available before native is ready. * Cache for attributes of {@link PseudoTab} to be available before native is ready.
...@@ -29,7 +37,12 @@ public class TabAttributeCache { ...@@ -29,7 +37,12 @@ public class TabAttributeCache {
private final TabModelObserver mTabModelObserver; private final TabModelObserver mTabModelObserver;
private final TabModelSelectorTabObserver mTabModelSelectorTabObserver; private final TabModelSelectorTabObserver mTabModelSelectorTabObserver;
private final TabModelSelectorObserver mTabModelSelectorObserver; private final TabModelSelectorObserver mTabModelSelectorObserver;
private final LifetimeAssert mLifetimeAssert = LifetimeAssert.create(this);
interface LastSearchTermProvider {
String getLastSearchTerm(Tab tab);
}
private static LastSearchTermProvider sLastSearchTermProviderForTests;
private static SharedPreferences getSharedPreferences() { private static SharedPreferences getSharedPreferences() {
if (sPref == null) { if (sPref == null) {
...@@ -68,6 +81,16 @@ public class TabAttributeCache { ...@@ -68,6 +81,16 @@ public class TabAttributeCache {
assert newRootId == ((TabImpl) tab).getRootId(); assert newRootId == ((TabImpl) tab).getRootId();
cacheRootId(tab.getId(), newRootId); cacheRootId(tab.getId(), newRootId);
} }
@Override
public void onDidFinishNavigation(Tab tab, NavigationHandle navigationHandle) {
if (tab.isIncognito()) return;
if (!navigationHandle.isInMainFrame()) return;
if (tab.getWebContents() == null) return;
// TODO(crbug.com/1048255): skip cacheLastSearchTerm() according to
// isValidSearchFormUrl() and PageTransition.GENERATED for optimization.
cacheLastSearchTerm(tab);
}
}; };
mTabModelObserver = new EmptyTabModelObserver() { mTabModelObserver = new EmptyTabModelObserver() {
...@@ -79,6 +102,7 @@ public class TabAttributeCache { ...@@ -79,6 +102,7 @@ public class TabAttributeCache {
.remove(getUrlKey(id)) .remove(getUrlKey(id))
.remove(getTitleKey(id)) .remove(getTitleKey(id))
.remove(getRootIdKey(id)) .remove(getRootIdKey(id))
.remove(getLastSearchTermKey(id))
.apply(); .apply();
} }
}; };
...@@ -87,7 +111,7 @@ public class TabAttributeCache { ...@@ -87,7 +111,7 @@ public class TabAttributeCache {
@Override @Override
public void onTabStateInitialized() { public void onTabStateInitialized() {
// TODO(wychen): after this cache is enabled by default, we only need to populate it // TODO(wychen): after this cache is enabled by default, we only need to populate it
// once. // once.
TabModelFilter filter = TabModelFilter filter =
mTabModelSelector.getTabModelFilterProvider().getTabModelFilter(false); mTabModelSelector.getTabModelFilterProvider().getTabModelFilter(false);
for (int i = 0; i < filter.getCount(); i++) { for (int i = 0; i < filter.getCount(); i++) {
...@@ -96,6 +120,8 @@ public class TabAttributeCache { ...@@ -96,6 +120,8 @@ public class TabAttributeCache {
cacheTitle(tab.getId(), tab.getTitle()); cacheTitle(tab.getId(), tab.getTitle());
cacheRootId(tab.getId(), ((TabImpl) tab).getRootId()); cacheRootId(tab.getId(), ((TabImpl) tab).getRootId());
} }
Tab currentTab = mTabModelSelector.getCurrentTab();
if (currentTab != null) cacheLastSearchTerm(currentTab);
filter.addObserver(mTabModelObserver); filter.addObserver(mTabModelObserver);
} }
}; };
...@@ -180,10 +206,110 @@ public class TabAttributeCache { ...@@ -180,10 +206,110 @@ public class TabAttributeCache {
cacheRootId(id, rootId); cacheRootId(id, rootId);
} }
private static String getLastSearchTermKey(int id) {
return id + "_last_search_term";
}
/**
* Get the last search term of the default search engine of a {@link PseudoTab} in the
* navigation stack.
*
* @param id The ID of the {@link PseudoTab}.
* @return The last search term. Null if none.
*/
public static @Nullable String getLastSearchTerm(int id) {
return getSharedPreferences().getString(getLastSearchTermKey(id), null);
}
private static void cacheLastSearchTerm(Tab tab) {
if (tab.getWebContents() == null) return;
cacheLastSearchTerm(tab.getId(), findLastSearchTerm(tab));
}
private static void cacheLastSearchTerm(int id, String searchTerm) {
getSharedPreferences().edit().putString(getLastSearchTermKey(id), searchTerm).apply();
}
/**
* Find the latest search term from the navigation stack.
* @param tab The tab to find from.
* @return The search term. Null for no results.
*/
@VisibleForTesting
static @Nullable String findLastSearchTerm(Tab tab) {
if (sLastSearchTermProviderForTests != null) {
return sLastSearchTermProviderForTests.getLastSearchTerm(tab);
}
assert tab.getWebContents() != null;
NavigationController controller = tab.getWebContents().getNavigationController();
NavigationHistory history = controller.getNavigationHistory();
if (!TextUtils.isEmpty(
TemplateUrlServiceFactory.get().getSearchQueryForUrl(tab.getUrl()))) {
// If we are already at a search result page, do not show the last search term.
return null;
}
for (int i = history.getCurrentEntryIndex() - 1; i >= 0; i--) {
String url = history.getEntryAtIndex(i).getOriginalUrl();
String query = TemplateUrlServiceFactory.get().getSearchQueryForUrl(url);
if (!TextUtils.isEmpty(query)) {
return removeEscapedCodePoints(query);
}
}
return null;
}
/**
* {@link TemplateUrlService#getSearchQueryForUrl(String)} can leave some code points
* unescaped for security reasons. See ShouldUnescapeCodePoint().
* In our use case, dropping the unescaped code points shouldn't introduce security issues,
* and loss of information is fine because the string is not going to be used other than
* showing in the UI.
* @return the rest of code points
*/
@VisibleForTesting
static String removeEscapedCodePoints(String string) {
StringBuilder sb = new StringBuilder();
for (int i = 0; i < string.length(); i++) {
if (string.charAt(i) != '%' || i + 2 >= string.length()) {
sb.append(string.charAt(i));
continue;
}
if (Character.digit(string.charAt(i + 1), 16) == -1
|| Character.digit(string.charAt(i + 2), 16) == -1) {
sb.append(string.charAt(i));
continue;
}
i += 2;
}
return sb.toString();
}
/**
* Set the LastSearchTermProvider for testing.
* @param lastSearchTermProvider The mocking object.
*/
@VisibleForTesting
static void setLastSearchTermMockForTesting(LastSearchTermProvider lastSearchTermProvider) {
sLastSearchTermProviderForTests = lastSearchTermProvider;
}
/**
* Set the last search term for a {@link PseudoTab}.
* @param id The ID of the {@link PseudoTab}.
* @param searchTerm The last search term
*/
@VisibleForTesting
public static void setLastSearchTermForTesting(int id, String searchTerm) {
cacheLastSearchTerm(id, searchTerm);
}
/** /**
* Clear everything in the storage. * Clear everything in the storage.
*/ */
static void clearAllForTesting() { @VisibleForTesting
public static void clearAllForTesting() {
getSharedPreferences().edit().clear().apply(); getSharedPreferences().edit().clear().apply();
} }
...@@ -195,6 +321,5 @@ public class TabAttributeCache { ...@@ -195,6 +321,5 @@ public class TabAttributeCache {
mTabModelSelector.getTabModelFilterProvider().getTabModelFilter(false).removeObserver( mTabModelSelector.getTabModelFilterProvider().getTabModelFilter(false).removeObserver(
mTabModelObserver); mTabModelObserver);
mTabModelSelector.removeObserver(mTabModelSelectorObserver); mTabModelSelector.removeObserver(mTabModelSelectorObserver);
LifetimeAssert.setSafeToGc(mLifetimeAssert, true);
} }
} }
...@@ -15,6 +15,7 @@ import android.support.graphics.drawable.AnimatedVectorDrawableCompat; ...@@ -15,6 +15,7 @@ import android.support.graphics.drawable.AnimatedVectorDrawableCompat;
import android.support.v4.content.res.ResourcesCompat; import android.support.v4.content.res.ResourcesCompat;
import android.support.v4.graphics.drawable.DrawableCompat; import android.support.v4.graphics.drawable.DrawableCompat;
import android.support.v4.view.ViewCompat; import android.support.v4.view.ViewCompat;
import android.text.TextUtils;
import android.view.View; import android.view.View;
import android.view.ViewGroup; import android.view.ViewGroup;
import android.widget.ImageView; import android.widget.ImageView;
...@@ -31,6 +32,7 @@ import org.chromium.chrome.tab_ui.R; ...@@ -31,6 +32,7 @@ import org.chromium.chrome.tab_ui.R;
import org.chromium.ui.modelutil.PropertyKey; import org.chromium.ui.modelutil.PropertyKey;
import org.chromium.ui.modelutil.PropertyModel; import org.chromium.ui.modelutil.PropertyModel;
import org.chromium.ui.widget.ButtonCompat; import org.chromium.ui.widget.ButtonCompat;
import org.chromium.ui.widget.ChipView;
import org.chromium.ui.widget.ChromeImageView; import org.chromium.ui.widget.ChromeImageView;
import org.chromium.ui.widget.ViewLookupCachingFrameLayout; import org.chromium.ui.widget.ViewLookupCachingFrameLayout;
...@@ -123,6 +125,14 @@ class TabGridViewBinder { ...@@ -123,6 +125,14 @@ class TabGridViewBinder {
(int) res.getDimension(R.dimen.tab_list_selected_inset)); (int) res.getDimension(R.dimen.tab_list_selected_inset));
view.setForeground(model.get(TabProperties.IS_SELECTED) ? drawable : null); view.setForeground(model.get(TabProperties.IS_SELECTED) ? drawable : null);
} }
if (TabUiFeatureUtilities.isSearchTermChipEnabled()) {
ChipView searchButton = (ChipView) view.fastFindViewById(R.id.search_button);
searchButton.getPrimaryTextView().setTextAlignment(View.TEXT_ALIGNMENT_VIEW_START);
searchButton.getPrimaryTextView().setEllipsize(TextUtils.TruncateAt.END);
// TODO(crbug.com/1048255): The selected state of ChipView doesn't look elevated.
// Fix the elevation in style instead.
searchButton.setSelected(false);
}
} else if (TabProperties.FAVICON == propertyKey) { } else if (TabProperties.FAVICON == propertyKey) {
Drawable favicon = model.get(TabProperties.FAVICON); Drawable favicon = model.get(TabProperties.FAVICON);
ImageView faviconView = (ImageView) view.fastFindViewById(R.id.tab_favicon); ImageView faviconView = (ImageView) view.fastFindViewById(R.id.tab_favicon);
...@@ -182,6 +192,29 @@ class TabGridViewBinder { ...@@ -182,6 +192,29 @@ class TabGridViewBinder {
updateColor(view, model.get(TabProperties.IS_INCOGNITO), TabProperties.UiType.CLOSABLE); updateColor(view, model.get(TabProperties.IS_INCOGNITO), TabProperties.UiType.CLOSABLE);
} else if (TabProperties.ACCESSIBILITY_DELEGATE == propertyKey) { } else if (TabProperties.ACCESSIBILITY_DELEGATE == propertyKey) {
view.setAccessibilityDelegate(model.get(TabProperties.ACCESSIBILITY_DELEGATE)); view.setAccessibilityDelegate(model.get(TabProperties.ACCESSIBILITY_DELEGATE));
} else if (TabUiFeatureUtilities.isSearchTermChipEnabled()
&& TabProperties.SEARCH_QUERY == propertyKey) {
String query = model.get(TabProperties.SEARCH_QUERY);
ChipView searchButton = (ChipView) view.fastFindViewById(R.id.search_button);
if (TextUtils.isEmpty(query)) {
searchButton.setVisibility(View.GONE);
} else {
searchButton.setVisibility(View.VISIBLE);
searchButton.getPrimaryTextView().setText(query);
searchButton.setIcon(R.drawable.ic_search, true);
}
} else if (TabUiFeatureUtilities.isSearchTermChipEnabled()
&& TabProperties.SEARCH_LISTENER == propertyKey) {
TabListMediator.TabActionListener listener = model.get(TabProperties.SEARCH_LISTENER);
ChipView searchButton = (ChipView) view.fastFindViewById(R.id.search_button);
if (listener == null) {
searchButton.setOnClickListener(null);
return;
}
searchButton.setOnClickListener(v -> {
int tabId = model.get(TabProperties.TAB_ID);
listener.run(tabId);
});
} }
} }
......
...@@ -23,6 +23,7 @@ import android.support.v7.content.res.AppCompatResources; ...@@ -23,6 +23,7 @@ import android.support.v7.content.res.AppCompatResources;
import android.support.v7.widget.GridLayoutManager; import android.support.v7.widget.GridLayoutManager;
import android.support.v7.widget.RecyclerView; import android.support.v7.widget.RecyclerView;
import android.support.v7.widget.helper.ItemTouchHelper; import android.support.v7.widget.helper.ItemTouchHelper;
import android.text.TextUtils;
import android.util.Pair; import android.util.Pair;
import android.view.View; import android.view.View;
import android.view.accessibility.AccessibilityNodeInfo; import android.view.accessibility.AccessibilityNodeInfo;
...@@ -36,11 +37,13 @@ import org.chromium.base.Callback; ...@@ -36,11 +37,13 @@ import org.chromium.base.Callback;
import org.chromium.base.Log; import org.chromium.base.Log;
import org.chromium.base.metrics.RecordHistogram; import org.chromium.base.metrics.RecordHistogram;
import org.chromium.base.metrics.RecordUserAction; import org.chromium.base.metrics.RecordUserAction;
import org.chromium.base.task.PostTask;
import org.chromium.chrome.browser.compositor.layouts.content.TabContentManager; import org.chromium.chrome.browser.compositor.layouts.content.TabContentManager;
import org.chromium.chrome.browser.flags.CachedFeatureFlags; import org.chromium.chrome.browser.flags.CachedFeatureFlags;
import org.chromium.chrome.browser.multiwindow.MultiWindowUtils; import org.chromium.chrome.browser.multiwindow.MultiWindowUtils;
import org.chromium.chrome.browser.native_page.NativePageFactory; import org.chromium.chrome.browser.native_page.NativePageFactory;
import org.chromium.chrome.browser.profiles.Profile; import org.chromium.chrome.browser.profiles.Profile;
import org.chromium.chrome.browser.search_engines.TemplateUrlServiceFactory;
import org.chromium.chrome.browser.tab.EmptyTabObserver; import org.chromium.chrome.browser.tab.EmptyTabObserver;
import org.chromium.chrome.browser.tab.Tab; import org.chromium.chrome.browser.tab.Tab;
import org.chromium.chrome.browser.tab.TabFeatureUtilities; import org.chromium.chrome.browser.tab.TabFeatureUtilities;
...@@ -56,6 +59,7 @@ import org.chromium.chrome.browser.tabmodel.TabModelFilter; ...@@ -56,6 +59,7 @@ import org.chromium.chrome.browser.tabmodel.TabModelFilter;
import org.chromium.chrome.browser.tabmodel.TabModelObserver; import org.chromium.chrome.browser.tabmodel.TabModelObserver;
import org.chromium.chrome.browser.tabmodel.TabModelSelector; import org.chromium.chrome.browser.tabmodel.TabModelSelector;
import org.chromium.chrome.browser.tabmodel.TabModelUtils; import org.chromium.chrome.browser.tabmodel.TabModelUtils;
import org.chromium.chrome.browser.tasks.pseudotab.TabAttributeCache;
import org.chromium.chrome.browser.tasks.tab_groups.EmptyTabGroupModelFilterObserver; import org.chromium.chrome.browser.tasks.tab_groups.EmptyTabGroupModelFilterObserver;
import org.chromium.chrome.browser.tasks.tab_groups.TabGroupModelFilter; import org.chromium.chrome.browser.tasks.tab_groups.TabGroupModelFilter;
import org.chromium.chrome.browser.tasks.tab_groups.TabGroupUtils; import org.chromium.chrome.browser.tasks.tab_groups.TabGroupUtils;
...@@ -64,7 +68,12 @@ import org.chromium.chrome.browser.util.UrlUtilities; ...@@ -64,7 +68,12 @@ import org.chromium.chrome.browser.util.UrlUtilities;
import org.chromium.chrome.tab_ui.R; import org.chromium.chrome.tab_ui.R;
import org.chromium.components.browser_ui.widget.selectable_list.SelectionDelegate; import org.chromium.components.browser_ui.widget.selectable_list.SelectionDelegate;
import org.chromium.components.feature_engagement.FeatureConstants; import org.chromium.components.feature_engagement.FeatureConstants;
import org.chromium.content_public.browser.LoadUrlParams;
import org.chromium.content_public.browser.NavigationController;
import org.chromium.content_public.browser.NavigationHandle; import org.chromium.content_public.browser.NavigationHandle;
import org.chromium.content_public.browser.NavigationHistory;
import org.chromium.content_public.browser.UiThreadTaskTraits;
import org.chromium.ui.base.PageTransition;
import org.chromium.ui.modelutil.PropertyModel; import org.chromium.ui.modelutil.PropertyModel;
import org.chromium.ui.modelutil.SimpleRecyclerViewAdapter; import org.chromium.ui.modelutil.SimpleRecyclerViewAdapter;
...@@ -1003,6 +1012,12 @@ class TabListMediator { ...@@ -1003,6 +1012,12 @@ class TabListMediator {
mModel.get(index).model.set(TabProperties.TITLE, getLatestTitleForTab(tab)); mModel.get(index).model.set(TabProperties.TITLE, getLatestTitleForTab(tab));
mModel.get(index).model.set(TabProperties.URL, getUrlForTab(tab)); mModel.get(index).model.set(TabProperties.URL, getUrlForTab(tab));
if (TabUiFeatureUtilities.isSearchTermChipEnabled() && mUiType == UiType.CLOSABLE) {
mModel.get(index).model.set(TabProperties.SEARCH_QUERY, getLastSearchTerm(tab));
mModel.get(index).model.set(TabProperties.SEARCH_LISTENER,
SearchTermChipUtils.getSearchQueryListener(tab, mTabSelectedListener));
}
updateFaviconForTab(tab, null); updateFaviconForTab(tab, null);
boolean forceUpdate = isSelected && !quickMode; boolean forceUpdate = isSelected && !quickMode;
if (mThumbnailProvider != null && mVisible if (mThumbnailProvider != null && mVisible
...@@ -1192,6 +1207,12 @@ class TabListMediator { ...@@ -1192,6 +1207,12 @@ class TabListMediator {
.with(CARD_TYPE, TAB) .with(CARD_TYPE, TAB)
.build(); .build();
if (TabUiFeatureUtilities.isSearchTermChipEnabled() && mUiType == UiType.CLOSABLE) {
tabInfo.set(TabProperties.SEARCH_QUERY, getLastSearchTerm(tab));
tabInfo.set(TabProperties.SEARCH_LISTENER,
SearchTermChipUtils.getSearchQueryListener(tab, mTabSelectedListener));
}
if (mUiType == UiType.SELECTABLE) { if (mUiType == UiType.SELECTABLE) {
// Incognito in both light/dark theme is the same as non-incognito mode in dark theme. // Incognito in both light/dark theme is the same as non-incognito mode in dark theme.
// Non-incognito mode and incognito in both light/dark themes in dark theme all look // Non-incognito mode and incognito in both light/dark themes in dark theme all look
...@@ -1233,6 +1254,15 @@ class TabListMediator { ...@@ -1233,6 +1254,15 @@ class TabListMediator {
tab.addObserver(mTabObserver); tab.addObserver(mTabObserver);
} }
private String getLastSearchTerm(Tab tab) {
assert TabUiFeatureUtilities.isSearchTermChipEnabled();
if (mActionsOnAllRelatedTabs && CachedFeatureFlags.isTabGroupsAndroidEnabled()
&& getRelatedTabsForId(tab.getId()).size() > 1) {
return null;
}
return TabAttributeCache.getLastSearchTerm(tab.getId());
}
private String getUrlForTab(Tab tab) { private String getUrlForTab(Tab tab) {
if (!CachedFeatureFlags.isTabGroupsAndroidContinuationEnabled()) return ""; if (!CachedFeatureFlags.isTabGroupsAndroidContinuationEnabled()) return "";
if (!mActionsOnAllRelatedTabs) return tab.getUrl(); if (!mActionsOnAllRelatedTabs) return tab.getUrl();
...@@ -1391,4 +1421,58 @@ class TabListMediator { ...@@ -1391,4 +1421,58 @@ class TabListMediator {
View.AccessibilityDelegate getAccessibilityDelegateForTesting() { View.AccessibilityDelegate getAccessibilityDelegateForTesting() {
return mAccessibilityDelegate; return mAccessibilityDelegate;
} }
/**
* These functions are wrapped in an inner class here for the formal equivalence checker, and
* it has to be at the end of the file. Otherwise the lambda and interface orders would be
* changed, resulting in differences.
*/
@VisibleForTesting
static class SearchTermChipUtils {
private static TabObserver sLazyNavigateToLastSearchQuery = new EmptyTabObserver() {
@Override
public void onPageLoadStarted(Tab tab, String url) {
assert tab.getWebContents() != null;
if (tab.getWebContents() == null) return;
// Directly calling navigateToLastSearchQuery() would lead to unsafe re-entrant
// calls to NavigateToPendingEntry.
PostTask.postTask(
UiThreadTaskTraits.USER_BLOCKING, () -> navigateToLastSearchQuery(tab));
tab.removeObserver(sLazyNavigateToLastSearchQuery);
}
};
@VisibleForTesting
static void navigateToLastSearchQuery(Tab tab) {
if (tab.getWebContents() == null) {
tab.addObserver(sLazyNavigateToLastSearchQuery);
return;
}
NavigationController controller = tab.getWebContents().getNavigationController();
NavigationHistory history = controller.getNavigationHistory();
for (int i = history.getCurrentEntryIndex() - 1; i >= 0; i--) {
int offset = i - history.getCurrentEntryIndex();
if (!controller.canGoToOffset(offset)) continue;
String url = history.getEntryAtIndex(i).getOriginalUrl();
String query = TemplateUrlServiceFactory.get().getSearchQueryForUrl(url);
if (TextUtils.isEmpty(query)) continue;
tab.loadUrl(new LoadUrlParams(url, PageTransition.KEYWORD_GENERATED));
return;
}
}
private static TabActionListener getSearchQueryListener(
Tab originalTab, TabActionListener select) {
return (tabId) -> {
if (originalTab == null) return;
assert tabId == originalTab.getId();
RecordUserAction.record("TabGrid.TabSearchChipTapped");
select.run(tabId);
navigateToLastSearchQuery(originalTab);
};
}
}
} }
...@@ -33,6 +33,7 @@ import org.chromium.chrome.browser.tab.Tab; ...@@ -33,6 +33,7 @@ import org.chromium.chrome.browser.tab.Tab;
import org.chromium.chrome.browser.tabmodel.TabCreatorManager; import org.chromium.chrome.browser.tabmodel.TabCreatorManager;
import org.chromium.chrome.browser.tabmodel.TabList; import org.chromium.chrome.browser.tabmodel.TabList;
import org.chromium.chrome.browser.tabmodel.TabModelSelector; import org.chromium.chrome.browser.tabmodel.TabModelSelector;
import org.chromium.chrome.browser.tasks.pseudotab.TabAttributeCache;
import org.chromium.chrome.browser.tasks.tab_management.suggestions.TabSuggestionsOrchestrator; import org.chromium.chrome.browser.tasks.tab_management.suggestions.TabSuggestionsOrchestrator;
import org.chromium.chrome.browser.ui.messages.snackbar.SnackbarManager; import org.chromium.chrome.browser.ui.messages.snackbar.SnackbarManager;
import org.chromium.chrome.tab_ui.R; import org.chromium.chrome.tab_ui.R;
...@@ -68,6 +69,7 @@ public class TabSwitcherCoordinator ...@@ -68,6 +69,7 @@ public class TabSwitcherCoordinator
private final MessageCardProviderCoordinator mMessageCardProviderCoordinator; private final MessageCardProviderCoordinator mMessageCardProviderCoordinator;
private TabSuggestionsOrchestrator mTabSuggestionsOrchestrator; private TabSuggestionsOrchestrator mTabSuggestionsOrchestrator;
private NewTabTileCoordinator mNewTabTileCoordinator; private NewTabTileCoordinator mNewTabTileCoordinator;
private TabAttributeCache mTabAttributeCache;
private final MenuOrKeyboardActionController private final MenuOrKeyboardActionController
.MenuOrKeyboardActionHandler mTabSwitcherMenuActionHandler = .MenuOrKeyboardActionHandler mTabSwitcherMenuActionHandler =
...@@ -187,6 +189,11 @@ public class TabSwitcherCoordinator ...@@ -187,6 +189,11 @@ public class TabSwitcherCoordinator
} }
} }
if (TabUiFeatureUtilities.isSearchTermChipEnabled()
&& mode != TabListCoordinator.TabListMode.CAROUSEL) {
mTabAttributeCache = new TabAttributeCache(mTabModelSelector);
}
mMenuOrKeyboardActionController = menuOrKeyboardActionController; mMenuOrKeyboardActionController = menuOrKeyboardActionController;
mMenuOrKeyboardActionController.registerMenuOrKeyboardActionHandler( mMenuOrKeyboardActionController.registerMenuOrKeyboardActionHandler(
mTabSwitcherMenuActionHandler); mTabSwitcherMenuActionHandler);
...@@ -387,5 +394,8 @@ public class TabSwitcherCoordinator ...@@ -387,5 +394,8 @@ public class TabSwitcherCoordinator
mTabSelectionEditorCoordinator.destroy(); mTabSelectionEditorCoordinator.destroy();
mMediator.destroy(); mMediator.destroy();
mLifecycleDispatcher.unregister(this); mLifecycleDispatcher.unregister(this);
if (mTabAttributeCache != null) {
mTabAttributeCache.destroy();
}
} }
} }
// Copyright 2020 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.
package org.chromium.chrome.browser.tasks.tab_management;
import org.chromium.chrome.browser.flags.ChromeFeatureList;
/**
* A class to handle the state of flags for tab_management.
*/
public class TabUiFeatureUtilities {
private static Boolean sSearchTermChipEnabledForTesting;
/**
* Set whether the search term chip in Grid tab switcher is enabled for testing.
*/
public static void setSearchTermChipEnabledForTesting(Boolean enabled) {
sSearchTermChipEnabledForTesting = enabled;
}
/**
* @return Whether the search term chip in Grid tab switcher is enabled.
*/
public static boolean isSearchTermChipEnabled() {
if (sSearchTermChipEnabledForTesting != null) return sSearchTermChipEnabledForTesting;
return ChromeFeatureList.getFieldTrialParamByFeatureAsBoolean(
ChromeFeatureList.TAB_GRID_LAYOUT_ANDROID, "enable_search_term_chip", false);
}
}
...@@ -29,6 +29,7 @@ import org.junit.runner.RunWith; ...@@ -29,6 +29,7 @@ import org.junit.runner.RunWith;
import org.chromium.base.Callback; import org.chromium.base.Callback;
import org.chromium.chrome.browser.flags.CachedFeatureFlags; import org.chromium.chrome.browser.flags.CachedFeatureFlags;
import org.chromium.chrome.browser.tab.Tab;
import org.chromium.chrome.tab_ui.R; import org.chromium.chrome.tab_ui.R;
import org.chromium.chrome.test.ChromeJUnit4ClassRunner; import org.chromium.chrome.test.ChromeJUnit4ClassRunner;
import org.chromium.components.browser_ui.widget.selectable_list.SelectionDelegate; import org.chromium.components.browser_ui.widget.selectable_list.SelectionDelegate;
...@@ -38,6 +39,7 @@ import org.chromium.ui.modelutil.PropertyModel; ...@@ -38,6 +39,7 @@ import org.chromium.ui.modelutil.PropertyModel;
import org.chromium.ui.modelutil.PropertyModelChangeProcessor; import org.chromium.ui.modelutil.PropertyModelChangeProcessor;
import org.chromium.ui.test.util.DummyUiActivityTestCase; import org.chromium.ui.test.util.DummyUiActivityTestCase;
import org.chromium.ui.widget.ButtonCompat; import org.chromium.ui.widget.ButtonCompat;
import org.chromium.ui.widget.ChipView;
import java.lang.ref.WeakReference; import java.lang.ref.WeakReference;
import java.util.concurrent.atomic.AtomicBoolean; import java.util.concurrent.atomic.AtomicBoolean;
...@@ -118,6 +120,7 @@ public class TabListViewHolderTest extends DummyUiActivityTestCase { ...@@ -118,6 +120,7 @@ public class TabListViewHolderTest extends DummyUiActivityTestCase {
public void setUpTest() throws Exception { public void setUpTest() throws Exception {
super.setUpTest(); super.setUpTest();
CachedFeatureFlags.enableTabThumbnailAspectRatioForTesting(false); CachedFeatureFlags.enableTabThumbnailAspectRatioForTesting(false);
TabUiFeatureUtilities.setSearchTermChipEnabledForTesting(true);
ViewGroup view = new LinearLayout(getActivity()); ViewGroup view = new LinearLayout(getActivity());
FrameLayout.LayoutParams params = new FrameLayout.LayoutParams( FrameLayout.LayoutParams params = new FrameLayout.LayoutParams(
ViewGroup.LayoutParams.MATCH_PARENT, ViewGroup.LayoutParams.MATCH_PARENT); ViewGroup.LayoutParams.MATCH_PARENT, ViewGroup.LayoutParams.MATCH_PARENT);
...@@ -482,12 +485,55 @@ public class TabListViewHolderTest extends DummyUiActivityTestCase { ...@@ -482,12 +485,55 @@ public class TabListViewHolderTest extends DummyUiActivityTestCase {
Assert.assertEquals(View.GONE, actionButton.getVisibility()); Assert.assertEquals(View.GONE, actionButton.getVisibility());
} }
@Test
@MediumTest
@UiThreadTest
public void testSearchTermChip() {
String searchTerm = "hello world";
testGridSelected(mTabGridView, mGridModel);
ChipView searchButton = mTabGridView.findViewById(R.id.search_button);
mGridModel.set(TabProperties.SEARCH_QUERY, searchTerm);
Assert.assertEquals(View.VISIBLE, searchButton.getVisibility());
Assert.assertEquals(searchTerm, searchButton.getPrimaryTextView().getText());
mGridModel.set(TabProperties.SEARCH_QUERY, null);
Assert.assertEquals(View.GONE, searchButton.getVisibility());
mGridModel.set(TabProperties.SEARCH_QUERY, searchTerm);
Assert.assertEquals(View.VISIBLE, searchButton.getVisibility());
mGridModel.set(TabProperties.SEARCH_QUERY, null);
Assert.assertEquals(View.GONE, searchButton.getVisibility());
}
@Test
@MediumTest
@UiThreadTest
public void testSearchListener() {
ChipView searchButton = mTabGridView.findViewById(R.id.search_button);
AtomicInteger clickedTabId = new AtomicInteger(Tab.INVALID_TAB_ID);
TabListMediator.TabActionListener searchListener = clickedTabId::set;
mGridModel.set(TabProperties.SEARCH_LISTENER, searchListener);
searchButton.performClick();
Assert.assertEquals(TAB1_ID, clickedTabId.get());
clickedTabId.set(Tab.INVALID_TAB_ID);
mGridModel.set(TabProperties.SEARCH_LISTENER, null);
searchButton.performClick();
Assert.assertEquals(Tab.INVALID_TAB_ID, clickedTabId.get());
}
@Override @Override
public void tearDownTest() throws Exception { public void tearDownTest() throws Exception {
mStripMCP.destroy(); mStripMCP.destroy();
mGridMCP.destroy(); mGridMCP.destroy();
mSelectableMCP.destroy(); mSelectableMCP.destroy();
CachedFeatureFlags.enableTabThumbnailAspectRatioForTesting(null); CachedFeatureFlags.enableTabThumbnailAspectRatioForTesting(null);
TabUiFeatureUtilities.setSearchTermChipEnabledForTesting(null);
super.tearDownTest(); super.tearDownTest();
} }
} }
...@@ -331,7 +331,7 @@ public class TabUiTestHelper { ...@@ -331,7 +331,7 @@ public class TabUiTestHelper {
/** /**
* Create a {@link OverviewModeBehaviorWatcher} to inspect overview hide. * Create a {@link OverviewModeBehaviorWatcher} to inspect overview hide.
*/ */
static OverviewModeBehaviorWatcher createOverviewHideWatcher(ChromeTabbedActivity cta) { public static OverviewModeBehaviorWatcher createOverviewHideWatcher(ChromeTabbedActivity cta) {
return new OverviewModeBehaviorWatcher(cta.getLayoutManager(), false, true); return new OverviewModeBehaviorWatcher(cta.getLayoutManager(), false, true);
} }
......
...@@ -2,5 +2,6 @@ include_rules = [ ...@@ -2,5 +2,6 @@ include_rules = [
"+chrome/browser/util", "+chrome/browser/util",
"+content/public/android/java/src/org/chromium/content_public/browser", "+content/public/android/java/src/org/chromium/content_public/browser",
"+components/feature_engagement/public/android/java/src/org/chromium/components/feature_engagement", "+components/feature_engagement/public/android/java/src/org/chromium/components/feature_engagement",
"+components/search_engines/android/java/src/org/chromium/components/search_engines",
"+chrome/lib/lifecycle/public/android/java/src/org/chromium/chrome/browser/lifecycle" "+chrome/lib/lifecycle/public/android/java/src/org/chromium/chrome/browser/lifecycle"
] ]
...@@ -16,6 +16,7 @@ public_tab_management_java_sources = [ ...@@ -16,6 +16,7 @@ public_tab_management_java_sources = [
"//chrome/android/features/tab_ui/java/src/org/chromium/chrome/browser/tasks/tab_management/TabManagementDelegate.java", "//chrome/android/features/tab_ui/java/src/org/chromium/chrome/browser/tasks/tab_management/TabManagementDelegate.java",
"//chrome/android/features/tab_ui/java/src/org/chromium/chrome/browser/tasks/tab_management/TabManagementModuleProvider.java", "//chrome/android/features/tab_ui/java/src/org/chromium/chrome/browser/tasks/tab_management/TabManagementModuleProvider.java",
"//chrome/android/features/tab_ui/java/src/org/chromium/chrome/browser/tasks/tab_management/TabSwitcher.java", "//chrome/android/features/tab_ui/java/src/org/chromium/chrome/browser/tasks/tab_management/TabSwitcher.java",
"//chrome/android/features/tab_ui/java/src/org/chromium/chrome/browser/tasks/tab_management/TabUiFeatureUtilities.java",
"//chrome/android/features/tab_ui/java/src/org/chromium/chrome/browser/tasks/tab_management/suggestions/TabContext.java", "//chrome/android/features/tab_ui/java/src/org/chromium/chrome/browser/tasks/tab_management/suggestions/TabContext.java",
"//chrome/android/features/tab_ui/java/src/org/chromium/chrome/browser/tasks/tab_management/suggestions/TabSuggestion.java", "//chrome/android/features/tab_ui/java/src/org/chromium/chrome/browser/tasks/tab_management/suggestions/TabSuggestion.java",
"//chrome/android/features/tab_ui/java/src/org/chromium/chrome/browser/tasks/tab_management/suggestions/TabSuggestionFeedback.java", "//chrome/android/features/tab_ui/java/src/org/chromium/chrome/browser/tasks/tab_management/suggestions/TabSuggestionFeedback.java",
......
...@@ -22295,6 +22295,14 @@ should be able to be added at any place in this file. ...@@ -22295,6 +22295,14 @@ should be able to be added at any place in this file.
<description>User drags a tab to reorder it.</description> <description>User drags a tab to reorder it.</description>
</action> </action>
<action name="TabGrid.TabSearchChipTapped">
<owner>yusufo@chromium.org</owner>
<owner>wychen@chromium.org</owner>
<description>
User tapped on the search term chip on a tab card in the TabGrid.
</description>
</action>
<action name="TabGridDialog"> <action name="TabGridDialog">
<owner>yusufo@chromium.org</owner> <owner>yusufo@chromium.org</owner>
<owner>wychen@chromium.org</owner> <owner>wychen@chromium.org</owner>
......
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