chromium/chrome/android/java/src/org/chromium/chrome/browser/bookmarks/BookmarkBridge.java

// Copyright 2013 The Chromium Authors
// 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.bookmarks;

import android.text.TextUtils;
import android.util.Pair;

import androidx.annotation.NonNull;
import androidx.annotation.Nullable;
import androidx.annotation.VisibleForTesting;

import com.google.protobuf.InvalidProtocolBufferException;

import org.jni_zero.CalledByNative;
import org.jni_zero.JniType;
import org.jni_zero.NativeMethods;

import org.chromium.base.ContextUtils;
import org.chromium.base.ObserverList;
import org.chromium.base.ThreadUtils;
import org.chromium.base.metrics.RecordUserAction;
import org.chromium.chrome.browser.partnerbookmarks.PartnerBookmarksShim;
import org.chromium.chrome.browser.profiles.Profile;
import org.chromium.components.bookmarks.BookmarkId;
import org.chromium.components.bookmarks.BookmarkItem;
import org.chromium.components.bookmarks.BookmarkType;
import org.chromium.components.power_bookmarks.PowerBookmarkMeta;
import org.chromium.components.power_bookmarks.PowerBookmarkType;
import org.chromium.url.GURL;

import java.util.ArrayList;
import java.util.List;

/**
 * Provides the communication channel for Android to fetch and manipulate the bookmark model stored
 * in native.
 */
class BookmarkBridge {
    private final ObserverList<BookmarkModelObserver> mObservers = new ObserverList<>();

    private long mNativeBookmarkBridge;
    private boolean mIsDestroyed;
    private boolean mIsDoingExtensiveChanges;
    private boolean mIsNativeBookmarkModelLoaded;

    // Lazily set pseudo-constants. These should never change at runtime. Used to avoid crossing
    // JNI to fetch information.
    private @Nullable BookmarkId mRootFolderId;
    private @Nullable BookmarkId mMobileFolderId;
    private @Nullable BookmarkId mOtherFolderId;
    private @Nullable BookmarkId mDesktopFolderId;
    private @Nullable BookmarkId mLocalOrSyncableReadingListFolderId;

    /** Returns whether account bookmark folders are currently active. */
    public boolean areAccountBookmarkFoldersActive() {
        ThreadUtils.assertOnUiThread();
        return BookmarkBridgeJni.get().areAccountBookmarkFoldersActive(mNativeBookmarkBridge);
    }

    /**
     * Handler to fetch the bookmarks, titles, urls and folder hierarchy.
     *
     * @param profile Profile instance corresponding to the active profile.
     */
    static BookmarkModel getForProfile(Profile profile) {
        ThreadUtils.assertOnUiThread();
        return BookmarkBridgeJni.get().nativeGetForProfile(profile);
    }

    @CalledByNative
    static BookmarkModel createBookmarkModel(long nativeBookmarkBridge) {
        return new BookmarkModel(nativeBookmarkBridge);
    }

    BookmarkBridge(long nativeBookmarkBridge) {
        mNativeBookmarkBridge = nativeBookmarkBridge;
        mIsDoingExtensiveChanges =
                BookmarkBridgeJni.get().isDoingExtensiveChanges(mNativeBookmarkBridge);
    }

    /** Destroys this instance so no further calls can be executed. */
    void destroy() {
        mIsDestroyed = true;
        if (mNativeBookmarkBridge != 0) {
            BookmarkBridgeJni.get().destroy(mNativeBookmarkBridge);
            mNativeBookmarkBridge = 0;
            mIsNativeBookmarkModelLoaded = false;
        }
        mObservers.clear();
    }

    /** Returns whether the bridge has been destroyed. */
    private boolean isDestroyed() {
        return mIsDestroyed;
    }

    /** Returns the most recently added BookmarkId */
    public @Nullable BookmarkId getMostRecentlyAddedUserBookmarkIdForUrl(@NonNull GURL url) {
        ThreadUtils.assertOnUiThread();
        if (mNativeBookmarkBridge == 0) return null;
        assert mIsNativeBookmarkModelLoaded;
        return BookmarkBridgeJni.get()
                .getMostRecentlyAddedUserBookmarkIdForUrl(mNativeBookmarkBridge, url);
    }

    /**
     * Load an empty partner bookmark shim for testing. The root node for bookmark will be an empty
     * node.
     */
    public void loadEmptyPartnerBookmarkShimForTesting() {
        BookmarkBridgeJni.get()
                .loadEmptyPartnerBookmarkShimForTesting(mNativeBookmarkBridge); // IN-TEST
    }

    /**
     * Load a fake partner bookmark shim for testing. To see (or edit) the titles and URLs of the
     * partner bookmarks, go to bookmark_bridge.cc.
     */
    public void loadFakePartnerBookmarkShimForTesting() {
        BookmarkBridgeJni.get()
                .loadFakePartnerBookmarkShimForTesting(mNativeBookmarkBridge); // IN-TEST
    }

    /**
     * Add an observer to bookmark model changes.
     *
     * @param observer The observer to be added.
     */
    public void addObserver(BookmarkModelObserver observer) {
        mObservers.addObserver(observer);
    }

    /**
     * Remove an observer of bookmark model changes.
     *
     * @param observer The observer to be removed.
     */
    public void removeObserver(BookmarkModelObserver observer) {
        mObservers.removeObserver(observer);
    }

    /**
     * @return Whether or not the underlying bookmark model is loaded.
     */
    public boolean isBookmarkModelLoaded() {
        return mIsNativeBookmarkModelLoaded;
    }

