From 761b604e14b14a86a357816b266e77d458137c83 Mon Sep 17 00:00:00 2001 From: cyBerta Date: Fri, 25 Oct 2019 17:10:13 +0200 Subject: implement error handling for edge case when Android throws an nullpointer exception while it tries to prepare the VpnService --- app/src/main/java/de/blinkt/openvpn/LaunchVPN.java | 13 +++- .../main/java/se/leap/bitmaskclient/Constants.java | 1 + .../se/leap/bitmaskclient/EipSetupObserver.java | 8 +++ .../java/se/leap/bitmaskclient/MainActivity.java | 34 +++++++--- .../bitmaskclient/MainActivityErrorDialog.java | 49 ++++++++------ .../main/java/se/leap/bitmaskclient/eip/EIP.java | 67 +++---------------- .../leap/bitmaskclient/eip/EipResultBroadcast.java | 77 ++++++++++++++++++++++ app/src/main/res/values/strings.xml | 1 + 8 files changed, 165 insertions(+), 85 deletions(-) create mode 100644 app/src/main/java/se/leap/bitmaskclient/eip/EipResultBroadcast.java (limited to 'app/src/main') diff --git a/app/src/main/java/de/blinkt/openvpn/LaunchVPN.java b/app/src/main/java/de/blinkt/openvpn/LaunchVPN.java index 60e4abb6..70f8e445 100644 --- a/app/src/main/java/de/blinkt/openvpn/LaunchVPN.java +++ b/app/src/main/java/de/blinkt/openvpn/LaunchVPN.java @@ -26,7 +26,9 @@ import de.blinkt.openvpn.core.VpnStatus; import se.leap.bitmaskclient.MainActivity; import se.leap.bitmaskclient.R; +import static se.leap.bitmaskclient.Constants.EIP_ACTION_PREPARE_VPN; import static se.leap.bitmaskclient.Constants.PROVIDER_PROFILE; +import static se.leap.bitmaskclient.eip.EipResultBroadcast.tellToReceiverOrBroadcast; /** * This Activity actually handles two stages of a launcher shortcut's life cycle. @@ -62,6 +64,7 @@ public class LaunchVPN extends Activity { private static final int START_VPN_PROFILE = 70; + private static final String TAG = LaunchVPN.class.getName(); private VpnProfile mSelectedProfile; @@ -180,7 +183,15 @@ public class LaunchVPN extends Activity { return; } - Intent intent = VpnService.prepare(this); + Intent intent = null; + try { + intent = VpnService.prepare(this.getApplicationContext()); + } catch (NullPointerException npe) { + tellToReceiverOrBroadcast(this.getApplicationContext(), EIP_ACTION_PREPARE_VPN, RESULT_CANCELED); + finish(); + return; + } + // Check if we want to fix /dev/tun SharedPreferences prefs = Preferences.getDefaultSharedPreferences(this); boolean usecm9fix = prefs.getBoolean("useCM9Fix", false); diff --git a/app/src/main/java/se/leap/bitmaskclient/Constants.java b/app/src/main/java/se/leap/bitmaskclient/Constants.java index 720cd1c4..d3c09f08 100644 --- a/app/src/main/java/se/leap/bitmaskclient/Constants.java +++ b/app/src/main/java/se/leap/bitmaskclient/Constants.java @@ -51,6 +51,7 @@ public interface Constants { String EIP_ACTION_START_ALWAYS_ON_VPN = "se.leap.bitmaskclient.START_ALWAYS_ON_VPN"; String EIP_ACTION_START_BLOCKING_VPN = "se.leap.bitmaskclient.EIP_ACTION_START_BLOCKING_VPN"; String EIP_ACTION_STOP_BLOCKING_VPN = "se.leap.bitmaskclient.EIP_ACTION_STOP_BLOCKING_VPN"; + String EIP_ACTION_PREPARE_VPN = "se.leap.bitmaskclient.EIP_ACTION_PREPARE_VPN"; String EIP_RECEIVER = "EIP.RECEIVER"; String EIP_REQUEST = "EIP.REQUEST"; diff --git a/app/src/main/java/se/leap/bitmaskclient/EipSetupObserver.java b/app/src/main/java/se/leap/bitmaskclient/EipSetupObserver.java index 7ded9069..53bd9002 100644 --- a/app/src/main/java/se/leap/bitmaskclient/EipSetupObserver.java +++ b/app/src/main/java/se/leap/bitmaskclient/EipSetupObserver.java @@ -34,6 +34,7 @@ import static se.leap.bitmaskclient.Constants.BROADCAST_GATEWAY_SETUP_OBSERVER_E import static se.leap.bitmaskclient.Constants.BROADCAST_PROVIDER_API_EVENT; import static se.leap.bitmaskclient.Constants.BROADCAST_RESULT_CODE; import static se.leap.bitmaskclient.Constants.BROADCAST_RESULT_KEY; +import static se.leap.bitmaskclient.Constants.EIP_ACTION_PREPARE_VPN; import static se.leap.bitmaskclient.Constants.EIP_ACTION_START; import static se.leap.bitmaskclient.Constants.EIP_ACTION_START_ALWAYS_ON_VPN; import static se.leap.bitmaskclient.Constants.EIP_REQUEST; @@ -176,6 +177,13 @@ class EipSetupObserver extends BroadcastReceiver implements VpnStatus.StateListe EipStatus.refresh(); } break; + case EIP_ACTION_PREPARE_VPN: + if (resultCode == RESULT_CANCELED) { + VpnStatus.logError("Error preparing VpnService."); + finishGatewaySetup(false); + EipStatus.refresh(); + } + break; default: break; } diff --git a/app/src/main/java/se/leap/bitmaskclient/MainActivity.java b/app/src/main/java/se/leap/bitmaskclient/MainActivity.java index ad7c8ae3..39c4a8f1 100644 --- a/app/src/main/java/se/leap/bitmaskclient/MainActivity.java +++ b/app/src/main/java/se/leap/bitmaskclient/MainActivity.java @@ -35,6 +35,7 @@ import java.util.Observable; import java.util.Observer; import se.leap.bitmaskclient.drawer.NavigationDrawerFragment; +import se.leap.bitmaskclient.eip.EIP; import se.leap.bitmaskclient.eip.EipCommand; import se.leap.bitmaskclient.fragments.ExcludeAppsFragment; import se.leap.bitmaskclient.fragments.LogFragment; @@ -43,6 +44,7 @@ import se.leap.bitmaskclient.utils.PreferenceHelper; import static se.leap.bitmaskclient.Constants.ASK_TO_CANCEL_VPN; import static se.leap.bitmaskclient.Constants.BROADCAST_RESULT_CODE; import static se.leap.bitmaskclient.Constants.BROADCAST_RESULT_KEY; +import static se.leap.bitmaskclient.Constants.EIP_ACTION_PREPARE_VPN; import static se.leap.bitmaskclient.Constants.EIP_ACTION_START; import static se.leap.bitmaskclient.Constants.EIP_REQUEST; import static se.leap.bitmaskclient.Constants.PROVIDER_KEY; @@ -58,6 +60,7 @@ import static se.leap.bitmaskclient.ProviderAPI.USER_MESSAGE; import static se.leap.bitmaskclient.R.string.downloading_vpn_certificate_failed; import static se.leap.bitmaskclient.R.string.vpn_certificate_user_message; import static se.leap.bitmaskclient.eip.EIP.EIPErrors.ERROR_INVALID_VPN_CERTIFICATE; +import static se.leap.bitmaskclient.eip.EIP.EIPErrors.ERROR_VPN_PREPARE; import static se.leap.bitmaskclient.utils.PreferenceHelper.storeProviderInPreferences; @@ -88,17 +91,12 @@ public class MainActivity extends AppCompatActivity implements EipSetupListener, preferences = getSharedPreferences(SHARED_PREFERENCES, MODE_PRIVATE); provider = ProviderObservable.getInstance().getCurrentProvider(); + EipSetupObserver.addListener(this); // Set up the drawer. navigationDrawerFragment.setUp(R.id.navigation_drawer, findViewById(R.id.drawer_layout)); handleIntentAction(getIntent()); } - @Override - protected void onResume() { - super.onResume(); - EipSetupObserver.addListener(this); - } - @Override public void onBackPressed() { FragmentManagerEnhanced fragmentManagerEnhanced = new FragmentManagerEnhanced(getSupportFragmentManager()); @@ -214,8 +212,8 @@ public class MainActivity extends AppCompatActivity implements EipSetupListener, } @Override - protected void onPause() { - super.onPause(); + protected void onDestroy() { + super.onDestroy(); EipSetupObserver.removeListener(this); } @@ -247,6 +245,11 @@ public class MainActivity extends AppCompatActivity implements EipSetupListener, } } break; + case EIP_ACTION_PREPARE_VPN: + if (resultCode == RESULT_CANCELED) { + showMainActivityErrorDialog(getString(R.string.vpn_error_establish), ERROR_VPN_PREPARE); + } + break; } } @@ -297,7 +300,22 @@ public class MainActivity extends AppCompatActivity implements EipSetupListener, e.printStackTrace(); Log.w(TAG, "error dialog leaked!"); } + } + /** + * Shows an error dialog + */ + public void showMainActivityErrorDialog(String reasonToFail, EIP.EIPErrors error) { + try { + FragmentTransaction fragmentTransaction = new FragmentManagerEnhanced( + this.getSupportFragmentManager()).removePreviousFragment( + MainActivityErrorDialog.TAG); + DialogFragment newFragment = MainActivityErrorDialog.newInstance(provider, reasonToFail, error); + newFragment.show(fragmentTransaction, MainActivityErrorDialog.TAG); + } catch (IllegalStateException | NullPointerException e) { + e.printStackTrace(); + Log.w(TAG, "error dialog leaked!"); + } } /** diff --git a/app/src/main/java/se/leap/bitmaskclient/MainActivityErrorDialog.java b/app/src/main/java/se/leap/bitmaskclient/MainActivityErrorDialog.java index 6ce21918..5e3d8f73 100644 --- a/app/src/main/java/se/leap/bitmaskclient/MainActivityErrorDialog.java +++ b/app/src/main/java/se/leap/bitmaskclient/MainActivityErrorDialog.java @@ -32,6 +32,7 @@ import se.leap.bitmaskclient.eip.EipCommand; import static se.leap.bitmaskclient.ProviderAPI.UPDATE_INVALID_VPN_CERTIFICATE; import static se.leap.bitmaskclient.R.string.warning_option_try_ovpn; import static se.leap.bitmaskclient.R.string.warning_option_try_pt; +import static se.leap.bitmaskclient.eip.EIP.EIPErrors.ERROR_VPN_PREPARE; import static se.leap.bitmaskclient.eip.EIP.EIPErrors.UNKNOWN; import static se.leap.bitmaskclient.eip.EIP.EIPErrors.valueOf; import static se.leap.bitmaskclient.eip.EIP.ERRORS; @@ -59,9 +60,17 @@ public class MainActivityErrorDialog extends DialogFragment { * @return a new instance of this DialogFragment. */ public static DialogFragment newInstance(Provider provider, String reasonToFail) { + return newInstance(provider, reasonToFail, UNKNOWN); + } + + /** + * @return a new instance of this DialogFragment. + */ + public static DialogFragment newInstance(Provider provider, String reasonToFail, EIP.EIPErrors error) { MainActivityErrorDialog dialogFragment = new MainActivityErrorDialog(); dialogFragment.reasonToFail = reasonToFail; dialogFragment.provider = provider; + dialogFragment.downloadError = error; return dialogFragment; } @@ -99,6 +108,7 @@ public class MainActivityErrorDialog extends DialogFragment { @NonNull public Dialog onCreateDialog(Bundle savedInstanceState) { AlertDialog.Builder builder = new AlertDialog.Builder(getActivity()); + Context applicationContext = getContext().getApplicationContext(); builder.setMessage(reasonToFail) .setNegativeButton(R.string.cancel, (dialog, id) -> { }); @@ -108,28 +118,29 @@ public class MainActivityErrorDialog extends DialogFragment { ProviderAPICommand.execute(getContext(), UPDATE_INVALID_VPN_CERTIFICATE, provider)); break; case NO_MORE_GATEWAYS: - Context context = getContext(); - if (context != null) { - Context applicationContext = context.getApplicationContext(); - if (provider.supportsPluggableTransports()) { - if (getUsePluggableTransports(applicationContext)) { - builder.setPositiveButton(warning_option_try_ovpn, ((dialog, which) -> { - usePluggableTransports(applicationContext, false); - EipCommand.startVPN(applicationContext.getApplicationContext(), false); - })); - } else { - builder.setPositiveButton(warning_option_try_pt, ((dialog, which) -> { - usePluggableTransports(applicationContext, true); - EipCommand.startVPN(applicationContext.getApplicationContext(), false); - })); - } + if (provider.supportsPluggableTransports()) { + if (getUsePluggableTransports(applicationContext)) { + builder.setPositiveButton(warning_option_try_ovpn, ((dialog, which) -> { + usePluggableTransports(applicationContext, false); + EipCommand.startVPN(applicationContext, false); + })); } else { - builder.setPositiveButton(R.string.retry,(dialog, which) -> { - EipCommand.startVPN(applicationContext.getApplicationContext(), false); - }); + builder.setPositiveButton(warning_option_try_pt, ((dialog, which) -> { + usePluggableTransports(applicationContext, true); + EipCommand.startVPN(applicationContext, false); + })); } + } else { + builder.setPositiveButton(R.string.retry, (dialog, which) -> { + EipCommand.startVPN(applicationContext, false); + }); } - + break; + case ERROR_VPN_PREPARE: + builder.setPositiveButton(R.string.retry, (dialog, which) -> { + EipCommand.startVPN(applicationContext, false); + }); + break; default: break; } diff --git a/app/src/main/java/se/leap/bitmaskclient/eip/EIP.java b/app/src/main/java/se/leap/bitmaskclient/eip/EIP.java index 5ce8b6e2..e37305f2 100644 --- a/app/src/main/java/se/leap/bitmaskclient/eip/EIP.java +++ b/app/src/main/java/se/leap/bitmaskclient/eip/EIP.java @@ -52,17 +52,13 @@ import de.blinkt.openvpn.core.connection.Connection; import se.leap.bitmaskclient.OnBootReceiver; import se.leap.bitmaskclient.ProviderObservable; import se.leap.bitmaskclient.R; -import se.leap.bitmaskclient.utils.ConfigHelper; import se.leap.bitmaskclient.utils.PreferenceHelper; import static android.app.Activity.RESULT_CANCELED; import static android.app.Activity.RESULT_OK; -import static android.content.Intent.CATEGORY_DEFAULT; import static de.blinkt.openvpn.core.connection.Connection.TransportType.OBFS4; import static de.blinkt.openvpn.core.connection.Connection.TransportType.OPENVPN; -import static se.leap.bitmaskclient.Constants.BROADCAST_EIP_EVENT; import static se.leap.bitmaskclient.Constants.BROADCAST_GATEWAY_SETUP_OBSERVER_EVENT; -import static se.leap.bitmaskclient.Constants.BROADCAST_RESULT_CODE; import static se.leap.bitmaskclient.Constants.BROADCAST_RESULT_KEY; import static se.leap.bitmaskclient.Constants.EIP_ACTION_CHECK_CERT_VALIDITY; import static se.leap.bitmaskclient.Constants.EIP_ACTION_IS_RUNNING; @@ -73,7 +69,6 @@ import static se.leap.bitmaskclient.Constants.EIP_ACTION_STOP_BLOCKING_VPN; import static se.leap.bitmaskclient.Constants.EIP_EARLY_ROUTES; import static se.leap.bitmaskclient.Constants.EIP_N_CLOSEST_GATEWAY; import static se.leap.bitmaskclient.Constants.EIP_RECEIVER; -import static se.leap.bitmaskclient.Constants.EIP_REQUEST; import static se.leap.bitmaskclient.Constants.EIP_RESTART_ON_BOOT; import static se.leap.bitmaskclient.Constants.PROVIDER_PROFILE; import static se.leap.bitmaskclient.Constants.PROVIDER_VPN_CERTIFICATE; @@ -82,6 +77,7 @@ import static se.leap.bitmaskclient.R.string.vpn_certificate_is_invalid; import static se.leap.bitmaskclient.R.string.warning_client_parsing_error_gateways; import static se.leap.bitmaskclient.eip.EIP.EIPErrors.ERROR_INVALID_VPN_CERTIFICATE; import static se.leap.bitmaskclient.eip.EIP.EIPErrors.NO_MORE_GATEWAYS; +import static se.leap.bitmaskclient.eip.EipResultBroadcast.tellToReceiverOrBroadcast; import static se.leap.bitmaskclient.utils.ConfigHelper.ensureNotOnMainThread; import static se.leap.bitmaskclient.utils.PreferenceHelper.getUsePluggableTransports; @@ -117,7 +113,8 @@ public final class EIP extends JobIntentService implements Observer { public enum EIPErrors { UNKNOWN, ERROR_INVALID_VPN_CERTIFICATE, - NO_MORE_GATEWAYS + NO_MORE_GATEWAYS, + ERROR_VPN_PREPARE } /** @@ -212,24 +209,24 @@ public final class EIP extends JobIntentService implements Observer { if (!isVPNCertificateValid()) { setErrorResult(result, vpn_certificate_is_invalid, ERROR_INVALID_VPN_CERTIFICATE.toString()); - tellToReceiverOrBroadcast(EIP_ACTION_START, RESULT_CANCELED, result); + tellToReceiverOrBroadcast(this, EIP_ACTION_START, RESULT_CANCELED, result); return; } GatewaysManager gatewaysManager = gatewaysFromPreferences(); if (gatewaysManager.isEmpty()) { setErrorResult(result, warning_client_parsing_error_gateways, null); - tellToReceiverOrBroadcast(EIP_ACTION_START, RESULT_CANCELED, result); + tellToReceiverOrBroadcast(this, EIP_ACTION_START, RESULT_CANCELED, result); return; } Gateway gateway = gatewaysManager.select(nClosestGateway); if (launchActiveGateway(gateway, nClosestGateway)) { - tellToReceiverOrBroadcast(EIP_ACTION_START, RESULT_OK); + tellToReceiverOrBroadcast(this, EIP_ACTION_START, RESULT_OK); } else { setErrorResult(result, NO_MORE_GATEWAYS.toString(), getStringResourceForNoMoreGateways(), getString(R.string.app_name)); - tellToReceiverOrBroadcast(EIP_ACTION_START, RESULT_CANCELED, result); + tellToReceiverOrBroadcast(this, EIP_ACTION_START, RESULT_CANCELED, result); } } @@ -284,7 +281,7 @@ public final class EIP extends JobIntentService implements Observer { private void stopEIP() { VpnStatus.updateStateString("STOPPING", "STOPPING VPN", R.string.state_exiting, ConnectionStatus.LEVEL_STOPPING); int resultCode = stop() ? RESULT_OK : RESULT_CANCELED; - tellToReceiverOrBroadcast(EIP_ACTION_STOP, resultCode); + tellToReceiverOrBroadcast(this, EIP_ACTION_STOP, resultCode); } /** @@ -296,7 +293,7 @@ public final class EIP extends JobIntentService implements Observer { int resultCode = (eipStatus.isConnected()) ? RESULT_OK : RESULT_CANCELED; - tellToReceiverOrBroadcast(EIP_ACTION_IS_RUNNING, resultCode); + tellToReceiverOrBroadcast(this, EIP_ACTION_IS_RUNNING, resultCode); } /** @@ -316,7 +313,7 @@ public final class EIP extends JobIntentService implements Observer { int resultCode = isVPNCertificateValid() ? RESULT_OK : RESULT_CANCELED; - tellToReceiverOrBroadcast(EIP_ACTION_CHECK_CERT_VALIDITY, resultCode); + tellToReceiverOrBroadcast(this, EIP_ACTION_CHECK_CERT_VALIDITY, resultCode); } /** @@ -329,50 +326,6 @@ public final class EIP extends JobIntentService implements Observer { return validator.isValid(); } - /** - * send resultCode and resultData to receiver or - * broadcast the result if no receiver is defined - * - * @param action the action that has been performed - * @param resultCode RESULT_OK if action was successful RESULT_CANCELED otherwise - * @param resultData other data to broadcast or return to receiver - */ - private void tellToReceiverOrBroadcast(String action, int resultCode, Bundle resultData) { - resultData.putString(EIP_REQUEST, action); - if (mResultRef.get() != null) { - mResultRef.get().send(resultCode, resultData); - } else { - broadcastEvent(resultCode, resultData); - } - } - - /** - * send resultCode and resultData to receiver or - * broadcast the result if no receiver is defined - * - * @param action the action that has been performed - * @param resultCode RESULT_OK if action was successful RESULT_CANCELED otherwise - */ - private void tellToReceiverOrBroadcast(String action, int resultCode) { - tellToReceiverOrBroadcast(action, resultCode, new Bundle()); - } - - /** - * broadcast result - * - * @param resultCode RESULT_OK if action was successful RESULT_CANCELED otherwise - * @param resultData other data to broadcast or return to receiver - */ - private void broadcastEvent(int resultCode, Bundle resultData) { - Intent intentUpdate = new Intent(BROADCAST_EIP_EVENT); - intentUpdate.addCategory(CATEGORY_DEFAULT); - intentUpdate.putExtra(BROADCAST_RESULT_CODE, resultCode); - intentUpdate.putExtra(BROADCAST_RESULT_KEY, resultData); - Log.d(TAG, "sending broadcast"); - LocalBroadcastManager.getInstance(this).sendBroadcast(intentUpdate); - } - - /** * helper function to add error to result bundle * diff --git a/app/src/main/java/se/leap/bitmaskclient/eip/EipResultBroadcast.java b/app/src/main/java/se/leap/bitmaskclient/eip/EipResultBroadcast.java new file mode 100644 index 00000000..e1efb375 --- /dev/null +++ b/app/src/main/java/se/leap/bitmaskclient/eip/EipResultBroadcast.java @@ -0,0 +1,77 @@ +package se.leap.bitmaskclient.eip; + +import android.content.Context; +import android.content.Intent; +import android.os.Bundle; +import android.os.ResultReceiver; +import android.support.v4.content.LocalBroadcastManager; +import android.util.Log; + +import static android.content.Intent.CATEGORY_DEFAULT; +import static se.leap.bitmaskclient.Constants.BROADCAST_EIP_EVENT; +import static se.leap.bitmaskclient.Constants.BROADCAST_RESULT_CODE; +import static se.leap.bitmaskclient.Constants.BROADCAST_RESULT_KEY; +import static se.leap.bitmaskclient.Constants.EIP_REQUEST; + +public class EipResultBroadcast { + private static final String TAG = EipResultBroadcast.class.getSimpleName(); + + + /** + * send resultCode and resultData to receiver or + * broadcast the result if no receiver is defined + * + * @param action the action that has been performed + * @param resultCode RESULT_OK if action was successful RESULT_CANCELED otherwise + */ + public static void tellToReceiverOrBroadcast(Context context, String action, int resultCode) { + tellToReceiverOrBroadcast(context, action, resultCode, null, new Bundle()); + } + + /** + * send resultCode and resultData to receiver or + * broadcast the result if no receiver is defined + * + * @param action the action that has been performed + * @param resultCode RESULT_OK if action was successful RESULT_CANCELED otherwise + * @param resultData other data to broadcast or return to receiver + */ + public static void tellToReceiverOrBroadcast(Context context, String action, int resultCode, ResultReceiver receiver, Bundle resultData) { + resultData.putString(EIP_REQUEST, action); + if (receiver != null) { + receiver.send(resultCode, resultData); + } else { + broadcastEvent(context, resultCode, resultData); + } + } + + /** + * send resultCode and resultData to receiver or + * broadcast the result if no receiver is defined + * + * @param action the action that has been performed + * @param resultCode RESULT_OK if action was successful RESULT_CANCELED otherwise + * @param resultData other data to broadcast or return to receiver + */ + public static void tellToReceiverOrBroadcast(Context context, String action, int resultCode, Bundle resultData) { + resultData.putString(EIP_REQUEST, action); + broadcastEvent(context, resultCode, resultData); + } + + + + /** + * broadcast result + * + * @param resultCode RESULT_OK if action was successful RESULT_CANCELED otherwise + * @param resultData other data to broadcast or return to receiver + */ + public static void broadcastEvent(Context context, int resultCode, Bundle resultData) { + Intent intentUpdate = new Intent(BROADCAST_EIP_EVENT); + intentUpdate.addCategory(CATEGORY_DEFAULT); + intentUpdate.putExtra(BROADCAST_RESULT_CODE, resultCode); + intentUpdate.putExtra(BROADCAST_RESULT_KEY, resultData); + Log.d(TAG, "sending broadcast"); + LocalBroadcastManager.getInstance(context).sendBroadcast(intentUpdate); + } +} diff --git a/app/src/main/res/values/strings.xml b/app/src/main/res/values/strings.xml index 06aeeacf..6c2f0236 100644 --- a/app/src/main/res/values/strings.xml +++ b/app/src/main/res/values/strings.xml @@ -127,4 +127,5 @@ %s could not connect using obfuscated vpn connections. Do you want to try to connect using a standard vpn? Try obfuscated connection Try standard connection + Android failed to establish the VPN service. -- cgit v1.2.3