diff --git a/app/src/main/java/com/openipc/pixelpilot/VideoActivity.java b/app/src/main/java/com/openipc/pixelpilot/VideoActivity.java index a3588f17..d23df482 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."); @@ -1547,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"); - startService(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;