21

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?

EJK
  • 11,784
  • 3
  • 34
  • 53
Alexander Galkin
  • 10,800
  • 9
  • 56
  • 111

8 Answers8

8

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.

Gillsoft AB
  • 4,145
  • 2
  • 17
  • 35
  • While I can confirm that adding timeout parameter makes it work with a test web page, this still doesn't work if I try opening Google or Yelp in that WebView. This is what I actually need -- I need that other pages (not mine) would have access to location data and show proper results. – Alexander Galkin Aug 21 '18 at 20:08
  • I would personally take the position from the device itself in "native" code and then add a webView.addJavascriptInterface(new Tracker(webView), "alexander") that I would use to create a bridge between the native code and the webview so a webpage can call your native function. Then others, could use alexander.lat, alexander.lon when you load a page (if you talk to them), if not use some injection that overrides navigator.geolocation using the above method. – Gillsoft AB Aug 21 '18 at 20:14
  • Look at the binding section for creating a bridge: https://developer.android.com/guide/webapps/webview#BindingJavaScript – Gillsoft AB Aug 21 '18 at 20:20
  • Also try adding: webView.setWebViewClient(new WebViewClient()); mWebSettings.setPluginState(WebSettings.PluginState.ON); **AFTER** mWebSettings.setJavaScriptEnabled(true); – Gillsoft AB Aug 21 '18 at 20:23
  • I know Android bridge, I use it already in the app to communicate with some *known* pages. But I need a generic solution, that would work for all pages, and most importantly for the ones like Google, Yelp etc (I am talking about mobile web pages, not the apps!). I have no problems to acquire position in my Java code (and use it too), but it is essential that a WebView-hosted page can do this too. – Alexander Galkin Aug 21 '18 at 20:46
  • Ok, have you tried to use: **callback.invoke(origin, true, false);** I think I read somewhere that if you change domain you need a new approval, and since you saved the user choice it might fail, did you try that? I might remember wrong tho. – Gillsoft AB Aug 21 '18 at 20:57
  • Yes, I tried both `false` and `true` for remembering the option. I also see the proper domains in the list (see my question). – Alexander Galkin Aug 21 '18 at 21:06
  • Thank you for this! Adding the options with timeout made it work for me in my WebView app! – Laurens Swart Apr 09 '21 at 19:02
4

Looks like there are 2 different issues in your case:

  1. getCurrentPosition never fails
  2. getCurrentPosition never succeed

First point could be just because method has infinite timeout

The default value is Infinity, meaning that getCurrentPosition() won't return until the position is available.

Second point could be tricky, there is a param maximumAge which means

The PositionOptions.maximumAge property is a positive long value indicating the maximum age in milliseconds of a possible cached position that is acceptable to return. If set to 0, it means that the device cannot use a cached position and must attempt to retrieve the real current position. If set to Infinity the device must return a cached position regardless of its age.

0 by default means that device won't use cached position and will try to fetch the real one and it could be an issue for long response.

Also you could check this reporst which could mean that this API doesn't work really good on Android: https://github.com/ionic-team/ionic-native/issues/1958 https://issues.apache.org/jira/browse/CB-13241

*Cordova leaves geolocation stuff for browser.

dilix
  • 3,510
  • 3
  • 27
  • 53
1

Change your request for permission like this,

ActivityCompat.requestPermissions(this, new String[]{
            Manifest.permission.ACCESS_FINE_LOCATION,
            Manifest.permission.ACCESS_COARSE_LOCATION
    },0);

This seems to work.

Saikrishna Rajaraman
  • 2,621
  • 2
  • 11
  • 26
1

For applications targeting Android N and later SDKs (API level > Build.VERSION_CODES.M), the method onGeolocationPermissionsShowPrompt (String origin, GeolocationPermissions.Callback callback) is only called for requests originating from secure origins such as HTTPS. On non-secure origins, geolocation requests are automatically denied.

You could narrow down your problem by yourself if you had tried putting a breakpoint or a log inside the method.

You have two options:

  1. Target a lower level API, which is obviously much easier but not really appreciated.
  2. Set up SSL in your website.
Bertram Gilfoyle
  • 8,353
  • 5
  • 36
  • 61
1

Actually, navigator is a child of browser global object, the window. you should first access to window then call the navigator. In some modern browsers, In addition to the window, some child object is presented like a global object, for example: location, navigator and etc.

The WebView has no global object window. So you can add it manually, for this action please read this medium article.

Maybe your code will be like below:

my_web_view.evaluateJavascript("javascript: " + "updateFromAndroid(\"" + edit_text_to_web.text + "\")", null);

And then add JavaScript interface, like the window object, to this evaluator:

my_web_view.addJavascriptInterface(JavaScriptInterface(), JAVASCRIPT_OBJ); //JAVASCRIPT_OBJ: like window, like navigator, like location and etc.
AmerllicA
  • 15,720
  • 11
  • 72
  • 103
1

In case some of you are still experiencing this issue, I was able to get the navigator.geolocation.getCurrentPosition() to work for me by setting some values for the options parameter, as follows:

if (navigator.geolocation) {
    navigator.geolocation.getCurrentPosition(  
      async (p) => {
        await this.workMyCurrentPosition(p);
      },
      (e) => this.navigatorError(e),
      { timeout: 7000, enableHighAccuracy: true, maximumAge: 0 }
    );
}

Hope it works for you!

Manuel Hernandez
  • 321
  • 3
  • 10
0

There is an extensive post on this subject:

navigator.geolocation.getCurrentPosition sometimes works sometimes doesn't

Apparently the solution is to check on navigator.geolocation then make the call:

if(navigator.geolocation) { // dummy call
    navigator.geolocation.getCurrentPosition(success, error)
}
RonTLV
  • 2,010
  • 2
  • 19
  • 31
0

I experienced this problem on Android 8. The error callback was called with error.code==1 "PERMISSION_DENIED".

https://developers.google.com/web/updates/2016/04/geolocation-on-secure-contexts-only Geolocation API Removed from Unsecured Origins in Chrome 50

Meaning, the URL of your app must begin with https:// for the call to work.

Lee Irvine
  • 2,350
  • 1
  • 11
  • 12