Files
cromite/build/patches/Use-list-mode-for-tab-switcher.patch
2026-05-19 10:48:25 +02:00

1884 lines
94 KiB
Diff

From: uazo <uazo@users.noreply.github.com>
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 @@
+<?xml version="1.0" encoding="utf-8"?>
+<!--
+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.
+-->
+
+<shape xmlns:android="http://schemas.android.com/apk/res/android">
+ <stroke
+ android:width="2dp"
+ android:color="@macro/default_control_color_active" />
+ <corners android:radius="@dimen/tab_list_card_radius" />
+</shape>
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 @@
+<?xml version="1.0" encoding="utf-8"?>
+<!--
+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.
+-->
+
+<shape xmlns:android="http://schemas.android.com/apk/res/android">
+ <stroke
+ android:width="2dp"
+ android:color="@color/baseline_primary_80" />
+ <corners android:radius="@dimen/tab_list_card_radius" />
+</shape>
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 @@
+<?xml version="1.0" encoding="utf-8"?>
+<!--
+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.
+-->
+<org.chromium.chrome.browser.tasks.tab_management.TabListView
+ xmlns:android="http://schemas.android.com/apk/res/android"
+ xmlns:app="http://schemas.android.com/apk/res-auto"
+ xmlns:tools="http://schemas.android.com/tools"
+ android:layout_width="match_parent"
+ android:layout_height="wrap_content">
+
+ <LinearLayout
+ android:id="@+id/content_view"
+ android:layout_width="match_parent"
+ android:layout_height="match_parent"
+ android:orientation="horizontal"
+ android:background="@drawable/menu_bg_tinted">
+
+ <include layout="@layout/modern_list_item_view" />
+
+ </LinearLayout>
+
+</org.chromium.chrome.browser.tasks.tab_management.TabListView>
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.
-->
<resources xmlns:tools="http://schemas.android.com/tools">
<dimen name="tab_list_selected_inset">7dp</dimen>
+ <dimen name="tab_list_selected_inset_low_end">6dp</dimen>
<dimen name="tab_list_selected_margin">7dp</dimen>
<dimen name="tab_grid_price_card_radius">4dp</dimen>
+ <dimen name="tab_list_card_radius">4dp</dimen>
<dimen name="tab_list_mini_card_radius">4dp</dimen>
<dimen name="tab_list_mini_card_frame_size">1dp</dimen>
<dimen name="tab_grid_close_button_size">18dp</dimen>
@@ -43,6 +45,8 @@ found in the LICENSE file.
<dimen name="swipe_to_dismiss_threshold">144dp</dimen>
<dimen name="long_press_cancel_threshold">20dp</dimen>
<dimen name="selection_tab_grid_toggle_button_inset">14dp</dimen>
+ <dimen name="selection_tab_list_toggle_button_lateral_inset">18dp</dimen>
+ <dimen name="selection_tab_list_toggle_button_vertical_inset">22dp</dimen>
<!-- Dimens for incognito reauth promo message icon -->
<dimen name="incognito_reauth_promo_message_icon_width">52dp</dimen>
@@ -117,6 +121,7 @@ found in the LICENSE file.
<dimen name="tab_card_label_icon_inset">3dp</dimen>
<dimen name="tab_card_label_icon_radius">8dp</dimen>
<dimen name="tab_card_label_height">24dp</dimen>
+ <dimen name="tab_card_label_list_margin_end">9dp</dimen>
<dimen name="tab_card_label_elev">6dp</dimen>
<!-- Dimens for TabGroupSync features -->
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<String> DIALOG_UNGROUP_BAR_TEXT =
new WritableObjectPropertyKey<>();
+ public static final WritableObjectPropertyKey<Integer> 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
*
* <p>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<Tab> 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<GURL> 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<TabListEditorItemSelectionId> {
+ 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<String> 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" />
+ <org.chromium.components.browser_ui.settings.ChromeSwitchPreference
+ android:key="use_tab_switcher_list_mode"
+ android:title="@string/tab_switcher_list_mode_title"
+ android:summary="@string/tab_switcher_list_mode_summary"
+ app:featureName="TabSwitcherListMode"
+ app:needRestart="true" />
<PreferenceCategory
android:key="security_section"
android:title="@string/security_section_title" />
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<void(void)>;
static int64_t JNI_FaviconHelper_Init(JNIEnv* env) {
return reinterpret_cast<intptr_t>(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<GURL> 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<FaviconHelper> favicon_helper_;
+ raw_ptr<favicon::FaviconService> favicon_service_;
+ std::vector<GURL> urls_;
+ int desire_size_in_pixel_;
+ JobFinishedCallback job_finished_callback_;
+ favicon_base::FaviconResultsCallback result_callback_;
+ int favicon_expected_count_;
+ std::vector<favicon_base::FaviconRawBitmapResult> favicon_raw_bitmap_results_;
+ int favicon_result_count_;
+
+ base::WeakPtrFactory<Job> weak_ptr_factory_{this};
+};
+
+FaviconHelper::Job::Job(FaviconHelper* favicon_helper,
+ favicon::FaviconService* favicon_service,
+ std::vector<GURL> 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<base::CancelableTaskTracker>();
}
@@ -58,6 +155,67 @@ void FaviconHelper::Destroy(JNIEnv* env) {
delete this;
}
+jboolean FaviconHelper::GetComposedFaviconImage(
+ JNIEnv* env,
+ Profile* profile,
+ const std::vector<GURL>& gurls,
+ int32_t j_desired_size_in_pixel,
+ const JavaRef<jobject>&
+ 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<int>(j_desired_size_in_pixel);
+
+ favicon_base::FaviconResultsCallback callback_runner = base::BindOnce(
+ &FaviconHelper::OnComposedFaviconBitmapResultsAvailable,
+ weak_ptr_factory_.GetWeakPtr(),
+ ScopedJavaGlobalRef<jobject>(j_composed_favicon_image_callback),
+ desired_size_in_pixel);
+
+ GetComposedFaviconImageInternal(favicon_service, gurls,
+ static_cast<int>(j_desired_size_in_pixel),
+ std::move(callback_runner));
+
+ return true;
+}
+
+void FaviconHelper::GetComposedFaviconImageInternal(
+ favicon::FaviconService* favicon_service,
+ std::vector<GURL> 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<Job>(
+ 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<gfx::Size>& 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<int> 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<jobject>& 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<jobject>& j_favicon_image_callback,
+ const int desired_size_in_pixel,
+ const std::vector<favicon_base::FaviconRawBitmapResult>& results) {
+ JNIEnv* env = AttachCurrentThread();
+ std::vector<SkBitmap> result_bitmaps;
+ std::vector<GURL> 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<jobject> j_favicon_bitmap;
+ if (!result_bitmaps.empty()) {
+ std::unique_ptr<SkBitmap> 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<GURL>& gurls,
+ int32_t j_desired_size_in_pixel,
+ const base::android::JavaRef<jobject>& 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<GURL> 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<gfx::Size>& sizes);
+
// This function is expected to be bound to a WeakPtr<FaviconHelper>, 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<jobject>& j_favicon_image_callback,
const favicon_base::FaviconRawBitmapResult& result);
+ void OnComposedFaviconBitmapResultsAvailable(
+ const base::android::JavaRef<jobject>& j_favicon_image_callback,
+ const int desired_size_in_pixel,
+ const std::vector<favicon_base::FaviconRawBitmapResult>& result);
+
std::unique_ptr<base::CancelableTaskTracker> cancelable_task_tracker_;
+ std::map<int, std::unique_ptr<Job>> id_to_job_;
+ int last_used_job_id_;
+
base::WeakPtrFactory<FaviconHelper> 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 <https://www.gnu.org/licenses/>.
+*/
+
+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<GURL> urls, boolean isIncognito) {
+ return new TabFaviconFetcher() {
+ @Override
+ public void fetch(Callback<TabFavicon> faviconCallback) {
+ getComposedFaviconImageAsync(urls, isIncognito, faviconCallback);
+ }
+ };
+ }
+
+ private void getComposedFaviconImageAsync(
+ List<GURL> urls, boolean isIncognito, Callback<TabFavicon> 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.
*
- * <p>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>") 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<GURL> 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>") 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 @@
+<?xml version="1.0" encoding="utf-8"?>
+<grit-part>
+ <message name="IDS_TAB_SWITCHER_LIST_MODE_TITLE" desc="Text for 'Use list mode for tab switcher' settings-privacy option.">
+ Use list mode for tab switcher
+ </message>
+ <message name="IDS_TAB_SWITCHER_LIST_MODE_SUMMARY" desc="Summary text for 'Use list mode for tab switcher' settings-privacy option.">
+ Activates the list view of tabs in the tab switcher
+ </message>
+</grit-part>
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 @@
+<?xml version="1.0" encoding="utf-8"?>
+<!--
+Copyright 2017 The Chromium Authors
+Use of this source code is governed by a BSD-style license that can be
+found in the LICENSE file.
+-->
+
+<vector xmlns:android="http://schemas.android.com/apk/res/android"
+ android:width="16dp"
+ android:height="16dp"
+ android:viewportWidth="16"
+ android:viewportHeight="16">
+
+ <path
+ android:pathData="M -1 -1 H 17 V 17 H -1 V -1 Z" />
+ <path
+ android:fillAlpha="0"
+ android:fillType="evenOdd"
+ android:strokeAlpha="0"
+ android:strokeWidth="1"
+ android:pathData="M 0 0 H 16 V 16 H 0 V 0 Z" />
+ <path
+ android:fillColor="@color/google_red_600"
+ android:fillType="evenOdd"
+ android:pathData="M0.5,14l15,0l-7.5,-13l-7.5,13l0,0zm8.5,-2l-2,0l0,-2l2,0l0,2l0,0zm0,-3l-2,0l0,-3l2,0l0,3l0,0z" />
+</vector>
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" />
+ <FrameLayout
+ android:id="@+id/after_title_container"
+ android:layout_marginStart="6dp"
+ android:layout_width="wrap_content"
+ android:layout_height="wrap_content"
+ android:layout_gravity="center_vertical"
+ android:visibility="gone"
+ app:layout_constraintVertical_bias="0.5"
+ app:layout_constraintBottom_toBottomOf="@+id/title"
+ app:layout_constraintTop_toTopOf="@+id/title"
+ app:layout_constraintStart_toEndOf="@+id/title"
+ app:layout_constraintEnd_toStartOf="@+id/space_anchor" />
+
<!-- Anchor to ensure the chain terminates without any elements being pushed offscreen. -->
<Space
android:id="@+id/space_anchor"
@@ -35,6 +48,18 @@ found in the LICENSE file.
android:layout_height="match_parent"
app:layout_constraintEnd_toEndOf="parent" />
+ <FrameLayout
+ android:id="@+id/before_description_container"
+ android:layout_gravity="center_vertical|start"
+ android:layout_width="wrap_content"
+ android:layout_height="wrap_content"
+ app:layout_constraintHorizontal_bias="0.0"
+ app:layout_constraintHorizontal_chainStyle="packed"
+ app:layout_constraintTop_toBottomOf="@+id/title"
+ app:layout_constraintBottom_toBottomOf="@+id/description"
+ app:layout_constraintStart_toStartOf="parent"
+ app:layout_constraintEnd_toStartOf="@+id/description" />
+
<TextView
android:id="@+id/description"
android:layout_gravity="center_vertical|start"
@@ -43,10 +68,8 @@ found in the LICENSE file.
android:maxLines="1"
android:ellipsize="end"
android:textAppearance="@style/TextAppearance.TextMedium.Secondary"
- app:layout_constraintHorizontal_bias="0.0"
- app:layout_constraintHorizontal_chainStyle="packed"
app:layout_constraintTop_toBottomOf="@+id/title"
- app:layout_constraintStart_toStartOf="parent"
+ app:layout_constraintStart_toEndOf="@+id/before_description_container"
app:layout_constraintEnd_toStartOf="@+id/chip" />
<!-- An optional chip view. Used for history. -->
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);
--