CaptivePortalLoginActivity.java revision cd29cb66f92b008e8547f70b30223ce8dbc1fb86
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.provider.Settings.Global;
35import android.util.ArrayMap;
36import android.util.Log;
37import android.view.Menu;
38import android.view.MenuItem;
39import android.view.View;
40import android.view.Window;
41import android.webkit.SslErrorHandler;
42import android.webkit.WebChromeClient;
43import android.webkit.WebSettings;
44import android.webkit.WebView;
45import android.webkit.WebViewClient;
46import android.widget.ProgressBar;
47
48import java.io.IOException;
49import java.net.HttpURLConnection;
50import java.net.MalformedURLException;
51import java.net.URL;
52import java.lang.InterruptedException;
53import java.lang.reflect.Field;
54import java.lang.reflect.Method;
55
56public 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
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.
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
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
176    public boolean onCreateOptionsMenu(Menu menu) {
177        getMenuInflater().inflate(R.menu.captive_portal_login, menu);
178        return true;
179    }
180
181    @Override
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
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
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
238        public void onPageStarted(WebView view, String url, Bitmap favicon) {
239            if (firstPageLoad) return;
240            testForCaptivePortal();
241        }
242
243        @Override
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
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
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