Home | History | Annotate | Download | only in browser
      1 // Copyright 2012 The Chromium Authors. All rights reserved.
      2 // Use of this source code is governed by a BSD-style license that can be
      3 // found in the LICENSE file.
      4 
      5 package org.chromium.content.browser;
      6 
      7 import android.test.suitebuilder.annotation.SmallTest;
      8 
      9 import org.chromium.base.test.util.Feature;
     10 import org.chromium.content.browser.test.util.TestCallbackHelperContainer;
     11 import org.chromium.content_shell_apk.ContentShellActivity;
     12 
     13 import java.lang.annotation.Annotation;
     14 import java.lang.annotation.ElementType;
     15 import java.lang.annotation.Retention;
     16 import java.lang.annotation.RetentionPolicy;
     17 import java.lang.annotation.Target;
     18 import java.lang.ref.WeakReference;
     19 
     20 /**
     21  * Part of the test suite for the Java Bridge. Tests a number of features including ...
     22  * - The type of injected objects
     23  * - The type of their methods
     24  * - Replacing objects
     25  * - Removing objects
     26  * - Access control
     27  * - Calling methods on returned objects
     28  * - Multiply injected objects
     29  * - Threading
     30  * - Inheritance
     31  */
     32 public class JavaBridgeBasicsTest extends JavaBridgeTestBase {
     33     private class TestController extends Controller {
     34         private int mIntValue;
     35         private long mLongValue;
     36         private String mStringValue;
     37         private boolean mBooleanValue;
     38 
     39         public synchronized void setIntValue(int x) {
     40             mIntValue = x;
     41             notifyResultIsReady();
     42         }
     43         public synchronized void setLongValue(long x) {
     44             mLongValue = x;
     45             notifyResultIsReady();
     46         }
     47         public synchronized void setStringValue(String x) {
     48             mStringValue = x;
     49             notifyResultIsReady();
     50         }
     51         public synchronized void setBooleanValue(boolean x) {
     52             mBooleanValue = x;
     53             notifyResultIsReady();
     54         }
     55 
     56         public synchronized int waitForIntValue() {
     57             waitForResult();
     58             return mIntValue;
     59         }
     60         public synchronized long waitForLongValue() {
     61             waitForResult();
     62             return mLongValue;
     63         }
     64         public synchronized String waitForStringValue() {
     65             waitForResult();
     66             return mStringValue;
     67         }
     68         public synchronized boolean waitForBooleanValue() {
     69             waitForResult();
     70             return mBooleanValue;
     71         }
     72 
     73         public synchronized String getStringValue() {
     74             return mStringValue;
     75         }
     76     }
     77 
     78     private static class ObjectWithStaticMethod {
     79         public static String staticMethod() {
     80             return "foo";
     81         }
     82     }
     83 
     84     TestController mTestController;
     85 
     86     @Override
     87     protected void setUp() throws Exception {
     88         super.setUp();
     89         mTestController = new TestController();
     90         setUpContentView(mTestController, "testController");
     91     }
     92 
     93     @Override
     94     protected ContentShellActivity launchContentShellWithUrl(String url) {
     95         // Expose a global function "gc()" into pages.
     96         return launchContentShellWithUrlAndCommandLineArgs(
     97                 url, new String[]{ "--js-flags=--expose-gc" });
     98     }
     99 
    100     // Note that this requires that we can pass a JavaScript string to Java.
    101     protected String executeJavaScriptAndGetStringResult(String script) throws Throwable {
    102         executeJavaScript("testController.setStringValue(" + script + ");");
    103         return mTestController.waitForStringValue();
    104     }
    105 
    106     protected void injectObjectAndReload(final Object object, final String name) throws Throwable {
    107         injectObjectAndReload(object, name, null);
    108     }
    109 
    110     protected void injectObjectAndReload(final Object object, final String name,
    111             final Class<? extends Annotation> requiredAnnotation) throws Throwable {
    112         TestCallbackHelperContainer.OnPageFinishedHelper onPageFinishedHelper =
    113                 mTestCallbackHelperContainer.getOnPageFinishedHelper();
    114         int currentCallCount = onPageFinishedHelper.getCallCount();
    115         runTestOnUiThread(new Runnable() {
    116             @Override
    117             public void run() {
    118                 getContentViewCore().addPossiblyUnsafeJavascriptInterface(object,
    119                         name, requiredAnnotation);
    120                 getContentViewCore().reload(true);
    121             }
    122         });
    123         onPageFinishedHelper.waitForCallback(currentCallCount);
    124     }
    125 
    126     protected void synchronousPageReload() throws Throwable {
    127         TestCallbackHelperContainer.OnPageFinishedHelper onPageFinishedHelper =
    128                 mTestCallbackHelperContainer.getOnPageFinishedHelper();
    129         int currentCallCount = onPageFinishedHelper.getCallCount();
    130         runTestOnUiThread(new Runnable() {
    131             @Override
    132             public void run() {
    133                 getContentViewCore().reload(true);
    134             }
    135         });
    136         onPageFinishedHelper.waitForCallback(currentCallCount);
    137     }
    138 
    139     // Note that this requires that we can pass a JavaScript boolean to Java.
    140     private void assertRaisesException(String script) throws Throwable {
    141         executeJavaScript("try {" +
    142                           script + ";" +
    143                           "  testController.setBooleanValue(false);" +
    144                           "} catch (exception) {" +
    145                           "  testController.setBooleanValue(true);" +
    146                           "}");
    147         assertTrue(mTestController.waitForBooleanValue());
    148     }
    149 
    150     @SmallTest
    151     @Feature({"AndroidWebView", "Android-JavaBridge"})
    152     public void testTypeOfInjectedObject() throws Throwable {
    153         assertEquals("object", executeJavaScriptAndGetStringResult("typeof testController"));
    154     }
    155 
    156     @SmallTest
    157     @Feature({"AndroidWebView", "Android-JavaBridge"})
    158     public void testAdditionNotReflectedUntilReload() throws Throwable {
    159         assertEquals("undefined", executeJavaScriptAndGetStringResult("typeof testObject"));
    160         runTestOnUiThread(new Runnable() {
    161             @Override
    162             public void run() {
    163                 getContentViewCore().addPossiblyUnsafeJavascriptInterface(
    164                         new Object(), "testObject", null);
    165             }
    166         });
    167         assertEquals("undefined", executeJavaScriptAndGetStringResult("typeof testObject"));
    168         synchronousPageReload();
    169         assertEquals("object", executeJavaScriptAndGetStringResult("typeof testObject"));
    170     }
    171 
    172     @SmallTest
    173     @Feature({"AndroidWebView", "Android-JavaBridge"})
    174     public void testRemovalNotReflectedUntilReload() throws Throwable {
    175         injectObjectAndReload(new Object(), "testObject");
    176         assertEquals("object", executeJavaScriptAndGetStringResult("typeof testObject"));
    177         runTestOnUiThread(new Runnable() {
    178             @Override
    179             public void run() {
    180                 getContentViewCore().removeJavascriptInterface("testObject");
    181             }
    182         });
    183         assertEquals("object", executeJavaScriptAndGetStringResult("typeof testObject"));
    184         synchronousPageReload();
    185         assertEquals("undefined", executeJavaScriptAndGetStringResult("typeof testObject"));
    186     }
    187 
    188     @SmallTest
    189     @Feature({"AndroidWebView", "Android-JavaBridge"})
    190     public void testRemoveObjectNotAdded() throws Throwable {
    191         TestCallbackHelperContainer.OnPageFinishedHelper onPageFinishedHelper =
    192                 mTestCallbackHelperContainer.getOnPageFinishedHelper();
    193         int currentCallCount = onPageFinishedHelper.getCallCount();
    194         runTestOnUiThread(new Runnable() {
    195             @Override
    196             public void run() {
    197                 getContentViewCore().removeJavascriptInterface("foo");
    198                 getContentViewCore().reload(true);
    199             }
    200         });
    201         onPageFinishedHelper.waitForCallback(currentCallCount);
    202         assertEquals("undefined", executeJavaScriptAndGetStringResult("typeof foo"));
    203     }
    204 
    205     @SmallTest
    206     @Feature({"AndroidWebView", "Android-JavaBridge"})
    207     public void testTypeOfMethod() throws Throwable {
    208         assertEquals("function",
    209                 executeJavaScriptAndGetStringResult("typeof testController.setStringValue"));
    210     }
    211 
    212     @SmallTest
    213     @Feature({"AndroidWebView", "Android-JavaBridge"})
    214     public void testTypeOfInvalidMethod() throws Throwable {
    215         assertEquals("undefined", executeJavaScriptAndGetStringResult("typeof testController.foo"));
    216     }
    217 
    218     @SmallTest
    219     @Feature({"AndroidWebView", "Android-JavaBridge"})
    220     public void testCallingInvalidMethodRaisesException() throws Throwable {
    221         assertRaisesException("testController.foo()");
    222     }
    223 
    224     @SmallTest
    225     @Feature({"AndroidWebView", "Android-JavaBridge"})
    226     public void testUncaughtJavaExceptionRaisesJavaScriptException() throws Throwable {
    227         injectObjectAndReload(new Object() {
    228             public void method() { throw new RuntimeException("foo"); }
    229         }, "testObject");
    230         assertRaisesException("testObject.method()");
    231     }
    232 
    233     // Note that this requires that we can pass a JavaScript string to Java.
    234     @SmallTest
    235     @Feature({"AndroidWebView", "Android-JavaBridge"})
    236     public void testTypeOfStaticMethod() throws Throwable {
    237         injectObjectAndReload(new ObjectWithStaticMethod(), "testObject");
    238         executeJavaScript("testController.setStringValue(typeof testObject.staticMethod)");
    239         assertEquals("function", mTestController.waitForStringValue());
    240     }
    241 
    242     // Note that this requires that we can pass a JavaScript string to Java.
    243     @SmallTest
    244     @Feature({"AndroidWebView", "Android-JavaBridge"})
    245     public void testCallStaticMethod() throws Throwable {
    246         injectObjectAndReload(new ObjectWithStaticMethod(), "testObject");
    247         executeJavaScript("testController.setStringValue(testObject.staticMethod())");
    248         assertEquals("foo", mTestController.waitForStringValue());
    249     }
    250 
    251     @SmallTest
    252     @Feature({"AndroidWebView", "Android-JavaBridge"})
    253     public void testPrivateMethodNotExposed() throws Throwable {
    254         injectObjectAndReload(new Object() {
    255             private void method() {}
    256             protected void method2() {}
    257         }, "testObject");
    258         assertEquals("undefined",
    259                 executeJavaScriptAndGetStringResult("typeof testObject.method"));
    260         assertEquals("undefined",
    261                 executeJavaScriptAndGetStringResult("typeof testObject.method2"));
    262     }
    263 
    264     @SmallTest
    265     @Feature({"AndroidWebView", "Android-JavaBridge"})
    266     public void testReplaceInjectedObject() throws Throwable {
    267         injectObjectAndReload(new Object() {
    268             public void method() { mTestController.setStringValue("object 1"); }
    269         }, "testObject");
    270         executeJavaScript("testObject.method()");
    271         assertEquals("object 1", mTestController.waitForStringValue());
    272 
    273         injectObjectAndReload(new Object() {
    274             public void method() { mTestController.setStringValue("object 2"); }
    275         }, "testObject");
    276         executeJavaScript("testObject.method()");
    277         assertEquals("object 2", mTestController.waitForStringValue());
    278     }
    279 
    280     @SmallTest
    281     @Feature({"AndroidWebView", "Android-JavaBridge"})
    282     public void testInjectNullObjectIsIgnored() throws Throwable {
    283         injectObjectAndReload(null, "testObject");
    284         assertEquals("undefined", executeJavaScriptAndGetStringResult("typeof testObject"));
    285     }
    286 
    287     @SmallTest
    288     @Feature({"AndroidWebView", "Android-JavaBridge"})
    289     public void testReplaceInjectedObjectWithNullObjectIsIgnored() throws Throwable {
    290         injectObjectAndReload(new Object(), "testObject");
    291         assertEquals("object", executeJavaScriptAndGetStringResult("typeof testObject"));
    292         injectObjectAndReload(null, "testObject");
    293         assertEquals("object", executeJavaScriptAndGetStringResult("typeof testObject"));
    294     }
    295 
    296     @SmallTest
    297     @Feature({"AndroidWebView", "Android-JavaBridge"})
    298     public void testCallOverloadedMethodWithDifferentNumberOfArguments() throws Throwable {
    299         injectObjectAndReload(new Object() {
    300             public void method() { mTestController.setStringValue("0 args"); }
    301             public void method(int x) { mTestController.setStringValue("1 arg"); }
    302             public void method(int x, int y) { mTestController.setStringValue("2 args"); }
    303         }, "testObject");
    304         executeJavaScript("testObject.method()");
    305         assertEquals("0 args", mTestController.waitForStringValue());
    306         executeJavaScript("testObject.method(42)");
    307         assertEquals("1 arg", mTestController.waitForStringValue());
    308         executeJavaScript("testObject.method(null)");
    309         assertEquals("1 arg", mTestController.waitForStringValue());
    310         executeJavaScript("testObject.method(undefined)");
    311         assertEquals("1 arg", mTestController.waitForStringValue());
    312         executeJavaScript("testObject.method(42, 42)");
    313         assertEquals("2 args", mTestController.waitForStringValue());
    314     }
    315 
    316     @SmallTest
    317     @Feature({"AndroidWebView", "Android-JavaBridge"})
    318     public void testCallMethodWithWrongNumberOfArgumentsRaisesException() throws Throwable {
    319         assertRaisesException("testController.setIntValue()");
    320         assertRaisesException("testController.setIntValue(42, 42)");
    321     }
    322 
    323     @SmallTest
    324     @Feature({"AndroidWebView", "Android-JavaBridge"})
    325     public void testObjectPersistsAcrossPageLoads() throws Throwable {
    326         assertEquals("object", executeJavaScriptAndGetStringResult("typeof testController"));
    327         synchronousPageReload();
    328         assertEquals("object", executeJavaScriptAndGetStringResult("typeof testController"));
    329     }
    330 
    331     @SmallTest
    332     @Feature({"AndroidWebView", "Android-JavaBridge"})
    333     public void testCustomPropertiesCleanedUpOnPageReloads() throws Throwable {
    334         assertEquals("object", executeJavaScriptAndGetStringResult("typeof testController"));
    335         executeJavaScript("testController.myProperty = 42;");
    336         assertEquals("42", executeJavaScriptAndGetStringResult("testController.myProperty"));
    337         synchronousPageReload();
    338         assertEquals("object", executeJavaScriptAndGetStringResult("typeof testController"));
    339         assertEquals("undefined", executeJavaScriptAndGetStringResult("testController.myProperty"));
    340     }
    341 
    342     @SmallTest
    343     @Feature({"AndroidWebView", "Android-JavaBridge"})
    344     public void testSameObjectInjectedMultipleTimes() throws Throwable {
    345         class TestObject {
    346             private int mNumMethodInvocations;
    347             public void method() { mTestController.setIntValue(++mNumMethodInvocations); }
    348         }
    349         final TestObject testObject = new TestObject();
    350         TestCallbackHelperContainer.OnPageFinishedHelper onPageFinishedHelper =
    351                 mTestCallbackHelperContainer.getOnPageFinishedHelper();
    352         int currentCallCount = onPageFinishedHelper.getCallCount();
    353         runTestOnUiThread(new Runnable() {
    354             @Override
    355             public void run() {
    356                 getContentViewCore().addPossiblyUnsafeJavascriptInterface(
    357                         testObject, "testObject1", null);
    358                 getContentViewCore().addPossiblyUnsafeJavascriptInterface(
    359                         testObject, "testObject2", null);
    360                 getContentViewCore().reload(true);
    361             }
    362         });
    363         onPageFinishedHelper.waitForCallback(currentCallCount);
    364         executeJavaScript("testObject1.method()");
    365         assertEquals(1, mTestController.waitForIntValue());
    366         executeJavaScript("testObject2.method()");
    367         assertEquals(2, mTestController.waitForIntValue());
    368     }
    369 
    370     @SmallTest
    371     @Feature({"AndroidWebView", "Android-JavaBridge"})
    372     public void testCallMethodOnReturnedObject() throws Throwable {
    373         injectObjectAndReload(new Object() {
    374             public Object getInnerObject() {
    375                 return new Object() {
    376                     public void method(int x) { mTestController.setIntValue(x); }
    377                 };
    378             }
    379         }, "testObject");
    380         executeJavaScript("testObject.getInnerObject().method(42)");
    381         assertEquals(42, mTestController.waitForIntValue());
    382     }
    383 
    384     @SmallTest
    385     @Feature({"AndroidWebView", "Android-JavaBridge"})
    386     public void testReturnedObjectInjectedElsewhere() throws Throwable {
    387         class InnerObject {
    388             private int mNumMethodInvocations;
    389             public void method() { mTestController.setIntValue(++mNumMethodInvocations); }
    390         }
    391         final InnerObject innerObject = new InnerObject();
    392         final Object object = new Object() {
    393             public InnerObject getInnerObject() {
    394                 return innerObject;
    395             }
    396         };
    397         TestCallbackHelperContainer.OnPageFinishedHelper onPageFinishedHelper =
    398                 mTestCallbackHelperContainer.getOnPageFinishedHelper();
    399         int currentCallCount = onPageFinishedHelper.getCallCount();
    400         runTestOnUiThread(new Runnable() {
    401             @Override
    402             public void run() {
    403                 getContentViewCore().addPossiblyUnsafeJavascriptInterface(
    404                         object, "testObject", null);
    405                 getContentViewCore().addPossiblyUnsafeJavascriptInterface(
    406                         innerObject, "innerObject", null);
    407                 getContentViewCore().reload(true);
    408             }
    409         });
    410         onPageFinishedHelper.waitForCallback(currentCallCount);
    411         executeJavaScript("testObject.getInnerObject().method()");
    412         assertEquals(1, mTestController.waitForIntValue());
    413         executeJavaScript("innerObject.method()");
    414         assertEquals(2, mTestController.waitForIntValue());
    415     }
    416 
    417     // Verify that Java objects returned from bridge object methods are dereferenced
    418     // on the Java side once they have been fully dereferenced on the JS side.
    419     // Failing this test would mean that methods returning objects effectively create a memory
    420     // leak.
    421     @SmallTest
    422     @Feature({"AndroidWebView", "Android-JavaBridge"})
    423     public void testReturnedObjectIsGarbageCollected() throws Throwable {
    424         // Make sure V8 exposes "gc" property on the global object (enabled with --expose-gc flag)
    425         assertEquals("function", executeJavaScriptAndGetStringResult("typeof gc"));
    426         class InnerObject {
    427         }
    428         class TestObject {
    429             public InnerObject getInnerObject() {
    430                 InnerObject inner = new InnerObject();
    431                 weakRefForInner = new WeakReference<InnerObject>(inner);
    432                 return inner;
    433             }
    434             // A weak reference is used to check InnerObject instance reachability.
    435             WeakReference<InnerObject> weakRefForInner;
    436         }
    437         TestObject object = new TestObject();
    438         injectObjectAndReload(object, "testObject");
    439         // Initially, store a reference to the inner object in JS to make sure it's not
    440         // garbage-collected prematurely.
    441         assertEquals("object", executeJavaScriptAndGetStringResult(
    442                         "(function() { " +
    443                         "globalInner = testObject.getInnerObject(); return typeof globalInner; " +
    444                         "})()"));
    445         assertTrue(object.weakRefForInner.get() != null);
    446         // Check that returned Java object is being held by the Java bridge, thus it's not
    447         // collected.  Note that despite that what JavaDoc says about invoking "gc()", both Dalvik
    448         // and ART actually run the collector.
    449         Runtime.getRuntime().gc();
    450         assertTrue(object.weakRefForInner.get() != null);
    451         // Now dereference the inner object in JS and run GC to collect the interface object.
    452         assertEquals("true", executeJavaScriptAndGetStringResult(
    453                         "(function() { " +
    454                         "delete globalInner; gc(); return (typeof globalInner == 'undefined'); " +
    455                         "})()"));
    456         // Force GC on the Java side again. The bridge had to release the inner object, so it must
    457         // be collected this time.
    458         Runtime.getRuntime().gc();
    459         assertEquals(null, object.weakRefForInner.get());
    460     }
    461 
    462     @SmallTest
    463     @Feature({"AndroidWebView", "Android-JavaBridge"})
    464     public void testSameReturnedObjectUsesSameWrapper() throws Throwable {
    465         class InnerObject {
    466         }
    467         final InnerObject innerObject = new InnerObject();
    468         final Object injectedTestObject = new Object() {
    469             public InnerObject getInnerObject() {
    470                 return innerObject;
    471             }
    472         };
    473         injectObjectAndReload(injectedTestObject, "injectedTestObject");
    474         executeJavaScript("inner1 = injectedTestObject.getInnerObject()");
    475         executeJavaScript("inner2 = injectedTestObject.getInnerObject()");
    476         assertEquals("object", executeJavaScriptAndGetStringResult("typeof inner1"));
    477         assertEquals("object", executeJavaScriptAndGetStringResult("typeof inner2"));
    478         assertEquals("true", executeJavaScriptAndGetStringResult("inner1 === inner2"));
    479     }
    480 
    481     @SmallTest
    482     @Feature({"AndroidWebView", "Android-JavaBridge"})
    483     public void testMethodInvokedOnBackgroundThread() throws Throwable {
    484         injectObjectAndReload(new Object() {
    485             public void captureThreadId() {
    486                 mTestController.setLongValue(Thread.currentThread().getId());
    487             }
    488         }, "testObject");
    489         executeJavaScript("testObject.captureThreadId()");
    490         final long threadId = mTestController.waitForLongValue();
    491         assertFalse(threadId == Thread.currentThread().getId());
    492         runTestOnUiThread(new Runnable() {
    493             @Override
    494             public void run() {
    495                 assertFalse(threadId == Thread.currentThread().getId());
    496             }
    497         });
    498     }
    499 
    500     @SmallTest
    501     @Feature({"AndroidWebView", "Android-JavaBridge"})
    502     public void testPublicInheritedMethod() throws Throwable {
    503         class Base {
    504             public void method(int x) { mTestController.setIntValue(x); }
    505         }
    506         class Derived extends Base {
    507         }
    508         injectObjectAndReload(new Derived(), "testObject");
    509         assertEquals("function", executeJavaScriptAndGetStringResult("typeof testObject.method"));
    510         executeJavaScript("testObject.method(42)");
    511         assertEquals(42, mTestController.waitForIntValue());
    512     }
    513 
    514     @SmallTest
    515     @Feature({"AndroidWebView", "Android-JavaBridge"})
    516     public void testPrivateInheritedMethod() throws Throwable {
    517         class Base {
    518             private void method() {}
    519         }
    520         class Derived extends Base {
    521         }
    522         injectObjectAndReload(new Derived(), "testObject");
    523         assertEquals("undefined", executeJavaScriptAndGetStringResult("typeof testObject.method"));
    524     }
    525 
    526     @SmallTest
    527     @Feature({"AndroidWebView", "Android-JavaBridge"})
    528     public void testOverriddenMethod() throws Throwable {
    529         class Base {
    530             public void method() { mTestController.setStringValue("base"); }
    531         }
    532         class Derived extends Base {
    533             @Override
    534             public void method() { mTestController.setStringValue("derived"); }
    535         }
    536         injectObjectAndReload(new Derived(), "testObject");
    537         executeJavaScript("testObject.method()");
    538         assertEquals("derived", mTestController.waitForStringValue());
    539     }
    540 
    541     @SmallTest
    542     @Feature({"AndroidWebView", "Android-JavaBridge"})
    543     public void testEnumerateMembers() throws Throwable {
    544         injectObjectAndReload(new Object() {
    545             public void method() {}
    546             private void privateMethod() {}
    547             public int field;
    548             private int privateField;
    549         }, "testObject");
    550         executeJavaScript(
    551                 "var result = \"\"; " +
    552                 "for (x in testObject) { result += \" \" + x } " +
    553                 "testController.setStringValue(result);");
    554         assertEquals(" equals getClass hashCode method notify notifyAll toString wait",
    555                 mTestController.waitForStringValue());
    556     }
    557 
    558     @SmallTest
    559     @Feature({"AndroidWebView", "Android-JavaBridge"})
    560     public void testReflectPublicMethod() throws Throwable {
    561         injectObjectAndReload(new Object() {
    562             public Class<?> myGetClass() { return getClass(); }
    563             public String method() { return "foo"; }
    564         }, "testObject");
    565         assertEquals("foo", executeJavaScriptAndGetStringResult(
    566                 "testObject.myGetClass().getMethod('method', null).invoke(testObject, null)" +
    567                 ".toString()"));
    568     }
    569 
    570     @SmallTest
    571     @Feature({"AndroidWebView", "Android-JavaBridge"})
    572     public void testReflectPublicField() throws Throwable {
    573         injectObjectAndReload(new Object() {
    574             public Class<?> myGetClass() { return getClass(); }
    575             public String field = "foo";
    576         }, "testObject");
    577         assertEquals("foo", executeJavaScriptAndGetStringResult(
    578                 "testObject.myGetClass().getField('field').get(testObject).toString()"));
    579     }
    580 
    581     @SmallTest
    582     @Feature({"AndroidWebView", "Android-JavaBridge"})
    583     public void testReflectPrivateMethodRaisesException() throws Throwable {
    584         injectObjectAndReload(new Object() {
    585             public Class<?> myGetClass() { return getClass(); }
    586             private void method() {};
    587         }, "testObject");
    588         assertRaisesException("testObject.myGetClass().getMethod('method', null)");
    589         // getDeclaredMethod() is able to access a private method, but invoke()
    590         // throws a Java exception.
    591         assertRaisesException(
    592                 "testObject.myGetClass().getDeclaredMethod('method', null)." +
    593                 "invoke(testObject, null)");
    594     }
    595 
    596     @SmallTest
    597     @Feature({"AndroidWebView", "Android-JavaBridge"})
    598     public void testReflectPrivateFieldRaisesException() throws Throwable {
    599         injectObjectAndReload(new Object() {
    600             public Class<?> myGetClass() { return getClass(); }
    601             private int field;
    602         }, "testObject");
    603         assertRaisesException("testObject.myGetClass().getField('field')");
    604         // getDeclaredField() is able to access a private field, but getInt()
    605         // throws a Java exception.
    606         assertRaisesException(
    607                 "testObject.myGetClass().getDeclaredField('field').getInt(testObject)");
    608     }
    609 
    610     @SmallTest
    611     @Feature({"AndroidWebView", "Android-JavaBridge"})
    612     public void testAllowNonAnnotatedMethods() throws Throwable {
    613         injectObjectAndReload(new Object() {
    614             public String allowed() { return "foo"; }
    615         }, "testObject", null);
    616 
    617         // Test calling a method of an explicitly inherited class (Base#allowed()).
    618         assertEquals("foo", executeJavaScriptAndGetStringResult("testObject.allowed()"));
    619 
    620         // Test calling a method of an implicitly inherited class (Object#toString()).
    621         assertEquals("string", executeJavaScriptAndGetStringResult("typeof testObject.toString()"));
    622     }
    623 
    624     @SmallTest
    625     @Feature({"AndroidWebView", "Android-JavaBridge"})
    626     public void testAllowOnlyAnnotatedMethods() throws Throwable {
    627         injectObjectAndReload(new Object() {
    628             @JavascriptInterface
    629             public String allowed() { return "foo"; }
    630 
    631             public String disallowed() { return "bar"; }
    632         }, "testObject", JavascriptInterface.class);
    633 
    634         // getClass() is an Object method and does not have the @JavascriptInterface annotation and
    635         // should not be able to be called.
    636         assertRaisesException("testObject.getClass()");
    637         assertEquals("undefined", executeJavaScriptAndGetStringResult(
    638                 "typeof testObject.getClass"));
    639 
    640         // allowed() is marked with the @JavascriptInterface annotation and should be allowed to be
    641         // called.
    642         assertEquals("foo", executeJavaScriptAndGetStringResult("testObject.allowed()"));
    643 
    644         // disallowed() is not marked with the @JavascriptInterface annotation and should not be
    645         // able to be called.
    646         assertRaisesException("testObject.disallowed()");
    647         assertEquals("undefined", executeJavaScriptAndGetStringResult(
    648                 "typeof testObject.disallowed"));
    649     }
    650 
    651     @SmallTest
    652     @Feature({"AndroidWebView", "Android-JavaBridge"})
    653     public void testAnnotationRequirementRetainsPropertyAcrossObjects() throws Throwable {
    654         class Test {
    655             @JavascriptInterface
    656             public String safe() { return "foo"; }
    657 
    658             public String unsafe() { return "bar"; }
    659         }
    660 
    661         class TestReturner {
    662             @JavascriptInterface
    663             public Test getTest() { return new Test(); }
    664         }
    665 
    666         // First test with safe mode off.
    667         injectObjectAndReload(new TestReturner(), "unsafeTestObject", null);
    668 
    669         // safe() should be able to be called regardless of whether or not we are in safe mode.
    670         assertEquals("foo", executeJavaScriptAndGetStringResult(
    671                 "unsafeTestObject.getTest().safe()"));
    672         // unsafe() should be able to be called because we are not in safe mode.
    673         assertEquals("bar", executeJavaScriptAndGetStringResult(
    674                 "unsafeTestObject.getTest().unsafe()"));
    675 
    676         // Now test with safe mode on.
    677         injectObjectAndReload(new TestReturner(), "safeTestObject", JavascriptInterface.class);
    678 
    679         // safe() should be able to be called regardless of whether or not we are in safe mode.
    680         assertEquals("foo", executeJavaScriptAndGetStringResult(
    681                 "safeTestObject.getTest().safe()"));
    682         // unsafe() should not be able to be called because we are in safe mode.
    683         assertRaisesException("safeTestObject.getTest().unsafe()");
    684         assertEquals("undefined", executeJavaScriptAndGetStringResult(
    685                 "typeof safeTestObject.getTest().unsafe"));
    686         // getClass() is an Object method and does not have the @JavascriptInterface annotation and
    687         // should not be able to be called.
    688         assertRaisesException("safeTestObject.getTest().getClass()");
    689         assertEquals("undefined", executeJavaScriptAndGetStringResult(
    690                 "typeof safeTestObject.getTest().getClass"));
    691     }
    692 
    693     @SmallTest
    694     @Feature({"AndroidWebView", "Android-JavaBridge"})
    695     public void testAnnotationDoesNotGetInherited() throws Throwable {
    696         class Base {
    697             @JavascriptInterface
    698             public void base() { }
    699         }
    700 
    701         class Child extends Base {
    702             @Override
    703             public void base() { }
    704         }
    705 
    706         injectObjectAndReload(new Child(), "testObject", JavascriptInterface.class);
    707 
    708         // base() is inherited.  The inherited method does not have the @JavascriptInterface
    709         // annotation and should not be able to be called.
    710         assertRaisesException("testObject.base()");
    711         assertEquals("undefined", executeJavaScriptAndGetStringResult(
    712                 "typeof testObject.base"));
    713     }
    714 
    715     @SuppressWarnings("javadoc")
    716     @Retention(RetentionPolicy.RUNTIME)
    717     @Target({ElementType.METHOD})
    718     @interface TestAnnotation {
    719     }
    720 
    721     @SmallTest
    722     @Feature({"AndroidWebView", "Android-JavaBridge"})
    723     public void testCustomAnnotationRestriction() throws Throwable {
    724         class Test {
    725             @TestAnnotation
    726             public String checkTestAnnotationFoo() { return "bar"; }
    727 
    728             @JavascriptInterface
    729             public String checkJavascriptInterfaceFoo() { return "bar"; }
    730         }
    731 
    732         // Inject javascriptInterfaceObj and require the JavascriptInterface annotation.
    733         injectObjectAndReload(new Test(), "javascriptInterfaceObj", JavascriptInterface.class);
    734 
    735         // Test#testAnnotationFoo() should fail, as it isn't annotated with JavascriptInterface.
    736         assertRaisesException("javascriptInterfaceObj.checkTestAnnotationFoo()");
    737         assertEquals("undefined", executeJavaScriptAndGetStringResult(
    738                 "typeof javascriptInterfaceObj.checkTestAnnotationFoo"));
    739 
    740         // Test#javascriptInterfaceFoo() should pass, as it is annotated with JavascriptInterface.
    741         assertEquals("bar", executeJavaScriptAndGetStringResult(
    742                 "javascriptInterfaceObj.checkJavascriptInterfaceFoo()"));
    743 
    744         // Inject testAnnotationObj and require the TestAnnotation annotation.
    745         injectObjectAndReload(new Test(), "testAnnotationObj", TestAnnotation.class);
    746 
    747         // Test#testAnnotationFoo() should pass, as it is annotated with TestAnnotation.
    748         assertEquals("bar", executeJavaScriptAndGetStringResult(
    749                 "testAnnotationObj.checkTestAnnotationFoo()"));
    750 
    751         // Test#javascriptInterfaceFoo() should fail, as it isn't annotated with TestAnnotation.
    752         assertRaisesException("testAnnotationObj.checkJavascriptInterfaceFoo()");
    753         assertEquals("undefined", executeJavaScriptAndGetStringResult(
    754                 "typeof testAnnotationObj.checkJavascriptInterfaceFoo"));
    755     }
    756 
    757     @SmallTest
    758     @Feature({"AndroidWebView", "Android-JavaBridge"})
    759     public void testAddJavascriptInterfaceIsSafeByDefault() throws Throwable {
    760         class Test {
    761             public String blocked() { return "bar"; }
    762 
    763             @JavascriptInterface
    764             public String allowed() { return "bar"; }
    765         }
    766 
    767         // Manually inject the Test object, making sure to use the
    768         // ContentViewCore#addJavascriptInterface, not the possibly unsafe version.
    769         TestCallbackHelperContainer.OnPageFinishedHelper onPageFinishedHelper =
    770                 mTestCallbackHelperContainer.getOnPageFinishedHelper();
    771         int currentCallCount = onPageFinishedHelper.getCallCount();
    772         runTestOnUiThread(new Runnable() {
    773             @Override
    774             public void run() {
    775                 getContentViewCore().addJavascriptInterface(new Test(),
    776                         "testObject");
    777                 getContentViewCore().reload(true);
    778             }
    779         });
    780         onPageFinishedHelper.waitForCallback(currentCallCount);
    781 
    782         // Test#allowed() should pass, as it is annotated with JavascriptInterface.
    783         assertEquals("bar", executeJavaScriptAndGetStringResult(
    784                 "testObject.allowed()"));
    785 
    786         // Test#blocked() should fail, as it isn't annotated with JavascriptInterface.
    787         assertRaisesException("testObject.blocked()");
    788         assertEquals("undefined", executeJavaScriptAndGetStringResult(
    789                 "typeof testObject.blocked"));
    790     }
    791 
    792     @SmallTest
    793     @Feature({"AndroidWebView", "Android-JavaBridge"})
    794     public void testObjectsInspection() throws Throwable {
    795         class Test {
    796             @JavascriptInterface
    797             public String m1() { return "foo"; }
    798 
    799             @JavascriptInterface
    800             public String m2() { return "bar"; }
    801 
    802             @JavascriptInterface
    803             public String m2(int x) { return "bar " + x; }
    804         }
    805 
    806         final String jsObjectKeysTestTemplate = "Object.keys(%s).toString()";
    807         final String jsForInTestTemplate =
    808                 "(function(){" +
    809                 "  var s=[]; for(var m in %s) s.push(m); return s.join(\",\")" +
    810                 "})()";
    811         final String inspectableObjectName = "testObj1";
    812         final String nonInspectableObjectName = "testObj2";
    813 
    814         // Inspection is enabled by default.
    815         injectObjectAndReload(new Test(), inspectableObjectName, JavascriptInterface.class);
    816 
    817         assertEquals("m1,m2", executeJavaScriptAndGetStringResult(
    818                         String.format(jsObjectKeysTestTemplate, inspectableObjectName)));
    819         assertEquals("m1,m2", executeJavaScriptAndGetStringResult(
    820                         String.format(jsForInTestTemplate, inspectableObjectName)));
    821 
    822         runTestOnUiThread(new Runnable() {
    823             @Override
    824             public void run() {
    825                 getContentViewCore().setAllowJavascriptInterfacesInspection(false);
    826             }
    827         });
    828 
    829         injectObjectAndReload(new Test(), nonInspectableObjectName, JavascriptInterface.class);
    830 
    831         assertEquals("", executeJavaScriptAndGetStringResult(
    832                         String.format(jsObjectKeysTestTemplate, nonInspectableObjectName)));
    833         assertEquals("", executeJavaScriptAndGetStringResult(
    834                         String.format(jsForInTestTemplate, nonInspectableObjectName)));
    835     }
    836 
    837     @SmallTest
    838     @Feature({"AndroidWebView", "Android-JavaBridge"})
    839     public void testAccessToObjectGetClassIsBlocked() throws Throwable {
    840         injectObjectAndReload(new Object(), "testObject");
    841         assertEquals("function", executeJavaScriptAndGetStringResult("typeof testObject.getClass"));
    842         assertRaisesException("testObject.getClass()");
    843     }
    844 }
    845