@poolse/react-native-calls 0.1.3 → 0.2.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/android/build.gradle +7 -0
- package/android/consumer-rules.pro +11 -0
- package/android/src/main/AndroidManifest.xml +2 -0
- package/android/src/main/java/dev/poolse/callcore/CallCoreRuntime.kt +245 -110
- package/android/src/main/java/dev/poolse/callcore/CallMachine.kt +113 -18
- package/android/src/main/java/dev/poolse/callcore/Ports.kt +17 -2
- package/android/src/main/java/dev/poolse/callcore/android/AndroidRingSurface.kt +51 -4
- package/android/src/main/java/dev/poolse/callcore/android/CallActionReceiver.kt +4 -3
- package/android/src/main/java/dev/poolse/callcore/android/CallCore.kt +253 -26
- package/android/src/main/java/dev/poolse/callcore/android/CallForegroundService.kt +228 -47
- package/android/src/main/java/dev/poolse/callcore/android/CallHttpClient.kt +97 -46
- package/android/src/main/java/dev/poolse/callcore/android/CallMessagingService.kt +59 -13
- package/android/src/main/java/dev/poolse/callcore/android/IncomingCallActivity.kt +100 -12
- package/android/src/main/java/expo/modules/poolsecalls/PoolseCallsModule.kt +75 -17
- package/android/src/main/livekit/java/dev/poolse/callcore/livekit/LiveKitMediaPort.kt +190 -16
- package/android/src/test/java/dev/poolse/callcore/android/CallHttpClientTest.kt +61 -0
- package/dist/index.cjs +10 -0
- package/dist/index.cjs.map +1 -1
- package/dist/index.d.cts +29 -3
- package/dist/index.d.ts +29 -3
- package/dist/index.js +6 -1
- package/dist/index.js.map +1 -1
- package/package.json +2 -1
- package/src/index.ts +39 -2
|
@@ -50,7 +50,12 @@ sealed interface Event {
|
|
|
50
50
|
) : Event
|
|
51
51
|
|
|
52
52
|
data class RingEnded(val callId: String, val reason: String) : Event
|
|
53
|
-
|
|
53
|
+
/**
|
|
54
|
+
* The server finished the call. [reason] is the push's own reason
|
|
55
|
+
* (`finished`, `declined_all`, `no_answer`, `cancelled`); it only
|
|
56
|
+
* colours the caller's ended screen when the call never got answered.
|
|
57
|
+
*/
|
|
58
|
+
data class CallFinished(val callId: String, val reason: String? = null) : Event
|
|
54
59
|
|
|
55
60
|
data object UserAnswer : Event
|
|
56
61
|
data object UserDecline : Event
|
|
@@ -62,10 +67,18 @@ sealed interface Event {
|
|
|
62
67
|
data class HttpOk(val created: Boolean = true) : Event
|
|
63
68
|
data object HttpGone : Event
|
|
64
69
|
data object HttpTimeout : Event
|
|
70
|
+
/**
|
|
71
|
+
* Terminal refusal that is NOT "the call is over": 401/403 (grant
|
|
72
|
+
* spent, wrong device), 503, or an unparseable 2xx. Ends as `failed`,
|
|
73
|
+
* never as `finished`, so the UI does not claim the other side hung up.
|
|
74
|
+
*/
|
|
75
|
+
data class HttpFailed(val code: String = "failed") : Event
|
|
65
76
|
|
|
66
77
|
data object MediaConnected : Event
|
|
67
78
|
data object MediaFailed : Event
|
|
68
79
|
data object MediaLost : Event
|
|
80
|
+
/** The SFU is recovering the session on its own (ICE restart, resume). Show it; do not rejoin. */
|
|
81
|
+
data object MediaReconnecting : Event
|
|
69
82
|
data object MediaRecovered : Event
|
|
70
83
|
data object RemoteJoined : Event
|
|
71
84
|
|
|
@@ -74,6 +87,8 @@ sealed interface Event {
|
|
|
74
87
|
|
|
75
88
|
data object RingDeadline : Event
|
|
76
89
|
data object ReconnectBudgetElapsed : Event
|
|
90
|
+
/** CONNECTING watchdog: neither the server nor the SFU got us in. */
|
|
91
|
+
data object ConnectTimeout : Event
|
|
77
92
|
data object LingerElapsed : Event
|
|
78
93
|
}
|
|
79
94
|
|
|
@@ -115,17 +130,37 @@ object CallMachine {
|
|
|
115
130
|
|
|
116
131
|
is Event.CallFinished ->
|
|
117
132
|
if (e.callId == s.callId) when (s.phase) {
|
|
118
|
-
|
|
133
|
+
// The finish push can overtake (or replace) ring_ended: a ring
|
|
134
|
+
// for a finished call is over either way.
|
|
135
|
+
Phase.RINGING_IN -> Transition(MachineState.IDLE, listOf(Effect.RingtoneStop, Effect.DismissRing))
|
|
136
|
+
// Caller still ringing out: nobody ever joined, so the push's
|
|
137
|
+
// reason (no_answer / declined_all / cancelled) is the truth the
|
|
138
|
+
// ended screen should show. The server already finished the call
|
|
139
|
+
// — no hangup to send.
|
|
140
|
+
Phase.RINGING_OUT -> Transition(
|
|
141
|
+
s.copy(phase = Phase.ENDED, reason = ringOutcome(e.reason)),
|
|
142
|
+
listOf(Effect.RingbackStop, Effect.MediaLeave, Effect.CallSessionStop),
|
|
143
|
+
)
|
|
144
|
+
Phase.ACTIVE, Phase.RECONNECTING -> Transition(
|
|
119
145
|
s.copy(phase = Phase.ENDED, reason = EndReason.FINISHED),
|
|
120
|
-
|
|
121
|
-
if (s.phase == Phase.RINGING_OUT) add(Effect.RingbackStop)
|
|
122
|
-
add(Effect.MediaLeave); add(Effect.CallSessionStop)
|
|
123
|
-
},
|
|
146
|
+
listOf(Effect.MediaLeave, Effect.CallSessionStop),
|
|
124
147
|
)
|
|
148
|
+
// CONNECTING may already have joined media (HTTP_OK came back):
|
|
149
|
+
// leave it, or the room outlives the call.
|
|
125
150
|
Phase.CONNECTING -> Transition(
|
|
126
151
|
s.copy(phase = Phase.ENDED, reason = EndReason.FINISHED),
|
|
127
|
-
listOf(Effect.CallSessionStop),
|
|
152
|
+
listOf(Effect.MediaLeave, Effect.CallSessionStop),
|
|
128
153
|
)
|
|
154
|
+
// Lingering "ended": a later finish with a real outcome (the FCM
|
|
155
|
+
// push after a reason-less socket event, or vice versa) corrects
|
|
156
|
+
// an unanswered outgoing call's generic "finished" to what
|
|
157
|
+
// actually happened. Display only — nothing left to tear down.
|
|
158
|
+
Phase.ENDED -> {
|
|
159
|
+
val outcome = ringOutcome(e.reason)
|
|
160
|
+
if (s.direction == Direction.OUTGOING && s.reason == EndReason.FINISHED &&
|
|
161
|
+
outcome != EndReason.FINISHED
|
|
162
|
+
) Transition(s.copy(reason = outcome), emptyList()) else noop(s)
|
|
163
|
+
}
|
|
129
164
|
else -> noop(s)
|
|
130
165
|
} else noop(s)
|
|
131
166
|
|
|
@@ -199,8 +234,9 @@ object CallMachine {
|
|
|
199
234
|
else -> noop(s)
|
|
200
235
|
}
|
|
201
236
|
|
|
202
|
-
Event.HttpTimeout ->
|
|
203
|
-
|
|
237
|
+
Event.HttpTimeout -> when {
|
|
238
|
+
s.phase != Phase.CONNECTING -> noop(s)
|
|
239
|
+
s.httpAttempts < 2 -> Transition(
|
|
204
240
|
s.copy(httpAttempts = s.httpAttempts + 1),
|
|
205
241
|
listOf(
|
|
206
242
|
when (s.direction) {
|
|
@@ -209,24 +245,73 @@ object CallMachine {
|
|
|
209
245
|
else -> Effect.HttpAnswer
|
|
210
246
|
},
|
|
211
247
|
),
|
|
212
|
-
)
|
|
248
|
+
)
|
|
249
|
+
// Out of retries. A timeout only ever answers the CONNECTING
|
|
250
|
+
// request, so there is no bundle and media can never join:
|
|
251
|
+
// waiting on it was the "stuck in connecting forever" bug.
|
|
252
|
+
else -> Transition(
|
|
253
|
+
s.copy(phase = Phase.ENDED, reason = EndReason.FAILED),
|
|
254
|
+
listOf(Effect.HttpHangup, Effect.CallSessionStop),
|
|
255
|
+
)
|
|
256
|
+
}
|
|
257
|
+
|
|
258
|
+
is Event.HttpFailed -> when (s.phase) {
|
|
259
|
+
// HTTP_HANGUP still runs: with no bundle it resolves the ring
|
|
260
|
+
// grant, so an incoming caller is not left ringing a dead answer.
|
|
261
|
+
Phase.CONNECTING -> Transition(
|
|
262
|
+
s.copy(phase = Phase.ENDED, reason = EndReason.FAILED),
|
|
263
|
+
listOf(Effect.HttpHangup, Effect.CallSessionStop),
|
|
264
|
+
)
|
|
265
|
+
// Reconnect refused for auth: the control token is dead, so a
|
|
266
|
+
// hangup with it would be refused too — just leave.
|
|
267
|
+
Phase.RECONNECTING -> Transition(
|
|
268
|
+
s.copy(phase = Phase.ENDED, reason = EndReason.FAILED),
|
|
269
|
+
listOf(Effect.MediaLeave, Effect.CallSessionStop),
|
|
270
|
+
)
|
|
271
|
+
else -> noop(s)
|
|
272
|
+
}
|
|
273
|
+
|
|
274
|
+
Event.ConnectTimeout ->
|
|
275
|
+
if (s.phase == Phase.CONNECTING) Transition(
|
|
276
|
+
s.copy(phase = Phase.ENDED, reason = EndReason.FAILED),
|
|
277
|
+
listOf(Effect.MediaLeave, Effect.HttpHangup, Effect.CallSessionStop),
|
|
278
|
+
) else noop(s)
|
|
213
279
|
|
|
214
280
|
Event.MediaConnected -> when (s.phase) {
|
|
215
281
|
Phase.CONNECTING -> Transition(s.copy(phase = Phase.ACTIVE), emptyList())
|
|
282
|
+
// A rejoin after /reconnect lands as a fresh connect.
|
|
283
|
+
Phase.RECONNECTING -> Transition(s.copy(phase = Phase.ACTIVE), emptyList())
|
|
216
284
|
else -> noop(s) // ringing_out keeps ringing until a remote joins
|
|
217
285
|
}
|
|
218
286
|
|
|
219
|
-
Event.MediaFailed ->
|
|
220
|
-
|
|
287
|
+
Event.MediaFailed -> when (s.phase) {
|
|
288
|
+
Phase.CONNECTING -> Transition(
|
|
221
289
|
s.copy(phase = Phase.ENDED, reason = EndReason.FAILED),
|
|
222
290
|
listOf(Effect.HttpHangup, Effect.CallSessionStop),
|
|
223
|
-
)
|
|
291
|
+
)
|
|
292
|
+
// The caller's own media never came up: the callees would answer
|
|
293
|
+
// into an empty room. Give up and tell the server.
|
|
294
|
+
Phase.RINGING_OUT -> Transition(
|
|
295
|
+
s.copy(phase = Phase.ENDED, reason = EndReason.FAILED),
|
|
296
|
+
listOf(Effect.RingbackStop, Effect.MediaLeave, Effect.HttpHangup, Effect.CallSessionStop),
|
|
297
|
+
)
|
|
298
|
+
else -> noop(s) // reconnecting: the budget timer is the backstop
|
|
299
|
+
}
|
|
224
300
|
|
|
225
|
-
|
|
226
|
-
|
|
227
|
-
|
|
228
|
-
|
|
229
|
-
|
|
301
|
+
// Lost = the SFU gave up (unexpected disconnect). Ask the server for
|
|
302
|
+
// a fresh token and rejoin — also from RECONNECTING, where the SFU's
|
|
303
|
+
// own recovery just failed.
|
|
304
|
+
Event.MediaLost -> when (s.phase) {
|
|
305
|
+
Phase.ACTIVE -> Transition(s.copy(phase = Phase.RECONNECTING), listOf(Effect.HttpReconnect))
|
|
306
|
+
Phase.RECONNECTING -> Transition(s, listOf(Effect.HttpReconnect))
|
|
307
|
+
else -> noop(s)
|
|
308
|
+
}
|
|
309
|
+
|
|
310
|
+
// The SFU is resuming the same session itself: only reflect it.
|
|
311
|
+
// Tearing the room down here fought LiveKit's own recovery.
|
|
312
|
+
Event.MediaReconnecting ->
|
|
313
|
+
if (s.phase == Phase.ACTIVE) Transition(s.copy(phase = Phase.RECONNECTING), emptyList())
|
|
314
|
+
else noop(s)
|
|
230
315
|
|
|
231
316
|
Event.MediaRecovered ->
|
|
232
317
|
if (s.phase == Phase.RECONNECTING) Transition(s.copy(phase = Phase.ACTIVE), emptyList())
|
|
@@ -245,6 +330,7 @@ object CallMachine {
|
|
|
245
330
|
reason = when (e.outcome) {
|
|
246
331
|
"busy_all" -> EndReason.BUSY_ALL
|
|
247
332
|
"no_answer" -> EndReason.NO_ANSWER
|
|
333
|
+
"cancelled" -> EndReason.CANCELLED
|
|
248
334
|
else -> EndReason.DECLINED_ALL
|
|
249
335
|
},
|
|
250
336
|
),
|
|
@@ -288,5 +374,14 @@ object CallMachine {
|
|
|
288
374
|
else -> Transition(s, listOf(Effect.HttpDeclineBusy(e.callId)))
|
|
289
375
|
}
|
|
290
376
|
|
|
377
|
+
/** A finish push's reason, read as the caller's unanswered outcome. */
|
|
378
|
+
private fun ringOutcome(reason: String?): EndReason = when (reason) {
|
|
379
|
+
"no_answer", "timeout" -> EndReason.NO_ANSWER
|
|
380
|
+
"declined_all", "declined" -> EndReason.DECLINED_ALL
|
|
381
|
+
"busy_all" -> EndReason.BUSY_ALL
|
|
382
|
+
"cancelled" -> EndReason.CANCELLED
|
|
383
|
+
else -> EndReason.FINISHED
|
|
384
|
+
}
|
|
385
|
+
|
|
291
386
|
private fun noop(s: MachineState) = Transition(s, emptyList())
|
|
292
387
|
}
|
|
@@ -19,8 +19,13 @@ interface HttpPort {
|
|
|
19
19
|
fun decline(callId: String, auth: String, busy: Boolean)
|
|
20
20
|
/** POST /calls/:id/hangup with the call-control token; fire-and-forget with retry. */
|
|
21
21
|
fun hangup(callId: String, controlToken: String)
|
|
22
|
-
/**
|
|
23
|
-
|
|
22
|
+
/**
|
|
23
|
+
* POST /calls/:id/reconnect with the call-control token. The server
|
|
24
|
+
* answers with the FULL bundle (same shape as answer/join); [current]
|
|
25
|
+
* is passed so an older server's `{livekit:{url,token}}` refresh can
|
|
26
|
+
* still be applied onto it.
|
|
27
|
+
*/
|
|
28
|
+
fun reconnect(current: CallSessionBundle, cb: (HttpResult) -> Unit)
|
|
24
29
|
}
|
|
25
30
|
|
|
26
31
|
sealed interface HttpResult {
|
|
@@ -32,6 +37,11 @@ sealed interface HttpResult {
|
|
|
32
37
|
data object Timeout : HttpResult
|
|
33
38
|
/** 401/403 — grant spent, expired, or wrong device. Terminal. */
|
|
34
39
|
data class AuthFailed(val code: String) : HttpResult
|
|
40
|
+
/**
|
|
41
|
+
* Terminal but not auth: 503 (no SFU capacity), another 4xx, or a 2xx
|
|
42
|
+
* whose body would not parse. Retrying would only repeat it.
|
|
43
|
+
*/
|
|
44
|
+
data class Failed(val code: String) : HttpResult
|
|
35
45
|
}
|
|
36
46
|
|
|
37
47
|
data class CallSessionBundle(
|
|
@@ -52,6 +62,11 @@ interface MediaPort {
|
|
|
52
62
|
fun setMuted(muted: Boolean)
|
|
53
63
|
fun setCamera(on: Boolean)
|
|
54
64
|
fun switchCamera()
|
|
65
|
+
/**
|
|
66
|
+
* Route call audio to the loudspeaker (`true`) or back to the private
|
|
67
|
+
* route — earpiece, or a connected headset/Bluetooth device (`false`).
|
|
68
|
+
*/
|
|
69
|
+
fun setSpeaker(on: Boolean)
|
|
55
70
|
}
|
|
56
71
|
|
|
57
72
|
/** The ring UX: CallStyle notification + full-screen activity + tones. */
|
|
@@ -10,6 +10,7 @@ import android.media.MediaPlayer
|
|
|
10
10
|
import android.media.Ringtone
|
|
11
11
|
import android.media.RingtoneManager
|
|
12
12
|
import android.os.Build
|
|
13
|
+
import android.util.Log
|
|
13
14
|
import androidx.core.app.NotificationCompat
|
|
14
15
|
import androidx.core.app.NotificationManagerCompat
|
|
15
16
|
import androidx.core.app.Person
|
|
@@ -26,7 +27,10 @@ import dev.poolse.callcore.RingSurfacePort
|
|
|
26
27
|
* on the loudspeaker and the ringtone in the earpiece. Both players use
|
|
27
28
|
* explicit [AudioAttributes]: ringtone on the ringtone stream (loud,
|
|
28
29
|
* speaker), ringback on voice-communication (quiet, follows the call
|
|
29
|
-
* route).
|
|
30
|
+
* route). The ringback waits for [setCommunicationReady]: before the
|
|
31
|
+
* SFU's audio handler has switched the device into communication mode,
|
|
32
|
+
* a voice-communication stream still routes to the loudspeaker — the
|
|
33
|
+
* "ringback blares from the speaker" bug.
|
|
30
34
|
*
|
|
31
35
|
* Tap actions resolve to broadcasts handled by [CallActionReceiver],
|
|
32
36
|
* which forwards into the single native entry point so that notification
|
|
@@ -35,11 +39,25 @@ import dev.poolse.callcore.RingSurfacePort
|
|
|
35
39
|
*/
|
|
36
40
|
internal class AndroidRingSurface(
|
|
37
41
|
private val context: Context,
|
|
38
|
-
|
|
42
|
+
/** Read per use, so a re-configure (icon, ringback) applies at once. */
|
|
43
|
+
private val configOf: () -> RingSurfaceConfig,
|
|
39
44
|
) : RingSurfacePort {
|
|
40
45
|
|
|
46
|
+
private val config get() = configOf()
|
|
47
|
+
|
|
41
48
|
private var ringtone: Ringtone? = null
|
|
42
49
|
private var ringback: MediaPlayer? = null
|
|
50
|
+
private var ringbackWanted = false
|
|
51
|
+
private var communicationReady = false
|
|
52
|
+
|
|
53
|
+
/**
|
|
54
|
+
* The call's audio path is (or is no longer) in communication mode.
|
|
55
|
+
* A ringback requested earlier starts now, through the call route.
|
|
56
|
+
*/
|
|
57
|
+
fun setCommunicationReady(ready: Boolean) {
|
|
58
|
+
communicationReady = ready
|
|
59
|
+
if (ready && ringbackWanted && ringback == null) playRingback()
|
|
60
|
+
}
|
|
43
61
|
|
|
44
62
|
override fun show(ring: RingContext) {
|
|
45
63
|
ensureChannel()
|
|
@@ -93,6 +111,9 @@ internal class AndroidRingSurface(
|
|
|
93
111
|
.setOngoing(true)
|
|
94
112
|
.setAutoCancel(false)
|
|
95
113
|
.setFullScreenIntent(fullScreen, true)
|
|
114
|
+
// Tapping the heads-up (not a button) opens the ring screen too;
|
|
115
|
+
// without this the tap did nothing.
|
|
116
|
+
.setContentIntent(fullScreen)
|
|
96
117
|
.setStyle(
|
|
97
118
|
NotificationCompat.CallStyle.forIncomingCall(
|
|
98
119
|
caller,
|
|
@@ -102,13 +123,26 @@ internal class AndroidRingSurface(
|
|
|
102
123
|
)
|
|
103
124
|
.build()
|
|
104
125
|
|
|
105
|
-
|
|
126
|
+
// The ring-mode foreground service adopts this exact notification.
|
|
127
|
+
shownRing = ring.callId to notification
|
|
128
|
+
try {
|
|
106
129
|
NotificationManagerCompat.from(context).notify(ring.callId.notificationId(), notification)
|
|
130
|
+
} catch (e: SecurityException) {
|
|
131
|
+
// POST_NOTIFICATIONS denied: the ringtone still plays, but there is
|
|
132
|
+
// no surface. Loud in the log — this is a support question.
|
|
133
|
+
Log.e(TAG, "ring notification refused (POST_NOTIFICATIONS?)", e)
|
|
107
134
|
}
|
|
108
135
|
}
|
|
109
136
|
|
|
110
137
|
override fun dismiss(callId: String) {
|
|
111
|
-
|
|
138
|
+
if (shownRing?.first == callId) shownRing = null
|
|
139
|
+
runCatching { NotificationManagerCompat.from(context).cancel(callId.notificationId()) }
|
|
140
|
+
// The full-screen activity is its own window: tell it to close.
|
|
141
|
+
runCatching {
|
|
142
|
+
context.sendBroadcast(
|
|
143
|
+
Intent(ACTION_RING_CLOSED).setPackage(context.packageName).putExtra(EXTRA_CALL_ID, callId),
|
|
144
|
+
)
|
|
145
|
+
}
|
|
112
146
|
}
|
|
113
147
|
|
|
114
148
|
override fun ringtoneStart() {
|
|
@@ -133,6 +167,11 @@ internal class AndroidRingSurface(
|
|
|
133
167
|
|
|
134
168
|
override fun ringbackStart() {
|
|
135
169
|
ringbackStop()
|
|
170
|
+
ringbackWanted = true
|
|
171
|
+
if (communicationReady) playRingback()
|
|
172
|
+
}
|
|
173
|
+
|
|
174
|
+
private fun playRingback() {
|
|
136
175
|
val resId = config.ringbackRawRes
|
|
137
176
|
if (resId == 0) return
|
|
138
177
|
runCatching {
|
|
@@ -152,6 +191,7 @@ internal class AndroidRingSurface(
|
|
|
152
191
|
}
|
|
153
192
|
|
|
154
193
|
override fun ringbackStop() {
|
|
194
|
+
ringbackWanted = false
|
|
155
195
|
runCatching {
|
|
156
196
|
ringback?.stop()
|
|
157
197
|
ringback?.release()
|
|
@@ -184,6 +224,13 @@ internal class AndroidRingSurface(
|
|
|
184
224
|
const val ACTION_OPEN = "OPEN"
|
|
185
225
|
const val EXTRA_RING = "poolse_ring"
|
|
186
226
|
const val EXTRA_AUTO_ANSWER = "poolse_auto_answer"
|
|
227
|
+
/** Package-local broadcast: the ring for [EXTRA_CALL_ID] is over. */
|
|
228
|
+
const val ACTION_RING_CLOSED = "dev.poolse.callcore.RING_CLOSED"
|
|
229
|
+
const val EXTRA_CALL_ID = "poolse_call_id"
|
|
230
|
+
private const val TAG = "PoolseRingSurface"
|
|
231
|
+
|
|
232
|
+
/** The CallStyle notification on screen, for the ring-mode service to wear. */
|
|
233
|
+
@Volatile internal var shownRing: Pair<String, android.app.Notification>? = null
|
|
187
234
|
|
|
188
235
|
/** Stable per-call notification id, so teardown targets the right one. */
|
|
189
236
|
fun String.notificationId(): Int = hashCode()
|
|
@@ -19,10 +19,11 @@ import dev.poolse.callcore.android.AndroidRingSurface.Companion.EXTRA_RING
|
|
|
19
19
|
class CallActionReceiver : BroadcastReceiver() {
|
|
20
20
|
override fun onReceive(context: Context, intent: Intent) {
|
|
21
21
|
val ring = intent.getBundleExtra(EXTRA_RING)?.toRingContext() ?: return
|
|
22
|
+
val core = CallCore.getOrNull(context) ?: return
|
|
22
23
|
when (intent.action) {
|
|
23
|
-
"$ACTION_PREFIX.$ACTION_ANSWER" ->
|
|
24
|
-
"$ACTION_PREFIX.$ACTION_DECLINE" ->
|
|
25
|
-
"$ACTION_PREFIX.$ACTION_OPEN" ->
|
|
24
|
+
"$ACTION_PREFIX.$ACTION_ANSWER" -> core.onUserAnswer(ring)
|
|
25
|
+
"$ACTION_PREFIX.$ACTION_DECLINE" -> core.onUserDecline(ring)
|
|
26
|
+
"$ACTION_PREFIX.$ACTION_OPEN" -> core.onUserOpen(ring)
|
|
26
27
|
}
|
|
27
28
|
}
|
|
28
29
|
}
|