Add proper support for always-on VPN

The VPN can now be started by the system when the always-on VPN is
enabled. In this case, the stop button is hidden and the previous
configuration is used.

Closes #165

Null intents (e.g. due to START_STICKY) are now handled, which prevents
ForegroundServiceStartNotAllowedException in Android 12.

Fixes #175
This commit is contained in:
emanuele-f
2022-01-20 23:12:00 +01:00
parent 472b7f4a18
commit e76499d02a
3 changed files with 43 additions and 27 deletions
@@ -92,6 +92,7 @@ public class CaptureService extends VpnService implements Runnable {
private static final int NOTIFY_ID_VPNSERVICE = 1;
private static CaptureService INSTANCE;
private ParcelFileDescriptor mParcelFileDescriptor;
private boolean mIsAlwaysOnVPN;
private CaptureSettings mSettings;
private Billing mBilling;
private Handler mHandler;
@@ -171,32 +172,40 @@ public class CaptureService extends VpnService implements Runnable {
}
private int abortStart() {
// NOTE: startForeground must be called before stopSelf, otherwise an exception will occur
// NOTE: startForeground must be called before stopSelf, otherwise an exception will occur:
// android.app.ForegroundServiceDidNotStartInTimeException: Context.startForegroundService() did not then call Service.startForeground()
setupNotifications();
// Note: in Android 12, this may generate a ForegroundServiceStartNotAllowedException
// if called when the app is in background.
startForeground(NOTIFY_ID_VPNSERVICE, getStatusNotification());
stopSelf();
sendServiceStatus(SERVICE_STATUS_STOPPED);
return START_STICKY;
return START_NOT_STICKY;
}
@Override
public int onStartCommand(Intent intent, int flags, int startId) {
public int onStartCommand(@Nullable Intent intent, int flags, int startId) {
SharedPreferences prefs = PreferenceManager.getDefaultSharedPreferences(this);
mHandler = new Handler(Looper.getMainLooper());
mBilling = Billing.newInstance(this);
if (intent == null) {
Log.d(CaptureService.TAG, "NULL intent onStartCommand");
return abortStart();
}
Log.d(CaptureService.TAG, "onStartCommand");
mSettings = (CaptureSettings) intent.getSerializableExtra("settings");
if (mSettings == null) {
Log.e(CaptureService.TAG, "Missing capture settings");
return abortStart();
}
// NOTE: a null intent may be delivered due to START_STICKY
mSettings = (CaptureSettings) ((intent == null) ? null : intent.getSerializableExtra("settings"));
if(mSettings == null) {
// An Intent without extras is delivered in case of always on VPN
// https://developer.android.com/guide/topics/connectivity/vpn#always-on
mIsAlwaysOnVPN = (intent != null);
Log.d(CaptureService.TAG, "Missing capture settings, using previous ones");
mSettings = new CaptureSettings(prefs);
if(mIsAlwaysOnVPN)
mSettings.root_capture = false;
} else
mIsAlwaysOnVPN = false;
// Retrieve DNS server
dns_server = FALLBACK_DNS_SERVER;
@@ -284,7 +293,6 @@ public class CaptureService extends VpnService implements Runnable {
} else
app_filter_uid = -1;
SharedPreferences prefs = PreferenceManager.getDefaultSharedPreferences(this);
mMalwareDetectionEnabled = Prefs.isMalwareDetectionEnabled(this, prefs);
if(!mSettings.root_capture) {
@@ -357,6 +365,8 @@ public class CaptureService extends VpnService implements Runnable {
setupNotifications();
startForeground(NOTIFY_ID_VPNSERVICE, getStatusNotification());
// If the service is killed (e.g. due to low memory), then restart it with a NULL intent
return START_STICKY;
}
@@ -621,6 +631,10 @@ public class CaptureService extends VpnService implements Runnable {
(INSTANCE.mCaptureThread != null));
}
public static boolean isAlwaysOnVPN() {
return((INSTANCE != null) && INSTANCE.mIsAlwaysOnVPN);
}
private void checkBlacklistsUpdates() {
if(!mMalwareDetectionEnabled || (mBlacklistsUpdateThread != null))
return;
@@ -338,6 +338,7 @@ private void refreshPcapDumpInfo() {
ContextCompat.getDrawable(requireContext(), android.R.drawable.ic_media_play));
mMenuItemStartBtn.setTitle(R.string.start_button);
mMenuItemStartBtn.setEnabled(true);
mMenuItemStartBtn.setVisible(true);
mMenuSettings.setEnabled(true);
}
@@ -359,6 +360,7 @@ private void refreshPcapDumpInfo() {
ContextCompat.getDrawable(requireContext(), R.drawable.ic_media_stop));
mMenuItemStartBtn.setTitle(R.string.stop_button);
mMenuItemStartBtn.setEnabled(true);
mMenuItemStartBtn.setVisible(!CaptureService.isAlwaysOnVPN());
mMenuSettings.setEnabled(false);
}
@@ -7,19 +7,19 @@ import android.os.Bundle;
import java.io.Serializable;
public class CaptureSettings implements Serializable {
public final Prefs.DumpMode dump_mode;
public final String app_filter;
public final String collector_address;
public final int collector_port;
public final int http_server_port;
public final boolean socks5_enabled;
public final String socks5_proxy_address;
public final int socks5_proxy_port;
public final boolean ipv6_enabled;
public final boolean root_capture;
public final boolean pcapdroid_trailer;
public final String capture_interface;
public final String pcap_uri;
public Prefs.DumpMode dump_mode;
public String app_filter;
public String collector_address;
public int collector_port;
public int http_server_port;
public boolean socks5_enabled;
public String socks5_proxy_address;
public int socks5_proxy_port;
public boolean ipv6_enabled;
public boolean root_capture;
public boolean pcapdroid_trailer;
public String capture_interface;
public String pcap_uri;
public CaptureSettings(SharedPreferences prefs) {
dump_mode = Prefs.getDumpMode(prefs);