Skip to content

Commit 106cbd7

Browse files
authored
feat: Control incoming call from native (#7066)
1 parent 306f8cc commit 106cbd7

Some content is hidden

Large Commits have some content hidden by default. Use the searchbox below for content that may be hidden.

52 files changed

+661
-186
lines changed

.cursor/skills/agent-skills

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1 @@
1+
Subproject commit a4f602ffb4aeaf4199fa97b7162f9c9d1f655904

android/app/src/main/AndroidManifest.xml

Lines changed: 0 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -133,11 +133,6 @@
133133
android:enabled="true"
134134
android:exported="false" />
135135

136-
<service
137-
android:name="chat.rocket.reactnative.voip.VoipForegroundService"
138-
android:exported="false"
139-
android:foregroundServiceType="phoneCall" />
140-
141136
<service android:name="io.wazo.callkeep.VoiceConnectionService"
142137
android:label="Wazo"
143138
android:permission="android.permission.BIND_TELECOM_CONNECTION_SERVICE"

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

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -29,20 +29,20 @@ class MainActivity : ReactActivity() {
2929
override fun onCreate(savedInstanceState: Bundle?) {
3030
RNBootSplash.init(this, R.style.BootTheme)
3131
super.onCreate(null)
32-
32+
3333
// Handle notification intents
3434
intent?.let { NotificationIntentHandler.handleIntent(this, it) }
3535
}
3636

3737
public override fun onNewIntent(intent: Intent) {
3838
super.onNewIntent(intent)
3939
setIntent(intent)
40-
40+
4141
// Handle notification intents when activity is already running
4242
NotificationIntentHandler.handleIntent(this, intent)
4343
}
4444

4545
override fun invokeDefaultOnBackPressed() {
4646
moveTaskToBack(true)
4747
}
48-
}
48+
}

android/app/src/main/java/chat/rocket/reactnative/notification/NotificationIntentHandler.kt

Lines changed: 1 addition & 42 deletions
Original file line numberDiff line numberDiff line change
@@ -6,11 +6,6 @@ import android.os.Bundle
66
import android.util.Log
77
import com.google.gson.GsonBuilder
88
import chat.rocket.reactnative.voip.VoipNotification
9-
import chat.rocket.reactnative.voip.VoipModule
10-
import chat.rocket.reactnative.voip.VoipPayload
11-
import android.os.Build
12-
import android.app.KeyguardManager
13-
import android.app.Activity
149

