diff --git a/app/src/main/AndroidManifest.xml b/app/src/main/AndroidManifest.xml
index fc05e68b..7ab6b7ee 100644
--- a/app/src/main/AndroidManifest.xml
+++ b/app/src/main/AndroidManifest.xml
@@ -128,6 +128,14 @@
+
+
+
+
diff --git a/app/src/main/java/com/emanuelef/remote_capture/CaptureHelper.java b/app/src/main/java/com/emanuelef/remote_capture/CaptureHelper.java
index eeae174d..c59fde9a 100644
--- a/app/src/main/java/com/emanuelef/remote_capture/CaptureHelper.java
+++ b/app/src/main/java/com/emanuelef/remote_capture/CaptureHelper.java
@@ -21,6 +21,7 @@ package com.emanuelef.remote_capture;
import android.app.Activity;
import android.content.ActivityNotFoundException;
+import android.content.Context;
import android.content.Intent;
import android.net.VpnService;
import android.os.Handler;
@@ -33,6 +34,7 @@ import androidx.activity.ComponentActivity;
import androidx.activity.result.ActivityResult;
import androidx.activity.result.ActivityResultLauncher;
import androidx.activity.result.contract.ActivityResultContracts;
+import androidx.annotation.Nullable;
import androidx.appcompat.app.AlertDialog;
import androidx.core.content.ContextCompat;
@@ -41,33 +43,40 @@ import java.net.UnknownHostException;
public class CaptureHelper {
private static final String TAG = "CaptureHelper";
- private final ComponentActivity mActivity;
- private final ActivityResultLauncher mLauncher;
+ private final Context mContext;
+ private final @Nullable ActivityResultLauncher mLauncher;
private final boolean mResolveHosts;
private CaptureSettings mSettings;
private CaptureStartListener mListener;
public CaptureHelper(ComponentActivity activity, boolean resolve_hosts) {
- mActivity = activity;
+ mContext = activity;
mResolveHosts = resolve_hosts;
mLauncher = activity.registerForActivityResult(
new ActivityResultContracts.StartActivityForResult(), this::captureServiceResult);
}
+ /** Note: This constructor does not handle the first-time VPN prepare */
+ public CaptureHelper(Context context) {
+ mContext = context;
+ mResolveHosts = true;
+ mLauncher = null;
+ }
+
private void captureServiceResult(final ActivityResult result) {
if(result.getResultCode() == Activity.RESULT_OK)
resolveHosts();
else if(mListener != null) {
- Utils.showToastLong(mActivity, R.string.vpn_setup_failed);
+ Utils.showToastLong(mContext, R.string.vpn_setup_failed);
mListener.onCaptureStartResult(false);
}
}
private void startCaptureOk() {
- final Intent intent = new Intent(mActivity, CaptureService.class);
+ final Intent intent = new Intent(mContext, CaptureService.class);
intent.putExtra("settings", mSettings);
- ContextCompat.startForegroundService(mActivity, intent);
+ ContextCompat.startForegroundService(mContext, intent);
if(mListener != null)
mListener.onCaptureStartResult(true);
}
@@ -121,7 +130,7 @@ public class CaptureHelper {
if(failed_host == null)
startCaptureOk();
else {
- Utils.showToastLong(mActivity, R.string.host_resolution_failed, failed_host);
+ Utils.showToastLong(mContext, R.string.host_resolution_failed, failed_host);
mListener.onCaptureStartResult(false);
}
});
@@ -139,23 +148,26 @@ public class CaptureHelper {
return;
}
- Intent vpnPrepareIntent = VpnService.prepare(mActivity);
+ Intent vpnPrepareIntent = VpnService.prepare(mContext);
if(vpnPrepareIntent != null) {
- new AlertDialog.Builder(mActivity)
- .setMessage(R.string.vpn_setup_msg)
- .setPositiveButton(R.string.ok, (dialog, whichButton) -> {
- try {
- mLauncher.launch(vpnPrepareIntent);
- } catch (ActivityNotFoundException e) {
- Utils.showToastLong(mActivity, R.string.no_intent_handler_found);
+ if (mLauncher != null)
+ new AlertDialog.Builder(mContext)
+ .setMessage(R.string.vpn_setup_msg)
+ .setPositiveButton(R.string.ok, (dialog, whichButton) -> {
+ try {
+ mLauncher.launch(vpnPrepareIntent);
+ } catch (ActivityNotFoundException e) {
+ Utils.showToastLong(mContext, R.string.no_intent_handler_found);
+ mListener.onCaptureStartResult(false);
+ }
+ })
+ .setOnCancelListener(dialog -> {
+ Utils.showToastLong(mContext, R.string.vpn_setup_failed);
mListener.onCaptureStartResult(false);
- }
- })
- .setOnCancelListener(dialog -> {
- Utils.showToastLong(mActivity, R.string.vpn_setup_failed);
- mListener.onCaptureStartResult(false);
- })
- .show();
+ })
+ .show();
+ else if (mListener != null)
+ mListener.onCaptureStartResult(false);
} else
resolveHosts();
}
diff --git a/app/src/main/java/com/emanuelef/remote_capture/CaptureService.java b/app/src/main/java/com/emanuelef/remote_capture/CaptureService.java
index 99b47342..631c9bcb 100644
--- a/app/src/main/java/com/emanuelef/remote_capture/CaptureService.java
+++ b/app/src/main/java/com/emanuelef/remote_capture/CaptureService.java
@@ -14,7 +14,7 @@
* You should have received a copy of the GNU General Public License
* along with PCAPdroid. If not, see .
*
- * Copyright 2020-21 - Emanuele Faranda
+ * Copyright 2020-24 - Emanuele Faranda
*/
package com.emanuelef.remote_capture;
@@ -112,6 +112,7 @@ public class CaptureService extends VpnService implements Runnable {
final Condition mCaptureStopped = mLock.newCondition();
private ParcelFileDescriptor mParcelFileDescriptor;
private boolean mIsAlwaysOnVPN;
+ private boolean mRevoked;
private SharedPreferences mPrefs;
private CaptureSettings mSettings;
private Billing mBilling;
@@ -236,6 +237,9 @@ public class CaptureService extends VpnService implements Runnable {
return abortStart();
}
+ if (VpnReconnectService.isAvailable())
+ VpnReconnectService.stopService();
+
mHandler = new Handler(Looper.getMainLooper());
mBilling = Billing.newInstance(this);
@@ -605,6 +609,7 @@ public class CaptureService extends VpnService implements Runnable {
@Override
public void onRevoke() {
Log.d(CaptureService.TAG, "onRevoke");
+ mRevoked = true;
stopService();
super.onRevoke();
}
@@ -1404,6 +1409,15 @@ public class CaptureService extends VpnService implements Runnable {
reloadDecryptionList();
reloadBlocklist();
reloadFirewallWhitelist();
+ } else if (cur_status == ServiceStatus.STOPPED) {
+ if (mRevoked && Prefs.restartOnDisconnect(mPrefs) && !mIsAlwaysOnVPN && (isVpnCapture() == 1)) {
+ if (android.os.Build.VERSION.SDK_INT >= android.os.Build.VERSION_CODES.M) {
+ Log.i(TAG, "VPN disconnected, starting reconnect service");
+
+ final Intent intent = new Intent(this, VpnReconnectService.class);
+ ContextCompat.startForegroundService(this, intent);
+ }
+ }
}
}
diff --git a/app/src/main/java/com/emanuelef/remote_capture/Utils.java b/app/src/main/java/com/emanuelef/remote_capture/Utils.java
index c2604e35..e1cd63bd 100644
--- a/app/src/main/java/com/emanuelef/remote_capture/Utils.java
+++ b/app/src/main/java/com/emanuelef/remote_capture/Utils.java
@@ -646,7 +646,7 @@ public class Utils {
// Using the deprecated API instead to keep things simple.
// https://developer.android.com/reference/android/net/ConnectivityManager#getAllNetworks()
@SuppressWarnings("deprecation")
- public static boolean hasVPNRunning(Context context) {
+ public static Network getRunningVpn(Context context) {
ConnectivityManager cm = (ConnectivityManager) context.getSystemService(Context.CONNECTIVITY_SERVICE);
if(cm != null) {
try {
@@ -657,7 +657,7 @@ public class Utils {
if ((cap != null) && cap.hasTransport(NetworkCapabilities.TRANSPORT_VPN)) {
Log.d("hasVPNRunning", "detected VPN connection: " + net.toString());
- return true;
+ return net;
}
}
} catch (SecurityException e) {
@@ -666,7 +666,7 @@ public class Utils {
}
}
- return false;
+ return null;
}
public static void showToast(Context context, int id, Object... args) {
diff --git a/app/src/main/java/com/emanuelef/remote_capture/VpnReconnectService.java b/app/src/main/java/com/emanuelef/remote_capture/VpnReconnectService.java
new file mode 100644
index 00000000..97da1423
--- /dev/null
+++ b/app/src/main/java/com/emanuelef/remote_capture/VpnReconnectService.java
@@ -0,0 +1,266 @@
+/*
+ * This file is part of PCAPdroid.
+ *
+ * PCAPdroid is free software: you can redistribute it and/or modify
+ * it under the terms of the GNU General Public License as published by
+ * the Free Software Foundation, either version 3 of the License, or
+ * (at your option) any later version.
+ *
+ * PCAPdroid is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ * GNU General Public License for more details.
+ *
+ * You should have received a copy of the GNU General Public License
+ * along with PCAPdroid. If not, see .
+ *
+ * Copyright 2020-24 - Emanuele Faranda
+ */
+
+package com.emanuelef.remote_capture;
+
+import android.annotation.SuppressLint;
+import android.app.Notification;
+import android.app.NotificationChannel;
+import android.app.NotificationManager;
+import android.app.PendingIntent;
+import android.app.Service;
+import android.content.Context;
+import android.content.Intent;
+import android.content.pm.ServiceInfo;
+import android.net.ConnectivityManager;
+import android.net.Network;
+import android.net.NetworkCapabilities;
+import android.net.NetworkRequest;
+import android.os.Build;
+import android.os.Handler;
+import android.os.IBinder;
+import android.os.Looper;
+
+import androidx.annotation.NonNull;
+import androidx.annotation.Nullable;
+import androidx.annotation.RequiresApi;
+import androidx.core.app.NotificationCompat;
+import androidx.core.content.ContextCompat;
+import androidx.preference.PreferenceManager;
+
+import com.emanuelef.remote_capture.activities.MainActivity;
+import com.emanuelef.remote_capture.model.CaptureSettings;
+
+/**
+ * Service which waits for other apps VPNService to terminate before
+ * restarting the capture.
+ */
+@RequiresApi(api = Build.VERSION_CODES.M)
+public class VpnReconnectService extends Service {
+ private static final String TAG = "VpnReconnectService";
+ private static final String NOTIFY_CHAN_VPNRECONNECT = "VPN Reconnection";
+ public static final int NOTIFY_ID_VPNRECONNECT = 10;
+ private static final String STOP_ACTION = "stop";
+
+ private static VpnReconnectService INSTANCE;
+ private Handler mHandler;
+ private ConnectivityManager.NetworkCallback mNetworkCallback;
+ private Network mActiveVpnNetwork;
+
+ @Nullable
+ @Override
+ public IBinder onBind(Intent intent) {
+ return null;
+ }
+
+ @Override
+ public void onCreate() {
+ Log.d(TAG, "onCreate");
+ mHandler = new Handler(Looper.getMainLooper());
+
+ INSTANCE = this;
+ super.onCreate();
+ }
+
+ @Override
+ public void onDestroy() {
+ Log.d(TAG, "onDestroy");
+
+ unregisterNetworkCallback();
+ INSTANCE = null;
+ super.onDestroy();
+ }
+
+ @Override
+ public int onStartCommand(@Nullable Intent intent, int flags, int startId) {
+ Log.d(TAG, "onStartCommand");
+
+ if ((intent != null) && (intent.getAction() != null) && (intent.getAction().equals(STOP_ACTION))) {
+ Utils.showToastLong(this, R.string.vpn_reconnection_aborted);
+ stopService();
+ return START_NOT_STICKY;
+ }
+
+ if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.UPSIDE_DOWN_CAKE)
+ startForeground(NOTIFY_ID_VPNRECONNECT, buildNotification(), ServiceInfo.FOREGROUND_SERVICE_TYPE_SPECIAL_USE);
+ else
+ startForeground(NOTIFY_ID_VPNRECONNECT, buildNotification());
+
+ mHandler.postDelayed(() -> {
+ Log.i(TAG, "Could not detect a VPN within the timeout, automatic reconnection aborted");
+ stopService();
+ }, 10000);
+
+ if (!registerNetworkCallbacks()) {
+ stopService();
+ return START_NOT_STICKY;
+ }
+
+ return START_STICKY;
+ }
+
+ private Notification buildNotification() {
+ if(Build.VERSION.SDK_INT >= Build.VERSION_CODES.O) {
+ NotificationManager nm = (NotificationManager) getSystemService(Context.NOTIFICATION_SERVICE);
+
+ NotificationChannel chan = new NotificationChannel(NOTIFY_CHAN_VPNRECONNECT,
+ NOTIFY_CHAN_VPNRECONNECT, NotificationManager.IMPORTANCE_LOW); // low: no sound
+ chan.setShowBadge(false);
+ nm.createNotificationChannel(chan);
+ }
+
+ // Status notification builder
+ PendingIntent startMainApp = PendingIntent.getActivity(this, 0,
+ new Intent(this, MainActivity.class), Utils.getIntentFlags(PendingIntent.FLAG_UPDATE_CURRENT));
+
+ Intent abortReconnectIntent = new Intent(this, VpnReconnectService.class);
+ abortReconnectIntent.setAction(STOP_ACTION);
+ PendingIntent abortReconnect = PendingIntent.getService(this, 0, abortReconnectIntent, Utils.getIntentFlags(0));
+
+ NotificationCompat.Builder builder = new NotificationCompat.Builder(this, NOTIFY_CHAN_VPNRECONNECT)
+ .setSmallIcon(R.drawable.ic_logo)
+ .setColor(ContextCompat.getColor(this, R.color.colorPrimary))
+ .setContentIntent(startMainApp)
+ .setDeleteIntent(abortReconnect)
+ .setOngoing(true)
+ .setAutoCancel(false)
+ .setContentTitle(getString(R.string.vpn_reconnection))
+ .setContentText(getString(R.string.waiting_for_vpn_disconnect))
+ .setVisibility(NotificationCompat.VISIBILITY_PUBLIC)
+ .setCategory(NotificationCompat.CATEGORY_STATUS)
+ .setPriority(NotificationCompat.PRIORITY_LOW); // see IMPORTANCE_LOW
+
+ Log.d(TAG, "running");
+ return builder.build();
+ }
+
+ private void checkAvailableNetwork(ConnectivityManager cm, Network network) {
+ if (network.equals(mActiveVpnNetwork))
+ return;
+
+ NetworkCapabilities cap = cm.getNetworkCapabilities(network);
+ if ((cap != null) && cap.hasTransport(NetworkCapabilities.TRANSPORT_VPN)) {
+ mActiveVpnNetwork = network;
+ Log.d(TAG, "Detected active VPN network: " + mActiveVpnNetwork);
+
+ // cancel the deadline timer / onLost timer
+ mHandler.removeCallbacksAndMessages(null);
+ }
+ }
+
+ private boolean registerNetworkCallbacks() {
+ ConnectivityManager cm = (ConnectivityManager) getSystemService(Service.CONNECTIVITY_SERVICE);
+
+ mNetworkCallback = new ConnectivityManager.NetworkCallback() {
+ @Override
+ public void onAvailable(@NonNull Network network) {
+ Log.d(TAG, "onAvailable: " + network);
+
+ checkAvailableNetwork(cm, network);
+ }
+
+ @Override
+ public void onLost(@NonNull Network network) {
+ Log.d(TAG, "onLost: " + network);
+
+ // NOTE: when onLost is called, the TRANSPORT_VPN capability may already have been removed
+ if (network.equals(mActiveVpnNetwork)) {
+ // NOTE: onAvailable and onLost may be called multiple times before the actual VPN is started.
+ // Use a debounce delay to prevent mis-detection
+ mHandler.postDelayed(() -> {
+ Log.i(TAG, "Active VPN disconnected, starting the capture");
+ unregisterNetworkCallback();
+
+ Context ctx = VpnReconnectService.this;
+ CaptureSettings settings = new CaptureSettings(ctx, PreferenceManager.getDefaultSharedPreferences(ctx));
+
+ CaptureHelper helper = new CaptureHelper(ctx);
+ helper.setListener(success -> stopService());
+ helper.startCapture(settings);
+ }, 3000);
+ }
+ }
+ };
+
+ try {
+ Log.d(TAG, "registerNetworkCallback");
+
+ NetworkRequest.Builder builder = new NetworkRequest.Builder()
+ .removeCapability(NetworkCapabilities.NET_CAPABILITY_NOT_VPN);
+
+ // necessary to see other apps network events on Android 12+
+ if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.S)
+ builder.setIncludeOtherUidNetworks(true);
+
+ cm.registerNetworkCallback(builder.build(), mNetworkCallback);
+ } catch (SecurityException e) {
+ // this is a bug in Android 11 - https://issuetracker.google.com/issues/175055271?pli=1
+ e.printStackTrace();
+
+ Log.e(TAG, "registerNetworkCallback failed");
+ mNetworkCallback = null;
+ return false;
+ }
+
+ // The VPN may already be active
+ Network net = Utils.getRunningVpn(this);
+ if (net != null)
+ checkAvailableNetwork(cm, net);
+
+ return true;
+ }
+
+ private void unregisterNetworkCallback() {
+ if(mNetworkCallback != null) {
+ ConnectivityManager cm = (ConnectivityManager) getSystemService(Service.CONNECTIVITY_SERVICE);
+
+ try {
+ Log.d(TAG, "unregisterNetworkCallback");
+ cm.unregisterNetworkCallback(mNetworkCallback);
+ } catch(IllegalArgumentException e) {
+ Log.w(TAG, "unregisterNetworkCallback failed: " + e);
+ }
+
+ mNetworkCallback = null;
+ }
+ }
+
+ @SuppressLint("ObsoleteSdkInt")
+ @RequiresApi(api = Build.VERSION_CODES.BASE)
+ public static boolean isAvailable() {
+ return android.os.Build.VERSION.SDK_INT >= android.os.Build.VERSION_CODES.M;
+ }
+
+ @SuppressWarnings("deprecation")
+ public static void stopService() {
+ Log.d(TAG, "stopService called");
+ VpnReconnectService service = INSTANCE;
+ if (service == null)
+ return;
+
+ service.unregisterNetworkCallback();
+
+ if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.N)
+ service.stopForeground(STOP_FOREGROUND_REMOVE);
+ else
+ service.stopForeground(true);
+
+ service.stopSelf();
+ }
+}
diff --git a/app/src/main/java/com/emanuelef/remote_capture/activities/MainActivity.java b/app/src/main/java/com/emanuelef/remote_capture/activities/MainActivity.java
index f9c68069..55e0e9d4 100644
--- a/app/src/main/java/com/emanuelef/remote_capture/activities/MainActivity.java
+++ b/app/src/main/java/com/emanuelef/remote_capture/activities/MainActivity.java
@@ -63,6 +63,7 @@ import com.emanuelef.remote_capture.ConnectionsRegister;
import com.emanuelef.remote_capture.Log;
import com.emanuelef.remote_capture.MitmReceiver;
import com.emanuelef.remote_capture.PCAPdroid;
+import com.emanuelef.remote_capture.VpnReconnectService;
import com.emanuelef.remote_capture.activities.prefs.SettingsActivity;
import com.emanuelef.remote_capture.fragments.ConnectionsFragment;
import com.emanuelef.remote_capture.fragments.StatusFragment;
@@ -85,7 +86,6 @@ import java.io.File;
import java.io.IOException;
import java.io.InputStream;
import java.io.OutputStream;
-import java.nio.file.Files;
import java.util.HashSet;
import java.util.concurrent.ExecutorService;
import java.util.concurrent.Executors;
@@ -720,6 +720,9 @@ public class MainActivity extends BaseActivity implements NavigationView.OnNavig
}
public void startCapture() {
+ if (VpnReconnectService.isAvailable())
+ VpnReconnectService.stopService();
+
if(showRemoteServerAlert())
return;
@@ -729,7 +732,7 @@ public class MainActivity extends BaseActivity implements NavigationView.OnNavig
return;
}
- if(!Prefs.isRootCaptureEnabled(mPrefs) && Utils.hasVPNRunning(this)) {
+ if(!Prefs.isRootCaptureEnabled(mPrefs) && (Utils.getRunningVpn(this) != null)) {
new AlertDialog.Builder(this)
.setTitle(R.string.active_vpn_detected)
.setMessage(R.string.disconnect_vpn_confirm)
diff --git a/app/src/main/java/com/emanuelef/remote_capture/activities/prefs/SettingsActivity.java b/app/src/main/java/com/emanuelef/remote_capture/activities/prefs/SettingsActivity.java
index 240c5266..4d396d46 100644
--- a/app/src/main/java/com/emanuelef/remote_capture/activities/prefs/SettingsActivity.java
+++ b/app/src/main/java/com/emanuelef/remote_capture/activities/prefs/SettingsActivity.java
@@ -44,6 +44,7 @@ import com.emanuelef.remote_capture.Log;
import com.emanuelef.remote_capture.PCAPdroid;
import com.emanuelef.remote_capture.Utils;
import com.emanuelef.remote_capture.MitmAddon;
+import com.emanuelef.remote_capture.VpnReconnectService;
import com.emanuelef.remote_capture.activities.BaseActivity;
import com.emanuelef.remote_capture.activities.MainActivity;
import com.emanuelef.remote_capture.activities.MitmSetupWizard;
@@ -147,6 +148,7 @@ public class SettingsActivity extends BaseActivity implements PreferenceFragment
private SwitchPreference mMalwareDetectionEnabled;
private SwitchPreference mTrailerEnabled;
private SwitchPreference mPcapngEnabled;
+ private SwitchPreference mRestartOnDisconnect;
private Billing mIab;
private boolean mHasStartedMitmWizard;
private boolean mRootDecryptionNoticeShown = false;
@@ -266,6 +268,9 @@ public class SettingsActivity extends BaseActivity implements PreferenceFragment
} else
mRootCaptureEnabled.setVisible(false);
+ mRestartOnDisconnect = requirePreference(Prefs.PREF_RESTART_ON_DISCONNECT);
+ mRestartOnDisconnect.setVisible(VpnReconnectService.isAvailable());
+
mDnsSettings = requirePreference("dns_settings");;
mVpnExceptions = requirePreference(Prefs.PREF_VPN_EXCEPTIONS);
mVpnExceptions.setOnPreferenceClickListener(preference -> {
@@ -409,6 +414,9 @@ public class SettingsActivity extends BaseActivity implements PreferenceFragment
socks5ProxyHideShow(mTlsDecryption.isChecked(), false);
}
+ if (VpnReconnectService.isAvailable())
+ mRestartOnDisconnect.setVisible(!enabled);
+
mIpMode.setVisible(!enabled);
mCapInterface.setVisible(enabled);
mVpnExceptions.setVisible(!enabled);
diff --git a/app/src/main/java/com/emanuelef/remote_capture/model/Prefs.java b/app/src/main/java/com/emanuelef/remote_capture/model/Prefs.java
index fc998ced..0ce759bf 100644
--- a/app/src/main/java/com/emanuelef/remote_capture/model/Prefs.java
+++ b/app/src/main/java/com/emanuelef/remote_capture/model/Prefs.java
@@ -109,6 +109,7 @@ public class Prefs {
public static final String PREF_DNS_SERVER_V6 = "dns_v6";
public static final String PREF_USE_SYSTEM_DNS = "system_dns";
public static final String PREF_PCAPNG_ENABLED = "pcapng_format";
+ public static final String PREF_RESTART_ON_DISCONNECT = "restart_on_disconnect";
public enum DumpMode {
NONE,
@@ -225,6 +226,7 @@ public class Prefs {
&& p.getBoolean(PREF_PCAPNG_ENABLED, true));
}
public static boolean startAtBoot(SharedPreferences p) { return(p.getBoolean(PREF_START_AT_BOOT, false)); }
+ public static boolean restartOnDisconnect(SharedPreferences p) { return(p.getBoolean(PREF_RESTART_ON_DISCONNECT, false)); }
public static boolean isTLSDecryptionSetupDone(SharedPreferences p) { return(p.getBoolean(PREF_TLS_DECRYPTION_SETUP_DONE, false)); }
public static boolean getFullPayloadMode(SharedPreferences p) { return(p.getBoolean(PREF_FULL_PAYLOAD, false)); }
public static boolean isPrivateDnsBlockingEnabled(SharedPreferences p) { return(p.getBoolean(PREF_AUTO_BLOCK_PRIVATE_DNS, true)); }
diff --git a/app/src/main/res/values/strings.xml b/app/src/main/res/values/strings.xml
index 664786ba..185dd901 100644
--- a/app/src/main/res/values/strings.xml
+++ b/app/src/main/res/values/strings.xml
@@ -505,4 +505,9 @@
Both
What\'s new
Raw bytes
+ Restart on disconnection
+ Automatically restart the capture after being stopped by other VPN apps
+ VPN reconnection
+ VPN reconnection aborted
+ Waiting for the active VPN to disconnect…
diff --git a/app/src/main/res/xml/root_preferences.xml b/app/src/main/res/xml/root_preferences.xml
index 3923a96f..37b63514 100644
--- a/app/src/main/res/xml/root_preferences.xml
+++ b/app/src/main/res/xml/root_preferences.xml
@@ -153,6 +153,13 @@
app:summary="@string/start_at_boot_summary"
android:defaultValue="false" />
+
+