android: CharonVpnService - safer foreground and teardown handling
- DISCONNECT (plain startService) no longer promotes to the foreground, and startForeground() refusals on Android 12+ are logged, not fatal - drop the placeholder notification when nothing was running, so the service does not stay in the foreground with a stale Disabled state - onDestroy: shutdownNow/await the profile executor before closing the database; profile loads catch errors instead of killing the process - unbind by bind result, not by mService (binding leaked if destroyed before onServiceConnected) - the connection thread's crash guard can no longer throw itself Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
1 parent
2b40eebf5b
commit
358dcb94b2
1 file changed
+83
-10
+83
-10
@@ -94,6 +94,7 @@ import java.util.SortedSet;
|
|||||||
import java.util.TreeSet;
|
import java.util.TreeSet;
|
||||||
import java.util.concurrent.ExecutorService;
|
import java.util.concurrent.ExecutorService;
|
||||||
import java.util.concurrent.Executors;
|
import java.util.concurrent.Executors;
|
||||||
|
import java.util.concurrent.TimeUnit;
|
||||||
|
|
||||||
import androidx.core.app.NotificationCompat;
|
import androidx.core.app.NotificationCompat;
|
||||||
import androidx.core.content.ContextCompat;
|
import androidx.core.content.ContextCompat;
|
||||||
@@ -169,6 +170,7 @@ public class CharonVpnService extends VpnService implements Runnable, VpnStateSe
|
|||||||
private UnderlyingNetworkTracker mUnderlyingTracker;
|
private UnderlyingNetworkTracker mUnderlyingTracker;
|
||||||
private Handler mHandler;
|
private Handler mHandler;
|
||||||
private VpnStateService mService;
|
private VpnStateService mService;
|
||||||
|
private boolean mBound;
|
||||||
private final Object mServiceLock = new Object();
|
private final Object mServiceLock = new Object();
|
||||||
private final ServiceConnection mServiceConnection = new ServiceConnection()
|
private final ServiceConnection mServiceConnection = new ServiceConnection()
|
||||||
{
|
{
|
||||||
@@ -217,8 +219,14 @@ public class CharonVpnService extends VpnService implements Runnable, VpnStateSe
|
|||||||
* first millisecond, and it avoids the system killing a fresh process
|
* first millisecond, and it avoids the system killing a fresh process
|
||||||
* when the profile fails to load before addNotification() runs. The
|
* when the profile fails to load before addNotification() runs. The
|
||||||
* state-specific notification replaces this one (via stateChanged()). */
|
* state-specific notification replaces this one (via stateChanged()). */
|
||||||
mShowNotification = true;
|
boolean disconnect = intent != null && DISCONNECT_ACTION.equals(intent.getAction());
|
||||||
startForeground(VPN_STATE_NOTIFICATION_ID, buildNotification(false));
|
if (!disconnect)
|
||||||
|
{ /* DISCONNECT arrives via plain startService(): promoting to the
|
||||||
|
* foreground from the background is not allowed there (Android 12+)
|
||||||
|
* and would crash, and there is nothing to keep alive anyway */
|
||||||
|
mShowNotification = true;
|
||||||
|
startForegroundSafe();
|
||||||
|
}
|
||||||
if (intent != null)
|
if (intent != null)
|
||||||
{
|
{
|
||||||
if (VPN_SERVICE_ACTION.equals(intent.getAction()))
|
if (VPN_SERVICE_ACTION.equals(intent.getAction()))
|
||||||
@@ -282,7 +290,16 @@ public class CharonVpnService extends VpnService implements Runnable, VpnStateSe
|
|||||||
{
|
{
|
||||||
return null;
|
return null;
|
||||||
}
|
}
|
||||||
return mDataSource.getVpnProfile(uuid);
|
try
|
||||||
|
{
|
||||||
|
return mDataSource.getVpnProfile(uuid);
|
||||||
|
}
|
||||||
|
catch (RuntimeException e)
|
||||||
|
{ /* e.g. the service is being destroyed and closed the database;
|
||||||
|
* an uncaught exception on the executor would kill the process */
|
||||||
|
DiagLog.i(TAG, "loading profile failed: " + e);
|
||||||
|
return null;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@@ -391,8 +408,8 @@ public class CharonVpnService extends VpnService implements Runnable, VpnStateSe
|
|||||||
/* use a separate thread as main thread for charon */
|
/* use a separate thread as main thread for charon */
|
||||||
mConnectionHandler = new Thread(this);
|
mConnectionHandler = new Thread(this);
|
||||||
/* the thread is started when the service is bound */
|
/* the thread is started when the service is bound */
|
||||||
bindService(new Intent(this, VpnStateService.class),
|
mBound = bindService(new Intent(this, VpnStateService.class),
|
||||||
mServiceConnection, Service.BIND_AUTO_CREATE);
|
mServiceConnection, Service.BIND_AUTO_CREATE);
|
||||||
|
|
||||||
createNotificationChannel();
|
createNotificationChannel();
|
||||||
}
|
}
|
||||||
@@ -418,12 +435,28 @@ public class CharonVpnService extends VpnService implements Runnable, VpnStateSe
|
|||||||
{
|
{
|
||||||
e.printStackTrace();
|
e.printStackTrace();
|
||||||
}
|
}
|
||||||
if (mService != null)
|
synchronized (mServiceLock)
|
||||||
{
|
{
|
||||||
mService.unregisterListener(this);
|
if (mService != null)
|
||||||
|
{
|
||||||
|
mService.unregisterListener(this);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if (mBound)
|
||||||
|
{ /* unbind even if onServiceConnected() never ran, or the binding leaks */
|
||||||
unbindService(mServiceConnection);
|
unbindService(mServiceConnection);
|
||||||
|
mBound = false;
|
||||||
|
}
|
||||||
|
/* queued profile loads must not run against the closed database */
|
||||||
|
mProfileExecutor.shutdownNow();
|
||||||
|
try
|
||||||
|
{
|
||||||
|
mProfileExecutor.awaitTermination(2, TimeUnit.SECONDS);
|
||||||
|
}
|
||||||
|
catch (InterruptedException e)
|
||||||
|
{
|
||||||
|
Thread.currentThread().interrupt();
|
||||||
}
|
}
|
||||||
mProfileExecutor.shutdown();
|
|
||||||
mDataSource.close();
|
mDataSource.close();
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -461,6 +494,11 @@ public class CharonVpnService extends VpnService implements Runnable, VpnStateSe
|
|||||||
if (mNextProfile == null)
|
if (mNextProfile == null)
|
||||||
{
|
{
|
||||||
setState(State.DISABLED);
|
setState(State.DISABLED);
|
||||||
|
/* nothing was running (e.g. DISCONNECT while already
|
||||||
|
* disconnected, or the profile failed to load): drop the
|
||||||
|
* placeholder notification so the service is not kept
|
||||||
|
* in the foreground with a stale "Disabled" state */
|
||||||
|
removeNotification();
|
||||||
if (mTerminate)
|
if (mTerminate)
|
||||||
{
|
{
|
||||||
break;
|
break;
|
||||||
@@ -537,7 +575,25 @@ public class CharonVpnService extends VpnService implements Runnable, VpnStateSe
|
|||||||
* and let the regular retry machinery handle it. */
|
* and let the regular retry machinery handle it. */
|
||||||
Log.e(TAG, "Exception in the connection thread", ex);
|
Log.e(TAG, "Exception in the connection thread", ex);
|
||||||
DiagLog.i(TAG, "exception in connection thread: " + ex);
|
DiagLog.i(TAG, "exception in connection thread: " + ex);
|
||||||
stopCurrentConnection();
|
try
|
||||||
|
{
|
||||||
|
stopCurrentConnection();
|
||||||
|
}
|
||||||
|
catch (Exception again)
|
||||||
|
{ /* the exception may have come from stopCurrentConnection()
|
||||||
|
* itself (e.g. a broken next profile): drop that profile
|
||||||
|
* so this guard cannot throw again and kill the thread */
|
||||||
|
DiagLog.i(TAG, "cleanup after exception failed: " + again);
|
||||||
|
mNextProfile = null;
|
||||||
|
if (mCurrentProfile != null)
|
||||||
|
{
|
||||||
|
SimpleFetcher.disable();
|
||||||
|
deinitializeCharon();
|
||||||
|
sUserspacePushActive = false;
|
||||||
|
}
|
||||||
|
removeNotification();
|
||||||
|
mBuilderAdapter.closeBlocking();
|
||||||
|
}
|
||||||
setError(ErrorState.GENERIC_ERROR);
|
setError(ErrorState.GENERIC_ERROR);
|
||||||
setState(State.DISABLED);
|
setState(State.DISABLED);
|
||||||
mCurrentProfile = null;
|
mCurrentProfile = null;
|
||||||
@@ -593,11 +649,28 @@ public class CharonVpnService extends VpnService implements Runnable, VpnStateSe
|
|||||||
public void run()
|
public void run()
|
||||||
{
|
{
|
||||||
mShowNotification = true;
|
mShowNotification = true;
|
||||||
startForeground(VPN_STATE_NOTIFICATION_ID, buildNotification(false));
|
startForegroundSafe();
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Promote the service to the foreground. Never throws: on Android 12+ the
|
||||||
|
* system may refuse the promotion while the app is in the background, which
|
||||||
|
* must not crash the process.
|
||||||
|
*/
|
||||||
|
private void startForegroundSafe()
|
||||||
|
{
|
||||||
|
try
|
||||||
|
{
|
||||||
|
startForeground(VPN_STATE_NOTIFICATION_ID, buildNotification(false));
|
||||||
|
}
|
||||||
|
catch (RuntimeException e)
|
||||||
|
{
|
||||||
|
DiagLog.i(TAG, "startForeground refused: " + e);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Remove the permanent notification.
|
* Remove the permanent notification.
|
||||||
*/
|
*/
|
||||||
|
|||||||
Reference in new issue
Block a user