Increase worker intervals; Remove periodic "muted until" checker; redraw main list on "back button" press; polling now uses since=..

This commit is contained in:
Philipp Heckel 2022-01-02 01:37:09 +01:00
parent 91d13bdd13
commit ae1e439f37
8 changed files with 74 additions and 59 deletions

View file

@ -12,8 +12,8 @@ android {
minSdkVersion 21 minSdkVersion 21
targetSdkVersion 30 targetSdkVersion 30
versionCode 13 versionCode 14
versionName "1.5.0" versionName "1.5.1"
testInstrumentationRunner "androidx.test.runner.AndroidJUnitRunner" testInstrumentationRunner "androidx.test.runner.AndroidJUnitRunner"

View file

@ -26,7 +26,7 @@ class ApiService {
.writeTimeout(15, TimeUnit.SECONDS) .writeTimeout(15, TimeUnit.SECONDS)
.build() .build()
private val subscriberClient = OkHttpClient.Builder() private val subscriberClient = OkHttpClient.Builder()
.readTimeout(77, TimeUnit.SECONDS) // Assuming that keepalive messages are more frequent than this .readTimeout(5, TimeUnit.MINUTES) // Assuming that keepalive messages are more frequent than this
.build() .build()
fun publish(baseUrl: String, topic: String, message: String, title: String, priority: Int, tags: List<String>, delay: String) { fun publish(baseUrl: String, topic: String, message: String, title: String, priority: Int, tags: List<String>, delay: String) {
@ -58,7 +58,12 @@ class ApiService {
} }
fun poll(subscriptionId: Long, baseUrl: String, topic: String): List<Notification> { fun poll(subscriptionId: Long, baseUrl: String, topic: String): List<Notification> {
val url = topicUrlJsonPoll(baseUrl, topic) return poll(subscriptionId, baseUrl, topic, 0)
}
fun poll(subscriptionId: Long, baseUrl: String, topic: String, since: Long): List<Notification> {
val sinceVal = if (since == 0L) "all" else since.toString()
val url = topicUrlJsonPoll(baseUrl, topic, sinceVal)
Log.d(TAG, "Polling topic $url") Log.d(TAG, "Polling topic $url")
val request = Request.Builder() val request = Request.Builder()

View file

@ -286,8 +286,8 @@ class SubscriberService : Service() {
companion object { companion object {
const val TAG = "NtfySubscriberService" const val TAG = "NtfySubscriberService"
const val AUTO_RESTART_WORKER_VERSION = BuildConfig.VERSION_CODE const val SERVICE_START_WORKER_VERSION = BuildConfig.VERSION_CODE
const val AUTO_RESTART_WORKER_WORK_NAME_PERIODIC = "NtfyAutoRestartWorkerPeriodic" const val SERVICE_START_WORKER_WORK_NAME_PERIODIC = "NtfyAutoRestartWorkerPeriodic" // Do not change!
private const val WAKE_LOCK_TAG = "SubscriberService:lock" private const val WAKE_LOCK_TAG = "SubscriberService:lock"
private const val NOTIFICATION_CHANNEL_ID = "ntfy-subscriber" private const val NOTIFICATION_CHANNEL_ID = "ntfy-subscriber"

View file

@ -6,7 +6,6 @@ import android.util.Log
import androidx.core.content.ContextCompat import androidx.core.content.ContextCompat
import androidx.work.* import androidx.work.*
import io.heckel.ntfy.app.Application 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. * This class only manages the SubscriberService, i.e. it starts or stops it.
@ -21,7 +20,7 @@ class SubscriberServiceManager(private val context: Context) {
fun refresh() { fun refresh() {
Log.d(TAG, "Enqueuing work to refresh subscriber service") Log.d(TAG, "Enqueuing work to refresh subscriber service")
val workManager = WorkManager.getInstance(context) val workManager = WorkManager.getInstance(context)
val startServiceRequest = OneTimeWorkRequest.Builder(RefreshWorker::class.java).build() val startServiceRequest = OneTimeWorkRequest.Builder(ServiceStartWorker::class.java).build()
workManager.enqueue(startServiceRequest) workManager.enqueue(startServiceRequest)
} }
@ -29,10 +28,10 @@ class SubscriberServiceManager(private val context: Context) {
* Starts or stops the foreground service by figuring out how many instant delivery subscriptions * Starts or stops the foreground service by figuring out how many instant delivery subscriptions
* exist. If there's > 0, then we need a foreground service. * exist. If there's > 0, then we need a foreground service.
*/ */
class RefreshWorker(private val context: Context, params: WorkerParameters) : Worker(context, params) { class ServiceStartWorker(private val context: Context, params: WorkerParameters) : Worker(context, params) {
override fun doWork(): Result { override fun doWork(): Result {
if (context.applicationContext !is Application) { if (context.applicationContext !is Application) {
Log.d(TAG, "RefreshWorker: Failed, no application found (work ID: ${this.id})") Log.d(TAG, "ServiceStartWorker: Failed, no application found (work ID: ${this.id})")
return Result.failure() return Result.failure()
} }
val app = context.applicationContext as Application val app = context.applicationContext as Application
@ -43,7 +42,7 @@ class SubscriberServiceManager(private val context: Context) {
if (serviceState == SubscriberService.ServiceState.STOPPED && action == SubscriberService.Action.STOP) { if (serviceState == SubscriberService.ServiceState.STOPPED && action == SubscriberService.Action.STOP) {
return Result.success() return Result.success()
} }
Log.d(TAG, "RefreshWorker: Starting foreground service with action $action (work ID: ${this.id})") Log.d(TAG, "ServiceStartWorker: Starting foreground service with action $action (work ID: ${this.id})")
Intent(context, SubscriberService::class.java).also { Intent(context, SubscriberService::class.java).also {
it.action = action.name it.action = action.name
ContextCompat.startForegroundService(context, it) ContextCompat.startForegroundService(context, it)

View file

@ -27,10 +27,7 @@ import io.heckel.ntfy.service.SubscriberService
import io.heckel.ntfy.service.SubscriberServiceManager 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.*
import kotlinx.coroutines.delay
import kotlinx.coroutines.isActive
import kotlinx.coroutines.launch
import java.util.* import java.util.*
import java.util.concurrent.TimeUnit import java.util.concurrent.TimeUnit
import kotlin.random.Random import kotlin.random.Random
@ -116,7 +113,13 @@ class MainActivity : AppCompatActivity(), ActionMode.Callback, AddFragment.Subsc
// Background things // Background things
startPeriodicPollWorker() startPeriodicPollWorker()
startPeriodicServiceRefreshWorker() startPeriodicServiceRestartWorker()
}
override fun onResume() {
super.onResume()
showHideNotificationMenuItems()
redrawList()
} }
private fun startPeriodicPollWorker() { private fun startPeriodicPollWorker() {
@ -132,75 +135,74 @@ class MainActivity : AppCompatActivity(), ActionMode.Callback, AddFragment.Subsc
val constraints = Constraints.Builder() val constraints = Constraints.Builder()
.setRequiredNetworkType(NetworkType.CONNECTED) .setRequiredNetworkType(NetworkType.CONNECTED)
.build() .build()
val work = PeriodicWorkRequestBuilder<PollWorker>(MINIMUM_PERIODIC_WORKER_INTERVAL, TimeUnit.MINUTES) val work = PeriodicWorkRequestBuilder<PollWorker>(POLL_WORKER_INTERVAL_MINUTES, TimeUnit.MINUTES)
.setConstraints(constraints) .setConstraints(constraints)
.addTag(PollWorker.TAG) .addTag(PollWorker.TAG)
.addTag(PollWorker.WORK_NAME_PERIODIC) .addTag(PollWorker.WORK_NAME_PERIODIC)
.build() .build()
Log.d(TAG, "Poll worker: Scheduling period work every ${MINIMUM_PERIODIC_WORKER_INTERVAL} minutes") Log.d(TAG, "Poll worker: Scheduling period work every $POLL_WORKER_INTERVAL_MINUTES minutes")
workManager!!.enqueueUniquePeriodicWork(PollWorker.WORK_NAME_PERIODIC, workPolicy, work) workManager!!.enqueueUniquePeriodicWork(PollWorker.WORK_NAME_PERIODIC, workPolicy, work)
} }
private fun startPeriodicServiceRefreshWorker() { private fun startPeriodicServiceRestartWorker() {
val workerVersion = repository.getAutoRestartWorkerVersion() val workerVersion = repository.getAutoRestartWorkerVersion()
val workPolicy = if (workerVersion == SubscriberService.AUTO_RESTART_WORKER_VERSION) { val workPolicy = if (workerVersion == SubscriberService.SERVICE_START_WORKER_VERSION) {
Log.d(TAG, "Auto restart worker version matches: choosing KEEP as existing work policy") Log.d(TAG, "ServiceStartWorker version matches: choosing KEEP as existing work policy")
ExistingPeriodicWorkPolicy.KEEP ExistingPeriodicWorkPolicy.KEEP
} else { } else {
Log.d(TAG, "Auto restart worker version DOES NOT MATCH: choosing REPLACE as existing work policy") Log.d(TAG, "ServiceStartWorker version DOES NOT MATCH: choosing REPLACE as existing work policy")
repository.setAutoRestartWorkerVersion(SubscriberService.AUTO_RESTART_WORKER_VERSION) repository.setAutoRestartWorkerVersion(SubscriberService.SERVICE_START_WORKER_VERSION)
ExistingPeriodicWorkPolicy.REPLACE ExistingPeriodicWorkPolicy.REPLACE
} }
val work = PeriodicWorkRequestBuilder<SubscriberServiceManager.RefreshWorker>(MINIMUM_PERIODIC_WORKER_INTERVAL, TimeUnit.MINUTES) val work = PeriodicWorkRequestBuilder<SubscriberServiceManager.ServiceStartWorker>(SERVICE_START_WORKER_INTERVAL_MINUTES, TimeUnit.MINUTES)
.addTag(SubscriberService.TAG) .addTag(SubscriberService.TAG)
.addTag(SubscriberService.AUTO_RESTART_WORKER_WORK_NAME_PERIODIC) .addTag(SubscriberService.SERVICE_START_WORKER_WORK_NAME_PERIODIC)
.build() .build()
Log.d(TAG, "Auto restart worker: Scheduling period work every $MINIMUM_PERIODIC_WORKER_INTERVAL minutes") Log.d(TAG, "ServiceStartWorker: Scheduling period work every $SERVICE_START_WORKER_INTERVAL_MINUTES minutes")
workManager?.enqueueUniquePeriodicWork(SubscriberService.AUTO_RESTART_WORKER_WORK_NAME_PERIODIC, workPolicy, work) workManager?.enqueueUniquePeriodicWork(SubscriberService.SERVICE_START_WORKER_WORK_NAME_PERIODIC, workPolicy, work)
} }
override fun onCreateOptionsMenu(menu: Menu): Boolean { override fun onCreateOptionsMenu(menu: Menu): Boolean {
menuInflater.inflate(R.menu.menu_main_action_bar, menu) menuInflater.inflate(R.menu.menu_main_action_bar, menu)
this.menu = menu this.menu = menu
showHideNotificationMenuItems() showHideNotificationMenuItems()
startNotificationMutedChecker() // This is done here, because then we know that we've initialized the menu checkSubscriptionsMuted() // This is done here, because then we know that we've initialized the menu
return true return true
} }
private fun startNotificationMutedChecker() { private fun checkSubscriptionsMuted(delayMillis: Long = 0L) {
lifecycleScope.launch(Dispatchers.IO) { lifecycleScope.launch(Dispatchers.IO) {
delay(5000) // Just to be sure we've initialized all the things, we wait a bit ... delay(delayMillis) // Just to be sure we've initialized all the things, we wait a bit ...
while (isActive) { Log.d(TAG, "Checking global and subscription-specific 'muted until' timestamp")
Log.d(DetailActivity.TAG, "Checking global and subscription-specific 'muted until' timestamp")
// Check global // Check global
val changed = repository.checkGlobalMutedUntil() val changed = repository.checkGlobalMutedUntil()
if (changed) { if (changed) {
Log.d(TAG, "Global muted until timestamp expired; updating prefs") Log.d(TAG, "Global muted until timestamp expired; updating prefs")
showHideNotificationMenuItems() showHideNotificationMenuItems()
} }
// Check subscriptions // Check subscriptions
var rerenderList = false var rerenderList = false
repository.getSubscriptions().forEach { subscription -> repository.getSubscriptions().forEach { subscription ->
val mutedUntilExpired = subscription.mutedUntil > 1L && System.currentTimeMillis()/1000 > subscription.mutedUntil val mutedUntilExpired = subscription.mutedUntil > 1L && System.currentTimeMillis()/1000 > subscription.mutedUntil
if (mutedUntilExpired) { if (mutedUntilExpired) {
Log.d(TAG, "Subscription ${subscription.id}: Muted until timestamp expired, updating subscription") Log.d(TAG, "Subscription ${subscription.id}: Muted until timestamp expired, updating subscription")
val newSubscription = subscription.copy(mutedUntil = 0L) val newSubscription = subscription.copy(mutedUntil = 0L)
repository.updateSubscription(newSubscription) repository.updateSubscription(newSubscription)
rerenderList = true rerenderList = true
}
} }
if (rerenderList) { }
redrawList() if (rerenderList) {
} redrawList()
delay(60_000)
} }
} }
} }
private fun showHideNotificationMenuItems() { private fun showHideNotificationMenuItems() {
if (!this::menu.isInitialized) {
return
}
val mutedUntilSeconds = repository.getGlobalMutedUntil() val mutedUntilSeconds = repository.getGlobalMutedUntil()
runOnUiThread { runOnUiThread {
val notificationsEnabledItem = menu.findItem(R.id.main_menu_notifications_enabled) val notificationsEnabledItem = menu.findItem(R.id.main_menu_notifications_enabled)
@ -502,6 +504,9 @@ class MainActivity : AppCompatActivity(), ActionMode.Callback, AddFragment.Subsc
} }
private fun redrawList() { private fun redrawList() {
if (!this::mainList.isInitialized) {
return
}
runOnUiThread { runOnUiThread {
mainList.adapter = adapter // Oh, what a hack ... mainList.adapter = adapter // Oh, what a hack ...
} }
@ -516,9 +521,11 @@ class MainActivity : AppCompatActivity(), ActionMode.Callback, AddFragment.Subsc
const val EXTRA_SUBSCRIPTION_MUTED_UNTIL = "subscriptionMutedUntil" const val EXTRA_SUBSCRIPTION_MUTED_UNTIL = "subscriptionMutedUntil"
const val ANIMATION_DURATION = 80L const val ANIMATION_DURATION = 80L
// As per Documentation: The minimum repeat interval that can be defined is 15 minutes // 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. // (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! // Thanks to varunon9 (https://gist.github.com/varunon9/f2beec0a743c96708eb0ef971a9ff9cd) for this!
const val MINIMUM_PERIODIC_WORKER_INTERVAL = 16L
const val POLL_WORKER_INTERVAL_MINUTES = 2 * 60L
const val SERVICE_START_WORKER_INTERVAL_MINUTES = 6 * 60L
} }
} }

View file

@ -25,7 +25,7 @@ class SettingsActivity : AppCompatActivity() {
super.onCreate(savedInstanceState) super.onCreate(savedInstanceState)
setContentView(R.layout.activity_settings) setContentView(R.layout.activity_settings)
Log.d(MainActivity.TAG, "Create $this") Log.d(TAG, "Create $this")
if (savedInstanceState == null) { if (savedInstanceState == null) {
supportFragmentManager supportFragmentManager
@ -185,4 +185,8 @@ class SettingsActivity : AppCompatActivity() {
} }
} }
} }
companion object {
const val TAG = "NtfySettingsActivity"
}
} }

View file

@ -12,7 +12,7 @@ import java.util.*
fun topicUrl(baseUrl: String, topic: String) = "${baseUrl}/${topic}" fun topicUrl(baseUrl: String, topic: String) = "${baseUrl}/${topic}"
fun topicUrlUp(baseUrl: String, topic: String) = "${baseUrl}/${topic}?up=1" // UnifiedPush fun topicUrlUp(baseUrl: String, topic: String) = "${baseUrl}/${topic}?up=1" // UnifiedPush
fun topicUrlJson(baseUrl: String, topic: String, since: String) = "${topicUrl(baseUrl, topic)}/json?since=$since" fun topicUrlJson(baseUrl: String, topic: String, since: String) = "${topicUrl(baseUrl, topic)}/json?since=$since"
fun topicUrlJsonPoll(baseUrl: String, topic: String) = "${topicUrl(baseUrl, topic)}/json?poll=1" fun topicUrlJsonPoll(baseUrl: String, topic: String, since: String) = "${topicUrl(baseUrl, topic)}/json?poll=1&since=$since"
fun topicShortUrl(baseUrl: String, topic: String) = fun topicShortUrl(baseUrl: String, topic: String) =
topicUrl(baseUrl, topic) topicUrl(baseUrl, topic)
.replace("http://", "") .replace("http://", "")

View file

@ -32,7 +32,7 @@ class PollWorker(ctx: Context, params: WorkerParameters) : CoroutineWorker(ctx,
repository.getSubscriptions().forEach{ subscription -> repository.getSubscriptions().forEach{ subscription ->
try { try {
val notifications = api.poll(subscription.id, subscription.baseUrl, subscription.topic) val notifications = api.poll(subscription.id, subscription.baseUrl, subscription.topic, since = subscription.lastActive)
val newNotifications = repository val newNotifications = repository
.onlyNewNotifications(subscription.id, notifications) .onlyNewNotifications(subscription.id, notifications)
.map { it.copy(notificationId = Random.nextInt()) } .map { it.copy(notificationId = Random.nextInt()) }
@ -53,6 +53,6 @@ class PollWorker(ctx: Context, params: WorkerParameters) : CoroutineWorker(ctx,
companion object { companion object {
const val VERSION = BuildConfig.VERSION_CODE const val VERSION = BuildConfig.VERSION_CODE
const val TAG = "NtfyPollWorker" const val TAG = "NtfyPollWorker"
const val WORK_NAME_PERIODIC = "NtfyPollWorkerPeriodic" const val WORK_NAME_PERIODIC = "NtfyPollWorkerPeriodic" // Do not change
} }
} }