[go: nahoru, domu]

[reading list] Support different reading lists for back gesture

- Store the most recently used url when CTA is launched from reading
  list.
- Use ReadingListBackPressHandler for all RL back gestures (reduces
  code duplication).

Bug: 41497486
Change-Id: Ide40fd1bc3821aeeb2f5d4ca991c5d5c52b6644f
Reviewed-on: https://chromium-review.googlesource.com/c/chromium/src/+/5271343
Commit-Queue: Brandon Wylie <wylieb@google.com>
Reviewed-by: Sky Malice <skym@chromium.org>
Reviewed-by: Calder Kitagawa <ckitagawa@chromium.org>
Reviewed-by: Lijin Shen <lazzzis@google.com>
Cr-Commit-Position: refs/heads/main@{#1258668}
diff --git a/chrome/android/java/src/org/chromium/chrome/browser/ChromeTabbedActivity.java b/chrome/android/java/src/org/chromium/chrome/browser/ChromeTabbedActivity.java
index 7bb804cf..b46184c 100644
--- a/chrome/android/java/src/org/chromium/chrome/browser/ChromeTabbedActivity.java
+++ b/chrome/android/java/src/org/chromium/chrome/browser/ChromeTabbedActivity.java
@@ -164,7 +164,6 @@
 import org.chromium.chrome.browser.quick_delete.QuickDeleteDelegateImpl;
 import org.chromium.chrome.browser.quick_delete.QuickDeleteMetricsDelegate;
 import org.chromium.chrome.browser.read_later.ReadingListBackPressHandler;
-import org.chromium.chrome.browser.read_later.ReadingListUtils;
 import org.chromium.chrome.browser.reengagement.ReengagementNotificationController;
 import org.chromium.chrome.browser.search_engines.SearchEngineChoiceNotification;
 import org.chromium.chrome.browser.settings.SettingsLauncherImpl;
@@ -1185,10 +1184,9 @@
                     TaskTraits.UI_DEFAULT,
                     mCallbackController.makeCancelable(
                             this::maybeCreateIncognitoTabSnapshotController));
