Skip to content

Commit cfc6120

Browse files
committed
Refactor notification handling and improve React Native integration
- Added a static instance of MainApplication for easier access across classes. - Implemented a queue for pending notifications that arrive before React Native is initialized. - Updated notification processing to handle message-id-only notifications more effectively. - Refactored encryption methods to eliminate context dependency, enhancing performance and reliability. - Increased timeout for E2E notification processing to 15 seconds for better handling of cold starts.
1 parent 8be42dd commit cfc6120

File tree

6 files changed

+127
-194
lines changed

6 files changed

+127
-194
lines changed

android/app/src/main/java/chat/rocket/reactnative/MainApplication.kt

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -37,6 +37,12 @@ import chat.rocket.reactnative.notification.VideoConfTurboPackage
3737
* - Message-id-only notification loading
3838
*/
3939
open class MainApplication : Application(), ReactApplication {
40+
companion object {
41+
// Change from private to public (default) to allow Java access
42+
@JvmStatic
43+
lateinit var instance: MainApplication
44+
private set
45+
}
4046

4147
override val reactNativeHost: ReactNativeHost =
4248
object : DefaultReactNativeHost(this) {
@@ -61,6 +67,8 @@ open class MainApplication : Application(), ReactApplication {
6167

6268
override fun onCreate() {
6369
super.onCreate()
70+
instance = this // Set the static reference here
71+
6472
SoLoader.init(this, OpenSourceMergedSoMapping)
6573
Bugsnag.start(this)
6674

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

Lines changed: 58 additions & 139 deletions
Original file line numberDiff line numberDiff line change
@@ -27,6 +27,7 @@
2727
import java.util.List;
2828
import java.util.Map;
2929
import java.util.concurrent.ConcurrentHashMap;
30+
import java.util.concurrent.CopyOnWriteArrayList;
3031

3132
import chat.rocket.reactnative.BuildConfig;
3233
import chat.rocket.reactnative.MainActivity;
@@ -46,6 +47,20 @@ public class CustomPushNotification {
4647
public static volatile ReactApplicationContext reactApplicationContext;
4748
private static final Gson gson = new Gson();
4849
private static final Map<String, List<Bundle>> notificationMessages = new ConcurrentHashMap<>();
50+
private static final List<PendingNotification> pendingNotifications = new CopyOnWriteArrayList<>();
51+
52+
/**
53+
* Holds a notification that arrived before React Native was initialized.
54+
*/
55+
private static class PendingNotification {
56+
final Context context;
57+
final Bundle bundle;
58+
59+
PendingNotification(Context context, Bundle bundle) {
60+
this.context = context;
61+
this.bundle = bundle;
62+
}
63+
}
4964

5065
// Constants
5166
public static final String KEY_REPLY = "KEY_REPLY";
@@ -70,21 +85,32 @@ public CustomPushNotification(Context context, Bundle bundle) {
7085
/**
7186
* Sets the React application context when React Native initializes.
7287
* Called from MainApplication when React context is ready.
88+
* Processes any notifications that were queued before React was ready.
7389
*/
7490
public static void setReactContext(ReactApplicationContext context) {
7591
reactApplicationContext = context;
92+
93+
// Process any notifications that arrived before React was initialized
94+
if (!pendingNotifications.isEmpty()) {
95+
int count = pendingNotifications.size();
96+
Log.i(TAG, "React initialized, processing " + count + " queued notification(s)");
97+
98+
for (PendingNotification pending : pendingNotifications) {
99+
try {
100+
CustomPushNotification notification = new CustomPushNotification(pending.context, pending.bundle);
101+
notification.handleNotification();
102+
} catch (Exception e) {
103+
Log.e(TAG, "Failed to process queued notification", e);
104+
}
105+
}
106+
107+
pendingNotifications.clear();
108+
}
76109
}
77110

78111
public static void clearMessages(int notId) {
79112
notificationMessages.remove(Integer.toString(notId));
80113
}
81-
82-
/**
83-
* Check if React Native is initialized
84-
*/
85-
private boolean isReactInitialized() {
86-
return reactApplicationContext != null;
87-
}
88114

89115
public void onReceived() {
90116
String notId = mBundle.getString("notId");
@@ -101,71 +127,32 @@ public void onReceived() {
101127
return;
102128
}
103129

104-
// Check if React is ready - needed for MMKV access (avatars, encryption, message-id-only)
105-
if (!isReactInitialized()) {
106-
Log.w(TAG, "React not initialized yet, waiting before processing notification...");
107-
108-
// Wait for React to initialize with timeout
109-
new Thread(() -> {
110-
int attempts = 0;
111-
int maxAttempts = 50; // 5 seconds total (50 * 100ms)
112-
113-
while (!isReactInitialized() && attempts < maxAttempts) {
114-
try {
115-
Thread.sleep(100); // Wait 100ms
116-
attempts++;
117-
118-
if (attempts % 10 == 0 && ENABLE_VERBOSE_LOGS) {
119-
Log.d(TAG, "Still waiting for React initialization... (" + (attempts * 100) + "ms elapsed)");
120-
}
121-
} catch (InterruptedException e) {
122-
Log.e(TAG, "Wait interrupted", e);
123-
Thread.currentThread().interrupt();
124-
return;
125-
}
126-
}
127-
128-
if (isReactInitialized()) {
129-
Log.i(TAG, "React initialized after " + (attempts * 100) + "ms, proceeding with notification");
130-
try {
131-
handleNotification();
132-
} catch (Exception e) {
133-
Log.e(TAG, "Failed to process notification after React initialization", e);
134-
}
135-
} else {
136-
Log.e(TAG, "Timeout waiting for React initialization after " + (maxAttempts * 100) + "ms, processing without MMKV");
137-
try {
138-
handleNotification();
139-
} catch (Exception e) {
140-
Log.e(TAG, "Failed to process notification without React context", e);
141-
}
142-
}
143-
}).start();
144-
145-
return; // Exit early, notification will be processed in the thread
146-
}
147-
148-
if (ENABLE_VERBOSE_LOGS) {
149-
Log.d(TAG, "React already initialized, proceeding with notification");
150-
}
151-
130+
// Process notification
131+
// E2E notifications can be processed immediately (no React needed after SQLiteDatabase refactor)
132+
// Other notifications (message-id-only, avatars) still need React for MMKV access
152133
try {
153134
handleNotification();
154135
} catch (Exception e) {
155-
Log.e(TAG, "Failed to process notification on main thread", e);
136+
Log.e(TAG, "Failed to process notification", e);
156137
}
157138
}
158139

159140
private void handleNotification() {
160141
Ejson receivedEjson = safeFromJson(mBundle.getString("ejson", "{}"), Ejson.class);
161142

143+
// Message-id-only notifications need React for MMKV access (tokens for API calls)
162144
if (receivedEjson != null && receivedEjson.notificationType != null && receivedEjson.notificationType.equals("message-id-only")) {
163-
Log.d(TAG, "Detected message-id-only notification, will fetch full content from server");
145+
if (reactApplicationContext == null) {
146+
Log.w(TAG, "React not initialized, queueing message-id-only notification for later...");
147+
pendingNotifications.add(new PendingNotification(mContext, mBundle));
148+
return;
149+
}
164150
loadNotificationAndProcess(receivedEjson);
165151
return; // Exit early, notification will be processed in callback
166152
}
167153

168-
// For non-message-id-only notifications, process immediately
154+
// For other notifications (including E2E), process immediately
155+
// E2E notifications can decrypt without React (using SQLiteDatabase directly)
169156
processNotification();
170157
}
171158

@@ -174,19 +161,11 @@ private void loadNotificationAndProcess(Ejson ejson) {
174161
@Override
175162
public void call(@Nullable Bundle bundle) {
176163
if (bundle != null) {
177-
Log.d(TAG, "Successfully loaded notification content from server, updating notification props");
178-
179-
if (ENABLE_VERBOSE_LOGS) {
180-
Log.d(TAG, "[BEFORE update] bundle.notificationLoaded=" + bundle.getBoolean("notificationLoaded", false));
181-
Log.d(TAG, "[BEFORE update] bundle.title=" + (bundle.getString("title") != null ? "[present]" : "[null]"));
182-
Log.d(TAG, "[BEFORE update] bundle.message length=" + (bundle.getString("message") != null ? bundle.getString("message").length() : 0));
183-
}
184-
185164
synchronized(CustomPushNotification.this) {
186165
mBundle = bundle;
187166
}
188167
} else {
189-
Log.w(TAG, "Failed to load notification content from server, will display placeholder notification");
168+
Log.e(TAG, "Failed to load notification content from server");
190169
}
191170

192171
processNotification();
@@ -198,15 +177,6 @@ private void processNotification() {
198177
Ejson loadedEjson = safeFromJson(mBundle.getString("ejson", "{}"), Ejson.class);
199178
String notId = mBundle.getString("notId", "1");
200179

201-
if (ENABLE_VERBOSE_LOGS) {
202-
Log.d(TAG, "[processNotification] notId=" + notId);
203-
Log.d(TAG, "[processNotification] bundle.notificationLoaded=" + mBundle.getBoolean("notificationLoaded", false));
204-
Log.d(TAG, "[processNotification] bundle.title=" + (mBundle.getString("title") != null ? "[present]" : "[null]"));
205-
Log.d(TAG, "[processNotification] bundle.message length=" + (mBundle.getString("message") != null ? mBundle.getString("message").length() : 0));
206-
Log.d(TAG, "[processNotification] loadedEjson.notificationType=" + (loadedEjson != null ? loadedEjson.notificationType : "null"));
207-
Log.d(TAG, "[processNotification] loadedEjson.sender=" + (loadedEjson != null && loadedEjson.sender != null ? loadedEjson.sender.username : "null"));
208-
}
209-
210180
// Handle E2E encrypted notifications
211181
if (isE2ENotification(loadedEjson)) {
212182
handleE2ENotification(mBundle, loadedEjson, notId);
@@ -225,54 +195,21 @@ private boolean isE2ENotification(Ejson ejson) {
225195
}
226196

227197
/**
228-
* Handles E2E encrypted notifications by delegating to the async processor.
198+
* Handles E2E encrypted notifications.
199+
* Decrypts immediately using standard SQLiteDatabase (no React Native dependency needed).
229200
*/
230201
private void handleE2ENotification(Bundle bundle, Ejson ejson, String notId) {
231-
// Check if React context is immediately available
232-
if (reactApplicationContext != null) {
233-
// Fast path: decrypt immediately
234-
String decrypted = Encryption.shared.decryptMessage(ejson, reactApplicationContext);
235-
236-
if (decrypted != null) {
237-
bundle.putString("message", decrypted);
238-
mBundle = bundle;
239-
ejson = safeFromJson(bundle.getString("ejson", "{}"), Ejson.class);
240-
showNotification(bundle, ejson, notId);
241-
} else {
242-
Log.w(TAG, "E2E decryption failed for notification");
243-
}
244-
return;
245-
}
246-
247-
// Slow path: wait for React context asynchronously
248-
Log.i(TAG, "Waiting for React context to decrypt E2E notification");
249-
250-
E2ENotificationProcessor processor = new E2ENotificationProcessor(
251-
// Context provider
252-
() -> reactApplicationContext,
253-
254-
// Callback
255-
new E2ENotificationProcessor.NotificationCallback() {
256-
@Override
257-
public void onDecryptionComplete(Bundle decryptedBundle, Ejson decryptedEjson, String notificationId) {
258-
mBundle = decryptedBundle;
259-
Ejson finalEjson = safeFromJson(decryptedBundle.getString("ejson", "{}"), Ejson.class);
260-
showNotification(decryptedBundle, finalEjson, notificationId);
261-
}
262-
263-
@Override
264-
public void onDecryptionFailed(Bundle originalBundle, Ejson originalEjson, String notificationId) {
265-
Log.w(TAG, "E2E decryption failed for notification");
266-
}
267-
268-
@Override
269-
public void onTimeout(Bundle originalBundle, Ejson originalEjson, String notificationId) {
270-
Log.w(TAG, "Timeout waiting for React context for E2E notification");
271-
}
272-
}
273-
);
202+
// Decrypt immediately using static MainApplication.instance (no Context parameter needed)
203+
String decrypted = Encryption.shared.decryptMessage(ejson);
274204

275-
processor.processAsync(bundle, ejson, notId);
205+
if (decrypted != null) {
206+
bundle.putString("message", decrypted);
207+
mBundle = bundle;
208+
ejson = safeFromJson(bundle.getString("ejson", "{}"), Ejson.class);
209+
showNotification(bundle, ejson, notId);
210+
} else {
211+
Log.e(TAG, "E2E decryption failed, notification will not be shown");
212+
}
276213
}
277214

278215
/**
@@ -302,13 +239,7 @@ private void showNotification(Bundle bundle, Ejson ejson, String notId) {
302239
return;
303240
} else {
304241
// Show regular notification
305-
if (ENABLE_VERBOSE_LOGS) {
306-
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));
307-
}
308242
notificationMessages.get(notId).add(bundle);
309-
if (ENABLE_VERBOSE_LOGS) {
310-
Log.d(TAG, "[After add] notificationMessages[" + notId + "].size=" + notificationMessages.get(notId).size());
311-
}
312243
postNotification(Integer.parseInt(notId));
313244
}
314245
}
@@ -364,7 +295,6 @@ private void createNotificationChannel() {
364295
}
365296

366297
private Notification.Builder buildNotification(int notificationId) {
367-
String notId = Integer.toString(notificationId);
368298
String title = mBundle.getString("title");
369299
String message = mBundle.getString("message");
370300
Boolean notificationLoaded = mBundle.getBoolean("notificationLoaded", false);
@@ -386,14 +316,6 @@ private Notification.Builder buildNotification(int notificationId) {
386316
}
387317
}
388318

389-
if (ENABLE_VERBOSE_LOGS) {
390-
Log.d(TAG, "[buildNotification] notId=" + notId);
391-
Log.d(TAG, "[buildNotification] notificationLoaded=" + notificationLoaded);
392-
Log.d(TAG, "[buildNotification] title=" + (title != null ? "[present]" : "[null]"));
393-
Log.d(TAG, "[buildNotification] notificationTitle=" + (notificationTitle != null ? "[present]" : "[null]"));
394-
Log.d(TAG, "[buildNotification] message length=" + (message != null ? message.length() : 0));
395-
}
396-
397319
// Create pending intent to open the app
398320
Intent intent = new Intent(mContext, MainActivity.class);
399321
intent.setFlags(Intent.FLAG_ACTIVITY_NEW_TASK | Intent.FLAG_ACTIVITY_CLEAR_TOP);
@@ -516,9 +438,6 @@ private void notificationColor(Notification.Builder notification) {
516438
private void notificationStyle(Notification.Builder notification, int notId, Bundle bundle) {
517439
List<Bundle> bundles = notificationMessages.get(Integer.toString(notId));
518440

519-
if (ENABLE_VERBOSE_LOGS) {
520-
Log.d(TAG, "[notificationStyle] notId=" + notId + ", bundles=" + (bundles != null ? bundles.size() : "null"));
521-
}
522441

523442
if (Build.VERSION.SDK_INT < Build.VERSION_CODES.N) {
524443
Notification.InboxStyle messageStyle = new Notification.InboxStyle();

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

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -24,7 +24,7 @@ public class E2ENotificationProcessor {
2424

2525
// Configuration constants
2626
private static final int POLLING_INTERVAL_MS = 100; // Check every 100ms
27-
private static final int MAX_WAIT_TIME_MS = 3000; // Wait up to 3 seconds
27+
private static final int MAX_WAIT_TIME_MS = 15000; // Wait up to 15 seconds (for cold start)
2828
private static final int MAX_ATTEMPTS = MAX_WAIT_TIME_MS / POLLING_INTERVAL_MS;
2929

3030
private final Handler mainHandler;
@@ -109,7 +109,7 @@ private void decryptAndNotify(final ReactApplicationContext reactContext,
109109
// Decrypt in background thread to avoid blocking
110110
new Thread(() -> {
111111
try {
112-
String decrypted = Encryption.shared.decryptMessage(ejson, reactContext);
112+
String decrypted = Encryption.shared.decryptMessage(ejson);
113113

114114
if (decrypted != null) {
115115
bundle.putString("message", decrypted);

0 commit comments

Comments
 (0)