    /**
     * Schedules a runnable to run after the bookmark model is loaded. If the model is already
     * loaded, executes the runnable immediately. If not, also kick off partner bookmark reading.
     *
     * @return Whether the given runnable is executed synchronously.
     */
    public boolean finishLoadingBookmarkModel(final Runnable runAfterModelLoaded) {
        if (isBookmarkModelLoaded()) {
            runAfterModelLoaded.run();
            return true;
        }

        addObserver(
                new BookmarkModelObserver() {
                    @Override
                    public void bookmarkModelLoaded() {
                        removeObserver(this);
                        runAfterModelLoaded.run();
                    }

                    @Override
                    public void bookmarkModelChanged() {}
                });

        // Start reading as a fail-safe measure to avoid waiting forever if the caller forgets to
        // call kickOffReading().
        PartnerBookmarksShim.kickOffReading(ContextUtils.getApplicationContext());
        return false;
    }

    /**
     * Gets the {@link BookmarkItem} which is referenced by the given {@link BookmarkId}.
     *
     * @param id The {@link BookmarkId} used to lookup the corresponding {@link BookmarkItem}.
     * @return A BookmarkItem instance for the given BookmarkId. <code>null</code> if it doesn't
     *     exist.
     */
    public @Nullable BookmarkItem getBookmarkById(@Nullable BookmarkId id) {
        ThreadUtils.assertOnUiThread();
        if (mNativeBookmarkBridge == 0) return null;
        assert mIsNativeBookmarkModelLoaded;
        if (id == null) return null;

        return BookmarkBridgeJni.get()
                .getBookmarkById(mNativeBookmarkBridge, id.getId(), id.getType());
    }

    /**
     * @return The top level folders, including special folders (managed bookmarks, reading list,
     *     partner bookmarks). Will show empty folder according to the logic in BookmarkClient.
     */
    public List<BookmarkId> getTopLevelFolderIds() {
        return getTopLevelFolderIds(/* ignoreVisibility= */ false);
    }

    /**
     * @param ignoreVisibility Whether the visible while empty logic, found in BookmarkClient, is
     *     used when gathering nodes. When true, all folders are shown regardless of client defined
     *     visibility. When false, the client defined visibility rules are used. See
     *     components/bookmarks/browser/bookmark_client.h for more information. When account
     *     bookmarks are active, only a subset of the local folders are included when this is true.
     *     This is to avoid overloading the user with a lof of unnecessary local folders (folders
     *     included are the local Mobile and Reading List folders).
     * @return The top level folders, including special folders (managed bookmarks, reading list,
     *     partner bookmarks).
     */
    public List<BookmarkId> getTopLevelFolderIds(boolean ignoreVisibility) {
        ThreadUtils.assertOnUiThread();
        if (mNativeBookmarkBridge == 0) return new ArrayList<>();
        assert mIsNativeBookmarkModelLoaded;
        List<BookmarkId> result = new ArrayList<>();
        BookmarkBridgeJni.get()
                .getTopLevelFolderIds(mNativeBookmarkBridge, ignoreVisibility, result);
        return result;
    }

    /** Returns the local/syncable synthetic reading list folder. */
    public BookmarkId getLocalOrSyncableReadingListFolder() {
        ThreadUtils.assertOnUiThread();
        if (mNativeBookmarkBridge == 0) return null;
        assert mIsNativeBookmarkModelLoaded;
        if (mLocalOrSyncableReadingListFolderId == null) {
            mLocalOrSyncableReadingListFolderId =
                    BookmarkBridgeJni.get()
                            .getLocalOrSyncableReadingListFolder(mNativeBookmarkBridge);
        }
        return mLocalOrSyncableReadingListFolderId;
    }

    /**
     * Returns the account synthetic reading list folder. Function will return null if the required
     * conditions to use account-bound data aren't satisfied: - The user is signed-in and not
     * syncing. - The user has the kReadingList sync data type enabled.
     */
    public BookmarkId getAccountReadingListFolder() {
        ThreadUtils.assertOnUiThread();
        if (mNativeBookmarkBridge == 0) return null;
        assert mIsNativeBookmarkModelLoaded;

        // Note: The account reading list folder isn't cached because the availability can change
        // during runtime.
        return BookmarkBridgeJni.get().getAccountReadingListFolder(mNativeBookmarkBridge);
    }

    /** Returns the default reading list location. */
    public BookmarkId getDefaultReadingListFolder() {
        ThreadUtils.assertOnUiThread();
        if (mNativeBookmarkBridge == 0) return null;
        assert mIsNativeBookmarkModelLoaded;
        return BookmarkBridgeJni.get().getDefaultReadingListFolder(mNativeBookmarkBridge);
    }

    /** Returns the default bookmark location. */
    public BookmarkId getDefaultBookmarkFolder() {
        ThreadUtils.assertOnUiThread();
        if (mNativeBookmarkBridge == 0) return null;
        assert mIsNativeBookmarkModelLoaded;
        return BookmarkBridgeJni.get().getDefaultBookmarkFolder(mNativeBookmarkBridge);
    }

    /**
     * Populates folderList with BookmarkIds of folders users can move bookmarks to and all folders
     * have corresponding depth value in depthList. Folders having depths of 0 will be shown as
     * top-layered folders. These include "Desktop Folder" itself as well as all children of
     * "mobile" and "other". Children of 0-depth folders have depth of 1, and so on.
     *
     * <p>The result list will be sorted alphabetically by title. "mobile", "other", root node,
     * managed folder, partner folder are NOT included as results.
     */
    @VisibleForTesting
    public void getAllFoldersWithDepths(List<BookmarkId> folderList, List<Integer> depthList) {
        ThreadUtils.assertOnUiThread();
        if (mNativeBookmarkBridge == 0) return;
        assert mIsNativeBookmarkModelLoaded;
        BookmarkBridgeJni.get()
                .getAllFoldersWithDepths(mNativeBookmarkBridge, folderList, depthList);
    }

