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.content.BroadcastReceiver;
     20 import android.content.Context;
     21 import android.content.Intent;
     22 import android.content.IntentFilter;
     23 import android.database.ContentObserver;
     24 import android.net.ConnectivityManager;
     25 import android.net.IConnectivityManager;
     26 import android.net.wifi.WifiInfo;
     27 import android.net.wifi.WifiManager;
     28 import android.os.Handler;
     29 import android.os.Message;
     30 import android.os.RemoteException;
     31 import android.os.SystemClock;
     32 import android.provider.Settings;
     33 import android.telephony.CellIdentityCdma;
     34 import android.telephony.CellIdentityGsm;
     35 import android.telephony.CellIdentityLte;
     36 import android.telephony.CellIdentityWcdma;
     37 import android.telephony.CellInfo;
     38 import android.telephony.CellInfoCdma;
     39 import android.telephony.CellInfoGsm;
     40 import android.telephony.CellInfoLte;
     41 import android.telephony.CellInfoWcdma;
     42 import android.telephony.TelephonyManager;
     43 
     44 import com.android.internal.util.State;
     45 import com.android.internal.util.StateMachine;
     46 
     47 import java.io.IOException;
     48 import java.net.HttpURLConnection;
     49 import java.net.InetAddress;
     50 import java.net.Inet4Address;
     51 import java.net.SocketTimeoutException;
     52 import java.net.URL;
     53 import java.net.UnknownHostException;
     54 import java.util.List;
     55 
     56 /**
     57  * This class allows captive portal detection on a network.
     58  * @hide
     59  */
     60 public class CaptivePortalTracker extends StateMachine {
     61     private static final boolean DBG = true;
     62     private static final String TAG = "CaptivePortalTracker";
     63 
     64     private static final String DEFAULT_SERVER = "clients3.google.com";
     65 
     66     private static final int SOCKET_TIMEOUT_MS = 10000;
     67 
     68     public static final String ACTION_NETWORK_CONDITIONS_MEASURED =
     69             "android.net.conn.NETWORK_CONDITIONS_MEASURED";
     70     public static final String EXTRA_CONNECTIVITY_TYPE = "extra_connectivity_type";
     71     public static final String EXTRA_NETWORK_TYPE = "extra_network_type";
     72     public static final String EXTRA_RESPONSE_RECEIVED = "extra_response_received";
     73     public static final String EXTRA_IS_CAPTIVE_PORTAL = "extra_is_captive_portal";
     74     public static final String EXTRA_CELL_ID = "extra_cellid";
     75     public static final String EXTRA_SSID = "extra_ssid";
     76     public static final String EXTRA_BSSID = "extra_bssid";
     77     /** real time since boot */
     78     public static final String EXTRA_REQUEST_TIMESTAMP_MS = "extra_request_timestamp_ms";
     79     public static final String EXTRA_RESPONSE_TIMESTAMP_MS = "extra_response_timestamp_ms";
     80 
     81     private static final String PERMISSION_ACCESS_NETWORK_CONDITIONS =
     82             "android.permission.ACCESS_NETWORK_CONDITIONS";
     83 
     84     private String mServer;
     85     private String mUrl;
     86     private boolean mIsCaptivePortalCheckEnabled = false;
     87     private IConnectivityManager mConnService;
     88     private TelephonyManager mTelephonyManager;
     89     private WifiManager mWifiManager;
     90     private Context mContext;
     91     private NetworkInfo mNetworkInfo;
     92 
     93     private static final int CMD_DETECT_PORTAL          = 0;
     94     private static final int CMD_CONNECTIVITY_CHANGE    = 1;
     95     private static final int CMD_DELAYED_CAPTIVE_CHECK  = 2;
     96 
     97     /* This delay happens every time before we do a captive check on a network */
     98     private static final int DELAYED_CHECK_INTERVAL_MS = 10000;
     99     private int mDelayedCheckToken = 0;
    100 
    101     private State mDefaultState = new DefaultState();
    102     private State mNoActiveNetworkState = new NoActiveNetworkState();
    103     private State mActiveNetworkState = new ActiveNetworkState();
    104     private State mDelayedCaptiveCheckState = new DelayedCaptiveCheckState();
    105 
    106     private static final String SETUP_WIZARD_PACKAGE = "com.google.android.setupwizard";
    107     private boolean mDeviceProvisioned = false;
    108     private ProvisioningObserver mProvisioningObserver;
    109 
    110     private CaptivePortalTracker(Context context, IConnectivityManager cs) {
    111         super(TAG);
    112 
    113         mContext = context;
    114         mConnService = cs;
    115         mTelephonyManager = (TelephonyManager) context.getSystemService(Context.TELEPHONY_SERVICE);
    116         mWifiManager = (WifiManager) context.getSystemService(Context.WIFI_SERVICE);
    117         mProvisioningObserver = new ProvisioningObserver();
    118 
    119         IntentFilter filter = new IntentFilter();
    120         filter.addAction(ConnectivityManager.CONNECTIVITY_ACTION);
    121         filter.addAction(ConnectivityManager.CONNECTIVITY_ACTION_IMMEDIATE);
    122         mContext.registerReceiver(mReceiver, filter);
    123 
    124         mServer = Settings.Global.getString(mContext.getContentResolver(),
    125                 Settings.Global.CAPTIVE_PORTAL_SERVER);
    126         if (mServer == null) mServer = DEFAULT_SERVER;
    127 
    128         mIsCaptivePortalCheckEnabled = Settings.Global.getInt(mContext.getContentResolver(),
    129                 Settings.Global.CAPTIVE_PORTAL_DETECTION_ENABLED, 1) == 1;
    130 
    131         addState(mDefaultState);
    132             addState(mNoActiveNetworkState, mDefaultState);
    133             addState(mActiveNetworkState, mDefaultState);
    134                 addState(mDelayedCaptiveCheckState, mActiveNetworkState);
    135         setInitialState(mNoActiveNetworkState);
    136     }
    137 
    138     private class ProvisioningObserver extends ContentObserver {
    139         ProvisioningObserver() {
    140             super(new Handler());
    141             mContext.getContentResolver().registerContentObserver(Settings.Global.getUriFor(
    142                     Settings.Global.DEVICE_PROVISIONED), false, this);
    143             onChange(false); // load initial value
    144         }
    145 
    146         @Override
    147         public void onChange(boolean selfChange) {
    148             mDeviceProvisioned = Settings.Global.getInt(mContext.getContentResolver(),
    149                     Settings.Global.DEVICE_PROVISIONED, 0) != 0;
    150         }
    151     }
    152 
    153     private final BroadcastReceiver mReceiver = new BroadcastReceiver() {
    154         @Override
    155         public void onReceive(Context context, Intent intent) {
    156             String action = intent.getAction();
    157             // Normally, we respond to CONNECTIVITY_ACTION, allowing time for the change in
    158             // connectivity to stabilize, but if the device is not yet provisioned, respond
    159             // immediately to speed up transit through the setup wizard.
    160             if ((mDeviceProvisioned && action.equals(ConnectivityManager.CONNECTIVITY_ACTION))
    161                     || (!mDeviceProvisioned
    162                             && action.equals(ConnectivityManager.CONNECTIVITY_ACTION_IMMEDIATE))) {
    163                 NetworkInfo info = intent.getParcelableExtra(
    164                         ConnectivityManager.EXTRA_NETWORK_INFO);
    165                 sendMessage(obtainMessage(CMD_CONNECTIVITY_CHANGE, info));
    166             }
    167         }
    168     };
    169 
    170     public static CaptivePortalTracker makeCaptivePortalTracker(Context context,
    171             IConnectivityManager cs) {
    172         CaptivePortalTracker captivePortal = new CaptivePortalTracker(context, cs);
    173         captivePortal.start();
    174         return captivePortal;
    175     }
    176 
    177     public void detectCaptivePortal(NetworkInfo info) {
    178         sendMessage(obtainMessage(CMD_DETECT_PORTAL, info));
    179     }
    180 
    181     private class DefaultState extends State {
    182 
    183         @Override
    184         public boolean processMessage(Message message) {
    185             if (DBG) log(getName() + message.toString());
    186             switch (message.what) {
    187                 case CMD_DETECT_PORTAL:
    188                     NetworkInfo info = (NetworkInfo) message.obj;
    189                     // Checking on a secondary connection is not supported
    190                     // yet
    191                     notifyPortalCheckComplete(info);
    192                     break;
    193                 case CMD_CONNECTIVITY_CHANGE:
    194                 case CMD_DELAYED_CAPTIVE_CHECK:
    195                     break;
    196                 default:
    197                     loge("Ignoring " + message);
    198                     break;
    199             }
    200             return HANDLED;
    201         }
    202     }
    203 
    204     private class NoActiveNetworkState extends State {
    205         @Override
    206         public void enter() {
    207             setNotificationOff();
    208             mNetworkInfo = null;
    209         }
    210 
    211         @Override
    212         public boolean processMessage(Message message) {
    213             if (DBG) log(getName() + message.toString());
    214             InetAddress server;
    215             NetworkInfo info;
    216             switch (message.what) {
    217                 case CMD_CONNECTIVITY_CHANGE:
    218                     info = (NetworkInfo) message.obj;
    219                     if (info.getType() == ConnectivityManager.TYPE_WIFI) {
    220                         if (info.isConnected() && isActiveNetwork(info)) {
    221                             mNetworkInfo = info;
    222                             transitionTo(mDelayedCaptiveCheckState);
    223                         }
    224                     } else {
    225                         log(getName() + " not a wifi connectivity change, ignore");
    226                     }
    227                     break;
    228                 default:
    229                     return NOT_HANDLED;
    230             }
    231             return HANDLED;
    232         }
    233     }
    234 
    235     private class ActiveNetworkState extends State {
    236         @Override
    237         public boolean processMessage(Message message) {
    238             NetworkInfo info;
    239             switch (message.what) {
    240                case CMD_CONNECTIVITY_CHANGE:
    241                     info = (NetworkInfo) message.obj;
    242                     if (!info.isConnected()
    243                             && info.getType() == mNetworkInfo.getType()) {
    244                         if (DBG) log("Disconnected from active network " + info);
    245                         transitionTo(mNoActiveNetworkState);
    246                     } else if (info.getType() != mNetworkInfo.getType() &&
    247                             info.isConnected() &&
    248                             isActiveNetwork(info)) {
    249                         if (DBG) log("Active network switched " + info);
    250                         deferMessage(message);
    251                         transitionTo(mNoActiveNetworkState);
    252                     }
    253                     break;
    254                 default:
    255                     return NOT_HANDLED;
    256             }
    257             return HANDLED;
    258         }
    259     }
    260 
    261 
    262 
    263     private class DelayedCaptiveCheckState extends State {
    264         @Override
    265         public void enter() {
    266             Message message = obtainMessage(CMD_DELAYED_CAPTIVE_CHECK, ++mDelayedCheckToken, 0);
    267             if (mDeviceProvisioned) {
    268                 sendMessageDelayed(message, DELAYED_CHECK_INTERVAL_MS);
    269             } else {
    270                 sendMessage(message);
    271             }
    272         }
    273 
    274         @Override
    275         public boolean processMessage(Message message) {
    276             if (DBG) log(getName() + message.toString());
    277             switch (message.what) {
    278                 case CMD_DELAYED_CAPTIVE_CHECK:
    279                     setNotificationOff();
    280 
    281                     if (message.arg1 == mDelayedCheckToken) {
    282                         InetAddress server = lookupHost(mServer);
    283                         boolean captive = server != null && isCaptivePortal(server);
    284                         if (captive) {
    285                             if (DBG) log("Captive network " + mNetworkInfo);
    286                         } else {
    287                             if (DBG) log("Not captive network " + mNetworkInfo);
    288                         }
    289                         notifyPortalCheckCompleted(mNetworkInfo, captive);
    290                         if (mDeviceProvisioned) {
    291                             if (captive) {
    292                                 // Setup Wizard will assist the user in connecting to a captive
    293                                 // portal, so make the notification visible unless during setup
    294                                 try {
    295                                     mConnService.setProvisioningNotificationVisible(true,
    296                                         mNetworkInfo.getType(), mNetworkInfo.getExtraInfo(), mUrl);
    297                                 } catch(RemoteException e) {
    298                                     e.printStackTrace();
    299                                 }
    300                             }
    301                         } else {
    302                             Intent intent = new Intent(
    303                                     ConnectivityManager.ACTION_CAPTIVE_PORTAL_TEST_COMPLETED);
    304                             intent.putExtra(ConnectivityManager.EXTRA_IS_CAPTIVE_PORTAL, captive);
    305                             intent.setPackage(SETUP_WIZARD_PACKAGE);
    306                             mContext.sendBroadcast(intent);
    307                         }
    308 
    309                         transitionTo(mActiveNetworkState);
    310                     }
    311                     break;
    312                 default:
    313                     return NOT_HANDLED;
    314             }
    315             return HANDLED;
    316         }
    317     }
    318 
    319     private void notifyPortalCheckComplete(NetworkInfo info) {
    320         if (info == null) {
    321             loge("notifyPortalCheckComplete on null");
    322             return;
    323         }
    324         try {
    325             if (DBG) log("notifyPortalCheckComplete: ni=" + info);
    326             mConnService.captivePortalCheckComplete(info);
    327         } catch(RemoteException e) {
    328             e.printStackTrace();
    329         }
    330     }
    331 
    332     private void notifyPortalCheckCompleted(NetworkInfo info, boolean isCaptivePortal) {
    333         if (info == null) {
    334             loge("notifyPortalCheckComplete on null");
    335             return;
    336         }
    337         try {
    338             if (DBG) log("notifyPortalCheckCompleted: captive=" + isCaptivePortal + " ni=" + info);
    339             mConnService.captivePortalCheckCompleted(info, isCaptivePortal);
    340         } catch(RemoteException e) {
    341             e.printStackTrace();
    342         }
    343     }
    344 
    345     private boolean isActiveNetwork(NetworkInfo info) {
    346         try {
    347             NetworkInfo active = mConnService.getActiveNetworkInfo();
    348             if (active != null && active.getType() == info.getType()) {
    349                 return true;
    350             }
    351         } catch (RemoteException e) {
    352             e.printStackTrace();
    353         }
    354         return false;
    355     }
    356 
    357     private void setNotificationOff() {
    358         try {
    359             if (mNetworkInfo != null) {
    360                 mConnService.setProvisioningNotificationVisible(false, mNetworkInfo.getType(),
    361                     null, null);
    362             }
    363         } catch (RemoteException e) {
    364             log("setNotificationOff: " + e);
    365         }
    366     }
    367 
    368     /**
    369      * Do a URL fetch on a known server to see if we get the data we expect.
    370      * Measure the response time and broadcast that.
    371      */
    372     private boolean isCaptivePortal(InetAddress server) {
    373         HttpURLConnection urlConnection = null;
    374         if (!mIsCaptivePortalCheckEnabled) return false;
    375 
    376         mUrl = "http://" + server.getHostAddress() + "/generate_204";
    377         if (DBG) log("Checking " + mUrl);
    378         long requestTimestamp = -1;
    379         try {
    380             URL url = new URL(mUrl);
    381             urlConnection = (HttpURLConnection) url.openConnection();
    382             urlConnection.setInstanceFollowRedirects(false);
    383             urlConnection.setConnectTimeout(SOCKET_TIMEOUT_MS);
    384             urlConnection.setReadTimeout(SOCKET_TIMEOUT_MS);
    385             urlConnection.setUseCaches(false);
    386 
    387             // Time how long it takes to get a response to our request
    388             requestTimestamp = SystemClock.elapsedRealtime();
    389 
    390             urlConnection.getInputStream();
    391 
    392             // Time how long it takes to get a response to our request
    393             long responseTimestamp = SystemClock.elapsedRealtime();
    394 
    395             // we got a valid response, but not from the real google
    396             int rspCode = urlConnection.getResponseCode();
    397             boolean isCaptivePortal = rspCode != 204;
    398 
    399             sendNetworkConditionsBroadcast(true /* response received */, isCaptivePortal,
    400                     requestTimestamp, responseTimestamp);
    401 
    402             if (DBG) log("isCaptivePortal: ret=" + isCaptivePortal + " rspCode=" + rspCode);
    403             return isCaptivePortal;
    404         } catch (IOException e) {
    405             if (DBG) log("Probably not a portal: exception " + e);
    406             if (requestTimestamp != -1) {
    407                 sendFailedCaptivePortalCheckBroadcast(requestTimestamp);
    408             } // else something went wrong with setting up the urlConnection
    409             return false;
    410         } finally {
    411             if (urlConnection != null) {
    412                 urlConnection.disconnect();
    413             }
    414         }
    415     }
    416 
    417     private InetAddress lookupHost(String hostname) {
    418         InetAddress inetAddress[];
    419         try {
    420             inetAddress = InetAddress.getAllByName(hostname);
    421         } catch (UnknownHostException e) {
    422             sendFailedCaptivePortalCheckBroadcast(SystemClock.elapsedRealtime());
    423             return null;
    424         }
    425 
    426         for (InetAddress a : inetAddress) {
    427             if (a instanceof Inet4Address) return a;
    428         }
    429 
    430         sendFailedCaptivePortalCheckBroadcast(SystemClock.elapsedRealtime());
    431         return null;
    432     }
    433 
    434     private void sendFailedCaptivePortalCheckBroadcast(long requestTimestampMs) {
    435         sendNetworkConditionsBroadcast(false /* response received */, false /* ignored */,
    436                 requestTimestampMs, 0 /* ignored */);
    437     }
    438 
    439     /**
    440      * @param responseReceived - whether or not we received a valid HTTP response to our request.
    441      * If false, isCaptivePortal and responseTimestampMs are ignored
    442      */
    443     private void sendNetworkConditionsBroadcast(boolean responseReceived, boolean isCaptivePortal,
    444             long requestTimestampMs, long responseTimestampMs) {
    445         if (Settings.Global.getInt(mContext.getContentResolver(),
    446                 Settings.Global.WIFI_SCAN_ALWAYS_AVAILABLE, 0) == 0) {
    447             if (DBG) log("Don't send network conditions - lacking user consent.");
    448             return;
    449         }
    450 
    451         Intent latencyBroadcast = new Intent(ACTION_NETWORK_CONDITIONS_MEASURED);
    452         switch (mNetworkInfo.getType()) {
    453             case ConnectivityManager.TYPE_WIFI:
    454                 WifiInfo currentWifiInfo = mWifiManager.getConnectionInfo();
    455                 if (currentWifiInfo != null) {
    456                     latencyBroadcast.putExtra(EXTRA_SSID, currentWifiInfo.getSSID());
    457                     latencyBroadcast.putExtra(EXTRA_BSSID, currentWifiInfo.getBSSID());
    458                 } else {
    459                     if (DBG) logw("network info is TYPE_WIFI but no ConnectionInfo found");
    460                     return;
    461                 }
    462                 break;
    463             case ConnectivityManager.TYPE_MOBILE:
    464                 latencyBroadcast.putExtra(EXTRA_NETWORK_TYPE, mTelephonyManager.getNetworkType());
    465                 List<CellInfo> info = mTelephonyManager.getAllCellInfo();
    466                 if (info == null) return;
    467                 StringBuffer uniqueCellId = new StringBuffer();
    468                 int numRegisteredCellInfo = 0;
    469                 for (CellInfo cellInfo : info) {
    470                     if (cellInfo.isRegistered()) {
    471                         numRegisteredCellInfo++;
    472                         if (numRegisteredCellInfo > 1) {
    473                             if (DBG) log("more than one registered CellInfo.  Can't " +
    474                                     "tell which is active.  Bailing.");
    475                             return;
    476                         }
    477                         if (cellInfo instanceof CellInfoCdma) {
    478                             CellIdentityCdma cellId = ((CellInfoCdma) cellInfo).getCellIdentity();
    479                             latencyBroadcast.putExtra(EXTRA_CELL_ID, cellId);
    480                         } else if (cellInfo instanceof CellInfoGsm) {
    481                             CellIdentityGsm cellId = ((CellInfoGsm) cellInfo).getCellIdentity();
    482                             latencyBroadcast.putExtra(EXTRA_CELL_ID, cellId);
    483                         } else if (cellInfo instanceof CellInfoLte) {
    484                             CellIdentityLte cellId = ((CellInfoLte) cellInfo).getCellIdentity();
    485                             latencyBroadcast.putExtra(EXTRA_CELL_ID, cellId);
    486                         } else if (cellInfo instanceof CellInfoWcdma) {
    487                             CellIdentityWcdma cellId = ((CellInfoWcdma) cellInfo).getCellIdentity();
    488                             latencyBroadcast.putExtra(EXTRA_CELL_ID, cellId);
    489                         } else {
    490                             if (DBG) logw("Registered cellinfo is unrecognized");
    491                             return;
    492                         }
    493                     }
    494                 }
    495                 break;
    496             default:
    497                 return;
    498         }
    499         latencyBroadcast.putExtra(EXTRA_CONNECTIVITY_TYPE, mNetworkInfo.getType());
    500         latencyBroadcast.putExtra(EXTRA_RESPONSE_RECEIVED, responseReceived);
    501         latencyBroadcast.putExtra(EXTRA_REQUEST_TIMESTAMP_MS, requestTimestampMs);
    502 
    503         if (responseReceived) {
    504             latencyBroadcast.putExtra(EXTRA_IS_CAPTIVE_PORTAL, isCaptivePortal);
    505             latencyBroadcast.putExtra(EXTRA_RESPONSE_TIMESTAMP_MS, responseTimestampMs);
    506         }
    507         mContext.sendBroadcast(latencyBroadcast, PERMISSION_ACCESS_NETWORK_CONDITIONS);
    508     }
    509 }
    510