CaptivePortalLoginActivity.java revision 868f6243bc6021465938a7b764bef8fd001cb39c
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
17package com.android.captiveportallogin;
18
19import android.app.Activity;
20import android.app.LoadedApk;
21import android.content.Context;
22import android.content.Intent;
23import android.graphics.Bitmap;
24import android.net.ConnectivityManager;
25import android.net.ConnectivityManager.NetworkCallback;
26import android.net.Network;
27import android.net.NetworkCapabilities;
28import android.net.NetworkRequest;
29import android.net.Proxy;
30import android.net.Uri;
31import android.net.http.SslError;
32import android.os.Bundle;
33import android.provider.Settings;
34import android.util.ArrayMap;
35import android.util.Log;
36import android.view.Menu;
37import android.view.MenuItem;
38import android.webkit.SslErrorHandler;
39import android.webkit.WebChromeClient;
40import android.webkit.WebSettings;
41import android.webkit.WebView;
42import android.webkit.WebViewClient;
43import android.widget.ProgressBar;
44import android.widget.TextView;
45
46import java.io.IOException;
47import java.net.HttpURLConnection;
48import java.net.MalformedURLException;
49import java.net.URL;
50import java.lang.InterruptedException;
51import java.lang.reflect.Field;
52import java.lang.reflect.Method;
53
54public class CaptivePortalLoginActivity extends Activity {
55    private static final String TAG = "CaptivePortalLogin";
56    private static final String DEFAULT_SERVER = "connectivitycheck.android.com";
57    private static final int SOCKET_TIMEOUT_MS = 10000;
58
59    private enum Result { DISMISSED, UNWANTED, WANTED_AS_IS };
60
61    private URL mURL;
62    private Network mNetwork;
63    private String mResponseToken;
64    private NetworkCallback mNetworkCallback;
65    private ConnectivityManager mCm;
66
67    @Override
68    protected void onCreate(Bundle savedInstanceState) {
69        super.onCreate(savedInstanceState);
70
71        String server = Settings.Global.getString(getContentResolver(), "captive_portal_server");
72        if (server == null) server = DEFAULT_SERVER;
73        mCm = ConnectivityManager.from(this);
74        try {
75            mURL = new URL("http", server, "/generate_204");
76        } catch (MalformedURLException e) {
77            // System misconfigured, bail out in a way that at least provides network access.
78            Log.e(TAG, "Invalid captive portal URL, server=" + server);
79            done(Result.WANTED_AS_IS);
80        }
81        mNetwork = getIntent().getParcelableExtra(ConnectivityManager.EXTRA_NETWORK);
82        mResponseToken = getIntent().getStringExtra(ConnectivityManager.EXTRA_CAPTIVE_PORTAL_TOKEN);
83
84        // Also initializes proxy system properties.
85        mCm.bindProcessToNetwork(mNetwork);
86
87        // Proxy system properties must be initialized before setContentView is called because
88        // setContentView initializes the WebView logic which in turn reads the system properties.
89        setContentView(R.layout.activity_captive_portal_login);
90
91        getActionBar().setDisplayShowHomeEnabled(false);
92
93        // Exit app if Network disappears.
94        final NetworkCapabilities networkCapabilities = mCm.getNetworkCapabilities(mNetwork);
95        if (networkCapabilities == null) {
96            finish();
97            return;
98        }
99        mNetworkCallback = new NetworkCallback() {
100            @Override
101            public void onLost(Network lostNetwork) {
102                if (mNetwork.equals(lostNetwork)) done(Result.UNWANTED);
103            }
104        };
105        final NetworkRequest.Builder builder = new NetworkRequest.Builder();
106        for (int transportType : networkCapabilities.getTransportTypes()) {
107            builder.addTransportType(transportType);
108        }
109        mCm.registerNetworkCallback(builder.build(), mNetworkCallback);
110
111        final WebView myWebView = (WebView) findViewById(R.id.webview);
112        myWebView.clearCache(true);
113        WebSettings webSettings = myWebView.getSettings();
114        webSettings.setJavaScriptEnabled(true);
115        myWebView.setWebViewClient(new MyWebViewClient());
116        myWebView.setWebChromeClient(new MyWebChromeClient());
117        // Start initial page load so WebView finishes loading proxy settings.
118        // Actual load of mUrl is initiated by MyWebViewClient.
119        myWebView.loadData("", "text/html", null);
120    }
121
122    // Find WebView's proxy BroadcastReceiver and prompt it to read proxy system properties.
123    private void setWebViewProxy() {
124        LoadedApk loadedApk = getApplication().mLoadedApk;
125        try {
126            Field receiversField = LoadedApk.class.getDeclaredField("mReceivers");
127            receiversField.setAccessible(true);
128            ArrayMap receivers = (ArrayMap) receiversField.get(loadedApk);
129            for (Object receiverMap : receivers.values()) {
130                for (Object rec : ((ArrayMap) receiverMap).keySet()) {
131                    Class clazz = rec.getClass();
132                    if (clazz.getName().contains("ProxyChangeListener")) {
133                        Method onReceiveMethod = clazz.getDeclaredMethod("onReceive", Context.class,
134                                Intent.class);
135                        Intent intent = new Intent(Proxy.PROXY_CHANGE_ACTION);
136                        onReceiveMethod.invoke(rec, getApplicationContext(), intent);
137                        Log.v(TAG, "Prompting WebView proxy reload.");
138                    }
139                }
140            }
141        } catch (Exception e) {
142            Log.e(TAG, "Exception while setting WebView proxy: " + e);
143        }
144    }
145
146    private void done(Result result) {
147        if (mNetworkCallback != null) {
148            mCm.unregisterNetworkCallback(mNetworkCallback);
149            mNetworkCallback = null;
150        }
151        switch (result) {
152            case DISMISSED:
153                mCm.reportCaptivePortalDismissed(mNetwork, mResponseToken);
154                break;
155            case UNWANTED:
156                mCm.ignoreNetworkWithCaptivePortal(mNetwork, mResponseToken);
157                break;
158            case WANTED_AS_IS:
159                mCm.useNetworkWithCaptivePortal(mNetwork, mResponseToken);
160                break;
161        }
162        finish();
163    }
164
165    @Override
166    public boolean onCreateOptionsMenu(Menu menu) {
167        getMenuInflater().inflate(R.menu.captive_portal_login, menu);
168        return true;
169    }
170
171    @Override
172    public void onBackPressed() {
173        WebView myWebView = (WebView) findViewById(R.id.webview);
174        if (myWebView.canGoBack()) {
175            myWebView.goBack();
176        } else {
177            super.onBackPressed();
178        }
179    }
180
181    @Override
182    public boolean onOptionsItemSelected(MenuItem item) {
183        int id = item.getItemId();
184        if (id == R.id.action_use_network) {
185            done(Result.WANTED_AS_IS);
186            return true;
187        }
188        if (id == R.id.action_do_not_use_network) {
189            done(Result.UNWANTED);
190            return true;
191        }
192        return super.onOptionsItemSelected(item);
193    }
194
195    @Override
196    public void onDestroy() {
197        super.onDestroy();
198
199        if (mNetworkCallback != null) {
200            mCm.unregisterNetworkCallback(mNetworkCallback);
201            mNetworkCallback = null;
202        }
203    }
204
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(Result.DISMISSED);
229                }
230            }
231        }).start();
232    }
233
234    private class MyWebViewClient extends WebViewClient {
235        private static final String INTERNAL_ASSETS = "file:///android_asset/";
236        private boolean firstPageLoad = true;
237
238        @Override
239        public void onPageStarted(WebView view, String url, Bitmap favicon) {
240            if (firstPageLoad) return;
241            testForCaptivePortal();
242        }
243
244        @Override
245        public void onPageFinished(WebView view, String url) {
246            if (firstPageLoad) {
247                firstPageLoad = false;
248                // Now that WebView has loaded at least one page we know it has read in the proxy
249                // settings.  Now prompt the WebView read the Network-specific proxy settings.
250                setWebViewProxy();
251                // Load the real page.
252                view.loadUrl(mURL.toString());
253                return;
254            }
255            // For internally generated pages, leave URL bar listing prior URL as this is the URL
256            // the page refers to.
257            if (!url.startsWith(INTERNAL_ASSETS)) {
258                final TextView myUrlBar = (TextView) findViewById(R.id.url_bar);
259                myUrlBar.setText(url);
260            }
261            testForCaptivePortal();
262        }
263
264        // A web page consisting of a large broken lock icon to indicate SSL failure.
265        final static String SSL_ERROR_HTML = "<!DOCTYPE html><html><head><style>" +
266                "html { width:100%; height:100%; " +
267                "       background:url(locked_page.png) center center no-repeat; }" +
268                "</style></head><body></body></html>";
269
270        @Override
271        public void onReceivedSslError(WebView view, SslErrorHandler handler, SslError error) {
272            Log.w(TAG, "SSL error; displaying broken lock icon.");
273            view.loadDataWithBaseURL(INTERNAL_ASSETS, SSL_ERROR_HTML, "text/HTML", "UTF-8", null);
274        }
275    }
276
277    private class MyWebChromeClient extends WebChromeClient {
278        @Override
279        public void onProgressChanged(WebView view, int newProgress) {
280            final ProgressBar myProgressBar = (ProgressBar) findViewById(R.id.progress_bar);
281            myProgressBar.setProgress(newProgress);
282        }
283    }
284}
285