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