From 79c0e91e8de3298b54a689eea097b7eeac2c28d6 Mon Sep 17 00:00:00 2001 From: Philipp Heckel Date: Tue, 19 Apr 2022 09:15:06 -0400 Subject: [PATCH] Actions WIP --- .../main/java/io/heckel/ntfy/db/Database.kt | 31 +++++--- .../io/heckel/ntfy/msg/BroadcastService.kt | 17 +++- .../java/io/heckel/ntfy/msg/DownloadWorker.kt | 8 +- .../main/java/io/heckel/ntfy/msg/Message.kt | 5 +- .../heckel/ntfy/msg/NotificationDispatcher.kt | 3 +- .../io/heckel/ntfy/msg/NotificationParser.kt | 2 +- .../io/heckel/ntfy/msg/NotificationService.kt | 78 +++++++++++-------- .../io/heckel/ntfy/msg/UserActionWorker.kt | 70 +++++++++++++---- .../java/io/heckel/ntfy/ui/DetailAdapter.kt | 8 +- .../java/io/heckel/ntfy/work/DeleteWorker.kt | 4 +- app/src/main/res/values/strings.xml | 1 + 11 files changed, 153 insertions(+), 74 deletions(-) diff --git a/app/src/main/java/io/heckel/ntfy/db/Database.kt b/app/src/main/java/io/heckel/ntfy/db/Database.kt index c5fac86..92d9249 100644 --- a/app/src/main/java/io/heckel/ntfy/db/Database.kt +++ b/app/src/main/java/io/heckel/ntfy/db/Database.kt @@ -73,20 +73,33 @@ data class Attachment( @ColumnInfo(name = "progress") val progress: Int, // Progress during download, -1 if not downloaded ) { constructor(name: String, type: String?, size: Long?, expires: Long?, url: String) : - this(name, type, size, expires, url, null, PROGRESS_NONE) + this(name, type, size, expires, url, null, ATTACHMENT_PROGRESS_NONE) } +const val ATTACHMENT_PROGRESS_NONE = -1 +const val ATTACHMENT_PROGRESS_INDETERMINATE = -2 +const val ATTACHMENT_PROGRESS_FAILED = -3 +const val ATTACHMENT_PROGRESS_DELETED = -4 +const val ATTACHMENT_PROGRESS_DONE = 100 + @Entity data class Action( @ColumnInfo(name = "id") val id: String, // Synthetic ID to identify result, and easily pass via Broadcast and WorkManager - @ColumnInfo(name = "action") val action: String, + @ColumnInfo(name = "action") val action: String, // "view", "http" or "broadcast" @ColumnInfo(name = "label") val label: String, - @ColumnInfo(name = "url") val url: String?, // used in "view" and "http" - @ColumnInfo(name = "method") val method: String?, // used in "http" - @ColumnInfo(name = "headers") val headers: Map?, // used in "http" - @ColumnInfo(name = "body") val body: String?, // used in "http" + @ColumnInfo(name = "url") val url: String?, // used in "view" and "http" actions + @ColumnInfo(name = "method") val method: String?, // used in "http" action + @ColumnInfo(name = "headers") val headers: Map?, // used in "http" action + @ColumnInfo(name = "body") val body: String?, // used in "http" action + @ColumnInfo(name = "extras") val extras: Map?, // used in "broadcast" action + @ColumnInfo(name = "progress") val progress: Int?, // used to indicate progress in popup + @ColumnInfo(name = "error") val error: String?, // used to indicate errors in popup ) +const val ACTION_PROGRESS_ONGOING = 1 +const val ACTION_PROGRESS_SUCCESS = 2 +const val ACTION_PROGRESS_FAILED = 3 + class Converters { private val gson = Gson() @@ -102,12 +115,6 @@ class Converters { } } -const val PROGRESS_NONE = -1 -const val PROGRESS_INDETERMINATE = -2 -const val PROGRESS_FAILED = -3 -const val PROGRESS_DELETED = -4 -const val PROGRESS_DONE = 100 - @Entity data class User( @PrimaryKey @ColumnInfo(name = "baseUrl") val baseUrl: String, 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 d39bb6c..7cf5a55 100644 --- a/app/src/main/java/io/heckel/ntfy/msg/BroadcastService.kt +++ b/app/src/main/java/io/heckel/ntfy/msg/BroadcastService.kt @@ -2,8 +2,8 @@ package io.heckel.ntfy.msg import android.content.Context import android.content.Intent -import android.util.Base64 import io.heckel.ntfy.R +import io.heckel.ntfy.db.Action import io.heckel.ntfy.db.Notification import io.heckel.ntfy.db.Repository import io.heckel.ntfy.db.Subscription @@ -17,7 +17,7 @@ import kotlinx.coroutines.launch * in order to facilitate tasks app integrations. */ class BroadcastService(private val ctx: Context) { - fun send(subscription: Subscription, notification: Notification, muted: Boolean) { + fun sendMessage(subscription: Subscription, notification: Notification, muted: Boolean) { val intent = Intent() intent.action = MESSAGE_RECEIVED_ACTION intent.putExtra("id", notification.id) @@ -34,7 +34,17 @@ class BroadcastService(private val ctx: Context) { intent.putExtra("muted", muted) intent.putExtra("muted_str", muted.toString()) - Log.d(TAG, "Sending intent broadcast: $intent") + Log.d(TAG, "Sending message intent broadcast: $intent") + ctx.sendBroadcast(intent) + } + + fun sendUserAction(action: Action) { + val intent = Intent() + intent.action = USER_ACTION_ACTION + action.extras?.forEach { (key, value) -> + intent.putExtra(key, value) + } + Log.d(TAG, "Sending user action intent broadcast: $intent") ctx.sendBroadcast(intent) } @@ -109,5 +119,6 @@ class BroadcastService(private val ctx: Context) { // These constants cannot be changed without breaking the contract; also see manifest private const val MESSAGE_RECEIVED_ACTION = "io.heckel.ntfy.MESSAGE_RECEIVED" private const val MESSAGE_SEND_ACTION = "io.heckel.ntfy.SEND_MESSAGE" + private const val USER_ACTION_ACTION = "io.heckel.ntfy.USER_ACTION" } } diff --git a/app/src/main/java/io/heckel/ntfy/msg/DownloadWorker.kt b/app/src/main/java/io/heckel/ntfy/msg/DownloadWorker.kt index a8eed41..df97a81 100644 --- a/app/src/main/java/io/heckel/ntfy/msg/DownloadWorker.kt +++ b/app/src/main/java/io/heckel/ntfy/msg/DownloadWorker.kt @@ -91,13 +91,13 @@ class DownloadWorker(private val context: Context, params: WorkerParameters) : W while (bytes >= 0) { if (System.currentTimeMillis() - lastProgress > NOTIFICATION_UPDATE_INTERVAL_MILLIS) { if (isStopped) { // Canceled by user - save(attachment.copy(progress = PROGRESS_NONE)) + save(attachment.copy(progress = ATTACHMENT_PROGRESS_NONE)) return // File will be deleted in onStopped() } val progress = if (attachment.size != null && attachment.size!! > 0) { (bytesCopied.toFloat()/attachment.size!!.toFloat()*100).toInt() } else { - PROGRESS_INDETERMINATE + ATTACHMENT_PROGRESS_INDETERMINATE } save(attachment.copy(progress = progress)) lastProgress = System.currentTimeMillis() @@ -114,7 +114,7 @@ class DownloadWorker(private val context: Context, params: WorkerParameters) : W save(attachment.copy( size = bytesCopied, contentUri = uri.toString(), - progress = PROGRESS_DONE + progress = ATTACHMENT_PROGRESS_DONE )) } } catch (e: Exception) { @@ -155,7 +155,7 @@ class DownloadWorker(private val context: Context, params: WorkerParameters) : W private fun failed(e: Exception) { Log.w(TAG, "Attachment download failed", e) - save(attachment.copy(progress = PROGRESS_FAILED)) + save(attachment.copy(progress = ATTACHMENT_PROGRESS_FAILED)) maybeDeleteFile() } diff --git a/app/src/main/java/io/heckel/ntfy/msg/Message.kt b/app/src/main/java/io/heckel/ntfy/msg/Message.kt index 7d66814..1d86c17 100644 --- a/app/src/main/java/io/heckel/ntfy/msg/Message.kt +++ b/app/src/main/java/io/heckel/ntfy/msg/Message.kt @@ -34,11 +34,12 @@ data class MessageAttachment( data class MessageAction( val id: String, val action: String, - val label: String, + val label: String, // "view", "broadcast" or "http" val url: String?, // used in "view" and "http" - val method: String?, // used in "http" + val method: String?, // used in "http", default is POST (!) val headers: Map?, // used in "http" val body: String?, // used in "http" + val extras: Map?, // used in "broadcast" ) const val MESSAGE_ENCODING_BASE64 = "base64" 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 bfc4f86..e1afdf6 100644 --- a/app/src/main/java/io/heckel/ntfy/msg/NotificationDispatcher.kt +++ b/app/src/main/java/io/heckel/ntfy/msg/NotificationDispatcher.kt @@ -1,7 +1,6 @@ package io.heckel.ntfy.msg import android.content.Context -import android.util.Base64 import io.heckel.ntfy.db.Notification import io.heckel.ntfy.db.Repository import io.heckel.ntfy.db.Subscription @@ -35,7 +34,7 @@ class NotificationDispatcher(val context: Context, val repository: Repository) { notifier.display(subscription, notification) } if (broadcast) { - broadcaster.send(subscription, notification, muted) + broadcaster.sendMessage(subscription, notification, muted) } if (distribute) { safeLet(subscription.upAppId, subscription.upConnectorToken) { appId, connectorToken -> diff --git a/app/src/main/java/io/heckel/ntfy/msg/NotificationParser.kt b/app/src/main/java/io/heckel/ntfy/msg/NotificationParser.kt index dd60b78..4d00781 100644 --- a/app/src/main/java/io/heckel/ntfy/msg/NotificationParser.kt +++ b/app/src/main/java/io/heckel/ntfy/msg/NotificationParser.kt @@ -33,7 +33,7 @@ class NotificationParser { } else null val actions = if (message.actions != null) { message.actions.map { a -> - Action(a.id, a.action, a.label, a.url, a.method, a.headers, a.body) + Action(a.id, a.action, a.label, a.url, a.method, a.headers, a.body, a.extras, null, null) } } else null val notification = Notification( 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 b8ab7fb..db3b70d 100644 --- a/app/src/main/java/io/heckel/ntfy/msg/NotificationService.kt +++ b/app/src/main/java/io/heckel/ntfy/msg/NotificationService.kt @@ -66,7 +66,7 @@ class NotificationService(val context: Context) { maybeAddBrowseAction(builder, notification) maybeAddDownloadAction(builder, notification) maybeAddCancelAction(builder, notification) - maybeAddCustomActions(builder, notification) + maybeAddUserActions(builder, notification) maybeCreateNotificationChannel(notification.priority) notificationManager.notify(notification.notificationId, builder.build()) @@ -90,43 +90,55 @@ class NotificationService(val context: Context) { val bitmapStream = resolver.openInputStream(Uri.parse(contentUri)) val bitmap = BitmapFactory.decodeStream(bitmapStream) builder - .setContentText(formatMessage(notification)) + .setContentText(maybeAppendActionErrors(formatMessage(notification), notification)) .setLargeIcon(bitmap) .setStyle(NotificationCompat.BigPictureStyle() .bigPicture(bitmap) .bigLargeIcon(null)) } catch (_: Exception) { - val message = formatMessageMaybeWithAttachmentInfo(notification) + val message = maybeAppendActionErrors(formatMessageMaybeWithAttachmentInfos(notification), notification) builder .setContentText(message) .setStyle(NotificationCompat.BigTextStyle().bigText(message)) } } else { - val message = formatMessageMaybeWithAttachmentInfo(notification) + val message = maybeAppendActionErrors(formatMessageMaybeWithAttachmentInfos(notification), notification) builder .setContentText(message) .setStyle(NotificationCompat.BigTextStyle().bigText(message)) } } - private fun formatMessageMaybeWithAttachmentInfo(notification: Notification): String { + private fun formatMessageMaybeWithAttachmentInfos(notification: Notification): String { val message = formatMessage(notification) val attachment = notification.attachment ?: return message - val infos = if (attachment.size != null) { + val attachmentInfos = if (attachment.size != null) { "${attachment.name}, ${formatBytes(attachment.size)}" } else { attachment.name } if (attachment.progress in 0..99) { - return context.getString(R.string.notification_popup_file_downloading, infos, attachment.progress, message) + return context.getString(R.string.notification_popup_file_downloading, attachmentInfos, attachment.progress, message) } - if (attachment.progress == PROGRESS_DONE) { - return context.getString(R.string.notification_popup_file_download_successful, message, infos) + if (attachment.progress == ATTACHMENT_PROGRESS_DONE) { + return context.getString(R.string.notification_popup_file_download_successful, message, attachmentInfos) } - if (attachment.progress == PROGRESS_FAILED) { - return context.getString(R.string.notification_popup_file_download_failed, message, infos) + if (attachment.progress == ATTACHMENT_PROGRESS_FAILED) { + return context.getString(R.string.notification_popup_file_download_failed, 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}" } - return context.getString(R.string.notification_popup_file, message, infos) } private fun setClickAction(builder: NotificationCompat.Builder, subscription: Subscription, notification: Notification) { @@ -135,7 +147,7 @@ class NotificationService(val context: Context) { } else { try { val uri = Uri.parse(notification.click) - val viewIntent = PendingIntent.getActivity(context, 0, Intent(Intent.ACTION_VIEW, uri), PendingIntent.FLAG_IMMUTABLE) + val viewIntent = PendingIntent.getActivity(context, Random().nextInt(), Intent(Intent.ACTION_VIEW, uri), PendingIntent.FLAG_IMMUTABLE) builder.setContentIntent(viewIntent) } catch (e: Exception) { builder.setContentIntent(detailActivityIntent(subscription)) @@ -159,7 +171,7 @@ class NotificationService(val context: Context) { setDataAndType(contentUri, notification.attachment.type ?: "application/octet-stream") // Required for Android <= P addFlags(Intent.FLAG_GRANT_READ_URI_PERMISSION) } - val pendingIntent = PendingIntent.getActivity(context, 0, intent, PendingIntent.FLAG_IMMUTABLE) + val pendingIntent = PendingIntent.getActivity(context, Random().nextInt(), intent, PendingIntent.FLAG_IMMUTABLE) builder.addAction(NotificationCompat.Action.Builder(0, context.getString(R.string.notification_popup_action_open), pendingIntent).build()) } } @@ -169,18 +181,18 @@ class NotificationService(val context: Context) { val intent = Intent(android.app.DownloadManager.ACTION_VIEW_DOWNLOADS).apply { addFlags(Intent.FLAG_GRANT_READ_URI_PERMISSION) } - val pendingIntent = PendingIntent.getActivity(context, 0, intent, PendingIntent.FLAG_IMMUTABLE) + val pendingIntent = PendingIntent.getActivity(context, Random().nextInt(), intent, PendingIntent.FLAG_IMMUTABLE) builder.addAction(NotificationCompat.Action.Builder(0, context.getString(R.string.notification_popup_action_browse), pendingIntent).build()) } } private fun maybeAddDownloadAction(builder: NotificationCompat.Builder, notification: Notification) { - if (notification.attachment?.contentUri == null && listOf(PROGRESS_NONE, PROGRESS_FAILED).contains(notification.attachment?.progress)) { + if (notification.attachment?.contentUri == null && listOf(ATTACHMENT_PROGRESS_NONE, ATTACHMENT_PROGRESS_FAILED).contains(notification.attachment?.progress)) { val intent = Intent(context, UserActionBroadcastReceiver::class.java).apply { putExtra(BROADCAST_EXTRA_TYPE, BROADCAST_TYPE_DOWNLOAD_START) putExtra(BROADCAST_EXTRA_NOTIFICATION_ID, notification.id) } - val pendingIntent = PendingIntent.getBroadcast(context, 0, 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) builder.addAction(NotificationCompat.Action.Builder(0, context.getString(R.string.notification_popup_action_download), pendingIntent).build()) } } @@ -191,47 +203,51 @@ class NotificationService(val context: Context) { putExtra(BROADCAST_EXTRA_TYPE, BROADCAST_TYPE_DOWNLOAD_CANCEL) putExtra(BROADCAST_EXTRA_NOTIFICATION_ID, notification.id) } - val pendingIntent = PendingIntent.getBroadcast(context, 0, 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) builder.addAction(NotificationCompat.Action.Builder(0, context.getString(R.string.notification_popup_action_cancel), pendingIntent).build()) } } - private fun maybeAddCustomActions(builder: NotificationCompat.Builder, notification: Notification) { + private fun maybeAddUserActions(builder: NotificationCompat.Builder, notification: Notification) { notification.actions?.forEach { action -> when (action.action.lowercase(Locale.getDefault())) { ACTION_VIEW -> maybeAddViewUserAction(builder, action) - ACTION_HTTP -> maybeAddHttpUserAction(builder, notification, action) + ACTION_HTTP, ACTION_BROADCAST -> maybeAddHttpOrBroadcastUserAction(builder, notification, action) } } } private fun maybeAddViewUserAction(builder: NotificationCompat.Builder, action: Action) { - Log.d(TAG, "Adding user action $action") try { val url = action.url ?: return val intent = Intent(Intent.ACTION_VIEW, Uri.parse(url)).apply { addFlags(Intent.FLAG_GRANT_READ_URI_PERMISSION) } - val pendingIntent = PendingIntent.getActivity(context, 0, intent, PendingIntent.FLAG_IMMUTABLE) + val pendingIntent = PendingIntent.getActivity(context, Random().nextInt(), intent, PendingIntent.FLAG_IMMUTABLE) builder.addAction(NotificationCompat.Action.Builder(0, action.label, pendingIntent).build()) } catch (e: Exception) { Log.w(TAG, "Unable to add open user action", e) } } - private fun maybeAddHttpUserAction(builder: NotificationCompat.Builder, notification: Notification, action: Action) { + private fun maybeAddHttpOrBroadcastUserAction(builder: NotificationCompat.Builder, notification: Notification, action: Action) { val intent = Intent(context, UserActionBroadcastReceiver::class.java).apply { putExtra(BROADCAST_EXTRA_TYPE, BROADCAST_TYPE_USER_ACTION) putExtra(BROADCAST_EXTRA_NOTIFICATION_ID, notification.id) putExtra(BROADCAST_EXTRA_ACTION_ID, action.id) } - val pendingIntent = PendingIntent.getBroadcast(context, 0, intent, PendingIntent.FLAG_UPDATE_CURRENT or PendingIntent.FLAG_IMMUTABLE) - builder.addAction(NotificationCompat.Action.Builder(0, action.label, pendingIntent).build()) + val pendingIntent = PendingIntent.getBroadcast(context, Random().nextInt(), intent, PendingIntent.FLAG_UPDATE_CURRENT or PendingIntent.FLAG_IMMUTABLE) + val label = when (action.progress) { + 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()) } class UserActionBroadcastReceiver : BroadcastReceiver() { override fun onReceive(context: Context, intent: Intent) { - Log.d(TAG, "Notification user action intent received: $intent") val type = intent.getStringExtra(BROADCAST_EXTRA_TYPE) ?: return val notificationId = intent.getStringExtra(BROADCAST_EXTRA_NOTIFICATION_ID) ?: return when (type) { @@ -306,19 +322,19 @@ class NotificationService(val context: Context) { } companion object { + val ACTION_VIEW = "view" + val ACTION_HTTP = "http" + val ACTION_BROADCAST = "broadcast" + 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_EXTRA_ACTION_JSON = "actionJson" 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" - - private const val ACTION_VIEW = "view" - private const val ACTION_HTTP = "http" + 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_LOW = "ntfy-low" diff --git a/app/src/main/java/io/heckel/ntfy/msg/UserActionWorker.kt b/app/src/main/java/io/heckel/ntfy/msg/UserActionWorker.kt index d53c353..b6fba90 100644 --- a/app/src/main/java/io/heckel/ntfy/msg/UserActionWorker.kt +++ b/app/src/main/java/io/heckel/ntfy/msg/UserActionWorker.kt @@ -3,55 +3,99 @@ package io.heckel.ntfy.msg import android.content.Context import androidx.work.Worker import androidx.work.WorkerParameters +import io.heckel.ntfy.R import io.heckel.ntfy.app.Application -import io.heckel.ntfy.db.Action +import io.heckel.ntfy.db.* +import io.heckel.ntfy.msg.NotificationService.Companion.ACTION_BROADCAST +import io.heckel.ntfy.msg.NotificationService.Companion.ACTION_HTTP import io.heckel.ntfy.util.Log import okhttp3.OkHttpClient import okhttp3.Request import okhttp3.RequestBody.Companion.toRequestBody +import java.util.* import java.util.concurrent.TimeUnit class UserActionWorker(private val context: Context, params: WorkerParameters) : Worker(context, params) { private val client = OkHttpClient.Builder() - .callTimeout(15, TimeUnit.MINUTES) // Total timeout for entire request + .callTimeout(60, TimeUnit.SECONDS) // Total timeout for entire request .connectTimeout(15, TimeUnit.SECONDS) .readTimeout(15, TimeUnit.SECONDS) .writeTimeout(15, TimeUnit.SECONDS) .build() + private val notifier = NotificationService(context) + private val broadcaster = BroadcastService(context) + private lateinit var repository: Repository + private lateinit var subscription: Subscription + private lateinit var notification: Notification + private lateinit var action: Action override fun doWork(): Result { if (context.applicationContext !is Application) return Result.failure() val notificationId = inputData.getString(INPUT_DATA_NOTIFICATION_ID) ?: return Result.failure() val actionId = inputData.getString(INPUT_DATA_ACTION_ID) ?: return Result.failure() val app = context.applicationContext as Application - val notification = app.repository.getNotification(notificationId) ?: return Result.failure() - val action = notification.actions?.first { it.id == actionId } ?: return Result.failure() + + repository = app.repository + notification = repository.getNotification(notificationId) ?: return Result.failure() + subscription = repository.getSubscription(notification.subscriptionId) ?: return Result.failure() + action = notification.actions?.first { it.id == actionId } ?: return Result.failure() Log.d(TAG, "Executing action $action for notification $notification") - http(context, action) - + try { + when (action.action) { + ACTION_HTTP -> performHttpAction(action) + ACTION_BROADCAST -> performBroadcastAction(action) + } + } catch (e: Exception) { + Log.w(TAG, "Error executing action: ${e.message}", e) + save(action.copy( + progress = ACTION_PROGRESS_FAILED, + error = context.getString(R.string.notification_popup_user_action_failed, action.label, e.message) + )) + } return Result.success() } + private fun performHttpAction(action: Action) { + save(action.copy(progress = ACTION_PROGRESS_ONGOING, error = null)) - fun http(context: Context, action: Action) { // FIXME Worker! val url = action.url ?: return - val method = action.method ?: "GET" + val method = action.method ?: "POST" // (not GET, because POST as a default makes more sense!) val body = action.body ?: "" - Log.d(TAG, "HTTP POST againt ${action.url}") - val request = Request.Builder() + val builder = Request.Builder() .url(url) - .addHeader("User-Agent", ApiService.USER_AGENT) .method(method, body.toRequestBody()) - .build() + .addHeader("User-Agent", ApiService.USER_AGENT) + action.headers?.forEach { (key, value) -> + builder.addHeader(key, value) + } + val request = builder.build() + + Log.d(TAG, "Executing HTTP request: ${method.uppercase(Locale.getDefault())} ${action.url}") client.newCall(request).execute().use { response -> if (response.isSuccessful) { + save(action.copy(progress = ACTION_PROGRESS_SUCCESS, error = null)) return } - throw Exception("Unexpected server response ${response.code}") + throw Exception("HTTP ${response.code}") } } + private fun performBroadcastAction(action: Action) { + broadcaster.sendUserAction(action) + save(action.copy(progress = ACTION_PROGRESS_SUCCESS, error = null)) + } + + private fun save(newAction: Action) { + Log.d(TAG, "Updating action: $newAction") + val newActions = notification.actions?.map { a -> if (a.id == newAction.id) newAction else a } + val newNotification = notification.copy(actions = newActions) + action = newAction + notification = newNotification + notifier.update(subscription, notification) + repository.updateNotification(notification) + } + companion object { const val INPUT_DATA_NOTIFICATION_ID = "notificationId" const val INPUT_DATA_ACTION_ID = "actionId" 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 4ff5e63..64a361d 100644 --- a/app/src/main/java/io/heckel/ntfy/ui/DetailAdapter.kt +++ b/app/src/main/java/io/heckel/ntfy/ui/DetailAdapter.kt @@ -217,10 +217,10 @@ class DetailAdapter(private val activity: Activity, private val repository: Repo private fun formatAttachmentDetails(context: Context, attachment: Attachment, exists: Boolean): String { val name = attachment.name - val notYetDownloaded = !exists && attachment.progress == PROGRESS_NONE + val notYetDownloaded = !exists && attachment.progress == ATTACHMENT_PROGRESS_NONE val downloading = !exists && attachment.progress in 0..99 - val deleted = !exists && (attachment.progress == PROGRESS_DONE || attachment.progress == PROGRESS_DELETED) - val failed = !exists && attachment.progress == PROGRESS_FAILED + val deleted = !exists && (attachment.progress == ATTACHMENT_PROGRESS_DONE || attachment.progress == ATTACHMENT_PROGRESS_DELETED) + val failed = !exists && attachment.progress == ATTACHMENT_PROGRESS_FAILED val expired = attachment.expires != null && attachment.expires < System.currentTimeMillis()/1000 val expires = attachment.expires != null && attachment.expires > System.currentTimeMillis()/1000 val infos = mutableListOf() @@ -357,7 +357,7 @@ class DetailAdapter(private val activity: Activity, private val repository: Repo if (!deleted) throw Exception("no rows deleted") val newAttachment = attachment.copy( contentUri = null, - progress = PROGRESS_DELETED + progress = ATTACHMENT_PROGRESS_DELETED ) val newNotification = notification.copy(attachment = newAttachment) GlobalScope.launch(Dispatchers.IO) { diff --git a/app/src/main/java/io/heckel/ntfy/work/DeleteWorker.kt b/app/src/main/java/io/heckel/ntfy/work/DeleteWorker.kt index ee3772b..bd2c642 100644 --- a/app/src/main/java/io/heckel/ntfy/work/DeleteWorker.kt +++ b/app/src/main/java/io/heckel/ntfy/work/DeleteWorker.kt @@ -5,7 +5,7 @@ import android.net.Uri import androidx.work.CoroutineWorker import androidx.work.WorkerParameters import io.heckel.ntfy.BuildConfig -import io.heckel.ntfy.db.PROGRESS_DELETED +import io.heckel.ntfy.db.ATTACHMENT_PROGRESS_DELETED import io.heckel.ntfy.db.Repository import io.heckel.ntfy.ui.DetailAdapter import io.heckel.ntfy.util.Log @@ -48,7 +48,7 @@ class DeleteWorker(ctx: Context, params: WorkerParameters) : CoroutineWorker(ctx } val newAttachment = attachment.copy( contentUri = null, - progress = PROGRESS_DELETED + progress = ATTACHMENT_PROGRESS_DELETED ) val newNotification = notification.copy(attachment = newAttachment) repository.updateNotification(newNotification) diff --git a/app/src/main/res/values/strings.xml b/app/src/main/res/values/strings.xml index 6562e40..6c01467 100644 --- a/app/src/main/res/values/strings.xml +++ b/app/src/main/res/values/strings.xml @@ -220,6 +220,7 @@ Downloading %1$s, %2$d%%\n%3$s %1$s\nFile: %2$s, downloaded %1$s\nFile: %2$s, download failed + "%1$s" failed: %2$s Settings