| // 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(); |
| } |
| } |