3232
3333import java .util .ArrayList ;
3434import java .util .Date ;
35- import java .util .HashMap ;
3635import java .util .Iterator ;
3736import java .util .List ;
3837import java .util .Map ;
38+ import java .util .concurrent .ConcurrentHashMap ;
3939import java .util .concurrent .ExecutionException ;
4040
41+ import chat .rocket .reactnative .BuildConfig ;
4142import chat .rocket .reactnative .R ;
4243
4344/**
4748 * For E2E notifications, waits for React Native initialization before decrypting and displaying.
4849 */
4950public class CustomPushNotification extends PushNotification {
50- private static final String TAG = "RocketChat.Push" ;
51+ private static final String TAG = "RocketChat.CustomPush" ;
52+ private static final boolean ENABLE_VERBOSE_LOGS = BuildConfig .DEBUG ;
5153
5254 // Shared state
53- public static ReactApplicationContext reactApplicationContext ;
55+ public static volatile ReactApplicationContext reactApplicationContext ;
5456 private static final Gson gson = new Gson ();
55- private static final Map <String , List <Bundle >> notificationMessages = new HashMap <>();
57+ private static final Map <String , List <Bundle >> notificationMessages = new ConcurrentHashMap <>();
5658
5759 // Constants
5860 public static final String KEY_REPLY = "KEY_REPLY" ;
@@ -81,28 +83,140 @@ public static void clearMessages(int notId) {
8183
8284 @ Override
8385 public void onReceived () throws InvalidNotificationException {
86+ Bundle bundle = mNotificationProps .asBundle ();
87+ String notId = bundle .getString ("notId" );
88+
89+ if (notId == null || notId .isEmpty ()) {
90+ throw new InvalidNotificationException ("Missing notification ID" );
91+ }
92+
93+ try {
94+ Integer .parseInt (notId );
95+ } catch (NumberFormatException e ) {
96+ throw new InvalidNotificationException ("Invalid notification ID format: " + notId );
97+ }
98+
99+ // Check if React is ready - needed for MMKV access (avatars, encryption, message-id-only)
100+ if (!mAppLifecycleFacade .isReactInitialized ()) {
101+ android .util .Log .w (TAG , "React not initialized yet, waiting before processing notification..." );
102+
103+ // Wait for React to initialize with timeout
104+ new Thread (() -> {
105+ int attempts = 0 ;
106+ int maxAttempts = 50 ; // 5 seconds total (50 * 100ms)
107+
108+ while (!mAppLifecycleFacade .isReactInitialized () && attempts < maxAttempts ) {
109+ try {
110+ Thread .sleep (100 ); // Wait 100ms
111+ attempts ++;
112+
113+ if (attempts % 10 == 0 && ENABLE_VERBOSE_LOGS ) {
114+ android .util .Log .d (TAG , "Still waiting for React initialization... (" + (attempts * 100 ) + "ms elapsed)" );
115+ }
116+ } catch (InterruptedException e ) {
117+ android .util .Log .e (TAG , "Wait interrupted" , e );
118+ Thread .currentThread ().interrupt ();
119+ return ;
120+ }
121+ }
122+
123+ if (mAppLifecycleFacade .isReactInitialized ()) {
124+ android .util .Log .i (TAG , "React initialized after " + (attempts * 100 ) + "ms, proceeding with notification" );
125+ try {
126+ handleNotification ();
127+ } catch (Exception e ) {
128+ android .util .Log .e (TAG , "Failed to process notification after React initialization" , e );
129+ }
130+ } else {
131+ android .util .Log .e (TAG , "Timeout waiting for React initialization after " + (maxAttempts * 100 ) + "ms, processing without MMKV" );
132+ try {
133+ handleNotification ();
134+ } catch (Exception e ) {
135+ android .util .Log .e (TAG , "Failed to process notification without React context" , e );
136+ }
137+ }
138+ }).start ();
139+
140+ return ; // Exit early, notification will be processed in the thread
141+ }
142+
143+ if (ENABLE_VERBOSE_LOGS ) {
144+ android .util .Log .d (TAG , "React already initialized, proceeding with notification" );
145+ }
84146
85- // Load notification data from server if needed
147+ try {
148+ handleNotification ();
149+ } catch (Exception e ) {
150+ android .util .Log .e (TAG , "Failed to process notification on main thread" , e );
151+ throw new InvalidNotificationException ("Notification processing failed: " + e .getMessage ());
152+ }
153+ }
154+
155+ private void handleNotification () {
86156 Bundle received = mNotificationProps .asBundle ();
87157 Ejson receivedEjson = safeFromJson (received .getString ("ejson" , "{}" ), Ejson .class );
88158
89- if (receivedEjson != null && receivedEjson .notificationType != null &&
90- receivedEjson .notificationType .equals ("message-id-only" )) {
91- notificationLoad (receivedEjson , new Callback () {
92- @ Override
93- public void call (@ Nullable Bundle bundle ) {
94- if (bundle != null ) {
159+ if (receivedEjson != null && receivedEjson .notificationType != null && receivedEjson .notificationType .equals ("message-id-only" )) {
160+ android .util .Log .d (TAG , "Detected message-id-only notification, will fetch full content from server" );
161+ loadNotificationAndProcess (receivedEjson );
162+ return ; // Exit early, notification will be processed in callback
163+ }
164+
165+ // For non-message-id-only notifications, process immediately
166+ processNotification ();
167+ }
168+
169+ private void loadNotificationAndProcess (Ejson ejson ) {
170+ notificationLoad (ejson , new Callback () {
171+ @ Override
172+ public void call (@ Nullable Bundle bundle ) {
173+ if (bundle != null ) {
174+ android .util .Log .d (TAG , "Successfully loaded notification content from server, updating notification props" );
175+
176+ if (ENABLE_VERBOSE_LOGS ) {
177+ // BEFORE createProps
178+ android .util .Log .d (TAG , "[BEFORE createProps] bundle.notificationLoaded=" + bundle .getBoolean ("notificationLoaded" , false ));
179+ android .util .Log .d (TAG , "[BEFORE createProps] bundle.title=" + (bundle .getString ("title" ) != null ? "[present]" : "[null]" ));
180+ android .util .Log .d (TAG , "[BEFORE createProps] bundle.message length=" + (bundle .getString ("message" ) != null ? bundle .getString ("message" ).length () : 0 ));
181+ android .util .Log .d (TAG , "[BEFORE createProps] bundle has ejson=" + (bundle .getString ("ejson" ) != null ));
182+ }
183+
184+ synchronized (CustomPushNotification .this ) {
95185 mNotificationProps = createProps (bundle );
96186 }
187+
188+ if (ENABLE_VERBOSE_LOGS ) {
189+ // AFTER createProps - verify it worked
190+ Bundle verifyBundle = mNotificationProps .asBundle ();
191+ android .util .Log .d (TAG , "[AFTER createProps] mNotificationProps.notificationLoaded=" + verifyBundle .getBoolean ("notificationLoaded" , false ));
192+ android .util .Log .d (TAG , "[AFTER createProps] mNotificationProps.title=" + (verifyBundle .getString ("title" ) != null ? "[present]" : "[null]" ));
193+ android .util .Log .d (TAG , "[AFTER createProps] mNotificationProps.message length=" + (verifyBundle .getString ("message" ) != null ? verifyBundle .getString ("message" ).length () : 0 ));
194+ android .util .Log .d (TAG , "[AFTER createProps] mNotificationProps has ejson=" + (verifyBundle .getString ("ejson" ) != null ));
195+ }
196+ } else {
197+ android .util .Log .w (TAG , "Failed to load notification content from server, will display placeholder notification" );
97198 }
98- });
99- }
100-
101- // Re-read values (may have changed from notificationLoad)
199+
200+ processNotification ();
201+ }
202+ });
203+ }
204+
205+ private void processNotification () {
206+ // We should re-read these values since that can be changed by notificationLoad
102207 Bundle bundle = mNotificationProps .asBundle ();
103208 Ejson loadedEjson = safeFromJson (bundle .getString ("ejson" , "{}" ), Ejson .class );
104209 String notId = bundle .getString ("notId" , "1" );
105210
211+ if (ENABLE_VERBOSE_LOGS ) {
212+ android .util .Log .d (TAG , "[processNotification] notId=" + notId );
213+ android .util .Log .d (TAG , "[processNotification] bundle.notificationLoaded=" + bundle .getBoolean ("notificationLoaded" , false ));
214+ android .util .Log .d (TAG , "[processNotification] bundle.title=" + (bundle .getString ("title" ) != null ? "[present]" : "[null]" ));
215+ android .util .Log .d (TAG , "[processNotification] bundle.message length=" + (bundle .getString ("message" ) != null ? bundle .getString ("message" ).length () : 0 ));
216+ android .util .Log .d (TAG , "[processNotification] loadedEjson.notificationType=" + (loadedEjson != null ? loadedEjson .notificationType : "null" ));
217+ android .util .Log .d (TAG , "[processNotification] loadedEjson.sender=" + (loadedEjson != null && loadedEjson .sender != null ? loadedEjson .sender .username : "null" ));
218+ }
219+
106220 // Handle E2E encrypted notifications
107221 if (isE2ENotification (loadedEjson )) {
108222 handleE2ENotification (bundle , loadedEjson , notId );
@@ -191,15 +305,26 @@ private void showNotification(Bundle bundle, Ejson ejson, String notId) {
191305 bundle .putLong ("time" , new Date ().getTime ());
192306 bundle .putString ("username" , hasSender ? ejson .sender .username : title );
193307 bundle .putString ("senderId" , hasSender ? ejson .sender ._id : "1" );
194- bundle .putString ("avatarUri" , ejson != null ? ejson .getAvatarUri () : null );
308+
309+ String avatarUri = ejson != null ? ejson .getAvatarUri () : null ;
310+ if (ENABLE_VERBOSE_LOGS ) {
311+ android .util .Log .d (TAG , "[showNotification] avatarUri=" + (avatarUri != null ? "[present]" : "[null]" ));
312+ }
313+ bundle .putString ("avatarUri" , avatarUri );
195314
196315 // Handle special notification types
197316 if (ejson != null && ejson .notificationType instanceof String &&
198317 ejson .notificationType .equals ("videoconf" )) {
199318 notifyReceivedToJS ();
200319 } else {
201320 // Show regular notification
321+ if (ENABLE_VERBOSE_LOGS ) {
322+ android .util .Log .d (TAG , "[Before add to notificationMessages] notId=" + notId + ", bundle.message length=" + (bundle .getString ("message" ) != null ? bundle .getString ("message" ).length () : 0 ) + ", bundle.notificationLoaded=" + bundle .getBoolean ("notificationLoaded" , false ));
323+ }
202324 notificationMessages .get (notId ).add (bundle );
325+ if (ENABLE_VERBOSE_LOGS ) {
326+ android .util .Log .d (TAG , "[After add] notificationMessages[" + notId + "].size=" + notificationMessages .get (notId ).size ());
327+ }
203328 postNotification (Integer .parseInt (notId ));
204329 notifyReceivedToJS ();
205330 }
@@ -224,6 +349,16 @@ protected Notification.Builder getNotificationBuilder(PendingIntent intent) {
224349 Boolean notificationLoaded = bundle .getBoolean ("notificationLoaded" , false );
225350 Ejson ejson = safeFromJson (bundle .getString ("ejson" , "{}" ), Ejson .class );
226351
352+ if (ENABLE_VERBOSE_LOGS ) {
353+ android .util .Log .d (TAG , "[getNotificationBuilder] notId=" + notId );
354+ android .util .Log .d (TAG , "[getNotificationBuilder] notificationLoaded=" + notificationLoaded );
355+ android .util .Log .d (TAG , "[getNotificationBuilder] title=" + (title != null ? "[present]" : "[null]" ));
356+ android .util .Log .d (TAG , "[getNotificationBuilder] message length=" + (message != null ? message .length () : 0 ));
357+ android .util .Log .d (TAG , "[getNotificationBuilder] ejson=" + (ejson != null ? "present" : "null" ));
358+ android .util .Log .d (TAG , "[getNotificationBuilder] ejson.notificationType=" + (ejson != null ? ejson .notificationType : "null" ));
359+ android .util .Log .d (TAG , "[getNotificationBuilder] ejson.sender=" + (ejson != null && ejson .sender != null ? ejson .sender .username : "null" ));
360+ }
361+
227362 notification
228363 .setContentTitle (title )
229364 .setContentText (message )
@@ -240,11 +375,13 @@ protected Notification.Builder getNotificationBuilder(PendingIntent intent) {
240375
241376 // if notificationType is null (RC < 3.5) or notificationType is different of message-id-only or notification was loaded successfully
242377 if (ejson == null || ejson .notificationType == null || !ejson .notificationType .equals ("message-id-only" ) || notificationLoaded ) {
378+ android .util .Log .i (TAG , "[getNotificationBuilder] ✅ Rendering FULL notification style (ejson=" + (ejson != null ) + ", notificationType=" + (ejson != null ? ejson .notificationType : "null" ) + ", notificationLoaded=" + notificationLoaded + ")" );
243379 notificationStyle (notification , notificationId , bundle );
244380 notificationReply (notification , notificationId , bundle );
245381
246382 // message couldn't be loaded from server (Fallback notification)
247383 } else {
384+ android .util .Log .w (TAG , "[getNotificationBuilder] ⚠️ Rendering FALLBACK notification (ejson=" + (ejson != null ) + ", notificationType=" + (ejson != null ? ejson .notificationType : "null" ) + ", notificationLoaded=" + notificationLoaded + ")" );
248385 // iterate over the current notification ids to dismiss fallback notifications from same server
249386 for (Map .Entry <String , List <Bundle >> bundleList : notificationMessages .entrySet ()) {
250387 // iterate over the notifications with this id (same host + rid)
@@ -257,7 +394,16 @@ protected Notification.Builder getNotificationBuilder(PendingIntent intent) {
257394 if (ejson != null && notEjson != null && ejson .serverURL ().equals (notEjson .serverURL ())) {
258395 String id = not .getString ("notId" );
259396 // cancel this notification
260- notificationManager .cancel (Integer .parseInt (id ));
397+ if (notificationManager != null ) {
398+ try {
399+ notificationManager .cancel (Integer .parseInt (id ));
400+ if (ENABLE_VERBOSE_LOGS ) {
401+ android .util .Log .d (TAG , "Cancelled previous fallback notification from same server" );
402+ }
403+ } catch (NumberFormatException e ) {
404+ android .util .Log .e (TAG , "Invalid notification ID for cancel: " + id , e );
405+ }
406+ }
261407 }
262408 }
263409 }
@@ -274,14 +420,42 @@ private void notifyReceivedToJS() {
274420 }
275421
276422 private Bitmap getAvatar (String uri ) {
423+ if (uri == null || uri .isEmpty ()) {
424+ if (ENABLE_VERBOSE_LOGS ) {
425+ android .util .Log .w (TAG , "getAvatar called with null/empty URI" );
426+ }
427+ return largeIcon ();
428+ }
429+
430+ // Sanitize URL for logging (remove query params with tokens)
431+ String sanitizedUri = uri ;
432+ int queryStart = uri .indexOf ("?" );
433+ if (queryStart != -1 ) {
434+ sanitizedUri = uri .substring (0 , queryStart ) + "?[auth_params]" ;
435+ }
436+
437+ if (ENABLE_VERBOSE_LOGS ) {
438+ android .util .Log .d (TAG , "Fetching avatar from: " + sanitizedUri );
439+ }
440+
277441 try {
278- return Glide .with (mContext )
442+ Bitmap avatar = Glide .with (mContext )
279443 .asBitmap ()
280444 .apply (RequestOptions .bitmapTransform (new RoundedCorners (10 )))
281445 .load (uri )
282446 .submit (100 , 100 )
283447 .get ();
448+
449+ if (avatar != null ) {
450+ if (ENABLE_VERBOSE_LOGS ) {
451+ android .util .Log .d (TAG , "Successfully loaded avatar" );
452+ }
453+ } else {
454+ android .util .Log .w (TAG , "Avatar loaded but is null" );
455+ }
456+ return avatar != null ? avatar : largeIcon ();
284457 } catch (final ExecutionException | InterruptedException e ) {
458+ android .util .Log .e (TAG , "Failed to fetch avatar: " + e .getMessage (), e );
285459 return largeIcon ();
286460 }
287461 }
@@ -324,7 +498,11 @@ private void notificationChannel(Notification.Builder notification) {
324498 NotificationManager .IMPORTANCE_HIGH );
325499
326500 final NotificationManager notificationManager = (NotificationManager ) mContext .getSystemService (Context .NOTIFICATION_SERVICE );
327- notificationManager .createNotificationChannel (channel );
501+ if (notificationManager != null ) {
502+ notificationManager .createNotificationChannel (channel );
503+ } else {
504+ android .util .Log .e (TAG , "NotificationManager is null, cannot create notification channel" );
505+ }
328506
329507 notification .setChannelId (CHANNEL_ID );
330508 }
@@ -351,6 +529,15 @@ private void notificationColor(Notification.Builder notification) {
351529 private void notificationStyle (Notification .Builder notification , int notId , Bundle bundle ) {
352530 List <Bundle > bundles = notificationMessages .get (Integer .toString (notId ));
353531
532+ if (ENABLE_VERBOSE_LOGS ) {
533+ android .util .Log .d (TAG , "[notificationStyle] notId=" + notId + ", bundles=" + (bundles != null ? bundles .size () : "null" ));
534+ if (bundles != null && bundles .size () > 0 ) {
535+ Bundle firstBundle = bundles .get (0 );
536+ android .util .Log .d (TAG , "[notificationStyle] first bundle.message length=" + (firstBundle .getString ("message" ) != null ? firstBundle .getString ("message" ).length () : 0 ));
537+ android .util .Log .d (TAG , "[notificationStyle] first bundle.notificationLoaded=" + firstBundle .getBoolean ("notificationLoaded" , false ));
538+ }
539+ }
540+
354541 if (Build .VERSION .SDK_INT < Build .VERSION_CODES .N ) {
355542 Notification .InboxStyle messageStyle = new Notification .InboxStyle ();
356543 if (bundles != null ) {
0 commit comments