    /**
     * Calls {@link #getAllFoldersWithDepths(List, List)} and remove all folders and children in
     * bookmarksToMove. This method is useful when finding a list of possible parent folers when
     * moving some folders (a folder cannot be moved to its own children).
     */
    public void getMoveDestinations(
            List<BookmarkId> folderList,
            List<Integer> depthList,
            List<BookmarkId> bookmarksToMove) {
        if (mNativeBookmarkBridge == 0) return;
        ThreadUtils.assertOnUiThread();
        assert mIsNativeBookmarkModelLoaded;
        BookmarkBridgeJni.get()
                .getAllFoldersWithDepths(mNativeBookmarkBridge, folderList, depthList);
        if (bookmarksToMove == null || bookmarksToMove.size() == 0) return;

        boolean shouldTrim = false;
        int trimThreshold = -1;
        for (int i = 0; i < folderList.size(); i++) {
            int depth = depthList.get(i);
            if (shouldTrim) {
                if (depth <= trimThreshold) {
                    shouldTrim = false;
                    trimThreshold = -1;
                } else {
                    folderList.remove(i);
                    depthList.remove(i);
                    i--;
                }
            }
            // Do not use else here because shouldTrim could be set true after if (shouldTrim)
            // statement.
            if (!shouldTrim) {
                BookmarkId folder = folderList.get(i);
                if (bookmarksToMove.contains(folder)) {
                    shouldTrim = true;
                    trimThreshold = depth;
                    folderList.remove(i);
                    depthList.remove(i);
                    i--;
                }
            }
        }
    }

    /** Returns the BookmarkId for root folder node. */
    public BookmarkId getRootFolderId() {
        ThreadUtils.assertOnUiThread();
        if (mNativeBookmarkBridge == 0) return null;
        assert mIsNativeBookmarkModelLoaded;
        if (mRootFolderId == null) {
            mRootFolderId = BookmarkBridgeJni.get().getRootFolderId(mNativeBookmarkBridge);
        }
        return mRootFolderId;
    }

    /** Returns the BookmarkId for Mobile folder node. */
    public BookmarkId getMobileFolderId() {
        ThreadUtils.assertOnUiThread();
        if (mNativeBookmarkBridge == 0) return null;
        assert mIsNativeBookmarkModelLoaded;
        if (mMobileFolderId == null) {
            mMobileFolderId = BookmarkBridgeJni.get().getMobileFolderId(mNativeBookmarkBridge);
        }
        return mMobileFolderId;
    }

    /** Returns Id representing the special "other" folder from bookmark model. */
    public BookmarkId getOtherFolderId() {
        ThreadUtils.assertOnUiThread();
        if (mNativeBookmarkBridge == 0) return null;
        assert mIsNativeBookmarkModelLoaded;
        if (mOtherFolderId == null) {
            mOtherFolderId = BookmarkBridgeJni.get().getOtherFolderId(mNativeBookmarkBridge);
        }
        return mOtherFolderId;
    }

    /** Returns the BookmarkId representing special "desktop" folder, namely "bookmark bar". */
    public BookmarkId getDesktopFolderId() {
        ThreadUtils.assertOnUiThread();
        if (mNativeBookmarkBridge == 0) return null;
        assert mIsNativeBookmarkModelLoaded;
        if (mDesktopFolderId == null) {
            mDesktopFolderId = BookmarkBridgeJni.get().getDesktopFolderId(mNativeBookmarkBridge);
        }
        return mDesktopFolderId;
    }

    /** Returns the id representing the special account "mobile" folder from bookmark model. */
    public BookmarkId getAccountMobileFolderId() {
        ThreadUtils.assertOnUiThread();
        if (mNativeBookmarkBridge == 0) return null;
        assert mIsNativeBookmarkModelLoaded;
        return BookmarkBridgeJni.get().getAccountMobileFolderId(mNativeBookmarkBridge);
    }

    /** Returns the id representing the special account "other" folder from bookmark model. */
    public BookmarkId getAccountOtherFolderId() {
        ThreadUtils.assertOnUiThread();
        if (mNativeBookmarkBridge == 0) return null;
        assert mIsNativeBookmarkModelLoaded;
        return BookmarkBridgeJni.get().getAccountOtherFolderId(mNativeBookmarkBridge);
    }

    /**
     * @return BookmarkId representing special account "desktop" folder, namely "bookmark bar".
     */
    public BookmarkId getAccountDesktopFolderId() {
        ThreadUtils.assertOnUiThread();
        if (mNativeBookmarkBridge == 0) return null;
        assert mIsNativeBookmarkModelLoaded;
        return BookmarkBridgeJni.get().getAccountDesktopFolderId(mNativeBookmarkBridge);
    }

    /**
     * Gets Bookmark GUID which is immutable and differs from the BookmarkId in that it is
     * consistent across different clients and stable throughout the lifetime of the bookmark, with
     * the exception of nodes added to the Managed Bookmarks folder, whose GUIDs are re-assigned at
     * start-up every time.
     *
     * @return Bookmark GUID of the given node.
     */
    public String getBookmarkGuidByIdForTesting(BookmarkId id) {
        ThreadUtils.assertOnUiThread();
        if (mNativeBookmarkBridge == 0) return null;
        assert mIsNativeBookmarkModelLoaded;
        return BookmarkBridgeJni.get()
                .getBookmarkGuidByIdForTesting(mNativeBookmarkBridge, id.getId(), id.getType());
    }

