1 /* 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 17 package com.android.captiveportallogin; 18 19 import static android.net.ConnectivityManager.EXTRA_CAPTIVE_PORTAL_PROBE_SPEC; 20 21 import android.app.Activity; 22 import android.app.AlertDialog; 23 import android.app.Application; 24 import android.content.Context; 25 import android.content.DialogInterface; 26 import android.content.Intent; 27 import android.graphics.Bitmap; 28 import android.net.CaptivePortal; 29 import android.net.ConnectivityManager; 30 import android.net.ConnectivityManager.NetworkCallback; 31 import android.net.Network; 32 import android.net.NetworkCapabilities; 33 import android.net.NetworkRequest; 34 import android.net.Proxy; 35 import android.net.Uri; 36 import android.net.captiveportal.CaptivePortalProbeSpec; 37 import android.net.http.SslCertificate; 38 import android.net.http.SslError; 39 import android.net.wifi.WifiInfo; 40 import android.net.wifi.WifiManager; 41 import android.os.Bundle; 42 import android.os.SystemProperties; 43 import android.text.TextUtils; 44 import android.util.ArrayMap; 45 import android.util.Log; 46 import android.util.SparseArray; 47 import android.util.TypedValue; 48 import android.view.LayoutInflater; 49 import android.view.Menu; 50 import android.view.MenuItem; 51 import android.view.View; 52 import android.webkit.CookieManager; 53 import android.webkit.SslErrorHandler; 54 import android.webkit.WebChromeClient; 55 import android.webkit.WebResourceRequest; 56 import android.webkit.WebSettings; 57 import android.webkit.WebView; 58 import android.webkit.WebViewClient; 59 import android.widget.LinearLayout; 60 import android.widget.ProgressBar; 61 import android.widget.TextView; 62 63 import androidx.swiperefreshlayout.widget.SwipeRefreshLayout; 64 65 import com.android.internal.logging.nano.MetricsProto.MetricsEvent; 66 67 import java.io.IOException; 68 import java.lang.reflect.Field; 69 import java.lang.reflect.Method; 70 import java.net.HttpURLConnection; 71 import java.net.MalformedURLException; 72 import java.net.URL; 73 import java.util.Objects; 74 import java.util.Random; 75 import java.util.concurrent.atomic.AtomicBoolean; 76 77 public class CaptivePortalLoginActivity extends Activity { 78 private static final String TAG = CaptivePortalLoginActivity.class.getSimpleName(); 79 private static final boolean DBG = true; 80 private static final boolean VDBG = false; 81 82 private static final int SOCKET_TIMEOUT_MS = 10000; 83 public static final String HTTP_LOCATION_HEADER_NAME = "Location"; 84 85 private enum Result { 86 DISMISSED(MetricsEvent.ACTION_CAPTIVE_PORTAL_LOGIN_RESULT_DISMISSED), 87 UNWANTED(MetricsEvent.ACTION_CAPTIVE_PORTAL_LOGIN_RESULT_UNWANTED), 88 WANTED_AS_IS(MetricsEvent.ACTION_CAPTIVE_PORTAL_LOGIN_RESULT_WANTED_AS_IS); 89 90 final int metricsEvent; Result(int metricsEvent)91 Result(int metricsEvent) { this.metricsEvent = metricsEvent; } 92 }; 93 94 private URL mUrl; 95 private CaptivePortalProbeSpec mProbeSpec; 96 private String mUserAgent; 97 private Network mNetwork; 98 private CaptivePortal mCaptivePortal; 99 private NetworkCallback mNetworkCallback; 100 private ConnectivityManager mCm; 101 private WifiManager mWifiManager; 102 private boolean mLaunchBrowser = false; 103 private MyWebViewClient mWebViewClient; 104 private SwipeRefreshLayout mSwipeRefreshLayout; 105 // Ensures that done() happens once exactly, handling concurrent callers with atomic operations. 106 private final AtomicBoolean isDone = new AtomicBoolean(false); 107 108 @Override onCreate(Bundle savedInstanceState)109 protected void onCreate(Bundle savedInstanceState) { 110 super.onCreate(savedInstanceState); 111 112 mCaptivePortal = getIntent().getParcelableExtra(ConnectivityManager.EXTRA_CAPTIVE_PORTAL); 113 logMetricsEvent(MetricsEvent.ACTION_CAPTIVE_PORTAL_LOGIN_ACTIVITY); 114 115 mCm = getSystemService(ConnectivityManager.class); 116 mWifiManager = getSystemService(WifiManager.class); 117 mNetwork = getIntent().getParcelableExtra(ConnectivityManager.EXTRA_NETWORK); 118 mUserAgent = 119 getIntent().getStringExtra(ConnectivityManager.EXTRA_CAPTIVE_PORTAL_USER_AGENT); 120 mUrl = getUrl(); 121 if (mUrl == null) { 122 // getUrl() failed to parse the url provided in the intent: bail out in a way that 123 // at least provides network access. 124 done(Result.WANTED_AS_IS); 125 return; 126 } 127 if (DBG) { 128 Log.d(TAG, String.format("onCreate for %s", mUrl.toString())); 129 } 130 131 final String spec = getIntent().getStringExtra(EXTRA_CAPTIVE_PORTAL_PROBE_SPEC); 132 try { 133 mProbeSpec = CaptivePortalProbeSpec.parseSpecOrNull(spec); 134 } catch (Exception e) { 135 // Make extra sure that invalid configurations do not cause crashes 136 mProbeSpec = null; 137 } 138 139 mNetworkCallback = new NetworkCallback() { 140 @Override 141 public void onLost(Network lostNetwork) { 142 // If the network disappears while the app is up, exit. 143 if (mNetwork.equals(lostNetwork)) done(Result.UNWANTED); 144 } 145 }; 146 mCm.registerNetworkCallback(new NetworkRequest.Builder().build(), mNetworkCallback); 147 148 // If the network has disappeared, exit. 149 final NetworkCapabilities networkCapabilities = mCm.getNetworkCapabilities(mNetwork); 150 if (networkCapabilities == null) { 151 finishAndRemoveTask(); 152 return; 153 } 154 155 // Also initializes proxy system properties. 156 mNetwork = mNetwork.getPrivateDnsBypassingCopy(); 157 mCm.bindProcessToNetwork(mNetwork); 158 159 // Proxy system properties must be initialized before setContentView is called because 160 // setContentView initializes the WebView logic which in turn reads the system properties. 161 setContentView(R.layout.activity_captive_portal_login); 162 163 getActionBar().setDisplayShowHomeEnabled(false); 164 getActionBar().setElevation(0); // remove shadow 165 getActionBar().setTitle(getHeaderTitle()); 166 getActionBar().setSubtitle(""); 167 168 final WebView webview = getWebview(); 169 webview.clearCache(true); 170 CookieManager.getInstance().setAcceptThirdPartyCookies(webview, true); 171 WebSettings webSettings = webview.getSettings(); 172 webSettings.setJavaScriptEnabled(true); 173 webSettings.setMixedContentMode(WebSettings.MIXED_CONTENT_COMPATIBILITY_MODE); 174 webSettings.setUseWideViewPort(true); 175 webSettings.setLoadWithOverviewMode(true); 176 webSettings.setSupportZoom(true); 177 webSettings.setBuiltInZoomControls(true); 178 webSettings.setDisplayZoomControls(false); 179 webSettings.setDomStorageEnabled(true); 180 mWebViewClient = new MyWebViewClient(); 181 webview.setWebViewClient(mWebViewClient); 182 webview.setWebChromeClient(new MyWebChromeClient()); 183 // Start initial page load so WebView finishes loading proxy settings. 184 // Actual load of mUrl is initiated by MyWebViewClient. 185 webview.loadData("", "text/html", null); 186 187 mSwipeRefreshLayout = findViewById(R.id.swipe_refresh); 188 mSwipeRefreshLayout.setOnRefreshListener(() -> { 189 webview.reload(); 190 mSwipeRefreshLayout.setRefreshing(true); 191 }); 192 193 } 194 195 // Find WebView's proxy BroadcastReceiver and prompt it to read proxy system properties. setWebViewProxy()196 private void setWebViewProxy() { 197 // TODO: migrate to androidx WebView proxy setting API as soon as it is finalized 198 try { 199 final Field loadedApkField = Application.class.getDeclaredField("mLoadedApk"); 200 final Class<?> loadedApkClass = loadedApkField.getType(); 201 final Object loadedApk = loadedApkField.get(getApplication()); 202 Field receiversField = loadedApkClass.getDeclaredField("mReceivers"); 203 receiversField.setAccessible(true); 204 ArrayMap receivers = (ArrayMap) receiversField.get(loadedApk); 205 for (Object receiverMap : receivers.values()) { 206 for (Object rec : ((ArrayMap) receiverMap).keySet()) { 207 Class clazz = rec.getClass(); 208 if (clazz.getName().contains("ProxyChangeListener")) { 209 Method onReceiveMethod = clazz.getDeclaredMethod("onReceive", Context.class, 210 Intent.class); 211 Intent intent = new Intent(Proxy.PROXY_CHANGE_ACTION); 212 onReceiveMethod.invoke(rec, getApplicationContext(), intent); 213 Log.v(TAG, "Prompting WebView proxy reload."); 214 } 215 } 216 } 217 } catch (Exception e) { 218 Log.e(TAG, "Exception while setting WebView proxy: " + e); 219 } 220 } 221 done(Result result)222 private void done(Result result) { 223 if (isDone.getAndSet(true)) { 224 // isDone was already true: done() already called 225 return; 226 } 227 if (DBG) { 228 Log.d(TAG, String.format("Result %s for %s", result.name(), mUrl.toString())); 229 } 230 logMetricsEvent(result.metricsEvent); 231 switch (result) { 232 case DISMISSED: 233 mCaptivePortal.reportCaptivePortalDismissed(); 234 break; 235 case UNWANTED: 236 mCaptivePortal.ignoreNetwork(); 237 break; 238 case WANTED_AS_IS: 239 mCaptivePortal.useNetwork(); 240 break; 241 } 242 finishAndRemoveTask(); 243 } 244 245 @Override onCreateOptionsMenu(Menu menu)246 public boolean onCreateOptionsMenu(Menu menu) { 247 getMenuInflater().inflate(R.menu.captive_portal_login, menu); 248 return true; 249 } 250 251 @Override onBackPressed()252 public void onBackPressed() { 253 WebView myWebView = findViewById(R.id.webview); 254 if (myWebView.canGoBack() && mWebViewClient.allowBack()) { 255 myWebView.goBack(); 256 } else { 257 super.onBackPressed(); 258 } 259 } 260 261 @Override onOptionsItemSelected(MenuItem item)262 public boolean onOptionsItemSelected(MenuItem item) { 263 final Result result; 264 final String action; 265 final int id = item.getItemId(); 266 switch (id) { 267 case R.id.action_use_network: 268 result = Result.WANTED_AS_IS; 269 action = "USE_NETWORK"; 270 break; 271 case R.id.action_do_not_use_network: 272 result = Result.UNWANTED; 273 action = "DO_NOT_USE_NETWORK"; 274 break; 275 default: 276 return super.onOptionsItemSelected(item); 277 } 278 if (DBG) { 279 Log.d(TAG, String.format("onOptionsItemSelect %s for %s", action, mUrl.toString())); 280 } 281 done(result); 282 return true; 283 } 284 285 @Override onDestroy()286 public void onDestroy() { 287 super.onDestroy(); 288 final WebView webview = (WebView) findViewById(R.id.webview); 289 if (webview != null) { 290 webview.stopLoading(); 291 webview.setWebViewClient(null); 292 webview.setWebChromeClient(null); 293 webview.destroy(); 294 } 295 if (mNetworkCallback != null) { 296 // mNetworkCallback is not null if mUrl is not null. 297 mCm.unregisterNetworkCallback(mNetworkCallback); 298 } 299 if (mLaunchBrowser) { 300 // Give time for this network to become default. After 500ms just proceed. 301 for (int i = 0; i < 5; i++) { 302 // TODO: This misses when mNetwork underlies a VPN. 303 if (mNetwork.equals(mCm.getActiveNetwork())) break; 304 try { 305 Thread.sleep(100); 306 } catch (InterruptedException e) { 307 } 308 } 309 final String url = mUrl.toString(); 310 if (DBG) { 311 Log.d(TAG, "starting activity with intent ACTION_VIEW for " + url); 312 } 313 startActivity(new Intent(Intent.ACTION_VIEW, Uri.parse(url))); 314 } 315 } 316 getUrl()317 private URL getUrl() { 318 String url = getIntent().getStringExtra(ConnectivityManager.EXTRA_CAPTIVE_PORTAL_URL); 319 if (url == null) { 320 url = mCm.getCaptivePortalServerUrl(); 321 } 322 return makeURL(url); 323 } 324 makeURL(String url)325 private static URL makeURL(String url) { 326 try { 327 return new URL(url); 328 } catch (MalformedURLException e) { 329 Log.e(TAG, "Invalid URL " + url); 330 } 331 return null; 332 } 333 host(URL url)334 private static String host(URL url) { 335 if (url == null) { 336 return null; 337 } 338 return url.getHost(); 339 } 340 sanitizeURL(URL url)341 private static String sanitizeURL(URL url) { 342 // In non-Debug build, only show host to avoid leaking private info. 343 return isDebuggable() ? Objects.toString(url) : host(url); 344 } 345 isDebuggable()346 private static boolean isDebuggable() { 347 return SystemProperties.getInt("ro.debuggable", 0) == 1; 348 } 349 testForCaptivePortal()350 private void testForCaptivePortal() { 351 // TODO: reuse NetworkMonitor facilities for consistent captive portal detection. 352 new Thread(new Runnable() { 353 public void run() { 354 // Give time for captive portal to open. 355 try { 356 Thread.sleep(1000); 357 } catch (InterruptedException e) { 358 } 359 HttpURLConnection urlConnection = null; 360 int httpResponseCode = 500; 361 String locationHeader = null; 362 try { 363 urlConnection = (HttpURLConnection) mNetwork.openConnection(mUrl); 364 urlConnection.setInstanceFollowRedirects(false); 365 urlConnection.setConnectTimeout(SOCKET_TIMEOUT_MS); 366 urlConnection.setReadTimeout(SOCKET_TIMEOUT_MS); 367 urlConnection.setUseCaches(false); 368 if (mUserAgent != null) { 369 urlConnection.setRequestProperty("User-Agent", mUserAgent); 370 } 371 // cannot read request header after connection 372 String requestHeader = urlConnection.getRequestProperties().toString(); 373 374 urlConnection.getInputStream(); 375 httpResponseCode = urlConnection.getResponseCode(); 376 locationHeader = urlConnection.getHeaderField(HTTP_LOCATION_HEADER_NAME); 377 if (DBG) { 378 Log.d(TAG, "probe at " + mUrl + 379 " ret=" + httpResponseCode + 380 " request=" + requestHeader + 381 " headers=" + urlConnection.getHeaderFields()); 382 } 383 } catch (IOException e) { 384 } finally { 385 if (urlConnection != null) urlConnection.disconnect(); 386 } 387 if (isDismissed(httpResponseCode, locationHeader, mProbeSpec)) { 388 done(Result.DISMISSED); 389 } 390 } 391 }).start(); 392 } 393 isDismissed( int httpResponseCode, String locationHeader, CaptivePortalProbeSpec probeSpec)394 private static boolean isDismissed( 395 int httpResponseCode, String locationHeader, CaptivePortalProbeSpec probeSpec) { 396 return (probeSpec != null) 397 ? probeSpec.getResult(httpResponseCode, locationHeader).isSuccessful() 398 : (httpResponseCode == 204); 399 } 400 401 private class MyWebViewClient extends WebViewClient { 402 private static final String INTERNAL_ASSETS = "file:///android_asset/"; 403 404 private final String mBrowserBailOutToken = Long.toString(new Random().nextLong()); 405 private final String mCertificateOutToken = Long.toString(new Random().nextLong()); 406 // How many Android device-independent-pixels per scaled-pixel 407 // dp/sp = (px/sp) / (px/dp) = (1/sp) / (1/dp) 408 private final float mDpPerSp = TypedValue.applyDimension(TypedValue.COMPLEX_UNIT_SP, 1, 409 getResources().getDisplayMetrics()) / 410 TypedValue.applyDimension(TypedValue.COMPLEX_UNIT_DIP, 1, 411 getResources().getDisplayMetrics()); 412 private int mPagesLoaded; 413 private String mMainFrameUrl; 414 415 // If we haven't finished cleaning up the history, don't allow going back. allowBack()416 public boolean allowBack() { 417 return mPagesLoaded > 1; 418 } 419 420 private String mSslErrorTitle = null; 421 private SslErrorHandler mSslErrorHandler = null; 422 private SslError mSslError = null; 423 424 @Override onPageStarted(WebView view, String urlString, Bitmap favicon)425 public void onPageStarted(WebView view, String urlString, Bitmap favicon) { 426 if (urlString.contains(mBrowserBailOutToken)) { 427 mLaunchBrowser = true; 428 done(Result.WANTED_AS_IS); 429 return; 430 } 431 // The first page load is used only to cause the WebView to 432 // fetch the proxy settings. Don't update the URL bar, and 433 // don't check if the captive portal is still there. 434 if (mPagesLoaded == 0) { 435 return; 436 } 437 final URL url = makeURL(urlString); 438 Log.d(TAG, "onPageStarted: " + sanitizeURL(url)); 439 // For internally generated pages, leave URL bar listing prior URL as this is the URL 440 // the page refers to. 441 if (!urlString.startsWith(INTERNAL_ASSETS)) { 442 String subtitle = (url != null) ? getHeaderSubtitle(url) : urlString; 443 getActionBar().setSubtitle(subtitle); 444 } 445 getProgressBar().setVisibility(View.VISIBLE); 446 testForCaptivePortal(); 447 } 448 449 @Override onPageFinished(WebView view, String url)450 public void onPageFinished(WebView view, String url) { 451 mPagesLoaded++; 452 getProgressBar().setVisibility(View.INVISIBLE); 453 mSwipeRefreshLayout.setRefreshing(false); 454 if (mPagesLoaded == 1) { 455 // 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. 459 view.loadUrl(mUrl.toString()); 460 return; 461 } else if (mPagesLoaded == 2) { 462 // Prevent going back to empty first page. 463 // 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(); 466 view.clearHistory(); 467 } 468 testForCaptivePortal(); 469 } 470 471 // Convert Android scaled-pixels (sp) to HTML size. sp(int sp)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. 478 // HTML px's are scaled just like dp's, so just add "px" suffix. 479 return Integer.toString((int)dp) + "px"; 480 } 481 482 // Check if webview is trying to load the main frame and record its url. 483 @Override shouldOverrideUrlLoading(WebView view, WebResourceRequest request)484 public boolean shouldOverrideUrlLoading(WebView view, WebResourceRequest request) { 485 if (request.isForMainFrame()) { 486 mMainFrameUrl = request.getUrl().toString(); 487 } 488 // Be careful that two shouldOverrideUrlLoading methods are overridden, but 489 // shouldOverrideUrlLoading(WebView view, String url) was deprecated in API level 24. 490 // TODO: delete deprecated one ?? 491 return shouldOverrideUrlLoading(view, mMainFrameUrl); 492 } 493 494 // A web page consisting of a large broken lock icon to indicate SSL failure. 495 496 @Override onReceivedSslError(WebView view, SslErrorHandler handler, SslError error)497 public void onReceivedSslError(WebView view, SslErrorHandler handler, SslError error) { 498 final URL errorUrl = makeURL(error.getUrl()); 499 final URL mainFrameUrl = makeURL(mMainFrameUrl); 500 Log.d(TAG, String.format("SSL error: %s, url: %s, certificate: %s", 501 sslErrorName(error), sanitizeURL(errorUrl), error.getCertificate())); 502 if (errorUrl == null 503 // Ignore SSL errors from resources by comparing the main frame url with SSL 504 // error url. 505 || !errorUrl.equals(mainFrameUrl)) { 506 Log.d(TAG, "onReceivedSslError: mMainFrameUrl = " + mMainFrameUrl); 507 handler.cancel(); 508 return; 509 } 510 logMetricsEvent(MetricsEvent.CAPTIVE_PORTAL_LOGIN_ACTIVITY_SSL_ERROR); 511 final String sslErrorPage = makeSslErrorPage(); 512 view.loadDataWithBaseURL(INTERNAL_ASSETS, sslErrorPage, "text/HTML", "UTF-8", null); 513 mSslErrorTitle = view.getTitle() == null ? "" : view.getTitle(); 514 mSslErrorHandler = handler; 515 mSslError = error; 516 } 517 makeSslErrorPage()518 private String makeSslErrorPage() { 519 final String warningMsg = getString(R.string.ssl_error_warning); 520 final String exampleMsg = getString(R.string.ssl_error_example); 521 final String continueMsg = getString(R.string.ssl_error_continue); 522 final String certificateMsg = getString(R.string.ssl_error_view_certificate); 523 return String.join("\n", 524 "<html>", 525 "<head>", 526 " <meta name=\"viewport\" content=\"width=device-width, initial-scale=1\">", 527 " <style>", 528 " body {", 529 " background-color:#fafafa;", 530 " margin:auto;", 531 " width:80%;", 532 " margin-top: 96px", 533 " }", 534 " img {", 535 " height:48px;", 536 " width:48px;", 537 " }", 538 " div.warn {", 539 " font-size:" + sp(16) + ";", 540 " line-height:1.28;", 541 " margin-top:16px;", 542 " opacity:0.87;", 543 " }", 544 " div.example {", 545 " font-size:" + sp(14) + ";", 546 " line-height:1.21905;", 547 " margin-top:16px;", 548 " opacity:0.54;", 549 " }", 550 " a {", 551 " color:#4285F4;", 552 " display:inline-block;", 553 " font-size:" + sp(14) + ";", 554 " font-weight:bold;", 555 " height:48px;", 556 " margin-top:24px;", 557 " text-decoration:none;", 558 " text-transform:uppercase;", 559 " }", 560 " a.certificate {", 561 " margin-top:0px;", 562 " }", 563 " </style>", 564 "</head>", 565 "<body>", 566 " <p><img src=quantum_ic_warning_amber_96.png><br>", 567 " <div class=warn>" + warningMsg + "</div>", 568 " <div class=example>" + exampleMsg + "</div>", 569 " <a href=" + mBrowserBailOutToken + ">" + continueMsg + "</a><br>", 570 " <a class=certificate href=" + mCertificateOutToken + ">" + certificateMsg + 571 "</a>", 572 "</body>", 573 "</html>"); 574 } 575 576 @Override shouldOverrideUrlLoading(WebView view, String url)577 public boolean shouldOverrideUrlLoading (WebView view, String url) { 578 if (url.startsWith("tel:")) { 579 startActivity(new Intent(Intent.ACTION_DIAL, Uri.parse(url))); 580 return true; 581 } 582 if (url.contains(mCertificateOutToken) && mSslError != null) { 583 showSslAlertDialog(mSslErrorHandler, mSslError, mSslErrorTitle); 584 return true; 585 } 586 return false; 587 } showSslAlertDialog(SslErrorHandler handler, SslError error, String title)588 private void showSslAlertDialog(SslErrorHandler handler, SslError error, String title) { 589 final LayoutInflater factory = LayoutInflater.from(CaptivePortalLoginActivity.this); 590 final View sslWarningView = factory.inflate(R.layout.ssl_warning, null); 591 592 // Set Security certificate 593 setViewSecurityCertificate(sslWarningView.findViewById(R.id.certificate_layout), error); 594 ((TextView) sslWarningView.findViewById(R.id.ssl_error_type)) 595 .setText(sslErrorName(error)); 596 ((TextView) sslWarningView.findViewById(R.id.title)).setText(mSslErrorTitle); 597 ((TextView) sslWarningView.findViewById(R.id.address)).setText(error.getUrl()); 598 599 AlertDialog sslAlertDialog = new AlertDialog.Builder(CaptivePortalLoginActivity.this) 600 .setTitle(R.string.ssl_security_warning_title) 601 .setView(sslWarningView) 602 .setPositiveButton(R.string.ok, (DialogInterface dialog, int whichButton) -> { 603 // handler.cancel is called via OnCancelListener. 604 dialog.cancel(); 605 }) 606 .setOnCancelListener((DialogInterface dialogInterface) -> handler.cancel()) 607 .create(); 608 sslAlertDialog.show(); 609 } 610 setViewSecurityCertificate(LinearLayout certificateLayout, SslError error)611 private void setViewSecurityCertificate(LinearLayout certificateLayout, SslError error) { 612 ((TextView) certificateLayout.findViewById(R.id.ssl_error_msg)) 613 .setText(sslErrorMessage(error)); 614 SslCertificate cert = error.getCertificate(); 615 // TODO: call the method directly once inflateCertificateView is @SystemApi 616 try { 617 final View certificateView = (View) SslCertificate.class.getMethod( 618 "inflateCertificateView", Context.class) 619 .invoke(cert, CaptivePortalLoginActivity.this); 620 certificateLayout.addView(certificateView); 621 } catch (ReflectiveOperationException | SecurityException e) { 622 Log.e(TAG, "Could not create certificate view", e); 623 } 624 } 625 } 626 627 private class MyWebChromeClient extends WebChromeClient { 628 @Override onProgressChanged(WebView view, int newProgress)629 public void onProgressChanged(WebView view, int newProgress) { 630 getProgressBar().setProgress(newProgress); 631 } 632 } 633 getProgressBar()634 private ProgressBar getProgressBar() { 635 return findViewById(R.id.progress_bar); 636 } 637 getWebview()638 private WebView getWebview() { 639 return findViewById(R.id.webview); 640 } 641 getHeaderTitle()642 private String getHeaderTitle() { 643 NetworkCapabilities nc = mCm.getNetworkCapabilities(mNetwork); 644 final String ssid = getSsid(); 645 if (TextUtils.isEmpty(ssid) 646 || nc == null || !nc.hasTransport(NetworkCapabilities.TRANSPORT_WIFI)) { 647 return getString(R.string.action_bar_label); 648 } 649 return getString(R.string.action_bar_title, ssid); 650 } 651 652 // TODO: remove once SSID is obtained from NetworkCapabilities getSsid()653 private String getSsid() { 654 if (mWifiManager == null) { 655 return null; 656 } 657 final WifiInfo wifiInfo = mWifiManager.getConnectionInfo(); 658 return removeDoubleQuotes(wifiInfo.getSSID()); 659 } 660 removeDoubleQuotes(String string)661 private static String removeDoubleQuotes(String string) { 662 if (string == null) return null; 663 final int length = string.length(); 664 if ((length > 1) && (string.charAt(0) == '"') && (string.charAt(length - 1) == '"')) { 665 return string.substring(1, length - 1); 666 } 667 return string; 668 } 669 getHeaderSubtitle(URL url)670 private String getHeaderSubtitle(URL url) { 671 String host = host(url); 672 final String https = "https"; 673 if (https.equals(url.getProtocol())) { 674 return https + "://" + host; 675 } 676 return host; 677 } 678 logMetricsEvent(int event)679 private void logMetricsEvent(int event) { 680 mCaptivePortal.logEvent(event, getPackageName()); 681 } 682 683 private static final SparseArray<String> SSL_ERRORS = new SparseArray<>(); 684 static { SSL_ERRORS.put(SslError.SSL_NOTYETVALID, "SSL_NOTYETVALID")685 SSL_ERRORS.put(SslError.SSL_NOTYETVALID, "SSL_NOTYETVALID"); SSL_ERRORS.put(SslError.SSL_EXPIRED, "SSL_EXPIRED")686 SSL_ERRORS.put(SslError.SSL_EXPIRED, "SSL_EXPIRED"); SSL_ERRORS.put(SslError.SSL_IDMISMATCH, "SSL_IDMISMATCH")687 SSL_ERRORS.put(SslError.SSL_IDMISMATCH, "SSL_IDMISMATCH"); SSL_ERRORS.put(SslError.SSL_UNTRUSTED, "SSL_UNTRUSTED")688 SSL_ERRORS.put(SslError.SSL_UNTRUSTED, "SSL_UNTRUSTED"); SSL_ERRORS.put(SslError.SSL_DATE_INVALID, "SSL_DATE_INVALID")689 SSL_ERRORS.put(SslError.SSL_DATE_INVALID, "SSL_DATE_INVALID"); SSL_ERRORS.put(SslError.SSL_INVALID, "SSL_INVALID")690 SSL_ERRORS.put(SslError.SSL_INVALID, "SSL_INVALID"); 691 } 692 sslErrorName(SslError error)693 private static String sslErrorName(SslError error) { 694 return SSL_ERRORS.get(error.getPrimaryError(), "UNKNOWN"); 695 } 696 697 private static final SparseArray<Integer> SSL_ERROR_MSGS = new SparseArray<>(); 698 static { SSL_ERROR_MSGS.put(SslError.SSL_NOTYETVALID, R.string.ssl_error_not_yet_valid)699 SSL_ERROR_MSGS.put(SslError.SSL_NOTYETVALID, R.string.ssl_error_not_yet_valid); SSL_ERROR_MSGS.put(SslError.SSL_EXPIRED, R.string.ssl_error_expired)700 SSL_ERROR_MSGS.put(SslError.SSL_EXPIRED, R.string.ssl_error_expired); SSL_ERROR_MSGS.put(SslError.SSL_IDMISMATCH, R.string.ssl_error_mismatch)701 SSL_ERROR_MSGS.put(SslError.SSL_IDMISMATCH, R.string.ssl_error_mismatch); SSL_ERROR_MSGS.put(SslError.SSL_UNTRUSTED, R.string.ssl_error_untrusted)702 SSL_ERROR_MSGS.put(SslError.SSL_UNTRUSTED, R.string.ssl_error_untrusted); SSL_ERROR_MSGS.put(SslError.SSL_DATE_INVALID, R.string.ssl_error_date_invalid)703 SSL_ERROR_MSGS.put(SslError.SSL_DATE_INVALID, R.string.ssl_error_date_invalid); SSL_ERROR_MSGS.put(SslError.SSL_INVALID, R.string.ssl_error_invalid)704 SSL_ERROR_MSGS.put(SslError.SSL_INVALID, R.string.ssl_error_invalid); 705 } 706 sslErrorMessage(SslError error)707 private static Integer sslErrorMessage(SslError error) { 708 return SSL_ERROR_MSGS.get(error.getPrimaryError(), R.string.ssl_error_unknown); 709 } 710 } 711