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