This commit is contained in:
Philipp Heckel 2022-04-17 14:29:29 -04:00
parent f62b7fa952
commit 686616d4d2
8 changed files with 357 additions and 72 deletions

View file

@ -0,0 +1,302 @@
{
"formatVersion": 1,
"database": {
"version": 10,
"identityHash": "c1b4f54d1d3111dc5c8f02e8fa960ceb",
"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, `upAppId` TEXT, `upConnectorToken` TEXT, 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
},
{
"fieldPath": "upAppId",
"columnName": "upAppId",
"affinity": "TEXT",
"notNull": false
},
{
"fieldPath": "upConnectorToken",
"columnName": "upConnectorToken",
"affinity": "TEXT",
"notNull": false
}
],
"primaryKey": {
"columnNames": [
"id"
],
"autoGenerate": false
},
"indices": [
{
"name": "index_Subscription_baseUrl_topic",
"unique": true,
"columnNames": [
"baseUrl",
"topic"
],
"orders": [],
"createSql": "CREATE UNIQUE INDEX IF NOT EXISTS `index_Subscription_baseUrl_topic` ON `${TABLE_NAME}` (`baseUrl`, `topic`)"
},
{
"name": "index_Subscription_upConnectorToken",
"unique": true,
"columnNames": [
"upConnectorToken"
],
"orders": [],
"createSql": "CREATE UNIQUE INDEX IF NOT EXISTS `index_Subscription_upConnectorToken` ON `${TABLE_NAME}` (`upConnectorToken`)"
}
],
"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, `encoding` TEXT NOT NULL, `notificationId` INTEGER NOT NULL, `priority` INTEGER NOT NULL DEFAULT 3, `tags` TEXT NOT NULL, `click` TEXT NOT NULL, `actions` TEXT, `deleted` INTEGER NOT NULL, `attachment_name` TEXT, `attachment_type` TEXT, `attachment_size` INTEGER, `attachment_expires` INTEGER, `attachment_url` TEXT, `attachment_contentUri` TEXT, `attachment_progress` INTEGER, 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": "encoding",
"columnName": "encoding",
"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": "click",
"columnName": "click",
"affinity": "TEXT",
"notNull": true
},
{
"fieldPath": "actions",
"columnName": "actions",
"affinity": "TEXT",
"notNull": false
},
{
"fieldPath": "deleted",
"columnName": "deleted",
"affinity": "INTEGER",
"notNull": true
},
{
"fieldPath": "attachment.name",
"columnName": "attachment_name",
"affinity": "TEXT",
"notNull": false
},
{
"fieldPath": "attachment.type",
"columnName": "attachment_type",
"affinity": "TEXT",
"notNull": false
},
{
"fieldPath": "attachment.size",
"columnName": "attachment_size",
"affinity": "INTEGER",
"notNull": false
},
{
"fieldPath": "attachment.expires",
"columnName": "attachment_expires",
"affinity": "INTEGER",
"notNull": false
},
{
"fieldPath": "attachment.url",
"columnName": "attachment_url",
"affinity": "TEXT",
"notNull": false
},
{
"fieldPath": "attachment.contentUri",
"columnName": "attachment_contentUri",
"affinity": "TEXT",
"notNull": false
},
{
"fieldPath": "attachment.progress",
"columnName": "attachment_progress",
"affinity": "INTEGER",
"notNull": false
}
],
"primaryKey": {
"columnNames": [
"id",
"subscriptionId"
],
"autoGenerate": false
},
"indices": [],
"foreignKeys": []
},
{
"tableName": "User",
"createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`baseUrl` TEXT NOT NULL, `username` TEXT NOT NULL, `password` TEXT NOT NULL, PRIMARY KEY(`baseUrl`))",
"fields": [
{
"fieldPath": "baseUrl",
"columnName": "baseUrl",
"affinity": "TEXT",
"notNull": true
},
{
"fieldPath": "username",
"columnName": "username",
"affinity": "TEXT",
"notNull": true
},
{
"fieldPath": "password",
"columnName": "password",
"affinity": "TEXT",
"notNull": true
}
],
"primaryKey": {
"columnNames": [
"baseUrl"
],
"autoGenerate": false
},
"indices": [],
"foreignKeys": []
},
{
"tableName": "Log",
"createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`id` INTEGER PRIMARY KEY AUTOINCREMENT NOT NULL, `timestamp` INTEGER NOT NULL, `tag` TEXT NOT NULL, `level` INTEGER NOT NULL, `message` TEXT NOT NULL, `exception` TEXT)",
"fields": [
{
"fieldPath": "id",
"columnName": "id",
"affinity": "INTEGER",
"notNull": true
},
{
"fieldPath": "timestamp",
"columnName": "timestamp",
"affinity": "INTEGER",
"notNull": true
},
{
"fieldPath": "tag",
"columnName": "tag",
"affinity": "TEXT",
"notNull": true
},
{
"fieldPath": "level",
"columnName": "level",
"affinity": "INTEGER",
"notNull": true
},
{
"fieldPath": "message",
"columnName": "message",
"affinity": "TEXT",
"notNull": true
},
{
"fieldPath": "exception",
"columnName": "exception",
"affinity": "TEXT",
"notNull": false
}
],
"primaryKey": {
"columnNames": [
"id"
],
"autoGenerate": true
},
"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, 'c1b4f54d1d3111dc5c8f02e8fa960ceb')"
]
}
}

