• Home
  • Line#
  • Scopes#
  • Navigate#
  • Raw
  • Download
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