1
0
Fork 0

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> </activity>
<!-- Subscriber foreground service for hosts other than ntfy.sh --> <!-- Subscriber foreground service for hosts other than ntfy.sh -->
<service android:name=".msg.SubscriberService"/> <service android:name=".service.SubscriberService"/>
<!-- Subscriber service restart on reboot --> <!-- 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> <intent-filter>
<action android:name="android.intent.action.BOOT_COMPLETED"/> <action android:name="android.intent.action.BOOT_COMPLETED"/>
</intent-filter> </intent-filter>
</receiver> </receiver>
<!-- Subscriber service restart on destruction --> <!-- 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"/> android:exported="false"/>
<!-- Broadcast receiver to send messages via intents --> <!-- 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 { private fun checkNotify(subscription: Subscription, notification: Notification, muted: Boolean): Boolean {
if (subscription.upAppId != "") { if (subscription.upAppId != null) {
return false return false
} }
val detailsVisible = repository.detailViewSubscriptionId.get() == notification.subscriptionId 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 android.util.Log
import io.heckel.ntfy.data.ConnectionState import io.heckel.ntfy.data.ConnectionState
import io.heckel.ntfy.data.Notification import io.heckel.ntfy.data.Notification
import io.heckel.ntfy.data.Subscription import io.heckel.ntfy.data.Subscription
import io.heckel.ntfy.msg.ApiService
import io.heckel.ntfy.util.topicUrl import io.heckel.ntfy.util.topicUrl
import kotlinx.coroutines.* import kotlinx.coroutines.*
import okhttp3.Call import okhttp3.Call

View file

@ -1,4 +1,4 @@
package io.heckel.ntfy.msg package io.heckel.ntfy.service
import android.app.* import android.app.*
import android.content.BroadcastReceiver import android.content.BroadcastReceiver
@ -11,8 +11,6 @@ import android.os.SystemClock
import android.util.Log import android.util.Log
import androidx.core.app.NotificationCompat import androidx.core.app.NotificationCompat
import androidx.core.content.ContextCompat import androidx.core.content.ContextCompat
import androidx.work.OneTimeWorkRequest
import androidx.work.WorkManager
import androidx.work.Worker import androidx.work.Worker
import androidx.work.WorkerParameters import androidx.work.WorkerParameters
import io.heckel.ntfy.BuildConfig import io.heckel.ntfy.BuildConfig
@ -20,6 +18,8 @@ import io.heckel.ntfy.R
import io.heckel.ntfy.app.Application import io.heckel.ntfy.app.Application
import io.heckel.ntfy.data.ConnectionState import io.heckel.ntfy.data.ConnectionState
import io.heckel.ntfy.data.Subscription 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.ui.MainActivity
import io.heckel.ntfy.util.topicUrl import io.heckel.ntfy.util.topicUrl
import kotlinx.coroutines.* import kotlinx.coroutines.*
@ -70,8 +70,8 @@ class SubscriberService : Service() {
val action = intent.action val action = intent.action
Log.d(TAG, "using an intent with action $action") Log.d(TAG, "using an intent with action $action")
when (action) { when (action) {
Actions.START.name -> startService() Action.START.name -> startService()
Actions.STOP.name -> stopService() Action.STOP.name -> stopService()
else -> Log.e(TAG, "This should never happen. No action in the received intent") else -> Log.e(TAG, "This should never happen. No action in the received intent")
} }
} else { } else {
@ -259,13 +259,7 @@ class SubscriberService : Service() {
class BootStartReceiver : BroadcastReceiver() { class BootStartReceiver : BroadcastReceiver() {
override fun onReceive(context: Context, intent: Intent) { override fun onReceive(context: Context, intent: Intent) {
Log.d(TAG, "BootStartReceiver: onReceive called") Log.d(TAG, "BootStartReceiver: onReceive called")
if (intent.action == Intent.ACTION_BOOT_COMPLETED && readServiceState(context) == ServiceState.STARTED) { SubscriberServiceManager.refresh(context)
Intent(context, SubscriberService::class.java).also {
it.action = Actions.START.name
Log.d(TAG, "BootStartReceiver: Starting subscriber service")
ContextCompat.startForegroundService(context, it)
}
}
} }
} }
@ -276,27 +270,11 @@ class SubscriberService : Service() {
class AutoRestartReceiver : BroadcastReceiver() { class AutoRestartReceiver : BroadcastReceiver() {
override fun onReceive(context: Context, intent: Intent) { override fun onReceive(context: Context, intent: Intent) {
Log.d(TAG, "AutoRestartReceiver: onReceive called") Log.d(TAG, "AutoRestartReceiver: onReceive called")
val workManager = WorkManager.getInstance(context) SubscriberServiceManager.refresh(context)
val startServiceRequest = OneTimeWorkRequest.Builder(AutoRestartWorker::class.java).build()
workManager.enqueue(startServiceRequest)
} }
} }
class AutoRestartWorker(private val context: Context, params: WorkerParameters) : Worker(context, params) { enum class Action {
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, START,
STOP 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.ClipData
import android.content.ClipboardManager import android.content.ClipboardManager
import android.content.Context import android.content.Context
import android.content.Intent
import android.content.Intent.ACTION_VIEW
import android.net.Uri
import android.os.Bundle import android.os.Bundle
import android.text.Html import android.text.Html
import android.util.Log import android.util.Log
@ -26,12 +23,12 @@ import io.heckel.ntfy.BuildConfig
import io.heckel.ntfy.R import io.heckel.ntfy.R
import io.heckel.ntfy.app.Application import io.heckel.ntfy.app.Application
import io.heckel.ntfy.data.Notification import io.heckel.ntfy.data.Notification
import io.heckel.ntfy.data.Subscription
import io.heckel.ntfy.util.topicShortUrl import io.heckel.ntfy.util.topicShortUrl
import io.heckel.ntfy.util.topicUrl import io.heckel.ntfy.util.topicUrl
import io.heckel.ntfy.firebase.FirebaseMessenger import io.heckel.ntfy.firebase.FirebaseMessenger
import io.heckel.ntfy.msg.ApiService import io.heckel.ntfy.msg.ApiService
import io.heckel.ntfy.msg.NotificationService import io.heckel.ntfy.msg.NotificationService
import io.heckel.ntfy.service.SubscriberServiceManager
import io.heckel.ntfy.util.fadeStatusBarColor import io.heckel.ntfy.util.fadeStatusBarColor
import io.heckel.ntfy.util.formatDateShort import io.heckel.ntfy.util.formatDateShort
import kotlinx.coroutines.* import kotlinx.coroutines.*
@ -45,7 +42,7 @@ class DetailActivity : AppCompatActivity(), ActionMode.Callback, NotificationFra
private val repository by lazy { (application as Application).repository } private val repository by lazy { (application as Application).repository }
private val api = ApiService() private val api = ApiService()
private val messenger = FirebaseMessenger() 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 notifier: NotificationService? = null // Context-dependent
private var appBaseUrl: String? = 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") Log.d(MainActivity.TAG, "Create $this")
// Dependencies that depend on Context // Dependencies that depend on Context
subscriberManager = SubscriberManager(this) serviceManager = SubscriberServiceManager(this)
notifier = NotificationService(this) notifier = NotificationService(this)
appBaseUrl = getString(R.string.app_base_url) appBaseUrl = getString(R.string.app_base_url)
@ -149,7 +146,7 @@ class DetailActivity : AppCompatActivity(), ActionMode.Callback, NotificationFra
// React to changes in fast delivery setting // React to changes in fast delivery setting
repository.getSubscriptionIdsWithInstantStatusLiveData().observe(this) { repository.getSubscriptionIdsWithInstantStatusLiveData().observe(this) {
subscriberManager?.refreshService(it) serviceManager?.refresh()
} }
// Mark this subscription as "open" so we don't receive notifications for it // 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.work.PollWorker
import io.heckel.ntfy.firebase.FirebaseMessenger import io.heckel.ntfy.firebase.FirebaseMessenger
import io.heckel.ntfy.msg.* 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.fadeStatusBarColor
import io.heckel.ntfy.util.formatDateShort import io.heckel.ntfy.util.formatDateShort
import kotlinx.coroutines.Dispatchers import kotlinx.coroutines.Dispatchers
@ -52,7 +54,7 @@ class MainActivity : AppCompatActivity(), ActionMode.Callback, AddFragment.Subsc
private var actionMode: ActionMode? = null private var actionMode: ActionMode? = null
private var workManager: WorkManager? = null // Context-dependent private var workManager: WorkManager? = null // Context-dependent
private var dispatcher: NotificationDispatcher? = 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 private var appBaseUrl: String? = null // Context-dependent
override fun onCreate(savedInstanceState: Bundle?) { override fun onCreate(savedInstanceState: Bundle?) {
@ -64,7 +66,7 @@ class MainActivity : AppCompatActivity(), ActionMode.Callback, AddFragment.Subsc
// Dependencies that depend on Context // Dependencies that depend on Context
workManager = WorkManager.getInstance(this) workManager = WorkManager.getInstance(this)
dispatcher = NotificationDispatcher(this, repository) dispatcher = NotificationDispatcher(this, repository)
subscriberManager = SubscriberManager(this) serviceManager = SubscriberServiceManager(this)
appBaseUrl = getString(R.string.app_base_url) appBaseUrl = getString(R.string.app_base_url)
// Action bar // Action bar
@ -105,7 +107,7 @@ class MainActivity : AppCompatActivity(), ActionMode.Callback, AddFragment.Subsc
// React to changes in instant delivery setting // React to changes in instant delivery setting
viewModel.listIdsWithInstantStatus().observe(this) { viewModel.listIdsWithInstantStatus().observe(this) {
subscriberManager?.refreshService(it) serviceManager?.refresh()
} }
// Create notification channels right away, so we can configure them immediately after installing the app // 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 // Background things
startPeriodicPollWorker() startPeriodicPollWorker()
startPeriodicAutoRestartWorker() startPeriodicServiceRefreshWorker()
} }
private fun startPeriodicPollWorker() { private fun startPeriodicPollWorker() {
@ -141,7 +143,7 @@ class MainActivity : AppCompatActivity(), ActionMode.Callback, AddFragment.Subsc
workManager!!.enqueueUniquePeriodicWork(PollWorker.WORK_NAME_PERIODIC, workPolicy, work) workManager!!.enqueueUniquePeriodicWork(PollWorker.WORK_NAME_PERIODIC, workPolicy, work)
} }
private fun startPeriodicAutoRestartWorker() { private fun startPeriodicServiceRefreshWorker() {
val workerVersion = repository.getAutoRestartWorkerVersion() val workerVersion = repository.getAutoRestartWorkerVersion()
val workPolicy = if (workerVersion == SubscriberService.AUTO_RESTART_WORKER_VERSION) { val workPolicy = if (workerVersion == SubscriberService.AUTO_RESTART_WORKER_VERSION) {
Log.d(TAG, "Auto restart worker version matches: choosing KEEP as existing work policy") 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) repository.setAutoRestartWorkerVersion(SubscriberService.AUTO_RESTART_WORKER_VERSION)
ExistingPeriodicWorkPolicy.REPLACE 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.TAG)
.addTag(SubscriberService.AUTO_RESTART_WORKER_WORK_NAME_PERIODIC) .addTag(SubscriberService.AUTO_RESTART_WORKER_WORK_NAME_PERIODIC)
.build() .build()
Log.d(TAG, "Auto restart worker: Scheduling period work every ${MINIMUM_PERIODIC_WORKER_INTERVAL} minutes") 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) workManager?.enqueueUniquePeriodicWork(SubscriberService.AUTO_RESTART_WORKER_WORK_NAME_PERIODIC, workPolicy, work)
} }
override fun onCreateOptionsMenu(menu: Menu): Boolean { override fun onCreateOptionsMenu(menu: Menu): Boolean {
@ -323,7 +325,7 @@ class MainActivity : AppCompatActivity(), ActionMode.Callback, AddFragment.Subsc
private fun displayUnifiedPushToast(subscription: Subscription) { private fun displayUnifiedPushToast(subscription: Subscription) {
runOnUiThread { runOnUiThread {
val appId = subscription.upAppId ?: "" val appId = subscription.upAppId ?: return@runOnUiThread
val toastMessage = getString(R.string.main_unified_push_toast, appId) val toastMessage = getString(R.string.main_unified_push_toast, appId)
Toast.makeText(this@MainActivity, toastMessage, Toast.LENGTH_LONG).show() 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.app.Application
import io.heckel.ntfy.data.Repository import io.heckel.ntfy.data.Repository
import io.heckel.ntfy.data.Subscription 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.randomString
import io.heckel.ntfy.util.topicUrlUp import io.heckel.ntfy.util.topicUrlUp
import kotlinx.coroutines.Dispatchers import kotlinx.coroutines.Dispatchers
@ -52,6 +52,8 @@ class BroadcastReceiver : android.content.BroadcastReceiver() {
} }
return@launch return@launch
} }
// Add subscription
val baseUrl = context.getString(R.string.app_base_url) // FIXME val baseUrl = context.getString(R.string.app_base_url) // FIXME
val topic = UP_PREFIX + randomString(TOPIC_LENGTH) val topic = UP_PREFIX + randomString(TOPIC_LENGTH)
val endpoint = topicUrlUp(baseUrl, topic) val endpoint = topicUrlUp(baseUrl, topic)
@ -68,13 +70,12 @@ class BroadcastReceiver : android.content.BroadcastReceiver() {
lastActive = Date().time/1000 lastActive = Date().time/1000
) )
// Add subscription
Log.d(TAG, "Adding subscription with for app $appId (connectorToken $connectorToken): $subscription") Log.d(TAG, "Adding subscription with for app $appId (connectorToken $connectorToken): $subscription")
repository.addSubscription(subscription) repository.addSubscription(subscription)
distributor.sendEndpoint(appId, connectorToken, endpoint) distributor.sendEndpoint(appId, connectorToken, endpoint)
// Refresh (and maybe start) foreground service // 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) } existingSubscription.upAppId?.let { appId -> distributor.sendUnregistered(appId, connectorToken) }
// Refresh (and maybe stop) foreground service // 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 { companion object {
private const val TAG = "NtfyUpBroadcastRecv" private const val TAG = "NtfyUpBroadcastRecv"
private const val UP_PREFIX = "up" 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.app.Application
import io.heckel.ntfy.data.Notification import io.heckel.ntfy.data.Notification
import io.heckel.ntfy.msg.* import io.heckel.ntfy.msg.*
import io.heckel.ntfy.service.SubscriberService
import io.heckel.ntfy.util.toPriority import io.heckel.ntfy.util.toPriority
import kotlinx.coroutines.CoroutineScope import kotlinx.coroutines.CoroutineScope
import kotlinx.coroutines.SupervisorJob import kotlinx.coroutines.SupervisorJob