From 903a4584a8f72988475a56bfcd604daf7aad99a0 Mon Sep 17 00:00:00 2001
From: callebtc <93376500+callebtc@users.noreply.github.com>
Date: Fri, 2 Jan 2026 16:52:06 +0700
Subject: [PATCH] Prevent quit notification from reappearing (#530)
* shutdown sequence
* Prevent quit notification from reappearing
* Restrict force-finish broadcast
* Cancel quit shutdown on relaunch
---
app/src/main/AndroidManifest.xml | 6 ++++
.../java/com/bitchat/android/MainActivity.kt | 35 +++++++++++++++++++
.../android/service/AppShutdownCoordinator.kt | 32 +++++++++++++++--
.../android/service/MeshForegroundService.kt | 19 ++++++++++
.../com/bitchat/android/util/AppConstants.kt | 2 ++
5 files changed, 92 insertions(+), 2 deletions(-)
diff --git a/app/src/main/AndroidManifest.xml b/app/src/main/AndroidManifest.xml
index b228fd34..f4ca2b89 100644
--- a/app/src/main/AndroidManifest.xml
+++ b/app/src/main/AndroidManifest.xml
@@ -19,6 +19,8 @@
+
+
@@ -46,6 +48,10 @@
+
+
= 33) {
+ registerReceiver(
+ forceFinishReceiver,
+ filter,
+ com.bitchat.android.util.AppConstants.UI.PERMISSION_FORCE_FINISH,
+ null,
+ android.content.Context.RECEIVER_NOT_EXPORTED
+ )
+ } else {
+ @Suppress("DEPRECATION")
+ registerReceiver(
+ forceFinishReceiver,
+ filter,
+ com.bitchat.android.util.AppConstants.UI.PERMISSION_FORCE_FINISH,
+ null
+ )
+ }
+
// Check if this is a quit request from the notification
if (intent.getBooleanExtra("ACTION_QUIT_APP", false)) {
android.util.Log.d("MainActivity", "Quit request received in onCreate, finishing activity")
finish()
return
}
+
+ com.bitchat.android.service.AppShutdownCoordinator.cancelPendingShutdown()
// Enable edge-to-edge display for modern Android look
enableEdgeToEdge()
@@ -646,6 +677,8 @@ class MainActivity : OrientationAwareActivity() {
finish()
return
}
+
+ com.bitchat.android.service.AppShutdownCoordinator.cancelPendingShutdown()
// Handle notification intents when app is already running
if (mainViewModel.onboardingState.value == OnboardingState.COMPLETE) {
@@ -759,6 +792,8 @@ class MainActivity : OrientationAwareActivity() {
override fun onDestroy() {
super.onDestroy()
+ try { unregisterReceiver(forceFinishReceiver) } catch (_: Exception) { }
+
// Cleanup location status manager
try {
locationStatusManager.cleanup()
diff --git a/app/src/main/java/com/bitchat/android/service/AppShutdownCoordinator.kt b/app/src/main/java/com/bitchat/android/service/AppShutdownCoordinator.kt
index d4e00104..1c3abaf9 100644
--- a/app/src/main/java/com/bitchat/android/service/AppShutdownCoordinator.kt
+++ b/app/src/main/java/com/bitchat/android/service/AppShutdownCoordinator.kt
@@ -9,10 +9,13 @@ import com.bitchat.android.net.TorMode
import kotlinx.coroutines.CoroutineScope
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.SupervisorJob
+import kotlinx.coroutines.Job
import kotlinx.coroutines.async
import kotlinx.coroutines.delay
+import kotlinx.coroutines.isActive
import kotlinx.coroutines.launch
import kotlinx.coroutines.withTimeoutOrNull
+import java.util.concurrent.atomic.AtomicLong
/**
* Coordinates a full application shutdown:
@@ -24,6 +27,15 @@ import kotlinx.coroutines.withTimeoutOrNull
*/
object AppShutdownCoordinator {
private val scope = CoroutineScope(Dispatchers.Default + SupervisorJob())
+ private val shutdownToken = AtomicLong(0L)
+ @Volatile
+ private var shutdownJob: Job? = null
+
+ fun cancelPendingShutdown() {
+ shutdownToken.incrementAndGet()
+ shutdownJob?.cancel()
+ shutdownJob = null
+ }
fun requestFullShutdownAndKill(
app: Application,
@@ -32,7 +44,16 @@ object AppShutdownCoordinator {
stopForeground: () -> Unit,
stopService: () -> Unit
) {
- scope.launch {
+ val token = shutdownToken.incrementAndGet()
+ shutdownJob?.cancel()
+ val job = scope.launch {
+ // Signal UI to finish gracefully before we kill the process
+ try {
+ val intent = android.content.Intent(com.bitchat.android.util.AppConstants.UI.ACTION_FORCE_FINISH)
+ .setPackage(app.packageName)
+ app.sendBroadcast(intent, com.bitchat.android.util.AppConstants.UI.PERMISSION_FORCE_FINISH)
+ } catch (_: Exception) { }
+
// Stop mesh (best-effort)
try { mesh?.stopServices() } catch (_: Exception) { }
@@ -56,12 +77,19 @@ object AppShutdownCoordinator {
}
// Stop the service itself
+ if (!isActive || shutdownToken.get() != token) return@launch
try { stopService() } catch (_: Exception) { }
// Hard kill the app process
+ if (!isActive || shutdownToken.get() != token) return@launch
try { Process.killProcess(Process.myPid()) } catch (_: Exception) { }
try { System.exit(0) } catch (_: Exception) { }
}
+ shutdownJob = job
+ job.invokeOnCompletion {
+ if (shutdownJob === job) {
+ shutdownJob = null
+ }
+ }
}
}
-
diff --git a/app/src/main/java/com/bitchat/android/service/MeshForegroundService.kt b/app/src/main/java/com/bitchat/android/service/MeshForegroundService.kt
index 7510fa06..e92274f8 100644
--- a/app/src/main/java/com/bitchat/android/service/MeshForegroundService.kt
+++ b/app/src/main/java/com/bitchat/android/service/MeshForegroundService.kt
@@ -115,6 +115,7 @@ class MeshForegroundService : Service() {
private val serviceJob = Job()
private val scope = CoroutineScope(Dispatchers.Default + serviceJob)
private var isInForeground: Boolean = false
+ private var isShuttingDown: Boolean = false
override fun onCreate() {
super.onCreate()
@@ -133,6 +134,13 @@ class MeshForegroundService : Service() {
}
override fun onStartCommand(intent: Intent?, flags: Int, startId: Int): Int {
+ if (isShuttingDown && intent?.action == ACTION_START) {
+ AppShutdownCoordinator.cancelPendingShutdown()
+ isShuttingDown = false
+ }
+ if (isShuttingDown && intent?.action != ACTION_QUIT) {
+ return START_NOT_STICKY
+ }
when (intent?.action) {
ACTION_STOP -> {
// Stop FGS and mesh cleanly
@@ -145,6 +153,12 @@ class MeshForegroundService : Service() {
return START_NOT_STICKY
}
ACTION_QUIT -> {
+ isShuttingDown = true
+ updateJob?.cancel()
+ updateJob = null
+ try { stopForeground(true) } catch (_: Exception) { }
+ notificationManager.cancel(NOTIFICATION_ID)
+ isInForeground = false
// Fully stop all background activity, stop Tor (without changing setting), then kill the app
AppShutdownCoordinator.requestFullShutdownAndKill(
app = application,
@@ -208,6 +222,7 @@ class MeshForegroundService : Service() {
}
private fun ensureMeshStarted() {
+ if (isShuttingDown) return
if (!hasBluetoothPermissions()) return
try {
android.util.Log.d("MeshForegroundService", "Ensuring mesh service is started")
@@ -218,6 +233,10 @@ class MeshForegroundService : Service() {
}
private fun updateNotification(force: Boolean) {
+ if (isShuttingDown) {
+ notificationManager.cancel(NOTIFICATION_ID)
+ return
+ }
val count = meshService?.getActivePeerCount() ?: 0
val notification = buildNotification(count)
if (MeshServicePreferences.isBackgroundEnabled(true) && hasAllRequiredPermissions()) {
diff --git a/app/src/main/java/com/bitchat/android/util/AppConstants.kt b/app/src/main/java/com/bitchat/android/util/AppConstants.kt
index 9881cc2f..ca031df9 100644
--- a/app/src/main/java/com/bitchat/android/util/AppConstants.kt
+++ b/app/src/main/java/com/bitchat/android/util/AppConstants.kt
@@ -115,6 +115,8 @@ object AppConstants {
const val MESSAGE_DEDUP_TIMEOUT_MS: Long = 30_000L
const val SYSTEM_EVENT_DEDUP_TIMEOUT_MS: Long = 5_000L
const val ACTIVE_PEERS_NOTIFICATION_INTERVAL_MS: Long = 300_000L
+ const val ACTION_FORCE_FINISH: String = "com.bitchat.android.ACTION_FORCE_FINISH"
+ const val PERMISSION_FORCE_FINISH: String = "com.bitchat.android.permission.FORCE_FINISH"
}
object Media {