    /**
     * @return The number of children that the given node has.
     */
    public int getChildCount(BookmarkId id) {
        ThreadUtils.assertOnUiThread();
        if (mNativeBookmarkBridge == 0) return 0;
        assert mIsNativeBookmarkModelLoaded;
        return BookmarkBridgeJni.get()
                .getChildCount(mNativeBookmarkBridge, id.getId(), id.getType());
    }

    /**
     * Reads sub-folder IDs, sub-bookmark IDs, or both of the given folder.
     *
     * @return Child IDs of the given folder, with the specified type.
     */
    public List<BookmarkId> getChildIds(BookmarkId id) {
        ThreadUtils.assertOnUiThread();
        if (mNativeBookmarkBridge == 0) return new ArrayList<>();
        assert mIsNativeBookmarkModelLoaded;

        List<BookmarkId> result = new ArrayList<>();
        BookmarkBridgeJni.get()
                .getChildIds(mNativeBookmarkBridge, id.getId(), id.getType(), result);
        return result;
    }

    /**
     * Gets the child of a folder at the specific position.
     *
     * @param folderId Id of the parent folder
     * @param index Position of child among all children in folder
     * @return BookmarkId of the child, which will be null if folderId does not point to a folder or
     *     index is invalid.
     */
    public BookmarkId getChildAt(BookmarkId folderId, int index) {
        ThreadUtils.assertOnUiThread();
        if (mNativeBookmarkBridge == 0) return null;
        assert mIsNativeBookmarkModelLoaded;
        return BookmarkBridgeJni.get()
                .getChildAt(mNativeBookmarkBridge, folderId.getId(), folderId.getType(), index);
    }

    /**
     * Get the total number of bookmarks in the sub tree of the specified folder.
     *
     * @param id The {@link BookmarkId} of the folder to be queried.
     * @return The total number of bookmarks in the folder.
     */
    public int getTotalBookmarkCount(BookmarkId id) {
        ThreadUtils.assertOnUiThread();
        if (mNativeBookmarkBridge == 0) return 0;
        assert mIsNativeBookmarkModelLoaded;
        return BookmarkBridgeJni.get()
                .getTotalBookmarkCount(mNativeBookmarkBridge, id.getId(), id.getType());
    }

    /**
     * Synchronously gets a list of bookmarks that match the specified search query.
     *
     * @param query Keyword used for searching bookmarks.
     * @param maxNumberOfResult Maximum number of result to fetch.
     * @return List of bookmark IDs that are related to the given query.
     */
    public List<BookmarkId> searchBookmarks(String query, int maxNumberOfResult) {
        return searchBookmarks(query, null, null, maxNumberOfResult);
    }

    /**
     * Synchronously gets a list of bookmarks that match the specified search query.
     *
     * @param query Keyword used for searching bookmarks.
     * @param tags A list of tags the resulting bookmarks should have.
     * @param powerBookmarkType The type of power bookmark type to search for (or null for all).
     * @param maxNumberOfResult Maximum number of result to fetch.
     * @return List of bookmark IDs that are related to the given query.
     */
    public List<BookmarkId> searchBookmarks(
            String query,
            @Nullable String[] tags,
            @Nullable PowerBookmarkType powerBookmarkType,
            int maxNumberOfResult) {
        ThreadUtils.assertOnUiThread();
        if (mNativeBookmarkBridge == 0) return new ArrayList<>();
        List<BookmarkId> bookmarkMatches = new ArrayList<>();
        int typeInt = powerBookmarkType == null ? -1 : powerBookmarkType.getNumber();
        BookmarkBridgeJni.get()
                .searchBookmarks(
                        mNativeBookmarkBridge,
                        bookmarkMatches,
                        query,
                        tags,
                        typeInt,
                        maxNumberOfResult);
        return bookmarkMatches;
    }

    /**
     * Synchronously gets a list of bookmarks of the given type
     *
     * @param powerBookmarkType The type of power bookmark type to search for (or null for all).
     * @return List of bookmark IDs that are related to the given query.
     */
    public List<BookmarkId> getBookmarksOfType(@NonNull PowerBookmarkType powerBookmarkType) {
        ThreadUtils.assertOnUiThread();
        if (mNativeBookmarkBridge == 0) return new ArrayList<>();
        List<BookmarkId> bookmarkMatches = new ArrayList<>();
        int typeInt = powerBookmarkType.getNumber();
        BookmarkBridgeJni.get().getBookmarksOfType(mNativeBookmarkBridge, bookmarkMatches, typeInt);
        return bookmarkMatches;
    }

    /** Set title of the given bookmark. */
    public void setBookmarkTitle(BookmarkId id, String title) {
        ThreadUtils.assertOnUiThread();
        if (mNativeBookmarkBridge == 0) return;
        assert mIsNativeBookmarkModelLoaded;
        BookmarkBridgeJni.get()
                .setBookmarkTitle(mNativeBookmarkBridge, id.getId(), id.getType(), title);
    }

    /** Set URL of the given bookmark. */
    public void setBookmarkUrl(BookmarkId id, GURL url) {
        ThreadUtils.assertOnUiThread();
        if (mNativeBookmarkBridge == 0) return;
        assert mIsNativeBookmarkModelLoaded;
        assert id.getType() == BookmarkType.NORMAL;
        BookmarkBridgeJni.get()
                .setBookmarkUrl(mNativeBookmarkBridge, id.getId(), id.getType(), url);
    }

    /**
     * Retrieve the PowerBookmarkMeta for a node if it exists.
     *
     * @param id The {@link BookmarkId} of the bookmark to fetch the meta for.
     * @return The meta or null if none exists.
     */
    public @Nullable PowerBookmarkMeta getPowerBookmarkMeta(@Nullable BookmarkId id) {
        if (mNativeBookmarkBridge == 0) return null;
        if (id == null) return null;
        byte[] protoBytes =
                BookmarkBridgeJni.get()
                        .getPowerBookmarkMeta(mNativeBookmarkBridge, id.getId(), id.getType());

        if (protoBytes == null) return null;

        try {
            return PowerBookmarkMeta.parseFrom(protoBytes);
        } catch (InvalidProtocolBufferException ex) {
            deletePowerBookmarkMeta(id);
            return null;
        }
    }

