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