1/*
2 * Copyright (C) 2011 The Android Open Source Project
3 *
4 * Licensed under the Apache License, Version 2.0 (the "License");
5 * you may not use this file except in compliance with the License.
6 * You may obtain a copy of the License at
7 *
8 *      http://www.apache.org/licenses/LICENSE-2.0
9 *
10 * Unless required by applicable law or agreed to in writing, software
11 * distributed under the License is distributed on an "AS IS" BASIS,
12 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13 * See the License for the specific language governing permissions and
14 * limitations under the License.
15 */
16
17/**
18 * Part of the test suite for the WebView's Java Bridge. Tests a number of features including ...
19 * - The type of injected objects
20 * - The type of their methods
21 * - Replacing objects
22 * - Removing objects
23 * - Access control
24 * - Calling methods on returned objects
25 * - Multiply injected objects
26 * - Threading
27 * - Inheritance
28 *
29 * To run this test ...
30 *  adb shell am instrument -w -e class com.android.webviewtests.JavaBridgeBasicsTest \
31 *     com.android.webviewtests/android.test.InstrumentationTestRunner
32 */
33
34package com.android.webviewtests;
35
36public class JavaBridgeBasicsTest extends JavaBridgeTestBase {
37    private class TestController extends Controller {
38        private int mIntValue;
39        private long mLongValue;
40        private String mStringValue;
41        private boolean mBooleanValue;
42
43        public synchronized void setIntValue(int x) {
44            mIntValue = x;
45            notifyResultIsReady();
46        }
47        public synchronized void setLongValue(long x) {
48            mLongValue = x;
49            notifyResultIsReady();
50        }
51        public synchronized void setStringValue(String x) {
52            mStringValue = x;
53            notifyResultIsReady();
54        }
55        public synchronized void setBooleanValue(boolean x) {
56            mBooleanValue = x;
57            notifyResultIsReady();
58        }
59
60        public synchronized int waitForIntValue() {
61            waitForResult();
62            return mIntValue;
63        }
64        public synchronized long waitForLongValue() {
65            waitForResult();
66            return mLongValue;
67        }
68        public synchronized String waitForStringValue() {
69            waitForResult();
70            return mStringValue;
71        }
72        public synchronized boolean waitForBooleanValue() {
73            waitForResult();
74            return mBooleanValue;
75        }
76    }
77
78    private static class ObjectWithStaticMethod {
79        public static String staticMethod() {
80            return "foo";
81        }
82    }
83
84    TestController mTestController;
85
86    @Override
87    protected void setUp() throws Exception {
88        super.setUp();
89        mTestController = new TestController();
90        setUpWebView(mTestController, "testController");
91    }
92
93    // Note that this requires that we can pass a JavaScript string to Java.
94    protected String executeJavaScriptAndGetStringResult(String script) throws Throwable {
95        executeJavaScript("testController.setStringValue(" + script + ");");
96        return mTestController.waitForStringValue();
97    }
98
99    protected void injectObjectAndReload(final Object object, final String name) throws Throwable {
100        runTestOnUiThread(new Runnable() {
101            @Override
102            public void run() {
103                getWebView().addJavascriptInterface(object, name);
104                getWebView().reload();
105            }
106        });
107        mWebViewClient.waitForOnPageFinished();
108    }
109
110    // Note that this requires that we can pass a JavaScript boolean to Java.
111    private void assertRaisesException(String script) throws Throwable {
112        executeJavaScript("try {" +
113                          script + ";" +
114                          "  testController.setBooleanValue(false);" +
115                          "} catch (exception) {" +
116                          "  testController.setBooleanValue(true);" +
117                          "}");
118        assertTrue(mTestController.waitForBooleanValue());
119    }
120
121    public void testTypeOfInjectedObject() throws Throwable {
122        assertEquals("object", executeJavaScriptAndGetStringResult("typeof testController"));
123    }
124
125    public void testAdditionNotReflectedUntilReload() throws Throwable {
126        assertEquals("undefined", executeJavaScriptAndGetStringResult("typeof testObject"));
127        runTestOnUiThread(new Runnable() {
128            @Override
129            public void run() {
130                getWebView().addJavascriptInterface(new Object(), "testObject");
131            }
132        });
133        assertEquals("undefined", executeJavaScriptAndGetStringResult("typeof testObject"));
134        runTestOnUiThread(new Runnable() {
135            @Override
136            public void run() {
137                getWebView().reload();
138            }
139        });
140        mWebViewClient.waitForOnPageFinished();
141        assertEquals("object", executeJavaScriptAndGetStringResult("typeof testObject"));
142    }
143
144    public void testRemovalNotReflectedUntilReload() throws Throwable {
145        injectObjectAndReload(new Object(), "testObject");
146        assertEquals("object", executeJavaScriptAndGetStringResult("typeof testObject"));
147        runTestOnUiThread(new Runnable() {
148            @Override
149            public void run() {
150                getWebView().removeJavascriptInterface("testObject");
151            }
152        });
153        assertEquals("object", executeJavaScriptAndGetStringResult("typeof testObject"));
154        runTestOnUiThread(new Runnable() {
155            @Override
156            public void run() {
157                getWebView().reload();
158            }
159        });
160        mWebViewClient.waitForOnPageFinished();
161        assertEquals("undefined", executeJavaScriptAndGetStringResult("typeof testObject"));
162    }
163
164    public void testRemoveObjectNotAdded() throws Throwable {
165        runTestOnUiThread(new Runnable() {
166            @Override
167            public void run() {
168                getWebView().removeJavascriptInterface("foo");
169                getWebView().reload();
170            }
171        });
172        mWebViewClient.waitForOnPageFinished();
173        assertEquals("undefined", executeJavaScriptAndGetStringResult("typeof foo"));
174    }
175
176    public void testTypeOfMethod() throws Throwable {
177        assertEquals("function",
178                executeJavaScriptAndGetStringResult("typeof testController.setStringValue"));
179    }
180
181    public void testTypeOfInvalidMethod() throws Throwable {
182        assertEquals("undefined", executeJavaScriptAndGetStringResult("typeof testController.foo"));
183    }
184
185    public void testCallingInvalidMethodRaisesException() throws Throwable {
186        assertRaisesException("testController.foo()");
187    }
188
189    public void testUncaughtJavaExceptionRaisesJavaException() throws Throwable {
190        injectObjectAndReload(new Object() {
191            public void method() { throw new RuntimeException("foo"); }
192        }, "testObject");
193        assertRaisesException("testObject.method()");
194    }
195
196    // Note that this requires that we can pass a JavaScript string to Java.
197    public void testTypeOfStaticMethod() throws Throwable {
198        injectObjectAndReload(new ObjectWithStaticMethod(), "testObject");
199        executeJavaScript("testController.setStringValue(typeof testObject.staticMethod)");
200        assertEquals("function", mTestController.waitForStringValue());
201    }
202
203    // Note that this requires that we can pass a JavaScript string to Java.
204    public void testCallStaticMethod() throws Throwable {
205        injectObjectAndReload(new ObjectWithStaticMethod(), "testObject");
206        executeJavaScript("testController.setStringValue(testObject.staticMethod())");
207        assertEquals("foo", mTestController.waitForStringValue());
208    }
209
210    public void testPrivateMethodNotExposed() throws Throwable {
211        injectObjectAndReload(new Object() {
212            private void method() {}
213        }, "testObject");
214        assertEquals("undefined",
215                executeJavaScriptAndGetStringResult("typeof testObject.method"));
216    }
217
218    public void testReplaceInjectedObject() throws Throwable {
219        injectObjectAndReload(new Object() {
220            public void method() { mTestController.setStringValue("object 1"); }
221        }, "testObject");
222        executeJavaScript("testObject.method()");
223        assertEquals("object 1", mTestController.waitForStringValue());
224
225        injectObjectAndReload(new Object() {
226            public void method() { mTestController.setStringValue("object 2"); }
227        }, "testObject");
228        executeJavaScript("testObject.method()");
229        assertEquals("object 2", mTestController.waitForStringValue());
230    }
231
232    public void testInjectNullObjectIsIgnored() throws Throwable {
233        injectObjectAndReload(null, "testObject");
234        assertEquals("undefined", executeJavaScriptAndGetStringResult("typeof testObject"));
235    }
236
237    public void testReplaceInjectedObjectWithNullObjectIsIgnored() throws Throwable {
238        injectObjectAndReload(new Object(), "testObject");
239        assertEquals("object", executeJavaScriptAndGetStringResult("typeof testObject"));
240        injectObjectAndReload(null, "testObject");
241        assertEquals("object", executeJavaScriptAndGetStringResult("typeof testObject"));
242    }
243
244    public void testCallOverloadedMethodWithDifferentNumberOfArguments() throws Throwable {
245        injectObjectAndReload(new Object() {
246            public void method() { mTestController.setStringValue("0 args"); }
247            public void method(int x) { mTestController.setStringValue("1 arg"); }
248            public void method(int x, int y) { mTestController.setStringValue("2 args"); }
249        }, "testObject");
250        executeJavaScript("testObject.method()");
251        assertEquals("0 args", mTestController.waitForStringValue());
252        executeJavaScript("testObject.method(42)");
253        assertEquals("1 arg", mTestController.waitForStringValue());
254        executeJavaScript("testObject.method(null)");
255        assertEquals("1 arg", mTestController.waitForStringValue());
256        executeJavaScript("testObject.method(undefined)");
257        assertEquals("1 arg", mTestController.waitForStringValue());
258        executeJavaScript("testObject.method(42, 42)");
259        assertEquals("2 args", mTestController.waitForStringValue());
260    }
261
262    public void testCallMethodWithWrongNumberOfArgumentsRaisesException() throws Throwable {
263        assertRaisesException("testController.setIntValue()");
264        assertRaisesException("testController.setIntValue(42, 42)");
265    }
266
267    public void testObjectPersistsAcrossPageLoads() throws Throwable {
268        assertEquals("object", executeJavaScriptAndGetStringResult("typeof testController"));
269        runTestOnUiThread(new Runnable() {
270            @Override
271            public void run() {
272                getWebView().reload();
273            }
274        });
275        mWebViewClient.waitForOnPageFinished();
276        assertEquals("object", executeJavaScriptAndGetStringResult("typeof testController"));
277    }
278
279    public void testSameObjectInjectedMultipleTimes() throws Throwable {
280        class TestObject {
281            private int mNumMethodInvocations;
282            public void method() { mTestController.setIntValue(++mNumMethodInvocations); }
283        }
284        final TestObject testObject = new TestObject();
285        runTestOnUiThread(new Runnable() {
286            @Override
287            public void run() {
288                getWebView().addJavascriptInterface(testObject, "testObject1");
289                getWebView().addJavascriptInterface(testObject, "testObject2");
290                getWebView().reload();
291            }
292        });
293        mWebViewClient.waitForOnPageFinished();
294        executeJavaScript("testObject1.method()");
295        assertEquals(1, mTestController.waitForIntValue());
296        executeJavaScript("testObject2.method()");
297        assertEquals(2, mTestController.waitForIntValue());
298    }
299
300    public void testCallMethodOnReturnedObject() throws Throwable {
301        injectObjectAndReload(new Object() {
302            public Object getInnerObject() {
303                return new Object() {
304                    public void method(int x) { mTestController.setIntValue(x); }
305                };
306            }
307        }, "testObject");
308        executeJavaScript("testObject.getInnerObject().method(42)");
309        assertEquals(42, mTestController.waitForIntValue());
310    }
311
312    public void testReturnedObjectInjectedElsewhere() throws Throwable {
313        class InnerObject {
314            private int mNumMethodInvocations;
315            public void method() { mTestController.setIntValue(++mNumMethodInvocations); }
316        }
317        final InnerObject innerObject = new InnerObject();
318        final Object object = new Object() {
319            public InnerObject getInnerObject() {
320                return innerObject;
321            }
322        };
323        runTestOnUiThread(new Runnable() {
324            @Override
325            public void run() {
326                getWebView().addJavascriptInterface(object, "testObject");
327                getWebView().addJavascriptInterface(innerObject, "innerObject");
328                getWebView().reload();
329            }
330        });
331        mWebViewClient.waitForOnPageFinished();
332        executeJavaScript("testObject.getInnerObject().method()");
333        assertEquals(1, mTestController.waitForIntValue());
334        executeJavaScript("innerObject.method()");
335        assertEquals(2, mTestController.waitForIntValue());
336    }
337
338    public void testMethodInvokedOnBackgroundThread() throws Throwable {
339        injectObjectAndReload(new Object() {
340            public void captureThreadId() {
341                mTestController.setLongValue(Thread.currentThread().getId());
342            }
343        }, "testObject");
344        executeJavaScript("testObject.captureThreadId()");
345        final long threadId = mTestController.waitForLongValue();
346        assertFalse(threadId == Thread.currentThread().getId());
347        runTestOnUiThread(new Runnable() {
348            @Override
349            public void run() {
350                assertFalse(threadId == Thread.currentThread().getId());
351            }
352        });
353    }
354
355    public void testPublicInheritedMethod() throws Throwable {
356        class Base {
357            public void method(int x) { mTestController.setIntValue(x); }
358        }
359        class Derived extends Base {
360        }
361        injectObjectAndReload(new Derived(), "testObject");
362        assertEquals("function", executeJavaScriptAndGetStringResult("typeof testObject.method"));
363        executeJavaScript("testObject.method(42)");
364        assertEquals(42, mTestController.waitForIntValue());
365    }
366
367    public void testPrivateInheritedMethod() throws Throwable {
368        class Base {
369            private void method() {}
370        }
371        class Derived extends Base {
372        }
373        injectObjectAndReload(new Derived(), "testObject");
374        assertEquals("undefined", executeJavaScriptAndGetStringResult("typeof testObject.method"));
375    }
376
377    public void testOverriddenMethod() throws Throwable {
378        class Base {
379            public void method() { mTestController.setStringValue("base"); }
380        }
381        class Derived extends Base {
382            public void method() { mTestController.setStringValue("derived"); }
383        }
384        injectObjectAndReload(new Derived(), "testObject");
385        executeJavaScript("testObject.method()");
386        assertEquals("derived", mTestController.waitForStringValue());
387    }
388
389    public void testEnumerateMembers() throws Throwable {
390        injectObjectAndReload(new Object() {
391            public void method() {}
392            private void privateMethod() {}
393            public int field;
394            private int privateField;
395        }, "testObject");
396        executeJavaScript(
397                "var result = \"\"; " +
398                "for (x in testObject) { result += \" \" + x } " +
399                "testController.setStringValue(result);");
400        // LIVECONNECT_COMPLIANCE: Should be able to enumerate members.
401        assertEquals("", mTestController.waitForStringValue());
402    }
403
404    public void testReflectPublicMethod() throws Throwable {
405        injectObjectAndReload(new Object() {
406            public String method() { return "foo"; }
407        }, "testObject");
408        assertEquals("foo", executeJavaScriptAndGetStringResult(
409                "testObject.getClass().getMethod('method', null).invoke(testObject, null)" +
410                ".toString()"));
411    }
412
413    public void testReflectPublicField() throws Throwable {
414        injectObjectAndReload(new Object() {
415            public String field = "foo";
416        }, "testObject");
417        assertEquals("foo", executeJavaScriptAndGetStringResult(
418                "testObject.getClass().getField('field').get(testObject).toString()"));
419    }
420
421    public void testReflectPrivateMethodRaisesException() throws Throwable {
422        injectObjectAndReload(new Object() {
423            private void method() {};
424        }, "testObject");
425        assertRaisesException("testObject.getClass().getMethod('method', null)");
426        // getDeclaredMethod() is able to access a private method, but invoke()
427        // throws a Java exception.
428        assertRaisesException(
429                "testObject.getClass().getDeclaredMethod('method', null).invoke(testObject, null)");
430    }
431
432    public void testReflectPrivateFieldRaisesException() throws Throwable {
433        injectObjectAndReload(new Object() {
434            private int field;
435        }, "testObject");
436        assertRaisesException("testObject.getClass().getField('field')");
437        // getDeclaredField() is able to access a private field, but getInt()
438        // throws a Java exception.
439        assertRaisesException(
440                "testObject.getClass().getDeclaredField('field').getInt(testObject)");
441    }
442}
443