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