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