This repository has been archived by the owner on Jul 5, 2021. It is now read-only.
-
Notifications
You must be signed in to change notification settings - Fork 94
/
FocusWebViewClient.java
289 lines (246 loc) · 12.2 KB
/
FocusWebViewClient.java
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
/* -*- 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.graphics.Bitmap;
import android.net.Uri;
import android.net.http.SslError;
import androidx.annotation.NonNull;
import android.text.TextUtils;
import android.webkit.HttpAuthHandler;
import android.webkit.SslErrorHandler;
import android.webkit.WebResourceRequest;
import android.webkit.WebResourceResponse;
import android.webkit.WebView;
import android.webkit.WebViewClient;
import org.mozilla.focus.utils.AppConstants;
import org.mozilla.focus.utils.Settings;
import org.mozilla.focus.utils.SupportUtils;
import org.mozilla.rocket.tabs.TabViewClient;
import org.mozilla.urlutils.UrlUtils;
/**
* 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 TabViewClient viewClient;
private static final String GOOGLE_OAUTH2_PREFIX = "https://accounts.google.com/o/oauth2/";
private static final String IGNORE_GOOGLE_WEBVIEW_BLOCKING_PARAM = "&suppress_webview_warning=true";
private WebViewDebugOverlay debugOverlay;
private ErrorPageDelegate errorPageDelegate;
FocusWebViewClient(Context context) {
super(context);
}
public void setViewClient(TabViewClient callback) {
this.viewClient = callback;
}
@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 (currentPageURL != null && UrlUtils.urlsMatchExceptForTrailingSlash(currentPageURL, requestURL)) {
view.post(new Runnable() {
@Override
public void run() {
if (viewClient != null) {
viewClient.onURLChanged(currentPageURL);
}
}
});
}
}
return super.shouldInterceptRequest(view, request);
}
@Override
public void onPageStarted(WebView view, String url, Bitmap favicon) {
if (viewClient != null) {
viewClient.updateFailingUrl(url, false);
viewClient.onPageStarted(url);
}
if (this.errorPageDelegate != null) {
this.errorPageDelegate.onPageStarted();
}
this.debugOverlay.recordLifecycle("onPageStarted:" + url, true);
super.onPageStarted(view, url, favicon);
}
@Override
public void onPageFinished(WebView view, final String url) {
if (viewClient != null) {
viewClient.onPageFinished(view.getCertificate() != null);
}
super.onPageFinished(view, url);
this.debugOverlay.updateHistory();
this.debugOverlay.recordLifecycle("onPageFinished:" + url, false);
}
private static boolean shouldOverrideInternalPages(WebView webView, String url) {
if (SupportUtils.isTemplateSupportPages(url)) {
SupportUtils.loadSupportPages(webView, url);
return true;
}
return false;
}
@Override
public boolean shouldOverrideUrlLoading(WebView view, String url) {
view.getSettings().setLoadsImagesAutomatically(true);
if (url == null) {
// in case of null url, we won't crash app in release build
if (AppConstants.isDevBuild()) {
throw new RuntimeException("Got null url in FocsWebViewClient.shouldOverrideUrlLoading");
} else {
return super.shouldOverrideUrlLoading(view, "");
}
}
// A workaround for Google SSO since they're blocking us even when we had changed the agent
if (url.startsWith(GOOGLE_OAUTH2_PREFIX) && !url.endsWith(IGNORE_GOOGLE_WEBVIEW_BLOCKING_PARAM)) {
view.loadUrl(url.concat(IGNORE_GOOGLE_WEBVIEW_BLOCKING_PARAM));
return true;
}
if (shouldOverrideInternalPages(view, url)) {
return true;
}
// Allow pages to blank themselves by loading about:blank. While it's a little incorrect to let pages
// access our internal URLs, Chrome allows loads to about:blank and, to ensure our behavior conforms
// to the behavior that most of the web is developed against, we do too.
if (url.equals(SupportUtils.BLANK_URL)) {
return false;
}
// 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()) &&
viewClient != null &&
viewClient.handleExternalUrl(url)) {
return true;
}
view.getSettings().setLoadsImagesAutomatically(!Settings.getInstance(view.getContext()).shouldBlockImages());
return super.shouldOverrideUrlLoading(view, url);
}
@Override
public void onReceivedSslError(WebView view, SslErrorHandler handler, SslError error) {
super.onReceivedSslError(view, handler, error);
this.debugOverlay.recordLifecycle("onReceivedSslError:" + error.getUrl(), false);
// 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)) {
if (this.errorPageDelegate != null) {
this.errorPageDelegate.onReceivedSslError(view, handler, error);
}
}
}
@Override
public void onReceivedError(final WebView webView, int errorCode,
final String description, String failingUrl) {
if (viewClient != null) {
viewClient.updateFailingUrl(failingUrl, true);
}
this.debugOverlay.recordLifecycle("onReceivedError:" + failingUrl, false);
// 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;
}
if (this.errorPageDelegate != null) {
this.errorPageDelegate.onReceivedError(webView, desiredErrorCode, description, failingUrl);
}
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)) {
if (this.errorPageDelegate != null) {
this.errorPageDelegate.onReceivedError(webView, errorCode, description, failingUrl);
}
return;
}
super.onReceivedError(webView, errorCode, description, failingUrl);
}
@Override
public void onReceivedHttpError(WebView view, WebResourceRequest request, WebResourceResponse errorResponse) {
super.onReceivedHttpError(view, request, errorResponse);
Uri url = request.getUrl();
if (url != null) {
this.debugOverlay.recordLifecycle("onReceivedHttpError:" + url.toString(), false);
if (request.isForMainFrame() && TextUtils.equals(currentPageURL, url.toString())) {
if (this.errorPageDelegate != null) {
this.errorPageDelegate.onReceivedHttpError(view, request, errorResponse);
}
}
}
}
@Override
public void doUpdateVisitedHistory(WebView view, String url, boolean isReload) {
if (viewClient != null) {
viewClient.onURLChanged(view.getUrl());
}
super.doUpdateVisitedHistory(view, url, isReload);
this.debugOverlay.updateHistory();
}
@Override
public void onReceivedHttpAuthRequest(WebView view, HttpAuthHandler handler, String host, String realm) {
final TabViewClient.HttpAuthCallback httpAuthCallback = new TabViewClient.HttpAuthCallback() {
@Override
public void proceed(String username, String password) {
handler.proceed(username, password);
}
@Override
public void cancel() {
handler.cancel();
}
};
if (viewClient != null) {
viewClient.onHttpAuthRequest(httpAuthCallback, host, realm);
}
}
final void setDebugOverlay(@NonNull WebViewDebugOverlay overlay) {
this.debugOverlay = overlay;
}
public void setErrorPageDelegate(ErrorPageDelegate errorPageDelegate) {
this.errorPageDelegate = errorPageDelegate;
}
}