Backup+restore, Firebase, formatting, custom intent action
This commit is contained in:
parent
79c0e91e8d
commit
2625513216
11 changed files with 173 additions and 56 deletions
|
@ -2,6 +2,7 @@ package io.heckel.ntfy.backup
|
||||||
|
|
||||||
import android.content.Context
|
import android.content.Context
|
||||||
import android.net.Uri
|
import android.net.Uri
|
||||||
|
import androidx.room.ColumnInfo
|
||||||
import com.google.gson.Gson
|
import com.google.gson.Gson
|
||||||
import com.google.gson.GsonBuilder
|
import com.google.gson.GsonBuilder
|
||||||
import com.google.gson.stream.JsonReader
|
import com.google.gson.stream.JsonReader
|
||||||
|
@ -109,6 +110,25 @@ class Backuper(val context: Context) {
|
||||||
}
|
}
|
||||||
notifications.forEach { n ->
|
notifications.forEach { n ->
|
||||||
try {
|
try {
|
||||||
|
val actions = if (n.actions != null) {
|
||||||
|
n.actions.map { a ->
|
||||||
|
io.heckel.ntfy.db.Action(
|
||||||
|
id = a.id,
|
||||||
|
action = a.action,
|
||||||
|
label = a.label,
|
||||||
|
url = a.url,
|
||||||
|
method = a.method,
|
||||||
|
headers = a.headers,
|
||||||
|
body = a.body,
|
||||||
|
intent = a.intent,
|
||||||
|
extras = a.extras,
|
||||||
|
progress = a.progress,
|
||||||
|
error = a.error
|
||||||
|
)
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
null
|
||||||
|
}
|
||||||
val attachment = if (n.attachment != null) {
|
val attachment = if (n.attachment != null) {
|
||||||
io.heckel.ntfy.db.Attachment(
|
io.heckel.ntfy.db.Attachment(
|
||||||
name = n.attachment.name,
|
name = n.attachment.name,
|
||||||
|
@ -133,7 +153,7 @@ class Backuper(val context: Context) {
|
||||||
priority = n.priority,
|
priority = n.priority,
|
||||||
tags = n.tags,
|
tags = n.tags,
|
||||||
click = n.click,
|
click = n.click,
|
||||||
actions = null, // FIXME
|
actions = actions,
|
||||||
attachment = attachment,
|
attachment = attachment,
|
||||||
deleted = n.deleted
|
deleted = n.deleted
|
||||||
))
|
))
|
||||||
|
@ -202,6 +222,25 @@ class Backuper(val context: Context) {
|
||||||
|
|
||||||
private suspend fun createNotificationList(): List<Notification> {
|
private suspend fun createNotificationList(): List<Notification> {
|
||||||
return repository.getNotifications().map { n ->
|
return repository.getNotifications().map { n ->
|
||||||
|
val actions = if (n.actions != null) {
|
||||||
|
n.actions.map { a ->
|
||||||
|
Action(
|
||||||
|
id = a.id,
|
||||||
|
action = a.action,
|
||||||
|
label = a.label,
|
||||||
|
url = a.url,
|
||||||
|
method = a.method,
|
||||||
|
headers = a.headers,
|
||||||
|
body = a.body,
|
||||||
|
intent = a.intent,
|
||||||
|
extras = a.extras,
|
||||||
|
progress = a.progress,
|
||||||
|
error = a.error
|
||||||
|
)
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
null
|
||||||
|
}
|
||||||
val attachment = if (n.attachment != null) {
|
val attachment = if (n.attachment != null) {
|
||||||
Attachment(
|
Attachment(
|
||||||
name = n.attachment.name,
|
name = n.attachment.name,
|
||||||
|
@ -225,6 +264,7 @@ class Backuper(val context: Context) {
|
||||||
priority = n.priority,
|
priority = n.priority,
|
||||||
tags = n.tags,
|
tags = n.tags,
|
||||||
click = n.click,
|
click = n.click,
|
||||||
|
actions = actions,
|
||||||
attachment = attachment,
|
attachment = attachment,
|
||||||
deleted = n.deleted
|
deleted = n.deleted
|
||||||
)
|
)
|
||||||
|
@ -291,10 +331,25 @@ data class Notification(
|
||||||
val priority: Int, // 1=min, 3=default, 5=max
|
val priority: Int, // 1=min, 3=default, 5=max
|
||||||
val tags: String,
|
val tags: String,
|
||||||
val click: String, // URL/intent to open on notification click
|
val click: String, // URL/intent to open on notification click
|
||||||
|
val actions: List<Action>?,
|
||||||
val attachment: Attachment?,
|
val attachment: Attachment?,
|
||||||
val deleted: Boolean
|
val deleted: Boolean
|
||||||
)
|
)
|
||||||
|
|
||||||
|
data class Action(
|
||||||
|
val id: String, // Synthetic ID to identify result, and easily pass via Broadcast and WorkManager
|
||||||
|
val action: String, // "view", "http" or "broadcast"
|
||||||
|
val label: String,
|
||||||
|
val url: String?, // used in "view" and "http" actions
|
||||||
|
val method: String?, // used in "http" action
|
||||||
|
val headers: Map<String,String>?, // used in "http" action
|
||||||
|
val body: String?, // used in "http" action
|
||||||
|
val intent: String?, // used in "broadcast" action
|
||||||
|
val extras: Map<String,String>?, // used in "broadcast" action
|
||||||
|
val progress: Int?, // used to indicate progress in popup
|
||||||
|
val error: String? // used to indicate errors in popup
|
||||||
|
)
|
||||||
|
|
||||||
data class Attachment(
|
data class Attachment(
|
||||||
val name: String, // Filename
|
val name: String, // Filename
|
||||||
val type: String?, // MIME type
|
val type: String?, // MIME type
|
||||||
|
@ -305,7 +360,6 @@ data class Attachment(
|
||||||
val progress: Int, // Progress during download, -1 if not downloaded
|
val progress: Int, // Progress during download, -1 if not downloaded
|
||||||
)
|
)
|
||||||
|
|
||||||
|
|
||||||
data class User(
|
data class User(
|
||||||
val baseUrl: String,
|
val baseUrl: String,
|
||||||
val username: String,
|
val username: String,
|
||||||
|
|
|
@ -91,6 +91,7 @@ data class Action(
|
||||||
@ColumnInfo(name = "method") val method: String?, // used in "http" action
|
@ColumnInfo(name = "method") val method: String?, // used in "http" action
|
||||||
@ColumnInfo(name = "headers") val headers: Map<String,String>?, // used in "http" action
|
@ColumnInfo(name = "headers") val headers: Map<String,String>?, // used in "http" action
|
||||||
@ColumnInfo(name = "body") val body: String?, // used in "http" action
|
@ColumnInfo(name = "body") val body: String?, // used in "http" action
|
||||||
|
@ColumnInfo(name = "intent") val intent: String?, // used in "broadcast" action
|
||||||
@ColumnInfo(name = "extras") val extras: Map<String,String>?, // used in "broadcast" action
|
@ColumnInfo(name = "extras") val extras: Map<String,String>?, // used in "broadcast" action
|
||||||
@ColumnInfo(name = "progress") val progress: Int?, // used to indicate progress in popup
|
@ColumnInfo(name = "progress") val progress: Int?, // used to indicate progress in popup
|
||||||
@ColumnInfo(name = "error") val error: String?, // used to indicate errors in popup
|
@ColumnInfo(name = "error") val error: String?, // used to indicate errors in popup
|
||||||
|
|
|
@ -34,17 +34,17 @@ class BroadcastService(private val ctx: Context) {
|
||||||
intent.putExtra("muted", muted)
|
intent.putExtra("muted", muted)
|
||||||
intent.putExtra("muted_str", muted.toString())
|
intent.putExtra("muted_str", muted.toString())
|
||||||
|
|
||||||
Log.d(TAG, "Sending message intent broadcast: $intent")
|
Log.d(TAG, "Sending message intent broadcast: ${intent.action} with extras ${intent.extras}")
|
||||||
ctx.sendBroadcast(intent)
|
ctx.sendBroadcast(intent)
|
||||||
}
|
}
|
||||||
|
|
||||||
fun sendUserAction(action: Action) {
|
fun sendUserAction(action: Action) {
|
||||||
val intent = Intent()
|
val intent = Intent()
|
||||||
intent.action = USER_ACTION_ACTION
|
intent.action = action.intent ?: USER_ACTION_ACTION
|
||||||
action.extras?.forEach { (key, value) ->
|
action.extras?.forEach { (key, value) ->
|
||||||
intent.putExtra(key, value)
|
intent.putExtra(key, value)
|
||||||
}
|
}
|
||||||
Log.d(TAG, "Sending user action intent broadcast: $intent")
|
Log.d(TAG, "Sending user action intent broadcast: ${intent.action} with extras ${intent.extras}")
|
||||||
ctx.sendBroadcast(intent)
|
ctx.sendBroadcast(intent)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -35,11 +35,12 @@ data class MessageAction(
|
||||||
val id: String,
|
val id: String,
|
||||||
val action: String,
|
val action: String,
|
||||||
val label: String, // "view", "broadcast" or "http"
|
val label: String, // "view", "broadcast" or "http"
|
||||||
val url: String?, // used in "view" and "http"
|
val url: String?, // used in "view" and "http" actions
|
||||||
val method: String?, // used in "http", default is POST (!)
|
val method: String?, // used in "http" action, default is POST (!)
|
||||||
val headers: Map<String,String>?, // used in "http"
|
val headers: Map<String,String>?, // used in "http" action
|
||||||
val body: String?, // used in "http"
|
val body: String?, // used in "http" action
|
||||||
val extras: Map<String,String>?, // used in "broadcast"
|
val intent: String?, // used in "broadcast" action
|
||||||
|
val extras: Map<String,String>?, // used in "broadcast" action
|
||||||
)
|
)
|
||||||
|
|
||||||
const val MESSAGE_ENCODING_BASE64 = "base64"
|
const val MESSAGE_ENCODING_BASE64 = "base64"
|
||||||
|
|
|
@ -1,13 +1,13 @@
|
||||||
package io.heckel.ntfy.msg
|
package io.heckel.ntfy.msg
|
||||||
|
|
||||||
import android.util.Base64
|
|
||||||
import com.google.gson.Gson
|
import com.google.gson.Gson
|
||||||
|
import com.google.gson.reflect.TypeToken
|
||||||
import io.heckel.ntfy.db.Action
|
import io.heckel.ntfy.db.Action
|
||||||
import io.heckel.ntfy.db.Attachment
|
import io.heckel.ntfy.db.Attachment
|
||||||
import io.heckel.ntfy.db.Notification
|
import io.heckel.ntfy.db.Notification
|
||||||
import io.heckel.ntfy.util.joinTags
|
import io.heckel.ntfy.util.joinTags
|
||||||
import io.heckel.ntfy.util.randomString
|
|
||||||
import io.heckel.ntfy.util.toPriority
|
import io.heckel.ntfy.util.toPriority
|
||||||
|
import java.lang.reflect.Type
|
||||||
|
|
||||||
class NotificationParser {
|
class NotificationParser {
|
||||||
private val gson = Gson()
|
private val gson = Gson()
|
||||||
|
@ -33,7 +33,7 @@ class NotificationParser {
|
||||||
} else null
|
} else null
|
||||||
val actions = if (message.actions != null) {
|
val actions = if (message.actions != null) {
|
||||||
message.actions.map { a ->
|
message.actions.map { a ->
|
||||||
Action(a.id, a.action, a.label, a.url, a.method, a.headers, a.body, a.extras, null, null)
|
Action(a.id, a.action, a.label, a.url, a.method, a.headers, a.body, a.intent, a.extras, null, null)
|
||||||
}
|
}
|
||||||
} else null
|
} else null
|
||||||
val notification = Notification(
|
val notification = Notification(
|
||||||
|
@ -54,5 +54,17 @@ class NotificationParser {
|
||||||
return NotificationWithTopic(message.topic, notification)
|
return NotificationWithTopic(message.topic, notification)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Parse JSON array to Action list. The indirection via MessageAction is probably
|
||||||
|
* not necessary, but for "good form".
|
||||||
|
*/
|
||||||
|
fun parseActions(s: String?): List<Action>? {
|
||||||
|
val listType: Type = object : TypeToken<List<MessageAction>?>() {}.type
|
||||||
|
val messageActions: List<MessageAction>? = gson.fromJson(s, listType)
|
||||||
|
return messageActions?.map { a ->
|
||||||
|
Action(a.id, a.action, a.label, a.url, a.method, a.headers, a.body, a.intent, a.extras, null, null)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
data class NotificationWithTopic(val topic: String, val notification: Notification)
|
data class NotificationWithTopic(val topic: String, val notification: Notification)
|
||||||
}
|
}
|
||||||
|
|
|
@ -129,18 +129,6 @@ class NotificationService(val context: Context) {
|
||||||
return context.getString(R.string.notification_popup_file, message, attachmentInfos)
|
return context.getString(R.string.notification_popup_file, message, attachmentInfos)
|
||||||
}
|
}
|
||||||
|
|
||||||
private fun maybeAppendActionErrors(message: String, notification: Notification): String {
|
|
||||||
val actionErrors = notification.actions
|
|
||||||
.orEmpty()
|
|
||||||
.mapNotNull { action -> action.error }
|
|
||||||
.joinToString("\n")
|
|
||||||
if (actionErrors.isEmpty()) {
|
|
||||||
return message
|
|
||||||
} else {
|
|
||||||
return "${message}\n\n${actionErrors}"
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
private fun setClickAction(builder: NotificationCompat.Builder, subscription: Subscription, notification: Notification) {
|
private fun setClickAction(builder: NotificationCompat.Builder, subscription: Subscription, notification: Notification) {
|
||||||
if (notification.click == "") {
|
if (notification.click == "") {
|
||||||
builder.setContentIntent(detailActivityIntent(subscription))
|
builder.setContentIntent(detailActivityIntent(subscription))
|
||||||
|
@ -218,6 +206,10 @@ class NotificationService(val context: Context) {
|
||||||
}
|
}
|
||||||
|
|
||||||
private fun maybeAddViewUserAction(builder: NotificationCompat.Builder, action: Action) {
|
private fun maybeAddViewUserAction(builder: NotificationCompat.Builder, action: Action) {
|
||||||
|
// Note that this function is (almost) duplicated in DetailAdapter, since we need to be able
|
||||||
|
// to open a link from the detail activity as well. We can't do this in the UserActionWorker,
|
||||||
|
// because the behavior is kind of weird in Android.
|
||||||
|
|
||||||
try {
|
try {
|
||||||
val url = action.url ?: return
|
val url = action.url ?: return
|
||||||
val intent = Intent(Intent.ACTION_VIEW, Uri.parse(url)).apply {
|
val intent = Intent(Intent.ACTION_VIEW, Uri.parse(url)).apply {
|
||||||
|
@ -237,12 +229,7 @@ class NotificationService(val context: Context) {
|
||||||
putExtra(BROADCAST_EXTRA_ACTION_ID, action.id)
|
putExtra(BROADCAST_EXTRA_ACTION_ID, action.id)
|
||||||
}
|
}
|
||||||
val pendingIntent = PendingIntent.getBroadcast(context, Random().nextInt(), intent, PendingIntent.FLAG_UPDATE_CURRENT or PendingIntent.FLAG_IMMUTABLE)
|
val pendingIntent = PendingIntent.getBroadcast(context, Random().nextInt(), intent, PendingIntent.FLAG_UPDATE_CURRENT or PendingIntent.FLAG_IMMUTABLE)
|
||||||
val label = when (action.progress) {
|
val label = formatActionLabel(action)
|
||||||
ACTION_PROGRESS_ONGOING -> action.label + " …"
|
|
||||||
ACTION_PROGRESS_SUCCESS -> action.label + " ✔️"
|
|
||||||
ACTION_PROGRESS_FAILED -> action.label + " ❌️"
|
|
||||||
else -> action.label
|
|
||||||
}
|
|
||||||
builder.addAction(NotificationCompat.Action.Builder(0, label, pendingIntent).build())
|
builder.addAction(NotificationCompat.Action.Builder(0, label, pendingIntent).build())
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -322,20 +309,20 @@ class NotificationService(val context: Context) {
|
||||||
}
|
}
|
||||||
|
|
||||||
companion object {
|
companion object {
|
||||||
val ACTION_VIEW = "view"
|
const val ACTION_VIEW = "view"
|
||||||
val ACTION_HTTP = "http"
|
const val ACTION_HTTP = "http"
|
||||||
val ACTION_BROADCAST = "broadcast"
|
const val ACTION_BROADCAST = "broadcast"
|
||||||
|
|
||||||
|
const val BROADCAST_EXTRA_TYPE = "type"
|
||||||
|
const val BROADCAST_EXTRA_NOTIFICATION_ID = "notificationId"
|
||||||
|
const val BROADCAST_EXTRA_ACTION_ID = "action"
|
||||||
|
|
||||||
|
const val BROADCAST_TYPE_DOWNLOAD_START = "io.heckel.ntfy.DOWNLOAD_ACTION_START"
|
||||||
|
const val BROADCAST_TYPE_DOWNLOAD_CANCEL = "io.heckel.ntfy.DOWNLOAD_ACTION_CANCEL"
|
||||||
|
const val BROADCAST_TYPE_USER_ACTION = "io.heckel.ntfy.USER_ACTION_RUN"
|
||||||
|
|
||||||
private const val TAG = "NtfyNotifService"
|
private const val TAG = "NtfyNotifService"
|
||||||
|
|
||||||
private const val BROADCAST_EXTRA_TYPE = "type"
|
|
||||||
private const val BROADCAST_EXTRA_NOTIFICATION_ID = "notificationId"
|
|
||||||
private const val BROADCAST_EXTRA_ACTION_ID = "action"
|
|
||||||
|
|
||||||
private const val BROADCAST_TYPE_DOWNLOAD_START = "io.heckel.ntfy.DOWNLOAD_ACTION_START"
|
|
||||||
private const val BROADCAST_TYPE_DOWNLOAD_CANCEL = "io.heckel.ntfy.DOWNLOAD_ACTION_CANCEL"
|
|
||||||
private const val BROADCAST_TYPE_USER_ACTION = "io.heckel.ntfy.USER_ACTION_RUN"
|
|
||||||
|
|
||||||
private const val CHANNEL_ID_MIN = "ntfy-min"
|
private const val CHANNEL_ID_MIN = "ntfy-min"
|
||||||
private const val CHANNEL_ID_LOW = "ntfy-low"
|
private const val CHANNEL_ID_LOW = "ntfy-low"
|
||||||
private const val CHANNEL_ID_DEFAULT = "ntfy"
|
private const val CHANNEL_ID_DEFAULT = "ntfy"
|
||||||
|
|
|
@ -1,6 +1,8 @@
|
||||||
package io.heckel.ntfy.msg
|
package io.heckel.ntfy.msg
|
||||||
|
|
||||||
import android.content.Context
|
import android.content.Context
|
||||||
|
import android.content.Intent
|
||||||
|
import android.net.Uri
|
||||||
import androidx.work.Worker
|
import androidx.work.Worker
|
||||||
import androidx.work.WorkerParameters
|
import androidx.work.WorkerParameters
|
||||||
import io.heckel.ntfy.R
|
import io.heckel.ntfy.R
|
||||||
|
@ -8,6 +10,7 @@ import io.heckel.ntfy.app.Application
|
||||||
import io.heckel.ntfy.db.*
|
import io.heckel.ntfy.db.*
|
||||||
import io.heckel.ntfy.msg.NotificationService.Companion.ACTION_BROADCAST
|
import io.heckel.ntfy.msg.NotificationService.Companion.ACTION_BROADCAST
|
||||||
import io.heckel.ntfy.msg.NotificationService.Companion.ACTION_HTTP
|
import io.heckel.ntfy.msg.NotificationService.Companion.ACTION_HTTP
|
||||||
|
import io.heckel.ntfy.msg.NotificationService.Companion.ACTION_VIEW
|
||||||
import io.heckel.ntfy.util.Log
|
import io.heckel.ntfy.util.Log
|
||||||
import okhttp3.OkHttpClient
|
import okhttp3.OkHttpClient
|
||||||
import okhttp3.Request
|
import okhttp3.Request
|
||||||
|
@ -43,8 +46,11 @@ class UserActionWorker(private val context: Context, params: WorkerParameters) :
|
||||||
Log.d(TAG, "Executing action $action for notification $notification")
|
Log.d(TAG, "Executing action $action for notification $notification")
|
||||||
try {
|
try {
|
||||||
when (action.action) {
|
when (action.action) {
|
||||||
ACTION_HTTP -> performHttpAction(action)
|
// ACTION_VIEW is not handled here. It has to be handled in the foreground to avoid
|
||||||
|
// weird Android behavior.
|
||||||
|
|
||||||
ACTION_BROADCAST -> performBroadcastAction(action)
|
ACTION_BROADCAST -> performBroadcastAction(action)
|
||||||
|
ACTION_HTTP -> performHttpAction(action)
|
||||||
}
|
}
|
||||||
} catch (e: Exception) {
|
} catch (e: Exception) {
|
||||||
Log.w(TAG, "Error executing action: ${e.message}", e)
|
Log.w(TAG, "Error executing action: ${e.message}", e)
|
||||||
|
@ -56,6 +62,11 @@ class UserActionWorker(private val context: Context, params: WorkerParameters) :
|
||||||
return Result.success()
|
return Result.success()
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private fun performBroadcastAction(action: Action) {
|
||||||
|
broadcaster.sendUserAction(action)
|
||||||
|
save(action.copy(progress = ACTION_PROGRESS_SUCCESS, error = null))
|
||||||
|
}
|
||||||
|
|
||||||
private fun performHttpAction(action: Action) {
|
private fun performHttpAction(action: Action) {
|
||||||
save(action.copy(progress = ACTION_PROGRESS_ONGOING, error = null))
|
save(action.copy(progress = ACTION_PROGRESS_ONGOING, error = null))
|
||||||
|
|
||||||
|
@ -81,11 +92,6 @@ class UserActionWorker(private val context: Context, params: WorkerParameters) :
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
private fun performBroadcastAction(action: Action) {
|
|
||||||
broadcaster.sendUserAction(action)
|
|
||||||
save(action.copy(progress = ACTION_PROGRESS_SUCCESS, error = null))
|
|
||||||
}
|
|
||||||
|
|
||||||
private fun save(newAction: Action) {
|
private fun save(newAction: Action) {
|
||||||
Log.d(TAG, "Updating action: $newAction")
|
Log.d(TAG, "Updating action: $newAction")
|
||||||
val newActions = notification.actions?.map { a -> if (a.id == newAction.id) newAction else a }
|
val newActions = notification.actions?.map { a -> if (a.id == newAction.id) newAction else a }
|
||||||
|
|
|
@ -25,6 +25,8 @@ import io.heckel.ntfy.R
|
||||||
import io.heckel.ntfy.db.*
|
import io.heckel.ntfy.db.*
|
||||||
import io.heckel.ntfy.msg.DownloadManager
|
import io.heckel.ntfy.msg.DownloadManager
|
||||||
import io.heckel.ntfy.msg.DownloadWorker
|
import io.heckel.ntfy.msg.DownloadWorker
|
||||||
|
import io.heckel.ntfy.msg.NotificationService
|
||||||
|
import io.heckel.ntfy.msg.NotificationService.Companion.ACTION_VIEW
|
||||||
import io.heckel.ntfy.util.*
|
import io.heckel.ntfy.util.*
|
||||||
import kotlinx.coroutines.Dispatchers
|
import kotlinx.coroutines.Dispatchers
|
||||||
import kotlinx.coroutines.GlobalScope
|
import kotlinx.coroutines.GlobalScope
|
||||||
|
@ -81,7 +83,7 @@ class DetailAdapter(private val activity: Activity, private val repository: Repo
|
||||||
val unmatchedTags = unmatchedTags(splitTags(notification.tags))
|
val unmatchedTags = unmatchedTags(splitTags(notification.tags))
|
||||||
|
|
||||||
dateView.text = formatDateShort(notification.timestamp)
|
dateView.text = formatDateShort(notification.timestamp)
|
||||||
messageView.text = formatMessage(notification)
|
messageView.text = maybeAppendActionErrors(formatMessage(notification), notification)
|
||||||
newDotImageView.visibility = if (notification.notificationId == 0) View.GONE else View.VISIBLE
|
newDotImageView.visibility = if (notification.notificationId == 0) View.GONE else View.VISIBLE
|
||||||
itemView.setOnClickListener { onClick(notification) }
|
itemView.setOnClickListener { onClick(notification) }
|
||||||
itemView.setOnLongClickListener { onLongClick(notification); true }
|
itemView.setOnLongClickListener { onLongClick(notification); true }
|
||||||
|
@ -179,6 +181,7 @@ class DetailAdapter(private val activity: Activity, private val repository: Repo
|
||||||
val attachment = notification.attachment // May be null
|
val attachment = notification.attachment // May be null
|
||||||
val hasAttachment = attachment != null
|
val hasAttachment = attachment != null
|
||||||
val hasClickLink = notification.click != ""
|
val hasClickLink = notification.click != ""
|
||||||
|
val hasUserActions = notification.actions?.isNotEmpty() ?: false
|
||||||
val downloadItem = popup.menu.findItem(R.id.detail_item_menu_download)
|
val downloadItem = popup.menu.findItem(R.id.detail_item_menu_download)
|
||||||
val cancelItem = popup.menu.findItem(R.id.detail_item_menu_cancel)
|
val cancelItem = popup.menu.findItem(R.id.detail_item_menu_cancel)
|
||||||
val openItem = popup.menu.findItem(R.id.detail_item_menu_open)
|
val openItem = popup.menu.findItem(R.id.detail_item_menu_open)
|
||||||
|
@ -199,6 +202,12 @@ class DetailAdapter(private val activity: Activity, private val repository: Repo
|
||||||
if (hasClickLink) {
|
if (hasClickLink) {
|
||||||
copyContentsItem.setOnMenuItemClickListener { copyContents(context, notification) }
|
copyContentsItem.setOnMenuItemClickListener { copyContents(context, notification) }
|
||||||
}
|
}
|
||||||
|
if (notification.actions != null && notification.actions.isNotEmpty()) {
|
||||||
|
notification.actions.forEach { action ->
|
||||||
|
val actionItem = popup.menu.add(formatActionLabel(action))
|
||||||
|
actionItem.setOnMenuItemClickListener { runAction(context, notification, action) }
|
||||||
|
}
|
||||||
|
}
|
||||||
openItem.isVisible = hasAttachment && exists
|
openItem.isVisible = hasAttachment && exists
|
||||||
downloadItem.isVisible = hasAttachment && !exists && !expired && !inProgress
|
downloadItem.isVisible = hasAttachment && !exists && !expired && !inProgress
|
||||||
deleteItem.isVisible = hasAttachment && exists
|
deleteItem.isVisible = hasAttachment && exists
|
||||||
|
@ -208,7 +217,7 @@ class DetailAdapter(private val activity: Activity, private val repository: Repo
|
||||||
copyContentsItem.isVisible = notification.click != ""
|
copyContentsItem.isVisible = notification.click != ""
|
||||||
val noOptions = !openItem.isVisible && !saveFileItem.isVisible && !downloadItem.isVisible
|
val noOptions = !openItem.isVisible && !saveFileItem.isVisible && !downloadItem.isVisible
|
||||||
&& !copyUrlItem.isVisible && !cancelItem.isVisible && !deleteItem.isVisible
|
&& !copyUrlItem.isVisible && !cancelItem.isVisible && !deleteItem.isVisible
|
||||||
&& !copyContentsItem.isVisible
|
&& !copyContentsItem.isVisible && !hasUserActions
|
||||||
if (noOptions) {
|
if (noOptions) {
|
||||||
return null
|
return null
|
||||||
}
|
}
|
||||||
|
@ -401,6 +410,31 @@ class DetailAdapter(private val activity: Activity, private val repository: Repo
|
||||||
copyToClipboard(context, notification)
|
copyToClipboard(context, notification)
|
||||||
return true
|
return true
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private fun runAction(context: Context, notification: Notification, action: Action): Boolean {
|
||||||
|
when (action.action) {
|
||||||
|
ACTION_VIEW -> runViewAction(context, action)
|
||||||
|
else -> runOtherUserAction(context, notification, action)
|
||||||
|
}
|
||||||
|
return true
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun runViewAction(context: Context, action: Action) {
|
||||||
|
val url = action.url ?: return
|
||||||
|
val intent = Intent(Intent.ACTION_VIEW, Uri.parse(url)).apply {
|
||||||
|
addFlags(Intent.FLAG_GRANT_READ_URI_PERMISSION)
|
||||||
|
}
|
||||||
|
context.startActivity(intent)
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun runOtherUserAction(context: Context, notification: Notification, action: Action) {
|
||||||
|
val intent = Intent(context, NotificationService.UserActionBroadcastReceiver::class.java).apply {
|
||||||
|
putExtra(NotificationService.BROADCAST_EXTRA_TYPE, NotificationService.BROADCAST_TYPE_USER_ACTION)
|
||||||
|
putExtra(NotificationService.BROADCAST_EXTRA_NOTIFICATION_ID, notification.id)
|
||||||
|
putExtra(NotificationService.BROADCAST_EXTRA_ACTION_ID, action.id)
|
||||||
|
}
|
||||||
|
context.sendBroadcast(intent)
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
object TopicDiffCallback : DiffUtil.ItemCallback<Notification>() {
|
object TopicDiffCallback : DiffUtil.ItemCallback<Notification>() {
|
||||||
|
|
|
@ -23,9 +23,7 @@ import android.widget.ImageView
|
||||||
import android.widget.Toast
|
import android.widget.Toast
|
||||||
import androidx.appcompat.app.AppCompatDelegate
|
import androidx.appcompat.app.AppCompatDelegate
|
||||||
import io.heckel.ntfy.R
|
import io.heckel.ntfy.R
|
||||||
import io.heckel.ntfy.db.Notification
|
import io.heckel.ntfy.db.*
|
||||||
import io.heckel.ntfy.db.Repository
|
|
||||||
import io.heckel.ntfy.db.Subscription
|
|
||||||
import io.heckel.ntfy.msg.MESSAGE_ENCODING_BASE64
|
import io.heckel.ntfy.msg.MESSAGE_ENCODING_BASE64
|
||||||
import okhttp3.MediaType
|
import okhttp3.MediaType
|
||||||
import okhttp3.MediaType.Companion.toMediaTypeOrNull
|
import okhttp3.MediaType.Companion.toMediaTypeOrNull
|
||||||
|
@ -185,6 +183,27 @@ fun formatTitle(notification: Notification): String {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
fun formatActionLabel(action: Action): String {
|
||||||
|
return when (action.progress) {
|
||||||
|
ACTION_PROGRESS_ONGOING -> action.label + " …"
|
||||||
|
ACTION_PROGRESS_SUCCESS -> action.label + " ✔️"
|
||||||
|
ACTION_PROGRESS_FAILED -> action.label + " ❌️"
|
||||||
|
else -> action.label
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
fun maybeAppendActionErrors(message: String, notification: Notification): String {
|
||||||
|
val actionErrors = notification.actions
|
||||||
|
.orEmpty()
|
||||||
|
.mapNotNull { action -> action.error }
|
||||||
|
.joinToString("\n")
|
||||||
|
if (actionErrors.isEmpty()) {
|
||||||
|
return message
|
||||||
|
} else {
|
||||||
|
return "${message}\n\n${actionErrors}"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
// Checks in the most horrible way if a content URI exists; I couldn't find a better way
|
// Checks in the most horrible way if a content URI exists; I couldn't find a better way
|
||||||
fun fileExists(context: Context, contentUri: String?): Boolean {
|
fun fileExists(context: Context, contentUri: String?): Boolean {
|
||||||
return try {
|
return try {
|
||||||
|
|
|
@ -4,7 +4,7 @@
|
||||||
The translatable="false" attribute is just an additional safety. -->
|
The translatable="false" attribute is just an additional safety. -->
|
||||||
|
|
||||||
<!-- Main app constants -->
|
<!-- Main app constants -->
|
||||||
<string name="app_name" translatable="false">Ntfy</string>
|
<string name="app_name" translatable="false">ntfy</string>
|
||||||
<string name="app_base_url" translatable="false">https://ntfy.sh</string> <!-- If changed, you must also change google-services.json! -->
|
<string name="app_base_url" translatable="false">https://ntfy.sh</string> <!-- If changed, you must also change google-services.json! -->
|
||||||
|
|
||||||
<!-- Main activity -->
|
<!-- Main activity -->
|
||||||
|
|
|
@ -13,6 +13,7 @@ import io.heckel.ntfy.util.Log
|
||||||
import io.heckel.ntfy.msg.ApiService
|
import io.heckel.ntfy.msg.ApiService
|
||||||
import io.heckel.ntfy.msg.MESSAGE_ENCODING_BASE64
|
import io.heckel.ntfy.msg.MESSAGE_ENCODING_BASE64
|
||||||
import io.heckel.ntfy.msg.NotificationDispatcher
|
import io.heckel.ntfy.msg.NotificationDispatcher
|
||||||
|
import io.heckel.ntfy.msg.NotificationParser
|
||||||
import io.heckel.ntfy.service.SubscriberService
|
import io.heckel.ntfy.service.SubscriberService
|
||||||
import io.heckel.ntfy.util.toPriority
|
import io.heckel.ntfy.util.toPriority
|
||||||
import io.heckel.ntfy.util.topicShortUrl
|
import io.heckel.ntfy.util.topicShortUrl
|
||||||
|
@ -27,6 +28,7 @@ class FirebaseService : FirebaseMessagingService() {
|
||||||
private val dispatcher by lazy { NotificationDispatcher(this, repository) }
|
private val dispatcher by lazy { NotificationDispatcher(this, repository) }
|
||||||
private val job = SupervisorJob()
|
private val job = SupervisorJob()
|
||||||
private val messenger = FirebaseMessenger()
|
private val messenger = FirebaseMessenger()
|
||||||
|
private val parser = NotificationParser()
|
||||||
|
|
||||||
override fun onMessageReceived(remoteMessage: RemoteMessage) {
|
override fun onMessageReceived(remoteMessage: RemoteMessage) {
|
||||||
// Init log (this is done in all entrypoints)
|
// Init log (this is done in all entrypoints)
|
||||||
|
@ -88,6 +90,7 @@ class FirebaseService : FirebaseMessagingService() {
|
||||||
val priority = data["priority"]?.toIntOrNull()
|
val priority = data["priority"]?.toIntOrNull()
|
||||||
val tags = data["tags"]
|
val tags = data["tags"]
|
||||||
val click = data["click"]
|
val click = data["click"]
|
||||||
|
val actions = data["actions"] // JSON array as string, sigh ...
|
||||||
val encoding = data["encoding"]
|
val encoding = data["encoding"]
|
||||||
val attachmentName = data["attachment_name"] ?: "attachment.bin"
|
val attachmentName = data["attachment_name"] ?: "attachment.bin"
|
||||||
val attachmentType = data["attachment_type"]
|
val attachmentType = data["attachment_type"]
|
||||||
|
@ -131,13 +134,13 @@ class FirebaseService : FirebaseMessagingService() {
|
||||||
priority = toPriority(priority),
|
priority = toPriority(priority),
|
||||||
tags = tags ?: "",
|
tags = tags ?: "",
|
||||||
click = click ?: "",
|
click = click ?: "",
|
||||||
actions = null, // FIXME
|
actions = parser.parseActions(actions),
|
||||||
attachment = attachment,
|
attachment = attachment,
|
||||||
notificationId = Random.nextInt(),
|
notificationId = Random.nextInt(),
|
||||||
deleted = false
|
deleted = false
|
||||||
)
|
)
|
||||||
if (repository.addNotification(notification)) {
|
if (repository.addNotification(notification)) {
|
||||||
Log.d(TAG, "Dispatching notification for message: from=${remoteMessage.from}, fcmprio=${remoteMessage.priority}, fcmprio_orig=${remoteMessage.originalPriority}, data=${data}")
|
Log.d(TAG, "Dispatching notification: from=${remoteMessage.from}, fcmprio=${remoteMessage.priority}, fcmprio_orig=${remoteMessage.originalPriority}, data=${data}")
|
||||||
dispatcher.dispatch(subscription, notification)
|
dispatcher.dispatch(subscription, notification)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
Loading…
Reference in a new issue