]>
Commit | Line | Data |
---|---|---|
d1220566 | 1 | /* |
59693d6c | 2 | * Copyright (C) 2012-2017 Tobias Brunner |
19ef2aec TB |
3 | * |
4 | * Copyright (C) secunet Security Networks AG | |
d1220566 TB |
5 | * |
6 | * This program is free software; you can redistribute it and/or modify it | |
7 | * under the terms of the GNU General Public License as published by the | |
8 | * Free Software Foundation; either version 2 of the License, or (at your | |
9 | * option) any later version. See <http://www.fsf.org/copyleft/gpl.txt>. | |
10 | * | |
11 | * This program is distributed in the hope that it will be useful, but | |
12 | * WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY | |
13 | * or FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License | |
14 | * for more details. | |
15 | */ | |
16 | ||
17 | package org.strongswan.android.logic; | |
18 | ||
d1220566 | 19 | import android.app.Service; |
1b887772 | 20 | import android.content.Context; |
d1220566 TB |
21 | import android.content.Intent; |
22 | import android.os.Binder; | |
063230c2 | 23 | import android.os.Bundle; |
d1220566 TB |
24 | import android.os.Handler; |
25 | import android.os.IBinder; | |
dc351a30 | 26 | import android.os.Looper; |
68afdd34 TB |
27 | import android.os.Message; |
28 | import android.os.SystemClock; | |
d1220566 | 29 | |
a7d679ff | 30 | import org.strongswan.android.R; |
b14507dd | 31 | import org.strongswan.android.data.VpnProfile; |
063230c2 | 32 | import org.strongswan.android.data.VpnProfileDataSource; |
3eda52f0 | 33 | import org.strongswan.android.data.VpnType; |
b14507dd TB |
34 | import org.strongswan.android.logic.imc.ImcState; |
35 | import org.strongswan.android.logic.imc.RemediationInstruction; | |
3eda52f0 | 36 | import org.strongswan.android.ui.VpnProfileControlActivity; |
b14507dd | 37 | |
68afdd34 | 38 | import java.lang.ref.WeakReference; |
b14507dd TB |
39 | import java.util.Collections; |
40 | import java.util.HashSet; | |
41 | import java.util.LinkedList; | |
42 | import java.util.List; | |
43 | import java.util.concurrent.Callable; | |
44 | ||
3b9696fc TB |
45 | import androidx.core.content.ContextCompat; |
46 | ||
d1220566 TB |
47 | public class VpnStateService extends Service |
48 | { | |
b14507dd | 49 | private final HashSet<VpnStateListener> mListeners = new HashSet<VpnStateListener>(); |
d1220566 | 50 | private final IBinder mBinder = new LocalBinder(); |
38172313 | 51 | private long mConnectionID = 0; |
d1220566 TB |
52 | private Handler mHandler; |
53 | private VpnProfile mProfile; | |
54 | private State mState = State.DISABLED; | |
55 | private ErrorState mError = ErrorState.NO_ERROR; | |
dc52cfab | 56 | private ImcState mImcState = ImcState.UNKNOWN; |
a05acd76 | 57 | private final LinkedList<RemediationInstruction> mRemediationInstructions = new LinkedList<RemediationInstruction>(); |
8e3b921a | 58 | private static final long RETRY_INTERVAL = 1000; |
1350ee1e | 59 | /* cap the retry interval at 2 minutes */ |
8e3b921a MP |
60 | private static final long MAX_RETRY_INTERVAL = 120000; |
61 | private static final int RETRY_MSG = 1; | |
62 | private final RetryTimeoutProvider mTimeoutProvider = new RetryTimeoutProvider(); | |
68afdd34 TB |
63 | private long mRetryTimeout; |
64 | private long mRetryIn; | |
d1220566 TB |
65 | |
66 | public enum State | |
67 | { | |
68 | DISABLED, | |
69 | CONNECTING, | |
70 | CONNECTED, | |
71 | DISCONNECTING, | |
72 | } | |
73 | ||
74 | public enum ErrorState | |
75 | { | |
76 | NO_ERROR, | |
77 | AUTH_FAILED, | |
78 | PEER_AUTH_FAILED, | |
79 | LOOKUP_FAILED, | |
80 | UNREACHABLE, | |
81 | GENERIC_ERROR, | |
f0b3e303 | 82 | PASSWORD_MISSING, |
ab5dbbc4 | 83 | CERTIFICATE_UNAVAILABLE, |
d1220566 TB |
84 | } |
85 | ||
86 | /** | |
87 | * Listener interface for bound clients that are interested in changes to | |
88 | * this Service. | |
89 | */ | |
90 | public interface VpnStateListener | |
91 | { | |
8e3b921a | 92 | void stateChanged(); |
d1220566 TB |
93 | } |
94 | ||
95 | /** | |
96 | * Simple Binder that allows to directly access this Service class itself | |
97 | * after binding to it. | |
98 | */ | |
99 | public class LocalBinder extends Binder | |
100 | { | |
101 | public VpnStateService getService() | |
102 | { | |
103 | return VpnStateService.this; | |
104 | } | |
105 | } | |
106 | ||
107 | @Override | |
108 | public void onCreate() | |
109 | { | |
110 | /* this handler allows us to notify listeners from the UI thread and | |
111 | * not from the threads that actually report any state changes */ | |
dc351a30 | 112 | mHandler = new RetryHandler(getMainLooper(), this); |
d1220566 TB |
113 | } |
114 | ||
115 | @Override | |
116 | public IBinder onBind(Intent intent) | |
117 | { | |
118 | return mBinder; | |
119 | } | |
120 | ||
121 | @Override | |
122 | public void onDestroy() | |
123 | { | |
124 | } | |
125 | ||
126 | /** | |
127 | * Register a listener with this Service. We assume this is called from | |
128 | * the main thread so no synchronization is happening. | |
129 | * | |
130 | * @param listener listener to register | |
131 | */ | |
132 | public void registerListener(VpnStateListener listener) | |
133 | { | |
134 | mListeners.add(listener); | |
135 | } | |
136 | ||
137 | /** | |
138 | * Unregister a listener from this Service. | |
139 | * | |
140 | * @param listener listener to unregister | |
141 | */ | |
142 | public void unregisterListener(VpnStateListener listener) | |
143 | { | |
144 | mListeners.remove(listener); | |
145 | } | |
146 | ||
147 | /** | |
148 | * Get the current VPN profile. | |
149 | * | |
150 | * @return profile | |
151 | */ | |
152 | public VpnProfile getProfile() | |
153 | { /* only updated from the main thread so no synchronization needed */ | |
154 | return mProfile; | |
155 | } | |
156 | ||
38172313 TB |
157 | /** |
158 | * Get the current connection ID. May be used to track which state | |
159 | * changes have already been handled. | |
160 | * | |
161 | * Is increased when startConnection() is called. | |
162 | * | |
163 | * @return connection ID | |
164 | */ | |
165 | public long getConnectionID() | |
166 | { /* only updated from the main thread so no synchronization needed */ | |
167 | return mConnectionID; | |
168 | } | |
169 | ||
68afdd34 TB |
170 | /** |
171 | * Get the total number of seconds until there is an automatic retry to reconnect. | |
8e3b921a | 172 | * |
68afdd34 TB |
173 | * @return total number of seconds until the retry |
174 | */ | |
175 | public int getRetryTimeout() | |
176 | { | |
177 | return (int)(mRetryTimeout / 1000); | |
178 | } | |
179 | ||
180 | /** | |
181 | * Get the number of seconds until there is an automatic retry to reconnect. | |
8e3b921a | 182 | * |
68afdd34 TB |
183 | * @return number of seconds until the retry |
184 | */ | |
185 | public int getRetryIn() | |
186 | { | |
187 | return (int)(mRetryIn / 1000); | |
188 | } | |
189 | ||
d1220566 TB |
190 | /** |
191 | * Get the current state. | |
192 | * | |
193 | * @return state | |
194 | */ | |
195 | public State getState() | |
196 | { /* only updated from the main thread so no synchronization needed */ | |
197 | return mState; | |
198 | } | |
199 | ||
200 | /** | |
201 | * Get the current error, if any. | |
202 | * | |
203 | * @return error | |
204 | */ | |
205 | public ErrorState getErrorState() | |
206 | { /* only updated from the main thread so no synchronization needed */ | |
207 | return mError; | |
208 | } | |
209 | ||
a7d679ff TB |
210 | /** |
211 | * Get a description of the current error, if any. | |
212 | * | |
213 | * @return error description text id | |
214 | */ | |
215 | public int getErrorText() | |
216 | { | |
217 | switch (mError) | |
218 | { | |
219 | case AUTH_FAILED: | |
220 | if (mImcState == ImcState.BLOCK) | |
221 | { | |
222 | return R.string.error_assessment_failed; | |
223 | } | |
224 | else | |
225 | { | |
226 | return R.string.error_auth_failed; | |
227 | } | |
228 | case PEER_AUTH_FAILED: | |
229 | return R.string.error_peer_auth_failed; | |
230 | case LOOKUP_FAILED: | |
231 | return R.string.error_lookup_failed; | |
232 | case UNREACHABLE: | |
233 | return R.string.error_unreachable; | |
f0b3e303 TB |
234 | case PASSWORD_MISSING: |
235 | return R.string.error_password_missing; | |
ab5dbbc4 TB |
236 | case CERTIFICATE_UNAVAILABLE: |
237 | return R.string.error_certificate_unavailable; | |
a7d679ff TB |
238 | default: |
239 | return R.string.error_generic; | |
240 | } | |
241 | } | |
242 | ||
dc52cfab TB |
243 | /** |
244 | * Get the current IMC state, if any. | |
245 | * | |
246 | * @return imc state | |
247 | */ | |
248 | public ImcState getImcState() | |
249 | { /* only updated from the main thread so no synchronization needed */ | |
250 | return mImcState; | |
251 | } | |
252 | ||
a05acd76 TB |
253 | /** |
254 | * Get the remediation instructions, if any. | |
255 | * | |
256 | * @return read-only list of instructions | |
257 | */ | |
258 | public List<RemediationInstruction> getRemediationInstructions() | |
259 | { /* only updated from the main thread so no synchronization needed */ | |
260 | return Collections.unmodifiableList(mRemediationInstructions); | |
261 | } | |
262 | ||
1b887772 TB |
263 | /** |
264 | * Disconnect any existing connection and shutdown the daemon, the | |
265 | * VpnService is not stopped but it is reset so new connections can be | |
266 | * started. | |
267 | */ | |
268 | public void disconnect() | |
269 | { | |
3f71118b | 270 | /* reset any potential retry timer and error state */ |
68afdd34 | 271 | resetRetryTimer(); |
3f71118b TB |
272 | setError(ErrorState.NO_ERROR); |
273 | ||
1b887772 TB |
274 | /* as soon as the TUN device is created by calling establish() on the |
275 | * VpnService.Builder object the system binds to the service and keeps | |
276 | * bound until the file descriptor of the TUN device is closed. thus | |
277 | * calling stopService() here would not stop (destroy) the service yet, | |
59693d6c | 278 | * instead we call startService() with a specific action which shuts down |
1b887772 TB |
279 | * the daemon (and closes the TUN device, if any) */ |
280 | Context context = getApplicationContext(); | |
281 | Intent intent = new Intent(context, CharonVpnService.class); | |
59693d6c | 282 | intent.setAction(CharonVpnService.DISCONNECT_ACTION); |
1b887772 TB |
283 | context.startService(intent); |
284 | } | |
285 | ||
1350ee1e TB |
286 | /** |
287 | * Connect (or reconnect) a profile | |
8e3b921a | 288 | * |
1350ee1e | 289 | * @param profileInfo optional profile info (basically the UUID and password), taken from the |
8e3b921a | 290 | * previous profile if null |
1350ee1e TB |
291 | * @param fromScratch true if this is a manual retry/reconnect or a completely new connection |
292 | */ | |
293 | public void connect(Bundle profileInfo, boolean fromScratch) | |
294 | { | |
295 | /* we assume we have the necessary permission */ | |
296 | Context context = getApplicationContext(); | |
297 | Intent intent = new Intent(context, CharonVpnService.class); | |
298 | if (profileInfo == null) | |
299 | { | |
300 | profileInfo = new Bundle(); | |
6f9b96ac | 301 | profileInfo.putString(VpnProfileDataSource.KEY_UUID, mProfile.getUUID().toString()); |
1350ee1e TB |
302 | /* pass the previous password along */ |
303 | profileInfo.putString(VpnProfileDataSource.KEY_PASSWORD, mProfile.getPassword()); | |
304 | } | |
305 | if (fromScratch) | |
306 | { | |
307 | /* reset if this is a manual retry or a new connection */ | |
308 | mTimeoutProvider.reset(); | |
309 | } | |
fb3772ec TB |
310 | else |
311 | { /* mark this as an automatic retry */ | |
312 | profileInfo.putBoolean(CharonVpnService.KEY_IS_RETRY, true); | |
313 | } | |
1350ee1e | 314 | intent.putExtras(profileInfo); |
5bdb800a | 315 | ContextCompat.startForegroundService(context, intent); |
1350ee1e TB |
316 | } |
317 | ||
063230c2 TB |
318 | /** |
319 | * Reconnect to the previous profile. | |
320 | */ | |
321 | public void reconnect() | |
322 | { | |
323 | if (mProfile == null) | |
324 | { | |
325 | return; | |
326 | } | |
3eda52f0 TB |
327 | if (mProfile.getVpnType().has(VpnType.VpnTypeFeature.USER_PASS)) |
328 | { | |
329 | if (mProfile.getPassword() == null || | |
330 | mError == ErrorState.AUTH_FAILED) | |
331 | { /* show a dialog if we either don't have the password or if it might be the wrong | |
332 | * one (which is or isn't stored with the profile, let the activity decide) */ | |
333 | Intent intent = new Intent(this, VpnProfileControlActivity.class); | |
334 | intent.addFlags(Intent.FLAG_ACTIVITY_NEW_TASK); | |
335 | intent.setAction(VpnProfileControlActivity.START_PROFILE); | |
8e3b921a | 336 | intent.putExtra(VpnProfileControlActivity.EXTRA_VPN_PROFILE_UUID, mProfile.getUUID().toString()); |
3eda52f0 TB |
337 | startActivity(intent); |
338 | /* reset the retry timer immediately in case the user needs more time to enter the password */ | |
339 | notifyListeners(() -> { | |
340 | resetRetryTimer(); | |
341 | return true; | |
342 | }); | |
343 | return; | |
344 | } | |
345 | } | |
1350ee1e | 346 | connect(null, true); |
063230c2 TB |
347 | } |
348 | ||
d1220566 TB |
349 | /** |
350 | * Update state and notify all listeners about the change. By using a Handler | |
351 | * this is done from the main UI thread and not the initial reporter thread. | |
352 | * Also, in doing the actual state change from the main thread, listeners | |
353 | * see all changes and none are skipped. | |
354 | * | |
355 | * @param change the state update to perform before notifying listeners, returns true if state changed | |
356 | */ | |
357 | private void notifyListeners(final Callable<Boolean> change) | |
358 | { | |
8e3b921a MP |
359 | mHandler.post(new Runnable() |
360 | { | |
d1220566 TB |
361 | @Override |
362 | public void run() | |
363 | { | |
364 | try | |
365 | { | |
366 | if (change.call()) | |
367 | { /* otherwise there is no need to notify the listeners */ | |
368 | for (VpnStateListener listener : mListeners) | |
369 | { | |
370 | listener.stateChanged(); | |
371 | } | |
372 | } | |
373 | } | |
374 | catch (Exception e) | |
375 | { | |
376 | e.printStackTrace(); | |
377 | } | |
378 | } | |
379 | }); | |
380 | } | |
381 | ||
382 | /** | |
38172313 TB |
383 | * Called when a connection is started. Sets the currently active VPN |
384 | * profile, resets IMC and Error state variables, sets the State to | |
385 | * CONNECTING, increases the connection ID, and notifies all listeners. | |
d1220566 TB |
386 | * |
387 | * May be called from threads other than the main thread. | |
388 | * | |
389 | * @param profile current profile | |
390 | */ | |
38172313 | 391 | public void startConnection(final VpnProfile profile) |
d1220566 | 392 | { |
8e3b921a MP |
393 | notifyListeners(new Callable<Boolean>() |
394 | { | |
d1220566 | 395 | @Override |
38172313 | 396 | public Boolean call() throws Exception |
d1220566 | 397 | { |
68afdd34 | 398 | resetRetryTimer(); |
38172313 | 399 | VpnStateService.this.mConnectionID++; |
d1220566 | 400 | VpnStateService.this.mProfile = profile; |
38172313 TB |
401 | VpnStateService.this.mState = State.CONNECTING; |
402 | VpnStateService.this.mError = ErrorState.NO_ERROR; | |
403 | VpnStateService.this.mImcState = ImcState.UNKNOWN; | |
cfed5679 | 404 | VpnStateService.this.mRemediationInstructions.clear(); |
38172313 | 405 | return true; |
d1220566 TB |
406 | } |
407 | }); | |
408 | } | |
409 | ||
410 | /** | |
411 | * Update the state and notify all listeners, if changed. | |
412 | * | |
413 | * May be called from threads other than the main thread. | |
414 | * | |
415 | * @param state new state | |
416 | */ | |
417 | public void setState(final State state) | |
418 | { | |
8e3b921a MP |
419 | notifyListeners(new Callable<Boolean>() |
420 | { | |
d1220566 TB |
421 | @Override |
422 | public Boolean call() throws Exception | |
423 | { | |
1350ee1e TB |
424 | if (state == State.CONNECTED) |
425 | { /* reset counter in case there is an error later on */ | |
426 | mTimeoutProvider.reset(); | |
427 | } | |
d1220566 TB |
428 | if (VpnStateService.this.mState != state) |
429 | { | |
430 | VpnStateService.this.mState = state; | |
431 | return true; | |
432 | } | |
433 | return false; | |
434 | } | |
435 | }); | |
436 | } | |
437 | ||
438 | /** | |
439 | * Set the current error state and notify all listeners, if changed. | |
440 | * | |
441 | * May be called from threads other than the main thread. | |
442 | * | |
443 | * @param error error state | |
444 | */ | |
445 | public void setError(final ErrorState error) | |
446 | { | |
8e3b921a MP |
447 | notifyListeners(new Callable<Boolean>() |
448 | { | |
d1220566 TB |
449 | @Override |
450 | public Boolean call() throws Exception | |
451 | { | |
452 | if (VpnStateService.this.mError != error) | |
453 | { | |
68afdd34 TB |
454 | if (VpnStateService.this.mError == ErrorState.NO_ERROR) |
455 | { | |
456 | setRetryTimer(error); | |
457 | } | |
458 | else if (error == ErrorState.NO_ERROR) | |
459 | { | |
460 | resetRetryTimer(); | |
461 | } | |
d1220566 TB |
462 | VpnStateService.this.mError = error; |
463 | return true; | |
464 | } | |
465 | return false; | |
466 | } | |
467 | }); | |
468 | } | |
dc52cfab TB |
469 | |
470 | /** | |
471 | * Set the current IMC state and notify all listeners, if changed. | |
472 | * | |
a05acd76 TB |
473 | * Setting the state to UNKNOWN clears all remediation instructions. |
474 | * | |
dc52cfab TB |
475 | * May be called from threads other than the main thread. |
476 | * | |
d5070425 | 477 | * @param state IMC state |
dc52cfab TB |
478 | */ |
479 | public void setImcState(final ImcState state) | |
480 | { | |
8e3b921a MP |
481 | notifyListeners(new Callable<Boolean>() |
482 | { | |
dc52cfab TB |
483 | @Override |
484 | public Boolean call() throws Exception | |
485 | { | |
a05acd76 TB |
486 | if (state == ImcState.UNKNOWN) |
487 | { | |
488 | VpnStateService.this.mRemediationInstructions.clear(); | |
489 | } | |
dc52cfab TB |
490 | if (VpnStateService.this.mImcState != state) |
491 | { | |
492 | VpnStateService.this.mImcState = state; | |
493 | return true; | |
494 | } | |
495 | return false; | |
496 | } | |
497 | }); | |
498 | } | |
a05acd76 TB |
499 | |
500 | /** | |
501 | * Add the given remediation instruction to the internal list. Listeners | |
502 | * are not notified. | |
503 | * | |
504 | * Instructions are cleared if the IMC state is set to UNKNOWN. | |
505 | * | |
506 | * May be called from threads other than the main thread. | |
507 | * | |
508 | * @param instruction remediation instruction | |
509 | */ | |
510 | public void addRemediationInstruction(final RemediationInstruction instruction) | |
511 | { | |
8e3b921a MP |
512 | mHandler.post(new Runnable() |
513 | { | |
a05acd76 TB |
514 | @Override |
515 | public void run() | |
516 | { | |
517 | VpnStateService.this.mRemediationInstructions.add(instruction); | |
518 | } | |
519 | }); | |
520 | } | |
68afdd34 TB |
521 | |
522 | /** | |
523 | * Sets the retry timer | |
524 | */ | |
525 | private void setRetryTimer(ErrorState error) | |
526 | { | |
1350ee1e TB |
527 | mRetryTimeout = mRetryIn = mTimeoutProvider.getTimeout(error); |
528 | if (mRetryTimeout <= 0) | |
68afdd34 TB |
529 | { |
530 | return; | |
531 | } | |
532 | mHandler.sendMessageAtTime(mHandler.obtainMessage(RETRY_MSG), SystemClock.uptimeMillis() + RETRY_INTERVAL); | |
533 | } | |
534 | ||
535 | /** | |
536 | * Reset the retry timer | |
537 | */ | |
538 | private void resetRetryTimer() | |
539 | { | |
540 | mRetryTimeout = 0; | |
541 | mRetryIn = 0; | |
542 | } | |
543 | ||
544 | /** | |
545 | * Special Handler subclass that handles the retry countdown (more accurate than CountDownTimer) | |
546 | */ | |
8e3b921a MP |
547 | private static class RetryHandler extends Handler |
548 | { | |
68afdd34 TB |
549 | WeakReference<VpnStateService> mService; |
550 | ||
dc351a30 | 551 | public RetryHandler(Looper looper, VpnStateService service) |
68afdd34 | 552 | { |
dc351a30 | 553 | super(looper); |
68afdd34 TB |
554 | mService = new WeakReference<>(service); |
555 | } | |
556 | ||
557 | @Override | |
558 | public void handleMessage(Message msg) | |
559 | { | |
560 | /* handle retry countdown */ | |
561 | if (mService.get().mRetryTimeout <= 0) | |
562 | { | |
563 | return; | |
564 | } | |
565 | mService.get().mRetryIn -= RETRY_INTERVAL; | |
566 | if (mService.get().mRetryIn > 0) | |
567 | { | |
568 | /* calculate next interval before notifying listeners */ | |
569 | long next = SystemClock.uptimeMillis() + RETRY_INTERVAL; | |
570 | ||
571 | for (VpnStateListener listener : mService.get().mListeners) | |
572 | { | |
573 | listener.stateChanged(); | |
574 | } | |
575 | sendMessageAtTime(obtainMessage(RETRY_MSG), next); | |
576 | } | |
577 | else | |
578 | { | |
1350ee1e | 579 | mService.get().connect(null, false); |
68afdd34 TB |
580 | } |
581 | } | |
582 | } | |
1350ee1e TB |
583 | |
584 | /** | |
585 | * Class that handles an exponential backoff for retry timeouts | |
586 | */ | |
587 | private static class RetryTimeoutProvider | |
588 | { | |
589 | private long mRetry; | |
590 | ||
591 | private long getBaseTimeout(ErrorState error) | |
592 | { | |
593 | switch (error) | |
594 | { | |
595 | case AUTH_FAILED: | |
596 | return 10000; | |
597 | case PEER_AUTH_FAILED: | |
598 | return 5000; | |
599 | case LOOKUP_FAILED: | |
600 | return 5000; | |
601 | case UNREACHABLE: | |
602 | return 5000; | |
603 | case PASSWORD_MISSING: | |
604 | /* this needs user intervention (entering the password) */ | |
605 | return 0; | |
606 | case CERTIFICATE_UNAVAILABLE: | |
607 | /* if this is because the device has to be unlocked we might be able to reconnect */ | |
608 | return 5000; | |
609 | default: | |
610 | return 10000; | |
611 | } | |
612 | } | |
613 | ||
614 | /** | |
615 | * Called each time a new retry timeout is started. The timeout increases until reset() is | |
616 | * called and the base timeout is returned again. | |
8e3b921a | 617 | * |
1350ee1e TB |
618 | * @param error Error state |
619 | */ | |
620 | public long getTimeout(ErrorState error) | |
621 | { | |
622 | long timeout = (long)(getBaseTimeout(error) * Math.pow(2, mRetry++)); | |
623 | /* return the result rounded to seconds */ | |
624 | return Math.min((timeout / 1000) * 1000, MAX_RETRY_INTERVAL); | |
625 | } | |
626 | ||
627 | /** | |
628 | * Reset the retry counter. | |
629 | */ | |
630 | public void reset() | |
631 | { | |
632 | mRetry = 0; | |
633 | } | |
634 | } | |
d1220566 | 635 | } |