From 3260418ed70831c239f3d264a329fa60f4fb81dc Mon Sep 17 00:00:00 2001 From: iflyhere <57563846+iflyhere@users.noreply.github.com> Date: Sat, 10 Oct 2026 17:48:29 +0200 Subject: [PATCH 1/2] Do not crash when the activity is created in the background startVpnService() calls startService() unguarded, from onCreate() as well as onResume(), and Android 12 and later refuse a service start from a background app with BackgroundServiceStartNotAllowedException. VideoActivity can be created in the background - that is how its USB filter launches it when the adapter is plugged in with the screen off - and then the exception escapes onCreate() and the app is gone before it has shown anything. Reproduced on a Quest 3 by starting the activity while the headset was asleep: FATAL EXCEPTION: main Unable to start activity ... VideoActivity: android.app.BackgroundServiceStartNotAllowedException: Not allowed to start service Intent { cmp=com.openipc.pixelpilot/.WfbNgVpnService }: app is in background at VideoActivity.startVpnService(VideoActivity.java:1298) at VideoActivity.onCreate(VideoActivity.java:340) The start is skipped with a warning instead. Nothing is lost by that: onResume() calls startVpnService() again once the app is actually in front, which is when the tunnel is wanted. The stop in onPause() and the start after the VPN consent go through the same guard, since both can run with the app on its way to the background. It catches IllegalStateException, which BackgroundServiceStartNotAllowedException extends, rather than naming a class that only exists from API 31 - minSdk is lower than that. --- .../com/openipc/pixelpilot/VideoActivity.java | 27 +++++++++++++++---- 1 file changed, 22 insertions(+), 5 deletions(-) diff --git a/app/src/main/java/com/openipc/pixelpilot/VideoActivity.java b/app/src/main/java/com/openipc/pixelpilot/VideoActivity.java index a3588f17..f28021ad 100644 --- a/app/src/main/java/com/openipc/pixelpilot/VideoActivity.java +++ b/app/src/main/java/com/openipc/pixelpilot/VideoActivity.java @@ -1294,12 +1294,30 @@ private void startVpnService() { if (intent != null) { startActivityForResult(intent, VPN_REQUEST_CODE); } else { - Intent serviceIntent = new Intent(this, WfbNgVpnService.class); - startService(serviceIntent); + startServiceIfAllowed(new Intent(this, WfbNgVpnService.class)); } } + /** + * Starts a service, or does nothing if the app is in the background. + * + *
Android 12 and later refuse a service start from a background app with + * BackgroundServiceStartNotAllowedException, and this activity can be created in the + * background: started while the screen is off, it threw from onCreate() and took the app + * down before it had shown anything. Nothing is lost by skipping it, because onResume() calls + * startVpnService() again once the app is actually in front. + */ + private void startServiceIfAllowed(Intent intent) { + try { + startService(intent); + } catch (IllegalStateException e) { + // BackgroundServiceStartNotAllowedException is an IllegalStateException. Catching the + // parent avoids naming a class that only exists from API 31, below minSdk. + Log.w(TAG, "not starting " + intent.getComponent() + " from the background", e); + } + } + private Uri openDvrFile() { String dvrFolder = getSharedPreferences("general", Context.MODE_PRIVATE).getString("dvr_folder_", ""); @@ -1452,8 +1470,7 @@ protected void onActivityResult(int requestCode, int resultCode, @Nullable Inten } else if (requestCode == 100) { // VPN_REQUEST_CODE is 100 if (resultCode == RESULT_OK) { // VPN permission granted, start the VPN service - Intent serviceIntent = new Intent(this, WfbNgVpnService.class); - startService(serviceIntent); + startServiceIfAllowed(new Intent(this, WfbNgVpnService.class)); } else { // VPN permission not granted Log.e(TAG, "VPN permission was not granted by the user."); @@ -1551,7 +1568,7 @@ protected void onPause() { Log.w(TAG, "onPause: stopping service"); Intent intent = new Intent(this, WfbNgVpnService.class); intent.setAction("STOP_SERVICE"); - startService(intent); + startServiceIfAllowed(intent); } @Override From 7a0bf4b5b2df4393a40490a332ef249696e3022c Mon Sep 17 00:00:00 2001 From: iflyhere <57563846+iflyhere@users.noreply.github.com> Date: Sat, 10 Oct 2026 18:20:07 +0200 Subject: [PATCH 2/2] Stop the tunnel directly instead of with a service start From review: onPause() sent STOP_SERVICE through startServiceIfAllowed(), so whenever Android refused that start the stop was dropped with it and the tunnel stayed up. How often that happens is narrower than it might look. Going to sleep with the app in front, the stop went through - onPause() still ran while the app was allowed to start services, and the service logged its own shutdown. The crash this branch fixes came from onCreate() in an activity started while already asleep, not from onPause() on the way into sleep. But nothing guarantees onPause() runs before the app counts as background, and a stop routed through a call that may be refused silently is the wrong shape whether or not it bites today. stopService() would not have been the fix. Once the interface is established the system holds its own binding to a VpnService, so stopService() leaves the service - and the tunnel - running until the interface is closed, and onDestroy() never comes. The service runs in the app's own process, so onPause() now reaches the running instance and shuts it down directly: worker threads stopped, interface closed, stopSelf(). That cannot be refused. The STOP_SERVICE action does the same through the same method. --- .../com/openipc/pixelpilot/VideoActivity.java | 7 +- .../openipc/pixelpilot/WfbNgVpnService.java | 76 ++++++++++++++----- 2 files changed, 59 insertions(+), 24 deletions(-) diff --git a/app/src/main/java/com/openipc/pixelpilot/VideoActivity.java b/app/src/main/java/com/openipc/pixelpilot/VideoActivity.java index f28021ad..d23df482 100644 --- a/app/src/main/java/com/openipc/pixelpilot/VideoActivity.java +++ b/app/src/main/java/com/openipc/pixelpilot/VideoActivity.java @@ -1564,11 +1564,10 @@ protected void onPause() { videoPlayer.stopAudio(); wfbLinkManager.stopAdapters(); - // Stop VPN service + // Stop VPN service - directly, not with a STOP_SERVICE start, which Android can refuse + // from the background. See WfbNgVpnService.stopRunning(). Log.w(TAG, "onPause: stopping service"); - Intent intent = new Intent(this, WfbNgVpnService.class); - intent.setAction("STOP_SERVICE"); - startServiceIfAllowed(intent); + WfbNgVpnService.stopRunning(); } @Override diff --git a/app/src/main/java/com/openipc/pixelpilot/WfbNgVpnService.java b/app/src/main/java/com/openipc/pixelpilot/WfbNgVpnService.java index 6ab18a5c..8b93f038 100644 --- a/app/src/main/java/com/openipc/pixelpilot/WfbNgVpnService.java +++ b/app/src/main/java/com/openipc/pixelpilot/WfbNgVpnService.java @@ -32,30 +32,63 @@ public class WfbNgVpnService extends VpnService { // Control flags private volatile boolean isRunning = false; + // The instance that is running, if any - see stopRunning(). + private static volatile WfbNgVpnService running; + @Override - public int onStartCommand(Intent intent, int flags, int startId) { - if (intent != null && "STOP_SERVICE".equals(intent.getAction())) { - Log.i(TAG, "VPN Service stopping"); - // Stop threads - isRunning = false; + public void onCreate() { + super.onCreate(); + running = this; + } - if (udpToVpnThread != null) { - udpToVpnThread.interrupt(); - } - if (vpnToUdpThread != null) { - vpnToUdpThread.interrupt(); - } + /** + * Takes the tunnel down if it is up. Safe to call from anywhere in the app's process, in any + * state. + * + *
Not through startService(): Android refuses a service start from the background, and a + * stop sent that way is dropped along with it. onPause() normally still runs while the app + * may start services - going to sleep with the app in front, the stop went through - but + * nothing guarantees it, and an activity created in the background pauses there too. Not + * through stopService() either: once the interface is established the system holds its own + * binding to a VpnService, and stopService() leaves it running until the interface is closed. + * The service lives in this process, so closing it directly cannot be refused. + */ + public static void stopRunning() { + final WfbNgVpnService service = running; + if (service != null) { + service.shutDown(); + } + } - // Close the interface - if (vpnInterface != null) { - try { - vpnInterface.close(); - } catch (IOException e) { - Log.e(TAG, "Failed to close VPN interface", e); - } - vpnInterface = null; + /** Stops the worker threads, closes the interface and stops the service. */ + private void shutDown() { + Log.i(TAG, "VPN Service stopping"); + // Stop threads + isRunning = false; + + if (udpToVpnThread != null) { + udpToVpnThread.interrupt(); + } + if (vpnToUdpThread != null) { + vpnToUdpThread.interrupt(); + } + + // Close the interface + if (vpnInterface != null) { + try { + vpnInterface.close(); + } catch (IOException e) { + Log.e(TAG, "Failed to close VPN interface", e); } - stopSelf(); + vpnInterface = null; + } + stopSelf(); + } + + @Override + public int onStartCommand(Intent intent, int flags, int startId) { + if (intent != null && "STOP_SERVICE".equals(intent.getAction())) { + shutDown(); return START_NOT_STICKY; } @@ -211,6 +244,9 @@ public void run() { public void onDestroy() { super.onDestroy(); Log.i(TAG, "VPN Service destroyed"); + if (running == this) { + running = null; + } // Stop threads isRunning = false;