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