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.setExact(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