android: Add an automatic reconnect on errors
authorTobias Brunner <tobias@strongswan.org>
Fri, 15 Jun 2018 12:40:01 +0000 (14:40 +0200)
committerTobias Brunner <tobias@strongswan.org>
Tue, 3 Jul 2018 09:31:38 +0000 (11:31 +0200)
This way the connection will be attempted to be kept up even on "fatal"
errors like authentication failures.

src/frontends/android/app/src/main/java/org/strongswan/android/logic/VpnStateService.java
src/frontends/android/app/src/main/java/org/strongswan/android/ui/VpnStateFragment.java
src/frontends/android/app/src/main/res/layout/vpn_state_fragment.xml
src/frontends/android/app/src/main/res/values-de/strings.xml
src/frontends/android/app/src/main/res/values-pl/strings.xml
src/frontends/android/app/src/main/res/values-ru/strings.xml
src/frontends/android/app/src/main/res/values-ua/strings.xml
src/frontends/android/app/src/main/res/values-zh-rCN/strings.xml
src/frontends/android/app/src/main/res/values-zh-rTW/strings.xml
src/frontends/android/app/src/main/res/values/strings.xml

index 9f11e70..a0db087 100644 (file)
@@ -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<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
        {
@@ -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
@@ -152,6 +159,24 @@ public class VpnStateService extends Service
        }
 
        /**
+        * 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 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<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();
+                       }
+               }
+       }
 }
index 38549cb..4042eb7 100644 (file)
@@ -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();
        }
 }
index adc9c86..fece125 100644 (file)
@@ -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"
             android:gravity="end" >
 
             <Button
-                android:id="@+id/dismiss_error"
+                android:id="@+id/show_log"
                 android:layout_width="wrap_content"
                 android:layout_height="wrap_content"
                 android:layout_marginLeft="8dp"
-                android:text="@string/dismiss"
+                android:text="@string/show_log"
                 android:textColor="@color/primary"
                 android:textSize="14sp"
                 android:textStyle="bold"
index 7bfaac5..4dc812c 100644 (file)
     <string name="disconnect_active_connection">Dies trennt die aktuelle VPN Verbindung!</string>
     <string name="connect">Verbinden</string>
     <string name="retry">Wiederholen</string>
+    <plurals name="retry_in">
+        <item quantity="one">Wiederholen in %1$d Sekunde</item>
+        <item quantity="other">Wiederholen in %1$d Sekunden</item>
+    </plurals>
+    <string name="cancel_retry">Wiederholen abbrechen</string>
 
     <!-- Quick Settings tile -->
     <string name="tile_default">VPN umschalten</string>
index 962fb6e..d4cf10e 100644 (file)
     <string name="disconnect_active_connection">This will disconnect the active VPN connection!</string>
     <string name="connect">Połącz</string>
     <string name="retry">Retry</string>
+    <plurals name="retry_in">
+        <item quantity="one">Retry in %1$d second</item>
+        <item quantity="other">Retry in %1$d seconds</item>
+    </plurals>
+    <string name="cancel_retry">Cancel retry</string>
 
     <!-- Quick Settings tile -->
     <string name="tile_default">Toggle VPN</string>
index 23f6416..be34c2d 100644 (file)
     <string name="disconnect_active_connection">This will disconnect the active VPN connection!</string>
     <string name="connect">Соединить</string>
     <string name="retry">Retry</string>
+    <plurals name="retry_in">
+        <item quantity="one">Retry in %1$d second</item>
+        <item quantity="other">Retry in %1$d seconds</item>
+    </plurals>
+    <string name="cancel_retry">Cancel retry</string>
 
     <!-- Quick Settings tile -->
     <string name="tile_default">Toggle VPN</string>
index 731ca46..faad4a7 100644 (file)
     <string name="disconnect_active_connection">This will disconnect the active VPN connection!</string>
     <string name="connect">Підключити</string>
     <string name="retry">Retry</string>
+    <plurals name="retry_in">
+        <item quantity="one">Retry in %1$d second</item>
+        <item quantity="other">Retry in %1$d seconds</item>
+    </plurals>
+    <string name="cancel_retry">Cancel retry</string>
 
     <!-- Quick Settings tile -->
     <string name="tile_default">Toggle VPN</string>
index 65b12ad..5b350fa 100644 (file)
     <string name="disconnect_active_connection">This will disconnect the active VPN connection!</string>
     <string name="connect">连接</string>
     <string name="retry">Retry</string>
+    <plurals name="retry_in">
+        <item quantity="one">Retry in %1$d second</item>
+        <item quantity="other">Retry in %1$d seconds</item>
+    </plurals>
+    <string name="cancel_retry">Cancel retry</string>
 
     <!-- Quick Settings tile -->
     <string name="tile_default">Toggle VPN</string>
index d142318..4d5afc7 100644 (file)
     <string name="disconnect_active_connection">This will disconnect the active VPN connection!</string>
     <string name="connect">連線</string>
     <string name="retry">Retry</string>
+    <plurals name="retry_in">
+        <item quantity="one">Retry in %1$d second</item>
+        <item quantity="other">Retry in %1$d seconds</item>
+    </plurals>
+    <string name="cancel_retry">Cancel retry</string>
 
     <!-- Quick Settings tile -->
     <string name="tile_default">Toggle VPN</string>
index dd675c4..c4b697b 100644 (file)
     <string name="disconnect_active_connection">This will disconnect the active VPN connection!</string>
     <string name="connect">Connect</string>
     <string name="retry">Retry</string>
+    <plurals name="retry_in">
+        <item quantity="one">Retry in %1$d second</item>
+        <item quantity="other">Retry in %1$d seconds</item>
+    </plurals>
+    <string name="cancel_retry">Cancel retry</string>
 
     <!-- Quick Settings tile -->
     <string name="tile_default">Toggle VPN</string>