]> git.ipfire.org Git - thirdparty/strongswan.git/blobdiff - src/frontends/android/app/src/main/java/org/strongswan/android/logic/VpnStateService.java
android: Add an automatic reconnect on errors
[thirdparty/strongswan.git] / src / frontends / android / app / src / main / java / org / strongswan / android / logic / VpnStateService.java
index e35277d8c0ae087886e54fc8efe25b808e90cca2..a0db087bfe7c35cb1533c7a428115da31b46c37e 100644 (file)
@@ -1,6 +1,6 @@
 /*
- * 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;
@@ -43,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
        {
@@ -60,6 +70,8 @@ public class VpnStateService extends Service
                LOOKUP_FAILED,
                UNREACHABLE,
                GENERIC_ERROR,
+               PASSWORD_MISSING,
+               CERTIFICATE_UNAVAILABLE,
        }
 
        /**
@@ -88,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
@@ -146,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.
         *
@@ -166,6 +196,39 @@ public class VpnStateService extends Service
                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.
         *
@@ -193,14 +256,36 @@ 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
                 * 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);
        }
 
@@ -251,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;
@@ -300,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;
                                }
@@ -357,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();
+                       }
+               }
+       }
 }