blob: 5a233d192b6cb515f3047ee19d06f814fd1bd1d0 [file] [log] [blame]
// Copyright 2023 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 static org.chromium.components.browser_ui.widget.listmenu.BasicListMenu.buildMenuListItem;
import android.content.Context;
import android.content.res.Resources;
import android.graphics.Bitmap;
import android.graphics.drawable.BitmapDrawable;
import android.graphics.drawable.Drawable;
import android.text.TextUtils;
import android.util.Pair;
import android.view.accessibility.AccessibilityManager;
import androidx.annotation.Nullable;
import androidx.annotation.VisibleForTesting;
import androidx.recyclerview.widget.RecyclerView;
import org.chromium.base.Callback;
import org.chromium.base.ObserverList;
import org.chromium.base.metrics.RecordUserAction;
import org.chromium.base.supplier.ObservableSupplierImpl;
import org.chromium.chrome.R;
import org.chromium.chrome.browser.app.bookmarks.BookmarkAddEditFolderActivity;
import org.chromium.chrome.browser.app.bookmarks.BookmarkFolderSelectActivity;
import org.chromium.chrome.browser.bookmarks.BookmarkListEntry.ViewType;
import org.chromium.chrome.browser.bookmarks.BookmarkRow.Location;
import org.chromium.chrome.browser.bookmarks.BookmarkUiPrefs.BookmarkRowDisplayPref;
import org.chromium.chrome.browser.bookmarks.BookmarkUiPrefs.BookmarkRowSortOrder;
import org.chromium.chrome.browser.bookmarks.BookmarkUiPrefs.Observer;
import org.chromium.chrome.browser.bookmarks.BookmarkUiState.BookmarkUiMode;
import org.chromium.chrome.browser.feature_engagement.TrackerFactory;
import org.chromium.chrome.browser.flags.ChromeFeatureList;
import org.chromium.chrome.browser.partnerbookmarks.PartnerBookmarksReader;
import org.chromium.chrome.browser.profiles.Profile;
import org.chromium.chrome.browser.ui.favicon.FaviconUtils;
import org.chromium.chrome.browser.ui.native_page.BasicNativePage;
import org.chromium.chrome.browser.ui.signin.SyncPromoController.SyncPromoState;
import org.chromium.components.bookmarks.BookmarkId;
import org.chromium.components.bookmarks.BookmarkItem;
import org.chromium.components.bookmarks.BookmarkType;
import org.chromium.components.browser_ui.widget.RoundedIconGenerator;
import org.chromium.components.browser_ui.widget.dragreorder.DragReorderableRecyclerViewAdapter;
import org.chromium.components.browser_ui.widget.dragreorder.DragReorderableRecyclerViewAdapter.DragListener;
import org.chromium.components.browser_ui.widget.dragreorder.DragReorderableRecyclerViewAdapter.DraggabilityProvider;
import org.chromium.components.browser_ui.widget.dragreorder.DragStateDelegate;
import org.chromium.components.browser_ui.widget.listmenu.BasicListMenu;
import org.chromium.components.browser_ui.widget.listmenu.ListMenu;
import org.chromium.components.browser_ui.widget.listmenu.ListMenuItemProperties;
import org.chromium.components.browser_ui.widget.selectable_list.SelectableListLayout;
import org.chromium.components.browser_ui.widget.selectable_list.SelectionDelegate;
import org.chromium.components.browser_ui.widget.selectable_list.SelectionDelegate.SelectionObserver;
import org.chromium.components.favicon.LargeIconBridge;
import org.chromium.components.feature_engagement.EventConstants;
import org.chromium.components.image_fetcher.ImageFetcher;
import org.chromium.components.power_bookmarks.PowerBookmarkMeta;
import org.chromium.ui.modelutil.MVCListAdapter.ListItem;
import org.chromium.ui.modelutil.MVCListAdapter.ModelList;
import org.chromium.ui.modelutil.PropertyModel;
import org.chromium.url.GURL;
import java.util.List;
import java.util.Objects;
import java.util.Stack;
/** Responsible for BookmarkManager business logic. */
// TODO(crbug.com/1416611): Remove BookmarkDelegate if possible.
class BookmarkManagerMediator
implements BookmarkDelegate, TestingDelegate, PartnerBookmarksReader.FaviconUpdateObserver {
private static final String EMPTY_QUERY = null;
private static final long MICRO_CURRENCY_QUOTIENT = 1000000;
private static boolean sPreventLoadingForTesting;
/**
* Keeps track of whether drag is enabled / active for bookmark lists.
*/
private class BookmarkDragStateDelegate implements DragStateDelegate {
private BookmarkDelegate mBookmarkDelegate;
private SelectionDelegate<BookmarkId> mSelectionDelegate;
private AccessibilityManager mA11yManager;
private AccessibilityManager.AccessibilityStateChangeListener mA11yChangeListener;
private boolean mA11yEnabled;
void onBookmarkDelegateInitialized(BookmarkDelegate delegate) {
mBookmarkDelegate = delegate;
mSelectionDelegate = delegate.getSelectionDelegate();
mA11yManager =
(AccessibilityManager) getSelectableListLayout().getContext().getSystemService(
Context.ACCESSIBILITY_SERVICE);
mA11yEnabled = mA11yManager.isEnabled();
mA11yChangeListener = enabled -> mA11yEnabled = enabled;
mA11yManager.addAccessibilityStateChangeListener(mA11yChangeListener);
}
void destroy() {
if (mA11yManager != null) {
mA11yManager.removeAccessibilityStateChangeListener(mA11yChangeListener);
}
}
// DragStateDelegate implementation
@Override
public boolean getDragEnabled() {
return !mA11yEnabled && mBookmarkDelegate.getCurrentUiMode() == BookmarkUiMode.FOLDER;
}
@Override
public boolean getDragActive() {
return getDragEnabled() && mSelectionDelegate.isSelectionEnabled();
}
@VisibleForTesting
@Override
public void setA11yStateForTesting(boolean a11yEnabled) {
if (mA11yManager != null) {
mA11yManager.removeAccessibilityStateChangeListener(mA11yChangeListener);
}
mA11yChangeListener = null;
mA11yManager = null;
mA11yEnabled = a11yEnabled;
}
}
private final BookmarkModelObserver mBookmarkModelObserver = new BookmarkModelObserver() {
@Override
public void bookmarkNodeChildrenReordered(BookmarkItem node) {
refresh();
}
@Override
public void bookmarkNodeRemoved(BookmarkItem parent, int oldIndex, BookmarkItem node,
boolean isDoingExtensiveChanges) {
clearHighlight();
if (getCurrentUiMode() == BookmarkUiMode.FOLDER) {
// If the folder is removed in folder mode, show the parent folder or falls back to
// all bookmarks mode.
if (Objects.equals(node.getId(), getCurrentFolderId())) {
if (mBookmarkModel.getTopLevelFolderIds(true, true).contains(node.getId())) {
openFolder(mBookmarkModel.getDefaultFolderViewLocation());
} else {
openFolder(parent.getId());
}
} else {
if (node.isFolder()) {
refresh();
} else {
int deletedPosition = getPositionForBookmark(node.getId());
if (deletedPosition >= 0) {
mModelList.removeAt(deletedPosition);
syncAdapterAndSelectionDelegate();
}
}
}
} else if (getCurrentUiMode() == BookmarkUiMode.SEARCHING) {
// We cannot rely on removing the specific list item that corresponds to the
// removed node because the node might be a parent with children also shown
// in the list.
search(mSearchText);
}
}
@Override
public void bookmarkNodeChanged(BookmarkItem node) {
clearHighlight();
if (getCurrentUiMode() == BookmarkUiMode.FOLDER
&& Objects.equals(node.getId(), getCurrentFolderId())) {
refresh();
} else {
super.bookmarkNodeChanged(node);
}
}
@Override
public void bookmarkModelChanged() {
clearHighlight();
if (getCurrentUiMode() == BookmarkUiMode.SEARCHING) {
if (!TextUtils.equals(mSearchText, EMPTY_QUERY)) {
search(mSearchText);
} else {
onEndSearch();
}
} else {
refresh();
}
}
};
private final Stack<BookmarkUiState> mStateStack = new Stack<>() {
@Override
public BookmarkUiState push(BookmarkUiState item) {
// The back press state depends on the size of stack. So push/pop item first in order
// to keep the size update-to-date.
var state = super.push(item);
onBackPressStateChanged();
return state;
}
@Override
public synchronized BookmarkUiState pop() {
var state = super.pop();
onBackPressStateChanged();
return state;
}
};
private final BookmarkUiObserver mBookmarkUiObserver = new BookmarkUiObserver() {
@Override
public void onDestroy() {
removeUiObserver(mBookmarkUiObserver);
getSelectionDelegate().removeObserver(mSelectionObserver);
mPromoHeaderManager.destroy();
}
@Override
public void onFolderStateSet(BookmarkId folder) {
clearHighlight();
mSearchText = EMPTY_QUERY;
mDragReorderableRecyclerViewAdapter.enableDrag();
setBookmarks(mBookmarkQueryHandler.buildBookmarkListForParent(getCurrentFolderId()));
updateEmptyViewText();
}
private void updateEmptyViewText() {
assert getCurrentFolderId() != null;
if (BookmarkId.SHOPPING_FOLDER.equals(getCurrentFolderId())) {
getSelectableListLayout().setEmptyViewText(
R.string.tracked_products_empty_list_title);
} else if (getCurrentFolderId().getType() == BookmarkType.READING_LIST) {
TrackerFactory.getTrackerForProfile(mProfile).notifyEvent(
EventConstants.READ_LATER_BOOKMARK_FOLDER_OPENED);
if (ChromeFeatureList.isEnabled(ChromeFeatureList.EMPTY_STATES)) {
getSelectableListLayout().setEmptyStateImageRes(
R.drawable.reading_list_empty_state_illustration);
getSelectableListLayout().setEmptyStateViewText(
R.string.reading_list_manager_empty_state,
R.string.reading_list_manager_save_page_to_read_later);
} else {
getSelectableListLayout().setEmptyViewText(
R.string.reading_list_empty_list_title);
}
} else {
if (ChromeFeatureList.isEnabled(ChromeFeatureList.EMPTY_STATES)) {
getSelectableListLayout().setEmptyStateImageRes(
R.drawable.bookmark_empty_state_illustration);
getSelectableListLayout().setEmptyStateViewText(
R.string.bookmark_manager_empty_state,
R.string.bookmark_manager_back_to_page_by_adding_bookmark);
} else {
getSelectableListLayout().setEmptyViewText(R.string.bookmarks_folder_empty);
}
}
}
@Override
public void onSearchStateSet() {
clearHighlight();
mDragReorderableRecyclerViewAdapter.disableDrag();
// Headers should not appear in Search mode.
// Don't need to notify because we need to redraw everything in the next step.
updateHeader();
removeSectionHeaders();
}
};
private final SelectionObserver<BookmarkId> mSelectionObserver = new SelectionObserver<>() {
@Override
public void onSelectionStateChange(List<BookmarkId> selectedBookmarks) {
clearHighlight();
}
};
private final DragListener mDragListener = new DragListener() {
@Override
public void onSwap() {
setOrder();
}
};
private final DraggabilityProvider mDraggabilityProvider = new DraggabilityProvider() {
@Override
public boolean isActivelyDraggable(PropertyModel propertyModel) {
BookmarkId bookmarkId = propertyModel.get(BookmarkManagerProperties.BOOKMARK_ID);
return mSelectionDelegate.isItemSelected(bookmarkId)
&& isPassivelyDraggable(propertyModel);
}
@Override
public boolean isPassivelyDraggable(PropertyModel propertyModel) {
BookmarkListEntry bookmarkListEntry =
propertyModel.get(BookmarkManagerProperties.BOOKMARK_LIST_ENTRY);
BookmarkItem bookmarkItem = bookmarkListEntry.getBookmarkItem();
return bookmarkItem.isReorderable();
}
};
private final BookmarkUiPrefs.Observer mBookmarkUiPrefsObserver = new Observer() {
@Override
public void onBookmarkRowDisplayPrefChanged(@BookmarkRowDisplayPref int displayPref) {
mModelList.clear();
if (getCurrentUiMode() == BookmarkUiMode.SEARCHING) {
search(mSearchText);
} else {
setBookmarks(
mBookmarkQueryHandler.buildBookmarkListForParent(getCurrentFolderId()));
}
}
@Override
public void onBookmarkRowSortOrderChanged(@BookmarkRowSortOrder int sortOrder) {
refresh();
}
};
private final ObserverList<BookmarkUiObserver> mUiObservers = new ObserverList<>();
private final BookmarkDragStateDelegate mDragStateDelegate = new BookmarkDragStateDelegate();
private final Context mContext;
private final BookmarkModel mBookmarkModel;
private final BookmarkOpener mBookmarkOpener;
// TODO(crbug.com/1416611): Remove reference to SelectableListLayout.
// Owned by BookmarkManager(Coordinator).
private final SelectableListLayout<BookmarkId> mSelectableListLayout;
private final SelectionDelegate<BookmarkId> mSelectionDelegate;
// TODO(crbug.com/1416611): Remove reference to RecyclerView.
// Owned by BookmarkManager(Coordinator).
private final RecyclerView mRecyclerView;
private final DragReorderableRecyclerViewAdapter mDragReorderableRecyclerViewAdapter;
private final LargeIconBridge mLargeIconBridge;
// Whether we're showing in a dialog UI which is only true for phones.
private final boolean mIsDialogUi;
private final boolean mIsIncognito;
private final ObservableSupplierImpl<Boolean> mBackPressStateSupplier;
private final Profile mProfile;
private final BookmarkPromoHeader mPromoHeaderManager;
private final BookmarkUndoController mBookmarkUndoController;
private final BookmarkQueryHandler mBookmarkQueryHandler;
private final ModelList mModelList;
private final BookmarkUiPrefs mBookmarkUiPrefs;
private final RoundedIconGenerator mIconGenerator;
private final int mFetchFaviconSize;
private final int mDisplayFaviconSize;
private final Runnable mHideKeyboardRunnable;
private final ImageFetcher mImageFetcher;
// Whether this instance has been destroyed.
private boolean mIsDestroyed;
private String mInitialUrl;
private boolean mFaviconsNeedRefresh;
private BasicNativePage mNativePage;
// There can only be one promo header at a time. This takes on one of the values:
// ViewType.PERSONALIZED_SIGNIN_PROMO, ViewType.SYNC_PROMO, or ViewType.INVALID.
private @ViewType int mPromoHeaderType = ViewType.INVALID;
private String mSearchText;
// Keep track of the currently highlighted bookmark - used for "show in folder" action.
private BookmarkId mHighlightedBookmark;
BookmarkManagerMediator(Context context, BookmarkModel bookmarkModel,
BookmarkOpener bookmarkOpener, SelectableListLayout<BookmarkId> selectableListLayout,
SelectionDelegate<BookmarkId> selectionDelegate, RecyclerView recyclerView,
DragReorderableRecyclerViewAdapter dragReorderableRecyclerViewAdapter,
LargeIconBridge largeIconBridge, boolean isDialogUi, boolean isIncognito,
ObservableSupplierImpl<Boolean> backPressStateSupplier, Profile profile,
BookmarkUndoController bookmarkUndoController, ModelList modelList,
BookmarkUiPrefs bookmarkUiPrefs, Runnable hideKeyboardRunnable,
ImageFetcher imageFetcher) {
mContext = context;
mBookmarkModel = bookmarkModel;
mBookmarkModel.addObserver(mBookmarkModelObserver);
mBookmarkOpener = bookmarkOpener;
mSelectableListLayout = selectableListLayout;
mSelectableListLayout.getHandleBackPressChangedSupplier().addObserver(
(x) -> onBackPressStateChanged());
mSelectionDelegate = selectionDelegate;
mRecyclerView = recyclerView;
mDragReorderableRecyclerViewAdapter = dragReorderableRecyclerViewAdapter;
mDragReorderableRecyclerViewAdapter.addDragListener(mDragListener);
mDragReorderableRecyclerViewAdapter.setLongPressDragDelegate(
() -> mDragStateDelegate.getDragActive());
mLargeIconBridge = largeIconBridge;
mIsDialogUi = isDialogUi;
mIsIncognito = isIncognito;
mBackPressStateSupplier = backPressStateSupplier;
mProfile = profile;
mPromoHeaderManager = new BookmarkPromoHeader(mContext, mProfile, this::updateHeader);
mBookmarkUndoController = bookmarkUndoController;
if (BookmarkFeatures.isAndroidImprovedBookmarksEnabled()) {
mBookmarkQueryHandler =
new ImprovedBookmarkQueryHandler(mBookmarkModel, bookmarkUiPrefs);
} else {
mBookmarkQueryHandler = new LegacyBookmarkQueryHandler(mBookmarkModel, bookmarkUiPrefs);
}
mModelList = modelList;
mBookmarkUiPrefs = bookmarkUiPrefs;
mBookmarkUiPrefs.addObserver(mBookmarkUiPrefsObserver);
mHideKeyboardRunnable = hideKeyboardRunnable;
mImageFetcher = imageFetcher;
final @BookmarkRowDisplayPref int displayPref =
mBookmarkUiPrefs.getBookmarkRowDisplayPref();
mIconGenerator = BookmarkUtils.getRoundedIconGenerator(
mContext, mContext.getResources(), displayPref);
mFetchFaviconSize = BookmarkUtils.getFaviconFetchSize(mContext.getResources());
mDisplayFaviconSize =
BookmarkUtils.getFaviconDisplaySize(mContext.getResources(), displayPref);
// Previously we were waiting for BookmarkModel to be loaded, but it's not necessary.
PartnerBookmarksReader.addFaviconUpdateObserver(this);
initializeToLoadingState();
if (!sPreventLoadingForTesting) {
mBookmarkModel.finishLoadingBookmarkModel(this::onBookmarkModelLoaded);
}
}
void onBookmarkModelLoaded() {
mDragStateDelegate.onBookmarkDelegateInitialized(this);
// TODO(https://crbug.com/1413463): This logic is here to keep the same execution order
// from when it was in the original adapter. It doesn't conceptually make sense to be here,
// and should happen earlier.
addUiObserver(mBookmarkUiObserver);
mSelectionDelegate.addObserver(mSelectionObserver);
if (!TextUtils.isEmpty(mInitialUrl)) {
setState(BookmarkUiState.createStateFromUrl(mInitialUrl, mBookmarkModel));
}
}
void onDestroy() {
mIsDestroyed = true;
mBookmarkModel.removeObserver(mBookmarkModelObserver);
mLargeIconBridge.destroy();
PartnerBookmarksReader.removeFaviconUpdateObserver(this);
mBookmarkUndoController.destroy();
mDragStateDelegate.destroy();
mBookmarkQueryHandler.destroy();
mBookmarkUiPrefs.removeObserver(mBookmarkUiPrefsObserver);
for (BookmarkUiObserver observer : mUiObservers) {
observer.onDestroy();
}
assert mUiObservers.size() == 0;
}
void onAttachedToWindow() {
mBookmarkUndoController.setEnabled(true);
}
void onDetachedFromWindow() {
mBookmarkUndoController.setEnabled(false);
}
/**
* See BookmarkManager(Coordinator)#onBackPressed.
*/
boolean onBackPressed() {
if (mIsDestroyed) return false;
// TODO(twellington): Replicate this behavior for other list UIs during unification.
if (mSelectableListLayout.onBackPressed()) {
return true;
}
if (!mStateStack.empty()) {
mStateStack.pop();
if (!mStateStack.empty()) {
setState(mStateStack.pop());
return true;
}
}
return false;
}
/**
* See BookmarkManager(Coordinator)#setBasicNativePage.
*/
void setBasicNativePage(BasicNativePage nativePage) {
mNativePage = nativePage;
}
/**
* See BookmarkManager(Coordinator)#updateForUrl
*/
void updateForUrl(String url) {
// Bookmark model is null if the manager has been destroyed.
if (mBookmarkModel == null) return;
if (mBookmarkModel.isBookmarkModelLoaded()) {
BookmarkUiState searchState = null;
if (!mStateStack.isEmpty() && mStateStack.peek().mUiMode == BookmarkUiMode.SEARCHING) {
searchState = mStateStack.pop();
}
setState(BookmarkUiState.createStateFromUrl(url, mBookmarkModel));
if (searchState != null) setState(searchState);
} else {
mInitialUrl = url;
}
}
BookmarkPromoHeader getPromoHeaderManager() {
return mPromoHeaderManager;
}
BookmarkId getIdByPosition(int position) {
BookmarkListEntry entry = getItemByPosition(position);
if (entry == null || entry.getBookmarkItem() == null) return null;
return entry.getBookmarkItem().getId();
}
/**
* Synchronously searches for the given query.
* @param query The query text to search for.
*/
void search(@Nullable String query) {
mSearchText = query == null ? "" : query.trim();
setBookmarks(mBookmarkQueryHandler.buildBookmarkListForSearch(mSearchText));
}
public void setOrder() {
assert !topLevelFoldersShowing() : "Cannot reorder top-level folders!";
assert getCurrentFolderId().getType()
!= BookmarkType.PARTNER : "Cannot reorder partner bookmarks!";
assert getCurrentUiMode()
== BookmarkUiMode.FOLDER : "Can only reorder items from folder mode!";
int startIndex = getBookmarkItemStartIndex();
int endIndex = getBookmarkItemEndIndex();
// Get the new order for the IDs.
long[] newOrder = new long[endIndex - startIndex + 1];
for (int i = startIndex; i <= endIndex; i++) {
BookmarkItem bookmarkItem = getItemByPosition(i).getBookmarkItem();
assert bookmarkItem != null;
newOrder[i - startIndex] = bookmarkItem.getId().getId();
}
mBookmarkModel.reorderBookmarks(getCurrentFolderId(), newOrder);
if (mDragStateDelegate.getDragActive()) {
RecordUserAction.record("MobileBookmarkManagerDragReorder");
}
updateAllLocations();
}
public boolean isReorderable(BookmarkListEntry entry) {
return entry != null && entry.getBookmarkItem() != null
&& entry.getBookmarkItem().isReorderable();
}
DraggabilityProvider getDraggabilityProvider() {
return mDraggabilityProvider;
}
// TestingDelegate implementation.
@Override
public BookmarkId getIdByPositionForTesting(int position) {
return getIdByPosition(position);
}
@Override
public void searchForTesting(@Nullable String query) {
search(query);
}
@Override
public void simulateSignInForTesting() {
mBookmarkUiObserver.onFolderStateSet(getCurrentFolderId());
}
// BookmarkDelegate implementation.
@Override
public void moveDownOne(BookmarkId bookmarkId) {
int pos = getPositionForBookmark(bookmarkId);
assert isReorderable(getItemByPosition(pos));
mModelList.move(pos, pos + 1);
setOrder();
}
@Override
public void moveUpOne(BookmarkId bookmarkId) {
int pos = getPositionForBookmark(bookmarkId);
assert isReorderable(getItemByPosition(pos));
mModelList.move(pos, pos - 1);
setOrder();
}
@Override
public void onBookmarkItemMenuOpened() {
mHideKeyboardRunnable.run();
}
@Override
public boolean isDialogUi() {
return mIsDialogUi;
}
@Override
public void openFolder(BookmarkId folder) {
RecordUserAction.record("MobileBookmarkManagerOpenFolder");
setState(BookmarkUiState.createFolderState(folder, mBookmarkModel));
mRecyclerView.scrollToPosition(0);
}
@Override
public SelectionDelegate<BookmarkId> getSelectionDelegate() {
return mSelectionDelegate;
}
@Override
public SelectableListLayout<BookmarkId> getSelectableListLayout() {
return mSelectableListLayout;
}
@Override
public void notifyStateChange(BookmarkUiObserver observer) {
int state = getCurrentUiMode();
observer.onUiModeChanged(state);
switch (state) {
case BookmarkUiMode.FOLDER:
observer.onFolderStateSet(getCurrentFolderId());
break;
case BookmarkUiMode.LOADING:
break;
case BookmarkUiMode.SEARCHING:
observer.onSearchStateSet();
break;
default:
assert false : "State not valid";
break;
}
}
@Override
public void openBookmark(BookmarkId bookmark) {
if (!mBookmarkOpener.openBookmarkInCurrentTab(bookmark, mIsIncognito)) return;
// Close bookmark UI. Keep the reading list page open.
if (bookmark != null && bookmark.getType() != BookmarkType.READING_LIST) {
BookmarkUtils.finishActivityOnPhone(mContext);
}
}
@Override
public void openBookmarksInNewTabs(List<BookmarkId> bookmarks, boolean incognito) {
if (mBookmarkOpener.openBookmarksInNewTabs(bookmarks, incognito)) {
BookmarkUtils.finishActivityOnPhone(mContext);
}
}
@Override
public void openSearchUi() {
setState(BookmarkUiState.createSearchState());
mSelectableListLayout.onStartSearch(R.string.bookmark_no_result);
}
@Override
public void addUiObserver(BookmarkUiObserver observer) {
mUiObservers.addObserver(observer);
notifyStateChange(observer);
}
@Override
public void removeUiObserver(BookmarkUiObserver observer) {
mUiObservers.removeObserver(observer);
}
@Override
public BookmarkModel getModel() {
return mBookmarkModel;
}
@Override
public @BookmarkUiMode int getCurrentUiMode() {
if (mStateStack.isEmpty()) return BookmarkUiMode.LOADING;
return mStateStack.peek().mUiMode;
}
@Override
public LargeIconBridge getLargeIconBridge() {
return mLargeIconBridge;
}
@Override
public DragStateDelegate getDragStateDelegate() {
return mDragStateDelegate;
}
@Override
public void highlightBookmark(BookmarkId bookmarkId) {
assert mHighlightedBookmark == null : "There should not already be a highlighted bookmark!";
int index = getPositionForBookmark(bookmarkId);
mRecyclerView.scrollToPosition(index);
mHighlightedBookmark = bookmarkId;
mModelList.get(index).model.set(BookmarkManagerProperties.IS_HIGHLIGHTED, true);
}
// SearchDelegate implementation.
// Actual interface implemented in BookmarkManager(Coordinator).
void onEndSearch() {
mSelectableListLayout.onEndSearch();
// Pop the search state off the stack.
mStateStack.pop();
// Set the state back to the folder that was previously being viewed. Listeners will be
// notified of the change and the list of bookmarks will be updated.
setState(mStateStack.pop());
}
// PartnerBookmarksReader.FaviconUpdateObserver implementation.
@Override
public void onUpdateFavicon(String url) {
assert mBookmarkModel.isBookmarkModelLoaded();
mLargeIconBridge.clearFavicon(new GURL(url));
mFaviconsNeedRefresh = true;
}
@Override
public void onCompletedFaviconLoading() {
assert mBookmarkModel.isBookmarkModelLoaded();
if (mFaviconsNeedRefresh) {
refresh();
mFaviconsNeedRefresh = false;
}
}
// Private methods.
private @Nullable BookmarkId getCurrentFolderId() {
return mStateStack.isEmpty() ? null : mStateStack.peek().mFolder;
}
/**
* Puts all UI elements to loading state. This state might be overridden synchronously by
* {@link #updateForUrl(String)}, if the bookmark model is already loaded.
*/
private void initializeToLoadingState() {
assert mStateStack.isEmpty();
setState(BookmarkUiState.createLoadingState());
}
/**
* This is the ultimate internal method that updates UI and controls backstack. And it is the
* only method that pushes states to {@link #mStateStack}.
*
* <p>If the given state is not valid, all_bookmark state will be shown. Afterwards, this method
* checks the current state: if currently in loading state, it pops it out and adds the new
* state to the back stack. It also notifies the {@link #mNativePage} (if any) that the
* url has changed.
*
* <p>Also note that even if we store states to {@link #mStateStack}, on tablet the back
* navigation and back button are not controlled by the manager: the tab handles back key and
* backstack navigation.
*/
private void setState(BookmarkUiState state) {
if (!state.isValid(mBookmarkModel)) {
state = BookmarkUiState.createFolderState(
mBookmarkModel.getDefaultFolderViewLocation(), mBookmarkModel);
}
if (!mStateStack.isEmpty() && mStateStack.peek().equals(state)) return;
// The loading state is not persisted in history stack and once we have a valid state it
// shall be removed.
if (!mStateStack.isEmpty() && mStateStack.peek().mUiMode == BookmarkUiMode.LOADING) {
mStateStack.pop();
}
mStateStack.push(state);
notifyUi(state);
}
private void notifyUi(BookmarkUiState state) {
if (state.mUiMode == BookmarkUiMode.FOLDER) {
// Loading and searching states may be pushed to the stack but should never be stored in
// preferences.
BookmarkUtils.setLastUsedUrl(mContext, state.mUrl);
// If a loading state is replaced by another loading state, do not notify this change.
if (mNativePage != null) {
mNativePage.onStateChange(state.mUrl, false);
}
}
for (BookmarkUiObserver observer : mUiObservers) {
notifyStateChange(observer);
}
}
// TODO(lazzzis): This method can be moved to adapter after bookmark reordering launches.
/**
* Some bookmarks may be moved to another folder or removed in another devices. However, it may
* still be stored by {@link #mSelectionDelegate}, which causes incorrect selection counting.
*/
private void syncAdapterAndSelectionDelegate() {
for (BookmarkId node : mSelectionDelegate.getSelectedItemsAsList()) {
if (mSelectionDelegate.isItemSelected(node) && getPositionForBookmark(node) == -1) {
mSelectionDelegate.toggleSelectionForItem(node);
}
}
}
private void onBackPressStateChanged() {
if (mIsDestroyed) {
mBackPressStateSupplier.set(false);
return;
}
mBackPressStateSupplier.set(
Boolean.TRUE.equals(mSelectableListLayout.getHandleBackPressChangedSupplier().get())
|| mStateStack.size() > 1);
}
/** @return The position of the given bookmark in adapter. Will return -1 if not found. */
private int getPositionForBookmark(BookmarkId bookmark) {
assert bookmark != null;
int position = -1;
for (int i = 0; i < getItemCount(); i++) {
if (bookmark.equals(getIdByPosition(i))) {
position = i;
break;
}
}
return position;
}
private void setBookmarks(List<BookmarkListEntry> bookmarkListEntryList) {
clearHighlight();
// Restore the header, if it exists, then update it.
if (hasPromoHeader()) {
updateOrAdd(0, buildPersonalizedPromoListItem());
}
updateHeader();
// This method is called due to unknown model changes, and we're basically rebuilding every
// row. However we need to avoid doing this in a way that'll cause flicker. So we replace
// items in place so that the recycler view doesn't see everything being removed and added
// back, but instead it sees items being changed.
// TODO(https://crbug.com/1413463): Rework promo/header methods to simplify initial index.
int index = hasPromoHeader() ? 1 : 0;
for (BookmarkListEntry bookmarkListEntry : bookmarkListEntryList) {
updateOrAdd(index++, buildBookmarkListItem(bookmarkListEntry));
}
if (mModelList.size() > index) {
mModelList.removeRange(index, mModelList.size() - index);
}
updateAllLocations();
syncAdapterAndSelectionDelegate();
}
private void updateOrAdd(int index, ListItem listItem) {
if (mModelList.size() > index) {
mModelList.update(index, listItem);
} else {
mModelList.add(index, listItem);
}
}
private static boolean isMovable(PropertyModel propertyModel) {
BookmarkListEntry bookmarkListEntry =
propertyModel.get(BookmarkManagerProperties.BOOKMARK_LIST_ENTRY);
if (bookmarkListEntry == null) return false;
BookmarkItem bookmarkItem = bookmarkListEntry.getBookmarkItem();
if (bookmarkItem == null) return false;
return BookmarkUtils.isMovable(bookmarkItem);
}
private int firstIndexWithLocation(int start, int stop, int delta) {
for (int i = start; i != stop; i += delta) {
ListItem listItem = mModelList.get(i);
final @ViewType int viewType = listItem.type;
if ((viewType == ViewType.BOOKMARK || viewType == ViewType.FOLDER
|| viewType == ViewType.SHOPPING_POWER_BOOKMARK
|| viewType == ViewType.IMPROVED_BOOKMARK_COMPACT
|| viewType == ViewType.IMPROVED_BOOKMARK_VISUAL)
&& isMovable(listItem.model)) {
return i;
}
}
return -1;
}
private void updateAllLocations() {
int startIndex = firstIndexWithLocation(0, mModelList.size(), 1);
int lastIndex = firstIndexWithLocation(mModelList.size() - 1, -1, -1);
if (startIndex < 0 || lastIndex < 0) {
return;
}
if (startIndex == lastIndex) {
mModelList.get(startIndex).model.set(BookmarkManagerProperties.LOCATION, Location.SOLO);
} else {
mModelList.get(startIndex).model.set(BookmarkManagerProperties.LOCATION, Location.TOP);
mModelList.get(lastIndex).model.set(
BookmarkManagerProperties.LOCATION, Location.BOTTOM);
}
for (int i = startIndex + 1; i < lastIndex; i++) {
mModelList.get(i).model.set(BookmarkManagerProperties.LOCATION, Location.MIDDLE);
}
}
/** Refresh the list of bookmarks within the currently visible folder. */
private void refresh() {
notifyUi(mStateStack.peek());
}
/**
* Updates mPromoHeaderType. Makes sure that the 0th index of getElements() is consistent with
* the promo header. This 0th index is null iff there is a promo header.
*/
private void updateHeader() {
boolean wasShowingPromo = hasPromoHeader();
int currentUiState = getCurrentUiMode();
if (currentUiState == BookmarkUiMode.LOADING) {
return;
} else if (currentUiState == BookmarkUiMode.SEARCHING) {
mPromoHeaderType = ViewType.INVALID;
} else {
switch (mPromoHeaderManager.getPromoState()) {
case SyncPromoState.NO_PROMO:
mPromoHeaderType = ViewType.INVALID;
break;
case SyncPromoState.PROMO_FOR_SIGNED_OUT_STATE:
mPromoHeaderType = ViewType.PERSONALIZED_SIGNIN_PROMO;
break;
case SyncPromoState.PROMO_FOR_SIGNED_IN_STATE:
mPromoHeaderType = ViewType.PERSONALIZED_SYNC_PROMO;
break;
case SyncPromoState.PROMO_FOR_SYNC_TURNED_OFF_STATE:
mPromoHeaderType = ViewType.SYNC_PROMO;
break;
default:
assert false : "Unexpected value for promo state!";
}
}
boolean willShowPromo = hasPromoHeader();
if (!wasShowingPromo && willShowPromo) {
mModelList.add(0, buildPersonalizedPromoListItem());
} else if (wasShowingPromo && !willShowPromo) {
mModelList.removeAt(0);
}
}
/** Removes all section headers from the current list. */
private void removeSectionHeaders() {
for (int i = mModelList.size() - 1; i >= 0; i--) {
if (mModelList.get(i).type == ViewType.SECTION_HEADER) {
mModelList.removeAt(i);
}
}
}
private int getBookmarkItemStartIndex() {
return hasPromoHeader() ? 1 : 0;
}
private int getBookmarkItemEndIndex() {
int endIndex = mModelList.size() - 1;
BookmarkItem bookmarkItem = getItemByPosition(endIndex).getBookmarkItem();
if (bookmarkItem == null || !BookmarkUtils.isMovable(bookmarkItem)) {
endIndex--;
}
return endIndex;
}
private boolean hasPromoHeader() {
return mPromoHeaderType != ViewType.INVALID;
}
/**
* Return true iff the currently-open folder is the root folder
* (which is true iff the top-level folders are showing)
*/
private boolean topLevelFoldersShowing() {
return Objects.equals(getCurrentFolderId(), mBookmarkModel.getRootFolderId());
}
/** Clears the highlighted bookmark, if there is one. */
private void clearHighlight() {
mHighlightedBookmark = null;
}
private BookmarkListEntry getItemByPosition(int position) {
ListItem listItem = mModelList.get(position);
PropertyModel propertyModel = listItem.model;
return propertyModel.get(BookmarkManagerProperties.BOOKMARK_LIST_ENTRY);
}
private int getItemCount() {
return mModelList.size();
}
private ListItem buildPersonalizedPromoListItem() {
BookmarkListEntry bookmarkListEntry =
BookmarkListEntry.createSyncPromoHeader(mPromoHeaderType);
PropertyModel propertyModel = new PropertyModel(BookmarkManagerProperties.ALL_KEYS);
propertyModel.set(BookmarkManagerProperties.BOOKMARK_LIST_ENTRY, bookmarkListEntry);
propertyModel.set(BookmarkManagerProperties.BOOKMARK_PROMO_HEADER, mPromoHeaderManager);
return new ListItem(bookmarkListEntry.getViewType(), propertyModel);
}
private ListItem buildBookmarkListItem(BookmarkListEntry bookmarkListEntry) {
if (bookmarkListEntry.getViewType() == ViewType.IMPROVED_BOOKMARK_COMPACT
|| bookmarkListEntry.getViewType() == ViewType.IMPROVED_BOOKMARK_VISUAL) {
return buildImprovedBookmarkRow(bookmarkListEntry);
}
BookmarkItem bookmarkItem = bookmarkListEntry.getBookmarkItem();
BookmarkId bookmarkId = bookmarkItem == null ? null : bookmarkItem.getId();
PropertyModel propertyModel = new PropertyModel(BookmarkManagerProperties.ALL_KEYS);
propertyModel.set(BookmarkManagerProperties.BOOKMARK_LIST_ENTRY, bookmarkListEntry);
propertyModel.set(BookmarkManagerProperties.BOOKMARK_ID, bookmarkId);
propertyModel.set(BookmarkManagerProperties.IS_FROM_FILTER_VIEW,
BookmarkId.SHOPPING_FOLDER.equals(getCurrentFolderId()));
boolean isHighlighted = Objects.equals(bookmarkId, mHighlightedBookmark);
propertyModel.set(BookmarkManagerProperties.IS_HIGHLIGHTED, isHighlighted);
propertyModel.set(BookmarkManagerProperties.CLEAR_HIGHLIGHT, this::clearHighlight);
if (bookmarkListEntry.getViewType() == ViewType.SHOPPING_FILTER) {
propertyModel.set(BookmarkManagerProperties.OPEN_FOLDER, this::openFolder);
}
return new ListItem(bookmarkListEntry.getViewType(), propertyModel);
}
@VisibleForTesting
ListItem buildImprovedBookmarkRow(BookmarkListEntry bookmarkListEntry) {
PropertyModel propertyModel = new PropertyModel(ImprovedBookmarkRowProperties.ALL_KEYS);
BookmarkItem item = bookmarkListEntry.getBookmarkItem();
BookmarkId id = item.getId();
PowerBookmarkMeta meta = bookmarkListEntry.getPowerBookmarkMeta();
propertyModel.set(BookmarkManagerProperties.BOOKMARK_LIST_ENTRY, bookmarkListEntry);
propertyModel.set(BookmarkManagerProperties.BOOKMARK_ID, id);
propertyModel.set(ImprovedBookmarkRowProperties.TITLE, item.getTitle());
propertyModel.set(ImprovedBookmarkRowProperties.DESCRIPTION,
item.isFolder() ? BookmarkUtils.getFolderDescriptionText(
id, mBookmarkModel, mContext.getResources())
: item.getUrlForDisplay());
resolveIconForBookmark(item, propertyModel);
propertyModel.set(
ImprovedBookmarkRowProperties.POPUP_LISTENER, this::onBookmarkItemMenuOpened);
propertyModel.set(ImprovedBookmarkRowProperties.SELECTED, false);
propertyModel.set(ImprovedBookmarkRowProperties.SELECTION_ACTIVE, false);
propertyModel.set(ImprovedBookmarkRowProperties.DRAG_ENABLED, false);
// TODO(crbug.com/1442044): Invesigate caching ModelList for the menu.
propertyModel.set(ImprovedBookmarkRowProperties.LIST_MENU_BUTTON_DELEGATE,
() -> createListMenuForBookmark(propertyModel));
propertyModel.set(ImprovedBookmarkRowProperties.EDITABLE, item.isEditable());
propertyModel.set(
ImprovedBookmarkRowProperties.OPEN_BOOKMARK_CALLBACK, () -> openBookmarkId(id));
if (meta != null && meta.hasShoppingSpecifics()) {
propertyModel.set(ImprovedBookmarkRowProperties.ACCESSORY_VIEW,
new ShoppingAccessoryCoordinator(mContext, meta.getShoppingSpecifics())
.getView());
} else {
propertyModel.set(ImprovedBookmarkRowProperties.ACCESSORY_VIEW, null);
}
return new ListItem(bookmarkListEntry.getViewType(), propertyModel);
}
// ImprovedBookmarkRow methods.
private void resolveIconForBookmark(BookmarkItem item, PropertyModel model) {
boolean useImages = BookmarkFeatures.isAndroidImprovedBookmarksEnabled()
&& mBookmarkUiPrefs.getBookmarkRowDisplayPref() == BookmarkRowDisplayPref.VISUAL;
if (item.isFolder()) {
if (useImages) {
model.set(ImprovedBookmarkRowProperties.FOLDER_CHILD_COUNT,
BookmarkUtils.getChildCountForDisplay(item.getId(), mBookmarkModel));
// TODO(crbug.com/1440863): Support reading list special placeholder case.
model.set(ImprovedBookmarkRowProperties.FOLDER_DRAWABLES, new Pair<>(null, null));
// TODO(crbug.com/1444251): Extract fetching logic to standalone class.
resolveFolderDrawables(model, /*primaryDrawable=*/null, /*secondaryDrawable=*/null,
mBookmarkModel.getChildIds(item.getId()), /*index=*/0);
} else {
model.set(ImprovedBookmarkRowProperties.BOOKMARK_DRAWABLE,
BookmarkUtils.getFolderIcon(mContext, item.getId().getType()));
}
} else {
if (useImages) {
// TODO(crbug.com/1444251): Extract fetching logic to standalone class.
getBookmarkDrawable(item.getId(), (drawable) -> {
if (drawable == null) {
resolveFaviconForBookmark(item, model);
} else {
model.set(ImprovedBookmarkRowProperties.BOOKMARK_DRAWABLE, drawable);
}
});
} else {
resolveFaviconForBookmark(item, model);
}
}
}
private void resolveFolderDrawables(PropertyModel model, Drawable primaryDrawable,
Drawable secondaryDrawable, List<BookmarkId> childIds, int index) {
if (index == childIds.size() || (primaryDrawable != null && secondaryDrawable != null)) {
model.set(ImprovedBookmarkRowProperties.FOLDER_DRAWABLES,
new Pair<>(primaryDrawable, secondaryDrawable));
return;
}
BookmarkItem item = mBookmarkModel.getBookmarkById(childIds.get(index));
getBookmarkDrawable(childIds.get(index), (drawable) -> {
Drawable newPrimaryDrawable = primaryDrawable;
Drawable newSecondaryDrawable = secondaryDrawable;
if (newPrimaryDrawable == null) {
newPrimaryDrawable = drawable;
} else {
newSecondaryDrawable = drawable;
}
resolveFolderDrawables(
model, newPrimaryDrawable, newSecondaryDrawable, childIds, index + 1);
});
}
private void getBookmarkDrawable(BookmarkId id, Callback<BitmapDrawable> callback) {
BookmarkItem item = mBookmarkModel.getBookmarkById(id);
mBookmarkModel.getImageUrlForBookmark(item.getUrl(), (imageUrl) -> {
if (imageUrl == null) {
callback.onResult(null);
return;
}
Resources res = mContext.getResources();
int size = BookmarkUtils.getDisplayIconSize(
res, mBookmarkUiPrefs.getBookmarkRowDisplayPref());
mImageFetcher.fetchImage(ImageFetcher.Params.create(imageUrl,
ImageFetcher.POWER_BOOKMARKS_CLIENT_NAME, size, size),
(image) -> {
if (image == null) {
callback.onResult(null);
} else {
callback.onResult(new BitmapDrawable(res, image));
}
});
});
}
private void resolveFaviconForBookmark(BookmarkItem item, PropertyModel model) {
mLargeIconBridge.getLargeIconForUrl(item.getUrl(), mFetchFaviconSize,
(Bitmap icon, int fallbackColor, boolean isFallbackColorDefault, int iconType) -> {
Drawable iconDrawable = FaviconUtils.getIconDrawableWithoutFilter(icon,
item.getUrl(), fallbackColor, mIconGenerator, mContext.getResources(),
mDisplayFaviconSize);
model.set(ImprovedBookmarkRowProperties.BOOKMARK_DRAWABLE, iconDrawable);
});
}
@VisibleForTesting
ModelList createListMenuModelList(BookmarkId bookmarkId, @Location int location) {
BookmarkItem bookmarkItem = mBookmarkModel.getBookmarkById(bookmarkId);
// Reading list items can sometimes be movable (for type swapping purposes), but for
// UI purposes they shouldn't be movable.
boolean canMove = bookmarkItem != null && BookmarkUtils.isMovable(bookmarkItem);
ModelList listItems = new ModelList();
if (bookmarkId.getType() == BookmarkType.READING_LIST) {
if (bookmarkItem != null) {
listItems.add(buildMenuListItem(bookmarkItem.isRead()
? R.string.reading_list_mark_as_unread
: R.string.reading_list_mark_as_read,
0, 0));
}
}
listItems.add(buildMenuListItem(R.string.bookmark_item_select, 0, 0));
listItems.add(buildMenuListItem(R.string.bookmark_item_edit, 0, 0));
listItems.add(buildMenuListItem(R.string.bookmark_item_move, 0, 0, canMove));
listItems.add(buildMenuListItem(R.string.bookmark_item_delete, 0, 0));
boolean canReorder = bookmarkItem != null && bookmarkItem.isReorderable()
&& !Objects.equals(getCurrentFolderId(), mBookmarkModel.getRootFolderId());
if (getCurrentUiMode() == BookmarkUiMode.SEARCHING) {
listItems.add(buildMenuListItem(R.string.bookmark_show_in_folder, 0, 0));
} else if (getCurrentUiMode() == BookmarkUiMode.FOLDER && location != Location.SOLO
&& canReorder) {
// Only add move up / move down buttons if there is more than 1 item.
if (location != Location.TOP) {
listItems.add(buildMenuListItem(R.string.menu_item_move_up, 0, 0));
}
if (location != Location.BOTTOM) {
listItems.add(buildMenuListItem(R.string.menu_item_move_down, 0, 0));
}
}
return listItems;
}
@VisibleForTesting
ListMenu createListMenuForBookmark(PropertyModel model) {
BookmarkId bookmarkId = model.get(BookmarkManagerProperties.BOOKMARK_ID);
ModelList listItems =
createListMenuModelList(bookmarkId, model.get(BookmarkManagerProperties.LOCATION));
ListMenu.Delegate delegate = item -> {
int textId = item.get(ListMenuItemProperties.TITLE_ID);
if (textId == R.string.bookmark_item_select) {
mSelectionDelegate.toggleSelectionForItem(bookmarkId);
RecordUserAction.record("Android.BookmarkPage.SelectFromMenu");
if (bookmarkId.getType() == BookmarkType.READING_LIST) {
RecordUserAction.record("Android.BookmarkPage.ReadingList.SelectFromMenu");
}
} else if (textId == R.string.bookmark_item_edit) {
BookmarkItem bookmarkItem = mBookmarkModel.getBookmarkById(bookmarkId);
if (bookmarkItem.isFolder()) {
BookmarkAddEditFolderActivity.startEditFolderActivity(
mContext, bookmarkItem.getId());
} else {
BookmarkUtils.startEditActivity(mContext, bookmarkItem.getId());
}
} else if (textId == R.string.reading_list_mark_as_read) {
BookmarkItem bookmarkItem = mBookmarkModel.getBookmarkById(bookmarkId);
mBookmarkModel.setReadStatusForReadingList(bookmarkItem.getUrl(), /*read=*/true);
RecordUserAction.record("Android.BookmarkPage.ReadingList.MarkAsRead");
} else if (textId == R.string.reading_list_mark_as_unread) {
BookmarkItem bookmarkItem = mBookmarkModel.getBookmarkById(bookmarkId);
mBookmarkModel.setReadStatusForReadingList(bookmarkItem.getUrl(), /*read=*/false);
RecordUserAction.record("Android.BookmarkPage.ReadingList.MarkAsUnread");
} else if (textId == R.string.bookmark_item_move) {
BookmarkFolderSelectActivity.startFolderSelectActivity(mContext, bookmarkId);
RecordUserAction.record("MobileBookmarkManagerMoveToFolder");
} else if (textId == R.string.bookmark_item_delete) {
if (mBookmarkModel != null) {
mBookmarkModel.deleteBookmarks(bookmarkId);
RecordUserAction.record("Android.BookmarkPage.RemoveItem");
if (bookmarkId.getType() == BookmarkType.READING_LIST) {
RecordUserAction.record("Android.BookmarkPage.ReadingList.RemoveItem");
}
}
} else if (textId == R.string.bookmark_show_in_folder) {
BookmarkItem bookmarkItem = mBookmarkModel.getBookmarkById(bookmarkId);
openFolder(bookmarkItem.getParentId());
highlightBookmark(bookmarkId);
RecordUserAction.record("MobileBookmarkManagerShowInFolder");
} else if (textId == R.string.menu_item_move_up) {
moveUpOne(bookmarkId);
RecordUserAction.record("MobileBookmarkManagerMoveUp");
} else if (textId == R.string.menu_item_move_down) {
moveDownOne(bookmarkId);
RecordUserAction.record("MobileBookmarkManagerMoveDown");
};
};
return new BasicListMenu(mContext, listItems, delegate);
}
void openBookmarkId(BookmarkId id) {
BookmarkItem item = mBookmarkModel.getBookmarkById(id);
if (item.isFolder()) {
openFolder(id);
} else {
openBookmark(id);
}
}
// Testing methods.
/** Whether to prevent the bookmark model from fully loading for testing. */
static void preventLoadingForTesting(boolean preventLoading) {
sPreventLoadingForTesting = preventLoading;
}
void clearStateStackForTesting() {
mStateStack.clear();
}
BookmarkUndoController getUndoControllerForTesting() {
return mBookmarkUndoController;
}
}