javascriptandroidandroid-webvieww3c-geolocation

navigator.geolocation.getCurrentPosition() never returns in WebView on Android


I am trying to access the HTML Geolocation API available in Android WebView (using SDK version 24).

The main problem is that the call to navigator.geolocation.getCurrentPosition() in JavaScript never returns (neither with an error, nor with position data), while on application side I check for permissions and properly pass them to WebView using android.webkit.GeolocationPermissions.Callback class.

UPDATE: Just to clarify here, by "never returns" I mean that none of the two supplied callbacks navigator.geolocation.getCurrentPosition(success, error) are ever called.

In a sample app I built to test this (with just one small activity hosting WebView) I declare the permissions in manifest and request them properly on App start. I see the prompt and can grant or deny permission to location information.

Manifest:

<uses-permission android:name="android.permission.INTERNET" />
<uses-permission android:name="android.permission.ACCESS_FINE_LOCATION" />
<uses-permission android:name="android.permission.ACCESS_COARSE_LOCATION" />

Code in the main form:

public boolean checkFineLocationPermission() {
    if (ContextCompat.checkSelfPermission(this, Manifest.permission.ACCESS_FINE_LOCATION)
            != PackageManager.PERMISSION_GRANTED) {

        if (ActivityCompat.shouldShowRequestPermissionRationale(this,
                Manifest.permission.ACCESS_FINE_LOCATION) && !mIsPermissionDialogShown) {
            showPermissionDialog(R.string.dialog_permission_location);
        } else {
            ActivityCompat.requestPermissions(this, new String[]{Manifest.permission.ACCESS_FINE_LOCATION},
                    PERMISSION_ACCESS_FINE_LOCATION);
        }
        return false;
    } else {
        return true;
    }
} 

I can check for permissions during runtime using Context.checkSelfPermission() and I see that the respective permissions are granted to my app.

Then I try to open a web page in a WebView control. I enable all required options in the settings:

    mWebSettings.setJavaScriptEnabled(true);
    mWebSettings.setAppCacheEnabled(true);
    mWebSettings.setDatabaseEnabled(true);
    mWebSettings.setDomStorageEnabled(true);
    mWebSettings.setGeolocationEnabled(true);
    mWebSettings.setJavaScriptCanOpenWindowsAutomatically(true);
    mWebSettings.setSupportZoom(true);

I use the following WebChromeClient overload for handling geolocation requests from JavaScript:

protected class EmbeddedChromeClient extends android.webkit.WebChromeClient {
    @Override
    public void onGeolocationPermissionsShowPrompt(String origin,
                                                   android.webkit.GeolocationPermissions.Callback callback) {

        // do we need to request permissions ?
        if (ContextCompat.checkSelfPermission(EmbeddedBrowserActivity.this, Manifest.permission.ACCESS_FINE_LOCATION) != PackageManager.PERMISSION_GRANTED) {
            // this should never happen, it means user revoked permissions
            // need to warn and quit?
            callback.invoke(origin, false, false);
        }
        else {
            callback.invoke(origin, true, true);
        }
    }
}

To test this I use the following code (taken from Mozilla API help page, shortened here):

function geoFindMe() {
  function success(position) {}
  function error() {}
  navigator.geolocation.getCurrentPosition(success, error);
}

What I see is that the call tonavigator.geolocation.getCurrentPosition(success, error) in JavaScript never returns. I see that onGeolocationPermissionsShowPrompt() method in Java gets properly called and as I check for permissions there I always get the result 0, i.e. PackageManager.PERMISSION_GRANTED, so callback.invoke(origin, true, true) is executed on every call. If I try several times, I see several calls to my Java code. Still, nothing happens on the JavaScript side here after I call invoke().

I added the code to check for granted permissions using the invocation of getOrigins(ValueCallback<Set<String>> callback) in GeolocationPermissions class, as described here in the documentation. I see in the callback that my origins are allowed to request locations (they are listed in the set).

Any ideas what might be wrong here?


Solution

  • Try with options to set timeout (source):

    var options = {
        enableHighAccuracy: true,
        timeout: 10000,
        maximumAge: 0
    };
    
    navigator.geolocation.getCurrentPosition(success, error, options);
    

    If it fails then try to override getCurrentPosition (source):

    (function() {
    
    if (navigator.geolocation) {
        function PositionError(code, message) {
            this.code = code;
            this.message = message;
        }
    
        PositionError.PERMISSION_DENIED = 1;
        PositionError.POSITION_UNAVAILABLE = 2;
        PositionError.TIMEOUT = 3;
        PositionError.prototype = new Error();
    
        navigator.geolocation._getCurrentPosition = navigator.geolocation.getCurrentPosition;
    
        navigator.geolocation.getCurrentPosition = function(success, failure, options) {
            var successHandler = function(position) {
                if ((position.coords.latitude == 0 && position.coords.longitude == 0) ||
                    (position.coords.latitude == 37.38600158691406 && position.coords.longitude == -122.08200073242188)) 
                    return failureHandler(new PositionError(PositionError.POSITION_UNAVAILABLE, 'Position unavailable')); 
    
                failureHandler = function() {};
                success(position);
            }
    
            var failureHandler = function(error) {
                failureHandler = function() {};
                failure(error);
            }
    
            navigator.geolocation._getCurrentPosition(successHandler, failureHandler, options);
    
            window.setTimeout(function() { failureHandler(new PositionError(PositionError.TIMEOUT, 'Timed out')) }, 10000);
        }
    }
    })();
    

    As a third option annotate with @JavascriptInterface (source) in EmbeddedChromeClient

    Also add at the proper place in your code:

    mWebSettings.setJavaScriptEnabled(true);
    //...
    mWebSettings.setSupportZoom(true);
    webView.addJavascriptInterface(new EmbeddedChromeClient(webView), "injectedObject");
    webView.loadData("html here", "text/html", null);
    

    The last option is just to use tags in html, load the html from disk storage, replace tags in the calling function, load the html/string in the webView. I have used this approach before in Android when positioning frustrated me too much. Then you don't have to worry about https either.