JavaBridgeBasicsTest.java revision 0529e5d033099cbfc42635f6f6183833b09dff6e
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 getContentViewCore().addPossiblyUnsafeJavascriptInterface(object, 122 name, requiredAnnotation); 123 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 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 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 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 getContentViewCore().removeJavascriptInterface("foo"); 201 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 getContentViewCore().addPossiblyUnsafeJavascriptInterface( 359 testObject, "testObject1", null); 360 getContentViewCore().addPossiblyUnsafeJavascriptInterface( 361 testObject, "testObject2", null); 362 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 getContentViewCore().addPossiblyUnsafeJavascriptInterface( 406 object, "testObject", null); 407 getContentViewCore().addPossiblyUnsafeJavascriptInterface( 408 innerObject, "innerObject", null); 409 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 getContentViewCore().addJavascriptInterface(new Test(), 789 "testObject"); 790 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 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 getContentViewCore().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