From 8fd7bb5639e255f4559efbc48bdaa8e9f2b3c99d Mon Sep 17 00:00:00 2001 From: Philipp Heckel Date: Sat, 27 Nov 2021 16:18:09 -0500 Subject: [PATCH] Priorities, titles and tags --- app/build.gradle | 3 + .../io.heckel.ntfy.data.Database/4.json | 138 ++++++++++++++++++ app/src/main/AndroidManifest.xml | 1 + .../main/java/io/heckel/ntfy/data/Database.kt | 20 ++- app/src/main/java/io/heckel/ntfy/data/Util.kt | 10 -- .../java/io/heckel/ntfy/msg/ApiService.kt | 41 +++++- .../io/heckel/ntfy/msg/NotificationService.kt | 61 ++++++-- .../heckel/ntfy/msg/SubscriberConnection.kt | 2 +- .../io/heckel/ntfy/msg/SubscriberService.kt | 2 +- .../java/io/heckel/ntfy/ui/AddFragment.kt | 4 +- .../java/io/heckel/ntfy/ui/DetailActivity.kt | 14 +- .../java/io/heckel/ntfy/ui/DetailAdapter.kt | 39 ++++- .../java/io/heckel/ntfy/ui/MainActivity.kt | 4 +- .../java/io/heckel/ntfy/ui/MainAdapter.kt | 3 +- app/src/main/java/io/heckel/ntfy/ui/Util.kt | 22 --- app/src/main/java/io/heckel/ntfy/util/Util.kt | 95 ++++++++++++ .../main/res/drawable/ic_priority_1_24dp.xml | 26 ++++ .../main/res/drawable/ic_priority_2_24dp.xml | 19 +++ .../main/res/drawable/ic_priority_4_24dp.xml | 20 +++ .../main/res/drawable/ic_priority_5_24dp.xml | 26 ++++ .../main/res/layout/fragment_detail_item.xml | 44 ++++-- app/src/main/res/values/colors.xml | 3 + app/src/main/res/values/strings.xml | 9 +- .../heckel/ntfy/firebase/FirebaseService.kt | 9 ++ assets/arrow_drop_down_black_24dp.svg | 1 + assets/priority_1_24dp.svg | 47 ++++++ assets/priority_2_24dp.svg | 43 ++++++ assets/priority_4_24dp.svg | 43 ++++++ assets/priority_4_alt_24dp.svg | 39 +++++ assets/priority_5_24dp.svg | 39 +++++ assets/priority_5_alt2_24dp.svg | 47 ++++++ assets/priority_5_alt_24dp.svg | 47 ++++++ assets/priority_high_black_24dp.svg | 1 + assets/priority_high_circle_red_24dp.svg | 44 ++++++ assets/report_black_24dp.svg | 1 + assets/warning_black_24dp.svg | 1 + 36 files changed, 890 insertions(+), 78 deletions(-) create mode 100644 app/schemas/io.heckel.ntfy.data.Database/4.json delete mode 100644 app/src/main/java/io/heckel/ntfy/data/Util.kt delete mode 100644 app/src/main/java/io/heckel/ntfy/ui/Util.kt create mode 100644 app/src/main/java/io/heckel/ntfy/util/Util.kt create mode 100644 app/src/main/res/drawable/ic_priority_1_24dp.xml create mode 100644 app/src/main/res/drawable/ic_priority_2_24dp.xml create mode 100644 app/src/main/res/drawable/ic_priority_4_24dp.xml create mode 100644 app/src/main/res/drawable/ic_priority_5_24dp.xml create mode 100644 assets/arrow_drop_down_black_24dp.svg create mode 100644 assets/priority_1_24dp.svg create mode 100644 assets/priority_2_24dp.svg create mode 100644 assets/priority_4_24dp.svg create mode 100644 assets/priority_4_alt_24dp.svg create mode 100644 assets/priority_5_24dp.svg create mode 100644 assets/priority_5_alt2_24dp.svg create mode 100644 assets/priority_5_alt_24dp.svg create mode 100644 assets/priority_high_black_24dp.svg create mode 100644 assets/priority_high_circle_red_24dp.svg create mode 100644 assets/report_black_24dp.svg create mode 100644 assets/warning_black_24dp.svg diff --git a/app/build.gradle b/app/build.gradle index bbe79de..bde4f87 100644 --- a/app/build.gradle +++ b/app/build.gradle @@ -91,4 +91,7 @@ dependencies { // LiveData implementation "androidx.lifecycle:lifecycle-livedata-ktx:$rootProject.liveDataVersion" implementation 'androidx.legacy:legacy-support-v4:1.0.0' + + // Emojis (tags and such) + implementation 'com.vdurmont:emoji-java:5.1.1' } diff --git a/app/schemas/io.heckel.ntfy.data.Database/4.json b/app/schemas/io.heckel.ntfy.data.Database/4.json new file mode 100644 index 0000000..eefb0af --- /dev/null +++ b/app/schemas/io.heckel.ntfy.data.Database/4.json @@ -0,0 +1,138 @@ +{ + "formatVersion": 1, + "database": { + "version": 4, + "identityHash": "06bd845a8d39dd10549f1aeb6b40d7c5", + "entities": [ + { + "tableName": "Subscription", + "createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`id` INTEGER NOT NULL, `baseUrl` TEXT NOT NULL, `topic` TEXT NOT NULL, `instant` INTEGER NOT NULL, `mutedUntil` INTEGER NOT NULL, PRIMARY KEY(`id`))", + "fields": [ + { + "fieldPath": "id", + "columnName": "id", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "baseUrl", + "columnName": "baseUrl", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "topic", + "columnName": "topic", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "instant", + "columnName": "instant", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "mutedUntil", + "columnName": "mutedUntil", + "affinity": "INTEGER", + "notNull": true + } + ], + "primaryKey": { + "columnNames": [ + "id" + ], + "autoGenerate": false + }, + "indices": [ + { + "name": "index_Subscription_baseUrl_topic", + "unique": true, + "columnNames": [ + "baseUrl", + "topic" + ], + "createSql": "CREATE UNIQUE INDEX IF NOT EXISTS `index_Subscription_baseUrl_topic` ON `${TABLE_NAME}` (`baseUrl`, `topic`)" + } + ], + "foreignKeys": [] + }, + { + "tableName": "Notification", + "createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`id` TEXT NOT NULL, `subscriptionId` INTEGER NOT NULL, `timestamp` INTEGER NOT NULL, `title` TEXT NOT NULL, `message` TEXT NOT NULL, `notificationId` INTEGER NOT NULL, `priority` INTEGER NOT NULL DEFAULT 3, `tags` TEXT NOT NULL, `deleted` INTEGER NOT NULL, PRIMARY KEY(`id`, `subscriptionId`))", + "fields": [ + { + "fieldPath": "id", + "columnName": "id", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "subscriptionId", + "columnName": "subscriptionId", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "timestamp", + "columnName": "timestamp", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "title", + "columnName": "title", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "message", + "columnName": "message", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "notificationId", + "columnName": "notificationId", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "priority", + "columnName": "priority", + "affinity": "INTEGER", + "notNull": true, + "defaultValue": "3" + }, + { + "fieldPath": "tags", + "columnName": "tags", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "deleted", + "columnName": "deleted", + "affinity": "INTEGER", + "notNull": true + } + ], + "primaryKey": { + "columnNames": [ + "id", + "subscriptionId" + ], + "autoGenerate": false + }, + "indices": [], + "foreignKeys": [] + } + ], + "views": [], + "setupQueries": [ + "CREATE TABLE IF NOT EXISTS room_master_table (id INTEGER PRIMARY KEY,identity_hash TEXT)", + "INSERT OR REPLACE INTO room_master_table (id,identity_hash) VALUES(42, '06bd845a8d39dd10549f1aeb6b40d7c5')" + ] + } +} \ No newline at end of file diff --git a/app/src/main/AndroidManifest.xml b/app/src/main/AndroidManifest.xml index d3c2351..9210db0 100644 --- a/app/src/main/AndroidManifest.xml +++ b/app/src/main/AndroidManifest.xml @@ -11,6 +11,7 @@ + ) { val url = topicUrl(baseUrl, topic) Log.d(TAG, "Publishing to $url") - val request = Request.Builder().url(url).put(message.toRequestBody()).build(); - client.newCall(request).execute().use { response -> + var builder = Request.Builder() + .url(url) + .addHeader("X-Priority", priority.toString()) + .put(message.toRequestBody()) + if (tags.isNotEmpty()) { + builder = builder.addHeader("X-Tags", tags.joinToString(",")) + } + if (title.isNotEmpty()) { + builder = builder.addHeader("X-Title", title) + } + client.newCall(builder.build()).execute().use { response -> if (!response.isSuccessful) { throw Exception("Unexpected response ${response.code} when publishing to $url") } @@ -87,7 +98,10 @@ class ApiService { id = message.id, subscriptionId = 0, // TO BE SET downstream timestamp = message.time, + title = message.title ?: "", message = message.message, + priority = toPriority(message.priority), + tags = joinTags(message.tags), notificationId = Random.nextInt(), deleted = false ) @@ -109,7 +123,17 @@ class ApiService { private fun fromString(subscriptionId: Long, s: String): Notification { val message = gson.fromJson(s, Message::class.java) - return Notification(message.id, subscriptionId, message.time, message.message, notificationId = 0, deleted = false) + return Notification( + id = message.id, + subscriptionId = subscriptionId, + timestamp = message.time, + title = message.title ?: "", + message = message.message, + priority = toPriority(message.priority), + tags = joinTags(message.tags), + notificationId = 0, + deleted = false + ) } /* This annotation ensures that proguard still works in production builds, @@ -120,6 +144,9 @@ class ApiService { val time: Long, val event: String, val topic: String, + val priority: Int?, + val tags: List?, + val title: String?, val message: String ) diff --git a/app/src/main/java/io/heckel/ntfy/msg/NotificationService.kt b/app/src/main/java/io/heckel/ntfy/msg/NotificationService.kt index 0eb3882..57409b4 100644 --- a/app/src/main/java/io/heckel/ntfy/msg/NotificationService.kt +++ b/app/src/main/java/io/heckel/ntfy/msg/NotificationService.kt @@ -6,23 +6,25 @@ import android.app.PendingIntent import android.app.TaskStackBuilder import android.content.Context import android.content.Intent +import android.graphics.Color import android.media.RingtoneManager import android.os.Build import android.util.Log +import androidx.annotation.RequiresApi import androidx.core.app.NotificationCompat import androidx.core.content.ContextCompat import io.heckel.ntfy.R import io.heckel.ntfy.data.Notification import io.heckel.ntfy.data.Subscription -import io.heckel.ntfy.data.topicShortUrl +import io.heckel.ntfy.util.topicShortUrl import io.heckel.ntfy.ui.DetailActivity import io.heckel.ntfy.ui.MainActivity -import kotlin.random.Random +import io.heckel.ntfy.util.formatMessage +import io.heckel.ntfy.util.formatTitle class NotificationService(val context: Context) { fun send(subscription: Subscription, notification: Notification) { - val title = topicShortUrl(subscription.baseUrl, subscription.topic) - Log.d(TAG, "Displaying notification $title: ${notification.message}") + Log.d(TAG, "Displaying notification $notification") // Create an Intent for the activity you want to start val intent = Intent(context, DetailActivity::class.java) @@ -36,22 +38,33 @@ class NotificationService(val context: Context) { getPendingIntent(0, PendingIntent.FLAG_UPDATE_CURRENT) // Get the PendingIntent containing the entire back stack } + val title = formatTitle(subscription, notification) + val message = formatMessage(notification) val defaultSoundUri = RingtoneManager.getDefaultUri(RingtoneManager.TYPE_NOTIFICATION) - val notificationBuilder = NotificationCompat.Builder(context, CHANNEL_ID) + val channelId = toChannelId(notification.priority) + var notificationBuilder = NotificationCompat.Builder(context, channelId) .setSmallIcon(R.drawable.ic_notification) .setColor(ContextCompat.getColor(context, R.color.primaryColor)) .setContentTitle(title) - .setContentText(notification.message) - .setStyle(NotificationCompat.BigTextStyle().bigText(notification.message)) + .setContentText(message) + .setStyle(NotificationCompat.BigTextStyle().bigText(message)) .setSound(defaultSoundUri) .setContentIntent(pendingIntent) // Click target for notification .setAutoCancel(true) // Cancel when notification is clicked + if (notification.priority == 4) { + notificationBuilder = notificationBuilder + .setVibrate(longArrayOf(500, 500, 500, 500, 500, 500)) + .setLights(Color.YELLOW, 3000, 3000) + } else if (notification.priority == 5) { + notificationBuilder = notificationBuilder + .setVibrate(longArrayOf(1000, 500, 1000, 500, 1000, 500)) + .setLights(Color.RED, 3000, 3000) + } + val notificationManager = context.getSystemService(Context.NOTIFICATION_SERVICE) as NotificationManager if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O) { - val channelName = context.getString(R.string.channel_notifications_name) // Show's up in UI - val channel = NotificationChannel(CHANNEL_ID, channelName, NotificationManager.IMPORTANCE_DEFAULT) - notificationManager.createNotificationChannel(channel) + createNotificationChannel(notificationManager, notification) } notificationManager.notify(notification.notificationId, notificationBuilder.build()) } @@ -64,8 +77,34 @@ class NotificationService(val context: Context) { } } + @RequiresApi(Build.VERSION_CODES.O) + private fun createNotificationChannel(notificationManager: NotificationManager, notification: Notification) { + val channel = when (notification.priority) { + 1 -> NotificationChannel(CHANNEL_ID_MIN, context.getString(R.string.channel_notifications_min_name), NotificationManager.IMPORTANCE_MIN) + 2 -> NotificationChannel(CHANNEL_ID_LOW, context.getString(R.string.channel_notifications_low_name), NotificationManager.IMPORTANCE_LOW) + 4 -> NotificationChannel(CHANNEL_ID_HIGH, context.getString(R.string.channel_notifications_high_name), NotificationManager.IMPORTANCE_HIGH) + 5 -> NotificationChannel(CHANNEL_ID_MAX, context.getString(R.string.channel_notifications_max_name), NotificationManager.IMPORTANCE_MAX) + else -> NotificationChannel(CHANNEL_ID_DEFAULT, context.getString(R.string.channel_notifications_default_name), NotificationManager.IMPORTANCE_DEFAULT) + } + notificationManager.createNotificationChannel(channel) + } + + private fun toChannelId(priority: Int): String { + return when (priority) { + 1 -> CHANNEL_ID_MIN + 2 -> CHANNEL_ID_LOW + 4 -> CHANNEL_ID_HIGH + 5 -> CHANNEL_ID_MAX + else -> CHANNEL_ID_DEFAULT + } + } + companion object { private const val TAG = "NtfyNotificationService" - private const val CHANNEL_ID = "ntfy" + private const val CHANNEL_ID_MIN = "ntfy-min" + private const val CHANNEL_ID_LOW = "ntfy-low" + private const val CHANNEL_ID_DEFAULT = "ntfy" + private const val CHANNEL_ID_HIGH = "ntfy-high" + private const val CHANNEL_ID_MAX = "ntfy-max" } } diff --git a/app/src/main/java/io/heckel/ntfy/msg/SubscriberConnection.kt b/app/src/main/java/io/heckel/ntfy/msg/SubscriberConnection.kt index 6e4e98f..e26cd68 100644 --- a/app/src/main/java/io/heckel/ntfy/msg/SubscriberConnection.kt +++ b/app/src/main/java/io/heckel/ntfy/msg/SubscriberConnection.kt @@ -4,7 +4,7 @@ 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.data.topicUrl +import io.heckel.ntfy.util.topicUrl import kotlinx.coroutines.* import okhttp3.Call import java.util.concurrent.atomic.AtomicBoolean 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 a9f574f..07b19e3 100644 --- a/app/src/main/java/io/heckel/ntfy/msg/SubscriberService.kt +++ b/app/src/main/java/io/heckel/ntfy/msg/SubscriberService.kt @@ -15,7 +15,7 @@ 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.data.topicUrl +import io.heckel.ntfy.util.topicUrl import io.heckel.ntfy.ui.MainActivity import kotlinx.coroutines.* import java.util.concurrent.ConcurrentHashMap diff --git a/app/src/main/java/io/heckel/ntfy/ui/AddFragment.kt b/app/src/main/java/io/heckel/ntfy/ui/AddFragment.kt index 0472144..36f80db 100644 --- a/app/src/main/java/io/heckel/ntfy/ui/AddFragment.kt +++ b/app/src/main/java/io/heckel/ntfy/ui/AddFragment.kt @@ -121,8 +121,10 @@ class AddFragment : DialogFragment() { if (baseUrls.count() == 1) { baseUrlLayout.setEndIconDrawable(R.drawable.ic_cancel_gray_24dp) baseUrlText.setText(baseUrls.first()) - } else { + } else if (baseUrls.count() > 1) { baseUrlLayout.setEndIconDrawable(R.drawable.ic_drop_down_gray_24dp) + } else { + baseUrlLayout.setEndIconDrawable(0) } } } 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 83baac0..53dffdd 100644 --- a/app/src/main/java/io/heckel/ntfy/ui/DetailActivity.kt +++ b/app/src/main/java/io/heckel/ntfy/ui/DetailActivity.kt @@ -27,11 +27,13 @@ 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.data.topicShortUrl -import io.heckel.ntfy.data.topicUrl +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.util.fadeStatusBarColor +import io.heckel.ntfy.util.formatDateShort import kotlinx.coroutines.* import java.util.* import kotlin.random.Random @@ -324,8 +326,12 @@ class DetailActivity : AppCompatActivity(), ActionMode.Callback, NotificationFra lifecycleScope.launch(Dispatchers.IO) { try { - val message = getString(R.string.detail_test_message, Date().toString()) - api.publish(subscriptionBaseUrl, subscriptionTopic, message) + val possibleTags = listOf("warning", "skull", "success", "triangular_flag_on_post", "de", "dog", "rotating_light", "cat", "bike") + val priority = Random.nextInt(1, 6) + val tags = possibleTags.shuffled().take(Random.nextInt(0, 3)) + val title = if (Random.nextBoolean()) getString(R.string.detail_test_title) else "" + val message = getString(R.string.detail_test_message, priority) + api.publish(subscriptionBaseUrl, subscriptionTopic, message, title, priority, tags) } catch (e: Exception) { runOnUiThread { Toast diff --git a/app/src/main/java/io/heckel/ntfy/ui/DetailAdapter.kt b/app/src/main/java/io/heckel/ntfy/ui/DetailAdapter.kt index e3a5534..b39dd34 100644 --- a/app/src/main/java/io/heckel/ntfy/ui/DetailAdapter.kt +++ b/app/src/main/java/io/heckel/ntfy/ui/DetailAdapter.kt @@ -3,12 +3,16 @@ package io.heckel.ntfy.ui import android.view.LayoutInflater import android.view.View import android.view.ViewGroup +import android.widget.ImageView import android.widget.TextView +import androidx.core.content.ContextCompat import androidx.recyclerview.widget.DiffUtil import androidx.recyclerview.widget.ListAdapter import androidx.recyclerview.widget.RecyclerView import io.heckel.ntfy.R import io.heckel.ntfy.data.Notification +import io.heckel.ntfy.util.formatMessage +import io.heckel.ntfy.util.formatTitle import java.util.* class DetailAdapter(private val onClick: (Notification) -> Unit, private val onLongClick: (Notification) -> Unit) : @@ -39,20 +43,51 @@ class DetailAdapter(private val onClick: (Notification) -> Unit, private val onL class DetailViewHolder(itemView: View, private val selected: Set, val onClick: (Notification) -> Unit, val onLongClick: (Notification) -> Unit) : RecyclerView.ViewHolder(itemView) { private var notification: Notification? = null + private val priorityImageView: ImageView = itemView.findViewById(R.id.detail_item_priority_image) private val dateView: TextView = itemView.findViewById(R.id.detail_item_date_text) + private val titleView: TextView = itemView.findViewById(R.id.detail_item_title_text) private val messageView: TextView = itemView.findViewById(R.id.detail_item_message_text) - private val newImageView: View = itemView.findViewById(R.id.detail_item_new) + private val newImageView: View = itemView.findViewById(R.id.detail_item_new_dot) fun bind(notification: Notification) { this.notification = notification + dateView.text = Date(notification.timestamp * 1000).toString() - messageView.text = notification.message + messageView.text = formatMessage(notification) newImageView.visibility = if (notification.notificationId == 0) View.GONE else View.VISIBLE itemView.setOnClickListener { onClick(notification) } itemView.setOnLongClickListener { onLongClick(notification); true } + if (notification.title != "") { + titleView.visibility = View.VISIBLE + titleView.text = formatTitle(notification) + } else { + titleView.visibility = View.GONE + } if (selected.contains(notification.id)) { itemView.setBackgroundResource(R.color.primarySelectedRowColor); } + val ctx = itemView.context + when (notification.priority) { + 1 -> { + priorityImageView.visibility = View.VISIBLE + priorityImageView.setImageDrawable(ContextCompat.getDrawable(ctx, R.drawable.ic_priority_1_24dp)) + } + 2 -> { + priorityImageView.visibility = View.VISIBLE + priorityImageView.setImageDrawable(ContextCompat.getDrawable(ctx, R.drawable.ic_priority_2_24dp)) + } + 3 -> { + priorityImageView.visibility = View.GONE + } + 4 -> { + priorityImageView.visibility = View.VISIBLE + priorityImageView.setImageDrawable(ContextCompat.getDrawable(ctx, R.drawable.ic_priority_4_24dp)) + } + 5 -> { + priorityImageView.visibility = View.VISIBLE + priorityImageView.setImageDrawable(ContextCompat.getDrawable(ctx, R.drawable.ic_priority_5_24dp)) + } + } } } 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 9ce23b9..73953e4 100644 --- a/app/src/main/java/io/heckel/ntfy/ui/MainActivity.kt +++ b/app/src/main/java/io/heckel/ntfy/ui/MainActivity.kt @@ -22,11 +22,13 @@ import androidx.work.* import io.heckel.ntfy.R import io.heckel.ntfy.app.Application import io.heckel.ntfy.data.Subscription -import io.heckel.ntfy.data.topicShortUrl +import io.heckel.ntfy.util.topicShortUrl import io.heckel.ntfy.msg.ApiService import io.heckel.ntfy.msg.NotificationService import io.heckel.ntfy.work.PollWorker import io.heckel.ntfy.firebase.FirebaseMessenger +import io.heckel.ntfy.util.fadeStatusBarColor +import io.heckel.ntfy.util.formatDateShort import kotlinx.coroutines.Dispatchers import kotlinx.coroutines.delay import kotlinx.coroutines.isActive 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 9cf7e24..adf52a5 100644 --- a/app/src/main/java/io/heckel/ntfy/ui/MainAdapter.kt +++ b/app/src/main/java/io/heckel/ntfy/ui/MainAdapter.kt @@ -11,11 +11,10 @@ import androidx.recyclerview.widget.RecyclerView import io.heckel.ntfy.R import io.heckel.ntfy.data.ConnectionState import io.heckel.ntfy.data.Subscription -import io.heckel.ntfy.data.topicShortUrl +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) : ListAdapter(TopicDiffCallback) { val selected = mutableSetOf() // Subscription IDs diff --git a/app/src/main/java/io/heckel/ntfy/ui/Util.kt b/app/src/main/java/io/heckel/ntfy/ui/Util.kt deleted file mode 100644 index 0834354..0000000 --- a/app/src/main/java/io/heckel/ntfy/ui/Util.kt +++ /dev/null @@ -1,22 +0,0 @@ -package io.heckel.ntfy.ui - -import android.animation.ArgbEvaluator -import android.animation.ValueAnimator -import android.view.Window -import java.text.DateFormat -import java.util.* - -// Status bar color fading to match action bar, see https://stackoverflow.com/q/51150077/1440785 -fun fadeStatusBarColor(window: Window, fromColor: Int, toColor: Int) { - val statusBarColorAnimation = ValueAnimator.ofObject(ArgbEvaluator(), fromColor, toColor) - statusBarColorAnimation.addUpdateListener { animator -> - val color = animator.animatedValue as Int - window.statusBarColor = color - } - statusBarColorAnimation.start() -} - -fun formatDateShort(timestampSecs: Long): String { - val mutedUntilDate = Date(timestampSecs*1000) - return DateFormat.getDateTimeInstance(DateFormat.SHORT, DateFormat.SHORT).format(mutedUntilDate) -} diff --git a/app/src/main/java/io/heckel/ntfy/util/Util.kt b/app/src/main/java/io/heckel/ntfy/util/Util.kt new file mode 100644 index 0000000..3b72167 --- /dev/null +++ b/app/src/main/java/io/heckel/ntfy/util/Util.kt @@ -0,0 +1,95 @@ +package io.heckel.ntfy.util + +import android.animation.ArgbEvaluator +import android.animation.ValueAnimator +import android.view.Window +import com.vdurmont.emoji.EmojiManager +import io.heckel.ntfy.data.Notification +import io.heckel.ntfy.data.Subscription +import java.text.DateFormat +import java.util.* + +fun topicUrl(baseUrl: String, topic: String) = "${baseUrl}/${topic}" +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 topicShortUrl(baseUrl: String, topic: String) = + topicUrl(baseUrl, topic) + .replace("http://", "") + .replace("https://", "") + +fun formatDateShort(timestampSecs: Long): String { + val mutedUntilDate = Date(timestampSecs*1000) + return DateFormat.getDateTimeInstance(DateFormat.SHORT, DateFormat.SHORT).format(mutedUntilDate) +} + +fun toPriority(priority: Int?): Int { + if (priority != null && (1..5).contains(priority)) return priority + else return 3 +} + +fun joinTags(tags: List?): String { + return tags?.joinToString(",") ?: "" +} + +fun toTags(tags: String?): String { + return tags ?: "" +} + +fun emojify(tags: List): List { + return tags.mapNotNull { + when (it.toLowerCase()) { + "warn", "warning" -> "\u26A0\uFE0F" + "success" -> "\u2714\uFE0F" + "failure" -> "\u274C" + else -> EmojiManager.getForAlias(it)?.unicode + } + } +} + +/** + * Prepend tags/emojis to message, but only if there is a non-empty title. + * Otherwise the tags will be prepended to the title. + */ +fun formatMessage(notification: Notification): String { + return if (notification.title != "") { + notification.message + } else { + val emojis = emojify(notification.tags.split(",")) + if (emojis.isEmpty()) { + notification.message + } else { + emojis.joinToString("") + " " + notification.message + } + } +} + +/** + * See above; prepend emojis to title if the title is non-empty. + * Otherwise, they are prepended to the message. + */ +fun formatTitle(subscription: Subscription, notification: Notification): String { + return if (notification.title != "") { + formatTitle(notification) + } else { + topicShortUrl(subscription.baseUrl, subscription.topic) + } +} + +fun formatTitle(notification: Notification): String { + val emojis = emojify(notification.tags.split(",")) + return if (emojis.isEmpty()) { + notification.title + } else { + emojis.joinToString("") + " " + notification.title + } +} + +// Status bar color fading to match action bar, see https://stackoverflow.com/q/51150077/1440785 +fun fadeStatusBarColor(window: Window, fromColor: Int, toColor: Int) { + val statusBarColorAnimation = ValueAnimator.ofObject(ArgbEvaluator(), fromColor, toColor) + statusBarColorAnimation.addUpdateListener { animator -> + val color = animator.animatedValue as Int + window.statusBarColor = color + } + statusBarColorAnimation.start() +} diff --git a/app/src/main/res/drawable/ic_priority_1_24dp.xml b/app/src/main/res/drawable/ic_priority_1_24dp.xml new file mode 100644 index 0000000..58ff930 --- /dev/null +++ b/app/src/main/res/drawable/ic_priority_1_24dp.xml @@ -0,0 +1,26 @@ + + + + + diff --git a/app/src/main/res/drawable/ic_priority_2_24dp.xml b/app/src/main/res/drawable/ic_priority_2_24dp.xml new file mode 100644 index 0000000..129c30f --- /dev/null +++ b/app/src/main/res/drawable/ic_priority_2_24dp.xml @@ -0,0 +1,19 @@ + + + + diff --git a/app/src/main/res/drawable/ic_priority_4_24dp.xml b/app/src/main/res/drawable/ic_priority_4_24dp.xml new file mode 100644 index 0000000..671ad67 --- /dev/null +++ b/app/src/main/res/drawable/ic_priority_4_24dp.xml @@ -0,0 +1,20 @@ + + + + diff --git a/app/src/main/res/drawable/ic_priority_5_24dp.xml b/app/src/main/res/drawable/ic_priority_5_24dp.xml new file mode 100644 index 0000000..9eaf7da --- /dev/null +++ b/app/src/main/res/drawable/ic_priority_5_24dp.xml @@ -0,0 +1,26 @@ + + + + + diff --git a/app/src/main/res/layout/fragment_detail_item.xml b/app/src/main/res/layout/fragment_detail_item.xml index 1afb5b3..591540c 100644 --- a/app/src/main/res/layout/fragment_detail_item.xml +++ b/app/src/main/res/layout/fragment_detail_item.xml @@ -6,34 +6,58 @@ android:layout_height="wrap_content" android:background="?android:attr/selectableItemBackground" android:orientation="horizontal" android:clickable="true" - android:focusable="true" android:paddingBottom="10dp" - android:paddingTop="10dp" android:paddingStart="16dp" - android:paddingEnd="10dp"> + android:focusable="true" +> + + android:textAppearance="@style/TextAppearance.AppCompat.Small" + app:layout_constraintTop_toTopOf="parent" + android:layout_marginTop="10dp" app:layout_constraintStart_toStartOf="parent" + android:layout_marginStart="10dp"/> + app:layout_constraintTop_toBottomOf="@id/detail_item_title_text" + app:layout_constraintBottom_toBottomOf="parent" + android:layout_marginBottom="10dp" + app:layout_constraintStart_toStartOf="parent" android:layout_marginStart="10dp" + app:layout_constraintEnd_toEndOf="parent" android:layout_marginEnd="10dp"/> + + diff --git a/app/src/main/res/values/colors.xml b/app/src/main/res/values/colors.xml index 2f1e5cb..07315be 100644 --- a/app/src/main/res/values/colors.xml +++ b/app/src/main/res/values/colors.xml @@ -9,5 +9,8 @@ #EEEEEE #C30000 + + #C30000 + #E10000 diff --git a/app/src/main/res/values/strings.xml b/app/src/main/res/values/strings.xml index 565662b..6b1cdc0 100644 --- a/app/src/main/res/values/strings.xml +++ b/app/src/main/res/values/strings.xml @@ -6,7 +6,11 @@ ntfy.sh - Notifications + Notifications (Min Priority) + Notifications (Low Priority) + Notifications (Default Priority) + Notifications (High Priority) + Notifications (Max Priority) Subscription Service Listening for incoming notifications You are subscribed to instant delivery topics @@ -88,7 +92,8 @@ Permanently delete Cancel - This is a test notification from the Ntfy Android app. It was sent at %1$s. + Test: You can set a title if you like + This is a test notification from the Ntfy Android app. It has a priority of %1$d. If you send another one, it may look different. Could not send test message: %1$s Copied to clipboard Instant delivery enabled 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 5d8aff6..66876aa 100644 --- a/app/src/play/java/io/heckel/ntfy/firebase/FirebaseService.kt +++ b/app/src/play/java/io/heckel/ntfy/firebase/FirebaseService.kt @@ -7,6 +7,9 @@ import io.heckel.ntfy.R import io.heckel.ntfy.app.Application import io.heckel.ntfy.data.Notification import io.heckel.ntfy.msg.NotificationService +import io.heckel.ntfy.util.joinTags +import io.heckel.ntfy.util.toPriority +import io.heckel.ntfy.util.toTags import kotlinx.coroutines.CoroutineScope import kotlinx.coroutines.SupervisorJob import kotlinx.coroutines.launch @@ -29,7 +32,10 @@ class FirebaseService : FirebaseMessagingService() { val id = data["id"] val timestamp = data["time"]?.toLongOrNull() val topic = data["topic"] + val title = data["title"] val message = data["message"] + val priority = data["priority"]?.toIntOrNull() + val tags = data["tags"] if (id == null || topic == null || message == null || timestamp == null) { Log.d(TAG, "Discarding unexpected message: from=${remoteMessage.from}, data=${data}") return @@ -45,8 +51,11 @@ class FirebaseService : FirebaseMessagingService() { id = id, subscriptionId = subscription.id, timestamp = timestamp, + title = title ?: "", message = message, notificationId = Random.nextInt(), + priority = toPriority(priority), + tags = toTags(tags), deleted = false ) val shouldNotify = repository.addNotification(notification) diff --git a/assets/arrow_drop_down_black_24dp.svg b/assets/arrow_drop_down_black_24dp.svg new file mode 100644 index 0000000..63ee544 --- /dev/null +++ b/assets/arrow_drop_down_black_24dp.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/assets/priority_1_24dp.svg b/assets/priority_1_24dp.svg new file mode 100644 index 0000000..df6a0a4 --- /dev/null +++ b/assets/priority_1_24dp.svg @@ -0,0 +1,47 @@ + + + + + + + + diff --git a/assets/priority_2_24dp.svg b/assets/priority_2_24dp.svg new file mode 100644 index 0000000..10a89ad --- /dev/null +++ b/assets/priority_2_24dp.svg @@ -0,0 +1,43 @@ + + + + + + + diff --git a/assets/priority_4_24dp.svg b/assets/priority_4_24dp.svg new file mode 100644 index 0000000..a1723cf --- /dev/null +++ b/assets/priority_4_24dp.svg @@ -0,0 +1,43 @@ + + + + + + + diff --git a/assets/priority_4_alt_24dp.svg b/assets/priority_4_alt_24dp.svg new file mode 100644 index 0000000..1dc3831 --- /dev/null +++ b/assets/priority_4_alt_24dp.svg @@ -0,0 +1,39 @@ + + + + + + diff --git a/assets/priority_5_24dp.svg b/assets/priority_5_24dp.svg new file mode 100644 index 0000000..71bc0e2 --- /dev/null +++ b/assets/priority_5_24dp.svg @@ -0,0 +1,39 @@ + + + + + + diff --git a/assets/priority_5_alt2_24dp.svg b/assets/priority_5_alt2_24dp.svg new file mode 100644 index 0000000..2e2c444 --- /dev/null +++ b/assets/priority_5_alt2_24dp.svg @@ -0,0 +1,47 @@ + + + + + + + + diff --git a/assets/priority_5_alt_24dp.svg b/assets/priority_5_alt_24dp.svg new file mode 100644 index 0000000..d7ae1b7 --- /dev/null +++ b/assets/priority_5_alt_24dp.svg @@ -0,0 +1,47 @@ + + + + + + + + diff --git a/assets/priority_high_black_24dp.svg b/assets/priority_high_black_24dp.svg new file mode 100644 index 0000000..86ade34 --- /dev/null +++ b/assets/priority_high_black_24dp.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/assets/priority_high_circle_red_24dp.svg b/assets/priority_high_circle_red_24dp.svg new file mode 100644 index 0000000..6b9c943 --- /dev/null +++ b/assets/priority_high_circle_red_24dp.svg @@ -0,0 +1,44 @@ + + + + + + + diff --git a/assets/report_black_24dp.svg b/assets/report_black_24dp.svg new file mode 100644 index 0000000..19bd916 --- /dev/null +++ b/assets/report_black_24dp.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/assets/warning_black_24dp.svg b/assets/warning_black_24dp.svg new file mode 100644 index 0000000..22cf31b --- /dev/null +++ b/assets/warning_black_24dp.svg @@ -0,0 +1 @@ + \ No newline at end of file