// Copyright 2012 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.content.browser;

import android.os.Handler;
import android.os.Looper;
import android.test.suitebuilder.annotation.SmallTest;

import org.chromium.base.test.util.DisabledTest;
import org.chromium.base.test.util.Feature;
import org.chromium.content.browser.test.util.TestCallbackHelperContainer;
import org.chromium.content_shell_apk.ContentShellActivity;

import java.lang.annotation.Annotation;
import java.lang.annotation.ElementType;
import java.lang.annotation.Retention;
import java.lang.annotation.RetentionPolicy;
import java.lang.annotation.Target;
import java.lang.ref.WeakReference;

/**
 * Part of the test suite for the Java Bridge. Tests a number of features including ...
 * - The type of injected objects
 * - The type of their methods
 * - Replacing objects
 * - Removing objects
 * - Access control
 * - Calling methods on returned objects
 * - Multiply injected objects
 * - Threading
 * - Inheritance
 */
public class JavaBridgeBasicsTest extends JavaBridgeTestBase {
    private class TestController extends Controller {
        private int mIntValue;
        private long mLongValue;
        private String mStringValue;
        private boolean mBooleanValue;

        public synchronized void setIntValue(int x) {
            mIntValue = x;
            notifyResultIsReady();
        }
        public synchronized void setLongValue(long x) {
            mLongValue = x;
            notifyResultIsReady();
        }
        public synchronized void setStringValue(String x) {
            mStringValue = x;
            notifyResultIsReady();
        }
        public synchronized void setBooleanValue(boolean x) {
            mBooleanValue = x;
            notifyResultIsReady();
        }

        public synchronized int waitForIntValue() {
            waitForResult();
            return mIntValue;
        }
        public synchronized long waitForLongValue() {
            waitForResult();
            return mLongValue;
        }
        public synchronized String waitForStringValue() {
            waitForResult();
            return mStringValue;
        }
        public synchronized boolean waitForBooleanValue() {
            waitForResult();
            return mBooleanValue;
        }

        public synchronized String getStringValue() {
            return mStringValue;
        }
    }

    private static class ObjectWithStaticMethod {
        public static String staticMethod() {
            return "foo";
        }
    }

    TestController mTestController;

    @Override
    protected void setUp() throws Exception {
        super.setUp();
        mTestController = new TestController();
        setUpContentView(mTestController, "testController");
    }

    @Override
    protected ContentShellActivity launchContentShellWithUrl(String url) {
        // Expose a global function "gc()" into pages.
        return launchContentShellWithUrlAndCommandLineArgs(
                url, new String[]{ "--js-flags=--expose-gc" });
    }

    // Note that this requires that we can pass a JavaScript string to Java.
    protected String executeJavaScriptAndGetStringResult(String script) throws Throwable {
        executeJavaScript("testController.setStringValue(" + script + ");");
        return mTestController.waitForStringValue();
    }

    protected void injectObjectAndReload(final Object object, final String name) throws Throwable {
        injectObjectAndReload(object, name, null);
    }

    protected void injectObjectAndReload(final Object object, final String name,
            final Class<? extends Annotation> requiredAnnotation) throws Throwable {
        TestCallbackHelperContainer.OnPageFinishedHelper onPageFinishedHelper =
                mTestCallbackHelperContainer.getOnPageFinishedHelper();
        int currentCallCount = onPageFinishedHelper.getCallCount();
        runTestOnUiThread(new Runnable() {
            @Override
            public void run() {
                getContentViewCore().addPossiblyUnsafeJavascriptInterface(object,
                        name, requiredAnnotation);
                getContentViewCore().reload(true);
            }
        });
        onPageFinishedHelper.waitForCallback(currentCallCount);
    }

    protected void synchronousPageReload() throws Throwable {
        TestCallbackHelperContainer.OnPageFinishedHelper onPageFinishedHelper =
                mTestCallbackHelperContainer.getOnPageFinishedHelper();
        int currentCallCount = onPageFinishedHelper.getCallCount();
        runTestOnUiThread(new Runnable() {
            @Override
            public void run() {
                getContentViewCore().reload(true);
            }
        });
        onPageFinishedHelper.waitForCallback(currentCallCount);
    }

    // Note that this requires that we can pass a JavaScript boolean to Java.
    private void assertRaisesException(String script) throws Throwable {
        executeJavaScript("try {" +
                          script + ";" +
                          "  testController.setBooleanValue(false);" +
                          "} catch (exception) {" +
                          "  testController.setBooleanValue(true);" +
                          "}");
        assertTrue(mTestController.waitForBooleanValue());
    }

