Skip to content

Commit 788cb8d

Browse files
authored
fix(android): Improve push notification loading reliability and add diagnostic logging (#6711)
1 parent bcd351f commit 788cb8d

File tree

5 files changed

+484
-46
lines changed

5 files changed

+484
-46
lines changed

android/app/src/main/java/chat/rocket/reactnative/notification/CustomPushNotification.java

Lines changed: 206 additions & 19 deletions
Original file line numberDiff line numberDiff line change
@@ -32,12 +32,13 @@
3232

3333
import java.util.ArrayList;
3434
import java.util.Date;
35-
import java.util.HashMap;
3635
import java.util.Iterator;
3736
import java.util.List;
3837
import java.util.Map;
38+
import java.util.concurrent.ConcurrentHashMap;
3939
import java.util.concurrent.ExecutionException;
4040

41+
import chat.rocket.reactnative.BuildConfig;
4142
import chat.rocket.reactnative.R;
4243

4344
/**
@@ -47,12 +48,13 @@
4748
* For E2E notifications, waits for React Native initialization before decrypting and displaying.
4849
*/
4950
public 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

Comments
 (0)