From 630e911172cf704877bae809cede2a5cbb0618e6 Mon Sep 17 00:00:00 2001 From: emanuele-f Date: Sun, 18 Feb 2024 22:34:26 +0100 Subject: [PATCH] Add toggle for auto-reconnection on VPN termination This allows the user to run other VPN apps, disconnecting PCAPdroid, and restart it when they terminate. Closes #411 --- app/src/main/AndroidManifest.xml | 8 + .../remote_capture/CaptureHelper.java | 56 ++-- .../remote_capture/CaptureService.java | 16 +- .../com/emanuelef/remote_capture/Utils.java | 6 +- .../remote_capture/VpnReconnectService.java | 266 ++++++++++++++++++ .../activities/MainActivity.java | 7 +- .../activities/prefs/SettingsActivity.java | 8 + .../emanuelef/remote_capture/model/Prefs.java | 2 + app/src/main/res/values/strings.xml | 5 + app/src/main/res/xml/root_preferences.xml | 7 + 10 files changed, 353 insertions(+), 28 deletions(-) create mode 100644 app/src/main/java/com/emanuelef/remote_capture/VpnReconnectService.java 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" /> + +