Skip to content

Commit 9e54a4b

Browse files
authored
feat(voip): migrate Android accept/reject from DDP to REST (#7127)
1 parent 2eae6b6 commit 9e54a4b

3 files changed

Lines changed: 192 additions & 237 deletions

File tree

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

Lines changed: 5 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -21,10 +21,13 @@ class DDPClient {
2121

2222
companion object {
2323
private const val TAG = "RocketChat.DDPClient"
24+
private val sharedClient = OkHttpClient.Builder()
25+
.pingInterval(30, TimeUnit.SECONDS)
26+
.build()
2427
}
2528

2629
private var webSocket: WebSocket? = null
27-
private var client: OkHttpClient? = null
30+
private val client: OkHttpClient = sharedClient
2831
private var sendCounter = 0
2932
private var isConnected = false
3033
private val mainHandler = Handler(Looper.getMainLooper())
@@ -40,14 +43,9 @@ class DDPClient {
4043

4144
Log.d(TAG, "Connecting to $wsUrl")
4245

43-
val httpClient = OkHttpClient.Builder()
44-
.pingInterval(30, TimeUnit.SECONDS)
45-
.build()
46-
client = httpClient
47-
4846
val request = Request.Builder().url(wsUrl).build()
4947

50-
webSocket = httpClient.newWebSocket(request, object : WebSocketListener() {
48+
webSocket = client.newWebSocket(request, object : WebSocketListener() {
5149
override fun onOpen(webSocket: WebSocket, response: Response) {
5250
Log.d(TAG, "WebSocket opened")
5351
val connectMsg = JSONObject().apply {
@@ -142,8 +140,6 @@ class DDPClient {
142140
onCollectionMessage = null
143141
webSocket?.close(1000, null)
144142
webSocket = null
145-
client?.dispatcher?.executorService?.shutdown()
146-
client = null
147143
}
148144

149145
private fun nextMessage(msg: String): JSONObject {
Lines changed: 134 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,134 @@
1+
package chat.rocket.reactnative.voip
2+
3+
import android.os.Handler
4+
import android.os.Looper
5+
import android.util.Log
6+
import chat.rocket.reactnative.notification.Ejson
7+
import okhttp3.Call
8+
import okhttp3.Callback
9+
import okhttp3.MediaType.Companion.toMediaType
10+
import okhttp3.OkHttpClient
11+
import okhttp3.Request
12+
import okhttp3.RequestBody.Companion.toRequestBody
13+
import org.json.JSONArray
14+
import org.json.JSONObject
15+
import java.io.IOException
16+
17+
/**
18+
* REST client for `POST /api/v1/media-calls.answer` used by accept/reject flows.
19+
*
20+
* Mirrors the iOS [MediaCallsAnswerRequest.swift][ios/Shared/RocketChat/API/MediaCallsAnswerRequest.swift]
21+
* and replaces the DDP `sendAcceptSignal` / `sendRejectSignal` / `queueAcceptSignal` / `queueRejectSignal`
22+
* methods in [VoipNotification].
23+
*
24+
* Auth headers (`x-user-id` / `x-auth-token`) are resolved from [Ejson] at call time,
25+
* matching the pattern used by [chat.rocket.reactnative.notification.ReplyBroadcast].
26+
*
27+
* [fetch]'s `onResult` is always invoked on the main thread.
28+
*
29+
* @param callId The call identifier from the VoIP payload.
30+
* @param contractId The device-unique contract identifier (`Settings.Secure.ANDROID_ID`).
31+
* @param answer Either `"accept"` or `"reject"`.
32+
* @param supportedFeatures Optional list of supported features (e.g. `["audio"]`); sent only for accept.
33+
*/
34+
class MediaCallsAnswerRequest(
35+
private val callId: String,
36+
private val contractId: String,
37+
private val answer: String,
38+
private val supportedFeatures: List<String>? = null
39+
) {
40+
companion object {
41+
private const val TAG = "RocketChat.MediaCallsAnswerRequest"
42+
private val JSON_MEDIA_TYPE = "application/json; charset=utf-8".toMediaType()
43+
private val httpClient = OkHttpClient()
44+
private val mainHandler = Handler(Looper.getMainLooper())
45+
46+
@JvmStatic
47+
fun fetch(
48+
context: android.content.Context,
49+
host: String,
50+
callId: String,
51+
contractId: String,
52+
answer: String,
53+
supportedFeatures: List<String>? = null,
54+
onResult: (Boolean) -> Unit
55+
) {
56+
val request = MediaCallsAnswerRequest(callId, contractId, answer, supportedFeatures)
57+
request.execute(context, host, onResult)
58+
}
59+
}
60+
61+
/**
62+
* Builds the JSON body for the request:
63+
* ```json
64+
* {
65+
* "callId": "<callId>",
66+
* "contractId": "<contractId>",
67+
* "answer": "<accept|reject>",
68+
* "supportedFeatures": ["audio"] // only when non-null for accept
69+
* }
70+
* ```
71+
*/
72+
private fun buildBody(): JSONObject {
73+
val json = JSONObject().apply {
74+
put("callId", callId)
75+
put("contractId", contractId)
76+
put("answer", answer)
77+
}
78+
supportedFeatures?.let { features ->
79+
val arr = JSONArray()
80+
features.forEach { arr.put(it) }
81+
json.put("supportedFeatures", arr)
82+
}
83+
return json
84+
}
85+
86+
private fun execute(
87+
context: android.content.Context,
88+
host: String,
89+
onResult: (Boolean) -> Unit
90+
) {
91+
val ejson = Ejson().apply { this.host = host }
92+
val userId = ejson.userId()
93+
val token = ejson.token()
94+
95+
if (userId.isNullOrEmpty() || token.isNullOrEmpty()) {
96+
Log.w(TAG, "Missing credentials for $host — cannot send media-call answer")
97+
mainHandler.post { onResult(false) }
98+
return
99+
}
100+
101+
val serverUrl = host.removeSuffix("/")
102+
val url = "$serverUrl/api/v1/media-calls.answer"
103+
104+
val body = buildBody().toString()
105+
val requestBody = body.toRequestBody(JSON_MEDIA_TYPE)
106+
107+
val request = Request.Builder()
108+
.header("x-user-id", userId)
109+
.header("x-auth-token", token)
110+
.url(url)
111+
.post(requestBody)
112+
.build()
113+
114+
httpClient.newCall(request).enqueue(object : Callback {
115+
override fun onFailure(call: Call, e: IOException) {
116+
Log.e(TAG, "MediaCallsAnswerRequest failed for callId=$callId: ${e.message}")
117+
mainHandler.post { onResult(false) }
118+
}
119+
120+
override fun onResponse(call: Call, response: okhttp3.Response) {
121+
response.use {
122+
val code = it.code
123+
val success = code in 200..299
124+
if (success) {
125+
Log.d(TAG, "MediaCallsAnswerRequest response for callId=$callId: code=$code success=true")
126+
} else {
127+
Log.w(TAG, "MediaCallsAnswerRequest failed callId=$callId code=$code answer=$answer")
128+
}
129+
mainHandler.post { onResult(success) }
130+
}
131+
}
132+
})
133+
}
134+
}

0 commit comments

Comments
 (0)