From 275581cfa394a049d14388f5655ef54c3ee44bce Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?lollipopkit=F0=9F=8F=B3=EF=B8=8F=E2=80=8D=E2=9A=A7?= =?UTF-8?q?=EF=B8=8F?= <10864310+lollipopkit@users.noreply.github.com> Date: Sun, 14 Sep 2025 22:34:01 +0800 Subject: [PATCH] fix: notification permission (#914) --- .../tech/lolli/toolbox/ForegroundService.kt | 94 ++++++++++++++----- .../kotlin/tech/lolli/toolbox/MainActivity.kt | 47 ++++++++-- 2 files changed, 107 insertions(+), 34 deletions(-) diff --git a/android/app/src/main/kotlin/tech/lolli/toolbox/ForegroundService.kt b/android/app/src/main/kotlin/tech/lolli/toolbox/ForegroundService.kt index 9139a243..c15b46e0 100644 --- a/android/app/src/main/kotlin/tech/lolli/toolbox/ForegroundService.kt +++ b/android/app/src/main/kotlin/tech/lolli/toolbox/ForegroundService.kt @@ -2,6 +2,8 @@ package tech.lolli.toolbox import android.app.* import android.content.Intent +import android.content.pm.ServiceInfo +import android.graphics.drawable.Icon import android.os.Build import android.os.IBinder import android.util.Log @@ -48,19 +50,22 @@ class ForegroundService : Service() { override fun onStartCommand(intent: Intent?, flags: Int, startId: Int): Int { try { + // Check notification permission for Android 13+ if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.TIRAMISU && androidx.core.content.ContextCompat.checkSelfPermission( this, android.Manifest.permission.POST_NOTIFICATIONS ) != android.content.pm.PackageManager.PERMISSION_GRANTED ) { - Log.w("ForegroundService", "Notification permission denied. Stopping service.") - stopForegroundService() + Log.w("ForegroundService", "Notification permission denied. Stopping service gracefully.") + // Don't call stopForegroundService() here as we haven't started foreground yet + stopSelf() return START_NOT_STICKY } if (intent == null) { Log.w("ForegroundService", "onStartCommand called with null intent") - stopForegroundService() + // Don't call stopForegroundService() here as we haven't started foreground yet + stopSelf() return START_NOT_STICKY } @@ -101,33 +106,62 @@ class ForegroundService : Service() { private fun createNotificationChannel() { if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O) { - val manager = getSystemService(NotificationManager::class.java) - if (manager == null) { - Log.e("ForegroundService", "Failed to get NotificationManager") - return + try { + val manager = getSystemService(NotificationManager::class.java) + if (manager == null) { + Log.e("ForegroundService", "Failed to get NotificationManager") + return + } + val serviceChannel = NotificationChannel( + chanId, + "ForegroundServiceChannel", + NotificationManager.IMPORTANCE_DEFAULT + ).apply { + description = "For foreground service" + } + manager.createNotificationChannel(serviceChannel) + Log.d("ForegroundService", "Notification channel created successfully") + } catch (e: Exception) { + logError("Failed to create notification channel", e) } - val serviceChannel = NotificationChannel( - chanId, - "ForegroundServiceChannel", - NotificationManager.IMPORTANCE_DEFAULT - ).apply { - description = "For foreground service" - } - manager.createNotificationChannel(serviceChannel) } } private fun ensureForeground(notification: Notification) { try { + // Double-check notification permission before starting foreground service + if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.TIRAMISU && + androidx.core.content.ContextCompat.checkSelfPermission( + this, android.Manifest.permission.POST_NOTIFICATIONS + ) != android.content.pm.PackageManager.PERMISSION_GRANTED + ) { + Log.w("ForegroundService", "Cannot start foreground service without notification permission") + stopSelf() + return + } + if (!isFgStarted) { - startForeground(NOTIFICATION_ID, notification) + if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.Q) { + startForeground(NOTIFICATION_ID, notification, ServiceInfo.FOREGROUND_SERVICE_TYPE_DATA_SYNC) + } else { + startForeground(NOTIFICATION_ID, notification) + } isFgStarted = true + Log.d("ForegroundService", "Foreground service started successfully") } else { val nm = getSystemService(NotificationManager::class.java) - nm?.notify(NOTIFICATION_ID, notification) + if (nm != null) { + nm.notify(NOTIFICATION_ID, notification) + } else { + Log.w("ForegroundService", "NotificationManager is null, cannot update notification") + } } + } catch (e: SecurityException) { + logError("Security exception when starting foreground service (likely missing permission)", e) + stopSelf() } catch (e: Exception) { logError("Failed to start/update foreground", e) + // Don't stop the service for other exceptions, just log them } } @@ -143,21 +177,22 @@ class ForegroundService : Service() { val builder = if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O) { Notification.Builder(this, chanId) } else { + @Suppress("DEPRECATION") Notification.Builder(this) } // Use the earliest session's start time for chronometer val earliestStartTime = sessions.minOfOrNull { it.startWhen } ?: System.currentTimeMillis() - val title = when { - count == 0 -> "Server Box" - count == 1 -> sessions.first().title + val title = when (count) { + 0 -> "Server Box" + 1 -> sessions.first().title else -> "SSH sessions: $count active" } - val contentText = when { - count == 0 -> "Ready for connections" - count == 1 -> { + val contentText = when (count) { + 0 -> "Ready for connections" + 1 -> { val session = sessions.first() "${session.subtitle} ยท ${session.status}" } @@ -189,7 +224,13 @@ class ForegroundService : Service() { .setOngoing(true) .setOnlyAlertOnce(true) .setContentIntent(pendingIntent) - .addAction(android.R.drawable.ic_delete, "Stop All", stopPending) + .addAction( + Notification.Action.Builder( + Icon.createWithResource(this, android.R.drawable.ic_delete), + "Stop All", + stopPending + ).build() + ) if (style != null) { notification.setStyle(style) @@ -260,7 +301,10 @@ class ForegroundService : Service() { private fun stopForegroundService() { try { - stopForeground(true) + if (isFgStarted) { + stopForeground(STOP_FOREGROUND_REMOVE) + isFgStarted = false + } } catch (e: Exception) { logError("Error stopping foreground", e) } diff --git a/android/app/src/main/kotlin/tech/lolli/toolbox/MainActivity.kt b/android/app/src/main/kotlin/tech/lolli/toolbox/MainActivity.kt index 5565e2d9..58bb90f1 100644 --- a/android/app/src/main/kotlin/tech/lolli/toolbox/MainActivity.kt +++ b/android/app/src/main/kotlin/tech/lolli/toolbox/MainActivity.kt @@ -105,19 +105,24 @@ class MainActivity: FlutterFragmentActivity() { private fun reqPerm() { if (Build.VERSION.SDK_INT < Build.VERSION_CODES.TIRAMISU) return - // Check if we already have the permission to avoid unnecessary prompts - if (ContextCompat.checkSelfPermission(this, Manifest.permission.POST_NOTIFICATIONS) - != PackageManager.PERMISSION_GRANTED) { - try { + try { + // Check if we already have the permission to avoid unnecessary prompts + if (ContextCompat.checkSelfPermission(this, Manifest.permission.POST_NOTIFICATIONS) + != PackageManager.PERMISSION_GRANTED) { + // Check if we should show rationale + if (ActivityCompat.shouldShowRequestPermissionRationale(this, Manifest.permission.POST_NOTIFICATIONS)) { + android.util.Log.i("MainActivity", "User previously denied notification permission") + } + ActivityCompat.requestPermissions( this, arrayOf(Manifest.permission.POST_NOTIFICATIONS), 123, ) - } catch (e: Exception) { - // Log error but don't crash - android.util.Log.e("MainActivity", "Failed to request permissions: ${e.message}") } + } catch (e: Exception) { + // Log error but don't crash + android.util.Log.e("MainActivity", "Failed to request permissions: ${e.message}") } } @@ -163,13 +168,37 @@ class MainActivity: FlutterFragmentActivity() { } } val filter = IntentFilter(ACTION_STOP_ALL_CONNECTIONS) - registerReceiver(stopAllReceiver, filter) + if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.TIRAMISU) { + ContextCompat.registerReceiver(this, stopAllReceiver, filter, ContextCompat.RECEIVER_NOT_EXPORTED) + } else { + registerReceiver(stopAllReceiver, filter) + } + } + + override fun onRequestPermissionsResult( + requestCode: Int, + permissions: Array, + grantResults: IntArray + ) { + super.onRequestPermissionsResult(requestCode, permissions, grantResults) + if (requestCode == 123) { + if (grantResults.isNotEmpty() && grantResults[0] == PackageManager.PERMISSION_GRANTED) { + android.util.Log.i("MainActivity", "Notification permission granted") + } else { + android.util.Log.w("MainActivity", "Notification permission denied") + // Optionally inform user about the limitation + } + } } override fun onDestroy() { super.onDestroy() stopAllReceiver?.let { - unregisterReceiver(it) + try { + unregisterReceiver(it) + } catch (e: Exception) { + android.util.Log.e("MainActivity", "Failed to unregister receiver: ${e.message}") + } stopAllReceiver = null } }