-
Notifications
You must be signed in to change notification settings - Fork 1.4k
feat: Voice support #6918
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
base: develop
Are you sure you want to change the base?
feat: Voice support #6918
Changes from all commits
4702ee7
e532171
1299b0e
d2cef3d
b0b78cd
0d18314
4b33a79
ece7e27
c3dd2ae
1df1b29
8f9129e
8a5de04
a9ec70d
d6229d9
e718561
26502cb
db29a47
2b16f4b
eae9137
bb2a8bb
10593d6
b6766f3
ac85af8
9b28770
5c5e2be
01e42e2
aa3ca88
548e855
abbb072
77cb36e
59f25eb
cd74d43
9b71cf9
7348135
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,10 @@ | ||
| export default { | ||
| setup: jest.fn(), | ||
| canMakeMultipleCalls: jest.fn(), | ||
| displayIncomingCall: jest.fn(), | ||
| endCall: jest.fn(), | ||
| setCurrentCallActive: jest.fn(), | ||
| addEventListener: jest.fn((event, callback) => ({ | ||
| remove: jest.fn() | ||
| })) | ||
| }; |
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,72 @@ | ||
| package chat.rocket.reactnative.notification | ||
|
|
||
| import com.facebook.react.bridge.ReactApplicationContext | ||
| import java.security.MessageDigest | ||
|
|
||
| /** | ||
| * CallIdUUID - Converts a callId string to a deterministic UUID v5. | ||
| * This is used by CallKeep which requires UUIDs, while the server sends random callId strings. | ||
| * | ||
| * The algorithm matches the iOS implementation in CallIdUUID.swift to ensure | ||
| * consistency across platforms. | ||
| */ | ||
| object CallIdUUID { | ||
|
|
||
| // Fixed namespace UUID for VoIP calls (RFC 4122 URL namespace) | ||
| // Using the standard URL namespace UUID: 6ba7b811-9dad-11d1-80b4-00c04fd430c8 | ||
| private val NAMESPACE_UUID = byteArrayOf( | ||
| 0x6b.toByte(), 0xa7.toByte(), 0xb8.toByte(), 0x11.toByte(), | ||
| 0x9d.toByte(), 0xad.toByte(), | ||
| 0x11.toByte(), 0xd1.toByte(), | ||
| 0x80.toByte(), 0xb4.toByte(), | ||
| 0x00.toByte(), 0xc0.toByte(), 0x4f.toByte(), 0xd4.toByte(), 0x30.toByte(), 0xc8.toByte() | ||
| ) | ||
|
|
||
| /** | ||
| * Generates a UUID v5 from a callId string. | ||
| * Uses SHA-1 hash of namespace + callId, then formats as UUID v5. | ||
| * | ||
| * @param callId The call ID string to convert | ||
| * @return A deterministic UUID string in the format xxxxxxxx-xxxx-xxxx-xxxx-xxxxxxxxxxxx | ||
| */ | ||
| @JvmStatic | ||
| fun generateUUIDv5(callId: String): String { | ||
| // Concatenate namespace UUID bytes with callId UTF-8 bytes | ||
| val callIdBytes = callId.toByteArray(Charsets.UTF_8) | ||
| val data = ByteArray(NAMESPACE_UUID.size + callIdBytes.size) | ||
| System.arraycopy(NAMESPACE_UUID, 0, data, 0, NAMESPACE_UUID.size) | ||
| System.arraycopy(callIdBytes, 0, data, NAMESPACE_UUID.size, callIdBytes.size) | ||
|
|
||
| // SHA-1 hash | ||
| val md = MessageDigest.getInstance("SHA-1") | ||
| val hash = md.digest(data) | ||
|
|
||
| // Set version (4 bits) to 5 (0101) | ||
| hash[6] = ((hash[6].toInt() and 0x0F) or 0x50).toByte() | ||
|
|
||
| // Set variant (2 bits) to 10 | ||
| hash[8] = ((hash[8].toInt() and 0x3F) or 0x80).toByte() | ||
|
|
||
| // Format as UUID string (only use first 16 bytes) | ||
| return String.format( | ||
| "%02x%02x%02x%02x-%02x%02x-%02x%02x-%02x%02x-%02x%02x%02x%02x%02x%02x", | ||
| hash[0].toInt() and 0xFF, hash[1].toInt() and 0xFF, hash[2].toInt() and 0xFF, hash[3].toInt() and 0xFF, | ||
| hash[4].toInt() and 0xFF, hash[5].toInt() and 0xFF, | ||
| hash[6].toInt() and 0xFF, hash[7].toInt() and 0xFF, | ||
| hash[8].toInt() and 0xFF, hash[9].toInt() and 0xFF, | ||
| hash[10].toInt() and 0xFF, hash[11].toInt() and 0xFF, hash[12].toInt() and 0xFF, | ||
| hash[13].toInt() and 0xFF, hash[14].toInt() and 0xFF, hash[15].toInt() and 0xFF | ||
| ) | ||
| } | ||
| } | ||
|
|
||
| /** | ||
| * React Native TurboModule implementation for CallIdUUID. | ||
| * Exposes the CallIdUUID functionality to JavaScript. | ||
| */ | ||
| class CallIdUUIDModule(reactContext: ReactApplicationContext) : NativeCallIdUUIDSpec(reactContext) { | ||
|
|
||
| override fun getName() = NativeCallIdUUIDSpec.NAME | ||
|
|
||
| override fun toUUID(callId: String): String = CallIdUUID.generateUUIDv5(callId) | ||
| } |
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,30 @@ | ||
| package chat.rocket.reactnative.notification | ||
|
|
||
| import com.facebook.react.BaseReactPackage | ||
| import com.facebook.react.bridge.NativeModule | ||
| import com.facebook.react.bridge.ReactApplicationContext | ||
| import com.facebook.react.module.model.ReactModuleInfo | ||
| import com.facebook.react.module.model.ReactModuleInfoProvider | ||
|
|
||
| class CallIdUUIDTurboPackage : BaseReactPackage() { | ||
|
|
||
| override fun getModule(name: String, reactContext: ReactApplicationContext): NativeModule? = | ||
| if (name == NativeCallIdUUIDSpec.NAME) { | ||
| CallIdUUIDModule(reactContext) | ||
| } else { | ||
| null | ||
| } | ||
|
|
||
| override fun getReactModuleInfoProvider() = ReactModuleInfoProvider { | ||
| mapOf( | ||
| NativeCallIdUUIDSpec.NAME to ReactModuleInfo( | ||
| name = NativeCallIdUUIDSpec.NAME, | ||
| className = NativeCallIdUUIDSpec.NAME, | ||
| canOverrideExistingModule = false, | ||
| needsEagerInit = false, | ||
| isCxxModule = false, | ||
| isTurboModule = true | ||
| ) | ||
| ) | ||
| } | ||
| } |
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,19 @@ | ||
| package chat.rocket.reactnative.notification | ||
|
|
||
| import com.facebook.react.bridge.ReactApplicationContext | ||
| import com.facebook.react.bridge.ReactContextBaseJavaModule | ||
| import com.facebook.react.bridge.ReactMethod | ||
| import com.facebook.react.turbomodule.core.interfaces.TurboModule | ||
|
|
||
| abstract class NativeCallIdUUIDSpec(reactContext: ReactApplicationContext) : | ||
| ReactContextBaseJavaModule(reactContext), TurboModule { | ||
|
|
||
| companion object { | ||
| const val NAME = "CallIdUUID" | ||
| } | ||
|
|
||
| override fun getName(): String = NAME | ||
|
|
||
| @ReactMethod(isBlockingSynchronousMethod = true) | ||
| abstract fun toUUID(callId: String): String | ||
| } |
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,23 @@ | ||
| package chat.rocket.reactnative.notification | ||
|
|
||
| import com.facebook.react.bridge.Promise | ||
| import com.facebook.react.bridge.ReactApplicationContext | ||
| import com.facebook.react.bridge.ReactContextBaseJavaModule | ||
| import com.facebook.react.bridge.ReactMethod | ||
| import com.facebook.react.turbomodule.core.interfaces.TurboModule | ||
|
|
||
| abstract class NativeVoipSpec(reactContext: ReactApplicationContext) : | ||
| ReactContextBaseJavaModule(reactContext), TurboModule { | ||
|
|
||
| companion object { | ||
| const val NAME = "VoipModule" | ||
| } | ||
|
|
||
| override fun getName(): String = NAME | ||
|
|
||
| @ReactMethod | ||
| abstract fun getPendingVoipCall(promise: Promise) | ||
|
|
||
| @ReactMethod | ||
| abstract fun clearPendingVoipCall() | ||
| } |
| Original file line number | Diff line number | Diff line change | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|
|
|
@@ -17,11 +17,16 @@ class NotificationIntentHandler { | |||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| /** | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| * Handles a notification Intent from MainActivity. | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| * Processes both video conf and regular notification intents. | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| * Processes VoIP, video conf, and regular notification intents. | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| */ | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| @JvmStatic | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| fun handleIntent(context: Context, intent: Intent) { | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| // Handle video conf action first | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| // Handle VoIP action first | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| if (handleVoipIntent(context, intent)) { | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| return | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| } | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| // Handle video conf action | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| if (handleVideoConfIntent(context, intent)) { | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| return | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| } | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
|
|
@@ -30,6 +35,39 @@ class NotificationIntentHandler { | |||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| handleNotificationIntent(context, intent) | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| } | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| /** | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| * Handles VoIP call notification Intent. | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| * @return true if this was a VoIP intent, false otherwise | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| */ | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| @JvmStatic | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| private fun handleVoipIntent(context: Context, intent: Intent): Boolean { | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| if (!intent.getBooleanExtra("voipAction", false)) { | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| return false | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| } | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| val notificationId = intent.getIntExtra("notificationId", 0) | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| val event = intent.getStringExtra("event") ?: return true | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| val callId = intent.getStringExtra("callId") ?: "" | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| val callUUID = intent.getStringExtra("callUUID") ?: "" | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| val callerName = intent.getStringExtra("callerName") ?: "" | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| val host = intent.getStringExtra("host") ?: "" | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| Log.d(TAG, "Handling VoIP intent - event: $event, callId: $callId, callUUID: $callUUID") | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| VoipNotification.cancelById(context, notificationId) | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| VoipModule.storePendingVoipCall(context, callId, callUUID, callerName, host, event) | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| // Emit event to JS if app is running | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| if (event == "accept") { | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| VoipModule.emitCallAnswered(callUUID) | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| } | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| // Clear the voip flag to prevent re-processing | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| intent.removeExtra("voipAction") | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| return true | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
|
Comment on lines
+43
to
+68
Contributor
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Handle malformed VoIP intents without leaving stale state. 🛠️ Suggested fix- val notificationId = intent.getIntExtra("notificationId", 0)
- val event = intent.getStringExtra("event") ?: return true
+ val notificationId = intent.getIntExtra("notificationId", 0)
+ val event = intent.getStringExtra("event")
+ if (event.isNullOrEmpty()) {
+ if (notificationId != 0) {
+ VoipNotification.cancelById(context, notificationId)
+ }
+ intent.removeExtra("voipAction")
+ return true
+ }
@@
- VoipNotification.cancelById(context, notificationId)
+ if (notificationId != 0) {
+ VoipNotification.cancelById(context, notificationId)
+ }📝 Committable suggestion
Suggested change
🤖 Prompt for AI Agents |
||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| } | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| /** | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| * Handles video conference notification Intent. | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| * @return true if this was a video conf intent, false otherwise | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| Original file line number | Diff line number | Diff line change |
|---|---|---|
|
|
@@ -4,17 +4,20 @@ import android.os.Bundle | |
| import android.util.Log | ||
| import com.google.firebase.messaging.FirebaseMessagingService | ||
| import com.google.firebase.messaging.RemoteMessage | ||
| import com.google.gson.Gson | ||
|
|
||
| /** | ||
| * Custom Firebase Messaging Service for Rocket.Chat. | ||
| * | ||
| * Handles incoming FCM messages and routes them to CustomPushNotification | ||
| * for advanced processing (E2E decryption, MessagingStyle, direct reply, etc.) | ||
| * Handles incoming FCM messages and routes them to the appropriate handler: | ||
| * - VoipNotification for VoIP calls (notificationType: "voip") | ||
| * - CustomPushNotification for regular messages and video conferences | ||
| */ | ||
| class RCFirebaseMessagingService : FirebaseMessagingService() { | ||
|
|
||
| companion object { | ||
| private const val TAG = "RocketChat.FCM" | ||
| private val gson = Gson() | ||
| } | ||
|
|
||
| override fun onMessageReceived(remoteMessage: RemoteMessage) { | ||
|
|
@@ -33,7 +36,20 @@ class RCFirebaseMessagingService : FirebaseMessagingService() { | |
| } | ||
| } | ||
|
|
||
| // Process the notification | ||
| val voipPayload = parseVoipPayload(data) | ||
| if (voipPayload != null && voipPayload.isVoipIncomingCall()) { | ||
| Log.d(TAG, "Detected new VoIP payload format, routing to VoipNotification handler") | ||
| try { | ||
| val voipNotification = VoipNotification(this) | ||
| // TODO: no need for bundle, just use voipPayload | ||
| voipNotification.showIncomingCall(bundle, voipPayload) | ||
| } catch (e: Exception) { | ||
| Log.e(TAG, "Error processing VoIP notification", e) | ||
| } | ||
| return | ||
| } | ||
|
|
||
| // Process regular notifications via CustomPushNotification | ||
| try { | ||
| val notification = CustomPushNotification(this, bundle) | ||
| notification.onReceived() | ||
|
|
@@ -42,6 +58,48 @@ class RCFirebaseMessagingService : FirebaseMessagingService() { | |
| } | ||
| } | ||
|
|
||
| /** | ||
| * Parses the new VoIP payload format from FCM data map. | ||
| * Returns null if the payload doesn't match the new format. | ||
| */ | ||
| private fun parseVoipPayload(data: Map<String, String>): VoipPayload? { | ||
| val type = data["type"] | ||
| val hasEjson = data.containsKey("ejson") && !data["ejson"].isNullOrEmpty() | ||
|
|
||
| if (type != "incoming_call" || hasEjson) { | ||
| return null | ||
| } | ||
|
|
||
| return try { | ||
| VoipPayload( | ||
| callId = data["callId"], | ||
| calleeId = data["calleeId"], | ||
| caller = data["caller"], | ||
| host = data["host"], | ||
| type = data["type"] | ||
| ) | ||
| } catch (e: Exception) { | ||
| Log.e(TAG, "Failed to parse VoIP payload", e) | ||
| null | ||
| } | ||
| } | ||
|
Comment on lines
+65
to
+85
Contributor
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Validate required VoIP fields before constructing payload. 🛠️ Proposed guardrails- val type = data["type"]
+ val type = data["type"] ?: return null
val hasEjson = data.containsKey("ejson") && !data["ejson"].isNullOrEmpty()
if (type != "incoming_call" || hasEjson) {
return null
}
+ val callId = data["callId"]?.takeIf { it.isNotBlank() } ?: return null
+ val calleeId = data["calleeId"]?.takeIf { it.isNotBlank() } ?: return null
+ val caller = data["caller"]?.takeIf { it.isNotBlank() } ?: return null
+ val host = data["host"]?.takeIf { it.isNotBlank() } ?: return null
+
return try {
VoipPayload(
- callId = data["callId"],
- calleeId = data["calleeId"],
- caller = data["caller"],
- host = data["host"],
- type = data["type"]
+ callId = callId,
+ calleeId = calleeId,
+ caller = caller,
+ host = host,
+ type = type
)
} catch (e: Exception) {
Log.e(TAG, "Failed to parse VoIP payload", e)
null🤖 Prompt for AI Agents |
||
|
|
||
| /** | ||
| * Safely parses ejson string to Ejson object. | ||
| */ | ||
| private fun parseEjson(ejsonStr: String?): Ejson? { | ||
| if (ejsonStr.isNullOrEmpty() || ejsonStr == "{}") { | ||
| return null | ||
| } | ||
|
|
||
| return try { | ||
| gson.fromJson(ejsonStr, Ejson::class.java) | ||
| } catch (e: Exception) { | ||
| Log.e(TAG, "Failed to parse ejson", e) | ||
| null | ||
| } | ||
| } | ||
|
|
||
| override fun onNewToken(token: String) { | ||
| Log.d(TAG, "FCM token refreshed") | ||
| // Token handling is done by expo-notifications JS layer | ||
|
|
||
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Add
android:required="false"to avoid filtering devices.Without
required="false", Play Store will filter out devices that lack these hardware features. Most VoIP apps should work on devices without dedicated audio output or microphone hardware (e.g., tablets using Bluetooth).Proposed fix
📝 Committable suggestion
🤖 Prompt for AI Agents