Add clear=true support

This commit is contained in:
Philipp Heckel 2022-04-22 22:45:33 -04:00
parent b2c6abcfd1
commit f15d7654c8
7 changed files with 86 additions and 15 deletions

View file

@ -10,6 +10,7 @@
<uses-permission android:name="android.permission.WRITE_EXTERNAL_STORAGE" android:maxSdkVersion="28"/> <!-- Only required on SDK <= 28 -->
<uses-permission android:name="android.permission.REQUEST_INSTALL_PACKAGES"/> <!-- To install packages downloaded through ntfy; craazyy! -->
<uses-permission android:name="android.permission.SCHEDULE_EXACT_ALARM"/> <!-- To reschedule the websocket retry -->
<uses-permission android:name="android.permission.BROADCAST_CLOSE_SYSTEM_DIALOGS"/> <!-- To close the notification drawer, see UserActionWorker -->
<application
android:name=".app.Application"

View file

@ -2,7 +2,6 @@ package io.heckel.ntfy.backup
import android.content.Context
import android.net.Uri
import androidx.room.ColumnInfo
import com.google.gson.Gson
import com.google.gson.GsonBuilder
import com.google.gson.stream.JsonReader
@ -116,6 +115,7 @@ class Backuper(val context: Context) {
id = a.id,
action = a.action,
label = a.label,
clear = a.clear,
url = a.url,
method = a.method,
headers = a.headers,
@ -228,6 +228,7 @@ class Backuper(val context: Context) {
id = a.id,
action = a.action,
label = a.label,
clear = a.clear,
url = a.url,
method = a.method,
headers = a.headers,
@ -340,6 +341,7 @@ 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 clear: Boolean?, // clear notification after successful execution
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

View file

@ -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<String,String>?, // used in "http" action

View file

@ -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<String,String>?, // used in "http" action

View file

@ -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<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)
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
)
}
}

View file

@ -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"

View file

@ -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 {