// Copyright 2024 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 org.mockito.Mockito;
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.url.GURL;
import java.util.ArrayList;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
import java.util.Objects;
import java.util.stream.Collectors;
/**
* Fake {@link BookmarkModel} for use in tests. Instead of faking the BookmarkModel implementation
* instead the BookmarkBridge.Natives interface is faked and substituted through TEST_HOOKS. This
* allows the production BookmarkModel/Bridge to be used as-is.
*/
public class FakeBookmarkModel extends BookmarkModel {
public static final String OTHER_FOLDER_TITLE = "Other bookmarks";
public static final String DESKTOP_FOLDER_TITLE = "Bookmarks bar";
public static final String MOBILE_FOLDER_TITLE = "Mobile bookmarks";
public static final String PARTNER_FOLDER_TITLE = "Parter bookmarks";
public static final String READING_LIST_FOLDER_TITLE = "Reading list";
// Factory constructor for the FakeBoomkarkModel
public static FakeBookmarkModel createModel() {
// Temporary Jni mock.
BookmarkBridgeJni.TEST_HOOKS.setInstanceForTesting(
Mockito.mock(BookmarkBridge.Natives.class));
FakeBookmarkModel fakeBookmarkModel = new FakeBookmarkModel();
return fakeBookmarkModel;
}
// Used to assign nodes unique ids.
private int mNextNodeId;
// Stores a mapping from BookmarkId to BookmarkItem, and also serves parent lookup requests.
private final Map<BookmarkId, BookmarkItem> mBookmarkIdToItemMap = new HashMap<>();
// Stores a mapping from BookmarkId to PowerBookmarkMeta (in byte[] form).
private final Map<BookmarkId, byte[]> mBookmarkIdToPowerBookmarkMetaMap = new HashMap<>();
private BookmarkId mRootFolderId;
private BookmarkId mOtherFolderId;
private BookmarkId mDesktopFolderId;
private BookmarkId mMobileFolderId;
private BookmarkId mAccountOtherFolderId;
private BookmarkId mAccountDesktopFolderId;
private BookmarkId mAccountMobileFolderId;
private BookmarkId mPartnerFolderId;
private BookmarkId mLocalOrSyncableReadingListFolderId;
private BookmarkId mAccountReadingListFolderId;
private boolean mAreAccountBookmarkFoldersActive;
private FakeBookmarkModel() {
// The native bookmark bridge pointer will be ignored because the JNI is mocked by
// BookmarkBridgeNatives.
super(/* nativeBookmarkBridge= */ 1);
BookmarkBridgeJni.TEST_HOOKS.setInstanceForTesting(new BookmarkBridgeNatives());
setupTopLevelFolders();
bookmarkModelLoaded();
}
// Public extensions to the BookmarkModel API for testing.
/** Adds a managed folder, parent cannot be the root. */
public BookmarkId addManagedFolder(BookmarkId parent, String title) {
return addFolder(parent, title, /* isManaged= */ true);
}
/** Adds a partner bookmark to the partner bookmark folder. */
public BookmarkId addPartnerBookmarkItem(String title, GURL url) {
BookmarkId id = new BookmarkId(mNextNodeId++, BookmarkType.PARTNER);
return addBookmarkItem(
id,
getPartnerFolderId(),
title,
url,
/* isFolder= */ false,
/* isEditable= */ false,
/* isManaged= */ false,
/* read= */ false,
/* isAccountBookmark= */ false);
}
public void setAreAccountBookmarkFoldersActive(boolean active) {
mAreAccountBookmarkFoldersActive = active;
}
// Private functions used internally.
private void setupTopLevelFolders() {
// Setup the root folder structure.
mRootFolderId =
addPermanentFolder(
BookmarkType.NORMAL,
/* parent= */ null,
/* title= */ "",
/* isAccountBookmark= */ false);
mOtherFolderId =
addPermanentFolder(
BookmarkType.NORMAL,
mRootFolderId,
OTHER_FOLDER_TITLE,
/* isAccountBookmark= */ false);
mDesktopFolderId =
addPermanentFolder(
BookmarkType.NORMAL,
mRootFolderId,
DESKTOP_FOLDER_TITLE,
/* isAccountBookmark= */ false);
mMobileFolderId =
addPermanentFolder(
BookmarkType.NORMAL,
mRootFolderId,
MOBILE_FOLDER_TITLE,
/* isAccountBookmark= */ false);
mAccountOtherFolderId =
addPermanentFolder(
BookmarkType.NORMAL,
mRootFolderId,
OTHER_FOLDER_TITLE,
/* isAccountBookmark= */ true);
mAccountDesktopFolderId =
addPermanentFolder(
BookmarkType.NORMAL,
mRootFolderId,
DESKTOP_FOLDER_TITLE,
/* isAccountBookmark= */ true);
mAccountMobileFolderId =
addPermanentFolder(
BookmarkType.NORMAL,
mRootFolderId,
MOBILE_FOLDER_TITLE,
/* isAccountBookmark= */ true);
mPartnerFolderId =
addPermanentFolder(
BookmarkType.NORMAL,
mMobileFolderId,
PARTNER_FOLDER_TITLE,
/* isAccountBookmark= */ false);
mLocalOrSyncableReadingListFolderId =
addPermanentFolder(
BookmarkType.READING_LIST,
mRootFolderId,
READING_LIST_FOLDER_TITLE,
/* isAccountBookmark= */ false);
mAccountReadingListFolderId =
addPermanentFolder(
BookmarkType.READING_LIST,
mRootFolderId,
READING_LIST_FOLDER_TITLE,
/* isAccountBookmark= */ true);
}
private BookmarkId addBookmark(
@BookmarkType int type, BookmarkId parent, String title, GURL url) {
assert !parent.equals(mRootFolderId);
assert type == parent.getType();
return addBookmarkItem(
type,
parent,
title,
url,
/* isFolder= */ false,
/* isEditable= */ true,
/* isManaged= */ false,
/* read= */ false,
FakeBookmarkModel.this.isAccountBookmark(parent));
}
private BookmarkId addFolder(BookmarkId parent, String title) {
return addFolder(parent, title, /* isManaged= */ false);
}
private BookmarkId addFolder(BookmarkId parent, String title, boolean isManaged) {
assert !parent.equals(mRootFolderId);
assert parent.getType() == BookmarkType.NORMAL;
return addBookmarkItem(
BookmarkType.NORMAL,
parent,
title,
/* url= */ null,
/* isFolder= */ true,
/* isEditable= */ true,
isManaged,
/* read= */ false,
FakeBookmarkModel.this.isAccountBookmark(parent));
}
private BookmarkId addPermanentFolder(
@BookmarkType int type, BookmarkId parent, String title, boolean isAccountBookmark) {
return addBookmarkItem(
type,
parent,
title,
/* url= */ null,
/* isFolder= */ true,
/* isEditable= */ false,
/* isManaged= */ false,
/* read= */ false,
isAccountBookmark);
}
private BookmarkId addBookmarkItem(
@BookmarkType int type,
BookmarkId parent,
String title,
GURL url,
boolean isFolder,
boolean isEditable,
boolean isManaged,
boolean read,
boolean isAccountBookmark) {
BookmarkId id = new BookmarkId(mNextNodeId++, type);
return addBookmarkItem(
id, parent, title, url, isFolder, isEditable, isManaged, read, isAccountBookmark);
}
private BookmarkId addBookmarkItem(
BookmarkId id,
BookmarkId parent,
String title,
GURL url,
boolean isFolder,
boolean isEditable,
boolean isManaged,
boolean read,
boolean isAccountBookmark) {
assert !mBookmarkIdToItemMap.containsKey(id);
mBookmarkIdToItemMap.put(
id,
new BookmarkItem(
id,
title,
url,
isFolder,
parent,
isEditable,
isManaged,
/* dateAdded= */ 0,
read,
/* dateLastOpened= */ 0,
isAccountBookmark));
return id;
}
private void updateBookmarkItem(
BookmarkId id,
BookmarkId parent,
String title,
GURL url,
boolean isFolder,
boolean isEditable,
boolean isManaged,
boolean read,
boolean isAccountBookmark) {
assert mBookmarkIdToItemMap.containsKey(id);
mBookmarkIdToItemMap.put(
id,
new BookmarkItem(
id,
title,
url,
isFolder,
parent,
isEditable,
isManaged,
/* dateAdded= */ 0,
read,
/* dateLastOpened= */ 0,
isAccountBookmark));
}
// BookmarkBridge.Natives implementation.
private class BookmarkBridgeNatives implements BookmarkBridge.Natives {
@Override
public BookmarkModel nativeGetForProfile(Profile profile) {
return FakeBookmarkModel.this;
}
@Override
public boolean areAccountBookmarkFoldersActive(long nativeBookmarkBridge) {
return FakeBookmarkModel.this.mAreAccountBookmarkFoldersActive;
}
@Override
public BookmarkId getMostRecentlyAddedUserBookmarkIdForUrl(
long nativeBookmarkBridge, GURL url) {
return null;
}
@Override
public BookmarkItem getBookmarkById(long nativeBookmarkBridge, long id, int type) {
return mBookmarkIdToItemMap.get(new BookmarkId(id, type));
}
@Override
public void getTopLevelFolderIds(
long nativeBookmarkBridge,
boolean ignoreVisibility,
List<BookmarkId> bookmarksList) {
bookmarksList.addAll(FakeBookmarkModel.this.getChildIds(mRootFolderId));
// Remove all account folders if the feature flag is disabled.
if (!areAccountBookmarkFoldersActive(nativeBookmarkBridge)) {
bookmarksList.remove(mAccountOtherFolderId);
bookmarksList.remove(mAccountDesktopFolderId);
bookmarksList.remove(mAccountMobileFolderId);
bookmarksList.remove(mAccountReadingListFolderId);
}
}
@Override
public BookmarkId getLocalOrSyncableReadingListFolder(long nativeBookmarkBridge) {
return mLocalOrSyncableReadingListFolderId;
}
@Override
public BookmarkId getAccountReadingListFolder(long nativeBookmarkBridge) {
return mAccountReadingListFolderId;
}
@Override
public BookmarkId getDefaultReadingListFolder(long nativeBookmarkBridge) {
return areAccountBookmarkFoldersActive(nativeBookmarkBridge)
? mAccountReadingListFolderId
: mLocalOrSyncableReadingListFolderId;
}
@Override
public BookmarkId getDefaultBookmarkFolder(long nativeBookmarkBridge) {
return areAccountBookmarkFoldersActive(nativeBookmarkBridge)
? mAccountMobileFolderId
: mMobileFolderId;
}
@Override
public void getAllFoldersWithDepths(
long nativeBookmarkBridge, List<BookmarkId> folderList, List<Integer> depthList) {
assert false : "Not implemented!";
}
@Override
public BookmarkId getRootFolderId(long nativeBookmarkBridge) {
return mRootFolderId;
}
@Override
public BookmarkId getMobileFolderId(long nativeBookmarkBridge) {
return mMobileFolderId;
}
@Override
public BookmarkId getOtherFolderId(long nativeBookmarkBridge) {
return mOtherFolderId;
}
@Override
public BookmarkId getDesktopFolderId(long nativeBookmarkBridge) {
return mDesktopFolderId;
}
@Override
public BookmarkId getAccountMobileFolderId(long nativeBookmarkBridge) {
return mAccountMobileFolderId;
}
@Override
public BookmarkId getAccountOtherFolderId(long nativeBookmarkBridge) {
return mAccountOtherFolderId;
}
@Override
public BookmarkId getAccountDesktopFolderId(long nativeBookmarkBridge) {
return mAccountDesktopFolderId;
}
@Override
public BookmarkId getPartnerFolderId(long nativeBookmarkBridge) {
return mPartnerFolderId;
}
@Override
public String getBookmarkGuidByIdForTesting(long nativeBookmarkBridge, long id, int type) {
assert false : "Not implemented!";
return null;
}
@Override
public int getChildCount(long nativeBookmarkBridge, long id, int type) {
List<BookmarkId> childIds = new ArrayList<>();
getChildIds(nativeBookmarkBridge, id, type, childIds);
return childIds.size();
}
@Override
public void getChildIds(
long nativeBookmarkBridge, long id, int type, List<BookmarkId> bookmarksList) {
BookmarkId parentId = new BookmarkId(id, type);
bookmarksList.addAll(
mBookmarkIdToItemMap.values().stream()
.filter(item -> Objects.equals(item.getParentId(), parentId))
.map(item -> item.getId())
.sorted((first, second) -> Long.compare(first.getId(), second.getId()))
.collect(Collectors.toList()));
}
@Override
public BookmarkId getChildAt(long nativeBookmarkBridge, long id, int type, int index) {
List<BookmarkId> childIds = new ArrayList<>();
getChildIds(nativeBookmarkBridge, id, type, childIds);
return childIds.get(index);
}
@Override
public int getTotalBookmarkCount(long nativeBookmarkBridge, long id, int type) {
List<BookmarkId> children =
FakeBookmarkModel.this.getChildIds(new BookmarkId(id, type));
int size = children.size();
while (!children.isEmpty()) {
BookmarkId childId = children.remove(0);
BookmarkItem childItem = FakeBookmarkModel.this.getBookmarkById(childId);
if (!childItem.isFolder()) {
continue;
}
for (BookmarkId subChildId : FakeBookmarkModel.this.getChildIds(childId)) {
size++;
children.add(subChildId);
}
}
return size;
}
@Override
public void setBookmarkTitle(long nativeBookmarkBridge, long id, int type, String title) {
BookmarkId bookmarkId = new BookmarkId(id, type);
BookmarkItem item = FakeBookmarkModel.this.getBookmarkById(bookmarkId);
FakeBookmarkModel.this.updateBookmarkItem(
bookmarkId,
item.getParentId(),
title,
item.getUrl(),
item.isFolder(),
item.isEditable(),
item.isManaged(),
item.isRead(),
item.isAccountBookmark());
}
@Override
public void setBookmarkUrl(long nativeBookmarkBridge, long id, int type, GURL url) {
BookmarkId bookmarkId = new BookmarkId(id, type);
BookmarkItem item = FakeBookmarkModel.this.getBookmarkById(bookmarkId);
FakeBookmarkModel.this.updateBookmarkItem(
bookmarkId,
item.getParentId(),
item.getTitle(),
url,
item.isFolder(),
item.isEditable(),
item.isManaged(),
item.isRead(),
item.isAccountBookmark());
}
@Override
public byte[] getPowerBookmarkMeta(long nativeBookmarkBridge, long id, int type) {
return mBookmarkIdToPowerBookmarkMetaMap.get(new BookmarkId(id, type));
}
@Override
public void setPowerBookmarkMeta(
long nativeBookmarkBridge, long id, int type, byte[] meta) {
mBookmarkIdToPowerBookmarkMetaMap.put(new BookmarkId(id, type), meta);
}
@Override
public void deletePowerBookmarkMeta(long nativeBookmarkBridge, long id, int type) {
mBookmarkIdToPowerBookmarkMetaMap.remove(new BookmarkId(id, type));
}
@Override
public boolean doesBookmarkExist(long nativeBookmarkBridge, long id, int type) {
return mBookmarkIdToItemMap.containsKey(new BookmarkId(id, type));
}
@Override
public void getBookmarksForFolder(
long nativeBookmarkBridge, BookmarkId folderId, List<BookmarkItem> bookmarksList) {}
@Override
public boolean isFolderVisible(long nativeBookmarkBridge, long id, int type) {
return true;
}
@Override
public BookmarkId addFolder(
long nativeBookmarkBridge, BookmarkId parent, int index, String title) {
return FakeBookmarkModel.this.addFolder(parent, title);
}
@Override
public void deleteBookmark(long nativeBookmarkBridge, BookmarkId bookmarkId) {
mBookmarkIdToItemMap.remove(bookmarkId);
mBookmarkIdToPowerBookmarkMetaMap.remove(bookmarkId);
}
@Override
public void removeAllUserBookmarks(long nativeBookmarkBridge) {
mBookmarkIdToItemMap.clear();
setupTopLevelFolders();
}
@Override
public void moveBookmark(
long nativeBookmarkBridge,
BookmarkId bookmarkId,
BookmarkId newParentId,
int index) {
BookmarkItem item = FakeBookmarkModel.this.getBookmarkById(bookmarkId);
FakeBookmarkModel.this.updateBookmarkItem(
bookmarkId,
newParentId,
item.getTitle(),
item.getUrl(),
item.isFolder(),
item.isEditable(),
item.isManaged(),
item.isRead(),
item.isAccountBookmark());
}
@Override
public BookmarkId addBookmark(
long nativeBookmarkBridge, BookmarkId parent, int index, String title, GURL url) {
return FakeBookmarkModel.this.addBookmark(BookmarkType.NORMAL, parent, title, url);
}
@Override
public BookmarkId addToReadingList(
long nativeBookmarkBridge, BookmarkId parentId, String title, GURL url) {
return FakeBookmarkModel.this.addBookmark(
BookmarkType.READING_LIST, parentId, title, url);
}
@Override
public void setReadStatus(long nativeBookmarkBridge, BookmarkId bookmarkId, boolean read) {
BookmarkItem item = FakeBookmarkModel.this.getBookmarkById(bookmarkId);
FakeBookmarkModel.this.updateBookmarkItem(
bookmarkId,
item.getParentId(),
item.getTitle(),
item.getUrl(),
item.isFolder(),
item.isEditable(),
item.isManaged(),
read,
item.isAccountBookmark());
}
@Override
public int getUnreadCount(long nativeBookmarkBridge, BookmarkId id) {
List<BookmarkId> childIds = FakeBookmarkModel.this.getChildIds(id);
return (int)
childIds.stream()
.map(childId -> FakeBookmarkModel.this.getBookmarkById(childId))
.filter(item -> !item.isRead())
.count();
}
@Override
public boolean isAccountBookmark(long nativeBookmarkBridge, BookmarkId id) {
BookmarkItem item = FakeBookmarkModel.this.getBookmarkById(id);
BookmarkId parentId = item.getParentId();
BookmarkItem parentItem =
parentId == null ? null : FakeBookmarkModel.this.getBookmarkById(parentId);
return item.isAccountBookmark()
|| (parentItem != null && parentItem.isAccountBookmark());
}
@Override
public void undo(long nativeBookmarkBridge) {
assert false : "Not implemented!";
}
@Override
public void startGroupingUndos(long nativeBookmarkBridge) {
// No-op
}
@Override
public void endGroupingUndos(long nativeBookmarkBridge) {
// No-op
}
@Override
public void loadEmptyPartnerBookmarkShimForTesting(long nativeBookmarkBridge) {
assert false : "Not implemented!";
}
@Override
public void loadFakePartnerBookmarkShimForTesting(long nativeBookmarkBridge) {
assert false : "Not implemented!";
}
@Override
public void searchBookmarks(
long nativeBookmarkBridge,
List<BookmarkId> bookmarkMatches,
String query,
String[] tags,
int powerBookmarkType,
int maxNumber) {
bookmarkMatches.addAll(
mBookmarkIdToItemMap.values().stream()
.filter(
item ->
item.getTitle().contains(query)
|| item.getUrlForDisplay().contains(query))
.map(item -> item.getId())
.sorted((first, second) -> Long.compare(first.getId(), second.getId()))
.collect(Collectors.toList()));
}
@Override
public void getBookmarksOfType(
long nativeBookmarkBridge,
List<BookmarkId> bookmarkMatches,
int powerBookmarkType) {
assert false : "Not implemented!";
}
@Override
public boolean isDoingExtensiveChanges(long nativeBookmarkBridge) {
return false;
}
@Override
public void destroy(long nativeBookmarkBridge) {
mBookmarkIdToItemMap.clear();
setupTopLevelFolders();
}
@Override
public boolean isEditBookmarksEnabled(long nativeBookmarkBridge) {
assert false : "Not implemented!";
return false;
}
@Override
public void reorderChildren(
long nativeBookmarkBridge, BookmarkId parent, long[] orderedNodes) {
assert false : "Not implemented!";
}
@Override
public boolean isBookmarked(long nativeBookmarkBridge, GURL url) {
return mBookmarkIdToItemMap.values().stream()
.filter(item -> item.getUrl().equals(url))
.count()
> 0;
}
}
}