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    /* 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        DhcpInfoInternal dhcpInfoInternal = new DhcpInfoInternal();
352
353        if (dhcpAction == DhcpAction.START) {
354            if (DBG) Log.d(TAG, "DHCP request on " + mInterfaceName);
355            success = NetworkUtils.runDhcp(mInterfaceName, dhcpInfoInternal);
356            mDhcpInfo = dhcpInfoInternal;
357        } else if (dhcpAction == DhcpAction.RENEW) {
358            if (DBG) Log.d(TAG, "DHCP renewal on " + mInterfaceName);
359            success = NetworkUtils.runDhcpRenew(mInterfaceName, dhcpInfoInternal);
360            dhcpInfoInternal.updateFromDhcpRequest(mDhcpInfo);
361        }
362
363        if (success) {
364            if (DBG) Log.d(TAG, "DHCP succeeded on " + mInterfaceName);
365            long leaseDuration = dhcpInfoInternal.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.set(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            mController.obtainMessage(CMD_POST_DHCP_ACTION, DHCP_SUCCESS, 0, dhcpInfoInternal)
386                .sendToTarget();
387        } else {
388            Log.e(TAG, "DHCP failed on " + mInterfaceName + ": " +
389                    NetworkUtils.getDhcpError());
390            NetworkUtils.stopDhcp(mInterfaceName);
391            mController.obtainMessage(CMD_POST_DHCP_ACTION, DHCP_FAILURE, 0)
392                .sendToTarget();
393        }
394        return success;
395    }
396}
397