@poolse/react-native-calls 0.10.0 → 0.10.1
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/src/main/java/dev/poolse/callcore/android/AndroidRingSurface.kt +18 -1
- package/android/src/main/java/dev/poolse/callcore/android/CallCore.kt +26 -2
- package/android/src/main/java/dev/poolse/callcore/android/CallForegroundService.kt +19 -5
- package/android/src/main/java/dev/poolse/callcore/android/IncomingCallActivity.kt +11 -4
- package/ios/Platform/CallCore.swift +13 -9
- package/package.json +1 -1
|
@@ -181,6 +181,19 @@ internal class AndroidRingSurface(
|
|
|
181
181
|
)
|
|
182
182
|
}
|
|
183
183
|
|
|
184
|
+
// End & answer launches an ACTIVITY, like the ring's Answer: only an
|
|
185
|
+
// activity started from the tap may bring the app forward (Android
|
|
186
|
+
// 10+), and the call that connects should be seen.
|
|
187
|
+
val answerWaiting = PendingIntent.getActivity(
|
|
188
|
+
context,
|
|
189
|
+
ring.callId.hashCode() + 4,
|
|
190
|
+
Intent(context, IncomingCallActivity::class.java)
|
|
191
|
+
.putExtra(EXTRA_RING, extras)
|
|
192
|
+
.putExtra(EXTRA_WAITING_ANSWER, true)
|
|
193
|
+
.addFlags(Intent.FLAG_ACTIVITY_NEW_TASK or Intent.FLAG_ACTIVITY_CLEAR_TOP),
|
|
194
|
+
PendingIntent.FLAG_UPDATE_CURRENT or PendingIntent.FLAG_IMMUTABLE,
|
|
195
|
+
)
|
|
196
|
+
|
|
184
197
|
val who = ring.conversationName ?: ring.callerName ?: "Someone"
|
|
185
198
|
val what = if (ring.media == "video") "video call" else "call"
|
|
186
199
|
val notification = NotificationCompat.Builder(context, WAITING_CHANNEL_ID)
|
|
@@ -195,7 +208,10 @@ internal class AndroidRingSurface(
|
|
|
195
208
|
.setOnlyAlertOnce(true)
|
|
196
209
|
.apply { if (open != null) setContentIntent(open) }
|
|
197
210
|
.addAction(0, "Decline", actionPending(ACTION_WAITING_DECLINE, 3))
|
|
198
|
-
.addAction(0, "End & answer",
|
|
211
|
+
.addAction(0, "End & answer", answerWaiting)
|
|
212
|
+
// Never outlive the ring: if its ring_ended/finished push is lost,
|
|
213
|
+
// the system takes the heads-up down at the deadline anyway.
|
|
214
|
+
.setTimeoutAfter((ring.ringDeadlineEpochMs - System.currentTimeMillis()).coerceAtLeast(10_000L))
|
|
199
215
|
.build()
|
|
200
216
|
|
|
201
217
|
waitingCallId = ring.callId
|
|
@@ -345,6 +361,7 @@ internal class AndroidRingSurface(
|
|
|
345
361
|
private const val WAITING_BEEP_VOLUME = 70
|
|
346
362
|
const val EXTRA_RING = "poolse_ring"
|
|
347
363
|
const val EXTRA_AUTO_ANSWER = "poolse_auto_answer"
|
|
364
|
+
const val EXTRA_WAITING_ANSWER = "poolse_waiting_answer"
|
|
348
365
|
/** Package-local broadcast: the ring for [EXTRA_CALL_ID] is over. */
|
|
349
366
|
const val ACTION_RING_CLOSED = "dev.poolse.callcore.RING_CLOSED"
|
|
350
367
|
const val EXTRA_CALL_ID = "poolse_call_id"
|
|
@@ -122,6 +122,23 @@ class CallCore private constructor(context: Context) {
|
|
|
122
122
|
return bringAppToFront(ring.callId, from)
|
|
123
123
|
}
|
|
124
124
|
|
|
125
|
+
/**
|
|
126
|
+
* End & answer from the call-waiting heads-up. Only for the call still
|
|
127
|
+
* waiting (a stale notification is just taken down); brings the app
|
|
128
|
+
* forward from [from], like [onUserAnswer].
|
|
129
|
+
*/
|
|
130
|
+
fun onUserAnswerWaiting(ring: RingContext, from: Context? = null): Boolean {
|
|
131
|
+
if (runtime.currentState().waiting?.callId != ring.callId) {
|
|
132
|
+
runCatching {
|
|
133
|
+
androidx.core.app.NotificationManagerCompat.from(appContext)
|
|
134
|
+
.cancel(with(AndroidRingSurface) { ring.callId.notificationId() })
|
|
135
|
+
}
|
|
136
|
+
return false
|
|
137
|
+
}
|
|
138
|
+
runtime.answerWaiting(ring.callId)
|
|
139
|
+
return bringAppToFront(ring.callId, from)
|
|
140
|
+
}
|
|
141
|
+
|
|
125
142
|
fun onUserDecline(ring: RingContext) {
|
|
126
143
|
runtime.onRing(ring)
|
|
127
144
|
runtime.decline()
|
|
@@ -205,8 +222,10 @@ class CallCore private constructor(context: Context) {
|
|
|
205
222
|
}
|
|
206
223
|
resetExtras(null, null)
|
|
207
224
|
} else if (state.callId != extrasCallId) {
|
|
208
|
-
// `pending:<conv>` → the server-minted id is the same call
|
|
209
|
-
|
|
225
|
+
// `pending:<conv>` → the server-minted id is the same call — but
|
|
226
|
+
// only for that conversation's own placed call: a waiting call
|
|
227
|
+
// promoted over a still-pending one is a different call.
|
|
228
|
+
if (isRenameOfPending(extrasCallId, state)) extrasCallId = state.callId
|
|
210
229
|
else resetExtras(state.callId, state.media)
|
|
211
230
|
}
|
|
212
231
|
if (state.phase != Phase.IDLE && state.media != extrasMedia) {
|
|
@@ -246,6 +265,11 @@ class CallCore private constructor(context: Context) {
|
|
|
246
265
|
runCatching { listener?.invoke(snap) }.onFailure { Log.w(TAG, "snapshot listener failed", it) }
|
|
247
266
|
}
|
|
248
267
|
|
|
268
|
+
private fun isRenameOfPending(previous: String?, state: MachineState): Boolean =
|
|
269
|
+
previous != null && previous.startsWith("pending:") &&
|
|
270
|
+
state.direction != dev.poolse.callcore.Direction.INCOMING &&
|
|
271
|
+
previous == "pending:${state.conversationId}"
|
|
272
|
+
|
|
249
273
|
private fun resetExtras(callId: String?, media: String?) {
|
|
250
274
|
extrasCallId = callId
|
|
251
275
|
extrasMedia = media
|
|
@@ -220,14 +220,22 @@ class CallForegroundService : Service() {
|
|
|
220
220
|
|
|
221
221
|
override fun start(callId: String, media: String) = onMain {
|
|
222
222
|
desired = Desired(Mode.CALL, callId, media)
|
|
223
|
-
|
|
223
|
+
launchOrSwitch()
|
|
224
224
|
}
|
|
225
225
|
|
|
226
226
|
override fun stop() = onMain {
|
|
227
227
|
desired = null
|
|
228
|
-
//
|
|
229
|
-
//
|
|
230
|
-
|
|
228
|
+
// Deferred to the end of this main-thread turn: a call handing over
|
|
229
|
+
// to the next (call waiting: end & answer, or a promoted ring) stops
|
|
230
|
+
// and starts in one dispatch, and must switch the RUNNING service
|
|
231
|
+
// in place — a fresh start from the background is refused on
|
|
232
|
+
// Android 12+ (and loses the microphone type on 14+). A start in
|
|
233
|
+
// the same turn sets `desired` again and this does nothing.
|
|
234
|
+
main.post {
|
|
235
|
+
// Only a started instance with nothing pending may stop now; a
|
|
236
|
+
// pending start stops itself after startForeground.
|
|
237
|
+
if (desired == null && pendingStarts == 0) instance?.stopNow()
|
|
238
|
+
}
|
|
231
239
|
}
|
|
232
240
|
|
|
233
241
|
/** Ring mode for [callId]; a no-op while a call holds the service. */
|
|
@@ -236,7 +244,7 @@ class CallForegroundService : Service() {
|
|
|
236
244
|
if (d?.mode == Mode.CALL) return@onMain
|
|
237
245
|
if (d?.mode == Mode.RING && d.callId == callId) return@onMain
|
|
238
246
|
desired = Desired(Mode.RING, callId, "audio")
|
|
239
|
-
|
|
247
|
+
launchOrSwitch()
|
|
240
248
|
}
|
|
241
249
|
|
|
242
250
|
/**
|
|
@@ -255,6 +263,12 @@ class CallForegroundService : Service() {
|
|
|
255
263
|
if (desired?.mode == Mode.RING) stop()
|
|
256
264
|
}
|
|
257
265
|
|
|
266
|
+
/** A running, settled instance switches in place; otherwise start one. */
|
|
267
|
+
private fun launchOrSwitch() {
|
|
268
|
+
val running = instance
|
|
269
|
+
if (running != null && pendingStarts == 0) running.reconcile(desired) else launch()
|
|
270
|
+
}
|
|
271
|
+
|
|
258
272
|
private fun launch() {
|
|
259
273
|
if (!::appContext.isInitialized) return
|
|
260
274
|
val intent = Intent(appContext, CallForegroundService::class.java)
|
|
@@ -93,7 +93,14 @@ class IncomingCallActivity : Activity() {
|
|
|
93
93
|
// background receiver is not).
|
|
94
94
|
if (intent.getBooleanExtra(AndroidRingSurface.EXTRA_AUTO_ANSWER, false)) {
|
|
95
95
|
ring = next
|
|
96
|
-
answerAndForward(
|
|
96
|
+
answerAndForward { core.onUserAnswer(next, this) }
|
|
97
|
+
return
|
|
98
|
+
}
|
|
99
|
+
// End & answer on a call-waiting heads-up: the same, for the call
|
|
100
|
+
// waiting beside the current one.
|
|
101
|
+
if (intent.getBooleanExtra(AndroidRingSurface.EXTRA_WAITING_ANSWER, false)) {
|
|
102
|
+
ring = next
|
|
103
|
+
answerAndForward { core.onUserAnswerWaiting(next, this) }
|
|
97
104
|
return
|
|
98
105
|
}
|
|
99
106
|
|
|
@@ -118,12 +125,12 @@ class IncomingCallActivity : Activity() {
|
|
|
118
125
|
* behind the lock screen. onStop (we got covered) finishes us; a
|
|
119
126
|
* timer is the fallback.
|
|
120
127
|
*/
|
|
121
|
-
private fun answerAndForward(
|
|
128
|
+
private fun answerAndForward(answer: () -> Boolean) {
|
|
122
129
|
awaitingForward = true
|
|
123
130
|
pulse?.cancel()
|
|
124
131
|
pulse = null
|
|
125
132
|
setContentView(View(this).apply { setBackgroundColor(Color.parseColor("#0B0B14")) })
|
|
126
|
-
val launched =
|
|
133
|
+
val launched = answer()
|
|
127
134
|
if (!launched) { finish(); return }
|
|
128
135
|
main.postDelayed({ if (!isFinishing) finish() }, FORWARD_FALLBACK_MS)
|
|
129
136
|
}
|
|
@@ -238,7 +245,7 @@ class IncomingCallActivity : Activity() {
|
|
|
238
245
|
}
|
|
239
246
|
val accept = actionButton("Answer", "✓", "#22C55E") {
|
|
240
247
|
val core = CallCore.getOrNull(this)
|
|
241
|
-
if (core == null) finish() else answerAndForward(
|
|
248
|
+
if (core == null) finish() else answerAndForward { core.onUserAnswer(ring, this) }
|
|
242
249
|
}
|
|
243
250
|
buttons.addView(decline, LinearLayout.LayoutParams(wrap(), wrap()).apply { rightMargin = px(52) })
|
|
244
251
|
buttons.addView(accept, LinearLayout.LayoutParams(wrap(), wrap()).apply { leftMargin = px(52) })
|
|
@@ -234,13 +234,10 @@ final class CallCore {
|
|
|
234
234
|
}
|
|
235
235
|
|
|
236
236
|
/// Call waiting: end this call, answer the waiting one. CallKit follows
|
|
237
|
-
/// from the resulting state
|
|
238
|
-
/// answers the new one
|
|
237
|
+
/// from the resulting state: reconcileCallKit reports the old call
|
|
238
|
+
/// ended, then answers the new one.
|
|
239
239
|
func answerWaiting(callId: String? = nil) {
|
|
240
|
-
onMain { [self] in
|
|
241
|
-
localEnd = true
|
|
242
|
-
runtime.answerWaiting(callId: callId)
|
|
243
|
-
}
|
|
240
|
+
onMain { [self] in runtime.answerWaiting(callId: callId) }
|
|
244
241
|
}
|
|
245
242
|
|
|
246
243
|
/// Call waiting: turn the waiting call away (its caller hears busy).
|
|
@@ -314,8 +311,13 @@ final class CallCore {
|
|
|
314
311
|
if extrasCallId != nil { mediaPort.leave() }
|
|
315
312
|
resetExtras(nil, nil)
|
|
316
313
|
} else if state.callId != extrasCallId {
|
|
317
|
-
|
|
318
|
-
|
|
314
|
+
// `pending:<conv>` → the server-minted id is the same call — but only
|
|
315
|
+
// for that conversation's own placed call: a waiting call promoted
|
|
316
|
+
// over a still-pending one is a different call (renaming would give
|
|
317
|
+
// CallKit two calls with one id, and the incoming one never answered).
|
|
318
|
+
if let old = extrasCallId, old.hasPrefix("pending:"), state.direction != .incoming,
|
|
319
|
+
old == "pending:\(state.conversationId ?? "")" {
|
|
320
|
+
if let new = state.callId { callKit.rename(from: old, to: new) }
|
|
319
321
|
extrasCallId = state.callId
|
|
320
322
|
} else {
|
|
321
323
|
// A different call (a waiting one promoted): nothing of the last
|
|
@@ -485,7 +487,6 @@ extension CallCore: CallKitActions {
|
|
|
485
487
|
// End & Accept may answer the waiting call before ending the current
|
|
486
488
|
// one: answering it ends the current call itself.
|
|
487
489
|
if s.waiting?.callId == callId {
|
|
488
|
-
localEnd = true
|
|
489
490
|
runtime.answerWaiting(callId: callId)
|
|
490
491
|
return
|
|
491
492
|
}
|
|
@@ -523,6 +524,9 @@ extension CallCore: CallKitActions {
|
|
|
523
524
|
func callKitReset() {
|
|
524
525
|
let s = runtime.currentState()
|
|
525
526
|
guard s.phase != .idle && s.phase != .ended else { return }
|
|
527
|
+
// CallKit dropped every call, the waiting one included: turn it away
|
|
528
|
+
// first, or ending the current call would ring it afresh.
|
|
529
|
+
if let w = s.waiting { runtime.declineWaiting(callId: w.callId) }
|
|
526
530
|
localEnd = true
|
|
527
531
|
if s.phase == .ringingIn { runtime.decline() } else { runtime.hangUp() }
|
|
528
532
|
}
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@poolse/react-native-calls",
|
|
3
|
-
"version": "0.10.
|
|
3
|
+
"version": "0.10.1",
|
|
4
4
|
"description": "Native call core for poolse — Kotlin/Swift calling engine with a thin React Native bridge. Ring, answer and carry audio with the app killed and the phone locked.",
|
|
5
5
|
"license": "MIT",
|
|
6
6
|
"type": "module",
|