blob: 9488afb446c46304ca5f5bcb5c6f6e02467b6dbb [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;
lucaslin8efd38b2019-03-19 19:15:54 +080056import android.webkit.WebResourceRequest;
Paul Jensen869868be2014-05-15 10:33:05 -040057import android.webkit.WebSettings;
58import android.webkit.WebView;
59import android.webkit.WebViewClient;
Takayuki, Oguracc387812018-09-13 12:40:28 +090060import android.widget.LinearLayout;
Paul Jensen8f333f12014-08-05 22:52:16 -040061import android.widget.ProgressBar;
Paul Jensen5344a4a2015-05-06 07:39:36 -040062import android.widget.TextView;
Paul Jensen869868be2014-05-15 10:33:05 -040063
Hugo Benichi9e8ab432017-06-05 14:52:24 +090064import com.android.internal.logging.nano.MetricsProto.MetricsEvent;
65
Paul Jensen869868be2014-05-15 10:33:05 -040066import java.io.IOException;
Remi NGUYEN VANa4bcc862019-01-28 13:28:35 +090067import java.lang.reflect.Field;
68import java.lang.reflect.Method;
Paul Jensen869868be2014-05-15 10:33:05 -040069import java.net.HttpURLConnection;
70import java.net.MalformedURLException;
71import java.net.URL;
Hugo Benichi12df4652017-06-17 13:36:35 +090072import java.util.Objects;
Paul Jensen65636fb2015-05-06 14:40:59 -040073import java.util.Random;
Hugo Benichia173a632017-06-17 12:47:33 +090074import java.util.concurrent.atomic.AtomicBoolean;
Paul Jensen869868be2014-05-15 10:33:05 -040075
76public class CaptivePortalLoginActivity extends Activity {
Hugo Benichi7f086e12016-12-06 15:36:30 +090077 private static final String TAG = CaptivePortalLoginActivity.class.getSimpleName();
78 private static final boolean DBG = true;
Hugo Benichi60d5f462017-06-02 10:12:09 +090079 private static final boolean VDBG = false;
Hugo Benichi7f086e12016-12-06 15:36:30 +090080
Paul Jensen869868be2014-05-15 10:33:05 -040081 private static final int SOCKET_TIMEOUT_MS = 10000;
Remi NGUYEN VANa4bcc862019-01-28 13:28:35 +090082 public static final String HTTP_LOCATION_HEADER_NAME = "Location";
Paul Jensen869868be2014-05-15 10:33:05 -040083
Hugo Benichi9e8ab432017-06-05 14:52:24 +090084 private enum Result {
85 DISMISSED(MetricsEvent.ACTION_CAPTIVE_PORTAL_LOGIN_RESULT_DISMISSED),
86 UNWANTED(MetricsEvent.ACTION_CAPTIVE_PORTAL_LOGIN_RESULT_UNWANTED),
87 WANTED_AS_IS(MetricsEvent.ACTION_CAPTIVE_PORTAL_LOGIN_RESULT_WANTED_AS_IS);
88
89 final int metricsEvent;
90 Result(int metricsEvent) { this.metricsEvent = metricsEvent; }
91 };
Paul Jensen869868be2014-05-15 10:33:05 -040092
Hugo Benichi7f086e12016-12-06 15:36:30 +090093 private URL mUrl;
Remi NGUYEN VAN9f855052018-05-22 17:04:17 +090094 private CaptivePortalProbeSpec mProbeSpec;
Hugo Benichi2c021972016-12-14 08:23:40 +090095 private String mUserAgent;
Paul Jensen25a217c2015-02-27 22:55:47 -050096 private Network mNetwork;
Paul Jensen49e3edf2015-05-22 10:50:39 -040097 private CaptivePortal mCaptivePortal;
Paul Jensen8df099d2014-09-26 15:19:17 -040098 private NetworkCallback mNetworkCallback;
Paul Jensen25a217c2015-02-27 22:55:47 -050099 private ConnectivityManager mCm;
Remi NGUYEN VAN47274272019-01-30 23:39:24 +0900100 private WifiManager mWifiManager;
Paul Jensen65636fb2015-05-06 14:40:59 -0400101 private boolean mLaunchBrowser = false;
Paul Jensene836b682015-05-19 12:30:56 -0400102 private MyWebViewClient mWebViewClient;
Chalard Jean8fa6ea32018-03-09 22:28:51 +0900103 private SwipeRefreshLayout mSwipeRefreshLayout;
Hugo Benichia173a632017-06-17 12:47:33 +0900104 // Ensures that done() happens once exactly, handling concurrent callers with atomic operations.
105 private final AtomicBoolean isDone = new AtomicBoolean(false);
Paul Jensen869868be2014-05-15 10:33:05 -0400106
107 @Override
108 protected void onCreate(Bundle savedInstanceState) {
109 super.onCreate(savedInstanceState);
Hugo Benichi9e8ab432017-06-05 14:52:24 +0900110
Remi NGUYEN VANde602212019-01-30 15:22:01 +0900111 mCaptivePortal = getIntent().getParcelableExtra(ConnectivityManager.EXTRA_CAPTIVE_PORTAL);
Hugo Benichi9e8ab432017-06-05 14:52:24 +0900112 logMetricsEvent(MetricsEvent.ACTION_CAPTIVE_PORTAL_LOGIN_ACTIVITY);
113
Remi NGUYEN VAN47274272019-01-30 23:39:24 +0900114 mCm = getSystemService(ConnectivityManager.class);
115 mWifiManager = getSystemService(WifiManager.class);
Paul Jensen25a217c2015-02-27 22:55:47 -0500116 mNetwork = getIntent().getParcelableExtra(ConnectivityManager.EXTRA_NETWORK);
Hugo Benichiec88fd62017-03-07 15:10:03 +0900117 mUserAgent =
118 getIntent().getStringExtra(ConnectivityManager.EXTRA_CAPTIVE_PORTAL_USER_AGENT);
Hugo Benichi7f086e12016-12-06 15:36:30 +0900119 mUrl = getUrl();
120 if (mUrl == null) {
121 // getUrl() failed to parse the url provided in the intent: bail out in a way that
122 // at least provides network access.
123 done(Result.WANTED_AS_IS);
124 return;
125 }
126 if (DBG) {
127 Log.d(TAG, String.format("onCreate for %s", mUrl.toString()));
128 }
Paul Jensen869868be2014-05-15 10:33:05 -0400129
Remi NGUYEN VAN9f855052018-05-22 17:04:17 +0900130 final String spec = getIntent().getStringExtra(EXTRA_CAPTIVE_PORTAL_PROBE_SPEC);
131 try {
132 mProbeSpec = CaptivePortalProbeSpec.parseSpecOrNull(spec);
133 } catch (Exception e) {
134 // Make extra sure that invalid configurations do not cause crashes
135 mProbeSpec = null;
136 }
137
Hisanobu, Watanabe67c1d262018-08-20 17:46:54 +0900138 mNetworkCallback = new NetworkCallback() {
139 @Override
140 public void onLost(Network lostNetwork) {
141 // If the network disappears while the app is up, exit.
142 if (mNetwork.equals(lostNetwork)) done(Result.UNWANTED);
143 }
144 };
145 mCm.registerNetworkCallback(new NetworkRequest.Builder().build(), mNetworkCallback);
146
147 // If the network has disappeared, exit.
148 final NetworkCapabilities networkCapabilities = mCm.getNetworkCapabilities(mNetwork);
149 if (networkCapabilities == null) {
150 finishAndRemoveTask();
151 return;
152 }
153
Paul Jensene0bef712014-12-10 15:12:18 -0500154 // Also initializes proxy system properties.
Erik Klinef4fa9822018-04-27 22:48:33 +0900155 mNetwork = mNetwork.getPrivateDnsBypassingCopy();
Paul Jensen25a217c2015-02-27 22:55:47 -0500156 mCm.bindProcessToNetwork(mNetwork);
Paul Jensen88eb0fa2014-10-02 13:43:39 -0400157
158 // Proxy system properties must be initialized before setContentView is called because
159 // setContentView initializes the WebView logic which in turn reads the system properties.
160 setContentView(R.layout.activity_captive_portal_login);
161
Hugo Benichia2066492017-05-17 09:26:30 +0900162 getActionBar().setDisplayShowHomeEnabled(false);
163 getActionBar().setElevation(0); // remove shadow
164 getActionBar().setTitle(getHeaderTitle());
165 getActionBar().setSubtitle("");
166
167 final WebView webview = getWebview();
168 webview.clearCache(true);
Lorenzo Colittia0398502018-03-26 01:32:33 +0900169 CookieManager.getInstance().setAcceptThirdPartyCookies(webview, true);
Hugo Benichia2066492017-05-17 09:26:30 +0900170 WebSettings webSettings = webview.getSettings();
Paul Jensen869868be2014-05-15 10:33:05 -0400171 webSettings.setJavaScriptEnabled(true);
Lorenzo Colittib55bf382016-10-21 18:41:25 +0900172 webSettings.setMixedContentMode(WebSettings.MIXED_CONTENT_COMPATIBILITY_MODE);
Hugo Benichi94c5fb32017-05-11 09:58:14 +0900173 webSettings.setUseWideViewPort(true);
174 webSettings.setLoadWithOverviewMode(true);
175 webSettings.setSupportZoom(true);
176 webSettings.setBuiltInZoomControls(true);
177 webSettings.setDisplayZoomControls(false);
Sehee Parkc2e966d2018-11-15 16:46:27 +0900178 webSettings.setDomStorageEnabled(true);
Paul Jensene836b682015-05-19 12:30:56 -0400179 mWebViewClient = new MyWebViewClient();
Hugo Benichia2066492017-05-17 09:26:30 +0900180 webview.setWebViewClient(mWebViewClient);
181 webview.setWebChromeClient(new MyWebChromeClient());
Paul Jensen88eb0fa2014-10-02 13:43:39 -0400182 // Start initial page load so WebView finishes loading proxy settings.
183 // Actual load of mUrl is initiated by MyWebViewClient.
Hugo Benichia2066492017-05-17 09:26:30 +0900184 webview.loadData("", "text/html", null);
Chalard Jean8fa6ea32018-03-09 22:28:51 +0900185
186 mSwipeRefreshLayout = findViewById(R.id.swipe_refresh);
187 mSwipeRefreshLayout.setOnRefreshListener(() -> {
188 webview.reload();
189 mSwipeRefreshLayout.setRefreshing(true);
190 });
191
Paul Jensen88eb0fa2014-10-02 13:43:39 -0400192 }
193
194 // Find WebView's proxy BroadcastReceiver and prompt it to read proxy system properties.
195 private void setWebViewProxy() {
Remi NGUYEN VAN47274272019-01-30 23:39:24 +0900196 // TODO: migrate to androidx WebView proxy setting API as soon as it is finalized
Paul Jensen88eb0fa2014-10-02 13:43:39 -0400197 try {
Remi NGUYEN VAN47274272019-01-30 23:39:24 +0900198 final Field loadedApkField = Application.class.getDeclaredField("mLoadedApk");
199 final Class<?> loadedApkClass = loadedApkField.getType();
200 final Object loadedApk = loadedApkField.get(getApplication());
201 Field receiversField = loadedApkClass.getDeclaredField("mReceivers");
Paul Jensen88eb0fa2014-10-02 13:43:39 -0400202 receiversField.setAccessible(true);
203 ArrayMap receivers = (ArrayMap) receiversField.get(loadedApk);
204 for (Object receiverMap : receivers.values()) {
205 for (Object rec : ((ArrayMap) receiverMap).keySet()) {
206 Class clazz = rec.getClass();
207 if (clazz.getName().contains("ProxyChangeListener")) {
208 Method onReceiveMethod = clazz.getDeclaredMethod("onReceive", Context.class,
209 Intent.class);
210 Intent intent = new Intent(Proxy.PROXY_CHANGE_ACTION);
211 onReceiveMethod.invoke(rec, getApplicationContext(), intent);
212 Log.v(TAG, "Prompting WebView proxy reload.");
213 }
214 }
215 }
216 } catch (Exception e) {
217 Log.e(TAG, "Exception while setting WebView proxy: " + e);
218 }
Paul Jensen869868be2014-05-15 10:33:05 -0400219 }
220
Paul Jensen25a217c2015-02-27 22:55:47 -0500221 private void done(Result result) {
Hugo Benichia173a632017-06-17 12:47:33 +0900222 if (isDone.getAndSet(true)) {
223 // isDone was already true: done() already called
224 return;
225 }
Hugo Benichi7f086e12016-12-06 15:36:30 +0900226 if (DBG) {
227 Log.d(TAG, String.format("Result %s for %s", result.name(), mUrl.toString()));
228 }
Hugo Benichi9e8ab432017-06-05 14:52:24 +0900229 logMetricsEvent(result.metricsEvent);
Paul Jensen25a217c2015-02-27 22:55:47 -0500230 switch (result) {
231 case DISMISSED:
Paul Jensen49e3edf2015-05-22 10:50:39 -0400232 mCaptivePortal.reportCaptivePortalDismissed();
Paul Jensen25a217c2015-02-27 22:55:47 -0500233 break;
234 case UNWANTED:
Paul Jensen49e3edf2015-05-22 10:50:39 -0400235 mCaptivePortal.ignoreNetwork();
Paul Jensen25a217c2015-02-27 22:55:47 -0500236 break;
237 case WANTED_AS_IS:
Paul Jensen49e3edf2015-05-22 10:50:39 -0400238 mCaptivePortal.useNetwork();
Paul Jensen25a217c2015-02-27 22:55:47 -0500239 break;
240 }
Paul Jensen6a776c832016-07-19 13:19:39 -0400241 finishAndRemoveTask();
Paul Jensen869868be2014-05-15 10:33:05 -0400242 }
243
244 @Override
245 public boolean onCreateOptionsMenu(Menu menu) {
246 getMenuInflater().inflate(R.menu.captive_portal_login, menu);
247 return true;
248 }
249
250 @Override
Paul Jensenb6ea9ee2014-07-18 12:27:23 -0400251 public void onBackPressed() {
Alan Viverette51efddb2017-04-05 10:00:01 -0400252 WebView myWebView = findViewById(R.id.webview);
Paul Jensene836b682015-05-19 12:30:56 -0400253 if (myWebView.canGoBack() && mWebViewClient.allowBack()) {
Paul Jensenb6ea9ee2014-07-18 12:27:23 -0400254 myWebView.goBack();
255 } else {
256 super.onBackPressed();
257 }
258 }
259
260 @Override
Paul Jensen869868be2014-05-15 10:33:05 -0400261 public boolean onOptionsItemSelected(MenuItem item) {
Hugo Benichi7f086e12016-12-06 15:36:30 +0900262 final Result result;
263 final String action;
264 final int id = item.getItemId();
265 switch (id) {
266 case R.id.action_use_network:
267 result = Result.WANTED_AS_IS;
268 action = "USE_NETWORK";
269 break;
270 case R.id.action_do_not_use_network:
271 result = Result.UNWANTED;
272 action = "DO_NOT_USE_NETWORK";
273 break;
274 default:
275 return super.onOptionsItemSelected(item);
Paul Jensen869868be2014-05-15 10:33:05 -0400276 }
Hugo Benichi7f086e12016-12-06 15:36:30 +0900277 if (DBG) {
278 Log.d(TAG, String.format("onOptionsItemSelect %s for %s", action, mUrl.toString()));
Paul Jensen869868be2014-05-15 10:33:05 -0400279 }
Hugo Benichi7f086e12016-12-06 15:36:30 +0900280 done(result);
281 return true;
Paul Jensen869868be2014-05-15 10:33:05 -0400282 }
283
Paul Jensen868f6242015-05-18 12:48:28 -0400284 @Override
285 public void onDestroy() {
286 super.onDestroy();
Takayuki, Oguracc387812018-09-13 12:40:28 +0900287 final WebView webview = (WebView) findViewById(R.id.webview);
288 if (webview != null) {
289 webview.stopLoading();
290 webview.setWebViewClient(null);
291 webview.setWebChromeClient(null);
292 webview.destroy();
293 }
Paul Jensen868f6242015-05-18 12:48:28 -0400294 if (mNetworkCallback != null) {
Hugo Benichia173a632017-06-17 12:47:33 +0900295 // mNetworkCallback is not null if mUrl is not null.
Paul Jensen868f6242015-05-18 12:48:28 -0400296 mCm.unregisterNetworkCallback(mNetworkCallback);
Paul Jensen868f6242015-05-18 12:48:28 -0400297 }
Paul Jensen65636fb2015-05-06 14:40:59 -0400298 if (mLaunchBrowser) {
299 // Give time for this network to become default. After 500ms just proceed.
300 for (int i = 0; i < 5; i++) {
301 // TODO: This misses when mNetwork underlies a VPN.
302 if (mNetwork.equals(mCm.getActiveNetwork())) break;
303 try {
304 Thread.sleep(100);
305 } catch (InterruptedException e) {
306 }
307 }
Hugo Benichi7f086e12016-12-06 15:36:30 +0900308 final String url = mUrl.toString();
309 if (DBG) {
310 Log.d(TAG, "starting activity with intent ACTION_VIEW for " + url);
311 }
312 startActivity(new Intent(Intent.ACTION_VIEW, Uri.parse(url)));
Paul Jensen65636fb2015-05-06 14:40:59 -0400313 }
Paul Jensen868f6242015-05-18 12:48:28 -0400314 }
315
Hugo Benichi7f086e12016-12-06 15:36:30 +0900316 private URL getUrl() {
317 String url = getIntent().getStringExtra(ConnectivityManager.EXTRA_CAPTIVE_PORTAL_URL);
318 if (url == null) {
319 url = mCm.getCaptivePortalServerUrl();
320 }
Hugo Benichia2066492017-05-17 09:26:30 +0900321 return makeURL(url);
322 }
323
324 private static URL makeURL(String url) {
Hugo Benichi7f086e12016-12-06 15:36:30 +0900325 try {
326 return new URL(url);
327 } catch (MalformedURLException e) {
Hugo Benichia2066492017-05-17 09:26:30 +0900328 Log.e(TAG, "Invalid URL " + url);
Hugo Benichi7f086e12016-12-06 15:36:30 +0900329 }
330 return null;
331 }
332
Hugo Benichi12df4652017-06-17 13:36:35 +0900333 private static String host(URL url) {
334 if (url == null) {
335 return null;
336 }
337 return url.getHost();
338 }
339
340 private static String sanitizeURL(URL url) {
341 // In non-Debug build, only show host to avoid leaking private info.
Remi NGUYEN VAN47274272019-01-30 23:39:24 +0900342 return isDebuggable() ? Objects.toString(url) : host(url);
343 }
344
345 private static boolean isDebuggable() {
346 return SystemProperties.getInt("ro.debuggable", 0) == 1;
Hugo Benichi12df4652017-06-17 13:36:35 +0900347 }
348
Paul Jensen869868be2014-05-15 10:33:05 -0400349 private void testForCaptivePortal() {
Hugo Benichi2c021972016-12-14 08:23:40 +0900350 // TODO: reuse NetworkMonitor facilities for consistent captive portal detection.
Paul Jensen869868be2014-05-15 10:33:05 -0400351 new Thread(new Runnable() {
352 public void run() {
353 // Give time for captive portal to open.
354 try {
355 Thread.sleep(1000);
356 } catch (InterruptedException e) {
357 }
358 HttpURLConnection urlConnection = null;
359 int httpResponseCode = 500;
Remi NGUYEN VAN9f855052018-05-22 17:04:17 +0900360 String locationHeader = null;
Paul Jensen869868be2014-05-15 10:33:05 -0400361 try {
Erik Klinef4fa9822018-04-27 22:48:33 +0900362 urlConnection = (HttpURLConnection) mNetwork.openConnection(mUrl);
Paul Jensen869868be2014-05-15 10:33:05 -0400363 urlConnection.setInstanceFollowRedirects(false);
364 urlConnection.setConnectTimeout(SOCKET_TIMEOUT_MS);
365 urlConnection.setReadTimeout(SOCKET_TIMEOUT_MS);
366 urlConnection.setUseCaches(false);
Hugo Benichi2c021972016-12-14 08:23:40 +0900367 if (mUserAgent != null) {
368 urlConnection.setRequestProperty("User-Agent", mUserAgent);
369 }
Hugo Benichiec88fd62017-03-07 15:10:03 +0900370 // cannot read request header after connection
371 String requestHeader = urlConnection.getRequestProperties().toString();
372
Paul Jensen869868be2014-05-15 10:33:05 -0400373 urlConnection.getInputStream();
374 httpResponseCode = urlConnection.getResponseCode();
Remi NGUYEN VAN9f855052018-05-22 17:04:17 +0900375 locationHeader = urlConnection.getHeaderField(HTTP_LOCATION_HEADER_NAME);
Hugo Benichiec88fd62017-03-07 15:10:03 +0900376 if (DBG) {
377 Log.d(TAG, "probe at " + mUrl +
378 " ret=" + httpResponseCode +
379 " request=" + requestHeader +
380 " headers=" + urlConnection.getHeaderFields());
381 }
Paul Jensen869868be2014-05-15 10:33:05 -0400382 } catch (IOException e) {
383 } finally {
384 if (urlConnection != null) urlConnection.disconnect();
385 }
Remi NGUYEN VAN9f855052018-05-22 17:04:17 +0900386 if (isDismissed(httpResponseCode, locationHeader, mProbeSpec)) {
Paul Jensen25a217c2015-02-27 22:55:47 -0500387 done(Result.DISMISSED);
Paul Jensen869868be2014-05-15 10:33:05 -0400388 }
389 }
390 }).start();
391 }
392
Remi NGUYEN VAN9f855052018-05-22 17:04:17 +0900393 private static boolean isDismissed(
394 int httpResponseCode, String locationHeader, CaptivePortalProbeSpec probeSpec) {
395 return (probeSpec != null)
396 ? probeSpec.getResult(httpResponseCode, locationHeader).isSuccessful()
397 : (httpResponseCode == 204);
398 }
399
Paul Jensen869868be2014-05-15 10:33:05 -0400400 private class MyWebViewClient extends WebViewClient {
Paul Jensen5344a4a2015-05-06 07:39:36 -0400401 private static final String INTERNAL_ASSETS = "file:///android_asset/";
Hugo Benichi60d5f462017-06-02 10:12:09 +0900402
Paul Jensen65636fb2015-05-06 14:40:59 -0400403 private final String mBrowserBailOutToken = Long.toString(new Random().nextLong());
Takayuki, Oguracc387812018-09-13 12:40:28 +0900404 private final String mCertificateOutToken = Long.toString(new Random().nextLong());
Paul Jensen65636fb2015-05-06 14:40:59 -0400405 // How many Android device-independent-pixels per scaled-pixel
406 // dp/sp = (px/sp) / (px/dp) = (1/sp) / (1/dp)
407 private final float mDpPerSp = TypedValue.applyDimension(TypedValue.COMPLEX_UNIT_SP, 1,
408 getResources().getDisplayMetrics()) /
409 TypedValue.applyDimension(TypedValue.COMPLEX_UNIT_DIP, 1,
410 getResources().getDisplayMetrics());
Paul Jensene836b682015-05-19 12:30:56 -0400411 private int mPagesLoaded;
lucaslin8efd38b2019-03-19 19:15:54 +0800412 private String mMainFrameUrl;
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));
Paul Jensene836b682015-05-19 12:30:56 -0400438 // For internally generated pages, leave URL bar listing prior URL as this is the URL
439 // the page refers to.
Hugo Benichi12df4652017-06-17 13:36:35 +0900440 if (!urlString.startsWith(INTERNAL_ASSETS)) {
441 String subtitle = (url != null) ? getHeaderSubtitle(url) : urlString;
442 getActionBar().setSubtitle(subtitle);
Paul Jensene836b682015-05-19 12:30:56 -0400443 }
Hugo Benichia2066492017-05-17 09:26:30 +0900444 getProgressBar().setVisibility(View.VISIBLE);
Paul Jensen869868be2014-05-15 10:33:05 -0400445 testForCaptivePortal();
446 }
447
448 @Override
449 public void onPageFinished(WebView view, String url) {
Paul Jensene836b682015-05-19 12:30:56 -0400450 mPagesLoaded++;
Hugo Benichia2066492017-05-17 09:26:30 +0900451 getProgressBar().setVisibility(View.INVISIBLE);
Chalard Jean8fa6ea32018-03-09 22:28:51 +0900452 mSwipeRefreshLayout.setRefreshing(false);
Paul Jensene836b682015-05-19 12:30:56 -0400453 if (mPagesLoaded == 1) {
Paul Jensen88eb0fa2014-10-02 13:43:39 -0400454 // Now that WebView has loaded at least one page we know it has read in the proxy
455 // settings. Now prompt the WebView read the Network-specific proxy settings.
456 setWebViewProxy();
457 // Load the real page.
Hugo Benichi7f086e12016-12-06 15:36:30 +0900458 view.loadUrl(mUrl.toString());
Paul Jensen88eb0fa2014-10-02 13:43:39 -0400459 return;
Paul Jensene836b682015-05-19 12:30:56 -0400460 } else if (mPagesLoaded == 2) {
461 // Prevent going back to empty first page.
susnata97640402017-06-23 09:13:05 -0700462 // Fix for missing focus, see b/62449959 for details. Remove it once we get a
463 // newer version of WebView (60.x.y).
464 view.requestFocus();
Paul Jensene836b682015-05-19 12:30:56 -0400465 view.clearHistory();
Paul Jensen5344a4a2015-05-06 07:39:36 -0400466 }
Paul Jensen869868be2014-05-15 10:33:05 -0400467 testForCaptivePortal();
468 }
Paul Jensenfc8022f2014-12-09 15:18:40 -0500469
Paul Jensen65636fb2015-05-06 14:40:59 -0400470 // Convert Android scaled-pixels (sp) to HTML size.
471 private String sp(int sp) {
472 // Convert sp to dp's.
473 float dp = sp * mDpPerSp;
474 // Apply a scale factor to make things look right.
475 dp *= 1.3;
476 // Convert dp's to HTML size.
Hugo Benichi60d5f462017-06-02 10:12:09 +0900477 // HTML px's are scaled just like dp's, so just add "px" suffix.
478 return Integer.toString((int)dp) + "px";
Paul Jensen65636fb2015-05-06 14:40:59 -0400479 }
480
lucaslin8efd38b2019-03-19 19:15:54 +0800481 // Check if webview is trying to load the main frame and record its url.
482 @Override
483 public boolean shouldOverrideUrlLoading(WebView view, WebResourceRequest request) {
484 if (request.isForMainFrame()) {
485 mMainFrameUrl = request.getUrl().toString();
486 }
487 return false;
488 }
489
Paul Jensenfc8022f2014-12-09 15:18:40 -0500490 // A web page consisting of a large broken lock icon to indicate SSL failure.
Paul Jensenfc8022f2014-12-09 15:18:40 -0500491
492 @Override
493 public void onReceivedSslError(WebView view, SslErrorHandler handler, SslError error) {
lucaslin8efd38b2019-03-19 19:15:54 +0800494 final URL errorUrl = makeURL(error.getUrl());
495 final URL mainFrameUrl = makeURL(mMainFrameUrl);
Hugo Benichi12df4652017-06-17 13:36:35 +0900496 Log.d(TAG, String.format("SSL error: %s, url: %s, certificate: %s",
lucaslin8efd38b2019-03-19 19:15:54 +0800497 sslErrorName(error), sanitizeURL(errorUrl), error.getCertificate()));
498 if (errorUrl == null
499 // Ignore SSL errors from resources by comparing the main frame url with SSL
500 // error url.
501 || !errorUrl.equals(mainFrameUrl)) {
502 Log.d(TAG, "onReceivedSslError: mMainFrameUrl = " + mMainFrameUrl);
Hugo Benichi12df4652017-06-17 13:36:35 +0900503 handler.cancel();
504 return;
Hugo Benichi60d5f462017-06-02 10:12:09 +0900505 }
Hugo Benichi12df4652017-06-17 13:36:35 +0900506 logMetricsEvent(MetricsEvent.CAPTIVE_PORTAL_LOGIN_ACTIVITY_SSL_ERROR);
507 final String sslErrorPage = makeSslErrorPage();
Hugo Benichi60d5f462017-06-02 10:12:09 +0900508 view.loadDataWithBaseURL(INTERNAL_ASSETS, sslErrorPage, "text/HTML", "UTF-8", null);
Takayuki, Oguracc387812018-09-13 12:40:28 +0900509 mSslErrorTitle = view.getTitle() == null ? "" : view.getTitle();
510 mSslErrorHandler = handler;
511 mSslError = error;
Hugo Benichi60d5f462017-06-02 10:12:09 +0900512 }
513
514 private String makeSslErrorPage() {
515 final String warningMsg = getString(R.string.ssl_error_warning);
516 final String exampleMsg = getString(R.string.ssl_error_example);
517 final String continueMsg = getString(R.string.ssl_error_continue);
Takayuki, Oguracc387812018-09-13 12:40:28 +0900518 final String certificateMsg = getString(R.string.ssl_error_view_certificate);
Hugo Benichi60d5f462017-06-02 10:12:09 +0900519 return String.join("\n",
520 "<html>",
521 "<head>",
522 " <meta name=\"viewport\" content=\"width=device-width, initial-scale=1\">",
523 " <style>",
524 " body {",
525 " background-color:#fafafa;",
526 " margin:auto;",
527 " width:80%;",
528 " margin-top: 96px",
529 " }",
530 " img {",
531 " height:48px;",
532 " width:48px;",
533 " }",
534 " div.warn {",
535 " font-size:" + sp(16) + ";",
536 " line-height:1.28;",
537 " margin-top:16px;",
538 " opacity:0.87;",
539 " }",
540 " div.example {",
541 " font-size:" + sp(14) + ";",
542 " line-height:1.21905;",
543 " margin-top:16px;",
544 " opacity:0.54;",
545 " }",
546 " a {",
547 " color:#4285F4;",
548 " display:inline-block;",
549 " font-size:" + sp(14) + ";",
550 " font-weight:bold;",
551 " height:48px;",
552 " margin-top:24px;",
553 " text-decoration:none;",
554 " text-transform:uppercase;",
555 " }",
Takayuki, Oguracc387812018-09-13 12:40:28 +0900556 " a.certificate {",
557 " margin-top:0px;",
558 " }",
Hugo Benichi60d5f462017-06-02 10:12:09 +0900559 " </style>",
560 "</head>",
561 "<body>",
562 " <p><img src=quantum_ic_warning_amber_96.png><br>",
563 " <div class=warn>" + warningMsg + "</div>",
564 " <div class=example>" + exampleMsg + "</div>",
Takayuki, Oguracc387812018-09-13 12:40:28 +0900565 " <a href=" + mBrowserBailOutToken + ">" + continueMsg + "</a><br>",
566 " <a class=certificate href=" + mCertificateOutToken + ">" + certificateMsg +
567 "</a>",
Hugo Benichi60d5f462017-06-02 10:12:09 +0900568 "</body>",
569 "</html>");
Paul Jensenfc8022f2014-12-09 15:18:40 -0500570 }
Paul Jensenfd54da92015-06-09 07:50:51 -0400571
572 @Override
573 public boolean shouldOverrideUrlLoading (WebView view, String url) {
574 if (url.startsWith("tel:")) {
575 startActivity(new Intent(Intent.ACTION_DIAL, Uri.parse(url)));
576 return true;
577 }
Takayuki, Oguracc387812018-09-13 12:40:28 +0900578 if (url.contains(mCertificateOutToken) && mSslError != null) {
579 showSslAlertDialog(mSslErrorHandler, mSslError, mSslErrorTitle);
580 return true;
581 }
Paul Jensenfd54da92015-06-09 07:50:51 -0400582 return false;
583 }
Takayuki, Oguracc387812018-09-13 12:40:28 +0900584 private void showSslAlertDialog(SslErrorHandler handler, SslError error, String title) {
585 final LayoutInflater factory = LayoutInflater.from(CaptivePortalLoginActivity.this);
586 final View sslWarningView = factory.inflate(R.layout.ssl_warning, null);
587
588 // Set Security certificate
589 setViewSecurityCertificate(sslWarningView.findViewById(R.id.certificate_layout), error);
590 ((TextView) sslWarningView.findViewById(R.id.ssl_error_type))
591 .setText(sslErrorName(error));
592 ((TextView) sslWarningView.findViewById(R.id.title)).setText(mSslErrorTitle);
593 ((TextView) sslWarningView.findViewById(R.id.address)).setText(error.getUrl());
594
595 AlertDialog sslAlertDialog = new AlertDialog.Builder(CaptivePortalLoginActivity.this)
596 .setTitle(R.string.ssl_security_warning_title)
597 .setView(sslWarningView)
598 .setPositiveButton(R.string.ok, (DialogInterface dialog, int whichButton) -> {
599 // handler.cancel is called via OnCancelListener.
600 dialog.cancel();
601 })
602 .setOnCancelListener((DialogInterface dialogInterface) -> handler.cancel())
603 .create();
604 sslAlertDialog.show();
605 }
606
607 private void setViewSecurityCertificate(LinearLayout certificateLayout, SslError error) {
Remi NGUYEN VAN47274272019-01-30 23:39:24 +0900608 ((TextView) certificateLayout.findViewById(R.id.ssl_error_msg))
609 .setText(sslErrorMessage(error));
Takayuki, Oguracc387812018-09-13 12:40:28 +0900610 SslCertificate cert = error.getCertificate();
Remi NGUYEN VAN47274272019-01-30 23:39:24 +0900611 // TODO: call the method directly once inflateCertificateView is @SystemApi
612 try {
613 final View certificateView = (View) SslCertificate.class.getMethod(
614 "inflateCertificateView", Context.class)
615 .invoke(cert, CaptivePortalLoginActivity.this);
616 certificateLayout.addView(certificateView);
617 } catch (ReflectiveOperationException | SecurityException e) {
618 Log.e(TAG, "Could not create certificate view", e);
619 }
Takayuki, Oguracc387812018-09-13 12:40:28 +0900620 }
Paul Jensen869868be2014-05-15 10:33:05 -0400621 }
622
623 private class MyWebChromeClient extends WebChromeClient {
624 @Override
625 public void onProgressChanged(WebView view, int newProgress) {
Hugo Benichia2066492017-05-17 09:26:30 +0900626 getProgressBar().setProgress(newProgress);
Paul Jensen869868be2014-05-15 10:33:05 -0400627 }
628 }
Hugo Benichia2066492017-05-17 09:26:30 +0900629
630 private ProgressBar getProgressBar() {
631 return findViewById(R.id.progress_bar);
632 }
633
634 private WebView getWebview() {
635 return findViewById(R.id.webview);
636 }
637
638 private String getHeaderTitle() {
Hugo Benichi3a222972017-06-01 12:58:49 +0900639 NetworkCapabilities nc = mCm.getNetworkCapabilities(mNetwork);
Remi NGUYEN VAN47274272019-01-30 23:39:24 +0900640 final String ssid = getSsid();
641 if (TextUtils.isEmpty(ssid)
642 || nc == null || !nc.hasTransport(NetworkCapabilities.TRANSPORT_WIFI)) {
Hugo Benichi3a222972017-06-01 12:58:49 +0900643 return getString(R.string.action_bar_label);
644 }
Remi NGUYEN VAN47274272019-01-30 23:39:24 +0900645 return getString(R.string.action_bar_title, ssid);
646 }
647
648 // TODO: remove once SSID is obtained from NetworkCapabilities
649 private String getSsid() {
650 if (mWifiManager == null) {
651 return null;
652 }
653 final WifiInfo wifiInfo = mWifiManager.getConnectionInfo();
654 return removeDoubleQuotes(wifiInfo.getSSID());
655 }
656
657 private static String removeDoubleQuotes(String string) {
658 if (string == null) return null;
659 final int length = string.length();
660 if ((length > 1) && (string.charAt(0) == '"') && (string.charAt(length - 1) == '"')) {
661 return string.substring(1, length - 1);
662 }
663 return string;
Hugo Benichia2066492017-05-17 09:26:30 +0900664 }
665
Hugo Benichi12df4652017-06-17 13:36:35 +0900666 private String getHeaderSubtitle(URL url) {
667 String host = host(url);
Hugo Benichia2066492017-05-17 09:26:30 +0900668 final String https = "https";
669 if (https.equals(url.getProtocol())) {
Hugo Benichi12df4652017-06-17 13:36:35 +0900670 return https + "://" + host;
Hugo Benichia2066492017-05-17 09:26:30 +0900671 }
Hugo Benichi12df4652017-06-17 13:36:35 +0900672 return host;
Hugo Benichia2066492017-05-17 09:26:30 +0900673 }
Hugo Benichi9e8ab432017-06-05 14:52:24 +0900674
675 private void logMetricsEvent(int event) {
Remi NGUYEN VANde602212019-01-30 15:22:01 +0900676 mCaptivePortal.logEvent(event, getPackageName());
Hugo Benichi9e8ab432017-06-05 14:52:24 +0900677 }
Hugo Benichi04d78602017-07-19 21:20:53 +0900678
679 private static final SparseArray<String> SSL_ERRORS = new SparseArray<>();
680 static {
681 SSL_ERRORS.put(SslError.SSL_NOTYETVALID, "SSL_NOTYETVALID");
682 SSL_ERRORS.put(SslError.SSL_EXPIRED, "SSL_EXPIRED");
683 SSL_ERRORS.put(SslError.SSL_IDMISMATCH, "SSL_IDMISMATCH");
684 SSL_ERRORS.put(SslError.SSL_UNTRUSTED, "SSL_UNTRUSTED");
685 SSL_ERRORS.put(SslError.SSL_DATE_INVALID, "SSL_DATE_INVALID");
686 SSL_ERRORS.put(SslError.SSL_INVALID, "SSL_INVALID");
687 }
688
689 private static String sslErrorName(SslError error) {
690 return SSL_ERRORS.get(error.getPrimaryError(), "UNKNOWN");
691 }
Takayuki, Oguracc387812018-09-13 12:40:28 +0900692
693 private static final SparseArray<Integer> SSL_ERROR_MSGS = new SparseArray<>();
694 static {
695 SSL_ERROR_MSGS.put(SslError.SSL_NOTYETVALID, R.string.ssl_error_not_yet_valid);
696 SSL_ERROR_MSGS.put(SslError.SSL_EXPIRED, R.string.ssl_error_expired);
697 SSL_ERROR_MSGS.put(SslError.SSL_IDMISMATCH, R.string.ssl_error_mismatch);
698 SSL_ERROR_MSGS.put(SslError.SSL_UNTRUSTED, R.string.ssl_error_untrusted);
699 SSL_ERROR_MSGS.put(SslError.SSL_DATE_INVALID, R.string.ssl_error_date_invalid);
700 SSL_ERROR_MSGS.put(SslError.SSL_INVALID, R.string.ssl_error_invalid);
701 }
702
703 private static Integer sslErrorMessage(SslError error) {
704 return SSL_ERROR_MSGS.get(error.getPrimaryError(), R.string.ssl_error_unknown);
705 }
Paul Jensen869868be2014-05-15 10:33:05 -0400706}