From: uazo Date: Tue, 12 Dec 2023 16:38:10 +0000 Subject: Use list mode for tab switcher Added a setting for the option need: Add-cromite-flags-support.patch License: GPL-2.0-or-later - https://spdx.org/licenses/GPL-2.0-or-later.html --- chrome/android/features/tab_ui/BUILD.gn | 4 + .../res/drawable/selected_tab_background.xml | 13 + .../selected_tab_background_incognito.xml | 13 + .../java/res/layout/tab_list_card_item.xml | 25 ++ .../tab_ui/java/res/values/dimens.xml | 5 + .../TabGridDialogCoordinator.java | 11 +- .../tab_management/TabGridDialogMediator.java | 8 + .../TabGridDialogProperties.java | 3 + .../tab_management/TabGridDialogView.java | 13 + .../TabGridDialogViewBinder.java | 8 + .../TabListContainerViewBinder.java | 34 +- .../tab_management/TabListCoordinator.java | 37 +- .../TabListEditorCoordinator.java | 2 +- .../tab_management/TabListItemAnimator.java | 2 +- .../tasks/tab_management/TabListMediator.java | 22 +- .../tasks/tab_management/TabListView.java | 123 ++++++ .../tab_management/TabListViewBinder.java | 353 ++++++++++++++++++ .../tab_management/TabSwitcherPaneBase.java | 4 +- .../TabSwitcherPaneCoordinatorFactory.java | 5 +- .../tab_management/TabUiFeatureUtilities.java | 7 + .../PinnedTabStripCoordinator.java | 10 +- .../tab_ui/tab_management_java_sources.gni | 2 + .../java/res/xml/privacy_preferences.xml | 6 + .../chrome/browser/app/ChromeActivity.java | 5 +- chrome/browser/android/favicon_helper.cc | 214 ++++++++++- chrome/browser/android/favicon_helper.h | 23 ++ .../flags/cromite/sTabSwitcherListMode.java | 33 ++ .../tab_ui/TabListFaviconProvider.java | 111 ++++++ .../browser/ui/favicon/FaviconHelper.java | 57 ++- .../Use-list-mode-for-tab-switcher.grdp | 9 + .../browser/toolbar/top/ToolbarPhone.java | 6 +- components/browser_ui/styles/android/BUILD.gn | 1 + .../java/res/drawable/ic_warning_red_16dp.xml | 26 ++ .../layout/title_and_description_layout.xml | 31 +- .../tab-switcher-list-mode.inc | 3 + 35 files changed, 1186 insertions(+), 43 deletions(-) create mode 100644 chrome/android/features/tab_ui/java/res/drawable/selected_tab_background.xml create mode 100644 chrome/android/features/tab_ui/java/res/drawable/selected_tab_background_incognito.xml create mode 100644 chrome/android/features/tab_ui/java/res/layout/tab_list_card_item.xml create mode 100644 chrome/android/features/tab_ui/java/src/org/chromium/chrome/browser/tasks/tab_management/TabListView.java create mode 100644 chrome/android/features/tab_ui/java/src/org/chromium/chrome/browser/tasks/tab_management/TabListViewBinder.java create mode 100644 chrome/browser/flags/android/java/src/org/chromium/chrome/browser/flags/cromite/sTabSwitcherListMode.java create mode 100644 chrome/browser/ui/android/strings/cromite_android_chrome_strings_grd/Use-list-mode-for-tab-switcher.grdp create mode 100644 components/browser_ui/styles/android/java/res/drawable/ic_warning_red_16dp.xml create mode 100644 cromite_flags/chrome/browser/flags/android/chrome_feature_list_cc/tab-switcher-list-mode.inc diff --git a/chrome/android/features/tab_ui/BUILD.gn b/chrome/android/features/tab_ui/BUILD.gn --- a/chrome/android/features/tab_ui/BUILD.gn +++ b/chrome/android/features/tab_ui/BUILD.gn @@ -52,6 +52,8 @@ android_resources("java_resources") { "java/res/drawable/pinned_tab_strip_item_background.xml", "java/res/drawable/price_card_background.xml", "java/res/drawable/price_card_scrim.xml", + "java/res/drawable/selected_tab_background.xml", + "java/res/drawable/selected_tab_background_incognito.xml", "java/res/drawable/tab_grid_card_background.xml", "java/res/drawable/tab_grid_card_highlight.xml", "java/res/drawable/tab_grid_dialog_background.xml", @@ -112,6 +114,8 @@ android_resources("java_resources") { "java/res/layout/tab_group_visual_data_dialog.xml", "java/res/layout/tab_group_visual_data_text_input_layout.xml", "java/res/layout/tab_hover_card_holder.xml", + "java/res/layout/tab_list_card_item.xml", + "java/res/layout/tab_list_card_item.xml", "java/res/layout/tab_list_editor_action_view.xml", "java/res/layout/tab_list_editor_layout.xml", "java/res/layout/tab_list_editor_toolbar.xml", diff --git a/chrome/android/features/tab_ui/java/res/drawable/selected_tab_background.xml b/chrome/android/features/tab_ui/java/res/drawable/selected_tab_background.xml new file mode 100644 --- /dev/null +++ b/chrome/android/features/tab_ui/java/res/drawable/selected_tab_background.xml @@ -0,0 +1,13 @@ + + + + + + + diff --git a/chrome/android/features/tab_ui/java/res/drawable/selected_tab_background_incognito.xml b/chrome/android/features/tab_ui/java/res/drawable/selected_tab_background_incognito.xml new file mode 100644 --- /dev/null +++ b/chrome/android/features/tab_ui/java/res/drawable/selected_tab_background_incognito.xml @@ -0,0 +1,13 @@ + + + + + + + diff --git a/chrome/android/features/tab_ui/java/res/layout/tab_list_card_item.xml b/chrome/android/features/tab_ui/java/res/layout/tab_list_card_item.xml new file mode 100644 --- /dev/null +++ b/chrome/android/features/tab_ui/java/res/layout/tab_list_card_item.xml @@ -0,0 +1,25 @@ + + + + + + + + + + + diff --git a/chrome/android/features/tab_ui/java/res/values/dimens.xml b/chrome/android/features/tab_ui/java/res/values/dimens.xml --- a/chrome/android/features/tab_ui/java/res/values/dimens.xml +++ b/chrome/android/features/tab_ui/java/res/values/dimens.xml @@ -6,8 +6,10 @@ found in the LICENSE file. --> 7dp + 6dp 7dp 4dp + 4dp 4dp 1dp 18dp @@ -43,6 +45,8 @@ found in the LICENSE file. 144dp 20dp 14dp + 18dp + 22dp 52dp @@ -117,6 +121,7 @@ found in the LICENSE file. 3dp 8dp 24dp + 9dp 6dp diff --git a/chrome/android/features/tab_ui/java/src/org/chromium/chrome/browser/tasks/tab_management/TabGridDialogCoordinator.java b/chrome/android/features/tab_ui/java/src/org/chromium/chrome/browser/tasks/tab_management/TabGridDialogCoordinator.java --- a/chrome/android/features/tab_ui/java/src/org/chromium/chrome/browser/tasks/tab_management/TabGridDialogCoordinator.java +++ b/chrome/android/features/tab_ui/java/src/org/chromium/chrome/browser/tasks/tab_management/TabGridDialogCoordinator.java @@ -244,7 +244,9 @@ public class TabGridDialogCoordinator implements TabGridDialogMediator.DialogCon // take in a mode parameter instead. mTabListCoordinator = new TabListCoordinator( - TabListMode.GRID, + TabUiFeatureUtilities.shouldUseListMode() + ? TabListMode.LIST + : TabListMode.GRID, activity, mBrowserControlsStateProvider, mModalDialogManager, @@ -361,6 +363,11 @@ public class TabGridDialogCoordinator implements TabGridDialogMediator.DialogCon : "SnackbarManager should have been created or the activity was already" + " finishing."; + @TabListMode + int mode = + TabUiFeatureUtilities.shouldUseListMode() + ? TabListMode.LIST + : TabListMode.GRID; ViewGroup container = mDialogView.findViewById(R.id.dialog_container_view); mTabListEditorCoordinator = new TabListEditorCoordinator( @@ -371,7 +378,7 @@ public class TabGridDialogCoordinator implements TabGridDialogMediator.DialogCon mCurrentTabGroupModelFilterSupplier, mTabContentManager, mTabListCoordinator::setRecyclerViewPosition, - TabListMode.GRID, + mode, /* displayGroups= */ false, mSnackbarManager, mBottomSheetController, diff --git a/chrome/android/features/tab_ui/java/src/org/chromium/chrome/browser/tasks/tab_management/TabGridDialogMediator.java b/chrome/android/features/tab_ui/java/src/org/chromium/chrome/browser/tasks/tab_management/TabGridDialogMediator.java --- a/chrome/android/features/tab_ui/java/src/org/chromium/chrome/browser/tasks/tab_management/TabGridDialogMediator.java +++ b/chrome/android/features/tab_ui/java/src/org/chromium/chrome/browser/tasks/tab_management/TabGridDialogMediator.java @@ -55,6 +55,7 @@ import org.chromium.chrome.browser.tab.TabLaunchType; import org.chromium.chrome.browser.tab.TabSelectionType; import org.chromium.chrome.browser.tab_group_sync.TabGroupSyncServiceFactory; import org.chromium.chrome.browser.tab_ui.RecyclerViewPosition; +import org.chromium.chrome.browser.tab_ui.TabCardThemeUtil; import org.chromium.chrome.browser.tabmodel.TabClosingSource; import org.chromium.chrome.browser.tabmodel.TabClosureParamsUtils; import org.chromium.chrome.browser.tabmodel.TabCreatorUtil; @@ -900,6 +901,13 @@ public class TabGridDialogMediator TabGridDialogProperties.DIALOG_UNGROUP_BAR_HOVERED_TEXT_COLOR, ungroupBarHoveredTextColor); mModel.set(TabGridDialogProperties.IS_INCOGNITO, isIncognito); + if (TabUiFeatureUtilities.shouldUseListMode()) { + int animationBackgroundColor = + TabCardThemeUtil.getCardViewBackgroundColor( + mActivity, isIncognito, /* isSelected= */ false, /*color*/ null); + mModel.set( + TabGridDialogProperties.ANIMATION_BACKGROUND_COLOR, animationBackgroundColor); + } } private void updateDialogScrollPosition() { diff --git a/chrome/android/features/tab_ui/java/src/org/chromium/chrome/browser/tasks/tab_management/TabGridDialogProperties.java b/chrome/android/features/tab_ui/java/src/org/chromium/chrome/browser/tasks/tab_management/TabGridDialogProperties.java --- a/chrome/android/features/tab_ui/java/src/org/chromium/chrome/browser/tasks/tab_management/TabGridDialogProperties.java +++ b/chrome/android/features/tab_ui/java/src/org/chromium/chrome/browser/tasks/tab_management/TabGridDialogProperties.java @@ -65,6 +65,8 @@ class TabGridDialogProperties { new WritableIntPropertyKey(); public static final WritableObjectPropertyKey DIALOG_UNGROUP_BAR_TEXT = new WritableObjectPropertyKey<>(); + public static final WritableObjectPropertyKey ANIMATION_BACKGROUND_COLOR = + new WritableObjectPropertyKey<>(); /** * Integer, but not {@link WritableIntPropertyKey} so that we can force update on the same @@ -147,6 +149,7 @@ class TabGridDialogProperties { TAB_GROUP_COLOR_ID, IS_INCOGNITO, COLOR_ICON_CLICK_LISTENER, + ANIMATION_BACKGROUND_COLOR, HAIRLINE_COLOR, HAIRLINE_VISIBILITY, FORCE_ANIMATION_TO_FINISH, diff --git a/chrome/android/features/tab_ui/java/src/org/chromium/chrome/browser/tasks/tab_management/TabGridDialogView.java b/chrome/android/features/tab_ui/java/src/org/chromium/chrome/browser/tasks/tab_management/TabGridDialogView.java --- a/chrome/android/features/tab_ui/java/src/org/chromium/chrome/browser/tasks/tab_management/TabGridDialogView.java +++ b/chrome/android/features/tab_ui/java/src/org/chromium/chrome/browser/tasks/tab_management/TabGridDialogView.java @@ -1141,6 +1141,19 @@ public class TabGridDialogView extends FrameLayout { mHairline.setVisibility(visible ? VISIBLE : GONE); } + /** + * Updates the background color for the animation card. + * + * @param colorInt The new color to use. + */ + void updateAnimationBackgroundColor(@ColorInt int colorInt) { + assert TabUiFeatureUtilities.shouldUseListMode(); + updateAnimationCardView(null); + Drawable animationBackground = + mAnimationCardView.findViewById(R.id.card_view).getBackground(); + DrawableCompat.setTint(animationBackground, colorInt); + } + /** * Update the ungroup bar background color. * diff --git a/chrome/android/features/tab_ui/java/src/org/chromium/chrome/browser/tasks/tab_management/TabGridDialogViewBinder.java b/chrome/android/features/tab_ui/java/src/org/chromium/chrome/browser/tasks/tab_management/TabGridDialogViewBinder.java --- a/chrome/android/features/tab_ui/java/src/org/chromium/chrome/browser/tasks/tab_management/TabGridDialogViewBinder.java +++ b/chrome/android/features/tab_ui/java/src/org/chromium/chrome/browser/tasks/tab_management/TabGridDialogViewBinder.java @@ -6,6 +6,7 @@ package org.chromium.chrome.browser.tasks.tab_management; import static org.chromium.build.NullUtil.assumeNonNull; import static org.chromium.chrome.browser.tasks.tab_management.TabGridDialogProperties.ADD_CLICK_LISTENER; +import static org.chromium.chrome.browser.tasks.tab_management.TabGridDialogProperties.ANIMATION_BACKGROUND_COLOR; import static org.chromium.chrome.browser.tasks.tab_management.TabGridDialogProperties.ANIMATION_SOURCE_VIEW; import static org.chromium.chrome.browser.tasks.tab_management.TabGridDialogProperties.APP_HEADER_HEIGHT; import static org.chromium.chrome.browser.tasks.tab_management.TabGridDialogProperties.BROWSER_CONTROLS_STATE_PROVIDER; @@ -227,6 +228,13 @@ class TabGridDialogViewBinder { } else if (COLOR_ICON_CLICK_LISTENER == propertyKey) { viewHolder.toolbarView.setColorIconOnClickListener( model.get(COLOR_ICON_CLICK_LISTENER)); + } else if (ANIMATION_BACKGROUND_COLOR == propertyKey) { + // Only set in LIST mode not GRID mode. Will always be set in LIST mode. Mode is not + // mutable without restarting the app. + if (model.get(ANIMATION_BACKGROUND_COLOR) != null) { + viewHolder.dialogView.updateAnimationBackgroundColor( + model.get(ANIMATION_BACKGROUND_COLOR)); + } } else if (FORCE_ANIMATION_TO_FINISH == propertyKey) { if (model.get(FORCE_ANIMATION_TO_FINISH)) { viewHolder.dialogView.forceAnimationToFinish(); diff --git a/chrome/android/features/tab_ui/java/src/org/chromium/chrome/browser/tasks/tab_management/TabListContainerViewBinder.java b/chrome/android/features/tab_ui/java/src/org/chromium/chrome/browser/tasks/tab_management/TabListContainerViewBinder.java --- a/chrome/android/features/tab_ui/java/src/org/chromium/chrome/browser/tasks/tab_management/TabListContainerViewBinder.java +++ b/chrome/android/features/tab_ui/java/src/org/chromium/chrome/browser/tasks/tab_management/TabListContainerViewBinder.java @@ -86,10 +86,7 @@ class TabListContainerViewBinder { mTabListRecyclerView = tabListRecyclerView; mPaneHairline = paneHairline; mSupplementaryContainer = supplementaryContainer; - mSearchBoxGapPx = - tabListRecyclerView - .getResources() - .getDimensionPixelSize(R.dimen.hub_search_box_gap); + mSearchBoxGapPx = 0; } } @@ -305,14 +302,27 @@ class TabListContainerViewBinder { } if (width <= 0 || height <= 0) return 0; + @TabListMode int mode = model.get(MODE); LinearLayoutManager layoutManager = (LinearLayoutManager) view.getLayoutManager(); - assert model.get(MODE) == TabListMode.GRID; - GridLayoutManager gridLayoutManager = (GridLayoutManager) layoutManager; - assumeNonNull(gridLayoutManager); - int cardWidth = width / gridLayoutManager.getSpanCount(); - int cardHeight = - TabUtils.deriveGridCardHeight( - cardWidth, view.getContext(), browserControlsStateProvider); - return Math.max(0, height / 2 - cardHeight / 2); + if (mode == TabListMode.GRID) { + GridLayoutManager gridLayoutManager = (GridLayoutManager) layoutManager; + assumeNonNull(gridLayoutManager); + int cardWidth = width / gridLayoutManager.getSpanCount(); + int cardHeight = + TabUtils.deriveGridCardHeight( + cardWidth, view.getContext(), browserControlsStateProvider); + return Math.max(0, height / 2 - cardHeight / 2); + } + if (mode == TabListMode.LIST) { + // Avoid divide by 0 when there are no tabs. + if (layoutManager.getItemCount() == 0) return 0; + + return Math.max( + 0, + height / 2 + - view.computeVerticalScrollRange() / layoutManager.getItemCount() / 2); + } + assert false : "Unexpected MODE when setting INITIAL_SCROLL_INDEX."; + return 0; } } diff --git a/chrome/android/features/tab_ui/java/src/org/chromium/chrome/browser/tasks/tab_management/TabListCoordinator.java b/chrome/android/features/tab_ui/java/src/org/chromium/chrome/browser/tasks/tab_management/TabListCoordinator.java --- a/chrome/android/features/tab_ui/java/src/org/chromium/chrome/browser/tasks/tab_management/TabListCoordinator.java +++ b/chrome/android/features/tab_ui/java/src/org/chromium/chrome/browser/tasks/tab_management/TabListCoordinator.java @@ -122,13 +122,13 @@ public class TabListCoordinator implements PriceWelcomeMessageProvider, DestroyO * *

