Fix #220, Action "view" with "clear=true"
This commit is contained in:
parent
4ad6846802
commit
e4255212ef
4 changed files with 108 additions and 52 deletions
|
@ -30,7 +30,6 @@
|
||||||
android:exported="true">
|
android:exported="true">
|
||||||
<intent-filter>
|
<intent-filter>
|
||||||
<action android:name="android.intent.action.MAIN"/>
|
<action android:name="android.intent.action.MAIN"/>
|
||||||
|
|
||||||
<category android:name="android.intent.category.LAUNCHER"/>
|
<category android:name="android.intent.category.LAUNCHER"/>
|
||||||
</intent-filter>
|
</intent-filter>
|
||||||
</activity>
|
</activity>
|
||||||
|
@ -81,6 +80,12 @@
|
||||||
</intent-filter>
|
</intent-filter>
|
||||||
</activity>
|
</activity>
|
||||||
|
|
||||||
|
<!-- Hack: Activity used for "view" action button with "clear=true" (to be able to cancel notifications and show a URL) -->
|
||||||
|
<activity
|
||||||
|
android:name=".msg.NotificationService$ViewActionWithClearActivity"
|
||||||
|
android:exported="false">
|
||||||
|
</activity>
|
||||||
|
|
||||||
<!-- Subscriber foreground service for hosts other than ntfy.sh -->
|
<!-- Subscriber foreground service for hosts other than ntfy.sh -->
|
||||||
<service android:name=".service.SubscriberService"/>
|
<service android:name=".service.SubscriberService"/>
|
||||||
|
|
||||||
|
|
|
@ -1,9 +1,6 @@
|
||||||
package io.heckel.ntfy.msg
|
package io.heckel.ntfy.msg
|
||||||
|
|
||||||
import android.app.NotificationChannel
|
import android.app.*
|
||||||
import android.app.NotificationManager
|
|
||||||
import android.app.PendingIntent
|
|
||||||
import android.app.TaskStackBuilder
|
|
||||||
import android.content.BroadcastReceiver
|
import android.content.BroadcastReceiver
|
||||||
import android.content.Context
|
import android.content.Context
|
||||||
import android.content.Intent
|
import android.content.Intent
|
||||||
|
@ -11,16 +8,19 @@ import android.graphics.BitmapFactory
|
||||||
import android.media.RingtoneManager
|
import android.media.RingtoneManager
|
||||||
import android.net.Uri
|
import android.net.Uri
|
||||||
import android.os.Build
|
import android.os.Build
|
||||||
|
import android.os.Bundle
|
||||||
import androidx.core.app.NotificationCompat
|
import androidx.core.app.NotificationCompat
|
||||||
import androidx.core.content.ContextCompat
|
import androidx.core.content.ContextCompat
|
||||||
import io.heckel.ntfy.R
|
import io.heckel.ntfy.R
|
||||||
import io.heckel.ntfy.db.*
|
import io.heckel.ntfy.db.*
|
||||||
|
import io.heckel.ntfy.db.Notification
|
||||||
import io.heckel.ntfy.ui.Colors
|
import io.heckel.ntfy.ui.Colors
|
||||||
import io.heckel.ntfy.ui.DetailActivity
|
import io.heckel.ntfy.ui.DetailActivity
|
||||||
import io.heckel.ntfy.ui.MainActivity
|
import io.heckel.ntfy.ui.MainActivity
|
||||||
import io.heckel.ntfy.util.*
|
import io.heckel.ntfy.util.*
|
||||||
import java.util.*
|
import java.util.*
|
||||||
|
|
||||||
|
|
||||||
class NotificationService(val context: Context) {
|
class NotificationService(val context: Context) {
|
||||||
private val notificationManager = context.getSystemService(Context.NOTIFICATION_SERVICE) as NotificationManager
|
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() {
|
fun createNotificationChannels() {
|
||||||
(1..5).forEach { priority -> maybeCreateNotificationChannel(priority) }
|
(1..5).forEach { priority -> maybeCreateNotificationChannel(priority) }
|
||||||
}
|
}
|
||||||
|
@ -201,31 +208,32 @@ class NotificationService(val context: Context) {
|
||||||
|
|
||||||
private fun maybeAddUserActions(builder: NotificationCompat.Builder, notification: Notification) {
|
private fun maybeAddUserActions(builder: NotificationCompat.Builder, notification: Notification) {
|
||||||
notification.actions?.forEach { action ->
|
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 actionType = action.action.lowercase(Locale.getDefault())
|
||||||
val viewActionWithoutClear = actionType == ACTION_VIEW && action.clear != true
|
if (actionType == ACTION_VIEW) {
|
||||||
if (viewActionWithoutClear) {
|
// Hack: Action "view" with "clear=true" is a special case, because it's apparently impossible to start a
|
||||||
addViewUserActionWithoutClear(builder, action)
|
// 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 {
|
} else {
|
||||||
addHttpOrBroadcastUserAction(builder, notification, action)
|
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) {
|
private fun addViewUserActionWithoutClear(builder: NotificationCompat.Builder, action: Action) {
|
||||||
Log.d(TAG, "Adding view action (no clear) for ${action.url}")
|
|
||||||
try {
|
try {
|
||||||
val url = action.url ?: return
|
val url = action.url ?: return
|
||||||
val intent = Intent(Intent.ACTION_VIEW, Uri.parse(url)).apply {
|
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)
|
val pendingIntent = PendingIntent.getActivity(context, Random().nextInt(), intent, PendingIntent.FLAG_IMMUTABLE)
|
||||||
builder.addAction(NotificationCompat.Action.Builder(0, action.label, pendingIntent).build())
|
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) {
|
private fun addHttpOrBroadcastUserAction(builder: NotificationCompat.Builder, notification: Notification, action: Action) {
|
||||||
val intent = Intent(context, UserActionBroadcastReceiver::class.java).apply {
|
val intent = Intent(context, UserActionBroadcastReceiver::class.java).apply {
|
||||||
putExtra(BROADCAST_EXTRA_TYPE, BROADCAST_TYPE_USER_ACTION)
|
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())
|
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() {
|
class UserActionBroadcastReceiver : BroadcastReceiver() {
|
||||||
override fun onReceive(context: Context, intent: Intent) {
|
override fun onReceive(context: Context, intent: Intent) {
|
||||||
val type = intent.getStringExtra(BROADCAST_EXTRA_TYPE) ?: return
|
val type = intent.getStringExtra(BROADCAST_EXTRA_TYPE) ?: return
|
||||||
|
@ -261,12 +296,13 @@ class NotificationService(val context: Context) {
|
||||||
}
|
}
|
||||||
|
|
||||||
private fun detailActivityIntent(subscription: Subscription): PendingIntent? {
|
private fun detailActivityIntent(subscription: Subscription): PendingIntent? {
|
||||||
val intent = Intent(context, DetailActivity::class.java)
|
val intent = Intent(context, DetailActivity::class.java).apply {
|
||||||
intent.putExtra(MainActivity.EXTRA_SUBSCRIPTION_ID, subscription.id)
|
putExtra(MainActivity.EXTRA_SUBSCRIPTION_ID, subscription.id)
|
||||||
intent.putExtra(MainActivity.EXTRA_SUBSCRIPTION_BASE_URL, subscription.baseUrl)
|
putExtra(MainActivity.EXTRA_SUBSCRIPTION_BASE_URL, subscription.baseUrl)
|
||||||
intent.putExtra(MainActivity.EXTRA_SUBSCRIPTION_TOPIC, subscription.topic)
|
putExtra(MainActivity.EXTRA_SUBSCRIPTION_TOPIC, subscription.topic)
|
||||||
intent.putExtra(MainActivity.EXTRA_SUBSCRIPTION_INSTANT, subscription.instant)
|
putExtra(MainActivity.EXTRA_SUBSCRIPTION_INSTANT, subscription.instant)
|
||||||
intent.putExtra(MainActivity.EXTRA_SUBSCRIPTION_MUTED_UNTIL, subscription.mutedUntil)
|
putExtra(MainActivity.EXTRA_SUBSCRIPTION_MUTED_UNTIL, subscription.mutedUntil)
|
||||||
|
}
|
||||||
return TaskStackBuilder.create(context).run {
|
return TaskStackBuilder.create(context).run {
|
||||||
addNextIntentWithParentStack(intent) // Add the intent, which inflates the back stack
|
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
|
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 {
|
companion object {
|
||||||
const val ACTION_VIEW = "view"
|
const val ACTION_VIEW = "view"
|
||||||
const val ACTION_HTTP = "http"
|
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_DEFAULT = "ntfy"
|
||||||
private const val CHANNEL_ID_HIGH = "ntfy-high"
|
private const val CHANNEL_ID_HIGH = "ntfy-high"
|
||||||
private const val CHANNEL_ID_MAX = "ntfy-max"
|
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"
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -1,8 +1,6 @@
|
||||||
package io.heckel.ntfy.msg
|
package io.heckel.ntfy.msg
|
||||||
|
|
||||||
import android.content.Context
|
import android.content.Context
|
||||||
import android.content.Intent
|
|
||||||
import android.net.Uri
|
|
||||||
import androidx.work.Worker
|
import androidx.work.Worker
|
||||||
import androidx.work.WorkerParameters
|
import androidx.work.WorkerParameters
|
||||||
import io.heckel.ntfy.R
|
import io.heckel.ntfy.R
|
||||||
|
@ -10,7 +8,6 @@ import io.heckel.ntfy.app.Application
|
||||||
import io.heckel.ntfy.db.*
|
import io.heckel.ntfy.db.*
|
||||||
import io.heckel.ntfy.msg.NotificationService.Companion.ACTION_BROADCAST
|
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_HTTP
|
||||||
import io.heckel.ntfy.msg.NotificationService.Companion.ACTION_VIEW
|
|
||||||
import io.heckel.ntfy.util.Log
|
import io.heckel.ntfy.util.Log
|
||||||
import okhttp3.OkHttpClient
|
import okhttp3.OkHttpClient
|
||||||
import okhttp3.Request
|
import okhttp3.Request
|
||||||
|
@ -46,10 +43,7 @@ class UserActionWorker(private val context: Context, params: WorkerParameters) :
|
||||||
Log.d(TAG, "Executing action $action for notification $notification")
|
Log.d(TAG, "Executing action $action for notification $notification")
|
||||||
try {
|
try {
|
||||||
when (action.action) {
|
when (action.action) {
|
||||||
// ACTION_VIEW is not handled here. It has to be handled in the foreground to avoid
|
// ACTION_VIEW is not handled here. It's handled in the NotificationService and DetailAdapter.
|
||||||
// weird Android behavior.
|
|
||||||
|
|
||||||
ACTION_VIEW -> performViewAction(action)
|
|
||||||
ACTION_BROADCAST -> performBroadcastAction(action)
|
ACTION_BROADCAST -> performBroadcastAction(action)
|
||||||
ACTION_HTTP -> performHttpAction(action)
|
ACTION_HTTP -> performHttpAction(action)
|
||||||
}
|
}
|
||||||
|
@ -63,26 +57,6 @@ class UserActionWorker(private val context: Context, params: WorkerParameters) :
|
||||||
return Result.success()
|
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) {
|
private fun performBroadcastAction(action: Action) {
|
||||||
broadcaster.sendUserAction(action)
|
broadcaster.sendUserAction(action)
|
||||||
if (action.clear == true) {
|
if (action.clear == true) {
|
||||||
|
|
|
@ -9,6 +9,7 @@ Bugs:
|
||||||
* Error in sending test notification in different languages (#209, thanks to @StoyanDimitrov for reporting)
|
* 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)
|
* "[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)
|
* 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:
|
Additional translations:
|
||||||
* Japanese (thanks to @shak)
|
* Japanese (thanks to @shak)
|
||||||
|
|
Loading…
Reference in a new issue