• 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 import static android.net.captiveportal.CaptivePortalProbeSpec.HTTP_LOCATION_HEADER_NAME;
21 
22 import android.app.Activity;
23 import android.app.LoadedApk;
24 import android.content.Context;
25 import android.content.Intent;
26 import android.graphics.Bitmap;
27 import android.net.CaptivePortal;
28 import android.net.ConnectivityManager;
29 import android.net.ConnectivityManager.NetworkCallback;
30 import android.net.Network;
31 import android.net.NetworkCapabilities;
32 import android.net.NetworkRequest;
33 import android.net.Proxy;
34 import android.net.Uri;
35 import android.net.captiveportal.CaptivePortalProbeSpec;
36 import android.net.dns.ResolvUtil;
37 import android.net.http.SslError;
38 import android.net.wifi.WifiInfo;
39 import android.os.Build;
40 import android.os.Bundle;
41 import android.provider.Settings;
42 import android.support.v4.widget.SwipeRefreshLayout;
43 import android.text.TextUtils;
44 import android.util.ArrayMap;
45 import android.util.Log;
46 import android.util.TypedValue;
47 import android.util.SparseArray;
48 import android.view.Menu;
49 import android.view.MenuItem;
50 import android.view.View;
51 import android.webkit.CookieManager;
52 import android.webkit.SslErrorHandler;
53 import android.webkit.WebChromeClient;
54 import android.webkit.WebSettings;
55 import android.webkit.WebView;
56 import android.webkit.WebView;
57 import android.webkit.WebViewClient;
58 import android.widget.ProgressBar;
59 import android.widget.TextView;
60 
61 import com.android.internal.logging.MetricsLogger;
62 import com.android.internal.logging.nano.MetricsProto.MetricsEvent;
63 
64 import java.io.IOException;
65 import java.net.HttpURLConnection;
66 import java.net.MalformedURLException;
67 import java.net.URL;
68 import java.lang.InterruptedException;
69 import java.lang.reflect.Field;
70 import java.lang.reflect.Method;
71 import java.util.Objects;
72 import java.util.Random;
73 import java.util.concurrent.atomic.AtomicBoolean;
74 
75 public class CaptivePortalLoginActivity extends Activity {
76     private static final String TAG = CaptivePortalLoginActivity.class.getSimpleName();
77     private static final boolean DBG = true;
78     private static final boolean VDBG = false;
79 
80     private static final int SOCKET_TIMEOUT_MS = 10000;
81 
82     private enum Result {
83         DISMISSED(MetricsEvent.ACTION_CAPTIVE_PORTAL_LOGIN_RESULT_DISMISSED),
84         UNWANTED(MetricsEvent.ACTION_CAPTIVE_PORTAL_LOGIN_RESULT_UNWANTED),
85         WANTED_AS_IS(MetricsEvent.ACTION_CAPTIVE_PORTAL_LOGIN_RESULT_WANTED_AS_IS);
86 
87         final int metricsEvent;
Result(int metricsEvent)88         Result(int metricsEvent) { this.metricsEvent = metricsEvent; }
89     };
90 
91     private URL mUrl;
92     private CaptivePortalProbeSpec mProbeSpec;
93     private String mUserAgent;
94     private Network mNetwork;
95     private CaptivePortal mCaptivePortal;
96     private NetworkCallback mNetworkCallback;
97     private ConnectivityManager mCm;
98     private boolean mLaunchBrowser = false;
99     private MyWebViewClient mWebViewClient;
100     private SwipeRefreshLayout mSwipeRefreshLayout;
101     // Ensures that done() happens once exactly, handling concurrent callers with atomic operations.
102     private final AtomicBoolean isDone = new AtomicBoolean(false);
103 
104     @Override
onCreate(Bundle savedInstanceState)105     protected void onCreate(Bundle savedInstanceState) {
106         super.onCreate(savedInstanceState);
107 
108         logMetricsEvent(MetricsEvent.ACTION_CAPTIVE_PORTAL_LOGIN_ACTIVITY);
109 
110         mCm = ConnectivityManager.from(this);
111         mNetwork = getIntent().getParcelableExtra(ConnectivityManager.EXTRA_NETWORK);
112         mCaptivePortal = getIntent().getParcelableExtra(ConnectivityManager.EXTRA_CAPTIVE_PORTAL);
113         mUserAgent =
114                 getIntent().getStringExtra(ConnectivityManager.EXTRA_CAPTIVE_PORTAL_USER_AGENT);
115         mUrl = getUrl();
116         if (mUrl == null) {
117             // getUrl() failed to parse the url provided in the intent: bail out in a way that
118             // at least provides network access.
119             done(Result.WANTED_AS_IS);
120             return;
121         }
122         if (DBG) {
123             Log.d(TAG, String.format("onCreate for %s", mUrl.toString()));
124         }
125 
126         final String spec = getIntent().getStringExtra(EXTRA_CAPTIVE_PORTAL_PROBE_SPEC);
127         try {
128             mProbeSpec = CaptivePortalProbeSpec.parseSpecOrNull(spec);
129         } catch (Exception e) {
130             // Make extra sure that invalid configurations do not cause crashes
131             mProbeSpec = null;
132         }
133 
134         // Also initializes proxy system properties.
135         mCm.bindProcessToNetwork(mNetwork);
136         mCm.setProcessDefaultNetworkForHostResolution(
137                 ResolvUtil.getNetworkWithUseLocalNameserversFlag(mNetwork));
138 
139         // Proxy system properties must be initialized before setContentView is called because
140         // setContentView initializes the WebView logic which in turn reads the system properties.
141         setContentView(R.layout.activity_captive_portal_login);
142 
143         // Exit app if Network disappears.
144         final NetworkCapabilities networkCapabilities = mCm.getNetworkCapabilities(mNetwork);
145         if (networkCapabilities == null) {
146             finishAndRemoveTask();
147             return;
148         }
149         mNetworkCallback = new NetworkCallback() {
150             @Override
151             public void onLost(Network lostNetwork) {
152                 if (mNetwork.equals(lostNetwork)) done(Result.UNWANTED);
153             }
154         };
155         final NetworkRequest.Builder builder = new NetworkRequest.Builder();
156         for (int transportType : networkCapabilities.getTransportTypes()) {
157             builder.addTransportType(transportType);
158         }
159         mCm.registerNetworkCallback(builder.build(), mNetworkCallback);
160 
161         getActionBar().setDisplayShowHomeEnabled(false);
162         getActionBar().setElevation(0); // remove shadow
163         getActionBar().setTitle(getHeaderTitle());
164         getActionBar().setSubtitle("");
165 
166         final WebView webview = getWebview();
167         webview.clearCache(true);
168         CookieManager.getInstance().setAcceptThirdPartyCookies(webview, true);
169         WebSettings webSettings = webview.getSettings();
170         webSettings.setJavaScriptEnabled(true);
171         webSettings.setMixedContentMode(WebSettings.MIXED_CONTENT_COMPATIBILITY_MODE);
172         webSettings.setUseWideViewPort(true);
173         webSettings.setLoadWithOverviewMode(true);
174         webSettings.setSupportZoom(true);
175         webSettings.setBuiltInZoomControls(true);
176         webSettings.setDisplayZoomControls(false);
177         mWebViewClient = new MyWebViewClient();
178         webview.setWebViewClient(mWebViewClient);
179         webview.setWebChromeClient(new MyWebChromeClient());
180         // Start initial page load so WebView finishes loading proxy settings.
181         // Actual load of mUrl is initiated by MyWebViewClient.
182         webview.loadData("", "text/html", null);
183 
184         mSwipeRefreshLayout = findViewById(R.id.swipe_refresh);
185         mSwipeRefreshLayout.setOnRefreshListener(() -> {
186                 webview.reload();
187                 mSwipeRefreshLayout.setRefreshing(true);
188             });
189 
190     }
191 
192     // Find WebView's proxy BroadcastReceiver and prompt it to read proxy system properties.
setWebViewProxy()193     private void setWebViewProxy() {
194         LoadedApk loadedApk = getApplication().mLoadedApk;
195         try {
196             Field receiversField = LoadedApk.class.getDeclaredField("mReceivers");
197             receiversField.setAccessible(true);
198             ArrayMap receivers = (ArrayMap) receiversField.get(loadedApk);
199             for (Object receiverMap : receivers.values()) {
200                 for (Object rec : ((ArrayMap) receiverMap).keySet()) {
201                     Class clazz = rec.getClass();
202                     if (clazz.getName().contains("ProxyChangeListener")) {
203                         Method onReceiveMethod = clazz.getDeclaredMethod("onReceive", Context.class,
204                                 Intent.class);
205                         Intent intent = new Intent(Proxy.PROXY_CHANGE_ACTION);
206                         onReceiveMethod.invoke(rec, getApplicationContext(), intent);
207                         Log.v(TAG, "Prompting WebView proxy reload.");
208                     }
209                 }
210             }
211         } catch (Exception e) {
212             Log.e(TAG, "Exception while setting WebView proxy: " + e);
213         }
214     }
215 
done(Result result)216     private void done(Result result) {
217         if (isDone.getAndSet(true)) {
218             // isDone was already true: done() already called
219             return;
220         }
221         if (DBG) {
222             Log.d(TAG, String.format("Result %s for %s", result.name(), mUrl.toString()));
223         }
224         logMetricsEvent(result.metricsEvent);
225         switch (result) {
226             case DISMISSED:
227                 mCaptivePortal.reportCaptivePortalDismissed();
228                 break;
229             case UNWANTED:
230                 mCaptivePortal.ignoreNetwork();
231                 break;
232             case WANTED_AS_IS:
233                 mCaptivePortal.useNetwork();
234                 break;
235         }
236         finishAndRemoveTask();
237     }
238 
239     @Override
onCreateOptionsMenu(Menu menu)240     public boolean onCreateOptionsMenu(Menu menu) {
241         getMenuInflater().inflate(R.menu.captive_portal_login, menu);
242         return true;
243     }
244 
245     @Override
onBackPressed()246     public void onBackPressed() {
247         WebView myWebView = findViewById(R.id.webview);
248         if (myWebView.canGoBack() && mWebViewClient.allowBack()) {
249             myWebView.goBack();
250         } else {
251             super.onBackPressed();
252         }
253     }
254 
255     @Override
onOptionsItemSelected(MenuItem item)256     public boolean onOptionsItemSelected(MenuItem item) {
257         final Result result;
258         final String action;
259         final int id = item.getItemId();
260         switch (id) {
261             case R.id.action_use_network:
262                 result = Result.WANTED_AS_IS;
263                 action = "USE_NETWORK";
264                 break;
265             case R.id.action_do_not_use_network:
266                 result = Result.UNWANTED;
267                 action = "DO_NOT_USE_NETWORK";
268                 break;
269             default:
270                 return super.onOptionsItemSelected(item);
271         }
272         if (DBG) {
273             Log.d(TAG, String.format("onOptionsItemSelect %s for %s", action, mUrl.toString()));
274         }
275         done(result);
276         return true;
277     }
278 
279     @Override
onDestroy()280     public void onDestroy() {
281         super.onDestroy();
282         if (mNetworkCallback != null) {
283             // mNetworkCallback is not null if mUrl is not null.
284             mCm.unregisterNetworkCallback(mNetworkCallback);
285         }
286         if (mLaunchBrowser) {
287             // Give time for this network to become default. After 500ms just proceed.
288             for (int i = 0; i < 5; i++) {
289                 // TODO: This misses when mNetwork underlies a VPN.
290                 if (mNetwork.equals(mCm.getActiveNetwork())) break;
291                 try {
292                     Thread.sleep(100);
293                 } catch (InterruptedException e) {
294                 }
295             }
296             final String url = mUrl.toString();
297             if (DBG) {
298                 Log.d(TAG, "starting activity with intent ACTION_VIEW for " + url);
299             }
300             startActivity(new Intent(Intent.ACTION_VIEW, Uri.parse(url)));
301         }
302     }
303 
getUrl()304     private URL getUrl() {
305         String url = getIntent().getStringExtra(ConnectivityManager.EXTRA_CAPTIVE_PORTAL_URL);
306         if (url == null) {
307             url = mCm.getCaptivePortalServerUrl();
308         }
309         return makeURL(url);
310     }
311 
makeURL(String url)312     private static URL makeURL(String url) {
313         try {
314             return new URL(url);
315         } catch (MalformedURLException e) {
316             Log.e(TAG, "Invalid URL " + url);
317         }
318         return null;
319     }
320 
host(URL url)321     private static String host(URL url) {
322         if (url == null) {
323             return null;
324         }
325         return url.getHost();
326     }
327 
sanitizeURL(URL url)328     private static String sanitizeURL(URL url) {
329         // In non-Debug build, only show host to avoid leaking private info.
330         return Build.IS_DEBUGGABLE ? Objects.toString(url) : host(url);
331     }
332 
testForCaptivePortal()333     private void testForCaptivePortal() {
334         // TODO: reuse NetworkMonitor facilities for consistent captive portal detection.
335         new Thread(new Runnable() {
336             public void run() {
337                 final Network network = ResolvUtil.makeNetworkWithPrivateDnsBypass(mNetwork);
338                 // Give time for captive portal to open.
339                 try {
340                     Thread.sleep(1000);
341                 } catch (InterruptedException e) {
342                 }
343                 HttpURLConnection urlConnection = null;
344                 int httpResponseCode = 500;
345                 String locationHeader = null;
346                 try {
347                     urlConnection = (HttpURLConnection) network.openConnection(mUrl);
348                     urlConnection.setInstanceFollowRedirects(false);
349                     urlConnection.setConnectTimeout(SOCKET_TIMEOUT_MS);
350                     urlConnection.setReadTimeout(SOCKET_TIMEOUT_MS);
351                     urlConnection.setUseCaches(false);
352                     if (mUserAgent != null) {
353                        urlConnection.setRequestProperty("User-Agent", mUserAgent);
354                     }
355                     // cannot read request header after connection
356                     String requestHeader = urlConnection.getRequestProperties().toString();
357 
358                     urlConnection.getInputStream();
359                     httpResponseCode = urlConnection.getResponseCode();
360                     locationHeader = urlConnection.getHeaderField(HTTP_LOCATION_HEADER_NAME);
361                     if (DBG) {
362                         Log.d(TAG, "probe at " + mUrl +
363                                 " ret=" + httpResponseCode +
364                                 " request=" + requestHeader +
365                                 " headers=" + urlConnection.getHeaderFields());
366                     }
367                 } catch (IOException e) {
368                 } finally {
369                     if (urlConnection != null) urlConnection.disconnect();
370                 }
371                 if (isDismissed(httpResponseCode, locationHeader, mProbeSpec)) {
372                     done(Result.DISMISSED);
373                 }
374             }
375         }).start();
376     }
377 
isDismissed( int httpResponseCode, String locationHeader, CaptivePortalProbeSpec probeSpec)378     private static boolean isDismissed(
379             int httpResponseCode, String locationHeader, CaptivePortalProbeSpec probeSpec) {
380         return (probeSpec != null)
381                 ? probeSpec.getResult(httpResponseCode, locationHeader).isSuccessful()
382                 : (httpResponseCode == 204);
383     }
384 
385     private class MyWebViewClient extends WebViewClient {
386         private static final String INTERNAL_ASSETS = "file:///android_asset/";
387 
388         private final String mBrowserBailOutToken = Long.toString(new Random().nextLong());
389         // How many Android device-independent-pixels per scaled-pixel
390         // dp/sp = (px/sp) / (px/dp) = (1/sp) / (1/dp)
391         private final float mDpPerSp = TypedValue.applyDimension(TypedValue.COMPLEX_UNIT_SP, 1,
392                     getResources().getDisplayMetrics()) /
393                     TypedValue.applyDimension(TypedValue.COMPLEX_UNIT_DIP, 1,
394                     getResources().getDisplayMetrics());
395         private int mPagesLoaded;
396         // the host of the page that this webview is currently loading. Can be null when undefined.
397         private String mHostname;
398 
399         // If we haven't finished cleaning up the history, don't allow going back.
allowBack()400         public boolean allowBack() {
401             return mPagesLoaded > 1;
402         }
403 
404         @Override
onPageStarted(WebView view, String urlString, Bitmap favicon)405         public void onPageStarted(WebView view, String urlString, Bitmap favicon) {
406             if (urlString.contains(mBrowserBailOutToken)) {
407                 mLaunchBrowser = true;
408                 done(Result.WANTED_AS_IS);
409                 return;
410             }
411             // The first page load is used only to cause the WebView to
412             // fetch the proxy settings.  Don't update the URL bar, and
413             // don't check if the captive portal is still there.
414             if (mPagesLoaded == 0) {
415                 return;
416             }
417             final URL url = makeURL(urlString);
418             Log.d(TAG, "onPageStarted: " + sanitizeURL(url));
419             mHostname = host(url);
420             // For internally generated pages, leave URL bar listing prior URL as this is the URL
421             // the page refers to.
422             if (!urlString.startsWith(INTERNAL_ASSETS)) {
423                 String subtitle = (url != null) ? getHeaderSubtitle(url) : urlString;
424                 getActionBar().setSubtitle(subtitle);
425             }
426             getProgressBar().setVisibility(View.VISIBLE);
427             testForCaptivePortal();
428         }
429 
430         @Override
onPageFinished(WebView view, String url)431         public void onPageFinished(WebView view, String url) {
432             mPagesLoaded++;
433             getProgressBar().setVisibility(View.INVISIBLE);
434             mSwipeRefreshLayout.setRefreshing(false);
435             if (mPagesLoaded == 1) {
436                 // Now that WebView has loaded at least one page we know it has read in the proxy
437                 // settings.  Now prompt the WebView read the Network-specific proxy settings.
438                 setWebViewProxy();
439                 // Load the real page.
440                 view.loadUrl(mUrl.toString());
441                 return;
442             } else if (mPagesLoaded == 2) {
443                 // Prevent going back to empty first page.
444                 // Fix for missing focus, see b/62449959 for details. Remove it once we get a
445                 // newer version of WebView (60.x.y).
446                 view.requestFocus();
447                 view.clearHistory();
448             }
449             testForCaptivePortal();
450         }
451 
452         // Convert Android scaled-pixels (sp) to HTML size.
sp(int sp)453         private String sp(int sp) {
454             // Convert sp to dp's.
455             float dp = sp * mDpPerSp;
456             // Apply a scale factor to make things look right.
457             dp *= 1.3;
458             // Convert dp's to HTML size.
459             // HTML px's are scaled just like dp's, so just add "px" suffix.
460             return Integer.toString((int)dp) + "px";
461         }
462 
463         // A web page consisting of a large broken lock icon to indicate SSL failure.
464 
465         @Override
onReceivedSslError(WebView view, SslErrorHandler handler, SslError error)466         public void onReceivedSslError(WebView view, SslErrorHandler handler, SslError error) {
467             final URL url = makeURL(error.getUrl());
468             final String host = host(url);
469             Log.d(TAG, String.format("SSL error: %s, url: %s, certificate: %s",
470                     sslErrorName(error), sanitizeURL(url), error.getCertificate()));
471             if (url == null || !Objects.equals(host, mHostname)) {
472                 // Ignore ssl errors for resources coming from a different hostname than the page
473                 // that we are currently loading, and only cancel the request.
474                 handler.cancel();
475                 return;
476             }
477             logMetricsEvent(MetricsEvent.CAPTIVE_PORTAL_LOGIN_ACTIVITY_SSL_ERROR);
478             final String sslErrorPage = makeSslErrorPage();
479             view.loadDataWithBaseURL(INTERNAL_ASSETS, sslErrorPage, "text/HTML", "UTF-8", null);
480         }
481 
makeSslErrorPage()482         private String makeSslErrorPage() {
483             final String warningMsg = getString(R.string.ssl_error_warning);
484             final String exampleMsg = getString(R.string.ssl_error_example);
485             final String continueMsg = getString(R.string.ssl_error_continue);
486             return String.join("\n",
487                     "<html>",
488                     "<head>",
489                     "  <meta name=\"viewport\" content=\"width=device-width, initial-scale=1\">",
490                     "  <style>",
491                     "    body {",
492                     "      background-color:#fafafa;",
493                     "      margin:auto;",
494                     "      width:80%;",
495                     "      margin-top: 96px",
496                     "    }",
497                     "    img {",
498                     "      height:48px;",
499                     "      width:48px;",
500                     "    }",
501                     "    div.warn {",
502                     "      font-size:" + sp(16) + ";",
503                     "      line-height:1.28;",
504                     "      margin-top:16px;",
505                     "      opacity:0.87;",
506                     "    }",
507                     "    div.example {",
508                     "      font-size:" + sp(14) + ";",
509                     "      line-height:1.21905;",
510                     "      margin-top:16px;",
511                     "      opacity:0.54;",
512                     "    }",
513                     "    a {",
514                     "      color:#4285F4;",
515                     "      display:inline-block;",
516                     "      font-size:" + sp(14) + ";",
517                     "      font-weight:bold;",
518                     "      height:48px;",
519                     "      margin-top:24px;",
520                     "      text-decoration:none;",
521                     "      text-transform:uppercase;",
522                     "    }",
523                     "  </style>",
524                     "</head>",
525                     "<body>",
526                     "  <p><img src=quantum_ic_warning_amber_96.png><br>",
527                     "  <div class=warn>" + warningMsg + "</div>",
528                     "  <div class=example>" + exampleMsg + "</div>",
529                     "  <a href=" + mBrowserBailOutToken + ">" + continueMsg + "</a>",
530                     "</body>",
531                     "</html>");
532         }
533 
534         @Override
shouldOverrideUrlLoading(WebView view, String url)535         public boolean shouldOverrideUrlLoading (WebView view, String url) {
536             if (url.startsWith("tel:")) {
537                 startActivity(new Intent(Intent.ACTION_DIAL, Uri.parse(url)));
538                 return true;
539             }
540             return false;
541         }
542     }
543 
544     private class MyWebChromeClient extends WebChromeClient {
545         @Override
onProgressChanged(WebView view, int newProgress)546         public void onProgressChanged(WebView view, int newProgress) {
547             getProgressBar().setProgress(newProgress);
548         }
549     }
550 
getProgressBar()551     private ProgressBar getProgressBar() {
552         return findViewById(R.id.progress_bar);
553     }
554 
getWebview()555     private WebView getWebview() {
556         return findViewById(R.id.webview);
557     }
558 
getHeaderTitle()559     private String getHeaderTitle() {
560         NetworkCapabilities nc = mCm.getNetworkCapabilities(mNetwork);
561         if (nc == null || TextUtils.isEmpty(nc.getSSID())
562             || !nc.hasTransport(NetworkCapabilities.TRANSPORT_WIFI)) {
563             return getString(R.string.action_bar_label);
564         }
565         return getString(R.string.action_bar_title, WifiInfo.removeDoubleQuotes(nc.getSSID()));
566     }
567 
getHeaderSubtitle(URL url)568     private String getHeaderSubtitle(URL url) {
569         String host = host(url);
570         final String https = "https";
571         if (https.equals(url.getProtocol())) {
572             return https + "://" + host;
573         }
574         return host;
575     }
576 
logMetricsEvent(int event)577     private void logMetricsEvent(int event) {
578         MetricsLogger.action(this, event, getPackageName());
579     }
580 
581     private static final SparseArray<String> SSL_ERRORS = new SparseArray<>();
582     static {
SSL_ERRORS.put(SslError.SSL_NOTYETVALID, "SSL_NOTYETVALID")583         SSL_ERRORS.put(SslError.SSL_NOTYETVALID,  "SSL_NOTYETVALID");
SSL_ERRORS.put(SslError.SSL_EXPIRED, "SSL_EXPIRED")584         SSL_ERRORS.put(SslError.SSL_EXPIRED,      "SSL_EXPIRED");
SSL_ERRORS.put(SslError.SSL_IDMISMATCH, "SSL_IDMISMATCH")585         SSL_ERRORS.put(SslError.SSL_IDMISMATCH,   "SSL_IDMISMATCH");
SSL_ERRORS.put(SslError.SSL_UNTRUSTED, "SSL_UNTRUSTED")586         SSL_ERRORS.put(SslError.SSL_UNTRUSTED,    "SSL_UNTRUSTED");
SSL_ERRORS.put(SslError.SSL_DATE_INVALID, "SSL_DATE_INVALID")587         SSL_ERRORS.put(SslError.SSL_DATE_INVALID, "SSL_DATE_INVALID");
SSL_ERRORS.put(SslError.SSL_INVALID, "SSL_INVALID")588         SSL_ERRORS.put(SslError.SSL_INVALID,      "SSL_INVALID");
589     }
590 
sslErrorName(SslError error)591     private static String sslErrorName(SslError error) {
592         return SSL_ERRORS.get(error.getPrimaryError(), "UNKNOWN");
593     }
594 }
595