mirror of
https://github.com/permissionlesstech/bitchat-android.git
synced 2026-07-25 22:05:26 +00:00
Prevent quit notification from reappearing (#530)
* shutdown sequence * Prevent quit notification from reappearing * Restrict force-finish broadcast * Cancel quit shutdown on relaunch
This commit is contained in:
@@ -19,6 +19,8 @@
|
|||||||
|
|
||||||
<!-- Notification permissions -->
|
<!-- Notification permissions -->
|
||||||
<uses-permission android:name="android.permission.POST_NOTIFICATIONS" />
|
<uses-permission android:name="android.permission.POST_NOTIFICATIONS" />
|
||||||
|
<!-- Signature permission for internal UI shutdown broadcasts -->
|
||||||
|
<uses-permission android:name="com.bitchat.android.permission.FORCE_FINISH" />
|
||||||
<!-- Foreground service and boot permissions for long-running background mesh -->
|
<!-- Foreground service and boot permissions for long-running background mesh -->
|
||||||
<uses-permission android:name="android.permission.FOREGROUND_SERVICE" />
|
<uses-permission android:name="android.permission.FOREGROUND_SERVICE" />
|
||||||
<!-- Connected device foreground service type for BLE operations (API 34+) -->
|
<!-- Connected device foreground service type for BLE operations (API 34+) -->
|
||||||
@@ -46,6 +48,10 @@
|
|||||||
<uses-feature android:name="android.hardware.bluetooth_le" android:required="true" />
|
<uses-feature android:name="android.hardware.bluetooth_le" android:required="true" />
|
||||||
<uses-feature android:name="android.hardware.bluetooth" android:required="true" />
|
<uses-feature android:name="android.hardware.bluetooth" android:required="true" />
|
||||||
|
|
||||||
|
<permission
|
||||||
|
android:name="com.bitchat.android.permission.FORCE_FINISH"
|
||||||
|
android:protectionLevel="signature" />
|
||||||
|
|
||||||
<application
|
<application
|
||||||
android:name=".BitchatApplication"
|
android:name=".BitchatApplication"
|
||||||
android:allowBackup="false"
|
android:allowBackup="false"
|
||||||
|
|||||||
@@ -63,9 +63,38 @@ class MainActivity : OrientationAwareActivity() {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private val forceFinishReceiver = object : android.content.BroadcastReceiver() {
|
||||||
|
override fun onReceive(context: android.content.Context, intent: android.content.Intent) {
|
||||||
|
if (intent.action == com.bitchat.android.util.AppConstants.UI.ACTION_FORCE_FINISH) {
|
||||||
|
android.util.Log.i("MainActivity", "Received force finish broadcast, closing UI")
|
||||||
|
finishAffinity()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
override fun onCreate(savedInstanceState: Bundle?) {
|
override fun onCreate(savedInstanceState: Bundle?) {
|
||||||
super.onCreate(savedInstanceState)
|
super.onCreate(savedInstanceState)
|
||||||
|
|
||||||
|
// Register receiver for force finish signal from shutdown coordinator
|
||||||
|
val filter = android.content.IntentFilter(com.bitchat.android.util.AppConstants.UI.ACTION_FORCE_FINISH)
|
||||||
|
if (android.os.Build.VERSION.SDK_INT >= 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
|
// Check if this is a quit request from the notification
|
||||||
if (intent.getBooleanExtra("ACTION_QUIT_APP", false)) {
|
if (intent.getBooleanExtra("ACTION_QUIT_APP", false)) {
|
||||||
android.util.Log.d("MainActivity", "Quit request received in onCreate, finishing activity")
|
android.util.Log.d("MainActivity", "Quit request received in onCreate, finishing activity")
|
||||||
@@ -73,6 +102,8 @@ class MainActivity : OrientationAwareActivity() {
|
|||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
|
com.bitchat.android.service.AppShutdownCoordinator.cancelPendingShutdown()
|
||||||
|
|
||||||
// Enable edge-to-edge display for modern Android look
|
// Enable edge-to-edge display for modern Android look
|
||||||
enableEdgeToEdge()
|
enableEdgeToEdge()
|
||||||
|
|
||||||
@@ -647,6 +678,8 @@ class MainActivity : OrientationAwareActivity() {
|
|||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
|
com.bitchat.android.service.AppShutdownCoordinator.cancelPendingShutdown()
|
||||||
|
|
||||||
// Handle notification intents when app is already running
|
// Handle notification intents when app is already running
|
||||||
if (mainViewModel.onboardingState.value == OnboardingState.COMPLETE) {
|
if (mainViewModel.onboardingState.value == OnboardingState.COMPLETE) {
|
||||||
handleNotificationIntent(intent)
|
handleNotificationIntent(intent)
|
||||||
@@ -759,6 +792,8 @@ class MainActivity : OrientationAwareActivity() {
|
|||||||
override fun onDestroy() {
|
override fun onDestroy() {
|
||||||
super.onDestroy()
|
super.onDestroy()
|
||||||
|
|
||||||
|
try { unregisterReceiver(forceFinishReceiver) } catch (_: Exception) { }
|
||||||
|
|
||||||
// Cleanup location status manager
|
// Cleanup location status manager
|
||||||
try {
|
try {
|
||||||
locationStatusManager.cleanup()
|
locationStatusManager.cleanup()
|
||||||
|
|||||||
@@ -9,10 +9,13 @@ import com.bitchat.android.net.TorMode
|
|||||||
import kotlinx.coroutines.CoroutineScope
|
import kotlinx.coroutines.CoroutineScope
|
||||||
import kotlinx.coroutines.Dispatchers
|
import kotlinx.coroutines.Dispatchers
|
||||||
import kotlinx.coroutines.SupervisorJob
|
import kotlinx.coroutines.SupervisorJob
|
||||||
|
import kotlinx.coroutines.Job
|
||||||
import kotlinx.coroutines.async
|
import kotlinx.coroutines.async
|
||||||
import kotlinx.coroutines.delay
|
import kotlinx.coroutines.delay
|
||||||
|
import kotlinx.coroutines.isActive
|
||||||
import kotlinx.coroutines.launch
|
import kotlinx.coroutines.launch
|
||||||
import kotlinx.coroutines.withTimeoutOrNull
|
import kotlinx.coroutines.withTimeoutOrNull
|
||||||
|
import java.util.concurrent.atomic.AtomicLong
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Coordinates a full application shutdown:
|
* Coordinates a full application shutdown:
|
||||||
@@ -24,6 +27,15 @@ import kotlinx.coroutines.withTimeoutOrNull
|
|||||||
*/
|
*/
|
||||||
object AppShutdownCoordinator {
|
object AppShutdownCoordinator {
|
||||||
private val scope = CoroutineScope(Dispatchers.Default + SupervisorJob())
|
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(
|
fun requestFullShutdownAndKill(
|
||||||
app: Application,
|
app: Application,
|
||||||
@@ -32,7 +44,16 @@ object AppShutdownCoordinator {
|
|||||||
stopForeground: () -> Unit,
|
stopForeground: () -> Unit,
|
||||||
stopService: () -> 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)
|
// Stop mesh (best-effort)
|
||||||
try { mesh?.stopServices() } catch (_: Exception) { }
|
try { mesh?.stopServices() } catch (_: Exception) { }
|
||||||
|
|
||||||
@@ -56,12 +77,19 @@ object AppShutdownCoordinator {
|
|||||||
}
|
}
|
||||||
|
|
||||||
// Stop the service itself
|
// Stop the service itself
|
||||||
|
if (!isActive || shutdownToken.get() != token) return@launch
|
||||||
try { stopService() } catch (_: Exception) { }
|
try { stopService() } catch (_: Exception) { }
|
||||||
|
|
||||||
// Hard kill the app process
|
// Hard kill the app process
|
||||||
|
if (!isActive || shutdownToken.get() != token) return@launch
|
||||||
try { Process.killProcess(Process.myPid()) } catch (_: Exception) { }
|
try { Process.killProcess(Process.myPid()) } catch (_: Exception) { }
|
||||||
try { System.exit(0) } catch (_: Exception) { }
|
try { System.exit(0) } catch (_: Exception) { }
|
||||||
}
|
}
|
||||||
|
shutdownJob = job
|
||||||
|
job.invokeOnCompletion {
|
||||||
|
if (shutdownJob === job) {
|
||||||
|
shutdownJob = null
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -115,6 +115,7 @@ class MeshForegroundService : Service() {
|
|||||||
private val serviceJob = Job()
|
private val serviceJob = Job()
|
||||||
private val scope = CoroutineScope(Dispatchers.Default + serviceJob)
|
private val scope = CoroutineScope(Dispatchers.Default + serviceJob)
|
||||||
private var isInForeground: Boolean = false
|
private var isInForeground: Boolean = false
|
||||||
|
private var isShuttingDown: Boolean = false
|
||||||
|
|
||||||
override fun onCreate() {
|
override fun onCreate() {
|
||||||
super.onCreate()
|
super.onCreate()
|
||||||
@@ -133,6 +134,13 @@ class MeshForegroundService : Service() {
|
|||||||
}
|
}
|
||||||
|
|
||||||
override fun onStartCommand(intent: Intent?, flags: Int, startId: Int): Int {
|
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) {
|
when (intent?.action) {
|
||||||
ACTION_STOP -> {
|
ACTION_STOP -> {
|
||||||
// Stop FGS and mesh cleanly
|
// Stop FGS and mesh cleanly
|
||||||
@@ -145,6 +153,12 @@ class MeshForegroundService : Service() {
|
|||||||
return START_NOT_STICKY
|
return START_NOT_STICKY
|
||||||
}
|
}
|
||||||
ACTION_QUIT -> {
|
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
|
// Fully stop all background activity, stop Tor (without changing setting), then kill the app
|
||||||
AppShutdownCoordinator.requestFullShutdownAndKill(
|
AppShutdownCoordinator.requestFullShutdownAndKill(
|
||||||
app = application,
|
app = application,
|
||||||
@@ -208,6 +222,7 @@ class MeshForegroundService : Service() {
|
|||||||
}
|
}
|
||||||
|
|
||||||
private fun ensureMeshStarted() {
|
private fun ensureMeshStarted() {
|
||||||
|
if (isShuttingDown) return
|
||||||
if (!hasBluetoothPermissions()) return
|
if (!hasBluetoothPermissions()) return
|
||||||
try {
|
try {
|
||||||
android.util.Log.d("MeshForegroundService", "Ensuring mesh service is started")
|
android.util.Log.d("MeshForegroundService", "Ensuring mesh service is started")
|
||||||
@@ -218,6 +233,10 @@ class MeshForegroundService : Service() {
|
|||||||
}
|
}
|
||||||
|
|
||||||
private fun updateNotification(force: Boolean) {
|
private fun updateNotification(force: Boolean) {
|
||||||
|
if (isShuttingDown) {
|
||||||
|
notificationManager.cancel(NOTIFICATION_ID)
|
||||||
|
return
|
||||||
|
}
|
||||||
val count = meshService?.getActivePeerCount() ?: 0
|
val count = meshService?.getActivePeerCount() ?: 0
|
||||||
val notification = buildNotification(count)
|
val notification = buildNotification(count)
|
||||||
if (MeshServicePreferences.isBackgroundEnabled(true) && hasAllRequiredPermissions()) {
|
if (MeshServicePreferences.isBackgroundEnabled(true) && hasAllRequiredPermissions()) {
|
||||||
|
|||||||
@@ -115,6 +115,8 @@ object AppConstants {
|
|||||||
const val MESSAGE_DEDUP_TIMEOUT_MS: Long = 30_000L
|
const val MESSAGE_DEDUP_TIMEOUT_MS: Long = 30_000L
|
||||||
const val SYSTEM_EVENT_DEDUP_TIMEOUT_MS: Long = 5_000L
|
const val SYSTEM_EVENT_DEDUP_TIMEOUT_MS: Long = 5_000L
|
||||||
const val ACTIVE_PEERS_NOTIFICATION_INTERVAL_MS: Long = 300_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 {
|
object Media {
|
||||||
|
|||||||
Reference in New Issue
Block a user