32

The problem is rather simple. In the application we want to keep track of the current url being displayed. For that we use shouldOverrideUrlLoading callback from the WebViewClient by saving the url into a class field for every update. Here is the relevant code:

    mWebView.getSettings().setJavaScriptEnabled(true);
    mWebView.getSettings().setDomStorageEnabled(true); 
    mWebView.setWebViewClient(new WebViewClient() {
        @Override
        public boolean shouldOverrideUrlLoading(WebView view, String url) {
            mCurrentUrl = url;

            // If we don't return false then any redirect (like redirecting to the mobile
            // version of the page) or any link click will open the web browser (like an
            // implicit intent).
            return false;
        }



        @Override
        public void onPageFinished(WebView view, String url) {
            super.onPageFinished(view, url);
            ...
        }
    });
    mWebView.loadUrl(mInitialUrl);

However, there is at least one scenario, where the callback never gets triggered and the mCurrentUrl field doesnt get updated.

The url: https://m.pandora.net/es-es/products/bracelets/556000

Last updated url (shouldOverrideUrlLoading never gets called when clicking the product): https://m.pandora.net/es-es/products/bracelets

I have tried with callbacks like onPageStarted(), but the url also gets filtered and there doesn't seem to be an accessible one upstream since its protected code.

Reading android documentation about WebView I found this:

https://developer.android.com/guide/webapps/migrating.html#URLs

The new WebView applies additional restrictions when requesting resources and resolving links that use a custom URL scheme. For example, if you implement callbacks such as shouldOverrideUrlLoading() or shouldInterceptRequest(), then WebView invokes them only for valid URLs.

But still doesnt make sense since the above url is generic and should meet the standard.

Any alternative or solution to this?

BlackHatSamurai
  • 23,275
  • 22
  • 95
  • 156
SuppressWarnings
  • 4,134
  • 4
  • 25
  • 33
  • Anything special about the URL, like is the request using `post` method? – Swordsman Oct 19 '16 at 10:08
  • Have you tried overriding the `onReceivedError` method in your custom WebViewClient? Maybe you can check out the request and error parameters from that callback to see what's going on. Also, maybe try typing that URL into your device's browser, just to see if there's something wrong with the actual web page loading. – w3bshark Oct 20 '16 at 04:29
  • The page works fine, no error received either. I think the answer lies within the HTML5 History API which can change the url displayed in the browser without actually launching a request (because the content works within a script frame) – SuppressWarnings Oct 20 '16 at 08:30
  • Have you tried to do `mCurrentUrl = url` INSIDE `onPageFinished(WebView, String)`? If you just want the current url, this should work, isn't? – TroniPM Oct 20 '16 at 17:19
  • @PMateus Yes, it doesnt work either. The answer seems to be related to what Leo Nikkila has posted here. – SuppressWarnings Oct 21 '16 at 08:18
  • Yeah... I see. Well, you can see my answer (too long to post as comment). – TroniPM Oct 21 '16 at 15:15

10 Answers10

36

When you click a product on that web page, it loads the new content in with JavaScript and updates the visible URL in the address bar using the HTML5 History APIs.

From the above MDN article:

This will cause the URL bar to display http://mozilla.org/bar.html, but won't cause the browser to load bar.html or even check that bar.html exists.

These are sometimes called single-page applications. Since the actual loaded page doesn’t change, the WebView callback for page loads isn’t called.

In case you know precisely what kind of HTTP request you want to intercept, you could use the shouldInterceptRequest callback that gets called for each request. It’s likely that the web application loads some data from an API, for example when a product is shown, which you could then detect.

If detecting this isn’t possible, but you’re in control of the web application, you could use the Android JavaScript interface to invoke methods within the Android application directly from the web page.

If you’re not in control of the loaded page, you could still try to inject a local JavaScript file into the web page and observe when the history APIs are used, then call methods in your Android application over the JS interface. I tried observing these events in Chrome with the method described in the previous link and it seems to work fine.

Leo Nikkilä
  • 1,547
  • 18
  • 29
13

Maybe this helps someone, although the signature in the question is correct, but Android Studio suggests the following method signature:

