diff --git a/app/src/main/AndroidManifest.xml b/app/src/main/AndroidManifest.xml
index 1146d7d..369e85d 100644
--- a/app/src/main/AndroidManifest.xml
+++ b/app/src/main/AndroidManifest.xml
@@ -49,6 +49,13 @@
+
+
+
+
+
+
+
toSubscriptionList(list) }
}
- fun getSubscriptionIdsLiveData(): LiveData> {
+ fun getSubscriptionIdsWithInstantStatusLiveData(): LiveData>> {
return subscriptionDao
.listFlow()
.asLiveData()
- .map { list -> list.map { it.id }.toSet() }
+ .map { list -> list.map { Pair(it.id, it.instant) }.toSet() }
}
fun getSubscriptions(): List {
return toSubscriptionList(subscriptionDao.list())
}
+ @Suppress("RedundantSuspendModifier")
+ @WorkerThread
+ suspend fun getSubscription(subscriptionId: Long): Subscription? {
+ return toSubscription(subscriptionDao.get(subscriptionId))
+ }
+
@Suppress("RedundantSuspendModifier")
@WorkerThread
suspend fun getSubscription(baseUrl: String, topic: String): Subscription? {
@@ -42,6 +48,12 @@ class Repository(private val subscriptionDao: SubscriptionDao, private val notif
subscriptionDao.add(subscription)
}
+ @Suppress("RedundantSuspendModifier")
+ @WorkerThread
+ suspend fun updateSubscription(subscription: Subscription) {
+ subscriptionDao.update(subscription)
+ }
+
@Suppress("RedundantSuspendModifier")
@WorkerThread
suspend fun removeSubscription(subscriptionId: Long) {
diff --git a/app/src/main/java/io/heckel/ntfy/msg/ApiService.kt b/app/src/main/java/io/heckel/ntfy/msg/ApiService.kt
index 52700a8..fec1629 100644
--- a/app/src/main/java/io/heckel/ntfy/msg/ApiService.kt
+++ b/app/src/main/java/io/heckel/ntfy/msg/ApiService.kt
@@ -79,10 +79,12 @@ class ApiService {
}
}
} catch (e: Exception) {
+ Log.e(TAG, "Connection to $url failed (1): ${e.message}", e)
fail(e)
}
}
override fun onFailure(call: Call, e: IOException) {
+ Log.e(TAG, "Connection to $url failed (2): ${e.message}", e)
fail(e)
}
})
diff --git a/app/src/main/java/io/heckel/ntfy/msg/FirebaseService.kt b/app/src/main/java/io/heckel/ntfy/msg/FirebaseService.kt
index 9017ba7..0088f3b 100644
--- a/app/src/main/java/io/heckel/ntfy/msg/FirebaseService.kt
+++ b/app/src/main/java/io/heckel/ntfy/msg/FirebaseService.kt
@@ -32,6 +32,7 @@ class FirebaseService : FirebaseMessagingService() {
Log.d(TAG, "Discarding unexpected message: from=${remoteMessage.from}, data=${data}")
return
}
+ Log.d(TAG, "Received notification: from=${remoteMessage.from}, data=${data}")
CoroutineScope(job).launch {
val baseUrl = getString(R.string.app_base_url) // Everything from Firebase comes from main service URL!
diff --git a/app/src/main/java/io/heckel/ntfy/msg/StartReceiver.kt b/app/src/main/java/io/heckel/ntfy/msg/StartReceiver.kt
new file mode 100644
index 0000000..3f88e38
--- /dev/null
+++ b/app/src/main/java/io/heckel/ntfy/msg/StartReceiver.kt
@@ -0,0 +1,6 @@
+package io.heckel.ntfy.msg
+
+import android.content.BroadcastReceiver
+import android.content.Context
+import android.content.Intent
+import android.os.Build
diff --git a/app/src/main/java/io/heckel/ntfy/msg/SubscriberService.kt b/app/src/main/java/io/heckel/ntfy/msg/SubscriberService.kt
index dd031ce..2f8022d 100644
--- a/app/src/main/java/io/heckel/ntfy/msg/SubscriberService.kt
+++ b/app/src/main/java/io/heckel/ntfy/msg/SubscriberService.kt
@@ -1,6 +1,7 @@
package io.heckel.ntfy.msg
import android.app.*
+import android.content.BroadcastReceiver
import android.content.Context
import android.content.Intent
import android.os.Build
@@ -33,10 +34,8 @@ class SubscriberService : Service() {
private val calls = ConcurrentHashMap() // Subscription ID -> Cal
private val api = ApiService()
private val notifier = NotificationService(this)
-
- override fun onBind(intent: Intent): IBinder? {
- return null // We don't provide binding, so return null
- }
+ private var notificationManager: NotificationManager? = null
+ private var notification: Notification? = null
override fun onStartCommand(intent: Intent?, flags: Int, startId: Int): Int {
Log.d(TAG, "onStartCommand executed with startId: $startId")
@@ -56,29 +55,24 @@ class SubscriberService : Service() {
override fun onCreate() {
super.onCreate()
- Log.d(TAG, "The service has been created".toUpperCase())
- val notification = createNotification()
- startForeground(SERVICE_ID, notification)
+ Log.d(TAG, "Subscriber service has been created")
+
+ val title = getString(R.string.channel_subscriber_notification_title)
+ val text = getString(R.string.channel_subscriber_notification_text)
+ notificationManager = createNotificationChannel()
+ notification = createNotification(title, text)
+
+ startForeground(NOTIFICATION_SERVICE_ID, notification)
}
override fun onDestroy() {
super.onDestroy()
- Log.d(TAG, "The service has been destroyed".toUpperCase())
- }
-
- override fun onTaskRemoved(rootIntent: Intent) {
- val restartServiceIntent = Intent(applicationContext, SubscriberService::class.java).also {
- it.setPackage(packageName)
- };
- val restartServicePendingIntent: PendingIntent = PendingIntent.getService(this, 1, restartServiceIntent, PendingIntent.FLAG_ONE_SHOT);
- applicationContext.getSystemService(Context.ALARM_SERVICE);
- val alarmService: AlarmManager = applicationContext.getSystemService(Context.ALARM_SERVICE) as AlarmManager;
- alarmService.set(AlarmManager.ELAPSED_REALTIME, SystemClock.elapsedRealtime() + 1000, restartServicePendingIntent);
+ Log.d(TAG, "Subscriber service has been destroyed")
}
private fun startService() {
if (isServiceStarted) {
- launchOrCancelJobs()
+ launchAndCancelJobs()
return
}
Log.d(TAG, "Starting the foreground service task")
@@ -89,7 +83,7 @@ class SubscriberService : Service() {
acquire()
}
}
- launchOrCancelJobs()
+ launchAndCancelJobs()
}
private fun stopService() {
@@ -118,100 +112,116 @@ class SubscriberService : Service() {
saveServiceState(this, ServiceState.STOPPED)
}
- private fun launchOrCancelJobs() = GlobalScope.launch(Dispatchers.IO) {
- val subscriptions = repository.getSubscriptions().filter { s -> s.instant }
- val subscriptionIds = subscriptions.map { it.id }
- Log.d(TAG, "Starting/stopping jobs for current subscriptions")
- Log.d(TAG, "- Subscriptions: $subscriptions")
- Log.d(TAG, "- Jobs: $jobs")
- Log.d(TAG, "- HTTP calls: $calls")
- subscriptions.forEach { subscription ->
- if (!jobs.containsKey(subscription.id)) {
- Log.d(TAG, "Starting job for $subscription")
- jobs[subscription.id] = launchJob(this, subscription)
- }
- }
- jobs.keys().toList().forEach { subscriptionId ->
- if (!subscriptionIds.contains(subscriptionId)) {
- Log.d(TAG, "Cancelling job for $subscriptionId")
- val job = jobs.remove(subscriptionId)
- val call = calls.remove(subscriptionId)
- job?.cancel()
- call?.cancel()
+ private fun launchAndCancelJobs() =
+ GlobalScope.launch(Dispatchers.IO) {
+ val subscriptions = repository.getSubscriptions().filter { s -> s.instant }
+ val subscriptionIds = subscriptions.map { it.id }
+ Log.d(TAG, "Refreshing subscriptions")
+ Log.d(TAG, "- Subscriptions: $subscriptions")
+ Log.d(TAG, "- Jobs: $jobs")
+ Log.d(TAG, "- HTTP calls: $calls")
+ subscriptions.forEach { subscription ->
+ if (!jobs.containsKey(subscription.id)) {
+ jobs[subscription.id] = launchJob(this, subscription)
+ }
+ }
+ jobs.keys().toList().forEach { subscriptionId ->
+ if (!subscriptionIds.contains(subscriptionId)) {
+ cancelJob(subscriptionId)
+ }
}
}
+
+ private fun cancelJob(subscriptionId: Long?) {
+ Log.d(TAG, "Cancelling job for $subscriptionId")
+ val job = jobs.remove(subscriptionId)
+ val call = calls.remove(subscriptionId)
+ job?.cancel()
+ call?.cancel()
}
- private fun launchJob(scope: CoroutineScope, subscription: Subscription): Job = scope.launch(Dispatchers.IO) {
- val url = topicUrl(subscription.baseUrl, subscription.topic)
- Log.d(TAG, "[$url] Starting connection job")
- var since = 0L
- var retryMillis = 0L
- while (isActive && isServiceStarted) {
- Log.d(TAG, "[$url] (Re-)starting subscription for $subscription")
- val startTime = System.currentTimeMillis()
+ private fun launchJob(scope: CoroutineScope, subscription: Subscription): Job =
+ scope.launch(Dispatchers.IO) {
+ val url = topicUrl(subscription.baseUrl, subscription.topic)
+ Log.d(TAG, "[$url] Starting connection job")
- try {
- val failed = AtomicBoolean(false)
+ // Retry-loop: if the connection fails, we retry unless the job or service is cancelled/stopped
+ var since = 0L
+ var retryMillis = 0L
+ while (isActive && isServiceStarted) {
+ Log.d(TAG, "[$url] (Re-)starting subscription for $subscription")
+ val startTime = System.currentTimeMillis()
val notify = { n: io.heckel.ntfy.data.Notification ->
- Log.d(TAG, "[$url] Received new notification: $n")
since = n.timestamp
- scope.launch(Dispatchers.IO) {
- val added = repository.addNotification(n)
- if (added) {
- Log.d(TAG, "[$url] Showing notification: $n")
- notifier.send(subscription, n.message)
- }
+ onNotificationReceived(scope, subscription, n)
+ }
+ val failed = AtomicBoolean(false)
+ val fail = { e: Exception -> failed.set(true) }
+
+ // Call /json subscribe endpoint and loop until the call fails, is canceled,
+ // or the job or service are cancelled/stopped
+ try {
+ val call = api.subscribe(subscription.id, subscription.baseUrl, subscription.topic, since, notify, fail)
+ calls[subscription.id] = call
+ while (!failed.get() && !call.isCanceled() && isActive && isServiceStarted) {
+ Log.d(TAG, "[$url] Connection is active (failed=$failed, callCanceled=${call.isCanceled()}, jobActive=$isActive, serviceStarted=$isServiceStarted")
+ delay(CONNECTION_LOOP_DELAY_MILLIS) // Resumes immediately if job is cancelled
}
- Unit
+ } catch (e: Exception) {
+ Log.e(TAG, "[$url] Connection failed: ${e.message}", e)
}
- val fail = { e: Exception ->
- Log.e(TAG, "[$url] Connection failed (1): ${e.message}", e)
- failed.set(true)
+
+ // If we're not cancelled yet, wait little before retrying (incremental back-off)
+ if (isActive && isServiceStarted) {
+ retryMillis = nextRetryMillis(retryMillis, startTime)
+ Log.d(TAG, "Connection failed, retrying connection in ${retryMillis/1000}s ...")
+ delay(retryMillis)
}
- val call = api.subscribe(subscription.id, subscription.baseUrl, subscription.topic, since, notify, fail)
- calls[subscription.id] = call
- while (!failed.get() && !call.isCanceled() && isActive && isServiceStarted) {
- Log.d(TAG, "[$url] Connection is active (failed=$failed, callCanceled=${call.isCanceled()}, jobActive=$isActive, serviceStarted=$isServiceStarted")
- delay(CONNECTION_LOOP_DELAY_MILLIS) // Resumes immediately if job is cancelled
- }
- } catch (e: Exception) {
- Log.e(TAG, "[$url] Connection failed (2): ${e.message}", e)
}
- if (isActive && isServiceStarted) {
- val connectionDurationMillis = System.currentTimeMillis() - startTime
- if (connectionDurationMillis > RETRY_RESET_AFTER_MILLIS) {
- retryMillis = RETRY_STEP_MILLIS
- } else if (retryMillis + RETRY_STEP_MILLIS >= RETRY_MAX_MILLIS) {
- retryMillis = RETRY_MAX_MILLIS
- } else {
- retryMillis += RETRY_STEP_MILLIS
- }
- Log.d(TAG, "Connection failed, retrying connection in ${retryMillis/1000}s ...")
- delay(retryMillis)
+ Log.d(TAG, "[$url] Connection job SHUT DOWN")
+ }
+
+ private fun onNotificationReceived(scope: CoroutineScope, subscription: Subscription, n: io.heckel.ntfy.data.Notification) {
+ val url = topicUrl(subscription.baseUrl, subscription.topic)
+ Log.d(TAG, "[$url] Received notification: $n")
+ scope.launch(Dispatchers.IO) {
+ val added = repository.addNotification(n)
+ if (added) {
+ Log.d(TAG, "[$url] Showing notification: $n")
+ notifier.send(subscription, n.message)
}
}
- Log.d(TAG, "[$url] Connection job SHUT DOWN")
}
- private fun createNotification(): Notification {
+ private fun nextRetryMillis(retryMillis: Long, startTime: Long): Long {
+ val connectionDurationMillis = System.currentTimeMillis() - startTime
+ if (connectionDurationMillis > RETRY_RESET_AFTER_MILLIS) {
+ return RETRY_STEP_MILLIS
+ } else if (retryMillis + RETRY_STEP_MILLIS >= RETRY_MAX_MILLIS) {
+ return RETRY_MAX_MILLIS
+ }
+ return retryMillis + RETRY_STEP_MILLIS
+ }
+
+ private fun createNotificationChannel(): NotificationManager? {
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O) {
val notificationManager = getSystemService(Context.NOTIFICATION_SERVICE) as NotificationManager
val channelName = getString(R.string.channel_subscriber_service_name) // Show's up in UI
- val channel = NotificationChannel(CHANNEL_ID, channelName, NotificationManager.IMPORTANCE_LOW).let {
+ val channel = NotificationChannel(NOTIFICATION_CHANNEL_ID, channelName, NotificationManager.IMPORTANCE_LOW).let {
it.setShowBadge(false) // Don't show long-press badge
it
}
notificationManager.createNotificationChannel(channel)
+ return notificationManager
}
+ return null
+ }
+ private fun createNotification(title: String, text: String): Notification {
val pendingIntent: PendingIntent = Intent(this, MainActivity::class.java).let { notificationIntent ->
PendingIntent.getActivity(this, 0, notificationIntent, 0)
}
-
- val title = getString(R.string.channel_subscriber_notification_title)
- val text = getString(R.string.channel_subscriber_notification_text)
- return NotificationCompat.Builder(this, CHANNEL_ID)
+ return NotificationCompat.Builder(this, NOTIFICATION_CHANNEL_ID)
.setSmallIcon(R.drawable.ic_notification_icon)
.setContentTitle(title)
.setContentText(text)
@@ -221,6 +231,39 @@ class SubscriberService : Service() {
.build()
}
+ override fun onBind(intent: Intent): IBinder? {
+ return null // We don't provide binding, so return null
+ }
+
+ /* This re-schedules the task when the "Clear recent apps" button is pressed */
+ override fun onTaskRemoved(rootIntent: Intent) {
+ val restartServiceIntent = Intent(applicationContext, SubscriberService::class.java).also {
+ it.setPackage(packageName)
+ };
+ val restartServicePendingIntent: PendingIntent = PendingIntent.getService(this, 1, restartServiceIntent, PendingIntent.FLAG_ONE_SHOT);
+ applicationContext.getSystemService(Context.ALARM_SERVICE);
+ val alarmService: AlarmManager = applicationContext.getSystemService(Context.ALARM_SERVICE) as AlarmManager;
+ alarmService.set(AlarmManager.ELAPSED_REALTIME, SystemClock.elapsedRealtime() + 1000, restartServicePendingIntent);
+ }
+
+ /* This re-starts the service on reboot; see manifest */
+ class StartReceiver : BroadcastReceiver() {
+ override fun onReceive(context: Context, intent: Intent) {
+ if (intent.action == Intent.ACTION_BOOT_COMPLETED && readServiceState(context) == ServiceState.STARTED) {
+ Intent(context, SubscriberService::class.java).also {
+ it.action = Actions.START.name
+ if (Build.VERSION.SDK_INT >= 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)
+ }
+ }
+ }
+ }
+
enum class Actions {
START,
STOP
@@ -234,14 +277,14 @@ class SubscriberService : Service() {
companion object {
private const val TAG = "NtfySubscriberService"
private const val WAKE_LOCK_TAG = "SubscriberService:lock"
- private const val CHANNEL_ID = "ntfy-subscriber"
- private const val SERVICE_ID = 2586
+ private const val NOTIFICATION_CHANNEL_ID = "ntfy-subscriber"
+ private const val NOTIFICATION_SERVICE_ID = 2586
private const val SHARED_PREFS_ID = "SubscriberService"
private const val SHARED_PREFS_SERVICE_STATE = "ServiceState"
private const val CONNECTION_LOOP_DELAY_MILLIS = 30_000L
private const val RETRY_STEP_MILLIS = 5_000L
private const val RETRY_MAX_MILLIS = 60_000L
- private const val RETRY_RESET_AFTER_MILLIS = 30_000L
+ private const val RETRY_RESET_AFTER_MILLIS = 60_000L // Must be larger than CONNECTION_LOOP_DELAY_MILLIS
fun saveServiceState(context: Context, state: ServiceState) {
val sharedPrefs = context.getSharedPreferences(SHARED_PREFS_ID, Context.MODE_PRIVATE)
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 0336ec1..8c1d550 100644
--- a/app/src/main/java/io/heckel/ntfy/ui/DetailActivity.kt
+++ b/app/src/main/java/io/heckel/ntfy/ui/DetailActivity.kt
@@ -19,12 +19,14 @@ import androidx.appcompat.app.AppCompatActivity
import androidx.core.content.ContextCompat
import androidx.lifecycle.lifecycleScope
import androidx.recyclerview.widget.RecyclerView
+import androidx.work.WorkManager
import io.heckel.ntfy.R
import io.heckel.ntfy.app.Application
import io.heckel.ntfy.data.Notification
import io.heckel.ntfy.data.topicShortUrl
import io.heckel.ntfy.data.topicUrl
import io.heckel.ntfy.msg.ApiService
+import io.heckel.ntfy.msg.NotificationService
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.launch
import java.util.*
@@ -37,16 +39,20 @@ class DetailActivity : AppCompatActivity(), ActionMode.Callback {
}
private val repository by lazy { (application as Application).repository }
private val api = ApiService()
+ private var subscriberManager: SubscriberManager? = null // Context-dependent
// Which subscription are we looking at
private var subscriptionId: Long = 0L // Set in onCreate()
private var subscriptionBaseUrl: String = "" // Set in onCreate()
private var subscriptionTopic: String = "" // Set in onCreate()
- private var subscriptionInstant: Boolean = false // Set in onCreate()
+ private var subscriptionInstant: Boolean = false // Set in onCreate() & updated by options menu!
+
+ // UI elements
+ private lateinit var adapter: DetailAdapter
+ private lateinit var mainList: RecyclerView
+ private lateinit var menu: Menu
// Action mode stuff
- private lateinit var mainList: RecyclerView
- private lateinit var adapter: DetailAdapter
private var actionMode: ActionMode? = null
override fun onCreate(savedInstanceState: Bundle?) {
@@ -55,6 +61,9 @@ class DetailActivity : AppCompatActivity(), ActionMode.Callback {
Log.d(MainActivity.TAG, "Create $this")
+ // Dependencies that depend on Context
+ subscriberManager = SubscriberManager(this)
+
// Show 'Back' button
supportActionBar?.setDisplayHomeAsUpEnabled(true)
@@ -101,10 +110,17 @@ class DetailActivity : AppCompatActivity(), ActionMode.Callback {
}
}
}
+
+ // React to changes in fast delivery setting
+ repository.getSubscriptionIdsWithInstantStatusLiveData().observe(this) {
+ subscriberManager?.refreshService(it)
+ }
}
override fun onCreateOptionsMenu(menu: Menu): Boolean {
menuInflater.inflate(R.menu.detail_action_bar_menu, menu)
+ this.menu = menu
+ showHideInstantMenuItems(subscriptionInstant)
return true
}
@@ -118,6 +134,14 @@ class DetailActivity : AppCompatActivity(), ActionMode.Callback {
onRefreshClick()
true
}
+ R.id.detail_menu_enable_instant -> {
+ onInstantEnableClick(enable = true)
+ true
+ }
+ R.id.detail_menu_disable_instant -> {
+ onInstantEnableClick(enable = false)
+ true
+ }
R.id.detail_menu_copy_url -> {
onCopyUrlClick()
true
@@ -183,6 +207,42 @@ class DetailActivity : AppCompatActivity(), ActionMode.Callback {
}
}
+ private fun onInstantEnableClick(enable: Boolean) {
+ Log.d(TAG, "Toggling instant delivery setting for ${topicShortUrl(subscriptionBaseUrl, subscriptionTopic)}")
+
+ lifecycleScope.launch(Dispatchers.IO) {
+ val subscription = repository.getSubscription(subscriptionId)
+ val newSubscription = subscription?.copy(instant = enable)
+ newSubscription?.let { repository.updateSubscription(newSubscription) }
+ showHideInstantMenuItems(enable)
+ runOnUiThread {
+ if (enable) {
+ Toast.makeText(this@DetailActivity, getString(R.string.detail_instant_delivery_enabled), Toast.LENGTH_SHORT)
+ .show()
+ } else {
+ Toast.makeText(this@DetailActivity, getString(R.string.detail_instant_delivery_disabled), Toast.LENGTH_SHORT)
+ .show()
+ }
+ }
+ }
+ }
+
+ private fun showHideInstantMenuItems(enable: Boolean) {
+ subscriptionInstant = enable
+ runOnUiThread {
+ val appBaseUrl = getString(R.string.app_base_url)
+ val enableInstantItem = menu.findItem(R.id.detail_menu_enable_instant)
+ val disableInstantItem = menu.findItem(R.id.detail_menu_disable_instant)
+ if (subscriptionBaseUrl == appBaseUrl) {
+ enableInstantItem?.isVisible = !subscriptionInstant
+ disableInstantItem?.isVisible = subscriptionInstant
+ } else {
+ enableInstantItem?.isVisible = false
+ disableInstantItem?.isVisible = false
+ }
+ }
+ }
+
private fun onDeleteClick() {
Log.d(TAG, "Deleting subscription ${topicShortUrl(subscriptionBaseUrl, subscriptionTopic)}")
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 6dcd9e4..fd8a21a 100644
--- a/app/src/main/java/io/heckel/ntfy/ui/MainActivity.kt
+++ b/app/src/main/java/io/heckel/ntfy/ui/MainActivity.kt
@@ -51,6 +51,7 @@ class MainActivity : AppCompatActivity(), ActionMode.Callback {
private var actionMode: ActionMode? = null
private var workManager: WorkManager? = null // Context-dependent
private var notifier: NotificationService? = null // Context-dependent
+ private var subscriberManager: SubscriberManager? = null // Context-dependent
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
@@ -61,6 +62,7 @@ class MainActivity : AppCompatActivity(), ActionMode.Callback {
// Dependencies that depend on Context
workManager = WorkManager.getInstance(this)
notifier = NotificationService(this)
+ subscriberManager = SubscriberManager(this)
// Action bar
title = getString(R.string.main_action_bar_title)
@@ -93,13 +95,13 @@ class MainActivity : AppCompatActivity(), ActionMode.Callback {
}
}
- viewModel.listIds().observe(this) {
- maybeStartOrStopSubscriberService()
+ // React to changes in fast delivery setting
+ viewModel.listIdsWithInstantStatus().observe(this) {
+ subscriberManager?.refreshService(it)
}
// Background things
startPeriodicWorker()
- maybeStartOrStopSubscriberService()
}
private fun startPeriodicWorker() {
@@ -239,35 +241,6 @@ class MainActivity : AppCompatActivity(), ActionMode.Callback {
}
}
- private fun maybeStartOrStopSubscriberService() {
- Log.d(TAG, "Triggering subscriber service refresh")
- lifecycleScope.launch(Dispatchers.IO) {
- val instantSubscriptions = repository.getSubscriptions().filter { s -> s.instant }
- if (instantSubscriptions.isEmpty()) {
- performActionOnSubscriberService(Actions.STOP)
- } else {
- performActionOnSubscriberService(Actions.START)
- }
- }
- }
-
- private fun performActionOnSubscriberService(action: Actions) {
- val serviceState = readServiceState(this)
- if (serviceState == ServiceState.STOPPED && action == Actions.STOP) {
- return
- }
- val intent = Intent(this, SubscriberService::class.java)
- intent.action = action.name
- if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O) {
- Log.d(TAG, "Performing SubscriberService action: ${action.name} (as foreground service, API >= 26)")
- startForegroundService(intent)
- return
- } else {
- Log.d(TAG, "Performing SubscriberService action: ${action.name} (as background service, API >= 26)")
- startService(intent)
- }
- }
-
private fun startDetailView(subscription: Subscription) {
Log.d(TAG, "Entering detail view for subscription $subscription")
diff --git a/app/src/main/java/io/heckel/ntfy/ui/MainAdapter.kt b/app/src/main/java/io/heckel/ntfy/ui/MainAdapter.kt
index 863c3a6..315b4e7 100644
--- a/app/src/main/java/io/heckel/ntfy/ui/MainAdapter.kt
+++ b/app/src/main/java/io/heckel/ntfy/ui/MainAdapter.kt
@@ -48,6 +48,7 @@ class MainAdapter(private val onClick: (Subscription) -> Unit, private val onLon
private val nameView: TextView = itemView.findViewById(R.id.main_item_text)
private val statusView: TextView = itemView.findViewById(R.id.main_item_status)
private val dateView: TextView = itemView.findViewById(R.id.main_item_date)
+ private val instantImageView: View = itemView.findViewById(R.id.main_item_instant_image)
fun bind(subscription: Subscription) {
this.subscription = subscription
@@ -66,6 +67,11 @@ class MainAdapter(private val onClick: (Subscription) -> Unit, private val onLon
nameView.text = topicShortUrl(subscription.baseUrl, subscription.topic)
statusView.text = statusMessage
dateView.text = dateText
+ if (subscription.instant) {
+ instantImageView.visibility = View.VISIBLE
+ } else {
+ instantImageView.visibility = View.GONE
+ }
itemView.setOnClickListener { onClick(subscription) }
itemView.setOnLongClickListener { onLongClick(subscription); true }
if (selected.contains(subscription.id)) {
diff --git a/app/src/main/java/io/heckel/ntfy/ui/MainViewModel.kt b/app/src/main/java/io/heckel/ntfy/ui/MainViewModel.kt
index 234afca..f0b0275 100644
--- a/app/src/main/java/io/heckel/ntfy/ui/MainViewModel.kt
+++ b/app/src/main/java/io/heckel/ntfy/ui/MainViewModel.kt
@@ -14,8 +14,8 @@ class SubscriptionsViewModel(private val repository: Repository) : ViewModel() {
return repository.getSubscriptionsLiveData()
}
- fun listIds(): LiveData> {
- return repository.getSubscriptionIdsLiveData()
+ fun listIdsWithInstantStatus(): LiveData>> {
+ return repository.getSubscriptionIdsWithInstantStatusLiveData()
}
fun add(subscription: Subscription) = viewModelScope.launch(Dispatchers.IO) {
diff --git a/app/src/main/java/io/heckel/ntfy/ui/SubscriberManager.kt b/app/src/main/java/io/heckel/ntfy/ui/SubscriberManager.kt
new file mode 100644
index 0000000..a348d8a
--- /dev/null
+++ b/app/src/main/java/io/heckel/ntfy/ui/SubscriberManager.kt
@@ -0,0 +1,44 @@
+package io.heckel.ntfy.ui
+
+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.launch
+
+/**
+ * This class only manages the SubscriberService, i.e. it starts or stops it.
+ * It's used in multiple activities.
+ */
+class SubscriberManager(private val activity: ComponentActivity) {
+ fun refreshService(subscriptionIdsWithInstantStatus: Set>) {
+ Log.d(MainActivity.TAG, "Triggering subscriber service refresh")
+ activity.lifecycleScope.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(activity)
+ if (serviceState == SubscriberService.ServiceState.STOPPED && action == SubscriberService.Actions.STOP) {
+ return
+ }
+ val intent = Intent(activity, 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)")
+ activity.startForegroundService(intent)
+ } else {
+ Log.d(MainActivity.TAG, "Performing SubscriberService action: ${action.name} (as background service, API >= 26)")
+ activity.startService(intent)
+ }
+ }
+}
diff --git a/app/src/main/res/drawable/ic_bolt_black_24dp.xml b/app/src/main/res/drawable/ic_bolt_black_24dp.xml
new file mode 100644
index 0000000..722999e
--- /dev/null
+++ b/app/src/main/res/drawable/ic_bolt_black_24dp.xml
@@ -0,0 +1,9 @@
+
+
+
diff --git a/app/src/main/res/drawable/baseline_delete_20.xml b/app/src/main/res/drawable/ic_delete_gray_20dp.xml
similarity index 100%
rename from app/src/main/res/drawable/baseline_delete_20.xml
rename to app/src/main/res/drawable/ic_delete_gray_20dp.xml
diff --git a/app/src/main/res/layout/main_fragment_item.xml b/app/src/main/res/layout/main_fragment_item.xml
index 199da81..a5035bf 100644
--- a/app/src/main/res/layout/main_fragment_item.xml
+++ b/app/src/main/res/layout/main_fragment_item.xml
@@ -1,39 +1,47 @@
-
+
-
-
-
-
-
+ android:layout_width="37dp"
+ android:layout_height="37dp" app:srcCompat="@drawable/ic_sms_gray_48dp"
+ android:id="@+id/main_item_image" app:layout_constraintTop_toTopOf="parent"
+ app:layout_constraintStart_toStartOf="parent"
+ android:layout_marginStart="15dp" android:layout_marginTop="12dp"/>
-
-
+ android:text="ntfy.sh/example"
+ android:layout_width="0dp"
+ android:layout_height="wrap_content" android:id="@+id/main_item_text"
+ app:layout_constraintTop_toTopOf="parent"
+ app:layout_constraintBottom_toTopOf="@+id/main_item_status"
+ android:layout_marginStart="10dp" app:layout_constraintStart_toEndOf="@+id/main_item_image"
+ app:layout_constraintVertical_bias="0.0" android:textAppearance="@style/TextAppearance.AppCompat.Medium"
+ android:textColor="@color/primaryTextColor" android:layout_marginTop="10dp"
+ app:layout_constraintEnd_toStartOf="@+id/main_item_instant_image"/>
+
+
+
+
diff --git a/app/src/main/res/menu/detail_action_bar_menu.xml b/app/src/main/res/menu/detail_action_bar_menu.xml
index 9b169ef..3a0d49a 100644
--- a/app/src/main/res/menu/detail_action_bar_menu.xml
+++ b/app/src/main/res/menu/detail_action_bar_menu.xml
@@ -1,5 +1,8 @@
-