/* * Copyright (C) 2011 The Android Open Source Project * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. * You may obtain a copy of the License at * * http://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, software * distributed under the License is distributed on an "AS IS" BASIS, * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. * See the License for the specific language governing permissions and * limitations under the License. */ package android.net; import com.android.internal.util.Protocol; import com.android.internal.util.State; import com.android.internal.util.StateMachine; import android.app.AlarmManager; import android.app.PendingIntent; import android.content.BroadcastReceiver; import android.content.Context; import android.content.Intent; import android.content.IntentFilter; import android.net.DhcpInfoInternal; import android.net.NetworkUtils; import android.os.Message; import android.os.PowerManager; import android.os.SystemClock; import android.util.Log; /** * StateMachine that interacts with the native DHCP client and can talk to * a controller that also needs to be a StateMachine * * The Dhcp state machine provides the following features: * - Wakeup and renewal using the native DHCP client (which will not renew * on its own when the device is in suspend state and this can lead to device * holding IP address beyond expiry) * - A notification right before DHCP request or renewal is started. This * can be used for any additional setup before DHCP. For example, wifi sets * BT-Wifi coex settings right before DHCP is initiated * * @hide */ public class DhcpStateMachine extends StateMachine { private static final String TAG = "DhcpStateMachine"; private static final boolean DBG = false; /* A StateMachine that controls the DhcpStateMachine */ private StateMachine mController; private Context mContext; private BroadcastReceiver mBroadcastReceiver; private AlarmManager mAlarmManager; private PendingIntent mDhcpRenewalIntent; private PowerManager.WakeLock mDhcpRenewWakeLock; private static final String WAKELOCK_TAG = "DHCP"; //Remember DHCP configuration from first request private DhcpInfoInternal mDhcpInfo; private static final int DHCP_RENEW = 0; private static final String ACTION_DHCP_RENEW = "android.net.wifi.DHCP_RENEW"; //Used for sanity check on setting up renewal private static final int MIN_RENEWAL_TIME_SECS = 5 * 60; // 5 minutes private enum DhcpAction { START, RENEW }; private String mInterfaceName; private boolean mRegisteredForPreDhcpNotification = false; private static final int BASE = Protocol.BASE_DHCP; /* Commands from controller to start/stop DHCP */ public static final int CMD_START_DHCP = BASE + 1; public static final int CMD_STOP_DHCP = BASE + 2; public static final int CMD_RENEW_DHCP = BASE + 3; /* Notification from DHCP state machine prior to DHCP discovery/renewal */ public static final int CMD_PRE_DHCP_ACTION = BASE + 4; /* Notification from DHCP state machine post DHCP discovery/renewal. Indicates * success/failure */ public static final int CMD_POST_DHCP_ACTION = BASE + 5; /* Notification from DHCP state machine before quitting */ public static final int CMD_ON_QUIT = BASE + 6; /* Command from controller to indicate DHCP discovery/renewal can continue * after pre DHCP action is complete */ public static final int CMD_PRE_DHCP_ACTION_COMPLETE = BASE + 7; /* Message.arg1 arguments to CMD_POST_DHCP notification */ public static final int DHCP_SUCCESS = 1; public static final int DHCP_FAILURE = 2; private State mDefaultState = new DefaultState(); private State mStoppedState = new StoppedState(); private State mWaitBeforeStartState = new WaitBeforeStartState(); private State mRunningState = new RunningState(); private State mWaitBeforeRenewalState = new WaitBeforeRenewalState(); private DhcpStateMachine(Context context, StateMachine controller, String intf) { super(TAG); mContext = context; mController = controller; mInterfaceName = intf; mAlarmManager = (AlarmManager)mContext.getSystemService(Context.ALARM_SERVICE); Intent dhcpRenewalIntent = new Intent(ACTION_DHCP_RENEW, null); mDhcpRenewalIntent = PendingIntent.getBroadcast(mContext, DHCP_RENEW, dhcpRenewalIntent, 0); PowerManager powerManager = (PowerManager)mContext.getSystemService(Context.POWER_SERVICE); mDhcpRenewWakeLock = powerManager.newWakeLock(PowerManager.PARTIAL_WAKE_LOCK, WAKELOCK_TAG); mDhcpRenewWakeLock.setReferenceCounted(false); mBroadcastReceiver = new BroadcastReceiver() { @Override public void onReceive(Context context, Intent intent) { //DHCP renew if (DBG) Log.d(TAG, "Sending a DHCP renewal " + this); //Lock released after 40s in worst case scenario mDhcpRenewWakeLock.acquire(40000); sendMessage(CMD_RENEW_DHCP); } }; mContext.registerReceiver(mBroadcastReceiver, new IntentFilter(ACTION_DHCP_RENEW)); addState(mDefaultState); addState(mStoppedState, mDefaultState); addState(mWaitBeforeStartState, mDefaultState); addState(mRunningState, mDefaultState); addState(mWaitBeforeRenewalState, mDefaultState); setInitialState(mStoppedState); } public static DhcpStateMachine makeDhcpStateMachine(Context context, StateMachine controller, String intf) { DhcpStateMachine dsm = new DhcpStateMachine(context, controller, intf); dsm.start(); return dsm; } /** * This sends a notification right before DHCP request/renewal so that the * controller can do certain actions before DHCP packets are sent out. * When the controller is ready, it sends a CMD_PRE_DHCP_ACTION_COMPLETE message * to indicate DHCP can continue * * This is used by Wifi at this time for the purpose of doing BT-Wifi coex * handling during Dhcp */ public void registerForPreDhcpNotification() { mRegisteredForPreDhcpNotification = true; } /** * Quit the DhcpStateMachine. * * @hide */ public void doQuit() { quit(); } protected void onQuitting() { mController.sendMessage(CMD_ON_QUIT); } class DefaultState extends State { @Override public void exit() { mContext.unregisterReceiver(mBroadcastReceiver); } @Override public boolean processMessage(Message message) { if (DBG) Log.d(TAG, getName() + message.toString() + "\n"); switch (message.what) { case CMD_RENEW_DHCP: Log.e(TAG, "Error! Failed to handle a DHCP renewal on " + mInterfaceName); mDhcpRenewWakeLock.release(); break; default: Log.e(TAG, "Error! unhandled message " + message); break; } return HANDLED; } } class StoppedState extends State { @Override public void enter() { if (DBG) Log.d(TAG, getName() + "\n"); } @Override public boolean processMessage(Message message) { boolean retValue = HANDLED; if (DBG) Log.d(TAG, getName() + message.toString() + "\n"); switch (message.what) { case CMD_START_DHCP: if (mRegisteredForPreDhcpNotification) { /* Notify controller before starting DHCP */ mController.sendMessage(CMD_PRE_DHCP_ACTION); transitionTo(mWaitBeforeStartState); } else { if (runDhcp(DhcpAction.START)) { transitionTo(mRunningState); } } break; case CMD_STOP_DHCP: //ignore break; default: retValue = NOT_HANDLED; break; } return retValue; } } class WaitBeforeStartState extends State { @Override public void enter() { if (DBG) Log.d(TAG, getName() + "\n"); } @Override public boolean processMessage(Message message) { boolean retValue = HANDLED; if (DBG) Log.d(TAG, getName() + message.toString() + "\n"); switch (message.what) { case CMD_PRE_DHCP_ACTION_COMPLETE: if (runDhcp(DhcpAction.START)) { transitionTo(mRunningState); } else { transitionTo(mStoppedState); } break; case CMD_STOP_DHCP: transitionTo(mStoppedState); break; case CMD_START_DHCP: //ignore break; default: retValue = NOT_HANDLED; break; } return retValue; } } class RunningState extends State { @Override public void enter() { if (DBG) Log.d(TAG, getName() + "\n"); } @Override public boolean processMessage(Message message) { boolean retValue = HANDLED; if (DBG) Log.d(TAG, getName() + message.toString() + "\n"); switch (message.what) { case CMD_STOP_DHCP: mAlarmManager.cancel(mDhcpRenewalIntent); if (!NetworkUtils.stopDhcp(mInterfaceName)) { Log.e(TAG, "Failed to stop Dhcp on " + mInterfaceName); } transitionTo(mStoppedState); break; case CMD_RENEW_DHCP: if (mRegisteredForPreDhcpNotification) { /* Notify controller before starting DHCP */ mController.sendMessage(CMD_PRE_DHCP_ACTION); transitionTo(mWaitBeforeRenewalState); //mDhcpRenewWakeLock is released in WaitBeforeRenewalState } else { if (!runDhcp(DhcpAction.RENEW)) { transitionTo(mStoppedState); } mDhcpRenewWakeLock.release(); } break; case CMD_START_DHCP: //ignore break; default: retValue = NOT_HANDLED; } return retValue; } } class WaitBeforeRenewalState extends State { @Override public void enter() { if (DBG) Log.d(TAG, getName() + "\n"); } @Override public boolean processMessage(Message message) { boolean retValue = HANDLED; if (DBG) Log.d(TAG, getName() + message.toString() + "\n"); switch (message.what) { case CMD_STOP_DHCP: mAlarmManager.cancel(mDhcpRenewalIntent); if (!NetworkUtils.stopDhcp(mInterfaceName)) { Log.e(TAG, "Failed to stop Dhcp on " + mInterfaceName); } transitionTo(mStoppedState); break; case CMD_PRE_DHCP_ACTION_COMPLETE: if (runDhcp(DhcpAction.RENEW)) { transitionTo(mRunningState); } else { transitionTo(mStoppedState); } break; case CMD_START_DHCP: //ignore break; default: retValue = NOT_HANDLED; break; } return retValue; } @Override public void exit() { mDhcpRenewWakeLock.release(); } } private boolean runDhcp(DhcpAction dhcpAction) { boolean success = false; DhcpInfoInternal dhcpInfoInternal = new DhcpInfoInternal(); if (dhcpAction == DhcpAction.START) { /* Stop any existing DHCP daemon before starting new */ NetworkUtils.stopDhcp(mInterfaceName); if (DBG) Log.d(TAG, "DHCP request on " + mInterfaceName); success = NetworkUtils.runDhcp(mInterfaceName, dhcpInfoInternal); mDhcpInfo = dhcpInfoInternal; } else if (dhcpAction == DhcpAction.RENEW) { if (DBG) Log.d(TAG, "DHCP renewal on " + mInterfaceName); success = NetworkUtils.runDhcpRenew(mInterfaceName, dhcpInfoInternal); dhcpInfoInternal.updateFromDhcpRequest(mDhcpInfo); } if (success) { if (DBG) Log.d(TAG, "DHCP succeeded on " + mInterfaceName); long leaseDuration = dhcpInfoInternal.leaseDuration; //int to long conversion //Sanity check for renewal if (leaseDuration >= 0) { //TODO: would be good to notify the user that his network configuration is //bad and that the device cannot renew below MIN_RENEWAL_TIME_SECS if (leaseDuration < MIN_RENEWAL_TIME_SECS) { leaseDuration = MIN_RENEWAL_TIME_SECS; } //Do it a bit earlier than half the lease duration time //to beat the native DHCP client and avoid extra packets //48% for one hour lease time = 29 minutes mAlarmManager.set(AlarmManager.ELAPSED_REALTIME_WAKEUP, SystemClock.elapsedRealtime() + leaseDuration * 480, //in milliseconds mDhcpRenewalIntent); } else { //infinite lease time, no renewal needed } mController.obtainMessage(CMD_POST_DHCP_ACTION, DHCP_SUCCESS, 0, dhcpInfoInternal) .sendToTarget(); } else { Log.e(TAG, "DHCP failed on " + mInterfaceName + ": " + NetworkUtils.getDhcpError()); NetworkUtils.stopDhcp(mInterfaceName); mController.obtainMessage(CMD_POST_DHCP_ACTION, DHCP_FAILURE, 0) .sendToTarget(); } return success; } }