From f62b7fa952fb557910b323edb0d2392ab620b398 Mon Sep 17 00:00:00 2001 From: Philipp Heckel Date: Sat, 16 Apr 2022 22:32:29 -0400 Subject: [PATCH] Derp --- app/src/main/AndroidManifest.xml | 2 +- .../java/io/heckel/ntfy/msg/ApiService.kt | 2 - .../io/heckel/ntfy/msg/DownloadManager.kt | 43 +++++---- .../io/heckel/ntfy/msg/NotificationService.kt | 90 +++++++++++++------ .../io/heckel/ntfy/msg/UserActionManager.kt | 37 ++++++++ .../io/heckel/ntfy/msg/UserActionWorker.kt | 68 ++++++++++++++ 6 files changed, 190 insertions(+), 52 deletions(-) create mode 100644 app/src/main/java/io/heckel/ntfy/msg/UserActionManager.kt create mode 100644 app/src/main/java/io/heckel/ntfy/msg/UserActionWorker.kt diff --git a/app/src/main/AndroidManifest.xml b/app/src/main/AndroidManifest.xml index 9fdb969..e5be012 100644 --- a/app/src/main/AndroidManifest.xml +++ b/app/src/main/AndroidManifest.xml @@ -123,7 +123,7 @@ diff --git a/app/src/main/java/io/heckel/ntfy/msg/ApiService.kt b/app/src/main/java/io/heckel/ntfy/msg/ApiService.kt index d3f4eca..0693834 100644 --- a/app/src/main/java/io/heckel/ntfy/msg/ApiService.kt +++ b/app/src/main/java/io/heckel/ntfy/msg/ApiService.kt @@ -1,6 +1,5 @@ package io.heckel.ntfy.msg -import android.net.Uri import android.os.Build import io.heckel.ntfy.BuildConfig import io.heckel.ntfy.db.Notification @@ -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 diff --git a/app/src/main/java/io/heckel/ntfy/msg/DownloadManager.kt b/app/src/main/java/io/heckel/ntfy/msg/DownloadManager.kt index 4ff45e2..4a6739c 100644 --- a/app/src/main/java/io/heckel/ntfy/msg/DownloadManager.kt +++ b/app/src/main/java/io/heckel/ntfy/msg/DownloadManager.kt @@ -13,30 +13,27 @@ 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 { - private const val TAG = "NtfyDownloadManager" - private const val DOWNLOAD_WORK_NAME_PREFIX = "io.heckel.ntfy.DOWNLOAD_FILE_" +object DownloadManager { + private const val TAG = "NtfyDownloadManager" + private const val DOWNLOAD_WORK_NAME_PREFIX = "io.heckel.ntfy.DOWNLOAD_FILE_" - fun enqueue(context: Context, notificationId: String, userAction: Boolean) { - val workManager = WorkManager.getInstance(context) - val workName = DOWNLOAD_WORK_NAME_PREFIX + notificationId - Log.d(TAG,"Enqueuing work to download attachment for notification $notificationId, work: $workName") - val workRequest = OneTimeWorkRequest.Builder(DownloadWorker::class.java) - .setInputData(workDataOf( - DownloadWorker.INPUT_DATA_ID to notificationId, - DownloadWorker.INPUT_DATA_USER_ACTION to userAction - )) - .build() - workManager.enqueueUniqueWork(workName, ExistingWorkPolicy.KEEP, workRequest) - } - - fun cancel(context: Context, id: String) { - val workManager = WorkManager.getInstance(context) - val workName = DOWNLOAD_WORK_NAME_PREFIX + id - Log.d(TAG, "Cancelling download for notification $id, work: $workName") - workManager.cancelUniqueWork(workName) - } + fun enqueue(context: Context, notificationId: String, userAction: Boolean) { + val workManager = WorkManager.getInstance(context) + val workName = DOWNLOAD_WORK_NAME_PREFIX + notificationId + Log.d(TAG,"Enqueuing work to download attachment for notification $notificationId, work: $workName") + val workRequest = OneTimeWorkRequest.Builder(DownloadWorker::class.java) + .setInputData(workDataOf( + DownloadWorker.INPUT_DATA_ID to notificationId, + DownloadWorker.INPUT_DATA_USER_ACTION to userAction + )) + .build() + workManager.enqueueUniqueWork(workName, ExistingWorkPolicy.KEEP, workRequest) + } + fun cancel(context: Context, id: String) { + val workManager = WorkManager.getInstance(context) + val workName = DOWNLOAD_WORK_NAME_PREFIX + id + Log.d(TAG, "Cancelling download for notification $id, work: $workName") + workManager.cancelUniqueWork(workName) } } 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 046389a..7d16e05 100644 --- a/app/src/main/java/io/heckel/ntfy/msg/NotificationService.kt +++ b/app/src/main/java/io/heckel/ntfy/msg/NotificationService.kt @@ -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()) } @@ -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()) } @@ -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()) } @@ -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()) } @@ -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) { @@ -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) } } } @@ -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" diff --git a/app/src/main/java/io/heckel/ntfy/msg/UserActionManager.kt b/app/src/main/java/io/heckel/ntfy/msg/UserActionManager.kt new file mode 100644 index 0000000..5b1e4fd --- /dev/null +++ b/app/src/main/java/io/heckel/ntfy/msg/UserActionManager.kt @@ -0,0 +1,37 @@ +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) + } +} diff --git a/app/src/main/java/io/heckel/ntfy/msg/UserActionWorker.kt b/app/src/main/java/io/heckel/ntfy/msg/UserActionWorker.kt new file mode 100644 index 0000000..d936064 --- /dev/null +++ b/app/src/main/java/io/heckel/ntfy/msg/UserActionWorker.kt @@ -0,0 +1,68 @@ +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.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) { + private val client = OkHttpClient.Builder() + .callTimeout(15, TimeUnit.MINUTES) // Total timeout for entire request + .connectTimeout(15, TimeUnit.SECONDS) + .readTimeout(15, TimeUnit.SECONDS) + .writeTimeout(15, TimeUnit.SECONDS) + .build() + + 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 app = context.applicationContext as Application + + http(context, url) + + return Result.success() + } + + + fun http(context: Context, url: String) { // FIXME Worker! + Log.d(TAG, "HTTP POST againt $url") + val request = Request.Builder() + .url(url) + .addHeader("User-Agent", ApiService.USER_AGENT) + .method("POST", "".toRequestBody()) + .build() + client.newCall(request).execute().use { response -> + if (response.isSuccessful) { + return + } + throw Exception("Unexpected server response ${response.code}") + } + } + + companion object { + const val INPUT_DATA_ID = "id" + const val INPUT_DATA_ACTION = "action" + const val INPUT_DATA_URL = "url" + + private const val TAG = "NtfyUserActWrk" + } +}