@poolse/react-native-calls 0.1.3 → 0.3.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 +269 -25
- 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/CallVideo.kt +36 -0
- package/android/src/main/java/dev/poolse/callcore/android/IncomingCallActivity.kt +100 -12
- package/android/src/main/java/dev/poolse/callcore/android/SnapshotJson.kt +15 -0
- package/android/src/main/java/expo/modules/poolsecalls/PoolseCallVideoView.kt +64 -0
- package/android/src/main/java/expo/modules/poolsecalls/PoolseCallsModule.kt +85 -17
- package/android/src/main/livekit/java/dev/poolse/callcore/livekit/LiveKitMediaPort.kt +325 -23
- package/android/src/main/livekit/java/dev/poolse/callcore/livekit/LiveKitVideoSurface.kt +107 -0
- package/android/src/test/java/dev/poolse/callcore/android/CallHttpClientTest.kt +61 -0
- package/dist/index.cjs +34 -0
- package/dist/index.cjs.map +1 -1
- package/dist/index.d.cts +52 -3
- package/dist/index.d.ts +52 -3
- package/dist/index.js +30 -2
- package/dist/index.js.map +1 -1
- package/package.json +2 -1
- package/src/expo-modules-core.d.ts +5 -0
- package/src/index.ts +91 -3
- package/src/react-shims.d.ts +22 -0
|
@@ -2,11 +2,17 @@ package dev.poolse.callcore.android
|
|
|
2
2
|
|
|
3
3
|
import android.animation.ValueAnimator
|
|
4
4
|
import android.app.Activity
|
|
5
|
+
import android.content.BroadcastReceiver
|
|
6
|
+
import android.content.Context
|
|
7
|
+
import android.content.Intent
|
|
8
|
+
import android.content.IntentFilter
|
|
5
9
|
import android.graphics.Color
|
|
6
10
|
import android.graphics.Typeface
|
|
7
11
|
import android.graphics.drawable.GradientDrawable
|
|
8
12
|
import android.os.Build
|
|
9
13
|
import android.os.Bundle
|
|
14
|
+
import android.os.Handler
|
|
15
|
+
import android.os.Looper
|
|
10
16
|
import android.view.Gravity
|
|
11
17
|
import android.view.View
|
|
12
18
|
import android.view.WindowManager
|
|
@@ -14,6 +20,10 @@ import android.view.animation.LinearInterpolator
|
|
|
14
20
|
import android.widget.FrameLayout
|
|
15
21
|
import android.widget.LinearLayout
|
|
16
22
|
import android.widget.TextView
|
|
23
|
+
import androidx.core.content.ContextCompat
|
|
24
|
+
import dev.poolse.callcore.RingContext
|
|
25
|
+
import dev.poolse.callcore.android.AndroidRingSurface.Companion.ACTION_RING_CLOSED
|
|
26
|
+
import dev.poolse.callcore.android.AndroidRingSurface.Companion.EXTRA_CALL_ID
|
|
17
27
|
import dev.poolse.callcore.android.AndroidRingSurface.Companion.EXTRA_RING
|
|
18
28
|
|
|
19
29
|
/**
|
|
@@ -23,41 +33,118 @@ import dev.poolse.callcore.android.AndroidRingSurface.Companion.EXTRA_RING
|
|
|
23
33
|
* ground, brand glow, a breathing ring) in plain views. The buttons
|
|
24
34
|
* resolve into [CallCore], the same entry point every other surface
|
|
25
35
|
* uses; answering brings the app forward over the keyguard.
|
|
36
|
+
*
|
|
37
|
+
* It closes itself when the ring ends anywhere else (caller cancelled,
|
|
38
|
+
* answered on another device, deadline) via the package-local
|
|
39
|
+
* [ACTION_RING_CLOSED] broadcast — before, a remotely-ended ring left
|
|
40
|
+
* this screen up with dead buttons.
|
|
26
41
|
*/
|
|
27
42
|
class IncomingCallActivity : Activity() {
|
|
28
43
|
|
|
29
44
|
private var pulse: ValueAnimator? = null
|
|
30
45
|
private val density by lazy { resources.displayMetrics.density }
|
|
31
46
|
private fun px(v: Int): Int = (v * density).toInt()
|
|
47
|
+
private val main = Handler(Looper.getMainLooper())
|
|
48
|
+
|
|
49
|
+
/** The ring on screen. */
|
|
50
|
+
private var ring: RingContext? = null
|
|
51
|
+
|
|
52
|
+
/**
|
|
53
|
+
* Answered: we are only waiting for the app to come forward over us.
|
|
54
|
+
* Our own DISMISS_RING broadcast must not close us before it does —
|
|
55
|
+
* finishing first left the app behind the keyguard.
|
|
56
|
+
*/
|
|
57
|
+
private var awaitingForward = false
|
|
58
|
+
|
|
59
|
+
private val ringClosed = object : BroadcastReceiver() {
|
|
60
|
+
override fun onReceive(context: Context, intent: Intent) {
|
|
61
|
+
val callId = intent.getStringExtra(EXTRA_CALL_ID) ?: return
|
|
62
|
+
if (callId == ring?.callId && !awaitingForward) finish()
|
|
63
|
+
}
|
|
64
|
+
}
|
|
32
65
|
|
|
33
66
|
override fun onCreate(savedInstanceState: Bundle?) {
|
|
34
67
|
super.onCreate(savedInstanceState)
|
|
35
68
|
showOverKeyguard()
|
|
69
|
+
ContextCompat.registerReceiver(
|
|
70
|
+
this,
|
|
71
|
+
ringClosed,
|
|
72
|
+
IntentFilter(ACTION_RING_CLOSED),
|
|
73
|
+
ContextCompat.RECEIVER_NOT_EXPORTED,
|
|
74
|
+
)
|
|
75
|
+
handle(intent)
|
|
76
|
+
}
|
|
36
77
|
|
|
37
|
-
|
|
38
|
-
|
|
78
|
+
/** singleInstance: a new ring (or the answer action) arrives here, not in onCreate. */
|
|
79
|
+
override fun onNewIntent(intent: Intent) {
|
|
80
|
+
super.onNewIntent(intent)
|
|
81
|
+
setIntent(intent)
|
|
82
|
+
handle(intent)
|
|
83
|
+
}
|
|
84
|
+
|
|
85
|
+
private fun handle(intent: Intent) {
|
|
86
|
+
val next = intent.getBundleExtra(EXTRA_RING)?.toRingContext()
|
|
87
|
+
if (next == null) { if (ring == null) finish(); return }
|
|
88
|
+
val core = CallCore.getOrNull(this)
|
|
89
|
+
if (core == null) { finish(); return }
|
|
39
90
|
|
|
40
91
|
// Answered from the notification: this activity exists only to be the
|
|
41
92
|
// foreground context that is ALLOWED to bring the app up (a
|
|
42
|
-
// background receiver is not).
|
|
43
|
-
// onUserAnswer launches the app over the keyguard.
|
|
93
|
+
// background receiver is not).
|
|
44
94
|
if (intent.getBooleanExtra(AndroidRingSurface.EXTRA_AUTO_ANSWER, false)) {
|
|
45
|
-
|
|
46
|
-
|
|
95
|
+
ring = next
|
|
96
|
+
answerAndForward(core, next)
|
|
97
|
+
return
|
|
98
|
+
}
|
|
99
|
+
|
|
100
|
+
// A stale tap (the ring resolved, or the process restarted after it
|
|
101
|
+
// ended): there is nothing to answer.
|
|
102
|
+
if (!core.isRingLive(next)) {
|
|
103
|
+
if (ring == null || ring?.callId == next.callId) finish()
|
|
47
104
|
return
|
|
48
105
|
}
|
|
106
|
+
ring = next
|
|
107
|
+
awaitingForward = false
|
|
108
|
+
pulse?.cancel()
|
|
109
|
+
val title = next.conversationName ?: next.callerName ?: "Incoming call"
|
|
110
|
+
val body = if (next.media == "video") "Incoming video call" else "Incoming call"
|
|
111
|
+
setContentView(buildScene(title, body, next))
|
|
112
|
+
}
|
|
113
|
+
|
|
114
|
+
/**
|
|
115
|
+
* Answer, then bring the app forward FROM THIS ACTIVITY, and stay
|
|
116
|
+
* (as a plain dark window) until the app is in front — finishing at
|
|
117
|
+
* once dropped the start on some OEMs and left the call audible
|
|
118
|
+
* behind the lock screen. onStop (we got covered) finishes us; a
|
|
119
|
+
* timer is the fallback.
|
|
120
|
+
*/
|
|
121
|
+
private fun answerAndForward(core: CallCore, ring: RingContext) {
|
|
122
|
+
awaitingForward = true
|
|
123
|
+
pulse?.cancel()
|
|
124
|
+
pulse = null
|
|
125
|
+
setContentView(View(this).apply { setBackgroundColor(Color.parseColor("#0B0B14")) })
|
|
126
|
+
val launched = core.onUserAnswer(ring, this)
|
|
127
|
+
if (!launched) { finish(); return }
|
|
128
|
+
main.postDelayed({ if (!isFinishing) finish() }, FORWARD_FALLBACK_MS)
|
|
129
|
+
}
|
|
49
130
|
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
131
|
+
override fun onStop() {
|
|
132
|
+
super.onStop()
|
|
133
|
+
if (awaitingForward && !isFinishing) finish()
|
|
53
134
|
}
|
|
54
135
|
|
|
55
136
|
override fun onDestroy() {
|
|
137
|
+
main.removeCallbacksAndMessages(null)
|
|
138
|
+
runCatching { unregisterReceiver(ringClosed) }
|
|
56
139
|
pulse?.cancel()
|
|
57
140
|
pulse = null
|
|
58
141
|
super.onDestroy()
|
|
59
142
|
}
|
|
60
143
|
|
|
144
|
+
private companion object {
|
|
145
|
+
const val FORWARD_FALLBACK_MS = 4_000L
|
|
146
|
+
}
|
|
147
|
+
|
|
61
148
|
private fun showOverKeyguard() {
|
|
62
149
|
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O_MR1) {
|
|
63
150
|
setShowWhenLocked(true)
|
|
@@ -72,7 +159,7 @@ class IncomingCallActivity : Activity() {
|
|
|
72
159
|
window.addFlags(WindowManager.LayoutParams.FLAG_KEEP_SCREEN_ON)
|
|
73
160
|
}
|
|
74
161
|
|
|
75
|
-
private fun buildScene(title: String, body: String, ring:
|
|
162
|
+
private fun buildScene(title: String, body: String, ring: RingContext): View {
|
|
76
163
|
val root = FrameLayout(this).apply { setBackgroundColor(Color.parseColor("#0B0B14")) }
|
|
77
164
|
|
|
78
165
|
root.addView(
|
|
@@ -147,10 +234,11 @@ class IncomingCallActivity : Activity() {
|
|
|
147
234
|
layoutParams = LinearLayout.LayoutParams(LinearLayout.LayoutParams.MATCH_PARENT, wrap())
|
|
148
235
|
}
|
|
149
236
|
val decline = actionButton("Decline", "✕", "#EF4444") {
|
|
150
|
-
CallCore.
|
|
237
|
+
CallCore.getOrNull(this)?.onUserDecline(ring); finish()
|
|
151
238
|
}
|
|
152
239
|
val accept = actionButton("Answer", "✓", "#22C55E") {
|
|
153
|
-
CallCore.
|
|
240
|
+
val core = CallCore.getOrNull(this)
|
|
241
|
+
if (core == null) finish() else answerAndForward(core, ring)
|
|
154
242
|
}
|
|
155
243
|
buttons.addView(decline, LinearLayout.LayoutParams(wrap(), wrap()).apply { rightMargin = px(52) })
|
|
156
244
|
buttons.addView(accept, LinearLayout.LayoutParams(wrap(), wrap()).apply { leftMargin = px(52) })
|
|
@@ -20,8 +20,23 @@ internal fun MachineState.toSnapshot(
|
|
|
20
20
|
audioRoute: String = "earpiece",
|
|
21
21
|
mutedSelf: Boolean = false,
|
|
22
22
|
cameraOn: Boolean = false,
|
|
23
|
+
cameraFacing: String? = null,
|
|
23
24
|
): Map<String, Any?>? {
|
|
24
25
|
if (phase == Phase.IDLE) return null
|
|
26
|
+
return buildMap {
|
|
27
|
+
putAll(snapshotCore(conversationName, participants, audioRoute, mutedSelf, cameraOn))
|
|
28
|
+
// Optional in the contract: present only while a camera is published.
|
|
29
|
+
if (cameraFacing != null) put("cameraFacing", cameraFacing)
|
|
30
|
+
}
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
private fun MachineState.snapshotCore(
|
|
34
|
+
conversationName: String?,
|
|
35
|
+
participants: List<Map<String, Any>>,
|
|
36
|
+
audioRoute: String,
|
|
37
|
+
mutedSelf: Boolean,
|
|
38
|
+
cameraOn: Boolean,
|
|
39
|
+
): Map<String, Any?> {
|
|
25
40
|
return mapOf(
|
|
26
41
|
"cv" to 1,
|
|
27
42
|
"callId" to callId,
|
|
@@ -0,0 +1,64 @@
|
|
|
1
|
+
package expo.modules.poolsecalls
|
|
2
|
+
|
|
3
|
+
import android.content.Context
|
|
4
|
+
import android.view.ViewGroup
|
|
5
|
+
import dev.poolse.callcore.android.CallCore
|
|
6
|
+
import dev.poolse.callcore.android.CallVideoSurface
|
|
7
|
+
import expo.modules.kotlin.AppContext
|
|
8
|
+
import expo.modules.kotlin.views.ExpoView
|
|
9
|
+
|
|
10
|
+
/**
|
|
11
|
+
* `<CallVideoView>` on Android: a thin host for the core's
|
|
12
|
+
* [CallVideoSurface]. All binding logic (which track, rebinds on
|
|
13
|
+
* subscribe/mute/rejoin) lives in the media adapter; this view only
|
|
14
|
+
* forwards props and ties the surface's lifetime to the window.
|
|
15
|
+
*/
|
|
16
|
+
class PoolseCallVideoView(context: Context, appContext: AppContext) : ExpoView(context, appContext) {
|
|
17
|
+
|
|
18
|
+
// The renderer is a native child React does not lay out: let Android
|
|
19
|
+
// measure it (and re-measure when the video's size changes).
|
|
20
|
+
override val shouldUseAndroidLayout = true
|
|
21
|
+
|
|
22
|
+
private var surface: CallVideoSurface? = null
|
|
23
|
+
|
|
24
|
+
var participantId: String? = null
|
|
25
|
+
set(v) { field = v; apply() }
|
|
26
|
+
var local: Boolean = false
|
|
27
|
+
set(v) { field = v; apply() }
|
|
28
|
+
var mirror: Boolean? = null
|
|
29
|
+
set(v) { field = v; apply() }
|
|
30
|
+
var fit: String = "cover"
|
|
31
|
+
set(v) { field = v; apply() }
|
|
32
|
+
|
|
33
|
+
override fun onAttachedToWindow() {
|
|
34
|
+
super.onAttachedToWindow()
|
|
35
|
+
attachSurface()
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
override fun onDetachedFromWindow() {
|
|
39
|
+
detachSurface()
|
|
40
|
+
super.onDetachedFromWindow()
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
private fun attachSurface() {
|
|
44
|
+
if (surface != null) return
|
|
45
|
+
// No core yet (never configured) or no video-capable SFU: stay empty.
|
|
46
|
+
val s = CallCore.peek()?.createVideoSurface(context) ?: return
|
|
47
|
+
surface = s
|
|
48
|
+
addView(s.view, LayoutParams(ViewGroup.LayoutParams.MATCH_PARENT, ViewGroup.LayoutParams.MATCH_PARENT))
|
|
49
|
+
apply()
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
internal fun detachSurface() {
|
|
53
|
+
val s = surface ?: return
|
|
54
|
+
surface = null
|
|
55
|
+
s.release()
|
|
56
|
+
removeView(s.view)
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
private fun apply() {
|
|
60
|
+
// Mounted before the core existed (configure still pending): try again.
|
|
61
|
+
val s = surface ?: run { if (isAttachedToWindow) attachSurface(); return }
|
|
62
|
+
s.configure(participantId, local, mirror, if (fit == "contain") "contain" else "cover")
|
|
63
|
+
}
|
|
64
|
+
}
|
|
@@ -1,7 +1,13 @@
|
|
|
1
1
|
package expo.modules.poolsecalls
|
|
2
2
|
|
|
3
|
+
import android.app.NotificationManager
|
|
4
|
+
import android.content.Context
|
|
5
|
+
import android.content.Intent
|
|
6
|
+
import android.net.Uri
|
|
7
|
+
import android.os.Build
|
|
8
|
+
import android.provider.Settings
|
|
9
|
+
import android.util.Log
|
|
3
10
|
import dev.poolse.callcore.android.CallCore
|
|
4
|
-
import dev.poolse.callcore.android.toSnapshot
|
|
5
11
|
import expo.modules.kotlin.modules.Module
|
|
6
12
|
import expo.modules.kotlin.modules.ModuleDefinition
|
|
7
13
|
|
|
@@ -15,6 +21,9 @@ import expo.modules.kotlin.modules.ModuleDefinition
|
|
|
15
21
|
* the JS side needs no R references. The SFU factory defaults to the
|
|
16
22
|
* shipped LiveKit adapter when present, else a no-op (audio-less), so a
|
|
17
23
|
* build without the LiveKit source set still rings and signals.
|
|
24
|
+
*
|
|
25
|
+
* Nothing here throws before `configure`: reads return idle, commands
|
|
26
|
+
* no-op (a fresh install renders its UI before JS has configured).
|
|
18
27
|
*/
|
|
19
28
|
class PoolseCallsModule : Module() {
|
|
20
29
|
|
|
@@ -23,45 +32,104 @@ class PoolseCallsModule : Module() {
|
|
|
23
32
|
|
|
24
33
|
Events("onCallState")
|
|
25
34
|
|
|
35
|
+
// <CallVideoView>: renders a participant's (or our own) camera.
|
|
36
|
+
View(PoolseCallVideoView::class) {
|
|
37
|
+
Name("PoolseCallVideo")
|
|
38
|
+
Prop("participantId") { view: PoolseCallVideoView, id: String? -> view.participantId = id }
|
|
39
|
+
Prop("local") { view: PoolseCallVideoView, local: Boolean? -> view.local = local ?: false }
|
|
40
|
+
Prop("mirror") { view: PoolseCallVideoView, mirror: Boolean? -> view.mirror = mirror }
|
|
41
|
+
Prop("fit") { view: PoolseCallVideoView, fit: String? -> view.fit = fit ?: "cover" }
|
|
42
|
+
OnViewDestroys { view: PoolseCallVideoView -> view.detachSurface() }
|
|
43
|
+
}
|
|
44
|
+
|
|
26
45
|
// Snapshots → the single JS event. Null (idle) becomes an explicit
|
|
27
|
-
// cleared payload so the JS side always has a definite state.
|
|
46
|
+
// cleared payload so the JS side always has a definite state. The
|
|
47
|
+
// listener is process-level, so subscribing before configure works.
|
|
28
48
|
OnStartObserving {
|
|
29
|
-
CallCore.
|
|
30
|
-
sendEvent("onCallState", mapOf("snapshot" to state.toSnapshot()))
|
|
31
|
-
}
|
|
49
|
+
CallCore.listener = { snapshot -> sendEvent("onCallState", mapOf("snapshot" to snapshot)) }
|
|
32
50
|
}
|
|
33
51
|
OnStopObserving {
|
|
34
|
-
CallCore.
|
|
52
|
+
CallCore.listener = null
|
|
35
53
|
}
|
|
36
54
|
|
|
37
55
|
AsyncFunction("configure") { apiBaseUrl: String, options: Map<String, Any?>? ->
|
|
56
|
+
val ctx = context ?: return@AsyncFunction null
|
|
38
57
|
// Resource NAMES go to the core, which resolves + persists them —
|
|
39
58
|
// so a killed app woken by a ring can rebuild this without JS.
|
|
40
59
|
CallCore.configure(
|
|
41
|
-
|
|
60
|
+
ctx,
|
|
42
61
|
apiBaseUrl,
|
|
43
62
|
options?.get("smallIcon") as? String,
|
|
44
63
|
options?.get("ringback") as? String,
|
|
45
64
|
)
|
|
65
|
+
// Materialise the core now, so a call already in flight (cold
|
|
66
|
+
// answer) is visible to the first getCallState.
|
|
67
|
+
CallCore.getOrNull(ctx)
|
|
68
|
+
null
|
|
46
69
|
}
|
|
47
70
|
|
|
48
71
|
Function("getCallState") {
|
|
49
|
-
mapOf("snapshot" to CallCore.
|
|
72
|
+
mapOf("snapshot" to (CallCore.peek() ?: context?.let(CallCore::getOrNull))?.snapshot())
|
|
50
73
|
}
|
|
51
74
|
|
|
52
75
|
AsyncFunction("place") { conversationId: String, media: String, sessionToken: String ->
|
|
53
|
-
|
|
76
|
+
core()?.place(conversationId, media, sessionToken)
|
|
54
77
|
}
|
|
55
78
|
AsyncFunction("join") { callId: String, conversationId: String?, sessionToken: String ->
|
|
56
|
-
|
|
79
|
+
core()?.join(callId, conversationId, sessionToken)
|
|
80
|
+
}
|
|
81
|
+
Function("answer") { core()?.answer() }
|
|
82
|
+
Function("decline") { core()?.decline() }
|
|
83
|
+
Function("hangUp") { core()?.hangUp() }
|
|
84
|
+
Function("setMuted") { muted: Boolean -> core()?.setMuted(muted) }
|
|
85
|
+
Function("setCamera") { on: Boolean -> core()?.setCamera(on) }
|
|
86
|
+
Function("switchCamera") { core()?.switchCamera() }
|
|
87
|
+
Function("setSpeaker") { on: Boolean -> core()?.setSpeaker(on) }
|
|
88
|
+
|
|
89
|
+
// Socket forwarding (open app): idempotent with the FCM path.
|
|
90
|
+
Function("onRingEnded") { callId: String, reason: String -> core()?.onRingEnded(callId, reason) }
|
|
91
|
+
Function("onCallFinished") { callId: String, reason: String? ->
|
|
92
|
+
core()?.onCallFinished(callId, reason?.ifEmpty { null })
|
|
93
|
+
}
|
|
94
|
+
|
|
95
|
+
// Android 14+: full-screen intents need a user grant for non-dialer apps.
|
|
96
|
+
Function("canUseFullScreenIntent") {
|
|
97
|
+
val ctx = context ?: return@Function true
|
|
98
|
+
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.UPSIDE_DOWN_CAKE) {
|
|
99
|
+
ctx.getSystemService(NotificationManager::class.java).canUseFullScreenIntent()
|
|
100
|
+
} else {
|
|
101
|
+
true
|
|
102
|
+
}
|
|
103
|
+
}
|
|
104
|
+
Function("openFullScreenIntentSettings") {
|
|
105
|
+
val ctx = context ?: return@Function null
|
|
106
|
+
val pkg = Uri.parse("package:${ctx.packageName}")
|
|
107
|
+
val intents = buildList {
|
|
108
|
+
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.UPSIDE_DOWN_CAKE) {
|
|
109
|
+
add(Intent(Settings.ACTION_MANAGE_APP_USE_FULL_SCREEN_INTENT, pkg))
|
|
110
|
+
}
|
|
111
|
+
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O) {
|
|
112
|
+
add(Intent(Settings.ACTION_APP_NOTIFICATION_SETTINGS).putExtra(Settings.EXTRA_APP_PACKAGE, ctx.packageName))
|
|
113
|
+
}
|
|
114
|
+
add(Intent(Settings.ACTION_APPLICATION_DETAILS_SETTINGS, pkg))
|
|
115
|
+
}
|
|
116
|
+
val opener = appContext.currentActivity
|
|
117
|
+
for (intent in intents) {
|
|
118
|
+
val ok = runCatching {
|
|
119
|
+
if (opener != null) opener.startActivity(intent)
|
|
120
|
+
else ctx.startActivity(intent.addFlags(Intent.FLAG_ACTIVITY_NEW_TASK))
|
|
121
|
+
}.onFailure { Log.w(TAG, "settings intent ${intent.action} unavailable", it) }.isSuccess
|
|
122
|
+
if (ok) break
|
|
123
|
+
}
|
|
124
|
+
null
|
|
57
125
|
}
|
|
58
|
-
Function("answer") { CallCore.get(context).answer() }
|
|
59
|
-
Function("decline") { CallCore.get(context).decline() }
|
|
60
|
-
Function("hangUp") { CallCore.get(context).hangUp() }
|
|
61
|
-
Function("setMuted") { muted: Boolean -> CallCore.get(context).setMuted(muted) }
|
|
62
|
-
Function("setCamera") { on: Boolean -> CallCore.get(context).setCamera(on) }
|
|
63
|
-
Function("switchCamera") { CallCore.get(context).switchCamera() }
|
|
64
126
|
}
|
|
65
127
|
|
|
66
|
-
private val context get() =
|
|
128
|
+
private val context: Context? get() = appContext.reactContext
|
|
129
|
+
|
|
130
|
+
private fun core(): CallCore? = context?.let(CallCore::getOrNull)
|
|
131
|
+
|
|
132
|
+
private companion object {
|
|
133
|
+
const val TAG = "PoolseCalls"
|
|
134
|
+
}
|
|
67
135
|
}
|