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 DhcpStateMachine(Context context, StateMachine controller, String intf)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 makeDhcpStateMachine(Context context, StateMachine controller, String intf)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 registerForPreDhcpNotification()165 public void registerForPreDhcpNotification() { 166 mRegisteredForPreDhcpNotification = true; 167 } 168 169 /** 170 * Quit the DhcpStateMachine. 171 * 172 * @hide 173 */ 174 @Override doQuit()175 public void doQuit() { 176 quit(); 177 } 178 onQuitting()179 protected void onQuitting() { 180 mController.sendMessage(CMD_ON_QUIT); 181 } 182 183 class DefaultState extends State { 184 @Override exit()185 public void exit() { 186 mContext.unregisterReceiver(mBroadcastReceiver); 187 } 188 @Override processMessage(Message message)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 enter()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 processMessage(Message message)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 enter()244 public void enter() { 245 if (DBG) Log.d(TAG, getName() + "\n"); 246 } 247 248 @Override processMessage(Message message)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 scheduleNextResultsCheck()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 enter()287 public void enter() { 288 if (DBG) Log.d(TAG, "Entering " + getName() + "\n"); 289 delaySeconds = 1; 290 scheduleNextResultsCheck(); 291 } 292 293 @Override processMessage(Message message)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 exit()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 enter()325 public void enter() { 326 if (DBG) Log.d(TAG, getName() + "\n"); 327 } 328 329 @Override processMessage(Message message)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 enter()363 public void enter() { 364 if (DBG) Log.d(TAG, getName() + "\n"); 365 } 366 367 @Override processMessage(Message message)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 exit()393 public void exit() { 394 mDhcpRenewWakeLock.release(); 395 } 396 } 397 dhcpSucceeded()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 runDhcpStart()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 runDhcpRenew()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