]> git.ipfire.org Git - thirdparty/strongswan.git/blob - src/frontends/android/app/src/main/java/org/strongswan/android/logic/VpnStateService.java
9f11e705b1ab76a1358918d93a610610eb883fac
[thirdparty/strongswan.git] / src / frontends / android / app / src / main / java / org / strongswan / android / logic / VpnStateService.java
1 /*
2 * Copyright (C) 2012-2017 Tobias Brunner
3 * HSR Hochschule fuer Technik Rapperswil
4 *
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>.
9 *
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
13 * for more details.
14 */
15
16 package org.strongswan.android.logic;
17
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
26 import org.strongswan.android.R;
27 import org.strongswan.android.data.VpnProfile;
28 import org.strongswan.android.data.VpnProfileDataSource;
29 import org.strongswan.android.logic.imc.ImcState;
30 import org.strongswan.android.logic.imc.RemediationInstruction;
31
32 import java.util.Collections;
33 import java.util.HashSet;
34 import java.util.LinkedList;
35 import java.util.List;
36 import java.util.concurrent.Callable;
37
38 public class VpnStateService extends Service
39 {
40 private final HashSet<VpnStateListener> mListeners = new HashSet<VpnStateListener>();
41 private final IBinder mBinder = new LocalBinder();
42 private long mConnectionID = 0;
43 private Handler mHandler;
44 private VpnProfile mProfile;
45 private State mState = State.DISABLED;
46 private ErrorState mError = ErrorState.NO_ERROR;
47 private ImcState mImcState = ImcState.UNKNOWN;
48 private final LinkedList<RemediationInstruction> mRemediationInstructions = new LinkedList<RemediationInstruction>();
49
50 public enum State
51 {
52 DISABLED,
53 CONNECTING,
54 CONNECTED,
55 DISCONNECTING,
56 }
57
58 public enum ErrorState
59 {
60 NO_ERROR,
61 AUTH_FAILED,
62 PEER_AUTH_FAILED,
63 LOOKUP_FAILED,
64 UNREACHABLE,
65 GENERIC_ERROR,
66 PASSWORD_MISSING,
67 CERTIFICATE_UNAVAILABLE,
68 }
69
70 /**
71 * Listener interface for bound clients that are interested in changes to
72 * this Service.
73 */
74 public interface VpnStateListener
75 {
76 public void stateChanged();
77 }
78
79 /**
80 * Simple Binder that allows to directly access this Service class itself
81 * after binding to it.
82 */
83 public class LocalBinder extends Binder
84 {
85 public VpnStateService getService()
86 {
87 return VpnStateService.this;
88 }
89 }
90
91 @Override
92 public void onCreate()
93 {
94 /* this handler allows us to notify listeners from the UI thread and
95 * not from the threads that actually report any state changes */
96 mHandler = new Handler();
97 }
98
99 @Override
100 public IBinder onBind(Intent intent)
101 {
102 return mBinder;
103 }
104
105 @Override
106 public void onDestroy()
107 {
108 }
109
110 /**
111 * Register a listener with this Service. We assume this is called from
112 * the main thread so no synchronization is happening.
113 *
114 * @param listener listener to register
115 */
116 public void registerListener(VpnStateListener listener)
117 {
118 mListeners.add(listener);
119 }
120
121 /**
122 * Unregister a listener from this Service.
123 *
124 * @param listener listener to unregister
125 */
126 public void unregisterListener(VpnStateListener listener)
127 {
128 mListeners.remove(listener);
129 }
130
131 /**
132 * Get the current VPN profile.
133 *
134 * @return profile
135 */
136 public VpnProfile getProfile()
137 { /* only updated from the main thread so no synchronization needed */
138 return mProfile;
139 }
140
141 /**
142 * Get the current connection ID. May be used to track which state
143 * changes have already been handled.
144 *
145 * Is increased when startConnection() is called.
146 *
147 * @return connection ID
148 */
149 public long getConnectionID()
150 { /* only updated from the main thread so no synchronization needed */
151 return mConnectionID;
152 }
153
154 /**
155 * Get the current state.
156 *
157 * @return state
158 */
159 public State getState()
160 { /* only updated from the main thread so no synchronization needed */
161 return mState;
162 }
163
164 /**
165 * Get the current error, if any.
166 *
167 * @return error
168 */
169 public ErrorState getErrorState()
170 { /* only updated from the main thread so no synchronization needed */
171 return mError;
172 }
173
174 /**
175 * Get a description of the current error, if any.
176 *
177 * @return error description text id
178 */
179 public int getErrorText()
180 {
181 switch (mError)
182 {
183 case AUTH_FAILED:
184 if (mImcState == ImcState.BLOCK)
185 {
186 return R.string.error_assessment_failed;
187 }
188 else
189 {
190 return R.string.error_auth_failed;
191 }
192 case PEER_AUTH_FAILED:
193 return R.string.error_peer_auth_failed;
194 case LOOKUP_FAILED:
195 return R.string.error_lookup_failed;
196 case UNREACHABLE:
197 return R.string.error_unreachable;
198 case PASSWORD_MISSING:
199 return R.string.error_password_missing;
200 case CERTIFICATE_UNAVAILABLE:
201 return R.string.error_certificate_unavailable;
202 default:
203 return R.string.error_generic;
204 }
205 }
206
207 /**
208 * Get the current IMC state, if any.
209 *
210 * @return imc state
211 */
212 public ImcState getImcState()
213 { /* only updated from the main thread so no synchronization needed */
214 return mImcState;
215 }
216
217 /**
218 * Get the remediation instructions, if any.
219 *
220 * @return read-only list of instructions
221 */
222 public List<RemediationInstruction> getRemediationInstructions()
223 { /* only updated from the main thread so no synchronization needed */
224 return Collections.unmodifiableList(mRemediationInstructions);
225 }
226
227 /**
228 * Disconnect any existing connection and shutdown the daemon, the
229 * VpnService is not stopped but it is reset so new connections can be
230 * started.
231 */
232 public void disconnect()
233 {
234 /* as soon as the TUN device is created by calling establish() on the
235 * VpnService.Builder object the system binds to the service and keeps
236 * bound until the file descriptor of the TUN device is closed. thus
237 * calling stopService() here would not stop (destroy) the service yet,
238 * instead we call startService() with a specific action which shuts down
239 * the daemon (and closes the TUN device, if any) */
240 Context context = getApplicationContext();
241 Intent intent = new Intent(context, CharonVpnService.class);
242 intent.setAction(CharonVpnService.DISCONNECT_ACTION);
243 context.startService(intent);
244 }
245
246 /**
247 * Reconnect to the previous profile.
248 */
249 public void reconnect()
250 {
251 if (mProfile == null)
252 {
253 return;
254 }
255 Bundle profileInfo = new Bundle();
256 profileInfo.putLong(VpnProfileDataSource.KEY_ID, mProfile.getId());
257 /* pass the previous password along */
258 profileInfo.putString(VpnProfileDataSource.KEY_PASSWORD, mProfile.getPassword());
259 /* we assume we have the necessary permission */
260 Context context = getApplicationContext();
261 Intent intent = new Intent(context, CharonVpnService.class);
262 intent.putExtras(profileInfo);
263 context.startService(intent);
264 }
265
266 /**
267 * Update state and notify all listeners about the change. By using a Handler
268 * this is done from the main UI thread and not the initial reporter thread.
269 * Also, in doing the actual state change from the main thread, listeners
270 * see all changes and none are skipped.
271 *
272 * @param change the state update to perform before notifying listeners, returns true if state changed
273 */
274 private void notifyListeners(final Callable<Boolean> change)
275 {
276 mHandler.post(new Runnable() {
277 @Override
278 public void run()
279 {
280 try
281 {
282 if (change.call())
283 { /* otherwise there is no need to notify the listeners */
284 for (VpnStateListener listener : mListeners)
285 {
286 listener.stateChanged();
287 }
288 }
289 }
290 catch (Exception e)
291 {
292 e.printStackTrace();
293 }
294 }
295 });
296 }
297
298 /**
299 * Called when a connection is started. Sets the currently active VPN
300 * profile, resets IMC and Error state variables, sets the State to
301 * CONNECTING, increases the connection ID, and notifies all listeners.
302 *
303 * May be called from threads other than the main thread.
304 *
305 * @param profile current profile
306 */
307 public void startConnection(final VpnProfile profile)
308 {
309 notifyListeners(new Callable<Boolean>() {
310 @Override
311 public Boolean call() throws Exception
312 {
313 VpnStateService.this.mConnectionID++;
314 VpnStateService.this.mProfile = profile;
315 VpnStateService.this.mState = State.CONNECTING;
316 VpnStateService.this.mError = ErrorState.NO_ERROR;
317 VpnStateService.this.mImcState = ImcState.UNKNOWN;
318 VpnStateService.this.mRemediationInstructions.clear();
319 return true;
320 }
321 });
322 }
323
324 /**
325 * Update the state and notify all listeners, if changed.
326 *
327 * May be called from threads other than the main thread.
328 *
329 * @param state new state
330 */
331 public void setState(final State state)
332 {
333 notifyListeners(new Callable<Boolean>() {
334 @Override
335 public Boolean call() throws Exception
336 {
337 if (VpnStateService.this.mState != state)
338 {
339 VpnStateService.this.mState = state;
340 return true;
341 }
342 return false;
343 }
344 });
345 }
346
347 /**
348 * Set the current error state and notify all listeners, if changed.
349 *
350 * May be called from threads other than the main thread.
351 *
352 * @param error error state
353 */
354 public void setError(final ErrorState error)
355 {
356 notifyListeners(new Callable<Boolean>() {
357 @Override
358 public Boolean call() throws Exception
359 {
360 if (VpnStateService.this.mError != error)
361 {
362 VpnStateService.this.mError = error;
363 return true;
364 }
365 return false;
366 }
367 });
368 }
369
370 /**
371 * Set the current IMC state and notify all listeners, if changed.
372 *
373 * Setting the state to UNKNOWN clears all remediation instructions.
374 *
375 * May be called from threads other than the main thread.
376 *
377 * @param state IMC state
378 */
379 public void setImcState(final ImcState state)
380 {
381 notifyListeners(new Callable<Boolean>() {
382 @Override
383 public Boolean call() throws Exception
384 {
385 if (state == ImcState.UNKNOWN)
386 {
387 VpnStateService.this.mRemediationInstructions.clear();
388 }
389 if (VpnStateService.this.mImcState != state)
390 {
391 VpnStateService.this.mImcState = state;
392 return true;
393 }
394 return false;
395 }
396 });
397 }
398
399 /**
400 * Add the given remediation instruction to the internal list. Listeners
401 * are not notified.
402 *
403 * Instructions are cleared if the IMC state is set to UNKNOWN.
404 *
405 * May be called from threads other than the main thread.
406 *
407 * @param instruction remediation instruction
408 */
409 public void addRemediationInstruction(final RemediationInstruction instruction)
410 {
411 mHandler.post(new Runnable() {
412 @Override
413 public void run()
414 {
415 VpnStateService.this.mRemediationInstructions.add(instruction);
416 }
417 });
418 }
419 }