    /**
     * Set the PowerBookmarkMeta for a node. This MUST be called in order to persist any changes
     * made to the proto in the java layer.
     *
     * @param id The ID of the bookmark to set the meta on.
     * @param meta The meta to store.
     */
    public void setPowerBookmarkMeta(BookmarkId id, PowerBookmarkMeta meta) {
        if (mNativeBookmarkBridge == 0) return;
        if (meta == null) return;
        BookmarkBridgeJni.get()
                .setPowerBookmarkMeta(
                        mNativeBookmarkBridge, id.getId(), id.getType(), meta.toByteArray());
    }

    /**
     * Delete the PowerBookmarkMeta from a node.
     *
     * @param id The ID of the bookmark to remove the meta from.
     */
    public void deletePowerBookmarkMeta(BookmarkId id) {
        if (mNativeBookmarkBridge == 0) return;
        BookmarkBridgeJni.get()
                .deletePowerBookmarkMeta(mNativeBookmarkBridge, id.getId(), id.getType());
    }

    /** Returns whether all of the given {@link BookmarkId}s exist in the current bookmark model. */
    public boolean doAllBookmarksExist(List<BookmarkId> bookmarkIds) {
        ThreadUtils.assertOnUiThread();
        for (BookmarkId bookmarkId : bookmarkIds) {
            if (!doesBookmarkExist(bookmarkId)) {
                return false;
            }
        }
        return true;
    }

    /**
     * @return Whether the given bookmark exist in the current bookmark model, e.g., not deleted.
     */
    public boolean doesBookmarkExist(BookmarkId id) {
        ThreadUtils.assertOnUiThread();
        if (mNativeBookmarkBridge == 0) return false;
        assert mIsNativeBookmarkModelLoaded;
        return BookmarkBridgeJni.get()
                .doesBookmarkExist(mNativeBookmarkBridge, id.getId(), id.getType());
    }

    /**
     * Fetches the bookmarks of the given folder. This is an always-synchronous version of another
     * getBookmarksForFolder function.
     *
     * @param folderId The parent folder id.
     * @return Bookmarks of the given folder.
     */
    public List<BookmarkItem> getBookmarksForFolder(BookmarkId folderId) {
        ThreadUtils.assertOnUiThread();
        if (mNativeBookmarkBridge == 0) return new ArrayList<>();
        assert mIsNativeBookmarkModelLoaded;
        List<BookmarkItem> result = new ArrayList<>();
        BookmarkBridgeJni.get().getBookmarksForFolder(mNativeBookmarkBridge, folderId, result);
        return result;
    }

    /**
     * Check whether the given folder should be visible. This is for top permanent folders that we
     * want to hide when there is no child.
     *
     * @return Whether the given folder should be visible.
     */
    public boolean isFolderVisible(BookmarkId id) {
        ThreadUtils.assertOnUiThread();
        if (mNativeBookmarkBridge == 0) return false;
        assert mIsNativeBookmarkModelLoaded;
        return BookmarkBridgeJni.get()
                .isFolderVisible(mNativeBookmarkBridge, id.getId(), id.getType());
    }

    /**
     * Deletes a specified bookmark node.
     *
     * @param bookmarkId The ID of the bookmark to be deleted.
     */
    public void deleteBookmark(BookmarkId bookmarkId) {
        ThreadUtils.assertOnUiThread();
        if (mNativeBookmarkBridge == 0) return;
        BookmarkBridgeJni.get().deleteBookmark(mNativeBookmarkBridge, bookmarkId);
    }

    /**
     * Removes all the non-permanent bookmark nodes that are editable by the user. Observers are
     * only notified when all nodes have been removed. There is no notification for individual node
     * removals.
     */
    public void removeAllUserBookmarks() {
        ThreadUtils.assertOnUiThread();
        if (mNativeBookmarkBridge == 0) return;
        assert mIsNativeBookmarkModelLoaded;
        BookmarkBridgeJni.get().removeAllUserBookmarks(mNativeBookmarkBridge);
    }

    /**
     * Move the bookmark to the new index within same folder or to a different folder.
     *
     * @param bookmarkId The id of the bookmark that is being moved.
     * @param newParentId The parent folder id.
     * @param index The new index for the bookmark, this argument is ignored if the types of
     *     bookmarkId and newParentId differ.
     */
    public void moveBookmark(BookmarkId bookmarkId, BookmarkId newParentId, int index) {
        ThreadUtils.assertOnUiThread();
        if (mNativeBookmarkBridge == 0) return;
        assert mIsNativeBookmarkModelLoaded;
        BookmarkBridgeJni.get().moveBookmark(mNativeBookmarkBridge, bookmarkId, newParentId, index);
    }

    /**
     * Add a new folder to the given parent folder
     *
     * @param parent Folder where to add. Must be a normal editable folder, instead of a partner
     *     bookmark folder or a managed bookmark folder or root node of the entire bookmark model.
     * @param index The position to locate the new folder
     * @param title The title text of the new folder
     * @return Id of the added node. If adding failed (index is invalid, string is null, parent is
     *     not editable), returns null.
     */
    public BookmarkId addFolder(BookmarkId parent, int index, String title) {
        ThreadUtils.assertOnUiThread();
        if (mNativeBookmarkBridge == 0) return null;
        assert parent.getType() == BookmarkType.NORMAL;
        assert index >= 0;
        assert title != null;

        return BookmarkBridgeJni.get().addFolder(mNativeBookmarkBridge, parent, index, title);
    }