    @SmallTest
    @Feature({"AndroidWebView", "Android-JavaBridge"})
    public void testTypeOfInjectedObject() throws Throwable {
        assertEquals("object", executeJavaScriptAndGetStringResult("typeof testController"));
    }

    @SmallTest
    @Feature({"AndroidWebView", "Android-JavaBridge"})
    public void testAdditionNotReflectedUntilReload() throws Throwable {
        assertEquals("undefined", executeJavaScriptAndGetStringResult("typeof testObject"));
        runTestOnUiThread(new Runnable() {
            @Override
            public void run() {
                getContentViewCore().addPossiblyUnsafeJavascriptInterface(
                        new Object(), "testObject", null);
            }
        });
        assertEquals("undefined", executeJavaScriptAndGetStringResult("typeof testObject"));
        synchronousPageReload();
        assertEquals("object", executeJavaScriptAndGetStringResult("typeof testObject"));
    }

    @SmallTest
    @Feature({"AndroidWebView", "Android-JavaBridge"})
    public void testRemovalNotReflectedUntilReload() throws Throwable {
        injectObjectAndReload(new Object(), "testObject");
        assertEquals("object", executeJavaScriptAndGetStringResult("typeof testObject"));
        runTestOnUiThread(new Runnable() {
            @Override
            public void run() {
                getContentViewCore().removeJavascriptInterface("testObject");
            }
        });
        assertEquals("object", executeJavaScriptAndGetStringResult("typeof testObject"));
        synchronousPageReload();
        assertEquals("undefined", executeJavaScriptAndGetStringResult("typeof testObject"));
    }

    @SmallTest
    @Feature({"AndroidWebView", "Android-JavaBridge"})
    public void testRemoveObjectNotAdded() throws Throwable {
        TestCallbackHelperContainer.OnPageFinishedHelper onPageFinishedHelper =
                mTestCallbackHelperContainer.getOnPageFinishedHelper();
        int currentCallCount = onPageFinishedHelper.getCallCount();
        runTestOnUiThread(new Runnable() {
            @Override
            public void run() {
                getContentViewCore().removeJavascriptInterface("foo");
                getContentViewCore().reload(true);
            }
        });
        onPageFinishedHelper.waitForCallback(currentCallCount);
        assertEquals("undefined", executeJavaScriptAndGetStringResult("typeof foo"));
    }

    @SmallTest
    @Feature({"AndroidWebView", "Android-JavaBridge"})
    public void testTypeOfMethod() throws Throwable {
        assertEquals("function",
                executeJavaScriptAndGetStringResult("typeof testController.setStringValue"));
    }

    @SmallTest
    @Feature({"AndroidWebView", "Android-JavaBridge"})
    public void testTypeOfInvalidMethod() throws Throwable {
        assertEquals("undefined", executeJavaScriptAndGetStringResult("typeof testController.foo"));
    }

    @SmallTest
    @Feature({"AndroidWebView", "Android-JavaBridge"})
    public void testCallingInvalidMethodRaisesException() throws Throwable {
        assertRaisesException("testController.foo()");
    }

    @SmallTest
    @Feature({"AndroidWebView", "Android-JavaBridge"})
    public void testUncaughtJavaExceptionRaisesJavaScriptException() throws Throwable {
        injectObjectAndReload(new Object() {
            public void method() { throw new RuntimeException("foo"); }
        }, "testObject");
        assertRaisesException("testObject.method()");
    }

    // Note that this requires that we can pass a JavaScript string to Java.
    @SmallTest
    @Feature({"AndroidWebView", "Android-JavaBridge"})
    public void testTypeOfStaticMethod() throws Throwable {
        injectObjectAndReload(new ObjectWithStaticMethod(), "testObject");
        executeJavaScript("testController.setStringValue(typeof testObject.staticMethod)");
        assertEquals("function", mTestController.waitForStringValue());
    }

    // Note that this requires that we can pass a JavaScript string to Java.
    @SmallTest
    @Feature({"AndroidWebView", "Android-JavaBridge"})
    public void testCallStaticMethod() throws Throwable {
        injectObjectAndReload(new ObjectWithStaticMethod(), "testObject");
        executeJavaScript("testController.setStringValue(testObject.staticMethod())");
        assertEquals("foo", mTestController.waitForStringValue());
    }

