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.DhcpResults;
     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 DhcpResults mDhcpResults;
     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 final 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     /* Notification from DHCP state machine before quitting */
     96     public static final int CMD_ON_QUIT                     = BASE + 6;
     97 
     98     /* Command from controller to indicate DHCP discovery/renewal can continue
     99      * after pre DHCP action is complete */
    100     public static final int CMD_PRE_DHCP_ACTION_COMPLETE    = BASE + 7;
    101 
    102     /* Message.arg1 arguments to CMD_POST_DHCP notification */
    103     public static final int DHCP_SUCCESS = 1;
    104     public static final int DHCP_FAILURE = 2;
    105 
    106     private State mDefaultState = new DefaultState();
    107     private State mStoppedState = new StoppedState();
    108     private State mWaitBeforeStartState = new WaitBeforeStartState();
    109     private State mRunningState = new RunningState();
    110     private State mWaitBeforeRenewalState = new WaitBeforeRenewalState();
    111 
    112     private DhcpStateMachine(Context context, StateMachine controller, String intf) {
    113         super(TAG);
    114 
    115         mContext = context;
    116         mController = controller;
    117         mInterfaceName = intf;
    118 
    119         mAlarmManager = (AlarmManager)mContext.getSystemService(Context.ALARM_SERVICE);
    120         Intent dhcpRenewalIntent = new Intent(ACTION_DHCP_RENEW, null);
    121         mDhcpRenewalIntent = PendingIntent.getBroadcast(mContext, DHCP_RENEW, dhcpRenewalIntent, 0);
    122 
    123         PowerManager powerManager = (PowerManager)mContext.getSystemService(Context.POWER_SERVICE);
    124         mDhcpRenewWakeLock = powerManager.newWakeLock(PowerManager.PARTIAL_WAKE_LOCK, WAKELOCK_TAG);
    125         mDhcpRenewWakeLock.setReferenceCounted(false);
    126 
    127         mBroadcastReceiver = new BroadcastReceiver() {
    128             @Override
    129             public void onReceive(Context context, Intent intent) {
    130                 //DHCP renew
    131                 if (DBG) Log.d(TAG, "Sending a DHCP renewal " + this);
    132                 //Lock released after 40s in worst case scenario
    133                 mDhcpRenewWakeLock.acquire(40000);
    134                 sendMessage(CMD_RENEW_DHCP);
    135             }
    136         };
    137         mContext.registerReceiver(mBroadcastReceiver, new IntentFilter(ACTION_DHCP_RENEW));
    138 
    139         addState(mDefaultState);
    140             addState(mStoppedState, mDefaultState);
    141             addState(mWaitBeforeStartState, mDefaultState);
    142             addState(mRunningState, mDefaultState);
    143             addState(mWaitBeforeRenewalState, mDefaultState);
    144 
    145         setInitialState(mStoppedState);
    146     }
    147 
    148     public static DhcpStateMachine makeDhcpStateMachine(Context context, StateMachine controller,
    149             String intf) {
    150         DhcpStateMachine dsm = new DhcpStateMachine(context, controller, intf);
    151         dsm.start();
    152         return dsm;
    153     }
    154 
    155     /**
    156      * This sends a notification right before DHCP request/renewal so that the
    157      * controller can do certain actions before DHCP packets are sent out.
    158      * When the controller is ready, it sends a CMD_PRE_DHCP_ACTION_COMPLETE message
    159      * to indicate DHCP can continue
    160      *
    161      * This is used by Wifi at this time for the purpose of doing BT-Wifi coex
    162      * handling during Dhcp
    163      */
    164     public void registerForPreDhcpNotification() {
    165         mRegisteredForPreDhcpNotification = true;
    166     }
    167 
    168     /**
    169      * Quit the DhcpStateMachine.
    170      *
    171      * @hide
    172      */
    173     public void doQuit() {
    174         quit();
    175     }
    176 
    177     protected void onQuitting() {
    178         mController.sendMessage(CMD_ON_QUIT);
    179     }
    180 
    181     class DefaultState extends State {
    182         @Override
    183         public void exit() {
    184             mContext.unregisterReceiver(mBroadcastReceiver);
    185         }
    186         @Override
    187         public boolean processMessage(Message message) {
    188             if (DBG) Log.d(TAG, getName() + message.toString() + "\n");
    189             switch (message.what) {
    190                 case CMD_RENEW_DHCP:
    191                     Log.e(TAG, "Error! Failed to handle a DHCP renewal on " + mInterfaceName);
    192                     mDhcpRenewWakeLock.release();
    193                     break;
    194                 default:
    195                     Log.e(TAG, "Error! unhandled message  " + message);
    196                     break;
    197             }
    198             return HANDLED;
    199         }
    200     }
    201 
    202 
    203     class StoppedState extends State {
    204         @Override
    205         public void enter() {
    206             if (DBG) Log.d(TAG, getName() + "\n");
    207         }
    208 
    209         @Override
    210         public boolean processMessage(Message message) {
    211             boolean retValue = HANDLED;
    212             if (DBG) Log.d(TAG, getName() + message.toString() + "\n");
    213             switch (message.what) {
    214                 case CMD_START_DHCP:
    215                     if (mRegisteredForPreDhcpNotification) {
    216                         /* Notify controller before starting DHCP */
    217                         mController.sendMessage(CMD_PRE_DHCP_ACTION);
    218                         transitionTo(mWaitBeforeStartState);
    219                     } else {
    220                         if (runDhcp(DhcpAction.START)) {
    221                             transitionTo(mRunningState);
    222                         }
    223                     }
    224                     break;
    225                 case CMD_STOP_DHCP:
    226                     //ignore
    227                     break;
    228                 default:
    229                     retValue = NOT_HANDLED;
    230                     break;
    231             }
    232             return retValue;
    233         }
    234     }
    235 
    236     class WaitBeforeStartState extends State {
    237         @Override
    238         public void enter() {
    239             if (DBG) Log.d(TAG, getName() + "\n");
    240         }
    241 
    242         @Override
    243         public boolean processMessage(Message message) {
    244             boolean retValue = HANDLED;
    245             if (DBG) Log.d(TAG, getName() + message.toString() + "\n");
    246             switch (message.what) {
    247                 case CMD_PRE_DHCP_ACTION_COMPLETE:
    248                     if (runDhcp(DhcpAction.START)) {
    249                         transitionTo(mRunningState);
    250                     } else {
    251                         transitionTo(mStoppedState);
    252                     }
    253                     break;
    254                 case CMD_STOP_DHCP:
    255                     transitionTo(mStoppedState);
    256                     break;
    257                 case CMD_START_DHCP:
    258                     //ignore
    259                     break;
    260                 default:
    261                     retValue = NOT_HANDLED;
    262                     break;
    263             }
    264             return retValue;
    265         }
    266     }
    267 
    268     class RunningState extends State {
    269         @Override
    270         public void enter() {
    271             if (DBG) Log.d(TAG, getName() + "\n");
    272         }
    273 
    274         @Override
    275         public boolean processMessage(Message message) {
    276             boolean retValue = HANDLED;
    277             if (DBG) Log.d(TAG, getName() + message.toString() + "\n");
    278             switch (message.what) {
    279                 case CMD_STOP_DHCP:
    280                     mAlarmManager.cancel(mDhcpRenewalIntent);
    281                     if (!NetworkUtils.stopDhcp(mInterfaceName)) {
    282                         Log.e(TAG, "Failed to stop Dhcp on " + mInterfaceName);
    283                     }
    284                     transitionTo(mStoppedState);
    285                     break;
    286                 case CMD_RENEW_DHCP:
    287                     if (mRegisteredForPreDhcpNotification) {
    288                         /* Notify controller before starting DHCP */
    289                         mController.sendMessage(CMD_PRE_DHCP_ACTION);
    290                         transitionTo(mWaitBeforeRenewalState);
    291                         //mDhcpRenewWakeLock is released in WaitBeforeRenewalState
    292                     } else {
    293                         if (!runDhcp(DhcpAction.RENEW)) {
    294                             transitionTo(mStoppedState);
    295                         }
    296                         mDhcpRenewWakeLock.release();
    297                     }
    298                     break;
    299                 case CMD_START_DHCP:
    300                     //ignore
    301                     break;
    302                 default:
    303                     retValue = NOT_HANDLED;
    304             }
    305             return retValue;
    306         }
    307     }
    308 
    309     class WaitBeforeRenewalState extends State {
    310         @Override
    311         public void enter() {
    312             if (DBG) Log.d(TAG, getName() + "\n");
    313         }
    314 
    315         @Override
    316         public boolean processMessage(Message message) {
    317             boolean retValue = HANDLED;
    318             if (DBG) Log.d(TAG, getName() + message.toString() + "\n");
    319             switch (message.what) {
    320                 case CMD_STOP_DHCP:
    321                     mAlarmManager.cancel(mDhcpRenewalIntent);
    322                     if (!NetworkUtils.stopDhcp(mInterfaceName)) {
    323                         Log.e(TAG, "Failed to stop Dhcp on " + mInterfaceName);
    324                     }
    325                     transitionTo(mStoppedState);
    326                     break;
    327                 case CMD_PRE_DHCP_ACTION_COMPLETE:
    328                     if (runDhcp(DhcpAction.RENEW)) {
    329                        transitionTo(mRunningState);
    330                     } else {
    331                        transitionTo(mStoppedState);
    332                     }
    333                     break;
    334                 case CMD_START_DHCP:
    335                     //ignore
    336                     break;
    337                 default:
    338                     retValue = NOT_HANDLED;
    339                     break;
    340             }
    341             return retValue;
    342         }
    343         @Override
    344         public void exit() {
    345             mDhcpRenewWakeLock.release();
    346         }
    347     }
    348 
    349     private boolean runDhcp(DhcpAction dhcpAction) {
    350         boolean success = false;
    351         DhcpResults dhcpResults = new DhcpResults();
    352 
    353         if (dhcpAction == DhcpAction.START) {
    354             /* Stop any existing DHCP daemon before starting new */
    355             NetworkUtils.stopDhcp(mInterfaceName);
    356             if (DBG) Log.d(TAG, "DHCP request on " + mInterfaceName);
    357             success = NetworkUtils.runDhcp(mInterfaceName, dhcpResults);
    358         } else if (dhcpAction == DhcpAction.RENEW) {
    359             if (DBG) Log.d(TAG, "DHCP renewal on " + mInterfaceName);
    360             success = NetworkUtils.runDhcpRenew(mInterfaceName, dhcpResults);
    361             if (success) dhcpResults.updateFromDhcpRequest(mDhcpResults);
    362         }
    363         if (success) {
    364             if (DBG) Log.d(TAG, "DHCP succeeded on " + mInterfaceName);
    365             long leaseDuration = dhcpResults.leaseDuration; //int to long conversion
    366 
    367             //Sanity check for renewal
    368             if (leaseDuration >= 0) {
    369                 //TODO: would be good to notify the user that his network configuration is
    370                 //bad and that the device cannot renew below MIN_RENEWAL_TIME_SECS
    371                 if (leaseDuration < MIN_RENEWAL_TIME_SECS) {
    372                     leaseDuration = MIN_RENEWAL_TIME_SECS;
    373                 }
    374                 //Do it a bit earlier than half the lease duration time
    375                 //to beat the native DHCP client and avoid extra packets
    376                 //48% for one hour lease time = 29 minutes
    377                 mAlarmManager.set(AlarmManager.ELAPSED_REALTIME_WAKEUP,
    378                         SystemClock.elapsedRealtime() +
    379                         leaseDuration * 480, //in milliseconds
    380                         mDhcpRenewalIntent);
    381             } else {
    382                 //infinite lease time, no renewal needed
    383             }
    384 
    385             mDhcpResults = dhcpResults;
    386             mController.obtainMessage(CMD_POST_DHCP_ACTION, DHCP_SUCCESS, 0, dhcpResults)
    387                 .sendToTarget();
    388         } else {
    389             Log.e(TAG, "DHCP failed on " + mInterfaceName + ": " +
    390                     NetworkUtils.getDhcpError());
    391             NetworkUtils.stopDhcp(mInterfaceName);
    392             mController.obtainMessage(CMD_POST_DHCP_ACTION, DHCP_FAILURE, 0)
    393                 .sendToTarget();
    394         }
    395         return success;
    396     }
    397 }
    398