    /**
     * Add a new bookmark to a specific position below parent.
     *
     * @param parent Folder where to add. Must be a normal editable folder, instead of a partner
     *     bookmark folder or a managed bookmark folder or root node of the entire bookmark model.
     * @param index The position where the bookmark will be placed in parent folder
     * @param title Title of the new bookmark. If empty, the URL will be used as the title.
     * @param url Url of the new bookmark
     * @return Id of the added node. If adding failed (index is invalid, string is null, parent is
     *     not editable), returns null.
     */
    public BookmarkId addBookmark(BookmarkId parent, int index, String title, GURL url) {
        ThreadUtils.assertOnUiThread();
        if (mNativeBookmarkBridge == 0) return null;
        assert parent.getType() == BookmarkType.NORMAL;
        assert index >= 0;
        assert title != null;
        assert url != null;

        recordBookmarkAdded();

        if (TextUtils.isEmpty(title)) title = url.getSpec();
        return BookmarkBridgeJni.get()
                .addBookmark(mNativeBookmarkBridge, parent, index, title, url);
    }

    /** Record the user action for adding a bookmark. */
    private void recordBookmarkAdded() {
        RecordUserAction.record("BookmarkAdded");
    }

    /** Undo the last undoable action on the top of the bookmark undo stack */
    public void undo() {
        ThreadUtils.assertOnUiThread();
        if (mNativeBookmarkBridge == 0) return;
        BookmarkBridgeJni.get().undo(mNativeBookmarkBridge);
    }

    /**
     * Start grouping actions for a single undo operation Note: This only works with BookmarkModel,
     * not partner bookmarks.
     */
    public void startGroupingUndos() {
        ThreadUtils.assertOnUiThread();
        if (mNativeBookmarkBridge == 0) return;
        BookmarkBridgeJni.get().startGroupingUndos(mNativeBookmarkBridge);
    }

    /**
     * End grouping actions for a single undo operation Note: This only works with BookmarkModel,
     * not partner bookmarks.
     */
    public void endGroupingUndos() {
        ThreadUtils.assertOnUiThread();
        if (mNativeBookmarkBridge == 0) return;
        BookmarkBridgeJni.get().endGroupingUndos(mNativeBookmarkBridge);
    }

    public boolean isEditBookmarksEnabled() {
        ThreadUtils.assertOnUiThread();
        if (mNativeBookmarkBridge == 0) return false;
        return BookmarkBridgeJni.get().isEditBookmarksEnabled(mNativeBookmarkBridge);
    }

    /** Notifies the observer that bookmark model has been loaded. */
    @VisibleForTesting
    public void notifyBookmarkModelLoaded() {
        // Call isBookmarkModelLoaded() to do the check since it could be overridden by the child
        // class to add the addition logic.
        if (isBookmarkModelLoaded()) {
            for (BookmarkModelObserver observer : mObservers) {
                observer.bookmarkModelLoaded();
            }
        }
    }

    /**
     * Reorders the bookmarks of the folder "parent" to be as specified by newOrder.
     *
     * @param parent The parent folder for the reordered bookmarks.
     * @param newOrder A list of bookmark IDs that represents the new order for these bookmarks.
     */
    public void reorderBookmarks(BookmarkId parent, long[] newOrder) {
        ThreadUtils.assertOnUiThread();
        if (mNativeBookmarkBridge == 0) return;
        BookmarkBridgeJni.get().reorderChildren(mNativeBookmarkBridge, parent, newOrder);
    }

    /**
     * Adds an item to the default reading list if it doesn't already exist.
     *
     * @param title The title to be used for the reading list item.
     * @param url The URL of the reading list item.
     * @return The bookmark ID created after saving the article to the reading list, or null on
     *     error.
     */
    public @Nullable BookmarkId addToDefaultReadingList(@NonNull String title, @NonNull GURL url) {
        ThreadUtils.assertOnUiThread();
        if (mNativeBookmarkBridge == 0) return null;
        assert title != null;
        assert url != null;
        assert mIsNativeBookmarkModelLoaded;

        return addToReadingList(getLocalOrSyncableReadingListFolder(), title, url);
    }

    /**
     * Adds an item to the given reading list if it doesn't already exist.
     *
     * @param parentId The parent reading list to add to.
     * @param title The title to be used for the reading list item.
     * @param url The URL of the reading list item.
     * @return The bookmark ID created after saving the article to the reading list, or null on
     *     error.
     */
    public @Nullable BookmarkId addToReadingList(
            @NonNull BookmarkId parentId, @NonNull String title, @NonNull GURL url) {
        ThreadUtils.assertOnUiThread();
        if (mNativeBookmarkBridge == 0) return null;
        assert parentId != null;
        assert title != null;
        assert url != null;
        assert mIsNativeBookmarkModelLoaded;

        return BookmarkBridgeJni.get()
                .addToReadingList(mNativeBookmarkBridge, parentId, title, url);
    }

    /**
     * Helper method to mark an item as read.
     *
     * @param id The {@link BookmarkId} to set the status for.
     * @param read Whether the item should be marked as read.
     */
    public void setReadStatusForReadingList(@NonNull BookmarkId id, boolean read) {
        if (mNativeBookmarkBridge == 0) return;
        assert id != null;
        BookmarkBridgeJni.get().setReadStatus(mNativeBookmarkBridge, id, read);
    }

