diff --git a/app/src/main/AndroidManifest.xml b/app/src/main/AndroidManifest.xml
index a8f0279..d3b8396 100644
--- a/app/src/main/AndroidManifest.xml
+++ b/app/src/main/AndroidManifest.xml
@@ -30,7 +30,6 @@
android:exported="true">
-
@@ -81,6 +80,12 @@
+
+
+
+
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 e0b2e94..f854a11 100644
--- a/app/src/main/java/io/heckel/ntfy/msg/NotificationService.kt
+++ b/app/src/main/java/io/heckel/ntfy/msg/NotificationService.kt
@@ -1,9 +1,6 @@
package io.heckel.ntfy.msg
-import android.app.NotificationChannel
-import android.app.NotificationManager
-import android.app.PendingIntent
-import android.app.TaskStackBuilder
+import android.app.*
import android.content.BroadcastReceiver
import android.content.Context
import android.content.Intent
@@ -11,16 +8,19 @@ import android.graphics.BitmapFactory
import android.media.RingtoneManager
import android.net.Uri
import android.os.Build
+import android.os.Bundle
import androidx.core.app.NotificationCompat
import androidx.core.content.ContextCompat
import io.heckel.ntfy.R
import io.heckel.ntfy.db.*
+import io.heckel.ntfy.db.Notification
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
@@ -48,6 +48,13 @@ class NotificationService(val context: Context) {
}
}
+ fun cancel(notificationId: Int) {
+ if (notificationId != 0) {
+ Log.d(TAG, "Cancelling notification ${notificationId}")
+ notificationManager.cancel(notificationId)
+ }
+ }
+
fun createNotificationChannels() {
(1..5).forEach { priority -> maybeCreateNotificationChannel(priority) }
}
@@ -201,31 +208,32 @@ class NotificationService(val context: Context) {
private fun maybeAddUserActions(builder: NotificationCompat.Builder, notification: Notification) {
notification.actions?.forEach { 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)
+ if (actionType == ACTION_VIEW) {
+ // Hack: Action "view" with "clear=true" is a special case, because it's apparently impossible to start a
+ // URL activity from PendingIntent.getActivity() and also close the notification. To clear it, we
+ // launch our own Activity (ViewActionWithClearActivity) which then calls the actual activity
+
+ if (action.clear == true) {
+ addViewUserActionWithClear(builder, notification, action)
+ } else {
+ addViewUserActionWithoutClear(builder, action)
+ }
} else {
addHttpOrBroadcastUserAction(builder, notification, action)
}
}
}
+ /**
+ * Open the URL and do NOT cancel the notification (clear=false). This uses a normal Intent with the given URL.
+ * The other case is much more interesting.
+ */
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 {
- addFlags(Intent.FLAG_GRANT_READ_URI_PERMISSION)
+ addFlags(Intent.FLAG_GRANT_READ_URI_PERMISSION or Intent.FLAG_ACTIVITY_NEW_TASK or Intent.FLAG_ACTIVITY_CLEAR_TASK)
}
val pendingIntent = PendingIntent.getActivity(context, Random().nextInt(), intent, PendingIntent.FLAG_IMMUTABLE)
builder.addAction(NotificationCompat.Action.Builder(0, action.label, pendingIntent).build())
@@ -234,6 +242,26 @@ class NotificationService(val context: Context) {
}
}
+ /**
+ * HACK: Open the URL and CANCEL the notification (clear=true). This is a SPECIAL case with a horrible workaround.
+ * We call our own activity ViewActionWithClearActivity and open the URL from there.
+ */
+ private fun addViewUserActionWithClear(builder: NotificationCompat.Builder, notification: Notification, action: Action) {
+ try {
+ val url = action.url ?: return
+ val intent = Intent(context, ViewActionWithClearActivity::class.java).apply {
+ addFlags(Intent.FLAG_GRANT_READ_URI_PERMISSION or Intent.FLAG_ACTIVITY_NEW_TASK or Intent.FLAG_ACTIVITY_CLEAR_TASK)
+ putExtra(VIEW_ACTION_EXTRA_URL, url)
+ putExtra(VIEW_ACTION_EXTRA_NOTIFICATION_ID, notification.notificationId)
+ }
+ val pendingIntent = PendingIntent.getActivity(context, Random().nextInt(), intent, PendingIntent.FLAG_IMMUTABLE)
+ builder.addAction(NotificationCompat.Action.Builder(0, action.label, pendingIntent).build())
+
+ } catch (e: Exception) {
+ Log.w(TAG, "Unable to add open user action", e)
+ }
+ }
+
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)
@@ -245,6 +273,13 @@ class NotificationService(val context: Context) {
builder.addAction(NotificationCompat.Action.Builder(0, label, pendingIntent).build())
}
+ /**
+ * Receives the broadcast from
+ * - the "http" and "broadcast" action button (the "view" actio is handled differently)
+ * - the "download"/"cancel" action button
+ *
+ * Then queues a Worker via WorkManager to execute the action in the background
+ */
class UserActionBroadcastReceiver : BroadcastReceiver() {
override fun onReceive(context: Context, intent: Intent) {
val type = intent.getStringExtra(BROADCAST_EXTRA_TYPE) ?: return
@@ -261,12 +296,13 @@ class NotificationService(val context: Context) {
}
private fun detailActivityIntent(subscription: Subscription): PendingIntent? {
- val intent = Intent(context, DetailActivity::class.java)
- intent.putExtra(MainActivity.EXTRA_SUBSCRIPTION_ID, subscription.id)
- intent.putExtra(MainActivity.EXTRA_SUBSCRIPTION_BASE_URL, subscription.baseUrl)
- intent.putExtra(MainActivity.EXTRA_SUBSCRIPTION_TOPIC, subscription.topic)
- intent.putExtra(MainActivity.EXTRA_SUBSCRIPTION_INSTANT, subscription.instant)
- intent.putExtra(MainActivity.EXTRA_SUBSCRIPTION_MUTED_UNTIL, subscription.mutedUntil)
+ val intent = Intent(context, DetailActivity::class.java).apply {
+ putExtra(MainActivity.EXTRA_SUBSCRIPTION_ID, subscription.id)
+ putExtra(MainActivity.EXTRA_SUBSCRIPTION_BASE_URL, subscription.baseUrl)
+ putExtra(MainActivity.EXTRA_SUBSCRIPTION_TOPIC, subscription.topic)
+ putExtra(MainActivity.EXTRA_SUBSCRIPTION_INSTANT, subscription.instant)
+ putExtra(MainActivity.EXTRA_SUBSCRIPTION_MUTED_UNTIL, subscription.mutedUntil)
+ }
return TaskStackBuilder.create(context).run {
addNextIntentWithParentStack(intent) // Add the intent, which inflates the back stack
getPendingIntent(0, PendingIntent.FLAG_UPDATE_CURRENT or PendingIntent.FLAG_IMMUTABLE) // Get the PendingIntent containing the entire back stack
@@ -320,6 +356,43 @@ class NotificationService(val context: Context) {
}
}
+ /**
+ * Activity used to launch a URL.
+ * .
+ * Horrible hack: Action "view" with "clear=true" is a special case, because it's apparently impossible to start a
+ * URL activity from PendingIntent.getActivity() and also close the notification. To clear it, we
+ * launch this activity which then calls the actual activity.
+ */
+ class ViewActionWithClearActivity : Activity() {
+ override fun onCreate(savedInstanceState: Bundle?) {
+ super.onCreate(savedInstanceState)
+ Log.d(TAG, "Creating $this")
+ val url = intent.getStringExtra(VIEW_ACTION_EXTRA_URL)
+ val notificationId = intent.getIntExtra(VIEW_ACTION_EXTRA_NOTIFICATION_ID, 0)
+ if (url == null) {
+ finish()
+ return
+ }
+
+ // Immediately start the actual activity
+ try {
+ val intent = Intent(Intent.ACTION_VIEW, Uri.parse(url)).apply {
+ addFlags(Intent.FLAG_GRANT_READ_URI_PERMISSION)
+ }
+ startActivity(intent)
+ } catch (e: Exception) {
+ Log.w(TAG, "Unable to start activity from URL $url", e)
+ }
+
+ // Cancel notification
+ val notifier = NotificationService(this)
+ notifier.cancel(notificationId)
+
+ // Close this activity
+ finish()
+ }
+ }
+
companion object {
const val ACTION_VIEW = "view"
const val ACTION_HTTP = "http"
@@ -340,5 +413,8 @@ class NotificationService(val context: Context) {
private const val CHANNEL_ID_DEFAULT = "ntfy"
private const val CHANNEL_ID_HIGH = "ntfy-high"
private const val CHANNEL_ID_MAX = "ntfy-max"
+
+ private const val VIEW_ACTION_EXTRA_URL = "url"
+ private const val VIEW_ACTION_EXTRA_NOTIFICATION_ID = "notificationId"
}
}
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 45d4199..bd12971 100644
--- a/app/src/main/java/io/heckel/ntfy/msg/UserActionWorker.kt
+++ b/app/src/main/java/io/heckel/ntfy/msg/UserActionWorker.kt
@@ -1,8 +1,6 @@
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
@@ -10,7 +8,6 @@ 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,10 +43,7 @@ class UserActionWorker(private val context: Context, params: WorkerParameters) :
Log.d(TAG, "Executing action $action for notification $notification")
try {
when (action.action) {
- // ACTION_VIEW is not handled here. It has to be handled in the foreground to avoid
- // weird Android behavior.
-
- ACTION_VIEW -> performViewAction(action)
+ // ACTION_VIEW is not handled here. It's handled in the NotificationService and DetailAdapter.
ACTION_BROADCAST -> performBroadcastAction(action)
ACTION_HTTP -> performHttpAction(action)
}
@@ -63,26 +57,6 @@ 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) {
diff --git a/fastlane/metadata/android/en-US/changelog/26.txt b/fastlane/metadata/android/en-US/changelog/26.txt
index b603d6d..7f4dcd3 100644
--- a/fastlane/metadata/android/en-US/changelog/26.txt
+++ b/fastlane/metadata/android/en-US/changelog/26.txt
@@ -9,6 +9,7 @@ Bugs:
* Error in sending test notification in different languages (#209, thanks to @StoyanDimitrov for reporting)
* "[x] Instant delivery in doze mode" checkbox does not work properly (#211)
* Disallow "http" GET/HEAD actions with body (#221, thanks to @cmeis for reporting)
+* Action "view" with "clear=true" does not work on some phones (#220, thanks to @cmeis for reporting)
Additional translations:
* Japanese (thanks to @shak)