    @SmallTest
    @Feature({"AndroidWebView", "Android-JavaBridge"})
    public void testPrivateMethodNotExposed() throws Throwable {
        injectObjectAndReload(new Object() {
            private void method() {}
            protected void method2() {}
        }, "testObject");
        assertEquals("undefined",
                executeJavaScriptAndGetStringResult("typeof testObject.method"));
        assertEquals("undefined",
                executeJavaScriptAndGetStringResult("typeof testObject.method2"));
    }

    @SmallTest
    @Feature({"AndroidWebView", "Android-JavaBridge"})
    public void testReplaceInjectedObject() throws Throwable {
        injectObjectAndReload(new Object() {
            public void method() { mTestController.setStringValue("object 1"); }
        }, "testObject");
        executeJavaScript("testObject.method()");
        assertEquals("object 1", mTestController.waitForStringValue());

        injectObjectAndReload(new Object() {
            public void method() { mTestController.setStringValue("object 2"); }
        }, "testObject");
        executeJavaScript("testObject.method()");
        assertEquals("object 2", mTestController.waitForStringValue());
    }

    @SmallTest
    @Feature({"AndroidWebView", "Android-JavaBridge"})
    public void testInjectNullObjectIsIgnored() throws Throwable {
        injectObjectAndReload(null, "testObject");
        assertEquals("undefined", executeJavaScriptAndGetStringResult("typeof testObject"));
    }

    @SmallTest
    @Feature({"AndroidWebView", "Android-JavaBridge"})
    public void testReplaceInjectedObjectWithNullObjectIsIgnored() throws Throwable {
        injectObjectAndReload(new Object(), "testObject");
        assertEquals("object", executeJavaScriptAndGetStringResult("typeof testObject"));
        injectObjectAndReload(null, "testObject");
        assertEquals("object", executeJavaScriptAndGetStringResult("typeof testObject"));
    }

    @SmallTest
    @Feature({"AndroidWebView", "Android-JavaBridge"})
    public void testCallOverloadedMethodWithDifferentNumberOfArguments() throws Throwable {
        injectObjectAndReload(new Object() {
            public void method() { mTestController.setStringValue("0 args"); }
            public void method(int x) { mTestController.setStringValue("1 arg"); }
            public void method(int x, int y) { mTestController.setStringValue("2 args"); }
        }, "testObject");
        executeJavaScript("testObject.method()");
        assertEquals("0 args", mTestController.waitForStringValue());
        executeJavaScript("testObject.method(42)");
        assertEquals("1 arg", mTestController.waitForStringValue());
        executeJavaScript("testObject.method(null)");
        assertEquals("1 arg", mTestController.waitForStringValue());
        executeJavaScript("testObject.method(undefined)");
        assertEquals("1 arg", mTestController.waitForStringValue());
        executeJavaScript("testObject.method(42, 42)");
        assertEquals("2 args", mTestController.waitForStringValue());
    }

    @SmallTest
    @Feature({"AndroidWebView", "Android-JavaBridge"})
    public void testCallMethodWithWrongNumberOfArgumentsRaisesException() throws Throwable {
        assertRaisesException("testController.setIntValue()");
        assertRaisesException("testController.setIntValue(42, 42)");
    }

    @SmallTest
    @Feature({"AndroidWebView", "Android-JavaBridge"})
    public void testObjectPersistsAcrossPageLoads() throws Throwable {
        assertEquals("object", executeJavaScriptAndGetStringResult("typeof testController"));
        synchronousPageReload();
        assertEquals("object", executeJavaScriptAndGetStringResult("typeof testController"));
    }

    @SmallTest
    @Feature({"AndroidWebView", "Android-JavaBridge"})
    public void testCustomPropertiesCleanedUpOnPageReloads() throws Throwable {
        assertEquals("object", executeJavaScriptAndGetStringResult("typeof testController"));
        executeJavaScript("testController.myProperty = 42;");
        assertEquals("42", executeJavaScriptAndGetStringResult("testController.myProperty"));
        synchronousPageReload();
        assertEquals("object", executeJavaScriptAndGetStringResult("typeof testController"));
        assertEquals("undefined", executeJavaScriptAndGetStringResult("testController.myProperty"));
    }

