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.
This commit is contained in:
+124
-1
@@ -22,6 +22,8 @@ import android.os.Binder;
|
|||||||
import android.os.Bundle;
|
import android.os.Bundle;
|
||||||
import android.os.Handler;
|
import android.os.Handler;
|
||||||
import android.os.IBinder;
|
import android.os.IBinder;
|
||||||
|
import android.os.Message;
|
||||||
|
import android.os.SystemClock;
|
||||||
|
|
||||||
import org.strongswan.android.R;
|
import org.strongswan.android.R;
|
||||||
import org.strongswan.android.data.VpnProfile;
|
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.ImcState;
|
||||||
import org.strongswan.android.logic.imc.RemediationInstruction;
|
import org.strongswan.android.logic.imc.RemediationInstruction;
|
||||||
|
|
||||||
|
import java.lang.ref.WeakReference;
|
||||||
import java.util.Collections;
|
import java.util.Collections;
|
||||||
import java.util.HashSet;
|
import java.util.HashSet;
|
||||||
import java.util.LinkedList;
|
import java.util.LinkedList;
|
||||||
@@ -46,6 +49,10 @@ public class VpnStateService extends Service
|
|||||||
private ErrorState mError = ErrorState.NO_ERROR;
|
private ErrorState mError = ErrorState.NO_ERROR;
|
||||||
private ImcState mImcState = ImcState.UNKNOWN;
|
private ImcState mImcState = ImcState.UNKNOWN;
|
||||||
private final LinkedList<RemediationInstruction> mRemediationInstructions = new LinkedList<RemediationInstruction>();
|
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
|
public enum State
|
||||||
{
|
{
|
||||||
@@ -93,7 +100,7 @@ public class VpnStateService extends Service
|
|||||||
{
|
{
|
||||||
/* this handler allows us to notify listeners from the UI thread and
|
/* this handler allows us to notify listeners from the UI thread and
|
||||||
* not from the threads that actually report any state changes */
|
* not from the threads that actually report any state changes */
|
||||||
mHandler = new Handler();
|
mHandler = new RetryHandler(this);
|
||||||
}
|
}
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
@@ -151,6 +158,24 @@ public class VpnStateService extends Service
|
|||||||
return mConnectionID;
|
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.
|
* Get the current state.
|
||||||
*
|
*
|
||||||
@@ -231,6 +256,7 @@ public class VpnStateService extends Service
|
|||||||
*/
|
*/
|
||||||
public void disconnect()
|
public void disconnect()
|
||||||
{
|
{
|
||||||
|
resetRetryTimer();
|
||||||
/* as soon as the TUN device is created by calling establish() on the
|
/* as soon as the TUN device is created by calling establish() on the
|
||||||
* VpnService.Builder object the system binds to the service and keeps
|
* VpnService.Builder object the system binds to the service and keeps
|
||||||
* bound until the file descriptor of the TUN device is closed. thus
|
* bound until the file descriptor of the TUN device is closed. thus
|
||||||
@@ -310,6 +336,7 @@ public class VpnStateService extends Service
|
|||||||
@Override
|
@Override
|
||||||
public Boolean call() throws Exception
|
public Boolean call() throws Exception
|
||||||
{
|
{
|
||||||
|
resetRetryTimer();
|
||||||
VpnStateService.this.mConnectionID++;
|
VpnStateService.this.mConnectionID++;
|
||||||
VpnStateService.this.mProfile = profile;
|
VpnStateService.this.mProfile = profile;
|
||||||
VpnStateService.this.mState = State.CONNECTING;
|
VpnStateService.this.mState = State.CONNECTING;
|
||||||
@@ -359,6 +386,14 @@ public class VpnStateService extends Service
|
|||||||
{
|
{
|
||||||
if (VpnStateService.this.mError != error)
|
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;
|
VpnStateService.this.mError = error;
|
||||||
return true;
|
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();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
+29
-21
@@ -41,11 +41,6 @@ import org.strongswan.android.logic.VpnStateService;
|
|||||||
import org.strongswan.android.logic.VpnStateService.ErrorState;
|
import org.strongswan.android.logic.VpnStateService.ErrorState;
|
||||||
import org.strongswan.android.logic.VpnStateService.State;
|
import org.strongswan.android.logic.VpnStateService.State;
|
||||||
import org.strongswan.android.logic.VpnStateService.VpnStateListener;
|
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
|
public class VpnStateFragment extends Fragment implements VpnStateListener
|
||||||
{
|
{
|
||||||
@@ -62,7 +57,7 @@ public class VpnStateFragment extends Fragment implements VpnStateListener
|
|||||||
private LinearLayout mErrorView;
|
private LinearLayout mErrorView;
|
||||||
private TextView mErrorText;
|
private TextView mErrorText;
|
||||||
private Button mErrorRetry;
|
private Button mErrorRetry;
|
||||||
private Button mDismissError;
|
private Button mShowLog;
|
||||||
private long mErrorConnectionID;
|
private long mErrorConnectionID;
|
||||||
private VpnStateService mService;
|
private VpnStateService mService;
|
||||||
private final ServiceConnection mServiceConnection = new ServiceConnection()
|
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);
|
View view = inflater.inflate(R.layout.vpn_state_fragment, null);
|
||||||
|
|
||||||
mActionButton = (Button)view.findViewById(R.id.action);
|
mActionButton = (Button)view.findViewById(R.id.action);
|
||||||
|
mActionButton.setOnClickListener(v -> clearError());
|
||||||
enableActionButton(null);
|
enableActionButton(null);
|
||||||
|
|
||||||
mErrorView = view.findViewById(R.id.vpn_error);
|
mErrorView = view.findViewById(R.id.vpn_error);
|
||||||
mErrorText = view.findViewById(R.id.vpn_error_text);
|
mErrorText = view.findViewById(R.id.vpn_error_text);
|
||||||
mErrorRetry = view.findViewById(R.id.retry);
|
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);
|
mProgress = (ProgressBar)view.findViewById(R.id.progress);
|
||||||
mStateView = (TextView)view.findViewById(R.id.vpn_state);
|
mStateView = (TextView)view.findViewById(R.id.vpn_state);
|
||||||
mColorStateBase = mStateView.getCurrentTextColor();
|
mColorStateBase = mStateView.getCurrentTextColor();
|
||||||
@@ -146,7 +142,10 @@ public class VpnStateFragment extends Fragment implements VpnStateListener
|
|||||||
mService.reconnect();
|
mService.reconnect();
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
mDismissError.setOnClickListener(v -> clearError());
|
mShowLog.setOnClickListener(v -> {
|
||||||
|
Intent intent = new Intent(getActivity(), LogActivity.class);
|
||||||
|
startActivity(intent);
|
||||||
|
});
|
||||||
|
|
||||||
return view;
|
return view;
|
||||||
}
|
}
|
||||||
@@ -194,7 +193,6 @@ public class VpnStateFragment extends Fragment implements VpnStateListener
|
|||||||
VpnProfile profile = mService.getProfile();
|
VpnProfile profile = mService.getProfile();
|
||||||
State state = mService.getState();
|
State state = mService.getState();
|
||||||
ErrorState error = mService.getErrorState();
|
ErrorState error = mService.getErrorState();
|
||||||
ImcState imcState = mService.getImcState();
|
|
||||||
String name = "";
|
String name = "";
|
||||||
|
|
||||||
if (getActivity() == null)
|
if (getActivity() == null)
|
||||||
@@ -207,12 +205,13 @@ public class VpnStateFragment extends Fragment implements VpnStateListener
|
|||||||
name = profile.getName();
|
name = profile.getName();
|
||||||
}
|
}
|
||||||
|
|
||||||
if (reportError(connectionID, name, error, imcState))
|
if (reportError(connectionID, name, error))
|
||||||
{
|
{
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
mProfileNameView.setText(name);
|
mProfileNameView.setText(name);
|
||||||
|
mProgress.setIndeterminate(true);
|
||||||
|
|
||||||
switch (state)
|
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)
|
if (error == ErrorState.NO_ERROR)
|
||||||
{
|
{
|
||||||
@@ -257,15 +256,26 @@ public class VpnStateFragment extends Fragment implements VpnStateListener
|
|||||||
mErrorConnectionID = connectionID;
|
mErrorConnectionID = connectionID;
|
||||||
mProfileNameView.setText(name);
|
mProfileNameView.setText(name);
|
||||||
showProfile(true);
|
showProfile(true);
|
||||||
mProgress.setVisibility(View.GONE);
|
|
||||||
mStateView.setText(R.string.state_error);
|
mStateView.setText(R.string.state_error);
|
||||||
mStateView.setTextColor(mColorStateError);
|
mStateView.setTextColor(mColorStateError);
|
||||||
enableActionButton(getString(R.string.show_log));
|
enableActionButton(getString(android.R.string.cancel));
|
||||||
mActionButton.setOnClickListener(v -> {
|
|
||||||
Intent intent = new Intent(getActivity(), LogActivity.class);
|
int retry = mService.getRetryIn();
|
||||||
startActivity(intent);
|
if (retry > 0)
|
||||||
});
|
{
|
||||||
mErrorText.setText(getString(R.string.error_format, getString(mService.getErrorText())));
|
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);
|
mErrorView.setVisibility(View.VISIBLE);
|
||||||
return true;
|
return true;
|
||||||
}
|
}
|
||||||
@@ -281,19 +291,17 @@ public class VpnStateFragment extends Fragment implements VpnStateListener
|
|||||||
mActionButton.setText(text);
|
mActionButton.setText(text);
|
||||||
mActionButton.setEnabled(text != null);
|
mActionButton.setEnabled(text != null);
|
||||||
mActionButton.setVisibility(text != null ? View.VISIBLE : View.GONE);
|
mActionButton.setVisibility(text != null ? View.VISIBLE : View.GONE);
|
||||||
mActionButton.setOnClickListener(mDisconnectListener);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
private void clearError()
|
private void clearError()
|
||||||
{
|
{
|
||||||
if (mService != null)
|
if (mService != null)
|
||||||
{
|
{
|
||||||
|
mService.disconnect();
|
||||||
if (mService.getConnectionID() == mErrorConnectionID)
|
if (mService.getConnectionID() == mErrorConnectionID)
|
||||||
{
|
{
|
||||||
mService.disconnect();
|
|
||||||
mService.setError(ErrorState.NO_ERROR);
|
mService.setError(ErrorState.NO_ERROR);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
updateView();
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -34,6 +34,7 @@
|
|||||||
android:layout_width="wrap_content"
|
android:layout_width="wrap_content"
|
||||||
android:layout_height="wrap_content"
|
android:layout_height="wrap_content"
|
||||||
android:layout_marginLeft="20dp"
|
android:layout_marginLeft="20dp"
|
||||||
|
android:layout_marginRight="20dp"
|
||||||
android:layout_marginTop="24dp"
|
android:layout_marginTop="24dp"
|
||||||
android:layout_marginBottom="12dp"
|
android:layout_marginBottom="12dp"
|
||||||
android:text="Failed to establish VPN: Server is unreachable"
|
android:text="Failed to establish VPN: Server is unreachable"
|
||||||
@@ -48,11 +49,11 @@
|
|||||||
android:gravity="end" >
|
android:gravity="end" >
|
||||||
|
|
||||||
<Button
|
<Button
|
||||||
android:id="@+id/dismiss_error"
|
android:id="@+id/show_log"
|
||||||
android:layout_width="wrap_content"
|
android:layout_width="wrap_content"
|
||||||
android:layout_height="wrap_content"
|
android:layout_height="wrap_content"
|
||||||
android:layout_marginLeft="8dp"
|
android:layout_marginLeft="8dp"
|
||||||
android:text="@string/dismiss"
|
android:text="@string/show_log"
|
||||||
android:textColor="@color/primary"
|
android:textColor="@color/primary"
|
||||||
android:textSize="14sp"
|
android:textSize="14sp"
|
||||||
android:textStyle="bold"
|
android:textStyle="bold"
|
||||||
|
|||||||
@@ -198,6 +198,11 @@
|
|||||||
<string name="disconnect_active_connection">Dies trennt die aktuelle VPN Verbindung!</string>
|
<string name="disconnect_active_connection">Dies trennt die aktuelle VPN Verbindung!</string>
|
||||||
<string name="connect">Verbinden</string>
|
<string name="connect">Verbinden</string>
|
||||||
<string name="retry">Wiederholen</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 -->
|
<!-- Quick Settings tile -->
|
||||||
<string name="tile_default">VPN umschalten</string>
|
<string name="tile_default">VPN umschalten</string>
|
||||||
|
|||||||
@@ -198,6 +198,11 @@
|
|||||||
<string name="disconnect_active_connection">This will disconnect the active VPN connection!</string>
|
<string name="disconnect_active_connection">This will disconnect the active VPN connection!</string>
|
||||||
<string name="connect">Połącz</string>
|
<string name="connect">Połącz</string>
|
||||||
<string name="retry">Retry</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 -->
|
<!-- Quick Settings tile -->
|
||||||
<string name="tile_default">Toggle VPN</string>
|
<string name="tile_default">Toggle VPN</string>
|
||||||
|
|||||||
@@ -195,6 +195,11 @@
|
|||||||
<string name="disconnect_active_connection">This will disconnect the active VPN connection!</string>
|
<string name="disconnect_active_connection">This will disconnect the active VPN connection!</string>
|
||||||
<string name="connect">Соединить</string>
|
<string name="connect">Соединить</string>
|
||||||
<string name="retry">Retry</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 -->
|
<!-- Quick Settings tile -->
|
||||||
<string name="tile_default">Toggle VPN</string>
|
<string name="tile_default">Toggle VPN</string>
|
||||||
|
|||||||
@@ -196,6 +196,11 @@
|
|||||||
<string name="disconnect_active_connection">This will disconnect the active VPN connection!</string>
|
<string name="disconnect_active_connection">This will disconnect the active VPN connection!</string>
|
||||||
<string name="connect">Підключити</string>
|
<string name="connect">Підключити</string>
|
||||||
<string name="retry">Retry</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 -->
|
<!-- Quick Settings tile -->
|
||||||
<string name="tile_default">Toggle VPN</string>
|
<string name="tile_default">Toggle VPN</string>
|
||||||
|
|||||||
@@ -195,6 +195,11 @@
|
|||||||
<string name="disconnect_active_connection">This will disconnect the active VPN connection!</string>
|
<string name="disconnect_active_connection">This will disconnect the active VPN connection!</string>
|
||||||
<string name="connect">连接</string>
|
<string name="connect">连接</string>
|
||||||
<string name="retry">Retry</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 -->
|
<!-- Quick Settings tile -->
|
||||||
<string name="tile_default">Toggle VPN</string>
|
<string name="tile_default">Toggle VPN</string>
|
||||||
|
|||||||
@@ -195,6 +195,11 @@
|
|||||||
<string name="disconnect_active_connection">This will disconnect the active VPN connection!</string>
|
<string name="disconnect_active_connection">This will disconnect the active VPN connection!</string>
|
||||||
<string name="connect">連線</string>
|
<string name="connect">連線</string>
|
||||||
<string name="retry">Retry</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 -->
|
<!-- Quick Settings tile -->
|
||||||
<string name="tile_default">Toggle VPN</string>
|
<string name="tile_default">Toggle VPN</string>
|
||||||
|
|||||||
@@ -198,6 +198,11 @@
|
|||||||
<string name="disconnect_active_connection">This will disconnect the active VPN connection!</string>
|
<string name="disconnect_active_connection">This will disconnect the active VPN connection!</string>
|
||||||
<string name="connect">Connect</string>
|
<string name="connect">Connect</string>
|
||||||
<string name="retry">Retry</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 -->
|
<!-- Quick Settings tile -->
|
||||||
<string name="tile_default">Toggle VPN</string>
|
<string name="tile_default">Toggle VPN</string>
|
||||||
|
|||||||
Reference in New Issue
Block a user