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