From f4c41b1851cc84419aa2f66f12ca4cc145f9ea07 Mon Sep 17 00:00:00 2001 From: Moritz Date: Mon, 16 Feb 2026 17:12:23 +0100 Subject: [PATCH] Add foreground service for reliable broadcast reception Android 8.0+ blocks implicit broadcasts to manifest-registered receivers. Added ListenerService that dynamically registers the NtfyReceiver, ensuring LOCATE commands from the ntfy app are always received. Also handles POST_NOTIFICATIONS permission. Co-Authored-By: Claude Opus 4.6 --- app/src/main/AndroidManifest.xml | 8 ++ .../helios_location_finder/ListenerService.kt | 80 +++++++++++++++++++ .../helios_location_finder/MainActivity.kt | 23 ++++++ 3 files changed, 111 insertions(+) create mode 100644 app/src/main/java/com/example/helios_location_finder/ListenerService.kt diff --git a/app/src/main/AndroidManifest.xml b/app/src/main/AndroidManifest.xml index 941b4d5..efadaff 100644 --- a/app/src/main/AndroidManifest.xml +++ b/app/src/main/AndroidManifest.xml @@ -6,6 +6,9 @@ + + + + + diff --git a/app/src/main/java/com/example/helios_location_finder/ListenerService.kt b/app/src/main/java/com/example/helios_location_finder/ListenerService.kt new file mode 100644 index 0000000..acafb32 --- /dev/null +++ b/app/src/main/java/com/example/helios_location_finder/ListenerService.kt @@ -0,0 +1,80 @@ +package com.example.helios_location_finder + +import android.app.Notification +import android.app.NotificationChannel +import android.app.NotificationManager +import android.app.PendingIntent +import android.app.Service +import android.content.Intent +import android.content.IntentFilter +import android.os.IBinder +import android.util.Log +import androidx.core.content.ContextCompat + +class ListenerService : Service() { + + companion object { + private const val TAG = "ListenerService" + private const val CHANNEL_ID = "helios_listener" + private const val NOTIFICATION_ID = 1 + + fun start(context: android.content.Context) { + val intent = Intent(context, ListenerService::class.java) + ContextCompat.startForegroundService(context, intent) + } + } + + private val receiver = NtfyReceiver() + + override fun onCreate() { + super.onCreate() + createNotificationChannel() + startForeground(NOTIFICATION_ID, buildNotification()) + + val filter = IntentFilter("io.heckel.ntfy.MESSAGE_RECEIVED") + ContextCompat.registerReceiver( + this, receiver, filter, ContextCompat.RECEIVER_EXPORTED + ) + Log.d(TAG, "Listener service started, receiver registered") + } + + override fun onDestroy() { + unregisterReceiver(receiver) + Log.d(TAG, "Listener service stopped, receiver unregistered") + super.onDestroy() + } + + override fun onStartCommand(intent: Intent?, flags: Int, startId: Int): Int { + return START_STICKY + } + + override fun onBind(intent: Intent?): IBinder? = null + + private fun createNotificationChannel() { + val channel = NotificationChannel( + CHANNEL_ID, + "Helios Tracker", + NotificationManager.IMPORTANCE_LOW + ).apply { + description = "Keeps Helios Tracker listening for LOCATE commands" + setShowBadge(false) + } + getSystemService(NotificationManager::class.java).createNotificationChannel(channel) + } + + private fun buildNotification(): Notification { + val pendingIntent = PendingIntent.getActivity( + this, 0, + Intent(this, MainActivity::class.java), + PendingIntent.FLAG_IMMUTABLE + ) + + return Notification.Builder(this, CHANNEL_ID) + .setContentTitle("Helios Tracker") + .setContentText("Lauscht auf LOCATE-Anfragen") + .setSmallIcon(android.R.drawable.ic_menu_mylocation) + .setContentIntent(pendingIntent) + .setOngoing(true) + .build() + } +} diff --git a/app/src/main/java/com/example/helios_location_finder/MainActivity.kt b/app/src/main/java/com/example/helios_location_finder/MainActivity.kt index 451365c..40bdf26 100644 --- a/app/src/main/java/com/example/helios_location_finder/MainActivity.kt +++ b/app/src/main/java/com/example/helios_location_finder/MainActivity.kt @@ -50,6 +50,7 @@ class MainActivity : ComponentActivity() { private val foregroundGranted = mutableStateOf(false) private val backgroundGranted = mutableStateOf(false) + private val serviceRunning = mutableStateOf(false) private val foregroundPermissionLauncher = registerForActivityResult( ActivityResultContracts.RequestMultiplePermissions() @@ -62,11 +63,19 @@ class MainActivity : ComponentActivity() { ActivityResultContracts.RequestPermission() ) { granted -> backgroundGranted.value = granted + if (granted) startListenerService() + } + + private val notificationPermissionLauncher = registerForActivityResult( + ActivityResultContracts.RequestPermission() + ) { granted -> + if (granted) startListenerService() } override fun onCreate(savedInstanceState: Bundle?) { super.onCreate(savedInstanceState) checkPermissions() + startListenerService() val listenTopic = mutableStateOf(Prefs.getListenTopic(this)) val replyTopic = mutableStateOf(Prefs.getReplyTopic(this)) @@ -133,6 +142,20 @@ class MainActivity : ComponentActivity() { backgroundPermissionLauncher.launch(Manifest.permission.ACCESS_BACKGROUND_LOCATION) } } + + private fun startListenerService() { + if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.TIRAMISU) { + if (ContextCompat.checkSelfPermission( + this, Manifest.permission.POST_NOTIFICATIONS + ) != PackageManager.PERMISSION_GRANTED + ) { + notificationPermissionLauncher.launch(Manifest.permission.POST_NOTIFICATIONS) + return + } + } + ListenerService.start(this) + serviceRunning.value = true + } } @Composable