1510
/**
1611
* Handles notification Intent processing from MainActivity.
@@ -27,8 +22,7 @@ class NotificationIntentHandler {
2722
*/
2823
@JvmStatic
2924
fun handleIntent(context: Context, intent: Intent) {
30-
// Handle VoIP action first
31-
if (handleVoipIntent(context, intent)) {
25+
if (VoipNotification.handleMainActivityVoipIntent(context, intent)) {
3226
return
3327
}
3428

@@ -41,41 +35,6 @@ class NotificationIntentHandler {
4135
handleNotificationIntent(context, intent)
4236
}
4337

44-
/**
45-
* Handles VoIP call notification Intent.
46-
* @return true if this was a VoIP intent, false otherwise
47-
*/
48-
@JvmStatic
49-
private fun handleVoipIntent(context: Context, intent: Intent): Boolean {
50-
if (!intent.getBooleanExtra("voipAction", false)) {
51-
return false
52-
}
53-
val voipPayload = VoipPayload.fromBundle(intent.extras)
54-
if (voipPayload == null || !voipPayload.isVoipIncomingCall()) {
55-
return false
56-
}
57-
58-
Log.d(TAG, "Handling VoIP intent - voipPayload: $voipPayload")
59-
60-
VoipNotification.cancelById(context, voipPayload.notificationId)
61-
VoipNotification.cancelTimeout(voipPayload.callId)
62-
VoipModule.storeInitialEvents(voipPayload)
63-
64-
if (context is Activity) {
65-
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O_MR1) {
66-
context.setShowWhenLocked(true)
67-
context.setTurnScreenOn(true)
68-
val keyguardManager = context.getSystemService(Context.KEYGUARD_SERVICE) as KeyguardManager
69-
keyguardManager.requestDismissKeyguard(context, null)
70-
}
71-
}
72-
73-
// Clear the voip flag to prevent re-processing
74-
intent.removeExtra("voipAction")
75-
76-
return true
77-
}
78-
7938
/**
8039
* Handles video conference notification Intent.
8140
* @return true if this was a video conf intent, false otherwise

android/app/src/main/java/chat/rocket/reactnative/notification/RCFirebaseMessagingService.kt

Lines changed: 0 additions & 18 deletions
Original file line numberDiff line numberDiff line change
@@ -4,7 +4,6 @@ import android.os.Bundle
44
import android.util.Log
55
import com.google.firebase.messaging.FirebaseMessagingService
66
import com.google.firebase.messaging.RemoteMessage
7-
import com.google.gson.Gson
87
import chat.rocket.reactnative.voip.VoipNotification
98
import chat.rocket.reactnative.voip.VoipPayload
109

@@ -19,7 +18,6 @@ class RCFirebaseMessagingService : FirebaseMessagingService() {
1918

2019
companion object {
2120
private const val TAG = "RocketChat.FCM"
22-
private val gson = Gson()
2321
}
2422

2523
override fun onMessageReceived(remoteMessage: RemoteMessage) {
@@ -53,22 +51,6 @@ class RCFirebaseMessagingService : FirebaseMessagingService() {
5351
}
5452
}
5553

56-
/**
57-
* Safely parses ejson string to Ejson object.
58-
*/
59-
private fun parseEjson(ejsonStr: String?): Ejson? {
60-
if (ejsonStr.isNullOrEmpty() || ejsonStr == "{}") {
61-
return null
62-
}
63-
64-
return try {
65-
gson.fromJson(ejsonStr, Ejson::class.java)
66-
} catch (e: Exception) {
67-
Log.e(TAG, "Failed to parse ejson", e)
68-
null
69-
}
70-
}
71-
7254
override fun onNewToken(token: String) {
7355
Log.d(TAG, "FCM token refreshed")
7456
// Token handling is done by expo-notifications JS layer

android/app/src/main/java/chat/rocket/reactnative/voip/IncomingCallActivity.kt

Lines changed: 2 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -24,7 +24,6 @@ import android.widget.FrameLayout
2424
import android.util.Log
2525
import android.view.ViewOutlineProvider
2626
import com.bumptech.glide.Glide
27-
import chat.rocket.reactnative.MainActivity
2827
import chat.rocket.reactnative.R
2928
import android.graphics.Typeface
3029
import chat.rocket.reactnative.notification.Ejson
@@ -283,15 +282,8 @@ class IncomingCallActivity : Activity() {
283282
clearTimeout()
284283
VoipNotification.cancelTimeout(payload.callId)
285284
stopRingtone()
286-
287-
// Launch MainActivity with call data
288-
val launchIntent = Intent(this, MainActivity::class.java).apply {
289-
flags = Intent.FLAG_ACTIVITY_NEW_TASK or Intent.FLAG_ACTIVITY_CLEAR_TOP
290-
putExtras(payload.toBundle())
291-
}
292-
startActivity(launchIntent)
293-
294-
finish()
285+
VoipNotification.handleAcceptAction(this, payload)
286+
// Activity finishes when ACTION_DISMISS is broadcast from handleAcceptAction (async DDP).
295287
}
296288

297289
private fun handleDecline(payload: VoipPayload) {

android/app/src/main/java/chat/rocket/reactnative/voip/VoipModule.kt

Lines changed: 25 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -17,6 +17,7 @@ class VoipModule(reactContext: ReactApplicationContext) : NativeVoipSpec(reactCo
1717
companion object {
1818
private const val TAG = "RocketChat.VoipModule"
1919
private const val EVENT_INITIAL_EVENTS = "VoipPushInitialEvents"
20+
private const val EVENT_VOIP_ACCEPT_FAILED = "VoipAcceptFailed"
2021

2122
private var reactContextRef: WeakReference<ReactApplicationContext>? = null
2223
private var initialEventsData: VoipPayload? = null
@@ -57,6 +58,30 @@ class VoipModule(reactContext: ReactApplicationContext) : NativeVoipSpec(reactCo
5758
emitInitialEventsEvent(voipPayload)
5859
}
5960

61+
/**
62+
* Stash native accept failure for cold start [getInitialEvents] and emit [EVENT_VOIP_ACCEPT_FAILED] when JS is running.
63+
*/
64+
@JvmStatic
65+
fun storeAcceptFailureForJs(payload: VoipPayload) {
66+
val failed = payload.copy(voipAcceptFailed = true)
67+
initialEventsData = failed
68+
emitVoipAcceptFailedEvent(failed)
69+
}
70+
71+
private fun emitVoipAcceptFailedEvent(voipPayload: VoipPayload) {
72+
try {
73+
reactContextRef?.get()?.let { context ->
74+
if (context.hasActiveReactInstance()) {
75+
context
76+
.getJSModule(DeviceEventManagerModule.RCTDeviceEventEmitter::class.java)
77+
.emit(EVENT_VOIP_ACCEPT_FAILED, voipPayload.toWritableMap())
78+
}
79+
}
80+
} catch (e: Exception) {
81+
Log.e(TAG, "Failed to emit VoipAcceptFailed", e)
82+
}
83+
}
84+
6085
@JvmStatic
6186
fun clearInitialEventsInternal() {
6287
try {

0 commit comments

Comments
 (0)