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