1 /* 2 * Copyright (C) 2012 The Android Open Source Project 3 * 4 * Licensed under the Apache License, Version 2.0 (the "License"); 5 * you may not use this file except in compliance with the License. 6 * You may obtain a copy of the License at 7 * 8 * http://www.apache.org/licenses/LICENSE-2.0 9 * 10 * Unless required by applicable law or agreed to in writing, software 11 * distributed under the License is distributed on an "AS IS" BASIS, 12 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 * See the License for the specific language governing permissions and 14 * limitations under the License. 15 */ 16 17 package android.webkit.cts; 18 19 import android.content.Context; 20 import android.cts.util.LocationUtils; 21 import android.cts.util.NullWebViewUtils; 22 import android.cts.util.PollingCheck; 23 import android.graphics.Bitmap; 24 import android.location.Criteria; 25 import android.location.Location; 26 import android.location.LocationListener; 27 import android.location.LocationManager; 28 import android.location.LocationProvider; 29 import android.os.Bundle; 30 import android.os.Looper; 31 import android.os.SystemClock; 32 import android.test.ActivityInstrumentationTestCase2; 33 import android.webkit.CookieManager; 34 import android.webkit.CookieSyncManager; 35 import android.webkit.GeolocationPermissions; 36 import android.webkit.JavascriptInterface; 37 import android.webkit.WebChromeClient; 38 import android.webkit.WebResourceResponse; 39 import android.webkit.WebView; 40 import android.webkit.WebViewClient; 41 import android.webkit.cts.WebViewOnUiThread.WaitForLoadedClient; 42 import android.webkit.cts.WebViewOnUiThread.WaitForProgressClient; 43 44 import java.io.ByteArrayInputStream; 45 import java.io.UnsupportedEncodingException; 46 import java.util.concurrent.Callable; 47 import java.util.Date; 48 import java.util.List; 49 import java.util.Random; 50 import java.util.regex.Matcher; 51 import java.util.regex.Pattern; 52 import java.util.Set; 53 import java.util.TreeSet; 54 55 import junit.framework.Assert; 56 57 public class GeolocationTest extends ActivityInstrumentationTestCase2<WebViewCtsActivity> { 58 59 // TODO Write additional tests to cover: 60 // - test that the errors are correct 61 // - test that use of gps and network location is correct 62 63 // The URLs does not matter since the tests will intercept the load, but it has to be a real 64 // url, and different domains. 65 private static final String URL_1 = "http://www.example.com"; 66 private static final String URL_2 = "http://www.example.org"; 67 68 private static final String JS_INTERFACE_NAME = "Android"; 69 private static final int POLLING_TIMEOUT = 60 * 1000; 70 private static final int LOCATION_THREAD_UPDATE_WAIT_MS = 250; 71 72 // static HTML page always injected instead of the url loaded 73 private static final String RAW_HTML = 74 "<!DOCTYPE html>\n" + 75 "<html>\n" + 76 " <head>\n" + 77 " <title>Geolocation</title>\n" + 78 " <script>\n" + 79 " function gotPos(position) {\n" + 80 " " + JS_INTERFACE_NAME + ".gotLocation();\n" + 81 " }\n" + 82 " function initiate_getCurrentPosition() {\n" + 83 " navigator.geolocation.getCurrentPosition(\n" + 84 " gotPos,\n" + 85 " handle_errors,\n" + 86 " {maximumAge:1000});\n" + 87 " }\n" + 88 " function handle_errors(error) {\n" + 89 " switch(error.code) {\n" + 90 " case error.PERMISSION_DENIED:\n" + 91 " " + JS_INTERFACE_NAME + ".errorDenied(); break;\n" + 92 " case error.POSITION_UNAVAILABLE:\n" + 93 " " + JS_INTERFACE_NAME + ".errorUnavailable(); break;\n" + 94 " case error.TIMEOUT:\n" + 95 " " + JS_INTERFACE_NAME + ".errorTimeout(); break;\n" + 96 " default: break;\n" + 97 " }\n" + 98 " }\n" + 99 " </script>\n" + 100 " </head>\n" + 101 " <body onload=\"initiate_getCurrentPosition();\">\n" + 102 " </body>\n" + 103 "</html>"; 104 105 private JavascriptStatusReceiver mJavascriptStatusReceiver; 106 private LocationManager mLocationManager; 107 private WebViewOnUiThread mOnUiThread; 108 private Thread mLocationUpdateThread; 109 private volatile boolean mLocationUpdateThreadExitRequested; 110 private List<String> mProviders; 111 112 public GeolocationTest() throws Exception { 113 super("com.android.cts.webkit", WebViewCtsActivity.class); 114 } 115 116 // Both this test and WebViewOnUiThread need to override some of the methods on WebViewClient, 117 // so this test sublclasses the WebViewClient from WebViewOnUiThread 118 private static class InterceptClient extends WaitForLoadedClient { 119 120 public InterceptClient(WebViewOnUiThread webViewOnUiThread) throws Exception { 121 super(webViewOnUiThread); 122 } 123 124 @Override 125 public WebResourceResponse shouldInterceptRequest(WebView view, String url) { 126 // Intercept all page loads with the same geolocation enabled page 127 try { 128 return new WebResourceResponse("text/html", "utf-8", 129 new ByteArrayInputStream(RAW_HTML.getBytes("UTF-8"))); 130 } catch(java.io.UnsupportedEncodingException e) { 131 return null; 132 } 133 } 134 } 135 136 @Override 137 protected void setUp() throws Exception { 138 super.setUp(); 139 140 LocationUtils.registerMockLocationProvider(getInstrumentation(), true); 141 WebView webview = getActivity().getWebView(); 142 143 if (webview != null) { 144 // Set up a WebView with JavaScript and Geolocation enabled 145 final String GEO_DIR = "geo_test"; 146 mOnUiThread = new WebViewOnUiThread(this, webview); 147 mOnUiThread.getSettings().setJavaScriptEnabled(true); 148 mOnUiThread.getSettings().setGeolocationEnabled(true); 149 mOnUiThread.getSettings().setGeolocationDatabasePath( 150 getActivity().getApplicationContext().getDir(GEO_DIR, 0).getPath()); 151 152 // Add a JsInterface to report back to the test when a location is received 153 mJavascriptStatusReceiver = new JavascriptStatusReceiver(); 154 mOnUiThread.addJavascriptInterface(mJavascriptStatusReceiver, JS_INTERFACE_NAME); 155 156 // Always intercept all loads with the same geolocation test page 157 mOnUiThread.setWebViewClient(new InterceptClient(mOnUiThread)); 158 // Clear all permissions before each test 159 GeolocationPermissions.getInstance().clearAll(); 160 // Cache this mostly because the lookup is two lines of code 161 mLocationManager = (LocationManager)getActivity().getApplicationContext() 162 .getSystemService(Context.LOCATION_SERVICE); 163 // Add a test provider before each test to inject a location 164 mProviders = mLocationManager.getAllProviders(); 165 for (String provider : mProviders) { 166 // Can't mock passive provider. 167 if (provider.equals(LocationManager.PASSIVE_PROVIDER)) { 168 mProviders.remove(provider); 169 break; 170 } 171 } 172 mProviders.add(LocationManager.FUSED_PROVIDER); 173 addTestProviders(); 174 } 175 } 176 177 @Override 178 protected void tearDown() throws Exception { 179 stopUpdateLocationThread(); 180 if (mProviders != null) { 181 // Remove the test provider after each test 182 for (String provider : mProviders) { 183 try { 184 // Work around b/11446702 by clearing the test provider before removing it 185 mLocationManager.clearTestProviderEnabled(provider); 186 mLocationManager.removeTestProvider(provider); 187 } catch (IllegalArgumentException e) {} // Not much to do about this 188 } 189 } 190 LocationUtils.registerMockLocationProvider(getInstrumentation(), false); 191 192 if (mOnUiThread != null) { 193 mOnUiThread.cleanUp(); 194 } 195 // This will null all member and static variables 196 super.tearDown(); 197 } 198 199 private void addTestProviders() { 200 for (String providerName : mProviders) { 201 LocationProvider provider = mLocationManager.getProvider(providerName); 202 mLocationManager.addTestProvider(provider.getName(), 203 provider.requiresNetwork(), //requiresNetwork, 204 provider.requiresSatellite(), // requiresSatellite, 205 provider.requiresCell(), // requiresCell, 206 provider.hasMonetaryCost(), // hasMonetaryCost, 207 provider.supportsAltitude(), // supportsAltitude, 208 provider.supportsSpeed(), // supportsSpeed, 209 provider.supportsBearing(), // supportsBearing, 210 provider.getPowerRequirement(), // powerRequirement 211 provider.getAccuracy()); // accuracy 212 mLocationManager.setTestProviderEnabled(provider.getName(), true); 213 } 214 } 215 216 private void startUpdateLocationThread() { 217 // Only start the thread once 218 if (mLocationUpdateThread == null) { 219 mLocationUpdateThreadExitRequested = false; 220 mLocationUpdateThread = new Thread() { 221 @Override 222 public void run() { 223 while (!mLocationUpdateThreadExitRequested) { 224 try { 225 Thread.sleep(LOCATION_THREAD_UPDATE_WAIT_MS); 226 } catch(Exception e) { 227 // Do nothing, an extra update is no problem 228 } 229 updateLocation(); 230 } 231 } 232 }; 233 mLocationUpdateThread.start(); 234 } 235 } 236 237 private void stopUpdateLocationThread() { 238 // Only stop the thread if it was started 239 if (mLocationUpdateThread != null) { 240 mLocationUpdateThreadExitRequested = true; 241 try { 242 mLocationUpdateThread.join(); 243 } catch (InterruptedException e) { 244 // Do nothing 245 } 246 mLocationUpdateThread = null; 247 } 248 } 249 250 // Update location with a fixed latitude and longtitude, sets the time to the current time. 251 private void updateLocation() { 252 for (int i = 0; i < mProviders.size(); i++) { 253 Location location = new Location(mProviders.get(i)); 254 location.setLatitude(40); 255 location.setLongitude(40); 256 location.setAccuracy(1.0f); 257 location.setTime(java.lang.System.currentTimeMillis()); 258 location.setElapsedRealtimeNanos(SystemClock.elapsedRealtimeNanos()); 259 mLocationManager.setTestProviderLocation(mProviders.get(i), location); 260 } 261 } 262 263 // Need to set the location just after loading the url. Setting it after each load instead of 264 // using a maximum age. 265 private void loadUrlAndUpdateLocation(String url) { 266 mOnUiThread.loadUrlAndWaitForCompletion(url); 267 startUpdateLocationThread(); 268 } 269 270 // WebChromeClient that accepts each location for one load. WebChromeClient is used in 271 // WebViewOnUiThread to detect when the page is loaded, so subclassing the one used there. 272 private static class TestSimpleGeolocationRequestWebChromeClient 273 extends WaitForProgressClient { 274 private boolean mReceivedRequest = false; 275 private final boolean mAccept; 276 private final boolean mRetain; 277 278 public TestSimpleGeolocationRequestWebChromeClient( 279 WebViewOnUiThread webViewOnUiThread, boolean accept, boolean retain) { 280 super(webViewOnUiThread); 281 this.mAccept = accept; 282 this.mRetain = retain; 283 } 284 285 @Override 286 public void onGeolocationPermissionsShowPrompt( 287 String origin, GeolocationPermissions.Callback callback) { 288 mReceivedRequest = true; 289 callback.invoke(origin, mAccept, mRetain); 290 } 291 } 292 293 // Test loading a page and accepting the domain for one load 294 public void testSimpleGeolocationRequestAcceptOnce() throws Exception { 295 if (!NullWebViewUtils.isWebViewAvailable()) { 296 return; 297 } 298 final TestSimpleGeolocationRequestWebChromeClient chromeClientAcceptOnce = 299 new TestSimpleGeolocationRequestWebChromeClient(mOnUiThread, true, false); 300 mOnUiThread.setWebChromeClient(chromeClientAcceptOnce); 301 loadUrlAndUpdateLocation(URL_1); 302 Callable<Boolean> receivedRequest = new Callable<Boolean>() { 303 @Override 304 public Boolean call() { 305 return chromeClientAcceptOnce.mReceivedRequest; 306 } 307 }; 308 PollingCheck.check("Geolocation prompt not called", POLLING_TIMEOUT, receivedRequest); 309 Callable<Boolean> receivedLocation = new Callable<Boolean>() { 310 @Override 311 public Boolean call() { 312 return mJavascriptStatusReceiver.mHasPosition; 313 } 314 }; 315 PollingCheck.check("JS didn't get position", POLLING_TIMEOUT, receivedLocation); 316 chromeClientAcceptOnce.mReceivedRequest = false; 317 // Load URL again, should receive callback again 318 loadUrlAndUpdateLocation(URL_1); 319 PollingCheck.check("Geolocation prompt not called", POLLING_TIMEOUT, receivedRequest); 320 PollingCheck.check("JS didn't get position", POLLING_TIMEOUT, receivedLocation); 321 } 322 323 private static class OriginCheck extends PollingCheck implements 324 android.webkit.ValueCallback<Set<String>> { 325 326 private boolean mReceived = false; 327 private final Set<String> mExpectedValue; 328 private Set<String> mReceivedValue = null; 329 330 public OriginCheck(Set<String> val) { 331 mExpectedValue = val; 332 } 333 334 @Override 335 protected boolean check() { 336 if (!mReceived) return false; 337 if (mExpectedValue.equals(mReceivedValue)) return true; 338 if (mExpectedValue.size() != mReceivedValue.size()) return false; 339 // Origins can have different strings even if they represent the same origin, 340 // for example http://www.example.com is the same origin as http://www.example.com/ 341 // and they are both valid representations 342 for (String origin : mReceivedValue) { 343 if (mExpectedValue.contains(origin)) continue; 344 if (origin.endsWith("/")) { 345 if (mExpectedValue.contains(origin.substring(0, origin.length() - 1))) { 346 continue; 347 } 348 } else { 349 if (mExpectedValue.contains(origin + "/")) continue; 350 } 351 return false; 352 } 353 return true; 354 } 355 @Override 356 public void onReceiveValue(Set<String> value) { 357 mReceived = true; 358 mReceivedValue = value; 359 } 360 } 361 362 // Class that waits and checks for a particular value being received 363 private static class BooleanCheck extends PollingCheck implements 364 android.webkit.ValueCallback<Boolean> { 365 366 private boolean mReceived = false; 367 private final boolean mExpectedValue; 368 private boolean mReceivedValue; 369 370 public BooleanCheck(boolean val) { 371 mExpectedValue = val; 372 } 373 374 @Override 375 protected boolean check() { 376 return mReceived && mReceivedValue == mExpectedValue; 377 } 378 379 @Override 380 public void onReceiveValue(Boolean value) { 381 mReceived = true; 382 mReceivedValue = value; 383 } 384 } 385 386 // Test loading a page and retaining the domain forever 387 public void testSimpleGeolocationRequestAcceptAlways() throws Exception { 388 if (!NullWebViewUtils.isWebViewAvailable()) { 389 return; 390 } 391 final TestSimpleGeolocationRequestWebChromeClient chromeClientAcceptAlways = 392 new TestSimpleGeolocationRequestWebChromeClient(mOnUiThread, true, true); 393 mOnUiThread.setWebChromeClient(chromeClientAcceptAlways); 394 // Load url once, and the callback should accept the domain for all future loads 395 loadUrlAndUpdateLocation(URL_1); 396 Callable<Boolean> receivedRequest = new Callable<Boolean>() { 397 @Override 398 public Boolean call() { 399 return chromeClientAcceptAlways.mReceivedRequest; 400 } 401 }; 402 PollingCheck.check("Geolocation prompt not called", POLLING_TIMEOUT, receivedRequest); 403 Callable<Boolean> receivedLocation = new Callable<Boolean>() { 404 @Override 405 public Boolean call() { 406 return mJavascriptStatusReceiver.mHasPosition; 407 } 408 }; 409 PollingCheck.check("JS didn't get position", POLLING_TIMEOUT, receivedLocation); 410 chromeClientAcceptAlways.mReceivedRequest = false; 411 mJavascriptStatusReceiver.clearState(); 412 // Load the same URL again 413 loadUrlAndUpdateLocation(URL_1); 414 PollingCheck.check("JS didn't get position", POLLING_TIMEOUT, receivedLocation); 415 // Assert prompt for geolocation permission is not called the second time 416 assertFalse(chromeClientAcceptAlways.mReceivedRequest); 417 // Check that the permission is in GeolocationPermissions 418 BooleanCheck trueCheck = new BooleanCheck(true); 419 GeolocationPermissions.getInstance().getAllowed(URL_1, trueCheck); 420 trueCheck.run(); 421 Set<String> acceptedOrigins = new TreeSet<String>(); 422 acceptedOrigins.add(URL_1); 423 OriginCheck originCheck = new OriginCheck(acceptedOrigins); 424 GeolocationPermissions.getInstance().getOrigins(originCheck); 425 originCheck.run(); 426 427 // URL_2 should get a prompt 428 chromeClientAcceptAlways.mReceivedRequest = false; 429 loadUrlAndUpdateLocation(URL_2); 430 // Checking the callback for geolocation permission prompt is called 431 PollingCheck.check("Geolocation prompt not called", POLLING_TIMEOUT, receivedRequest); 432 PollingCheck.check("JS didn't get position", POLLING_TIMEOUT, receivedLocation); 433 acceptedOrigins.add(URL_2); 434 originCheck = new OriginCheck(acceptedOrigins); 435 GeolocationPermissions.getInstance().getOrigins(originCheck); 436 originCheck.run(); 437 // Remove a domain manually that was added by the callback 438 GeolocationPermissions.getInstance().clear(URL_1); 439 acceptedOrigins.remove(URL_1); 440 originCheck = new OriginCheck(acceptedOrigins); 441 GeolocationPermissions.getInstance().getOrigins(originCheck); 442 originCheck.run(); 443 } 444 445 // Test the GeolocationPermissions API 446 public void testGeolocationPermissions() { 447 if (!NullWebViewUtils.isWebViewAvailable()) { 448 return; 449 } 450 Set<String> acceptedOrigins = new TreeSet<String>(); 451 BooleanCheck falseCheck = new BooleanCheck(false); 452 GeolocationPermissions.getInstance().getAllowed(URL_2, falseCheck); 453 falseCheck.run(); 454 OriginCheck originCheck = new OriginCheck(acceptedOrigins); 455 GeolocationPermissions.getInstance().getOrigins(originCheck); 456 originCheck.run(); 457 458 // Remove a domain that has not been allowed 459 GeolocationPermissions.getInstance().clear(URL_2); 460 acceptedOrigins.remove(URL_2); 461 originCheck = new OriginCheck(acceptedOrigins); 462 GeolocationPermissions.getInstance().getOrigins(originCheck); 463 originCheck.run(); 464 465 // Add a domain 466 acceptedOrigins.add(URL_2); 467 GeolocationPermissions.getInstance().allow(URL_2); 468 originCheck = new OriginCheck(acceptedOrigins); 469 GeolocationPermissions.getInstance().getOrigins(originCheck); 470 originCheck.run(); 471 BooleanCheck trueCheck = new BooleanCheck(true); 472 GeolocationPermissions.getInstance().getAllowed(URL_2, trueCheck); 473 trueCheck.run(); 474 475 // Add a domain 476 acceptedOrigins.add(URL_1); 477 GeolocationPermissions.getInstance().allow(URL_1); 478 originCheck = new OriginCheck(acceptedOrigins); 479 GeolocationPermissions.getInstance().getOrigins(originCheck); 480 originCheck.run(); 481 482 // Remove a domain that has been allowed 483 GeolocationPermissions.getInstance().clear(URL_2); 484 acceptedOrigins.remove(URL_2); 485 originCheck = new OriginCheck(acceptedOrigins); 486 GeolocationPermissions.getInstance().getOrigins(originCheck); 487 originCheck.run(); 488 falseCheck = new BooleanCheck(false); 489 GeolocationPermissions.getInstance().getAllowed(URL_2, falseCheck); 490 falseCheck.run(); 491 492 // Try to clear all domains 493 GeolocationPermissions.getInstance().clearAll(); 494 acceptedOrigins.clear(); 495 originCheck = new OriginCheck(acceptedOrigins); 496 GeolocationPermissions.getInstance().getOrigins(originCheck); 497 originCheck.run(); 498 499 // Add a domain 500 acceptedOrigins.add(URL_1); 501 GeolocationPermissions.getInstance().allow(URL_1); 502 originCheck = new OriginCheck(acceptedOrigins); 503 GeolocationPermissions.getInstance().getOrigins(originCheck); 504 originCheck.run(); 505 } 506 507 // Test loading pages and checks rejecting once and recjecting the domain forever 508 public void testSimpleGeolocationRequestReject() throws Exception { 509 if (!NullWebViewUtils.isWebViewAvailable()) { 510 return; 511 } 512 final TestSimpleGeolocationRequestWebChromeClient chromeClientRejectOnce = 513 new TestSimpleGeolocationRequestWebChromeClient(mOnUiThread, false, false); 514 mOnUiThread.setWebChromeClient(chromeClientRejectOnce); 515 // Load url once, and the callback should reject it once 516 mOnUiThread.loadUrlAndWaitForCompletion(URL_1); 517 Callable<Boolean> receivedRequest = new Callable<Boolean>() { 518 @Override 519 public Boolean call() { 520 return chromeClientRejectOnce.mReceivedRequest; 521 } 522 }; 523 PollingCheck.check("Geolocation prompt not called", POLLING_TIMEOUT, receivedRequest); 524 Callable<Boolean> locationDenied = new Callable<Boolean>() { 525 @Override 526 public Boolean call() { 527 return mJavascriptStatusReceiver.mDenied; 528 } 529 }; 530 PollingCheck.check("JS got position", POLLING_TIMEOUT, locationDenied); 531 // Same result should happen on next run 532 chromeClientRejectOnce.mReceivedRequest = false; 533 mOnUiThread.loadUrlAndWaitForCompletion(URL_1); 534 PollingCheck.check("Geolocation prompt not called", POLLING_TIMEOUT, receivedRequest); 535 PollingCheck.check("JS got position", POLLING_TIMEOUT, locationDenied); 536 537 // Try to reject forever 538 final TestSimpleGeolocationRequestWebChromeClient chromeClientRejectAlways = 539 new TestSimpleGeolocationRequestWebChromeClient(mOnUiThread, false, true); 540 mOnUiThread.setWebChromeClient(chromeClientRejectAlways); 541 mOnUiThread.loadUrlAndWaitForCompletion(URL_2); 542 PollingCheck.check("Geolocation prompt not called", POLLING_TIMEOUT, receivedRequest); 543 PollingCheck.check("JS didn't get position", POLLING_TIMEOUT, locationDenied); 544 // second load should now not get a prompt 545 chromeClientRejectAlways.mReceivedRequest = false; 546 mOnUiThread.loadUrlAndWaitForCompletion(URL_2); 547 PollingCheck.check("JS didn't get position", POLLING_TIMEOUT, locationDenied); 548 PollingCheck.check("Geolocation prompt not called", POLLING_TIMEOUT, receivedRequest); 549 550 // Test if it gets added to origins 551 Set<String> acceptedOrigins = new TreeSet<String>(); 552 acceptedOrigins.add(URL_2); 553 OriginCheck domainCheck = new OriginCheck(acceptedOrigins); 554 GeolocationPermissions.getInstance().getOrigins(domainCheck); 555 domainCheck.run(); 556 // And now check that getAllowed returns false 557 BooleanCheck falseCheck = new BooleanCheck(false); 558 GeolocationPermissions.getInstance().getAllowed(URL_1, falseCheck); 559 falseCheck.run(); 560 } 561 562 // Object added to the page via AddJavascriptInterface() that is used by the test Javascript to 563 // notify back to Java when a location or error is received. 564 public final static class JavascriptStatusReceiver { 565 public volatile boolean mHasPosition = false; 566 public volatile boolean mDenied = false; 567 public volatile boolean mUnavailable = false; 568 public volatile boolean mTimeout = false; 569 570 public void clearState() { 571 mHasPosition = false; 572 mDenied = false; 573 mUnavailable = false; 574 mTimeout = false; 575 } 576 577 @JavascriptInterface 578 public void errorDenied() { 579 mDenied = true; 580 } 581 582 @JavascriptInterface 583 public void errorUnavailable() { 584 mUnavailable = true; 585 } 586 587 @JavascriptInterface 588 public void errorTimeout() { 589 mTimeout = true; 590 } 591 592 @JavascriptInterface 593 public void gotLocation() { 594 mHasPosition = true; 595 } 596 } 597 } 598