Loading app/src/main/AndroidManifest.xml +1 −1 Original line number Diff line number Diff line Loading @@ -123,7 +123,7 @@ <!-- Broadcast receiver for the "Download"/"Cancel" attachment action in the notification popup --> <receiver android:name=".msg.NotificationService$DownloadBroadcastReceiver" android:name=".msg.NotificationService$UserActionBroadcastReceiver" android:enabled="true" android:exported="false"> </receiver> Loading app/src/main/java/io/heckel/ntfy/msg/ApiService.kt +0 −2 Original line number Diff line number Diff line package io.heckel.ntfy.msg import android.net.Uri import android.os.Build import io.heckel.ntfy.BuildConfig import io.heckel.ntfy.db.Notification Loading @@ -9,7 +8,6 @@ import io.heckel.ntfy.util.* import okhttp3.* import okhttp3.RequestBody.Companion.toRequestBody import java.io.IOException import java.net.URL import java.net.URLEncoder import java.nio.charset.StandardCharsets.UTF_8 import java.util.concurrent.TimeUnit Loading app/src/main/java/io/heckel/ntfy/msg/DownloadManager.kt +20 −23 Original line number Diff line number Diff line Loading @@ -13,8 +13,7 @@ import io.heckel.ntfy.util.Log * The indirection via WorkManager is required since this code may be executed * in a doze state and Internet may not be available. It's also best practice apparently. */ class DownloadManager { companion object { object DownloadManager { private const val TAG = "NtfyDownloadManager" private const val DOWNLOAD_WORK_NAME_PREFIX = "io.heckel.ntfy.DOWNLOAD_FILE_" Loading @@ -37,6 +36,4 @@ class DownloadManager { Log.d(TAG, "Cancelling download for notification $id, work: $workName") workManager.cancelUniqueWork(workName) } } } app/src/main/java/io/heckel/ntfy/msg/NotificationService.kt +64 −26 Original line number Diff line number Diff line Loading @@ -154,9 +154,10 @@ class NotificationService(val context: Context) { private fun maybeAddOpenAction(builder: NotificationCompat.Builder, notification: Notification) { if (notification.attachment?.contentUri != null) { val contentUri = Uri.parse(notification.attachment.contentUri) val intent = Intent(Intent.ACTION_VIEW, contentUri) intent.setDataAndType(contentUri, notification.attachment.type ?: "application/octet-stream") // Required for Android <= P intent.addFlags(Intent.FLAG_GRANT_READ_URI_PERMISSION) val intent = Intent(Intent.ACTION_VIEW, contentUri).apply { 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) builder.addAction(NotificationCompat.Action.Builder(0, context.getString(R.string.notification_popup_action_open), pendingIntent).build()) } Loading @@ -164,8 +165,9 @@ class NotificationService(val context: Context) { private fun maybeAddBrowseAction(builder: NotificationCompat.Builder, notification: Notification) { if (notification.attachment?.contentUri != null) { val intent = Intent(android.app.DownloadManager.ACTION_VIEW_DOWNLOADS) intent.addFlags(Intent.FLAG_GRANT_READ_URI_PERMISSION) 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) builder.addAction(NotificationCompat.Action.Builder(0, context.getString(R.string.notification_popup_action_browse), pendingIntent).build()) } Loading @@ -173,9 +175,10 @@ class NotificationService(val context: Context) { private fun maybeAddDownloadAction(builder: NotificationCompat.Builder, notification: Notification) { if (notification.attachment?.contentUri == null && listOf(PROGRESS_NONE, PROGRESS_FAILED).contains(notification.attachment?.progress)) { val intent = Intent(context, DownloadBroadcastReceiver::class.java) intent.putExtra("action", DOWNLOAD_ACTION_START) intent.putExtra("id", notification.id) 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) builder.addAction(NotificationCompat.Action.Builder(0, context.getString(R.string.notification_popup_action_download), pendingIntent).build()) } Loading @@ -183,9 +186,10 @@ class NotificationService(val context: Context) { private fun maybeAddCancelAction(builder: NotificationCompat.Builder, notification: Notification) { if (notification.attachment?.contentUri == null && notification.attachment?.progress in 0..99) { val intent = Intent(context, DownloadBroadcastReceiver::class.java) intent.putExtra("action", DOWNLOAD_ACTION_CANCEL) intent.putExtra("id", notification.id) val intent = Intent(context, UserActionBroadcastReceiver::class.java).apply { 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) builder.addAction(NotificationCompat.Action.Builder(0, context.getString(R.string.notification_popup_action_cancel), pendingIntent).build()) } Loading @@ -194,19 +198,19 @@ class NotificationService(val context: Context) { private fun maybeAddCustomActions(builder: NotificationCompat.Builder, notification: Notification) { notification.actions?.forEach { action -> when (action.action) { "view" -> maybeAddOpenUserAction(builder, notification, action) "view" -> maybeAddViewUserAction(builder, action) "http-post" -> maybeAddHttpPostUserAction(builder, notification, action) } } } private fun maybeAddOpenUserAction(builder: NotificationCompat.Builder, notification: Notification, action: Action) { private fun maybeAddViewUserAction(builder: NotificationCompat.Builder, action: Action) { Log.d(TAG, "Adding user action $action") val url = action.url ?: return try { val uri = Uri.parse(url) val intent = Intent(Intent.ACTION_VIEW, uri) intent.addFlags(Intent.FLAG_GRANT_READ_URI_PERMISSION) 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) builder.addAction(NotificationCompat.Action.Builder(0, action.label, pendingIntent).build()) } catch (e: Exception) { Loading @@ -214,13 +218,40 @@ class NotificationService(val context: Context) { } } class DownloadBroadcastReceiver : BroadcastReceiver() { private fun maybeAddHttpPostUserAction(builder: NotificationCompat.Builder, notification: Notification, action: Action) { val intent = Intent(context, UserActionBroadcastReceiver::class.java).apply { 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) } 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()) } class UserActionBroadcastReceiver : BroadcastReceiver() { override fun onReceive(context: Context, intent: Intent) { val id = intent.getStringExtra("id") ?: return val action = intent.getStringExtra("action") ?: return when (action) { DOWNLOAD_ACTION_START -> DownloadManager.enqueue(context, id, userAction = true) DOWNLOAD_ACTION_CANCEL -> DownloadManager.cancel(context, id) Log.d(TAG, "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) } } } Loading Loading @@ -287,8 +318,15 @@ class NotificationService(val context: Context) { companion object { private const val TAG = "NtfyNotifService" private const val DOWNLOAD_ACTION_START = "io.heckel.ntfy.DOWNLOAD_ACTION_START" private const val DOWNLOAD_ACTION_CANCEL = "io.heckel.ntfy.DOWNLOAD_ACTION_CANCEL" 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_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 CHANNEL_ID_MIN = "ntfy-min" private const val CHANNEL_ID_LOW = "ntfy-low" Loading app/src/main/java/io/heckel/ntfy/msg/UserActionManager.kt 0 → 100644 +37 −0 Original line number Diff line number Diff line package io.heckel.ntfy.msg import android.content.Context import androidx.work.ExistingWorkPolicy 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. * * The indirection via WorkManager is required since this code may be executed * in a doze state and Internet may not be available. It's also best practice, apparently. */ 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) { 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 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, )) .build() workManager.enqueueUniqueWork(workName, ExistingWorkPolicy.KEEP, workRequest) } } Loading
app/src/main/AndroidManifest.xml +1 −1 Original line number Diff line number Diff line Loading @@ -123,7 +123,7 @@ <!-- Broadcast receiver for the "Download"/"Cancel" attachment action in the notification popup --> <receiver android:name=".msg.NotificationService$DownloadBroadcastReceiver" android:name=".msg.NotificationService$UserActionBroadcastReceiver" android:enabled="true" android:exported="false"> </receiver> Loading
app/src/main/java/io/heckel/ntfy/msg/ApiService.kt +0 −2 Original line number Diff line number Diff line package io.heckel.ntfy.msg import android.net.Uri import android.os.Build import io.heckel.ntfy.BuildConfig import io.heckel.ntfy.db.Notification Loading @@ -9,7 +8,6 @@ import io.heckel.ntfy.util.* import okhttp3.* import okhttp3.RequestBody.Companion.toRequestBody import java.io.IOException import java.net.URL import java.net.URLEncoder import java.nio.charset.StandardCharsets.UTF_8 import java.util.concurrent.TimeUnit Loading
app/src/main/java/io/heckel/ntfy/msg/DownloadManager.kt +20 −23 Original line number Diff line number Diff line Loading @@ -13,8 +13,7 @@ import io.heckel.ntfy.util.Log * The indirection via WorkManager is required since this code may be executed * in a doze state and Internet may not be available. It's also best practice apparently. */ class DownloadManager { companion object { object DownloadManager { private const val TAG = "NtfyDownloadManager" private const val DOWNLOAD_WORK_NAME_PREFIX = "io.heckel.ntfy.DOWNLOAD_FILE_" Loading @@ -37,6 +36,4 @@ class DownloadManager { Log.d(TAG, "Cancelling download for notification $id, work: $workName") workManager.cancelUniqueWork(workName) } } }
app/src/main/java/io/heckel/ntfy/msg/NotificationService.kt +64 −26 Original line number Diff line number Diff line Loading @@ -154,9 +154,10 @@ class NotificationService(val context: Context) { private fun maybeAddOpenAction(builder: NotificationCompat.Builder, notification: Notification) { if (notification.attachment?.contentUri != null) { val contentUri = Uri.parse(notification.attachment.contentUri) val intent = Intent(Intent.ACTION_VIEW, contentUri) intent.setDataAndType(contentUri, notification.attachment.type ?: "application/octet-stream") // Required for Android <= P intent.addFlags(Intent.FLAG_GRANT_READ_URI_PERMISSION) val intent = Intent(Intent.ACTION_VIEW, contentUri).apply { 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) builder.addAction(NotificationCompat.Action.Builder(0, context.getString(R.string.notification_popup_action_open), pendingIntent).build()) } Loading @@ -164,8 +165,9 @@ class NotificationService(val context: Context) { private fun maybeAddBrowseAction(builder: NotificationCompat.Builder, notification: Notification) { if (notification.attachment?.contentUri != null) { val intent = Intent(android.app.DownloadManager.ACTION_VIEW_DOWNLOADS) intent.addFlags(Intent.FLAG_GRANT_READ_URI_PERMISSION) 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) builder.addAction(NotificationCompat.Action.Builder(0, context.getString(R.string.notification_popup_action_browse), pendingIntent).build()) } Loading @@ -173,9 +175,10 @@ class NotificationService(val context: Context) { private fun maybeAddDownloadAction(builder: NotificationCompat.Builder, notification: Notification) { if (notification.attachment?.contentUri == null && listOf(PROGRESS_NONE, PROGRESS_FAILED).contains(notification.attachment?.progress)) { val intent = Intent(context, DownloadBroadcastReceiver::class.java) intent.putExtra("action", DOWNLOAD_ACTION_START) intent.putExtra("id", notification.id) 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) builder.addAction(NotificationCompat.Action.Builder(0, context.getString(R.string.notification_popup_action_download), pendingIntent).build()) } Loading @@ -183,9 +186,10 @@ class NotificationService(val context: Context) { private fun maybeAddCancelAction(builder: NotificationCompat.Builder, notification: Notification) { if (notification.attachment?.contentUri == null && notification.attachment?.progress in 0..99) { val intent = Intent(context, DownloadBroadcastReceiver::class.java) intent.putExtra("action", DOWNLOAD_ACTION_CANCEL) intent.putExtra("id", notification.id) val intent = Intent(context, UserActionBroadcastReceiver::class.java).apply { 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) builder.addAction(NotificationCompat.Action.Builder(0, context.getString(R.string.notification_popup_action_cancel), pendingIntent).build()) } Loading @@ -194,19 +198,19 @@ class NotificationService(val context: Context) { private fun maybeAddCustomActions(builder: NotificationCompat.Builder, notification: Notification) { notification.actions?.forEach { action -> when (action.action) { "view" -> maybeAddOpenUserAction(builder, notification, action) "view" -> maybeAddViewUserAction(builder, action) "http-post" -> maybeAddHttpPostUserAction(builder, notification, action) } } } private fun maybeAddOpenUserAction(builder: NotificationCompat.Builder, notification: Notification, action: Action) { private fun maybeAddViewUserAction(builder: NotificationCompat.Builder, action: Action) { Log.d(TAG, "Adding user action $action") val url = action.url ?: return try { val uri = Uri.parse(url) val intent = Intent(Intent.ACTION_VIEW, uri) intent.addFlags(Intent.FLAG_GRANT_READ_URI_PERMISSION) 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) builder.addAction(NotificationCompat.Action.Builder(0, action.label, pendingIntent).build()) } catch (e: Exception) { Loading @@ -214,13 +218,40 @@ class NotificationService(val context: Context) { } } class DownloadBroadcastReceiver : BroadcastReceiver() { private fun maybeAddHttpPostUserAction(builder: NotificationCompat.Builder, notification: Notification, action: Action) { val intent = Intent(context, UserActionBroadcastReceiver::class.java).apply { 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) } 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()) } class UserActionBroadcastReceiver : BroadcastReceiver() { override fun onReceive(context: Context, intent: Intent) { val id = intent.getStringExtra("id") ?: return val action = intent.getStringExtra("action") ?: return when (action) { DOWNLOAD_ACTION_START -> DownloadManager.enqueue(context, id, userAction = true) DOWNLOAD_ACTION_CANCEL -> DownloadManager.cancel(context, id) Log.d(TAG, "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) } } } Loading Loading @@ -287,8 +318,15 @@ class NotificationService(val context: Context) { companion object { private const val TAG = "NtfyNotifService" private const val DOWNLOAD_ACTION_START = "io.heckel.ntfy.DOWNLOAD_ACTION_START" private const val DOWNLOAD_ACTION_CANCEL = "io.heckel.ntfy.DOWNLOAD_ACTION_CANCEL" 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_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 CHANNEL_ID_MIN = "ntfy-min" private const val CHANNEL_ID_LOW = "ntfy-low" Loading
app/src/main/java/io/heckel/ntfy/msg/UserActionManager.kt 0 → 100644 +37 −0 Original line number Diff line number Diff line package io.heckel.ntfy.msg import android.content.Context import androidx.work.ExistingWorkPolicy 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. * * The indirection via WorkManager is required since this code may be executed * in a doze state and Internet may not be available. It's also best practice, apparently. */ 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) { 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 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, )) .build() workManager.enqueueUniqueWork(workName, ExistingWorkPolicy.KEEP, workRequest) } }