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