• 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.ConnectivityManager;
25 import android.net.ConnectivityManager.NetworkCallback;
26 import android.net.Network;
27 import android.net.NetworkCapabilities;
28 import android.net.NetworkRequest;
29 import android.net.Proxy;
30 import android.net.Uri;
31 import android.net.http.SslError;
32 import android.os.Bundle;
33 import android.provider.Settings;
34 import android.provider.Settings.Global;
35 import android.util.ArrayMap;
36 import android.util.Log;
37 import android.view.Menu;
38 import android.view.MenuItem;
39 import android.view.View;
40 import android.view.Window;
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 
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 
56 public class CaptivePortalLoginActivity extends Activity {
57     private static final String TAG = "CaptivePortalLogin";
58     private static final String DEFAULT_SERVER = "connectivitycheck.android.com";
59     private static final int SOCKET_TIMEOUT_MS = 10000;
60 
61     // Keep this in sync with NetworkMonitor.
62     // Intent broadcast to ConnectivityService indicating sign-in is complete.
63     // Extras:
64     //     EXTRA_TEXT       = netId
65     //     LOGGED_IN_RESULT = one of the CAPTIVE_PORTAL_APP_RETURN_* values below.
66     //     RESPONSE_TOKEN   = data fragment from launching Intent
67     private static final String ACTION_CAPTIVE_PORTAL_LOGGED_IN =
68             "android.net.netmon.captive_portal_logged_in";
69     private static final String LOGGED_IN_RESULT = "result";
70     private static final int CAPTIVE_PORTAL_APP_RETURN_APPEASED = 0;
71     private static final int CAPTIVE_PORTAL_APP_RETURN_UNWANTED = 1;
72     private static final int CAPTIVE_PORTAL_APP_RETURN_WANTED_AS_IS = 2;
73     private static final String RESPONSE_TOKEN = "response_token";
74 
75     private URL mURL;
76     private int mNetId;
77     private String mResponseToken;
78     private NetworkCallback mNetworkCallback;
79 
80     @Override
onCreate(Bundle savedInstanceState)81     protected void onCreate(Bundle savedInstanceState) {
82         super.onCreate(savedInstanceState);
83 
84         String server = Settings.Global.getString(getContentResolver(), "captive_portal_server");
85         if (server == null) server = DEFAULT_SERVER;
86         try {
87             mURL = new URL("http", server, "/generate_204");
88             final Uri dataUri = getIntent().getData();
89             if (!dataUri.getScheme().equals("netid")) {
90                 throw new MalformedURLException();
91             }
92             mNetId = Integer.parseInt(dataUri.getSchemeSpecificPart());
93             mResponseToken = dataUri.getFragment();
94         } catch (MalformedURLException|NumberFormatException e) {
95             // System misconfigured, bail out in a way that at least provides network access.
96             done(CAPTIVE_PORTAL_APP_RETURN_WANTED_AS_IS);
97         }
98 
99         final ConnectivityManager cm = ConnectivityManager.from(this);
100         final Network network = new Network(mNetId);
101         // Also initializes proxy system properties.
102         cm.setProcessDefaultNetwork(network);
103 
104         // Proxy system properties must be initialized before setContentView is called because
105         // setContentView initializes the WebView logic which in turn reads the system properties.
106         setContentView(R.layout.activity_captive_portal_login);
107 
108         getActionBar().setDisplayShowHomeEnabled(false);
109 
110         // Exit app if Network disappears.
111         final NetworkCapabilities networkCapabilities = cm.getNetworkCapabilities(network);
112         if (networkCapabilities == null) {
113             finish();
114             return;
115         }
116         mNetworkCallback = new NetworkCallback() {
117             @Override
118             public void onLost(Network lostNetwork) {
119                 if (network.equals(lostNetwork)) done(CAPTIVE_PORTAL_APP_RETURN_UNWANTED);
120             }
121         };
122         final NetworkRequest.Builder builder = new NetworkRequest.Builder();
123         for (int transportType : networkCapabilities.getTransportTypes()) {
124             builder.addTransportType(transportType);
125         }
126         cm.registerNetworkCallback(builder.build(), mNetworkCallback);
127 
128         final WebView myWebView = (WebView) findViewById(R.id.webview);
129         myWebView.clearCache(true);
130         WebSettings webSettings = myWebView.getSettings();
131         webSettings.setJavaScriptEnabled(true);
132         myWebView.setWebViewClient(new MyWebViewClient());
133         myWebView.setWebChromeClient(new MyWebChromeClient());
134         // Start initial page load so WebView finishes loading proxy settings.
135         // Actual load of mUrl is initiated by MyWebViewClient.
136         myWebView.loadData("", "text/html", null);
137     }
138 
139     // Find WebView's proxy BroadcastReceiver and prompt it to read proxy system properties.
setWebViewProxy()140     private void setWebViewProxy() {
141         LoadedApk loadedApk = getApplication().mLoadedApk;
142         try {
143             Field receiversField = LoadedApk.class.getDeclaredField("mReceivers");
144             receiversField.setAccessible(true);
145             ArrayMap receivers = (ArrayMap) receiversField.get(loadedApk);
146             for (Object receiverMap : receivers.values()) {
147                 for (Object rec : ((ArrayMap) receiverMap).keySet()) {
148                     Class clazz = rec.getClass();
149                     if (clazz.getName().contains("ProxyChangeListener")) {
150                         Method onReceiveMethod = clazz.getDeclaredMethod("onReceive", Context.class,
151                                 Intent.class);
152                         Intent intent = new Intent(Proxy.PROXY_CHANGE_ACTION);
153                         onReceiveMethod.invoke(rec, getApplicationContext(), intent);
154                         Log.v(TAG, "Prompting WebView proxy reload.");
155                     }
156                 }
157             }
158         } catch (Exception e) {
159             Log.e(TAG, "Exception while setting WebView proxy: " + e);
160         }
161     }
162 
done(int result)163     private void done(int result) {
164         if (mNetworkCallback != null) {
165             ConnectivityManager.from(this).unregisterNetworkCallback(mNetworkCallback);
166         }
167         Intent intent = new Intent(ACTION_CAPTIVE_PORTAL_LOGGED_IN);
168         intent.putExtra(Intent.EXTRA_TEXT, String.valueOf(mNetId));
169         intent.putExtra(LOGGED_IN_RESULT, String.valueOf(result));
170         intent.putExtra(RESPONSE_TOKEN, mResponseToken);
171         sendBroadcast(intent);
172         finish();
173     }
174 
175     @Override
onCreateOptionsMenu(Menu menu)176     public boolean onCreateOptionsMenu(Menu menu) {
177         getMenuInflater().inflate(R.menu.captive_portal_login, menu);
178         return true;
179     }
180 
181     @Override
onBackPressed()182     public void onBackPressed() {
183         WebView myWebView = (WebView) findViewById(R.id.webview);
184         if (myWebView.canGoBack()) {
185             myWebView.goBack();
186         } else {
187             super.onBackPressed();
188         }
189     }
190 
191     @Override
onOptionsItemSelected(MenuItem item)192     public boolean onOptionsItemSelected(MenuItem item) {
193         int id = item.getItemId();
194         if (id == R.id.action_use_network) {
195             done(CAPTIVE_PORTAL_APP_RETURN_WANTED_AS_IS);
196             return true;
197         }
198         if (id == R.id.action_do_not_use_network) {
199             done(CAPTIVE_PORTAL_APP_RETURN_UNWANTED);
200             return true;
201         }
202         return super.onOptionsItemSelected(item);
203     }
204 
testForCaptivePortal()205     private void testForCaptivePortal() {
206         new Thread(new Runnable() {
207             public void run() {
208                 // Give time for captive portal to open.
209                 try {
210                     Thread.sleep(1000);
211                 } catch (InterruptedException e) {
212                 }
213                 HttpURLConnection urlConnection = null;
214                 int httpResponseCode = 500;
215                 try {
216                     urlConnection = (HttpURLConnection) mURL.openConnection();
217                     urlConnection.setInstanceFollowRedirects(false);
218                     urlConnection.setConnectTimeout(SOCKET_TIMEOUT_MS);
219                     urlConnection.setReadTimeout(SOCKET_TIMEOUT_MS);
220                     urlConnection.setUseCaches(false);
221                     urlConnection.getInputStream();
222                     httpResponseCode = urlConnection.getResponseCode();
223                 } catch (IOException e) {
224                 } finally {
225                     if (urlConnection != null) urlConnection.disconnect();
226                 }
227                 if (httpResponseCode == 204) {
228                     done(CAPTIVE_PORTAL_APP_RETURN_APPEASED);
229                 }
230             }
231         }).start();
232     }
233 
234     private class MyWebViewClient extends WebViewClient {
235         private boolean firstPageLoad = true;
236 
237         @Override
onPageStarted(WebView view, String url, Bitmap favicon)238         public void onPageStarted(WebView view, String url, Bitmap favicon) {
239             if (firstPageLoad) return;
240             testForCaptivePortal();
241         }
242 
243         @Override
onPageFinished(WebView view, String url)244         public void onPageFinished(WebView view, String url) {
245             if (firstPageLoad) {
246                 firstPageLoad = false;
247                 // Now that WebView has loaded at least one page we know it has read in the proxy
248                 // settings.  Now prompt the WebView read the Network-specific proxy settings.
249                 setWebViewProxy();
250                 // Load the real page.
251                 view.loadUrl(mURL.toString());
252                 return;
253             }
254             testForCaptivePortal();
255         }
256 
257         // A web page consisting of a large broken lock icon to indicate SSL failure.
258         final static String SSL_ERROR_HTML = "<!DOCTYPE html><html><head><style>" +
259                 "html { width:100%; height:100%; " +
260                 "       background:url(locked_page.png) center center no-repeat; }" +
261                 "</style></head><body></body></html>";
262 
263         @Override
onReceivedSslError(WebView view, SslErrorHandler handler, SslError error)264         public void onReceivedSslError(WebView view, SslErrorHandler handler, SslError error) {
265             Log.w(TAG, "SSL error; displaying broken lock icon.");
266             view.loadDataWithBaseURL("file:///android_asset/", SSL_ERROR_HTML, "text/HTML",
267                     "UTF-8", null);
268         }
269     }
270 
271     private class MyWebChromeClient extends WebChromeClient {
272         @Override
onProgressChanged(WebView view, int newProgress)273         public void onProgressChanged(WebView view, int newProgress) {
274             ProgressBar myProgressBar = (ProgressBar) findViewById(R.id.progress_bar);
275             myProgressBar.setProgress(newProgress);
276             myProgressBar.setVisibility(newProgress == 100 ? View.GONE : View.VISIBLE);
277         }
278     }
279 }
280