CaptivePortalLoginActivity.java revision 25a217c0fbda9bbaf58ec08b91115e99f73b727f
1ef22aa5727b96e9a0863ef71cfbe3dbdac339408Jean-Luc Brouillet/* 2ef22aa5727b96e9a0863ef71cfbe3dbdac339408Jean-Luc Brouillet * Copyright (C) 2014 The Android Open Source Project 3ef22aa5727b96e9a0863ef71cfbe3dbdac339408Jean-Luc Brouillet * 4ef22aa5727b96e9a0863ef71cfbe3dbdac339408Jean-Luc Brouillet * Licensed under the Apache License, Version 2.0 (the "License"); 5ef22aa5727b96e9a0863ef71cfbe3dbdac339408Jean-Luc Brouillet * you may not use this file except in compliance with the License. 6ef22aa5727b96e9a0863ef71cfbe3dbdac339408Jean-Luc Brouillet * You may obtain a copy of the License at 7ef22aa5727b96e9a0863ef71cfbe3dbdac339408Jean-Luc Brouillet * 8ef22aa5727b96e9a0863ef71cfbe3dbdac339408Jean-Luc Brouillet * http://www.apache.org/licenses/LICENSE-2.0 9ef22aa5727b96e9a0863ef71cfbe3dbdac339408Jean-Luc Brouillet * 10ef22aa5727b96e9a0863ef71cfbe3dbdac339408Jean-Luc Brouillet * Unless required by applicable law or agreed to in writing, software 11ef22aa5727b96e9a0863ef71cfbe3dbdac339408Jean-Luc Brouillet * distributed under the License is distributed on an "AS IS" BASIS, 12ef22aa5727b96e9a0863ef71cfbe3dbdac339408Jean-Luc Brouillet * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13ef22aa5727b96e9a0863ef71cfbe3dbdac339408Jean-Luc Brouillet * See the License for the specific language governing permissions and 14ef22aa5727b96e9a0863ef71cfbe3dbdac339408Jean-Luc Brouillet * limitations under the License. 15ef22aa5727b96e9a0863ef71cfbe3dbdac339408Jean-Luc Brouillet */ 16ef22aa5727b96e9a0863ef71cfbe3dbdac339408Jean-Luc Brouillet 17ef22aa5727b96e9a0863ef71cfbe3dbdac339408Jean-Luc Brouilletpackage com.android.captiveportallogin; 18ef22aa5727b96e9a0863ef71cfbe3dbdac339408Jean-Luc Brouillet 19ef22aa5727b96e9a0863ef71cfbe3dbdac339408Jean-Luc Brouilletimport android.app.Activity; 20ef22aa5727b96e9a0863ef71cfbe3dbdac339408Jean-Luc Brouilletimport android.app.LoadedApk; 21ef22aa5727b96e9a0863ef71cfbe3dbdac339408Jean-Luc Brouilletimport android.content.Context; 22ef22aa5727b96e9a0863ef71cfbe3dbdac339408Jean-Luc Brouilletimport android.content.Intent; 23c5e342b62bf25a98d15ae28ee97916b274296e94Jean-Luc Brouilletimport android.graphics.Bitmap; 24ef22aa5727b96e9a0863ef71cfbe3dbdac339408Jean-Luc Brouilletimport android.net.ConnectivityManager; 25ef22aa5727b96e9a0863ef71cfbe3dbdac339408Jean-Luc Brouilletimport android.net.ConnectivityManager.NetworkCallback; 26ef22aa5727b96e9a0863ef71cfbe3dbdac339408Jean-Luc Brouilletimport android.net.Network; 27ef22aa5727b96e9a0863ef71cfbe3dbdac339408Jean-Luc Brouilletimport android.net.NetworkCapabilities; 28ef22aa5727b96e9a0863ef71cfbe3dbdac339408Jean-Luc Brouilletimport android.net.NetworkRequest; 29ef22aa5727b96e9a0863ef71cfbe3dbdac339408Jean-Luc Brouilletimport android.net.Proxy; 30ef22aa5727b96e9a0863ef71cfbe3dbdac339408Jean-Luc Brouilletimport android.net.Uri; 31ef22aa5727b96e9a0863ef71cfbe3dbdac339408Jean-Luc Brouilletimport android.net.http.SslError; 32ef22aa5727b96e9a0863ef71cfbe3dbdac339408Jean-Luc Brouilletimport android.os.Bundle; 33ef22aa5727b96e9a0863ef71cfbe3dbdac339408Jean-Luc Brouilletimport android.provider.Settings; 34ef22aa5727b96e9a0863ef71cfbe3dbdac339408Jean-Luc Brouilletimport android.provider.Settings.Global; 35ef22aa5727b96e9a0863ef71cfbe3dbdac339408Jean-Luc Brouilletimport android.util.ArrayMap; 3647b5916d2c4c5d0e0d4f6b43075a23449c16345bDavid Grossimport android.util.Log; 37d56b2323c6e367d77bc226c5e35908d9512af79eMichael Butlerimport android.view.Menu; 38d56b2323c6e367d77bc226c5e35908d9512af79eMichael Butlerimport android.view.MenuItem; 39ef22aa5727b96e9a0863ef71cfbe3dbdac339408Jean-Luc Brouilletimport android.view.View; 40ef22aa5727b96e9a0863ef71cfbe3dbdac339408Jean-Luc Brouilletimport android.view.Window; 4147b5916d2c4c5d0e0d4f6b43075a23449c16345bDavid Grossimport android.webkit.SslErrorHandler; 42a116e12f3a150b3dfc6afa91b0997e551490a9f8Jean-Luc Brouilletimport android.webkit.WebChromeClient; 43820215d28bed6c90f696cde0f282445d16da432eMiao Wangimport android.webkit.WebSettings; 44ef22aa5727b96e9a0863ef71cfbe3dbdac339408Jean-Luc Brouilletimport android.webkit.WebView; 4547b5916d2c4c5d0e0d4f6b43075a23449c16345bDavid Grossimport android.webkit.WebViewClient; 4647b5916d2c4c5d0e0d4f6b43075a23449c16345bDavid Grossimport android.widget.ProgressBar; 4747b5916d2c4c5d0e0d4f6b43075a23449c16345bDavid Gross 48ef22aa5727b96e9a0863ef71cfbe3dbdac339408Jean-Luc Brouilletimport java.io.IOException; 49ef22aa5727b96e9a0863ef71cfbe3dbdac339408Jean-Luc Brouilletimport java.net.HttpURLConnection; 50ef22aa5727b96e9a0863ef71cfbe3dbdac339408Jean-Luc Brouilletimport java.net.MalformedURLException; 51ef22aa5727b96e9a0863ef71cfbe3dbdac339408Jean-Luc Brouilletimport java.net.URL; 52d56b2323c6e367d77bc226c5e35908d9512af79eMichael Butlerimport java.lang.InterruptedException; 53d56b2323c6e367d77bc226c5e35908d9512af79eMichael Butlerimport java.lang.reflect.Field; 54820215d28bed6c90f696cde0f282445d16da432eMiao Wangimport java.lang.reflect.Method; 55ef22aa5727b96e9a0863ef71cfbe3dbdac339408Jean-Luc Brouillet 56ef22aa5727b96e9a0863ef71cfbe3dbdac339408Jean-Luc Brouilletpublic class CaptivePortalLoginActivity extends Activity { 57ef22aa5727b96e9a0863ef71cfbe3dbdac339408Jean-Luc Brouillet private static final String TAG = "CaptivePortalLogin"; 58ef22aa5727b96e9a0863ef71cfbe3dbdac339408Jean-Luc Brouillet private static final String DEFAULT_SERVER = "connectivitycheck.android.com"; 59d56b2323c6e367d77bc226c5e35908d9512af79eMichael Butler private static final int SOCKET_TIMEOUT_MS = 10000; 60ef22aa5727b96e9a0863ef71cfbe3dbdac339408Jean-Luc Brouillet 61ef22aa5727b96e9a0863ef71cfbe3dbdac339408Jean-Luc Brouillet private enum Result { DISMISSED, UNWANTED, WANTED_AS_IS }; 62ef22aa5727b96e9a0863ef71cfbe3dbdac339408Jean-Luc Brouillet 63ef22aa5727b96e9a0863ef71cfbe3dbdac339408Jean-Luc Brouillet private URL mURL; 64ef22aa5727b96e9a0863ef71cfbe3dbdac339408Jean-Luc Brouillet private Network mNetwork; 65ef22aa5727b96e9a0863ef71cfbe3dbdac339408Jean-Luc Brouillet private String mResponseToken; 66ef22aa5727b96e9a0863ef71cfbe3dbdac339408Jean-Luc Brouillet private NetworkCallback mNetworkCallback; 67ef22aa5727b96e9a0863ef71cfbe3dbdac339408Jean-Luc Brouillet private ConnectivityManager mCm; 68ef22aa5727b96e9a0863ef71cfbe3dbdac339408Jean-Luc Brouillet 69ef22aa5727b96e9a0863ef71cfbe3dbdac339408Jean-Luc Brouillet @Override 70ef22aa5727b96e9a0863ef71cfbe3dbdac339408Jean-Luc Brouillet protected void onCreate(Bundle savedInstanceState) { 71ef22aa5727b96e9a0863ef71cfbe3dbdac339408Jean-Luc Brouillet super.onCreate(savedInstanceState); 72ef22aa5727b96e9a0863ef71cfbe3dbdac339408Jean-Luc Brouillet 73ef22aa5727b96e9a0863ef71cfbe3dbdac339408Jean-Luc Brouillet String server = Settings.Global.getString(getContentResolver(), "captive_portal_server"); 74ef22aa5727b96e9a0863ef71cfbe3dbdac339408Jean-Luc Brouillet if (server == null) server = DEFAULT_SERVER; 75ef22aa5727b96e9a0863ef71cfbe3dbdac339408Jean-Luc Brouillet mCm = ConnectivityManager.from(this); 76ef22aa5727b96e9a0863ef71cfbe3dbdac339408Jean-Luc Brouillet try { 77ef22aa5727b96e9a0863ef71cfbe3dbdac339408Jean-Luc Brouillet mURL = new URL("http", server, "/generate_204"); 78ef22aa5727b96e9a0863ef71cfbe3dbdac339408Jean-Luc Brouillet } catch (MalformedURLException e) { 79ef22aa5727b96e9a0863ef71cfbe3dbdac339408Jean-Luc Brouillet // System misconfigured, bail out in a way that at least provides network access. 80ef22aa5727b96e9a0863ef71cfbe3dbdac339408Jean-Luc Brouillet Log.e(TAG, "Invalid captive portal URL, server=" + server); 81ef22aa5727b96e9a0863ef71cfbe3dbdac339408Jean-Luc Brouillet done(Result.WANTED_AS_IS); 82ef22aa5727b96e9a0863ef71cfbe3dbdac339408Jean-Luc Brouillet } 83ef22aa5727b96e9a0863ef71cfbe3dbdac339408Jean-Luc Brouillet mNetwork = getIntent().getParcelableExtra(ConnectivityManager.EXTRA_NETWORK); 84 mResponseToken = getIntent().getStringExtra(ConnectivityManager.EXTRA_CAPTIVE_PORTAL_TOKEN); 85 86 // Also initializes proxy system properties. 87 mCm.bindProcessToNetwork(mNetwork); 88 89 // Proxy system properties must be initialized before setContentView is called because 90 // setContentView initializes the WebView logic which in turn reads the system properties. 91 setContentView(R.layout.activity_captive_portal_login); 92 93 getActionBar().setDisplayShowHomeEnabled(false); 94 95 // Exit app if Network disappears. 96 final NetworkCapabilities networkCapabilities = mCm.getNetworkCapabilities(mNetwork); 97 if (networkCapabilities == null) { 98 finish(); 99 return; 100 } 101 mNetworkCallback = new NetworkCallback() { 102 @Override 103 public void onLost(Network lostNetwork) { 104 if (mNetwork.equals(lostNetwork)) done(Result.UNWANTED); 105 } 106 }; 107 final NetworkRequest.Builder builder = new NetworkRequest.Builder(); 108 for (int transportType : networkCapabilities.getTransportTypes()) { 109 builder.addTransportType(transportType); 110 } 111 mCm.registerNetworkCallback(builder.build(), mNetworkCallback); 112 113 final WebView myWebView = (WebView) findViewById(R.id.webview); 114 myWebView.clearCache(true); 115 WebSettings webSettings = myWebView.getSettings(); 116 webSettings.setJavaScriptEnabled(true); 117 myWebView.setWebViewClient(new MyWebViewClient()); 118 myWebView.setWebChromeClient(new MyWebChromeClient()); 119 // Start initial page load so WebView finishes loading proxy settings. 120 // Actual load of mUrl is initiated by MyWebViewClient. 121 myWebView.loadData("", "text/html", null); 122 } 123 124 // Find WebView's proxy BroadcastReceiver and prompt it to read proxy system properties. 125 private void setWebViewProxy() { 126 LoadedApk loadedApk = getApplication().mLoadedApk; 127 try { 128 Field receiversField = LoadedApk.class.getDeclaredField("mReceivers"); 129 receiversField.setAccessible(true); 130 ArrayMap receivers = (ArrayMap) receiversField.get(loadedApk); 131 for (Object receiverMap : receivers.values()) { 132 for (Object rec : ((ArrayMap) receiverMap).keySet()) { 133 Class clazz = rec.getClass(); 134 if (clazz.getName().contains("ProxyChangeListener")) { 135 Method onReceiveMethod = clazz.getDeclaredMethod("onReceive", Context.class, 136 Intent.class); 137 Intent intent = new Intent(Proxy.PROXY_CHANGE_ACTION); 138 onReceiveMethod.invoke(rec, getApplicationContext(), intent); 139 Log.v(TAG, "Prompting WebView proxy reload."); 140 } 141 } 142 } 143 } catch (Exception e) { 144 Log.e(TAG, "Exception while setting WebView proxy: " + e); 145 } 146 } 147 148 private void done(Result result) { 149 if (mNetworkCallback != null) { 150 mCm.unregisterNetworkCallback(mNetworkCallback); 151 } 152 switch (result) { 153 case DISMISSED: 154 mCm.reportCaptivePortalDismissed(mNetwork, mResponseToken); 155 break; 156 case UNWANTED: 157 mCm.ignoreNetworkWithCaptivePortal(mNetwork, mResponseToken); 158 break; 159 case WANTED_AS_IS: 160 mCm.useNetworkWithCaptivePortal(mNetwork, mResponseToken); 161 break; 162 } 163 finish(); 164 } 165 166 @Override 167 public boolean onCreateOptionsMenu(Menu menu) { 168 getMenuInflater().inflate(R.menu.captive_portal_login, menu); 169 return true; 170 } 171 172 @Override 173 public void onBackPressed() { 174 WebView myWebView = (WebView) findViewById(R.id.webview); 175 if (myWebView.canGoBack()) { 176 myWebView.goBack(); 177 } else { 178 super.onBackPressed(); 179 } 180 } 181 182 @Override 183 public boolean onOptionsItemSelected(MenuItem item) { 184 int id = item.getItemId(); 185 if (id == R.id.action_use_network) { 186 done(Result.WANTED_AS_IS); 187 return true; 188 } 189 if (id == R.id.action_do_not_use_network) { 190 done(Result.UNWANTED); 191 return true; 192 } 193 return super.onOptionsItemSelected(item); 194 } 195 196 private void testForCaptivePortal() { 197 new Thread(new Runnable() { 198 public void run() { 199 // Give time for captive portal to open. 200 try { 201 Thread.sleep(1000); 202 } catch (InterruptedException e) { 203 } 204 HttpURLConnection urlConnection = null; 205 int httpResponseCode = 500; 206 try { 207 urlConnection = (HttpURLConnection) mURL.openConnection(); 208 urlConnection.setInstanceFollowRedirects(false); 209 urlConnection.setConnectTimeout(SOCKET_TIMEOUT_MS); 210 urlConnection.setReadTimeout(SOCKET_TIMEOUT_MS); 211 urlConnection.setUseCaches(false); 212 urlConnection.getInputStream(); 213 httpResponseCode = urlConnection.getResponseCode(); 214 } catch (IOException e) { 215 } finally { 216 if (urlConnection != null) urlConnection.disconnect(); 217 } 218 if (httpResponseCode == 204) { 219 done(Result.DISMISSED); 220 } 221 } 222 }).start(); 223 } 224 225 private class MyWebViewClient extends WebViewClient { 226 private boolean firstPageLoad = true; 227 228 @Override 229 public void onPageStarted(WebView view, String url, Bitmap favicon) { 230 if (firstPageLoad) return; 231 testForCaptivePortal(); 232 } 233 234 @Override 235 public void onPageFinished(WebView view, String url) { 236 if (firstPageLoad) { 237 firstPageLoad = false; 238 // Now that WebView has loaded at least one page we know it has read in the proxy 239 // settings. Now prompt the WebView read the Network-specific proxy settings. 240 setWebViewProxy(); 241 // Load the real page. 242 view.loadUrl(mURL.toString()); 243 return; 244 } 245 testForCaptivePortal(); 246 } 247 248 // A web page consisting of a large broken lock icon to indicate SSL failure. 249 final static String SSL_ERROR_HTML = "<!DOCTYPE html><html><head><style>" + 250 "html { width:100%; height:100%; " + 251 " background:url(locked_page.png) center center no-repeat; }" + 252 "</style></head><body></body></html>"; 253 254 @Override 255 public void onReceivedSslError(WebView view, SslErrorHandler handler, SslError error) { 256 Log.w(TAG, "SSL error; displaying broken lock icon."); 257 view.loadDataWithBaseURL("file:///android_asset/", SSL_ERROR_HTML, "text/HTML", 258 "UTF-8", null); 259 } 260 } 261 262 private class MyWebChromeClient extends WebChromeClient { 263 @Override 264 public void onProgressChanged(WebView view, int newProgress) { 265 ProgressBar myProgressBar = (ProgressBar) findViewById(R.id.progress_bar); 266 myProgressBar.setProgress(newProgress); 267 myProgressBar.setVisibility(newProgress == 100 ? View.GONE : View.VISIBLE); 268 } 269 } 270} 271