0

[Tab Switcher] TabSwitcherPane level TabSwitcherCoordinator elements

In the process of "forking" TabSwitcherCoordinator/Mediator some
elements of TabSwitcherCoordinator are better handled at the Pane level.

This CL contains the menu and keyboard and price tracking related
functionality that should be managed at the pane scope.

Bug: 1505772
Change-Id: Iffe1d3891d91f4947e2b0296d6df17ff9855cc92
Reviewed-on: https://chromium-review.googlesource.com/c/chromium/src/+/5131306
Code-Coverage: findit-for-me@appspot.gserviceaccount.com <findit-for-me@appspot.gserviceaccount.com>
Reviewed-by: Sky Malice <skym@chromium.org>
Commit-Queue: Calder Kitagawa <ckitagawa@chromium.org>
Cr-Commit-Position: refs/heads/main@{#1243476}
This commit is contained in:
Calder Kitagawa
2024-01-05 17:34:45 +00:00
committed by Chromium LUCI CQ
parent 7d97ef4d2e
commit 409472450f
10 changed files with 295 additions and 26 deletions

@ -10,13 +10,13 @@ import android.view.View.OnClickListener;
import androidx.annotation.NonNull;
import org.chromium.base.supplier.Supplier;
import org.chromium.chrome.browser.hub.LoadHint;
import org.chromium.chrome.browser.hub.Pane;
import org.chromium.chrome.browser.hub.PaneId;
import org.chromium.chrome.browser.hub.ResourceButtonData;
import org.chromium.chrome.browser.tabmodel.IncognitoTabModel;
import org.chromium.chrome.browser.tabmodel.IncognitoTabModelObserver;
import org.chromium.chrome.tab_ui.R;
import org.chromium.components.browser_ui.widget.MenuOrKeyboardActionController;
/** A {@link Pane} representing the incognito tab switcher. */
public class IncognitoTabSwitcherPane extends TabSwitcherPaneBase {
@ -44,16 +44,19 @@ public class IncognitoTabSwitcherPane extends TabSwitcherPaneBase {
* @param incognitoTabModelSupplier The supplier of the incognito tab model. Returns a valid
* model once native is loaded and will never change thereafter.
* @param newTabButtonClickListener The {@link OnClickListener} for the new tab button.
* @param menuOrKeyboardActionController allows access to menu or keyboard actions.
*/
IncognitoTabSwitcherPane(
@NonNull Context context,
@NonNull TabSwitcherPaneCoordinatorFactory factory,
@NonNull Supplier<IncognitoTabModel> incognitoTabModelSupplier,
@NonNull OnClickListener newTabButtonClickListener) {
@NonNull OnClickListener newTabButtonClickListener,
@NonNull MenuOrKeyboardActionController menuOrKeyboardActionController) {
super(
context,
factory,
newTabButtonClickListener,
menuOrKeyboardActionController,
org.chromium.chrome.browser.toolbar.R.string.button_new_incognito_tab);
mIncognitoTabModelSupplier = incognitoTabModelSupplier;
@ -90,9 +93,4 @@ public class IncognitoTabSwitcherPane extends TabSwitcherPaneBase {
public @PaneId int getPaneId() {
return PaneId.INCOGNITO_TAB_SWITCHER;
}
@Override
public void notifyLoadHint(@LoadHint int loadHint) {
// TODO(crbug/1505772): Implement.
}
}

@ -36,10 +36,10 @@ import org.mockito.junit.MockitoRule;
import org.chromium.base.test.BaseRobolectricTestRunner;
import org.chromium.chrome.browser.hub.DisplayButtonData;
import org.chromium.chrome.browser.hub.FullButtonData;
import org.chromium.chrome.browser.hub.LoadHint;
import org.chromium.chrome.browser.hub.PaneId;
import org.chromium.chrome.browser.tabmodel.IncognitoTabModel;
import org.chromium.chrome.browser.tabmodel.IncognitoTabModelObserver;
import org.chromium.components.browser_ui.widget.MenuOrKeyboardActionController;
/**
* Unit tests for {@link IncognitoTabSwitcherPane}. Refer to {@link TabSwitcherPaneUnitTest} for
@ -53,6 +53,7 @@ public class IncognitoTabSwitcherPaneUnitTest {
@Mock private TabSwitcherPaneCoordinator mTabSwitcherPaneCoordinator;
@Mock private View.OnClickListener mNewTabButtonClickListener;
@Mock private IncognitoTabModel mIncognitoTabModel;
@Mock private MenuOrKeyboardActionController mMenuOrKeyboardActionController;
@Captor private ArgumentCaptor<IncognitoTabModelObserver> mIncognitoTabModelObserverCaptor;
@ -76,7 +77,8 @@ public class IncognitoTabSwitcherPaneUnitTest {
mContext,
mTabSwitcherPaneCoordinatorFactory,
() -> mIncognitoTabModel,
mNewTabButtonClickListener);
mNewTabButtonClickListener,
mMenuOrKeyboardActionController);
}
@After
@ -116,15 +118,6 @@ public class IncognitoTabSwitcherPaneUnitTest {
assertEquals(PaneId.INCOGNITO_TAB_SWITCHER, mIncognitoTabSwitcherPane.getPaneId());
}
@Test
@SmallTest
public void testLoadHint() {
// TODO(crbug/1505772): this is a noop right now.
mIncognitoTabSwitcherPane.notifyLoadHint(LoadHint.COLD);
mIncognitoTabSwitcherPane.notifyLoadHint(LoadHint.WARM);
mIncognitoTabSwitcherPane.notifyLoadHint(LoadHint.HOT);
}
@Test
@SmallTest
public void testNewTabButton() {

@ -65,7 +65,7 @@ public class TabListCoordinator
*
* <p>NOTE: STRIP, LIST, and GRID modes will have height equal to that of the container view.
*/
@IntDef({TabListMode.GRID, TabListMode.STRIP, TabListMode.LIST})
@IntDef({TabListMode.GRID, TabListMode.STRIP, TabListMode.LIST, TabListMode.NUM_ENTRIES})
@Retention(RetentionPolicy.SOURCE)
public @interface TabListMode {
int GRID = 0;

@ -28,6 +28,7 @@ import org.chromium.chrome.browser.incognito.reauth.IncognitoReauthController;
import org.chromium.chrome.browser.layouts.LayoutStateProvider;
import org.chromium.chrome.browser.lifecycle.ActivityLifecycleDispatcher;
import org.chromium.chrome.browser.multiwindow.MultiWindowModeStateDispatcher;
import org.chromium.chrome.browser.profiles.ProfileProvider;
import org.chromium.chrome.browser.tabmodel.IncognitoStateProvider;
import org.chromium.chrome.browser.tabmodel.TabCreatorManager;
import org.chromium.chrome.browser.tabmodel.TabModelSelector;
@ -165,13 +166,17 @@ public interface TabManagementDelegate {
* Create a {@link TabSwitcher} and {@link Pane} for the Hub.
*
* @param activity The {@link Activity} that hosts the pane.
* @param profileProviderSupplier The supplier for profiles.
* @param tabModelSelector For access to {@link TabModel}.
* @param newTabButtonOnClickListener The listener for clicking the new tab button.
* @param menuOrKeyboardActionController Allows access to menu or keyboard actions.
* @param isIncognito Whether this is an incognito pane.
*/
Pair<TabSwitcher, Pane> createTabSwitcherPane(
@NonNull Activity activity,
@NonNull OneshotSupplier<ProfileProvider> profileProviderSupplier,
@NonNull TabModelSelector tabModelSelector,
@NonNull OnClickListener newTabButtonOnClickListener,
@NonNull MenuOrKeyboardActionController menuOrKeyboardActionController,
boolean isIncognito);
}

@ -13,6 +13,7 @@ import android.view.ViewGroup;
import androidx.annotation.NonNull;
import androidx.annotation.Nullable;
import org.chromium.base.ContextUtils;
import org.chromium.base.supplier.ObservableSupplier;
import org.chromium.base.supplier.OneshotSupplier;
import org.chromium.base.supplier.Supplier;
@ -27,9 +28,11 @@ import org.chromium.chrome.browser.incognito.reauth.IncognitoReauthController;
import org.chromium.chrome.browser.layouts.LayoutStateProvider;
import org.chromium.chrome.browser.lifecycle.ActivityLifecycleDispatcher;
import org.chromium.chrome.browser.multiwindow.MultiWindowModeStateDispatcher;
import org.chromium.chrome.browser.profiles.ProfileProvider;
import org.chromium.chrome.browser.tabmodel.IncognitoStateProvider;
import org.chromium.chrome.browser.tabmodel.IncognitoTabModel;
import org.chromium.chrome.browser.tabmodel.TabCreatorManager;
import org.chromium.chrome.browser.tabmodel.TabModelFilter;
import org.chromium.chrome.browser.tabmodel.TabModelSelector;
import org.chromium.chrome.browser.ui.messages.snackbar.SnackbarManager;
import org.chromium.components.browser_ui.bottomsheet.BottomSheetController;
@ -143,8 +146,10 @@ public class TabManagementDelegateImpl implements TabManagementDelegate {
@Override
public Pair<TabSwitcher, Pane> createTabSwitcherPane(
@NonNull Activity activity,
@NonNull OneshotSupplier<ProfileProvider> profileProviderSupplier,
@NonNull TabModelSelector tabModelSelector,
@NonNull OnClickListener newTabButtonOnClickListener,
@NonNull MenuOrKeyboardActionController menuOrKeyboardActionController,
boolean isIncognito) {
// TODO(crbug/1505772): Consider making this an activity scoped singleton and possibly
// hosting it in CTA/HubProvider.
@ -156,13 +161,20 @@ public class TabManagementDelegateImpl implements TabManagementDelegate {
activity,
factory,
() -> (IncognitoTabModel) tabModelSelector.getModel(true),
newTabButtonOnClickListener);
newTabButtonOnClickListener,
menuOrKeyboardActionController);
} else {
Supplier<TabModelFilter> tabModelFilterSupplier =
() -> tabModelSelector.getTabModelFilterProvider().getTabModelFilter(false);
pane =
new TabSwitcherPane(
activity,
ContextUtils.getAppSharedPreferences(),
profileProviderSupplier,
factory,
tabModelFilterSupplier,
newTabButtonOnClickListener,
menuOrKeyboardActionController,
new TabSwitcherPaneDrawableCoordinator(activity, tabModelSelector));
}
return Pair.create(new TabSwitcherPaneAdapter(pane), pane);

@ -5,50 +5,84 @@
package org.chromium.chrome.browser.tasks.tab_management;
import android.content.Context;
import android.content.SharedPreferences;
import android.content.SharedPreferences.OnSharedPreferenceChangeListener;
import android.view.View.OnClickListener;
import androidx.annotation.NonNull;
import androidx.annotation.Nullable;
import org.chromium.base.supplier.OneshotSupplier;
import org.chromium.base.supplier.Supplier;
import org.chromium.chrome.browser.hub.DrawableButtonData;
import org.chromium.chrome.browser.hub.LoadHint;
import org.chromium.chrome.browser.hub.Pane;
import org.chromium.chrome.browser.hub.PaneId;
import org.chromium.chrome.browser.price_tracking.PriceTrackingFeatures;
import org.chromium.chrome.browser.price_tracking.PriceTrackingUtilities;
import org.chromium.chrome.browser.profiles.ProfileProvider;
import org.chromium.chrome.browser.tabmodel.TabModelFilter;
import org.chromium.chrome.browser.tasks.tab_management.TabListCoordinator.TabListMode;
import org.chromium.chrome.tab_ui.R;
import org.chromium.components.browser_ui.widget.MenuOrKeyboardActionController;
/** A {@link Pane} representing the regular tab switcher. */
public class TabSwitcherPane extends TabSwitcherPaneBase {
private final @NonNull SharedPreferences mSharedPreferences;
private final @NonNull Supplier<TabModelFilter> mTabModelFilterSupplier;
private final @NonNull TabSwitcherPaneDrawableCoordinator mTabSwitcherPaneDrawableCoordinator;
private @Nullable OnSharedPreferenceChangeListener mPriceAnnotationsPrefListener;
private boolean mIsVisible;
/**
* @param context The activity context.
* @param sharedPreferences The app shared preferences.
* @param profileProviderSupplier The profile provider supplier.
* @param factory The factory used to construct {@link TabSwitcherPaneCoordinator}s.
* @param tabModelFilterSupplier The supplier of the regular {@link TabModelFilter}.
* @param newTabButtonClickListener The {@link OnClickListener} for the new tab button.
* @param menuOrKeyboardActionController Allows access to menu or keyboard actions.
* @param tabSwitcherPaneDrawableCoordinator The drawable to represent the pane.
*/
TabSwitcherPane(
@NonNull Context context,
@NonNull SharedPreferences sharedPreferences,
@NonNull OneshotSupplier<ProfileProvider> profileProviderSupplier,
@NonNull TabSwitcherPaneCoordinatorFactory factory,
@NonNull Supplier<TabModelFilter> tabModelFilterSupplier,
@NonNull OnClickListener newTabButtonClickListener,
@NonNull MenuOrKeyboardActionController menuOrKeyboardActionController,
@NonNull TabSwitcherPaneDrawableCoordinator tabSwitcherDrawableCoordinator) {
super(
context,
factory,
newTabButtonClickListener,
menuOrKeyboardActionController,
org.chromium.chrome.browser.toolbar.R.string.button_new_tab);
mSharedPreferences = sharedPreferences;
mTabModelFilterSupplier = tabModelFilterSupplier;
mTabSwitcherPaneDrawableCoordinator = tabSwitcherDrawableCoordinator;
// TODO(crbug/1505772): Update this string to not be an a11y string and it should probably
// just say "Tabs".
mReferenceButtonDataSupplier.set(
new DrawableButtonData(
org.chromium.chrome.tab_ui.R.string.accessibility_tab_switcher,
org.chromium.chrome.tab_ui.R.string.accessibility_tab_switcher,
R.string.accessibility_tab_switcher,
R.string.accessibility_tab_switcher,
tabSwitcherDrawableCoordinator.getTabSwitcherDrawable()));
profileProviderSupplier.onAvailable(this::onProfileProviderAvailable);
}
@Override
public void destroy() {
super.destroy();
mTabSwitcherPaneDrawableCoordinator.destroy();
if (mPriceAnnotationsPrefListener != null) {
mSharedPreferences.unregisterOnSharedPreferenceChangeListener(
mPriceAnnotationsPrefListener);
}
}
@Override
@ -58,6 +92,29 @@ public class TabSwitcherPane extends TabSwitcherPaneBase {
@Override
public void notifyLoadHint(@LoadHint int loadHint) {
// TODO(crbug/1505772): Implement.
super.notifyLoadHint(loadHint);
mIsVisible = loadHint == LoadHint.HOT;
}
private void onProfileProviderAvailable(@NonNull ProfileProvider profileProvider) {
if (!PriceTrackingFeatures.isPriceTrackingEnabled(profileProvider.getOriginalProfile())
&& getTabListMode() == TabListMode.GRID) {
return;
}
mPriceAnnotationsPrefListener =
(sharedPrefs, key) -> {
if (!PriceTrackingUtilities.TRACK_PRICES_ON_TABS.equals(key) || !mIsVisible) {
return;
}
TabModelFilter filter = mTabModelFilterSupplier.get();
@Nullable
TabSwitcherPaneCoordinator coordinator = getTabSwitcherPaneCoordinator();
if (filter.isCurrentlySelectedFilter()
&& filter.isTabModelRestored()
&& coordinator != null) {
coordinator.resetWithTabList(filter);
}
};
mSharedPreferences.registerOnSharedPreferenceChangeListener(mPriceAnnotationsPrefListener);
}
}

@ -15,6 +15,7 @@ import androidx.annotation.Nullable;
import androidx.annotation.StringRes;
import androidx.annotation.VisibleForTesting;
import org.chromium.base.metrics.RecordUserAction;
import org.chromium.base.supplier.ObservableSupplier;
import org.chromium.base.supplier.ObservableSupplierImpl;
import org.chromium.base.supplier.Supplier;
@ -26,8 +27,13 @@ import org.chromium.chrome.browser.hub.FullButtonData;
import org.chromium.chrome.browser.hub.HubContainerView;
import org.chromium.chrome.browser.hub.HubLayoutAnimatorProvider;
import org.chromium.chrome.browser.hub.HubLayoutConstants;
import org.chromium.chrome.browser.hub.LoadHint;
import org.chromium.chrome.browser.hub.Pane;
import org.chromium.chrome.browser.hub.ResourceButtonData;
import org.chromium.chrome.browser.tasks.tab_management.TabListCoordinator.TabListMode;
import org.chromium.chrome.tab_ui.R;
import org.chromium.components.browser_ui.widget.MenuOrKeyboardActionController;
import org.chromium.components.browser_ui.widget.MenuOrKeyboardActionController.MenuOrKeyboardActionHandler;
import org.chromium.ui.base.DeviceFormFactor;
/**
@ -39,6 +45,24 @@ public abstract class TabSwitcherPaneBase implements Pane {
new ObservableSupplierImpl<>();
protected final ObservableSupplierImpl<FullButtonData> mNewTabButtonDataSupplier =
new ObservableSupplierImpl<>();
private final MenuOrKeyboardActionHandler mMenuOrKeyboardActionHandler =
new MenuOrKeyboardActionHandler() {
@Override
public boolean handleMenuOrKeyboardAction(int id, boolean fromMenu) {
if (id == R.id.menu_select_tabs) {
@Nullable
TabSwitcherPaneCoordinator coordinator =
mTabSwitcherPaneCoordinatorSupplier.get();
if (coordinator == null) return false;
coordinator.showTabListEditor();
RecordUserAction.record("MobileMenuSelectTabs");
return true;
}
return false;
}
};
private final ObservableSupplierImpl<TabSwitcherPaneCoordinator>
mTabSwitcherPaneCoordinatorSupplier = new ObservableSupplierImpl<>();
private final TransitiveObservableSupplier<TabSwitcherPaneCoordinator, Boolean>
@ -47,6 +71,7 @@ public abstract class TabSwitcherPaneBase implements Pane {
mTabSwitcherPaneCoordinatorSupplier,
pc -> pc.getHandleBackPressChangedSupplier());
private final ViewGroup mRootView;
private final MenuOrKeyboardActionController mMenuOrKeyboardActionController;
private final TabSwitcherPaneCoordinatorFactory mFactory;
private boolean mNativeInitialized;
@ -56,6 +81,7 @@ public abstract class TabSwitcherPaneBase implements Pane {
* @param context The activity context.
* @param factory The factory used to construct {@link TabSwitcherPaneCoordinator}s.
* @param newTabButtonClickListener The {@link OnClickListener} for the new tab button.
* @param menuOrKeyboardActionController Allows access to menu or keyboard actions.
* @param newTabButtonContentDescriptionRes The resource for the new tab button content
* description.
*/
@ -63,8 +89,10 @@ public abstract class TabSwitcherPaneBase implements Pane {
@NonNull Context context,
@NonNull TabSwitcherPaneCoordinatorFactory factory,
@NonNull OnClickListener newTabButtonClickListener,
@NonNull MenuOrKeyboardActionController menuOrKeyboardActionController,
@StringRes int newTabButtonContentDescriptionRes) {
mFactory = factory;
mMenuOrKeyboardActionController = menuOrKeyboardActionController;
mNewTabButtonDataSupplier.set(
new DelegateButtonData(
@ -79,6 +107,8 @@ public abstract class TabSwitcherPaneBase implements Pane {
@Override
public void destroy() {
mMenuOrKeyboardActionController.unregisterMenuOrKeyboardActionHandler(
mMenuOrKeyboardActionHandler);
destroyTabSwitcherPaneCoordinator();
}
@ -87,6 +117,21 @@ public abstract class TabSwitcherPaneBase implements Pane {
return mRootView;
}
@Override
public void notifyLoadHint(@LoadHint int loadHint) {
// TODO(crbug/1502201): Figure out a more immediate signal for pane visibility. Due to
// WARM/COLD signals being posted this can lead to multiple HOT panes for a brief period.
// In this case multiple HOT panes might listen for the same menu event leading to a
// collision.
if (loadHint == LoadHint.HOT) {
mMenuOrKeyboardActionController.registerMenuOrKeyboardActionHandler(
mMenuOrKeyboardActionHandler);
} else {
mMenuOrKeyboardActionController.unregisterMenuOrKeyboardActionHandler(
mMenuOrKeyboardActionHandler);
}
}
@Override
public @NonNull ObservableSupplier<FullButtonData> getActionButtonDataSupplier() {
return mNewTabButtonDataSupplier;
@ -117,6 +162,7 @@ public abstract class TabSwitcherPaneBase implements Pane {
@Override
public @BackPressResult int handleBackPress() {
@Nullable
TabSwitcherPaneCoordinator coordinator = mTabSwitcherPaneCoordinatorSupplier.get();
if (coordinator == null) return BackPressResult.FAILURE;
return coordinator.handleBackPress();
@ -131,6 +177,7 @@ public abstract class TabSwitcherPaneBase implements Pane {
if (mNativeInitialized) return;
mNativeInitialized = true;
@Nullable
TabSwitcherPaneCoordinator coordinator = mTabSwitcherPaneCoordinatorSupplier.get();
if (coordinator != null) {
coordinator.initWithNative();
@ -139,6 +186,7 @@ public abstract class TabSwitcherPaneBase implements Pane {
/** Returns a {@link Supplier} that provides dialog visibility information. */
public @Nullable Supplier<Boolean> getTabGridDialogVisibilitySupplier() {
@Nullable
TabSwitcherPaneCoordinator coordinator = mTabSwitcherPaneCoordinatorSupplier.get();
if (coordinator == null) return null;
return coordinator.getTabGridDialogVisibilitySupplier();
@ -146,6 +194,7 @@ public abstract class TabSwitcherPaneBase implements Pane {
/** Returns a {@link TabSwitcherCustomViewManager} for supplying custom views. */
public @Nullable TabSwitcherCustomViewManager getTabSwitcherCustomViewManager() {
@Nullable
TabSwitcherPaneCoordinator coordinator = mTabSwitcherPaneCoordinatorSupplier.get();
if (coordinator == null) return null;
return coordinator.getTabSwitcherCustomViewManager();
@ -153,6 +202,7 @@ public abstract class TabSwitcherPaneBase implements Pane {
/** Returns the number of elements in the tab switcher's tab list model. */
public int getTabSwitcherTabListModelSize() {
@Nullable
TabSwitcherPaneCoordinator coordinator = mTabSwitcherPaneCoordinatorSupplier.get();
if (coordinator == null) return 0;
return coordinator.getTabSwitcherTabListModelSize();
@ -160,11 +210,17 @@ public abstract class TabSwitcherPaneBase implements Pane {
/** Set the tab switcher's RecyclerViewPosition. */
public void setTabSwitcherRecyclerViewPosition(RecyclerViewPosition position) {
@Nullable
TabSwitcherPaneCoordinator coordinator = mTabSwitcherPaneCoordinatorSupplier.get();
if (coordinator == null) return;
coordinator.setTabSwitcherRecyclerViewPosition(position);
}
protected @TabListMode int getTabListMode() {
return mFactory.getTabListMode();
}
@VisibleForTesting(otherwise = VisibleForTesting.PROTECTED)
protected @Nullable TabSwitcherPaneCoordinator getTabSwitcherPaneCoordinator() {
return mTabSwitcherPaneCoordinatorSupplier.get();
}
@ -173,7 +229,7 @@ public abstract class TabSwitcherPaneBase implements Pane {
void createTabSwitcherPaneCoordinator() {
if (mTabSwitcherPaneCoordinatorSupplier.hasValue()) return;
TabSwitcherPaneCoordinator coordinator = mFactory.create(mRootView);
@NonNull TabSwitcherPaneCoordinator coordinator = mFactory.create(mRootView);
mTabSwitcherPaneCoordinatorSupplier.set(coordinator);
if (mNativeInitialized) {
coordinator.initWithNative();
@ -183,7 +239,7 @@ public abstract class TabSwitcherPaneBase implements Pane {
protected void destroyTabSwitcherPaneCoordinator() {
if (!mTabSwitcherPaneCoordinatorSupplier.hasValue()) return;
TabSwitcherPaneCoordinator coordinator = mTabSwitcherPaneCoordinatorSupplier.get();
@NonNull TabSwitcherPaneCoordinator coordinator = mTabSwitcherPaneCoordinatorSupplier.get();
mTabSwitcherPaneCoordinatorSupplier.set(null);
mRootView.removeAllViews();
coordinator.destroy();

@ -11,6 +11,7 @@ import androidx.annotation.Nullable;
import org.chromium.base.supplier.ObservableSupplier;
import org.chromium.base.supplier.ObservableSupplierImpl;
import org.chromium.base.supplier.Supplier;
import org.chromium.chrome.browser.tabmodel.TabList;
import org.chromium.components.browser_ui.widget.gesture.BackPressHandler;
import org.chromium.components.browser_ui.widget.gesture.BackPressHandler.BackPressResult;
@ -29,6 +30,20 @@ public class TabSwitcherPaneCoordinator implements BackPressHandler {
/** Post native initialization. */
public void initWithNative() {}
/** Shows the tab list editor. */
public void showTabListEditor() {
assert false : "Not implemented.";
}
/**
* Resets the UI with the specified tabs.
*
* @param tabList The {@link TabList} to show tabs for.
*/
public void resetWithTabList(@Nullable TabList tabList) {
assert false : "Not implemented.";
}
/** Returns a {@link Supplier} that provides dialog visibility information. */
public @Nullable Supplier<Boolean> getTabGridDialogVisibilitySupplier() {
assert false : "Not implemented.";

@ -6,6 +6,8 @@ package org.chromium.chrome.browser.tasks.tab_management;
import android.view.ViewGroup;
import org.chromium.chrome.browser.tasks.tab_management.TabListCoordinator.TabListMode;
/** Holds dependencies for constructing a {@link TabSwitcherPane}. */
public class TabSwitcherPaneCoordinatorFactory {
TabSwitcherPaneCoordinatorFactory() {}
@ -20,4 +22,13 @@ public class TabSwitcherPaneCoordinatorFactory {
TabSwitcherPaneCoordinator create(ViewGroup parentView) {
return new TabSwitcherPaneCoordinator(parentView);
}
/** Returns the {@link TabListMode} of the produced {@link TabListCoordinator}s. */
public @TabListMode int getTabListMode() {
// This value will be determined at initialization time based on whether the device is
// low-end and is not subject to change. Certain behaviors are limited to LIST vs GRID
// mode and this information may be required even if a coordinator does not exist.
assert false : "Not implemented.";
return TabListMode.NUM_ENTRIES;
}
}

@ -11,6 +11,7 @@ import static org.junit.Assert.assertNull;
import static org.junit.Assert.assertTrue;
import static org.mockito.ArgumentMatchers.any;
import static org.mockito.ArgumentMatchers.isNull;
import static org.mockito.Mockito.atLeastOnce;
import static org.mockito.Mockito.doAnswer;
import static org.mockito.Mockito.never;
import static org.mockito.Mockito.times;
@ -18,6 +19,8 @@ import static org.mockito.Mockito.verify;
import static org.mockito.Mockito.when;
import android.content.Context;
import android.content.SharedPreferences;
import android.content.SharedPreferences.OnSharedPreferenceChangeListener;
import android.view.View;
import androidx.appcompat.content.res.AppCompatResources;
@ -29,12 +32,16 @@ import org.junit.Before;
import org.junit.Rule;
import org.junit.Test;
import org.junit.runner.RunWith;
import org.mockito.ArgumentCaptor;
import org.mockito.Captor;
import org.mockito.Mock;
import org.mockito.junit.MockitoJUnit;
import org.mockito.junit.MockitoRule;
import org.robolectric.shadows.ShadowLooper;
import org.chromium.base.supplier.ObservableSupplier;
import org.chromium.base.supplier.ObservableSupplierImpl;
import org.chromium.base.supplier.OneshotSupplierImpl;
import org.chromium.base.test.BaseRobolectricTestRunner;
import org.chromium.chrome.browser.hub.DisplayButtonData;
import org.chromium.chrome.browser.hub.FullButtonData;
@ -42,7 +49,15 @@ import org.chromium.chrome.browser.hub.HubContainerView;
import org.chromium.chrome.browser.hub.HubLayoutAnimationType;
import org.chromium.chrome.browser.hub.LoadHint;
import org.chromium.chrome.browser.hub.PaneId;
import org.chromium.chrome.browser.price_tracking.PriceTrackingFeatures;
import org.chromium.chrome.browser.price_tracking.PriceTrackingUtilities;
import org.chromium.chrome.browser.profiles.Profile;
import org.chromium.chrome.browser.profiles.ProfileProvider;
import org.chromium.chrome.browser.tabmodel.TabModelFilter;
import org.chromium.chrome.browser.tasks.tab_management.TabListCoordinator.TabListMode;
import org.chromium.chrome.browser.toolbar.TabSwitcherDrawable;
import org.chromium.components.browser_ui.widget.MenuOrKeyboardActionController;
import org.chromium.components.browser_ui.widget.MenuOrKeyboardActionController.MenuOrKeyboardActionHandler;
import org.chromium.components.browser_ui.widget.gesture.BackPressHandler.BackPressResult;
/** Unit tests for {@link TabSwitcherPane} and {@link TabSwitcherPaneBase}. */
@ -50,13 +65,23 @@ import org.chromium.components.browser_ui.widget.gesture.BackPressHandler.BackPr
public class TabSwitcherPaneUnitTest {
@Rule public MockitoRule mMockitoRule = MockitoJUnit.rule();
@Mock private SharedPreferences mSharedPreferences;
@Mock private Profile mProfile;
@Mock private ProfileProvider mProfileProvider;
@Mock private TabSwitcherPaneCoordinatorFactory mTabSwitcherPaneCoordinatorFactory;
@Mock private TabSwitcherPaneCoordinator mTabSwitcherPaneCoordinator;
@Mock private TabSwitcherPaneDrawableCoordinator mTabSwitcherPaneDrawableCoordinator;
@Mock private TabSwitcherDrawable mTabSwitcherDrawable;
@Mock private HubContainerView mHubContainerView;
@Mock private View.OnClickListener mNewTabButtonClickListener;
@Mock private TabModelFilter mTabModelFilter;
@Mock private MenuOrKeyboardActionController mMenuOrKeyboardActionController;
@Captor ArgumentCaptor<MenuOrKeyboardActionHandler> mMenuOrKeyboardActionHandlerCaptor;
@Captor ArgumentCaptor<OnSharedPreferenceChangeListener> mPriceAnnotationsPrefListenerCaptor;
private final OneshotSupplierImpl<ProfileProvider> mProfileProviderSupplier =
new OneshotSupplierImpl<>();
private Context mContext;
private ObservableSupplierImpl<Boolean> mHandleBackPressChangeSupplier =
new ObservableSupplierImpl<>();
@ -67,6 +92,13 @@ public class TabSwitcherPaneUnitTest {
public void setUp() {
mContext = ApplicationProvider.getApplicationContext();
when(mHubContainerView.getContext()).thenReturn(mContext);
PriceTrackingFeatures.setPriceTrackingEnabledForTesting(true);
PriceTrackingFeatures.setIsSignedInAndSyncEnabledForTesting(true);
when(mProfileProvider.getOriginalProfile()).thenReturn(mProfile);
mProfileProviderSupplier.set(mProfileProvider);
doAnswer(
invocation -> {
mTimesCreated++;
@ -74,9 +106,9 @@ public class TabSwitcherPaneUnitTest {
})
.when(mTabSwitcherPaneCoordinatorFactory)
.create(any());
when(mTabSwitcherPaneCoordinatorFactory.getTabListMode()).thenReturn(TabListMode.GRID);
when(mTabSwitcherPaneCoordinator.getHandleBackPressChangedSupplier())
.thenReturn(mHandleBackPressChangeSupplier);
mHandleBackPressChangeSupplier.set(false);
when(mTabSwitcherPaneDrawableCoordinator.getTabSwitcherDrawable())
.thenReturn(mTabSwitcherDrawable);
@ -92,15 +124,28 @@ public class TabSwitcherPaneUnitTest {
mTabSwitcherPane =
new TabSwitcherPane(
mContext,
mSharedPreferences,
mProfileProviderSupplier,
mTabSwitcherPaneCoordinatorFactory,
() -> mTabModelFilter,
mNewTabButtonClickListener,
mMenuOrKeyboardActionController,
mTabSwitcherPaneDrawableCoordinator);
ShadowLooper.runUiThreadTasks();
verify(mSharedPreferences)
.registerOnSharedPreferenceChangeListener(
mPriceAnnotationsPrefListenerCaptor.capture());
}
@After
public void tearDown() {
mTabSwitcherPane.destroy();
verify(mTabSwitcherPaneCoordinator, times(mTimesCreated)).destroy();
verify(mMenuOrKeyboardActionController, atLeastOnce())
.unregisterMenuOrKeyboardActionHandler(any());
verify(mSharedPreferences)
.unregisterOnSharedPreferenceChangeListener(
mPriceAnnotationsPrefListenerCaptor.getValue());
}
@Test
@ -209,6 +254,83 @@ public class TabSwitcherPaneUnitTest {
.getPlannedAnimationType());
}
@Test
@SmallTest
public void testPriceTracking() {
mTabSwitcherPane.notifyLoadHint(LoadHint.HOT);
assertNull(mTabSwitcherPane.getTabSwitcherPaneCoordinator());
when(mTabModelFilter.isCurrentlySelectedFilter()).thenReturn(true);
when(mTabModelFilter.isTabModelRestored()).thenReturn(true);
OnSharedPreferenceChangeListener listener = mPriceAnnotationsPrefListenerCaptor.getValue();
// Check this doesn't crash if there is no coordinator.
listener.onSharedPreferenceChanged(
mSharedPreferences, PriceTrackingUtilities.TRACK_PRICES_ON_TABS);
mTabSwitcherPane.initWithNative();
mTabSwitcherPane.createTabSwitcherPaneCoordinator();
TabSwitcherPaneCoordinator coordinator = mTabSwitcherPane.getTabSwitcherPaneCoordinator();
listener.onSharedPreferenceChanged(
mSharedPreferences, PriceTrackingUtilities.TRACK_PRICES_ON_TABS);
verify(coordinator).resetWithTabList(mTabModelFilter);
when(mTabModelFilter.isTabModelRestored()).thenReturn(false);
listener.onSharedPreferenceChanged(
mSharedPreferences, PriceTrackingUtilities.TRACK_PRICES_ON_TABS);
verify(coordinator).resetWithTabList(mTabModelFilter);
when(mTabModelFilter.isTabModelRestored()).thenReturn(true);
when(mTabModelFilter.isCurrentlySelectedFilter()).thenReturn(false);
listener.onSharedPreferenceChanged(
mSharedPreferences, PriceTrackingUtilities.TRACK_PRICES_ON_TABS);
verify(coordinator).resetWithTabList(mTabModelFilter);
when(mTabModelFilter.isTabModelRestored()).thenReturn(true);
listener.onSharedPreferenceChanged(mSharedPreferences, "foo");
verify(coordinator).resetWithTabList(mTabModelFilter);
mTabSwitcherPane.notifyLoadHint(LoadHint.WARM);
listener.onSharedPreferenceChanged(
mSharedPreferences, PriceTrackingUtilities.TRACK_PRICES_ON_TABS);
verify(coordinator).resetWithTabList(mTabModelFilter);
}
@Test
@SmallTest
public void testShowTabListEditor() {
verify(mMenuOrKeyboardActionController, never()).registerMenuOrKeyboardActionHandler(any());
mTabSwitcherPane.notifyLoadHint(LoadHint.HOT);
verify(mMenuOrKeyboardActionController)
.registerMenuOrKeyboardActionHandler(mMenuOrKeyboardActionHandlerCaptor.capture());
MenuOrKeyboardActionHandler handler = mMenuOrKeyboardActionHandlerCaptor.getValue();
// Check this doesn't crash if there is no coordinator.
assertFalse(
handler.handleMenuOrKeyboardAction(
org.chromium.chrome.tab_ui.R.id.menu_select_tabs, false));
mTabSwitcherPane.initWithNative();
mTabSwitcherPane.createTabSwitcherPaneCoordinator();
TabSwitcherPaneCoordinator coordinator = mTabSwitcherPane.getTabSwitcherPaneCoordinator();
assertFalse(
handler.handleMenuOrKeyboardAction(
org.chromium.chrome.tab_ui.R.id.new_tab_menu_id, false));
verify(coordinator, never()).showTabListEditor();
assertTrue(
handler.handleMenuOrKeyboardAction(
org.chromium.chrome.tab_ui.R.id.menu_select_tabs, false));
verify(coordinator).showTabListEditor();
mTabSwitcherPane.notifyLoadHint(LoadHint.WARM);
verify(mMenuOrKeyboardActionController).unregisterMenuOrKeyboardActionHandler(handler);
mTabSwitcherPane.notifyLoadHint(LoadHint.COLD);
verify(mMenuOrKeyboardActionController, times(2))
.unregisterMenuOrKeyboardActionHandler(handler);
}
@Test
@SmallTest
public void testGetDialogVisibilitySupplier() {