From d10344549f4f5e8aa14deabc4070666cbd8a336e Mon Sep 17 00:00:00 2001 From: Philipp Heckel Date: Sat, 1 Jan 2022 16:56:18 +0100 Subject: [PATCH] Add min priority and broadcast enabled switch, fix #57 --- .../java/io/heckel/ntfy/data/Repository.kt | 29 +++++++++- .../io/heckel/ntfy/msg/BroadcastService.kt | 53 +++++++++++------- .../heckel/ntfy/msg/NotificationDispatcher.kt | 27 +++++++-- .../java/io/heckel/ntfy/ui/MainActivity.kt | 3 +- .../java/io/heckel/ntfy/ui/MainAdapter.kt | 14 +++-- .../io/heckel/ntfy/ui/SettingsActivity.kt | 56 +++++++++++++++++-- app/src/main/java/io/heckel/ntfy/util/Util.kt | 11 ++++ app/src/main/res/values/strings.xml | 28 +++++++--- app/src/main/res/values/values.xml | 17 ++++++ app/src/main/res/xml/main_preferences.xml | 23 ++++++-- 10 files changed, 210 insertions(+), 51 deletions(-) create mode 100644 app/src/main/res/values/values.xml diff --git a/app/src/main/java/io/heckel/ntfy/data/Repository.kt b/app/src/main/java/io/heckel/ntfy/data/Repository.kt index 59b0112..0907ca4 100644 --- a/app/src/main/java/io/heckel/ntfy/data/Repository.kt +++ b/app/src/main/java/io/heckel/ntfy/data/Repository.kt @@ -143,9 +143,34 @@ class Repository(private val sharedPrefs: SharedPreferences, private val subscri .apply() } + fun setMinPriority(minPriority: Int) { + if (minPriority <= 1) { + sharedPrefs.edit() + .remove(SHARED_PREFS_MIN_PRIORITY) + .apply() + } else { + sharedPrefs.edit() + .putInt(SHARED_PREFS_MIN_PRIORITY, minPriority) + .apply() + } + } + + fun getMinPriority(): Int { + return sharedPrefs.getInt(SHARED_PREFS_MIN_PRIORITY, 1) // 1/low means all priorities + } + + fun getBroadcastEnabled(): Boolean { + return sharedPrefs.getBoolean(SHARED_PREFS_BROADCAST_ENABLED, true) // Enabled by default + } + + fun setBroadcastEnabled(enabled: Boolean) { + sharedPrefs.edit() + .putBoolean(SHARED_PREFS_BROADCAST_ENABLED, enabled) + .apply() + } fun getUnifiedPushEnabled(): Boolean { - return sharedPrefs.getBoolean(SHARED_PREFS_UNIFIED_PUSH_ENABLED, true) // Enabled by default! + return sharedPrefs.getBoolean(SHARED_PREFS_UNIFIED_PUSH_ENABLED, true) // Enabled by default } fun setUnifiedPushEnabled(enabled: Boolean) { @@ -263,6 +288,8 @@ class Repository(private val sharedPrefs: SharedPreferences, private val subscri const val SHARED_PREFS_POLL_WORKER_VERSION = "PollWorkerVersion" const val SHARED_PREFS_AUTO_RESTART_WORKER_VERSION = "AutoRestartWorkerVersion" const val SHARED_PREFS_MUTED_UNTIL_TIMESTAMP = "MutedUntil" + const val SHARED_PREFS_MIN_PRIORITY = "MinPriority" + const val SHARED_PREFS_BROADCAST_ENABLED = "BroadcastEnabled" const val SHARED_PREFS_UNIFIED_PUSH_ENABLED = "UnifiedPushEnabled" const val SHARED_PREFS_UNIFIED_PUSH_BASE_URL = "UnifiedPushBaseURL" diff --git a/app/src/main/java/io/heckel/ntfy/msg/BroadcastService.kt b/app/src/main/java/io/heckel/ntfy/msg/BroadcastService.kt index bc32d28..9d4e43f 100644 --- a/app/src/main/java/io/heckel/ntfy/msg/BroadcastService.kt +++ b/app/src/main/java/io/heckel/ntfy/msg/BroadcastService.kt @@ -13,8 +13,8 @@ import kotlinx.coroutines.GlobalScope import kotlinx.coroutines.launch /** - * The broadcast service is responsible for sending and receiving broadcasted intents - * in order to facilitate taks app integrations. + * The broadcast service is responsible for sending and receiving broadcast intents + * in order to facilitate tasks app integrations. */ class BroadcastService(private val ctx: Context) { fun send(subscription: Subscription, notification: Notification, muted: Boolean) { @@ -36,6 +36,10 @@ class BroadcastService(private val ctx: Context) { ctx.sendBroadcast(intent) } + /** + * This receiver is triggered when the SEND_MESSAGE intent is received. + * See AndroidManifest.xml for details. + */ class BroadcastReceiver : android.content.BroadcastReceiver() { override fun onReceive(context: Context, intent: Intent) { Log.d(TAG, "Broadcast received: $intent") @@ -46,24 +50,20 @@ class BroadcastService(private val ctx: Context) { private fun send(ctx: Context, intent: Intent) { val api = ApiService() - val baseUrl = intent.getStringExtra("base_url") ?: ctx.getString(R.string.app_base_url) - val topic = intent.getStringExtra("topic") ?: return - val message = intent.getStringExtra("message") ?: return - val title = intent.getStringExtra("title") ?: "" - val tags = intent.getStringExtra("tags") ?: "" - val priority = if (intent.getStringExtra("priority") != null) { - when (intent.getStringExtra("priority")) { - "min", "1" -> 1 - "low", "2" -> 2 - "default", "3" -> 3 - "high", "4" -> 4 - "urgent", "max", "5" -> 5 - else -> 0 - } - } else { - intent.getIntExtra("priority", 0) + val baseUrl = getStringExtra(intent, "base_url") ?: ctx.getString(R.string.app_base_url) + val topic = getStringExtra(intent, "topic") ?: return + val message = getStringExtra(intent, "message") ?: return + val title = getStringExtra(intent, "title") ?: "" + val tags = getStringExtra(intent,"tags") ?: "" + val priority = when (getStringExtra(intent, "priority")) { + "min", "1" -> 1 + "low", "2" -> 2 + "default", "3" -> 3 + "high", "4" -> 4 + "urgent", "max", "5" -> 5 + else -> 0 } - val delay = intent.getStringExtra("delay") ?: "" + val delay = getStringExtra(intent,"delay") ?: "" GlobalScope.launch(Dispatchers.IO) { api.publish( baseUrl = baseUrl, @@ -76,11 +76,26 @@ class BroadcastService(private val ctx: Context) { ) } } + + /** + * Gets an extra as a String value, even if the extra may be an int or a long. + */ + private fun getStringExtra(intent: Intent, name: String): String? { + if (intent.getStringExtra(name) != null) { + return intent.getStringExtra(name) + } else if (intent.getIntExtra(name, DOES_NOT_EXIST) != DOES_NOT_EXIST) { + return intent.getIntExtra(name, DOES_NOT_EXIST).toString() + } else if (intent.getLongExtra(name, DOES_NOT_EXIST.toLong()) != DOES_NOT_EXIST.toLong()) { + return intent.getLongExtra(name, DOES_NOT_EXIST.toLong()).toString() + } + return null + } } companion object { private const val TAG = "NtfyBroadcastService" private const val MESSAGE_RECEIVED_ACTION = "io.heckel.ntfy.MESSAGE_RECEIVED" private const val MESSAGE_SEND_ACTION = "io.heckel.ntfy.SEND_MESSAGE" // If changed, change in manifest too! + private const val DOES_NOT_EXIST = -2586000 } } 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 f2c13d0..c4cec51 100644 --- a/app/src/main/java/io/heckel/ntfy/msg/NotificationDispatcher.kt +++ b/app/src/main/java/io/heckel/ntfy/msg/NotificationDispatcher.kt @@ -21,10 +21,10 @@ class NotificationDispatcher(val context: Context, val repository: Repository) { } fun dispatch(subscription: Subscription, notification: Notification) { - val muted = checkMuted(subscription) - val notify = checkNotify(subscription, notification, muted) - val broadcast = subscription.upAppId == null // Never broadcast for UnifiedPush - val distribute = subscription.upAppId != null // Only distribute for UnifiedPush subscriptions + val muted = getMuted(subscription) + val notify = shouldNotify(subscription, notification, muted) + val broadcast = shouldBroadcast(subscription) + val distribute = shouldDistribute(subscription) if (notify) { notifier.send(subscription, notification) } @@ -38,15 +38,30 @@ class NotificationDispatcher(val context: Context, val repository: Repository) { } } - private fun checkNotify(subscription: Subscription, notification: Notification, muted: Boolean): Boolean { + private fun shouldNotify(subscription: Subscription, notification: Notification, muted: Boolean): Boolean { if (subscription.upAppId != null) { return false } + val priority = if (notification.priority > 0) notification.priority else 3 + if (priority < repository.getMinPriority()) { + return false + } val detailsVisible = repository.detailViewSubscriptionId.get() == notification.subscriptionId return !detailsVisible && !muted } - private fun checkMuted(subscription: Subscription): Boolean { + private fun shouldBroadcast(subscription: Subscription): Boolean { + if (subscription.upAppId != null) { // Never broadcast for UnifiedPush subscriptions + return false + } + return repository.getBroadcastEnabled() + } + + private fun shouldDistribute(subscription: Subscription): Boolean { + return subscription.upAppId != null // Only distribute for UnifiedPush subscriptions + } + + private fun getMuted(subscription: Subscription): Boolean { if (repository.isGlobalMuted()) { return true } 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 b571331..241c2e2 100644 --- a/app/src/main/java/io/heckel/ntfy/ui/MainActivity.kt +++ b/app/src/main/java/io/heckel/ntfy/ui/MainActivity.kt @@ -87,7 +87,7 @@ class MainActivity : AppCompatActivity(), ActionMode.Callback, AddFragment.Subsc val onSubscriptionLongClick = { s: Subscription -> onSubscriptionItemLongClick(s) } mainList = findViewById(R.id.main_subscriptions_list) - adapter = MainAdapter(onSubscriptionClick, onSubscriptionLongClick) + adapter = MainAdapter(repository, onSubscriptionClick, onSubscriptionLongClick) mainList.adapter = adapter viewModel.list().observe(this) { @@ -261,6 +261,7 @@ class MainActivity : AppCompatActivity(), ActionMode.Callback, AddFragment.Subsc repository.setGlobalMutedUntil(mutedUntilTimestamp) showHideNotificationMenuItems() runOnUiThread { + redrawList() // Update the "muted until" icons when (mutedUntilTimestamp) { 0L -> Toast.makeText(this@MainActivity, getString(R.string.notification_dialog_enabled_toast_message), Toast.LENGTH_LONG).show() 1L -> Toast.makeText(this@MainActivity, getString(R.string.notification_dialog_muted_forever_toast_message), Toast.LENGTH_LONG).show() 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 dfbc016..6a361ed 100644 --- a/app/src/main/java/io/heckel/ntfy/ui/MainAdapter.kt +++ b/app/src/main/java/io/heckel/ntfy/ui/MainAdapter.kt @@ -10,12 +10,13 @@ import androidx.recyclerview.widget.ListAdapter import androidx.recyclerview.widget.RecyclerView import io.heckel.ntfy.R import io.heckel.ntfy.data.ConnectionState +import io.heckel.ntfy.data.Repository import io.heckel.ntfy.data.Subscription import io.heckel.ntfy.util.topicShortUrl import java.text.DateFormat import java.util.* -class MainAdapter(private val onClick: (Subscription) -> Unit, private val onLongClick: (Subscription) -> Unit) : +class MainAdapter(private val repository: Repository, private val onClick: (Subscription) -> Unit, private val onLongClick: (Subscription) -> Unit) : ListAdapter(TopicDiffCallback) { val selected = mutableSetOf() // Subscription IDs @@ -23,7 +24,7 @@ class MainAdapter(private val onClick: (Subscription) -> Unit, private val onLon override fun onCreateViewHolder(parent: ViewGroup, viewType: Int): SubscriptionViewHolder { val view = LayoutInflater.from(parent.context) .inflate(R.layout.fragment_main_item, parent, false) - return SubscriptionViewHolder(view, selected, onClick, onLongClick) + return SubscriptionViewHolder(view, repository, selected, onClick, onLongClick) } /* Gets current topic and uses it to bind view. */ @@ -41,7 +42,7 @@ class MainAdapter(private val onClick: (Subscription) -> Unit, private val onLon } /* ViewHolder for Topic, takes in the inflated view and the onClick behavior. */ - class SubscriptionViewHolder(itemView: View, private val selected: Set, val onClick: (Subscription) -> Unit, val onLongClick: (Subscription) -> Unit) : + class SubscriptionViewHolder(itemView: View, private val repository: Repository, private val selected: Set, val onClick: (Subscription) -> Unit, val onLongClick: (Subscription) -> Unit) : RecyclerView.ViewHolder(itemView) { private var subscription: Subscription? = null private val context: Context = itemView.context @@ -78,11 +79,14 @@ class MainAdapter(private val onClick: (Subscription) -> Unit, private val onLon } else { dateStr } + val globalMutedUntil = repository.getGlobalMutedUntil() + val showMutedForeverIcon = (subscription.mutedUntil == 1L || globalMutedUntil == 1L) && subscription.upAppId == null + val showMutedUntilIcon = !showMutedForeverIcon && (subscription.mutedUntil > 1L || globalMutedUntil > 1L) && subscription.upAppId == null nameView.text = topicShortUrl(subscription.baseUrl, subscription.topic) statusView.text = statusMessage dateView.text = dateText - notificationDisabledUntilImageView.visibility = if (subscription.mutedUntil > 1L) View.VISIBLE else View.GONE - notificationDisabledForeverImageView.visibility = if (subscription.mutedUntil == 1L) View.VISIBLE else View.GONE + notificationDisabledUntilImageView.visibility = if (showMutedUntilIcon) View.VISIBLE else View.GONE + notificationDisabledForeverImageView.visibility = if (showMutedForeverIcon) View.VISIBLE else View.GONE instantImageView.visibility = if (subscription.instant) View.VISIBLE else View.GONE if (subscription.upAppId != null || subscription.newCount == 0) { newItemsView.visibility = View.GONE diff --git a/app/src/main/java/io/heckel/ntfy/ui/SettingsActivity.kt b/app/src/main/java/io/heckel/ntfy/ui/SettingsActivity.kt index c1a709d..5a60915 100644 --- a/app/src/main/java/io/heckel/ntfy/ui/SettingsActivity.kt +++ b/app/src/main/java/io/heckel/ntfy/ui/SettingsActivity.kt @@ -16,6 +16,7 @@ import io.heckel.ntfy.R import io.heckel.ntfy.app.Application import io.heckel.ntfy.data.Repository import io.heckel.ntfy.util.formatDateShort +import io.heckel.ntfy.util.toPriorityString class SettingsActivity : AppCompatActivity() { private val repository by lazy { (application as Application).repository } @@ -49,7 +50,7 @@ class SettingsActivity : AppCompatActivity() { // everybody has access to the repository. // Notifications muted until (global) - val mutedUntilPrefId = context?.getString(R.string.pref_notifications_muted_until) ?: return + val mutedUntilPrefId = context?.getString(R.string.settings_notifications_muted_until_key) ?: return val mutedUntilSummary = { s: Long -> when (s) { 0L -> getString(R.string.settings_notifications_muted_until_enabled) @@ -80,8 +81,53 @@ class SettingsActivity : AppCompatActivity() { true } - // UnifiedPush Enabled - val upEnabledPrefId = context?.getString(R.string.pref_unified_push_enabled) ?: return + // Minimum priority + val minPriorityPrefId = context?.getString(R.string.settings_notifications_min_priority_key) ?: return + val minPriority: ListPreference? = findPreference(minPriorityPrefId) + minPriority?.value = repository.getMinPriority().toString() + minPriority?.preferenceDataStore = object : PreferenceDataStore() { + override fun putString(key: String?, value: String?) { + val minPriorityValue = value?.toIntOrNull() ?:return + repository.setMinPriority(minPriorityValue) + } + override fun getString(key: String?, defValue: String?): String { + return repository.getMinPriority().toString() + } + } + minPriority?.summaryProvider = Preference.SummaryProvider { pref -> + val minPriorityValue = pref.value.toIntOrNull() ?: 1 // 1/low means all priorities + when (minPriorityValue) { + 1 -> getString(R.string.settings_notifications_min_priority_summary_any) + 5 -> getString(R.string.settings_notifications_min_priority_summary_max) + else -> { + val minPriorityString = toPriorityString(minPriorityValue) + getString(R.string.settings_notifications_min_priority_summary_x_or_higher, minPriorityValue, minPriorityString) + } + } + } + + // Broadcast enabled + val broadcastEnabledPrefId = context?.getString(R.string.settings_advanced_broadcast_key) ?: return + val broadcastEnabled: SwitchPreference? = findPreference(broadcastEnabledPrefId) + broadcastEnabled?.isChecked = repository.getBroadcastEnabled() + broadcastEnabled?.preferenceDataStore = object : PreferenceDataStore() { + override fun putBoolean(key: String?, value: Boolean) { + repository.setBroadcastEnabled(value) + } + override fun getBoolean(key: String?, defValue: Boolean): Boolean { + return repository.getBroadcastEnabled() + } + } + broadcastEnabled?.summaryProvider = Preference.SummaryProvider { pref -> + if (pref.isChecked) { + getString(R.string.settings_advanced_broadcast_summary_enabled) + } else { + getString(R.string.settings_advanced_broadcast_summary_disabled) + } + } + + // UnifiedPush enabled + val upEnabledPrefId = context?.getString(R.string.settings_unified_push_enabled_key) ?: return val upEnabled: SwitchPreference? = findPreference(upEnabledPrefId) upEnabled?.isChecked = repository.getUnifiedPushEnabled() upEnabled?.preferenceDataStore = object : PreferenceDataStore() { @@ -102,7 +148,7 @@ class SettingsActivity : AppCompatActivity() { // UnifiedPush Base URL val appBaseUrl = context?.getString(R.string.app_base_url) ?: return - val upBaseUrlPrefId = context?.getString(R.string.pref_unified_push_base_url) ?: return + val upBaseUrlPrefId = context?.getString(R.string.settings_unified_push_base_url_key) ?: return val upBaseUrl: EditTextPreference? = findPreference(upBaseUrlPrefId) upBaseUrl?.text = repository.getUnifiedPushBaseUrl() ?: "" upBaseUrl?.preferenceDataStore = object : PreferenceDataStore() { @@ -123,7 +169,7 @@ class SettingsActivity : AppCompatActivity() { } // Version - val versionPrefId = context?.getString(R.string.pref_version) ?: return + val versionPrefId = context?.getString(R.string.settings_about_version_key) ?: return val versionPref: Preference? = findPreference(versionPrefId) val version = getString(R.string.settings_about_version_format, BuildConfig.VERSION_NAME, BuildConfig.FLAVOR) versionPref?.summary = version diff --git a/app/src/main/java/io/heckel/ntfy/util/Util.kt b/app/src/main/java/io/heckel/ntfy/util/Util.kt index 23c3747..50fb66c 100644 --- a/app/src/main/java/io/heckel/ntfy/util/Util.kt +++ b/app/src/main/java/io/heckel/ntfy/util/Util.kt @@ -28,6 +28,17 @@ fun toPriority(priority: Int?): Int { else return 3 } +fun toPriorityString(priority: Int): String { + return when (priority) { + 1 -> "min" + 2 -> "low" + 3 -> "default" + 4 -> "high" + 5 -> "max" + else -> "default" + } +} + fun joinTags(tags: List?): String { return tags?.joinToString(",") ?: "" } diff --git a/app/src/main/res/values/strings.xml b/app/src/main/res/values/strings.xml index 4d97b73..8260b34 100644 --- a/app/src/main/res/values/strings.xml +++ b/app/src/main/res/values/strings.xml @@ -32,7 +32,7 @@ Notifications disabled until %1$s Settings Report a bug - https://heckel.io/ntfy-android + https://github.com/binwiederhier/ntfy/issues Visit ntfy.sh @@ -147,26 +147,38 @@ Settings Notifications - General settings for all subscribed topics + MutedUntil Pause notifications All notifications will be displayed Notifications muted until re-enabled Notifications muted until %1$s + MinPriority + Minimum priority + Notifications of all priorities are shown + Show notifications if priority is %1$d (%2$s) or higher + Show notifications if priority is 5 (max) + Any priority + Low priority and higher + Default priority and higher + High priority and higher + Only max priority UnifiedPush Allows other apps to use ntfy as a message distributor. Find out more at unifiedpush.org. + UnifiedPushEnabled Enable distributor Apps can use ntfy as distributor Apps cannot use ntfy as distributor + UnifiedPushBaseURL Server URL %1$s (default) + Advanced + BroadcastEnabled + Broadcast messages + Apps can receive incoming notifications as broadcasts + Apps cannot receive notifications as broadcasts About + Version Version ntfy %1$s (%2$s) Copied to clipboard - - - MutedUntil - UnifiedPushEnabled - UnifiedPushBaseURL - Version diff --git a/app/src/main/res/values/values.xml b/app/src/main/res/values/values.xml new file mode 100644 index 0000000..4dfb50c --- /dev/null +++ b/app/src/main/res/values/values.xml @@ -0,0 +1,17 @@ + + + + @string/settings_notifications_min_priority_min + @string/settings_notifications_min_priority_low + @string/settings_notifications_min_priority_default + @string/settings_notifications_min_priority_high + @string/settings_notifications_min_priority_max + + + 1 + 2 + 3 + 4 + 5 + + diff --git a/app/src/main/res/xml/main_preferences.xml b/app/src/main/res/xml/main_preferences.xml index c404af4..5295d26 100644 --- a/app/src/main/res/xml/main_preferences.xml +++ b/app/src/main/res/xml/main_preferences.xml @@ -3,28 +3,39 @@ app:title="@string/settings_title"> + + app:dependency="@string/settings_unified_push_enabled_key"/> + + +