blob: 91c04fa65e24d262cf0aedd596edabb69f749f91 [file] [log] [blame]
Paul Jensen869868be2014-05-15 10:33:05 -04001/*
2 * Copyright (C) 2014 The Android Open Source Project
3 *
4 * Licensed under the Apache License, Version 2.0 (the "License");
5 * you may not use this file except in compliance with the License.
6 * You may obtain a copy of the License at
7 *
8 * http://www.apache.org/licenses/LICENSE-2.0
9 *
10 * Unless required by applicable law or agreed to in writing, software
11 * distributed under the License is distributed on an "AS IS" BASIS,
12 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13 * See the License for the specific language governing permissions and
14 * limitations under the License.
15 */
16
17package com.android.captiveportallogin;
18
19import android.app.Activity;
Paul Jensen88eb0fa2014-10-02 13:43:39 -040020import android.app.LoadedApk;
21import android.content.Context;
Paul Jensen869868be2014-05-15 10:33:05 -040022import android.content.Intent;
23import android.graphics.Bitmap;
Paul Jensen49e3edf2015-05-22 10:50:39 -040024import android.net.CaptivePortal;
Paul Jensen869868be2014-05-15 10:33:05 -040025import android.net.ConnectivityManager;
Paul Jensen8df099d2014-09-26 15:19:17 -040026import android.net.ConnectivityManager.NetworkCallback;
Paul Jensen869868be2014-05-15 10:33:05 -040027import android.net.Network;
Paul Jensen8df099d2014-09-26 15:19:17 -040028import android.net.NetworkCapabilities;
29import android.net.NetworkRequest;
Paul Jensen88eb0fa2014-10-02 13:43:39 -040030import android.net.Proxy;
Paul Jensen88eb0fa2014-10-02 13:43:39 -040031import android.net.Uri;
Erik Klinec43d2f52018-03-21 07:18:33 -070032import android.net.dns.ResolvUtil;
Paul Jensenfc8022f2014-12-09 15:18:40 -050033import android.net.http.SslError;
Chalard Jean2dcccbc2018-04-12 11:52:37 +090034import android.net.wifi.WifiInfo;
Hugo Benichi12df4652017-06-17 13:36:35 +090035import android.os.Build;
Paul Jensen869868be2014-05-15 10:33:05 -040036import android.os.Bundle;
37import android.provider.Settings;
Chalard Jean94bc48f2018-03-09 22:28:51 +090038import android.support.v4.widget.SwipeRefreshLayout;
Adam Newmanc1b4efb2018-03-27 10:10:43 -070039import android.text.TextUtils;
Paul Jensen88eb0fa2014-10-02 13:43:39 -040040import android.util.ArrayMap;
41import android.util.Log;
Paul Jensen65636fb2015-05-06 14:40:59 -040042import android.util.TypedValue;
Hugo Benichi04d78602017-07-19 21:20:53 +090043import android.util.SparseArray;
Paul Jensen869868be2014-05-15 10:33:05 -040044import android.view.Menu;
45import android.view.MenuItem;
Hugo Benichia2066492017-05-17 09:26:30 +090046import android.view.View;
Lorenzo Colittic3c95ba2018-03-26 01:32:33 +090047import android.webkit.CookieManager;
Paul Jensenfc8022f2014-12-09 15:18:40 -050048import android.webkit.SslErrorHandler;
Paul Jensen869868be2014-05-15 10:33:05 -040049import android.webkit.WebChromeClient;
50import android.webkit.WebSettings;
51import android.webkit.WebView;
Hugo Benichi04d78602017-07-19 21:20:53 +090052import android.webkit.WebView;
Paul Jensen869868be2014-05-15 10:33:05 -040053import android.webkit.WebViewClient;
Paul Jensen8f333f12014-08-05 22:52:16 -040054import android.widget.ProgressBar;
Paul Jensen5344a4a2015-05-06 07:39:36 -040055import android.widget.TextView;
Paul Jensen869868be2014-05-15 10:33:05 -040056
Hugo Benichi9e8ab432017-06-05 14:52:24 +090057import com.android.internal.logging.MetricsLogger;
58import com.android.internal.logging.nano.MetricsProto.MetricsEvent;
59
Paul Jensen869868be2014-05-15 10:33:05 -040060import java.io.IOException;
61import java.net.HttpURLConnection;
62import java.net.MalformedURLException;
63import java.net.URL;
64import java.lang.InterruptedException;
Paul Jensen88eb0fa2014-10-02 13:43:39 -040065import java.lang.reflect.Field;
66import java.lang.reflect.Method;
Hugo Benichi12df4652017-06-17 13:36:35 +090067import java.util.Objects;
Paul Jensen65636fb2015-05-06 14:40:59 -040068import java.util.Random;
Hugo Benichia173a632017-06-17 12:47:33 +090069import java.util.concurrent.atomic.AtomicBoolean;
Paul Jensen869868be2014-05-15 10:33:05 -040070
71public class CaptivePortalLoginActivity extends Activity {
Hugo Benichi7f086e12016-12-06 15:36:30 +090072 private static final String TAG = CaptivePortalLoginActivity.class.getSimpleName();
73 private static final boolean DBG = true;
Hugo Benichi60d5f462017-06-02 10:12:09 +090074 private static final boolean VDBG = false;
Hugo Benichi7f086e12016-12-06 15:36:30 +090075
Paul Jensen869868be2014-05-15 10:33:05 -040076 private static final int SOCKET_TIMEOUT_MS = 10000;
77
Hugo Benichi9e8ab432017-06-05 14:52:24 +090078 private enum Result {
79 DISMISSED(MetricsEvent.ACTION_CAPTIVE_PORTAL_LOGIN_RESULT_DISMISSED),
80 UNWANTED(MetricsEvent.ACTION_CAPTIVE_PORTAL_LOGIN_RESULT_UNWANTED),
81 WANTED_AS_IS(MetricsEvent.ACTION_CAPTIVE_PORTAL_LOGIN_RESULT_WANTED_AS_IS);
82
83 final int metricsEvent;
84 Result(int metricsEvent) { this.metricsEvent = metricsEvent; }
85 };
Paul Jensen869868be2014-05-15 10:33:05 -040086
Hugo Benichi7f086e12016-12-06 15:36:30 +090087 private URL mUrl;
Hugo Benichi2c021972016-12-14 08:23:40 +090088 private String mUserAgent;
Paul Jensen25a217c2015-02-27 22:55:47 -050089 private Network mNetwork;
Paul Jensen49e3edf2015-05-22 10:50:39 -040090 private CaptivePortal mCaptivePortal;
Paul Jensen8df099d2014-09-26 15:19:17 -040091 private NetworkCallback mNetworkCallback;
Paul Jensen25a217c2015-02-27 22:55:47 -050092 private ConnectivityManager mCm;
Paul Jensen65636fb2015-05-06 14:40:59 -040093 private boolean mLaunchBrowser = false;
Paul Jensene836b682015-05-19 12:30:56 -040094 private MyWebViewClient mWebViewClient;
Chalard Jean94bc48f2018-03-09 22:28:51 +090095 private SwipeRefreshLayout mSwipeRefreshLayout;
Hugo Benichia173a632017-06-17 12:47:33 +090096 // Ensures that done() happens once exactly, handling concurrent callers with atomic operations.
97 private final AtomicBoolean isDone = new AtomicBoolean(false);
Paul Jensen869868be2014-05-15 10:33:05 -040098
99 @Override
100 protected void onCreate(Bundle savedInstanceState) {
101 super.onCreate(savedInstanceState);
Hugo Benichi9e8ab432017-06-05 14:52:24 +0900102
103 logMetricsEvent(MetricsEvent.ACTION_CAPTIVE_PORTAL_LOGIN_ACTIVITY);
104
Paul Jensen25a217c2015-02-27 22:55:47 -0500105 mCm = ConnectivityManager.from(this);
Paul Jensen25a217c2015-02-27 22:55:47 -0500106 mNetwork = getIntent().getParcelableExtra(ConnectivityManager.EXTRA_NETWORK);
Paul Jensen49e3edf2015-05-22 10:50:39 -0400107 mCaptivePortal = getIntent().getParcelableExtra(ConnectivityManager.EXTRA_CAPTIVE_PORTAL);
Hugo Benichiec88fd62017-03-07 15:10:03 +0900108 mUserAgent =
109 getIntent().getStringExtra(ConnectivityManager.EXTRA_CAPTIVE_PORTAL_USER_AGENT);
Hugo Benichi7f086e12016-12-06 15:36:30 +0900110 mUrl = getUrl();
111 if (mUrl == null) {
112 // getUrl() failed to parse the url provided in the intent: bail out in a way that
113 // at least provides network access.
114 done(Result.WANTED_AS_IS);
115 return;
116 }
117 if (DBG) {
118 Log.d(TAG, String.format("onCreate for %s", mUrl.toString()));
119 }
Paul Jensen869868be2014-05-15 10:33:05 -0400120
Paul Jensene0bef712014-12-10 15:12:18 -0500121 // Also initializes proxy system properties.
Paul Jensen25a217c2015-02-27 22:55:47 -0500122 mCm.bindProcessToNetwork(mNetwork);
Erik Klinec43d2f52018-03-21 07:18:33 -0700123 mCm.setProcessDefaultNetworkForHostResolution(
124 ResolvUtil.getNetworkWithUseLocalNameserversFlag(mNetwork));
Paul Jensen88eb0fa2014-10-02 13:43:39 -0400125
126 // Proxy system properties must be initialized before setContentView is called because
127 // setContentView initializes the WebView logic which in turn reads the system properties.
128 setContentView(R.layout.activity_captive_portal_login);
129
Paul Jensen8df099d2014-09-26 15:19:17 -0400130 // Exit app if Network disappears.
Paul Jensen25a217c2015-02-27 22:55:47 -0500131 final NetworkCapabilities networkCapabilities = mCm.getNetworkCapabilities(mNetwork);
Paul Jensen8df099d2014-09-26 15:19:17 -0400132 if (networkCapabilities == null) {
Paul Jensen6a776c832016-07-19 13:19:39 -0400133 finishAndRemoveTask();
Paul Jensen8df099d2014-09-26 15:19:17 -0400134 return;
135 }
136 mNetworkCallback = new NetworkCallback() {
137 @Override
138 public void onLost(Network lostNetwork) {
Paul Jensen25a217c2015-02-27 22:55:47 -0500139 if (mNetwork.equals(lostNetwork)) done(Result.UNWANTED);
Paul Jensen8df099d2014-09-26 15:19:17 -0400140 }
141 };
142 final NetworkRequest.Builder builder = new NetworkRequest.Builder();
143 for (int transportType : networkCapabilities.getTransportTypes()) {
144 builder.addTransportType(transportType);
145 }
Paul Jensen25a217c2015-02-27 22:55:47 -0500146 mCm.registerNetworkCallback(builder.build(), mNetworkCallback);
Paul Jensen869868be2014-05-15 10:33:05 -0400147
Hugo Benichia2066492017-05-17 09:26:30 +0900148 getActionBar().setDisplayShowHomeEnabled(false);
149 getActionBar().setElevation(0); // remove shadow
150 getActionBar().setTitle(getHeaderTitle());
151 getActionBar().setSubtitle("");
152
153 final WebView webview = getWebview();
154 webview.clearCache(true);
Lorenzo Colittic3c95ba2018-03-26 01:32:33 +0900155 CookieManager.getInstance().setAcceptThirdPartyCookies(webview, true);
Hugo Benichia2066492017-05-17 09:26:30 +0900156 WebSettings webSettings = webview.getSettings();
Paul Jensen869868be2014-05-15 10:33:05 -0400157 webSettings.setJavaScriptEnabled(true);
Lorenzo Colittib55bf382016-10-21 18:41:25 +0900158 webSettings.setMixedContentMode(WebSettings.MIXED_CONTENT_COMPATIBILITY_MODE);
Hugo Benichi94c5fb32017-05-11 09:58:14 +0900159 webSettings.setUseWideViewPort(true);
160 webSettings.setLoadWithOverviewMode(true);
161 webSettings.setSupportZoom(true);
162 webSettings.setBuiltInZoomControls(true);
163 webSettings.setDisplayZoomControls(false);
Paul Jensene836b682015-05-19 12:30:56 -0400164 mWebViewClient = new MyWebViewClient();
Hugo Benichia2066492017-05-17 09:26:30 +0900165 webview.setWebViewClient(mWebViewClient);
166 webview.setWebChromeClient(new MyWebChromeClient());
Paul Jensen88eb0fa2014-10-02 13:43:39 -0400167 // Start initial page load so WebView finishes loading proxy settings.
168 // Actual load of mUrl is initiated by MyWebViewClient.
Hugo Benichia2066492017-05-17 09:26:30 +0900169 webview.loadData("", "text/html", null);
Chalard Jean94bc48f2018-03-09 22:28:51 +0900170
171 mSwipeRefreshLayout = findViewById(R.id.swipe_refresh);
172 mSwipeRefreshLayout.setOnRefreshListener(() -> {
173 webview.reload();
174 mSwipeRefreshLayout.setRefreshing(true);
175 });
176
Paul Jensen88eb0fa2014-10-02 13:43:39 -0400177 }
178
179 // Find WebView's proxy BroadcastReceiver and prompt it to read proxy system properties.
180 private void setWebViewProxy() {
181 LoadedApk loadedApk = getApplication().mLoadedApk;
182 try {
183 Field receiversField = LoadedApk.class.getDeclaredField("mReceivers");
184 receiversField.setAccessible(true);
185 ArrayMap receivers = (ArrayMap) receiversField.get(loadedApk);
186 for (Object receiverMap : receivers.values()) {
187 for (Object rec : ((ArrayMap) receiverMap).keySet()) {
188 Class clazz = rec.getClass();
189 if (clazz.getName().contains("ProxyChangeListener")) {
190 Method onReceiveMethod = clazz.getDeclaredMethod("onReceive", Context.class,
191 Intent.class);
192 Intent intent = new Intent(Proxy.PROXY_CHANGE_ACTION);
193 onReceiveMethod.invoke(rec, getApplicationContext(), intent);
194 Log.v(TAG, "Prompting WebView proxy reload.");
195 }
196 }
197 }
198 } catch (Exception e) {
199 Log.e(TAG, "Exception while setting WebView proxy: " + e);
200 }
Paul Jensen869868be2014-05-15 10:33:05 -0400201 }
202
Paul Jensen25a217c2015-02-27 22:55:47 -0500203 private void done(Result result) {
Hugo Benichia173a632017-06-17 12:47:33 +0900204 if (isDone.getAndSet(true)) {
205 // isDone was already true: done() already called
206 return;
207 }
Hugo Benichi7f086e12016-12-06 15:36:30 +0900208 if (DBG) {
209 Log.d(TAG, String.format("Result %s for %s", result.name(), mUrl.toString()));
210 }
Hugo Benichi9e8ab432017-06-05 14:52:24 +0900211 logMetricsEvent(result.metricsEvent);
Paul Jensen25a217c2015-02-27 22:55:47 -0500212 switch (result) {
213 case DISMISSED:
Paul Jensen49e3edf2015-05-22 10:50:39 -0400214 mCaptivePortal.reportCaptivePortalDismissed();
Paul Jensen25a217c2015-02-27 22:55:47 -0500215 break;
216 case UNWANTED:
Paul Jensen49e3edf2015-05-22 10:50:39 -0400217 mCaptivePortal.ignoreNetwork();
Paul Jensen25a217c2015-02-27 22:55:47 -0500218 break;
219 case WANTED_AS_IS:
Paul Jensen49e3edf2015-05-22 10:50:39 -0400220 mCaptivePortal.useNetwork();
Paul Jensen25a217c2015-02-27 22:55:47 -0500221 break;
222 }
Paul Jensen6a776c832016-07-19 13:19:39 -0400223 finishAndRemoveTask();
Paul Jensen869868be2014-05-15 10:33:05 -0400224 }
225
226 @Override
227 public boolean onCreateOptionsMenu(Menu menu) {
228 getMenuInflater().inflate(R.menu.captive_portal_login, menu);
229 return true;
230 }
231
232 @Override
Paul Jensenb6ea9ee2014-07-18 12:27:23 -0400233 public void onBackPressed() {
Alan Viverette51efddb2017-04-05 10:00:01 -0400234 WebView myWebView = findViewById(R.id.webview);
Paul Jensene836b682015-05-19 12:30:56 -0400235 if (myWebView.canGoBack() && mWebViewClient.allowBack()) {
Paul Jensenb6ea9ee2014-07-18 12:27:23 -0400236 myWebView.goBack();
237 } else {
238 super.onBackPressed();
239 }
240 }
241
242 @Override
Paul Jensen869868be2014-05-15 10:33:05 -0400243 public boolean onOptionsItemSelected(MenuItem item) {
Hugo Benichi7f086e12016-12-06 15:36:30 +0900244 final Result result;
245 final String action;
246 final int id = item.getItemId();
247 switch (id) {
248 case R.id.action_use_network:
249 result = Result.WANTED_AS_IS;
250 action = "USE_NETWORK";
251 break;
252 case R.id.action_do_not_use_network:
253 result = Result.UNWANTED;
254 action = "DO_NOT_USE_NETWORK";
255 break;
256 default:
257 return super.onOptionsItemSelected(item);
Paul Jensen869868be2014-05-15 10:33:05 -0400258 }
Hugo Benichi7f086e12016-12-06 15:36:30 +0900259 if (DBG) {
260 Log.d(TAG, String.format("onOptionsItemSelect %s for %s", action, mUrl.toString()));
Paul Jensen869868be2014-05-15 10:33:05 -0400261 }
Hugo Benichi7f086e12016-12-06 15:36:30 +0900262 done(result);
263 return true;
Paul Jensen869868be2014-05-15 10:33:05 -0400264 }
265
Paul Jensen868f6242015-05-18 12:48:28 -0400266 @Override
267 public void onDestroy() {
268 super.onDestroy();
Paul Jensen868f6242015-05-18 12:48:28 -0400269 if (mNetworkCallback != null) {
Hugo Benichia173a632017-06-17 12:47:33 +0900270 // mNetworkCallback is not null if mUrl is not null.
Paul Jensen868f6242015-05-18 12:48:28 -0400271 mCm.unregisterNetworkCallback(mNetworkCallback);
Paul Jensen868f6242015-05-18 12:48:28 -0400272 }
Paul Jensen65636fb2015-05-06 14:40:59 -0400273 if (mLaunchBrowser) {
274 // Give time for this network to become default. After 500ms just proceed.
275 for (int i = 0; i < 5; i++) {
276 // TODO: This misses when mNetwork underlies a VPN.
277 if (mNetwork.equals(mCm.getActiveNetwork())) break;
278 try {
279 Thread.sleep(100);
280 } catch (InterruptedException e) {
281 }
282 }
Hugo Benichi7f086e12016-12-06 15:36:30 +0900283 final String url = mUrl.toString();
284 if (DBG) {
285 Log.d(TAG, "starting activity with intent ACTION_VIEW for " + url);
286 }
287 startActivity(new Intent(Intent.ACTION_VIEW, Uri.parse(url)));
Paul Jensen65636fb2015-05-06 14:40:59 -0400288 }
Paul Jensen868f6242015-05-18 12:48:28 -0400289 }
290
Hugo Benichi7f086e12016-12-06 15:36:30 +0900291 private URL getUrl() {
292 String url = getIntent().getStringExtra(ConnectivityManager.EXTRA_CAPTIVE_PORTAL_URL);
293 if (url == null) {
294 url = mCm.getCaptivePortalServerUrl();
295 }
Hugo Benichia2066492017-05-17 09:26:30 +0900296 return makeURL(url);
297 }
298
299 private static URL makeURL(String url) {
Hugo Benichi7f086e12016-12-06 15:36:30 +0900300 try {
301 return new URL(url);
302 } catch (MalformedURLException e) {
Hugo Benichia2066492017-05-17 09:26:30 +0900303 Log.e(TAG, "Invalid URL " + url);
Hugo Benichi7f086e12016-12-06 15:36:30 +0900304 }
305 return null;
306 }
307
Hugo Benichi12df4652017-06-17 13:36:35 +0900308 private static String host(URL url) {
309 if (url == null) {
310 return null;
311 }
312 return url.getHost();
313 }
314
315 private static String sanitizeURL(URL url) {
316 // In non-Debug build, only show host to avoid leaking private info.
317 return Build.IS_DEBUGGABLE ? Objects.toString(url) : host(url);
318 }
319
Paul Jensen869868be2014-05-15 10:33:05 -0400320 private void testForCaptivePortal() {
Hugo Benichi2c021972016-12-14 08:23:40 +0900321 // TODO: reuse NetworkMonitor facilities for consistent captive portal detection.
Paul Jensen869868be2014-05-15 10:33:05 -0400322 new Thread(new Runnable() {
323 public void run() {
Erik Klined324dce2018-04-25 17:27:28 +0900324 final Network network = ResolvUtil.makeNetworkWithPrivateDnsBypass(mNetwork);
Paul Jensen869868be2014-05-15 10:33:05 -0400325 // Give time for captive portal to open.
326 try {
327 Thread.sleep(1000);
328 } catch (InterruptedException e) {
329 }
330 HttpURLConnection urlConnection = null;
331 int httpResponseCode = 500;
332 try {
Erik Klined324dce2018-04-25 17:27:28 +0900333 urlConnection = (HttpURLConnection) network.openConnection(mUrl);
Paul Jensen869868be2014-05-15 10:33:05 -0400334 urlConnection.setInstanceFollowRedirects(false);
335 urlConnection.setConnectTimeout(SOCKET_TIMEOUT_MS);
336 urlConnection.setReadTimeout(SOCKET_TIMEOUT_MS);
337 urlConnection.setUseCaches(false);
Hugo Benichi2c021972016-12-14 08:23:40 +0900338 if (mUserAgent != null) {
339 urlConnection.setRequestProperty("User-Agent", mUserAgent);
340 }
Hugo Benichiec88fd62017-03-07 15:10:03 +0900341 // cannot read request header after connection
342 String requestHeader = urlConnection.getRequestProperties().toString();
343
Paul Jensen869868be2014-05-15 10:33:05 -0400344 urlConnection.getInputStream();
345 httpResponseCode = urlConnection.getResponseCode();
Hugo Benichiec88fd62017-03-07 15:10:03 +0900346 if (DBG) {
347 Log.d(TAG, "probe at " + mUrl +
348 " ret=" + httpResponseCode +
349 " request=" + requestHeader +
350 " headers=" + urlConnection.getHeaderFields());
351 }
Paul Jensen869868be2014-05-15 10:33:05 -0400352 } catch (IOException e) {
353 } finally {
354 if (urlConnection != null) urlConnection.disconnect();
355 }
356 if (httpResponseCode == 204) {
Paul Jensen25a217c2015-02-27 22:55:47 -0500357 done(Result.DISMISSED);
Paul Jensen869868be2014-05-15 10:33:05 -0400358 }
359 }
360 }).start();
361 }
362
363 private class MyWebViewClient extends WebViewClient {
Paul Jensen5344a4a2015-05-06 07:39:36 -0400364 private static final String INTERNAL_ASSETS = "file:///android_asset/";
Hugo Benichi60d5f462017-06-02 10:12:09 +0900365
Paul Jensen65636fb2015-05-06 14:40:59 -0400366 private final String mBrowserBailOutToken = Long.toString(new Random().nextLong());
367 // How many Android device-independent-pixels per scaled-pixel
368 // dp/sp = (px/sp) / (px/dp) = (1/sp) / (1/dp)
369 private final float mDpPerSp = TypedValue.applyDimension(TypedValue.COMPLEX_UNIT_SP, 1,
370 getResources().getDisplayMetrics()) /
371 TypedValue.applyDimension(TypedValue.COMPLEX_UNIT_DIP, 1,
372 getResources().getDisplayMetrics());
Paul Jensene836b682015-05-19 12:30:56 -0400373 private int mPagesLoaded;
Hugo Benichi12df4652017-06-17 13:36:35 +0900374 // the host of the page that this webview is currently loading. Can be null when undefined.
375 private String mHostname;
Paul Jensene836b682015-05-19 12:30:56 -0400376
377 // If we haven't finished cleaning up the history, don't allow going back.
378 public boolean allowBack() {
379 return mPagesLoaded > 1;
380 }
Paul Jensen88eb0fa2014-10-02 13:43:39 -0400381
Paul Jensen869868be2014-05-15 10:33:05 -0400382 @Override
Hugo Benichi12df4652017-06-17 13:36:35 +0900383 public void onPageStarted(WebView view, String urlString, Bitmap favicon) {
384 if (urlString.contains(mBrowserBailOutToken)) {
Paul Jensen65636fb2015-05-06 14:40:59 -0400385 mLaunchBrowser = true;
386 done(Result.WANTED_AS_IS);
387 return;
388 }
Paul Jensene836b682015-05-19 12:30:56 -0400389 // The first page load is used only to cause the WebView to
390 // fetch the proxy settings. Don't update the URL bar, and
391 // don't check if the captive portal is still there.
Hugo Benichi12df4652017-06-17 13:36:35 +0900392 if (mPagesLoaded == 0) {
393 return;
394 }
395 final URL url = makeURL(urlString);
Hugo Benichi04d78602017-07-19 21:20:53 +0900396 Log.d(TAG, "onPageStarted: " + sanitizeURL(url));
Hugo Benichi12df4652017-06-17 13:36:35 +0900397 mHostname = host(url);
Paul Jensene836b682015-05-19 12:30:56 -0400398 // For internally generated pages, leave URL bar listing prior URL as this is the URL
399 // the page refers to.
Hugo Benichi12df4652017-06-17 13:36:35 +0900400 if (!urlString.startsWith(INTERNAL_ASSETS)) {
401 String subtitle = (url != null) ? getHeaderSubtitle(url) : urlString;
402 getActionBar().setSubtitle(subtitle);
Paul Jensene836b682015-05-19 12:30:56 -0400403 }
Hugo Benichia2066492017-05-17 09:26:30 +0900404 getProgressBar().setVisibility(View.VISIBLE);
Paul Jensen869868be2014-05-15 10:33:05 -0400405 testForCaptivePortal();
406 }
407
408 @Override
409 public void onPageFinished(WebView view, String url) {
Paul Jensene836b682015-05-19 12:30:56 -0400410 mPagesLoaded++;
Hugo Benichia2066492017-05-17 09:26:30 +0900411 getProgressBar().setVisibility(View.INVISIBLE);
Chalard Jean94bc48f2018-03-09 22:28:51 +0900412 mSwipeRefreshLayout.setRefreshing(false);
Paul Jensene836b682015-05-19 12:30:56 -0400413 if (mPagesLoaded == 1) {
Paul Jensen88eb0fa2014-10-02 13:43:39 -0400414 // Now that WebView has loaded at least one page we know it has read in the proxy
415 // settings. Now prompt the WebView read the Network-specific proxy settings.
416 setWebViewProxy();
417 // Load the real page.
Hugo Benichi7f086e12016-12-06 15:36:30 +0900418 view.loadUrl(mUrl.toString());
Paul Jensen88eb0fa2014-10-02 13:43:39 -0400419 return;
Paul Jensene836b682015-05-19 12:30:56 -0400420 } else if (mPagesLoaded == 2) {
421 // Prevent going back to empty first page.
susnata97640402017-06-23 09:13:05 -0700422 // Fix for missing focus, see b/62449959 for details. Remove it once we get a
423 // newer version of WebView (60.x.y).
424 view.requestFocus();
Paul Jensene836b682015-05-19 12:30:56 -0400425 view.clearHistory();
Paul Jensen5344a4a2015-05-06 07:39:36 -0400426 }
Paul Jensen869868be2014-05-15 10:33:05 -0400427 testForCaptivePortal();
428 }
Paul Jensenfc8022f2014-12-09 15:18:40 -0500429
Paul Jensen65636fb2015-05-06 14:40:59 -0400430 // Convert Android scaled-pixels (sp) to HTML size.
431 private String sp(int sp) {
432 // Convert sp to dp's.
433 float dp = sp * mDpPerSp;
434 // Apply a scale factor to make things look right.
435 dp *= 1.3;
436 // Convert dp's to HTML size.
Hugo Benichi60d5f462017-06-02 10:12:09 +0900437 // HTML px's are scaled just like dp's, so just add "px" suffix.
438 return Integer.toString((int)dp) + "px";
Paul Jensen65636fb2015-05-06 14:40:59 -0400439 }
440
Paul Jensenfc8022f2014-12-09 15:18:40 -0500441 // A web page consisting of a large broken lock icon to indicate SSL failure.
Paul Jensenfc8022f2014-12-09 15:18:40 -0500442
443 @Override
444 public void onReceivedSslError(WebView view, SslErrorHandler handler, SslError error) {
Hugo Benichi12df4652017-06-17 13:36:35 +0900445 final URL url = makeURL(error.getUrl());
446 final String host = host(url);
447 Log.d(TAG, String.format("SSL error: %s, url: %s, certificate: %s",
Hugo Benichi04d78602017-07-19 21:20:53 +0900448 sslErrorName(error), sanitizeURL(url), error.getCertificate()));
Hugo Benichi12df4652017-06-17 13:36:35 +0900449 if (url == null || !Objects.equals(host, mHostname)) {
450 // Ignore ssl errors for resources coming from a different hostname than the page
451 // that we are currently loading, and only cancel the request.
452 handler.cancel();
453 return;
Hugo Benichi60d5f462017-06-02 10:12:09 +0900454 }
Hugo Benichi12df4652017-06-17 13:36:35 +0900455 logMetricsEvent(MetricsEvent.CAPTIVE_PORTAL_LOGIN_ACTIVITY_SSL_ERROR);
456 final String sslErrorPage = makeSslErrorPage();
Hugo Benichi60d5f462017-06-02 10:12:09 +0900457 view.loadDataWithBaseURL(INTERNAL_ASSETS, sslErrorPage, "text/HTML", "UTF-8", null);
458 }
459
460 private String makeSslErrorPage() {
461 final String warningMsg = getString(R.string.ssl_error_warning);
462 final String exampleMsg = getString(R.string.ssl_error_example);
463 final String continueMsg = getString(R.string.ssl_error_continue);
464 return String.join("\n",
465 "<html>",
466 "<head>",
467 " <meta name=\"viewport\" content=\"width=device-width, initial-scale=1\">",
468 " <style>",
469 " body {",
470 " background-color:#fafafa;",
471 " margin:auto;",
472 " width:80%;",
473 " margin-top: 96px",
474 " }",
475 " img {",
476 " height:48px;",
477 " width:48px;",
478 " }",
479 " div.warn {",
480 " font-size:" + sp(16) + ";",
481 " line-height:1.28;",
482 " margin-top:16px;",
483 " opacity:0.87;",
484 " }",
485 " div.example {",
486 " font-size:" + sp(14) + ";",
487 " line-height:1.21905;",
488 " margin-top:16px;",
489 " opacity:0.54;",
490 " }",
491 " a {",
492 " color:#4285F4;",
493 " display:inline-block;",
494 " font-size:" + sp(14) + ";",
495 " font-weight:bold;",
496 " height:48px;",
497 " margin-top:24px;",
498 " text-decoration:none;",
499 " text-transform:uppercase;",
500 " }",
501 " </style>",
502 "</head>",
503 "<body>",
504 " <p><img src=quantum_ic_warning_amber_96.png><br>",
505 " <div class=warn>" + warningMsg + "</div>",
506 " <div class=example>" + exampleMsg + "</div>",
507 " <a href=" + mBrowserBailOutToken + ">" + continueMsg + "</a>",
508 "</body>",
509 "</html>");
Paul Jensenfc8022f2014-12-09 15:18:40 -0500510 }
Paul Jensenfd54da92015-06-09 07:50:51 -0400511
512 @Override
513 public boolean shouldOverrideUrlLoading (WebView view, String url) {
514 if (url.startsWith("tel:")) {
515 startActivity(new Intent(Intent.ACTION_DIAL, Uri.parse(url)));
516 return true;
517 }
518 return false;
519 }
Paul Jensen869868be2014-05-15 10:33:05 -0400520 }
521
522 private class MyWebChromeClient extends WebChromeClient {
523 @Override
524 public void onProgressChanged(WebView view, int newProgress) {
Hugo Benichia2066492017-05-17 09:26:30 +0900525 getProgressBar().setProgress(newProgress);
Paul Jensen869868be2014-05-15 10:33:05 -0400526 }
527 }
Hugo Benichia2066492017-05-17 09:26:30 +0900528
529 private ProgressBar getProgressBar() {
530 return findViewById(R.id.progress_bar);
531 }
532
533 private WebView getWebview() {
534 return findViewById(R.id.webview);
535 }
536
537 private String getHeaderTitle() {
Hugo Benichi3a222972017-06-01 12:58:49 +0900538 NetworkCapabilities nc = mCm.getNetworkCapabilities(mNetwork);
Chalard Jean2dcccbc2018-04-12 11:52:37 +0900539 if (nc == null || TextUtils.isEmpty(nc.getSSID())
540 || !nc.hasTransport(NetworkCapabilities.TRANSPORT_WIFI)) {
Hugo Benichi3a222972017-06-01 12:58:49 +0900541 return getString(R.string.action_bar_label);
542 }
Chalard Jean2dcccbc2018-04-12 11:52:37 +0900543 return getString(R.string.action_bar_title, WifiInfo.removeDoubleQuotes(nc.getSSID()));
Hugo Benichia2066492017-05-17 09:26:30 +0900544 }
545
Hugo Benichi12df4652017-06-17 13:36:35 +0900546 private String getHeaderSubtitle(URL url) {
547 String host = host(url);
Hugo Benichia2066492017-05-17 09:26:30 +0900548 final String https = "https";
549 if (https.equals(url.getProtocol())) {
Hugo Benichi12df4652017-06-17 13:36:35 +0900550 return https + "://" + host;
Hugo Benichia2066492017-05-17 09:26:30 +0900551 }
Hugo Benichi12df4652017-06-17 13:36:35 +0900552 return host;
Hugo Benichia2066492017-05-17 09:26:30 +0900553 }
Hugo Benichi9e8ab432017-06-05 14:52:24 +0900554
555 private void logMetricsEvent(int event) {
556 MetricsLogger.action(this, event, getPackageName());
557 }
Hugo Benichi04d78602017-07-19 21:20:53 +0900558
559 private static final SparseArray<String> SSL_ERRORS = new SparseArray<>();
560 static {
561 SSL_ERRORS.put(SslError.SSL_NOTYETVALID, "SSL_NOTYETVALID");
562 SSL_ERRORS.put(SslError.SSL_EXPIRED, "SSL_EXPIRED");
563 SSL_ERRORS.put(SslError.SSL_IDMISMATCH, "SSL_IDMISMATCH");
564 SSL_ERRORS.put(SslError.SSL_UNTRUSTED, "SSL_UNTRUSTED");
565 SSL_ERRORS.put(SslError.SSL_DATE_INVALID, "SSL_DATE_INVALID");
566 SSL_ERRORS.put(SslError.SSL_INVALID, "SSL_INVALID");
567 }
568
569 private static String sslErrorName(SslError error) {
570 return SSL_ERRORS.get(error.getPrimaryError(), "UNKNOWN");
571 }
Paul Jensen869868be2014-05-15 10:33:05 -0400572}