blob: 2c9b6eb72b4dac99534019b32a6cc3ceb8e7cafe [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
Remi NGUYEN VAN9f855052018-05-22 17:04:17 +090019import static android.net.ConnectivityManager.EXTRA_CAPTIVE_PORTAL_PROBE_SPEC;
Remi NGUYEN VAN9f855052018-05-22 17:04:17 +090020
Paul Jensen869868be2014-05-15 10:33:05 -040021import android.app.Activity;
Takayuki, Oguracc387812018-09-13 12:40:28 +090022import android.app.AlertDialog;
Remi NGUYEN VAN47274272019-01-30 23:39:24 +090023import android.app.Application;
Paul Jensen88eb0fa2014-10-02 13:43:39 -040024import android.content.Context;
Takayuki, Oguracc387812018-09-13 12:40:28 +090025import android.content.DialogInterface;
Paul Jensen869868be2014-05-15 10:33:05 -040026import android.content.Intent;
27import android.graphics.Bitmap;
Paul Jensen49e3edf2015-05-22 10:50:39 -040028import android.net.CaptivePortal;
Paul Jensen869868be2014-05-15 10:33:05 -040029import android.net.ConnectivityManager;
Paul Jensen8df099d2014-09-26 15:19:17 -040030import android.net.ConnectivityManager.NetworkCallback;
Paul Jensen869868be2014-05-15 10:33:05 -040031import android.net.Network;
Paul Jensen8df099d2014-09-26 15:19:17 -040032import android.net.NetworkCapabilities;
33import android.net.NetworkRequest;
Paul Jensen88eb0fa2014-10-02 13:43:39 -040034import android.net.Proxy;
Paul Jensen88eb0fa2014-10-02 13:43:39 -040035import android.net.Uri;
Remi NGUYEN VAN9f855052018-05-22 17:04:17 +090036import android.net.captiveportal.CaptivePortalProbeSpec;
Takayuki, Oguracc387812018-09-13 12:40:28 +090037import android.net.http.SslCertificate;
Paul Jensenfc8022f2014-12-09 15:18:40 -050038import android.net.http.SslError;
Chalard Jean39620242018-04-12 11:52:37 +090039import android.net.wifi.WifiInfo;
Remi NGUYEN VAN47274272019-01-30 23:39:24 +090040import android.net.wifi.WifiManager;
Paul Jensen869868be2014-05-15 10:33:05 -040041import android.os.Bundle;
Remi NGUYEN VAN47274272019-01-30 23:39:24 +090042import android.os.SystemProperties;
Chalard Jean8fa6ea32018-03-09 22:28:51 +090043import android.support.v4.widget.SwipeRefreshLayout;
Chalard Jean39620242018-04-12 11:52:37 +090044import android.text.TextUtils;
Paul Jensen88eb0fa2014-10-02 13:43:39 -040045import android.util.ArrayMap;
46import android.util.Log;
Hugo Benichi04d78602017-07-19 21:20:53 +090047import android.util.SparseArray;
Takayuki, Oguracc387812018-09-13 12:40:28 +090048import android.util.TypedValue;
49import android.view.LayoutInflater;
Paul Jensen869868be2014-05-15 10:33:05 -040050import android.view.Menu;
51import android.view.MenuItem;
Hugo Benichia2066492017-05-17 09:26:30 +090052import android.view.View;
Lorenzo Colittia0398502018-03-26 01:32:33 +090053import android.webkit.CookieManager;
Paul Jensenfc8022f2014-12-09 15:18:40 -050054import android.webkit.SslErrorHandler;
Paul Jensen869868be2014-05-15 10:33:05 -040055import android.webkit.WebChromeClient;
56import android.webkit.WebSettings;
57import android.webkit.WebView;
58import android.webkit.WebViewClient;
Takayuki, Oguracc387812018-09-13 12:40:28 +090059import android.widget.LinearLayout;
Paul Jensen8f333f12014-08-05 22:52:16 -040060import android.widget.ProgressBar;
Paul Jensen5344a4a2015-05-06 07:39:36 -040061import android.widget.TextView;
Paul Jensen869868be2014-05-15 10:33:05 -040062
Hugo Benichi9e8ab432017-06-05 14:52:24 +090063import com.android.internal.logging.nano.MetricsProto.MetricsEvent;
64
Paul Jensen869868be2014-05-15 10:33:05 -040065import java.io.IOException;
Remi NGUYEN VANa4bcc862019-01-28 13:28:35 +090066import java.lang.reflect.Field;
67import java.lang.reflect.Method;
Paul Jensen869868be2014-05-15 10:33:05 -040068import java.net.HttpURLConnection;
69import java.net.MalformedURLException;
70import java.net.URL;
Hugo Benichi12df4652017-06-17 13:36:35 +090071import java.util.Objects;
Paul Jensen65636fb2015-05-06 14:40:59 -040072import java.util.Random;
Hugo Benichia173a632017-06-17 12:47:33 +090073import java.util.concurrent.atomic.AtomicBoolean;
Paul Jensen869868be2014-05-15 10:33:05 -040074
75public class CaptivePortalLoginActivity extends Activity {
Hugo Benichi7f086e12016-12-06 15:36:30 +090076 private static final String TAG = CaptivePortalLoginActivity.class.getSimpleName();
77 private static final boolean DBG = true;
Hugo Benichi60d5f462017-06-02 10:12:09 +090078 private static final boolean VDBG = false;
Hugo Benichi7f086e12016-12-06 15:36:30 +090079
Paul Jensen869868be2014-05-15 10:33:05 -040080 private static final int SOCKET_TIMEOUT_MS = 10000;
Remi NGUYEN VANa4bcc862019-01-28 13:28:35 +090081 public static final String HTTP_LOCATION_HEADER_NAME = "Location";
Paul Jensen869868be2014-05-15 10:33:05 -040082
Hugo Benichi9e8ab432017-06-05 14:52:24 +090083 private enum Result {
84 DISMISSED(MetricsEvent.ACTION_CAPTIVE_PORTAL_LOGIN_RESULT_DISMISSED),
85 UNWANTED(MetricsEvent.ACTION_CAPTIVE_PORTAL_LOGIN_RESULT_UNWANTED),
86 WANTED_AS_IS(MetricsEvent.ACTION_CAPTIVE_PORTAL_LOGIN_RESULT_WANTED_AS_IS);
87
88 final int metricsEvent;
89 Result(int metricsEvent) { this.metricsEvent = metricsEvent; }
90 };
Paul Jensen869868be2014-05-15 10:33:05 -040091
Hugo Benichi7f086e12016-12-06 15:36:30 +090092 private URL mUrl;
Remi NGUYEN VAN9f855052018-05-22 17:04:17 +090093 private CaptivePortalProbeSpec mProbeSpec;
Hugo Benichi2c021972016-12-14 08:23:40 +090094 private String mUserAgent;
Paul Jensen25a217c2015-02-27 22:55:47 -050095 private Network mNetwork;
Paul Jensen49e3edf2015-05-22 10:50:39 -040096 private CaptivePortal mCaptivePortal;
Paul Jensen8df099d2014-09-26 15:19:17 -040097 private NetworkCallback mNetworkCallback;
Paul Jensen25a217c2015-02-27 22:55:47 -050098 private ConnectivityManager mCm;
Remi NGUYEN VAN47274272019-01-30 23:39:24 +090099 private WifiManager mWifiManager;
Paul Jensen65636fb2015-05-06 14:40:59 -0400100 private boolean mLaunchBrowser = false;
Paul Jensene836b682015-05-19 12:30:56 -0400101 private MyWebViewClient mWebViewClient;
Chalard Jean8fa6ea32018-03-09 22:28:51 +0900102 private SwipeRefreshLayout mSwipeRefreshLayout;
Hugo Benichia173a632017-06-17 12:47:33 +0900103 // Ensures that done() happens once exactly, handling concurrent callers with atomic operations.
104 private final AtomicBoolean isDone = new AtomicBoolean(false);
Paul Jensen869868be2014-05-15 10:33:05 -0400105
106 @Override
107 protected void onCreate(Bundle savedInstanceState) {
108 super.onCreate(savedInstanceState);
Hugo Benichi9e8ab432017-06-05 14:52:24 +0900109
Remi NGUYEN VANde602212019-01-30 15:22:01 +0900110 mCaptivePortal = getIntent().getParcelableExtra(ConnectivityManager.EXTRA_CAPTIVE_PORTAL);
Hugo Benichi9e8ab432017-06-05 14:52:24 +0900111 logMetricsEvent(MetricsEvent.ACTION_CAPTIVE_PORTAL_LOGIN_ACTIVITY);
112
Remi NGUYEN VAN47274272019-01-30 23:39:24 +0900113 mCm = getSystemService(ConnectivityManager.class);
114 mWifiManager = getSystemService(WifiManager.class);
Paul Jensen25a217c2015-02-27 22:55:47 -0500115 mNetwork = getIntent().getParcelableExtra(ConnectivityManager.EXTRA_NETWORK);
Hugo Benichiec88fd62017-03-07 15:10:03 +0900116 mUserAgent =
117 getIntent().getStringExtra(ConnectivityManager.EXTRA_CAPTIVE_PORTAL_USER_AGENT);
Hugo Benichi7f086e12016-12-06 15:36:30 +0900118 mUrl = getUrl();
119 if (mUrl == null) {
120 // getUrl() failed to parse the url provided in the intent: bail out in a way that
121 // at least provides network access.
122 done(Result.WANTED_AS_IS);
123 return;
124 }
125 if (DBG) {
126 Log.d(TAG, String.format("onCreate for %s", mUrl.toString()));
127 }
Paul Jensen869868be2014-05-15 10:33:05 -0400128
Remi NGUYEN VAN9f855052018-05-22 17:04:17 +0900129 final String spec = getIntent().getStringExtra(EXTRA_CAPTIVE_PORTAL_PROBE_SPEC);
130 try {
131 mProbeSpec = CaptivePortalProbeSpec.parseSpecOrNull(spec);
132 } catch (Exception e) {
133 // Make extra sure that invalid configurations do not cause crashes
134 mProbeSpec = null;
135 }
136
Hisanobu, Watanabe67c1d262018-08-20 17:46:54 +0900137 mNetworkCallback = new NetworkCallback() {
138 @Override
139 public void onLost(Network lostNetwork) {
140 // If the network disappears while the app is up, exit.
141 if (mNetwork.equals(lostNetwork)) done(Result.UNWANTED);
142 }
143 };
144 mCm.registerNetworkCallback(new NetworkRequest.Builder().build(), mNetworkCallback);
145
146 // If the network has disappeared, exit.
147 final NetworkCapabilities networkCapabilities = mCm.getNetworkCapabilities(mNetwork);
148 if (networkCapabilities == null) {
149 finishAndRemoveTask();
150 return;
151 }
152
Paul Jensene0bef712014-12-10 15:12:18 -0500153 // Also initializes proxy system properties.
Erik Klinef4fa9822018-04-27 22:48:33 +0900154 mNetwork = mNetwork.getPrivateDnsBypassingCopy();
Paul Jensen25a217c2015-02-27 22:55:47 -0500155 mCm.bindProcessToNetwork(mNetwork);
Paul Jensen88eb0fa2014-10-02 13:43:39 -0400156
157 // Proxy system properties must be initialized before setContentView is called because
158 // setContentView initializes the WebView logic which in turn reads the system properties.
159 setContentView(R.layout.activity_captive_portal_login);
160
Hugo Benichia2066492017-05-17 09:26:30 +0900161 getActionBar().setDisplayShowHomeEnabled(false);
162 getActionBar().setElevation(0); // remove shadow
163 getActionBar().setTitle(getHeaderTitle());
164 getActionBar().setSubtitle("");
165
166 final WebView webview = getWebview();
167 webview.clearCache(true);
Lorenzo Colittia0398502018-03-26 01:32:33 +0900168 CookieManager.getInstance().setAcceptThirdPartyCookies(webview, true);
Hugo Benichia2066492017-05-17 09:26:30 +0900169 WebSettings webSettings = webview.getSettings();
Paul Jensen869868be2014-05-15 10:33:05 -0400170 webSettings.setJavaScriptEnabled(true);
Lorenzo Colittib55bf382016-10-21 18:41:25 +0900171 webSettings.setMixedContentMode(WebSettings.MIXED_CONTENT_COMPATIBILITY_MODE);
Hugo Benichi94c5fb32017-05-11 09:58:14 +0900172 webSettings.setUseWideViewPort(true);
173 webSettings.setLoadWithOverviewMode(true);
174 webSettings.setSupportZoom(true);
175 webSettings.setBuiltInZoomControls(true);
176 webSettings.setDisplayZoomControls(false);
Sehee Parkc2e966d2018-11-15 16:46:27 +0900177 webSettings.setDomStorageEnabled(true);
Paul Jensene836b682015-05-19 12:30:56 -0400178 mWebViewClient = new MyWebViewClient();
Hugo Benichia2066492017-05-17 09:26:30 +0900179 webview.setWebViewClient(mWebViewClient);
180 webview.setWebChromeClient(new MyWebChromeClient());
Paul Jensen88eb0fa2014-10-02 13:43:39 -0400181 // Start initial page load so WebView finishes loading proxy settings.
182 // Actual load of mUrl is initiated by MyWebViewClient.
Hugo Benichia2066492017-05-17 09:26:30 +0900183 webview.loadData("", "text/html", null);
Chalard Jean8fa6ea32018-03-09 22:28:51 +0900184
185 mSwipeRefreshLayout = findViewById(R.id.swipe_refresh);
186 mSwipeRefreshLayout.setOnRefreshListener(() -> {
187 webview.reload();
188 mSwipeRefreshLayout.setRefreshing(true);
189 });
190
Paul Jensen88eb0fa2014-10-02 13:43:39 -0400191 }
192
193 // Find WebView's proxy BroadcastReceiver and prompt it to read proxy system properties.
194 private void setWebViewProxy() {
Remi NGUYEN VAN47274272019-01-30 23:39:24 +0900195 // TODO: migrate to androidx WebView proxy setting API as soon as it is finalized
Paul Jensen88eb0fa2014-10-02 13:43:39 -0400196 try {
Remi NGUYEN VAN47274272019-01-30 23:39:24 +0900197 final Field loadedApkField = Application.class.getDeclaredField("mLoadedApk");
198 final Class<?> loadedApkClass = loadedApkField.getType();
199 final Object loadedApk = loadedApkField.get(getApplication());
200 Field receiversField = loadedApkClass.getDeclaredField("mReceivers");
Paul Jensen88eb0fa2014-10-02 13:43:39 -0400201 receiversField.setAccessible(true);
202 ArrayMap receivers = (ArrayMap) receiversField.get(loadedApk);
203 for (Object receiverMap : receivers.values()) {
204 for (Object rec : ((ArrayMap) receiverMap).keySet()) {
205 Class clazz = rec.getClass();
206 if (clazz.getName().contains("ProxyChangeListener")) {
207 Method onReceiveMethod = clazz.getDeclaredMethod("onReceive", Context.class,
208 Intent.class);
209 Intent intent = new Intent(Proxy.PROXY_CHANGE_ACTION);
210 onReceiveMethod.invoke(rec, getApplicationContext(), intent);
211 Log.v(TAG, "Prompting WebView proxy reload.");
212 }
213 }
214 }
215 } catch (Exception e) {
216 Log.e(TAG, "Exception while setting WebView proxy: " + e);
217 }
Paul Jensen869868be2014-05-15 10:33:05 -0400218 }
219
Paul Jensen25a217c2015-02-27 22:55:47 -0500220 private void done(Result result) {
Hugo Benichia173a632017-06-17 12:47:33 +0900221 if (isDone.getAndSet(true)) {
222 // isDone was already true: done() already called
223 return;
224 }
Hugo Benichi7f086e12016-12-06 15:36:30 +0900225 if (DBG) {
226 Log.d(TAG, String.format("Result %s for %s", result.name(), mUrl.toString()));
227 }
Hugo Benichi9e8ab432017-06-05 14:52:24 +0900228 logMetricsEvent(result.metricsEvent);
Paul Jensen25a217c2015-02-27 22:55:47 -0500229 switch (result) {
230 case DISMISSED:
Paul Jensen49e3edf2015-05-22 10:50:39 -0400231 mCaptivePortal.reportCaptivePortalDismissed();
Paul Jensen25a217c2015-02-27 22:55:47 -0500232 break;
233 case UNWANTED:
Paul Jensen49e3edf2015-05-22 10:50:39 -0400234 mCaptivePortal.ignoreNetwork();
Paul Jensen25a217c2015-02-27 22:55:47 -0500235 break;
236 case WANTED_AS_IS:
Paul Jensen49e3edf2015-05-22 10:50:39 -0400237 mCaptivePortal.useNetwork();
Paul Jensen25a217c2015-02-27 22:55:47 -0500238 break;
239 }
Paul Jensen6a776c832016-07-19 13:19:39 -0400240 finishAndRemoveTask();
Paul Jensen869868be2014-05-15 10:33:05 -0400241 }
242
243 @Override
244 public boolean onCreateOptionsMenu(Menu menu) {
245 getMenuInflater().inflate(R.menu.captive_portal_login, menu);
246 return true;
247 }
248
249 @Override
Paul Jensenb6ea9ee2014-07-18 12:27:23 -0400250 public void onBackPressed() {
Alan Viverette51efddb2017-04-05 10:00:01 -0400251 WebView myWebView = findViewById(R.id.webview);
Paul Jensene836b682015-05-19 12:30:56 -0400252 if (myWebView.canGoBack() && mWebViewClient.allowBack()) {
Paul Jensenb6ea9ee2014-07-18 12:27:23 -0400253 myWebView.goBack();
254 } else {
255 super.onBackPressed();
256 }
257 }
258
259 @Override
Paul Jensen869868be2014-05-15 10:33:05 -0400260 public boolean onOptionsItemSelected(MenuItem item) {
Hugo Benichi7f086e12016-12-06 15:36:30 +0900261 final Result result;
262 final String action;
263 final int id = item.getItemId();
264 switch (id) {
265 case R.id.action_use_network:
266 result = Result.WANTED_AS_IS;
267 action = "USE_NETWORK";
268 break;
269 case R.id.action_do_not_use_network:
270 result = Result.UNWANTED;
271 action = "DO_NOT_USE_NETWORK";
272 break;
273 default:
274 return super.onOptionsItemSelected(item);
Paul Jensen869868be2014-05-15 10:33:05 -0400275 }
Hugo Benichi7f086e12016-12-06 15:36:30 +0900276 if (DBG) {
277 Log.d(TAG, String.format("onOptionsItemSelect %s for %s", action, mUrl.toString()));
Paul Jensen869868be2014-05-15 10:33:05 -0400278 }
Hugo Benichi7f086e12016-12-06 15:36:30 +0900279 done(result);
280 return true;
Paul Jensen869868be2014-05-15 10:33:05 -0400281 }
282
Paul Jensen868f6242015-05-18 12:48:28 -0400283 @Override
284 public void onDestroy() {
285 super.onDestroy();
Takayuki, Oguracc387812018-09-13 12:40:28 +0900286 final WebView webview = (WebView) findViewById(R.id.webview);
287 if (webview != null) {
288 webview.stopLoading();
289 webview.setWebViewClient(null);
290 webview.setWebChromeClient(null);
291 webview.destroy();
292 }
Paul Jensen868f6242015-05-18 12:48:28 -0400293 if (mNetworkCallback != null) {
Hugo Benichia173a632017-06-17 12:47:33 +0900294 // mNetworkCallback is not null if mUrl is not null.
Paul Jensen868f6242015-05-18 12:48:28 -0400295 mCm.unregisterNetworkCallback(mNetworkCallback);
Paul Jensen868f6242015-05-18 12:48:28 -0400296 }
Paul Jensen65636fb2015-05-06 14:40:59 -0400297 if (mLaunchBrowser) {
298 // Give time for this network to become default. After 500ms just proceed.
299 for (int i = 0; i < 5; i++) {
300 // TODO: This misses when mNetwork underlies a VPN.
301 if (mNetwork.equals(mCm.getActiveNetwork())) break;
302 try {
303 Thread.sleep(100);
304 } catch (InterruptedException e) {
305 }
306 }
Hugo Benichi7f086e12016-12-06 15:36:30 +0900307 final String url = mUrl.toString();
308 if (DBG) {
309 Log.d(TAG, "starting activity with intent ACTION_VIEW for " + url);
310 }
311 startActivity(new Intent(Intent.ACTION_VIEW, Uri.parse(url)));
Paul Jensen65636fb2015-05-06 14:40:59 -0400312 }
Paul Jensen868f6242015-05-18 12:48:28 -0400313 }
314
Hugo Benichi7f086e12016-12-06 15:36:30 +0900315 private URL getUrl() {
316 String url = getIntent().getStringExtra(ConnectivityManager.EXTRA_CAPTIVE_PORTAL_URL);
317 if (url == null) {
318 url = mCm.getCaptivePortalServerUrl();
319 }
Hugo Benichia2066492017-05-17 09:26:30 +0900320 return makeURL(url);
321 }
322
323 private static URL makeURL(String url) {
Hugo Benichi7f086e12016-12-06 15:36:30 +0900324 try {
325 return new URL(url);
326 } catch (MalformedURLException e) {
Hugo Benichia2066492017-05-17 09:26:30 +0900327 Log.e(TAG, "Invalid URL " + url);
Hugo Benichi7f086e12016-12-06 15:36:30 +0900328 }
329 return null;
330 }
331
Hugo Benichi12df4652017-06-17 13:36:35 +0900332 private static String host(URL url) {
333 if (url == null) {
334 return null;
335 }
336 return url.getHost();
337 }
338
339 private static String sanitizeURL(URL url) {
340 // In non-Debug build, only show host to avoid leaking private info.
Remi NGUYEN VAN47274272019-01-30 23:39:24 +0900341 return isDebuggable() ? Objects.toString(url) : host(url);
342 }
343
344 private static boolean isDebuggable() {
345 return SystemProperties.getInt("ro.debuggable", 0) == 1;
Hugo Benichi12df4652017-06-17 13:36:35 +0900346 }
347
Paul Jensen869868be2014-05-15 10:33:05 -0400348 private void testForCaptivePortal() {
Hugo Benichi2c021972016-12-14 08:23:40 +0900349 // TODO: reuse NetworkMonitor facilities for consistent captive portal detection.
Paul Jensen869868be2014-05-15 10:33:05 -0400350 new Thread(new Runnable() {
351 public void run() {
352 // Give time for captive portal to open.
353 try {
354 Thread.sleep(1000);
355 } catch (InterruptedException e) {
356 }
357 HttpURLConnection urlConnection = null;
358 int httpResponseCode = 500;
Remi NGUYEN VAN9f855052018-05-22 17:04:17 +0900359 String locationHeader = null;
Paul Jensen869868be2014-05-15 10:33:05 -0400360 try {
Erik Klinef4fa9822018-04-27 22:48:33 +0900361 urlConnection = (HttpURLConnection) mNetwork.openConnection(mUrl);
Paul Jensen869868be2014-05-15 10:33:05 -0400362 urlConnection.setInstanceFollowRedirects(false);
363 urlConnection.setConnectTimeout(SOCKET_TIMEOUT_MS);
364 urlConnection.setReadTimeout(SOCKET_TIMEOUT_MS);
365 urlConnection.setUseCaches(false);
Hugo Benichi2c021972016-12-14 08:23:40 +0900366 if (mUserAgent != null) {
367 urlConnection.setRequestProperty("User-Agent", mUserAgent);
368 }
Hugo Benichiec88fd62017-03-07 15:10:03 +0900369 // cannot read request header after connection
370 String requestHeader = urlConnection.getRequestProperties().toString();
371
Paul Jensen869868be2014-05-15 10:33:05 -0400372 urlConnection.getInputStream();
373 httpResponseCode = urlConnection.getResponseCode();
Remi NGUYEN VAN9f855052018-05-22 17:04:17 +0900374 locationHeader = urlConnection.getHeaderField(HTTP_LOCATION_HEADER_NAME);
Hugo Benichiec88fd62017-03-07 15:10:03 +0900375 if (DBG) {
376 Log.d(TAG, "probe at " + mUrl +
377 " ret=" + httpResponseCode +
378 " request=" + requestHeader +
379 " headers=" + urlConnection.getHeaderFields());
380 }
Paul Jensen869868be2014-05-15 10:33:05 -0400381 } catch (IOException e) {
382 } finally {
383 if (urlConnection != null) urlConnection.disconnect();
384 }
Remi NGUYEN VAN9f855052018-05-22 17:04:17 +0900385 if (isDismissed(httpResponseCode, locationHeader, mProbeSpec)) {
Paul Jensen25a217c2015-02-27 22:55:47 -0500386 done(Result.DISMISSED);
Paul Jensen869868be2014-05-15 10:33:05 -0400387 }
388 }
389 }).start();
390 }
391
Remi NGUYEN VAN9f855052018-05-22 17:04:17 +0900392 private static boolean isDismissed(
393 int httpResponseCode, String locationHeader, CaptivePortalProbeSpec probeSpec) {
394 return (probeSpec != null)
395 ? probeSpec.getResult(httpResponseCode, locationHeader).isSuccessful()
396 : (httpResponseCode == 204);
397 }
398
Paul Jensen869868be2014-05-15 10:33:05 -0400399 private class MyWebViewClient extends WebViewClient {
Paul Jensen5344a4a2015-05-06 07:39:36 -0400400 private static final String INTERNAL_ASSETS = "file:///android_asset/";
Hugo Benichi60d5f462017-06-02 10:12:09 +0900401
Paul Jensen65636fb2015-05-06 14:40:59 -0400402 private final String mBrowserBailOutToken = Long.toString(new Random().nextLong());
Takayuki, Oguracc387812018-09-13 12:40:28 +0900403 private final String mCertificateOutToken = Long.toString(new Random().nextLong());
Paul Jensen65636fb2015-05-06 14:40:59 -0400404 // How many Android device-independent-pixels per scaled-pixel
405 // dp/sp = (px/sp) / (px/dp) = (1/sp) / (1/dp)
406 private final float mDpPerSp = TypedValue.applyDimension(TypedValue.COMPLEX_UNIT_SP, 1,
407 getResources().getDisplayMetrics()) /
408 TypedValue.applyDimension(TypedValue.COMPLEX_UNIT_DIP, 1,
409 getResources().getDisplayMetrics());
Paul Jensene836b682015-05-19 12:30:56 -0400410 private int mPagesLoaded;
Hugo Benichi12df4652017-06-17 13:36:35 +0900411 // the host of the page that this webview is currently loading. Can be null when undefined.
412 private String mHostname;
Paul Jensene836b682015-05-19 12:30:56 -0400413
414 // If we haven't finished cleaning up the history, don't allow going back.
415 public boolean allowBack() {
416 return mPagesLoaded > 1;
417 }
Paul Jensen88eb0fa2014-10-02 13:43:39 -0400418
Takayuki, Oguracc387812018-09-13 12:40:28 +0900419 private String mSslErrorTitle = null;
420 private SslErrorHandler mSslErrorHandler = null;
421 private SslError mSslError = null;
422
Paul Jensen869868be2014-05-15 10:33:05 -0400423 @Override
Hugo Benichi12df4652017-06-17 13:36:35 +0900424 public void onPageStarted(WebView view, String urlString, Bitmap favicon) {
425 if (urlString.contains(mBrowserBailOutToken)) {
Paul Jensen65636fb2015-05-06 14:40:59 -0400426 mLaunchBrowser = true;
427 done(Result.WANTED_AS_IS);
428 return;
429 }
Paul Jensene836b682015-05-19 12:30:56 -0400430 // The first page load is used only to cause the WebView to
431 // fetch the proxy settings. Don't update the URL bar, and
432 // don't check if the captive portal is still there.
Hugo Benichi12df4652017-06-17 13:36:35 +0900433 if (mPagesLoaded == 0) {
434 return;
435 }
436 final URL url = makeURL(urlString);
Hugo Benichi04d78602017-07-19 21:20:53 +0900437 Log.d(TAG, "onPageStarted: " + sanitizeURL(url));
Hugo Benichi12df4652017-06-17 13:36:35 +0900438 mHostname = host(url);
Paul Jensene836b682015-05-19 12:30:56 -0400439 // For internally generated pages, leave URL bar listing prior URL as this is the URL
440 // the page refers to.
Hugo Benichi12df4652017-06-17 13:36:35 +0900441 if (!urlString.startsWith(INTERNAL_ASSETS)) {
442 String subtitle = (url != null) ? getHeaderSubtitle(url) : urlString;
443 getActionBar().setSubtitle(subtitle);
Paul Jensene836b682015-05-19 12:30:56 -0400444 }
Hugo Benichia2066492017-05-17 09:26:30 +0900445 getProgressBar().setVisibility(View.VISIBLE);
Paul Jensen869868be2014-05-15 10:33:05 -0400446 testForCaptivePortal();
447 }
448
449 @Override
450 public void onPageFinished(WebView view, String url) {
Paul Jensene836b682015-05-19 12:30:56 -0400451 mPagesLoaded++;
Hugo Benichia2066492017-05-17 09:26:30 +0900452 getProgressBar().setVisibility(View.INVISIBLE);
Chalard Jean8fa6ea32018-03-09 22:28:51 +0900453 mSwipeRefreshLayout.setRefreshing(false);
Paul Jensene836b682015-05-19 12:30:56 -0400454 if (mPagesLoaded == 1) {
Paul Jensen88eb0fa2014-10-02 13:43:39 -0400455 // Now that WebView has loaded at least one page we know it has read in the proxy
456 // settings. Now prompt the WebView read the Network-specific proxy settings.
457 setWebViewProxy();
458 // Load the real page.
Hugo Benichi7f086e12016-12-06 15:36:30 +0900459 view.loadUrl(mUrl.toString());
Paul Jensen88eb0fa2014-10-02 13:43:39 -0400460 return;
Paul Jensene836b682015-05-19 12:30:56 -0400461 } else if (mPagesLoaded == 2) {
462 // Prevent going back to empty first page.
susnata97640402017-06-23 09:13:05 -0700463 // Fix for missing focus, see b/62449959 for details. Remove it once we get a
464 // newer version of WebView (60.x.y).
465 view.requestFocus();
Paul Jensene836b682015-05-19 12:30:56 -0400466 view.clearHistory();
Paul Jensen5344a4a2015-05-06 07:39:36 -0400467 }
Paul Jensen869868be2014-05-15 10:33:05 -0400468 testForCaptivePortal();
469 }
Paul Jensenfc8022f2014-12-09 15:18:40 -0500470
Paul Jensen65636fb2015-05-06 14:40:59 -0400471 // Convert Android scaled-pixels (sp) to HTML size.
472 private String sp(int sp) {
473 // Convert sp to dp's.
474 float dp = sp * mDpPerSp;
475 // Apply a scale factor to make things look right.
476 dp *= 1.3;
477 // Convert dp's to HTML size.
Hugo Benichi60d5f462017-06-02 10:12:09 +0900478 // HTML px's are scaled just like dp's, so just add "px" suffix.
479 return Integer.toString((int)dp) + "px";
Paul Jensen65636fb2015-05-06 14:40:59 -0400480 }
481
Paul Jensenfc8022f2014-12-09 15:18:40 -0500482 // A web page consisting of a large broken lock icon to indicate SSL failure.
Paul Jensenfc8022f2014-12-09 15:18:40 -0500483
484 @Override
485 public void onReceivedSslError(WebView view, SslErrorHandler handler, SslError error) {
Hugo Benichi12df4652017-06-17 13:36:35 +0900486 final URL url = makeURL(error.getUrl());
487 final String host = host(url);
488 Log.d(TAG, String.format("SSL error: %s, url: %s, certificate: %s",
Hugo Benichi04d78602017-07-19 21:20:53 +0900489 sslErrorName(error), sanitizeURL(url), error.getCertificate()));
Hugo Benichi12df4652017-06-17 13:36:35 +0900490 if (url == null || !Objects.equals(host, mHostname)) {
491 // Ignore ssl errors for resources coming from a different hostname than the page
492 // that we are currently loading, and only cancel the request.
493 handler.cancel();
494 return;
Hugo Benichi60d5f462017-06-02 10:12:09 +0900495 }
Hugo Benichi12df4652017-06-17 13:36:35 +0900496 logMetricsEvent(MetricsEvent.CAPTIVE_PORTAL_LOGIN_ACTIVITY_SSL_ERROR);
497 final String sslErrorPage = makeSslErrorPage();
Hugo Benichi60d5f462017-06-02 10:12:09 +0900498 view.loadDataWithBaseURL(INTERNAL_ASSETS, sslErrorPage, "text/HTML", "UTF-8", null);
Takayuki, Oguracc387812018-09-13 12:40:28 +0900499 mSslErrorTitle = view.getTitle() == null ? "" : view.getTitle();
500 mSslErrorHandler = handler;
501 mSslError = error;
Hugo Benichi60d5f462017-06-02 10:12:09 +0900502 }
503
504 private String makeSslErrorPage() {
505 final String warningMsg = getString(R.string.ssl_error_warning);
506 final String exampleMsg = getString(R.string.ssl_error_example);
507 final String continueMsg = getString(R.string.ssl_error_continue);
Takayuki, Oguracc387812018-09-13 12:40:28 +0900508 final String certificateMsg = getString(R.string.ssl_error_view_certificate);
Hugo Benichi60d5f462017-06-02 10:12:09 +0900509 return String.join("\n",
510 "<html>",
511 "<head>",
512 " <meta name=\"viewport\" content=\"width=device-width, initial-scale=1\">",
513 " <style>",
514 " body {",
515 " background-color:#fafafa;",
516 " margin:auto;",
517 " width:80%;",
518 " margin-top: 96px",
519 " }",
520 " img {",
521 " height:48px;",
522 " width:48px;",
523 " }",
524 " div.warn {",
525 " font-size:" + sp(16) + ";",
526 " line-height:1.28;",
527 " margin-top:16px;",
528 " opacity:0.87;",
529 " }",
530 " div.example {",
531 " font-size:" + sp(14) + ";",
532 " line-height:1.21905;",
533 " margin-top:16px;",
534 " opacity:0.54;",
535 " }",
536 " a {",
537 " color:#4285F4;",
538 " display:inline-block;",
539 " font-size:" + sp(14) + ";",
540 " font-weight:bold;",
541 " height:48px;",
542 " margin-top:24px;",
543 " text-decoration:none;",
544 " text-transform:uppercase;",
545 " }",
Takayuki, Oguracc387812018-09-13 12:40:28 +0900546 " a.certificate {",
547 " margin-top:0px;",
548 " }",
Hugo Benichi60d5f462017-06-02 10:12:09 +0900549 " </style>",
550 "</head>",
551 "<body>",
552 " <p><img src=quantum_ic_warning_amber_96.png><br>",
553 " <div class=warn>" + warningMsg + "</div>",
554 " <div class=example>" + exampleMsg + "</div>",
Takayuki, Oguracc387812018-09-13 12:40:28 +0900555 " <a href=" + mBrowserBailOutToken + ">" + continueMsg + "</a><br>",
556 " <a class=certificate href=" + mCertificateOutToken + ">" + certificateMsg +
557 "</a>",
Hugo Benichi60d5f462017-06-02 10:12:09 +0900558 "</body>",
559 "</html>");
Paul Jensenfc8022f2014-12-09 15:18:40 -0500560 }
Paul Jensenfd54da92015-06-09 07:50:51 -0400561
562 @Override
563 public boolean shouldOverrideUrlLoading (WebView view, String url) {
564 if (url.startsWith("tel:")) {
565 startActivity(new Intent(Intent.ACTION_DIAL, Uri.parse(url)));
566 return true;
567 }
Takayuki, Oguracc387812018-09-13 12:40:28 +0900568 if (url.contains(mCertificateOutToken) && mSslError != null) {
569 showSslAlertDialog(mSslErrorHandler, mSslError, mSslErrorTitle);
570 return true;
571 }
Paul Jensenfd54da92015-06-09 07:50:51 -0400572 return false;
573 }
Takayuki, Oguracc387812018-09-13 12:40:28 +0900574 private void showSslAlertDialog(SslErrorHandler handler, SslError error, String title) {
575 final LayoutInflater factory = LayoutInflater.from(CaptivePortalLoginActivity.this);
576 final View sslWarningView = factory.inflate(R.layout.ssl_warning, null);
577
578 // Set Security certificate
579 setViewSecurityCertificate(sslWarningView.findViewById(R.id.certificate_layout), error);
580 ((TextView) sslWarningView.findViewById(R.id.ssl_error_type))
581 .setText(sslErrorName(error));
582 ((TextView) sslWarningView.findViewById(R.id.title)).setText(mSslErrorTitle);
583 ((TextView) sslWarningView.findViewById(R.id.address)).setText(error.getUrl());
584
585 AlertDialog sslAlertDialog = new AlertDialog.Builder(CaptivePortalLoginActivity.this)
586 .setTitle(R.string.ssl_security_warning_title)
587 .setView(sslWarningView)
588 .setPositiveButton(R.string.ok, (DialogInterface dialog, int whichButton) -> {
589 // handler.cancel is called via OnCancelListener.
590 dialog.cancel();
591 })
592 .setOnCancelListener((DialogInterface dialogInterface) -> handler.cancel())
593 .create();
594 sslAlertDialog.show();
595 }
596
597 private void setViewSecurityCertificate(LinearLayout certificateLayout, SslError error) {
Remi NGUYEN VAN47274272019-01-30 23:39:24 +0900598 ((TextView) certificateLayout.findViewById(R.id.ssl_error_msg))
599 .setText(sslErrorMessage(error));
Takayuki, Oguracc387812018-09-13 12:40:28 +0900600 SslCertificate cert = error.getCertificate();
Remi NGUYEN VAN47274272019-01-30 23:39:24 +0900601 // TODO: call the method directly once inflateCertificateView is @SystemApi
602 try {
603 final View certificateView = (View) SslCertificate.class.getMethod(
604 "inflateCertificateView", Context.class)
605 .invoke(cert, CaptivePortalLoginActivity.this);
606 certificateLayout.addView(certificateView);
607 } catch (ReflectiveOperationException | SecurityException e) {
608 Log.e(TAG, "Could not create certificate view", e);
609 }
Takayuki, Oguracc387812018-09-13 12:40:28 +0900610 }
Paul Jensen869868be2014-05-15 10:33:05 -0400611 }
612
613 private class MyWebChromeClient extends WebChromeClient {
614 @Override
615 public void onProgressChanged(WebView view, int newProgress) {
Hugo Benichia2066492017-05-17 09:26:30 +0900616 getProgressBar().setProgress(newProgress);
Paul Jensen869868be2014-05-15 10:33:05 -0400617 }
618 }
Hugo Benichia2066492017-05-17 09:26:30 +0900619
620 private ProgressBar getProgressBar() {
621 return findViewById(R.id.progress_bar);
622 }
623
624 private WebView getWebview() {
625 return findViewById(R.id.webview);
626 }
627
628 private String getHeaderTitle() {
Hugo Benichi3a222972017-06-01 12:58:49 +0900629 NetworkCapabilities nc = mCm.getNetworkCapabilities(mNetwork);
Remi NGUYEN VAN47274272019-01-30 23:39:24 +0900630 final String ssid = getSsid();
631 if (TextUtils.isEmpty(ssid)
632 || nc == null || !nc.hasTransport(NetworkCapabilities.TRANSPORT_WIFI)) {
Hugo Benichi3a222972017-06-01 12:58:49 +0900633 return getString(R.string.action_bar_label);
634 }
Remi NGUYEN VAN47274272019-01-30 23:39:24 +0900635 return getString(R.string.action_bar_title, ssid);
636 }
637
638 // TODO: remove once SSID is obtained from NetworkCapabilities
639 private String getSsid() {
640 if (mWifiManager == null) {
641 return null;
642 }
643 final WifiInfo wifiInfo = mWifiManager.getConnectionInfo();
644 return removeDoubleQuotes(wifiInfo.getSSID());
645 }
646
647 private static String removeDoubleQuotes(String string) {
648 if (string == null) return null;
649 final int length = string.length();
650 if ((length > 1) && (string.charAt(0) == '"') && (string.charAt(length - 1) == '"')) {
651 return string.substring(1, length - 1);
652 }
653 return string;
Hugo Benichia2066492017-05-17 09:26:30 +0900654 }
655
Hugo Benichi12df4652017-06-17 13:36:35 +0900656 private String getHeaderSubtitle(URL url) {
657 String host = host(url);
Hugo Benichia2066492017-05-17 09:26:30 +0900658 final String https = "https";
659 if (https.equals(url.getProtocol())) {
Hugo Benichi12df4652017-06-17 13:36:35 +0900660 return https + "://" + host;
Hugo Benichia2066492017-05-17 09:26:30 +0900661 }
Hugo Benichi12df4652017-06-17 13:36:35 +0900662 return host;
Hugo Benichia2066492017-05-17 09:26:30 +0900663 }
Hugo Benichi9e8ab432017-06-05 14:52:24 +0900664
665 private void logMetricsEvent(int event) {
Remi NGUYEN VANde602212019-01-30 15:22:01 +0900666 mCaptivePortal.logEvent(event, getPackageName());
Hugo Benichi9e8ab432017-06-05 14:52:24 +0900667 }
Hugo Benichi04d78602017-07-19 21:20:53 +0900668
669 private static final SparseArray<String> SSL_ERRORS = new SparseArray<>();
670 static {
671 SSL_ERRORS.put(SslError.SSL_NOTYETVALID, "SSL_NOTYETVALID");
672 SSL_ERRORS.put(SslError.SSL_EXPIRED, "SSL_EXPIRED");
673 SSL_ERRORS.put(SslError.SSL_IDMISMATCH, "SSL_IDMISMATCH");
674 SSL_ERRORS.put(SslError.SSL_UNTRUSTED, "SSL_UNTRUSTED");
675 SSL_ERRORS.put(SslError.SSL_DATE_INVALID, "SSL_DATE_INVALID");
676 SSL_ERRORS.put(SslError.SSL_INVALID, "SSL_INVALID");
677 }
678
679 private static String sslErrorName(SslError error) {
680 return SSL_ERRORS.get(error.getPrimaryError(), "UNKNOWN");
681 }
Takayuki, Oguracc387812018-09-13 12:40:28 +0900682
683 private static final SparseArray<Integer> SSL_ERROR_MSGS = new SparseArray<>();
684 static {
685 SSL_ERROR_MSGS.put(SslError.SSL_NOTYETVALID, R.string.ssl_error_not_yet_valid);
686 SSL_ERROR_MSGS.put(SslError.SSL_EXPIRED, R.string.ssl_error_expired);
687 SSL_ERROR_MSGS.put(SslError.SSL_IDMISMATCH, R.string.ssl_error_mismatch);
688 SSL_ERROR_MSGS.put(SslError.SSL_UNTRUSTED, R.string.ssl_error_untrusted);
689 SSL_ERROR_MSGS.put(SslError.SSL_DATE_INVALID, R.string.ssl_error_date_invalid);
690 SSL_ERROR_MSGS.put(SslError.SSL_INVALID, R.string.ssl_error_invalid);
691 }
692
693 private static Integer sslErrorMessage(SslError error) {
694 return SSL_ERROR_MSGS.get(error.getPrimaryError(), R.string.ssl_error_unknown);
695 }
Paul Jensen869868be2014-05-15 10:33:05 -0400696}