    @SmallTest
    @Feature({"AndroidWebView", "Android-JavaBridge"})
    public void testSameObjectInjectedMultipleTimes() throws Throwable {
        class TestObject {
            private int mNumMethodInvocations;
            public void method() { mTestController.setIntValue(++mNumMethodInvocations); }
        }
        final TestObject testObject = new TestObject();
        TestCallbackHelperContainer.OnPageFinishedHelper onPageFinishedHelper =
                mTestCallbackHelperContainer.getOnPageFinishedHelper();
        int currentCallCount = onPageFinishedHelper.getCallCount();
        runTestOnUiThread(new Runnable() {
            @Override
            public void run() {
                getContentViewCore().addPossiblyUnsafeJavascriptInterface(
                        testObject, "testObject1", null);
                getContentViewCore().addPossiblyUnsafeJavascriptInterface(
                        testObject, "testObject2", null);
                getContentViewCore().reload(true);
            }
        });
        onPageFinishedHelper.waitForCallback(currentCallCount);
        executeJavaScript("testObject1.method()");
        assertEquals(1, mTestController.waitForIntValue());
        executeJavaScript("testObject2.method()");
        assertEquals(2, mTestController.waitForIntValue());
    }

    @SmallTest
    @Feature({"AndroidWebView", "Android-JavaBridge"})
    public void testCallMethodOnReturnedObject() throws Throwable {
        injectObjectAndReload(new Object() {
            public Object getInnerObject() {
                return new Object() {
                    public void method(int x) { mTestController.setIntValue(x); }
                };
            }
        }, "testObject");
        executeJavaScript("testObject.getInnerObject().method(42)");
        assertEquals(42, mTestController.waitForIntValue());
    }

    @SmallTest
    @Feature({"AndroidWebView", "Android-JavaBridge"})
    public void testReturnedObjectInjectedElsewhere() throws Throwable {
        class InnerObject {
            private int mNumMethodInvocations;
            public void method() { mTestController.setIntValue(++mNumMethodInvocations); }
        }
        final InnerObject innerObject = new InnerObject();
        final Object object = new Object() {
            public InnerObject getInnerObject() {
                return innerObject;
            }
        };
        TestCallbackHelperContainer.OnPageFinishedHelper onPageFinishedHelper =
                mTestCallbackHelperContainer.getOnPageFinishedHelper();
        int currentCallCount = onPageFinishedHelper.getCallCount();
        runTestOnUiThread(new Runnable() {
            @Override
            public void run() {
                getContentViewCore().addPossiblyUnsafeJavascriptInterface(
                        object, "testObject", null);
                getContentViewCore().addPossiblyUnsafeJavascriptInterface(
                        innerObject, "innerObject", null);
                getContentViewCore().reload(true);
            }
        });
        onPageFinishedHelper.waitForCallback(currentCallCount);
        executeJavaScript("testObject.getInnerObject().method()");
        assertEquals(1, mTestController.waitForIntValue());
        executeJavaScript("innerObject.method()");
        assertEquals(2, mTestController.waitForIntValue());
    }

    // Verify that Java objects returned from bridge object methods are dereferenced
    // on the Java side once they have been fully dereferenced on the JS side.
    // Failing this test would mean that methods returning objects effectively create a memory
    // leak.
    @SmallTest
    @Feature({"AndroidWebView", "Android-JavaBridge"})
    public void testReturnedObjectIsGarbageCollected() throws Throwable {
        // Make sure V8 exposes "gc" property on the global object (enabled with --expose-gc flag)
        assertEquals("function", executeJavaScriptAndGetStringResult("typeof gc"));
        class InnerObject {
        }
        class TestObject {
            public InnerObject getInnerObject() {
                InnerObject inner = new InnerObject();
                weakRefForInner = new WeakReference<InnerObject>(inner);
                return inner;
            }
            // A weak reference is used to check InnerObject instance reachability.
            WeakReference<InnerObject> weakRefForInner;
        }
        TestObject object = new TestObject();
        injectObjectAndReload(object, "testObject");
        // Initially, store a reference to the inner object in JS to make sure it's not
        // garbage-collected prematurely.
        assertEquals("object", executeJavaScriptAndGetStringResult(
                        "(function() { " +
                        "globalInner = testObject.getInnerObject(); return typeof globalInner; " +
                        "})()"));
        assertTrue(object.weakRefForInner.get() != null);
        // Check that returned Java object is being held by the Java bridge, thus it's not
        // collected.  Note that despite that what JavaDoc says about invoking "gc()", both Dalvik
        // and ART actually run the collector.
        Runtime.getRuntime().gc();
        assertTrue(object.weakRefForInner.get() != null);
        // Now dereference the inner object in JS and run GC to collect the interface object.
        assertEquals("true", executeJavaScriptAndGetStringResult(
                        "(function() { " +
                        "delete globalInner; gc(); return (typeof globalInner == 'undefined'); " +
                        "})()"));
        // Force GC on the Java side again. The bridge had to release the inner object, so it must
        // be collected this time.
        Runtime.getRuntime().gc();
        assertEquals(null, object.weakRefForInner.get());
    }

