Adding a WebView cts test for Geolocation

Change-Id: I33aeeeeaedb8d47f7adb8489e362ae1217be84c2
diff --git a/tests/tests/webkit/AndroidManifest.xml b/tests/tests/webkit/AndroidManifest.xml
index 9475451..493762e 100644
--- a/tests/tests/webkit/AndroidManifest.xml
+++ b/tests/tests/webkit/AndroidManifest.xml
@@ -19,6 +19,8 @@
     package="com.android.cts.webkit">
 
     <uses-permission android:name="android.permission.DISABLE_KEYGUARD" />
+    <uses-permission android:name="android.permission.ACCESS_LOCATION_EXTRA_COMMANDS"/>
+    <uses-permission android:name="android.permission.ACCESS_MOCK_LOCATION"/>
     <application>
         <uses-library android:name="android.test.runner" />
     </application>
diff --git a/tests/tests/webkit/src/android/webkit/cts/GeolocationTest.java b/tests/tests/webkit/src/android/webkit/cts/GeolocationTest.java
new file mode 100644
index 0000000..c4defca
--- /dev/null
+++ b/tests/tests/webkit/src/android/webkit/cts/GeolocationTest.java
@@ -0,0 +1,473 @@
+/*
+ * Copyright (C) 2012 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package android.webkit.cts;
+
+import android.content.Context;
+import android.cts.util.PollingCheck;
+import android.graphics.Bitmap;
+import android.location.Criteria;
+import android.location.Location;
+import android.location.LocationListener;
+import android.location.LocationManager;
+import android.location.LocationProvider;
+import android.os.Bundle;
+import android.os.Looper;
+import android.os.SystemClock;
+import android.test.ActivityInstrumentationTestCase2;
+import android.util.Log;
+import android.webkit.CookieManager;
+import android.webkit.CookieSyncManager;
+import android.webkit.GeolocationPermissions;
+import android.webkit.JavascriptInterface;
+import android.webkit.WebChromeClient;
+import android.webkit.WebResourceResponse;
+import android.webkit.WebView;
+import android.webkit.WebViewClient;
+import android.webkit.cts.WebViewOnUiThread.WaitForLoadedClient;
+import android.webkit.cts.WebViewOnUiThread.WaitForProgressClient;
+
+import java.io.ByteArrayInputStream;
+import java.io.UnsupportedEncodingException;
+import java.util.concurrent.Callable;
+import java.util.Date;
+import java.util.Random;
+import java.util.regex.Matcher;
+import java.util.regex.Pattern;
+import java.util.Set;
+import java.util.TreeSet;
+
+import junit.framework.Assert;
+
+public class GeolocationTest extends ActivityInstrumentationTestCase2<WebViewStubActivity> {
+
+    // TODO Write additional tests to cover:
+    // - test that the errors are correct
+    // - test that use of gps and network location is correct
+
+    // The URLs does not matter since the tests will intercept the load, but it has to be a real
+    // url, and different domains.
+    private static final String URL_1 = "http://www.example.com";
+    private static final String URL_2 = "http://www.example.org";
+
+    private static final String JS_INTERFACE_NAME = "Android";
+    private static final int POLLING_TIMEOUT = 2000;
+
+    // static HTML page always injected instead of the url loaded
+    private static final String RAW_HTML =
+            "<!DOCTYPE html>\n" +
+            "<html>\n" +
+            "  <head>\n" +
+            "    <title>Geolocation</title>\n" +
+            "    <script>\n" +
+            "      function gotPos(position) {\n" +
+            "        " + JS_INTERFACE_NAME + ".gotLocation();\n" +
+            "      }\n" +
+            "      function initiate_getCurrentPosition() {\n" +
+            "        navigator.geolocation.getCurrentPosition(\n" +
+            "            gotPos,\n" +
+            "            handle_errors,\n" +
+            "            {maximumAge:1000});\n" +
+            "      }\n" +
+            "      function handle_errors(error) {\n" +
+            "        switch(error.code) {\n" +
+            "          case error.PERMISSION_DENIED:\n" +
+            "            " + JS_INTERFACE_NAME + ".errorDenied(); break;\n" +
+            "          case error.POSITION_UNAVAILABLE:\n" +
+            "            " + JS_INTERFACE_NAME + ".errorUnavailable(); break;\n" +
+            "          case error.TIMEOUT:\n" +
+            "            " + JS_INTERFACE_NAME + ".errorTimeout(); break;\n" +
+            "          default: break;\n" +
+            "        }\n" +
+            "      }\n" +
+            "    </script>\n" +
+            "  </head>\n" +
+            "  <body onload=\"initiate_getCurrentPosition();\">\n" +
+            "  </body>\n" +
+            "</html>";
+
+    private JavascriptStatusReceiver mJavascriptStatusReceiver;
+    private LocationManager mLocationManager;
+    private WebViewOnUiThread mOnUiThread;
+
+    public GeolocationTest() throws Exception {
+        super("com.android.cts.stub", WebViewStubActivity.class);
+    }
+
+    // Both this test and WebViewOnUiThread need to override some of the methods on WebViewClient,
+    // so this test sublclasses the WebViewClient from WebViewOnUiThread
+    private static class InterceptClient extends WaitForLoadedClient {
+
+        public InterceptClient(WebViewOnUiThread webViewOnUiThread) throws Exception {
+            super(webViewOnUiThread);
+        }
+
+        @Override
+        public WebResourceResponse shouldInterceptRequest(WebView view, String url) {
+            // Intercept all page loads with the same geolocation enabled page
+            try {
+                return new WebResourceResponse("text/html", "utf-8",
+                    new ByteArrayInputStream(RAW_HTML.getBytes("UTF-8")));
+            } catch(java.io.UnsupportedEncodingException e) {
+                return null;
+            }
+        }
+    }
+
+    @Override
+    protected void setUp() throws Exception {
+        super.setUp();
+
+        // Set up a WebView with JavaScript and Geolocation enabled
+        final String GEO_DIR = "geo_test";
+        mOnUiThread = new WebViewOnUiThread(this, getActivity().getWebView());
+        mOnUiThread.getSettings().setJavaScriptEnabled(true);
+        mOnUiThread.getSettings().setGeolocationEnabled(true);
+        mOnUiThread.getSettings().setGeolocationDatabasePath(
+                getActivity().getApplicationContext().getDir(GEO_DIR, 0).getPath());
+
+        // Add a JsInterface to report back to the test when a location is received
+        mJavascriptStatusReceiver = new JavascriptStatusReceiver();
+        mOnUiThread.addJavascriptInterface(mJavascriptStatusReceiver, JS_INTERFACE_NAME);
+
+        // Always intercept all loads with the same geolocation test page
+        mOnUiThread.setWebViewClient(new InterceptClient(mOnUiThread));
+        // Clear all permissions before each test
+        GeolocationPermissions.getInstance().clearAll();
+        // Cache this mostly because the lookup is two lines of code
+        mLocationManager = (LocationManager)getActivity().getApplicationContext()
+                .getSystemService(Context.LOCATION_SERVICE);
+        // Add a test provider before each test to inject a location
+        addTestProvider(LocationManager.NETWORK_PROVIDER);
+    }
+
+    @Override
+    protected void tearDown() throws Exception {
+        // Remove the test provider after each test
+        try {
+          mLocationManager.removeTestProvider(LocationManager.NETWORK_PROVIDER);
+        } catch (IllegalArgumentException e) {} // Not much to do about this
+        mOnUiThread.cleanUp();
+        // This will null all member and static variables
+        super.tearDown();
+    }
+
+    // Update location with a fixed latitude and longtitude, sets the time to the current time.
+    private void updateLocation(final String providerName) {
+        Location location = new Location(providerName);
+        location.setLatitude(40);
+        location.setLongitude(40);
+        location.setAccuracy(1.0f);
+        location.setTime(java.lang.System.currentTimeMillis());
+        location.setElapsedRealtimeNanos(SystemClock.elapsedRealtimeNanos());
+        mLocationManager.setTestProviderLocation(providerName, location);
+    }
+
+    // Need to set the location just after loading the url. Setting it after each load instead of
+    // using a maximum age.
+    private void loadUrlAndUpdateLocation(String url) {
+        mOnUiThread.loadUrlAndWaitForCompletion(url);
+        updateLocation(LocationManager.NETWORK_PROVIDER);
+    }
+
+    // WebChromeClient that accepts each location for one load. WebChromeClient is used in
+    // WebViewOnUiThread to detect when the page is loaded, so subclassing the one used there.
+    private static class TestSimpleGeolocationRequestWebChromeClient
+                extends WaitForProgressClient {
+        private boolean receivedRequest = false;
+        private final boolean accept;
+        private final boolean retain;
+
+        public TestSimpleGeolocationRequestWebChromeClient(
+                WebViewOnUiThread webViewOnUiThread, boolean accept, boolean retain) {
+            super(webViewOnUiThread);
+            this.accept = accept;
+            this.retain = retain;
+        }
+
+        @Override
+        public void onGeolocationPermissionsShowPrompt(
+                String origin, GeolocationPermissions.Callback callback) {
+            receivedRequest = true;
+            callback.invoke(origin, accept, retain);
+        }
+    }
+
+    private void addTestProvider(final String providerName) {
+        mLocationManager.addTestProvider(providerName,
+                true, //requiresNetwork,
+                false, // requiresSatellite,
+                true,  // requiresCell,
+                false, // hasMonetaryCost,
+                false, // supportsAltitude,
+                false, // supportsSpeed,
+                false, // supportsBearing,
+                Criteria.POWER_MEDIUM, // powerRequirement
+                Criteria.ACCURACY_FINE); // accuracy
+        mLocationManager.setTestProviderEnabled(providerName, true);
+    }
+
+    // Test loading a page and accepting the domain for one load
+    public void testSimpleGeolocationRequestAcceptOnce() throws Exception {
+        final TestSimpleGeolocationRequestWebChromeClient chromeClientAcceptOnce =
+                new TestSimpleGeolocationRequestWebChromeClient(mOnUiThread, true, false);
+        mOnUiThread.setWebChromeClient(chromeClientAcceptOnce);
+        loadUrlAndUpdateLocation(URL_1);
+        Callable<Boolean> receivedRequest = new Callable<Boolean>() {
+            @Override
+            public Boolean call() {
+                return chromeClientAcceptOnce.receivedRequest;
+            }
+        };
+        PollingCheck.check("Geolocation prompt called", POLLING_TIMEOUT, receivedRequest);
+        Callable<Boolean> receivedLocation = new Callable<Boolean>() {
+            @Override
+            public Boolean call() {
+                return mJavascriptStatusReceiver.hasPosition;
+            }
+        };
+        PollingCheck.check("JS got position", POLLING_TIMEOUT, receivedLocation);
+        chromeClientAcceptOnce.receivedRequest = false;
+        // Load URL again, should receive callback again
+        loadUrlAndUpdateLocation(URL_1);
+        PollingCheck.check("Geolocation prompt called", POLLING_TIMEOUT, receivedRequest);
+        PollingCheck.check("JS got position", POLLING_TIMEOUT, receivedLocation);
+    }
+
+    // Class that waits and checks for a particular value being received
+    private static class ValueCheck<T> extends PollingCheck implements
+            android.webkit.ValueCallback<T> {
+        private boolean received = false;
+        private final T expectedValue;
+        private T receivedValue = null;
+
+        public ValueCheck(T val) {
+            expectedValue = val;
+        }
+
+        @Override
+        protected boolean check() {
+            return received && expectedValue.equals(receivedValue);
+        }
+        @Override
+        public void onReceiveValue(T value) {
+            received = true;
+            receivedValue = value;
+        }
+    }
+
+    // Test loading a page and retaining the domain forever
+    public void testSimpleGeolocationRequestAcceptAlways() throws Exception {
+        final TestSimpleGeolocationRequestWebChromeClient chromeClientAcceptAlways =
+                new TestSimpleGeolocationRequestWebChromeClient(mOnUiThread, true, true);
+        mOnUiThread.setWebChromeClient(chromeClientAcceptAlways);
+        // Load url once, and the callback should accept the domain for all future loads
+        loadUrlAndUpdateLocation(URL_1);
+        Callable<Boolean> receivedRequest = new Callable<Boolean>() {
+            @Override
+            public Boolean call() {
+                return chromeClientAcceptAlways.receivedRequest;
+            }
+        };
+        PollingCheck.check("Geolocation prompt called", POLLING_TIMEOUT, receivedRequest);
+        Callable<Boolean> receivedLocation = new Callable<Boolean>() {
+            @Override
+            public Boolean call() {
+                return mJavascriptStatusReceiver.hasPosition;
+            }
+        };
+        PollingCheck.check("JS got position", POLLING_TIMEOUT, receivedLocation);
+        chromeClientAcceptAlways.receivedRequest = false;
+        mJavascriptStatusReceiver.clearState();
+        // Load the same URL again
+        loadUrlAndUpdateLocation(URL_1);
+        PollingCheck.check("JS got position", POLLING_TIMEOUT, receivedLocation);
+        // Assert prompt for geolocation permission is not called the second time
+        assertFalse(chromeClientAcceptAlways.receivedRequest);
+        // Check that the permission is in GeolocationPermissions
+        ValueCheck<Boolean> trueCheck = new ValueCheck<Boolean>(true);
+        GeolocationPermissions.getInstance().getAllowed(URL_1, trueCheck);
+        trueCheck.run();
+        Set<String> acceptedOrigins = new TreeSet<String>();
+        acceptedOrigins.add(URL_1);
+        ValueCheck<Set<String>> originCheck = new ValueCheck<Set<String>>(acceptedOrigins);
+        GeolocationPermissions.getInstance().getOrigins(originCheck);
+        originCheck.run();
+
+        // URL_2 should get a prompt
+        chromeClientAcceptAlways.receivedRequest = false;
+        loadUrlAndUpdateLocation(URL_2);
+        // Checking the callback for geolocation permission prompt is called
+        PollingCheck.check("Geolocation prompt called", POLLING_TIMEOUT, receivedRequest);
+        PollingCheck.check("JS got position", POLLING_TIMEOUT, receivedLocation);
+        acceptedOrigins.add(URL_2);
+        originCheck = new ValueCheck<Set<String>>(acceptedOrigins);
+        GeolocationPermissions.getInstance().getOrigins(originCheck);
+        originCheck.run();
+        // Remove a domain manually that was added by the callback
+        GeolocationPermissions.getInstance().clear(URL_1);
+        acceptedOrigins.remove(URL_1);
+        originCheck = new ValueCheck<Set<String>>(acceptedOrigins);
+        GeolocationPermissions.getInstance().getOrigins(originCheck);
+        originCheck.run();
+    }
+
+    // Test the GeolocationPermissions API
+    public void testGeolocationPermissions() {
+        Set<String> acceptedOrigins = new TreeSet<String>();
+        ValueCheck<Boolean> falseCheck = new ValueCheck<Boolean>(false);
+        GeolocationPermissions.getInstance().getAllowed(URL_2, falseCheck);
+        falseCheck.run();
+        ValueCheck<Set<String>> originCheck = new ValueCheck<Set<String>>(acceptedOrigins);
+        GeolocationPermissions.getInstance().getOrigins(originCheck);
+        originCheck.run();
+
+        // Remove a domain that has not been allowed
+        GeolocationPermissions.getInstance().clear(URL_2);
+        acceptedOrigins.remove(URL_2);
+        originCheck = new ValueCheck<Set<String>>(acceptedOrigins);
+        GeolocationPermissions.getInstance().getOrigins(originCheck);
+        originCheck.run();
+
+        // Add a domain
+        acceptedOrigins.add(URL_2);
+        GeolocationPermissions.getInstance().allow(URL_2);
+        originCheck = new ValueCheck<Set<String>>(acceptedOrigins);
+        GeolocationPermissions.getInstance().getOrigins(originCheck);
+        originCheck.run();
+        ValueCheck<Boolean> trueCheck = new ValueCheck<Boolean>(true);
+        GeolocationPermissions.getInstance().getAllowed(URL_2, trueCheck);
+        trueCheck.run();
+
+        // Add a domain
+        acceptedOrigins.add(URL_1);
+        GeolocationPermissions.getInstance().allow(URL_1);
+        originCheck = new ValueCheck<Set<String>>(acceptedOrigins);
+        GeolocationPermissions.getInstance().getOrigins(originCheck);
+        originCheck.run();
+
+        // Remove a domain that has been allowed
+        GeolocationPermissions.getInstance().clear(URL_2);
+        acceptedOrigins.remove(URL_2);
+        originCheck = new ValueCheck<Set<String>>(acceptedOrigins);
+        GeolocationPermissions.getInstance().getOrigins(originCheck);
+        originCheck.run();
+        falseCheck = new ValueCheck<Boolean>(false);
+        GeolocationPermissions.getInstance().getAllowed(URL_2, falseCheck);
+        falseCheck.run();
+
+        // Try to clear all domains
+        GeolocationPermissions.getInstance().clearAll();
+        acceptedOrigins.clear();
+        originCheck = new ValueCheck<Set<String>>(acceptedOrigins);
+        GeolocationPermissions.getInstance().getOrigins(originCheck);
+        originCheck.run();
+
+        // Add a domain
+        acceptedOrigins.add(URL_1);
+        GeolocationPermissions.getInstance().allow(URL_1);
+        originCheck = new ValueCheck<Set<String>>(acceptedOrigins);
+        GeolocationPermissions.getInstance().getOrigins(originCheck);
+        originCheck.run();
+    }
+
+    // Test loading pages and checks rejecting once and recjecting the domain forever
+    public void testSimpleGeolocationRequestReject() throws Exception {
+        final TestSimpleGeolocationRequestWebChromeClient chromeClientRejectOnce =
+                new TestSimpleGeolocationRequestWebChromeClient(mOnUiThread, false, false);
+        mOnUiThread.setWebChromeClient(chromeClientRejectOnce);
+        // Load url once, and the callback should accept the domain for all future loads
+        mOnUiThread.loadUrlAndWaitForCompletion(URL_1);
+        Callable<Boolean> receivedRequest = new Callable<Boolean>() {
+            @Override
+            public Boolean call() {
+                return chromeClientRejectOnce.receivedRequest;
+            }
+        };
+        PollingCheck.check("Geolocation prompt called", POLLING_TIMEOUT, receivedRequest);
+        Callable<Boolean> locationDenied = new Callable<Boolean>() {
+            @Override
+            public Boolean call() {
+                return mJavascriptStatusReceiver.denied;
+            }
+        };
+        PollingCheck.check("JS got position", POLLING_TIMEOUT, locationDenied);
+        // Same result should happen on next run
+        mOnUiThread.loadUrlAndWaitForCompletion(URL_1);
+        PollingCheck.check("Geolocation prompt called", POLLING_TIMEOUT, receivedRequest);
+        PollingCheck.check("JS got position", POLLING_TIMEOUT, locationDenied);
+
+        // Try to reject forever
+        final TestSimpleGeolocationRequestWebChromeClient chromeClientRejectAlways =
+            new TestSimpleGeolocationRequestWebChromeClient(mOnUiThread, false, true);
+        mOnUiThread.setWebChromeClient(chromeClientRejectAlways);
+        mOnUiThread.loadUrlAndWaitForCompletion(URL_2);
+        PollingCheck.check("Geolocation prompt called", POLLING_TIMEOUT, receivedRequest);
+        PollingCheck.check("JS got position", POLLING_TIMEOUT, locationDenied);
+        // second load should now not get a prompt
+        chromeClientRejectAlways.receivedRequest = false;
+        mOnUiThread.loadUrlAndWaitForCompletion(URL_2);
+        PollingCheck.check("JS got position", POLLING_TIMEOUT, locationDenied);
+        PollingCheck.check("Geolocation prompt called", POLLING_TIMEOUT, receivedRequest);
+
+        // Test if it gets added to origins
+        Set<String> acceptedOrigins = new TreeSet<String>();
+        acceptedOrigins.add(URL_2);
+        ValueCheck<Set<String>> domainCheck = new ValueCheck<Set<String>>(acceptedOrigins);
+        GeolocationPermissions.getInstance().getOrigins(domainCheck);
+        domainCheck.run();
+        // And now check that getAllowed returns false
+        ValueCheck<Boolean> falseCheck = new ValueCheck<Boolean>(false);
+        GeolocationPermissions.getInstance().getAllowed(URL_1, falseCheck);
+        falseCheck.run();
+    }
+
+    // Object added to the page via AddJavascriptInterface() that is used by the test Javascript to
+    // notify back to Java when a location or error is received.
+    public final static class JavascriptStatusReceiver {
+        public volatile boolean hasPosition = false;
+        public volatile boolean denied = false;
+        public volatile boolean unavailable = false;
+        public volatile boolean timeout = false;
+
+        public void clearState() {
+            hasPosition = false;
+            denied = false;
+            unavailable = false;
+            timeout = false;
+        }
+
+        @JavascriptInterface
+        public void errorDenied() {
+            denied = true;
+        }
+
+        @JavascriptInterface
+        public void errorUnavailable() {
+            unavailable = true;
+        }
+
+        @JavascriptInterface
+        public void errorTimeout() {
+            timeout = true;
+        }
+
+        @JavascriptInterface
+        public void gotLocation() {
+            hasPosition = true;
+        }
+    }
+}