2 * Copyright (C) 2012-2017 Tobias Brunner
3 * HSR Hochschule fuer Technik Rapperswil
5 * This program is free software; you can redistribute it and/or modify it
6 * under the terms of the GNU General Public License as published by the
7 * Free Software Foundation; either version 2 of the License, or (at your
8 * option) any later version. See <http://www.fsf.org/copyleft/gpl.txt>.
10 * This program is distributed in the hope that it will be useful, but
11 * WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY
12 * or FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License
16 package org
.strongswan
.android
.logic
;
18 import android
.app
.Service
;
19 import android
.content
.Context
;
20 import android
.content
.Intent
;
21 import android
.os
.Binder
;
22 import android
.os
.Bundle
;
23 import android
.os
.Handler
;
24 import android
.os
.IBinder
;
25 import android
.os
.Message
;
26 import android
.os
.SystemClock
;
28 import org
.strongswan
.android
.R
;
29 import org
.strongswan
.android
.data
.VpnProfile
;
30 import org
.strongswan
.android
.data
.VpnProfileDataSource
;
31 import org
.strongswan
.android
.logic
.imc
.ImcState
;
32 import org
.strongswan
.android
.logic
.imc
.RemediationInstruction
;
34 import java
.lang
.ref
.WeakReference
;
35 import java
.util
.Collections
;
36 import java
.util
.HashSet
;
37 import java
.util
.LinkedList
;
38 import java
.util
.List
;
39 import java
.util
.concurrent
.Callable
;
41 public class VpnStateService
extends Service
43 private final HashSet
<VpnStateListener
> mListeners
= new HashSet
<VpnStateListener
>();
44 private final IBinder mBinder
= new LocalBinder();
45 private long mConnectionID
= 0;
46 private Handler mHandler
;
47 private VpnProfile mProfile
;
48 private State mState
= State
.DISABLED
;
49 private ErrorState mError
= ErrorState
.NO_ERROR
;
50 private ImcState mImcState
= ImcState
.UNKNOWN
;
51 private final LinkedList
<RemediationInstruction
> mRemediationInstructions
= new LinkedList
<RemediationInstruction
>();
52 private static long RETRY_INTERVAL
= 1000;
53 private static int RETRY_MSG
= 1;
54 private long mRetryTimeout
;
55 private long mRetryIn
;
65 public enum ErrorState
74 CERTIFICATE_UNAVAILABLE
,
78 * Listener interface for bound clients that are interested in changes to
81 public interface VpnStateListener
83 public void stateChanged();
87 * Simple Binder that allows to directly access this Service class itself
88 * after binding to it.
90 public class LocalBinder
extends Binder
92 public VpnStateService
getService()
94 return VpnStateService
.this;
99 public void onCreate()
101 /* this handler allows us to notify listeners from the UI thread and
102 * not from the threads that actually report any state changes */
103 mHandler
= new RetryHandler(this);
107 public IBinder
onBind(Intent intent
)
113 public void onDestroy()
118 * Register a listener with this Service. We assume this is called from
119 * the main thread so no synchronization is happening.
121 * @param listener listener to register
123 public void registerListener(VpnStateListener listener
)
125 mListeners
.add(listener
);
129 * Unregister a listener from this Service.
131 * @param listener listener to unregister
133 public void unregisterListener(VpnStateListener listener
)
135 mListeners
.remove(listener
);
139 * Get the current VPN profile.
143 public VpnProfile
getProfile()
144 { /* only updated from the main thread so no synchronization needed */
149 * Get the current connection ID. May be used to track which state
150 * changes have already been handled.
152 * Is increased when startConnection() is called.
154 * @return connection ID
156 public long getConnectionID()
157 { /* only updated from the main thread so no synchronization needed */
158 return mConnectionID
;
162 * Get the total number of seconds until there is an automatic retry to reconnect.
163 * @return total number of seconds until the retry
165 public int getRetryTimeout()
167 return (int)(mRetryTimeout
/ 1000);
171 * Get the number of seconds until there is an automatic retry to reconnect.
172 * @return number of seconds until the retry
174 public int getRetryIn()
176 return (int)(mRetryIn
/ 1000);
180 * Get the current state.
184 public State
getState()
185 { /* only updated from the main thread so no synchronization needed */
190 * Get the current error, if any.
194 public ErrorState
getErrorState()
195 { /* only updated from the main thread so no synchronization needed */
200 * Get a description of the current error, if any.
202 * @return error description text id
204 public int getErrorText()
209 if (mImcState
== ImcState
.BLOCK
)
211 return R
.string
.error_assessment_failed
;
215 return R
.string
.error_auth_failed
;
217 case PEER_AUTH_FAILED
:
218 return R
.string
.error_peer_auth_failed
;
220 return R
.string
.error_lookup_failed
;
222 return R
.string
.error_unreachable
;
223 case PASSWORD_MISSING
:
224 return R
.string
.error_password_missing
;
225 case CERTIFICATE_UNAVAILABLE
:
226 return R
.string
.error_certificate_unavailable
;
228 return R
.string
.error_generic
;
233 * Get the current IMC state, if any.
237 public ImcState
getImcState()
238 { /* only updated from the main thread so no synchronization needed */
243 * Get the remediation instructions, if any.
245 * @return read-only list of instructions
247 public List
<RemediationInstruction
> getRemediationInstructions()
248 { /* only updated from the main thread so no synchronization needed */
249 return Collections
.unmodifiableList(mRemediationInstructions
);
253 * Disconnect any existing connection and shutdown the daemon, the
254 * VpnService is not stopped but it is reset so new connections can be
257 public void disconnect()
260 /* as soon as the TUN device is created by calling establish() on the
261 * VpnService.Builder object the system binds to the service and keeps
262 * bound until the file descriptor of the TUN device is closed. thus
263 * calling stopService() here would not stop (destroy) the service yet,
264 * instead we call startService() with a specific action which shuts down
265 * the daemon (and closes the TUN device, if any) */
266 Context context
= getApplicationContext();
267 Intent intent
= new Intent(context
, CharonVpnService
.class);
268 intent
.setAction(CharonVpnService
.DISCONNECT_ACTION
);
269 context
.startService(intent
);
273 * Reconnect to the previous profile.
275 public void reconnect()
277 if (mProfile
== null)
281 Bundle profileInfo
= new Bundle();
282 profileInfo
.putLong(VpnProfileDataSource
.KEY_ID
, mProfile
.getId());
283 /* pass the previous password along */
284 profileInfo
.putString(VpnProfileDataSource
.KEY_PASSWORD
, mProfile
.getPassword());
285 /* we assume we have the necessary permission */
286 Context context
= getApplicationContext();
287 Intent intent
= new Intent(context
, CharonVpnService
.class);
288 intent
.putExtras(profileInfo
);
289 context
.startService(intent
);
293 * Update state and notify all listeners about the change. By using a Handler
294 * this is done from the main UI thread and not the initial reporter thread.
295 * Also, in doing the actual state change from the main thread, listeners
296 * see all changes and none are skipped.
298 * @param change the state update to perform before notifying listeners, returns true if state changed
300 private void notifyListeners(final Callable
<Boolean
> change
)
302 mHandler
.post(new Runnable() {
309 { /* otherwise there is no need to notify the listeners */
310 for (VpnStateListener listener
: mListeners
)
312 listener
.stateChanged();
325 * Called when a connection is started. Sets the currently active VPN
326 * profile, resets IMC and Error state variables, sets the State to
327 * CONNECTING, increases the connection ID, and notifies all listeners.
329 * May be called from threads other than the main thread.
331 * @param profile current profile
333 public void startConnection(final VpnProfile profile
)
335 notifyListeners(new Callable
<Boolean
>() {
337 public Boolean
call() throws Exception
340 VpnStateService
.this.mConnectionID
++;
341 VpnStateService
.this.mProfile
= profile
;
342 VpnStateService
.this.mState
= State
.CONNECTING
;
343 VpnStateService
.this.mError
= ErrorState
.NO_ERROR
;
344 VpnStateService
.this.mImcState
= ImcState
.UNKNOWN
;
345 VpnStateService
.this.mRemediationInstructions
.clear();
352 * Update the state and notify all listeners, if changed.
354 * May be called from threads other than the main thread.
356 * @param state new state
358 public void setState(final State state
)
360 notifyListeners(new Callable
<Boolean
>() {
362 public Boolean
call() throws Exception
364 if (VpnStateService
.this.mState
!= state
)
366 VpnStateService
.this.mState
= state
;
375 * Set the current error state and notify all listeners, if changed.
377 * May be called from threads other than the main thread.
379 * @param error error state
381 public void setError(final ErrorState error
)
383 notifyListeners(new Callable
<Boolean
>() {
385 public Boolean
call() throws Exception
387 if (VpnStateService
.this.mError
!= error
)
389 if (VpnStateService
.this.mError
== ErrorState
.NO_ERROR
)
391 setRetryTimer(error
);
393 else if (error
== ErrorState
.NO_ERROR
)
397 VpnStateService
.this.mError
= error
;
406 * Set the current IMC state and notify all listeners, if changed.
408 * Setting the state to UNKNOWN clears all remediation instructions.
410 * May be called from threads other than the main thread.
412 * @param state IMC state
414 public void setImcState(final ImcState state
)
416 notifyListeners(new Callable
<Boolean
>() {
418 public Boolean
call() throws Exception
420 if (state
== ImcState
.UNKNOWN
)
422 VpnStateService
.this.mRemediationInstructions
.clear();
424 if (VpnStateService
.this.mImcState
!= state
)
426 VpnStateService
.this.mImcState
= state
;
435 * Add the given remediation instruction to the internal list. Listeners
438 * Instructions are cleared if the IMC state is set to UNKNOWN.
440 * May be called from threads other than the main thread.
442 * @param instruction remediation instruction
444 public void addRemediationInstruction(final RemediationInstruction instruction
)
446 mHandler
.post(new Runnable() {
450 VpnStateService
.this.mRemediationInstructions
.add(instruction
);
456 * Sets the retry timer
458 private void setRetryTimer(ErrorState error
)
467 case PEER_AUTH_FAILED
:
476 case PASSWORD_MISSING
:
477 /* this needs user intervention (entering the password) */
480 case CERTIFICATE_UNAVAILABLE
:
481 /* if this is because the device has to be unlocked we might be able to reconnect */
488 mRetryTimeout
= mRetryIn
= timeout
;
493 mHandler
.sendMessageAtTime(mHandler
.obtainMessage(RETRY_MSG
), SystemClock
.uptimeMillis() + RETRY_INTERVAL
);
497 * Reset the retry timer
499 private void resetRetryTimer()
506 * Special Handler subclass that handles the retry countdown (more accurate than CountDownTimer)
508 private static class RetryHandler
extends Handler
{
509 WeakReference
<VpnStateService
> mService
;
511 public RetryHandler(VpnStateService service
)
513 mService
= new WeakReference
<>(service
);
517 public void handleMessage(Message msg
)
519 /* handle retry countdown */
520 if (mService
.get().mRetryTimeout
<= 0)
524 mService
.get().mRetryIn
-= RETRY_INTERVAL
;
525 if (mService
.get().mRetryIn
> 0)
527 /* calculate next interval before notifying listeners */
528 long next
= SystemClock
.uptimeMillis() + RETRY_INTERVAL
;
530 for (VpnStateListener listener
: mService
.get().mListeners
)
532 listener
.stateChanged();
534 sendMessageAtTime(obtainMessage(RETRY_MSG
), next
);
538 mService
.get().reconnect();