    /*
     * The current Java bridge implementation doesn't reuse JS wrappers when returning
     * the same object from a method. That looks wrong. For example, in the case of DOM,
     * wrappers are reused, which allows JS code to attach custom properties to interface
     * objects and use them regardless of the way the reference has been obtained:
     * via copying a JS reference or by calling the method one more time (assuming that
     * the method is supposed to return a reference to the same object each time).
     * TODO(mnaganov): Fix this in the new implementation.
     *
     * @SmallTest
     * @Feature({"AndroidWebView", "Android-JavaBridge"})
     */
    @DisabledTest
    public void testSameReturnedObjectUsesSameWrapper() throws Throwable {
        class InnerObject {
        }
        final InnerObject innerObject = new InnerObject();
        final Object injectedTestObject = new Object() {
            public InnerObject getInnerObject() {
                return innerObject;
            }
        };
        injectObjectAndReload(injectedTestObject, "injectedTestObject");
        executeJavaScript("inner1 = injectedTestObject.getInnerObject()");
        executeJavaScript("inner2 = injectedTestObject.getInnerObject()");
        assertEquals("object", executeJavaScriptAndGetStringResult("typeof inner1"));
        assertEquals("object", executeJavaScriptAndGetStringResult("typeof inner2"));
        assertEquals("true", executeJavaScriptAndGetStringResult("inner1 === inner2"));
    }

    @SmallTest
    @Feature({"AndroidWebView", "Android-JavaBridge"})
    public void testMethodInvokedOnBackgroundThread() throws Throwable {
        injectObjectAndReload(new Object() {
            public void captureThreadId() {
                mTestController.setLongValue(Thread.currentThread().getId());
            }
        }, "testObject");
        executeJavaScript("testObject.captureThreadId()");
        final long threadId = mTestController.waitForLongValue();
        assertFalse(threadId == Thread.currentThread().getId());
        runTestOnUiThread(new Runnable() {
            @Override
            public void run() {
                assertFalse(threadId == Thread.currentThread().getId());
            }
        });
    }

    @SmallTest
    @Feature({"AndroidWebView", "Android-JavaBridge"})
    public void testPublicInheritedMethod() throws Throwable {
        class Base {
            public void method(int x) { mTestController.setIntValue(x); }
        }
        class Derived extends Base {
        }
        injectObjectAndReload(new Derived(), "testObject");
        assertEquals("function", executeJavaScriptAndGetStringResult("typeof testObject.method"));
        executeJavaScript("testObject.method(42)");
        assertEquals(42, mTestController.waitForIntValue());
    }

    @SmallTest
    @Feature({"AndroidWebView", "Android-JavaBridge"})
    public void testPrivateInheritedMethod() throws Throwable {
        class Base {
            private void method() {}
        }
        class Derived extends Base {
        }
        injectObjectAndReload(new Derived(), "testObject");
        assertEquals("undefined", executeJavaScriptAndGetStringResult("typeof testObject.method"));
    }

    @SmallTest
    @Feature({"AndroidWebView", "Android-JavaBridge"})
    public void testOverriddenMethod() throws Throwable {
        class Base {
            public void method() { mTestController.setStringValue("base"); }
        }
        class Derived extends Base {
            @Override
            public void method() { mTestController.setStringValue("derived"); }
        }
        injectObjectAndReload(new Derived(), "testObject");
        executeJavaScript("testObject.method()");
        assertEquals("derived", mTestController.waitForStringValue());
    }

    @SmallTest
    @Feature({"AndroidWebView", "Android-JavaBridge"})
    public void testEnumerateMembers() throws Throwable {
        injectObjectAndReload(new Object() {
            public void method() {}
            private void privateMethod() {}
            public int field;
            private int privateField;
        }, "testObject");
        executeJavaScript(
                "var result = \"\"; " +
                "for (x in testObject) { result += \" \" + x } " +
                "testController.setStringValue(result);");
        assertEquals(" equals getClass hashCode method notify notifyAll toString wait",
                mTestController.waitForStringValue());
    }

    @SmallTest
    @Feature({"AndroidWebView", "Android-JavaBridge"})
    public void testReflectPublicMethod() throws Throwable {
        injectObjectAndReload(new Object() {
            public Class<?> myGetClass() { return getClass(); }
            public String method() { return "foo"; }
        }, "testObject");
        assertEquals("foo", executeJavaScriptAndGetStringResult(
                "testObject.myGetClass().getMethod('method', null).invoke(testObject, null)" +
                ".toString()"));
    }

