CaptivePortalLoginActivity.java revision e0bef71662d81caaaa0d7214fb0bef5d39996a69
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.os.Bundle;
32import android.provider.Settings;
33import android.provider.Settings.Global;
34import android.util.ArrayMap;
35import android.util.Log;
36import android.view.Menu;
37import android.view.MenuItem;
38import android.view.View;
39import android.view.Window;
40import android.webkit.WebChromeClient;
41import android.webkit.WebSettings;
42import android.webkit.WebView;
43import android.webkit.WebViewClient;
44import android.widget.ProgressBar;
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 = "clients3.google.com";
57    private static final int SOCKET_TIMEOUT_MS = 10000;
58
59    // Keep this in sync with NetworkMonitor.
60    // Intent broadcast to ConnectivityService indicating sign-in is complete.
61    // Extras:
62    //     EXTRA_TEXT       = netId
63    //     LOGGED_IN_RESULT = one of the CAPTIVE_PORTAL_APP_RETURN_* values below.
64    //     RESPONSE_TOKEN   = data fragment from launching Intent
65    private static final String ACTION_CAPTIVE_PORTAL_LOGGED_IN =
66            "android.net.netmon.captive_portal_logged_in";
67    private static final String LOGGED_IN_RESULT = "result";
68    private static final int CAPTIVE_PORTAL_APP_RETURN_APPEASED = 0;
69    private static final int CAPTIVE_PORTAL_APP_RETURN_UNWANTED = 1;
70    private static final int CAPTIVE_PORTAL_APP_RETURN_WANTED_AS_IS = 2;
71    private static final String RESPONSE_TOKEN = "response_token";
72
73    private URL mURL;
74    private int mNetId;
75    private String mResponseToken;
76    private NetworkCallback mNetworkCallback;
77
78    @Override
79    protected void onCreate(Bundle savedInstanceState) {
80        super.onCreate(savedInstanceState);
81
82        String server = Settings.Global.getString(getContentResolver(), "captive_portal_server");
83        if (server == null) server = DEFAULT_SERVER;
84        try {
85            mURL = new URL("http", server, "/generate_204");
86            final Uri dataUri = getIntent().getData();
87            if (!dataUri.getScheme().equals("netid")) {
88                throw new MalformedURLException();
89            }
90            mNetId = Integer.parseInt(dataUri.getSchemeSpecificPart());
91            mResponseToken = dataUri.getFragment();
92        } catch (MalformedURLException|NumberFormatException e) {
93            // System misconfigured, bail out in a way that at least provides network access.
94            done(CAPTIVE_PORTAL_APP_RETURN_WANTED_AS_IS);
95        }
96
97        final ConnectivityManager cm = ConnectivityManager.from(this);
98        final Network network = new Network(mNetId);
99        // Also initializes proxy system properties.
100        cm.setProcessDefaultNetwork(network);
101
102        // Proxy system properties must be initialized before setContentView is called because
103        // setContentView initializes the WebView logic which in turn reads the system properties.
104        setContentView(R.layout.activity_captive_portal_login);
105
106        getActionBar().setDisplayShowHomeEnabled(false);
107
108        // Exit app if Network disappears.
109        final NetworkCapabilities networkCapabilities = cm.getNetworkCapabilities(network);
110        if (networkCapabilities == null) {
111            finish();
112            return;
113        }
114        mNetworkCallback = new NetworkCallback() {
115            @Override
116            public void onLost(Network lostNetwork) {
117                if (network.equals(lostNetwork)) done(CAPTIVE_PORTAL_APP_RETURN_UNWANTED);
118            }
119        };
120        final NetworkRequest.Builder builder = new NetworkRequest.Builder();
121        for (int transportType : networkCapabilities.getTransportTypes()) {
122            builder.addTransportType(transportType);
123        }
124        cm.registerNetworkCallback(builder.build(), mNetworkCallback);
125
126        final WebView myWebView = (WebView) findViewById(R.id.webview);
127        myWebView.clearCache(true);
128        WebSettings webSettings = myWebView.getSettings();
129        webSettings.setJavaScriptEnabled(true);
130        myWebView.setWebViewClient(new MyWebViewClient());
131        myWebView.setWebChromeClient(new MyWebChromeClient());
132        // Start initial page load so WebView finishes loading proxy settings.
133        // Actual load of mUrl is initiated by MyWebViewClient.
134        myWebView.loadData("", "text/html", null);
135    }
136
137    // Find WebView's proxy BroadcastReceiver and prompt it to read proxy system properties.
138    private void setWebViewProxy() {
139        LoadedApk loadedApk = getApplication().mLoadedApk;
140        try {
141            Field receiversField = LoadedApk.class.getDeclaredField("mReceivers");
142            receiversField.setAccessible(true);
143            ArrayMap receivers = (ArrayMap) receiversField.get(loadedApk);
144            for (Object receiverMap : receivers.values()) {
145                for (Object rec : ((ArrayMap) receiverMap).keySet()) {
146                    Class clazz = rec.getClass();
147                    if (clazz.getName().contains("ProxyChangeListener")) {
148                        Method onReceiveMethod = clazz.getDeclaredMethod("onReceive", Context.class,
149                                Intent.class);
150                        Intent intent = new Intent(Proxy.PROXY_CHANGE_ACTION);
151                        onReceiveMethod.invoke(rec, getApplicationContext(), intent);
152                        Log.v(TAG, "Prompting WebView proxy reload.");
153                    }
154                }
155            }
156        } catch (Exception e) {
157            Log.e(TAG, "Exception while setting WebView proxy: " + e);
158        }
159    }
160
161    private void done(int result) {
162        if (mNetworkCallback != null) {
163            ConnectivityManager.from(this).unregisterNetworkCallback(mNetworkCallback);
164        }
165        Intent intent = new Intent(ACTION_CAPTIVE_PORTAL_LOGGED_IN);
166        intent.putExtra(Intent.EXTRA_TEXT, String.valueOf(mNetId));
167        intent.putExtra(LOGGED_IN_RESULT, String.valueOf(result));
168        intent.putExtra(RESPONSE_TOKEN, mResponseToken);
169        sendBroadcast(intent);
170        finish();
171    }
172
173    @Override
174    public boolean onCreateOptionsMenu(Menu menu) {
175        getMenuInflater().inflate(R.menu.captive_portal_login, menu);
176        return true;
177    }
178
179    @Override
180    public void onBackPressed() {
181        WebView myWebView = (WebView) findViewById(R.id.webview);
182        if (myWebView.canGoBack()) {
183            myWebView.goBack();
184        } else {
185            super.onBackPressed();
186        }
187    }
188
189    @Override
190    public boolean onOptionsItemSelected(MenuItem item) {
191        int id = item.getItemId();
192        if (id == R.id.action_use_network) {
193            done(CAPTIVE_PORTAL_APP_RETURN_WANTED_AS_IS);
194            return true;
195        }
196        if (id == R.id.action_do_not_use_network) {
197            done(CAPTIVE_PORTAL_APP_RETURN_UNWANTED);
198            return true;
199        }
200        return super.onOptionsItemSelected(item);
201    }
202
203    private void testForCaptivePortal() {
204        new Thread(new Runnable() {
205            public void run() {
206                // Give time for captive portal to open.
207                try {
208                    Thread.sleep(1000);
209                } catch (InterruptedException e) {
210                }
211                HttpURLConnection urlConnection = null;
212                int httpResponseCode = 500;
213                try {
214                    urlConnection = (HttpURLConnection) mURL.openConnection();
215                    urlConnection.setInstanceFollowRedirects(false);
216                    urlConnection.setConnectTimeout(SOCKET_TIMEOUT_MS);
217                    urlConnection.setReadTimeout(SOCKET_TIMEOUT_MS);
218                    urlConnection.setUseCaches(false);
219                    urlConnection.getInputStream();
220                    httpResponseCode = urlConnection.getResponseCode();
221                } catch (IOException e) {
222                } finally {
223                    if (urlConnection != null) urlConnection.disconnect();
224                }
225                if (httpResponseCode == 204) {
226                    done(CAPTIVE_PORTAL_APP_RETURN_APPEASED);
227                }
228            }
229        }).start();
230    }
231
232    private class MyWebViewClient extends WebViewClient {
233        private boolean firstPageLoad = true;
234
235        @Override
236        public void onPageStarted(WebView view, String url, Bitmap favicon) {
237            if (firstPageLoad) return;
238            testForCaptivePortal();
239        }
240
241        @Override
242        public void onPageFinished(WebView view, String url) {
243            if (firstPageLoad) {
244                firstPageLoad = false;
245                // Now that WebView has loaded at least one page we know it has read in the proxy
246                // settings.  Now prompt the WebView read the Network-specific proxy settings.
247                setWebViewProxy();
248                // Load the real page.
249                view.loadUrl(mURL.toString());
250                return;
251            }
252            testForCaptivePortal();
253        }
254    }
255
256    private class MyWebChromeClient extends WebChromeClient {
257        @Override
258        public void onProgressChanged(WebView view, int newProgress) {
259            ProgressBar myProgressBar = (ProgressBar) findViewById(R.id.progress_bar);
260            myProgressBar.setProgress(newProgress);
261            myProgressBar.setVisibility(newProgress == 100 ? View.GONE : View.VISIBLE);
262        }
263    }
264}
265