From 68afdd3464856a46b1cd05b24a0a133b78aed203 Mon Sep 17 00:00:00 2001 From: Tobias Brunner Date: Fri, 15 Jun 2018 14:40:01 +0200 Subject: [PATCH] android: Add an automatic reconnect on errors This way the connection will be attempted to be kept up even on "fatal" errors like authentication failures. --- .../android/logic/VpnStateService.java | 125 +++++++++++++++++- .../android/ui/VpnStateFragment.java | 50 ++++--- .../main/res/layout/vpn_state_fragment.xml | 5 +- .../app/src/main/res/values-de/strings.xml | 5 + .../app/src/main/res/values-pl/strings.xml | 5 + .../app/src/main/res/values-ru/strings.xml | 5 + .../app/src/main/res/values-ua/strings.xml | 5 + .../src/main/res/values-zh-rCN/strings.xml | 5 + .../src/main/res/values-zh-rTW/strings.xml | 5 + .../app/src/main/res/values/strings.xml | 5 + 10 files changed, 191 insertions(+), 24 deletions(-) diff --git a/src/frontends/android/app/src/main/java/org/strongswan/android/logic/VpnStateService.java b/src/frontends/android/app/src/main/java/org/strongswan/android/logic/VpnStateService.java index 9f11e705b1..a0db087bfe 100644 --- a/src/frontends/android/app/src/main/java/org/strongswan/android/logic/VpnStateService.java +++ b/src/frontends/android/app/src/main/java/org/strongswan/android/logic/VpnStateService.java @@ -22,6 +22,8 @@ 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; @@ -29,6 +31,7 @@ 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; @@ -46,6 +49,10 @@ public class VpnStateService extends Service private ErrorState mError = ErrorState.NO_ERROR; private ImcState mImcState = ImcState.UNKNOWN; private final LinkedList mRemediationInstructions = new LinkedList(); + private static long RETRY_INTERVAL = 1000; + private static int RETRY_MSG = 1; + private long mRetryTimeout; + private long mRetryIn; public enum State { @@ -93,7 +100,7 @@ public class VpnStateService extends Service { /* 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 @@ -151,6 +158,24 @@ public class VpnStateService extends Service 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. * @@ -231,6 +256,7 @@ public class VpnStateService extends Service */ 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 @@ -310,6 +336,7 @@ public class VpnStateService extends Service @Override public Boolean call() throws Exception { + resetRetryTimer(); VpnStateService.this.mConnectionID++; VpnStateService.this.mProfile = profile; VpnStateService.this.mState = State.CONNECTING; @@ -359,6 +386,14 @@ public class VpnStateService extends Service { 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; } @@ -416,4 +451,92 @@ public class VpnStateService extends Service } }); } + + /** + * 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 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(); + } + } + } } diff --git a/src/frontends/android/app/src/main/java/org/strongswan/android/ui/VpnStateFragment.java b/src/frontends/android/app/src/main/java/org/strongswan/android/ui/VpnStateFragment.java index 38549cb92c..4042eb7613 100644 --- a/src/frontends/android/app/src/main/java/org/strongswan/android/ui/VpnStateFragment.java +++ b/src/frontends/android/app/src/main/java/org/strongswan/android/ui/VpnStateFragment.java @@ -41,11 +41,6 @@ import org.strongswan.android.logic.VpnStateService; import org.strongswan.android.logic.VpnStateService.ErrorState; import org.strongswan.android.logic.VpnStateService.State; import org.strongswan.android.logic.VpnStateService.VpnStateListener; -import org.strongswan.android.logic.imc.ImcState; -import org.strongswan.android.logic.imc.RemediationInstruction; - -import java.util.ArrayList; -import java.util.List; public class VpnStateFragment extends Fragment implements VpnStateListener { @@ -62,7 +57,7 @@ public class VpnStateFragment extends Fragment implements VpnStateListener private LinearLayout mErrorView; private TextView mErrorText; private Button mErrorRetry; - private Button mDismissError; + private Button mShowLog; private long mErrorConnectionID; private VpnStateService mService; private final ServiceConnection mServiceConnection = new ServiceConnection() @@ -128,12 +123,13 @@ public class VpnStateFragment extends Fragment implements VpnStateListener View view = inflater.inflate(R.layout.vpn_state_fragment, null); mActionButton = (Button)view.findViewById(R.id.action); + mActionButton.setOnClickListener(v -> clearError()); enableActionButton(null); mErrorView = view.findViewById(R.id.vpn_error); mErrorText = view.findViewById(R.id.vpn_error_text); mErrorRetry = view.findViewById(R.id.retry); - mDismissError = view.findViewById(R.id.dismiss_error); + mShowLog = view.findViewById(R.id.show_log); mProgress = (ProgressBar)view.findViewById(R.id.progress); mStateView = (TextView)view.findViewById(R.id.vpn_state); mColorStateBase = mStateView.getCurrentTextColor(); @@ -146,7 +142,10 @@ public class VpnStateFragment extends Fragment implements VpnStateListener mService.reconnect(); } }); - mDismissError.setOnClickListener(v -> clearError()); + mShowLog.setOnClickListener(v -> { + Intent intent = new Intent(getActivity(), LogActivity.class); + startActivity(intent); + }); return view; } @@ -194,7 +193,6 @@ public class VpnStateFragment extends Fragment implements VpnStateListener VpnProfile profile = mService.getProfile(); State state = mService.getState(); ErrorState error = mService.getErrorState(); - ImcState imcState = mService.getImcState(); String name = ""; if (getActivity() == null) @@ -207,12 +205,13 @@ public class VpnStateFragment extends Fragment implements VpnStateListener name = profile.getName(); } - if (reportError(connectionID, name, error, imcState)) + if (reportError(connectionID, name, error)) { return; } mProfileNameView.setText(name); + mProgress.setIndeterminate(true); switch (state) { @@ -247,7 +246,7 @@ public class VpnStateFragment extends Fragment implements VpnStateListener } } - private boolean reportError(long connectionID, String name, ErrorState error, ImcState imcState) + private boolean reportError(long connectionID, String name, ErrorState error) { if (error == ErrorState.NO_ERROR) { @@ -257,15 +256,26 @@ public class VpnStateFragment extends Fragment implements VpnStateListener mErrorConnectionID = connectionID; mProfileNameView.setText(name); showProfile(true); - mProgress.setVisibility(View.GONE); mStateView.setText(R.string.state_error); mStateView.setTextColor(mColorStateError); - enableActionButton(getString(R.string.show_log)); - mActionButton.setOnClickListener(v -> { - Intent intent = new Intent(getActivity(), LogActivity.class); - startActivity(intent); - }); - mErrorText.setText(getString(R.string.error_format, getString(mService.getErrorText()))); + enableActionButton(getString(android.R.string.cancel)); + + int retry = mService.getRetryIn(); + if (retry > 0) + { + mProgress.setIndeterminate(false); + mProgress.setMax(mService.getRetryTimeout()); + mProgress.setProgress(retry); + mProgress.setVisibility(View.VISIBLE); + mStateView.setText(getResources().getQuantityString(R.plurals.retry_in, retry, retry)); + } + else if (mService.getRetryTimeout() <= 0) + { + mProgress.setVisibility(View.GONE); + } + + String text = getString(R.string.error_format, getString(mService.getErrorText())); + mErrorText.setText(text); mErrorView.setVisibility(View.VISIBLE); return true; } @@ -281,19 +291,17 @@ public class VpnStateFragment extends Fragment implements VpnStateListener mActionButton.setText(text); mActionButton.setEnabled(text != null); mActionButton.setVisibility(text != null ? View.VISIBLE : View.GONE); - mActionButton.setOnClickListener(mDisconnectListener); } private void clearError() { if (mService != null) { + mService.disconnect(); if (mService.getConnectionID() == mErrorConnectionID) { - mService.disconnect(); mService.setError(ErrorState.NO_ERROR); } } - updateView(); } } diff --git a/src/frontends/android/app/src/main/res/layout/vpn_state_fragment.xml b/src/frontends/android/app/src/main/res/layout/vpn_state_fragment.xml index adc9c8665b..fece125dd3 100644 --- a/src/frontends/android/app/src/main/res/layout/vpn_state_fragment.xml +++ b/src/frontends/android/app/src/main/res/layout/vpn_state_fragment.xml @@ -34,6 +34,7 @@ android:layout_width="wrap_content" android:layout_height="wrap_content" android:layout_marginLeft="20dp" + android:layout_marginRight="20dp" android:layout_marginTop="24dp" android:layout_marginBottom="12dp" android:text="Failed to establish VPN: Server is unreachable" @@ -48,11 +49,11 @@ android:gravity="end" >