    @SmallTest
    @Feature({"AndroidWebView", "Android-JavaBridge"})
    public void testReflectPublicField() throws Throwable {
        injectObjectAndReload(new Object() {
            public Class<?> myGetClass() { return getClass(); }
            public String field = "foo";
        }, "testObject");
        assertEquals("foo", executeJavaScriptAndGetStringResult(
                "testObject.myGetClass().getField('field').get(testObject).toString()"));
    }

    @SmallTest
    @Feature({"AndroidWebView", "Android-JavaBridge"})
    public void testReflectPrivateMethodRaisesException() throws Throwable {
        injectObjectAndReload(new Object() {
            public Class<?> myGetClass() { return getClass(); }
            private void method() {};
        }, "testObject");
        assertRaisesException("testObject.myGetClass().getMethod('method', null)");
        // getDeclaredMethod() is able to access a private method, but invoke()
        // throws a Java exception.
        assertRaisesException(
                "testObject.myGetClass().getDeclaredMethod('method', null)." +
                "invoke(testObject, null)");
    }

    @SmallTest
    @Feature({"AndroidWebView", "Android-JavaBridge"})
    public void testReflectPrivateFieldRaisesException() throws Throwable {
        injectObjectAndReload(new Object() {
            public Class<?> myGetClass() { return getClass(); }
            private int field;
        }, "testObject");
        assertRaisesException("testObject.myGetClass().getField('field')");
        // getDeclaredField() is able to access a private field, but getInt()
        // throws a Java exception.
        assertRaisesException(
                "testObject.myGetClass().getDeclaredField('field').getInt(testObject)");
    }

    @SmallTest
    @Feature({"AndroidWebView", "Android-JavaBridge"})
    public void testAllowNonAnnotatedMethods() throws Throwable {
        injectObjectAndReload(new Object() {
            public String allowed() { return "foo"; }
        }, "testObject", null);

        // Test calling a method of an explicitly inherited class (Base#allowed()).
        assertEquals("foo", executeJavaScriptAndGetStringResult("testObject.allowed()"));

        // Test calling a method of an implicitly inherited class (Object#toString()).
        assertEquals("string", executeJavaScriptAndGetStringResult("typeof testObject.toString()"));
    }

    @SmallTest
    @Feature({"AndroidWebView", "Android-JavaBridge"})
    public void testAllowOnlyAnnotatedMethods() throws Throwable {
        injectObjectAndReload(new Object() {
            @JavascriptInterface
            public String allowed() { return "foo"; }

            public String disallowed() { return "bar"; }
        }, "testObject", JavascriptInterface.class);

        // getClass() is an Object method and does not have the @JavascriptInterface annotation and
        // should not be able to be called.
        assertRaisesException("testObject.getClass()");
        assertEquals("undefined", executeJavaScriptAndGetStringResult(
                "typeof testObject.getClass"));

        // allowed() is marked with the @JavascriptInterface annotation and should be allowed to be
        // called.
        assertEquals("foo", executeJavaScriptAndGetStringResult("testObject.allowed()"));

        // disallowed() is not marked with the @JavascriptInterface annotation and should not be
        // able to be called.
        assertRaisesException("testObject.disallowed()");
        assertEquals("undefined", executeJavaScriptAndGetStringResult(
                "typeof testObject.disallowed"));
    }

    @SmallTest
    @Feature({"AndroidWebView", "Android-JavaBridge"})
    public void testAnnotationRequirementRetainsPropertyAcrossObjects() throws Throwable {
        class Test {
            @JavascriptInterface
            public String safe() { return "foo"; }

            public String unsafe() { return "bar"; }
        }

        class TestReturner {
            @JavascriptInterface
            public Test getTest() { return new Test(); }
        }

        // First test with safe mode off.
        injectObjectAndReload(new TestReturner(), "unsafeTestObject", null);

        // safe() should be able to be called regardless of whether or not we are in safe mode.
        assertEquals("foo", executeJavaScriptAndGetStringResult(
                "unsafeTestObject.getTest().safe()"));
        // unsafe() should be able to be called because we are not in safe mode.
        assertEquals("bar", executeJavaScriptAndGetStringResult(
                "unsafeTestObject.getTest().unsafe()"));

        // Now test with safe mode on.
        injectObjectAndReload(new TestReturner(), "safeTestObject", JavascriptInterface.class);

        // safe() should be able to be called regardless of whether or not we are in safe mode.
        assertEquals("foo", executeJavaScriptAndGetStringResult(
                "safeTestObject.getTest().safe()"));
        // unsafe() should not be able to be called because we are in safe mode.
        assertRaisesException("safeTestObject.getTest().unsafe()");
        assertEquals("undefined", executeJavaScriptAndGetStringResult(
                "typeof safeTestObject.getTest().unsafe"));
        // getClass() is an Object method and does not have the @JavascriptInterface annotation and
        // should not be able to be called.
        assertRaisesException("safeTestObject.getTest().getClass()");
        assertEquals("undefined", executeJavaScriptAndGetStringResult(
                "typeof safeTestObject.getTest().getClass"));
    }

