Commit 0193f679 authored by Evan Stade's avatar Evan Stade Committed by Commit Bot

WebLayer: add Tab#dipatchBeforeUnloadAndClose

This allows the embedder to run the beforeunload handler, if any,
before closing a tab.

Bug: 1055540,1025256
Change-Id: I894256d0a05f11d58fa4234265beb03b34a019b1
Reviewed-on: https://chromium-review.googlesource.com/c/chromium/src/+/2072922
Commit-Queue: Evan Stade <estade@chromium.org>
Reviewed-by: default avatarScott Violet <sky@chromium.org>
Cr-Commit-Position: refs/heads/master@{#745985}
parent 19353b59
......@@ -306,6 +306,12 @@ bool WebContentsAndroid::IsLoadingToDifferentDocument(
return web_contents_->IsLoadingToDifferentDocument();
}
void WebContentsAndroid::DispatchBeforeUnload(JNIEnv* env,
const JavaParamRef<jobject>& obj,
bool auto_cancel) {
web_contents_->DispatchBeforeUnload(auto_cancel);
}
void WebContentsAndroid::Stop(JNIEnv* env, const JavaParamRef<jobject>& obj) {
web_contents_->Stop();
}
......
......@@ -68,6 +68,10 @@ class CONTENT_EXPORT WebContentsAndroid {
JNIEnv* env,
const base::android::JavaParamRef<jobject>& obj) const;
void DispatchBeforeUnload(JNIEnv* env,
const base::android::JavaParamRef<jobject>& obj,
bool auto_cancel);
void Stop(JNIEnv* env, const base::android::JavaParamRef<jobject>& obj);
void Cut(JNIEnv* env, const base::android::JavaParamRef<jobject>& obj);
void Copy(JNIEnv* env, const base::android::JavaParamRef<jobject>& obj);
......
......@@ -438,6 +438,13 @@ public class WebContentsImpl implements WebContents, RenderFrameHostDelegate, Wi
mNativeWebContentsAndroid, WebContentsImpl.this);
}
@Override
public void dispatchBeforeUnload(boolean autoCancel) {
if (mNativeWebContentsAndroid == 0) return;
WebContentsImplJni.get().dispatchBeforeUnload(
mNativeWebContentsAndroid, WebContentsImpl.this, autoCancel);
}
@Override
public void stop() {
checkNotDestroyed();
......@@ -1053,6 +1060,8 @@ public class WebContentsImpl implements WebContents, RenderFrameHostDelegate, Wi
String getEncoding(long nativeWebContentsAndroid, WebContentsImpl caller);
boolean isLoading(long nativeWebContentsAndroid, WebContentsImpl caller);
boolean isLoadingToDifferentDocument(long nativeWebContentsAndroid, WebContentsImpl caller);
void dispatchBeforeUnload(
long nativeWebContentsAndroid, WebContentsImpl caller, boolean autoCancel);
void stop(long nativeWebContentsAndroid, WebContentsImpl caller);
void cut(long nativeWebContentsAndroid, WebContentsImpl caller);
void copy(long nativeWebContentsAndroid, WebContentsImpl caller);
......
......@@ -194,6 +194,14 @@ public interface WebContents extends Parcelable {
*/
boolean isLoadingToDifferentDocument();
/**
* Runs the beforeunload handler, if any. The tab will be closed if there's no beforeunload
* handler or if the user accepts closing.
*
* @param autoCancel See C++ WebContents for explanation.
*/
void dispatchBeforeUnload(boolean autoCancel);
/**
* Stop any pending navigation.
*/
......
......@@ -130,6 +130,9 @@ public class MockWebContents implements WebContents {
return false;
}
@Override
public void dispatchBeforeUnload(boolean autoCancel) {}
@Override
public void stop() {}
......
......@@ -9,6 +9,7 @@ android_library("weblayer_java_tests") {
testonly = true
sources = [
"src/org/chromium/weblayer/test/BrowserFragmentLifecycleTest.java",
"src/org/chromium/weblayer/test/CloseTabNewTabCallbackImpl.java",
"src/org/chromium/weblayer/test/CrashReporterTest.java",
"src/org/chromium/weblayer/test/DataClearingTest.java",
"src/org/chromium/weblayer/test/DownloadCallbackTest.java",
......@@ -28,6 +29,7 @@ android_library("weblayer_java_tests") {
"src/org/chromium/weblayer/test/SmokeTest.java",
"src/org/chromium/weblayer/test/TabCallbackTest.java",
"src/org/chromium/weblayer/test/TabListCallbackTest.java",
"src/org/chromium/weblayer/test/TabTest.java",
"src/org/chromium/weblayer/test/TopControlsTest.java",
"src/org/chromium/weblayer/test/WebLayerJUnit4ClassRunner.java",
"src/org/chromium/weblayer/test/WebLayerLoadingTest.java",
......
// 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.weblayer.test;
import org.chromium.base.test.util.CallbackHelper;
import org.chromium.weblayer.NewTabCallback;
import org.chromium.weblayer.Tab;
/**
* NewTabCallback test helper. Primarily used to wait for a tab to be closed.
*/
public class CloseTabNewTabCallbackImpl extends NewTabCallback {
private final CallbackHelper mCallbackHelper = new CallbackHelper();
@Override
public void onNewTab(Tab tab, int mode) {}
@Override
public void onCloseTab() {
mCallbackHelper.notifyCalled();
}
public void waitForCloseTab() {
try {
// waitForFirst() only handles a single call. If you need more convert from
// waitForFirst().
mCallbackHelper.waitForFirst();
} catch (Exception e) {
throw new RuntimeException(e);
}
}
public boolean hasClosed() {
return mCallbackHelper.getCallCount() > 0;
}
}
......@@ -11,10 +11,8 @@ import org.junit.Rule;
import org.junit.Test;
import org.junit.runner.RunWith;
import org.chromium.base.test.util.CallbackHelper;
import org.chromium.content_public.browser.test.util.CriteriaHelper;
import org.chromium.content_public.browser.test.util.TestThreadUtils;
import org.chromium.weblayer.NewTabCallback;
import org.chromium.weblayer.Tab;
import org.chromium.weblayer.shell.InstrumentationActivity;
......@@ -29,28 +27,6 @@ public class NewTabCallbackTest {
private InstrumentationActivity mActivity;
private static final class CloseTabNewTabCallbackImpl extends NewTabCallback {
private final CallbackHelper mCallbackHelper = new CallbackHelper();
@Override
public void onNewTab(Tab tab, int mode) {}
@Override
public void onCloseTab() {
mCallbackHelper.notifyCalled();
}
public void waitForCloseTab() {
try {
// waitForFirst() only handles a single call. If you need more convert from
// waitForFirst().
mCallbackHelper.waitForFirst();
} catch (Exception e) {
throw new RuntimeException(e);
}
}
}
@Test
@SmallTest
public void testNewBrowser() {
......
// 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.weblayer.test;
import android.support.test.filters.SmallTest;
import org.junit.Assert;
import org.junit.Rule;
import org.junit.Test;
import org.junit.runner.RunWith;
import org.chromium.content_public.browser.test.util.TestThreadUtils;
import org.chromium.weblayer.Tab;
import org.chromium.weblayer.shell.InstrumentationActivity;
import java.util.concurrent.CountDownLatch;
/**
* Tests for Tab.
*/
@RunWith(WebLayerJUnit4ClassRunner.class)
public class TabTest {
@Rule
public InstrumentationActivityTestRule mActivityTestRule =
new InstrumentationActivityTestRule();
private InstrumentationActivity mActivity;
@Test
@SmallTest
public void testBeforeUnload() throws InterruptedException {
String url = mActivityTestRule.getTestDataURL("before_unload.html");
mActivity = mActivityTestRule.launchShellWithUrl(url);
Assert.assertNotNull(mActivity);
Assert.assertTrue(mActivity.hasWindowFocus());
// Touch the view so that beforeunload will be respected (if the user doesn't interact with
// the tab, it's ignored).
EventUtils.simulateTouchCenterOfView(mActivity.getWindow().getDecorView());
// Round trip through the renderer to make sure te above touch is handled before we call
// dispatchBeforeUnloadAndClose().
mActivityTestRule.executeScriptSync("var x = 1", true);
TestThreadUtils.runOnUiThreadBlocking(
() -> { mActivity.getBrowser().getActiveTab().dispatchBeforeUnloadAndClose(); });
// Wait till the main window loses focus due to the app modal beforeunload dialog.
CountDownLatch noFocusLatch = new CountDownLatch(1);
CountDownLatch hasFocusLatch = new CountDownLatch(1);
TestThreadUtils.runOnUiThreadBlocking(() -> {
mActivity.getWindow()
.getDecorView()
.getViewTreeObserver()
.addOnWindowFocusChangeListener((boolean hasFocus) -> {
(hasFocus ? hasFocusLatch : noFocusLatch).countDown();
});
});
noFocusLatch.await();
// Verify closing the tab works still while beforeunload is showing (no crash).
TestThreadUtils.runOnUiThreadBlocking(() -> {
mActivity.getBrowser().destroyTab(mActivity.getBrowser().getActiveTab());
});
// Focus returns to the main window because the dialog is dismissed when the tab is
// destroyed.
hasFocusLatch.await();
}
@Test
@SmallTest
public void testBeforeUnloadNoHandler() {
String url = mActivityTestRule.getTestDataURL("simple_page.html");
mActivity = mActivityTestRule.launchShellWithUrl(url);
Assert.assertNotNull(mActivity);
CloseTabNewTabCallbackImpl callback = new CloseTabNewTabCallbackImpl();
// Verify that calling dispatchBeforeUnloadAndClose will close the tab asynchronously when
// there is no beforeunload handler.
Assert.assertFalse(TestThreadUtils.runOnUiThreadBlockingNoException(() -> {
Tab tab = mActivity.getBrowser().getActiveTab();
tab.setNewTabCallback(callback);
tab.dispatchBeforeUnloadAndClose();
return callback.hasClosed();
}));
callback.waitForCloseTab();
}
@Test
@SmallTest
public void testBeforeUnloadNoInteraction() {
String url = mActivityTestRule.getTestDataURL("before_unload.html");
mActivity = mActivityTestRule.launchShellWithUrl(url);
Assert.assertNotNull(mActivity);
CloseTabNewTabCallbackImpl callback = new CloseTabNewTabCallbackImpl();
// Verify that beforeunload is not run when there's no user action.
Assert.assertFalse(TestThreadUtils.runOnUiThreadBlockingNoException(() -> {
Tab tab = mActivity.getBrowser().getActiveTab();
tab.setNewTabCallback(callback);
tab.dispatchBeforeUnloadAndClose();
return callback.hasClosed();
}));
callback.waitForCloseTab();
}
}
......@@ -264,13 +264,13 @@ public class ContentView extends RelativeLayout
@Override
public InputConnection onCreateInputConnection(EditorInfo outAttrs) {
// Calls may come while/after WebContents is destroyed. See https://crbug.com/821750#c8.
if (mWebContents != null && mWebContents.isDestroyed()) return null;
if (mWebContents == null || mWebContents.isDestroyed()) return null;
return ImeAdapter.fromWebContents(mWebContents).onCreateInputConnection(outAttrs);
}
@Override
public boolean onCheckIsTextEditor() {
if (mWebContents != null && mWebContents.isDestroyed()) return false;
if (mWebContents == null || mWebContents.isDestroyed()) return false;
return ImeAdapter.fromWebContents(mWebContents).onCheckIsTextEditor();
}
......
......@@ -403,6 +403,12 @@ public final class TabImpl extends ITab.Stub {
if (controller != null) controller.dismissTabModalOverlay();
}
@Override
public void dispatchBeforeUnloadAndClose() {
StrictModeWorkaround.apply();
mWebContents.dispatchBeforeUnload(false);
}
@CalledByNative
private static RectF createRectF(float x, float y, float right, float bottom) {
return new RectF(x, y, right, bottom);
......
......@@ -37,4 +37,5 @@ interface ITab {
void findInPage(in String searchText, boolean forward) = 9;
void dismissTabModalOverlay() = 10;
void dispatchBeforeUnloadAndClose() = 11;
}
......@@ -162,6 +162,9 @@ public class Browser {
* Disposes a Tab. If {@link tab} is the active Tab, no Tab is made active. After this call
* {@link tab} should not be used.
*
* Note this will skip any beforeunload handlers. To run those first, use
* {@link Tab#dispatchBeforeUnloadAndClose} instead.
*
* @param tab The Tab to dispose.
*
* @throws IllegalStateException is {@link tab} is not in this Browser.
......
......@@ -215,6 +215,32 @@ public class Tab {
}
}
/**
* Runs the beforeunload handler for the main frame or any sub frame, if necessary; otherwise,
* asynchronously closes the tab.
*
* If there is a beforeunload handler a dialog is shown to the user which will allow them to
* choose whether to proceed with closing the tab. The closure will be notified via {@link
* NewTabCallback#onCloseTab}. The tab will not close if the user chooses to cancel the action.
* If there is no beforeunload handler, the tab closure will be asynchronous (but immediate) and
* will be notified in the same way.
*
* To close the tab synchronously without running beforeunload, use {@link Browser#destroyTab}.
*
* @since 82
*/
public void dispatchBeforeUnloadAndClose() {
ThreadCheck.ensureOnUiThread();
if (WebLayer.getSupportedMajorVersionInternal() < 82) {
throw new UnsupportedOperationException();
}
try {
mImpl.dispatchBeforeUnloadAndClose();
} catch (RemoteException e) {
throw new APICallException(e);
}
}
public void setNewTabCallback(@Nullable NewTabCallback callback) {
ThreadCheck.ensureOnUiThread();
mNewTabCallback = callback;
......
......@@ -451,8 +451,9 @@ public class WebLayerShellActivity extends FragmentActivity {
if (controller.canGoBack()) {
controller.goBack();
return;
} else if (!mPreviousTabList.isEmpty()) {
closeTab(mBrowser.getActiveTab());
}
if (!mPreviousTabList.isEmpty()) {
mBrowser.getActiveTab().dispatchBeforeUnloadAndClose();
return;
}
}
......
<html>
<body onbeforeunload="return runBeforeUnload()">
</body>
<script>
function runBeforeUnload() {
return "foo";
}
</script>
</html>
Markdown is supported
0%
or
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment