ntfy-android/app/src/main/java/io/heckel/ntfy/msg/NotificationService.kt

544 lines
26 KiB
Kotlin
Raw Normal View History

package io.heckel.ntfy.msg
import android.app.*
2022-05-04 11:59:33 +12:00
import android.content.ActivityNotFoundException
2022-04-22 11:47:18 +12:00
import android.content.BroadcastReceiver
import android.content.Context
import android.content.Intent
2022-11-29 00:00:41 +13:00
import android.media.AudioAttributes
import android.media.AudioManager
import android.media.RingtoneManager
2022-01-05 07:45:02 +13:00
import android.net.Uri
import android.os.Build
import android.os.Bundle
2022-05-04 11:59:33 +12:00
import android.widget.Toast
import androidx.core.app.NotificationCompat
2021-11-24 04:52:27 +13:00
import androidx.core.content.ContextCompat
import io.heckel.ntfy.R
2022-01-19 08:28:48 +13:00
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
2022-01-09 16:17:41 +13:00
import io.heckel.ntfy.util.*
2022-04-18 06:29:29 +12:00
import java.util.*
class NotificationService(val context: Context) {
2022-01-06 13:05:57 +13:00
private val notificationManager = context.getSystemService(Context.NOTIFICATION_SERVICE) as NotificationManager
2022-11-30 16:46:38 +13:00
private val repository = Repository.getInstance(context)
2022-01-06 13:05:57 +13:00
2022-01-04 12:54:18 +13:00
fun display(subscription: Subscription, notification: Notification) {
2021-11-28 10:18:09 +13:00
Log.d(TAG, "Displaying notification $notification")
2022-01-05 07:45:02 +13:00
displayInternal(subscription, notification)
2022-01-04 12:54:18 +13:00
}
fun update(subscription: Subscription, notification: Notification) {
val active = if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.M) {
notificationManager.activeNotifications.find { it.id == notification.notificationId } != null
} else {
true
}
if (active) {
Log.d(TAG, "Updating notification $notification")
displayInternal(subscription, notification, update = true)
}
}
2022-01-04 12:54:18 +13:00
fun cancel(notification: Notification) {
if (notification.notificationId != 0) {
Log.d(TAG, "Cancelling notification ${notification.id}: ${decodeMessage(notification)}")
2022-01-04 12:54:18 +13:00
notificationManager.cancel(notification.notificationId)
}
}
fun cancel(notificationId: Int) {
if (notificationId != 0) {
2022-12-07 15:42:09 +13:00
Log.d(TAG, "Cancelling notification $notificationId")
notificationManager.cancel(notificationId)
}
}
fun createDefaultNotificationChannels() {
2022-12-07 16:06:59 +13:00
maybeCreateNotificationGroup(DEFAULT_GROUP, context.getString(R.string.channel_notifications_group_default_name))
ALL_PRIORITIES.forEach { priority -> maybeCreateNotificationChannel(DEFAULT_GROUP, priority) }
}
fun createSubscriptionNotificationChannels(subscription: Subscription) {
2022-12-07 16:06:59 +13:00
val groupId = subscriptionGroupId(subscription)
maybeCreateNotificationGroup(groupId, subscriptionGroupName(subscription))
ALL_PRIORITIES.forEach { priority -> maybeCreateNotificationChannel(groupId, priority) }
}
fun deleteSubscriptionNotificationChannels(subscription: Subscription) {
2022-12-07 16:06:59 +13:00
val groupId = subscriptionGroupId(subscription)
ALL_PRIORITIES.forEach { priority -> maybeDeleteNotificationChannel(groupId, priority) }
maybeDeleteNotificationGroup(groupId)
}
fun channelsSupported(): Boolean {
return Build.VERSION.SDK_INT >= Build.VERSION_CODES.O
}
2022-12-07 16:06:59 +13:00
private fun subscriptionGroupId(subscription: Subscription): String {
return SUBSCRIPTION_GROUP_PREFIX + subscription.id.toString()
}
2022-12-07 16:06:59 +13:00
private fun subscriptionGroupName(subscription: Subscription): String {
return subscription.displayName ?: subscriptionTopicShortUrl(subscription)
2022-01-04 12:54:18 +13:00
}
private fun displayInternal(subscription: Subscription, notification: Notification, update: Boolean = false) {
2021-11-28 10:18:09 +13:00
val title = formatTitle(subscription, notification)
2022-12-07 16:06:59 +13:00
val groupId = if (subscription.dedicatedChannels) subscriptionGroupId(subscription) else DEFAULT_GROUP
val channelId = toChannelId(groupId, notification.priority)
val insistent = notification.priority == PRIORITY_MAX &&
(repository.getInsistentMaxPriorityEnabled() || subscription.insistent == Repository.INSISTENT_MAX_PRIORITY_ENABLED)
2022-01-06 13:05:57 +13:00
val builder = NotificationCompat.Builder(context, channelId)
2021-11-24 04:52:27 +13:00
.setSmallIcon(R.drawable.ic_notification)
2022-11-27 07:52:52 +13:00
.setColor(ContextCompat.getColor(context, Colors.notificationIcon(context)))
.setContentTitle(title)
2022-01-06 13:05:57 +13:00
.setOnlyAlertOnce(true) // Do not vibrate or play sound if already showing (updates!)
.setAutoCancel(true) // Cancel when notification is clicked
2022-05-09 14:57:52 +12:00
setStyleAndText(builder, subscription, notification) // Preview picture or big text style
setClickAction(builder, subscription, notification)
2022-11-30 16:46:38 +13:00
maybeSetDeleteIntent(builder, insistent)
maybeSetSound(builder, insistent, update)
maybeSetProgress(builder, notification)
2022-01-06 13:05:57 +13:00
maybeAddOpenAction(builder, notification)
maybeAddBrowseAction(builder, notification)
maybeAddDownloadAction(builder, notification)
2022-01-12 12:21:30 +13:00
maybeAddCancelAction(builder, notification)
2022-04-20 01:15:06 +12:00
maybeAddUserActions(builder, notification)
2022-01-06 13:05:57 +13:00
2022-12-07 16:06:59 +13:00
maybeCreateNotificationGroup(groupId, subscriptionGroupName(subscription))
maybeCreateNotificationChannel(groupId, notification.priority)
maybePlayInsistentSound(groupId, insistent)
2022-01-06 13:05:57 +13:00
notificationManager.notify(notification.notificationId, builder.build())
}
2022-11-30 16:46:38 +13:00
private fun maybeSetDeleteIntent(builder: NotificationCompat.Builder, insistent: Boolean) {
if (!insistent) {
return
2022-11-29 00:00:41 +13:00
}
2022-11-30 16:46:38 +13:00
val intent = Intent(context, DeleteBroadcastReceiver::class.java)
val pendingIntent = PendingIntent.getBroadcast(context, Random().nextInt(), intent, PendingIntent.FLAG_IMMUTABLE)
builder.setDeleteIntent(pendingIntent)
2022-01-06 13:05:57 +13:00
}
2022-11-30 16:46:38 +13:00
private fun maybeSetSound(builder: NotificationCompat.Builder, insistent: Boolean, update: Boolean) {
2022-12-05 14:28:57 +13:00
// Note that the sound setting is ignored in Android => O (26) in favor of notification channels
2022-11-30 16:46:38 +13:00
val hasSound = !update && !insistent
if (hasSound) {
2022-01-06 13:05:57 +13:00
val defaultSoundUri = RingtoneManager.getDefaultUri(RingtoneManager.TYPE_NOTIFICATION)
builder.setSound(defaultSoundUri)
} else {
builder.setSound(null)
}
}
2022-05-09 14:57:52 +12:00
private fun setStyleAndText(builder: NotificationCompat.Builder, subscription: Subscription, notification: Notification) {
val contentUri = notification.attachment?.contentUri
2022-01-09 16:17:41 +13:00
val isSupportedImage = supportedImage(notification.attachment?.type)
2022-05-09 14:57:52 +12:00
val subscriptionIcon = if (subscription.icon != null) subscription.icon.readBitmapFromUriOrNull(context) else null
2022-08-26 15:58:37 +12:00
val notificationIcon = if (notification.icon != null) notification.icon.contentUri?.readBitmapFromUriOrNull(context) else null
2022-07-17 08:32:09 +12:00
val largeIcon = notificationIcon ?: subscriptionIcon
2022-01-09 16:17:41 +13:00
if (contentUri != null && isSupportedImage) {
2022-01-06 13:05:57 +13:00
try {
2022-05-09 14:57:52 +12:00
val attachmentBitmap = contentUri.readBitmapFromUri(context)
2022-01-06 13:05:57 +13:00
builder
2022-04-20 01:15:06 +12:00
.setContentText(maybeAppendActionErrors(formatMessage(notification), notification))
2022-05-09 14:57:52 +12:00
.setLargeIcon(attachmentBitmap)
2022-01-06 13:05:57 +13:00
.setStyle(NotificationCompat.BigPictureStyle()
2022-05-09 14:57:52 +12:00
.bigPicture(attachmentBitmap)
2022-07-17 08:32:09 +12:00
.bigLargeIcon(largeIcon)) // May be null
2022-01-06 13:05:57 +13:00
} catch (_: Exception) {
2022-04-20 01:15:06 +12:00
val message = maybeAppendActionErrors(formatMessageMaybeWithAttachmentInfos(notification), notification)
builder
.setContentText(message)
.setStyle(NotificationCompat.BigTextStyle().bigText(message))
2022-01-06 13:05:57 +13:00
}
} else {
2022-04-20 01:15:06 +12:00
val message = maybeAppendActionErrors(formatMessageMaybeWithAttachmentInfos(notification), notification)
builder
.setContentText(message)
.setStyle(NotificationCompat.BigTextStyle().bigText(message))
2022-07-17 08:32:09 +12:00
.setLargeIcon(largeIcon) // May be null
}
}
2022-04-20 01:15:06 +12:00
private fun formatMessageMaybeWithAttachmentInfos(notification: Notification): String {
val message = formatMessage(notification)
val attachment = notification.attachment ?: return message
2022-04-20 01:15:06 +12:00
val attachmentInfos = if (attachment.size != null) {
"${attachment.name}, ${formatBytes(attachment.size)}"
} else {
attachment.name
}
if (attachment.progress in 0..99) {
2022-04-20 01:15:06 +12:00
return context.getString(R.string.notification_popup_file_downloading, attachmentInfos, attachment.progress, message)
2022-01-06 13:05:57 +13:00
}
2022-04-20 01:15:06 +12:00
if (attachment.progress == ATTACHMENT_PROGRESS_DONE) {
return context.getString(R.string.notification_popup_file_download_successful, message, attachmentInfos)
}
2022-04-20 01:15:06 +12:00
if (attachment.progress == ATTACHMENT_PROGRESS_FAILED) {
return context.getString(R.string.notification_popup_file_download_failed, message, attachmentInfos)
}
return context.getString(R.string.notification_popup_file, message, attachmentInfos)
}
private fun setClickAction(builder: NotificationCompat.Builder, subscription: Subscription, notification: Notification) {
2022-01-06 13:05:57 +13:00
if (notification.click == "") {
builder.setContentIntent(detailActivityIntent(subscription))
} else {
try {
val uri = Uri.parse(notification.click)
2022-04-20 01:15:06 +12:00
val viewIntent = PendingIntent.getActivity(context, Random().nextInt(), Intent(Intent.ACTION_VIEW, uri), PendingIntent.FLAG_IMMUTABLE)
2022-01-06 13:05:57 +13:00
builder.setContentIntent(viewIntent)
} catch (e: Exception) {
builder.setContentIntent(detailActivityIntent(subscription))
}
2022-01-05 07:45:02 +13:00
}
2022-01-06 13:05:57 +13:00
}
private fun maybeSetProgress(builder: NotificationCompat.Builder, notification: Notification) {
val progress = notification.attachment?.progress
if (progress in 0..99) {
builder.setProgress(100, progress!!, false)
2022-01-04 12:54:18 +13:00
} else {
2022-01-06 13:05:57 +13:00
builder.setProgress(0, 0, false) // Remove progress bar
}
}
private fun maybeAddOpenAction(builder: NotificationCompat.Builder, notification: Notification) {
if (!canOpenAttachment(notification.attachment)) {
return
}
if (notification.attachment?.contentUri != null) {
val contentUri = Uri.parse(notification.attachment.contentUri)
2022-04-17 14:32:29 +12:00
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)
}
2022-04-20 01:15:06 +12:00
val pendingIntent = PendingIntent.getActivity(context, Random().nextInt(), intent, PendingIntent.FLAG_IMMUTABLE)
builder.addAction(NotificationCompat.Action.Builder(0, context.getString(R.string.notification_popup_action_open), pendingIntent).build())
2022-01-04 12:54:18 +13:00
}
2022-01-06 13:05:57 +13:00
}
private fun maybeAddBrowseAction(builder: NotificationCompat.Builder, notification: Notification) {
if (notification.attachment?.contentUri != null) {
2022-04-17 14:32:29 +12:00
val intent = Intent(android.app.DownloadManager.ACTION_VIEW_DOWNLOADS).apply {
addFlags(Intent.FLAG_GRANT_READ_URI_PERMISSION)
}
2022-04-20 01:15:06 +12:00
val pendingIntent = PendingIntent.getActivity(context, Random().nextInt(), intent, PendingIntent.FLAG_IMMUTABLE)
builder.addAction(NotificationCompat.Action.Builder(0, context.getString(R.string.notification_popup_action_browse), pendingIntent).build())
2022-01-06 13:05:57 +13:00
}
}
private fun maybeAddDownloadAction(builder: NotificationCompat.Builder, notification: Notification) {
2022-04-20 01:15:06 +12:00
if (notification.attachment?.contentUri == null && listOf(ATTACHMENT_PROGRESS_NONE, ATTACHMENT_PROGRESS_FAILED).contains(notification.attachment?.progress)) {
2022-04-17 14:32:29 +12:00
val intent = Intent(context, UserActionBroadcastReceiver::class.java).apply {
putExtra(BROADCAST_EXTRA_TYPE, BROADCAST_TYPE_DOWNLOAD_START)
putExtra(BROADCAST_EXTRA_NOTIFICATION_ID, notification.id)
}
2022-04-20 01:15:06 +12:00
val pendingIntent = PendingIntent.getBroadcast(context, Random().nextInt(), 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())
}
}
2022-01-12 12:21:30 +13:00
private fun maybeAddCancelAction(builder: NotificationCompat.Builder, notification: Notification) {
if (notification.attachment?.contentUri == null && notification.attachment?.progress in 0..99) {
2022-04-17 14:32:29 +12:00
val intent = Intent(context, UserActionBroadcastReceiver::class.java).apply {
putExtra(BROADCAST_EXTRA_TYPE, BROADCAST_TYPE_DOWNLOAD_CANCEL)
putExtra(BROADCAST_EXTRA_NOTIFICATION_ID, notification.id)
}
2022-04-20 01:15:06 +12:00
val pendingIntent = PendingIntent.getBroadcast(context, Random().nextInt(), intent, PendingIntent.FLAG_UPDATE_CURRENT or PendingIntent.FLAG_IMMUTABLE)
2022-01-12 12:21:30 +13:00
builder.addAction(NotificationCompat.Action.Builder(0, context.getString(R.string.notification_popup_action_cancel), pendingIntent).build())
}
}
2022-04-20 01:15:06 +12:00
private fun maybeAddUserActions(builder: NotificationCompat.Builder, notification: Notification) {
2022-04-17 12:12:40 +12:00
notification.actions?.forEach { action ->
2022-04-23 14:45:33 +12:00
val actionType = action.action.lowercase(Locale.getDefault())
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)
}
2022-04-23 14:45:33 +12:00
} else {
addHttpOrBroadcastUserAction(builder, notification, action)
2022-04-17 12:12:40 +12:00
}
}
}
/**
* 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.
*/
2022-04-23 14:45:33 +12:00
private fun addViewUserActionWithoutClear(builder: NotificationCompat.Builder, action: Action) {
2022-04-17 12:12:40 +12:00
try {
2022-04-17 14:32:29 +12:00
val url = action.url ?: return
val intent = Intent(Intent.ACTION_VIEW, Uri.parse(url)).apply {
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())
} catch (e: Exception) {
Log.w(TAG, "Unable to add open user action", e)
}
}
/**
* 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)
2022-04-17 14:32:29 +12:00
}
2022-04-20 01:15:06 +12:00
val pendingIntent = PendingIntent.getActivity(context, Random().nextInt(), intent, PendingIntent.FLAG_IMMUTABLE)
2022-04-17 12:12:40 +12:00
builder.addAction(NotificationCompat.Action.Builder(0, action.label, pendingIntent).build())
} catch (e: Exception) {
Log.w(TAG, "Unable to add open user action", e)
}
}
2022-04-23 14:45:33 +12:00
private fun addHttpOrBroadcastUserAction(builder: NotificationCompat.Builder, notification: Notification, action: Action) {
2022-04-17 14:32:29 +12:00
val intent = Intent(context, UserActionBroadcastReceiver::class.java).apply {
2022-04-18 06:29:29 +12:00
putExtra(BROADCAST_EXTRA_TYPE, BROADCAST_TYPE_USER_ACTION)
2022-04-17 14:32:29 +12:00
putExtra(BROADCAST_EXTRA_NOTIFICATION_ID, notification.id)
2022-04-18 06:29:29 +12:00
putExtra(BROADCAST_EXTRA_ACTION_ID, action.id)
2022-04-17 14:32:29 +12:00
}
2022-04-20 01:15:06 +12:00
val pendingIntent = PendingIntent.getBroadcast(context, Random().nextInt(), intent, PendingIntent.FLAG_UPDATE_CURRENT or PendingIntent.FLAG_IMMUTABLE)
val label = formatActionLabel(action)
2022-04-20 01:15:06 +12:00
builder.addAction(NotificationCompat.Action.Builder(0, label, pendingIntent).build())
2022-04-17 14:32:29 +12:00
}
/**
* Receives the broadcast from
2022-07-17 08:32:09 +12:00
* - the "http" and "broadcast" action button (the "view" action is handled differently)
* - the "download"/"cancel" action button
*
* Then queues a Worker via WorkManager to execute the action in the background
*/
2022-04-17 14:32:29 +12:00
class UserActionBroadcastReceiver : BroadcastReceiver() {
override fun onReceive(context: Context, intent: Intent) {
2022-04-17 14:32:29 +12:00
val type = intent.getStringExtra(BROADCAST_EXTRA_TYPE) ?: return
val notificationId = intent.getStringExtra(BROADCAST_EXTRA_NOTIFICATION_ID) ?: return
when (type) {
2022-07-17 08:32:09 +12:00
BROADCAST_TYPE_DOWNLOAD_START -> DownloadManager.enqueue(context, notificationId, userAction = true, DownloadType.ATTACHMENT)
2022-04-17 14:32:29 +12:00
BROADCAST_TYPE_DOWNLOAD_CANCEL -> DownloadManager.cancel(context, notificationId)
2022-04-18 06:29:29 +12:00
BROADCAST_TYPE_USER_ACTION -> {
val actionId = intent.getStringExtra(BROADCAST_EXTRA_ACTION_ID) ?: return
UserActionManager.enqueue(context, notificationId, actionId)
}
2022-01-12 12:21:30 +13:00
}
}
}
2022-11-30 16:46:38 +13:00
/**
* Receives a broadcast when a notification is swiped away. This is currently
* only called for notifications with an insistent sound.
*/
class DeleteBroadcastReceiver : BroadcastReceiver() {
override fun onReceive(context: Context, intent: Intent) {
2022-12-05 14:11:46 +13:00
Log.d(TAG, "Media player: Stopping insistent ring")
2022-11-30 16:46:38 +13:00
val mediaPlayer = Repository.getInstance(context).mediaPlayer
mediaPlayer.stop()
}
}
2022-01-05 11:45:24 +13:00
private fun detailActivityIntent(subscription: Subscription): PendingIntent? {
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_DISPLAY_NAME, displayName(subscription))
putExtra(MainActivity.EXTRA_SUBSCRIPTION_INSTANT, subscription.instant)
putExtra(MainActivity.EXTRA_SUBSCRIPTION_MUTED_UNTIL, subscription.mutedUntil)
}
2022-01-05 11:45:24 +13:00
return TaskStackBuilder.create(context).run {
addNextIntentWithParentStack(intent) // Add the intent, which inflates the back stack
2022-11-29 00:42:37 +13:00
getPendingIntent(Random().nextInt(), PendingIntent.FLAG_UPDATE_CURRENT or PendingIntent.FLAG_IMMUTABLE) // Get the PendingIntent containing the entire back stack
2022-01-05 11:45:24 +13:00
}
}
2022-12-07 16:06:59 +13:00
private fun maybeCreateNotificationChannel(group: String, priority: Int) {
if (channelsSupported()) {
2021-11-30 08:06:08 +13:00
// Note: To change a notification channel, you must delete the old one and create a new one!
2022-12-07 16:06:59 +13:00
val channelId = toChannelId(group, priority)
2021-11-30 08:06:08 +13:00
val pause = 300L
val channel = when (priority) {
PRIORITY_MIN -> NotificationChannel(channelId, context.getString(R.string.channel_notifications_min_name), NotificationManager.IMPORTANCE_MIN)
PRIORITY_LOW -> NotificationChannel(channelId, context.getString(R.string.channel_notifications_low_name), NotificationManager.IMPORTANCE_LOW)
PRIORITY_HIGH -> {
2022-12-07 10:09:04 +13:00
val channel = NotificationChannel(channelId, context.getString(R.string.channel_notifications_high_name), NotificationManager.IMPORTANCE_HIGH)
2021-11-30 08:06:08 +13:00
channel.enableVibration(true)
channel.vibrationPattern = longArrayOf(
pause, 100, pause, 100, pause, 100,
pause, 2000
)
channel
}
PRIORITY_MAX -> {
2022-12-07 10:09:04 +13:00
val channel = NotificationChannel(channelId, context.getString(R.string.channel_notifications_max_name), NotificationManager.IMPORTANCE_HIGH) // IMPORTANCE_MAX does not exist
2021-11-30 08:06:08 +13:00
channel.enableLights(true)
channel.enableVibration(true)
2022-11-29 00:00:41 +13:00
channel.setBypassDnd(true)
2021-11-30 08:06:08 +13:00
channel.vibrationPattern = longArrayOf(
pause, 100, pause, 100, pause, 100,
pause, 2000,
pause, 100, pause, 100, pause, 100,
pause, 2000,
pause, 100, pause, 100, pause, 100,
pause, 2000
)
channel
}
2022-12-07 10:09:04 +13:00
else -> NotificationChannel(channelId, context.getString(R.string.channel_notifications_default_name), NotificationManager.IMPORTANCE_DEFAULT)
2021-11-30 08:06:08 +13:00
}
2022-12-07 16:06:59 +13:00
channel.group = group
2021-11-30 08:06:08 +13:00
notificationManager.createNotificationChannel(channel)
2021-11-28 10:18:09 +13:00
}
}
2022-12-07 16:06:59 +13:00
private fun maybeDeleteNotificationChannel(group: String, priority: Int) {
if (channelsSupported()) {
2022-12-07 16:06:59 +13:00
notificationManager.deleteNotificationChannel(toChannelId(group, priority))
}
}
private fun maybeCreateNotificationGroup(id: String, name: String) {
if (channelsSupported()) {
notificationManager.createNotificationChannelGroup(NotificationChannelGroup(id, name))
}
}
private fun maybeDeleteNotificationGroup(id: String) {
if (channelsSupported()) {
notificationManager.deleteNotificationChannelGroup(id)
}
}
private fun toChannelId(groupId: String, priority: Int): String {
2021-11-28 10:18:09 +13:00
return when (priority) {
PRIORITY_MIN -> groupId + GROUP_SUFFIX_PRIORITY_MIN
PRIORITY_LOW -> groupId + GROUP_SUFFIX_PRIORITY_LOW
PRIORITY_HIGH -> groupId + GROUP_SUFFIX_PRIORITY_HIGH
PRIORITY_MAX -> groupId + GROUP_SUFFIX_PRIORITY_MAX
else -> groupId + GROUP_SUFFIX_PRIORITY_DEFAULT
2021-11-28 10:18:09 +13:00
}
}
private fun maybePlayInsistentSound(groupId: String, insistent: Boolean) {
2022-11-30 16:46:38 +13:00
if (!insistent) {
return
}
try {
val mediaPlayer = repository.mediaPlayer
val audioManager = context.getSystemService(Context.AUDIO_SERVICE) as AudioManager
if (audioManager.getStreamVolume(AudioManager.STREAM_ALARM) != 0) {
2022-12-05 14:11:46 +13:00
Log.d(TAG, "Media player: Playing insistent alarm on alarm channel")
2022-11-30 16:46:38 +13:00
mediaPlayer.reset()
mediaPlayer.setDataSource(context, getInsistentSound(groupId))
2022-11-30 16:46:38 +13:00
mediaPlayer.setAudioAttributes(AudioAttributes.Builder().setUsage(AudioAttributes.USAGE_ALARM).build())
mediaPlayer.isLooping = true
2022-11-30 16:46:38 +13:00
mediaPlayer.prepare()
mediaPlayer.start()
2022-12-05 14:11:46 +13:00
} else {
Log.d(TAG, "Media player: Alarm volume is 0; not playing insistent alarm")
2022-11-30 16:46:38 +13:00
}
} catch (e: Exception) {
2022-12-05 14:11:46 +13:00
Log.w(TAG, "Media player: Failed to play insistent alarm", e)
2022-11-30 16:46:38 +13:00
}
}
private fun getInsistentSound(groupId: String): Uri {
return if (channelsSupported()) {
val channelId = toChannelId(groupId, PRIORITY_MAX)
val channel = notificationManager.getNotificationChannel(channelId)
channel.sound
} else {
RingtoneManager.getDefaultUri(RingtoneManager.TYPE_NOTIFICATION)
2021-11-28 10:18:09 +13:00
}
}
/**
* 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)
2022-05-04 11:59:33 +12:00
Log.d(TAG, "Created $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)
2022-05-04 11:59:33 +12:00
val message = if (e is ActivityNotFoundException) url else e.message
Toast
.makeText(this, getString(R.string.detail_item_cannot_open_url, message), Toast.LENGTH_LONG)
.show()
}
// 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"
const val ACTION_BROADCAST = "broadcast"
2022-04-20 01:15:06 +12:00
const val BROADCAST_EXTRA_TYPE = "type"
const val BROADCAST_EXTRA_NOTIFICATION_ID = "notificationId"
2022-04-23 14:45:33 +12:00
const val BROADCAST_EXTRA_ACTION_ID = "actionId"
2022-04-17 14:32:29 +12:00
const val BROADCAST_TYPE_DOWNLOAD_START = "io.heckel.ntfy.DOWNLOAD_ACTION_START"
const val BROADCAST_TYPE_DOWNLOAD_CANCEL = "io.heckel.ntfy.DOWNLOAD_ACTION_CANCEL"
const val BROADCAST_TYPE_USER_ACTION = "io.heckel.ntfy.USER_ACTION_RUN"
2022-04-17 14:32:29 +12:00
private const val TAG = "NtfyNotifService"
2022-12-07 15:42:09 +13:00
private const val DEFAULT_GROUP = "ntfy"
private const val SUBSCRIPTION_GROUP_PREFIX = "ntfy-subscription-"
2022-12-07 16:06:59 +13:00
private const val GROUP_SUFFIX_PRIORITY_MIN = "-min"
private const val GROUP_SUFFIX_PRIORITY_LOW = "-low"
private const val GROUP_SUFFIX_PRIORITY_DEFAULT = ""
private const val GROUP_SUFFIX_PRIORITY_HIGH = "-high"
private const val GROUP_SUFFIX_PRIORITY_MAX = "-max"
private const val VIEW_ACTION_EXTRA_URL = "url"
private const val VIEW_ACTION_EXTRA_NOTIFICATION_ID = "notificationId"
}
}