Skip to content

Commit

Permalink
[focus] Show error pages on various specific errors (mozilla-mobile/f…
Browse files Browse the repository at this point in the history
  • Loading branch information
ahunt committed Mar 14, 2017
1 parent e97a814 commit 1909780
Show file tree
Hide file tree
Showing 3 changed files with 258 additions and 0 deletions.
59 changes: 59 additions & 0 deletions mobile/android/focus-android/app/src/main/res/values/strings.xml
Original file line number Diff line number Diff line change
Expand Up @@ -105,11 +105,70 @@
<string name="firstrun_footer">To be clear, sites and service you use can still know where you\'ve been.</string>

<string name="your_rights">Your Rights</string>

<!-- This string is shown when the user has clicked a link that needs to be opened in a different app.
Argument 2 is the external app name, argument 1 is our name (i.e. Firefox Focus, or Firefox Klar). -->
<string name="external_app_prompt">This link will open in %2$s. Are you sure you want to exit %1$s?</string>
<string name="external_app_prompt_no_app_title">No app to handle link</string>
<string name="external_app_prompt_no_app">None of the apps on your device are able to open this link. Do you want to exit %1$s and open %2$s to find a suitable app?</string>
<!-- This label is shown above a list of apps that can be used to open a given link -->
<string name="external_multiple_apps_matched_exit">Exit Private Browsing?</string>

<!-- The page html title (i.e. the <title> tag content) -->
<string name="errorpage_title">Problem loading page</string>
<string name="errorpage_refresh">Try Again</string>

<string name="error_connectionfailure_title">Unable to connect</string>
<string name="error_connectionfailure_message">
<![CDATA[
<ul>
<li>The site could be temporarily unavailable or too busy. Try again in a few moments.</li>
<li>If you are unable to load any pages, check your mobile device’s data or Wi-Fi connection.</li>
</ul>
]]>
</string>

<string name="error_connect_title">@string/error_connectionfailure_title</string>
<string name="error_connect_message">@string/error_connectionfailure_message</string>

<string name="error_timeout_title">The connection timed out</string>
<string name="error_timeout_message">@string/error_connectionfailure_message</string>

<string name="error_hostLookup_title">Server not found</string>
<string name="error_hostLookup_message"><![CDATA[
<ul>
<li>Check the address for typing errors such as
<strong>ww</strong>.example.com instead of
<strong>www</strong>.example.com</li>
<li>If you are unable to load any pages, check your device’s data or Wi-Fi connection.</li>
</ul>
]]></string>


<string name="error_malformedURI_title">The address isn’t valid</string>
<string name="error_malformedURI_message"><![CDATA[
<ul>
<li>Web addresses are usually written like <strong>http://www.example.com/</strong></li>
<li>Make sure that you’re using forward slashes (i.e. <strong>/</strong>).</li>
</ul>
]]></string>

<string name="error_redirectLoop_title">The page isn’t redirecting properly</string>
<string name="error_redirectLoop_message"><![CDATA[<ul><li>This problem can sometimes be caused by disabling or refusing to accept cookies.</li></ul>]]></string>

<string name="error_unsupportedprotocol_title">The address wasn’t understood</string>
<string name="error_unsupportedprotocol_message"><![CDATA[<ul><li>You might need to install other software to open this address.</li></ul>]]></string>

<string name="error_sslhandshake_title">Secure Connection Failed</string>
<string name="error_sslhandshake_message"><![CDATA[
<ul>
<li>This could be a problem with the server’s configuration, or it could be
someone trying to impersonate the server.</li>
<li>If you have connected to this server successfully in the past, the error may
be temporary, and you can try again later.</li>
</ul>
]]></string>

<string name="error_generic_title">Oops.</string>
<string name="error_generic_message">We can’t load this page for some reason.</string>
</resources>
Original file line number Diff line number Diff line change
@@ -0,0 +1,177 @@
/* -*- Mode: Java; c-basic-offset: 4; tab-width: 20; 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.support.annotation.NonNull;
import android.support.annotation.Nullable;
import android.support.annotation.RawRes;
import android.support.v4.util.ArrayMap;
import android.support.v4.util.Pair;
import android.webkit.WebView;
import android.webkit.WebViewClient;

import org.mozilla.focus.R;

import java.io.BufferedReader;
import java.io.IOException;
import java.io.InputStream;
import java.io.InputStreamReader;
import java.util.HashMap;
import java.util.Map;

public class ErrorPage {

private static final HashMap<Integer, Pair<Integer, Integer>> errorDescriptionMap;

static {
errorDescriptionMap = new HashMap<>();

// Chromium's mapping (internal error code, to Android WebView error code) is described at:
// https://chromium.googlesource.com/chromium/src.git/+/master/android_webview/java/src/org/chromium/android_webview/ErrorCodeConversionHelper.java

errorDescriptionMap.put(WebViewClient.ERROR_UNKNOWN,
new Pair<>(R.string.error_connectionfailure_title, R.string.error_connectionfailure_message));

// This is probably the most commonly shown error. If there's no network, we inevitably
// show this.
errorDescriptionMap.put(WebViewClient.ERROR_HOST_LOOKUP,
new Pair<>(R.string.error_hostLookup_title, R.string.error_hostLookup_message));

// WebViewClient.ERROR_UNSUPPORTED_AUTH_SCHEME
// TODO: we don't actually handle this in firefox - does this happen in real life?

// WebViewClient.ERROR_AUTHENTICATION
// TODO: there's no point in implementing this until we actually support http auth (#159)

errorDescriptionMap.put(WebViewClient.ERROR_CONNECT,
new Pair<>(R.string.error_connect_title, R.string.error_connect_message));

// It's unclear what this actually means - it's not well documented. Based on looking at
// ErrorCodeConversionHelper this could happen if networking is disabled during load, in which
// case the generic error is good enough:
errorDescriptionMap.put(WebViewClient.ERROR_IO,
new Pair<>(R.string.error_connectionfailure_title, R.string.error_connectionfailure_message));

errorDescriptionMap.put(WebViewClient.ERROR_TIMEOUT,
new Pair<>(R.string.error_timeout_title, R.string.error_timeout_message));

errorDescriptionMap.put(WebViewClient.ERROR_REDIRECT_LOOP,
new Pair<>(R.string.error_redirectLoop_title, R.string.error_redirectLoop_message));

// We already try to handle external URLs if possible (i.e. we offer to open the corresponding
// app, if available for a given scheme). If we end up here that means no app exists.
// We could consider showing an "open google play" link here, but ultimately it's hard
// to know whether that's the right step, especially if there are no good apps for actually
// handling such a protocol there - moreover there doesn't seem to be a good way to search
// google play for apps supporting a given scheme.
errorDescriptionMap.put(WebViewClient.ERROR_UNSUPPORTED_SCHEME,
new Pair<>(R.string.error_unsupportedprotocol_title, R.string.error_unsupportedprotocol_message));

errorDescriptionMap.put(WebViewClient.ERROR_FAILED_SSL_HANDSHAKE,
new Pair<>(R.string.error_sslhandshake_title, R.string.error_sslhandshake_message));

errorDescriptionMap.put(WebViewClient.ERROR_BAD_URL,
new Pair<>(R.string.error_malformedURI_title, R.string.error_malformedURI_message));

// We don't support file:/// URLs, so we shouldn't have to handle errors opening files either
// WebViewClient.ERROR_FILE;
// WebViewClient.ERROR_FILE_NOT_FOUND;

// Seems to be an indication of OOM, insufficient resources, or too many queued DNS queries
errorDescriptionMap.put(WebViewClient.ERROR_TOO_MANY_REQUESTS,
new Pair<>(R.string.error_generic_title, R.string.error_generic_message));
}

public static boolean supportsErrorCode(final int errorCode) {
return (errorDescriptionMap.get(errorCode) != null);
}

/**
* Load a given resource file into a String.
*
* @param substitutionTable A table of substitions, e.g. %shortMessage% -> "Error loading page..."
* @return The file content, with all substitutions having being made.
*/
private static String loadResourceFile(@NonNull final Context context,
@NonNull final @RawRes int resourceID,
@Nullable final Map<String, String> substitutionTable) {

BufferedReader fileReader = null;

try {
final InputStream fileStream = context.getResources().openRawResource(resourceID);
fileReader = new BufferedReader(new InputStreamReader(fileStream));

final StringBuilder outputBuffer = new StringBuilder();

String line;
while ((line = fileReader.readLine()) != null) {
if (substitutionTable != null) {
for (final Map.Entry<String, String> entry : substitutionTable.entrySet()) {
line = line.replace(entry.getKey(), entry.getValue());
}
}

outputBuffer.append(line);
}

return outputBuffer.toString();
} catch (final IOException e) {
throw new IllegalStateException("Unable to load error page data");
} finally {
try {
if (fileReader != null) {
fileReader.close();
}
} catch (IOException e) {
// There's pretty much nothing we can do here. It doesn't seem right to crash
// just because we couldn't close a file?
}
}
}

public static void loadErrorPage(final WebView webView, final String desiredURL, final int errorCode) {
final Pair<Integer, Integer> errorResourceIDs = errorDescriptionMap.get(errorCode);

if (errorResourceIDs == null) {
throw new IllegalArgumentException("Cannot load error description for unsupported errorcode=" + errorCode);
}

// This is quite hacky: ideally we'd just load the css file directly using a '<link rel="stylesheet"'.
// However webkit thinks it's still loading the original page, which can be an https:// page.
// If mixed content blocking is enabled (which is probably what we want in Focus), then webkit
// will block file:///android_res/ links from being loaded - which blocks our css from being loaded.
// We could hack around that by enabling mixed content when loading an error page (and reenabling it
// once that's loaded), but doing that correctly and reliably isn't particularly simple. Loading
// the css data and stuffing it into our html is much simpler, especially since we're already doing
// string substitutions.
// As an added bonus: file:/// URIs are broken if the app-ID != app package, see:
// https://code.google.com/p/android/issues/detail?id=211768 (this breaks loading css via file:///
// references when running debug builds, and probably klar too) - which means this wouldn't
// be possible even if we hacked around the mixed content issues.
final String cssString = loadResourceFile(webView.getContext(), R.raw.errorpage_style, null);

final Map<String, String> substitutionMap = new ArrayMap<>();

final Resources resources = webView.getContext().getResources();

substitutionMap.put("%page-title%", resources.getString(R.string.errorpage_title));
substitutionMap.put("%button%", resources.getString(R.string.errorpage_refresh));

substitutionMap.put("%messageShort%", resources.getString(errorResourceIDs.first));
substitutionMap.put("%messageLong%", resources.getString(errorResourceIDs.second, desiredURL));

substitutionMap.put("%css%", cssString);

final String errorPage = loadResourceFile(webView.getContext(), R.raw.errorpage, substitutionMap);

// We could load the raw html file directly into the webview using a file:///android_res/
// URI - however we'd then need to do some JS hacking to do our String substitutions. Moreover
// we'd have to deal with the mixed-content issues detailed above in that case.
webView.loadDataWithBaseURL(desiredURL, errorPage, "text/html", "UTF8", desiredURL);
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -6,8 +6,11 @@

import android.content.Context;
import android.graphics.Bitmap;
import android.net.http.SslError;
import android.os.AsyncTask;
import android.support.annotation.WorkerThread;
import android.webkit.HttpAuthHandler;
import android.webkit.SslErrorHandler;
import android.webkit.WebResourceRequest;
import android.webkit.WebResourceResponse;
import android.webkit.WebView;
Expand Down Expand Up @@ -104,4 +107,23 @@ public boolean shouldOverrideUrlLoading(WebView view, String url) {
return super.shouldOverrideUrlLoading(view, url);
}

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

// 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.

// 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);
}
}

0 comments on commit 1909780

Please sign in to comment.