OSDN Git Service

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