Home | History | Annotate | Download | only in net
      1 /*
      2  * Copyright (C) 2011 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 com.android.internal.util.Protocol;
     20 import com.android.internal.util.State;
     21 import com.android.internal.util.StateMachine;
     22 
     23 import android.app.AlarmManager;
     24 import android.app.PendingIntent;
     25 import android.content.BroadcastReceiver;
     26 import android.content.Context;
     27 import android.content.Intent;
     28 import android.content.IntentFilter;
     29 import android.net.DhcpInfoInternal;
     30 import android.net.NetworkUtils;
     31 import android.os.Message;
     32 import android.os.PowerManager;
     33 import android.os.SystemClock;
     34 import android.util.Log;
     35 
     36 /**
     37  * StateMachine that interacts with the native DHCP client and can talk to
     38  * a controller that also needs to be a StateMachine
     39  *
     40  * The Dhcp state machine provides the following features:
     41  * - Wakeup and renewal using the native DHCP client  (which will not renew
     42  *   on its own when the device is in suspend state and this can lead to device
     43  *   holding IP address beyond expiry)
     44  * - A notification right before DHCP request or renewal is started. This
     45  *   can be used for any additional setup before DHCP. For example, wifi sets
     46  *   BT-Wifi coex settings right before DHCP is initiated
     47  *
     48  * @hide
     49  */
     50 public class DhcpStateMachine extends StateMachine {
     51 
     52     private static final String TAG = "DhcpStateMachine";
     53     private static final boolean DBG = false;
     54 
     55 
     56     /* A StateMachine that controls the DhcpStateMachine */
     57     private StateMachine mController;
     58 
     59     private Context mContext;
     60     private BroadcastReceiver mBroadcastReceiver;
     61     private AlarmManager mAlarmManager;
     62     private PendingIntent mDhcpRenewalIntent;
     63     private PowerManager.WakeLock mDhcpRenewWakeLock;
     64     private static final String WAKELOCK_TAG = "DHCP";
     65 
     66     //Remember DHCP configuration from first request
     67     private DhcpInfoInternal mDhcpInfo;
     68 
     69     private static final int DHCP_RENEW = 0;
     70     private static final String ACTION_DHCP_RENEW = "android.net.wifi.DHCP_RENEW";
     71 
     72     //Used for sanity check on setting up renewal
     73     private static final int MIN_RENEWAL_TIME_SECS = 5 * 60;  // 5 minutes
     74 
     75     private enum DhcpAction {
     76         START,
     77         RENEW
     78     };
     79 
     80     private String mInterfaceName;
     81     private boolean mRegisteredForPreDhcpNotification = false;
     82 
     83     private static final int BASE = Protocol.BASE_DHCP;
     84 
     85     /* Commands from controller to start/stop DHCP */
     86     public static final int CMD_START_DHCP                  = BASE + 1;
     87     public static final int CMD_STOP_DHCP                   = BASE + 2;
     88     public static final int CMD_RENEW_DHCP                  = BASE + 3;
     89 
     90     /* Notification from DHCP state machine prior to DHCP discovery/renewal */
     91     public static final int CMD_PRE_DHCP_ACTION             = BASE + 4;
     92     /* Notification from DHCP state machine post DHCP discovery/renewal. Indicates
     93      * success/failure */
     94     public static final int CMD_POST_DHCP_ACTION            = BASE + 5;
     95 
     96     /* Command from controller to indicate DHCP discovery/renewal can continue
     97      * after pre DHCP action is complete */
     98     public static final int CMD_PRE_DHCP_ACTION_COMPLETE    = BASE + 6;
     99 
    100     /* Message.arg1 arguments to CMD_POST_DHCP notification */
    101     public static final int DHCP_SUCCESS = 1;
    102     public static final int DHCP_FAILURE = 2;
    103 
    104     private State mDefaultState = new DefaultState();
    105     private State mStoppedState = new StoppedState();
    106     private State mWaitBeforeStartState = new WaitBeforeStartState();
    107     private State mRunningState = new RunningState();
    108     private State mWaitBeforeRenewalState = new WaitBeforeRenewalState();
    109 
    110     private DhcpStateMachine(Context context, StateMachine controller, String intf) {
    111         super(TAG);
    112 
    113         mContext = context;
    114         mController = controller;
    115         mInterfaceName = intf;
    116 
    117         mAlarmManager = (AlarmManager)mContext.getSystemService(Context.ALARM_SERVICE);
    118         Intent dhcpRenewalIntent = new Intent(ACTION_DHCP_RENEW, null);
    119         mDhcpRenewalIntent = PendingIntent.getBroadcast(mContext, DHCP_RENEW, dhcpRenewalIntent, 0);
    120 
    121         PowerManager powerManager = (PowerManager)mContext.getSystemService(Context.POWER_SERVICE);
    122         mDhcpRenewWakeLock = powerManager.newWakeLock(PowerManager.PARTIAL_WAKE_LOCK, WAKELOCK_TAG);
    123         mDhcpRenewWakeLock.setReferenceCounted(false);
    124 
    125         mBroadcastReceiver = new BroadcastReceiver() {
    126             @Override
    127             public void onReceive(Context context, Intent intent) {
    128                 //DHCP renew
    129                 if (DBG) Log.d(TAG, "Sending a DHCP renewal " + this);
    130                 //Lock released after 40s in worst case scenario
    131                 mDhcpRenewWakeLock.acquire(40000);
    132                 sendMessage(CMD_RENEW_DHCP);
    133             }
    134         };
    135         mContext.registerReceiver(mBroadcastReceiver, new IntentFilter(ACTION_DHCP_RENEW));
    136 
    137         addState(mDefaultState);
    138             addState(mStoppedState, mDefaultState);
    139             addState(mWaitBeforeStartState, mDefaultState);
    140             addState(mRunningState, mDefaultState);
    141             addState(mWaitBeforeRenewalState, mDefaultState);
    142 
    143         setInitialState(mStoppedState);
    144     }
    145 
    146     public static DhcpStateMachine makeDhcpStateMachine(Context context, StateMachine controller,
    147             String intf) {
    148         DhcpStateMachine dsm = new DhcpStateMachine(context, controller, intf);
    149         dsm.start();
    150         return dsm;
    151     }
    152 
    153     /**
    154      * This sends a notification right before DHCP request/renewal so that the
    155      * controller can do certain actions before DHCP packets are sent out.
    156      * When the controller is ready, it sends a CMD_PRE_DHCP_ACTION_COMPLETE message
    157      * to indicate DHCP can continue
    158      *
    159      * This is used by Wifi at this time for the purpose of doing BT-Wifi coex
    160      * handling during Dhcp
    161      */
    162     public void registerForPreDhcpNotification() {
    163         mRegisteredForPreDhcpNotification = true;
    164     }
    165 
    166     class DefaultState extends State {
    167         @Override
    168         public boolean processMessage(Message message) {
    169             if (DBG) Log.d(TAG, getName() + message.toString() + "\n");
    170             switch (message.what) {
    171                 case CMD_RENEW_DHCP:
    172                     Log.e(TAG, "Error! Failed to handle a DHCP renewal on " + mInterfaceName);
    173                     mDhcpRenewWakeLock.release();
    174                     break;
    175                 case SM_QUIT_CMD:
    176                     mContext.unregisterReceiver(mBroadcastReceiver);
    177                     //let parent kill the state machine
    178                     return NOT_HANDLED;
    179                 default:
    180                     Log.e(TAG, "Error! unhandled message  " + message);
    181                     break;
    182             }
    183             return HANDLED;
    184         }
    185     }
    186 
    187 
    188     class StoppedState extends State {
    189         @Override
    190         public void enter() {
    191             if (DBG) Log.d(TAG, getName() + "\n");
    192         }
    193 
    194         @Override
    195         public boolean processMessage(Message message) {
    196             boolean retValue = HANDLED;
    197             if (DBG) Log.d(TAG, getName() + message.toString() + "\n");
    198             switch (message.what) {
    199                 case CMD_START_DHCP:
    200                     if (mRegisteredForPreDhcpNotification) {
    201                         /* Notify controller before starting DHCP */
    202                         mController.sendMessage(CMD_PRE_DHCP_ACTION);
    203                         transitionTo(mWaitBeforeStartState);
    204                     } else {
    205                         if (runDhcp(DhcpAction.START)) {
    206                             transitionTo(mRunningState);
    207                         }
    208                     }
    209                     break;
    210                 case CMD_STOP_DHCP:
    211                     //ignore
    212                     break;
    213                 default:
    214                     retValue = NOT_HANDLED;
    215                     break;
    216             }
    217             return retValue;
    218         }
    219     }
    220 
    221     class WaitBeforeStartState extends State {
    222         @Override
    223         public void enter() {
    224             if (DBG) Log.d(TAG, getName() + "\n");
    225         }
    226 
    227         @Override
    228         public boolean processMessage(Message message) {
    229             boolean retValue = HANDLED;
    230             if (DBG) Log.d(TAG, getName() + message.toString() + "\n");
    231             switch (message.what) {
    232                 case CMD_PRE_DHCP_ACTION_COMPLETE:
    233                     if (runDhcp(DhcpAction.START)) {
    234                         transitionTo(mRunningState);
    235                     } else {
    236                         transitionTo(mStoppedState);
    237                     }
    238                     break;
    239                 case CMD_STOP_DHCP:
    240                     transitionTo(mStoppedState);
    241                     break;
    242                 case CMD_START_DHCP:
    243                     //ignore
    244                     break;
    245                 default:
    246                     retValue = NOT_HANDLED;
    247                     break;
    248             }
    249             return retValue;
    250         }
    251     }
    252 
    253     class RunningState extends State {
    254         @Override
    255         public void enter() {
    256             if (DBG) Log.d(TAG, getName() + "\n");
    257         }
    258 
    259         @Override
    260         public boolean processMessage(Message message) {
    261             boolean retValue = HANDLED;
    262             if (DBG) Log.d(TAG, getName() + message.toString() + "\n");
    263             switch (message.what) {
    264                 case CMD_STOP_DHCP:
    265                     mAlarmManager.cancel(mDhcpRenewalIntent);
    266                     if (!NetworkUtils.stopDhcp(mInterfaceName)) {
    267                         Log.e(TAG, "Failed to stop Dhcp on " + mInterfaceName);
    268                     }
    269                     transitionTo(mStoppedState);
    270                     break;
    271                 case CMD_RENEW_DHCP:
    272                     if (mRegisteredForPreDhcpNotification) {
    273                         /* Notify controller before starting DHCP */
    274                         mController.sendMessage(CMD_PRE_DHCP_ACTION);
    275                         transitionTo(mWaitBeforeRenewalState);
    276                         //mDhcpRenewWakeLock is released in WaitBeforeRenewalState
    277                     } else {
    278                         if (!runDhcp(DhcpAction.RENEW)) {
    279                             transitionTo(mStoppedState);
    280                         }
    281                         mDhcpRenewWakeLock.release();
    282                     }
    283                     break;
    284                 case CMD_START_DHCP:
    285                     //ignore
    286                     break;
    287                 default:
    288                     retValue = NOT_HANDLED;
    289             }
    290             return retValue;
    291         }
    292     }
    293 
    294     class WaitBeforeRenewalState extends State {
    295         @Override
    296         public void enter() {
    297             if (DBG) Log.d(TAG, getName() + "\n");
    298         }
    299 
    300         @Override
    301         public boolean processMessage(Message message) {
    302             boolean retValue = HANDLED;
    303             if (DBG) Log.d(TAG, getName() + message.toString() + "\n");
    304             switch (message.what) {
    305                 case CMD_STOP_DHCP:
    306                     mAlarmManager.cancel(mDhcpRenewalIntent);
    307                     if (!NetworkUtils.stopDhcp(mInterfaceName)) {
    308                         Log.e(TAG, "Failed to stop Dhcp on " + mInterfaceName);
    309                     }
    310                     transitionTo(mStoppedState);
    311                     break;
    312                 case CMD_PRE_DHCP_ACTION_COMPLETE:
    313                     if (runDhcp(DhcpAction.RENEW)) {
    314                        transitionTo(mRunningState);
    315                     } else {
    316                        transitionTo(mStoppedState);
    317                     }
    318                     break;
    319                 case CMD_START_DHCP:
    320                     //ignore
    321                     break;
    322                 default:
    323                     retValue = NOT_HANDLED;
    324                     break;
    325             }
    326             return retValue;
    327         }
    328         @Override
    329         public void exit() {
    330             mDhcpRenewWakeLock.release();
    331         }
    332     }
    333 
    334     private boolean runDhcp(DhcpAction dhcpAction) {
    335         boolean success = false;
    336         DhcpInfoInternal dhcpInfoInternal = new DhcpInfoInternal();
    337 
    338         if (dhcpAction == DhcpAction.START) {
    339             if (DBG) Log.d(TAG, "DHCP request on " + mInterfaceName);
    340             success = NetworkUtils.runDhcp(mInterfaceName, dhcpInfoInternal);
    341             mDhcpInfo = dhcpInfoInternal;
    342         } else if (dhcpAction == DhcpAction.RENEW) {
    343             if (DBG) Log.d(TAG, "DHCP renewal on " + mInterfaceName);
    344             success = NetworkUtils.runDhcpRenew(mInterfaceName, dhcpInfoInternal);
    345             dhcpInfoInternal.updateFromDhcpRequest(mDhcpInfo);
    346         }
    347 
    348         if (success) {
    349             if (DBG) Log.d(TAG, "DHCP succeeded on " + mInterfaceName);
    350            long leaseDuration = dhcpInfoInternal.leaseDuration; //int to long conversion
    351 
    352            //Sanity check for renewal
    353            //TODO: would be good to notify the user that his network configuration is
    354            //bad and that the device cannot renew below MIN_RENEWAL_TIME_SECS
    355            if (leaseDuration < MIN_RENEWAL_TIME_SECS) {
    356                leaseDuration = MIN_RENEWAL_TIME_SECS;
    357            }
    358            //Do it a bit earlier than half the lease duration time
    359            //to beat the native DHCP client and avoid extra packets
    360            //48% for one hour lease time = 29 minutes
    361            mAlarmManager.set(AlarmManager.ELAPSED_REALTIME_WAKEUP,
    362                    SystemClock.elapsedRealtime() +
    363                    leaseDuration * 480, //in milliseconds
    364                    mDhcpRenewalIntent);
    365 
    366             mController.obtainMessage(CMD_POST_DHCP_ACTION, DHCP_SUCCESS, 0, dhcpInfoInternal)
    367                 .sendToTarget();
    368         } else {
    369             Log.e(TAG, "DHCP failed on " + mInterfaceName + ": " +
    370                     NetworkUtils.getDhcpError());
    371             NetworkUtils.stopDhcp(mInterfaceName);
    372             mController.obtainMessage(CMD_POST_DHCP_ACTION, DHCP_FAILURE, 0)
    373                 .sendToTarget();
    374         }
    375         return success;
    376     }
    377 }
    378