View file

@ -2,7 +2,7 @@
"formatVersion": 1,
"database": {
"version": 9,
"identityHash": "c1b4f54d1d3111dc5c8f02e8fa960ceb",
"identityHash": "5bab75c3b41c53c9855fe3a7ef8f0669",
"entities": [
{
"tableName": "Subscription",
@ -82,7 +82,7 @@
},
{
"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, `encoding` TEXT NOT NULL, `notificationId` INTEGER NOT NULL, `priority` INTEGER NOT NULL DEFAULT 3, `tags` TEXT NOT NULL, `click` TEXT NOT NULL, `actions` TEXT, `deleted` INTEGER NOT NULL, `attachment_name` TEXT, `attachment_type` TEXT, `attachment_size` INTEGER, `attachment_expires` INTEGER, `attachment_url` TEXT, `attachment_contentUri` TEXT, `attachment_progress` INTEGER, PRIMARY KEY(`id`, `subscriptionId`))",
"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, `encoding` TEXT NOT NULL, `notificationId` INTEGER NOT NULL, `priority` INTEGER NOT NULL DEFAULT 3, `tags` TEXT NOT NULL, `click` TEXT NOT NULL, `deleted` INTEGER NOT NULL, `attachment_name` TEXT, `attachment_type` TEXT, `attachment_size` INTEGER, `attachment_expires` INTEGER, `attachment_url` TEXT, `attachment_contentUri` TEXT, `attachment_progress` INTEGER, PRIMARY KEY(`id`, `subscriptionId`))",
"fields": [
{
"fieldPath": "id",
@ -145,12 +145,6 @@
"affinity": "TEXT",
"notNull": true
},
{
"fieldPath": "actions",
"columnName": "actions",
"affinity": "TEXT",
"notNull": false
},
{
"fieldPath": "deleted",
"columnName": "deleted",
@ -296,7 +290,7 @@
"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, 'c1b4f54d1d3111dc5c8f02e8fa960ceb')"
"INSERT OR REPLACE INTO room_master_table (id,identity_hash) VALUES(42, '5bab75c3b41c53c9855fe3a7ef8f0669')"
]
}
}

View file

@ -78,9 +78,13 @@ data class Attachment(
@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 = "label") val label: String,
@ColumnInfo(name = "url") val url: 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<String,String>?, // used in "http"
@ColumnInfo(name = "body") val body: String?, // used in "http"
)
class Converters {
@ -126,7 +130,7 @@ data class LogEntry(
this(0, timestamp, tag, level, message, exception)
}
@androidx.room.Database(entities = [Subscription::class, Notification::class, User::class, LogEntry::class], version = 9)
@androidx.room.Database(entities = [Subscription::class, Notification::class, User::class, LogEntry::class], version = 10)
@TypeConverters(Converters::class)
abstract class Database : RoomDatabase() {
abstract fun subscriptionDao(): SubscriptionDao

View file

@ -32,9 +32,13 @@ data class MessageAttachment(
@Keep
data class MessageAction(
val id: String,
val action: String,
val label: String,
val url: String?,
val url: String?, // used in "view" and "http"
val method: String?, // used in "http"
val headers: Map<String,String>?, // used in "http"
val body: String?, // used in "http"
)
const val MESSAGE_ENCODING_BASE64 = "base64"

View file

@ -6,6 +6,7 @@ import io.heckel.ntfy.db.Action
import io.heckel.ntfy.db.Attachment
import io.heckel.ntfy.db.Notification
import io.heckel.ntfy.util.joinTags
import io.heckel.ntfy.util.randomString
import io.heckel.ntfy.util.toPriority
class NotificationParser {
@ -31,8 +32,8 @@ class NotificationParser {
)
} else null
val actions = if (message.actions != null) {
message.actions.map { action ->
Action(action.action, action.label, action.url)
message.actions.map { a ->
Action(a.id, a.action, a.label, a.url, a.method, a.headers, a.body)
}
} else null
val notification = Notification(

View file

@ -16,6 +16,7 @@ import io.heckel.ntfy.ui.Colors
import io.heckel.ntfy.ui.DetailActivity
import io.heckel.ntfy.ui.MainActivity
import io.heckel.ntfy.util.*
import java.util.*
class NotificationService(val context: Context) {
private val notificationManager = context.getSystemService(Context.NOTIFICATION_SERVICE) as NotificationManager
@ -197,9 +198,9 @@ class NotificationService(val context: Context) {
private fun maybeAddCustomActions(builder: NotificationCompat.Builder, notification: Notification) {
notification.actions?.forEach { action ->
when (action.action) {
"view" -> maybeAddViewUserAction(builder, action)
"http-post" -> maybeAddHttpPostUserAction(builder, notification, action)
when (action.action.lowercase(Locale.getDefault())) {
ACTION_VIEW -> maybeAddViewUserAction(builder, action)
ACTION_HTTP -> maybeAddHttpUserAction(builder, notification, action)
}
}
}
@ -218,12 +219,11 @@ class NotificationService(val context: Context) {
}
}
private fun maybeAddHttpPostUserAction(builder: NotificationCompat.Builder, notification: Notification, action: Action) {
private fun maybeAddHttpUserAction(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_TYPE, BROADCAST_TYPE_HTTP)
putExtra(BROADCAST_EXTRA_ACTION, action.action)
putExtra(BROADCAST_EXTRA_URL, action.url)
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())
@ -231,27 +231,16 @@ class NotificationService(val context: Context) {
class UserActionBroadcastReceiver : BroadcastReceiver() {
override fun onReceive(context: Context, intent: Intent) {
Log.d(TAG, "Received $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) {
BROADCAST_TYPE_DOWNLOAD_START, BROADCAST_TYPE_DOWNLOAD_CANCEL -> handleDownloadAction(context, type, notificationId)
BROADCAST_TYPE_HTTP -> handleCustomUserAction(context, intent, type, notificationId)
}
}
private fun handleDownloadAction(context: Context, type: String, notificationId: String) {
when (type) {
BROADCAST_TYPE_DOWNLOAD_START -> DownloadManager.enqueue(context, notificationId, userAction = true)
BROADCAST_TYPE_DOWNLOAD_CANCEL -> DownloadManager.cancel(context, notificationId)
}
}
private fun handleCustomUserAction(context: Context, intent: Intent, type: String, notificationId: String) {
val action = intent.getStringExtra(BROADCAST_EXTRA_ACTION) ?: return
val url = intent.getStringExtra(BROADCAST_EXTRA_URL) ?: return
when (type) {
BROADCAST_TYPE_HTTP -> UserActionManager.enqueue(context, notificationId, action, url)
BROADCAST_TYPE_USER_ACTION -> {
val actionId = intent.getStringExtra(BROADCAST_EXTRA_ACTION_ID) ?: return
UserActionManager.enqueue(context, notificationId, actionId)
}
}
}
}
@ -321,12 +310,15 @@ class NotificationService(val context: Context) {
private const val BROADCAST_EXTRA_TYPE = "type"
private const val BROADCAST_EXTRA_NOTIFICATION_ID = "notificationId"
private const val BROADCAST_EXTRA_ACTION = "action"
private const val BROADCAST_EXTRA_URL = "url"
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_HTTP = "io.heckel.ntfy.USER_ACTION_HTTP"
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 CHANNEL_ID_MIN = "ntfy-min"
private const val CHANNEL_ID_LOW = "ntfy-low"

View file

@ -6,10 +6,6 @@ import androidx.work.OneTimeWorkRequest
import androidx.work.WorkManager
import androidx.work.workDataOf
import io.heckel.ntfy.util.Log
import okhttp3.OkHttpClient
import okhttp3.Request
import okhttp3.RequestBody.Companion.toRequestBody
import java.util.concurrent.TimeUnit
/**
* Trigger user actions clicked from notification popups.
@ -21,15 +17,14 @@ object UserActionManager {
private const val TAG = "NtfyUserActionEx"
private const val WORK_NAME_PREFIX = "io.heckel.ntfy.USER_ACTION_"
fun enqueue(context: Context, notificationId: String, action: String, url: String) {
fun enqueue(context: Context, notificationId: String, actionId: String) {
val workManager = WorkManager.getInstance(context)
val workName = WORK_NAME_PREFIX + notificationId + action + url
Log.d(TAG,"Enqueuing work to execute user action for notification $notificationId, work: $workName")
val workName = WORK_NAME_PREFIX + notificationId + "_" + actionId
Log.d(TAG,"Enqueuing work to execute user action for notification $notificationId, action $actionId, work: $workName")
val workRequest = OneTimeWorkRequest.Builder(UserActionWorker::class.java)
.setInputData(workDataOf(
UserActionWorker.INPUT_DATA_ID to notificationId,
UserActionWorker.INPUT_DATA_ACTION to action,
UserActionWorker.INPUT_DATA_URL to url,
UserActionWorker.INPUT_DATA_NOTIFICATION_ID to notificationId,
UserActionWorker.INPUT_DATA_ACTION_ID to actionId,
))
.build()
workManager.enqueueUniqueWork(workName, ExistingWorkPolicy.KEEP, workRequest)

View file

@ -1,25 +1,14 @@
package io.heckel.ntfy.msg
import android.content.Context
import android.net.Uri
import android.os.Handler
import android.os.Looper
import android.webkit.MimeTypeMap
import android.widget.Toast
import androidx.core.content.FileProvider
import androidx.work.Worker
import androidx.work.WorkerParameters
import io.heckel.ntfy.BuildConfig
import io.heckel.ntfy.R
import io.heckel.ntfy.app.Application
import io.heckel.ntfy.db.*
import io.heckel.ntfy.db.Action
import io.heckel.ntfy.util.Log
import io.heckel.ntfy.util.ensureSafeNewFile
import okhttp3.OkHttpClient
import okhttp3.Request
import okhttp3.RequestBody.Companion.toRequestBody
import okhttp3.Response
import java.io.File
import java.util.concurrent.TimeUnit
class UserActionWorker(private val context: Context, params: WorkerParameters) : Worker(context, params) {
@ -32,23 +21,28 @@ class UserActionWorker(private val context: Context, params: WorkerParameters) :
override fun doWork(): Result {
if (context.applicationContext !is Application) return Result.failure()
val notificationId = inputData.getString(INPUT_DATA_ID) ?: return Result.failure()
val action = inputData.getString(INPUT_DATA_ACTION) ?: return Result.failure()
val url = inputData.getString(INPUT_DATA_URL) ?: 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()
http(context, url)
Log.d(TAG, "Executing action $action for notification $notification")
http(context, action)
return Result.success()
}
fun http(context: Context, url: String) { // FIXME Worker!
Log.d(TAG, "HTTP POST againt $url")
fun http(context: Context, action: Action) { // FIXME Worker!
val url = action.url ?: return
val method = action.method ?: "GET"
val body = action.body ?: ""
Log.d(TAG, "HTTP POST againt ${action.url}")
val request = Request.Builder()
.url(url)
.addHeader("User-Agent", ApiService.USER_AGENT)
.method("POST", "".toRequestBody())
.method(method, body.toRequestBody())
.build()
client.newCall(request).execute().use { response ->
if (response.isSuccessful) {
@ -59,9 +53,8 @@ class UserActionWorker(private val context: Context, params: WorkerParameters) :
}
companion object {
const val INPUT_DATA_ID = "id"
const val INPUT_DATA_ACTION = "action"
const val INPUT_DATA_URL = "url"
const val INPUT_DATA_NOTIFICATION_ID = "notificationId"
const val INPUT_DATA_ACTION_ID = "actionId"
private const val TAG = "NtfyUserActWrk"
}