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