    /**
     * Returns the total number of unread reading list items for the given {@link BookmarkId}.
     *
     * @param readingListParentId 1 of the 2 reading list parent ids.
     */
    public int getUnreadCount(@NonNull BookmarkId readingListParentId) {
        ThreadUtils.assertOnUiThread();
        if (mNativeBookmarkBridge == 0) return 0;
        assert readingListParentId != null;
        return BookmarkBridgeJni.get().getUnreadCount(mNativeBookmarkBridge, readingListParentId);
    }

    /** Returns whether the given {@link BookmarkId} belongs to the account. */
    public boolean isAccountBookmark(BookmarkId id) {
        ThreadUtils.assertOnUiThread();
        if (mNativeBookmarkBridge == 0) return false;
        return BookmarkBridgeJni.get().isAccountBookmark(mNativeBookmarkBridge, id);
    }

    /**
     * Checks whether supplied URL has already been bookmarked.
     *
     * @param url The URL to check.
     * @return Whether the URL has been bookmarked.
     */
    public boolean isBookmarked(GURL url) {
        ThreadUtils.assertOnUiThread();
        if (mNativeBookmarkBridge == 0) return false;
        return BookmarkBridgeJni.get().isBookmarked(mNativeBookmarkBridge, url);
    }

    public BookmarkId getPartnerFolderId() {
        ThreadUtils.assertOnUiThread();
        if (mNativeBookmarkBridge == 0) return null;

        assert mIsNativeBookmarkModelLoaded;
        return BookmarkBridgeJni.get().getPartnerFolderId(mNativeBookmarkBridge);
    }

    @CalledByNative
    @VisibleForTesting
    void bookmarkModelLoaded() {
        mIsNativeBookmarkModelLoaded = true;
        notifyBookmarkModelLoaded();
    }

    @CalledByNative
    private void destroyFromNative() {
        destroy();
    }

    @CalledByNative
    private void bookmarkNodeMoved(
            BookmarkItem oldParent, int oldIndex, BookmarkItem newParent, int newIndex) {
        if (mIsDoingExtensiveChanges) return;

        for (BookmarkModelObserver observer : mObservers) {
            observer.bookmarkNodeMoved(oldParent, oldIndex, newParent, newIndex);
        }
    }

    @CalledByNative
    private void bookmarkNodeAdded(BookmarkItem parent, int index) {
        if (mIsDoingExtensiveChanges) return;

        for (BookmarkModelObserver observer : mObservers) {
            observer.bookmarkNodeAdded(parent, index);
        }
    }

    @CalledByNative
    private void bookmarkNodeRemoved(BookmarkItem parent, int oldIndex, BookmarkItem node) {
        for (BookmarkModelObserver observer : mObservers) {
            observer.bookmarkNodeRemoved(parent, oldIndex, node, mIsDoingExtensiveChanges);
        }
    }

    @CalledByNative
    private void bookmarkAllUserNodesRemoved() {
        for (BookmarkModelObserver observer : mObservers) {
            observer.bookmarkAllUserNodesRemoved();
        }
    }

    @CalledByNative
    private void bookmarkNodeChanged(BookmarkItem node) {
        if (mIsDoingExtensiveChanges) return;

        for (BookmarkModelObserver observer : mObservers) {
            observer.bookmarkNodeChanged(node);
        }
    }

    @CalledByNative
    private void bookmarkNodeChildrenReordered(BookmarkItem node) {
        if (mIsDoingExtensiveChanges) return;

        for (BookmarkModelObserver observer : mObservers) {
            observer.bookmarkNodeChildrenReordered(node);
        }
    }

    @CalledByNative
    private void extensiveBookmarkChangesBeginning() {
        mIsDoingExtensiveChanges = true;
    }

    @CalledByNative
    private void extensiveBookmarkChangesEnded() {
        mIsDoingExtensiveChanges = false;
        bookmarkModelChanged();
    }

    @CalledByNative
    private void bookmarkModelChanged() {
        if (mIsDoingExtensiveChanges) return;

        for (BookmarkModelObserver observer : mObservers) {
            observer.bookmarkModelChanged();
        }
    }

    @CalledByNative
    private void editBookmarksEnabledChanged() {
        for (BookmarkModelObserver observer : mObservers) {
            observer.editBookmarksEnabledChanged();
        }
    }

    @CalledByNative
    private static BookmarkItem createBookmarkItem(
            long id,
            int type,
            @JniType("std::u16string") String title,
            @JniType("GURL") GURL url,
            boolean isFolder,
            long parentId,
            int parentIdType,
            boolean isEditable,
            boolean isManaged,
            long dateAdded,
            boolean read,
            long dateLastOpened,
            boolean isAccountBookmark) {
        return new BookmarkItem(
                new BookmarkId(id, type),
                title,
                url,
                isFolder,
                new BookmarkId(parentId, parentIdType),
                isEditable,
                isManaged,
                dateAdded,
                read,
                dateLastOpened,
                isAccountBookmark);
    }

    @CalledByNative
    private static void addToList(List<BookmarkItem> bookmarksList, BookmarkItem bookmark) {
        bookmarksList.add(bookmark);
    }

    @CalledByNative
    private static void addToBookmarkIdList(
            List<BookmarkId> bookmarkIdList, long id, @BookmarkType int type) {
        bookmarkIdList.add(new BookmarkId(id, type));
    }

    @CalledByNative
    private static void addToBookmarkIdListWithDepth(
            List<BookmarkId> folderList,
            long id,
            @BookmarkType int type,
            List<Integer> depthList,
            int depth) {
        folderList.add(new BookmarkId(id, type));
        depthList.add(depth);
    }

    @CalledByNative
    private static void clearLastUsedParent() {
        BookmarkUtils.clearLastUsedPrefs();
    }

    private static List<Pair<Integer, Integer>> createPairsList(int[] left, int[] right) {
        List<Pair<Integer, Integer>> pairList = new ArrayList<>();
        for (int i = 0; i < left.length; i++) {
            pairList.add(new Pair<>(left[i], right[i]));
        }
        return pairList;
    }

