Home | History | Annotate | Download | only in net
      1 /*
      2  * Copyright (C) 2012 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 
     17 package android.net;
     18 
     19 import android.app.Activity;
     20 import android.app.Notification;
     21 import android.app.NotificationManager;
     22 import android.app.PendingIntent;
     23 import android.content.BroadcastReceiver;
     24 import android.content.Context;
     25 import android.content.Intent;
     26 import android.content.IntentFilter;
     27 import android.content.res.Resources;
     28 import android.database.ContentObserver;
     29 import android.net.ConnectivityManager;
     30 import android.net.IConnectivityManager;
     31 import android.os.Handler;
     32 import android.os.UserHandle;
     33 import android.os.Message;
     34 import android.os.RemoteException;
     35 import android.provider.Settings;
     36 import android.telephony.TelephonyManager;
     37 import android.text.TextUtils;
     38 
     39 import com.android.internal.util.State;
     40 import com.android.internal.util.StateMachine;
     41 
     42 import java.io.IOException;
     43 import java.net.HttpURLConnection;
     44 import java.net.InetAddress;
     45 import java.net.Inet4Address;
     46 import java.net.SocketTimeoutException;
     47 import java.net.URL;
     48 import java.net.UnknownHostException;
     49 
     50 import com.android.internal.R;
     51 
     52 /**
     53  * This class allows captive portal detection on a network.
     54  * @hide
     55  */
     56 public class CaptivePortalTracker extends StateMachine {
     57     private static final boolean DBG = true;
     58     private static final String TAG = "CaptivePortalTracker";
     59 
     60     private static final String DEFAULT_SERVER = "clients3.google.com";
     61 
     62     private static final int SOCKET_TIMEOUT_MS = 10000;
     63 
     64     private String mServer;
     65     private String mUrl;
     66     private boolean mIsCaptivePortalCheckEnabled = false;
     67     private IConnectivityManager mConnService;
     68     private TelephonyManager mTelephonyManager;
     69     private Context mContext;
     70     private NetworkInfo mNetworkInfo;
     71 
     72     private static final int CMD_DETECT_PORTAL          = 0;
     73     private static final int CMD_CONNECTIVITY_CHANGE    = 1;
     74     private static final int CMD_DELAYED_CAPTIVE_CHECK  = 2;
     75 
     76     /* This delay happens every time before we do a captive check on a network */
     77     private static final int DELAYED_CHECK_INTERVAL_MS = 10000;
     78     private int mDelayedCheckToken = 0;
     79 
     80     private State mDefaultState = new DefaultState();
     81     private State mNoActiveNetworkState = new NoActiveNetworkState();
     82     private State mActiveNetworkState = new ActiveNetworkState();
     83     private State mDelayedCaptiveCheckState = new DelayedCaptiveCheckState();
     84 
     85     private static final String SETUP_WIZARD_PACKAGE = "com.google.android.setupwizard";
     86     private boolean mDeviceProvisioned = false;
     87     private ProvisioningObserver mProvisioningObserver;
     88 
     89     private CaptivePortalTracker(Context context, IConnectivityManager cs) {
     90         super(TAG);
     91 
     92         mContext = context;
     93         mConnService = cs;
     94         mTelephonyManager = (TelephonyManager) context.getSystemService(Context.TELEPHONY_SERVICE);
     95         mProvisioningObserver = new ProvisioningObserver();
     96 
     97         IntentFilter filter = new IntentFilter();
     98         filter.addAction(ConnectivityManager.CONNECTIVITY_ACTION);
     99         filter.addAction(ConnectivityManager.CONNECTIVITY_ACTION_IMMEDIATE);
    100         mContext.registerReceiver(mReceiver, filter);
    101 
    102         mServer = Settings.Global.getString(mContext.getContentResolver(),
    103                 Settings.Global.CAPTIVE_PORTAL_SERVER);
    104         if (mServer == null) mServer = DEFAULT_SERVER;
    105 
    106         mIsCaptivePortalCheckEnabled = Settings.Global.getInt(mContext.getContentResolver(),
    107                 Settings.Global.CAPTIVE_PORTAL_DETECTION_ENABLED, 1) == 1;
    108 
    109         addState(mDefaultState);
    110             addState(mNoActiveNetworkState, mDefaultState);
    111             addState(mActiveNetworkState, mDefaultState);
    112                 addState(mDelayedCaptiveCheckState, mActiveNetworkState);
    113         setInitialState(mNoActiveNetworkState);
    114     }
    115 
    116     private class ProvisioningObserver extends ContentObserver {
    117         ProvisioningObserver() {
    118             super(new Handler());
    119             mContext.getContentResolver().registerContentObserver(Settings.Global.getUriFor(
    120                     Settings.Global.DEVICE_PROVISIONED), false, this);
    121             onChange(false); // load initial value
    122         }
    123 
    124         @Override
    125         public void onChange(boolean selfChange) {
    126             mDeviceProvisioned = Settings.Global.getInt(mContext.getContentResolver(),
    127                     Settings.Global.DEVICE_PROVISIONED, 0) != 0;
    128         }
    129     }
    130 
    131     private final BroadcastReceiver mReceiver = new BroadcastReceiver() {
    132         @Override
    133         public void onReceive(Context context, Intent intent) {
    134             String action = intent.getAction();
    135             // Normally, we respond to CONNECTIVITY_ACTION, allowing time for the change in
    136             // connectivity to stabilize, but if the device is not yet provisioned, respond
    137             // immediately to speed up transit through the setup wizard.
    138             if ((mDeviceProvisioned && action.equals(ConnectivityManager.CONNECTIVITY_ACTION))
    139                     || (!mDeviceProvisioned
    140                             && action.equals(ConnectivityManager.CONNECTIVITY_ACTION_IMMEDIATE))) {
    141                 NetworkInfo info = intent.getParcelableExtra(
    142                         ConnectivityManager.EXTRA_NETWORK_INFO);
    143                 sendMessage(obtainMessage(CMD_CONNECTIVITY_CHANGE, info));
    144             }
    145         }
    146     };
    147 
    148     public static CaptivePortalTracker makeCaptivePortalTracker(Context context,
    149             IConnectivityManager cs) {
    150         CaptivePortalTracker captivePortal = new CaptivePortalTracker(context, cs);
    151         captivePortal.start();
    152         return captivePortal;
    153     }
    154 
    155     public void detectCaptivePortal(NetworkInfo info) {
    156         sendMessage(obtainMessage(CMD_DETECT_PORTAL, info));
    157     }
    158 
    159     private class DefaultState extends State {
    160         @Override
    161         public void enter() {
    162             setNotificationOff();
    163         }
    164 
    165         @Override
    166         public boolean processMessage(Message message) {
    167             if (DBG) log(getName() + message.toString());
    168             switch (message.what) {
    169                 case CMD_DETECT_PORTAL:
    170                     NetworkInfo info = (NetworkInfo) message.obj;
    171                     // Checking on a secondary connection is not supported
    172                     // yet
    173                     notifyPortalCheckComplete(info);
    174                     break;
    175                 case CMD_CONNECTIVITY_CHANGE:
    176                 case CMD_DELAYED_CAPTIVE_CHECK:
    177                     break;
    178                 default:
    179                     loge("Ignoring " + message);
    180                     break;
    181             }
    182             return HANDLED;
    183         }
    184     }
    185 
    186     private class NoActiveNetworkState extends State {
    187         @Override
    188         public void enter() {
    189             mNetworkInfo = null;
    190         }
    191 
    192         @Override
    193         public boolean processMessage(Message message) {
    194             if (DBG) log(getName() + message.toString());
    195             InetAddress server;
    196             NetworkInfo info;
    197             switch (message.what) {
    198                 case CMD_CONNECTIVITY_CHANGE:
    199                     info = (NetworkInfo) message.obj;
    200                     if (info.getType() == ConnectivityManager.TYPE_WIFI) {
    201                         if (info.isConnected() && isActiveNetwork(info)) {
    202                             mNetworkInfo = info;
    203                             transitionTo(mDelayedCaptiveCheckState);
    204                         }
    205                     } else {
    206                         log(getName() + " not a wifi connectivity change, ignore");
    207                     }
    208                     break;
    209                 default:
    210                     return NOT_HANDLED;
    211             }
    212             return HANDLED;
    213         }
    214     }
    215 
    216     private class ActiveNetworkState extends State {
    217         @Override
    218         public void enter() {
    219             setNotificationOff();
    220         }
    221 
    222         @Override
    223         public boolean processMessage(Message message) {
    224             NetworkInfo info;
    225             switch (message.what) {
    226                case CMD_CONNECTIVITY_CHANGE:
    227                     info = (NetworkInfo) message.obj;
    228                     if (!info.isConnected()
    229                             && info.getType() == mNetworkInfo.getType()) {
    230                         if (DBG) log("Disconnected from active network " + info);
    231                         transitionTo(mNoActiveNetworkState);
    232                     } else if (info.getType() != mNetworkInfo.getType() &&
    233                             info.isConnected() &&
    234                             isActiveNetwork(info)) {
    235                         if (DBG) log("Active network switched " + info);
    236                         deferMessage(message);
    237                         transitionTo(mNoActiveNetworkState);
    238                     }
    239                     break;
    240                 default:
    241                     return NOT_HANDLED;
    242             }
    243             return HANDLED;
    244         }
    245     }
    246 
    247 
    248 
    249     private class DelayedCaptiveCheckState extends State {
    250         @Override
    251         public void enter() {
    252             Message message = obtainMessage(CMD_DELAYED_CAPTIVE_CHECK, ++mDelayedCheckToken, 0);
    253             if (mDeviceProvisioned) {
    254                 sendMessageDelayed(message, DELAYED_CHECK_INTERVAL_MS);
    255             } else {
    256                 sendMessage(message);
    257             }
    258         }
    259 
    260         @Override
    261         public boolean processMessage(Message message) {
    262             if (DBG) log(getName() + message.toString());
    263             switch (message.what) {
    264                 case CMD_DELAYED_CAPTIVE_CHECK:
    265                     if (message.arg1 == mDelayedCheckToken) {
    266                         InetAddress server = lookupHost(mServer);
    267                         boolean captive = server != null && isCaptivePortal(server);
    268                         if (captive) {
    269                             if (DBG) log("Captive network " + mNetworkInfo);
    270                         } else {
    271                             if (DBG) log("Not captive network " + mNetworkInfo);
    272                         }
    273                         notifyPortalCheckCompleted(mNetworkInfo, captive);
    274                         if (mDeviceProvisioned) {
    275                             if (captive) {
    276                                 // Setup Wizard will assist the user in connecting to a captive
    277                                 // portal, so make the notification visible unless during setup
    278                                 try {
    279                                     mConnService.setProvisioningNotificationVisible(true,
    280                                         mNetworkInfo.getType(), mNetworkInfo.getExtraInfo(), mUrl);
    281                                 } catch(RemoteException e) {
    282                                     e.printStackTrace();
    283                                 }
    284                             }
    285                         } else {
    286                             Intent intent = new Intent(
    287                                     ConnectivityManager.ACTION_CAPTIVE_PORTAL_TEST_COMPLETED);
    288                             intent.putExtra(ConnectivityManager.EXTRA_IS_CAPTIVE_PORTAL, captive);
    289                             intent.setPackage(SETUP_WIZARD_PACKAGE);
    290                             mContext.sendBroadcast(intent);
    291                         }
    292 
    293                         transitionTo(mActiveNetworkState);
    294                     }
    295                     break;
    296                 default:
    297                     return NOT_HANDLED;
    298             }
    299             return HANDLED;
    300         }
    301     }
    302 
    303     private void notifyPortalCheckComplete(NetworkInfo info) {
    304         if (info == null) {
    305             loge("notifyPortalCheckComplete on null");
    306             return;
    307         }
    308         try {
    309             if (DBG) log("notifyPortalCheckComplete: ni=" + info);
    310             mConnService.captivePortalCheckComplete(info);
    311         } catch(RemoteException e) {
    312             e.printStackTrace();
    313         }
    314     }
    315 
    316     private void notifyPortalCheckCompleted(NetworkInfo info, boolean isCaptivePortal) {
    317         if (info == null) {
    318             loge("notifyPortalCheckComplete on null");
    319             return;
    320         }
    321         try {
    322             if (DBG) log("notifyPortalCheckCompleted: captive=" + isCaptivePortal + " ni=" + info);
    323             mConnService.captivePortalCheckCompleted(info, isCaptivePortal);
    324         } catch(RemoteException e) {
    325             e.printStackTrace();
    326         }
    327     }
    328 
    329     private boolean isActiveNetwork(NetworkInfo info) {
    330         try {
    331             NetworkInfo active = mConnService.getActiveNetworkInfo();
    332             if (active != null && active.getType() == info.getType()) {
    333                 return true;
    334             }
    335         } catch (RemoteException e) {
    336             e.printStackTrace();
    337         }
    338         return false;
    339     }
    340 
    341     private void setNotificationOff() {
    342         try {
    343             mConnService.setProvisioningNotificationVisible(false, ConnectivityManager.TYPE_NONE,
    344                     null, null);
    345         } catch (RemoteException e) {
    346             log("setNotificationOff: " + e);
    347         }
    348     }
    349 
    350     /**
    351      * Do a URL fetch on a known server to see if we get the data we expect
    352      */
    353     private boolean isCaptivePortal(InetAddress server) {
    354         HttpURLConnection urlConnection = null;
    355         if (!mIsCaptivePortalCheckEnabled) return false;
    356 
    357         mUrl = "http://" + server.getHostAddress() + "/generate_204";
    358         if (DBG) log("Checking " + mUrl);
    359         try {
    360             URL url = new URL(mUrl);
    361             urlConnection = (HttpURLConnection) url.openConnection();
    362             urlConnection.setInstanceFollowRedirects(false);
    363             urlConnection.setConnectTimeout(SOCKET_TIMEOUT_MS);
    364             urlConnection.setReadTimeout(SOCKET_TIMEOUT_MS);
    365             urlConnection.setUseCaches(false);
    366             urlConnection.getInputStream();
    367             // we got a valid response, but not from the real google
    368             return urlConnection.getResponseCode() != 204;
    369         } catch (IOException e) {
    370             if (DBG) log("Probably not a portal: exception " + e);
    371             return false;
    372         } finally {
    373             if (urlConnection != null) {
    374                 urlConnection.disconnect();
    375             }
    376         }
    377     }
    378 
    379     private InetAddress lookupHost(String hostname) {
    380         InetAddress inetAddress[];
    381         try {
    382             inetAddress = InetAddress.getAllByName(hostname);
    383         } catch (UnknownHostException e) {
    384             return null;
    385         }
    386 
    387         for (InetAddress a : inetAddress) {
    388             if (a instanceof Inet4Address) return a;
    389         }
    390         return null;
    391     }
    392 }
    393