[go: nahoru, domu]

blob: 429de123832d08dafee694b72bb341d794f36bdf [file] [log] [blame]
// Copyright 2015 The Chromium Authors
// 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.app.Activity;
import android.content.Context;
import android.content.ContextWrapper;
import android.os.Build;
import android.os.ResultReceiver;
import android.util.Pair;
import android.view.Window;
import android.view.WindowManager;
import android.webkit.JavascriptInterface;
import androidx.test.InstrumentationRegistry;
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.junit.runners.Parameterized;
import org.junit.runners.Parameterized.UseParametersRunnerFactory;
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.DoNotBatch;
import org.chromium.base.test.util.Feature;
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 org.chromium.ui.accessibility.AccessibilityState;
import java.lang.ref.PhantomReference;
import java.lang.ref.Reference;
import java.lang.ref.ReferenceQueue;
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 @DisableHardwareAcceleration is needed.
*/
@RunWith(Parameterized.class)
@UseParametersRunnerFactory(AwJUnit4ClassRunnerWithParameters.Factory.class)
@DoNotBatch(reason = "GC tests require full restarts")
public class AwContentsGarbageCollectionTest extends AwParameterizedTest {
@Rule public AwActivityTestRule mActivityTestRule;
public AwContentsGarbageCollectionTest(AwSettingsMutation param) {
mActivityTestRule =
new AwActivityTestRule(param.getMutation()) {
@Override
public TestDependencyFactory createTestDependencyFactory() {
if (mOverriddenFactory == null) {
return new TestDependencyFactory();
} else {
return mOverriddenFactory;
}
}
};
}
private TestDependencyFactory mOverriddenFactory;
@After
public void tearDown() {
mOverriddenFactory = 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
@DisableHardwareAcceleration
@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
@DisableHardwareAcceleration
@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
@DisableHardwareAcceleration
@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(
() -> {
// Enable a11y for testing.
AccessibilityState.setIsAnyAccessibilityServiceEnabledForTesting(
true);
// Initialize native object.
containerView.getAccessibilityNodeProvider();
WebContentsAccessibility webContentsA11y =
WebContentsAccessibility.fromWebContents(
containerView.getWebContents());
Assert.assertTrue(webContentsA11y.isNativeInitialized());
});
return null;
});
}
@Test
@DisableHardwareAcceleration
@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
@DisableHardwareAcceleration
@SmallTest
@Feature({"AndroidWebView"})
public void testReferenceFromContext() throws Throwable {
runAwContentsGcTest(
() -> {
TestAwContentsClient client = new TestAwContentsClient();
StrongRefTestContext context =
new StrongRefTestContext(mActivityTestRule.getActivity());
mOverriddenFactory = new GcTestDependencyFactory(context);
AwTestContainerView containerView =
mActivityTestRule.createAwTestContainerViewOnMainSync(client);
context.setAwContentsStrongRef(containerView.getAwContents());
mOverriddenFactory = null;
mActivityTestRule.loadUrlAsync(
containerView.getAwContents(),
ContentUrlConstants.ABOUT_BLANK_DISPLAY_URL);
containerView = null;
return null;
});
}
@Test
@DisableHardwareAcceleration
@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());
mOverriddenFactory = new GcTestDependencyFactory(context);
AwTestContainerView view =
mActivityTestRule.createAwTestContainerViewOnMainSync(client);
mOverriddenFactory = 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
@DisableHardwareAcceleration
@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;
});
}
@Test
@DisableHardwareAcceleration
@LargeTest
public void testActivityDoesNotLeak() throws Throwable {
// Test that Activity should not leak if view is still attached after activity is destroyed.
ReferenceQueue<Activity> referenceQueue = new ReferenceQueue<>();
PhantomReference<Activity> reference;
{
Activity activity = mActivityTestRule.getActivity();
reference = new PhantomReference<>(activity, referenceQueue);
TestAwContentsClient client = new TestAwContentsClient();
AwTestContainerView containerView =
mActivityTestRule.createAwTestContainerViewOnMainSync(client);
mActivityTestRule.loadUrlAsync(
containerView.getAwContents(), ContentUrlConstants.ABOUT_BLANK_DISPLAY_URL);
mActivityTestRule.recreateActivity();
boolean destroyed =
TestThreadUtils.runOnUiThreadBlockingNoException(() -> activity.isDestroyed());
Assert.assertTrue(destroyed);
}
Runtime.getRuntime().gc();
final long timeoutMs = 30000L;
CriteriaHelper.pollInstrumentationThread(
() -> {
Runtime.getRuntime().gc();
Reference enqueuedReference = referenceQueue.poll();
Criteria.checkThat(enqueuedReference, Matchers.notNullValue());
Criteria.checkThat(enqueuedReference, Matchers.is(reference));
},
timeoutMs,
CHECK_INTERVAL);
gcAndCheckAllAwContentsDestroyed();
}
// 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();
}
}