/*
- * Copyright (C) 2012-2013 Tobias Brunner
- * Hochschule fuer Technik Rapperswil
+ * Copyright (C) 2012-2017 Tobias Brunner
+ * HSR Hochschule fuer Technik Rapperswil
*
* This program is free software; you can redistribute it and/or modify it
* under the terms of the GNU General Public License as published by the
package org.strongswan.android.logic;
-import java.util.ArrayList;
-import java.util.Collections;
-import java.util.LinkedList;
-import java.util.List;
-import java.util.concurrent.Callable;
-
-import org.strongswan.android.data.VpnProfile;
-import org.strongswan.android.logic.imc.ImcState;
-import org.strongswan.android.logic.imc.RemediationInstruction;
-
import android.app.Service;
import android.content.Context;
import android.content.Intent;
import android.os.Binder;
+import android.os.Bundle;
import android.os.Handler;
import android.os.IBinder;
+import android.os.Message;
+import android.os.SystemClock;
+
+import org.strongswan.android.R;
+import org.strongswan.android.data.VpnProfile;
+import org.strongswan.android.data.VpnProfileDataSource;
+import org.strongswan.android.logic.imc.ImcState;
+import org.strongswan.android.logic.imc.RemediationInstruction;
+
+import java.lang.ref.WeakReference;
+import java.util.Collections;
+import java.util.HashSet;
+import java.util.LinkedList;
+import java.util.List;
+import java.util.concurrent.Callable;
public class VpnStateService extends Service
{
- private final List<VpnStateListener> mListeners = new ArrayList<VpnStateListener>();
+ private final HashSet<VpnStateListener> mListeners = new HashSet<VpnStateListener>();
private final IBinder mBinder = new LocalBinder();
private long mConnectionID = 0;
private Handler mHandler;
private ErrorState mError = ErrorState.NO_ERROR;
private ImcState mImcState = ImcState.UNKNOWN;
private final LinkedList<RemediationInstruction> mRemediationInstructions = new LinkedList<RemediationInstruction>();
+ private static long RETRY_INTERVAL = 1000;
+ private static int RETRY_MSG = 1;
+ private long mRetryTimeout;
+ private long mRetryIn;
public enum State
{
LOOKUP_FAILED,
UNREACHABLE,
GENERIC_ERROR,
+ PASSWORD_MISSING,
+ CERTIFICATE_UNAVAILABLE,
}
/**
{
/* this handler allows us to notify listeners from the UI thread and
* not from the threads that actually report any state changes */
- mHandler = new Handler();
+ mHandler = new RetryHandler(this);
}
@Override
return mConnectionID;
}
+ /**
+ * Get the total number of seconds until there is an automatic retry to reconnect.
+ * @return total number of seconds until the retry
+ */
+ public int getRetryTimeout()
+ {
+ return (int)(mRetryTimeout / 1000);
+ }
+
+ /**
+ * Get the number of seconds until there is an automatic retry to reconnect.
+ * @return number of seconds until the retry
+ */
+ public int getRetryIn()
+ {
+ return (int)(mRetryIn / 1000);
+ }
+
/**
* Get the current state.
*
return mError;
}
+ /**
+ * Get a description of the current error, if any.
+ *
+ * @return error description text id
+ */
+ public int getErrorText()
+ {
+ switch (mError)
+ {
+ case AUTH_FAILED:
+ if (mImcState == ImcState.BLOCK)
+ {
+ return R.string.error_assessment_failed;
+ }
+ else
+ {
+ return R.string.error_auth_failed;
+ }
+ case PEER_AUTH_FAILED:
+ return R.string.error_peer_auth_failed;
+ case LOOKUP_FAILED:
+ return R.string.error_lookup_failed;
+ case UNREACHABLE:
+ return R.string.error_unreachable;
+ case PASSWORD_MISSING:
+ return R.string.error_password_missing;
+ case CERTIFICATE_UNAVAILABLE:
+ return R.string.error_certificate_unavailable;
+ default:
+ return R.string.error_generic;
+ }
+ }
+
/**
* Get the current IMC state, if any.
*
*/
public void disconnect()
{
+ resetRetryTimer();
/* as soon as the TUN device is created by calling establish() on the
* VpnService.Builder object the system binds to the service and keeps
* bound until the file descriptor of the TUN device is closed. thus
* calling stopService() here would not stop (destroy) the service yet,
- * instead we call startService() with an empty Intent which shuts down
+ * instead we call startService() with a specific action which shuts down
* the daemon (and closes the TUN device, if any) */
Context context = getApplicationContext();
Intent intent = new Intent(context, CharonVpnService.class);
+ intent.setAction(CharonVpnService.DISCONNECT_ACTION);
+ context.startService(intent);
+ }
+
+ /**
+ * Reconnect to the previous profile.
+ */
+ public void reconnect()
+ {
+ if (mProfile == null)
+ {
+ return;
+ }
+ Bundle profileInfo = new Bundle();
+ profileInfo.putLong(VpnProfileDataSource.KEY_ID, mProfile.getId());
+ /* pass the previous password along */
+ profileInfo.putString(VpnProfileDataSource.KEY_PASSWORD, mProfile.getPassword());
+ /* we assume we have the necessary permission */
+ Context context = getApplicationContext();
+ Intent intent = new Intent(context, CharonVpnService.class);
+ intent.putExtras(profileInfo);
context.startService(intent);
}
@Override
public Boolean call() throws Exception
{
+ resetRetryTimer();
VpnStateService.this.mConnectionID++;
VpnStateService.this.mProfile = profile;
VpnStateService.this.mState = State.CONNECTING;
{
if (VpnStateService.this.mError != error)
{
+ if (VpnStateService.this.mError == ErrorState.NO_ERROR)
+ {
+ setRetryTimer(error);
+ }
+ else if (error == ErrorState.NO_ERROR)
+ {
+ resetRetryTimer();
+ }
VpnStateService.this.mError = error;
return true;
}
}
});
}
+
+ /**
+ * Sets the retry timer
+ */
+ private void setRetryTimer(ErrorState error)
+ {
+ long timeout;
+
+ switch (error)
+ {
+ case AUTH_FAILED:
+ timeout = 20000;
+ break;
+ case PEER_AUTH_FAILED:
+ timeout = 20000;
+ break;
+ case LOOKUP_FAILED:
+ timeout = 10000;
+ break;
+ case UNREACHABLE:
+ timeout = 10000;
+ break;
+ case PASSWORD_MISSING:
+ /* this needs user intervention (entering the password) */
+ timeout = 0;
+ break;
+ case CERTIFICATE_UNAVAILABLE:
+ /* if this is because the device has to be unlocked we might be able to reconnect */
+ timeout = 10000;
+ break;
+ default:
+ timeout = 20000;
+ break;
+ }
+ mRetryTimeout = mRetryIn = timeout;
+ if (timeout <= 0)
+ {
+ return;
+ }
+ mHandler.sendMessageAtTime(mHandler.obtainMessage(RETRY_MSG), SystemClock.uptimeMillis() + RETRY_INTERVAL);
+ }
+
+ /**
+ * Reset the retry timer
+ */
+ private void resetRetryTimer()
+ {
+ mRetryTimeout = 0;
+ mRetryIn = 0;
+ }
+
+ /**
+ * Special Handler subclass that handles the retry countdown (more accurate than CountDownTimer)
+ */
+ private static class RetryHandler extends Handler {
+ WeakReference<VpnStateService> mService;
+
+ public RetryHandler(VpnStateService service)
+ {
+ mService = new WeakReference<>(service);
+ }
+
+ @Override
+ public void handleMessage(Message msg)
+ {
+ /* handle retry countdown */
+ if (mService.get().mRetryTimeout <= 0)
+ {
+ return;
+ }
+ mService.get().mRetryIn -= RETRY_INTERVAL;
+ if (mService.get().mRetryIn > 0)
+ {
+ /* calculate next interval before notifying listeners */
+ long next = SystemClock.uptimeMillis() + RETRY_INTERVAL;
+
+ for (VpnStateListener listener : mService.get().mListeners)
+ {
+ listener.stateChanged();
+ }
+ sendMessageAtTime(obtainMessage(RETRY_MSG), next);
+ }
+ else
+ {
+ mService.get().reconnect();
+ }
+ }
+ }
}