public boolean shouldOverrideUrlLoading(WebView view, WebResourceRequest request) {

which then never called. It took me a while to notice that the right signature is:

public boolean shouldOverrideUrlLoading(WebView view, String url) {

Sorry if this not 100% fit the question, but I believe this may help someone in the same situation. It's not always easy to notice that the second parameter is different.

Mike Keskinov
  • 11,614
  • 6
  • 59
  • 87
  • 6
    The problem occurs on devices using API 23 and lower. To support all Android versions, one has to implement both callbacks. I filed a bug (with documentation) to Chromium: https://bugs.chromium.org/p/chromium/issues/detail?id=1064537 – saschpe Mar 25 '20 at 08:56
  • Just add `androidx.webkit:webkit:1.4.0` as dependency and use `WebViewClientCompat`. That way, you only need to override the method once. – Carsten Hagemann Jun 14 '21 at 11:40
  • Still not working for me. – Taslim Oseni Mar 09 '22 at 15:34
8

Please omit mWebView.getSettings().setDomStorageEnabled(true);

Then again try, if a new url found then will invoke shouldOverrideUrl()

IntelliJ Amiya
  • 74,896
  • 15
  • 165
  • 198
Jijo
  • 510
  • 7
  • 13
8

For me the problem was below line -

mWebView.getSettings().setSupportMultipleWindows(true);

After removing it shouldOverrideUrlLoading was being called.

Hrishikesh Kadam
  • 35,376
  • 3
  • 26
  • 36
  • I had to explicitly set it to false to make my downloads work. I guess because they had `target="_blank"` in the anchor. – Edgar Aug 03 '23 at 12:21
7

I had the same problem like you, and I've finished with extending of WebViewChromeClient with listening for callback to

public void onReceivedTitle(WebView view, String title)

mWebView.setWebChromeClient(mSWWebChromeClient);

private WebChromeClient mSWWebChromeClient = new WebChromeClient() {

        @Override
        public void onReceivedTitle(WebView view, String title) {
            super.onReceivedTitle(view, title);
            if (!view.getUrl().equals(mCurrentUrl)) {
                mCurrentUrl = view.getUrl();
                //make something
            }
        }

    }; 
Michael Katkov
  • 2,256
  • 1
  • 20
  • 17
6

after stumbling on this problem and searching for solutions, I've found the one that worked perfectly for me

https://stackoverflow.com/a/56395424/10506087

 override fun doUpdateVisitedHistory(view: WebView?, url: String?, isReload: Boolean) {
    // your code here
    super.doUpdateVisitedHistory(view, url, isReload)
}
Christian K
  • 69
  • 1
  • 3
5

Another approach you can try: Catch the url by javascript side. Initialize your webView with this:

webView.addJavascriptInterface(new WebAppInterface(getActivity()), "Android");

After page is completely loaded (You can use an algorithm to check this like this https://stackoverflow.com/a/6199854/4198633), then:

if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.KITKAT) {
    webView.evaluateJavascript("(function() {return window.location.href;})", new ValueCallback<String>() {
        @Override
        public void onReceiveValue(String url) {
            //do your scheme with variable "url"
        }
    });
} else {
    webView.loadUrl("javascript:Android.getURL(window.location.href);");
}

And declare your WebAppInterface:

public class WebAppInterface {
    Activity mContext;

    public WebAppInterface(Activity c) {
        mContext = c;
    }

    @JavascriptInterface
    public void getURL(final String url) {
        mContext.runOnUiThread(new Runnable() {
            @Override
            public void run() {
                //do your scheme with variable "url" in UIThread side. Over here you can call any method inside your activity/fragment
            }
        });

    }

}

You can do something like that to get url, or anything else inside the page.

Community
  • 1
  • 1
TroniPM
  • 329
  • 3
  • 13
4

Add webView.getSetting().setMixedContentMode(WebSettings.MIXED_CONTENT_ALWAYS_ALLOW); then shouldOverrideUrl will be triggered.

Saurabh Padwekar
  • 3,888
  • 1
  • 31
  • 37
  • Applying this appears to resolve the issue and `shouldOverrideUrl` is called when expected however there are security implications for setting [MIXED_CONTENT_ALWAYS_ALLOW](https://developer.android.com/reference/android/webkit/WebSettings#MIXED_CONTENT_ALWAYS_ALLOW) – DaveAlden Jul 07 '20 at 11:41
3

onProgressChanged is always triggered when reloading, loading new page with userclick or XmlHttpRequest. Compare the URL of previous load and the current load, you'll know it's reloading or loading a new page. This works perfect in my single page Web App.

First declare a global variable to store last URL.

String strLastUrl = null;

Then override onProgressChanged(WebView view, int progress)

mWebView.setWebChromeClient(new MyWebChromeClient(){

   @Override
   public void onProgressChanged(WebView view, int progress) {
            if (progress == 100) {
                //A fully loaded url will come here
                String StrNewUrl = view.getUrl();
                if(TextUtils.equals(StrNewUrl,strLastUrl)){
                   //same page was reloaded, not doing anything
                }else{
                   //a new page was loaded,write this new url to variable
                   strLastUrl = StrNewUrl;
                   //do your work here
                   Log.d("TAG", "A new page or xhr loaded, the new url is : " + strLastUrl);
                }
            }
            super.onProgressChanged(view, progress);
        }
});

I've also tried above solutions, but most of them have issue in my case:

  1. doUpdateVisitedHistory sometimes can not return correct url after "#" made by XmlHttpRequest.
  2. onReceivedTitle doesn't work in my case because the response retrieved by XMLHttpRequest does not have <title></title> tag.
  3. The JavascriptInterface method also works, but I'm afraid it will cause security related issues with javascript.
Lynch Chen
  • 178
  • 2
  • 16
  • 1
    Best solution, as Lynch has mentioned none of solutions provided above are working for me too but his answer worked. – Cosmic Dev Jun 25 '20 at 10:31
2
public class AndroidMobileAppSampleActivity extends Activity {
 /** Called when the activity is first created. */
String mCurrentUrl="";
@Override
public void onCreate(Bundle savedInstanceState) {
    super.onCreate(savedInstanceState);
    setContentView(R.layout.main);

    WebView mWebView = (WebView) findViewById(R.id.mainWebView);

    WebSettings webSettings = mWebView.getSettings();
    webSettings.setJavaScriptEnabled(true);

    mWebView.setWebViewClient(new MyCustomWebViewClient());
    mWebView.setScrollBarStyle(View.SCROLLBARS_INSIDE_OVERLAY);

    mWebView.loadUrl("https://m.pandora.net/es-es/products/bracelets/556000");

}

private class MyCustomWebViewClient extends WebViewClient {
    @Override
    public boolean shouldOverrideUrlLoading(WebView view, String url) {
        mCurrentUrl = url;
        Log.i("mCurrentUrl",""+mCurrentUrl);  
        view.loadUrl(url);

        return true;
    }
}
}

try this one...

Ganesh Pokale
  • 1,538
  • 1
  • 14
  • 28