blob: 72fe2e07e57ce517a9fa760a2ecd6b3638f62d10 [file] [log] [blame]
// Copyright 2015 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.android_webview.test;
import static org.chromium.android_webview.test.AwActivityTestRule.CHECK_INTERVAL;
import android.content.Context;
import android.content.ContextWrapper;
import android.os.Build;
import android.os.ResultReceiver;
import android.support.test.InstrumentationRegistry;
import android.util.Pair;
import android.view.Window;
import android.view.WindowManager;
import android.webkit.JavascriptInterface;
import androidx.test.filters.LargeTest;
import androidx.test.filters.SmallTest;
import org.hamcrest.Matchers;
import org.junit.After;
import org.junit.Assert;
import org.junit.Rule;
import org.junit.Test;
import org.junit.runner.RunWith;
import org.chromium.android_webview.AwContents;
import org.chromium.android_webview.gfx.AwGLFunctor;
import org.chromium.android_webview.test.AwActivityTestRule.TestDependencyFactory;
import org.chromium.base.test.util.Criteria;
import org.chromium.base.test.util.CriteriaHelper;
import org.chromium.base.test.util.CriteriaNotSatisfiedException;
import org.chromium.base.test.util.Feature;
import org.chromium.base.test.util.MinAndroidSdkLevel;
import org.chromium.content_public.browser.ImeAdapter;
import org.chromium.content_public.browser.WebContentsAccessibility;
import org.chromium.content_public.browser.test.util.TestThreadUtils;
import org.chromium.content_public.common.ContentUrlConstants;
import java.lang.ref.Reference;
import java.util.concurrent.Callable;
/**
* AwContents garbage collection tests. Most apps relies on WebView being
* garbage collected to release memory. These tests ensure that nothing
* accidentally prevents AwContents from garbage collected, leading to leaks.
* See crbug.com/544098 for why @DisableHardwareAccelerationForTest is needed.
* These tests are flaky on non-cq L bot (crbug.com/1085101) because the trick
* to remove InputMethodManager reference does not work on L. This is ok as
* long as these tests are running and passing on some cq bot, so just
* disabling them on L.
*/
@MinAndroidSdkLevel(Build.VERSION_CODES.M)
@RunWith(AwJUnit4ClassRunner.class)
public class AwContentsGarbageCollectionTest {
@Rule
public AwActivityTestRule mActivityTestRule = new AwActivityTestRule() {
@Override
public TestDependencyFactory createTestDependencyFactory() {
if (mOverridenFactory == null) {
return new TestDependencyFactory();
} else {
return mOverridenFactory;
}
}
};
private TestDependencyFactory mOverridenFactory;
@After
public void tearDown() {
mOverridenFactory = null;
}
private static class StrongRefTestContext extends ContextWrapper {
private AwContents mAwContents;
public void setAwContentsStrongRef(AwContents awContents) {
mAwContents = awContents;
}
public StrongRefTestContext(Context context) {
super(context);
}
}
private static class GcTestDependencyFactory extends TestDependencyFactory {
private final StrongRefTestContext mContext;
public GcTestDependencyFactory(StrongRefTestContext context) {
mContext = context;
}
@Override
public AwTestContainerView createAwTestContainerView(
AwTestRunnerActivity activity, boolean allowHardwareAcceleration) {
if (activity != mContext.getBaseContext()) Assert.fail();
return new AwTestContainerView(mContext, allowHardwareAcceleration);
}
}
private static class StrongRefTestAwContentsClient extends TestAwContentsClient {
private AwContents mAwContentsStrongRef;
public void setAwContentsStrongRef(AwContents awContents) {
mAwContentsStrongRef = awContents;
}
}
@Test
@DisableHardwareAccelerationForTest
@SmallTest
@Feature({"AndroidWebView"})
public void testCreateAndGcOneTime() throws Throwable {
runAwContentsGcTest(() -> {
TestAwContentsClient client = new TestAwContentsClient();
AwTestContainerView containerView =
mActivityTestRule.createAwTestContainerViewOnMainSync(client);
mActivityTestRule.loadUrlAsync(
containerView.getAwContents(), ContentUrlConstants.ABOUT_BLANK_DISPLAY_URL);
containerView = null;
return null;
});
}
@Test
@DisableHardwareAccelerationForTest
@SmallTest
@Feature({"AndroidWebView"})
public void testHoldKeyboardResultReceiver() throws Throwable {
runAwContentsGcTest(() -> {
TestAwContentsClient client = new TestAwContentsClient();
AwTestContainerView containerView =
mActivityTestRule.createAwTestContainerViewOnMainSync(client);
mActivityTestRule.loadUrlAsync(
containerView.getAwContents(), ContentUrlConstants.ABOUT_BLANK_DISPLAY_URL);
// When we call showSoftInput(), we pass a ResultReceiver object as a parameter.
// Android framework will hold the object reference until the matching
// ResultReceiver in InputMethodService (IME app) gets garbage-collected.
// WebView object wouldn't get gc'ed once OSK shows up because of this.
// It is difficult to show keyboard and wait until input method window shows up.
// Instead, we simply emulate Android's behavior by keeping strong references.
// See crbug.com/595613 for details.
ResultReceiver resultReceiver = TestThreadUtils.runOnUiThreadBlocking(
() -> ImeAdapter.fromWebContents(containerView.getWebContents())
.getNewShowKeyboardReceiver());
return resultReceiver;
});
}
@Test
@DisableHardwareAccelerationForTest
@SmallTest
@Feature({"AndroidWebView"})
public void testAccessibility() throws Throwable {
runAwContentsGcTest(() -> {
TestAwContentsClient client = new TestAwContentsClient();
AwTestContainerView containerView =
mActivityTestRule.createAwTestContainerViewOnMainSync(client);
mActivityTestRule.loadUrlAsync(
containerView.getAwContents(), ContentUrlConstants.ABOUT_BLANK_DISPLAY_URL);
TestThreadUtils.runOnUiThreadBlocking(() -> {
WebContentsAccessibility webContentsA11y =
WebContentsAccessibility.fromWebContents(containerView.getWebContents());
webContentsA11y.setState(true);
// Enable a11y for testing.
webContentsA11y.setAccessibilityEnabledForTesting();
// Initialize native object.
containerView.getAccessibilityNodeProvider();
Assert.assertTrue(webContentsA11y.isAccessibilityEnabled());
});
return null;
});
}
@Test
@DisableHardwareAccelerationForTest
@SmallTest
@Feature({"AndroidWebView"})
public void testReferenceFromClient() throws Throwable {
runAwContentsGcTest(() -> {
StrongRefTestAwContentsClient client = new StrongRefTestAwContentsClient();
AwTestContainerView containerView =
mActivityTestRule.createAwTestContainerViewOnMainSync(client);
client.setAwContentsStrongRef(containerView.getAwContents());
mActivityTestRule.loadUrlAsync(
containerView.getAwContents(), ContentUrlConstants.ABOUT_BLANK_DISPLAY_URL);
containerView = null;
return null;
});
}
@Test
@DisableHardwareAccelerationForTest
@SmallTest
@Feature({"AndroidWebView"})
public void testReferenceFromContext() throws Throwable {
runAwContentsGcTest(() -> {
TestAwContentsClient client = new TestAwContentsClient();
StrongRefTestContext context =
new StrongRefTestContext(mActivityTestRule.getActivity());
mOverridenFactory = new GcTestDependencyFactory(context);
AwTestContainerView containerView =
mActivityTestRule.createAwTestContainerViewOnMainSync(client);
mOverridenFactory = null;
mActivityTestRule.loadUrlAsync(
containerView.getAwContents(), ContentUrlConstants.ABOUT_BLANK_DISPLAY_URL);
containerView = null;
return null;
});
}
@Test
@DisableHardwareAccelerationForTest
@LargeTest
@Feature({"AndroidWebView"})
public void testCreateAndGcManyTimes() throws Throwable {
runAwContentsGcTest(() -> {
final int concurrentInstances = 4;
final int repetitions = 16;
for (int i = 0; i < repetitions; ++i) {
for (int j = 0; j < concurrentInstances; ++j) {
StrongRefTestAwContentsClient client = new StrongRefTestAwContentsClient();
StrongRefTestContext context =
new StrongRefTestContext(mActivityTestRule.getActivity());
mOverridenFactory = new GcTestDependencyFactory(context);
AwTestContainerView view =
mActivityTestRule.createAwTestContainerViewOnMainSync(client);
mOverridenFactory = null;
// Embedding app can hold onto a strong ref to the WebView from either
// WebViewClient or WebChromeClient. That should not prevent WebView from
// gc-ed. We simulate that behavior by making the equivalent change here,
// have AwContentsClient hold a strong ref to the AwContents object.
client.setAwContentsStrongRef(view.getAwContents());
context.setAwContentsStrongRef(view.getAwContents());
mActivityTestRule.loadUrlAsync(
view.getAwContents(), ContentUrlConstants.ABOUT_BLANK_DISPLAY_URL);
}
Assert.assertTrue(AwContents.getNativeInstanceCount() >= concurrentInstances);
Assert.assertTrue(
AwContents.getNativeInstanceCount() <= (i + 1) * concurrentInstances);
removeAllViews();
}
return null;
});
}
@Test
@DisableHardwareAccelerationForTest
@SmallTest
@Feature({"AndroidWebView"})
public void testGcAfterUsingJavascriptObject() throws Throwable {
runAwContentsGcTest(() -> {
// Javascript object with a reference to WebView.
class Test {
Test(int value, AwContents awContents) {
mValue = value;
mAwContents = awContents;
}
@JavascriptInterface
public int getValue() {
return mValue;
}
public AwContents getAwContents() {
return mAwContents;
}
private int mValue;
private AwContents mAwContents;
}
String html = "<html>Hello World</html>";
TestAwContentsClient contentsClient = new TestAwContentsClient();
AwTestContainerView containerView =
mActivityTestRule.createAwTestContainerViewOnMainSync(contentsClient);
AwActivityTestRule.enableJavaScriptOnUiThread(containerView.getAwContents());
final AwContents awContents = containerView.getAwContents();
final Test jsObject = new Test(42, awContents);
AwActivityTestRule.addJavascriptInterfaceOnUiThread(awContents, jsObject, "test");
mActivityTestRule.loadDataSync(
awContents, contentsClient.getOnPageFinishedHelper(), html, "text/html", false);
Assert.assertEquals(String.valueOf(42),
mActivityTestRule.executeJavaScriptAndWaitForResult(
awContents, contentsClient, "test.getValue()"));
containerView = null;
return null;
});
}
// This moves the test body that manipulates AwContents and such objects into
// a stack frame that's guaranteed to be cleared when the gc checks are run.
// Otherwise the thread may hold local references (ie from stack variables)
// to objects.
private void runAwContentsGcTest(Callable<Object> setup) throws Exception {
gcAndCheckAllAwContentsDestroyed();
Object heldObject = setup.call();
try {
removeAllViews();
// This clears a reference that InputMethodManager holds onto focused view.
TestThreadUtils.runOnUiThreadBlocking(() -> {
Window window = mActivityTestRule.getActivity().getWindow();
window.addFlags(WindowManager.LayoutParams.FLAG_LOCAL_FOCUS_MODE);
window.setLocalFocus(false, false);
window.clearFlags(WindowManager.LayoutParams.FLAG_LOCAL_FOCUS_MODE);
});
gcAndCheckAllAwContentsDestroyed();
} finally {
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.P) {
Reference.reachabilityFence(heldObject);
}
}
}
private void removeAllViews() {
InstrumentationRegistry.getInstrumentation().runOnMainSync(
() -> mActivityTestRule.getActivity().removeAllViews());
}
private void gcAndCheckAllAwContentsDestroyed() {
Runtime.getRuntime().gc();
Runnable criteria = () -> {
Pair<Integer, Integer> nativeCounts = null;
try {
nativeCounts = TestThreadUtils.runOnUiThreadBlocking(() -> {
return Pair.create(AwContents.getNativeInstanceCount(),
AwGLFunctor.getNativeInstanceCount());
});
} catch (Exception e) {
throw new CriteriaNotSatisfiedException(e);
}
Criteria.checkThat("AwContents count", (int) nativeCounts.first, Matchers.is(0));
Criteria.checkThat("AwGLFunctor count", (int) nativeCounts.second, Matchers.is(0));
};
// Depending on a single gc call can make this test flaky. It's possible
// that the WebView still has transient references during load so it does not get
// gc-ed in the one gc-call above. Instead call gc again if exit criteria fails to
// catch this case.
final long timeoutBetweenGcMs = 1000L;
for (int i = 0; i < 15; ++i) {
try {
CriteriaHelper.pollInstrumentationThread(
criteria, timeoutBetweenGcMs, CHECK_INTERVAL);
break;
} catch (AssertionError e) {
Runtime.getRuntime().gc();
}
}
// Ensure it passes w/o Assertions.
criteria.run();
}
}