CaptivePortalTracker.java revision bf34122a96ef3d02e9b7935e07eb4f9b04034828
1da6da0907b28d4704aabbdb1bbeb4300954670d1Irfan Sheriff/* 2da6da0907b28d4704aabbdb1bbeb4300954670d1Irfan Sheriff * Copyright (C) 2012 The Android Open Source Project 3da6da0907b28d4704aabbdb1bbeb4300954670d1Irfan Sheriff * 4da6da0907b28d4704aabbdb1bbeb4300954670d1Irfan Sheriff * Licensed under the Apache License, Version 2.0 (the "License"); 5da6da0907b28d4704aabbdb1bbeb4300954670d1Irfan Sheriff * you may not use this file except in compliance with the License. 6da6da0907b28d4704aabbdb1bbeb4300954670d1Irfan Sheriff * You may obtain a copy of the License at 7da6da0907b28d4704aabbdb1bbeb4300954670d1Irfan Sheriff * 8da6da0907b28d4704aabbdb1bbeb4300954670d1Irfan Sheriff * http://www.apache.org/licenses/LICENSE-2.0 9da6da0907b28d4704aabbdb1bbeb4300954670d1Irfan Sheriff * 10da6da0907b28d4704aabbdb1bbeb4300954670d1Irfan Sheriff * Unless required by applicable law or agreed to in writing, software 11da6da0907b28d4704aabbdb1bbeb4300954670d1Irfan Sheriff * distributed under the License is distributed on an "AS IS" BASIS, 12da6da0907b28d4704aabbdb1bbeb4300954670d1Irfan Sheriff * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13da6da0907b28d4704aabbdb1bbeb4300954670d1Irfan Sheriff * See the License for the specific language governing permissions and 14da6da0907b28d4704aabbdb1bbeb4300954670d1Irfan Sheriff * limitations under the License. 15da6da0907b28d4704aabbdb1bbeb4300954670d1Irfan Sheriff */ 16da6da0907b28d4704aabbdb1bbeb4300954670d1Irfan Sheriff 17da6da0907b28d4704aabbdb1bbeb4300954670d1Irfan Sheriffpackage android.net; 18da6da0907b28d4704aabbdb1bbeb4300954670d1Irfan Sheriff 199538bdd3c77968c7673719c580ae653ede4654d6Irfan Sheriffimport android.app.Activity; 20da6da0907b28d4704aabbdb1bbeb4300954670d1Irfan Sheriffimport android.app.Notification; 21da6da0907b28d4704aabbdb1bbeb4300954670d1Irfan Sheriffimport android.app.NotificationManager; 22da6da0907b28d4704aabbdb1bbeb4300954670d1Irfan Sheriffimport android.app.PendingIntent; 23da6da0907b28d4704aabbdb1bbeb4300954670d1Irfan Sheriffimport android.content.BroadcastReceiver; 24da6da0907b28d4704aabbdb1bbeb4300954670d1Irfan Sheriffimport android.content.Context; 25da6da0907b28d4704aabbdb1bbeb4300954670d1Irfan Sheriffimport android.content.Intent; 26da6da0907b28d4704aabbdb1bbeb4300954670d1Irfan Sheriffimport android.content.IntentFilter; 27da6da0907b28d4704aabbdb1bbeb4300954670d1Irfan Sheriffimport android.content.res.Resources; 28108da0cfa4a2f59cc953a4ec61314e69b61d6777Russell Brennerimport android.database.ContentObserver; 29da6da0907b28d4704aabbdb1bbeb4300954670d1Irfan Sheriffimport android.net.ConnectivityManager; 30da6da0907b28d4704aabbdb1bbeb4300954670d1Irfan Sheriffimport android.net.IConnectivityManager; 31108da0cfa4a2f59cc953a4ec61314e69b61d6777Russell Brennerimport android.os.Handler; 329538bdd3c77968c7673719c580ae653ede4654d6Irfan Sheriffimport android.os.UserHandle; 33da6da0907b28d4704aabbdb1bbeb4300954670d1Irfan Sheriffimport android.os.Message; 34da6da0907b28d4704aabbdb1bbeb4300954670d1Irfan Sheriffimport android.os.RemoteException; 35da6da0907b28d4704aabbdb1bbeb4300954670d1Irfan Sheriffimport android.provider.Settings; 36b8aad91f059527e04abaf8a83ed1ce6b5f09c55dIrfan Sheriffimport android.telephony.TelephonyManager; 37da6da0907b28d4704aabbdb1bbeb4300954670d1Irfan Sheriff 389538bdd3c77968c7673719c580ae653ede4654d6Irfan Sheriffimport com.android.internal.util.State; 399538bdd3c77968c7673719c580ae653ede4654d6Irfan Sheriffimport com.android.internal.util.StateMachine; 409538bdd3c77968c7673719c580ae653ede4654d6Irfan Sheriff 41da6da0907b28d4704aabbdb1bbeb4300954670d1Irfan Sheriffimport java.io.IOException; 42da6da0907b28d4704aabbdb1bbeb4300954670d1Irfan Sheriffimport java.net.HttpURLConnection; 43da6da0907b28d4704aabbdb1bbeb4300954670d1Irfan Sheriffimport java.net.InetAddress; 44da6da0907b28d4704aabbdb1bbeb4300954670d1Irfan Sheriffimport java.net.Inet4Address; 45bf34122a96ef3d02e9b7935e07eb4f9b04034828Wink Savilleimport java.net.SocketTimeoutException; 46da6da0907b28d4704aabbdb1bbeb4300954670d1Irfan Sheriffimport java.net.URL; 47da6da0907b28d4704aabbdb1bbeb4300954670d1Irfan Sheriffimport java.net.UnknownHostException; 48da6da0907b28d4704aabbdb1bbeb4300954670d1Irfan Sheriff 49da6da0907b28d4704aabbdb1bbeb4300954670d1Irfan Sheriffimport com.android.internal.R; 50da6da0907b28d4704aabbdb1bbeb4300954670d1Irfan Sheriff 51da6da0907b28d4704aabbdb1bbeb4300954670d1Irfan Sheriff/** 529538bdd3c77968c7673719c580ae653ede4654d6Irfan Sheriff * This class allows captive portal detection on a network. 53da6da0907b28d4704aabbdb1bbeb4300954670d1Irfan Sheriff * @hide 54da6da0907b28d4704aabbdb1bbeb4300954670d1Irfan Sheriff */ 559538bdd3c77968c7673719c580ae653ede4654d6Irfan Sheriffpublic class CaptivePortalTracker extends StateMachine { 56bf34122a96ef3d02e9b7935e07eb4f9b04034828Wink Saville private static final boolean DBG = true; 57da6da0907b28d4704aabbdb1bbeb4300954670d1Irfan Sheriff private static final String TAG = "CaptivePortalTracker"; 58da6da0907b28d4704aabbdb1bbeb4300954670d1Irfan Sheriff 59da6da0907b28d4704aabbdb1bbeb4300954670d1Irfan Sheriff private static final String DEFAULT_SERVER = "clients3.google.com"; 60da6da0907b28d4704aabbdb1bbeb4300954670d1Irfan Sheriff private static final String NOTIFICATION_ID = "CaptivePortal.Notification"; 61da6da0907b28d4704aabbdb1bbeb4300954670d1Irfan Sheriff 62da6da0907b28d4704aabbdb1bbeb4300954670d1Irfan Sheriff private static final int SOCKET_TIMEOUT_MS = 10000; 63da6da0907b28d4704aabbdb1bbeb4300954670d1Irfan Sheriff 64da6da0907b28d4704aabbdb1bbeb4300954670d1Irfan Sheriff private String mServer; 65da6da0907b28d4704aabbdb1bbeb4300954670d1Irfan Sheriff private String mUrl; 66da6da0907b28d4704aabbdb1bbeb4300954670d1Irfan Sheriff private boolean mNotificationShown = false; 67da6da0907b28d4704aabbdb1bbeb4300954670d1Irfan Sheriff private boolean mIsCaptivePortalCheckEnabled = false; 68da6da0907b28d4704aabbdb1bbeb4300954670d1Irfan Sheriff private IConnectivityManager mConnService; 69b8aad91f059527e04abaf8a83ed1ce6b5f09c55dIrfan Sheriff private TelephonyManager mTelephonyManager; 70da6da0907b28d4704aabbdb1bbeb4300954670d1Irfan Sheriff private Context mContext; 71da6da0907b28d4704aabbdb1bbeb4300954670d1Irfan Sheriff private NetworkInfo mNetworkInfo; 72da6da0907b28d4704aabbdb1bbeb4300954670d1Irfan Sheriff 739538bdd3c77968c7673719c580ae653ede4654d6Irfan Sheriff private static final int CMD_DETECT_PORTAL = 0; 749538bdd3c77968c7673719c580ae653ede4654d6Irfan Sheriff private static final int CMD_CONNECTIVITY_CHANGE = 1; 759538bdd3c77968c7673719c580ae653ede4654d6Irfan Sheriff private static final int CMD_DELAYED_CAPTIVE_CHECK = 2; 76da6da0907b28d4704aabbdb1bbeb4300954670d1Irfan Sheriff 779538bdd3c77968c7673719c580ae653ede4654d6Irfan Sheriff /* This delay happens every time before we do a captive check on a network */ 789538bdd3c77968c7673719c580ae653ede4654d6Irfan Sheriff private static final int DELAYED_CHECK_INTERVAL_MS = 10000; 799538bdd3c77968c7673719c580ae653ede4654d6Irfan Sheriff private int mDelayedCheckToken = 0; 809538bdd3c77968c7673719c580ae653ede4654d6Irfan Sheriff 819538bdd3c77968c7673719c580ae653ede4654d6Irfan Sheriff private State mDefaultState = new DefaultState(); 829538bdd3c77968c7673719c580ae653ede4654d6Irfan Sheriff private State mNoActiveNetworkState = new NoActiveNetworkState(); 839538bdd3c77968c7673719c580ae653ede4654d6Irfan Sheriff private State mActiveNetworkState = new ActiveNetworkState(); 849538bdd3c77968c7673719c580ae653ede4654d6Irfan Sheriff private State mDelayedCaptiveCheckState = new DelayedCaptiveCheckState(); 859538bdd3c77968c7673719c580ae653ede4654d6Irfan Sheriff 86108da0cfa4a2f59cc953a4ec61314e69b61d6777Russell Brenner private static final String SETUP_WIZARD_PACKAGE = "com.google.android.setupwizard"; 87108da0cfa4a2f59cc953a4ec61314e69b61d6777Russell Brenner private boolean mDeviceProvisioned = false; 88108da0cfa4a2f59cc953a4ec61314e69b61d6777Russell Brenner private ProvisioningObserver mProvisioningObserver; 89108da0cfa4a2f59cc953a4ec61314e69b61d6777Russell Brenner 909538bdd3c77968c7673719c580ae653ede4654d6Irfan Sheriff private CaptivePortalTracker(Context context, IConnectivityManager cs) { 919538bdd3c77968c7673719c580ae653ede4654d6Irfan Sheriff super(TAG); 92da6da0907b28d4704aabbdb1bbeb4300954670d1Irfan Sheriff 93da6da0907b28d4704aabbdb1bbeb4300954670d1Irfan Sheriff mContext = context; 94da6da0907b28d4704aabbdb1bbeb4300954670d1Irfan Sheriff mConnService = cs; 95b8aad91f059527e04abaf8a83ed1ce6b5f09c55dIrfan Sheriff mTelephonyManager = (TelephonyManager) context.getSystemService(Context.TELEPHONY_SERVICE); 96108da0cfa4a2f59cc953a4ec61314e69b61d6777Russell Brenner mProvisioningObserver = new ProvisioningObserver(); 97da6da0907b28d4704aabbdb1bbeb4300954670d1Irfan Sheriff 98da6da0907b28d4704aabbdb1bbeb4300954670d1Irfan Sheriff IntentFilter filter = new IntentFilter(); 99da6da0907b28d4704aabbdb1bbeb4300954670d1Irfan Sheriff filter.addAction(ConnectivityManager.CONNECTIVITY_ACTION); 100108da0cfa4a2f59cc953a4ec61314e69b61d6777Russell Brenner filter.addAction(ConnectivityManager.CONNECTIVITY_ACTION_IMMEDIATE); 101da6da0907b28d4704aabbdb1bbeb4300954670d1Irfan Sheriff mContext.registerReceiver(mReceiver, filter); 102da6da0907b28d4704aabbdb1bbeb4300954670d1Irfan Sheriff 103625239a05401bbf18b04d9874cea3f82da7c29a1Jeff Sharkey mServer = Settings.Global.getString(mContext.getContentResolver(), 104625239a05401bbf18b04d9874cea3f82da7c29a1Jeff Sharkey Settings.Global.CAPTIVE_PORTAL_SERVER); 105da6da0907b28d4704aabbdb1bbeb4300954670d1Irfan Sheriff if (mServer == null) mServer = DEFAULT_SERVER; 106da6da0907b28d4704aabbdb1bbeb4300954670d1Irfan Sheriff 107625239a05401bbf18b04d9874cea3f82da7c29a1Jeff Sharkey mIsCaptivePortalCheckEnabled = Settings.Global.getInt(mContext.getContentResolver(), 108625239a05401bbf18b04d9874cea3f82da7c29a1Jeff Sharkey Settings.Global.CAPTIVE_PORTAL_DETECTION_ENABLED, 1) == 1; 1099538bdd3c77968c7673719c580ae653ede4654d6Irfan Sheriff 1109538bdd3c77968c7673719c580ae653ede4654d6Irfan Sheriff addState(mDefaultState); 1119538bdd3c77968c7673719c580ae653ede4654d6Irfan Sheriff addState(mNoActiveNetworkState, mDefaultState); 1129538bdd3c77968c7673719c580ae653ede4654d6Irfan Sheriff addState(mActiveNetworkState, mDefaultState); 1139538bdd3c77968c7673719c580ae653ede4654d6Irfan Sheriff addState(mDelayedCaptiveCheckState, mActiveNetworkState); 1149538bdd3c77968c7673719c580ae653ede4654d6Irfan Sheriff setInitialState(mNoActiveNetworkState); 115da6da0907b28d4704aabbdb1bbeb4300954670d1Irfan Sheriff } 116da6da0907b28d4704aabbdb1bbeb4300954670d1Irfan Sheriff 117108da0cfa4a2f59cc953a4ec61314e69b61d6777Russell Brenner private class ProvisioningObserver extends ContentObserver { 118108da0cfa4a2f59cc953a4ec61314e69b61d6777Russell Brenner ProvisioningObserver() { 119108da0cfa4a2f59cc953a4ec61314e69b61d6777Russell Brenner super(new Handler()); 120108da0cfa4a2f59cc953a4ec61314e69b61d6777Russell Brenner mContext.getContentResolver().registerContentObserver(Settings.Global.getUriFor( 121108da0cfa4a2f59cc953a4ec61314e69b61d6777Russell Brenner Settings.Global.DEVICE_PROVISIONED), false, this); 122108da0cfa4a2f59cc953a4ec61314e69b61d6777Russell Brenner onChange(false); // load initial value 123108da0cfa4a2f59cc953a4ec61314e69b61d6777Russell Brenner } 124108da0cfa4a2f59cc953a4ec61314e69b61d6777Russell Brenner 125108da0cfa4a2f59cc953a4ec61314e69b61d6777Russell Brenner @Override 126108da0cfa4a2f59cc953a4ec61314e69b61d6777Russell Brenner public void onChange(boolean selfChange) { 127108da0cfa4a2f59cc953a4ec61314e69b61d6777Russell Brenner mDeviceProvisioned = Settings.Global.getInt(mContext.getContentResolver(), 128108da0cfa4a2f59cc953a4ec61314e69b61d6777Russell Brenner Settings.Global.DEVICE_PROVISIONED, 0) != 0; 129108da0cfa4a2f59cc953a4ec61314e69b61d6777Russell Brenner } 130108da0cfa4a2f59cc953a4ec61314e69b61d6777Russell Brenner } 131108da0cfa4a2f59cc953a4ec61314e69b61d6777Russell Brenner 132da6da0907b28d4704aabbdb1bbeb4300954670d1Irfan Sheriff private final BroadcastReceiver mReceiver = new BroadcastReceiver() { 133da6da0907b28d4704aabbdb1bbeb4300954670d1Irfan Sheriff @Override 134da6da0907b28d4704aabbdb1bbeb4300954670d1Irfan Sheriff public void onReceive(Context context, Intent intent) { 135da6da0907b28d4704aabbdb1bbeb4300954670d1Irfan Sheriff String action = intent.getAction(); 136108da0cfa4a2f59cc953a4ec61314e69b61d6777Russell Brenner // Normally, we respond to CONNECTIVITY_ACTION, allowing time for the change in 137108da0cfa4a2f59cc953a4ec61314e69b61d6777Russell Brenner // connectivity to stabilize, but if the device is not yet provisioned, respond 138108da0cfa4a2f59cc953a4ec61314e69b61d6777Russell Brenner // immediately to speed up transit through the setup wizard. 139108da0cfa4a2f59cc953a4ec61314e69b61d6777Russell Brenner if ((mDeviceProvisioned && action.equals(ConnectivityManager.CONNECTIVITY_ACTION)) 140108da0cfa4a2f59cc953a4ec61314e69b61d6777Russell Brenner || (!mDeviceProvisioned 141108da0cfa4a2f59cc953a4ec61314e69b61d6777Russell Brenner && action.equals(ConnectivityManager.CONNECTIVITY_ACTION_IMMEDIATE))) { 142da6da0907b28d4704aabbdb1bbeb4300954670d1Irfan Sheriff NetworkInfo info = intent.getParcelableExtra( 143da6da0907b28d4704aabbdb1bbeb4300954670d1Irfan Sheriff ConnectivityManager.EXTRA_NETWORK_INFO); 1449538bdd3c77968c7673719c580ae653ede4654d6Irfan Sheriff sendMessage(obtainMessage(CMD_CONNECTIVITY_CHANGE, info)); 145da6da0907b28d4704aabbdb1bbeb4300954670d1Irfan Sheriff } 146da6da0907b28d4704aabbdb1bbeb4300954670d1Irfan Sheriff } 147da6da0907b28d4704aabbdb1bbeb4300954670d1Irfan Sheriff }; 148da6da0907b28d4704aabbdb1bbeb4300954670d1Irfan Sheriff 1499538bdd3c77968c7673719c580ae653ede4654d6Irfan Sheriff public static CaptivePortalTracker makeCaptivePortalTracker(Context context, 150da6da0907b28d4704aabbdb1bbeb4300954670d1Irfan Sheriff IConnectivityManager cs) { 1519538bdd3c77968c7673719c580ae653ede4654d6Irfan Sheriff CaptivePortalTracker captivePortal = new CaptivePortalTracker(context, cs); 1529538bdd3c77968c7673719c580ae653ede4654d6Irfan Sheriff captivePortal.start(); 153da6da0907b28d4704aabbdb1bbeb4300954670d1Irfan Sheriff return captivePortal; 154da6da0907b28d4704aabbdb1bbeb4300954670d1Irfan Sheriff } 155da6da0907b28d4704aabbdb1bbeb4300954670d1Irfan Sheriff 1569538bdd3c77968c7673719c580ae653ede4654d6Irfan Sheriff public void detectCaptivePortal(NetworkInfo info) { 1579538bdd3c77968c7673719c580ae653ede4654d6Irfan Sheriff sendMessage(obtainMessage(CMD_DETECT_PORTAL, info)); 1589538bdd3c77968c7673719c580ae653ede4654d6Irfan Sheriff } 1599538bdd3c77968c7673719c580ae653ede4654d6Irfan Sheriff 1609538bdd3c77968c7673719c580ae653ede4654d6Irfan Sheriff private class DefaultState extends State { 1619538bdd3c77968c7673719c580ae653ede4654d6Irfan Sheriff @Override 1629538bdd3c77968c7673719c580ae653ede4654d6Irfan Sheriff public void enter() { 1639538bdd3c77968c7673719c580ae653ede4654d6Irfan Sheriff if (DBG) log(getName() + "\n"); 164da6da0907b28d4704aabbdb1bbeb4300954670d1Irfan Sheriff } 165da6da0907b28d4704aabbdb1bbeb4300954670d1Irfan Sheriff 166da6da0907b28d4704aabbdb1bbeb4300954670d1Irfan Sheriff @Override 1679538bdd3c77968c7673719c580ae653ede4654d6Irfan Sheriff public boolean processMessage(Message message) { 1689538bdd3c77968c7673719c580ae653ede4654d6Irfan Sheriff if (DBG) log(getName() + message.toString() + "\n"); 1699538bdd3c77968c7673719c580ae653ede4654d6Irfan Sheriff switch (message.what) { 1709538bdd3c77968c7673719c580ae653ede4654d6Irfan Sheriff case CMD_DETECT_PORTAL: 1719538bdd3c77968c7673719c580ae653ede4654d6Irfan Sheriff NetworkInfo info = (NetworkInfo) message.obj; 1729538bdd3c77968c7673719c580ae653ede4654d6Irfan Sheriff // Checking on a secondary connection is not supported 1739538bdd3c77968c7673719c580ae653ede4654d6Irfan Sheriff // yet 1749538bdd3c77968c7673719c580ae653ede4654d6Irfan Sheriff notifyPortalCheckComplete(info); 175da6da0907b28d4704aabbdb1bbeb4300954670d1Irfan Sheriff break; 1769538bdd3c77968c7673719c580ae653ede4654d6Irfan Sheriff case CMD_CONNECTIVITY_CHANGE: 1779538bdd3c77968c7673719c580ae653ede4654d6Irfan Sheriff case CMD_DELAYED_CAPTIVE_CHECK: 1789538bdd3c77968c7673719c580ae653ede4654d6Irfan Sheriff break; 1799538bdd3c77968c7673719c580ae653ede4654d6Irfan Sheriff default: 1809538bdd3c77968c7673719c580ae653ede4654d6Irfan Sheriff loge("Ignoring " + message); 1819538bdd3c77968c7673719c580ae653ede4654d6Irfan Sheriff break; 1829538bdd3c77968c7673719c580ae653ede4654d6Irfan Sheriff } 1839538bdd3c77968c7673719c580ae653ede4654d6Irfan Sheriff return HANDLED; 1849538bdd3c77968c7673719c580ae653ede4654d6Irfan Sheriff } 1859538bdd3c77968c7673719c580ae653ede4654d6Irfan Sheriff } 186da6da0907b28d4704aabbdb1bbeb4300954670d1Irfan Sheriff 1879538bdd3c77968c7673719c580ae653ede4654d6Irfan Sheriff private class NoActiveNetworkState extends State { 1889538bdd3c77968c7673719c580ae653ede4654d6Irfan Sheriff @Override 1899538bdd3c77968c7673719c580ae653ede4654d6Irfan Sheriff public void enter() { 1909538bdd3c77968c7673719c580ae653ede4654d6Irfan Sheriff if (DBG) log(getName() + "\n"); 1919538bdd3c77968c7673719c580ae653ede4654d6Irfan Sheriff mNetworkInfo = null; 1929538bdd3c77968c7673719c580ae653ede4654d6Irfan Sheriff /* Clear any previous notification */ 1939538bdd3c77968c7673719c580ae653ede4654d6Irfan Sheriff setNotificationVisible(false); 1949538bdd3c77968c7673719c580ae653ede4654d6Irfan Sheriff } 195da6da0907b28d4704aabbdb1bbeb4300954670d1Irfan Sheriff 1969538bdd3c77968c7673719c580ae653ede4654d6Irfan Sheriff @Override 1979538bdd3c77968c7673719c580ae653ede4654d6Irfan Sheriff public boolean processMessage(Message message) { 1989538bdd3c77968c7673719c580ae653ede4654d6Irfan Sheriff if (DBG) log(getName() + message.toString() + "\n"); 1999538bdd3c77968c7673719c580ae653ede4654d6Irfan Sheriff InetAddress server; 2009538bdd3c77968c7673719c580ae653ede4654d6Irfan Sheriff NetworkInfo info; 2019538bdd3c77968c7673719c580ae653ede4654d6Irfan Sheriff switch (message.what) { 2029538bdd3c77968c7673719c580ae653ede4654d6Irfan Sheriff case CMD_CONNECTIVITY_CHANGE: 2039538bdd3c77968c7673719c580ae653ede4654d6Irfan Sheriff info = (NetworkInfo) message.obj; 2049538bdd3c77968c7673719c580ae653ede4654d6Irfan Sheriff if (info.isConnected() && isActiveNetwork(info)) { 2059538bdd3c77968c7673719c580ae653ede4654d6Irfan Sheriff mNetworkInfo = info; 2069538bdd3c77968c7673719c580ae653ede4654d6Irfan Sheriff transitionTo(mDelayedCaptiveCheckState); 207da6da0907b28d4704aabbdb1bbeb4300954670d1Irfan Sheriff } 208da6da0907b28d4704aabbdb1bbeb4300954670d1Irfan Sheriff break; 209da6da0907b28d4704aabbdb1bbeb4300954670d1Irfan Sheriff default: 2109538bdd3c77968c7673719c580ae653ede4654d6Irfan Sheriff return NOT_HANDLED; 211da6da0907b28d4704aabbdb1bbeb4300954670d1Irfan Sheriff } 2129538bdd3c77968c7673719c580ae653ede4654d6Irfan Sheriff return HANDLED; 2139538bdd3c77968c7673719c580ae653ede4654d6Irfan Sheriff } 2149538bdd3c77968c7673719c580ae653ede4654d6Irfan Sheriff } 2159538bdd3c77968c7673719c580ae653ede4654d6Irfan Sheriff 2169538bdd3c77968c7673719c580ae653ede4654d6Irfan Sheriff private class ActiveNetworkState extends State { 2179538bdd3c77968c7673719c580ae653ede4654d6Irfan Sheriff @Override 2189538bdd3c77968c7673719c580ae653ede4654d6Irfan Sheriff public void enter() { 2199538bdd3c77968c7673719c580ae653ede4654d6Irfan Sheriff if (DBG) log(getName() + "\n"); 220da6da0907b28d4704aabbdb1bbeb4300954670d1Irfan Sheriff } 221da6da0907b28d4704aabbdb1bbeb4300954670d1Irfan Sheriff 2229538bdd3c77968c7673719c580ae653ede4654d6Irfan Sheriff @Override 2239538bdd3c77968c7673719c580ae653ede4654d6Irfan Sheriff public boolean processMessage(Message message) { 2249538bdd3c77968c7673719c580ae653ede4654d6Irfan Sheriff NetworkInfo info; 2259538bdd3c77968c7673719c580ae653ede4654d6Irfan Sheriff switch (message.what) { 2269538bdd3c77968c7673719c580ae653ede4654d6Irfan Sheriff case CMD_CONNECTIVITY_CHANGE: 2279538bdd3c77968c7673719c580ae653ede4654d6Irfan Sheriff info = (NetworkInfo) message.obj; 2289538bdd3c77968c7673719c580ae653ede4654d6Irfan Sheriff if (!info.isConnected() 2299538bdd3c77968c7673719c580ae653ede4654d6Irfan Sheriff && info.getType() == mNetworkInfo.getType()) { 2309538bdd3c77968c7673719c580ae653ede4654d6Irfan Sheriff if (DBG) log("Disconnected from active network " + info); 2319538bdd3c77968c7673719c580ae653ede4654d6Irfan Sheriff transitionTo(mNoActiveNetworkState); 2329538bdd3c77968c7673719c580ae653ede4654d6Irfan Sheriff } else if (info.getType() != mNetworkInfo.getType() && 2339538bdd3c77968c7673719c580ae653ede4654d6Irfan Sheriff info.isConnected() && 2349538bdd3c77968c7673719c580ae653ede4654d6Irfan Sheriff isActiveNetwork(info)) { 2359538bdd3c77968c7673719c580ae653ede4654d6Irfan Sheriff if (DBG) log("Active network switched " + info); 2369538bdd3c77968c7673719c580ae653ede4654d6Irfan Sheriff deferMessage(message); 2379538bdd3c77968c7673719c580ae653ede4654d6Irfan Sheriff transitionTo(mNoActiveNetworkState); 2389538bdd3c77968c7673719c580ae653ede4654d6Irfan Sheriff } 2399538bdd3c77968c7673719c580ae653ede4654d6Irfan Sheriff break; 2409538bdd3c77968c7673719c580ae653ede4654d6Irfan Sheriff default: 2419538bdd3c77968c7673719c580ae653ede4654d6Irfan Sheriff return NOT_HANDLED; 2429538bdd3c77968c7673719c580ae653ede4654d6Irfan Sheriff } 2439538bdd3c77968c7673719c580ae653ede4654d6Irfan Sheriff return HANDLED; 244da6da0907b28d4704aabbdb1bbeb4300954670d1Irfan Sheriff } 245da6da0907b28d4704aabbdb1bbeb4300954670d1Irfan Sheriff } 246da6da0907b28d4704aabbdb1bbeb4300954670d1Irfan Sheriff 2479538bdd3c77968c7673719c580ae653ede4654d6Irfan Sheriff 2489538bdd3c77968c7673719c580ae653ede4654d6Irfan Sheriff 2499538bdd3c77968c7673719c580ae653ede4654d6Irfan Sheriff private class DelayedCaptiveCheckState extends State { 2509538bdd3c77968c7673719c580ae653ede4654d6Irfan Sheriff @Override 2519538bdd3c77968c7673719c580ae653ede4654d6Irfan Sheriff public void enter() { 2529538bdd3c77968c7673719c580ae653ede4654d6Irfan Sheriff if (DBG) log(getName() + "\n"); 253108da0cfa4a2f59cc953a4ec61314e69b61d6777Russell Brenner Message message = obtainMessage(CMD_DELAYED_CAPTIVE_CHECK, ++mDelayedCheckToken, 0); 254108da0cfa4a2f59cc953a4ec61314e69b61d6777Russell Brenner if (mDeviceProvisioned) { 255108da0cfa4a2f59cc953a4ec61314e69b61d6777Russell Brenner sendMessageDelayed(message, DELAYED_CHECK_INTERVAL_MS); 256108da0cfa4a2f59cc953a4ec61314e69b61d6777Russell Brenner } else { 257108da0cfa4a2f59cc953a4ec61314e69b61d6777Russell Brenner sendMessage(message); 258108da0cfa4a2f59cc953a4ec61314e69b61d6777Russell Brenner } 2599538bdd3c77968c7673719c580ae653ede4654d6Irfan Sheriff } 2609538bdd3c77968c7673719c580ae653ede4654d6Irfan Sheriff 2619538bdd3c77968c7673719c580ae653ede4654d6Irfan Sheriff @Override 2629538bdd3c77968c7673719c580ae653ede4654d6Irfan Sheriff public boolean processMessage(Message message) { 2639538bdd3c77968c7673719c580ae653ede4654d6Irfan Sheriff if (DBG) log(getName() + message.toString() + "\n"); 2649538bdd3c77968c7673719c580ae653ede4654d6Irfan Sheriff switch (message.what) { 2659538bdd3c77968c7673719c580ae653ede4654d6Irfan Sheriff case CMD_DELAYED_CAPTIVE_CHECK: 2669538bdd3c77968c7673719c580ae653ede4654d6Irfan Sheriff if (message.arg1 == mDelayedCheckToken) { 2679538bdd3c77968c7673719c580ae653ede4654d6Irfan Sheriff InetAddress server = lookupHost(mServer); 268108da0cfa4a2f59cc953a4ec61314e69b61d6777Russell Brenner boolean captive = server != null && isCaptivePortal(server); 269108da0cfa4a2f59cc953a4ec61314e69b61d6777Russell Brenner if (captive) { 270108da0cfa4a2f59cc953a4ec61314e69b61d6777Russell Brenner if (DBG) log("Captive network " + mNetworkInfo); 271108da0cfa4a2f59cc953a4ec61314e69b61d6777Russell Brenner } else { 272108da0cfa4a2f59cc953a4ec61314e69b61d6777Russell Brenner if (DBG) log("Not captive network " + mNetworkInfo); 273108da0cfa4a2f59cc953a4ec61314e69b61d6777Russell Brenner } 274108da0cfa4a2f59cc953a4ec61314e69b61d6777Russell Brenner if (mDeviceProvisioned) { 275108da0cfa4a2f59cc953a4ec61314e69b61d6777Russell Brenner if (captive) { 276108da0cfa4a2f59cc953a4ec61314e69b61d6777Russell Brenner // Setup Wizard will assist the user in connecting to a captive 277108da0cfa4a2f59cc953a4ec61314e69b61d6777Russell Brenner // portal, so make the notification visible unless during setup 2789538bdd3c77968c7673719c580ae653ede4654d6Irfan Sheriff setNotificationVisible(true); 2799538bdd3c77968c7673719c580ae653ede4654d6Irfan Sheriff } 280108da0cfa4a2f59cc953a4ec61314e69b61d6777Russell Brenner } else { 281108da0cfa4a2f59cc953a4ec61314e69b61d6777Russell Brenner Intent intent = new Intent( 282108da0cfa4a2f59cc953a4ec61314e69b61d6777Russell Brenner ConnectivityManager.ACTION_CAPTIVE_PORTAL_TEST_COMPLETED); 283108da0cfa4a2f59cc953a4ec61314e69b61d6777Russell Brenner intent.putExtra(ConnectivityManager.EXTRA_IS_CAPTIVE_PORTAL, captive); 284108da0cfa4a2f59cc953a4ec61314e69b61d6777Russell Brenner intent.setPackage(SETUP_WIZARD_PACKAGE); 285108da0cfa4a2f59cc953a4ec61314e69b61d6777Russell Brenner mContext.sendBroadcast(intent); 2869538bdd3c77968c7673719c580ae653ede4654d6Irfan Sheriff } 287108da0cfa4a2f59cc953a4ec61314e69b61d6777Russell Brenner 2889538bdd3c77968c7673719c580ae653ede4654d6Irfan Sheriff transitionTo(mActiveNetworkState); 2899538bdd3c77968c7673719c580ae653ede4654d6Irfan Sheriff } 2909538bdd3c77968c7673719c580ae653ede4654d6Irfan Sheriff break; 2919538bdd3c77968c7673719c580ae653ede4654d6Irfan Sheriff default: 2929538bdd3c77968c7673719c580ae653ede4654d6Irfan Sheriff return NOT_HANDLED; 2939538bdd3c77968c7673719c580ae653ede4654d6Irfan Sheriff } 2949538bdd3c77968c7673719c580ae653ede4654d6Irfan Sheriff return HANDLED; 2959538bdd3c77968c7673719c580ae653ede4654d6Irfan Sheriff } 296da6da0907b28d4704aabbdb1bbeb4300954670d1Irfan Sheriff } 297da6da0907b28d4704aabbdb1bbeb4300954670d1Irfan Sheriff 2989538bdd3c77968c7673719c580ae653ede4654d6Irfan Sheriff private void notifyPortalCheckComplete(NetworkInfo info) { 2999538bdd3c77968c7673719c580ae653ede4654d6Irfan Sheriff if (info == null) { 3009538bdd3c77968c7673719c580ae653ede4654d6Irfan Sheriff loge("notifyPortalCheckComplete on null"); 3019538bdd3c77968c7673719c580ae653ede4654d6Irfan Sheriff return; 3029538bdd3c77968c7673719c580ae653ede4654d6Irfan Sheriff } 303da6da0907b28d4704aabbdb1bbeb4300954670d1Irfan Sheriff try { 3049538bdd3c77968c7673719c580ae653ede4654d6Irfan Sheriff mConnService.captivePortalCheckComplete(info); 305da6da0907b28d4704aabbdb1bbeb4300954670d1Irfan Sheriff } catch(RemoteException e) { 306da6da0907b28d4704aabbdb1bbeb4300954670d1Irfan Sheriff e.printStackTrace(); 307da6da0907b28d4704aabbdb1bbeb4300954670d1Irfan Sheriff } 308da6da0907b28d4704aabbdb1bbeb4300954670d1Irfan Sheriff } 309da6da0907b28d4704aabbdb1bbeb4300954670d1Irfan Sheriff 3109538bdd3c77968c7673719c580ae653ede4654d6Irfan Sheriff private boolean isActiveNetwork(NetworkInfo info) { 311da6da0907b28d4704aabbdb1bbeb4300954670d1Irfan Sheriff try { 3129538bdd3c77968c7673719c580ae653ede4654d6Irfan Sheriff NetworkInfo active = mConnService.getActiveNetworkInfo(); 3139538bdd3c77968c7673719c580ae653ede4654d6Irfan Sheriff if (active != null && active.getType() == info.getType()) { 3149538bdd3c77968c7673719c580ae653ede4654d6Irfan Sheriff return true; 3159538bdd3c77968c7673719c580ae653ede4654d6Irfan Sheriff } 316da6da0907b28d4704aabbdb1bbeb4300954670d1Irfan Sheriff } catch (RemoteException e) { 317da6da0907b28d4704aabbdb1bbeb4300954670d1Irfan Sheriff e.printStackTrace(); 318da6da0907b28d4704aabbdb1bbeb4300954670d1Irfan Sheriff } 3199538bdd3c77968c7673719c580ae653ede4654d6Irfan Sheriff return false; 320da6da0907b28d4704aabbdb1bbeb4300954670d1Irfan Sheriff } 321da6da0907b28d4704aabbdb1bbeb4300954670d1Irfan Sheriff 322da6da0907b28d4704aabbdb1bbeb4300954670d1Irfan Sheriff /** 323da6da0907b28d4704aabbdb1bbeb4300954670d1Irfan Sheriff * Do a URL fetch on a known server to see if we get the data we expect 324da6da0907b28d4704aabbdb1bbeb4300954670d1Irfan Sheriff */ 325da6da0907b28d4704aabbdb1bbeb4300954670d1Irfan Sheriff private boolean isCaptivePortal(InetAddress server) { 326da6da0907b28d4704aabbdb1bbeb4300954670d1Irfan Sheriff HttpURLConnection urlConnection = null; 327da6da0907b28d4704aabbdb1bbeb4300954670d1Irfan Sheriff if (!mIsCaptivePortalCheckEnabled) return false; 328da6da0907b28d4704aabbdb1bbeb4300954670d1Irfan Sheriff 329da6da0907b28d4704aabbdb1bbeb4300954670d1Irfan Sheriff mUrl = "http://" + server.getHostAddress() + "/generate_204"; 3309538bdd3c77968c7673719c580ae653ede4654d6Irfan Sheriff if (DBG) log("Checking " + mUrl); 331da6da0907b28d4704aabbdb1bbeb4300954670d1Irfan Sheriff try { 332da6da0907b28d4704aabbdb1bbeb4300954670d1Irfan Sheriff URL url = new URL(mUrl); 333da6da0907b28d4704aabbdb1bbeb4300954670d1Irfan Sheriff urlConnection = (HttpURLConnection) url.openConnection(); 334da6da0907b28d4704aabbdb1bbeb4300954670d1Irfan Sheriff urlConnection.setInstanceFollowRedirects(false); 335da6da0907b28d4704aabbdb1bbeb4300954670d1Irfan Sheriff urlConnection.setConnectTimeout(SOCKET_TIMEOUT_MS); 336da6da0907b28d4704aabbdb1bbeb4300954670d1Irfan Sheriff urlConnection.setReadTimeout(SOCKET_TIMEOUT_MS); 337da6da0907b28d4704aabbdb1bbeb4300954670d1Irfan Sheriff urlConnection.setUseCaches(false); 338da6da0907b28d4704aabbdb1bbeb4300954670d1Irfan Sheriff urlConnection.getInputStream(); 339da6da0907b28d4704aabbdb1bbeb4300954670d1Irfan Sheriff // we got a valid response, but not from the real google 340da6da0907b28d4704aabbdb1bbeb4300954670d1Irfan Sheriff return urlConnection.getResponseCode() != 204; 341bf34122a96ef3d02e9b7935e07eb4f9b04034828Wink Saville } catch (SocketTimeoutException e) { 342bf34122a96ef3d02e9b7935e07eb4f9b04034828Wink Saville if (DBG) log("Probably a portal: exception " + e); 343bf34122a96ef3d02e9b7935e07eb4f9b04034828Wink Saville return true; 344da6da0907b28d4704aabbdb1bbeb4300954670d1Irfan Sheriff } catch (IOException e) { 345da6da0907b28d4704aabbdb1bbeb4300954670d1Irfan Sheriff if (DBG) log("Probably not a portal: exception " + e); 346da6da0907b28d4704aabbdb1bbeb4300954670d1Irfan Sheriff return false; 347da6da0907b28d4704aabbdb1bbeb4300954670d1Irfan Sheriff } finally { 348da6da0907b28d4704aabbdb1bbeb4300954670d1Irfan Sheriff if (urlConnection != null) { 349da6da0907b28d4704aabbdb1bbeb4300954670d1Irfan Sheriff urlConnection.disconnect(); 350da6da0907b28d4704aabbdb1bbeb4300954670d1Irfan Sheriff } 351da6da0907b28d4704aabbdb1bbeb4300954670d1Irfan Sheriff } 352da6da0907b28d4704aabbdb1bbeb4300954670d1Irfan Sheriff } 353da6da0907b28d4704aabbdb1bbeb4300954670d1Irfan Sheriff 354da6da0907b28d4704aabbdb1bbeb4300954670d1Irfan Sheriff private InetAddress lookupHost(String hostname) { 355da6da0907b28d4704aabbdb1bbeb4300954670d1Irfan Sheriff InetAddress inetAddress[]; 356da6da0907b28d4704aabbdb1bbeb4300954670d1Irfan Sheriff try { 357da6da0907b28d4704aabbdb1bbeb4300954670d1Irfan Sheriff inetAddress = InetAddress.getAllByName(hostname); 358da6da0907b28d4704aabbdb1bbeb4300954670d1Irfan Sheriff } catch (UnknownHostException e) { 359da6da0907b28d4704aabbdb1bbeb4300954670d1Irfan Sheriff return null; 360da6da0907b28d4704aabbdb1bbeb4300954670d1Irfan Sheriff } 361da6da0907b28d4704aabbdb1bbeb4300954670d1Irfan Sheriff 362da6da0907b28d4704aabbdb1bbeb4300954670d1Irfan Sheriff for (InetAddress a : inetAddress) { 363da6da0907b28d4704aabbdb1bbeb4300954670d1Irfan Sheriff if (a instanceof Inet4Address) return a; 364da6da0907b28d4704aabbdb1bbeb4300954670d1Irfan Sheriff } 365da6da0907b28d4704aabbdb1bbeb4300954670d1Irfan Sheriff return null; 366da6da0907b28d4704aabbdb1bbeb4300954670d1Irfan Sheriff } 367da6da0907b28d4704aabbdb1bbeb4300954670d1Irfan Sheriff 368da6da0907b28d4704aabbdb1bbeb4300954670d1Irfan Sheriff private void setNotificationVisible(boolean visible) { 369da6da0907b28d4704aabbdb1bbeb4300954670d1Irfan Sheriff // if it should be hidden and it is already hidden, then noop 370da6da0907b28d4704aabbdb1bbeb4300954670d1Irfan Sheriff if (!visible && !mNotificationShown) { 371da6da0907b28d4704aabbdb1bbeb4300954670d1Irfan Sheriff return; 372da6da0907b28d4704aabbdb1bbeb4300954670d1Irfan Sheriff } 373da6da0907b28d4704aabbdb1bbeb4300954670d1Irfan Sheriff 374da6da0907b28d4704aabbdb1bbeb4300954670d1Irfan Sheriff Resources r = Resources.getSystem(); 375da6da0907b28d4704aabbdb1bbeb4300954670d1Irfan Sheriff NotificationManager notificationManager = (NotificationManager) mContext 376da6da0907b28d4704aabbdb1bbeb4300954670d1Irfan Sheriff .getSystemService(Context.NOTIFICATION_SERVICE); 377da6da0907b28d4704aabbdb1bbeb4300954670d1Irfan Sheriff 378da6da0907b28d4704aabbdb1bbeb4300954670d1Irfan Sheriff if (visible) { 3799538bdd3c77968c7673719c580ae653ede4654d6Irfan Sheriff CharSequence title; 380b8aad91f059527e04abaf8a83ed1ce6b5f09c55dIrfan Sheriff CharSequence details; 381ebb8f413e63dee6d96904a49a3508b97671e5fe8Irfan Sheriff int icon; 382b8aad91f059527e04abaf8a83ed1ce6b5f09c55dIrfan Sheriff switch (mNetworkInfo.getType()) { 383b8aad91f059527e04abaf8a83ed1ce6b5f09c55dIrfan Sheriff case ConnectivityManager.TYPE_WIFI: 384b8aad91f059527e04abaf8a83ed1ce6b5f09c55dIrfan Sheriff title = r.getString(R.string.wifi_available_sign_in, 0); 385b8aad91f059527e04abaf8a83ed1ce6b5f09c55dIrfan Sheriff details = r.getString(R.string.network_available_sign_in_detailed, 386b8aad91f059527e04abaf8a83ed1ce6b5f09c55dIrfan Sheriff mNetworkInfo.getExtraInfo()); 387ebb8f413e63dee6d96904a49a3508b97671e5fe8Irfan Sheriff icon = R.drawable.stat_notify_wifi_in_range; 388b8aad91f059527e04abaf8a83ed1ce6b5f09c55dIrfan Sheriff break; 389b8aad91f059527e04abaf8a83ed1ce6b5f09c55dIrfan Sheriff case ConnectivityManager.TYPE_MOBILE: 390b8aad91f059527e04abaf8a83ed1ce6b5f09c55dIrfan Sheriff title = r.getString(R.string.network_available_sign_in, 0); 391b8aad91f059527e04abaf8a83ed1ce6b5f09c55dIrfan Sheriff // TODO: Change this to pull from NetworkInfo once a printable 392b8aad91f059527e04abaf8a83ed1ce6b5f09c55dIrfan Sheriff // name has been added to it 393b8aad91f059527e04abaf8a83ed1ce6b5f09c55dIrfan Sheriff details = mTelephonyManager.getNetworkOperatorName(); 394ebb8f413e63dee6d96904a49a3508b97671e5fe8Irfan Sheriff icon = R.drawable.stat_notify_rssi_in_range; 395b8aad91f059527e04abaf8a83ed1ce6b5f09c55dIrfan Sheriff break; 396b8aad91f059527e04abaf8a83ed1ce6b5f09c55dIrfan Sheriff default: 397b8aad91f059527e04abaf8a83ed1ce6b5f09c55dIrfan Sheriff title = r.getString(R.string.network_available_sign_in, 0); 398b8aad91f059527e04abaf8a83ed1ce6b5f09c55dIrfan Sheriff details = r.getString(R.string.network_available_sign_in_detailed, 399b8aad91f059527e04abaf8a83ed1ce6b5f09c55dIrfan Sheriff mNetworkInfo.getExtraInfo()); 400ebb8f413e63dee6d96904a49a3508b97671e5fe8Irfan Sheriff icon = R.drawable.stat_notify_rssi_in_range; 401b8aad91f059527e04abaf8a83ed1ce6b5f09c55dIrfan Sheriff break; 4029538bdd3c77968c7673719c580ae653ede4654d6Irfan Sheriff } 403da6da0907b28d4704aabbdb1bbeb4300954670d1Irfan Sheriff 404da6da0907b28d4704aabbdb1bbeb4300954670d1Irfan Sheriff Notification notification = new Notification(); 405da6da0907b28d4704aabbdb1bbeb4300954670d1Irfan Sheriff notification.when = 0; 406ebb8f413e63dee6d96904a49a3508b97671e5fe8Irfan Sheriff notification.icon = icon; 407da6da0907b28d4704aabbdb1bbeb4300954670d1Irfan Sheriff notification.flags = Notification.FLAG_AUTO_CANCEL; 4089538bdd3c77968c7673719c580ae653ede4654d6Irfan Sheriff Intent intent = new Intent(Intent.ACTION_VIEW, Uri.parse(mUrl)); 4099538bdd3c77968c7673719c580ae653ede4654d6Irfan Sheriff intent.setFlags(Intent.FLAG_ACTIVITY_BROUGHT_TO_FRONT | 4109538bdd3c77968c7673719c580ae653ede4654d6Irfan Sheriff Intent.FLAG_ACTIVITY_NEW_TASK); 4119538bdd3c77968c7673719c580ae653ede4654d6Irfan Sheriff notification.contentIntent = PendingIntent.getActivity(mContext, 0, intent, 0); 412da6da0907b28d4704aabbdb1bbeb4300954670d1Irfan Sheriff notification.tickerText = title; 413da6da0907b28d4704aabbdb1bbeb4300954670d1Irfan Sheriff notification.setLatestEventInfo(mContext, title, details, notification.contentIntent); 414da6da0907b28d4704aabbdb1bbeb4300954670d1Irfan Sheriff 415da6da0907b28d4704aabbdb1bbeb4300954670d1Irfan Sheriff notificationManager.notify(NOTIFICATION_ID, 1, notification); 416da6da0907b28d4704aabbdb1bbeb4300954670d1Irfan Sheriff } else { 417da6da0907b28d4704aabbdb1bbeb4300954670d1Irfan Sheriff notificationManager.cancel(NOTIFICATION_ID, 1); 418da6da0907b28d4704aabbdb1bbeb4300954670d1Irfan Sheriff } 419da6da0907b28d4704aabbdb1bbeb4300954670d1Irfan Sheriff mNotificationShown = visible; 420da6da0907b28d4704aabbdb1bbeb4300954670d1Irfan Sheriff } 421da6da0907b28d4704aabbdb1bbeb4300954670d1Irfan Sheriff} 422