blob: f565fa2ccf88330b7d2488d3e53b8a7166a9ec4b [file] [log] [blame]
// Copyright 2020 The Chromium Authors. All rights reserved.
// 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;
import static org.chromium.chrome.browser.tasks.SingleTabViewProperties.CLICK_LISTENER;
import static org.chromium.chrome.browser.tasks.SingleTabViewProperties.FAVICON;
import static org.chromium.chrome.browser.tasks.SingleTabViewProperties.IS_VISIBLE;
import static org.chromium.chrome.browser.tasks.SingleTabViewProperties.TITLE;
import android.content.Context;
import android.graphics.drawable.Drawable;
import android.os.SystemClock;
import android.text.TextUtils;
import androidx.annotation.VisibleForTesting;
import org.chromium.base.ObserverList;
import org.chromium.base.StrictModeContext;
import org.chromium.base.metrics.RecordUserAction;
import org.chromium.chrome.browser.compositor.layouts.LayoutManagerImpl;
import org.chromium.chrome.browser.flags.CachedFeatureFlags;
import org.chromium.chrome.browser.flags.ChromeFeatureList;
import org.chromium.chrome.browser.tab.EmptyTabObserver;
import org.chromium.chrome.browser.tab.Tab;
import org.chromium.chrome.browser.tab.TabObserver;
import org.chromium.chrome.browser.tab.TabSelectionType;
import org.chromium.chrome.browser.tabmodel.TabList;
import org.chromium.chrome.browser.tabmodel.TabModel;
import org.chromium.chrome.browser.tabmodel.TabModelObserver;
import org.chromium.chrome.browser.tabmodel.TabModelSelector;
import org.chromium.chrome.browser.tabmodel.TabModelSelectorObserver;
import org.chromium.chrome.browser.tasks.pseudotab.PseudoTab;
import org.chromium.chrome.browser.tasks.tab_management.TabListFaviconProvider;
import org.chromium.chrome.browser.tasks.tab_management.TabSwitcher;
import org.chromium.chrome.browser.tasks.tab_management.TabUiFeatureUtilities;
import org.chromium.chrome.features.start_surface.StartSurfaceConfiguration;
import org.chromium.chrome.features.start_surface.StartSurfaceUserData;
import org.chromium.ui.modelutil.PropertyModel;
import org.chromium.url.GURL;
/** Mediator of the single tab tab switcher. */
public class SingleTabSwitcherMediator implements TabSwitcher.Controller {
@VisibleForTesting
public static final String SINGLE_TAB_TITLE_AVAILABLE_TIME_UMA = "SingleTabTitleAvailableTime";
private final ObserverList<TabSwitcher.OverviewModeObserver> mObservers = new ObserverList<>();
private final TabModelSelector mTabModelSelector;
private final PropertyModel mPropertyModel;
private final TabListFaviconProvider mTabListFaviconProvider;
private final TabModelObserver mNormalTabModelObserver;
private final TabModelSelectorObserver mTabModelSelectorObserver;
private TabSwitcher.OnTabSelectingListener mTabSelectingListener;
private boolean mShouldIgnoreNextSelect;
private boolean mSelectedTabDidNotChangedAfterShown;
private boolean mAddNormalTabModelObserverPending;
private Long mTabTitleAvailableTime;
private boolean mFaviconInitialized;
private Context mContext;
SingleTabSwitcherMediator(Context context, PropertyModel propertyModel,
TabModelSelector tabModelSelector, TabListFaviconProvider tabListFaviconProvider) {
mTabModelSelector = tabModelSelector;
mPropertyModel = propertyModel;
mTabListFaviconProvider = tabListFaviconProvider;
mContext = context;
mPropertyModel.set(FAVICON, mTabListFaviconProvider.getDefaultFaviconDrawable(false));
mPropertyModel.set(CLICK_LISTENER, v -> {
if (mTabSelectingListener != null
&& mTabModelSelector.getCurrentTabId() != TabList.INVALID_TAB_INDEX) {
selectTheCurrentTab();
StartSurfaceUserData.setOpenedFromStart(mTabModelSelector.getCurrentTab());
}
});
mNormalTabModelObserver = new TabModelObserver() {
@Override
public void didSelectTab(Tab tab, int type, int lastId) {
if (mTabModelSelector.isIncognitoSelected()) return;
assert overviewVisible();
mSelectedTabDidNotChangedAfterShown = false;
updateSelectedTab(tab);
if (type == TabSelectionType.FROM_CLOSE || mShouldIgnoreNextSelect) {
mShouldIgnoreNextSelect = false;
return;
}
mTabSelectingListener.onTabSelecting(LayoutManagerImpl.time(), tab.getId());
}
};
mTabModelSelectorObserver = new TabModelSelectorObserver() {
@Override
public void onTabModelSelected(TabModel newModel, TabModel oldModel) {
if (!newModel.isIncognito()) mShouldIgnoreNextSelect = true;
}
@Override
public void onTabStateInitialized() {
TabModel normalTabModel = mTabModelSelector.getModel(false);
if (mAddNormalTabModelObserverPending) {
mAddNormalTabModelObserverPending = false;
mTabModelSelector.getTabModelFilterProvider().addTabModelFilterObserver(
mNormalTabModelObserver);
}
int selectedTabIndex = normalTabModel.index();
if (selectedTabIndex != TabList.INVALID_TAB_INDEX) {
assert normalTabModel.getCount() > 0;
Tab tab = normalTabModel.getTabAt(selectedTabIndex);
mPropertyModel.set(TITLE, tab.getTitle());
if (mTabTitleAvailableTime == null) {
mTabTitleAvailableTime = SystemClock.elapsedRealtime();
}
// Favicon should be updated here unless mTabListFaviconProvider hasn't been
// initialized yet.
assert !mFaviconInitialized;
if (mTabListFaviconProvider.isInitialized()) {
mFaviconInitialized = true;
updateFavicon(tab);
}
}
}
};
}
void initWithNative() {
if (mFaviconInitialized || !mTabModelSelector.isTabStateInitialized()) return;
TabModel normalTabModel = mTabModelSelector.getModel(false);
int selectedTabIndex = normalTabModel.index();
if (selectedTabIndex != TabList.INVALID_TAB_INDEX) {
assert normalTabModel.getCount() > 0;
Tab tab = normalTabModel.getTabAt(selectedTabIndex);
updateFavicon(tab);
mFaviconInitialized = true;
}
}
private void updateFavicon(Tab tab) {
assert mTabListFaviconProvider.isInitialized();
mTabListFaviconProvider.getFaviconDrawableForUrlAsync(tab.getUrl(), false,
(Drawable favicon) -> { mPropertyModel.set(FAVICON, favicon); });
}
void setOnTabSelectingListener(TabSwitcher.OnTabSelectingListener listener) {
mTabSelectingListener = listener;
}
// Controller implementation
@Override
public boolean overviewVisible() {
return mPropertyModel.get(IS_VISIBLE);
}
@Override
public void addOverviewModeObserver(TabSwitcher.OverviewModeObserver observer) {
mObservers.addObserver(observer);
}
@Override
public void removeOverviewModeObserver(TabSwitcher.OverviewModeObserver observer) {
mObservers.removeObserver(observer);
}
@Override
public void hideOverview(boolean animate) {
mShouldIgnoreNextSelect = false;
mTabModelSelector.getTabModelFilterProvider().removeTabModelFilterObserver(
mNormalTabModelObserver);
mTabModelSelector.removeObserver(mTabModelSelectorObserver);
mPropertyModel.set(IS_VISIBLE, false);
mPropertyModel.set(FAVICON, mTabListFaviconProvider.getDefaultFaviconDrawable(false));
mPropertyModel.set(TITLE, "");
for (TabSwitcher.OverviewModeObserver observer : mObservers) {
observer.startedHiding();
}
for (TabSwitcher.OverviewModeObserver observer : mObservers) {
observer.finishedHiding();
}
}
@Override
public void showOverview(boolean animate) {
mSelectedTabDidNotChangedAfterShown = true;
mTabModelSelector.addObserver(mTabModelSelectorObserver);
if (CachedFeatureFlags.isEnabled(ChromeFeatureList.INSTANT_START)
&& !mTabModelSelector.isTabStateInitialized()) {
mAddNormalTabModelObserverPending = true;
PseudoTab activeTab;
try (StrictModeContext ignored = StrictModeContext.allowDiskReads()) {
activeTab = PseudoTab.getActiveTabFromStateFile(mContext);
}
if (activeTab != null) {
mPropertyModel.set(TITLE, activeTab.getTitle());
if (mTabTitleAvailableTime == null) {
mTabTitleAvailableTime = SystemClock.elapsedRealtime();
}
}
} else {
mTabModelSelector.getTabModelFilterProvider().addTabModelFilterObserver(
mNormalTabModelObserver);
TabModel normalTabModel = mTabModelSelector.getModel(false);
int selectedTabIndex = normalTabModel.index();
if (selectedTabIndex != TabList.INVALID_TAB_INDEX) {
assert normalTabModel.getCount() > 0;
updateSelectedTab(normalTabModel.getTabAt(selectedTabIndex));
if (mTabTitleAvailableTime == null) {
mTabTitleAvailableTime = SystemClock.elapsedRealtime();
}
}
}
mPropertyModel.set(IS_VISIBLE, true);
for (TabSwitcher.OverviewModeObserver observer : mObservers) {
observer.startedShowing();
}
for (TabSwitcher.OverviewModeObserver observer : mObservers) {
observer.finishedShowing();
}
}
@Override
public boolean onBackPressed(boolean isOnHomepage) {
// If currently on the Start surface, we will stop here. The back button will be handled by
// the ChromeTabbedActivity. See https://crbug.com/1187714.
if (isOnHomepage) return false;
if (overviewVisible() && !mTabModelSelector.isIncognitoSelected()
&& mTabModelSelector.getCurrentTabId() != TabList.INVALID_TAB_INDEX) {
selectTheCurrentTab();
return true;
}
return false;
}
@Override
public void enableRecordingFirstMeaningfulPaint(long activityCreateTimeMs) {}
@Override
public void onOverviewShownAtLaunch(long activityCreationTimeMs) {
if (mTabTitleAvailableTime == null) return;
StartSurfaceConfiguration.recordHistogram(SINGLE_TAB_TITLE_AVAILABLE_TIME_UMA,
mTabTitleAvailableTime - activityCreationTimeMs,
TabUiFeatureUtilities.supportInstantStart(false, mContext));
}
@Override
public boolean isDialogVisible() {
return false;
}
private void updateSelectedTab(Tab tab) {
if (tab.isLoading() && TextUtils.isEmpty(tab.getTitle())) {
TabObserver tabObserver = new EmptyTabObserver() {
@Override
public void onPageLoadFinished(Tab tab, GURL url) {
super.onPageLoadFinished(tab, url);
mPropertyModel.set(TITLE, tab.getTitle());
tab.removeObserver(this);
}
};
tab.addObserver(tabObserver);
} else {
mPropertyModel.set(TITLE, tab.getTitle());
}
mTabListFaviconProvider.getFaviconDrawableForUrlAsync(tab.getUrl(), false,
(Drawable favicon) -> { mPropertyModel.set(FAVICON, favicon); });
}
private void selectTheCurrentTab() {
assert !mTabModelSelector.isIncognitoSelected();
if (mSelectedTabDidNotChangedAfterShown) {
RecordUserAction.record("MobileTabReturnedToCurrentTab.SingleTabCard");
}
mTabSelectingListener.onTabSelecting(
LayoutManagerImpl.time(), mTabModelSelector.getCurrentTabId());
}
}