// Copyright 2014 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.tabmodel;
import org.chromium.chrome.browser.tab.Tab;
import org.chromium.chrome.browser.tab.TabAttributeKeys;
import org.chromium.chrome.browser.tab.TabAttributes;
import org.chromium.chrome.browser.tab.TabLaunchType;
/**
* Implementation of the TabModelOrderController based off of tab_strip_model_order_controller.cc
* and tab_strip_model.cc
*
* <p>TODO(crbug.com/40152902): Move to chrome/browser/tabmodel/internal when all usages are
* modularized.
*/
class TabModelOrderControllerImpl implements TabModelOrderController {
private static final int NO_TAB = -1;
private final TabModelSelector mTabModelSelector;
public TabModelOrderControllerImpl(TabModelSelector modelSelector) {
mTabModelSelector = modelSelector;
}
@Override
public int determineInsertionIndex(@TabLaunchType int type, int position, Tab newTab) {
if (type == TabLaunchType.FROM_BROWSER_ACTIONS || type == TabLaunchType.FROM_RECENT_TABS) {
return -1;
}
if (linkClicked(type)) {
position = determineInsertionIndex(type, newTab);
}
if (willOpenInForeground(type, newTab.isIncognitoBranded())) {
// Forget any existing relationships, we don't want to make things
// too confusing by having multiple groups active at the same time.
forgetAllOpeners();
}
// TODO(crbug.com/40877620): This is a bandaid fix to ensure tab groups are contiguous such
// that
// no tabs within a group are separate from one another and that no tab that is not part of
// a group can be added in-between members of a group. This doesn't address the issue of
// moving tabs to be between members of a group, however when a group is moved it is moved
// tab-by-tab so it is difficult to enforce anything there without significant refactoring.
position = getValidPositionConsideringRelatedTabs(newTab, position);
return position;
}
@Override
public int determineInsertionIndex(@TabLaunchType int type, Tab newTab) {
TabModel currentModel = mTabModelSelector.getCurrentModel();
if (sameModelType(currentModel, newTab)) {
Tab currentTab = TabModelUtils.getCurrentTab(currentModel);
if (currentTab == null) {
assert (currentModel.getCount() == 0);
return 0;
}
int currentId = currentTab.getId();
int currentIndex = TabModelUtils.getTabIndexById(currentModel, currentId);
if (willOpenInForeground(type, newTab.isIncognito())) {
// If the tab was opened in the foreground, insert it adjacent to its parent tab if
// that exists and that tab is not the current selected tab, else insert the tab
// adjacent to the current tab that opened that link.
Tab parentTab = currentModel.getTabById(newTab.getParentId());
if (parentTab != null && currentTab != parentTab) {
int parentTabIndex =
TabModelUtils.getTabIndexById(currentModel, parentTab.getId());
return parentTabIndex + 1;
}
return currentIndex + 1;
} else {
// If the tab was opened in the background, position at the end of
// it's 'group'.
int index = getIndexOfLastTabOpenedBy(currentId, currentIndex);
if (index != NO_TAB) {
return index + 1;
} else {
return currentIndex + 1;
}
}
} else {
// If the tab is opening in the other model type, just put it at the end.
return mTabModelSelector.getModel(newTab.isIncognito()).getCount();
}
}
/**
* Returns the index of the last tab in the model opened by the specified
* opener, starting at startIndex. To clarify, the tabs are traversed in the
* descending order of their position in the model. This means that the tab
* furthest in the stack with the given opener id will be returned.
*
* @param openerId The opener of interest.
* @param startIndex The start point of the search.
* @return The last tab if found, NO_TAB otherwise.
*/
private int getIndexOfLastTabOpenedBy(int openerId, int startIndex) {
TabModel currentModel = mTabModelSelector.getCurrentModel();
int count = currentModel.getCount();
for (int i = count - 1; i >= startIndex; i--) {
Tab tab = currentModel.getTabAt(i);
if (tab.getParentId() == openerId
&& TabAttributes.from(tab).get(TabAttributeKeys.GROUPED_WITH_PARENT, true)) {
return i;
}
}
return NO_TAB;
}
private int getValidPositionConsideringRelatedTabs(Tab newTab, int position) {
TabModelFilter filter =
mTabModelSelector
.getTabModelFilterProvider()
.getTabModelFilter(newTab.isIncognito());
return filter.getValidPosition(newTab, position);
}
/** Clear the opener attribute on all tabs in the model. */
void forgetAllOpeners() {
TabModel currentModel = mTabModelSelector.getCurrentModel();
int count = currentModel.getCount();
for (int i = 0; i < count; i++) {
TabAttributes.from(currentModel.getTabAt(i))
.set(TabAttributeKeys.GROUPED_WITH_PARENT, false);
}
}
/** Determine if a launch type is the result of linked being clicked. */
static boolean linkClicked(@TabLaunchType int type) {
return type == TabLaunchType.FROM_LINK
|| type == TabLaunchType.FROM_LONGPRESS_FOREGROUND
|| type == TabLaunchType.FROM_LONGPRESS_BACKGROUND
|| type == TabLaunchType.FROM_LONGPRESS_BACKGROUND_IN_GROUP
|| type == TabLaunchType.FROM_LONGPRESS_INCOGNITO;
}
@Override
public boolean willOpenInForeground(@TabLaunchType int type, boolean isNewTabIncognitoBranded) {
// Restore is handling the active index by itself.
if (type == TabLaunchType.FROM_RESTORE
|| type == TabLaunchType.FROM_BROWSER_ACTIONS
|| type == TabLaunchType.FROM_RESTORE_TABS_UI) {
return false;
}
return type != TabLaunchType.FROM_LONGPRESS_BACKGROUND
&& type != TabLaunchType.FROM_LONGPRESS_BACKGROUND_IN_GROUP
&& type != TabLaunchType.FROM_RECENT_TABS
&& type != TabLaunchType.FROM_SYNC_BACKGROUND
|| (!mTabModelSelector.isIncognitoBrandedModelSelected()
&& isNewTabIncognitoBranded);
}
/**
* @return {@code true} If both tabs have the same model type, {@code false} otherwise.
*/
static boolean sameModelType(TabModel model, Tab tab) {
return model.isIncognito() == tab.isIncognito();
}
}