blob: e4c66d5ec43dd80cf23ac6be7e4d9fe0ad1884bc [file] [log] [blame]
// Copyright 2019 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.weblayer_private;
import android.graphics.RectF;
import android.os.Build;
import android.os.RemoteException;
import android.text.TextUtils;
import android.util.AndroidRuntimeException;
import android.view.KeyEvent;
import android.view.MotionEvent;
import android.webkit.ValueCallback;
import org.chromium.base.Callback;
import org.chromium.base.annotations.CalledByNative;
import org.chromium.base.annotations.JNINamespace;
import org.chromium.base.annotations.NativeMethods;
import org.chromium.components.autofill.AutofillActionModeCallback;
import org.chromium.components.autofill.AutofillProvider;
import org.chromium.components.autofill.AutofillProviderImpl;
import org.chromium.components.browser_ui.util.BrowserControlsVisibilityDelegate;
import org.chromium.components.browser_ui.util.ComposedBrowserControlsVisibilityDelegate;
import org.chromium.components.embedder_support.contextmenu.ContextMenuParams;
import org.chromium.components.find_in_page.FindInPageBridge;
import org.chromium.components.find_in_page.FindMatchRectsDetails;
import org.chromium.components.find_in_page.FindResultBar;
import org.chromium.components.url_formatter.UrlFormatter;
import org.chromium.content_public.browser.LoadUrlParams;
import org.chromium.content_public.browser.NavigationHandle;
import org.chromium.content_public.browser.SelectionPopupController;
import org.chromium.content_public.browser.ViewEventSink;
import org.chromium.content_public.browser.Visibility;
import org.chromium.content_public.browser.WebContents;
import org.chromium.content_public.browser.WebContentsObserver;
import org.chromium.content_public.common.BrowserControlsState;
import org.chromium.ui.base.ViewAndroidDelegate;
import org.chromium.ui.base.WindowAndroid;
import org.chromium.url.GURL;
import org.chromium.weblayer_private.interfaces.IDownloadCallbackClient;
import org.chromium.weblayer_private.interfaces.IErrorPageCallbackClient;
import org.chromium.weblayer_private.interfaces.IFindInPageCallbackClient;
import org.chromium.weblayer_private.interfaces.IFullscreenCallbackClient;
import org.chromium.weblayer_private.interfaces.INavigationControllerClient;
import org.chromium.weblayer_private.interfaces.IObjectWrapper;
import org.chromium.weblayer_private.interfaces.ITab;
import org.chromium.weblayer_private.interfaces.ITabClient;
import org.chromium.weblayer_private.interfaces.ObjectWrapper;
import org.chromium.weblayer_private.interfaces.StrictModeWorkaround;
import java.util.ArrayList;
import java.util.List;
/**
* Implementation of ITab.
*/
@JNINamespace("weblayer")
public final class TabImpl extends ITab.Stub {
private static int sNextId = 1;
private long mNativeTab;
private ProfileImpl mProfile;
private WebContents mWebContents;
private WebContentsObserver mWebContentsObserver;
private TabCallbackProxy mTabCallbackProxy;
private NavigationControllerImpl mNavigationController;
private ErrorPageCallbackProxy mErrorPageCallbackProxy;
private FullscreenCallbackProxy mFullscreenCallbackProxy;
private ViewAndroidDelegate mViewAndroidDelegate;
// BrowserImpl this TabImpl is in. This is only null during creation.
private BrowserImpl mBrowser;
/**
* The AutofillProvider that integrates with system-level autofill. This is null until
* updateFromBrowser() is invoked.
*/
private AutofillProvider mAutofillProvider;
private NewTabCallbackProxy mNewTabCallbackProxy;
private ITabClient mClient;
private final int mId;
// A list of browser control visibility constraints, indexed by ImplControlsVisibilityReason.
private List<BrowserControlsVisibilityDelegate> mBrowserControlsDelegates;
// Computes a net browser control visibility constraint from constituent constraints.
private ComposedBrowserControlsVisibilityDelegate mBrowserControlsVisibility;
// Invoked when the computed visibility constraint changes.
private Callback<Integer> mConstraintsUpdatedCallback;
private IFindInPageCallbackClient mFindInPageCallbackClient;
private FindInPageBridge mFindInPageBridge;
private FindResultBar mFindResultBar;
// See usage note in {@link #onFindResultAvailable}.
boolean mWaitingForMatchRects;
private InterceptNavigationDelegateImpl mInterceptNavigationDelegate;
private static class InternalAccessDelegateImpl
implements ViewEventSink.InternalAccessDelegate {
@Override
public boolean super_onKeyUp(int keyCode, KeyEvent event) {
return false;
}
@Override
public boolean super_dispatchKeyEvent(KeyEvent event) {
return false;
}
@Override
public boolean super_onGenericMotionEvent(MotionEvent event) {
return false;
}
@Override
public void onScrollChanged(int lPix, int tPix, int oldlPix, int oldtPix) {}
}
public TabImpl(ProfileImpl profile, WindowAndroid windowAndroid) {
mId = ++sNextId;
init(profile, windowAndroid, TabImplJni.get().createTab(profile.getNativeProfile(), this));
}
/**
* This constructor is called when the native side triggers creation of a TabImpl
* (as happens with popups and other scenarios).
*/
public TabImpl(ProfileImpl profile, WindowAndroid windowAndroid, long nativeTab) {
mId = ++sNextId;
TabImplJni.get().setJavaImpl(nativeTab, TabImpl.this);
init(profile, windowAndroid, nativeTab);
}
private void init(ProfileImpl profile, WindowAndroid windowAndroid, long nativeTab) {
mProfile = profile;
mNativeTab = nativeTab;
mWebContents = TabImplJni.get().getWebContents(mNativeTab, TabImpl.this);
mViewAndroidDelegate = new ViewAndroidDelegate(null) {
@Override
public void onTopControlsChanged(int topControlsOffsetY, int topContentOffsetY,
int topControlsMinHeightOffsetY) {
BrowserViewController viewController = getViewController();
if (viewController != null) {
viewController.onTopControlsChanged(topControlsOffsetY, topContentOffsetY);
}
}
};
mWebContents.initialize("", mViewAndroidDelegate, new InternalAccessDelegateImpl(),
windowAndroid, WebContents.createDefaultInternalsHolder());
mWebContentsObserver = new WebContentsObserver() {
@Override
public void didStartNavigation(NavigationHandle navigationHandle) {
if (navigationHandle.isInMainFrame() && !navigationHandle.isSameDocument()) {
hideFindInPageUiAndNotifyClient();
}
}
};
mWebContents.addObserver(mWebContentsObserver);
mBrowserControlsDelegates = new ArrayList<BrowserControlsVisibilityDelegate>();
mBrowserControlsVisibility = new ComposedBrowserControlsVisibilityDelegate();
for (int i = 0; i < ImplControlsVisibilityReason.REASON_COUNT; ++i) {
BrowserControlsVisibilityDelegate delegate =
new BrowserControlsVisibilityDelegate(BrowserControlsState.BOTH);
mBrowserControlsDelegates.add(delegate);
mBrowserControlsVisibility.addDelegate(delegate);
}
mConstraintsUpdatedCallback = (constraints) -> onBrowserControlsStateUpdated(constraints);
mBrowserControlsVisibility.addObserver(mConstraintsUpdatedCallback);
mInterceptNavigationDelegate = new InterceptNavigationDelegateImpl(this);
}
public ProfileImpl getProfile() {
return mProfile;
}
public ITabClient getClient() {
return mClient;
}
/**
* Sets the BrowserImpl this TabImpl is contained in.
*/
public void attachToBrowser(BrowserImpl browser) {
mBrowser = browser;
updateFromBrowser();
SelectionPopupController controller =
SelectionPopupController.fromWebContents(mWebContents);
controller.setActionModeCallback(new ActionModeCallback(mWebContents));
}
public void updateFromBrowser() {
mWebContents.setTopLevelNativeWindow(mBrowser.getWindowAndroid());
mViewAndroidDelegate.setContainerView(mBrowser.getViewAndroidDelegateContainerView());
updateWebContentsVisibility();
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O) {
SelectionPopupController selectionController =
SelectionPopupController.fromWebContents(mWebContents);
if (mBrowser.getContext() == null) {
// The Context and ViewContainer in which Autofill was previously operating have
// gone away, so tear down |mAutofillProvider|.
mAutofillProvider = null;
selectionController.setNonSelectionActionModeCallback(null);
} else {
// Set up |mAutofillProvider| to operate in the new Context.
mAutofillProvider = new AutofillProviderImpl(
mBrowser.getContext(), mBrowser.getViewAndroidDelegateContainerView());
mAutofillProvider.setWebContents(mWebContents);
selectionController.setNonSelectionActionModeCallback(
new AutofillActionModeCallback(mBrowser.getContext(), mAutofillProvider));
}
TabImplJni.get().onAutofillProviderChanged(mNativeTab, mAutofillProvider);
}
}
public BrowserImpl getBrowser() {
return mBrowser;
}
@Override
public void setNewTabsEnabled(boolean enable) {
StrictModeWorkaround.apply();
if (enable && mNewTabCallbackProxy == null) {
mNewTabCallbackProxy = new NewTabCallbackProxy(this);
} else if (!enable && mNewTabCallbackProxy != null) {
mNewTabCallbackProxy.destroy();
mNewTabCallbackProxy = null;
}
}
@Override
public int getId() {
StrictModeWorkaround.apply();
return mId;
}
/**
* Called when this TabImpl becomes the active TabImpl.
*/
public void onDidGainActive(long topControlsContainerViewHandle) {
// attachToFragment() must be called before activate().
assert mBrowser != null;
TabImplJni.get().setTopControlsContainerView(
mNativeTab, TabImpl.this, topControlsContainerViewHandle);
updateWebContentsVisibility();
mWebContents.onShow();
}
/**
* Called when this TabImpl is no longer the active TabImpl.
*/
public void onDidLoseActive() {
hideFindInPageUiAndNotifyClient();
mWebContents.onHide();
updateWebContentsVisibility();
TabImplJni.get().setTopControlsContainerView(mNativeTab, TabImpl.this, 0);
}
/**
* Returns whether this Tab is visible.
*/
public boolean isVisible() {
return (mBrowser.getActiveTab() == this && mBrowser.isStarted());
}
private void updateWebContentsVisibility() {
boolean visibleNow = isVisible();
boolean webContentsVisible = mWebContents.getVisibility() == Visibility.VISIBLE;
if (visibleNow) {
if (!webContentsVisible) mWebContents.onShow();
} else {
if (webContentsVisible) mWebContents.onHide();
}
}
public void loadUrl(LoadUrlParams loadUrlParams) {
String url = loadUrlParams.getUrl();
if (url == null || url.isEmpty()) return;
GURL fixedUrl = UrlFormatter.fixupUrl(url);
if (!fixedUrl.isValid()) return;
loadUrlParams.setUrl(fixedUrl.getSpec());
getWebContents().getNavigationController().loadUrl(loadUrlParams);
}
public WebContents getWebContents() {
return mWebContents;
}
public AutofillProvider getAutofillProvider() {
return mAutofillProvider;
}
long getNativeTab() {
return mNativeTab;
}
@Override
public NavigationControllerImpl createNavigationController(INavigationControllerClient client) {
StrictModeWorkaround.apply();
// This should only be called once.
assert mNavigationController == null;
mNavigationController = new NavigationControllerImpl(this, client);
return mNavigationController;
}
@Override
public void setClient(ITabClient client) {
StrictModeWorkaround.apply();
mClient = client;
mTabCallbackProxy = new TabCallbackProxy(mNativeTab, client);
}
@Override
public void setDownloadCallbackClient(IDownloadCallbackClient client) {
StrictModeWorkaround.apply();
mProfile.setDownloadCallbackClient(client);
}
@Override
public void setErrorPageCallbackClient(IErrorPageCallbackClient client) {
StrictModeWorkaround.apply();
if (client != null) {
if (mErrorPageCallbackProxy == null) {
mErrorPageCallbackProxy = new ErrorPageCallbackProxy(mNativeTab, client);
} else {
mErrorPageCallbackProxy.setClient(client);
}
} else if (mErrorPageCallbackProxy != null) {
mErrorPageCallbackProxy.destroy();
mErrorPageCallbackProxy = null;
}
}
@Override
public void setFullscreenCallbackClient(IFullscreenCallbackClient client) {
StrictModeWorkaround.apply();
if (client != null) {
if (mFullscreenCallbackProxy == null) {
mFullscreenCallbackProxy = new FullscreenCallbackProxy(mNativeTab, client);
} else {
mFullscreenCallbackProxy.setClient(client);
}
} else if (mFullscreenCallbackProxy != null) {
mFullscreenCallbackProxy.destroy();
mFullscreenCallbackProxy = null;
}
}
@Override
public void executeScript(String script, boolean useSeparateIsolate, IObjectWrapper callback) {
StrictModeWorkaround.apply();
Callback<String> nativeCallback = new Callback<String>() {
@Override
public void onResult(String result) {
ValueCallback<String> unwrappedCallback =
(ValueCallback<String>) ObjectWrapper.unwrap(callback, ValueCallback.class);
if (unwrappedCallback != null) {
unwrappedCallback.onReceiveValue(result);
}
}
};
TabImplJni.get().executeScript(mNativeTab, script, useSeparateIsolate, nativeCallback);
}
@Override
public boolean setFindInPageCallbackClient(IFindInPageCallbackClient client) {
StrictModeWorkaround.apply();
if (client == null) {
// Null now to avoid calling onFindEnded.
mFindInPageCallbackClient = null;
hideFindInPageUiAndNotifyClient();
return true;
}
if (mFindInPageCallbackClient != null) return false;
BrowserViewController controller = getViewController();
if (controller == null) return false;
// Refuse to start a find session when the browser controls are forced hidden.
if (mBrowserControlsVisibility.get() == BrowserControlsState.HIDDEN) return false;
setBrowserControlsVisibilityConstraint(
ImplControlsVisibilityReason.FIND_IN_PAGE, BrowserControlsState.SHOWN);
mFindInPageCallbackClient = client;
assert mFindInPageBridge == null;
mFindInPageBridge = new FindInPageBridge(mWebContents);
assert mFindResultBar == null;
mFindResultBar =
new FindResultBar(mBrowser.getContext(), controller.getWebContentsOverlayView(),
mBrowser.getWindowAndroid(), mFindInPageBridge);
return true;
}
@Override
public void findInPage(String searchText, boolean forward) {
StrictModeWorkaround.apply();
if (mFindInPageBridge == null) return;
if (searchText.length() > 0) {
mFindInPageBridge.startFinding(searchText, forward, false);
} else {
mFindInPageBridge.stopFinding(true);
}
}
private void hideFindInPageUiAndNotifyClient() {
if (mFindInPageBridge == null) return;
mFindInPageBridge.stopFinding(true);
mFindResultBar.dismiss();
mFindResultBar = null;
mFindInPageBridge.destroy();
mFindInPageBridge = null;
setBrowserControlsVisibilityConstraint(
ImplControlsVisibilityReason.FIND_IN_PAGE, BrowserControlsState.BOTH);
try {
if (mFindInPageCallbackClient != null) mFindInPageCallbackClient.onFindEnded();
mFindInPageCallbackClient = null;
} catch (RemoteException e) {
throw new AndroidRuntimeException(e);
}
}
@Override
public void dispatchBeforeUnloadAndClose() {
StrictModeWorkaround.apply();
mWebContents.dispatchBeforeUnload(false);
}
@Override
public boolean dismissTransientUi() {
BrowserViewController viewController = getViewController();
if (viewController != null && viewController.dismissTabModalOverlay()) return true;
if (mWebContents.isFullscreenForCurrentTab()) {
mWebContents.exitFullscreen();
return true;
}
SelectionPopupController popup = SelectionPopupController.fromWebContents(mWebContents);
if (popup != null && popup.isSelectActionBarShowing()) {
popup.clearSelection();
return true;
}
return false;
}
@Override
public String getGuid() {
return TabImplJni.get().getGuid(mNativeTab);
}
@CalledByNative
private static RectF createRectF(float x, float y, float right, float bottom) {
return new RectF(x, y, right, bottom);
}
@CalledByNative
private static FindMatchRectsDetails createFindMatchRectsDetails(
int version, int numRects, RectF activeRect) {
return new FindMatchRectsDetails(version, numRects, activeRect);
}
@CalledByNative
private static void setMatchRectByIndex(
FindMatchRectsDetails findMatchRectsDetails, int index, RectF rect) {
findMatchRectsDetails.rects[index] = rect;
}
@CalledByNative
private void onFindResultAvailable(
int numberOfMatches, int activeMatchOrdinal, boolean finalUpdate) {
try {
if (mFindInPageCallbackClient != null) {
// The WebLayer API deals in indices instead of ordinals.
mFindInPageCallbackClient.onFindResult(
numberOfMatches, activeMatchOrdinal - 1, finalUpdate);
}
} catch (RemoteException e) {
throw new AndroidRuntimeException(e);
}
if (mFindResultBar != null) {
mFindResultBar.onFindResult();
if (finalUpdate) {
if (numberOfMatches > 0) {
mWaitingForMatchRects = true;
mFindInPageBridge.requestFindMatchRects(mFindResultBar.getRectsVersion());
} else {
// Match rects results that correlate to an earlier call to
// requestFindMatchRects might still come in, so set this sentinel to false to
// make sure we ignore them instead of showing stale results.
mWaitingForMatchRects = false;
mFindResultBar.clearMatchRects();
}
}
}
}
@CalledByNative
private void onFindMatchRectsAvailable(FindMatchRectsDetails matchRects) {
if (mFindResultBar != null && mWaitingForMatchRects) {
mFindResultBar.setMatchRects(
matchRects.version, matchRects.rects, matchRects.activeRect);
}
}
public void destroy() {
if (mTabCallbackProxy != null) {
mTabCallbackProxy.destroy();
mTabCallbackProxy = null;
}
if (mErrorPageCallbackProxy != null) {
mErrorPageCallbackProxy.destroy();
mErrorPageCallbackProxy = null;
}
if (mFullscreenCallbackProxy != null) {
mFullscreenCallbackProxy.destroy();
mFullscreenCallbackProxy = null;
}
if (mNewTabCallbackProxy != null) {
mNewTabCallbackProxy.destroy();
mNewTabCallbackProxy = null;
}
mInterceptNavigationDelegate.onTabDestroyed();
mInterceptNavigationDelegate = null;
// ObservableSupplierImpl.addObserver() posts a task to notify the observer, ensure the
// callback isn't run after destroy() is called (otherwise we'll get crashes as the native
// tab has been deleted).
mBrowserControlsVisibility.removeObserver(mConstraintsUpdatedCallback);
hideFindInPageUiAndNotifyClient();
mFindInPageCallbackClient = null;
mNavigationController = null;
mWebContents.removeObserver(mWebContentsObserver);
TabImplJni.get().deleteTab(mNativeTab);
mNativeTab = 0;
}
@CalledByNative
private boolean doBrowserControlsShrinkRendererSize() {
BrowserViewController viewController = getViewController();
return viewController != null && viewController.doBrowserControlsShrinkRendererSize();
}
@CalledByNative
public void setBrowserControlsVisibilityConstraint(
@ImplControlsVisibilityReason int reason, @BrowserControlsState int constraint) {
mBrowserControlsDelegates.get(reason).set(constraint);
}
@CalledByNative
public void showRepostFormWarningDialog() {
BrowserViewController viewController = getViewController();
if (viewController == null) {
mWebContents.getNavigationController().cancelPendingReload();
} else {
viewController.showRepostFormWarningDialog();
}
}
private static String nonEmptyOrNull(String s) {
return TextUtils.isEmpty(s) ? null : s;
}
@CalledByNative
private void showContextMenu(ContextMenuParams params) throws RemoteException {
if (WebLayerFactoryImpl.getClientMajorVersion() < 82) return;
mClient.showContextMenu(ObjectWrapper.wrap(params.getPageUrl()),
ObjectWrapper.wrap(nonEmptyOrNull(params.getLinkUrl())),
ObjectWrapper.wrap(nonEmptyOrNull(params.getLinkText())),
ObjectWrapper.wrap(nonEmptyOrNull(params.getTitleText())),
ObjectWrapper.wrap(nonEmptyOrNull(params.getSrcUrl())));
}
private void onBrowserControlsStateUpdated(int state) {
TabImplJni.get().updateBrowserControlsState(mNativeTab, state);
// If something has overridden the FIP's SHOWN constraint, cancel FIP. This causes FIP to
// dismiss when entering fullscreen.
if (state != BrowserControlsState.SHOWN) {
hideFindInPageUiAndNotifyClient();
}
}
/**
* Returns the BrowserViewController for this TabImpl, but only if this
* is the active TabImpl.
*/
private BrowserViewController getViewController() {
return (mBrowser.getActiveTab() == this) ? mBrowser.getViewController() : null;
}
@NativeMethods
interface Natives {
long createTab(long profile, TabImpl caller);
void setJavaImpl(long nativeTabImpl, TabImpl impl);
void onAutofillProviderChanged(long nativeTabImpl, AutofillProvider autofillProvider);
void setTopControlsContainerView(
long nativeTabImpl, TabImpl caller, long nativeTopControlsContainerView);
void deleteTab(long tab);
WebContents getWebContents(long nativeTabImpl, TabImpl caller);
void executeScript(long nativeTabImpl, String script, boolean useSeparateIsolate,
Callback<String> callback);
void updateBrowserControlsState(long nativeTabImpl, int newConstraint);
String getGuid(long nativeTabImpl);
}
}