Refactor subscriber manager (service starter)

This commit is contained in:
Philipp Heckel 2021-12-30 17:00:27 +01:00
parent 4efdce54ef
commit 1cca29df56
10 changed files with 89 additions and 108 deletions

View file

@ -44,17 +44,17 @@
</activity>
<!-- Subscriber foreground service for hosts other than ntfy.sh -->
<service android:name=".msg.SubscriberService"/>
<service android:name=".service.SubscriberService"/>
<!-- Subscriber service restart on reboot -->
<receiver android:name=".msg.SubscriberService$BootStartReceiver" android:enabled="true">
<receiver android:name=".service.SubscriberService$BootStartReceiver" android:enabled="true">
<intent-filter>
<action android:name="android.intent.action.BOOT_COMPLETED"/>
</intent-filter>
</receiver>
<!-- Subscriber service restart on destruction -->
<receiver android:name=".msg.SubscriberService$AutoRestartReceiver" android:enabled="true"
<receiver android:name=".service.SubscriberService$AutoRestartReceiver" android:enabled="true"
android:exported="false"/>
<!-- Broadcast receiver to send messages via intents -->

View file

@ -35,7 +35,7 @@ class NotificationDispatcher(val context: Context, val repository: Repository) {
}
private fun checkNotify(subscription: Subscription, notification: Notification, muted: Boolean): Boolean {
if (subscription.upAppId != "") {
if (subscription.upAppId != null) {
return false
}
val detailsVisible = repository.detailViewSubscriptionId.get() == notification.subscriptionId

View file

@ -1,9 +1,10 @@
package io.heckel.ntfy.msg
package io.heckel.ntfy.service
import android.util.Log
import io.heckel.ntfy.data.ConnectionState
import io.heckel.ntfy.data.Notification
import io.heckel.ntfy.data.Subscription
import io.heckel.ntfy.msg.ApiService
import io.heckel.ntfy.util.topicUrl
import kotlinx.coroutines.*
import okhttp3.Call

View file

@ -1,4 +1,4 @@
package io.heckel.ntfy.msg
package io.heckel.ntfy.service
import android.app.*
import android.content.BroadcastReceiver
@ -11,8 +11,6 @@ import android.os.SystemClock
import android.util.Log
import androidx.core.app.NotificationCompat
import androidx.core.content.ContextCompat
import androidx.work.OneTimeWorkRequest
import androidx.work.WorkManager
import androidx.work.Worker
import androidx.work.WorkerParameters
import io.heckel.ntfy.BuildConfig
@ -20,6 +18,8 @@ import io.heckel.ntfy.R
import io.heckel.ntfy.app.Application
import io.heckel.ntfy.data.ConnectionState
import io.heckel.ntfy.data.Subscription
import io.heckel.ntfy.msg.ApiService
import io.heckel.ntfy.msg.NotificationDispatcher
import io.heckel.ntfy.ui.MainActivity
import io.heckel.ntfy.util.topicUrl
import kotlinx.coroutines.*
@ -70,8 +70,8 @@ class SubscriberService : Service() {
val action = intent.action
Log.d(TAG, "using an intent with action $action")
when (action) {
Actions.START.name -> startService()
Actions.STOP.name -> stopService()
Action.START.name -> startService()
Action.STOP.name -> stopService()
else -> Log.e(TAG, "This should never happen. No action in the received intent")
}
} else {
@ -259,13 +259,7 @@ class SubscriberService : Service() {
class BootStartReceiver : BroadcastReceiver() {
override fun onReceive(context: Context, intent: Intent) {
Log.d(TAG, "BootStartReceiver: onReceive called")
if (intent.action == Intent.ACTION_BOOT_COMPLETED && readServiceState(context) == ServiceState.STARTED) {
Intent(context, SubscriberService::class.java).also {
it.action = Actions.START.name
Log.d(TAG, "BootStartReceiver: Starting subscriber service")
ContextCompat.startForegroundService(context, it)
}
}
SubscriberServiceManager.refresh(context)
}
}
@ -276,27 +270,11 @@ class SubscriberService : Service() {
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)
SubscriberServiceManager.refresh(context)
}
}
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 {
enum class Action {
START,
STOP
}

View file

@ -0,0 +1,54 @@
package io.heckel.ntfy.service
import android.content.Context
import android.content.Intent
import android.util.Log
import androidx.core.content.ContextCompat
import androidx.work.*
import io.heckel.ntfy.app.Application
import io.heckel.ntfy.up.BroadcastReceiver
/**
* This class only manages the SubscriberService, i.e. it starts or stops it.
* It's used in multiple activities.
*/
class SubscriberServiceManager(private val context: Context) {
fun refresh() {
Log.d(TAG, "Enqueuing work to refresh subscriber service")
val workManager = WorkManager.getInstance(context)
val startServiceRequest = OneTimeWorkRequest.Builder(RefreshWorker::class.java).build()
workManager.enqueue(startServiceRequest)
}
class RefreshWorker(private val context: Context, params: WorkerParameters) : Worker(context, params) {
override fun doWork(): Result {
if (context.applicationContext !is Application) {
Log.d(TAG, "RefreshWorker: Failed, no application found (work ID: ${this.id})")
return Result.failure()
}
val app = context.applicationContext as Application
val subscriptionIdsWithInstantStatus = app.repository.getSubscriptionIdsWithInstantStatus()
val instantSubscriptions = subscriptionIdsWithInstantStatus.toList().filter { (_, instant) -> instant }.size
val action = if (instantSubscriptions > 0) SubscriberService.Action.START else SubscriberService.Action.STOP
val serviceState = SubscriberService.readServiceState(context)
if (serviceState == SubscriberService.ServiceState.STOPPED && action == SubscriberService.Action.STOP) {
return Result.success()
}
Log.d(TAG, "RefreshWorker: Starting foreground service with action $action (work ID: ${this.id})")
Intent(context, SubscriberService::class.java).also {
it.action = action.name
ContextCompat.startForegroundService(context, it)
}
return Result.success()
}
}
companion object {
const val TAG = "NtfySubscriberMgr"
fun refresh(context: Context) {
val manager = SubscriberServiceManager(context)
manager.refresh()
}
}
}

View file

@ -4,9 +4,6 @@ import android.app.AlertDialog
import android.content.ClipData
import android.content.ClipboardManager
import android.content.Context
import android.content.Intent
import android.content.Intent.ACTION_VIEW
import android.net.Uri
import android.os.Bundle
import android.text.Html
import android.util.Log
@ -26,12 +23,12 @@ import io.heckel.ntfy.BuildConfig
import io.heckel.ntfy.R
import io.heckel.ntfy.app.Application
import io.heckel.ntfy.data.Notification
import io.heckel.ntfy.data.Subscription
import io.heckel.ntfy.util.topicShortUrl
import io.heckel.ntfy.util.topicUrl
import io.heckel.ntfy.firebase.FirebaseMessenger
import io.heckel.ntfy.msg.ApiService
import io.heckel.ntfy.msg.NotificationService
import io.heckel.ntfy.service.SubscriberServiceManager
import io.heckel.ntfy.util.fadeStatusBarColor
import io.heckel.ntfy.util.formatDateShort
import kotlinx.coroutines.*
@ -45,7 +42,7 @@ class DetailActivity : AppCompatActivity(), ActionMode.Callback, NotificationFra
private val repository by lazy { (application as Application).repository }
private val api = ApiService()
private val messenger = FirebaseMessenger()
private var subscriberManager: SubscriberManager? = null // Context-dependent
private var serviceManager: SubscriberServiceManager? = null // Context-dependent
private var notifier: NotificationService? = null // Context-dependent
private var appBaseUrl: String? = null // Context-dependent
@ -72,7 +69,7 @@ class DetailActivity : AppCompatActivity(), ActionMode.Callback, NotificationFra
Log.d(MainActivity.TAG, "Create $this")
// Dependencies that depend on Context
subscriberManager = SubscriberManager(this)
serviceManager = SubscriberServiceManager(this)
notifier = NotificationService(this)
appBaseUrl = getString(R.string.app_base_url)
@ -149,7 +146,7 @@ class DetailActivity : AppCompatActivity(), ActionMode.Callback, NotificationFra
// React to changes in fast delivery setting
repository.getSubscriptionIdsWithInstantStatusLiveData().observe(this) {
subscriberManager?.refreshService(it)
serviceManager?.refresh()
}
// Mark this subscription as "open" so we don't receive notifications for it

View file

@ -23,6 +23,8 @@ import io.heckel.ntfy.util.topicShortUrl
import io.heckel.ntfy.work.PollWorker
import io.heckel.ntfy.firebase.FirebaseMessenger
import io.heckel.ntfy.msg.*
import io.heckel.ntfy.service.SubscriberService
import io.heckel.ntfy.service.SubscriberServiceManager
import io.heckel.ntfy.util.fadeStatusBarColor
import io.heckel.ntfy.util.formatDateShort
import kotlinx.coroutines.Dispatchers
@ -52,7 +54,7 @@ class MainActivity : AppCompatActivity(), ActionMode.Callback, AddFragment.Subsc
private var actionMode: ActionMode? = null
private var workManager: WorkManager? = null // Context-dependent
private var dispatcher: NotificationDispatcher? = null // Context-dependent
private var subscriberManager: SubscriberManager? = null // Context-dependent
private var serviceManager: SubscriberServiceManager? = null // Context-dependent
private var appBaseUrl: String? = null // Context-dependent
override fun onCreate(savedInstanceState: Bundle?) {
@ -64,7 +66,7 @@ class MainActivity : AppCompatActivity(), ActionMode.Callback, AddFragment.Subsc
// Dependencies that depend on Context
workManager = WorkManager.getInstance(this)
dispatcher = NotificationDispatcher(this, repository)
subscriberManager = SubscriberManager(this)
serviceManager = SubscriberServiceManager(this)
appBaseUrl = getString(R.string.app_base_url)
// Action bar
@ -105,7 +107,7 @@ class MainActivity : AppCompatActivity(), ActionMode.Callback, AddFragment.Subsc
// React to changes in instant delivery setting
viewModel.listIdsWithInstantStatus().observe(this) {
subscriberManager?.refreshService(it)
serviceManager?.refresh()
}
// Create notification channels right away, so we can configure them immediately after installing the app
@ -116,7 +118,7 @@ class MainActivity : AppCompatActivity(), ActionMode.Callback, AddFragment.Subsc
// Background things
startPeriodicPollWorker()
startPeriodicAutoRestartWorker()
startPeriodicServiceRefreshWorker()
}
private fun startPeriodicPollWorker() {
@ -141,7 +143,7 @@ class MainActivity : AppCompatActivity(), ActionMode.Callback, AddFragment.Subsc
workManager!!.enqueueUniquePeriodicWork(PollWorker.WORK_NAME_PERIODIC, workPolicy, work)
}
private fun startPeriodicAutoRestartWorker() {
private fun startPeriodicServiceRefreshWorker() {
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")
@ -151,12 +153,12 @@ class MainActivity : AppCompatActivity(), ActionMode.Callback, AddFragment.Subsc
repository.setAutoRestartWorkerVersion(SubscriberService.AUTO_RESTART_WORKER_VERSION)
ExistingPeriodicWorkPolicy.REPLACE
}
val work = PeriodicWorkRequestBuilder<SubscriberService.AutoRestartWorker>(MINIMUM_PERIODIC_WORKER_INTERVAL, TimeUnit.MINUTES)
val work = PeriodicWorkRequestBuilder<SubscriberServiceManager.RefreshWorker>(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)
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 {
@ -323,7 +325,7 @@ class MainActivity : AppCompatActivity(), ActionMode.Callback, AddFragment.Subsc
private fun displayUnifiedPushToast(subscription: Subscription) {
runOnUiThread {
val appId = subscription.upAppId ?: ""
val appId = subscription.upAppId ?: return@runOnUiThread
val toastMessage = getString(R.string.main_unified_push_toast, appId)
Toast.makeText(this@MainActivity, toastMessage, Toast.LENGTH_LONG).show()
}

View file

@ -1,46 +0,0 @@
package io.heckel.ntfy.ui
import android.content.Context
import android.content.Intent
import android.os.Build
import android.util.Log
import androidx.activity.ComponentActivity
import androidx.lifecycle.lifecycleScope
import io.heckel.ntfy.msg.SubscriberService
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.GlobalScope
import kotlinx.coroutines.launch
/**
* This class only manages the SubscriberService, i.e. it starts or stops it.
* It's used in multiple activities.
*/
class SubscriberManager(private val context: Context) {
fun refreshService(subscriptionIdsWithInstantStatus: Set<Pair<Long, Boolean>>) { // Set<SubscriptionId -> IsInstant>
Log.d(MainActivity.TAG, "Triggering subscriber service refresh")
GlobalScope.launch(Dispatchers.IO) {
val instantSubscriptions = subscriptionIdsWithInstantStatus.toList().filter { (_, instant) -> instant }.size
if (instantSubscriptions == 0) {
performActionOnSubscriberService(SubscriberService.Actions.STOP)
} else {
performActionOnSubscriberService(SubscriberService.Actions.START)
}
}
}
private fun performActionOnSubscriberService(action: SubscriberService.Actions) {
val serviceState = SubscriberService.readServiceState(context)
if (serviceState == SubscriberService.ServiceState.STOPPED && action == SubscriberService.Actions.STOP) {
return
}
val intent = Intent(context, SubscriberService::class.java)
intent.action = action.name
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O) {
Log.d(MainActivity.TAG, "Performing SubscriberService action: ${action.name} (as foreground service, API >= 26)")
context.startForegroundService(intent)
} else {
Log.d(MainActivity.TAG, "Performing SubscriberService action: ${action.name} (as background service, API >= 26)")
context.startService(intent)
}
}
}

View file

@ -7,7 +7,7 @@ import io.heckel.ntfy.R
import io.heckel.ntfy.app.Application
import io.heckel.ntfy.data.Repository
import io.heckel.ntfy.data.Subscription
import io.heckel.ntfy.ui.SubscriberManager
import io.heckel.ntfy.service.SubscriberServiceManager
import io.heckel.ntfy.util.randomString
import io.heckel.ntfy.util.topicUrlUp
import kotlinx.coroutines.Dispatchers
@ -52,6 +52,8 @@ class BroadcastReceiver : android.content.BroadcastReceiver() {
}
return@launch
}
// Add subscription
val baseUrl = context.getString(R.string.app_base_url) // FIXME
val topic = UP_PREFIX + randomString(TOPIC_LENGTH)
val endpoint = topicUrlUp(baseUrl, topic)
@ -68,13 +70,12 @@ class BroadcastReceiver : android.content.BroadcastReceiver() {
lastActive = Date().time/1000
)
// Add subscription
Log.d(TAG, "Adding subscription with for app $appId (connectorToken $connectorToken): $subscription")
repository.addSubscription(subscription)
distributor.sendEndpoint(appId, connectorToken, endpoint)
// Refresh (and maybe start) foreground service
refreshSubscriberService(app, repository)
SubscriberServiceManager.refresh(app)
}
}
@ -97,17 +98,10 @@ class BroadcastReceiver : android.content.BroadcastReceiver() {
existingSubscription.upAppId?.let { appId -> distributor.sendUnregistered(appId, connectorToken) }
// Refresh (and maybe stop) foreground service
refreshSubscriberService(app, repository)
SubscriberServiceManager.refresh(context)
}
}
private fun refreshSubscriberService(context: Context, repository: Repository) {
Log.d(TAG, "Refreshing subscriber service")
val subscriptionIdsWithInstantStatus = repository.getSubscriptionIdsWithInstantStatus()
val subscriberManager = SubscriberManager(context)
subscriberManager.refreshService(subscriptionIdsWithInstantStatus)
}
companion object {
private const val TAG = "NtfyUpBroadcastRecv"
private const val UP_PREFIX = "up"

View file

@ -8,6 +8,7 @@ import io.heckel.ntfy.R
import io.heckel.ntfy.app.Application
import io.heckel.ntfy.data.Notification
import io.heckel.ntfy.msg.*
import io.heckel.ntfy.service.SubscriberService
import io.heckel.ntfy.util.toPriority
import kotlinx.coroutines.CoroutineScope
import kotlinx.coroutines.SupervisorJob