• 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 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.webkit.SslErrorHandler;
41 import android.webkit.WebChromeClient;
42 import android.webkit.WebSettings;
43 import android.webkit.WebView;
44 import android.webkit.WebViewClient;
45 import android.widget.ProgressBar;
46 import android.widget.TextView;
47 
48 import java.io.IOException;
49 import java.net.HttpURLConnection;
50 import java.net.MalformedURLException;
51 import java.net.URL;
52 import java.lang.InterruptedException;
53 import java.lang.reflect.Field;
54 import java.lang.reflect.Method;
55 import java.util.Random;
56 
57 public class CaptivePortalLoginActivity extends Activity {
58     private static final String TAG = "CaptivePortalLogin";
59     private static final String DEFAULT_SERVER = "connectivitycheck.gstatic.com";
60     private static final int SOCKET_TIMEOUT_MS = 10000;
61 
62     private enum Result { DISMISSED, UNWANTED, WANTED_AS_IS };
63 
64     private URL mURL;
65     private Network mNetwork;
66     private CaptivePortal mCaptivePortal;
67     private NetworkCallback mNetworkCallback;
68     private ConnectivityManager mCm;
69     private boolean mLaunchBrowser = false;
70     private MyWebViewClient mWebViewClient;
71 
72     @Override
onCreate(Bundle savedInstanceState)73     protected void onCreate(Bundle savedInstanceState) {
74         super.onCreate(savedInstanceState);
75 
76         String server = Settings.Global.getString(getContentResolver(), "captive_portal_server");
77         if (server == null) server = DEFAULT_SERVER;
78         mCm = ConnectivityManager.from(this);
79         try {
80             mURL = new URL("http", server, "/generate_204");
81         } catch (MalformedURLException e) {
82             // System misconfigured, bail out in a way that at least provides network access.
83             Log.e(TAG, "Invalid captive portal URL, server=" + server);
84             done(Result.WANTED_AS_IS);
85         }
86         mNetwork = getIntent().getParcelableExtra(ConnectivityManager.EXTRA_NETWORK);
87         mCaptivePortal = getIntent().getParcelableExtra(ConnectivityManager.EXTRA_CAPTIVE_PORTAL);
88 
89         // Also initializes proxy system properties.
90         mCm.bindProcessToNetwork(mNetwork);
91 
92         // Proxy system properties must be initialized before setContentView is called because
93         // setContentView initializes the WebView logic which in turn reads the system properties.
94         setContentView(R.layout.activity_captive_portal_login);
95 
96         getActionBar().setDisplayShowHomeEnabled(false);
97 
98         // Exit app if Network disappears.
99         final NetworkCapabilities networkCapabilities = mCm.getNetworkCapabilities(mNetwork);
100         if (networkCapabilities == null) {
101             finish();
102             return;
103         }
104         mNetworkCallback = new NetworkCallback() {
105             @Override
106             public void onLost(Network lostNetwork) {
107                 if (mNetwork.equals(lostNetwork)) done(Result.UNWANTED);
108             }
109         };
110         final NetworkRequest.Builder builder = new NetworkRequest.Builder();
111         for (int transportType : networkCapabilities.getTransportTypes()) {
112             builder.addTransportType(transportType);
113         }
114         mCm.registerNetworkCallback(builder.build(), mNetworkCallback);
115 
116         final WebView myWebView = (WebView) findViewById(R.id.webview);
117         myWebView.clearCache(true);
118         WebSettings webSettings = myWebView.getSettings();
119         webSettings.setJavaScriptEnabled(true);
120         mWebViewClient = new MyWebViewClient();
121         myWebView.setWebViewClient(mWebViewClient);
122         myWebView.setWebChromeClient(new MyWebChromeClient());
123         // Start initial page load so WebView finishes loading proxy settings.
124         // Actual load of mUrl is initiated by MyWebViewClient.
125         myWebView.loadData("", "text/html", null);
126     }
127 
128     // Find WebView's proxy BroadcastReceiver and prompt it to read proxy system properties.
setWebViewProxy()129     private void setWebViewProxy() {
130         LoadedApk loadedApk = getApplication().mLoadedApk;
131         try {
132             Field receiversField = LoadedApk.class.getDeclaredField("mReceivers");
133             receiversField.setAccessible(true);
134             ArrayMap receivers = (ArrayMap) receiversField.get(loadedApk);
135             for (Object receiverMap : receivers.values()) {
136                 for (Object rec : ((ArrayMap) receiverMap).keySet()) {
137                     Class clazz = rec.getClass();
138                     if (clazz.getName().contains("ProxyChangeListener")) {
139                         Method onReceiveMethod = clazz.getDeclaredMethod("onReceive", Context.class,
140                                 Intent.class);
141                         Intent intent = new Intent(Proxy.PROXY_CHANGE_ACTION);
142                         onReceiveMethod.invoke(rec, getApplicationContext(), intent);
143                         Log.v(TAG, "Prompting WebView proxy reload.");
144                     }
145                 }
146             }
147         } catch (Exception e) {
148             Log.e(TAG, "Exception while setting WebView proxy: " + e);
149         }
150     }
151 
done(Result result)152     private void done(Result result) {
153         if (mNetworkCallback != null) {
154             mCm.unregisterNetworkCallback(mNetworkCallback);
155             mNetworkCallback = null;
156         }
157         switch (result) {
158             case DISMISSED:
159                 mCaptivePortal.reportCaptivePortalDismissed();
160                 break;
161             case UNWANTED:
162                 mCaptivePortal.ignoreNetwork();
163                 break;
164             case WANTED_AS_IS:
165                 mCaptivePortal.useNetwork();
166                 break;
167         }
168         finish();
169     }
170 
171     @Override
onCreateOptionsMenu(Menu menu)172     public boolean onCreateOptionsMenu(Menu menu) {
173         getMenuInflater().inflate(R.menu.captive_portal_login, menu);
174         return true;
175     }
176 
177     @Override
onBackPressed()178     public void onBackPressed() {
179         WebView myWebView = (WebView) findViewById(R.id.webview);
180         if (myWebView.canGoBack() && mWebViewClient.allowBack()) {
181             myWebView.goBack();
182         } else {
183             super.onBackPressed();
184         }
185     }
186 
187     @Override
onOptionsItemSelected(MenuItem item)188     public boolean onOptionsItemSelected(MenuItem item) {
189         int id = item.getItemId();
190         if (id == R.id.action_use_network) {
191             done(Result.WANTED_AS_IS);
192             return true;
193         }
194         if (id == R.id.action_do_not_use_network) {
195             done(Result.UNWANTED);
196             return true;
197         }
198         return super.onOptionsItemSelected(item);
199     }
200 
201     @Override
onDestroy()202     public void onDestroy() {
203         super.onDestroy();
204 
205         if (mNetworkCallback != null) {
206             mCm.unregisterNetworkCallback(mNetworkCallback);
207             mNetworkCallback = null;
208         }
209         if (mLaunchBrowser) {
210             // Give time for this network to become default. After 500ms just proceed.
211             for (int i = 0; i < 5; i++) {
212                 // TODO: This misses when mNetwork underlies a VPN.
213                 if (mNetwork.equals(mCm.getActiveNetwork())) break;
214                 try {
215                     Thread.sleep(100);
216                 } catch (InterruptedException e) {
217                 }
218             }
219             startActivity(new Intent(Intent.ACTION_VIEW, Uri.parse(mURL.toString())));
220         }
221     }
222 
testForCaptivePortal()223     private void testForCaptivePortal() {
224         new Thread(new Runnable() {
225             public void run() {
226                 // Give time for captive portal to open.
227                 try {
228                     Thread.sleep(1000);
229                 } catch (InterruptedException e) {
230                 }
231                 HttpURLConnection urlConnection = null;
232                 int httpResponseCode = 500;
233                 try {
234                     urlConnection = (HttpURLConnection) mURL.openConnection();
235                     urlConnection.setInstanceFollowRedirects(false);
236                     urlConnection.setConnectTimeout(SOCKET_TIMEOUT_MS);
237                     urlConnection.setReadTimeout(SOCKET_TIMEOUT_MS);
238                     urlConnection.setUseCaches(false);
239                     urlConnection.getInputStream();
240                     httpResponseCode = urlConnection.getResponseCode();
241                 } catch (IOException e) {
242                 } finally {
243                     if (urlConnection != null) urlConnection.disconnect();
244                 }
245                 if (httpResponseCode == 204) {
246                     done(Result.DISMISSED);
247                 }
248             }
249         }).start();
250     }
251 
252     private class MyWebViewClient extends WebViewClient {
253         private static final String INTERNAL_ASSETS = "file:///android_asset/";
254         private final String mBrowserBailOutToken = Long.toString(new Random().nextLong());
255         // How many Android device-independent-pixels per scaled-pixel
256         // dp/sp = (px/sp) / (px/dp) = (1/sp) / (1/dp)
257         private final float mDpPerSp = TypedValue.applyDimension(TypedValue.COMPLEX_UNIT_SP, 1,
258                     getResources().getDisplayMetrics()) /
259                     TypedValue.applyDimension(TypedValue.COMPLEX_UNIT_DIP, 1,
260                     getResources().getDisplayMetrics());
261         private int mPagesLoaded;
262 
263         // If we haven't finished cleaning up the history, don't allow going back.
allowBack()264         public boolean allowBack() {
265             return mPagesLoaded > 1;
266         }
267 
268         @Override
onPageStarted(WebView view, String url, Bitmap favicon)269         public void onPageStarted(WebView view, String url, Bitmap favicon) {
270             if (url.contains(mBrowserBailOutToken)) {
271                 mLaunchBrowser = true;
272                 done(Result.WANTED_AS_IS);
273                 return;
274             }
275             // The first page load is used only to cause the WebView to
276             // fetch the proxy settings.  Don't update the URL bar, and
277             // don't check if the captive portal is still there.
278             if (mPagesLoaded == 0) return;
279             // For internally generated pages, leave URL bar listing prior URL as this is the URL
280             // the page refers to.
281             if (!url.startsWith(INTERNAL_ASSETS)) {
282                 final TextView myUrlBar = (TextView) findViewById(R.id.url_bar);
283                 myUrlBar.setText(url);
284             }
285             testForCaptivePortal();
286         }
287 
288         @Override
onPageFinished(WebView view, String url)289         public void onPageFinished(WebView view, String url) {
290             mPagesLoaded++;
291             if (mPagesLoaded == 1) {
292                 // Now that WebView has loaded at least one page we know it has read in the proxy
293                 // settings.  Now prompt the WebView read the Network-specific proxy settings.
294                 setWebViewProxy();
295                 // Load the real page.
296                 view.loadUrl(mURL.toString());
297                 return;
298             } else if (mPagesLoaded == 2) {
299                 // Prevent going back to empty first page.
300                 view.clearHistory();
301             }
302             testForCaptivePortal();
303         }
304 
305         // Convert Android device-independent-pixels (dp) to HTML size.
dp(int dp)306         private String dp(int dp) {
307             // HTML px's are scaled just like dp's, so just add "px" suffix.
308             return Integer.toString(dp) + "px";
309         }
310 
311         // Convert Android scaled-pixels (sp) to HTML size.
sp(int sp)312         private String sp(int sp) {
313             // Convert sp to dp's.
314             float dp = sp * mDpPerSp;
315             // Apply a scale factor to make things look right.
316             dp *= 1.3;
317             // Convert dp's to HTML size.
318             return dp((int)dp);
319         }
320 
321         // A web page consisting of a large broken lock icon to indicate SSL failure.
322         private final String SSL_ERROR_HTML = "<html><head><style>" +
323                 "body { margin-left:" + dp(48) + "; margin-right:" + dp(48) + "; " +
324                         "margin-top:" + dp(96) + "; background-color:#fafafa; }" +
325                 "img { width:" + dp(48) + "; height:" + dp(48) + "; }" +
326                 "div.warn { font-size:" + sp(16) + "; margin-top:" + dp(16) + "; " +
327                 "           opacity:0.87; line-height:1.28; }" +
328                 "div.example { font-size:" + sp(14) + "; margin-top:" + dp(16) + "; " +
329                 "              opacity:0.54; line-height:1.21905; }" +
330                 "a { font-size:" + sp(14) + "; text-decoration:none; text-transform:uppercase; " +
331                 "    margin-top:" + dp(24) + "; display:inline-block; color:#4285F4; " +
332                 "    height:" + dp(48) + "; font-weight:bold; }" +
333                 "</style></head><body><p><img src=quantum_ic_warning_amber_96.png><br>" +
334                 "<div class=warn>%s</div>" +
335                 "<div class=example>%s</div>" +
336                 "<a href=%s>%s</a></body></html>";
337 
338         @Override
onReceivedSslError(WebView view, SslErrorHandler handler, SslError error)339         public void onReceivedSslError(WebView view, SslErrorHandler handler, SslError error) {
340             Log.w(TAG, "SSL error (error: " + error.getPrimaryError() + " host: " +
341                     // Only show host to avoid leaking private info.
342                     Uri.parse(error.getUrl()).getHost() + " certificate: " +
343                     error.getCertificate() + "); displaying SSL warning.");
344             final String html = String.format(SSL_ERROR_HTML, getString(R.string.ssl_error_warning),
345                     getString(R.string.ssl_error_example), mBrowserBailOutToken,
346                     getString(R.string.ssl_error_continue));
347             view.loadDataWithBaseURL(INTERNAL_ASSETS, html, "text/HTML", "UTF-8", null);
348         }
349 
350         @Override
shouldOverrideUrlLoading(WebView view, String url)351         public boolean shouldOverrideUrlLoading (WebView view, String url) {
352             if (url.startsWith("tel:")) {
353                 startActivity(new Intent(Intent.ACTION_DIAL, Uri.parse(url)));
354                 return true;
355             }
356             return false;
357         }
358     }
359 
360     private class MyWebChromeClient extends WebChromeClient {
361         @Override
onProgressChanged(WebView view, int newProgress)362         public void onProgressChanged(WebView view, int newProgress) {
363             final ProgressBar myProgressBar = (ProgressBar) findViewById(R.id.progress_bar);
364             myProgressBar.setProgress(newProgress);
365         }
366     }
367 }
368