From f15d7654c8b5d67097c0ff3243de4d5ea1cab99f Mon Sep 17 00:00:00 2001 From: Philipp Heckel Date: Fri, 22 Apr 2022 22:45:33 -0400 Subject: [PATCH] Add clear=true support --- app/src/main/AndroidManifest.xml | 1 + .../java/io/heckel/ntfy/backup/Backuper.kt | 4 ++- .../main/java/io/heckel/ntfy/db/Database.kt | 1 + .../main/java/io/heckel/ntfy/msg/Message.kt | 2 +- .../io/heckel/ntfy/msg/NotificationParser.kt | 30 ++++++++++++++-- .../io/heckel/ntfy/msg/NotificationService.kt | 29 ++++++++++------ .../io/heckel/ntfy/msg/UserActionWorker.kt | 34 ++++++++++++++++++- 7 files changed, 86 insertions(+), 15 deletions(-) diff --git a/app/src/main/AndroidManifest.xml b/app/src/main/AndroidManifest.xml index e5be012..a8f0279 100644 --- a/app/src/main/AndroidManifest.xml +++ b/app/src/main/AndroidManifest.xml @@ -10,6 +10,7 @@ + ?, // used in "http" action 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 2c6545b..60c4fa9 100644 --- a/app/src/main/java/io/heckel/ntfy/db/Database.kt +++ b/app/src/main/java/io/heckel/ntfy/db/Database.kt @@ -87,6 +87,7 @@ 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, // "view", "http" or "broadcast" @ColumnInfo(name = "label") val label: String, + @ColumnInfo(name = "clear") val clear: Boolean?, // clear notification after successful execution @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 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 e2fcd76..04289ae 100644 --- a/app/src/main/java/io/heckel/ntfy/msg/Message.kt +++ b/app/src/main/java/io/heckel/ntfy/msg/Message.kt @@ -1,7 +1,6 @@ package io.heckel.ntfy.msg import androidx.annotation.Keep -import io.heckel.ntfy.db.Action /* This annotation ensures that proguard still works in production builds, * see https://stackoverflow.com/a/62753300/1440785 */ @@ -35,6 +34,7 @@ data class MessageAction( val id: String, val action: String, val label: String, // "view", "broadcast" or "http" + val clear: Boolean?, // clear notification after successful execution val url: String?, // used in "view" and "http" actions val method: String?, // used in "http" action, default is POST (!) val headers: Map?, // used in "http" action 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 b855d24..0faaf23 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,20 @@ 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, a.intent, a.extras, null, null) + Action( + id = a.id, + action = a.action, + label = a.label, + clear = a.clear, + url = a.url, + method = a.method, + headers = a.headers, + body = a.body, + intent = a.intent, + extras = a.extras, + progress = null, + error = null + ) } } else null val notification = Notification( @@ -62,7 +75,20 @@ class NotificationParser { val listType: Type = object : TypeToken?>() {}.type val messageActions: List? = 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) + Action( + id = a.id, + action = a.action, + label = a.label, + clear = a.clear, + url = a.url, + method = a.method, + headers = a.headers, + body = a.body, + intent = a.intent, + extras = a.extras, + progress = null, + error = null + ) } } 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 f612993..e0b2e94 100644 --- a/app/src/main/java/io/heckel/ntfy/msg/NotificationService.kt +++ b/app/src/main/java/io/heckel/ntfy/msg/NotificationService.kt @@ -201,18 +201,27 @@ class NotificationService(val context: Context) { 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, ACTION_BROADCAST -> maybeAddHttpOrBroadcastUserAction(builder, notification, action) + // ACTION_VIEW weirdness: + // It's apparently impossible to start an activity from PendingIntent.getActivity() and also close + // the notification. To clear it, we have to actually run our own code, which we do via the UserActionWorker. + // However, Android has a weird bug that does not allow a BroadcastReceiver or Worker to start an activity + // in the foreground and also close the notification drawer, without sending a deprecated Intent. So to not + // have to use this deprecated code in the majority case, we do this weird viewActionWithoutClear below. + // + // See https://stackoverflow.com/questions/18261969/clicking-android-notification-actions-does-not-close-notification-drawer + + val actionType = action.action.lowercase(Locale.getDefault()) + val viewActionWithoutClear = actionType == ACTION_VIEW && action.clear != true + if (viewActionWithoutClear) { + addViewUserActionWithoutClear(builder, action) + } else { + addHttpOrBroadcastUserAction(builder, notification, 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. - + private fun addViewUserActionWithoutClear(builder: NotificationCompat.Builder, action: Action) { + Log.d(TAG, "Adding view action (no clear) for ${action.url}") try { val url = action.url ?: return val intent = Intent(Intent.ACTION_VIEW, Uri.parse(url)).apply { @@ -225,7 +234,7 @@ class NotificationService(val context: Context) { } } - private fun maybeAddHttpOrBroadcastUserAction(builder: NotificationCompat.Builder, notification: Notification, action: Action) { + private fun addHttpOrBroadcastUserAction(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) @@ -318,7 +327,7 @@ class NotificationService(val context: Context) { const val BROADCAST_EXTRA_TYPE = "type" const val BROADCAST_EXTRA_NOTIFICATION_ID = "notificationId" - const val BROADCAST_EXTRA_ACTION_ID = "action" + const val BROADCAST_EXTRA_ACTION_ID = "actionId" const val BROADCAST_TYPE_DOWNLOAD_START = "io.heckel.ntfy.DOWNLOAD_ACTION_START" const val BROADCAST_TYPE_DOWNLOAD_CANCEL = "io.heckel.ntfy.DOWNLOAD_ACTION_CANCEL" 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 d505813..6cdca33 100644 --- a/app/src/main/java/io/heckel/ntfy/msg/UserActionWorker.kt +++ b/app/src/main/java/io/heckel/ntfy/msg/UserActionWorker.kt @@ -1,6 +1,8 @@ package io.heckel.ntfy.msg import android.content.Context +import android.content.Intent +import android.net.Uri import androidx.work.Worker import androidx.work.WorkerParameters import io.heckel.ntfy.R @@ -8,6 +10,7 @@ import io.heckel.ntfy.app.Application 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.msg.NotificationService.Companion.ACTION_VIEW import io.heckel.ntfy.util.Log import okhttp3.OkHttpClient import okhttp3.Request @@ -46,6 +49,7 @@ class UserActionWorker(private val context: Context, params: WorkerParameters) : // ACTION_VIEW is not handled here. It has to be handled in the foreground to avoid // weird Android behavior. + ACTION_VIEW -> performViewAction(action) ACTION_BROADCAST -> performBroadcastAction(action) ACTION_HTTP -> performHttpAction(action) } @@ -59,8 +63,31 @@ class UserActionWorker(private val context: Context, params: WorkerParameters) : return Result.success() } + private fun performViewAction(action: Action) { + val intent = Intent(Intent.ACTION_VIEW, Uri.parse(action.url)).apply { + addFlags(Intent.FLAG_GRANT_READ_URI_PERMISSION or Intent.FLAG_ACTIVITY_NEW_TASK) + } + context.startActivity(intent) + if (action.clear == true) { + notifier.cancel(notification) + } + + // Close notification drawer. This seems to be a bug in Android that when a new activity is started from + // a receiver or worker, the drawer does not close. Using this deprecated intent is the only option I have found. + // + // See https://stackoverflow.com/questions/18261969/clicking-android-notification-actions-does-not-close-notification-drawer + try { + context.sendBroadcast(Intent(Intent.ACTION_CLOSE_SYSTEM_DIALOGS)); + } catch (e: Exception) { + Log.w(TAG, "Cannot close system dialogs", e) + } + } + private fun performBroadcastAction(action: Action) { broadcaster.sendUserAction(action) + if (action.clear == true) { + notifier.cancel(notification) + } } private fun performHttpAction(action: Action) { @@ -90,12 +117,17 @@ class UserActionWorker(private val context: Context, params: WorkerParameters) : private fun save(newAction: Action) { Log.d(TAG, "Updating action: $newAction") + val clear = newAction.progress == ACTION_PROGRESS_SUCCESS && action.clear == true 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) + if (clear) { + notifier.cancel(notification) + } else { + notifier.update(subscription, notification) + } } companion object {