CaptivePortalLoginActivity.java revision 5344a4abdf239a19485a9c858b6cc3be96002eac
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        }
150        switch (result) {
151            case DISMISSED:
152                mCm.reportCaptivePortalDismissed(mNetwork, mResponseToken);
153                break;
154            case UNWANTED:
155                mCm.ignoreNetworkWithCaptivePortal(mNetwork, mResponseToken);
156                break;
157            case WANTED_AS_IS:
158                mCm.useNetworkWithCaptivePortal(mNetwork, mResponseToken);
159                break;
160        }
161        finish();
162    }
163
164    @Override
165    public boolean onCreateOptionsMenu(Menu menu) {
166        getMenuInflater().inflate(R.menu.captive_portal_login, menu);
167        return true;
168    }
169
170    @Override
171    public void onBackPressed() {
172        WebView myWebView = (WebView) findViewById(R.id.webview);
173        if (myWebView.canGoBack()) {
174            myWebView.goBack();
175        } else {
176            super.onBackPressed();
177        }
178    }
179
180    @Override
181    public boolean onOptionsItemSelected(MenuItem item) {
182        int id = item.getItemId();
183        if (id == R.id.action_use_network) {
184            done(Result.WANTED_AS_IS);
185            return true;
186        }
187        if (id == R.id.action_do_not_use_network) {
188            done(Result.UNWANTED);
189            return true;
190        }
191        return super.onOptionsItemSelected(item);
192    }
193
194    private void testForCaptivePortal() {
195        new Thread(new Runnable() {
196            public void run() {
197                // Give time for captive portal to open.
198                try {
199                    Thread.sleep(1000);
200                } catch (InterruptedException e) {
201                }
202                HttpURLConnection urlConnection = null;
203                int httpResponseCode = 500;
204                try {
205                    urlConnection = (HttpURLConnection) mURL.openConnection();
206                    urlConnection.setInstanceFollowRedirects(false);
207                    urlConnection.setConnectTimeout(SOCKET_TIMEOUT_MS);
208                    urlConnection.setReadTimeout(SOCKET_TIMEOUT_MS);
209                    urlConnection.setUseCaches(false);
210                    urlConnection.getInputStream();
211                    httpResponseCode = urlConnection.getResponseCode();
212                } catch (IOException e) {
213                } finally {
214                    if (urlConnection != null) urlConnection.disconnect();
215                }
216                if (httpResponseCode == 204) {
217                    done(Result.DISMISSED);
218                }
219            }
220        }).start();
221    }
222
223    private class MyWebViewClient extends WebViewClient {
224        private static final String INTERNAL_ASSETS = "file:///android_asset/";
225        private boolean firstPageLoad = true;
226
227        @Override
228        public void onPageStarted(WebView view, String url, Bitmap favicon) {
229            if (firstPageLoad) return;
230            testForCaptivePortal();
231        }
232
233        @Override
234        public void onPageFinished(WebView view, String url) {
235            if (firstPageLoad) {
236                firstPageLoad = false;
237                // Now that WebView has loaded at least one page we know it has read in the proxy
238                // settings.  Now prompt the WebView read the Network-specific proxy settings.
239                setWebViewProxy();
240                // Load the real page.
241                view.loadUrl(mURL.toString());
242                return;
243            }
244            // For internally generated pages, leave URL bar listing prior URL as this is the URL
245            // the page refers to.
246            if (!url.startsWith(INTERNAL_ASSETS)) {
247                final TextView myUrlBar = (TextView) findViewById(R.id.url_bar);
248                myUrlBar.setText(url);
249            }
250            testForCaptivePortal();
251        }
252
253        // A web page consisting of a large broken lock icon to indicate SSL failure.
254        final static String SSL_ERROR_HTML = "<!DOCTYPE html><html><head><style>" +
255                "html { width:100%; height:100%; " +
256                "       background:url(locked_page.png) center center no-repeat; }" +
257                "</style></head><body></body></html>";
258
259        @Override
260        public void onReceivedSslError(WebView view, SslErrorHandler handler, SslError error) {
261            Log.w(TAG, "SSL error; displaying broken lock icon.");
262            view.loadDataWithBaseURL(INTERNAL_ASSETS, SSL_ERROR_HTML, "text/HTML", "UTF-8", null);
263        }
264    }
265
266    private class MyWebChromeClient extends WebChromeClient {
267        @Override
268        public void onProgressChanged(WebView view, int newProgress) {
269            final ProgressBar myProgressBar = (ProgressBar) findViewById(R.id.progress_bar);
270            myProgressBar.setProgress(newProgress);
271        }
272    }
273}
274