diff --git a/app/src/main/AndroidManifest.xml b/app/src/main/AndroidManifest.xml
index 5b35ee8..931bab1 100644
--- a/app/src/main/AndroidManifest.xml
+++ b/app/src/main/AndroidManifest.xml
@@ -47,12 +47,16 @@
-
+
+
+
+
@@ -60,7 +64,6 @@
-
= Build.VERSION_CODES.O) {
- Log.d(TAG, "Starting subscriber service in >=26 Mode from a BroadcastReceiver")
- context.startForegroundService(it)
- return
- }
- Log.d(TAG, "Starting subscriber service in < 26 Mode from a BroadcastReceiver")
- context.startService(it)
+ Log.d(TAG, "BootStartReceiver: Starting subscriber service")
+ ContextCompat.startForegroundService(context, it)
}
}
}
}
+ // We are starting MyService via a worker and not directly because since Android 7
+ // (but officially since Lollipop!), any process called by a BroadcastReceiver
+ // (only manifest-declared receiver) is run at low priority and hence eventually
+ // killed by Android.
+ class AutoRestartReceiver : BroadcastReceiver() {
+ override fun onReceive(context: Context, intent: Intent) {
+ Log.d(TAG, "AutoRestartReceiver: onReceive called")
+ val workManager = WorkManager.getInstance(context)
+ val startServiceRequest = OneTimeWorkRequest.Builder(AutoRestartWorker::class.java).build()
+ workManager.enqueue(startServiceRequest)
+ }
+ }
+
+ class AutoRestartWorker(private val context: Context, params: WorkerParameters) : Worker(context, params) {
+ override fun doWork(): Result {
+ Log.d(TAG, "AutoRestartReceiver: doWork called for: " + this.getId())
+ if (readServiceState(context) == ServiceState.STARTED) {
+ Intent(context, SubscriberService::class.java).also {
+ it.action = Actions.START.name
+ Log.d(TAG, "AutoRestartReceiver: Starting subscriber service")
+ ContextCompat.startForegroundService(context, it)
+ }
+ }
+ return Result.success()
+ }
+ }
+
enum class Actions {
START,
STOP
@@ -261,7 +313,10 @@ class SubscriberService : Service() {
}
companion object {
- private const val TAG = "NtfySubscriberService"
+ const val TAG = "NtfySubscriberService"
+ const val AUTO_RESTART_WORKER_VERSION = BuildConfig.VERSION_CODE
+ const val AUTO_RESTART_WORKER_WORK_NAME_PERIODIC = "NtfyAutoRestartWorkerPeriodic"
+
private const val WAKE_LOCK_TAG = "SubscriberService:lock"
private const val NOTIFICATION_CHANNEL_ID = "ntfy-subscriber"
private const val NOTIFICATION_SERVICE_ID = 2586
diff --git a/app/src/main/java/io/heckel/ntfy/ui/MainActivity.kt b/app/src/main/java/io/heckel/ntfy/ui/MainActivity.kt
index 42a9d99..22def38 100644
--- a/app/src/main/java/io/heckel/ntfy/ui/MainActivity.kt
+++ b/app/src/main/java/io/heckel/ntfy/ui/MainActivity.kt
@@ -25,6 +25,7 @@ import io.heckel.ntfy.msg.NotificationService
import io.heckel.ntfy.work.PollWorker
import io.heckel.ntfy.firebase.FirebaseMessenger
import io.heckel.ntfy.msg.BroadcastService
+import io.heckel.ntfy.msg.SubscriberService
import io.heckel.ntfy.util.fadeStatusBarColor
import io.heckel.ntfy.util.formatDateShort
import kotlinx.coroutines.Dispatchers
@@ -115,13 +116,17 @@ class MainActivity : AppCompatActivity(), ActionMode.Callback, AddFragment.Subsc
// Create notification channels right away, so we can configure them immediately after installing the app
notifier!!.createNotificationChannels()
+ // Subscribe to control Firebase channel (so we can re-start the foreground service if it dies)
+ messenger.subscribe("~keepalive")
+
// Background things
- startPeriodicWorker()
+ startPeriodicPollWorker()
+ startPeriodicAutoRestartWorker()
}
- private fun startPeriodicWorker() {
- val pollWorkerVersion = repository.getPollWorkerVersion()
- val workPolicy = if (pollWorkerVersion == PollWorker.VERSION) {
+ private fun startPeriodicPollWorker() {
+ val workerVersion = repository.getPollWorkerVersion()
+ val workPolicy = if (workerVersion == PollWorker.VERSION) {
Log.d(TAG, "Poll worker version matches: choosing KEEP as existing work policy")
ExistingPeriodicWorkPolicy.KEEP
} else {
@@ -132,14 +137,33 @@ class MainActivity : AppCompatActivity(), ActionMode.Callback, AddFragment.Subsc
val constraints = Constraints.Builder()
.setRequiredNetworkType(NetworkType.CONNECTED)
.build()
- val work = PeriodicWorkRequestBuilder(15, TimeUnit.MINUTES)
+ val work = PeriodicWorkRequestBuilder(MINIMUM_PERIODIC_WORKER_INTERVAL, TimeUnit.MINUTES)
.setConstraints(constraints)
.addTag(PollWorker.TAG)
.addTag(PollWorker.WORK_NAME_PERIODIC)
.build()
+ Log.d(TAG, "Poll worker: Scheduling period work every ${MINIMUM_PERIODIC_WORKER_INTERVAL} minutes")
workManager!!.enqueueUniquePeriodicWork(PollWorker.WORK_NAME_PERIODIC, workPolicy, work)
}
+ private fun startPeriodicAutoRestartWorker() {
+ val workerVersion = repository.getAutoRestartWorkerVersion()
+ val workPolicy = if (workerVersion == SubscriberService.AUTO_RESTART_WORKER_VERSION) {
+ Log.d(TAG, "Auto restart worker version matches: choosing KEEP as existing work policy")
+ ExistingPeriodicWorkPolicy.KEEP
+ } else {
+ Log.d(TAG, "Auto restart worker version DOES NOT MATCH: choosing REPLACE as existing work policy")
+ repository.setAutoRestartWorkerVersion(SubscriberService.AUTO_RESTART_WORKER_VERSION)
+ ExistingPeriodicWorkPolicy.REPLACE
+ }
+ val work = PeriodicWorkRequestBuilder(MINIMUM_PERIODIC_WORKER_INTERVAL, TimeUnit.MINUTES)
+ .addTag(SubscriberService.TAG)
+ .addTag(SubscriberService.AUTO_RESTART_WORKER_WORK_NAME_PERIODIC)
+ .build()
+ Log.d(TAG, "Auto restart worker: Scheduling period work every ${MINIMUM_PERIODIC_WORKER_INTERVAL} minutes")
+ workManager!!.enqueueUniquePeriodicWork(SubscriberService.AUTO_RESTART_WORKER_WORK_NAME_PERIODIC, workPolicy, work)
+ }
+
override fun onCreateOptionsMenu(menu: Menu): Boolean {
menuInflater.inflate(R.menu.menu_main_action_bar, menu)
this.menu = menu
@@ -483,5 +507,10 @@ class MainActivity : AppCompatActivity(), ActionMode.Callback, AddFragment.Subsc
const val EXTRA_SUBSCRIPTION_INSTANT = "subscriptionInstant"
const val EXTRA_SUBSCRIPTION_MUTED_UNTIL = "subscriptionMutedUntil"
const val ANIMATION_DURATION = 80L
+
+ // As per Documentation: The minimum repeat interval that can be defined is 15 minutes
+ // (same as the JobScheduler API), but in practice 15 doesn't work. Using 16 here.
+ // Thanks to varunon9 (https://gist.github.com/varunon9/f2beec0a743c96708eb0ef971a9ff9cd) for this!
+ const val MINIMUM_PERIODIC_WORKER_INTERVAL = 16L
}
}
diff --git a/app/src/play/java/io/heckel/ntfy/firebase/FirebaseMessenger.kt b/app/src/play/java/io/heckel/ntfy/firebase/FirebaseMessenger.kt
index 18b92f6..872f1a3 100644
--- a/app/src/play/java/io/heckel/ntfy/firebase/FirebaseMessenger.kt
+++ b/app/src/play/java/io/heckel/ntfy/firebase/FirebaseMessenger.kt
@@ -9,10 +9,10 @@ class FirebaseMessenger {
.getInstance()
.subscribeToTopic(topic)
.addOnCompleteListener {
- Log.d(TAG, "Subscribing to topic complete: result=${it.result}, exception=${it.exception}, successful=${it.isSuccessful}")
+ Log.d(TAG, "Subscribing to topic $topic complete: result=${it.result}, exception=${it.exception}, successful=${it.isSuccessful}")
}
.addOnFailureListener {
- Log.e(TAG, "Subscribing to topic failed: $it")
+ Log.e(TAG, "Subscribing to topic $topic failed: $it")
}
}
diff --git a/app/src/play/java/io/heckel/ntfy/firebase/FirebaseService.kt b/app/src/play/java/io/heckel/ntfy/firebase/FirebaseService.kt
index 2dd4590..56c6235 100644
--- a/app/src/play/java/io/heckel/ntfy/firebase/FirebaseService.kt
+++ b/app/src/play/java/io/heckel/ntfy/firebase/FirebaseService.kt
@@ -1,13 +1,16 @@
package io.heckel.ntfy.firebase
+import android.content.Intent
import android.util.Log
import com.google.firebase.messaging.FirebaseMessagingService
import com.google.firebase.messaging.RemoteMessage
import io.heckel.ntfy.R
import io.heckel.ntfy.app.Application
import io.heckel.ntfy.data.Notification
+import io.heckel.ntfy.msg.ApiService
import io.heckel.ntfy.msg.BroadcastService
import io.heckel.ntfy.msg.NotificationService
+import io.heckel.ntfy.msg.SubscriberService
import io.heckel.ntfy.util.toPriority
import kotlinx.coroutines.CoroutineScope
import kotlinx.coroutines.SupervisorJob
@@ -23,11 +26,25 @@ class FirebaseService : FirebaseMessagingService() {
override fun onMessageReceived(remoteMessage: RemoteMessage) {
// We only process data messages
if (remoteMessage.data.isEmpty()) {
- Log.d(TAG, "Discarding unexpected message: from=${remoteMessage.from}")
+ Log.d(TAG, "Discarding unexpected message (1): from=${remoteMessage.from}")
return
}
- // Check if valid data, and send notification
+ // Dispatch event
+ val data = remoteMessage.data
+ when (data["event"]) {
+ ApiService.EVENT_KEEPALIVE -> handleKeepalive()
+ ApiService.EVENT_MESSAGE -> handleMessage(remoteMessage)
+ else -> Log.d(TAG, "Discarding unexpected message (2): from=${remoteMessage.from}, data=${data}")
+ }
+ }
+
+ private fun handleKeepalive() {
+ Log.d(TAG, "Keepalive received, sending auto restart broadcast for foregrounds service")
+ sendBroadcast(Intent(this, SubscriberService.AutoRestartReceiver::class.java)) // Restart it if necessary!
+ }
+
+ private fun handleMessage(remoteMessage: RemoteMessage) {
val data = remoteMessage.data
val id = data["id"]
val timestamp = data["time"]?.toLongOrNull()