Loading app/src/main/java/io/heckel/ntfy/data/Database.kt +2 −2 Original line number Diff line number Diff line Loading @@ -34,8 +34,8 @@ data class SubscriptionWithMetadata( val topic: String, val instant: Boolean, val mutedUntil: Long, val upAppId: String, val upConnectorToken: String, val upAppId: String?, val upConnectorToken: String?, val totalCount: Int, val newCount: Int, val lastActive: Long Loading app/src/main/java/io/heckel/ntfy/ui/MainActivity.kt +13 −3 Original line number Diff line number Diff line Loading @@ -283,8 +283,8 @@ class MainActivity : AppCompatActivity(), ActionMode.Callback, AddFragment.Subsc topic = topic, instant = instant, mutedUntil = 0, upAppId = "", upConnectorToken = "", upAppId = null, upConnectorToken = null, totalCount = 0, newCount = 0, lastActive = Date().time/1000 Loading Loading @@ -314,11 +314,21 @@ class MainActivity : AppCompatActivity(), ActionMode.Callback, AddFragment.Subsc private fun onSubscriptionItemClick(subscription: Subscription) { if (actionMode != null) { handleActionModeClick(subscription) } else if (subscription.upAppId != null) { // Not UnifiedPush displayUnifiedPushToast(subscription) } else { startDetailView(subscription) } } private fun displayUnifiedPushToast(subscription: Subscription) { runOnUiThread { val appId = subscription.upAppId ?: "" val toastMessage = getString(R.string.main_unified_push_toast, appId) Toast.makeText(this@MainActivity, toastMessage, Toast.LENGTH_LONG).show() } } private fun onSubscriptionItemLongClick(subscription: Subscription) { if (actionMode == null) { beginActionMode(subscription) Loading Loading @@ -415,7 +425,7 @@ class MainActivity : AppCompatActivity(), ActionMode.Callback, AddFragment.Subsc val dialog = builder .setMessage(R.string.main_action_mode_delete_dialog_message) .setPositiveButton(R.string.main_action_mode_delete_dialog_permanently_delete) { _, _ -> adapter.selected.map { viewModel.remove(it) } adapter.selected.map { subscriptionId -> viewModel.remove(this, subscriptionId) } finishActionMode() } .setNegativeButton(R.string.main_action_mode_delete_dialog_cancel) { _, _ -> Loading app/src/main/java/io/heckel/ntfy/ui/MainAdapter.kt +6 −4 Original line number Diff line number Diff line Loading @@ -55,7 +55,9 @@ class MainAdapter(private val onClick: (Subscription) -> Unit, private val onLon fun bind(subscription: Subscription) { this.subscription = subscription var statusMessage = if (subscription.totalCount == 1) { var statusMessage = if (subscription.upAppId != null) { context.getString(R.string.main_item_status_unified_push, subscription.upAppId) } else if (subscription.totalCount == 1) { context.getString(R.string.main_item_status_text_one, subscription.totalCount) } else { context.getString(R.string.main_item_status_text_not_one, subscription.totalCount) Loading @@ -82,11 +84,11 @@ class MainAdapter(private val onClick: (Subscription) -> Unit, private val onLon notificationDisabledUntilImageView.visibility = if (subscription.mutedUntil > 1L) View.VISIBLE else View.GONE notificationDisabledForeverImageView.visibility = if (subscription.mutedUntil == 1L) View.VISIBLE else View.GONE instantImageView.visibility = if (subscription.instant) View.VISIBLE else View.GONE if (subscription.newCount > 0) { if (subscription.upAppId != null || subscription.newCount == 0) { newItemsView.visibility = View.GONE } else { newItemsView.visibility = View.VISIBLE newItemsView.text = if (subscription.newCount <= 99) subscription.newCount.toString() else "99+" } else { newItemsView.visibility = View.GONE } itemView.setOnClickListener { onClick(subscription) } itemView.setOnLongClickListener { onLongClick(subscription); true } Loading app/src/main/java/io/heckel/ntfy/ui/MainViewModel.kt +8 −1 Original line number Diff line number Diff line package io.heckel.ntfy.ui import android.content.Context import androidx.lifecycle.LiveData import androidx.lifecycle.ViewModel import androidx.lifecycle.ViewModelProvider import androidx.lifecycle.viewModelScope import io.heckel.ntfy.data.* import io.heckel.ntfy.up.Distributor import kotlinx.coroutines.Dispatchers import kotlinx.coroutines.launch import kotlin.collections.List Loading @@ -22,7 +24,12 @@ class SubscriptionsViewModel(private val repository: Repository) : ViewModel() { repository.addSubscription(subscription) } fun remove(subscriptionId: Long) = viewModelScope.launch(Dispatchers.IO) { fun remove(context: Context, subscriptionId: Long) = viewModelScope.launch(Dispatchers.IO) { val subscription = repository.getSubscription(subscriptionId) ?: return@launch if (subscription.upAppId != null && subscription.upConnectorToken != null) { val distributor = Distributor(context) distributor.sendUnregistered(subscription.upAppId, subscription.upConnectorToken) } repository.removeAllNotifications(subscriptionId) repository.removeSubscription(subscriptionId) } Loading app/src/main/java/io/heckel/ntfy/up/BroadcastReceiver.kt +85 −56 Original line number Diff line number Diff line Loading @@ -5,6 +5,7 @@ import android.content.Intent import android.util.Log 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.util.randomString Loading @@ -17,27 +18,43 @@ import kotlin.random.Random class BroadcastReceiver : android.content.BroadcastReceiver() { override fun onReceive(context: Context?, intent: Intent?) { when (intent!!.action) { ACTION_REGISTER -> { val appId = intent.getStringExtra(EXTRA_APPLICATION) ?: "" val connectorToken = intent.getStringExtra(EXTRA_TOKEN) ?: "" Log.d(TAG, "Register: app=$appId, connectorToken=$connectorToken") if (appId.isBlank()) { Log.w(TAG, "Trying to register an app without packageName") if (context == null || intent == null) { return } val baseUrl = context!!.getString(R.string.app_base_url) // FIXME val topic = "up" + randomString(TOPIC_LENGTH) val endpoint = topicUrlUp(baseUrl, topic) val app = context!!.applicationContext as Application when (intent.action) { ACTION_REGISTER -> register(context, intent) ACTION_UNREGISTER -> unregister(context, intent) } } private fun register(context: Context, intent: Intent) { val appId = intent.getStringExtra(EXTRA_APPLICATION) ?: return val connectorToken = intent.getStringExtra(EXTRA_TOKEN) ?: return val app = context.applicationContext as Application val repository = app.repository val distributor = Distributor(app) Log.d(TAG, "REGISTER received for app $appId (connectorToken=$connectorToken)") if (appId.isBlank()) { Log.w(TAG, "Refusing registration: empty application") distributor.sendRegistrationRefused(appId, connectorToken) return } GlobalScope.launch(Dispatchers.IO) { val existingSubscription = repository.getSubscriptionByConnectorToken(connectorToken) if (existingSubscription != null) { if (existingSubscription.upAppId == appId) { val endpoint = topicUrlUp(existingSubscription.baseUrl, existingSubscription.topic) Log.d(TAG, "Subscription with connectorToken $connectorToken exists. Sending endpoint $endpoint.") distributor.sendEndpoint(appId, connectorToken, endpoint) } else { Log.d(TAG, "Subscription with connectorToken $connectorToken exists for a different app. Refusing registration.") distributor.sendRegistrationRefused(appId, connectorToken) } return@launch } val baseUrl = context.getString(R.string.app_base_url) // FIXME val topic = UP_PREFIX + randomString(TOPIC_LENGTH) val endpoint = topicUrlUp(baseUrl, topic) val subscription = Subscription( id = Random.nextLong(), baseUrl = baseUrl, Loading @@ -50,38 +67,50 @@ class BroadcastReceiver : android.content.BroadcastReceiver() { newCount = 0, lastActive = Date().time/1000 ) // Add subscription Log.d(TAG, "Adding subscription with for app $appId (connectorToken $connectorToken): $subscription") repository.addSubscription(subscription) val subscriptionIdsWithInstantStatus = repository.getSubscriptionIdsWithInstantStatus() val subscriberManager = SubscriberManager(app) subscriberManager.refreshService(subscriptionIdsWithInstantStatus) distributor.sendEndpoint(appId, connectorToken, endpoint) // Refresh (and maybe start) foreground service refreshSubscriberService(app, repository) } } ACTION_UNREGISTER -> { val connectorToken = intent.getStringExtra(EXTRA_TOKEN) ?: "" Log.d(TAG, "Unregister: connectorToken=$connectorToken") val app = context!!.applicationContext as Application private fun unregister(context: Context, intent: Intent) { val connectorToken = intent.getStringExtra(EXTRA_TOKEN) ?: return val app = context.applicationContext as Application val repository = app.repository val distributor = Distributor(app) Log.d(TAG, "UNREGISTER received (connectorToken=$connectorToken)") GlobalScope.launch(Dispatchers.IO) { val existingSubscription = repository.getSubscriptionByConnectorToken(connectorToken) if (existingSubscription == null) { Log.d(TAG, "Subscription with connectorToken $connectorToken does not exist. Ignoring.") return@launch } // Remove subscription Log.d(TAG, "Removing subscription ${existingSubscription.id} with connectorToken $connectorToken") repository.removeSubscription(existingSubscription.id) val subscriptionIdsWithInstantStatus = repository.getSubscriptionIdsWithInstantStatus() val subscriberManager = SubscriberManager(app) subscriberManager.refreshService(subscriptionIdsWithInstantStatus) existingSubscription.upAppId?.let { appId -> distributor.sendUnregistered(appId, connectorToken) } } existingSubscription.upAppId?.let { appId -> distributor.sendUnregistered(appId, connectorToken) } // Refresh (and maybe stop) foreground service refreshSubscriberService(app, repository) } } 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" private const val TOPIC_LENGTH = 16 } } Loading
app/src/main/java/io/heckel/ntfy/data/Database.kt +2 −2 Original line number Diff line number Diff line Loading @@ -34,8 +34,8 @@ data class SubscriptionWithMetadata( val topic: String, val instant: Boolean, val mutedUntil: Long, val upAppId: String, val upConnectorToken: String, val upAppId: String?, val upConnectorToken: String?, val totalCount: Int, val newCount: Int, val lastActive: Long Loading
app/src/main/java/io/heckel/ntfy/ui/MainActivity.kt +13 −3 Original line number Diff line number Diff line Loading @@ -283,8 +283,8 @@ class MainActivity : AppCompatActivity(), ActionMode.Callback, AddFragment.Subsc topic = topic, instant = instant, mutedUntil = 0, upAppId = "", upConnectorToken = "", upAppId = null, upConnectorToken = null, totalCount = 0, newCount = 0, lastActive = Date().time/1000 Loading Loading @@ -314,11 +314,21 @@ class MainActivity : AppCompatActivity(), ActionMode.Callback, AddFragment.Subsc private fun onSubscriptionItemClick(subscription: Subscription) { if (actionMode != null) { handleActionModeClick(subscription) } else if (subscription.upAppId != null) { // Not UnifiedPush displayUnifiedPushToast(subscription) } else { startDetailView(subscription) } } private fun displayUnifiedPushToast(subscription: Subscription) { runOnUiThread { val appId = subscription.upAppId ?: "" val toastMessage = getString(R.string.main_unified_push_toast, appId) Toast.makeText(this@MainActivity, toastMessage, Toast.LENGTH_LONG).show() } } private fun onSubscriptionItemLongClick(subscription: Subscription) { if (actionMode == null) { beginActionMode(subscription) Loading Loading @@ -415,7 +425,7 @@ class MainActivity : AppCompatActivity(), ActionMode.Callback, AddFragment.Subsc val dialog = builder .setMessage(R.string.main_action_mode_delete_dialog_message) .setPositiveButton(R.string.main_action_mode_delete_dialog_permanently_delete) { _, _ -> adapter.selected.map { viewModel.remove(it) } adapter.selected.map { subscriptionId -> viewModel.remove(this, subscriptionId) } finishActionMode() } .setNegativeButton(R.string.main_action_mode_delete_dialog_cancel) { _, _ -> Loading
app/src/main/java/io/heckel/ntfy/ui/MainAdapter.kt +6 −4 Original line number Diff line number Diff line Loading @@ -55,7 +55,9 @@ class MainAdapter(private val onClick: (Subscription) -> Unit, private val onLon fun bind(subscription: Subscription) { this.subscription = subscription var statusMessage = if (subscription.totalCount == 1) { var statusMessage = if (subscription.upAppId != null) { context.getString(R.string.main_item_status_unified_push, subscription.upAppId) } else if (subscription.totalCount == 1) { context.getString(R.string.main_item_status_text_one, subscription.totalCount) } else { context.getString(R.string.main_item_status_text_not_one, subscription.totalCount) Loading @@ -82,11 +84,11 @@ class MainAdapter(private val onClick: (Subscription) -> Unit, private val onLon notificationDisabledUntilImageView.visibility = if (subscription.mutedUntil > 1L) View.VISIBLE else View.GONE notificationDisabledForeverImageView.visibility = if (subscription.mutedUntil == 1L) View.VISIBLE else View.GONE instantImageView.visibility = if (subscription.instant) View.VISIBLE else View.GONE if (subscription.newCount > 0) { if (subscription.upAppId != null || subscription.newCount == 0) { newItemsView.visibility = View.GONE } else { newItemsView.visibility = View.VISIBLE newItemsView.text = if (subscription.newCount <= 99) subscription.newCount.toString() else "99+" } else { newItemsView.visibility = View.GONE } itemView.setOnClickListener { onClick(subscription) } itemView.setOnLongClickListener { onLongClick(subscription); true } Loading
app/src/main/java/io/heckel/ntfy/ui/MainViewModel.kt +8 −1 Original line number Diff line number Diff line package io.heckel.ntfy.ui import android.content.Context import androidx.lifecycle.LiveData import androidx.lifecycle.ViewModel import androidx.lifecycle.ViewModelProvider import androidx.lifecycle.viewModelScope import io.heckel.ntfy.data.* import io.heckel.ntfy.up.Distributor import kotlinx.coroutines.Dispatchers import kotlinx.coroutines.launch import kotlin.collections.List Loading @@ -22,7 +24,12 @@ class SubscriptionsViewModel(private val repository: Repository) : ViewModel() { repository.addSubscription(subscription) } fun remove(subscriptionId: Long) = viewModelScope.launch(Dispatchers.IO) { fun remove(context: Context, subscriptionId: Long) = viewModelScope.launch(Dispatchers.IO) { val subscription = repository.getSubscription(subscriptionId) ?: return@launch if (subscription.upAppId != null && subscription.upConnectorToken != null) { val distributor = Distributor(context) distributor.sendUnregistered(subscription.upAppId, subscription.upConnectorToken) } repository.removeAllNotifications(subscriptionId) repository.removeSubscription(subscriptionId) } Loading
app/src/main/java/io/heckel/ntfy/up/BroadcastReceiver.kt +85 −56 Original line number Diff line number Diff line Loading @@ -5,6 +5,7 @@ import android.content.Intent import android.util.Log 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.util.randomString Loading @@ -17,27 +18,43 @@ import kotlin.random.Random class BroadcastReceiver : android.content.BroadcastReceiver() { override fun onReceive(context: Context?, intent: Intent?) { when (intent!!.action) { ACTION_REGISTER -> { val appId = intent.getStringExtra(EXTRA_APPLICATION) ?: "" val connectorToken = intent.getStringExtra(EXTRA_TOKEN) ?: "" Log.d(TAG, "Register: app=$appId, connectorToken=$connectorToken") if (appId.isBlank()) { Log.w(TAG, "Trying to register an app without packageName") if (context == null || intent == null) { return } val baseUrl = context!!.getString(R.string.app_base_url) // FIXME val topic = "up" + randomString(TOPIC_LENGTH) val endpoint = topicUrlUp(baseUrl, topic) val app = context!!.applicationContext as Application when (intent.action) { ACTION_REGISTER -> register(context, intent) ACTION_UNREGISTER -> unregister(context, intent) } } private fun register(context: Context, intent: Intent) { val appId = intent.getStringExtra(EXTRA_APPLICATION) ?: return val connectorToken = intent.getStringExtra(EXTRA_TOKEN) ?: return val app = context.applicationContext as Application val repository = app.repository val distributor = Distributor(app) Log.d(TAG, "REGISTER received for app $appId (connectorToken=$connectorToken)") if (appId.isBlank()) { Log.w(TAG, "Refusing registration: empty application") distributor.sendRegistrationRefused(appId, connectorToken) return } GlobalScope.launch(Dispatchers.IO) { val existingSubscription = repository.getSubscriptionByConnectorToken(connectorToken) if (existingSubscription != null) { if (existingSubscription.upAppId == appId) { val endpoint = topicUrlUp(existingSubscription.baseUrl, existingSubscription.topic) Log.d(TAG, "Subscription with connectorToken $connectorToken exists. Sending endpoint $endpoint.") distributor.sendEndpoint(appId, connectorToken, endpoint) } else { Log.d(TAG, "Subscription with connectorToken $connectorToken exists for a different app. Refusing registration.") distributor.sendRegistrationRefused(appId, connectorToken) } return@launch } val baseUrl = context.getString(R.string.app_base_url) // FIXME val topic = UP_PREFIX + randomString(TOPIC_LENGTH) val endpoint = topicUrlUp(baseUrl, topic) val subscription = Subscription( id = Random.nextLong(), baseUrl = baseUrl, Loading @@ -50,38 +67,50 @@ class BroadcastReceiver : android.content.BroadcastReceiver() { newCount = 0, lastActive = Date().time/1000 ) // Add subscription Log.d(TAG, "Adding subscription with for app $appId (connectorToken $connectorToken): $subscription") repository.addSubscription(subscription) val subscriptionIdsWithInstantStatus = repository.getSubscriptionIdsWithInstantStatus() val subscriberManager = SubscriberManager(app) subscriberManager.refreshService(subscriptionIdsWithInstantStatus) distributor.sendEndpoint(appId, connectorToken, endpoint) // Refresh (and maybe start) foreground service refreshSubscriberService(app, repository) } } ACTION_UNREGISTER -> { val connectorToken = intent.getStringExtra(EXTRA_TOKEN) ?: "" Log.d(TAG, "Unregister: connectorToken=$connectorToken") val app = context!!.applicationContext as Application private fun unregister(context: Context, intent: Intent) { val connectorToken = intent.getStringExtra(EXTRA_TOKEN) ?: return val app = context.applicationContext as Application val repository = app.repository val distributor = Distributor(app) Log.d(TAG, "UNREGISTER received (connectorToken=$connectorToken)") GlobalScope.launch(Dispatchers.IO) { val existingSubscription = repository.getSubscriptionByConnectorToken(connectorToken) if (existingSubscription == null) { Log.d(TAG, "Subscription with connectorToken $connectorToken does not exist. Ignoring.") return@launch } // Remove subscription Log.d(TAG, "Removing subscription ${existingSubscription.id} with connectorToken $connectorToken") repository.removeSubscription(existingSubscription.id) val subscriptionIdsWithInstantStatus = repository.getSubscriptionIdsWithInstantStatus() val subscriberManager = SubscriberManager(app) subscriberManager.refreshService(subscriptionIdsWithInstantStatus) existingSubscription.upAppId?.let { appId -> distributor.sendUnregistered(appId, connectorToken) } } existingSubscription.upAppId?.let { appId -> distributor.sendUnregistered(appId, connectorToken) } // Refresh (and maybe stop) foreground service refreshSubscriberService(app, repository) } } 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" private const val TOPIC_LENGTH = 16 } }