1
+ package com.reactnativecustomtimernotification
2
+
3
+ import android.app.NotificationChannel
4
+ import android.app.NotificationManager
5
+ import android.content.Context
6
+ import android.os.Build
7
+ import android.os.Handler
8
+ import android.os.Looper
9
+ import android.os.SystemClock
10
+ import android.text.Html
11
+ import android.util.Log
12
+ import android.view.View
13
+ import android.widget.RemoteViews
14
+ import androidx.core.app.NotificationCompat
15
+ import kotlinx.coroutines.CoroutineScope
16
+ import kotlinx.coroutines.Dispatchers
17
+ import kotlinx.coroutines.launch
18
+ import kotlinx.coroutines.withContext
19
+ import kotlin.math.abs
20
+ import android.app.PendingIntent
21
+ import android.content.Intent
22
+
23
+ import android.content.BroadcastReceiver
24
+ import com.facebook.react.bridge.WritableMap
25
+ import com.facebook.react.modules.core.DeviceEventManagerModule
26
+ import android.content.IntentFilter
27
+ import com.facebook.react.bridge.Arguments
28
+ import com.facebook.react.bridge.ReactApplicationContext
29
+ import androidx.core.content.ContextCompat
30
+
31
+
32
+ data class NotificationConfig (
33
+ val notificationId : Int? ,
34
+ val gifUrl : String? ,
35
+ val title : String? ,
36
+ val subtitle : String? ,
37
+ val smallIcon : Int = android.R .drawable.ic_dialog_info,
38
+ val countdownDuration : Long = 5000 ,
39
+ val payload : String?
40
+ )
41
+
42
+
43
+ class AnimatedNotificationManager (
44
+ private val context : ReactApplicationContext ,
45
+ private val notificationManager : NotificationManager = context.getSystemService(Context .NOTIFICATION_SERVICE ) as NotificationManager
46
+ ) {
47
+ companion object {
48
+ private const val CHANNEL_ID = " animated_notification_channel"
49
+ private const val NOTIFICATION_ID = 1
50
+ private const val TAG = " AnimatedNotificationManager"
51
+ private const val GIF_MEMORY_LIMIT_MB = 4
52
+ private const val FRAME_INTERVAL_MS = 100
53
+ }
54
+ var disableCurrentNotification: Boolean = false
55
+ init {
56
+ createNotificationChannel()
57
+
58
+ context.registerReceiver(object : BroadcastReceiver () {
59
+ override fun onReceive (currentContext : Context , intent : Intent ) {
60
+ try {
61
+ val extras = intent.extras
62
+ val params: WritableMap = Arguments .createMap()
63
+ params.putString(" action" , extras!! .getString(" action" ))
64
+ params.putString(" payload" , extras!! .getString(" payload" ))
65
+ Log .d(TAG , extras?.getString(" payload" )? : " " )
66
+ if (extras!! .getString(" action" ) == " cancel" ){
67
+ disableCurrentNotification = true
68
+ }
69
+ context.getJSModule(DeviceEventManagerModule .RCTDeviceEventEmitter ::class .java)
70
+ .emit(
71
+ " notificationClick" ,
72
+ params
73
+ )
74
+ } catch (e: Exception ) {
75
+ Log .i(" ReactSystemNotification error" , e.toString())
76
+ }
77
+ }
78
+ }, IntentFilter (" NotificationEvent" ), ContextCompat .RECEIVER_EXPORTED )
79
+ }
80
+
81
+ private fun createNotificationChannel () {
82
+ if (Build .VERSION .SDK_INT >= Build .VERSION_CODES .O ) {
83
+ val channel = NotificationChannel (
84
+ CHANNEL_ID ,
85
+ " Animated Notifications" ,
86
+ NotificationManager .IMPORTANCE_HIGH
87
+ ).apply {
88
+ description = " Channel for animated and interactive notifications"
89
+ }
90
+ notificationManager.createNotificationChannel(channel)
91
+ }
92
+ }
93
+
94
+ fun showAnimatedNotification (config : NotificationConfig ) {
95
+ val notificationConfig = config
96
+
97
+ Log .d(TAG , " Initiating animated notification display" )
98
+
99
+ CoroutineScope (Dispatchers .Main ).launch {
100
+ try {
101
+ val notificationLayout = createNotificationLayout(notificationConfig)
102
+ val notification = buildNotification(notificationLayout, notificationConfig)
103
+
104
+ notificationManager.notify(NOTIFICATION_ID , notification.build())
105
+
106
+ scheduleNotificationUpdate(notificationLayout, notification, notificationConfig)
107
+ } catch (e: Exception ) {
108
+ Log .e(TAG , " Error displaying notification" , e)
109
+ }
110
+ }
111
+ }
112
+
113
+ private suspend fun createNotificationLayout (config : NotificationConfig ): RemoteViews = withContext(Dispatchers .Default ) {
114
+
115
+ val remoteViews = RemoteViews (context.packageName, R .layout.gen_notification_open)
116
+
117
+ if (config.gifUrl != = null ){
118
+ val frames = processGif(config.gifUrl, memoryLimitMB = GIF_MEMORY_LIMIT_MB )
119
+
120
+ frames.forEach { frame ->
121
+ val frameView = RemoteViews (context.packageName, R .layout.giffy_image)
122
+ frameView.setImageViewBitmap(R .id.frameImage, frame)
123
+ frameView.setViewVisibility(R .id.frameImage, View .VISIBLE )
124
+ remoteViews.addView(R .id.viewFlipper, frameView)
125
+ }
126
+ remoteViews.setInt(R .id.viewFlipper, " setFlipInterval" , FRAME_INTERVAL_MS )
127
+ } else {
128
+ remoteViews.setViewVisibility(R .id.viewFlipperContainer, View .GONE )
129
+ }
130
+
131
+ configureNotificationText(remoteViews, config)
132
+ configureChronometer(remoteViews, config.countdownDuration)
133
+
134
+ return @withContext remoteViews
135
+ }
136
+
137
+ private fun configureNotificationText (remoteViews : RemoteViews , config : NotificationConfig ) {
138
+ val titleHtml = if (config.title != null ) {
139
+ if (Build .VERSION .SDK_INT >= Build .VERSION_CODES .N ) {
140
+ Html .fromHtml(config.title, Html .FROM_HTML_MODE_COMPACT )
141
+ } else {
142
+ Html .fromHtml(config.title)
143
+ }
144
+ } else null
145
+
146
+ val subtitleHtml = if (config.title != null ) {
147
+ if (Build .VERSION .SDK_INT >= Build .VERSION_CODES .N ) {
148
+ Html .fromHtml(config.subtitle, Html .FROM_HTML_MODE_COMPACT )
149
+ } else {
150
+ Html .fromHtml(config.subtitle)
151
+ }
152
+ } else null
153
+
154
+ if (titleHtml != null )
155
+ remoteViews.setTextViewText(R .id.title, titleHtml)
156
+
157
+ if (subtitleHtml != null )
158
+ remoteViews.setTextViewText(R .id.subtitle, subtitleHtml)
159
+ }
160
+
161
+ private fun configureChronometer (remoteViews : RemoteViews , countdownDuration : Long ) {
162
+ val chronometerBaseTime = countdownDuration
163
+ remoteViews.setChronometerCountDown(R .id.simpleChronometer, true )
164
+ remoteViews.setChronometer(R .id.simpleChronometer, chronometerBaseTime, null , true )
165
+ }
166
+
167
+ private fun buildNotification (remoteViews : RemoteViews , config : NotificationConfig ): NotificationCompat .Builder {
168
+ val intent = Intent (context, NotificationEventReceiver ::class .java)
169
+ intent.flags = Intent .FLAG_ACTIVITY_CLEAR_TOP or Intent .FLAG_ACTIVITY_NEW_TASK
170
+ intent.putExtra(" id" ,config.notificationId);
171
+ intent.putExtra(" action" ," press" );
172
+ intent.putExtra(" payload" ,config.payload);
173
+ Log .d(TAG , config?.payload ? : " " )
174
+ var pendingIntent: PendingIntent ? = null ;
175
+
176
+ if (Build .VERSION .SDK_INT >= Build .VERSION_CODES .S ) {
177
+ pendingIntent = PendingIntent .getBroadcast(context, 0 , intent, PendingIntent .FLAG_IMMUTABLE );
178
+ } else {
179
+ pendingIntent = PendingIntent .getBroadcast(context, 0 , intent, PendingIntent .FLAG_UPDATE_CURRENT );
180
+ }
181
+
182
+ val onCancelIntent = Intent (context, OnClickBroadcastReceiver ::class .java)
183
+ onCancelIntent.putExtra(" id" ,config.notificationId);
184
+ onCancelIntent.putExtra(" action" ," cancel" );
185
+ onCancelIntent.putExtra(" payload" , config.payload);
186
+ var onDismissPendingIntent: PendingIntent ? = null ;
187
+
188
+ if (Build .VERSION .SDK_INT >= Build .VERSION_CODES .S ) {
189
+ onDismissPendingIntent = PendingIntent .getBroadcast(
190
+ context,
191
+ 0 ,
192
+ onCancelIntent,
193
+ PendingIntent .FLAG_IMMUTABLE // Set the mutability flag to mutable
194
+ );
195
+ } else {
196
+ onDismissPendingIntent =
197
+ PendingIntent .getBroadcast(context, 0 , onCancelIntent, 0 )
198
+ }
199
+
200
+ return NotificationCompat .Builder (context, CHANNEL_ID )
201
+ .setSmallIcon(config.smallIcon)
202
+ .setCustomContentView(remoteViews)
203
+ .setCustomBigContentView(remoteViews)
204
+ .setOnlyAlertOnce(true )
205
+ .setAutoCancel(true )
206
+ .setDeleteIntent(onDismissPendingIntent)
207
+ .setContentIntent(pendingIntent)
208
+
209
+
210
+ }
211
+
212
+ private fun scheduleNotificationUpdate (
213
+ remoteViews : RemoteViews ,
214
+ notification : NotificationCompat .Builder ,
215
+ config : NotificationConfig
216
+ ) {
217
+ val chronometerBaseTime = config.countdownDuration
218
+ val currentTime = SystemClock .elapsedRealtime()
219
+ val delay = abs(chronometerBaseTime - currentTime)
220
+ disableCurrentNotification = false
221
+
222
+ Handler (Looper .getMainLooper()).postDelayed({
223
+ if (! disableCurrentNotification){
224
+ Log .d(TAG , " Countdown complete, updating notification" )
225
+ remoteViews.setViewVisibility(R .id.simpleChronometer, View .GONE )
226
+ notification.setCustomContentView(remoteViews)
227
+ notificationManager.notify(NOTIFICATION_ID , notification.build())
228
+ }
229
+ }, delay)
230
+ }
231
+
232
+ fun removeNotification (id : Int ) {
233
+ notificationManager.cancel( id ) ;
234
+ }
235
+ }
0 commit comments