    @VisibleForTesting(otherwise = VisibleForTesting.PACKAGE_PRIVATE)
    @NativeMethods
    public interface Natives {
        BookmarkModel nativeGetForProfile(@JniType("Profile*") Profile profile);

        boolean areAccountBookmarkFoldersActive(long nativeBookmarkBridge);

        BookmarkId getMostRecentlyAddedUserBookmarkIdForUrl(
                long nativeBookmarkBridge, @JniType("GURL") GURL url);

        BookmarkItem getBookmarkById(long nativeBookmarkBridge, long id, int type);

        void getTopLevelFolderIds(
                long nativeBookmarkBridge,
                boolean ignoreVisibility,
                List<BookmarkId> bookmarksList);

        BookmarkId getLocalOrSyncableReadingListFolder(long nativeBookmarkBridge);

        BookmarkId getAccountReadingListFolder(long nativeBookmarkBridge);

        BookmarkId getDefaultReadingListFolder(long nativeBookmarkBridge);

        BookmarkId getDefaultBookmarkFolder(long nativeBookmarkBridge);

        // TODO(crbug.com/41487884): Remove this method.
        void getAllFoldersWithDepths(
                long nativeBookmarkBridge, List<BookmarkId> folderList, List<Integer> depthList);

        BookmarkId getRootFolderId(long nativeBookmarkBridge);

        BookmarkId getMobileFolderId(long nativeBookmarkBridge);

        BookmarkId getOtherFolderId(long nativeBookmarkBridge);

        BookmarkId getDesktopFolderId(long nativeBookmarkBridge);

        BookmarkId getAccountMobileFolderId(long nativeBookmarkBridge);

        BookmarkId getAccountOtherFolderId(long nativeBookmarkBridge);

        BookmarkId getAccountDesktopFolderId(long nativeBookmarkBridge);

        BookmarkId getPartnerFolderId(long nativeBookmarkBridge);

        @JniType("std::string")
        String getBookmarkGuidByIdForTesting( // IN-TEST
                long nativeBookmarkBridge, long id, int type);

        int getChildCount(long nativeBookmarkBridge, long id, int type);

        void getChildIds(
                long nativeBookmarkBridge, long id, int type, List<BookmarkId> bookmarksList);

        BookmarkId getChildAt(long nativeBookmarkBridge, long id, int type, int index);

        int getTotalBookmarkCount(long nativeBookmarkBridge, long id, int type);

        void setBookmarkTitle(
                long nativeBookmarkBridge,
                long id,
                int type,
                @JniType("std::u16string") String title);

        void setBookmarkUrl(
                long nativeBookmarkBridge, long id, int type, @JniType("GURL") GURL url);

        byte[] getPowerBookmarkMeta(long nativeBookmarkBridge, long id, int type);

        void setPowerBookmarkMeta(long nativeBookmarkBridge, long id, int type, byte[] meta);

        void deletePowerBookmarkMeta(long nativeBookmarkBridge, long id, int type);

        boolean doesBookmarkExist(long nativeBookmarkBridge, long id, int type);

        // TODO(crbug.com/41487884): Remove this method.
        void getBookmarksForFolder(
                long nativeBookmarkBridge, BookmarkId folderId, List<BookmarkItem> bookmarksList);

        boolean isFolderVisible(long nativeBookmarkBridge, long id, int type);

        BookmarkId addFolder(
                long nativeBookmarkBridge,
                BookmarkId parent,
                int index,
                @JniType("std::u16string") String title);

        void deleteBookmark(long nativeBookmarkBridge, BookmarkId bookmarkId);

        void removeAllUserBookmarks(long nativeBookmarkBridge);

        void moveBookmark(
                long nativeBookmarkBridge,
                BookmarkId bookmarkId,
                BookmarkId newParentId,
                int index);

        BookmarkId addBookmark(
                long nativeBookmarkBridge,
                BookmarkId parentId,
                int index,
                @JniType("std::u16string") String title,
                @JniType("GURL") GURL url);

        BookmarkId addToReadingList(
                long nativeBookmarkBridge,
                BookmarkId parentId,
                @JniType("std::string") String title,
                @JniType("GURL") GURL url);

        void setReadStatus(long nativeBookmarkBridge, BookmarkId id, boolean read);

        int getUnreadCount(long nativeBookmarkBridge, BookmarkId id);

        boolean isAccountBookmark(long nativeBookmarkBridge, BookmarkId id);

        void undo(long nativeBookmarkBridge);

        void startGroupingUndos(long nativeBookmarkBridge);

        void endGroupingUndos(long nativeBookmarkBridge);

        void loadEmptyPartnerBookmarkShimForTesting(long nativeBookmarkBridge); // IN-TEST

        void loadFakePartnerBookmarkShimForTesting(long nativeBookmarkBridge); // IN-TEST

        void searchBookmarks(
                long nativeBookmarkBridge,
                List<BookmarkId> bookmarkMatches,
                @JniType("std::u16string") String query,
                String[] tags,
                int powerBookmarkType,
                int maxNumber);

        void getBookmarksOfType(
                long nativeBookmarkBridge, List<BookmarkId> bookmarkMatches, int powerBookmarkType);

        boolean isDoingExtensiveChanges(long nativeBookmarkBridge);

        void destroy(long nativeBookmarkBridge);

        boolean isEditBookmarksEnabled(long nativeBookmarkBridge);

        void reorderChildren(long nativeBookmarkBridge, BookmarkId parent, long[] orderedNodes);

        boolean isBookmarked(long nativeBookmarkBridge, @JniType("GURL") GURL url);
    }
}