-            if (BackPressManager.isEnabled()) {
-                PostTask.postTask(TaskTraits.UI_DEFAULT, this::initializeBackPressHandlers);
-            }
-
+            // Always call into this function, even if BackPressManager is disabled to initialize
+            // back press managers which reduce code duplication in this class.
+            PostTask.postTask(TaskTraits.UI_DEFAULT, this::initializeBackPressHandlers);
             PostTask.postTask(
                     TaskTraits.UI_DEFAULT,
                     mCallbackController.makeCancelable(
@@ -3100,9 +3098,9 @@
 
         if (type == TabLaunchType.FROM_READING_LIST) {
             assert !isTablet() : "Not expecting to see FROM_READING_LIST on tablets";
-            ReadingListUtils.showReadingList(currentTab.isIncognito());
+            assert mReadingListBackPressHandler != null;
+            mReadingListBackPressHandler.handleBackPress();
             BackPressManager.record(BackPressHandler.Type.SHOW_READING_LIST);
-            if (webContents != null) webContents.dispatchBeforeUnload(false);
             return true;
         }
 
@@ -3176,6 +3174,14 @@
     }
 
     private void initializeBackPressHandlers() {
+        // Initialize some back press handlers early to reduce code duplication.
+        mReadingListBackPressHandler =
+                new ReadingListBackPressHandler(getActivityTabProvider(), mBookmarkModelSupplier);
+
+        if (!BackPressManager.isEnabled()) {
+            return;
+        }
+
         mBackPressManager.setHasSystemBackArm(true);
         if (mReturnToChromeBackPressHandler == null && !isTablet()) {
             mIsHandleTabSwitcherShownEnabled =
@@ -3193,9 +3199,7 @@
                     mReturnToChromeBackPressHandler,
                     BackPressHandler.Type.TAB_RETURN_TO_CHROME_START_SURFACE);
         }
-        if (mReadingListBackPressHandler == null && !isTablet()) {
-            mReadingListBackPressHandler =
-                    new ReadingListBackPressHandler(getActivityTabProvider());
+        if (!isTablet()) {
             mBackPressManager.addHandler(
                     mReadingListBackPressHandler, BackPressHandler.Type.SHOW_READING_LIST);
         }
diff --git a/chrome/android/java/src/org/chromium/chrome/browser/bookmarks/BookmarkUiState.java b/chrome/android/java/src/org/chromium/chrome/browser/bookmarks/BookmarkUiState.java
index 13ff842..7672119 100644
--- a/chrome/android/java/src/org/chromium/chrome/browser/bookmarks/BookmarkUiState.java
+++ b/chrome/android/java/src/org/chromium/chrome/browser/bookmarks/BookmarkUiState.java
@@ -73,7 +73,7 @@
     /**
      * @see #createStateFromUrl(Uri, BookmarkModel).
      */
-    static BookmarkUiState createStateFromUrl(String url, BookmarkModel bookmarkModel) {
+    public static BookmarkUiState createStateFromUrl(String url, BookmarkModel bookmarkModel) {
         if (SHOPPING_FILTER_URL.equals(url)) {
             return createShoppingFilterState();
         } else {
@@ -130,6 +130,10 @@
         mSearchText = queryString;
     }
 
+    public @Nullable BookmarkId getFolder() {
+        return mFolder;
+    }
+
     @Override
     public int hashCode() {
         return 31 * mUrl.hashCode() + mUiMode + Objects.hashCode(mSearchText);
diff --git a/chrome/android/java/src/org/chromium/chrome/browser/bookmarks/BookmarkUtils.java b/chrome/android/java/src/org/chromium/chrome/browser/bookmarks/BookmarkUtils.java
index 6268222..574dc432 100644
--- a/chrome/android/java/src/org/chromium/chrome/browser/bookmarks/BookmarkUtils.java
+++ b/chrome/android/java/src/org/chromium/chrome/browser/bookmarks/BookmarkUtils.java
@@ -624,7 +624,7 @@
      * @return The parent {@link BookmarkId} that the user used the last time or null if the user
      *     has never selected a parent folder to use.
      */
-    static BookmarkId getLastUsedParent() {
+    public static @Nullable BookmarkId getLastUsedParent() {
         SharedPreferencesManager preferences = ChromeSharedPreferences.getInstance();
         if (!preferences.contains(ChromePreferenceKeys.BOOKMARKS_LAST_USED_PARENT)) return null;
 
diff --git a/chrome/android/java/src/org/chromium/chrome/browser/read_later/ReadingListBackPressHandler.java b/chrome/android/java/src/org/chromium/chrome/browser/read_later/ReadingListBackPressHandler.java
index 05b0b5cb..a7bdb023 100644
--- a/chrome/android/java/src/org/chromium/chrome/browser/read_later/ReadingListBackPressHandler.java
+++ b/chrome/android/java/src/org/chromium/chrome/browser/read_later/ReadingListBackPressHandler.java
@@ -7,10 +7,16 @@
 import org.chromium.base.lifetime.Destroyable;
 import org.chromium.base.supplier.ObservableSupplier;
 import org.chromium.base.supplier.ObservableSupplierImpl;
+import org.chromium.base.supplier.OneShotCallback;
 import org.chromium.chrome.browser.ActivityTabProvider;
 import org.chromium.chrome.browser.ActivityTabProvider.ActivityTabTabObserver;
+import org.chromium.chrome.browser.bookmarks.BookmarkFeatures;
+import org.chromium.chrome.browser.bookmarks.BookmarkModel;
+import org.chromium.chrome.browser.bookmarks.BookmarkUiState;
+import org.chromium.chrome.browser.bookmarks.BookmarkUtils;
 import org.chromium.chrome.browser.tab.Tab;
 import org.chromium.chrome.browser.tab.TabLaunchType;
+import org.chromium.components.bookmarks.BookmarkId;
 import org.chromium.components.browser_ui.widget.gesture.BackPressHandler;
 import org.chromium.content_public.browser.WebContents;
 
@@ -24,22 +30,54 @@
     private final ActivityTabProvider mActivityTabProvider;
     private final ActivityTabTabObserver mActivityTabTabObserver;
 
-    public ReadingListBackPressHandler(ActivityTabProvider activityTabProvider) {
+    private BookmarkId mLastUsedParent;
+
+    public ReadingListBackPressHandler(
+            ActivityTabProvider activityTabProvider,
+            ObservableSupplier<BookmarkModel> bookmarkModelSupplier) {
         mActivityTabProvider = activityTabProvider;
         mActivityTabTabObserver =
                 new ActivityTabTabObserver(mActivityTabProvider, true) {
                     @Override
                     protected void onObservingDifferentTab(Tab tab, boolean hint) {
                         onBackPressStateChanged();
+
+                        // If this tab should intercept back press, start the process of tracking
+                        // the last url so that it can be reopened.
+                        if (shouldInterceptBackPress()) {
+                            new OneShotCallback<>(
+                                    bookmarkModelSupplier,
+                                    ReadingListBackPressHandler.this::setupLastUsedState);
+                        }
                     }
                 };
     }
 
+    // After {@link BookmarkModel} is available, load it then query the last used URL and store it
+    // in a {@link BookmarkId} which will be used to handle the back press.
+    private void setupLastUsedState(BookmarkModel bookmarkModel) {
+        bookmarkModel.finishLoadingBookmarkModel(
+                () -> {
+                    // Note: there's a slight (but unlikely) chance the the user changed the last
+                    // used url prior
+                    // to tracking it here.
+                    BookmarkUiState lastUsedState =
+                            BookmarkUiState.createStateFromUrl(
+                                    BookmarkUtils.getLastUsedUrl(), bookmarkModel);
+                    mLastUsedParent = lastUsedState.getFolder();
+                });
+    }
+
     @Override
     public @BackPressResult int handleBackPress() {
         Tab tab = mActivityTabProvider.get();
         int result = shouldInterceptBackPress() ? BackPressResult.SUCCESS : BackPressResult.FAILURE;
-        ReadingListUtils.showReadingList(tab.isIncognito());
+        if (BookmarkFeatures.isBookmarksAccountStorageEnabled()) {
+            BookmarkUtils.showBookmarkManager(null, mLastUsedParent, tab.isIncognito());
+        } else {
+            ReadingListUtils.showReadingList(tab.isIncognito());
+        }
+
         WebContents webContents = tab.getWebContents();
         if (webContents != null) webContents.dispatchBeforeUnload(false);
         return result;
diff --git a/chrome/android/javatests/src/org/chromium/chrome/browser/app/bookmarks/BookmarkTest.java b/chrome/android/javatests/src/org/chromium/chrome/browser/app/bookmarks/BookmarkTest.java
index a789640..2aaeaef3 100644
--- a/chrome/android/javatests/src/org/chromium/chrome/browser/app/bookmarks/BookmarkTest.java
+++ b/chrome/android/javatests/src/org/chromium/chrome/browser/app/bookmarks/BookmarkTest.java
@@ -8,6 +8,8 @@
 import static androidx.test.espresso.Espresso.pressBack;
 import static androidx.test.espresso.action.ViewActions.click;
 import static androidx.test.espresso.assertion.ViewAssertions.doesNotExist;
+import static androidx.test.espresso.assertion.ViewAssertions.matches;
+import static androidx.test.espresso.matcher.ViewMatchers.isDisplayed;
 import static androidx.test.espresso.matcher.ViewMatchers.withText;
 
 import static org.hamcrest.Matchers.equalTo;
@@ -34,6 +36,7 @@
 import androidx.annotation.Nullable;
 import androidx.recyclerview.widget.RecyclerView;
 import androidx.recyclerview.widget.RecyclerView.ViewHolder;
+import androidx.test.espresso.Espresso;
 import androidx.test.filters.MediumTest;
 import androidx.test.filters.SmallTest;
 import androidx.test.platform.app.InstrumentationRegistry;
@@ -117,6 +120,7 @@
 import org.chromium.components.power_bookmarks.PowerBookmarkMeta;
 import org.chromium.components.power_bookmarks.ShoppingSpecifics;
 import org.chromium.components.profile_metrics.BrowserProfileType;
+import org.chromium.components.sync.SyncFeatureMap;
 import org.chromium.components.sync.SyncService;
 import org.chromium.components.sync.SyncService.SyncStateChangedListener;
 import org.chromium.content_public.browser.test.util.TestThreadUtils;
@@ -140,7 +144,8 @@
 // TODO(1406059): Disabling the shopping CPA should not be a requirement for these tests.
 @DisableFeatures({
     ChromeFeatureList.CONTEXTUAL_PAGE_ACTION_PRICE_TRACKING,
-    ChromeFeatureList.ANDROID_IMPROVED_BOOKMARKS
+    ChromeFeatureList.ANDROID_IMPROVED_BOOKMARKS,
+    SyncFeatureMap.ENABLE_BOOKMARK_FOLDERS_FOR_ACCOUNT_STORAGE
 })
 // TODO(crbug.com/1426138): Investigate batching.
 @DoNotBatch(reason = "BookmarkTest has behaviours and thus can't be batched.")
@@ -440,6 +445,25 @@
         onView(withText("You'll find your reading list here"));
     }
 
+    @Test
+    @MediumTest
+    @Restriction({UiRestriction.RESTRICTION_TYPE_PHONE})
+    public void testOpenFromReadingListAndNavigateBack() throws Exception {
+        openBookmarkManager();
+        BookmarkTestUtil.waitForBookmarkModelLoaded();
+        runOnUiThreadBlocking(
+                () ->
+                        mBookmarkModel.addToReadingList(
+                                mBookmarkModel.getLocalOrSyncableReadingListFolder(),
+                                "test",
+                                new GURL("https://test.com")));
+
+        BookmarkTestUtil.openReadingList(mItemsContainer, mDelegate, mBookmarkModel);
+        onView(withText("test")).perform(click());
+        Espresso.pressBack();
+        onView(withText("test")).check(matches(isDisplayed()));
+    }
+
     // TODO(twellington): Write a folder navigation test for tablets that waits for the Tab hosting
     //                    the native page to update its url after navigations.
 
diff --git a/chrome/android/javatests/src/org/chromium/chrome/browser/app/bookmarks/ReadingListTest.java b/chrome/android/javatests/src/org/chromium/chrome/browser/app/bookmarks/ReadingListTest.java
index 698721f..30f9e44 100644
--- a/chrome/android/javatests/src/org/chromium/chrome/browser/app/bookmarks/ReadingListTest.java
+++ b/chrome/android/javatests/src/org/chromium/chrome/browser/app/bookmarks/ReadingListTest.java
@@ -73,6 +73,7 @@
 import org.chromium.components.browser_ui.widget.RecyclerViewTestUtils;
 import org.chromium.components.browser_ui.widget.selectable_list.SelectableListToolbar;
 import org.chromium.components.embedder_support.util.UrlConstants;
+import org.chromium.components.sync.SyncFeatureMap;
 import org.chromium.content_public.browser.test.util.TestThreadUtils;
 import org.chromium.content_public.browser.test.util.TouchCommon;
 import org.chromium.net.test.EmbeddedTestServer;
@@ -86,7 +87,10 @@
 /** Tests for the reading list in the bookmark manager. */
 @RunWith(ChromeJUnit4ClassRunner.class)
 @CommandLineFlags.Add({ChromeSwitches.DISABLE_FIRST_RUN_EXPERIENCE})
-@DisableFeatures({ChromeFeatureList.ANDROID_IMPROVED_BOOKMARKS})
+@DisableFeatures({
+    ChromeFeatureList.ANDROID_IMPROVED_BOOKMARKS,
+    SyncFeatureMap.ENABLE_BOOKMARK_FOLDERS_FOR_ACCOUNT_STORAGE
+})
 @DoNotBatch(reason = "BookmarkTest has behaviours and thus can't be batched.")
 public class ReadingListTest {
     @Rule
diff --git a/chrome/android/javatests/src/org/chromium/chrome/browser/bookmarks/AccountBookmarkTest.java b/chrome/android/javatests/src/org/chromium/chrome/browser/bookmarks/AccountBookmarkTest.java
index 17b1e16..f065d35 100644
--- a/chrome/android/javatests/src/org/chromium/chrome/browser/bookmarks/AccountBookmarkTest.java
+++ b/chrome/android/javatests/src/org/chromium/chrome/browser/bookmarks/AccountBookmarkTest.java
@@ -5,11 +5,10 @@
 package org.chromium.chrome.browser.bookmarks;
 
 import static androidx.test.espresso.Espresso.onView;
+import static androidx.test.espresso.action.ViewActions.click;
 import static androidx.test.espresso.assertion.ViewAssertions.matches;
-import static androidx.test.espresso.matcher.ViewMatchers.hasDescendant;
 import static androidx.test.espresso.matcher.ViewMatchers.isDescendantOfA;
 import static androidx.test.espresso.matcher.ViewMatchers.isDisplayed;
-import static androidx.test.espresso.matcher.ViewMatchers.withEffectiveVisibility;
 import static androidx.test.espresso.matcher.ViewMatchers.withId;
 import static androidx.test.espresso.matcher.ViewMatchers.withText;
 
@@ -18,14 +17,11 @@
 
 import static org.chromium.content_public.browser.test.util.TestThreadUtils.runOnUiThreadBlocking;
 
-import android.view.View;
-
 import androidx.recyclerview.widget.RecyclerView;
-import androidx.test.espresso.ViewInteraction;
-import androidx.test.espresso.matcher.ViewMatchers;
+import androidx.test.espresso.Espresso;
+import androidx.test.filters.MediumTest;
 import androidx.test.filters.SmallTest;
 
-import org.hamcrest.Matcher;
 import org.junit.Before;
 import org.junit.Rule;
 import org.junit.Test;
@@ -34,10 +30,12 @@
 
 import org.chromium.base.test.util.CommandLineFlags;
 import org.chromium.base.test.util.CriteriaHelper;
+import org.chromium.base.test.util.DisabledTest;
 import org.chromium.base.test.util.DoNotBatch;
 import org.chromium.base.test.util.Features;
 import org.chromium.base.test.util.Features.DisableFeatures;
 import org.chromium.base.test.util.Features.EnableFeatures;
+import org.chromium.base.test.util.Restriction;
 import org.chromium.chrome.browser.flags.ChromeFeatureList;
 import org.chromium.chrome.browser.flags.ChromeSwitches;
 import org.chromium.chrome.browser.profiles.Profile;
@@ -46,8 +44,11 @@
 import org.chromium.chrome.test.ChromeJUnit4ClassRunner;
 import org.chromium.chrome.test.R;
 import org.chromium.chrome.test.util.BookmarkTestRule;
+import org.chromium.chrome.test.util.BookmarkTestUtil;
 import org.chromium.components.browser_ui.widget.RecyclerViewTestUtils;
 import org.chromium.components.sync.SyncFeatureMap;
+import org.chromium.ui.test.util.UiRestriction;
+import org.chromium.url.GURL;
 
 @RunWith(ChromeJUnit4ClassRunner.class)
 @CommandLineFlags.Add({ChromeSwitches.DISABLE_FIRST_RUN_EXPERIENCE})
@@ -98,29 +99,44 @@
         checkTopLevelAccountFoldersDisplayed();
     }
 
+    @Test
+    @MediumTest
+    @Restriction({UiRestriction.RESTRICTION_TYPE_PHONE})
+    @EnableFeatures({ChromeFeatureList.REPLACE_SYNC_PROMOS_WITH_SIGN_IN_PROMOS})
+    @DisabledTest(
+            message =
+                    "Enable this test when reading list is available w/o restart crbug.com/1510547")
+    public void testOpenFromReadingListAndNavigateBack() throws Exception {
+        mSyncTestRule.setSelectedTypes(true, null);
+        CriteriaHelper.pollUiThread(() -> mBookmarkModel.getAccountReadingListFolder() != null);
+        RecyclerViewTestUtils.waitForStableMvcRecyclerView(
+                mBookmarkManagerCoordinator.getRecyclerViewForTesting());
+
+        runOnUiThreadBlocking(
+                () ->
+                        mBookmarkModel.addToReadingList(
+                                mBookmarkModel.getAccountReadingListFolder(),
+                                "test",
+                                new GURL("https://test.com")));
+
+        BookmarkTestUtil.getRecyclerRowViewInteraction(
+                        "Reading list", /* isAccountBookmark= */ true)
+                .perform(click());
+        onView(withText("test")).perform(click());
+        Espresso.pressBack();
+        onView(withText("test")).check(matches(isDisplayed()));
+    }
+
     private void checkTopLevelAccountFoldersDisplayed() {
         checkToolbarTitleMatches("Bookmarks");
         onView(withText("In your Google Account")).check(matches(isDisplayed()));
-        getRecyclerViewItem("Mobile bookmarks", true).check(matches(isDisplayed()));
+        BookmarkTestUtil.getRecyclerRowViewInteraction("Mobile bookmarks", true)
+                .check(matches(isDisplayed()));
         onView(withText("Only on this device")).check(matches(isDisplayed()));
-        getRecyclerViewItem("Mobile bookmarks", false).check(matches(isDisplayed()));
-        getRecyclerViewItem("Reading list", false).check(matches(isDisplayed()));
-    }
-
-    private ViewInteraction getRecyclerViewItem(String text, boolean isAccountBookmark) {
-        return onView(getRecyclerItemMatcher(text, isAccountBookmark));
-    }
-
-    private Matcher<View> getRecyclerItemMatcher(String text, boolean isAccountBookmark) {
-        ViewMatchers.Visibility visibility =
-                isAccountBookmark ? ViewMatchers.Visibility.GONE : ViewMatchers.Visibility.VISIBLE;
-        return allOf(
-                withId(R.id.container),
-                hasDescendant(withText(text)),
-                hasDescendant(
-                        allOf(
-                                withId(R.id.local_bookmark_image),
-                                withEffectiveVisibility(visibility))));
+        BookmarkTestUtil.getRecyclerRowViewInteraction("Mobile bookmarks", false)
+                .check(matches(isDisplayed()));
+        BookmarkTestUtil.getRecyclerRowViewInteraction("Reading list", false)
+                .check(matches(isDisplayed()));
     }
 
     // Checks the toolbar title against the given string.
diff --git a/chrome/android/javatests/src/org/chromium/chrome/browser/bookmarks/BookmarkOpenerTest.java b/chrome/android/javatests/src/org/chromium/chrome/browser/bookmarks/BookmarkOpenerTest.java
index d246eea..b0363ab 100644
--- a/chrome/android/javatests/src/org/chromium/chrome/browser/bookmarks/BookmarkOpenerTest.java
+++ b/chrome/android/javatests/src/org/chromium/chrome/browser/bookmarks/BookmarkOpenerTest.java
@@ -17,6 +17,7 @@
 import org.junit.Before;
 import org.junit.Rule;
 import org.junit.Test;
+import org.junit.rules.TestRule;
 import org.junit.runner.RunWith;
 
 import org.chromium.base.metrics.RecordHistogram;
@@ -24,6 +25,8 @@
 import org.chromium.base.test.util.CommandLineFlags;
 import org.chromium.base.test.util.CriteriaHelper;
 import org.chromium.base.test.util.DoNotBatch;
+import org.chromium.base.test.util.Features;
+import org.chromium.base.test.util.Features.DisableFeatures;
 import org.chromium.base.test.util.UserActionTester;
 import org.chromium.chrome.browser.app.bookmarks.BookmarkActivity;
 import org.chromium.chrome.browser.flags.ChromeSwitches;
@@ -38,6 +41,7 @@
 import org.chromium.components.bookmarks.BookmarkId;
 import org.chromium.components.browser_ui.widget.RecyclerViewTestUtils;
 import org.chromium.components.embedder_support.util.UrlConstants;
+import org.chromium.components.sync.SyncFeatureMap;
 import org.chromium.content_public.browser.test.util.TestThreadUtils;
 import org.chromium.ui.accessibility.AccessibilityState;
 import org.chromium.url.GURL;
@@ -49,10 +53,13 @@
 @RunWith(ChromeJUnit4ClassRunner.class)
 @CommandLineFlags.Add({ChromeSwitches.DISABLE_FIRST_RUN_EXPERIENCE})
 @DoNotBatch(reason = "Tabs can't be closed reliably between tests.")
+@DisableFeatures({SyncFeatureMap.ENABLE_BOOKMARK_FOLDERS_FOR_ACCOUNT_STORAGE})
 public class BookmarkOpenerTest {
     @Rule
     public ChromeTabbedActivityTestRule mActivityTestRule = new ChromeTabbedActivityTestRule();
 
+    @Rule public TestRule mProcessor = new Features.JUnitProcessor();
+
     private BookmarkOpener mBookmarkOpener;
 
     private BookmarkModel mBookmarkModel;
diff --git a/chrome/test/android/javatests/src/org/chromium/chrome/test/util/BookmarkTestUtil.java b/chrome/test/android/javatests/src/org/chromium/chrome/test/util/BookmarkTestUtil.java
index feecea6..704a330 100644
--- a/chrome/test/android/javatests/src/org/chromium/chrome/test/util/BookmarkTestUtil.java
+++ b/chrome/test/android/javatests/src/org/chromium/chrome/test/util/BookmarkTestUtil.java
@@ -6,9 +6,16 @@
 
 import static androidx.test.espresso.Espresso.onView;
 import static androidx.test.espresso.action.ViewActions.click;
+import static androidx.test.espresso.matcher.ViewMatchers.hasDescendant;
+import static androidx.test.espresso.matcher.ViewMatchers.withEffectiveVisibility;
+import static androidx.test.espresso.matcher.ViewMatchers.withId;
 import static androidx.test.espresso.matcher.ViewMatchers.withText;
 
+import static org.hamcrest.Matchers.allOf;
+
 import androidx.recyclerview.widget.RecyclerView;
+import androidx.test.espresso.ViewInteraction;
+import androidx.test.espresso.matcher.ViewMatchers;
 import androidx.test.platform.app.InstrumentationRegistry;
 
 import org.hamcrest.core.IsInstanceOf;
@@ -16,6 +23,7 @@
 import org.chromium.base.ApplicationStatus;
 import org.chromium.base.test.util.Criteria;
 import org.chromium.base.test.util.CriteriaHelper;
+import org.chromium.chrome.R;
 import org.chromium.chrome.browser.ChromeTabbedActivity;
 import org.chromium.chrome.browser.app.bookmarks.BookmarkActivity;
 import org.chromium.chrome.browser.app.bookmarks.BookmarkAddEditFolderActivity;
@@ -175,4 +183,22 @@
                     return false;
                 });
     }
+
+    /**
+     * Returns the {@link ViewInteraction} for a bookmark row with the given text and account-ness.
+     * The return value can be used to make assertions about.
+     */
+    public static ViewInteraction getRecyclerRowViewInteraction(
+            String text, boolean isAccountBookmark) {
+        ViewMatchers.Visibility visibility =
+                isAccountBookmark ? ViewMatchers.Visibility.GONE : ViewMatchers.Visibility.VISIBLE;
+        return onView(
+                allOf(
+                        withId(R.id.container),
+                        hasDescendant(withText(text)),
+                        hasDescendant(
+                                allOf(
+                                        withId(R.id.local_bookmark_image),
+                                        withEffectiveVisibility(visibility)))));
+    }
 }