NOTE: STRIP and GRID modes will have height equal to that of the container view. */ - @IntDef({TabListMode.GRID, TabListMode.STRIP, TabListMode.NUM_ENTRIES}) + @IntDef({TabListMode.GRID, TabListMode.STRIP, TabListMode.LIST, TabListMode.NUM_ENTRIES}) @Retention(RetentionPolicy.SOURCE) public @interface TabListMode { int GRID = 0; int STRIP = 1; // int CAROUSEL_DEPRECATED = 2; - // int LIST_DEPRECATED = 3; + int LIST = 3; int NUM_ENTRIES = 4; } @@ -374,6 +374,8 @@ public class TabListCoordinator implements PriceWelcomeMessageProvider, DestroyO assumeNonNull(model); if (mMode == TabListMode.GRID) { TabGridViewBinder.onViewRecycled(model, viewHolder.itemView); + } else if (mMode == TabListMode.LIST) { + TabListViewBinder.onViewRecycled(model, viewHolder.itemView); } else if (mMode == TabListMode.STRIP) { TabStripViewBinder.onViewRecycled(model, viewHolder.itemView); } @@ -446,6 +448,21 @@ public class TabListCoordinator implements PriceWelcomeMessageProvider, DestroyO .inflate(R.layout.tab_strip_item, parentView, false); }, TabStripViewBinder::bind); + } else if (mMode == TabListMode.LIST) { + mAdapter.registerType( + UiType.TAB, + parent -> { + ViewLookupCachingFrameLayout group = + (ViewLookupCachingFrameLayout) + LayoutInflater.from(activity) + .inflate( + R.layout.tab_list_card_item, + parentView, + false); + group.setClickable(true); + return group; + }, + TabListViewBinder::bindTab); } else { throw new IllegalArgumentException( "Attempting to create a tab list UI with invalid mode"); @@ -508,7 +525,7 @@ public class TabListCoordinator implements PriceWelcomeMessageProvider, DestroyO parentView.addView(mRecyclerView); } - // GRID has a fixed size. STRIP has a fixed size only if DATA_SHARING is off. + // GRID and LIST both have fixed size. STRIP has a fixed size only if DATA_SHARING is off. boolean hasFixedSize = mMode != TabListMode.STRIP || !TabUiUtils.isDataSharingFunctionalityEnabled(); mRecyclerView.setAdapter(mAdapter); @@ -540,9 +557,13 @@ public class TabListCoordinator implements PriceWelcomeMessageProvider, DestroyO Rect frame = new Rect(); mActivity.getWindow().getDecorView().getWindowVisibleDisplayFrame(frame); updateGridCardLayout(frame.width()); - } else if (mMode == TabListMode.STRIP) { + } else if (mMode == TabListMode.STRIP /*|| mMode == TabListMode.LIST*/) { LinearLayoutManager layoutManager = - new LinearLayoutManager(activity, LinearLayoutManager.HORIZONTAL, false) { + new LinearLayoutManager(activity, + mMode == TabListMode.LIST + ? LinearLayoutManager.VERTICAL + : LinearLayoutManager.HORIZONTAL, + false) { @Override public void onLayoutCompleted(RecyclerView.State state) { super.onLayoutCompleted(state); @@ -684,7 +705,7 @@ public class TabListCoordinator implements PriceWelcomeMessageProvider, DestroyO public void addTabListItemSizeChangedObserver(TabListItemSizeChangedObserver observer) { mTabListItemSizeChangedObserverList.addObserver(observer); Size size = mMediator.getDefaultGridCardSize(); - assert size != null; + if (size == null) return; observer.onSizeChanged(mMediator.getCurrentSpanCount(), size); } @@ -719,7 +740,7 @@ public class TabListCoordinator implements PriceWelcomeMessageProvider, DestroyO Size getThumbnailSize() { Size size = mMediator.getDefaultGridCardSize(); - assert size != null; + if (size == null) return new Size(0, 0); return TabUtils.deriveThumbnailSize(size, mActivity); } @@ -773,7 +794,7 @@ public class TabListCoordinator implements PriceWelcomeMessageProvider, DestroyO } private void configureRecyclerViewTouchHelpers() { - boolean modeAllowsDragAndDrop = mMode == TabListMode.GRID; + boolean modeAllowsDragAndDrop = mMode == TabListMode.GRID || mMode == TabListMode.LIST; boolean actionStateAllowsDragAndDrop = mTabActionState != TabActionState.SELECTABLE; if (mAllowDragAndDrop && modeAllowsDragAndDrop && actionStateAllowsDragAndDrop) { if (mOnBeforeItemTouchHelperItemTouchListener == null diff --git a/chrome/android/features/tab_ui/java/src/org/chromium/chrome/browser/tasks/tab_management/TabListEditorCoordinator.java b/chrome/android/features/tab_ui/java/src/org/chromium/chrome/browser/tasks/tab_management/TabListEditorCoordinator.java --- a/chrome/android/features/tab_ui/java/src/org/chromium/chrome/browser/tasks/tab_management/TabListEditorCoordinator.java +++ b/chrome/android/features/tab_ui/java/src/org/chromium/chrome/browser/tasks/tab_management/TabListEditorCoordinator.java @@ -421,7 +421,7 @@ public class TabListEditorCoordinator { mSnackbarManager = snackbarManager; mTabActionState = initialTabActionState; mTabContentManager = tabContentManager; - assert mode == TabListCoordinator.TabListMode.GRID; + assert mode == TabListCoordinator.TabListMode.GRID || mode == TabListCoordinator.TabListMode.LIST; mGridCardOnClickListenerProvider = gridCardOnClickListenerProvider; mModalDialogManager = modalDialogManager; mEdgeToEdgeSupplier = edgeToEdgeSupplier; diff --git a/chrome/android/features/tab_ui/java/src/org/chromium/chrome/browser/tasks/tab_management/TabListItemAnimator.java b/chrome/android/features/tab_ui/java/src/org/chromium/chrome/browser/tasks/tab_management/TabListItemAnimator.java --- a/chrome/android/features/tab_ui/java/src/org/chromium/chrome/browser/tasks/tab_management/TabListItemAnimator.java +++ b/chrome/android/features/tab_ui/java/src/org/chromium/chrome/browser/tasks/tab_management/TabListItemAnimator.java @@ -435,7 +435,7 @@ public class TabListItemAnimator extends SimpleItemAnimator { } Animator animator = null; - if (!shouldUseShrinkCloseAnimation(holder)) { + if (TabUiFeatureUtilities.shouldUseListMode() || !shouldUseShrinkCloseAnimation(holder)) { animator = buildGenericRemoveAnimator(holder); } else { animator = buildTabRemoveAnimator(holder); diff --git a/chrome/android/features/tab_ui/java/src/org/chromium/chrome/browser/tasks/tab_management/TabListMediator.java b/chrome/android/features/tab_ui/java/src/org/chromium/chrome/browser/tasks/tab_management/TabListMediator.java --- a/chrome/android/features/tab_ui/java/src/org/chromium/chrome/browser/tasks/tab_management/TabListMediator.java +++ b/chrome/android/features/tab_ui/java/src/org/chromium/chrome/browser/tasks/tab_management/TabListMediator.java @@ -2622,8 +2622,26 @@ class TabListMediator implements TabListNotificationHandler { private void updateFaviconForTab( PropertyModel model, Tab tab, @Nullable Bitmap icon, @Nullable GURL iconUrl) { if (mActionsOnAllRelatedTabs && isTabInTabGroup(tab)) { - model.set(TabProperties.FAVICON_FETCHER, null); - return; + List relatedTabList = getRelatedTabsForId(tab.getId()); + if (mMode != TabListMode.LIST) { + model.set(TabProperties.FAVICON_FETCHER, null); + return; + } else if (mMode == TabListMode.LIST && relatedTabList.size() > 1) { + // The order of the url list matches the multi-thumbnail. + List urls = new ArrayList<>(); + urls.add(tab.getUrl()); + for (int i = 0; urls.size() < 4 && i < relatedTabList.size(); i++) { + if (tab.getId() == relatedTabList.get(i).getId()) continue; + urls.add(relatedTabList.get(i).getUrl()); + } + + // For tab group card in list tab switcher, the favicon is the composed favicon. + model.set( + TabProperties.FAVICON_FETCHER, + mTabListFaviconProvider.getComposedFaviconImageFetcher( + urls, tab.isIncognito())); + return; + } } if (!mTabListFaviconProvider.isInitialized()) { return; diff --git a/chrome/android/features/tab_ui/java/src/org/chromium/chrome/browser/tasks/tab_management/TabListView.java b/chrome/android/features/tab_ui/java/src/org/chromium/chrome/browser/tasks/tab_management/TabListView.java new file mode 100644 --- /dev/null +++ b/chrome/android/features/tab_ui/java/src/org/chromium/chrome/browser/tasks/tab_management/TabListView.java @@ -0,0 +1,123 @@ +// 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.tasks.tab_management; + +import android.content.Context; +import android.graphics.Bitmap; +import android.graphics.BitmapFactory; +import android.graphics.drawable.Drawable; +import android.graphics.drawable.InsetDrawable; +import android.util.AttributeSet; +import android.view.View; +import android.view.accessibility.AccessibilityNodeInfo; +import android.widget.ImageView; + +import androidx.appcompat.content.res.AppCompatResources; +import androidx.vectordrawable.graphics.drawable.AnimatedVectorDrawableCompat; + +import org.chromium.build.annotations.NullMarked; +import org.chromium.chrome.browser.tasks.tab_management.TabProperties.TabActionState; +import org.chromium.chrome.tab_ui.R; +import org.chromium.components.browser_ui.widget.selectable_list.SelectableItemViewBase; + +// TODO(crbug.com/339038505): De-dupe logic in TabGridView. +/** Holds the view for a tab list. */ +@NullMarked +public class TabListView extends SelectableItemViewBase { + private @TabActionState int mTabActionState = TabActionState.UNSET; + private ImageView mActionButton; + + public TabListView(Context context, AttributeSet attrs) { + super(context, attrs); + setSelectionOnLongClick(false); + } + + @Override + protected void onFinishInflate() { + super.onFinishInflate(); + mActionButton = findViewById(R.id.end_button); + } + + void setTabActionState(@TabActionState int tabActionState) { + if (mTabActionState == tabActionState) return; + + mTabActionState = tabActionState; + int accessibilityMode = IMPORTANT_FOR_ACCESSIBILITY_YES; + if (mTabActionState == TabActionState.CLOSABLE) { + setTabActionButtonCloseDrawable(); + } else if (mTabActionState == TabActionState.SELECTABLE) { + accessibilityMode = IMPORTANT_FOR_ACCESSIBILITY_NO; + setTabActionButtonSelectionDrawable(); + } + + mActionButton.setImportantForAccessibility(accessibilityMode); + } + + private void setTabActionButtonCloseDrawable() { + assert mTabActionState != TabActionState.UNSET; + var resources = getResources(); + + mActionButton.setVisibility(View.VISIBLE); + int closeButtonSize = (int) resources.getDimension(R.dimen.tab_grid_close_button_size); + Bitmap bitmap = BitmapFactory.decodeResource(resources, R.drawable.btn_close); + Bitmap.createScaledBitmap(bitmap, closeButtonSize, closeButtonSize, true); + mActionButton.setImageBitmap(bitmap); + mActionButton.setBackground(null); + } + + private void setTabActionButtonSelectionDrawable() { + assert mTabActionState != TabActionState.UNSET; + var resources = getResources(); + + Drawable selectionListIcon = + AppCompatResources.getDrawable( + getContext(), R.drawable.tab_grid_selection_list_icon); + mActionButton.setVisibility(View.VISIBLE); + int lateralInset = + resources.getDimensionPixelSize( + R.dimen.selection_tab_list_toggle_button_lateral_inset); + int verticalInset = + resources.getDimensionPixelSize( + R.dimen.selection_tab_list_toggle_button_vertical_inset); + InsetDrawable drawable = + new InsetDrawable( + selectionListIcon, + lateralInset, + verticalInset, + lateralInset, + verticalInset); + mActionButton.setBackground(drawable); + findViewById(R.id.start_icon).setBackground(null); + mActionButton + .getBackground() + .setLevel(resources.getInteger(R.integer.list_item_level_default)); + mActionButton.setImageDrawable( + AnimatedVectorDrawableCompat.create( + getContext(), R.drawable.ic_check_googblue_20dp_animated)); + } + + // SelectableItemViewBase implementation. + + @Override + protected void updateView(boolean animate) {} + + @Override + protected void handleNonSelectionClick() {} + + // TODO(crbug.com/339038201): Consider capturing click events and discarding them while not in + // selection mode. + + // View implementation. + + @Override + public void onInitializeAccessibilityNodeInfo(AccessibilityNodeInfo info) { + super.onInitializeAccessibilityNodeInfo(info); + + if (mTabActionState == TabActionState.SELECTABLE) { + info.setCheckable(true); + info.setChecked(isChecked()); + } + } +} diff --git a/chrome/android/features/tab_ui/java/src/org/chromium/chrome/browser/tasks/tab_management/TabListViewBinder.java b/chrome/android/features/tab_ui/java/src/org/chromium/chrome/browser/tasks/tab_management/TabListViewBinder.java new file mode 100644 --- /dev/null +++ b/chrome/android/features/tab_ui/java/src/org/chromium/chrome/browser/tasks/tab_management/TabListViewBinder.java @@ -0,0 +1,353 @@ +// Copyright 2019 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.tasks.tab_management; + +import android.content.Context; +import android.content.res.ColorStateList; +import android.content.res.Resources; +import android.graphics.Bitmap; +import android.graphics.BitmapFactory; +import android.graphics.drawable.Drawable; +import android.graphics.drawable.InsetDrawable; +import android.view.LayoutInflater; +import android.view.View; +import android.view.ViewGroup; +import android.widget.FrameLayout; +import android.widget.ImageView; +import android.widget.TextView; + +import androidx.annotation.ColorInt; +import androidx.annotation.DrawableRes; +import androidx.annotation.Nullable; +import androidx.appcompat.content.res.AppCompatResources; +import androidx.core.content.res.ResourcesCompat; +import androidx.core.graphics.drawable.DrawableCompat; +import androidx.core.view.ViewCompat; +import androidx.core.widget.ImageViewCompat; +import androidx.vectordrawable.graphics.drawable.AnimatedVectorDrawableCompat; + +import org.chromium.chrome.browser.tab_ui.TabListFaviconProvider; +import org.chromium.chrome.browser.tab_ui.TabCardThemeUtil; +import org.chromium.chrome.browser.tasks.tab_management.TabActionListener; +import org.chromium.chrome.browser.tasks.tab_management.TabActionButtonData.TabActionButtonType; +import org.chromium.chrome.browser.tasks.tab_management.TabProperties.TabActionState; +import org.chromium.chrome.tab_ui.R; +import org.chromium.components.browser_ui.styles.SemanticColorUtils; +import org.chromium.ui.modelutil.PropertyKey; +import org.chromium.ui.modelutil.PropertyModel; +import org.chromium.ui.widget.ViewLookupCachingFrameLayout; + +/** {@link org.chromium.ui.modelutil.SimpleRecyclerViewMcp.ViewBinder} for tab List. */ +class TabListViewBinder { + /** + * Main entrypoint for binding TabListView + * + * @param view The view to bind to. + * @param model The model to bind. + */ + public static void bindTab( + PropertyModel model, ViewGroup view, @Nullable PropertyKey propertyKey) { + assert view instanceof ViewLookupCachingFrameLayout; + @TabActionState Integer tabActionState = model.get(TabProperties.TAB_ACTION_STATE); + if (tabActionState == null) { + assert false : "TAB_ACTION_STATE must be set before initial bindTab call."; + return; + } + + ((TabListView) view).setTabActionState(tabActionState); + bindListTab(model, (ViewLookupCachingFrameLayout) view, propertyKey); + if (tabActionState == TabActionState.CLOSABLE) { + bindClosableListTab(model, (ViewLookupCachingFrameLayout) view, propertyKey); + } else if (tabActionState == TabActionState.SELECTABLE) { + bindSelectableListTab(model, (ViewLookupCachingFrameLayout) view, propertyKey); + } else { + assert false : "Unsupported TabActionState provided to bindTab."; + } + } + + /** + * Handles any cleanup for recycled views that might be expensive to keep around in the pool. + * + * @param model The property model to possibly cleanup. + * @param view The view to possibly cleanup. + */ + public static void onViewRecycled(PropertyModel model, View view) { + if (view instanceof TabListView tabListView) { + ImageView faviconView = tabListView.findViewById(R.id.start_icon); + faviconView.setImageDrawable(null); + + FrameLayout colorContainer = tabListView.findViewById(R.id.after_title_container); + TabCardViewBinderUtils.detachTabGroupColorView(colorContainer); + + FrameLayout labelContainer = + tabListView.findViewById(R.id.before_description_container); + labelContainer.removeAllViews(); + } + } + + // TODO(crbug.com/40107066): Merge with TabGridViewBinder for shared properties. + private static void bindListTab( + PropertyModel model, ViewGroup view, @Nullable PropertyKey propertyKey) { + if (TabProperties.TITLE == propertyKey) { + String title = model.get(TabProperties.TITLE); + ((TextView) view.findViewById(R.id.title)).setText(title); + } else if (TabProperties.FAVICON_FETCHER == propertyKey) { + final TabListFaviconProvider.TabFaviconFetcher fetcher = + model.get(TabProperties.FAVICON_FETCHER); + if (fetcher == null) { + setFavicon(view, null); + return; + } + fetcher.fetch( + tabFavicon -> { + if (fetcher != model.get(TabProperties.FAVICON_FETCHER)) return; + + setFavicon(view, tabFavicon.getDefaultDrawable()); + }); + } else if (TabProperties.IS_SELECTED == propertyKey) { + boolean isSelected = model.get(TabProperties.IS_SELECTED); + boolean isIncognito = model.get(TabProperties.IS_INCOGNITO); + updateColors(view, isIncognito); + + @DrawableRes + int selectedTabBackground = + isIncognito + ? R.drawable.selected_tab_background_incognito + : R.drawable.selected_tab_background; + Resources res = view.getResources(); + Resources.Theme theme = view.getContext().getTheme(); + Drawable drawable = + new InsetDrawable( + ResourcesCompat.getDrawable(res, selectedTabBackground, theme), + (int) res.getDimension(R.dimen.tab_list_selected_inset_low_end)); + view.setForeground(isSelected ? drawable : null); + } else if (TabProperties.URL_DOMAIN == propertyKey) { + String domain = model.get(TabProperties.URL_DOMAIN); + ((TextView) view.findViewById(R.id.description)).setText(domain); + } else if (TabProperties.TAB_GROUP_COLOR_VIEW_PROVIDER == propertyKey) { + @Nullable + TabGroupColorViewProvider provider = + model.get(TabProperties.TAB_GROUP_COLOR_VIEW_PROVIDER); + FrameLayout container = view.findViewById(R.id.after_title_container); + TabCardViewBinderUtils.updateTabGroupColorView(container, provider); + } else if (TabProperties.TAB_ACTION_BUTTON_DATA == propertyKey) { + @Nullable TabActionButtonData data = model.get(TabProperties.TAB_ACTION_BUTTON_DATA); + @Nullable + TabActionListener tabActionListener = data == null ? null : data.tabActionListener; + ImageView actionButton = view.findViewById(R.id.end_button); + TabGridViewBinder.setNullableClickListener(tabActionListener, actionButton, model); + + if (data == null) return; + + Resources res = view.getResources(); + if (data.type == TabActionButtonType.OVERFLOW) { + actionButton.setImageDrawable( + ResourcesCompat.getDrawable( + res, R.drawable.ic_more_vert_24dp, view.getContext().getTheme())); + } else if (data.type == TabActionButtonType.CLOSE) { + int closeButtonSize = (int) res.getDimension(R.dimen.tab_grid_close_button_size); + Bitmap bitmap = BitmapFactory.decodeResource(res, R.drawable.btn_close); + Bitmap.createScaledBitmap(bitmap, closeButtonSize, closeButtonSize, true); + actionButton.setImageBitmap(bitmap); + } else if (data.type == TabActionButtonType.SELECT) { + // Intentional no-op. Handled as part of setTabActionState. + } else { + assert false : "Not reached"; + } + } else if (TabProperties.TAB_CLICK_LISTENER == propertyKey) { + TabGridViewBinder.setNullableClickListener( + model.get(TabProperties.TAB_CLICK_LISTENER), view, model); + } else if (TabProperties.TAB_LONG_CLICK_LISTENER == propertyKey) { + TabGridViewBinder.setNullableLongClickListener( + model.get(TabProperties.TAB_LONG_CLICK_LISTENER), view, model); + } else if (TabProperties.TAB_CARD_LABEL_DATA == propertyKey) { + // Ignore this data for tab card labels in selectable mode. + updateTabCardLabel(view, /* tabCardLabelData= */ null); + } + } + + /** + * Bind a closable tab to view. + * + * @param model The model to bind. + * @param view The view to bind to. + * @param propertyKey The property that changed. + */ + private static void bindClosableListTab( + PropertyModel model, ViewGroup view, @Nullable PropertyKey propertyKey) { + bindListTab(model, view, propertyKey); + + if (TabProperties.IS_SELECTED == propertyKey + || TabProperties.TAB_ACTION_BUTTON_DATA == propertyKey) { + ImageView closeButton = view.findViewById(R.id.end_button); + ImageViewCompat.setImageTintList( + closeButton, + TabCardThemeUtil.getActionButtonTintList( + view.getContext(), + model.get(TabProperties.IS_INCOGNITO), + /* isSelected= */ false, + /* color */ null)); + } else if (TabProperties.ACTION_BUTTON_DESCRIPTION_TEXT_RESOLVER == propertyKey) { + TextResolver actionButtonDescriptionTextResolver = + model.get(TabProperties.ACTION_BUTTON_DESCRIPTION_TEXT_RESOLVER); + CharSequence actionButtonDescriptionString = + TabCardViewBinderUtils.resolveNullSafe( + actionButtonDescriptionTextResolver, view.getContext()); + view.findViewById(R.id.end_button).setContentDescription(actionButtonDescriptionString); + } else if (TabProperties.TAB_CARD_LABEL_DATA == propertyKey) { + updateTabCardLabel(view, model.get(TabProperties.TAB_CARD_LABEL_DATA)); + } + } + + /** + * Bind color updates. + * + * @param view The root view of the item (either Selectable/ClosableTabListView). + * @param isIncognito Whether the model is in incognito mode. + * @param isSelected Whether the item is selected. + */ + private static void updateColors(ViewGroup view, boolean isIncognito) { + // TODO(crbug.com/40272756): isSelected is ignored as the selected row is only outlined not + // colored so it should use the unselected color. This will be addressed in a fixit. + + // Shared by both classes, from tab_list_card_item. + View contentView = view.findViewById(R.id.content_view); + contentView.getBackground().mutate(); + final @ColorInt int backgroundColor = + TabCardThemeUtil.getCardViewBackgroundColor( + view.getContext(), isIncognito, /* isSelected= */ false, /*colorId*/null); + ViewCompat.setBackgroundTintList(contentView, ColorStateList.valueOf(backgroundColor)); + + final @ColorInt int textColor = + TabCardThemeUtil.getTitleTextColor( + view.getContext(), isIncognito, /* isSelected= */ false, /*colorId*/null); + TextView titleView = view.findViewById(R.id.title); + TextView descriptionView = view.findViewById(R.id.description); + titleView.setTextColor(textColor); + descriptionView.setTextColor(textColor); + + ImageView faviconView = view.findViewById(R.id.start_icon); + if (faviconView.getBackground() == null) { + faviconView.setBackgroundResource(R.drawable.list_item_icon_modern_bg); + } + faviconView.getBackground().mutate(); + final @ColorInt int faviconBackgroundColor = + TabCardThemeUtil.getMiniThumbnailPlaceholderColor( + view.getContext(), isIncognito, /* isSelected= */ false, /*colorId*/null); + ViewCompat.setBackgroundTintList( + faviconView, ColorStateList.valueOf(faviconBackgroundColor)); + } + + /** + * Bind a selectable tab to view. + * + * @param model The model to bind. + * @param view The view to bind to. + * @param propertyKey The property that changed. + */ + private static void bindSelectableListTab( + PropertyModel model, ViewGroup view, @Nullable PropertyKey propertyKey) { + bindListTab(model, view, propertyKey); + + final int tabId = model.get(TabProperties.TAB_ID); + TabListView tabListView = (TabListView) view; + if (TabProperties.TAB_SELECTION_DELEGATE == propertyKey) { + tabListView.setSelectionDelegate(model.get(TabProperties.TAB_SELECTION_DELEGATE)); + tabListView.setItem(TabListEditorItemSelectionId.createTabId(tabId)); + } else if (TabProperties.IS_SELECTED == propertyKey + || TabProperties.TAB_ACTION_BUTTON_DATA == propertyKey) { + boolean isSelected = model.get(TabProperties.IS_SELECTED); + boolean isIncognito = model.get(TabProperties.IS_INCOGNITO); + ImageView actionButton = view.findViewById(R.id.end_button); + + Context context = view.getContext(); + Resources res = view.getResources(); + int level = TabCardViewBinderUtils.getCheckmarkLevel(res, isSelected); + ColorStateList backgroundColorStateList = + getBackgroundColorStateList(context, isSelected, isIncognito); + + var background = actionButton.getBackground(); + background.setLevel(level); + DrawableCompat.setTintList(background.mutate(), backgroundColorStateList); + + // The check should be invisible if not selected. + actionButton.getDrawable().setAlpha(isSelected ? 255 : 0); + ImageViewCompat.setImageTintList( + actionButton, + isSelected ? getCheckedDrawableColorStateList(context, isIncognito) : null); + if (isSelected) ((AnimatedVectorDrawableCompat) actionButton.getDrawable()).start(); + } + } + + private static void setFavicon(View view, Drawable favicon) { + ImageView faviconView = view.findViewById(R.id.start_icon); + faviconView.setImageDrawable(favicon); + } + + private static ColorStateList getCheckedDrawableColorStateList( + Context context, boolean isIncognito) { + return ColorStateList.valueOf( + isIncognito + ? context.getColor(R.color.default_icon_color_dark) + : SemanticColorUtils.getDefaultIconColorInverse(context)); + } + + private static ColorStateList getBackgroundColorStateList( + Context context, boolean isSelected, boolean isIncognito) { + if (isSelected) { + return ColorStateList.valueOf( + isIncognito + ? context.getColor(R.color.baseline_primary_80) + : SemanticColorUtils.getDefaultControlColorActive(context)); + } else { + return AppCompatResources.getColorStateList( + context, + isIncognito + ? R.color.default_icon_color_light + : R.color.default_icon_color_tint_list); + } + } + + private static void updateTabCardLabel( + ViewGroup view, @Nullable TabCardLabelData tabCardLabelData) { + @Nullable + TabCardLabelView labelView = getOrSetupTabCardLabelView(view, tabCardLabelData == null); + if (labelView != null) { + labelView.setData(tabCardLabelData); + } + } + + private static @Nullable TabCardLabelView getOrSetupTabCardLabelView( + ViewGroup view, boolean isDataNull) { + FrameLayout labelContainer = view.findViewById(R.id.before_description_container); + if (labelContainer.getChildCount() > 0) { + return (TabCardLabelView) labelContainer.getChildAt(0); + } else if (isDataNull) { + // Avoid eagerly creating the view in the event the data is null and it isn't already + // created. + return null; + } + Context context = labelContainer.getContext(); + TabCardLabelView labelView = + (TabCardLabelView) + LayoutInflater.from(context) + .inflate(R.layout.tab_card_label_layout, labelContainer, false); + labelContainer.addView(labelView); + + Resources res = context.getResources(); + int marginEnd = res.getDimensionPixelSize(R.dimen.tab_card_label_list_margin_end); + FrameLayout.LayoutParams params = (FrameLayout.LayoutParams) labelView.getLayoutParams(); + params.setMarginEnd(marginEnd); + labelView.setLayoutParams(params); + + // TODO(crbug.com/362306803): This is technically supposed to have elevation. However, + // propagating clipToPadding=false and clipChildren=false all the way from + // title_and_description_layout up to tab_list_card_item to make it look right is not worth + // the complexity/risk of impacting other list UI when this UI is likely to be deprecated in + // 2025. + labelView.setElevation(0); + return labelView; + } +} diff --git a/chrome/android/features/tab_ui/java/src/org/chromium/chrome/browser/tasks/tab_management/TabSwitcherPaneBase.java b/chrome/android/features/tab_ui/java/src/org/chromium/chrome/browser/tasks/tab_management/TabSwitcherPaneBase.java --- a/chrome/android/features/tab_ui/java/src/org/chromium/chrome/browser/tasks/tab_management/TabSwitcherPaneBase.java +++ b/chrome/android/features/tab_ui/java/src/org/chromium/chrome/browser/tasks/tab_management/TabSwitcherPaneBase.java @@ -305,7 +305,7 @@ public abstract class TabSwitcherPaneBase extends PaneBase || isFullSpaceModeOnAndroidXr; @Nullable Tab tab = getCurrentTab(); - if (tab == null || SysUtils.isLowEndDevice()) { + if (getTabListMode() == TabListMode.LIST || tab == null || SysUtils.isLowEndDevice()) { return FadeHubLayoutAnimationFactory.createFadeInAnimatorProvider( hubContainerView, HUB_LAYOUT_FADE_DURATION_MS, mOnToolbarAlphaChange); } else if (isFullSpaceModeOnAndroidXr && getTabListMode() == TabListMode.GRID) { @@ -334,7 +334,7 @@ public abstract class TabSwitcherPaneBase extends PaneBase HubContainerView hubContainerView) { assert !DeviceFormFactor.isNonMultiDisplayContextOnTablet(hubContainerView.getContext()); Tab tab = getCurrentTab(); - if (tab == null || SysUtils.isLowEndDevice()) { + if (getTabListMode() == TabListMode.LIST || tab == null || SysUtils.isLowEndDevice()) { return FadeHubLayoutAnimationFactory.createFadeOutAnimatorProvider( hubContainerView, HUB_LAYOUT_FADE_DURATION_MS, mOnToolbarAlphaChange); } diff --git a/chrome/android/features/tab_ui/java/src/org/chromium/chrome/browser/tasks/tab_management/TabSwitcherPaneCoordinatorFactory.java b/chrome/android/features/tab_ui/java/src/org/chromium/chrome/browser/tasks/tab_management/TabSwitcherPaneCoordinatorFactory.java --- a/chrome/android/features/tab_ui/java/src/org/chromium/chrome/browser/tasks/tab_management/TabSwitcherPaneCoordinatorFactory.java +++ b/chrome/android/features/tab_ui/java/src/org/chromium/chrome/browser/tasks/tab_management/TabSwitcherPaneCoordinatorFactory.java @@ -150,7 +150,10 @@ public class TabSwitcherPaneCoordinatorFactory { mModalDialogManager = modalDialogManager; mBottomSheetController = bottomSheetController; mDataSharingTabManager = dataSharingTabManager; - mMode = TabListCoordinator.TabListMode.GRID; + mMode = + TabUiFeatureUtilities.shouldUseListMode() + ? TabListCoordinator.TabListMode.LIST + : TabListCoordinator.TabListMode.GRID; mBackPressManager = backPressManager; mDesktopWindowStateManager = desktopWindowStateManager; mEdgeToEdgeSupplier = edgeToEdgeSupplier; diff --git a/chrome/android/features/tab_ui/java/src/org/chromium/chrome/browser/tasks/tab_management/TabUiFeatureUtilities.java b/chrome/android/features/tab_ui/java/src/org/chromium/chrome/browser/tasks/tab_management/TabUiFeatureUtilities.java --- a/chrome/android/features/tab_ui/java/src/org/chromium/chrome/browser/tasks/tab_management/TabUiFeatureUtilities.java +++ b/chrome/android/features/tab_ui/java/src/org/chromium/chrome/browser/tasks/tab_management/TabUiFeatureUtilities.java @@ -7,6 +7,8 @@ package org.chromium.chrome.browser.tasks.tab_management; import android.os.Build; import org.chromium.build.annotations.NullMarked; +import org.chromium.chrome.browser.flags.cromite.sTabSwitcherListMode; +import org.chromium.chrome.browser.flags.ChromeFeatureList; import java.util.Locale; import java.util.Set; @@ -16,6 +18,11 @@ import java.util.Set; public class TabUiFeatureUtilities { private static final Set TAB_TEARING_OEM_ALLOWLIST = Set.of("samsung"); + /** Returns whether the Grid Tab Switcher UI should use list mode. */ + public static boolean shouldUseListMode() { + return sTabSwitcherListMode.getInstance().isEnabled(); + } + /** Returns whether device OEM is allow-listed for tab tearing */ public static boolean doesOemSupportDragToCreateInstance() { return TAB_TEARING_OEM_ALLOWLIST.contains(Build.MANUFACTURER.toLowerCase(Locale.US)); diff --git a/chrome/android/features/tab_ui/java/src/org/chromium/chrome/browser/tasks/tab_management/pinned_tabs_strip/PinnedTabStripCoordinator.java b/chrome/android/features/tab_ui/java/src/org/chromium/chrome/browser/tasks/tab_management/pinned_tabs_strip/PinnedTabStripCoordinator.java --- a/chrome/android/features/tab_ui/java/src/org/chromium/chrome/browser/tasks/tab_management/pinned_tabs_strip/PinnedTabStripCoordinator.java +++ b/chrome/android/features/tab_ui/java/src/org/chromium/chrome/browser/tasks/tab_management/pinned_tabs_strip/PinnedTabStripCoordinator.java @@ -32,6 +32,7 @@ import org.chromium.chrome.browser.tasks.tab_management.TabListCoordinator; import org.chromium.chrome.browser.tasks.tab_management.TabListModel; import org.chromium.chrome.browser.tasks.tab_management.TabListRecyclerView; import org.chromium.chrome.browser.tasks.tab_management.TabProperties.UiType; +import org.chromium.chrome.browser.tasks.tab_management.TabUiFeatureUtilities; import org.chromium.components.browser_ui.bottomsheet.BottomSheetController; import org.chromium.ui.modaldialog.ModalDialogManager; import org.chromium.ui.modelutil.PropertyModel; @@ -106,6 +107,10 @@ public class PinnedTabStripCoordinator { RecyclerView tabGridListRecyclerView = tabListCoordinator.getContainerView(); TabListModel tabListModel = tabListCoordinator.getTabListModel(); + if (TabUiFeatureUtilities.shouldUseListMode()) { + mMediator = null; + return; + } mMediator = createMediator( activity, @@ -149,11 +154,12 @@ public class PinnedTabStripCoordinator { /** Called when the pinned tabs strip is scrolled. */ public void onScrolled() { - mMediator.onScrolled(); + if (mMediator != null) mMediator.onScrolled(); } /** Returns whether the pinned tabs strip is currently visible. */ public boolean isPinnedTabsBarVisible() { + if (mMediator == null) return false; return mMediator.isPinnedTabsBarVisible(); } @@ -197,6 +203,6 @@ public class PinnedTabStripCoordinator { } public void destroy() { - mMediator.destroy(); + if (mMediator != null) mMediator.destroy(); } } diff --git a/chrome/android/features/tab_ui/tab_management_java_sources.gni b/chrome/android/features/tab_ui/tab_management_java_sources.gni --- a/chrome/android/features/tab_ui/tab_management_java_sources.gni +++ b/chrome/android/features/tab_ui/tab_management_java_sources.gni @@ -179,6 +179,8 @@ internal_tab_management_java_sources = [ "//chrome/android/features/tab_ui/java/src/org/chromium/chrome/browser/tasks/tab_management/TabListNotificationHandler.java", "//chrome/android/features/tab_ui/java/src/org/chromium/chrome/browser/tasks/tab_management/TabListOnScrollListener.java", "//chrome/android/features/tab_ui/java/src/org/chromium/chrome/browser/tasks/tab_management/TabListRecyclerView.java", + "//chrome/android/features/tab_ui/java/src/org/chromium/chrome/browser/tasks/tab_management/TabListView.java", + "//chrome/android/features/tab_ui/java/src/org/chromium/chrome/browser/tasks/tab_management/TabListViewBinder.java", "//chrome/android/features/tab_ui/java/src/org/chromium/chrome/browser/tasks/tab_management/TabManagementDelegateImpl.java", "//chrome/android/features/tab_ui/java/src/org/chromium/chrome/browser/tasks/tab_management/TabManagementDelegateProvider.java", "//chrome/android/features/tab_ui/java/src/org/chromium/chrome/browser/tasks/tab_management/TabObjectLabeller.java", diff --git a/chrome/android/java/res/xml/privacy_preferences.xml b/chrome/android/java/res/xml/privacy_preferences.xml --- a/chrome/android/java/res/xml/privacy_preferences.xml +++ b/chrome/android/java/res/xml/privacy_preferences.xml @@ -82,6 +82,12 @@ found in the LICENSE file. android:summary="@string/allow_external_intent_requests_summary" app:featureName="AllowExternalIntentRequests" app:needRestart="false" /> + diff --git a/chrome/android/java/src/org/chromium/chrome/browser/app/ChromeActivity.java b/chrome/android/java/src/org/chromium/chrome/browser/app/ChromeActivity.java --- a/chrome/android/java/src/org/chromium/chrome/browser/app/ChromeActivity.java +++ b/chrome/android/java/src/org/chromium/chrome/browser/app/ChromeActivity.java @@ -199,6 +199,7 @@ import org.chromium.chrome.browser.tabmodel.TabModelSelectorTabObserver; import org.chromium.chrome.browser.tabmodel.TabModelUtils; import org.chromium.chrome.browser.task_manager.TaskManager; import org.chromium.chrome.browser.task_manager.TaskManagerFactory; +import org.chromium.chrome.browser.tasks.tab_management.TabUiFeatureUtilities; import org.chromium.chrome.browser.theme.ThemeModuleUtils; import org.chromium.chrome.browser.toolbar.ControlContainer; import org.chromium.chrome.browser.toolbar.ToolbarManager; @@ -700,8 +701,8 @@ public abstract class ChromeActivity extends AsyncInitializationActivity new TabContentManager( this, mBrowserControlsManagerSupplier.get(), - !TabUiFeatureUtilities.shouldUseListMode() - && !ContextUtils.getAppSharedPreferences().getBoolean(PrivacySettings.PREF_TABGRID_USE_ICONS, false), + !TabUiFeatureUtilities.shouldUseListMode() && + !ContextUtils.getAppSharedPreferences().getBoolean(PrivacySettings.PREF_TABGRID_USE_ICONS, false), tabModelSelector != null ? tabModelSelector::getTabById : null, TabWindowManagerSingleton.getInstance())); diff --git a/chrome/browser/android/favicon_helper.cc b/chrome/browser/android/favicon_helper.cc --- a/chrome/browser/android/favicon_helper.cc +++ b/chrome/browser/android/favicon_helper.cc @@ -45,12 +45,109 @@ using base::android::ConvertUTF8ToJavaString; using base::android::JavaRef; using base::android::ScopedJavaGlobalRef; using base::android::ScopedJavaLocalRef; +using JobFinishedCallback = base::OnceCallback; static int64_t JNI_FaviconHelper_Init(JNIEnv* env) { return reinterpret_cast(new FaviconHelper()); } -FaviconHelper::FaviconHelper() { +// This is used by the FaviconHelper::GetComposedFaviconImageInternal, and it is +// used to manage multiple FaviconService::GetRawFaviconForPageURL calls. The +// number of calls is the size of the urls_. The Job is destroyed after the +// number of calls have been reached, and the result_callback_ is finished. +class FaviconHelper::Job { + public: + Job(FaviconHelper* favicon_helper, + favicon::FaviconService* favicon_service, + std::vector urls, + int desire_size_in_pixel, + JobFinishedCallback job_finished_callback, + favicon_base::FaviconResultsCallback result_callback); + + Job(const Job&) = delete; + Job& operator=(const Job&) = delete; + + void Start(); + + private: + void OnFaviconAvailable(int favicon_index, + const favicon_base::FaviconRawBitmapResult& result); + raw_ptr favicon_helper_; + raw_ptr favicon_service_; + std::vector urls_; + int desire_size_in_pixel_; + JobFinishedCallback job_finished_callback_; + favicon_base::FaviconResultsCallback result_callback_; + int favicon_expected_count_; + std::vector favicon_raw_bitmap_results_; + int favicon_result_count_; + + base::WeakPtrFactory weak_ptr_factory_{this}; +}; + +FaviconHelper::Job::Job(FaviconHelper* favicon_helper, + favicon::FaviconService* favicon_service, + std::vector urls, + int desire_size_in_pixel, + JobFinishedCallback job_finished_callback, + favicon_base::FaviconResultsCallback result_callback) + : favicon_helper_(favicon_helper), + favicon_service_(favicon_service), + urls_(urls), + desire_size_in_pixel_(desire_size_in_pixel), + job_finished_callback_(std::move(job_finished_callback)), + result_callback_(std::move(result_callback)), + favicon_raw_bitmap_results_(4), + favicon_result_count_(0) { + favicon_expected_count_ = urls_.size(); +} + +void FaviconHelper::Job::Start() { + size_t urls_size = urls_.size(); + DCHECK(urls_size > 1 && urls_size <= 4); + if (urls_size <= 1 || urls_size > 4) { + return; + } + + for (size_t i = 0; i < urls_size; i++) { + favicon_base::FaviconRawBitmapCallback callback = + base::BindOnce(&FaviconHelper::Job::OnFaviconAvailable, + weak_ptr_factory_.GetWeakPtr(), i); + + favicon_helper_->GetLocalFaviconImageForURLInternal( + favicon_service_, urls_.at(i), desire_size_in_pixel_, + std::move(callback)); + } +} + +void FaviconHelper::Job::OnFaviconAvailable( + int favicon_index, + const favicon_base::FaviconRawBitmapResult& result) { + DCHECK(favicon_index >= 0 && favicon_index < 4); + + if (result.is_valid()) { + favicon_raw_bitmap_results_.at(favicon_index) = result; + favicon_result_count_++; + } else { + favicon_expected_count_--; + } + + if (favicon_result_count_ == favicon_expected_count_) { + size_t i = 0; + while (i < favicon_raw_bitmap_results_.size()) { + if (!favicon_raw_bitmap_results_[i].is_valid()) { + favicon_raw_bitmap_results_.erase(favicon_raw_bitmap_results_.begin() + + i); + continue; + } + i++; + } + std::move(result_callback_).Run(favicon_raw_bitmap_results_); + std::move(job_finished_callback_).Run(); + } +} + +FaviconHelper::FaviconHelper() : last_used_job_id_(0) { cancelable_task_tracker_ = std::make_unique(); } @@ -58,6 +155,67 @@ void FaviconHelper::Destroy(JNIEnv* env) { delete this; } +jboolean FaviconHelper::GetComposedFaviconImage( + JNIEnv* env, + Profile* profile, + const std::vector& gurls, + int32_t j_desired_size_in_pixel, + const JavaRef& + j_composed_favicon_image_callback) { + DCHECK(profile); + if (!profile) { + return false; + } + + favicon::FaviconService* favicon_service = + FaviconServiceFactory::GetForProfile(profile, + ServiceAccessType::EXPLICIT_ACCESS); + + DCHECK(favicon_service); + if (!favicon_service) { + return false; + } + + int desired_size_in_pixel = static_cast(j_desired_size_in_pixel); + + favicon_base::FaviconResultsCallback callback_runner = base::BindOnce( + &FaviconHelper::OnComposedFaviconBitmapResultsAvailable, + weak_ptr_factory_.GetWeakPtr(), + ScopedJavaGlobalRef(j_composed_favicon_image_callback), + desired_size_in_pixel); + + GetComposedFaviconImageInternal(favicon_service, gurls, + static_cast(j_desired_size_in_pixel), + std::move(callback_runner)); + + return true; +} + +void FaviconHelper::GetComposedFaviconImageInternal( + favicon::FaviconService* favicon_service, + std::vector urls, + int desired_size_in_pixel, + favicon_base::FaviconResultsCallback callback_runner) { + DCHECK(favicon_service); + + JobFinishedCallback job_finished_callback = + base::BindOnce(&FaviconHelper::OnJobFinished, + weak_ptr_factory_.GetWeakPtr(), ++last_used_job_id_); + + auto job = std::make_unique( + this, favicon_service, urls, desired_size_in_pixel, + std::move(job_finished_callback), std::move(callback_runner)); + + id_to_job_[last_used_job_id_] = std::move(job); + id_to_job_[last_used_job_id_]->Start(); +} + +void ::FaviconHelper::OnJobFinished(int job_id) { + DCHECK(id_to_job_.count(job_id)); + + id_to_job_.erase(job_id); +} + bool FaviconHelper::GetLocalFaviconImageForURL( JNIEnv* env, Profile* profile, @@ -143,6 +301,30 @@ bool FaviconHelper::GetForeignFaviconImageForURL( FaviconHelper::~FaviconHelper() = default; +// Return the index of |sizes| whose area is largest but not exceeds int type +// range. If all |sizes|'s area exceed int type range, return the first one. +size_t FaviconHelper::GetLargestSizeIndex(const std::vector& sizes) { + DCHECK(!sizes.empty()); + size_t ret = 0; + // Find the first element whose area doesn't exceed max value, then use it + // to compare with rest elements to find largest size index. + for (size_t i = 0; i < sizes.size(); ++i) { + base::CheckedNumeric checked_area = sizes[i].GetCheckedArea(); + if (checked_area.IsValid()) { + ret = i; + int largest_area = checked_area.ValueOrDie(); + for (i = ret + 1; i < sizes.size(); ++i) { + int area = sizes[i].GetCheckedArea().ValueOrDefault(-1); + if (largest_area < area) { + ret = i; + largest_area = area; + } + } + } + } + return ret; +} + void FaviconHelper::OnFaviconBitmapResultAvailable( const JavaRef& j_favicon_image_callback, const favicon_base::FaviconRawBitmapResult& result) { @@ -162,4 +344,34 @@ void FaviconHelper::OnFaviconBitmapResultAvailable( env, j_favicon_image_callback, j_favicon_bitmap, result.icon_url); } +void FaviconHelper::OnComposedFaviconBitmapResultsAvailable( + const JavaRef& j_favicon_image_callback, + const int desired_size_in_pixel, + const std::vector& results) { + JNIEnv* env = AttachCurrentThread(); + std::vector result_bitmaps; + std::vector icon_url_vector; + for (auto result : results) { + if (!result.is_valid()) { + continue; + } + icon_url_vector.push_back(result.icon_url); + SkBitmap favicon_bitmap = gfx::PNGCodec::Decode(*result.bitmap_data); + result_bitmaps.push_back(std::move(favicon_bitmap)); + } + ScopedJavaLocalRef j_favicon_bitmap; + if (!result_bitmaps.empty()) { + std::unique_ptr composed_bitmap = + compose_bitmaps_helper::ComposeBitmaps(std::move(result_bitmaps), + desired_size_in_pixel); + if (composed_bitmap && !composed_bitmap->isNull()) { + j_favicon_bitmap = gfx::ConvertToJavaBitmap(*composed_bitmap); + } + } + + // Call java side OnComposedFaviconBitmapResultsAvailable method. + Java_ComposedFaviconImageCallback_onComposedFaviconAvailable( + env, j_favicon_image_callback, j_favicon_bitmap, icon_url_vector); +} + DEFINE_JNI(FaviconHelper) diff --git a/chrome/browser/android/favicon_helper.h b/chrome/browser/android/favicon_helper.h --- a/chrome/browser/android/favicon_helper.h +++ b/chrome/browser/android/favicon_helper.h @@ -26,6 +26,12 @@ class FaviconHelper { FaviconHelper(const FaviconHelper&) = delete; FaviconHelper& operator=(const FaviconHelper&) = delete; + jboolean GetComposedFaviconImage( + JNIEnv* env, + Profile* profile, + const std::vector& gurls, + int32_t j_desired_size_in_pixel, + const base::android::JavaRef& j_favicon_image_callback); bool GetLocalFaviconImageForURL( JNIEnv* env, Profile* profile, @@ -44,11 +50,20 @@ class FaviconHelper { GURL url, int desired_size_in_pixel, favicon_base::FaviconRawBitmapCallback callback_runner); + void GetComposedFaviconImageInternal( + favicon::FaviconService* favicon_service, + std::vector urls, + int desired_size_in_pixel, + favicon_base::FaviconResultsCallback callback_runner); void OnJobFinished(int job_id); private: virtual ~FaviconHelper(); + class Job; + + static size_t GetLargestSizeIndex(const std::vector& sizes); + // This function is expected to be bound to a WeakPtr, so that // it won't be run if the FaviconHelper is deleted and // |j_favicon_image_callback| isn't executed in that case. @@ -56,8 +71,16 @@ class FaviconHelper { const base::android::JavaRef& j_favicon_image_callback, const favicon_base::FaviconRawBitmapResult& result); + void OnComposedFaviconBitmapResultsAvailable( + const base::android::JavaRef& j_favicon_image_callback, + const int desired_size_in_pixel, + const std::vector& result); + std::unique_ptr cancelable_task_tracker_; + std::map> id_to_job_; + int last_used_job_id_; + base::WeakPtrFactory weak_ptr_factory_{this}; }; diff --git a/chrome/browser/flags/android/java/src/org/chromium/chrome/browser/flags/cromite/sTabSwitcherListMode.java b/chrome/browser/flags/android/java/src/org/chromium/chrome/browser/flags/cromite/sTabSwitcherListMode.java new file mode 100644 --- /dev/null +++ b/chrome/browser/flags/android/java/src/org/chromium/chrome/browser/flags/cromite/sTabSwitcherListMode.java @@ -0,0 +1,33 @@ +/* + This file is part of Cromite. + + Cromite is free software: you can redistribute it and/or modify + it under the terms of the GNU General Public License as published by + the Free Software Foundation, either version 3 of the License, or + (at your option) any later version. + + Cromite is distributed in the hope that it will be useful, + but WITHOUT ANY WARRANTY; without even the implied warranty of + MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + GNU General Public License for more details. + + You should have received a copy of the GNU General Public License + along with Cromite. If not, see . +*/ + +package org.chromium.chrome.browser.flags.cromite; + +import org.chromium.components.cached_flags.CachedFlag; +import org.chromium.chrome.browser.flags.ChromeFeatureMap; + +public class sTabSwitcherListMode { + private static final CachedFlag sInstance = + new CachedFlag(ChromeFeatureMap.getInstance(), + "TabSwitcherListMode", false); + + private sTabSwitcherListMode() {} + + public static CachedFlag getInstance() { + return sInstance; + } +} diff --git a/chrome/browser/tab_ui/android/java/src/org/chromium/chrome/browser/tab_ui/TabListFaviconProvider.java b/chrome/browser/tab_ui/android/java/src/org/chromium/chrome/browser/tab_ui/TabListFaviconProvider.java --- a/chrome/browser/tab_ui/android/java/src/org/chromium/chrome/browser/tab_ui/TabListFaviconProvider.java +++ b/chrome/browser/tab_ui/android/java/src/org/chromium/chrome/browser/tab_ui/TabListFaviconProvider.java @@ -35,6 +35,8 @@ import org.chromium.url.GURL; import java.lang.annotation.Retention; import java.lang.annotation.RetentionPolicy; +import java.util.Arrays; +import java.util.List; import java.util.Objects; /** Provider for processed favicons in Tab list. */ @@ -132,6 +134,28 @@ public class TabListFaviconProvider { } } + /** Tracks the GURLS that were used for the composed favicon for the equality check. */ + @VisibleForTesting + public static class ComposedTabFavicon extends TabFavicon { + private final GURL[] mGurls; + + @VisibleForTesting + public ComposedTabFavicon(Drawable drawable, GURL[] gurls) { + super(drawable, drawable, false); + mGurls = gurls; + } + + @Override + public int hashCode() { + return Arrays.hashCode(mGurls); + } + + @Override + public boolean equals(Object obj) { + return (obj instanceof ComposedTabFavicon other) && Arrays.equals(mGurls, other.mGurls); + } + } + @IntDef({ StaticTabFaviconType.UNKNOWN, StaticTabFaviconType.ROUNDED_GLOBE, @@ -252,6 +276,8 @@ public class TabListFaviconProvider { private static LazyTabFaviconResolver sRoundedGlobeFavicon; private static LazyTabFaviconResolver sRoundedGlobeFaviconForStrip; private static LazyTabFaviconResolver sRoundedGlobeFaviconIncognito; + private static LazyTabFaviconResolver sRoundedComposedDefaultFavicon; + private static LazyTabFaviconResolver sRoundedComposedDefaultFaviconIncognito; /** These icons may fail to load. See crbug.com/324996488. */ private static LazyTabFaviconResolver sRoundedChromeFavicon; @@ -423,6 +449,51 @@ public class TabListFaviconProvider { }; } + /** + * Creates a fetcher that asynchronously creates a composed, up to 4 favicon, {{@link + * TabFavicon}}. + * + * @param urls List of urls, up to 4, whose favicon are requested to be composed. + * @param isIncognito Whether the processed composed favicon is used for incognito or not. + * @return a favicon fetcher that returns the composed favicon. + */ + public TabFaviconFetcher getComposedFaviconImageFetcher(List urls, boolean isIncognito) { + return new TabFaviconFetcher() { + @Override + public void fetch(Callback faviconCallback) { + getComposedFaviconImageAsync(urls, isIncognito, faviconCallback); + } + }; + } + + private void getComposedFaviconImageAsync( + List urls, boolean isIncognito, Callback faviconCallback) { + assert urls != null && urls.size() > 1 && urls.size() <= 4; + if (mFaviconHelper == null) { + faviconCallback.onResult(getRoundedGlobeFavicon(isIncognito)); + return; + } + mFaviconHelper.getComposedFaviconImage( + getProfile(isIncognito), + urls, + mFaviconSize, + (image, iconUrls) -> { + if (image == null) { + faviconCallback.onResult(getDefaultComposedImageFavicon(isIncognito)); + } else { + faviconCallback.onResult( + new ComposedTabFavicon( + processBitmap(image, mIsTabStrip), iconUrls)); + } + }); + } + + private TabFavicon getDefaultComposedImageFavicon(boolean isIncognito) { + return isIncognito + ? sRoundedComposedDefaultFaviconIncognito.get(mContext) + : colorFaviconWithTheme(sRoundedComposedDefaultFavicon.get(mContext)); + } + /** Returns the rounded Chrome favicon to use for native or internal pages. */ public TabFavicon getRoundedChromeFavicon(boolean isIncognito) { if (mIsTabStrip) { @@ -785,6 +856,46 @@ public class TabListFaviconProvider { StaticTabFaviconType.ROUNDED_CHROME_INCOGNITO); }); } + if (sRoundedComposedDefaultFavicon == null) { + sRoundedComposedDefaultFavicon = + new LazyTabFaviconResolver( + (context) -> { + Bitmap composedBitmap = + getResizedBitmapFromDrawable( + AppCompatResources.getDrawable( + context, R.drawable.ic_warning_red_16dp), + defaultFaviconSize); + return createChromeOwnedResourceTabFavicon( + context, + composedBitmap, + defaultFaviconSize, + cornerRadius, + defaultIconColor, + selectedIconColor, + false, + StaticTabFaviconType.ROUNDED_COMPOSED_DEFAULT); + }); + } + if (sRoundedComposedDefaultFaviconIncognito == null) { + sRoundedComposedDefaultFaviconIncognito = + new LazyTabFaviconResolver( + (context) -> { + Bitmap composedBitmap = + getResizedBitmapFromDrawable( + AppCompatResources.getDrawable( + context, R.drawable.ic_warning_red_16dp), + defaultFaviconSize); + return createChromeOwnedResourceTabFavicon( + context, + composedBitmap, + defaultFaviconSize, + cornerRadius, + incognitoIconColor, + incognitoSelectedIconColor, + false, + StaticTabFaviconType.ROUNDED_COMPOSED_DEFAULT_INCOGNITO); + }); + } // Tab strip favicons do not recolor when selected. if (sRoundedGlobeFaviconForStrip == null) { diff --git a/chrome/browser/ui/android/favicon/java/src/org/chromium/chrome/browser/ui/favicon/FaviconHelper.java b/chrome/browser/ui/android/favicon/java/src/org/chromium/chrome/browser/ui/favicon/FaviconHelper.java --- a/chrome/browser/ui/android/favicon/java/src/org/chromium/chrome/browser/ui/favicon/FaviconHelper.java +++ b/chrome/browser/ui/android/favicon/java/src/org/chromium/chrome/browser/ui/favicon/FaviconHelper.java @@ -25,12 +25,15 @@ import org.chromium.chrome.browser.profiles.Profile; import org.chromium.components.embedder_support.util.UrlUtilities; import org.chromium.url.GURL; +import java.util.List; + /** * This is a helper class to use favicon_service.cc's functionality. * - *

You can request a favicon image by web page URL. Note that an instance of this class should be - * created & used & destroyed (by destroy()) in the same thread due to the C++ - * base::CancelableTaskTracker class requirement. + * You can request a favicon image by web page URL. Note that an instance of + * this class should be created & used & destroyed (by destroy()) in the same + * thread due to the C++ base::CancelableTaskTracker class + * requirement. */ @NullMarked public class FaviconHelper { @@ -48,6 +51,17 @@ public class FaviconHelper { void onFaviconAvailable(Bitmap image, @JniType("GURL") GURL iconUrl); } + /** Similar to {@link FaviconImageCallback} but with a list of urls used in the image. */ + public interface ComposedFaviconImageCallback { + /** + * @param image A composed image that contains some or all of the requested favicons. + * @param iconUrls An ordered array of the icon urls that were used. + */ + @CalledByNative("ComposedFaviconImageCallback") + public void onComposedFaviconAvailable( + Bitmap image, @JniType("std::vector") GURL[] iconUrls); + } + /** Helper for generating default favicons and sharing the same icon between multiple views. */ public static class DefaultFaviconHelper { private @Nullable Bitmap mChromeDarkBitmap; @@ -230,6 +244,36 @@ public class FaviconHelper { faviconImageCallback); } + /** + * Get a composed, up to 4, Favicon bitmap for the requested arguments. + * @param profile Profile used for the FaviconService construction. + * @param urls The list of URLs whose favicon are requested to compose. Size should be 2 to 4. + * @param desiredSizeInPixel The size of the favicon in pixel we want to get. + * @param composedFaviconImageCallback A method to be called back when the result is available. + * Note that this callback is not called if this method returns false. + * @return True if GetLocalFaviconImageForURL is successfully called. + */ + public boolean getComposedFaviconImage( + Profile profile, + List urls, + int desiredSizeInPixel, + ComposedFaviconImageCallback composedFaviconImageCallback) { + assert mNativeFaviconHelper != 0; + + if (urls.size() <= 1 || urls.size() > 4) { + throw new IllegalStateException( + "Only able to compose 2 to 4 favicon, but requested " + urls.size()); + } + + return FaviconHelperJni.get() + .getComposedFaviconImage( + mNativeFaviconHelper, + profile, + urls.toArray(new GURL[0]), + desiredSizeInPixel, + composedFaviconImageCallback); + } + @VisibleForTesting(otherwise = VisibleForTesting.PACKAGE_PRIVATE) @NativeMethods public interface Natives { @@ -237,6 +281,13 @@ public class FaviconHelper { void destroy(long nativeFaviconHelper); + boolean getComposedFaviconImage( + long nativeFaviconHelper, + @JniType("Profile*") Profile profile, + @JniType("std::vector") GURL[] urls, + int desiredSizeInDip, + ComposedFaviconImageCallback composedFaviconImageCallback); + boolean getLocalFaviconImageForURL( long nativeFaviconHelper, @JniType("Profile*") Profile profile, diff --git a/chrome/browser/ui/android/strings/cromite_android_chrome_strings_grd/Use-list-mode-for-tab-switcher.grdp b/chrome/browser/ui/android/strings/cromite_android_chrome_strings_grd/Use-list-mode-for-tab-switcher.grdp new file mode 100644 --- /dev/null +++ b/chrome/browser/ui/android/strings/cromite_android_chrome_strings_grd/Use-list-mode-for-tab-switcher.grdp @@ -0,0 +1,9 @@ + + + + Use list mode for tab switcher + + + Activates the list view of tabs in the tab switcher + + diff --git a/chrome/browser/ui/android/toolbar/java/src/org/chromium/chrome/browser/toolbar/top/ToolbarPhone.java b/chrome/browser/ui/android/toolbar/java/src/org/chromium/chrome/browser/toolbar/top/ToolbarPhone.java --- a/chrome/browser/ui/android/toolbar/java/src/org/chromium/chrome/browser/toolbar/top/ToolbarPhone.java +++ b/chrome/browser/ui/android/toolbar/java/src/org/chromium/chrome/browser/toolbar/top/ToolbarPhone.java @@ -69,6 +69,7 @@ import org.chromium.build.annotations.Nullable; import org.chromium.chrome.browser.flags.ChromeFeatureList; import org.chromium.chrome.browser.ntp_customization.NtpCustomizationUtils; import org.chromium.chrome.browser.omnibox.LocationBar; +import org.chromium.chrome.browser.flags.cromite.sTabSwitcherListMode; import org.chromium.chrome.browser.omnibox.LocationBarBackgroundDrawable; import org.chromium.chrome.browser.omnibox.LocationBarBackgroundDrawable.HairlineBehavior; import org.chromium.chrome.browser.omnibox.LocationBarCoordinator; @@ -2140,7 +2141,7 @@ public class ToolbarPhone extends ToolbarLayout // On entering the tab switcher, set the focusability of the url bar to be false. This will // occur at the start of the enter event, and will later be reset to true upon finishing the // exit event. - if (inTabSwitcherMode) { + if (inTabSwitcherMode && !sTabSwitcherListMode.getInstance().isEnabled()) { mLocationBar.setUrlBarFocusable(false); } @@ -2368,7 +2369,8 @@ public class ToolbarPhone extends ToolbarLayout ToggleTabStackButtonCoordinator tabSwitcherButtonCoordinator = getTabSwitcherButtonCoordinator(); assumeNonNull(tabSwitcherButtonCoordinator); - tabSwitcherButtonCoordinator.getContainerView().setClickable(!hasFocus); + if (!sTabSwitcherListMode.getInstance().isEnabled()) + tabSwitcherButtonCoordinator.getContainerView().setClickable(!hasFocus); mHomeButton.setClickable(!hasFocus); triggerUrlFocusAnimation(hasFocus); } diff --git a/components/browser_ui/styles/android/BUILD.gn b/components/browser_ui/styles/android/BUILD.gn --- a/components/browser_ui/styles/android/BUILD.gn +++ b/components/browser_ui/styles/android/BUILD.gn @@ -247,6 +247,7 @@ android_resources("java_resources") { "java/res/drawable/ic_visibility_off_black.xml", "java/res/drawable/ic_volume_off_fill_24dp.xml", "java/res/drawable/ic_volume_up_fill_24dp.xml", + "java/res/drawable/ic_warning_red_16dp.xml", "java/res/drawable/ic_warning_red_24dp.xml", "java/res/drawable/ic_warning_red_outline_32dp.xml", "java/res/drawable/ic_web_24dp.xml", diff --git a/components/browser_ui/styles/android/java/res/drawable/ic_warning_red_16dp.xml b/components/browser_ui/styles/android/java/res/drawable/ic_warning_red_16dp.xml new file mode 100644 --- /dev/null +++ b/components/browser_ui/styles/android/java/res/drawable/ic_warning_red_16dp.xml @@ -0,0 +1,26 @@ + + + + + + + + + diff --git a/components/browser_ui/widget/android/java/res/layout/title_and_description_layout.xml b/components/browser_ui/widget/android/java/res/layout/title_and_description_layout.xml --- a/components/browser_ui/widget/android/java/res/layout/title_and_description_layout.xml +++ b/components/browser_ui/widget/android/java/res/layout/title_and_description_layout.xml @@ -25,9 +25,22 @@ found in the LICENSE file. app:layout_constrainedWidth="true" app:layout_constraintStart_toStartOf="parent" app:layout_constraintTop_toTopOf="parent" - app:layout_constraintEnd_toStartOf="@+id/space_anchor" + app:layout_constraintEnd_toStartOf="@+id/after_title_container" app:layout_constraintWidth_default="wrap" /> + + + + diff --git a/cromite_flags/chrome/browser/flags/android/chrome_feature_list_cc/tab-switcher-list-mode.inc b/cromite_flags/chrome/browser/flags/android/chrome_feature_list_cc/tab-switcher-list-mode.inc new file mode 100644 --- /dev/null +++ b/cromite_flags/chrome/browser/flags/android/chrome_feature_list_cc/tab-switcher-list-mode.inc @@ -0,0 +1,3 @@ +CROMITE_FEATURE(kTabSwitcherListMode, + "TabSwitcherListMode", + base::FEATURE_DISABLED_BY_DEFAULT); --