org.mozilla.focus.webkit.FocusWebViewClient.java Source code

Java tutorial

Introduction

Here is the source code for org.mozilla.focus.webkit.FocusWebViewClient.java

Source

/* -*- Mode: Java; c-basic-offset: 4; tab-width: 4; indent-tabs-mode: nil; -*-
 * This Source Code Form is subject to the terms of the Mozilla Public
 * License, v. 2.0. If a copy of the MPL was not distributed with this
 * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
package org.mozilla.focus.webkit;

import android.content.Context;
import android.content.res.Resources;
import android.graphics.Bitmap;
import android.net.Uri;
import android.net.http.SslError;
import android.support.v4.util.ArrayMap;
import android.webkit.SslErrorHandler;
import android.webkit.WebResourceRequest;
import android.webkit.WebResourceResponse;
import android.webkit.WebView;
import android.webkit.WebViewClient;

import org.mozilla.focus.R;
import org.mozilla.focus.utils.HtmlLoader;
import org.mozilla.focus.utils.SupportUtils;
import org.mozilla.focus.utils.UrlUtils;
import org.mozilla.focus.web.IWebView;

import java.util.Map;

/**
 * WebViewClient layer that handles browser specific WebViewClient functionality, such as error pages
 * and external URL handling.
 */
/* package */ class FocusWebViewClient extends TrackingProtectionWebViewClient {
    private final static String ERROR_PROTOCOL = "error:";

    private IWebView.Callback callback;
    private boolean errorReceived;

    public FocusWebViewClient(Context context) {
        super(context);
    }

    public void setCallback(IWebView.Callback callback) {
        this.callback = callback;
    }

    /**
     * Always ensure the following is wrapped in an anonymous function before execution.
     * (We don't wrap here, since this code might be run as part of a larger function, see
     * e.g. onLoadResource().)
     */
    private static final String CLEAR_VISITED_CSS = "let nSheets = document.styleSheets.length;"
            + "for (s=0; s < nSheets; s++) {" + "  let stylesheet = document.styleSheets[s];"
            + "  let nRules = stylesheet.cssRules ? stylesheet.cssRules.length : 0;" +
            // rules need to be removed by index. That modifies the whole list - it's easiest
            // to therefore process the list from the back, so that we don't need to care about
            // indexes changing after deletion (all indexes before the removed item are unchanged,
            // so by moving towards the start we'll always process all previously unprocessed items -
            // moving in the other direction we'd need to remember to process a given index
            // again which is more complicated).
            "  for (i = nRules - 1; i >= 0; i--) {" + "    let cssRule = stylesheet.cssRules[i];" +
            // Depending on style type, there might be no selector
            "    if (cssRule.selectorText && cssRule.selectorText.includes(':visited')) {"
            + "      let tokens = cssRule.selectorText.split(',');" + "      let j = tokens.length;"
            + "      while (j--) {" + "        if (tokens[j].includes(':visited')) {"
            + "          tokens.splice(j, 1);" + "        }" + "      }" + "      if (tokens.length == 0) {"
            + "        stylesheet.deleteRule(i);" + "      } else {"
            + "        cssRule.selectorText = tokens.join(',');" + "      }" + "    }" + "  }" + "}";

    @Override
    public void onLoadResource(WebView view, String url) {
        // We can't access the webview during shouldInterceptRequest(), however onLoadResource()
        // is called on the UI thread so we're allowed to do this now:
        view.evaluateJavascript("(function() {" +

                "function cleanupVisited() {" + CLEAR_VISITED_CSS + "}" +

                // Add an onLoad() listener so that we run the cleanup script every time
                // a <link>'d css stylesheet is loaded:
                "let links = document.getElementsByTagName('link');" + "for (i = 0; i < links.length; i++) {"
                + "  link = links[i];" + "  if (link.rel == 'stylesheet') {"
                + "    link.addEventListener('load', cleanupVisited, false);" + "  }" + "}" +

                "})();",

                null);

        super.onLoadResource(view, url);
    }

    @Override
    public WebResourceResponse shouldInterceptRequest(WebView view, WebResourceRequest request) {
        // Only update the user visible URL if:
        // 1. The purported site URL has actually been requested
        // 2. And it's being loaded for the main frame (and not a fake/hidden/iframe request)
        // Note also: shouldInterceptRequest() runs on a background thread, so we can't actually
        // query WebView.getURL().
        // We update the URL when loading has finished too (redirects can happen after a request has been
        // made in which case we don't get shouldInterceptRequest with the final URL), but this
        // allows us to update the URL during loading.
        if (request.isForMainFrame()) {

            // WebView will always add a trailing / to the request URL, but currentPageURL may or may
            // not have a trailing URL (usually no trailing / when a link is entered via UrlInputFragment),
            // hence we do a somewhat convoluted test:
            final String requestURL = request.getUrl().toString();
            if (UrlUtils.urlsMatchExceptForTrailingSlash(currentPageURL, requestURL)) {
                view.post(new Runnable() {
                    @Override
                    public void run() {
                        if (callback != null) {
                            callback.onURLChanged(currentPageURL);
                        }
                    }
                });
            }
        }

        return super.shouldInterceptRequest(view, request);
    }

    @Override
    public void onPageStarted(WebView view, String url, Bitmap favicon) {
        if (errorReceived) {
            // When dealing with error pages, webkit sometimes sends onPageStarted()
            // without a matching onPageFinished(). We hack around that by using
            // a flag to ignore the first onPageStarted() after onReceivedError() has
            // been called. (The usual chain is: onPageStarted(url), onReceivedError(url),
            // onPageFinished(url), onPageStarted(url), finally and only sometimes: onPageFinished().
            // Since the final onPageFinished isn't guaranteed (and we know we're showing an error
            // page already), we don't need to send the onPageStarted() callback a second time anyway.
            errorReceived = false;
        } else if (callback != null) {
            callback.onPageStarted(url);
        }

        super.onPageStarted(view, url, favicon);
    }

    @Override
    public void onPageFinished(WebView view, final String url) {
        if (callback != null) {
            callback.onPageFinished(view.getCertificate() != null);
            // The URL which is supplied in onPageFinished() could be fake (see #301), but webview's
            // URL is always correct _except_ for error pages
            final String viewURL = view.getUrl();
            if (!UrlUtils.isInternalErrorURL(viewURL)) {
                callback.onURLChanged(view.getUrl());
            }
        }
        super.onPageFinished(view, url);

        view.evaluateJavascript("(function() {" +

                CLEAR_VISITED_CSS +

                "})();",

                null);
    }

    @Override
    public boolean shouldOverrideUrlLoading(WebView view, String url) {
        if (url.equals("focusabout:")) {
            loadAbout(view);
            return true;
        }

        // shouldOverrideUrlLoading() is called for both the main frame, and iframes.
        // That can get problematic if an iframe tries to load an unsupported URL.
        // We then try to either handle that URL (ask to open relevant app), or extract
        // a fallback URL from the intent (or worst case fall back to an error page). In the
        // latter 2 cases, we explicitly open the fallback/error page in the main view.
        // Websites probably shouldn't use unsupported URLs in iframes, but we do need to
        // be careful to handle all valid schemes here to avoid redirecting due to such an iframe
        // (e.g. we don't want to redirect to a data: URI just because an iframe shows such
        // a URI).
        // (The API 24+ version of shouldOverrideUrlLoading() lets us determine whether
        // the request is for the main frame, and if it's not we could then completely
        // skip the external URL handling.)
        final Uri uri = Uri.parse(url);
        if (!UrlUtils.isSupportedProtocol(uri.getScheme()) && callback != null && callback.handleExternalUrl(url)) {
            return true;
        }

        return super.shouldOverrideUrlLoading(view, url);
    }

    @Override
    public void onReceivedSslError(WebView view, SslErrorHandler handler, SslError error) {
        handler.cancel();

        // Webkit can try to load the favicon for a bad page when you set a new URL. If we then
        // loadErrorPage() again, webkit tries to load the favicon again. We end up in onReceivedSSlError()
        // again, and we get an infinite loop of reloads (we also erroneously show the favicon URL
        // in the toolbar, but that's less noticeable). Hence we check whether this error is from
        // the desired page, or a page resource:
        if (error.getUrl().equals(currentPageURL)) {
            ErrorPage.loadErrorPage(view, error.getUrl(), WebViewClient.ERROR_FAILED_SSL_HANDSHAKE);
        }
    }

    @Override
    public void onReceivedError(final WebView webView, int errorCode, final String description, String failingUrl) {
        errorReceived = true;

        // This is a hack: onReceivedError(WebView, WebResourceRequest, WebResourceError) is API 23+ only,
        // - the WebResourceRequest would let us know if the error affects the main frame or not. As a workaround
        // we just check whether the failing URL is the current URL, which is enough to detect an error
        // in the main frame.

        // WebView swallows odd pages and only sends an error (i.e. it doesn't go through the usual
        // shouldOverrideUrlLoading), so we need to handle special pages here:
        // about: urls are even more odd: webview doesn't tell us _anything_, hence the use of
        // a different prefix:
        if (failingUrl.startsWith(ERROR_PROTOCOL)) {
            // format: error:<error_code>
            final int errorCodePosition = ERROR_PROTOCOL.length();
            final String errorCodeString = failingUrl.substring(errorCodePosition);

            int desiredErrorCode;
            try {
                desiredErrorCode = Integer.parseInt(errorCodeString);

                if (!ErrorPage.supportsErrorCode(desiredErrorCode)) {
                    // I don't think there's any good way of showing an error if there's an error
                    // in requesting an error page?
                    desiredErrorCode = WebViewClient.ERROR_BAD_URL;
                }
            } catch (final NumberFormatException e) {
                desiredErrorCode = WebViewClient.ERROR_BAD_URL;
            }
            ErrorPage.loadErrorPage(webView, failingUrl, desiredErrorCode);
            return;
        }

        // The API 23+ version also return a *slightly* more usable description, via WebResourceError.getError();
        // e.g.. "There was a network error.", whereas this version provides things like "net::ERR_NAME_NOT_RESOLVED"
        if (failingUrl.equals(currentPageURL) && ErrorPage.supportsErrorCode(errorCode)) {
            ErrorPage.loadErrorPage(webView, currentPageURL, errorCode);
            return;
        }

        super.onReceivedError(webView, errorCode, description, failingUrl);
    }

    private void loadAbout(final WebView webView) {
        final Resources resources = webView.getContext().getResources();

        final Map<String, String> substitutionMap = new ArrayMap<>();
        final String appName = webView.getContext().getResources().getString(R.string.app_name);
        final String learnMoreURL = SupportUtils.getManifestoURL();

        final String aboutContent = resources.getString(R.string.about_content, appName, learnMoreURL);
        substitutionMap.put("%about-content%", aboutContent);

        final String wordmark = HtmlLoader.loadPngAsDataURI(webView.getContext(), R.drawable.wordmark);
        substitutionMap.put("%wordmark%", wordmark);

        final String data = HtmlLoader.loadResourceFile(webView.getContext(), R.raw.about, substitutionMap);
        // We use a file:/// base URL so that we have the right origin to load file:/// css and
        // image resources.
        webView.loadDataWithBaseURL("file:///android_res/raw/about.html", data, "text/html", "UTF-8", null);
    }

}