    @SmallTest
    @Feature({"AndroidWebView", "Android-JavaBridge"})
    public void testAnnotationDoesNotGetInherited() throws Throwable {
        class Base {
            @JavascriptInterface
            public void base() { }
        }

        class Child extends Base {
            @Override
            public void base() { }
        }

        injectObjectAndReload(new Child(), "testObject", JavascriptInterface.class);

        // base() is inherited.  The inherited method does not have the @JavascriptInterface
        // annotation and should not be able to be called.
        assertRaisesException("testObject.base()");
        assertEquals("undefined", executeJavaScriptAndGetStringResult(
                "typeof testObject.base"));
    }

    @SuppressWarnings("javadoc")
    @Retention(RetentionPolicy.RUNTIME)
    @Target({ElementType.METHOD})
    @interface TestAnnotation {
    }

    @SmallTest
    @Feature({"AndroidWebView", "Android-JavaBridge"})
    public void testCustomAnnotationRestriction() throws Throwable {
        class Test {
            @TestAnnotation
            public String checkTestAnnotationFoo() { return "bar"; }

            @JavascriptInterface
            public String checkJavascriptInterfaceFoo() { return "bar"; }
        }

        // Inject javascriptInterfaceObj and require the JavascriptInterface annotation.
        injectObjectAndReload(new Test(), "javascriptInterfaceObj", JavascriptInterface.class);

        // Test#testAnnotationFoo() should fail, as it isn't annotated with JavascriptInterface.
        assertRaisesException("javascriptInterfaceObj.checkTestAnnotationFoo()");
        assertEquals("undefined", executeJavaScriptAndGetStringResult(
                "typeof javascriptInterfaceObj.checkTestAnnotationFoo"));

        // Test#javascriptInterfaceFoo() should pass, as it is annotated with JavascriptInterface.
        assertEquals("bar", executeJavaScriptAndGetStringResult(
                "javascriptInterfaceObj.checkJavascriptInterfaceFoo()"));

        // Inject testAnnotationObj and require the TestAnnotation annotation.
        injectObjectAndReload(new Test(), "testAnnotationObj", TestAnnotation.class);

        // Test#testAnnotationFoo() should pass, as it is annotated with TestAnnotation.
        assertEquals("bar", executeJavaScriptAndGetStringResult(
                "testAnnotationObj.checkTestAnnotationFoo()"));

        // Test#javascriptInterfaceFoo() should fail, as it isn't annotated with TestAnnotation.
        assertRaisesException("testAnnotationObj.checkJavascriptInterfaceFoo()");
        assertEquals("undefined", executeJavaScriptAndGetStringResult(
                "typeof testAnnotationObj.checkJavascriptInterfaceFoo"));
    }

    @SmallTest
    @Feature({"AndroidWebView", "Android-JavaBridge"})
    public void testAddJavascriptInterfaceIsSafeByDefault() throws Throwable {
        class Test {
            public String blocked() { return "bar"; }

            @JavascriptInterface
            public String allowed() { return "bar"; }
        }

        // Manually inject the Test object, making sure to use the
        // ContentViewCore#addJavascriptInterface, not the possibly unsafe version.
        TestCallbackHelperContainer.OnPageFinishedHelper onPageFinishedHelper =
                mTestCallbackHelperContainer.getOnPageFinishedHelper();
        int currentCallCount = onPageFinishedHelper.getCallCount();
        runTestOnUiThread(new Runnable() {
            @Override
            public void run() {
                getContentViewCore().addJavascriptInterface(new Test(),
                        "testObject");
                getContentViewCore().reload(true);
            }
        });
        onPageFinishedHelper.waitForCallback(currentCallCount);

        // Test#allowed() should pass, as it is annotated with JavascriptInterface.
        assertEquals("bar", executeJavaScriptAndGetStringResult(
                "testObject.allowed()"));

        // Test#blocked() should fail, as it isn't annotated with JavascriptInterface.
        assertRaisesException("testObject.blocked()");
        assertEquals("undefined", executeJavaScriptAndGetStringResult(
                "typeof testObject.blocked"));
    }

