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 DhcpStateMachine 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 BaseDhcpStateMachine {
     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 final String mInterfaceName;
     76     private boolean mRegisteredForPreDhcpNotification = false;
     77 
     78     private static final int BASE = Protocol.BASE_DHCP;
     79 
     80     /* Commands from controller to start/stop DHCP */
     81     public static final int CMD_START_DHCP                  = BASE + 1;
     82     public static final int CMD_STOP_DHCP                   = BASE + 2;
     83     public static final int CMD_RENEW_DHCP                  = BASE + 3;
     84 
     85     /* Notification from DHCP state machine prior to DHCP discovery/renewal */
     86     public static final int CMD_PRE_DHCP_ACTION             = BASE + 4;
     87     /* Notification from DHCP state machine post DHCP discovery/renewal. Indicates
     88      * success/failure */
     89     public static final int CMD_POST_DHCP_ACTION            = BASE + 5;
     90     /* Notification from DHCP state machine before quitting */
     91     public static final int CMD_ON_QUIT                     = BASE + 6;
     92 
     93     /* Command from controller to indicate DHCP discovery/renewal can continue
     94      * after pre DHCP action is complete */
     95     public static final int CMD_PRE_DHCP_ACTION_COMPLETE    = BASE + 7;
     96 
     97     /* Command from ourselves to see if DHCP results are available */
     98     private static final int CMD_GET_DHCP_RESULTS           = BASE + 8;
     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     private State mPollingState = new PollingState();
    110 
    111     private DhcpStateMachine(Context context, StateMachine controller, String intf) {
    112         super(TAG);
    113 
    114         mContext = context;
    115         mController = controller;
    116         mInterfaceName = intf;
    117 
    118         mAlarmManager = (AlarmManager)mContext.getSystemService(Context.ALARM_SERVICE);
    119         Intent dhcpRenewalIntent = new Intent(ACTION_DHCP_RENEW, null);
    120         mDhcpRenewalIntent = PendingIntent.getBroadcast(mContext, DHCP_RENEW, dhcpRenewalIntent, 0);
    121 
    122         PowerManager powerManager = (PowerManager)mContext.getSystemService(Context.POWER_SERVICE);
    123         mDhcpRenewWakeLock = powerManager.newWakeLock(PowerManager.PARTIAL_WAKE_LOCK, WAKELOCK_TAG);
    124         mDhcpRenewWakeLock.setReferenceCounted(false);
    125 
    126         mBroadcastReceiver = new BroadcastReceiver() {
    127             @Override
    128             public void onReceive(Context context, Intent intent) {
    129                 //DHCP renew
    130                 if (DBG) Log.d(TAG, "Sending a DHCP renewal " + this);
    131                 //Lock released after 40s in worst case scenario
    132                 mDhcpRenewWakeLock.acquire(40000);
    133                 sendMessage(CMD_RENEW_DHCP);
    134             }
    135         };
    136         mContext.registerReceiver(mBroadcastReceiver, new IntentFilter(ACTION_DHCP_RENEW));
    137 
    138         addState(mDefaultState);
    139             addState(mStoppedState, mDefaultState);
    140             addState(mWaitBeforeStartState, mDefaultState);
    141             addState(mPollingState, 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     @Override
    165     public void registerForPreDhcpNotification() {
    166         mRegisteredForPreDhcpNotification = true;
    167     }
    168 
    169     /**
    170      * Quit the DhcpStateMachine.
    171      *
    172      * @hide
    173      */
    174     @Override
    175     public void doQuit() {
    176         quit();
    177     }
    178 
    179     protected void onQuitting() {
    180         mController.sendMessage(CMD_ON_QUIT);
    181     }
    182 
    183     class DefaultState extends State {
    184         @Override
    185         public void exit() {
    186             mContext.unregisterReceiver(mBroadcastReceiver);
    187         }
    188         @Override
    189         public boolean processMessage(Message message) {
    190             if (DBG) Log.d(TAG, getName() + message.toString() + "\n");
    191             switch (message.what) {
    192                 case CMD_RENEW_DHCP:
    193                     Log.e(TAG, "Error! Failed to handle a DHCP renewal on " + mInterfaceName);
    194                     mDhcpRenewWakeLock.release();
    195                     break;
    196                 default:
    197                     Log.e(TAG, "Error! unhandled message  " + message);
    198                     break;
    199             }
    200             return HANDLED;
    201         }
    202     }
    203 
    204 
    205     class StoppedState extends State {
    206         @Override
    207         public void enter() {
    208             if (DBG) Log.d(TAG, getName() + "\n");
    209             if (!NetworkUtils.stopDhcp(mInterfaceName)) {
    210                 Log.e(TAG, "Failed to stop Dhcp on " + mInterfaceName);
    211             }
    212             mDhcpResults = null;
    213         }
    214 
    215         @Override
    216         public boolean processMessage(Message message) {
    217             boolean retValue = HANDLED;
    218             if (DBG) Log.d(TAG, getName() + message.toString() + "\n");
    219             switch (message.what) {
    220                 case CMD_START_DHCP:
    221                     if (mRegisteredForPreDhcpNotification) {
    222                         /* Notify controller before starting DHCP */
    223                         mController.sendMessage(CMD_PRE_DHCP_ACTION);
    224                         transitionTo(mWaitBeforeStartState);
    225                     } else {
    226                         if (runDhcpStart()) {
    227                             transitionTo(mRunningState);
    228                         }
    229                     }
    230                     break;
    231                 case CMD_STOP_DHCP:
    232                     //ignore
    233                     break;
    234                 default:
    235                     retValue = NOT_HANDLED;
    236                     break;
    237             }
    238             return retValue;
    239         }
    240     }
    241 
    242     class WaitBeforeStartState extends State {
    243         @Override
    244         public void enter() {
    245             if (DBG) Log.d(TAG, getName() + "\n");
    246         }
    247 
    248         @Override
    249         public boolean processMessage(Message message) {
    250             boolean retValue = HANDLED;
    251             if (DBG) Log.d(TAG, getName() + message.toString() + "\n");
    252             switch (message.what) {
    253                 case CMD_PRE_DHCP_ACTION_COMPLETE:
    254                     if (runDhcpStart()) {
    255                         transitionTo(mRunningState);
    256                     } else {
    257                         transitionTo(mPollingState);
    258                     }
    259                     break;
    260                 case CMD_STOP_DHCP:
    261                     transitionTo(mStoppedState);
    262                     break;
    263                 case CMD_START_DHCP:
    264                     //ignore
    265                     break;
    266                 default:
    267                     retValue = NOT_HANDLED;
    268                     break;
    269             }
    270             return retValue;
    271         }
    272     }
    273 
    274     class PollingState extends State {
    275         private static final long MAX_DELAY_SECONDS = 32;
    276         private long delaySeconds;
    277 
    278         private void scheduleNextResultsCheck() {
    279             sendMessageDelayed(obtainMessage(CMD_GET_DHCP_RESULTS), delaySeconds * 1000);
    280             delaySeconds *= 2;
    281             if (delaySeconds > MAX_DELAY_SECONDS) {
    282                 delaySeconds = MAX_DELAY_SECONDS;
    283             }
    284         }
    285 
    286         @Override
    287         public void enter() {
    288             if (DBG) Log.d(TAG, "Entering " + getName() + "\n");
    289             delaySeconds = 1;
    290             scheduleNextResultsCheck();
    291         }
    292 
    293         @Override
    294         public boolean processMessage(Message message) {
    295             boolean retValue = HANDLED;
    296             if (DBG) Log.d(TAG, getName() + message.toString() + "\n");
    297             switch (message.what) {
    298                 case CMD_GET_DHCP_RESULTS:
    299                     if (DBG) Log.d(TAG, "GET_DHCP_RESULTS on " + mInterfaceName);
    300                     if (dhcpSucceeded()) {
    301                         transitionTo(mRunningState);
    302                     } else {
    303                         scheduleNextResultsCheck();
    304                     }
    305                     break;
    306                 case CMD_STOP_DHCP:
    307                     transitionTo(mStoppedState);
    308                     break;
    309                 default:
    310                     retValue = NOT_HANDLED;
    311                     break;
    312             }
    313             return retValue;
    314         }
    315 
    316         @Override
    317         public void exit() {
    318             if (DBG) Log.d(TAG, "Exiting " + getName() + "\n");
    319             removeMessages(CMD_GET_DHCP_RESULTS);
    320         }
    321     }
    322 
    323     class RunningState extends State {
    324         @Override
    325         public void enter() {
    326             if (DBG) Log.d(TAG, getName() + "\n");
    327         }
    328 
    329         @Override
    330         public boolean processMessage(Message message) {
    331             boolean retValue = HANDLED;
    332             if (DBG) Log.d(TAG, getName() + message.toString() + "\n");
    333             switch (message.what) {
    334                 case CMD_STOP_DHCP:
    335                     mAlarmManager.cancel(mDhcpRenewalIntent);
    336                     transitionTo(mStoppedState);
    337                     break;
    338                 case CMD_RENEW_DHCP:
    339                     if (mRegisteredForPreDhcpNotification) {
    340                         /* Notify controller before starting DHCP */
    341                         mController.sendMessage(CMD_PRE_DHCP_ACTION);
    342                         transitionTo(mWaitBeforeRenewalState);
    343                         //mDhcpRenewWakeLock is released in WaitBeforeRenewalState
    344                     } else {
    345                         if (!runDhcpRenew()) {
    346                             transitionTo(mStoppedState);
    347                         }
    348                         mDhcpRenewWakeLock.release();
    349                     }
    350                     break;
    351                 case CMD_START_DHCP:
    352                     //ignore
    353                     break;
    354                 default:
    355                     retValue = NOT_HANDLED;
    356             }
    357             return retValue;
    358         }
    359     }
    360 
    361     class WaitBeforeRenewalState extends State {
    362         @Override
    363         public void enter() {
    364             if (DBG) Log.d(TAG, getName() + "\n");
    365         }
    366 
    367         @Override
    368         public boolean processMessage(Message message) {
    369             boolean retValue = HANDLED;
    370             if (DBG) Log.d(TAG, getName() + message.toString() + "\n");
    371             switch (message.what) {
    372                 case CMD_STOP_DHCP:
    373                     mAlarmManager.cancel(mDhcpRenewalIntent);
    374                     transitionTo(mStoppedState);
    375                     break;
    376                 case CMD_PRE_DHCP_ACTION_COMPLETE:
    377                     if (runDhcpRenew()) {
    378                        transitionTo(mRunningState);
    379                     } else {
    380                        transitionTo(mStoppedState);
    381                     }
    382                     break;
    383                 case CMD_START_DHCP:
    384                     //ignore
    385                     break;
    386                 default:
    387                     retValue = NOT_HANDLED;
    388                     break;
    389             }
    390             return retValue;
    391         }
    392         @Override
    393         public void exit() {
    394             mDhcpRenewWakeLock.release();
    395         }
    396     }
    397 
    398     private boolean dhcpSucceeded() {
    399         DhcpResults dhcpResults = new DhcpResults();
    400         if (!NetworkUtils.getDhcpResults(mInterfaceName, dhcpResults)) {
    401             return false;
    402         }
    403 
    404         if (DBG) Log.d(TAG, "DHCP results found for " + mInterfaceName);
    405         long leaseDuration = dhcpResults.leaseDuration; //int to long conversion
    406 
    407         //Sanity check for renewal
    408         if (leaseDuration >= 0) {
    409             //TODO: would be good to notify the user that his network configuration is
    410             //bad and that the device cannot renew below MIN_RENEWAL_TIME_SECS
    411             if (leaseDuration < MIN_RENEWAL_TIME_SECS) {
    412                 leaseDuration = MIN_RENEWAL_TIME_SECS;
    413             }
    414             //Do it a bit earlier than half the lease duration time
    415             //to beat the native DHCP client and avoid extra packets
    416             //48% for one hour lease time = 29 minutes
    417             mAlarmManager.setExact(AlarmManager.ELAPSED_REALTIME_WAKEUP,
    418                     SystemClock.elapsedRealtime() +
    419                     leaseDuration * 480, //in milliseconds
    420                     mDhcpRenewalIntent);
    421         } else {
    422             //infinite lease time, no renewal needed
    423         }
    424 
    425         // Fill in any missing fields in dhcpResults from the previous results.
    426         // If mDhcpResults is null (i.e. this is the first server response),
    427         // this is a noop.
    428         dhcpResults.updateFromDhcpRequest(mDhcpResults);
    429         mDhcpResults = dhcpResults;
    430         mController.obtainMessage(CMD_POST_DHCP_ACTION, DHCP_SUCCESS, 0, dhcpResults)
    431             .sendToTarget();
    432         return true;
    433     }
    434 
    435     private boolean runDhcpStart() {
    436         /* Stop any existing DHCP daemon before starting new */
    437         NetworkUtils.stopDhcp(mInterfaceName);
    438         mDhcpResults = null;
    439 
    440         if (DBG) Log.d(TAG, "DHCP request on " + mInterfaceName);
    441         if (!NetworkUtils.startDhcp(mInterfaceName) || !dhcpSucceeded()) {
    442             Log.e(TAG, "DHCP request failed on " + mInterfaceName + ": " +
    443                     NetworkUtils.getDhcpError());
    444             mController.obtainMessage(CMD_POST_DHCP_ACTION, DHCP_FAILURE, 0)
    445                     .sendToTarget();
    446             return false;
    447         }
    448         return true;
    449     }
    450 
    451     private boolean runDhcpRenew() {
    452         if (DBG) Log.d(TAG, "DHCP renewal on " + mInterfaceName);
    453         if (!NetworkUtils.startDhcpRenew(mInterfaceName) || !dhcpSucceeded()) {
    454             Log.e(TAG, "DHCP renew failed on " + mInterfaceName + ": " +
    455                     NetworkUtils.getDhcpError());
    456             mController.obtainMessage(CMD_POST_DHCP_ACTION, DHCP_FAILURE, 0)
    457                     .sendToTarget();
    458             return false;
    459         }
    460         return true;
    461     }
    462 }
    463