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