// Copyright 2015 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.app.Activity;
import android.content.ComponentName;
import android.content.Context;
import android.content.Intent;
import android.content.SharedPreferences.Editor;
import android.content.res.ColorStateList;
import android.content.res.Resources;
import android.graphics.drawable.Drawable;
import android.net.Uri;
import android.os.Handler;
import android.os.LocaleList;
import android.os.Looper;
import android.provider.Browser;
import android.text.TextUtils;
import androidx.annotation.ColorInt;
import androidx.annotation.NonNull;
import androidx.annotation.Nullable;
import androidx.annotation.VisibleForTesting;
import androidx.appcompat.content.res.AppCompatResources;
import org.chromium.base.BuildInfo;
import org.chromium.base.Callback;
import org.chromium.base.ContextUtils;
import org.chromium.base.Log;
import org.chromium.base.ThreadUtils;
import org.chromium.base.metrics.RecordHistogram;
import org.chromium.base.metrics.RecordUserAction;
import org.chromium.base.shared_preferences.SharedPreferencesManager;
import org.chromium.chrome.R;
import org.chromium.chrome.browser.ActivityUtils;
import org.chromium.chrome.browser.IntentHandler;
import org.chromium.chrome.browser.app.bookmarks.BookmarkActivity;
import org.chromium.chrome.browser.app.bookmarks.BookmarkEditActivity;
import org.chromium.chrome.browser.app.bookmarks.BookmarkFolderPickerActivity;
import org.chromium.chrome.browser.bookmarks.BookmarkUiPrefs.BookmarkRowDisplayPref;
import org.chromium.chrome.browser.commerce.ShoppingServiceFactory;
import org.chromium.chrome.browser.document.ChromeLauncherActivity;
import org.chromium.chrome.browser.feature_engagement.TrackerFactory;
import org.chromium.chrome.browser.preferences.ChromePreferenceKeys;
import org.chromium.chrome.browser.preferences.ChromeSharedPreferences;
import org.chromium.chrome.browser.profiles.Profile;
import org.chromium.chrome.browser.signin.services.IdentityServicesProvider;
import org.chromium.chrome.browser.tab.Tab;
import org.chromium.chrome.browser.ui.favicon.FaviconUtils;
import org.chromium.chrome.browser.ui.messages.snackbar.Snackbar;
import org.chromium.chrome.browser.ui.messages.snackbar.SnackbarManager;
import org.chromium.chrome.browser.ui.messages.snackbar.SnackbarManager.SnackbarController;
import org.chromium.chrome.browser.user_education.UserEducationHelper;
import org.chromium.components.bookmarks.BookmarkId;
import org.chromium.components.bookmarks.BookmarkItem;
import org.chromium.components.bookmarks.BookmarkType;
import org.chromium.components.browser_ui.bottomsheet.BottomSheetController;
import org.chromium.components.browser_ui.styles.ChromeColors;
import org.chromium.components.browser_ui.styles.SemanticColorUtils;
import org.chromium.components.browser_ui.widget.RoundedIconGenerator;
import org.chromium.components.commerce.core.ShoppingService;
import org.chromium.components.embedder_support.util.UrlConstants;
import org.chromium.components.feature_engagement.EventConstants;
import org.chromium.components.signin.identitymanager.IdentityManager;
import org.chromium.ui.UiUtils;
import org.chromium.ui.base.DeviceFormFactor;
import org.chromium.url.GURL;
import java.text.DateFormat;
import java.util.ArrayList;
import java.util.Date;
import java.util.List;
import java.util.Locale;
import java.util.Objects;
import java.util.concurrent.TimeUnit;
/** A class holding static util functions for bookmark. */
public class BookmarkUtils {
private static final String TAG = "BookmarkUtils";
private static final int READING_LIST_SESSION_LENGTH_MS = (int) TimeUnit.HOURS.toMillis(1);
/**
* If the tab has already been bookmarked, start {@link BookmarkEditActivity} for the normal
* bookmark or show the reading list page for reading list bookmark. If not, add the bookmark to
* {@link BookmarkModel}, and show a snackbar notifying the user.
*
* @param existingBookmarkItem The {@link BookmarkItem} if the tab has already been bookmarked.
* @param bookmarkModel The bookmark model.
* @param tab The tab to add or edit a bookmark.
* @param bottomSheetController The {@link BottomSheetController} used to show the bottom sheet.
* @param activity Current activity.
* @param bookmarkType Type of the added bookmark.
* @param callback Invoked with the resulting bookmark ID, which could be null if unsuccessful.
* @param fromExplicitTrackUi Whether the bookmark was added directly from a tracking ui (e.g.
* the shopping "track price" button).
*/
public static void addOrEditBookmark(
@Nullable BookmarkItem existingBookmarkItem,
BookmarkModel bookmarkModel,
Tab tab,
BottomSheetController bottomSheetController,
Activity activity,
@BookmarkType int bookmarkType,
Callback<BookmarkId> callback,
boolean fromExplicitTrackUi) {
assert bookmarkModel.isBookmarkModelLoaded();
if (existingBookmarkItem != null) {
startEditActivity(activity, existingBookmarkItem.getId());
callback.onResult(existingBookmarkItem.getId());
return;
}
BookmarkId parent = null;
if (fromExplicitTrackUi) {
// If account bookmarks are enabled and active, they take precedence, otherwise fall
// back to the local-or-syncable mobile folder, e.g. for users that have
// sync-the-feature enabled.
parent =
bookmarkModel.areAccountBookmarkFoldersActive()
? bookmarkModel.getAccountMobileFolderId()
: bookmarkModel.getMobileFolderId();
}
BookmarkId newBookmarkId =
addBookmarkInternal(
activity,
tab.getProfile(),
bookmarkModel,
tab.getTitle(),
tab.getOriginalUrl(),
parent,
bookmarkType);
showSaveFlow(
activity,
bottomSheetController,
tab.getProfile(),
newBookmarkId,
fromExplicitTrackUi,
/* wasBookmarkMoved= */ false,
/* isNewBookmark= */ true);
callback.onResult(newBookmarkId);
}
/**
* Shows the bookmark save flow with the given {@link BookmarkId}.
*
* @param activity The current Activity.
* @param bottomSheetController The BottomSheetController, used to show the save flow.
* @param profile The profile currently used.
* @param bookmarkId The BookmarkId to show the save flow for. Can be null in some cases (e.g. a
* bookmark fails to be added) and in this case, the function writes to logcat and exits
* without any action.
* @param fromExplicitTrackUi Whether the bookmark was added from the explicit UI (e.g. the
* price-track menu item).
* @param wasBookmarkMoved Whether the save flow is shown as a result of a moved bookmark.
* @param isNewBookmark Whether the bookmark is newly created.
*/
static void showSaveFlow(
@NonNull Activity activity,
@NonNull BottomSheetController bottomSheetController,
@NonNull Profile profile,
@Nullable BookmarkId bookmarkId,
boolean fromExplicitTrackUi,
boolean wasBookmarkMoved,
boolean isNewBookmark) {
if (bookmarkId == null) {
Log.e(TAG, "Null bookmark found when showing the save flow, aborting.");
return;
}
ShoppingService shoppingService = ShoppingServiceFactory.getForProfile(profile);
UserEducationHelper userEducationHelper =
new UserEducationHelper(activity, profile, new Handler(Looper.myLooper()));
// Redirect the original profile when getting the identity manager, it's not done
// automatically in native.
IdentityManager identityManager =
IdentityServicesProvider.get().getIdentityManager(profile.getOriginalProfile());
BookmarkSaveFlowCoordinator bookmarkSaveFlowCoordinator =
new BookmarkSaveFlowCoordinator(
activity,
bottomSheetController,
shoppingService,
userEducationHelper,
profile,
identityManager);
bookmarkSaveFlowCoordinator.show(
bookmarkId, fromExplicitTrackUi, wasBookmarkMoved, isNewBookmark);
}
// The legacy code path to add or edit bookmark without triggering the bookmark bottom sheet.
// Used for feed and GTS.
private static BookmarkId addBookmarkAndShowSnackbar(
BookmarkModel bookmarkModel,
Tab tab,
SnackbarManager snackbarManager,
Activity activity,
boolean fromCustomTab,
@BookmarkType int bookmarkType) {
BookmarkId parentId = null;
if (bookmarkType == BookmarkType.READING_LIST) {
parentId = bookmarkModel.getDefaultReadingListFolder();
}
BookmarkId bookmarkId =
addBookmarkInternal(
activity,
tab.getProfile(),
bookmarkModel,
tab.getTitle(),
tab.getOriginalUrl(),
/* parent= */ parentId,
BookmarkType.NORMAL);
Snackbar snackbar;
if (bookmarkId == null) {
snackbar =
Snackbar.make(
activity.getString(R.string.bookmark_page_failed),
new SnackbarController() {
@Override
public void onDismissNoAction(Object actionData) {}
@Override
public void onAction(Object actionData) {}
},
Snackbar.TYPE_NOTIFICATION,
Snackbar.UMA_BOOKMARK_ADDED)
.setSingleLine(false);
RecordUserAction.record("EnhancedBookmarks.AddingFailed");
} else {
String folderName =
bookmarkModel.getBookmarkTitle(
bookmarkModel.getBookmarkById(bookmarkId).getParentId());
SnackbarController snackbarController =
createSnackbarControllerForEditButton(activity, bookmarkId);
if (getLastUsedParent() == null) {
if (fromCustomTab) {
String packageLabel = BuildInfo.getInstance().hostPackageLabel;
snackbar =
Snackbar.make(
activity.getString(R.string.bookmark_page_saved, packageLabel),
snackbarController,
Snackbar.TYPE_ACTION,
Snackbar.UMA_BOOKMARK_ADDED);
} else {
snackbar =
Snackbar.make(
activity.getString(R.string.bookmark_page_saved_default),
snackbarController,
Snackbar.TYPE_ACTION,
Snackbar.UMA_BOOKMARK_ADDED);
}
} else {
snackbar =
Snackbar.make(
activity.getString(R.string.bookmark_page_saved_folder, folderName),
snackbarController,
Snackbar.TYPE_ACTION,
Snackbar.UMA_BOOKMARK_ADDED);
}
snackbar.setSingleLine(false)
.setAction(activity.getString(R.string.bookmark_item_edit), null);
}
snackbarManager.showSnackbar(snackbar);
return bookmarkId;
}
/**
* Add an article to the reading list. If the article was already loaded, the entry will be
* overwritten. After successful addition, a snackbar will be shown notifying the user about the
* result of the operation.
*
* @param activity The associated activity which is adding this reading list item.
* @param bookmarkModel The bookmark model that talks to the bookmark backend.
* @param title The title of the reading list item being added.
* @param url The associated URL.
* @param snackbarManager The snackbar manager that will be used to show a snackbar.
* @param profile The profile currently used.
* @param bottomSheetController The {@link BottomSheetController} which is used to show the
* BookmarkSaveFlow.
* @return The bookmark ID created after saving the article to the reading list.
* @deprecated Used only by feed, new users should rely on addOrEditBookmark (or the tab
* bookmarker).
*/
@Deprecated
public static BookmarkId addToReadingList(
@NonNull Activity activity,
@NonNull BookmarkModel bookmarkModel,
@NonNull String title,
@NonNull GURL url,
@NonNull SnackbarManager snackbarManager,
@NonNull Profile profile,
@NonNull BottomSheetController bottomSheetController) {
assert bookmarkModel.isBookmarkModelLoaded();
BookmarkId bookmarkId =
addBookmarkInternal(
activity,
profile,
bookmarkModel,
title,
url,
bookmarkModel.getDefaultReadingListFolder(),
BookmarkType.READING_LIST);
if (bookmarkId == null) {
return null;
}
// Reading list is aligned with the bookmark save flow used by all other bookmark saves.
// This is bundled with account bookmarks to modernize the infra.
if (bookmarkModel.areAccountBookmarkFoldersActive()) {
showSaveFlow(
activity,
bottomSheetController,
profile,
bookmarkId,
/* fromExplicitTrackUi= */ false,
/* wasBookmarkMoved= */ false,
/* isNewBookmark= */ true);
} else {
Snackbar snackbar =
Snackbar.make(
activity.getString(R.string.reading_list_saved),
new SnackbarController() {},
Snackbar.TYPE_ACTION,
Snackbar.UMA_READING_LIST_BOOKMARK_ADDED);
snackbarManager.showSnackbar(snackbar);
}
TrackerFactory.getTrackerForProfile(profile)
.notifyEvent(EventConstants.READ_LATER_ARTICLE_SAVED);
return bookmarkId;
}
/**
* Add all selected tabs from TabListEditor as bookmarks. This logic depends on the snackbar
* workflow above. Currently there is no support for adding the selected tabs or newly created
* folder directly to the reading list.
*
* @param activity The current activity.
* @param bookmarkModel The bookmark model.
* @param tabList The list of all currently selected tabs from the TabListEditor menu.
* @param snackbarManager The SnackbarManager used to show the snackbar.
*/
public static void addBookmarksOnMultiSelect(
Activity activity,
@NonNull BookmarkModel bookmarkModel,
@NonNull List<Tab> tabList,
@NonNull SnackbarManager snackbarManager) {
// TODO(crbug.com/40879467): Refactor the bookmark folder select activity to allow for the
// view to display in a dialog implementation approach.
assert bookmarkModel != null;
// For a single selected bookmark, default to the single tab-to-bookmark approach.
if (tabList.size() == 1) {
addBookmarkAndShowSnackbar(
bookmarkModel,
tabList.get(0),
snackbarManager,
activity,
false,
BookmarkType.NORMAL);
return;
}
// Current date time format with an example would be: Nov 17, 2022 4:34:20 PM PST
DateFormat dateFormat =
DateFormat.getDateTimeInstance(
DateFormat.MEDIUM, DateFormat.LONG, getLocale(activity));
String fileName =
activity.getString(
R.string.tab_selection_editor_add_bookmarks_folder_name,
dateFormat.format(new Date(System.currentTimeMillis())));
BookmarkId newFolder =
bookmarkModel.addFolder(bookmarkModel.getDefaultBookmarkFolder(), 0, fileName);
int tabsBookmarkedCount = 0;
for (Tab tab : tabList) {
BookmarkId tabToBookmark =
addBookmarkInternal(
activity,
tab.getProfile(),
bookmarkModel,
tab.getTitle(),
tab.getOriginalUrl(),
newFolder,
BookmarkType.NORMAL);
if (bookmarkModel.doesBookmarkExist(tabToBookmark)) {
tabsBookmarkedCount++;
}
}
RecordHistogram.recordCount100Histogram(
"Android.TabMultiSelectV2.BookmarkTabsCount", tabsBookmarkedCount);
SnackbarController snackbarController =
createSnackbarControllerForBookmarkFolderEditButton(activity, newFolder);
Snackbar snackbar =
Snackbar.make(
activity.getString(R.string.bookmark_page_saved_default),
snackbarController,
Snackbar.TYPE_ACTION,
Snackbar.UMA_BOOKMARK_ADDED);
snackbar.setSingleLine(false)
.setAction(activity.getString(R.string.bookmark_item_edit), null);
snackbarManager.showSnackbar(snackbar);
}
/**
* Adds a bookmark with the given {@link Tab} without showing save flow.
*
* @param context The current Android {@link Context}.
* @param tab The tab to add or edit a bookmark.
* @param bookmarkModel The current {@link BookmarkModel} which talks to native.
*/
public static BookmarkId addBookmarkWithoutShowingSaveFlow(
Context context, Tab tab, BookmarkModel bookmarkModel) {
BookmarkId parent =
bookmarkModel.areAccountBookmarkFoldersActive()
? bookmarkModel.getAccountMobileFolderId()
: bookmarkModel.getMobileFolderId();
return addBookmarkInternal(
context,
tab.getProfile(),
bookmarkModel,
tab.getTitle(),
tab.getOriginalUrl(),
parent,
BookmarkType.NORMAL);
}
/**
* Adds a bookmark with the given {@link Tab}. This will reset last used parent if it fails to
* add a bookmark.
*
* @param context The current Android {@link Context}.
* @param profile The profile being used when adding the bookmark.
* @param bookmarkModel The current {@link BookmarkModel} which talks to native.
* @param title The title of the new bookmark.
* @param url The {@link GURL} of the new bookmark.
* @param bookmarkType The {@link BookmarkType} of the bookmark.
* @param parent The {@link BookmarkId} which is the parent of the bookmark. If this is null,
* then the default parent is used.
*/
static BookmarkId addBookmarkInternal(
Context context,
Profile profile,
BookmarkModel bookmarkModel,
String title,
GURL url,
@Nullable BookmarkId parent,
@BookmarkType int bookmarkType) {
parent = parent == null ? getLastUsedParent() : parent;
BookmarkItem parentItem = null;
if (parent != null) {
parentItem = bookmarkModel.getBookmarkById(parent);
}
if (parent == null
|| parentItem == null
|| parentItem.isManaged()
|| !parentItem.isFolder()) {
parent =
bookmarkType == BookmarkType.READING_LIST
? bookmarkModel.getDefaultReadingListFolder()
: bookmarkModel.getDefaultBookmarkFolder();
}
// Reading list items will be added when either one of the 2 conditions is met:
// 1. The bookmark type explicitly specifies READING_LIST.
// 2. The last used parent implicitly specifies READING_LIST.
final BookmarkId bookmarkId;
if (bookmarkType == BookmarkType.READING_LIST
|| parent.getType() == BookmarkType.READING_LIST) {
bookmarkId = bookmarkModel.addToReadingList(parent, title, url);
} else {
// Use "New tab" as title for both incognito and regular NTP.
if (url.getSpec().equals(UrlConstants.NTP_URL)) {
title = context.getResources().getString(R.string.new_tab_title);
}
bookmarkId =
bookmarkModel.addBookmark(
parent, bookmarkModel.getChildCount(parent), title, url);
}
if (bookmarkId != null) {
BookmarkMetrics.recordBookmarkAdded(profile, bookmarkId);
setLastUsedParent(parent);
}
return bookmarkId;
}
/**
* Creates a snackbar controller for a case where "Edit" button is shown to edit the newly
* created bookmark.
*/
private static SnackbarController createSnackbarControllerForEditButton(
final Activity activity, final BookmarkId bookmarkId) {
return new SnackbarController() {
@Override
public void onDismissNoAction(Object actionData) {
RecordUserAction.record("EnhancedBookmarks.EditAfterCreateButtonNotClicked");
}
@Override
public void onAction(Object actionData) {
RecordUserAction.record("EnhancedBookmarks.EditAfterCreateButtonClicked");
startEditActivity(activity, bookmarkId);
}
};
}
/**
* Creates a snackbar controller for a case where "Edit" button is shown to edit a newly created
* bookmarks folder with bulk added bookmarks
*/
private static SnackbarController createSnackbarControllerForBookmarkFolderEditButton(
Context context, BookmarkId folder) {
return new SnackbarController() {
@Override
public void onDismissNoAction(Object actionData) {
RecordUserAction.record("TabMultiSelectV2.BookmarkTabsSnackbarEditNotClicked");
}
@Override
public void onAction(Object actionData) {
RecordUserAction.record("TabMultiSelectV2.BookmarkTabsSnackbarEditClicked");
BookmarkUtils.startEditActivity(context, folder);
}
};
}
/**
* Shows bookmark main UI.
*
* @param activity An activity to start the manager with.
* @param isIncognito Whether the bookmark manager is opened in incognito mode.
*/
public static void showBookmarkManager(Activity activity, boolean isIncognito) {
showBookmarkManager(activity, null, isIncognito);
}
/**
* Shows bookmark main UI.
*
* @param activity An activity to start the manager with. If null, the bookmark manager will be
* started as a new task.
* @param folderId The bookmark folder to open. If null, the bookmark manager will open the most
* recent folder.
* @param isIncognito Whether the bookmark UI is opened in incognito mode.
*/
public static void showBookmarkManager(
@Nullable Activity activity, @Nullable BookmarkId folderId, boolean isIncognito) {
ThreadUtils.assertOnUiThread();
Context context = activity == null ? ContextUtils.getApplicationContext() : activity;
String url = getFirstUrlToLoad(folderId);
if (ChromeSharedPreferences.getInstance()
.contains(ChromePreferenceKeys.BOOKMARKS_LAST_USED_URL)) {
RecordUserAction.record("MobileBookmarkManagerReopenBookmarksInSameSession");
}
if (DeviceFormFactor.isNonMultiDisplayContextOnTablet(context)) {
showBookmarkManagerOnTablet(
context,
activity == null ? null : activity.getComponentName(),
url,
isIncognito);
} else {
showBookmarkManagerOnPhone(activity, url, isIncognito);
}
}
private static void showBookmarkManagerOnPhone(
Activity activity, String url, boolean isIncognito) {
Intent intent =
new Intent(
activity == null ? ContextUtils.getApplicationContext() : activity,
BookmarkActivity.class);
intent.putExtra(IntentHandler.EXTRA_INCOGNITO_MODE, isIncognito);
intent.setData(Uri.parse(url));
if (activity != null) {
// Start from an existing activity.
intent.putExtra(IntentHandler.EXTRA_PARENT_COMPONENT, activity.getComponentName());
activity.startActivity(intent);
} else {
// Start a new task.
intent.addFlags(Intent.FLAG_ACTIVITY_NEW_TASK);
IntentHandler.startActivityForTrustedIntent(intent);
}
}
private static void showBookmarkManagerOnTablet(
Context context,
@Nullable ComponentName componentName,
String url,
boolean isIncognito) {
Intent intent = new Intent(Intent.ACTION_VIEW, Uri.parse(url));
intent.putExtra(IntentHandler.EXTRA_INCOGNITO_MODE, isIncognito);
intent.putExtra(
Browser.EXTRA_APPLICATION_ID, context.getApplicationContext().getPackageName());
intent.setFlags(Intent.FLAG_ACTIVITY_NEW_TASK);
if (componentName != null) {
ActivityUtils.setNonAliasedComponentForMainBrowsingActivity(intent, componentName);
} else {
// If the bookmark manager is shown in a tab on a phone (rather than in a separate
// activity) the component name may be null. Send the intent through
// ChromeLauncherActivity instead to avoid crashing. See crbug.com/615012.
intent.setClass(context.getApplicationContext(), ChromeLauncherActivity.class);
}
IntentHandler.startActivityForTrustedIntent(intent);
}
/**
* @return the bookmark folder URL to open.
*/
private static String getFirstUrlToLoad(@Nullable BookmarkId folderId) {
String url;
if (folderId == null) {
// Load most recently visited bookmark folder.
url = getLastUsedUrl();
} else {
// Load a specific folder.
url = BookmarkUiState.createFolderUrl(folderId).toString();
}
return TextUtils.isEmpty(url) ? UrlConstants.BOOKMARKS_URL : url;
}
/**
* Saves the last used url to preference. The saved url will be later queried by {@link
* #getLastUsedUrl()}.
*/
@VisibleForTesting(otherwise = VisibleForTesting.PACKAGE_PRIVATE)
public static void setLastUsedUrl(String url) {
ChromeSharedPreferences.getInstance()
.writeString(ChromePreferenceKeys.BOOKMARKS_LAST_USED_URL, url);
}
/** Fetches url representing the user's state last time they close the bookmark manager. */
@VisibleForTesting
public static String getLastUsedUrl() {
return ChromeSharedPreferences.getInstance()
.readString(
ChromePreferenceKeys.BOOKMARKS_LAST_USED_URL, UrlConstants.BOOKMARKS_URL);
}
@VisibleForTesting
public static void clearLastUsedPrefs() {
Editor editor = ChromeSharedPreferences.getInstance().getEditor();
editor.remove(ChromePreferenceKeys.BOOKMARKS_LAST_USED_PARENT);
editor.remove(ChromePreferenceKeys.BOOKMARKS_LAST_USED_URL);
editor.apply();
}
/** Save the last used {@link BookmarkId} as a folder to put new bookmarks to. */
public static void setLastUsedParent(BookmarkId bookmarkId) {
ChromeSharedPreferences.getInstance()
.writeString(
ChromePreferenceKeys.BOOKMARKS_LAST_USED_PARENT, bookmarkId.toString());
}
/**
* @return The parent {@link BookmarkId} that the user used the last time or null if the user
* has never selected a parent folder to use.
*/
public static @Nullable BookmarkId getLastUsedParent() {
SharedPreferencesManager preferences = ChromeSharedPreferences.getInstance();
if (!preferences.contains(ChromePreferenceKeys.BOOKMARKS_LAST_USED_PARENT)) return null;
return BookmarkId.getBookmarkIdFromString(
preferences.readString(ChromePreferenceKeys.BOOKMARKS_LAST_USED_PARENT, null));
}
/** Starts an {@link BookmarkEditActivity} for the given {@link BookmarkId}. */
public static void startEditActivity(Context context, BookmarkId bookmarkId) {
RecordUserAction.record("MobileBookmarkManagerEditBookmark");
Intent intent = new Intent(context, BookmarkEditActivity.class);
intent.putExtra(BookmarkEditActivity.INTENT_BOOKMARK_ID, bookmarkId.toString());
if (context instanceof BookmarkActivity) {
((BookmarkActivity) context)
.startActivityForResult(intent, BookmarkActivity.EDIT_BOOKMARK_REQUEST_CODE);
} else {
context.startActivity(intent);
}
}
/** Given the {@link BookmarkId}s, return a list of those ids serialized to string. */
public static ArrayList<String> bookmarkIdsToStringList(BookmarkId... bookmarkIds) {
ArrayList<String> bookmarkStrings = new ArrayList<>(bookmarkIds.length);
for (BookmarkId id : bookmarkIds) {
bookmarkStrings.add(id.toString());
}
return bookmarkStrings;
}
/**
* Given the {@link BookmarkId}s serialized {@link String}s, return a list of the {@link
* BookmarkId}s.
*/
public static List<BookmarkId> stringListToBookmarkIds(
BookmarkModel bookmarkModel, List<String> bookmarkIdStrings) {
List<BookmarkId> bookmarkIds = new ArrayList<>(bookmarkIdStrings.size());
for (String string : bookmarkIdStrings) {
BookmarkId bookmarkId = BookmarkId.getBookmarkIdFromString(string);
if (bookmarkModel.doesBookmarkExist(bookmarkId)) {
bookmarkIds.add(bookmarkId);
}
}
return bookmarkIds;
}
/** Starts an {@link BookmarkFolderPickerActivity} for the given {@link BookmarkId}s. */
public static void startFolderPickerActivity(Context context, BookmarkId... bookmarkIds) {
Intent intent = new Intent(context, BookmarkFolderPickerActivity.class);
intent.putStringArrayListExtra(
BookmarkFolderPickerActivity.INTENT_BOOKMARK_IDS,
BookmarkUtils.bookmarkIdsToStringList(bookmarkIds));
context.startActivity(intent);
}
/**
* @param context {@link Context} used to retrieve the drawable.
* @param bookmarkId The bookmark id of the folder.
* @param bookmarkModel The bookmark model.
* @return A {@link Drawable} to use for displaying bookmark folders.
*/
public static Drawable getFolderIcon(
Context context,
BookmarkId bookmarkId,
BookmarkModel bookmarkModel,
@BookmarkRowDisplayPref int displayPref) {
ColorStateList tint = getFolderIconTint(context, bookmarkId.getType());
if (bookmarkId.getType() == BookmarkType.READING_LIST) {
return UiUtils.getTintedDrawable(context, R.drawable.ic_reading_list_folder_24dp, tint);
} else if (bookmarkId.getType() == BookmarkType.NORMAL
&& Objects.equals(bookmarkId, bookmarkModel.getDesktopFolderId())) {
return UiUtils.getTintedDrawable(context, R.drawable.ic_toolbar_24dp, tint);
}
return UiUtils.getTintedDrawable(
context,
displayPref == BookmarkRowDisplayPref.VISUAL
? R.drawable.ic_folder_outline_24dp
: R.drawable.ic_folder_blue_24dp,
tint);
}
/**
* @param context {@link Context} used to retrieve the drawable.
* @param type The bookmark type of the folder.
* @return The tint used on the bookmark folder icon.
*/
// TODO(crbug.com/40282037): This function isn't used in the new bookmarks manager, remove it
// after android-improved-bookmarks is the default.
public static ColorStateList getFolderIconTint(Context context, @BookmarkType int type) {
if (type == BookmarkType.READING_LIST) {
return ColorStateList.valueOf(SemanticColorUtils.getDefaultIconColorAccent1(context));
}
return ColorStateList.valueOf(context.getColor(R.color.default_icon_color_tint_list));
}
/** Closes the {@link BookmarkActivity} on Phone. Does nothing on tablet. */
public static void finishActivityOnPhone(Context context) {
if (context instanceof BookmarkActivity) {
((Activity) context).finish();
}
}
/**
* Expires the stored last used url if Chrome has been in the background long enough to mark it
* as a new session. We're using the "Start Surface" concept of session here which is if the app
* has been in the background for X amount of time. Called from #onStartWithNative, after which
* the time stored in {@link ChromeInactivityTracker} is expired.
*
* @param timeSinceLastBackgroundedMs The time since Chrome has sent into the background.
*/
public static void maybeExpireLastBookmarkLocationForReadLater(
long timeSinceLastBackgroundedMs) {
if (timeSinceLastBackgroundedMs > READING_LIST_SESSION_LENGTH_MS) {
ChromeSharedPreferences.getInstance()
.removeKey(ChromePreferenceKeys.BOOKMARKS_LAST_USED_URL);
}
}
/** Returns whether this bookmark can be moved */
public static boolean isMovable(BookmarkModel bookmarkModel, BookmarkItem item) {
if (Objects.equals(item.getParentId(), bookmarkModel.getPartnerFolderId())) return false;
return item.isEditable();
}
/**
* Gets the display count for folders.
*
* @param id The bookmark to get the description for, must be a folder.
* @param bookmarkModel The bookmark model to get info on the bookmark.
*/
public static int getChildCountForDisplay(BookmarkId id, BookmarkModel bookmarkModel) {
if (id.getType() == BookmarkType.READING_LIST) {
return bookmarkModel.getUnreadCount(id);
} else {
return bookmarkModel.getTotalBookmarkCount(id);
}
}
/**
* Returns the description to use for the folder in bookmarks manager.
*
* @param id The bookmark to get the description for, must be a folder.
* @param bookmarkModel The bookmark model to get info on the bookmark.
* @param resources Android resources object to get strings.
*/
public static String getFolderDescriptionText(
BookmarkId id, BookmarkModel bookmarkModel, Resources resources) {
int count = getChildCountForDisplay(id, bookmarkModel);
if (id.getType() == BookmarkType.READING_LIST) {
return (count > 0)
? resources.getQuantityString(
R.plurals.reading_list_unread_page_count, count, count)
: resources.getString(R.string.reading_list_no_unread_pages);
} else {
return (count > 0)
? resources.getQuantityString(R.plurals.bookmarks_count, count, count)
: resources.getString(R.string.no_bookmarks);
}
}
/** Returns the RoundedIconGenerator with the appropriate size. */
public static RoundedIconGenerator getRoundedIconGenerator(
Context context, @BookmarkRowDisplayPref int displayPref) {
Resources res = context.getResources();
int iconSize = getFaviconDisplaySize(res);
return displayPref == BookmarkRowDisplayPref.VISUAL
? new RoundedIconGenerator(
iconSize,
iconSize,
iconSize / 2,
context.getColor(R.color.default_favicon_background_color),
getDisplayTextSize(res))
: FaviconUtils.createCircularIconGenerator(context);
}
/** Returns the size to use when fetching favicons. */
public static int getFaviconFetchSize(Resources resources) {
return resources.getDimensionPixelSize(R.dimen.tile_view_icon_min_size);
}
/** Returns the size to use when displaying an image. */
public static int getImageIconSize(
Resources resources, @BookmarkRowDisplayPref int displayPref) {
return displayPref == BookmarkRowDisplayPref.VISUAL
? resources.getDimensionPixelSize(R.dimen.improved_bookmark_start_image_size_visual)
: resources.getDimensionPixelSize(
R.dimen.improved_bookmark_start_image_size_compact);
}
/** Returns the size to use when displaying the favicon. */
public static int getFaviconDisplaySize(Resources resources) {
return resources.getDimensionPixelSize(R.dimen.tile_view_icon_size_modern);
}
/**
* Returns whether the given folder can have a folder added to it. Uses the base implementation
* of {@link #canAddBookmarkToParent} with the additional constraint that a folder can't be
* added to the reading list.
*/
public static boolean canAddFolderToParent(BookmarkModel bookmarkModel, BookmarkId parentId) {
if (!canAddBookmarkToParent(bookmarkModel, parentId)) {
return false;
}
if (isReadingListFolder(bookmarkModel, parentId)) {
return false;
}
return true;
}
/** Returns whether the given folder can have a bookmark added to it. */
public static boolean canAddBookmarkToParent(BookmarkModel bookmarkModel, BookmarkId parentId) {
BookmarkItem parentItem = bookmarkModel.getBookmarkById(parentId);
if (parentItem == null) return false;
if (parentItem.isManaged()) return false;
if (Objects.equals(parentId, bookmarkModel.getPartnerFolderId())) return false;
if (Objects.equals(parentId, bookmarkModel.getRootFolderId())) return false;
return true;
}
/** Returns whether the given id is a special folder. */
public static boolean isSpecialFolder(BookmarkModel bookmarkModel, BookmarkItem item) {
return item != null && Objects.equals(item.getParentId(), bookmarkModel.getRootFolderId());
}
/** Return the background color for the given {@link BookmarkType}. */
public static @ColorInt int getIconBackground(
Context context, BookmarkModel bookmarkModel, BookmarkItem item) {
if (isSpecialFolder(bookmarkModel, item)) {
return SemanticColorUtils.getColorPrimaryContainer(context);
} else {
return ChromeColors.getSurfaceColor(context, R.dimen.default_elevation_1);
}
}
/** Return the icon tint for the given {@link BookmarkType}. */
public static ColorStateList getIconTint(
Context context, BookmarkModel bookmarkModel, BookmarkItem item) {
if (isSpecialFolder(bookmarkModel, item)) {
return ColorStateList.valueOf(
SemanticColorUtils.getDefaultIconColorOnAccent1Container(context));
} else {
return AppCompatResources.getColorStateList(
context, R.color.default_icon_color_secondary_tint_list);
}
}
/** Return whether the given BookmarkId is a reading list folder. */
public static boolean isReadingListFolder(BookmarkModel boomkarkModel, BookmarkId bookmarkId) {
if (bookmarkId == null) {
return false;
}
return Objects.equals(bookmarkId, boomkarkModel.getLocalOrSyncableReadingListFolder())
|| Objects.equals(bookmarkId, boomkarkModel.getAccountReadingListFolder());
}
private static int getDisplayTextSize(Resources resources) {
return resources.getDimensionPixelSize(R.dimen.improved_bookmark_favicon_text_size);
}
private static Locale getLocale(Activity activity) {
LocaleList locales = activity.getResources().getConfiguration().getLocales();
if (locales.size() > 0) {
return locales.get(0);
}
@SuppressWarnings("deprecation")
Locale locale = activity.getResources().getConfiguration().locale;
return locale;
}
}