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