Skip to content
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
56 changes: 7 additions & 49 deletions android/app/src/main/java/chat/rocket/reactnative/MainActivity.kt
Original file line number Diff line number Diff line change
Expand Up @@ -9,9 +9,7 @@ import android.os.Bundle
import com.zoontek.rnbootsplash.RNBootSplash
import android.content.Intent
import android.content.res.Configuration
import chat.rocket.reactnative.notification.VideoConfModule
import chat.rocket.reactnative.notification.VideoConfNotification
import com.google.gson.GsonBuilder
import chat.rocket.reactnative.notification.NotificationIntentHandler

class MainActivity : ReactActivity() {

Expand All @@ -32,56 +30,16 @@ class MainActivity : ReactActivity() {
RNBootSplash.init(this, R.style.BootTheme)
super.onCreate(null)

// Handle video conf action from notification
intent?.let { handleVideoConfIntent(it) }
// Handle notification intents
intent?.let { NotificationIntentHandler.handleIntent(this, it) }
}

public override fun onNewIntent(intent: Intent) {
super.onNewIntent(intent)
// Handle video conf action when activity is already running
handleVideoConfIntent(intent)
}

private fun handleVideoConfIntent(intent: Intent) {
if (intent.getBooleanExtra("videoConfAction", false)) {
val notificationId = intent.getIntExtra("notificationId", 0)
val event = intent.getStringExtra("event") ?: return
val rid = intent.getStringExtra("rid") ?: ""
val callerId = intent.getStringExtra("callerId") ?: ""
val callerName = intent.getStringExtra("callerName") ?: ""
val host = intent.getStringExtra("host") ?: ""
val callId = intent.getStringExtra("callId") ?: ""

android.util.Log.d("RocketChat.MainActivity", "Handling video conf intent - event: $event, rid: $rid, host: $host, callId: $callId")

// Cancel the notification
if (notificationId != 0) {
VideoConfNotification.cancelById(this, notificationId)
}

// Store action for JS to pick up - include all required fields
val data = mapOf(
"notificationType" to "videoconf",
"rid" to rid,
"event" to event,
"host" to host,
"callId" to callId,
"caller" to mapOf(
"_id" to callerId,
"name" to callerName
)
)

val gson = GsonBuilder().create()
val jsonData = gson.toJson(data)

android.util.Log.d("RocketChat.MainActivity", "Storing video conf action: $jsonData")

VideoConfModule.storePendingAction(this, jsonData)

// Clear the video conf flag to prevent re-processing
intent.removeExtra("videoConfAction")
}
setIntent(intent)

// Handle notification intents when activity is already running
NotificationIntentHandler.handleIntent(this, intent)
}

override fun invokeDefaultOnBackPressed() {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -5,11 +5,8 @@ import android.content.res.Configuration
import com.facebook.react.PackageList
import com.facebook.react.ReactApplication
import com.facebook.react.ReactHost
import com.facebook.react.ReactInstanceEventListener
import com.facebook.react.ReactNativeHost
import com.facebook.react.ReactPackage
import com.facebook.react.bridge.ReactContext
import com.facebook.react.bridge.ReactApplicationContext
import com.facebook.react.defaults.DefaultNewArchitectureEntryPoint.load
import com.facebook.react.defaults.DefaultReactHost.getDefaultReactHost
import com.facebook.react.defaults.DefaultReactNativeHost
Expand All @@ -21,8 +18,8 @@ import expo.modules.ApplicationLifecycleDispatcher
import chat.rocket.reactnative.networking.SSLPinningTurboPackage;
import chat.rocket.reactnative.storage.MMKVKeyManager;
import chat.rocket.reactnative.storage.SecureStoragePackage;
import chat.rocket.reactnative.notification.CustomPushNotification;
import chat.rocket.reactnative.notification.VideoConfTurboPackage
import chat.rocket.reactnative.notification.PushNotificationTurboPackage

/**
* Main Application class.
Expand All @@ -45,6 +42,7 @@ open class MainApplication : Application(), ReactApplication {
add(SSLPinningTurboPackage())
add(WatermelonDBJSIPackage())
add(VideoConfTurboPackage())
add(PushNotificationTurboPackage())
add(SecureStoragePackage())
}

Expand All @@ -71,13 +69,6 @@ open class MainApplication : Application(), ReactApplication {
// Load the native entry point for the New Architecture
load()

// Register listener to set React context when initialized
reactHost.addReactInstanceEventListener(object : ReactInstanceEventListener {
override fun onReactContextInitialized(context: ReactContext) {
CustomPushNotification.setReactContext(context as ReactApplicationContext)
}
})

ApplicationLifecycleDispatcher.onApplicationCreate(this)
}

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -21,7 +21,6 @@
import com.bumptech.glide.Glide;
import com.bumptech.glide.load.resource.bitmap.RoundedCorners;
import com.bumptech.glide.request.RequestOptions;
import com.facebook.react.bridge.ReactApplicationContext;
import com.google.gson.Gson;

import java.util.ArrayList;
Expand Down Expand Up @@ -49,7 +48,6 @@ public class CustomPushNotification {
private static final boolean ENABLE_VERBOSE_LOGS = BuildConfig.DEBUG;

// Shared state
public static volatile ReactApplicationContext reactApplicationContext;
private static final Gson gson = new Gson();
private static final Map<String, List<Bundle>> notificationMessages = new ConcurrentHashMap<>();

Expand All @@ -61,7 +59,7 @@ public class CustomPushNotification {

// Instance fields
private final Context mContext;
private Bundle mBundle;
private volatile Bundle mBundle;
Comment thread
coderabbitai[bot] marked this conversation as resolved.
private final NotificationManager notificationManager;

public CustomPushNotification(Context context, Bundle bundle) {
Expand All @@ -73,24 +71,9 @@ public CustomPushNotification(Context context, Bundle bundle) {
createNotificationChannel();
}

/**
* Sets the React application context when React Native initializes.
* Called from MainApplication when React context is ready.
*/
public static void setReactContext(ReactApplicationContext context) {
reactApplicationContext = context;
}

public static void clearMessages(int notId) {
notificationMessages.remove(Integer.toString(notId));
}

/**
* Check if React Native is initialized
*/
private boolean isReactInitialized() {
return reactApplicationContext != null;
}

public void onReceived() {
String notId = mBundle.getString("notId");
Expand All @@ -107,58 +90,10 @@ public void onReceived() {
return;
}

// Check if React is ready - needed for MMKV access (avatars, encryption, message-id-only)
if (!isReactInitialized()) {
Log.w(TAG, "React not initialized yet, waiting before processing notification...");

// Wait for React to initialize with timeout
new Thread(() -> {
int attempts = 0;
int maxAttempts = 50; // 5 seconds total (50 * 100ms)

while (!isReactInitialized() && attempts < maxAttempts) {
try {
Thread.sleep(100); // Wait 100ms
attempts++;

if (attempts % 10 == 0 && ENABLE_VERBOSE_LOGS) {
Log.d(TAG, "Still waiting for React initialization... (" + (attempts * 100) + "ms elapsed)");
}
} catch (InterruptedException e) {
Log.e(TAG, "Wait interrupted", e);
Thread.currentThread().interrupt();
return;
}
}

if (isReactInitialized()) {
Log.i(TAG, "React initialized after " + (attempts * 100) + "ms, proceeding with notification");
try {
handleNotification();
} catch (Exception e) {
Log.e(TAG, "Failed to process notification after React initialization", e);
}
} else {
Log.e(TAG, "Timeout waiting for React initialization after " + (maxAttempts * 100) + "ms, processing without MMKV");
try {
handleNotification();
} catch (Exception e) {
Log.e(TAG, "Failed to process notification without React context", e);
}
}
}).start();

return; // Exit early, notification will be processed in the thread
}

if (ENABLE_VERBOSE_LOGS) {
Log.d(TAG, "React already initialized, proceeding with notification");
}

try {
handleNotification();
} catch (Exception e) {
Log.e(TAG, "Failed to process notification on main thread", e);
Log.e(TAG, "Failed to process notification", e);
}
}

Expand Down Expand Up @@ -201,26 +136,27 @@ public void call(@Nullable Bundle bundle) {
}

private void processNotification() {
Ejson loadedEjson = safeFromJson(mBundle.getString("ejson", "{}"), Ejson.class);
String notId = mBundle.getString("notId", "1");
final Bundle bundle = mBundle;
Ejson loadedEjson = safeFromJson(bundle.getString("ejson", "{}"), Ejson.class);
String notId = bundle.getString("notId", "1");

if (ENABLE_VERBOSE_LOGS) {
Log.d(TAG, "[processNotification] notId=" + notId);
Log.d(TAG, "[processNotification] bundle.notificationLoaded=" + mBundle.getBoolean("notificationLoaded", false));
Log.d(TAG, "[processNotification] bundle.title=" + (mBundle.getString("title") != null ? "[present]" : "[null]"));
Log.d(TAG, "[processNotification] bundle.message length=" + (mBundle.getString("message") != null ? mBundle.getString("message").length() : 0));
Log.d(TAG, "[processNotification] bundle.notificationLoaded=" + bundle.getBoolean("notificationLoaded", false));
Log.d(TAG, "[processNotification] bundle.title=" + (bundle.getString("title") != null ? "[present]" : "[null]"));
Log.d(TAG, "[processNotification] bundle.message length=" + (bundle.getString("message") != null ? bundle.getString("message").length() : 0));
Log.d(TAG, "[processNotification] loadedEjson.notificationType=" + (loadedEjson != null ? loadedEjson.notificationType : "null"));
Log.d(TAG, "[processNotification] loadedEjson.sender=" + (loadedEjson != null && loadedEjson.sender != null ? loadedEjson.sender.username : "null"));
}

// Handle E2E encrypted notifications
if (isE2ENotification(loadedEjson)) {
handleE2ENotification(mBundle, loadedEjson, notId);
return; // E2E processor will handle showing the notification
handleE2ENotification(bundle, loadedEjson, notId);
return;
}

// Handle regular (non-E2E) notifications
showNotification(mBundle, loadedEjson, notId);
// Handle regular notifications
showNotification(bundle, loadedEjson, notId);
}

/**
Expand All @@ -231,54 +167,36 @@ private boolean isE2ENotification(Ejson ejson) {
}

/**
* Handles E2E encrypted notifications by delegating to the async processor.
* Handles E2E encrypted notifications
*/
private void handleE2ENotification(Bundle bundle, Ejson ejson, String notId) {
// Check if React context is immediately available
if (reactApplicationContext != null) {
// Fast path: decrypt immediately
String decrypted = Encryption.shared.decryptMessage(ejson, reactApplicationContext);

if (decrypted != null) {
bundle.putString("message", decrypted);
if (Encryption.shared == null) {
Log.e(TAG, "Encryption singleton is null, cannot decrypt E2E notification");
bundle.putString("message", "Encrypted message");
synchronized(this) {
mBundle = bundle;
ejson = safeFromJson(bundle.getString("ejson", "{}"), Ejson.class);
showNotification(bundle, ejson, notId);
} else {
Log.w(TAG, "E2E decryption failed for notification");
}
showNotification(bundle, ejson, notId);
return;
}

// Slow path: wait for React context asynchronously
Log.i(TAG, "Waiting for React context to decrypt E2E notification");
String decrypted = Encryption.shared.decryptMessage(ejson, mContext);

E2ENotificationProcessor processor = new E2ENotificationProcessor(
// Context provider
() -> reactApplicationContext,

// Callback
new E2ENotificationProcessor.NotificationCallback() {
@Override
public void onDecryptionComplete(Bundle decryptedBundle, Ejson decryptedEjson, String notificationId) {
mBundle = decryptedBundle;
Ejson finalEjson = safeFromJson(decryptedBundle.getString("ejson", "{}"), Ejson.class);
showNotification(decryptedBundle, finalEjson, notificationId);
}

@Override
public void onDecryptionFailed(Bundle originalBundle, Ejson originalEjson, String notificationId) {
Log.w(TAG, "E2E decryption failed for notification");
}

@Override
public void onTimeout(Bundle originalBundle, Ejson originalEjson, String notificationId) {
Log.w(TAG, "Timeout waiting for React context for E2E notification");
}
if (decrypted != null) {
bundle.putString("message", decrypted);
synchronized(this) {
mBundle = bundle;
}
);

processor.processAsync(bundle, ejson, notId);
showNotification(bundle, ejson, notId);
} else {
Log.w(TAG, "E2E decryption failed for notification, showing fallback notification");
// Show fallback notification so user knows a message arrived
bundle.putString("message", "Encrypted message");
synchronized(this) {
mBundle = bundle;
}
showNotification(bundle, ejson, notId);
}
}
Comment thread
coderabbitai[bot] marked this conversation as resolved.

/**
Expand All @@ -305,6 +223,10 @@ private void showNotification(Bundle bundle, Ejson ejson, String notId) {
}
bundle.putString("avatarUri", avatarUri);

synchronized(this) {
mBundle = bundle;
}

// Handle special notification types
if (ejson != null && "videoconf".equals(ejson.notificationType)) {
handleVideoConfNotification(bundle, ejson);
Expand Down Expand Up @@ -373,11 +295,12 @@ private void createNotificationChannel() {
}

private Notification.Builder buildNotification(int notificationId) {
final Bundle bundle = mBundle;
String notId = Integer.toString(notificationId);
String title = mBundle.getString("title");
String message = mBundle.getString("message");
Boolean notificationLoaded = mBundle.getBoolean("notificationLoaded", false);
Ejson ejson = safeFromJson(mBundle.getString("ejson", "{}"), Ejson.class);
String title = bundle.getString("title");
String message = bundle.getString("message");
Boolean notificationLoaded = bundle.getBoolean("notificationLoaded", false);
Ejson ejson = safeFromJson(bundle.getString("ejson", "{}"), Ejson.class);

if (ENABLE_VERBOSE_LOGS) {
Log.d(TAG, "[buildNotification] notId=" + notId);
Expand All @@ -389,7 +312,7 @@ private Notification.Builder buildNotification(int notificationId) {
// Create pending intent to open the app
Intent intent = new Intent(mContext, MainActivity.class);
intent.setFlags(Intent.FLAG_ACTIVITY_NEW_TASK | Intent.FLAG_ACTIVITY_CLEAR_TOP);
intent.putExtras(mBundle);
intent.putExtras(bundle);

PendingIntent pendingIntent;
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.S) {
Expand All @@ -414,14 +337,14 @@ private Notification.Builder buildNotification(int notificationId) {
.setAutoCancel(true);

notificationColor(notification);
notificationIcons(notification, mBundle);
notificationIcons(notification, bundle);
notificationDismiss(notification, notificationId);

// if notificationType is null (RC < 3.5) or notificationType is different of message-id-only or notification was loaded successfully
if (ejson == null || ejson.notificationType == null || !ejson.notificationType.equals("message-id-only") || notificationLoaded) {
Log.i(TAG, "[buildNotification] ✅ Rendering FULL notification style");
notificationStyle(notification, notificationId, mBundle);
notificationReply(notification, notificationId, mBundle);
notificationStyle(notification, notificationId, bundle);
notificationReply(notification, notificationId, bundle);
} else {
Log.w(TAG, "[buildNotification] ⚠️ Rendering FALLBACK notification");
// Cancel previous fallback notifications from same server
Expand Down
Loading
Loading