From 1cca29df564ef57cbd29aa83f27ae1cb334c250b Mon Sep 17 00:00:00 2001 From: Philipp Heckel <pheckel@datto.com> Date: Thu, 30 Dec 2021 17:00:27 +0100 Subject: [PATCH] Refactor subscriber manager (service starter) --- app/src/main/AndroidManifest.xml | 6 +-- .../heckel/ntfy/msg/NotificationDispatcher.kt | 2 +- .../{msg => service}/SubscriberConnection.kt | 3 +- .../{msg => service}/SubscriberService.kt | 38 +++---------- .../ntfy/service/SubscriberServiceManager.kt | 54 +++++++++++++++++++ .../java/io/heckel/ntfy/ui/DetailActivity.kt | 11 ++-- .../java/io/heckel/ntfy/ui/MainActivity.kt | 20 +++---- .../io/heckel/ntfy/ui/SubscriberManager.kt | 46 ---------------- .../io/heckel/ntfy/up/BroadcastReceiver.kt | 16 ++---- .../heckel/ntfy/firebase/FirebaseService.kt | 1 + 10 files changed, 89 insertions(+), 108 deletions(-) rename app/src/main/java/io/heckel/ntfy/{msg => service}/SubscriberConnection.kt (98%) rename app/src/main/java/io/heckel/ntfy/{msg => service}/SubscriberService.kt (89%) create mode 100644 app/src/main/java/io/heckel/ntfy/service/SubscriberServiceManager.kt delete mode 100644 app/src/main/java/io/heckel/ntfy/ui/SubscriberManager.kt diff --git a/app/src/main/AndroidManifest.xml b/app/src/main/AndroidManifest.xml index 1beaca1..2fbf176 100644 --- a/app/src/main/AndroidManifest.xml +++ b/app/src/main/AndroidManifest.xml @@ -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 --> diff --git a/app/src/main/java/io/heckel/ntfy/msg/NotificationDispatcher.kt b/app/src/main/java/io/heckel/ntfy/msg/NotificationDispatcher.kt index aa251cd..18eb5b1 100644 --- a/app/src/main/java/io/heckel/ntfy/msg/NotificationDispatcher.kt +++ b/app/src/main/java/io/heckel/ntfy/msg/NotificationDispatcher.kt @@ -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 diff --git a/app/src/main/java/io/heckel/ntfy/msg/SubscriberConnection.kt b/app/src/main/java/io/heckel/ntfy/service/SubscriberConnection.kt similarity index 98% rename from app/src/main/java/io/heckel/ntfy/msg/SubscriberConnection.kt rename to app/src/main/java/io/heckel/ntfy/service/SubscriberConnection.kt index e26cd68..e8ae782 100644 --- a/app/src/main/java/io/heckel/ntfy/msg/SubscriberConnection.kt +++ b/app/src/main/java/io/heckel/ntfy/service/SubscriberConnection.kt @@ -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 diff --git a/app/src/main/java/io/heckel/ntfy/msg/SubscriberService.kt b/app/src/main/java/io/heckel/ntfy/service/SubscriberService.kt similarity index 89% rename from app/src/main/java/io/heckel/ntfy/msg/SubscriberService.kt rename to app/src/main/java/io/heckel/ntfy/service/SubscriberService.kt index 90187bd..d000268 100644 --- a/app/src/main/java/io/heckel/ntfy/msg/SubscriberService.kt +++ b/app/src/main/java/io/heckel/ntfy/service/SubscriberService.kt @@ -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 } diff --git a/app/src/main/java/io/heckel/ntfy/service/SubscriberServiceManager.kt b/app/src/main/java/io/heckel/ntfy/service/SubscriberServiceManager.kt new file mode 100644 index 0000000..e3d6174 --- /dev/null +++ b/app/src/main/java/io/heckel/ntfy/service/SubscriberServiceManager.kt @@ -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() + } + } +} diff --git a/app/src/main/java/io/heckel/ntfy/ui/DetailActivity.kt b/app/src/main/java/io/heckel/ntfy/ui/DetailActivity.kt index 9757c1f..4477a1b 100644 --- a/app/src/main/java/io/heckel/ntfy/ui/DetailActivity.kt +++ b/app/src/main/java/io/heckel/ntfy/ui/DetailActivity.kt @@ -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 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 01222fa..9829aa4 100644 --- a/app/src/main/java/io/heckel/ntfy/ui/MainActivity.kt +++ b/app/src/main/java/io/heckel/ntfy/ui/MainActivity.kt @@ -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() } diff --git a/app/src/main/java/io/heckel/ntfy/ui/SubscriberManager.kt b/app/src/main/java/io/heckel/ntfy/ui/SubscriberManager.kt deleted file mode 100644 index 141096e..0000000 --- a/app/src/main/java/io/heckel/ntfy/ui/SubscriberManager.kt +++ /dev/null @@ -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) - } - } -} diff --git a/app/src/main/java/io/heckel/ntfy/up/BroadcastReceiver.kt b/app/src/main/java/io/heckel/ntfy/up/BroadcastReceiver.kt index 74fba12..345ca31 100644 --- a/app/src/main/java/io/heckel/ntfy/up/BroadcastReceiver.kt +++ b/app/src/main/java/io/heckel/ntfy/up/BroadcastReceiver.kt @@ -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" 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 1df310c..a06ea2c 100644 --- a/app/src/play/java/io/heckel/ntfy/firebase/FirebaseService.kt +++ b/app/src/play/java/io/heckel/ntfy/firebase/FirebaseService.kt @@ -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