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 
     38 import com.android.internal.util.State;
     39 import com.android.internal.util.StateMachine;
     40 
     41 import java.io.IOException;
     42 import java.net.HttpURLConnection;
     43 import java.net.InetAddress;
     44 import java.net.Inet4Address;
     45 import java.net.URL;
     46 import java.net.UnknownHostException;
     47 
     48 import com.android.internal.R;
     49 
     50 /**
     51  * This class allows captive portal detection on a network.
     52  * @hide
     53  */
     54 public class CaptivePortalTracker extends StateMachine {
     55     private static final boolean DBG = false;
     56     private static final String TAG = "CaptivePortalTracker";
     57 
     58     private static final String DEFAULT_SERVER = "clients3.google.com";
     59     private static final String NOTIFICATION_ID = "CaptivePortal.Notification";
     60 
     61     private static final int SOCKET_TIMEOUT_MS = 10000;
     62 
     63     private String mServer;
     64     private String mUrl;
     65     private boolean mNotificationShown = false;
     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             if (DBG) log(getName() + "\n");
    163         }
    164 
    165         @Override
    166         public boolean processMessage(Message message) {
    167             if (DBG) log(getName() + message.toString() + "\n");
    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             if (DBG) log(getName() + "\n");
    190             mNetworkInfo = null;
    191             /* Clear any previous notification */
    192             setNotificationVisible(false);
    193         }
    194 
    195         @Override
    196         public boolean processMessage(Message message) {
    197             if (DBG) log(getName() + message.toString() + "\n");
    198             InetAddress server;
    199             NetworkInfo info;
    200             switch (message.what) {
    201                 case CMD_CONNECTIVITY_CHANGE:
    202                     info = (NetworkInfo) message.obj;
    203                     if (info.isConnected() && isActiveNetwork(info)) {
    204                         mNetworkInfo = info;
    205                         transitionTo(mDelayedCaptiveCheckState);
    206                     }
    207                     break;
    208                 default:
    209                     return NOT_HANDLED;
    210             }
    211             return HANDLED;
    212         }
    213     }
    214 
    215     private class ActiveNetworkState extends State {
    216         @Override
    217         public void enter() {
    218             if (DBG) log(getName() + "\n");
    219         }
    220 
    221         @Override
    222         public boolean processMessage(Message message) {
    223             NetworkInfo info;
    224             switch (message.what) {
    225                case CMD_CONNECTIVITY_CHANGE:
    226                     info = (NetworkInfo) message.obj;
    227                     if (!info.isConnected()
    228                             && info.getType() == mNetworkInfo.getType()) {
    229                         if (DBG) log("Disconnected from active network " + info);
    230                         transitionTo(mNoActiveNetworkState);
    231                     } else if (info.getType() != mNetworkInfo.getType() &&
    232                             info.isConnected() &&
    233                             isActiveNetwork(info)) {
    234                         if (DBG) log("Active network switched " + info);
    235                         deferMessage(message);
    236                         transitionTo(mNoActiveNetworkState);
    237                     }
    238                     break;
    239                 default:
    240                     return NOT_HANDLED;
    241             }
    242             return HANDLED;
    243         }
    244     }
    245 
    246 
    247 
    248     private class DelayedCaptiveCheckState extends State {
    249         @Override
    250         public void enter() {
    251             if (DBG) log(getName() + "\n");
    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() + "\n");
    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                         if (mDeviceProvisioned) {
    274                             if (captive) {
    275                                 // Setup Wizard will assist the user in connecting to a captive
    276                                 // portal, so make the notification visible unless during setup
    277                                 setNotificationVisible(true);
    278                             }
    279                         } else {
    280                             Intent intent = new Intent(
    281                                     ConnectivityManager.ACTION_CAPTIVE_PORTAL_TEST_COMPLETED);
    282                             intent.putExtra(ConnectivityManager.EXTRA_IS_CAPTIVE_PORTAL, captive);
    283                             intent.setPackage(SETUP_WIZARD_PACKAGE);
    284                             mContext.sendBroadcast(intent);
    285                         }
    286 
    287                         transitionTo(mActiveNetworkState);
    288                     }
    289                     break;
    290                 default:
    291                     return NOT_HANDLED;
    292             }
    293             return HANDLED;
    294         }
    295     }
    296 
    297     private void notifyPortalCheckComplete(NetworkInfo info) {
    298         if (info == null) {
    299             loge("notifyPortalCheckComplete on null");
    300             return;
    301         }
    302         try {
    303             mConnService.captivePortalCheckComplete(info);
    304         } catch(RemoteException e) {
    305             e.printStackTrace();
    306         }
    307     }
    308 
    309     private boolean isActiveNetwork(NetworkInfo info) {
    310         try {
    311             NetworkInfo active = mConnService.getActiveNetworkInfo();
    312             if (active != null && active.getType() == info.getType()) {
    313                 return true;
    314             }
    315         } catch (RemoteException e) {
    316             e.printStackTrace();
    317         }
    318         return false;
    319     }
    320 
    321     /**
    322      * Do a URL fetch on a known server to see if we get the data we expect
    323      */
    324     private boolean isCaptivePortal(InetAddress server) {
    325         HttpURLConnection urlConnection = null;
    326         if (!mIsCaptivePortalCheckEnabled) return false;
    327 
    328         mUrl = "http://" + server.getHostAddress() + "/generate_204";
    329         if (DBG) log("Checking " + mUrl);
    330         try {
    331             URL url = new URL(mUrl);
    332             urlConnection = (HttpURLConnection) url.openConnection();
    333             urlConnection.setInstanceFollowRedirects(false);
    334             urlConnection.setConnectTimeout(SOCKET_TIMEOUT_MS);
    335             urlConnection.setReadTimeout(SOCKET_TIMEOUT_MS);
    336             urlConnection.setUseCaches(false);
    337             urlConnection.getInputStream();
    338             // we got a valid response, but not from the real google
    339             return urlConnection.getResponseCode() != 204;
    340         } catch (IOException e) {
    341             if (DBG) log("Probably not a portal: exception " + e);
    342             return false;
    343         } finally {
    344             if (urlConnection != null) {
    345                 urlConnection.disconnect();
    346             }
    347         }
    348     }
    349 
    350     private InetAddress lookupHost(String hostname) {
    351         InetAddress inetAddress[];
    352         try {
    353             inetAddress = InetAddress.getAllByName(hostname);
    354         } catch (UnknownHostException e) {
    355             return null;
    356         }
    357 
    358         for (InetAddress a : inetAddress) {
    359             if (a instanceof Inet4Address) return a;
    360         }
    361         return null;
    362     }
    363 
    364     private void setNotificationVisible(boolean visible) {
    365         // if it should be hidden and it is already hidden, then noop
    366         if (!visible && !mNotificationShown) {
    367             return;
    368         }
    369 
    370         Resources r = Resources.getSystem();
    371         NotificationManager notificationManager = (NotificationManager) mContext
    372             .getSystemService(Context.NOTIFICATION_SERVICE);
    373 
    374         if (visible) {
    375             CharSequence title;
    376             CharSequence details;
    377             int icon;
    378             switch (mNetworkInfo.getType()) {
    379                 case ConnectivityManager.TYPE_WIFI:
    380                     title = r.getString(R.string.wifi_available_sign_in, 0);
    381                     details = r.getString(R.string.network_available_sign_in_detailed,
    382                             mNetworkInfo.getExtraInfo());
    383                     icon = R.drawable.stat_notify_wifi_in_range;
    384                     break;
    385                 case ConnectivityManager.TYPE_MOBILE:
    386                     title = r.getString(R.string.network_available_sign_in, 0);
    387                     // TODO: Change this to pull from NetworkInfo once a printable
    388                     // name has been added to it
    389                     details = mTelephonyManager.getNetworkOperatorName();
    390                     icon = R.drawable.stat_notify_rssi_in_range;
    391                     break;
    392                 default:
    393                     title = r.getString(R.string.network_available_sign_in, 0);
    394                     details = r.getString(R.string.network_available_sign_in_detailed,
    395                             mNetworkInfo.getExtraInfo());
    396                     icon = R.drawable.stat_notify_rssi_in_range;
    397                     break;
    398             }
    399 
    400             Notification notification = new Notification();
    401             notification.when = 0;
    402             notification.icon = icon;
    403             notification.flags = Notification.FLAG_AUTO_CANCEL;
    404             Intent intent = new Intent(Intent.ACTION_VIEW, Uri.parse(mUrl));
    405             intent.setFlags(Intent.FLAG_ACTIVITY_BROUGHT_TO_FRONT |
    406                     Intent.FLAG_ACTIVITY_NEW_TASK);
    407             notification.contentIntent = PendingIntent.getActivity(mContext, 0, intent, 0);
    408             notification.tickerText = title;
    409             notification.setLatestEventInfo(mContext, title, details, notification.contentIntent);
    410 
    411             notificationManager.notify(NOTIFICATION_ID, 1, notification);
    412         } else {
    413             notificationManager.cancel(NOTIFICATION_ID, 1);
    414         }
    415         mNotificationShown = visible;
    416     }
    417 }
    418