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 <noreply@anthropic.com>
This commit is contained in:
Moritz 2026-02-16 17:12:23 +01:00
parent 2f54b5a085
commit f4c41b1851
3 changed files with 111 additions and 0 deletions

View file

@ -6,6 +6,9 @@
<uses-permission android:name="android.permission.ACCESS_COARSE_LOCATION" /> <uses-permission android:name="android.permission.ACCESS_COARSE_LOCATION" />
<uses-permission android:name="android.permission.ACCESS_BACKGROUND_LOCATION" /> <uses-permission android:name="android.permission.ACCESS_BACKGROUND_LOCATION" />
<uses-permission android:name="android.permission.INTERNET" /> <uses-permission android:name="android.permission.INTERNET" />
<uses-permission android:name="android.permission.FOREGROUND_SERVICE" />
<uses-permission android:name="android.permission.FOREGROUND_SERVICE_LOCATION" />
<uses-permission android:name="android.permission.POST_NOTIFICATIONS" />
<application <application
android:allowBackup="true" android:allowBackup="true"
@ -28,6 +31,11 @@
</intent-filter> </intent-filter>
</activity> </activity>
<service
android:name=".ListenerService"
android:foregroundServiceType="location"
android:exported="false" />
<receiver <receiver
android:name=".NtfyReceiver" android:name=".NtfyReceiver"
android:exported="true"> android:exported="true">

View file

@ -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()
}
}

View file

@ -50,6 +50,7 @@ class MainActivity : ComponentActivity() {
private val foregroundGranted = mutableStateOf(false) private val foregroundGranted = mutableStateOf(false)
private val backgroundGranted = mutableStateOf(false) private val backgroundGranted = mutableStateOf(false)
private val serviceRunning = mutableStateOf(false)
private val foregroundPermissionLauncher = registerForActivityResult( private val foregroundPermissionLauncher = registerForActivityResult(
ActivityResultContracts.RequestMultiplePermissions() ActivityResultContracts.RequestMultiplePermissions()
@ -62,11 +63,19 @@ class MainActivity : ComponentActivity() {
ActivityResultContracts.RequestPermission() ActivityResultContracts.RequestPermission()
) { granted -> ) { granted ->
backgroundGranted.value = granted backgroundGranted.value = granted
if (granted) startListenerService()
}
private val notificationPermissionLauncher = registerForActivityResult(
ActivityResultContracts.RequestPermission()
) { granted ->
if (granted) startListenerService()
} }
override fun onCreate(savedInstanceState: Bundle?) { override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState) super.onCreate(savedInstanceState)
checkPermissions() checkPermissions()
startListenerService()
val listenTopic = mutableStateOf(Prefs.getListenTopic(this)) val listenTopic = mutableStateOf(Prefs.getListenTopic(this))
val replyTopic = mutableStateOf(Prefs.getReplyTopic(this)) val replyTopic = mutableStateOf(Prefs.getReplyTopic(this))
@ -133,6 +142,20 @@ class MainActivity : ComponentActivity() {
backgroundPermissionLauncher.launch(Manifest.permission.ACCESS_BACKGROUND_LOCATION) 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 @Composable