// Copyright (c) 2012 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; import android.app.Activity; import android.content.Context; import android.graphics.Color; import android.view.View; import org.chromium.base.CalledByNative; import org.chromium.base.ObserverList; import org.chromium.chrome.browser.infobar.AutoLoginProcessor; import org.chromium.chrome.browser.infobar.InfoBarContainer; import org.chromium.chrome.browser.profiles.Profile; import org.chromium.chrome.browser.ui.toolbar.ToolbarModelSecurityLevel; import org.chromium.content.browser.ContentView; import org.chromium.content.browser.ContentViewClient; import org.chromium.content.browser.ContentViewCore; import org.chromium.content.browser.NavigationClient; import org.chromium.content.browser.NavigationHistory; import org.chromium.content.browser.PageInfo; import org.chromium.content.browser.WebContentsObserverAndroid; import org.chromium.ui.WindowAndroid; import java.util.Iterator; import java.util.concurrent.atomic.AtomicInteger; /** * The basic Java representation of a tab. Contains and manages a {@link ContentView}. * * TabBase provides common functionality for ChromiumTestshell's Tab as well as Chrome on Android's * tab. It's intended to be extended both on Java and C++, with ownership managed by the subclass. * Because of the inner-workings of JNI, the subclass is responsible for constructing the native * subclass which in turn constructs TabAndroid (the native counterpart to TabBase) which in turn * sets the native pointer for TabBase. The same is true for destruction. The Java subclass must be * destroyed which will call into the native subclass and finally lead to the destruction of the * parent classes. */ public abstract class TabBase implements NavigationClient { public static final int INVALID_TAB_ID = -1; /** Used for automatically generating tab ids. */ private static final AtomicInteger sIdCounter = new AtomicInteger(); private int mNativeTabAndroid; /** Unique id of this tab (within its container). */ private final int mId; /** Whether or not this tab is an incognito tab. */ private final boolean mIncognito; /** An Application {@link Context}. Unlike {@link #mContext}, this is the only one that is * publicly exposed to help prevent leaking the {@link Activity}. */ private final Context mApplicationContext; /** The {@link Context} used to create {@link View}s and other Android components. Unlike * {@link #mApplicationContext}, this is not publicly exposed to help prevent leaking the * {@link Activity}. */ private final Context mContext; /** Gives {@link TabBase} a way to interact with the Android window. */ private final WindowAndroid mWindowAndroid; /** The current native page (e.g. chrome-native://newtab), or {@code null} if there is none. */ private NativePage mNativePage; /** The {@link ContentView} showing the current page or {@code null} if the tab is frozen. */ private ContentView mContentView; /** InfoBar container to show InfoBars for this tab. */ private InfoBarContainer mInfoBarContainer; /** The sync id of the TabBase if session sync is enabled. */ private int mSyncId; /** * The {@link ContentViewCore} for the current page, provided for convenience. This always * equals {@link ContentView#getContentViewCore()}, or {@code null} if mContentView is * {@code null}. */ private ContentViewCore mContentViewCore; /** * A list of TabBase observers. These are used to broadcast TabBase events to listeners. */ private final ObserverList<TabObserver> mObservers = new ObserverList<TabObserver>(); // Content layer Observers and Delegates private ContentViewClient mContentViewClient; private WebContentsObserverAndroid mWebContentsObserver; private TabBaseChromeWebContentsDelegateAndroid mWebContentsDelegate; /** * A basic {@link ChromeWebContentsDelegateAndroid} that forwards some calls to the registered * {@link TabObserver}s. Meant to be overridden by subclasses. */ public class TabBaseChromeWebContentsDelegateAndroid extends ChromeWebContentsDelegateAndroid { @Override public void onLoadProgressChanged(int progress) { for (TabObserver observer : mObservers) { observer.onLoadProgressChanged(TabBase.this, progress); } } @Override public void onUpdateUrl(String url) { for (TabObserver observer : mObservers) observer.onUpdateUrl(TabBase.this, url); } @Override public void showRepostFormWarningDialog(final ContentViewCore contentViewCore) { RepostFormWarningDialog warningDialog = new RepostFormWarningDialog( new Runnable() { @Override public void run() { contentViewCore.cancelPendingReload(); } }, new Runnable() { @Override public void run() { contentViewCore.continuePendingReload(); } }); Activity activity = (Activity)mContext; warningDialog.show(activity.getFragmentManager(), null); } @Override public void toggleFullscreenModeForTab(boolean enableFullscreen) { for (TabObserver observer: mObservers) { observer.onToggleFullscreenMode(TabBase.this, enableFullscreen); } } } private class TabBaseWebContentsObserverAndroid extends WebContentsObserverAndroid { public TabBaseWebContentsObserverAndroid(ContentViewCore contentViewCore) { super(contentViewCore); } @Override public void navigationEntryCommitted() { if (getNativePage() != null) { pushNativePageStateToNavigationEntry(); } } @Override public void didFailLoad(boolean isProvisionalLoad, boolean isMainFrame, int errorCode, String description, String failingUrl) { for (TabObserver observer : mObservers) { observer.onDidFailLoad(TabBase.this, isProvisionalLoad, isMainFrame, errorCode, description, failingUrl); } } } /** * Creates an instance of a {@link TabBase} with no id. * @param incognito Whether or not this tab is incognito. * @param context An instance of a {@link Context}. * @param window An instance of a {@link WindowAndroid}. */ public TabBase(boolean incognito, Context context, WindowAndroid window) { this(INVALID_TAB_ID, incognito, context, window); } /** * Creates an instance of a {@link TabBase}. * @param id The id this tab should be identified with. * @param incognito Whether or not this tab is incognito. * @param context An instance of a {@link Context}. * @param window An instance of a {@link WindowAndroid}. */ public TabBase(int id, boolean incognito, Context context, WindowAndroid window) { // We need a valid Activity Context to build the ContentView with. assert context == null || context instanceof Activity; mId = generateValidId(id); mIncognito = incognito; // TODO(dtrainor): Only store application context here. mContext = context; mApplicationContext = context != null ? context.getApplicationContext() : null; mWindowAndroid = window; } /** * Adds a {@link TabObserver} to be notified on {@link TabBase} changes. * @param observer The {@link TabObserver} to add. */ public final void addObserver(TabObserver observer) { mObservers.addObserver(observer); } /** * Removes a {@link TabObserver}. * @param observer The {@link TabObserver} to remove. */ public final void removeObserver(TabObserver observer) { mObservers.removeObserver(observer); } /** * @return Whether or not this tab has a previous navigation entry. */ public boolean canGoBack() { return mContentViewCore != null && mContentViewCore.canGoBack(); } /** * @return Whether or not this tab has a navigation entry after the current one. */ public boolean canGoForward() { return mContentViewCore != null && mContentViewCore.canGoForward(); } /** * Goes to the navigation entry before the current one. */ public void goBack() { if (mContentViewCore != null) mContentViewCore.goBack(); } /** * Goes to the navigation entry after the current one. */ public void goForward() { if (mContentViewCore != null) mContentViewCore.goForward(); } @Override public NavigationHistory getDirectedNavigationHistory(boolean isForward, int itemLimit) { if (mContentViewCore != null) { return mContentViewCore.getDirectedNavigationHistory(isForward, itemLimit); } else { return new NavigationHistory(); } } @Override public void goToNavigationIndex(int index) { if (mContentViewCore != null) mContentViewCore.goToNavigationIndex(index); } /** * Loads the current navigation if there is a pending lazy load (after tab restore). */ public void loadIfNecessary() { if (mContentViewCore != null) mContentViewCore.loadIfNecessary(); } /** * Requests the current navigation to be loaded upon the next call to loadIfNecessary(). */ protected void requestRestoreLoad() { if (mContentViewCore != null) mContentViewCore.requestRestoreLoad(); } /** * @return Whether or not the {@link TabBase} is currently showing an interstitial page, such as * a bad HTTPS page. */ public boolean isShowingInterstitialPage() { ContentViewCore contentViewCore = getContentViewCore(); return contentViewCore != null && contentViewCore.isShowingInterstitialPage(); } /** * @return Whether or not the tab has something valid to render. */ public boolean isReady() { return mNativePage != null || (mContentViewCore != null && mContentViewCore.isReady()); } /** * @return The {@link View} displaying the current page in the tab. This might be a * {@link ContentView} but could potentially be any instance of {@link View}. This can * be {@code null}, if the tab is frozen or being initialized or destroyed. */ public View getView() { PageInfo pageInfo = getPageInfo(); return pageInfo != null ? pageInfo.getView() : null; } /** * @return The width of the content of this tab. Can be 0 if there is no content. */ public int getWidth() { View view = getView(); return view != null ? view.getWidth() : 0; } /** * @return The height of the content of this tab. Can be 0 if there is no content. */ public int getHeight() { View view = getView(); return view != null ? view.getHeight() : 0; } /** * @return The application {@link Context} associated with this tab. */ protected Context getApplicationContext() { return mApplicationContext; } /** * * @return The infobar container. */ public final InfoBarContainer getInfoBarContainer() { return mInfoBarContainer; } /** * Create an {@code AutoLoginProcessor} to decide how to handle login * requests. */ protected abstract AutoLoginProcessor createAutoLoginProcessor(); /** * Reloads the current page content if it is a {@link ContentView}. */ public void reload() { // TODO(dtrainor): Should we try to rebuild the ContentView if it's frozen? if (mContentViewCore != null) mContentViewCore.reload(true); } /** * Reloads the current page content if it is a {@link ContentView}. * This version ignores the cache and reloads from the network. */ public void reloadIgnoringCache() { if (mContentViewCore != null) mContentViewCore.reloadIgnoringCache(true); } /** Stop the current navigation. */ public void stopLoading() { if (mContentViewCore != null) mContentViewCore.stopLoading(); } /** * @return The background color of the tab. */ public int getBackgroundColor() { return getPageInfo() != null ? getPageInfo().getBackgroundColor() : Color.WHITE; } /** * @return The profile associated with this tab. */ public Profile getProfile() { if (mNativeTabAndroid == 0) return null; return nativeGetProfileAndroid(mNativeTabAndroid); } /** * @return The id representing this tab. */ @CalledByNative public int getId() { return mId; } /** * @return Whether or not this tab is incognito. */ public boolean isIncognito() { return mIncognito; } /** * @return The {@link ContentView} associated with the current page, or {@code null} if * there is no current page or the current page is displayed using something besides a * {@link ContentView}. */ public ContentView getContentView() { return mNativePage == null ? mContentView : null; } /** * @return The {@link ContentViewCore} associated with the current page, or {@code null} if * there is no current page or the current page is displayed using something besides a * {@link ContentView}. */ public ContentViewCore getContentViewCore() { return mNativePage == null ? mContentViewCore : null; } /** * @return A {@link PageInfo} describing the current page. This is always not {@code null} * except during initialization, destruction, and when the tab is frozen. */ public PageInfo getPageInfo() { return mNativePage != null ? mNativePage : mContentView; } /** * @return The {@link NativePage} associated with the current page, or {@code null} if there is * no current page or the current page is displayed using something besides * {@link NativePage}. */ public NativePage getNativePage() { return mNativePage; } /** * @return Whether or not the {@link TabBase} represents a {@link NativePage}. */ public boolean isNativePage() { return mNativePage != null; } /** * Set whether or not the {@link ContentViewCore} should be using a desktop user agent for the * currently loaded page. * @param useDesktop If {@code true}, use a desktop user agent. Otherwise use a mobile one. * @param reloadOnChange Reload the page if the user agent has changed. */ public void setUseDesktopUserAgent(boolean useDesktop, boolean reloadOnChange) { if (mContentViewCore != null) { mContentViewCore.setUseDesktopUserAgent(useDesktop, reloadOnChange); } } /** * @return Whether or not the {@link ContentViewCore} is using a desktop user agent. */ public boolean getUseDesktopUserAgent() { return mContentViewCore != null && mContentViewCore.getUseDesktopUserAgent(); } /** * @return The current {ToolbarModelSecurityLevel} for the tab. */ public int getSecurityLevel() { if (mNativeTabAndroid == 0) return ToolbarModelSecurityLevel.NONE; return nativeGetSecurityLevel(mNativeTabAndroid); } /** * @return The sync id of the tab if session sync is enabled, {@code 0} otherwise. */ @CalledByNative protected int getSyncId() { return mSyncId; } /** * @param syncId The sync id of the tab if session sync is enabled. */ @CalledByNative protected void setSyncId(int syncId) { mSyncId = syncId; } /** * @return An {@link ObserverList.RewindableIterator} instance that points to all of * the current {@link TabObserver}s on this class. Note that calling * {@link Iterator#remove()} will throw an {@link UnsupportedOperationException}. */ protected ObserverList.RewindableIterator<TabObserver> getTabObservers() { return mObservers.rewindableIterator(); } /** * @return The {@link ContentViewClient} currently bound to any {@link ContentViewCore} * associated with the current page. There can still be a {@link ContentViewClient} * even when there is no {@link ContentViewCore}. */ protected ContentViewClient getContentViewClient() { return mContentViewClient; } /** * @param client The {@link ContentViewClient} to be bound to any current or new * {@link ContentViewCore}s associated with this {@link TabBase}. */ protected void setContentViewClient(ContentViewClient client) { if (mContentViewClient == client) return; ContentViewClient oldClient = mContentViewClient; mContentViewClient = client; if (mContentViewCore == null) return; if (mContentViewClient != null) { mContentViewCore.setContentViewClient(mContentViewClient); } else if (oldClient != null) { // We can't set a null client, but we should clear references to the last one. mContentViewCore.setContentViewClient(new ContentViewClient()); } } /** * Shows the given {@code nativePage} if it's not already showing. * @param nativePage The {@link NativePage} to show. */ protected void showNativePage(NativePage nativePage) { if (mNativePage == nativePage) return; destroyNativePageInternal(); mNativePage = nativePage; pushNativePageStateToNavigationEntry(); for (TabObserver observer : mObservers) observer.onContentChanged(this); } /** * Hides the current {@link NativePage}, if any, and shows the {@link ContentView}. */ protected void showRenderedPage() { if (mNativePage == null) return; destroyNativePageInternal(); for (TabObserver observer : mObservers) observer.onContentChanged(this); } /** * Initializes this {@link TabBase}. */ public void initialize() { } /** * A helper method to initialize a {@link ContentView} without any native WebContents pointer. */ protected final void initContentView() { initContentView(ContentViewUtil.createNativeWebContents(mIncognito)); } /** * Completes the {@link ContentView} specific initialization around a native WebContents * pointer. {@link #getPageInfo()} will still return the {@link NativePage} if there is one. * All initialization that needs to reoccur after a web contents swap should be added here. * <p /> * NOTE: If you attempt to pass a native WebContents that does not have the same incognito * state as this tab this call will fail. * * @param nativeWebContents The native web contents pointer. */ protected void initContentView(int nativeWebContents) { destroyNativePageInternal(); mContentView = ContentView.newInstance(mContext, nativeWebContents, getWindowAndroid()); mContentViewCore = mContentView.getContentViewCore(); mWebContentsDelegate = createWebContentsDelegate(); mWebContentsObserver = new TabBaseWebContentsObserverAndroid(mContentViewCore); if (mContentViewClient != null) mContentViewCore.setContentViewClient(mContentViewClient); assert mNativeTabAndroid != 0; nativeInitWebContents( mNativeTabAndroid, mIncognito, mContentViewCore, mWebContentsDelegate); // In the case where restoring a Tab or showing a prerendered one we already have a // valid infobar container, no need to recreate one. if (mInfoBarContainer == null) { // The InfoBarContainer needs to be created after the ContentView has been natively // initialized. mInfoBarContainer = new InfoBarContainer( (Activity) mContext, createAutoLoginProcessor(), getId(), getContentView(), nativeWebContents); } else { mInfoBarContainer.onParentViewChanged(getId(), getContentView()); } } /** * Cleans up all internal state, destroying any {@link NativePage} or {@link ContentView} * currently associated with this {@link TabBase}. Typically, pnce this call is made this * {@link TabBase} should no longer be used as subclasses usually destroy the native component. */ public void destroy() { for (TabObserver observer : mObservers) observer.onDestroyed(this); destroyNativePageInternal(); destroyContentView(true); if (mInfoBarContainer != null) { mInfoBarContainer.destroy(); mInfoBarContainer = null; } } /** * @return The url associated with the tab. */ @CalledByNative public String getUrl() { return mContentView != null ? mContentView.getUrl() : ""; } /** * @return The tab title. */ @CalledByNative public String getTitle() { return getPageInfo() != null ? getPageInfo().getTitle() : ""; } /** * Restores the tab if it is frozen or crashed. * @return true iff tab restore was triggered. */ @CalledByNative public boolean restoreIfNeeded() { return false; } private void destroyNativePageInternal() { if (mNativePage == null) return; mNativePage.destroy(); mNativePage = null; } /** * Destroys the current {@link ContentView}. * @param deleteNativeWebContents Whether or not to delete the native WebContents pointer. */ protected final void destroyContentView(boolean deleteNativeWebContents) { if (mContentView == null) return; destroyContentViewInternal(mContentView); if (mInfoBarContainer != null && mInfoBarContainer.getParent() != null) { mInfoBarContainer.removeFromParentView(); } if (mContentViewCore != null) mContentViewCore.destroy(); mContentView = null; mContentViewCore = null; mWebContentsDelegate = null; mWebContentsObserver = null; assert mNativeTabAndroid != 0; nativeDestroyWebContents(mNativeTabAndroid, deleteNativeWebContents); } /** * Gives subclasses the chance to clean up some state associated with this {@link ContentView}. * This is because {@link #getContentView()} can return {@code null} if a {@link NativePage} * is showing. * @param contentView The {@link ContentView} that should have associated state cleaned up. */ protected void destroyContentViewInternal(ContentView contentView) { } /** * A helper method to allow subclasses to build their own delegate. * @return An instance of a {@link TabBaseChromeWebContentsDelegateAndroid}. */ protected TabBaseChromeWebContentsDelegateAndroid createWebContentsDelegate() { return new TabBaseChromeWebContentsDelegateAndroid(); } /** * @return The {@link WindowAndroid} associated with this {@link TabBase}. */ protected WindowAndroid getWindowAndroid() { return mWindowAndroid; } /** * @return The current {@link TabBaseChromeWebContentsDelegateAndroid} instance. */ protected TabBaseChromeWebContentsDelegateAndroid getChromeWebContentsDelegateAndroid() { return mWebContentsDelegate; } /** * Launches all currently blocked popups that were spawned by the content of this tab. */ protected void launchBlockedPopups() { assert mContentViewCore != null; nativeLaunchBlockedPopups(mNativeTabAndroid); } /** * Called when the number of blocked popups has changed. * @param numPopups The current number of blocked popups. */ @CalledByNative protected void onBlockedPopupsStateChanged(int numPopups) { } /** * Called when the favicon of the content this tab represents changes. */ @CalledByNative protected void onFaviconUpdated() { for (TabObserver observer : mObservers) observer.onFaviconUpdated(this); } /** * @return The native pointer representing the native side of this {@link TabBase} object. */ @CalledByNative protected int getNativePtr() { return mNativeTabAndroid; } /** This is currently called when committing a pre-rendered page. */ @CalledByNative private void swapWebContents(final int newWebContents) { destroyContentView(false); initContentView(newWebContents); mContentViewCore.onShow(); mContentViewCore.attachImeAdapter(); for (TabObserver observer : mObservers) observer.onContentChanged(this); } @CalledByNative private void clearNativePtr() { assert mNativeTabAndroid != 0; mNativeTabAndroid = 0; } @CalledByNative private void setNativePtr(int nativePtr) { assert mNativeTabAndroid == 0; mNativeTabAndroid = nativePtr; } @CalledByNative private int getNativeInfoBarContainer() { return getInfoBarContainer().getNative(); } /** * Validates {@code id} and increments the internal counter to make sure future ids don't * collide. * @param id The current id. Maybe {@link #INVALID_TAB_ID}. * @return A new id if {@code id} was {@link #INVALID_TAB_ID}, or {@code id}. */ private static int generateValidId(int id) { if (id == INVALID_TAB_ID) id = generateNextId(); incrementIdCounterTo(id + 1); return id; } /** * @return An unused id. */ private static int generateNextId() { return sIdCounter.getAndIncrement(); } private void pushNativePageStateToNavigationEntry() { assert mNativeTabAndroid != 0 && getNativePage() != null; nativeSetActiveNavigationEntryTitleForUrl(mNativeTabAndroid, getNativePage().getUrl(), getNativePage().getTitle()); } /** * Ensures the counter is at least as high as the specified value. The counter should always * point to an unused ID (which will be handed out next time a request comes in). Exposed so * that anything externally loading tabs and ids can set enforce new tabs start at the correct * id. * TODO(aurimas): Investigate reducing the visiblity of this method. * @param id The minimum id we should hand out to the next new tab. */ public static void incrementIdCounterTo(int id) { int diff = id - sIdCounter.get(); if (diff <= 0) return; // It's possible idCounter has been incremented between the get above and the add below // but that's OK, because in the worst case we'll overly increment idCounter. sIdCounter.addAndGet(diff); } private native void nativeInitWebContents(int nativeTabAndroid, boolean incognito, ContentViewCore contentViewCore, ChromeWebContentsDelegateAndroid delegate); private native void nativeDestroyWebContents(int nativeTabAndroid, boolean deleteNative); private native Profile nativeGetProfileAndroid(int nativeTabAndroid); private native void nativeLaunchBlockedPopups(int nativeTabAndroid); private native int nativeGetSecurityLevel(int nativeTabAndroid); private native void nativeSetActiveNavigationEntryTitleForUrl(int nativeTabAndroid, String url, String title); }