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