2021-11-12 01:41:29 +01:00
package io.heckel.ntfy.msg
2022-01-09 03:32:43 +01:00
import android.app.*
import android.content.*
2022-01-06 01:05:57 +01:00
import android.graphics.BitmapFactory
2021-11-12 01:41:29 +01:00
import android.media.RingtoneManager
2022-01-04 19:45:02 +01:00
import android.net.Uri
2021-11-12 01:41:29 +01:00
import android.os.Build
import androidx.core.app.NotificationCompat
2021-11-23 16:52:27 +01:00
import androidx.core.content.ContextCompat
2021-11-12 01:41:29 +01:00
import io.heckel.ntfy.R
2022-01-18 20:28:48 +01:00
import io.heckel.ntfy.db.*
import io.heckel.ntfy.db.Notification
2022-02-09 22:20:24 +01:00
import io.heckel.ntfy.util.Log
2022-02-08 00:35:36 +01:00
import io.heckel.ntfy.ui.Colors
2021-11-12 01:41:29 +01:00
import io.heckel.ntfy.ui.DetailActivity
import io.heckel.ntfy.ui.MainActivity
2022-01-09 04:17:41 +01:00
import io.heckel.ntfy.util.*
2022-04-17 20:29:29 +02:00
import java.util.*
2021-11-12 01:41:29 +01:00
class NotificationService ( val context : Context ) {
2022-01-06 01:05:57 +01:00
private val notificationManager = context . getSystemService ( Context . NOTIFICATION _SERVICE ) as NotificationManager
2022-01-04 00:54:18 +01:00
fun display ( subscription : Subscription , notification : Notification ) {
2021-11-27 22:18:09 +01:00
Log . d ( TAG , " Displaying notification $notification " )
2022-01-04 19:45:02 +01:00
displayInternal ( subscription , notification )
2022-01-04 00:54:18 +01:00
}
2022-01-10 04:08:29 +01: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-05 21:40:40 +01:00
}
2022-01-04 00:54:18 +01:00
fun cancel ( notification : Notification ) {
if ( notification . notificationId != 0 ) {
2022-03-13 20:58:19 +01:00
Log . d ( TAG , " Cancelling notification ${notification.id} : ${decodeMessage(notification)} " )
2022-01-04 00:54:18 +01:00
notificationManager . cancel ( notification . notificationId )
}
}
fun createNotificationChannels ( ) {
2022-01-06 01:05:57 +01:00
( 1. . 5 ) . forEach { priority -> maybeCreateNotificationChannel ( priority ) }
2022-01-04 00:54:18 +01:00
}
2022-01-10 04:08:29 +01:00
private fun displayInternal ( subscription : Subscription , notification : Notification , update : Boolean = false ) {
2021-11-27 22:18:09 +01:00
val title = formatTitle ( subscription , notification )
val channelId = toChannelId ( notification . priority )
2022-01-06 01:05:57 +01:00
val builder = NotificationCompat . Builder ( context , channelId )
2021-11-23 16:52:27 +01:00
. setSmallIcon ( R . drawable . ic _notification )
2022-02-08 00:35:36 +01:00
. setColor ( ContextCompat . getColor ( context , Colors . notificationIcon ) )
2021-11-12 01:41:29 +01:00
. setContentTitle ( title )
2022-01-06 01:05:57 +01:00
. setOnlyAlertOnce ( true ) // Do not vibrate or play sound if already showing (updates!)
2021-11-12 01:41:29 +01:00
. setAutoCancel ( true ) // Cancel when notification is clicked
2022-01-11 23:00:18 +01:00
setStyleAndText ( builder , notification ) // Preview picture or big text style
setClickAction ( builder , subscription , notification )
2022-01-06 01:05:57 +01:00
maybeSetSound ( builder , update )
2022-01-10 04:08:29 +01:00
maybeSetProgress ( builder , notification )
2022-01-06 01:05:57 +01:00
maybeAddOpenAction ( builder , notification )
2022-01-09 03:32:43 +01:00
maybeAddBrowseAction ( builder , notification )
2022-01-11 23:00:18 +01:00
maybeAddDownloadAction ( builder , notification )
2022-01-12 00:21:30 +01:00
maybeAddCancelAction ( builder , notification )
2022-04-19 15:15:06 +02:00
maybeAddUserActions ( builder , notification )
2022-01-06 01:05:57 +01:00
maybeCreateNotificationChannel ( notification . priority )
notificationManager . notify ( notification . notificationId , builder . build ( ) )
}
2021-11-12 01:41:29 +01:00
2022-01-06 01:05:57 +01:00
private fun maybeSetSound ( builder : NotificationCompat . Builder , update : Boolean ) {
if ( ! update ) {
val defaultSoundUri = RingtoneManager . getDefaultUri ( RingtoneManager . TYPE _NOTIFICATION )
builder . setSound ( defaultSoundUri )
} else {
builder . setSound ( null )
}
}
2022-01-11 23:00:18 +01:00
private fun setStyleAndText ( builder : NotificationCompat . Builder , notification : Notification ) {
2022-01-09 03:32:43 +01:00
val contentUri = notification . attachment ?. contentUri
2022-01-09 04:17:41 +01:00
val isSupportedImage = supportedImage ( notification . attachment ?. type )
if ( contentUri != null && isSupportedImage ) {
2022-01-06 01:05:57 +01:00
try {
2022-01-09 03:32:43 +01:00
val resolver = context . applicationContext . contentResolver
val bitmapStream = resolver . openInputStream ( Uri . parse ( contentUri ) )
val bitmap = BitmapFactory . decodeStream ( bitmapStream )
2022-01-06 01:05:57 +01:00
builder
2022-04-19 15:15:06 +02:00
. setContentText ( maybeAppendActionErrors ( formatMessage ( notification ) , notification ) )
2022-01-06 01:05:57 +01:00
. setLargeIcon ( bitmap )
. setStyle ( NotificationCompat . BigPictureStyle ( )
. bigPicture ( bitmap )
. bigLargeIcon ( null ) )
} catch ( _ : Exception ) {
2022-04-19 15:15:06 +02:00
val message = maybeAppendActionErrors ( formatMessageMaybeWithAttachmentInfos ( notification ) , notification )
2022-01-11 23:00:18 +01:00
builder
. setContentText ( message )
. setStyle ( NotificationCompat . BigTextStyle ( ) . bigText ( message ) )
2022-01-06 01:05:57 +01:00
}
} else {
2022-04-19 15:15:06 +02:00
val message = maybeAppendActionErrors ( formatMessageMaybeWithAttachmentInfos ( notification ) , notification )
2022-01-11 23:00:18 +01:00
builder
. setContentText ( message )
. setStyle ( NotificationCompat . BigTextStyle ( ) . bigText ( message ) )
}
}
2022-04-19 15:15:06 +02:00
private fun formatMessageMaybeWithAttachmentInfos ( notification : Notification ) : String {
2022-01-11 23:00:18 +01:00
val message = formatMessage ( notification )
val attachment = notification . attachment ?: return message
2022-04-19 15:15:06 +02:00
val attachmentInfos = if ( attachment . size != null ) {
2022-01-11 23:00:18 +01:00
" ${attachment.name} , ${formatBytes(attachment.size)} "
} else {
attachment . name
}
if ( attachment . progress in 0. . 99 ) {
2022-04-19 15:15:06 +02:00
return context . getString ( R . string . notification _popup _file _downloading , attachmentInfos , attachment . progress , message )
2022-01-06 01:05:57 +01:00
}
2022-04-19 15:15:06 +02:00
if ( attachment . progress == ATTACHMENT _PROGRESS _DONE ) {
return context . getString ( R . string . notification _popup _file _download _successful , message , attachmentInfos )
2022-01-11 23:00:18 +01:00
}
2022-04-19 15:15:06 +02: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 )
}
2022-01-11 23:00:18 +01:00
private fun setClickAction ( builder : NotificationCompat . Builder , subscription : Subscription , notification : Notification ) {
2022-01-06 01:05:57 +01:00
if ( notification . click == " " ) {
builder . setContentIntent ( detailActivityIntent ( subscription ) )
} else {
try {
val uri = Uri . parse ( notification . click )
2022-04-19 15:15:06 +02:00
val viewIntent = PendingIntent . getActivity ( context , Random ( ) . nextInt ( ) , Intent ( Intent . ACTION _VIEW , uri ) , PendingIntent . FLAG _IMMUTABLE )
2022-01-06 01:05:57 +01:00
builder . setContentIntent ( viewIntent )
} catch ( e : Exception ) {
builder . setContentIntent ( detailActivityIntent ( subscription ) )
}
2022-01-04 19:45:02 +01:00
}
2022-01-06 01:05:57 +01:00
}
2022-01-10 04:08:29 +01:00
private fun maybeSetProgress ( builder : NotificationCompat . Builder , notification : Notification ) {
val progress = notification . attachment ?. progress
2022-01-09 03:32:43 +01:00
if ( progress in 0. . 99 ) {
2022-01-10 04:08:29 +01:00
builder . setProgress ( 100 , progress !! , false )
2022-01-04 00:54:18 +01:00
} else {
2022-01-06 01:05:57 +01:00
builder . setProgress ( 0 , 0 , false ) // Remove progress bar
}
}
2022-01-09 03:32:43 +01:00
private fun maybeAddOpenAction ( builder : NotificationCompat . Builder , notification : Notification ) {
2022-01-08 21:49:07 +01:00
if ( notification . attachment ?. contentUri != null ) {
val contentUri = Uri . parse ( notification . attachment . contentUri )
2022-04-17 04:32:29 +02: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-19 15:15:06 +02:00
val pendingIntent = PendingIntent . getActivity ( context , Random ( ) . nextInt ( ) , intent , PendingIntent . FLAG _IMMUTABLE )
2022-01-09 03:32:43 +01:00
builder . addAction ( NotificationCompat . Action . Builder ( 0 , context . getString ( R . string . notification _popup _action _open ) , pendingIntent ) . build ( ) )
2022-01-04 00:54:18 +01:00
}
2022-01-06 01:05:57 +01:00
}
2021-11-12 01:41:29 +01:00
2022-01-09 03:32:43 +01:00
private fun maybeAddBrowseAction ( builder : NotificationCompat . Builder , notification : Notification ) {
if ( notification . attachment ?. contentUri != null ) {
2022-04-17 04:32:29 +02:00
val intent = Intent ( android . app . DownloadManager . ACTION _VIEW _DOWNLOADS ) . apply {
addFlags ( Intent . FLAG _GRANT _READ _URI _PERMISSION )
}
2022-04-19 15:15:06 +02:00
val pendingIntent = PendingIntent . getActivity ( context , Random ( ) . nextInt ( ) , intent , PendingIntent . FLAG _IMMUTABLE )
2022-01-09 03:32:43 +01:00
builder . addAction ( NotificationCompat . Action . Builder ( 0 , context . getString ( R . string . notification _popup _action _browse ) , pendingIntent ) . build ( ) )
2022-01-06 01:05:57 +01:00
}
2021-11-15 22:24:31 +01:00
}
2022-01-11 23:00:18 +01:00
private fun maybeAddDownloadAction ( builder : NotificationCompat . Builder , notification : Notification ) {
2022-04-19 15:15:06 +02:00
if ( notification . attachment ?. contentUri == null && listOf ( ATTACHMENT _PROGRESS _NONE , ATTACHMENT _PROGRESS _FAILED ) . contains ( notification . attachment ?. progress ) ) {
2022-04-17 04:32:29 +02: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-19 15:15:06 +02:00
val pendingIntent = PendingIntent . getBroadcast ( context , Random ( ) . nextInt ( ) , intent , PendingIntent . FLAG _UPDATE _CURRENT or PendingIntent . FLAG _IMMUTABLE )
2022-01-11 23:00:18 +01:00
builder . addAction ( NotificationCompat . Action . Builder ( 0 , context . getString ( R . string . notification _popup _action _download ) , pendingIntent ) . build ( ) )
}
}
2022-01-12 00:21:30 +01:00
private fun maybeAddCancelAction ( builder : NotificationCompat . Builder , notification : Notification ) {
if ( notification . attachment ?. contentUri == null && notification . attachment ?. progress in 0. . 99 ) {
2022-04-17 04:32:29 +02: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-19 15:15:06 +02:00
val pendingIntent = PendingIntent . getBroadcast ( context , Random ( ) . nextInt ( ) , intent , PendingIntent . FLAG _UPDATE _CURRENT or PendingIntent . FLAG _IMMUTABLE )
2022-01-12 00:21:30 +01:00
builder . addAction ( NotificationCompat . Action . Builder ( 0 , context . getString ( R . string . notification _popup _action _cancel ) , pendingIntent ) . build ( ) )
}
}
2022-04-19 15:15:06 +02:00
private fun maybeAddUserActions ( builder : NotificationCompat . Builder , notification : Notification ) {
2022-04-17 02:12:40 +02:00
notification . actions ?. forEach { action ->
2022-04-17 20:29:29 +02:00
when ( action . action . lowercase ( Locale . getDefault ( ) ) ) {
ACTION _VIEW -> maybeAddViewUserAction ( builder , action )
2022-04-19 15:15:06 +02:00
ACTION _HTTP , ACTION _BROADCAST -> maybeAddHttpOrBroadcastUserAction ( builder , notification , action )
2022-04-17 02:12:40 +02:00
}
}
}
2022-04-17 04:32:29 +02:00
private fun maybeAddViewUserAction ( builder : NotificationCompat . Builder , action : Action ) {
2022-04-20 01:20:39 +02:00
// Note that this function is (almost) duplicated in DetailAdapter, since we need to be able
// to open a link from the detail activity as well. We can't do this in the UserActionWorker,
// because the behavior is kind of weird in Android.
2022-04-17 02:12:40 +02:00
try {
2022-04-17 04:32:29 +02:00
val url = action . url ?: return
val intent = Intent ( Intent . ACTION _VIEW , Uri . parse ( url ) ) . apply {
addFlags ( Intent . FLAG _GRANT _READ _URI _PERMISSION )
}
2022-04-19 15:15:06 +02:00
val pendingIntent = PendingIntent . getActivity ( context , Random ( ) . nextInt ( ) , intent , PendingIntent . FLAG _IMMUTABLE )
2022-04-17 02:12:40 +02: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-19 15:15:06 +02:00
private fun maybeAddHttpOrBroadcastUserAction ( builder : NotificationCompat . Builder , notification : Notification , action : Action ) {
2022-04-17 04:32:29 +02:00
val intent = Intent ( context , UserActionBroadcastReceiver :: class . java ) . apply {
2022-04-17 20:29:29 +02:00
putExtra ( BROADCAST _EXTRA _TYPE , BROADCAST _TYPE _USER _ACTION )
2022-04-17 04:32:29 +02:00
putExtra ( BROADCAST _EXTRA _NOTIFICATION _ID , notification . id )
2022-04-17 20:29:29 +02:00
putExtra ( BROADCAST _EXTRA _ACTION _ID , action . id )
2022-04-17 04:32:29 +02:00
}
2022-04-19 15:15:06 +02:00
val pendingIntent = PendingIntent . getBroadcast ( context , Random ( ) . nextInt ( ) , intent , PendingIntent . FLAG _UPDATE _CURRENT or PendingIntent . FLAG _IMMUTABLE )
2022-04-20 01:20:39 +02:00
val label = formatActionLabel ( action )
2022-04-19 15:15:06 +02:00
builder . addAction ( NotificationCompat . Action . Builder ( 0 , label , pendingIntent ) . build ( ) )
2022-04-17 04:32:29 +02:00
}
class UserActionBroadcastReceiver : BroadcastReceiver ( ) {
2022-01-11 23:00:18 +01:00
override fun onReceive ( context : Context , intent : Intent ) {
2022-04-17 04:32:29 +02:00
val type = intent . getStringExtra ( BROADCAST _EXTRA _TYPE ) ?: return
val notificationId = intent . getStringExtra ( BROADCAST _EXTRA _NOTIFICATION _ID ) ?: return
when ( type ) {
BROADCAST _TYPE _DOWNLOAD _START -> DownloadManager . enqueue ( context , notificationId , userAction = true )
BROADCAST _TYPE _DOWNLOAD _CANCEL -> DownloadManager . cancel ( context , notificationId )
2022-04-17 20:29:29 +02:00
BROADCAST _TYPE _USER _ACTION -> {
val actionId = intent . getStringExtra ( BROADCAST _EXTRA _ACTION _ID ) ?: return
UserActionManager . enqueue ( context , notificationId , actionId )
}
2022-01-12 00:21:30 +01:00
}
2022-01-11 23:00:18 +01:00
}
}
2022-01-04 23:45:24 +01:00
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 )
return TaskStackBuilder . create ( context ) . run {
addNextIntentWithParentStack ( intent ) // Add the intent, which inflates the back stack
2022-01-28 18:47:45 +01:00
getPendingIntent ( 0 , PendingIntent . FLAG _UPDATE _CURRENT or PendingIntent . FLAG _IMMUTABLE ) // Get the PendingIntent containing the entire back stack
2022-01-04 23:45:24 +01:00
}
}
2022-01-06 01:05:57 +01:00
private fun maybeCreateNotificationChannel ( priority : Int ) {
2021-11-29 20:06:08 +01:00
if ( Build . VERSION . SDK _INT >= Build . VERSION_CODES . O ) {
// Note: To change a notification channel, you must delete the old one and create a new one!
val pause = 300L
val channel = when ( priority ) {
1 -> NotificationChannel ( CHANNEL _ID _MIN , context . getString ( R . string . channel _notifications _min _name ) , NotificationManager . IMPORTANCE _MIN )
2 -> NotificationChannel ( CHANNEL _ID _LOW , context . getString ( R . string . channel _notifications _low _name ) , NotificationManager . IMPORTANCE _LOW )
4 -> {
val channel = NotificationChannel ( CHANNEL _ID _HIGH , context . getString ( R . string . channel _notifications _high _name ) , NotificationManager . IMPORTANCE _HIGH )
channel . enableVibration ( true )
channel . vibrationPattern = longArrayOf (
pause , 100 , pause , 100 , pause , 100 ,
pause , 2000
)
channel
}
5 -> {
2022-01-14 18:32:36 +01:00
val channel = NotificationChannel ( CHANNEL _ID _MAX , context . getString ( R . string . channel _notifications _max _name ) , NotificationManager . IMPORTANCE _HIGH ) // IMPORTANCE_MAX does not exist
2021-11-29 20:06:08 +01:00
channel . enableLights ( true )
channel . enableVibration ( true )
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
}
else -> NotificationChannel ( CHANNEL _ID _DEFAULT , context . getString ( R . string . channel _notifications _default _name ) , NotificationManager . IMPORTANCE _DEFAULT )
}
notificationManager . createNotificationChannel ( channel )
2021-11-27 22:18:09 +01:00
}
}
private fun toChannelId ( priority : Int ) : String {
return when ( priority ) {
1 -> CHANNEL _ID _MIN
2 -> CHANNEL _ID _LOW
4 -> CHANNEL _ID _HIGH
5 -> CHANNEL _ID _MAX
else -> CHANNEL _ID _DEFAULT
}
}
2021-11-12 01:41:29 +01:00
companion object {
2022-04-20 01:20:39 +02:00
const val ACTION _VIEW = " view "
const val ACTION _HTTP = " http "
const val ACTION _BROADCAST = " broadcast "
2022-04-19 15:15:06 +02:00
2022-04-20 01:20:39 +02:00
const val BROADCAST _EXTRA _TYPE = " type "
const val BROADCAST _EXTRA _NOTIFICATION _ID = " notificationId "
const val BROADCAST _EXTRA _ACTION _ID = " action "
2022-04-17 04:32:29 +02:00
2022-04-20 01:20:39 +02: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 04:32:29 +02:00
2022-04-20 01:20:39 +02:00
private const val TAG = " NtfyNotifService "
2022-01-09 03:32:43 +01:00
2021-11-27 22:18:09 +01:00
private const val CHANNEL _ID _MIN = " ntfy-min "
private const val CHANNEL _ID _LOW = " ntfy-low "
private const val CHANNEL _ID _DEFAULT = " ntfy "
private const val CHANNEL _ID _HIGH = " ntfy-high "
private const val CHANNEL _ID _MAX = " ntfy-max "
2021-11-12 01:41:29 +01:00
}
}