    @SmallTest
    @Feature({"AndroidWebView", "Android-JavaBridge"})
    public void testObjectsInspection() throws Throwable {
        class Test {
            @JavascriptInterface
            public String m1() { return "foo"; }

            @JavascriptInterface
            public String m2() { return "bar"; }

            @JavascriptInterface
            public String m2(int x) { return "bar " + x; }
        }

        final String jsObjectKeysTestTemplate = "Object.keys(%s).toString()";
        final String jsForInTestTemplate =
                "(function(){" +
                "  var s=[]; for(var m in %s) s.push(m); return s.join(\",\")" +
                "})()";
        final String inspectableObjectName = "testObj1";
        final String nonInspectableObjectName = "testObj2";

        // Inspection is enabled by default.
        injectObjectAndReload(new Test(), inspectableObjectName, JavascriptInterface.class);

        assertEquals("m1,m2", executeJavaScriptAndGetStringResult(
                        String.format(jsObjectKeysTestTemplate, inspectableObjectName)));
        assertEquals("m1,m2", executeJavaScriptAndGetStringResult(
                        String.format(jsForInTestTemplate, inspectableObjectName)));

        runTestOnUiThread(new Runnable() {
            @Override
            public void run() {
                getContentViewCore().setAllowJavascriptInterfacesInspection(false);
            }
        });

        injectObjectAndReload(new Test(), nonInspectableObjectName, JavascriptInterface.class);

        assertEquals("", executeJavaScriptAndGetStringResult(
                        String.format(jsObjectKeysTestTemplate, nonInspectableObjectName)));
        assertEquals("", executeJavaScriptAndGetStringResult(
                        String.format(jsForInTestTemplate, nonInspectableObjectName)));
    }

    @SmallTest
    @Feature({"AndroidWebView", "Android-JavaBridge"})
    public void testAccessToObjectGetClassIsBlocked() throws Throwable {
        injectObjectAndReload(new Object(), "testObject");
        assertEquals("function", executeJavaScriptAndGetStringResult("typeof testObject.getClass"));
        boolean securityExceptionThrown = false;
        try {
            final String result = executeJavaScriptAndWaitForExceptionSynchronously(
                    "typeof testObject.getClass()");
            fail("A call to java.lang.Object.getClass has been allowed, result: '" + result + "'");
        } catch (SecurityException exception) {
            securityExceptionThrown = true;
        }
        assertTrue(securityExceptionThrown);
    }

    // Unlike executeJavaScriptAndGetStringResult, this method is sitting on the UI thread
    // until a non-null result is obtained or a Java exception has been thrown. This method is
    // capable of catching Java RuntimeExceptions happening on the UI thread asynchronously.
    private String executeJavaScriptAndWaitForExceptionSynchronously(final String script)
            throws Throwable {
        class ExitLoopException extends RuntimeException {
        }
        mTestController.setStringValue(null);
        runTestOnUiThread(new Runnable() {
            @Override
            public void run() {
                getContentViewCore().loadUrl(new LoadUrlParams("javascript:(function() { " +
                                "testController.setStringValue(" + script + ") })()"));
                do {
                    final Boolean[] deactivateExitLoopTask = new Boolean[1];
                    deactivateExitLoopTask[0] = false;
                    // We can't use Loop.quit(), as this is the main looper, so we throw
                    // an exception to bail out from the loop.
                    new Handler(Looper.myLooper()).post(new Runnable() {
                        @Override
                        public void run() {
                            if (!deactivateExitLoopTask[0]) {
                                throw new ExitLoopException();
                            }
                        }
                    });
                    try {
                        Looper.loop();
                    } catch (ExitLoopException e) {
                        // Intentionally empty.
                    } catch (RuntimeException e) {
                        // Prevent the task that throws the ExitLoopException from exploding
                        // on the main loop outside of this function.
                        deactivateExitLoopTask[0] = true;
                        throw e;
                    }
                } while (mTestController.getStringValue() == null ||
                        // When an exception in an injected method happens, the function returns
                        // null. We ignore this and wait until the exception on the browser side
                        // will be thrown.
                        mTestController.getStringValue().equals("null"));
            }
        });
        return mTestController.getStringValue();
    }
}
