@poolse/react-native-calls 0.1.0 → 0.1.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.
@@ -106,27 +106,97 @@ class CallCore private constructor(context: Context) {
106
106
 
107
107
  companion object {
108
108
  const val EXTRA_LAUNCH_CALL = "poolse_launch_call"
109
+ private const val PREFS = "dev.poolse.callcore"
109
110
 
110
111
  @Volatile private var instance: CallCore? = null
111
112
  @Volatile private var config: CallCoreConfig? = null
112
113
 
113
114
  /**
114
- * Host apps call this once (the bridge does it on configure, or an
115
- * app can do it in Application.onCreate) before any ring can arrive.
115
+ * Configure the core and PERSIST the inputs. A ring can wake a killed
116
+ * app: the OS spawns the process cold to run [CallMessagingService],
117
+ * with no React context and no chance for the JS bridge to call this.
118
+ * Persisting lets [restoreConfig] rebuild the same config from disk on
119
+ * that cold start, so closed-app ringing works. The bridge calls this
120
+ * each time the app opens; the resource names (not the resolved ids)
121
+ * are stored, since ids are not stable across installs.
116
122
  */
117
- fun configure(config: CallCoreConfig) {
118
- this.config = config
123
+ fun configure(context: Context, apiBaseUrl: String, smallIconName: String?, ringbackName: String?) {
124
+ val ctx = context.applicationContext
125
+ ctx.getSharedPreferences(PREFS, Context.MODE_PRIVATE).edit()
126
+ .putString("apiBaseUrl", apiBaseUrl)
127
+ .putString("smallIcon", smallIconName)
128
+ .putString("ringback", ringbackName)
129
+ .apply()
130
+ config = build(ctx, apiBaseUrl, smallIconName, ringbackName)
119
131
  }
120
132
 
121
133
  fun get(context: Context): CallCore =
122
134
  instance ?: synchronized(this) {
123
- instance ?: CallCore(context).also { instance = it }
135
+ instance ?: run {
136
+ if (config == null) restoreConfig(context.applicationContext)
137
+ CallCore(context).also { instance = it }
138
+ }
124
139
  }
125
140
 
126
141
  internal fun peekConfig(): CallCoreConfig? = config
127
142
 
128
143
  internal fun requireConfig(): CallCoreConfig =
129
144
  config ?: error("CallCore.configure() must be called before use")
145
+
146
+ // Rebuild config from the persisted inputs on a cold start. Leaves
147
+ // config null if the app was never configured — impossible in
148
+ // practice, since registering for pushes requires opening the app.
149
+ private fun restoreConfig(context: Context) {
150
+ val p = context.getSharedPreferences(PREFS, Context.MODE_PRIVATE)
151
+ val url = p.getString("apiBaseUrl", null) ?: return
152
+ config = build(context, url, p.getString("smallIcon", null), p.getString("ringback", null))
153
+ }
154
+
155
+ private fun build(
156
+ context: Context,
157
+ apiBaseUrl: String,
158
+ smallIconName: String?,
159
+ ringbackName: String?,
160
+ ): CallCoreConfig {
161
+ val iconId = smallIconName
162
+ ?.let { context.resources.getIdentifier(it, "drawable", context.packageName) } ?: 0
163
+ val ringbackId = ringbackName
164
+ ?.let { context.resources.getIdentifier(it, "raw", context.packageName) } ?: 0
165
+ return CallCoreConfig(
166
+ apiBaseUrl = apiBaseUrl.trimEnd('/'),
167
+ // Fall back to the app's launcher icon rather than crash on a typo.
168
+ smallIcon = if (iconId != 0) iconId else context.applicationInfo.icon,
169
+ ringbackRawRes = ringbackId,
170
+ mediaFactory = resolveMediaFactory(),
171
+ )
172
+ }
173
+
174
+ /**
175
+ * The shipped LiveKit adapter is loaded reflectively so the core
176
+ * library carries no static dependency on the SFU source set. Absent
177
+ * (LiveKit not built in), calls still ring and signal; there is just
178
+ * no media — a supported degraded mode, not a crash.
179
+ */
180
+ @Suppress("UNCHECKED_CAST")
181
+ private fun resolveMediaFactory(): (Context, MediaEvents) -> MediaPort =
182
+ runCatching {
183
+ val companion = Class.forName("dev.poolse.callcore.livekit.LiveKitMediaPort\$Companion")
184
+ val livekit = Class.forName("dev.poolse.callcore.livekit.LiveKitMediaPort")
185
+ .getField("Companion").get(null)
186
+ companion.getMethod("getFactory").invoke(livekit) as (Context, MediaEvents) -> MediaPort
187
+ }.getOrElse {
188
+ { _, _ -> NoMediaPort }
189
+ }
190
+
191
+ /** Signalling-only fallback: the call rings and the state machine
192
+ * runs, there is simply no audio/video transport. */
193
+ private object NoMediaPort : MediaPort {
194
+ override fun join(bundle: dev.poolse.callcore.CallSessionBundle) {}
195
+ override fun leave() {}
196
+ override fun setMuted(muted: Boolean) {}
197
+ override fun setCamera(on: Boolean) {}
198
+ override fun switchCamera() {}
199
+ }
130
200
  }
131
201
  }
132
202
 
@@ -35,7 +35,14 @@ class PoolseCallsModule : Module() {
35
35
  }
36
36
 
37
37
  AsyncFunction("configure") { apiBaseUrl: String, options: Map<String, Any?>? ->
38
- CallCore.configure(buildConfig(apiBaseUrl, options))
38
+ // Resource NAMES go to the core, which resolves + persists them —
39
+ // so a killed app woken by a ring can rebuild this without JS.
40
+ CallCore.configure(
41
+ context,
42
+ apiBaseUrl,
43
+ options?.get("smallIcon") as? String,
44
+ options?.get("ringback") as? String,
45
+ )
39
46
  }
40
47
 
41
48
  Function("getCallState") {
@@ -57,53 +64,4 @@ class PoolseCallsModule : Module() {
57
64
  }
58
65
 
59
66
  private val context get() = requireNotNull(appContext.reactContext) { "no react context" }
60
-
61
- private fun buildConfig(apiBaseUrl: String, options: Map<String, Any?>?) =
62
- dev.poolse.callcore.android.CallCoreConfig(
63
- apiBaseUrl = apiBaseUrl.trimEnd('/'),
64
- smallIcon = resolveDrawable(options?.get("smallIcon") as? String),
65
- ringbackRawRes = resolveRaw(options?.get("ringback") as? String),
66
- mediaFactory = resolveMediaFactory(),
67
- )
68
-
69
- private fun resolveDrawable(name: String?): Int {
70
- val ctx = context
71
- val id = name?.let { ctx.resources.getIdentifier(it, "drawable", ctx.packageName) } ?: 0
72
- // Fall back to the app's launcher icon rather than crash on a typo.
73
- return if (id != 0) id else ctx.applicationInfo.icon
74
- }
75
-
76
- private fun resolveRaw(name: String?): Int {
77
- val ctx = context
78
- return name?.let { ctx.resources.getIdentifier(it, "raw", ctx.packageName) } ?: 0
79
- }
80
-
81
- /**
82
- * The shipped LiveKit adapter is loaded reflectively so the core
83
- * library does not statically depend on the SFU source set. Absent
84
- * (LiveKit not built in), calls still ring and signal; there is just
85
- * no media — a supported degraded mode, not a crash.
86
- */
87
- @Suppress("UNCHECKED_CAST")
88
- private fun resolveMediaFactory(): (android.content.Context, dev.poolse.callcore.android.MediaEvents) -> dev.poolse.callcore.MediaPort {
89
- return runCatching {
90
- val companion = Class.forName("dev.poolse.callcore.livekit.LiveKitMediaPort\$Companion")
91
- val instance = Class.forName("dev.poolse.callcore.livekit.LiveKitMediaPort")
92
- .getField("Companion").get(null)
93
- val getter = companion.getMethod("getFactory")
94
- getter.invoke(instance) as (android.content.Context, dev.poolse.callcore.android.MediaEvents) -> dev.poolse.callcore.MediaPort
95
- }.getOrElse {
96
- { _, _ -> NoMediaPort }
97
- }
98
- }
99
-
100
- /** Signalling-only fallback: the call rings and the state machine
101
- * runs, there is simply no audio/video transport. */
102
- private object NoMediaPort : dev.poolse.callcore.MediaPort {
103
- override fun join(bundle: dev.poolse.callcore.CallSessionBundle) {}
104
- override fun leave() {}
105
- override fun setMuted(muted: Boolean) {}
106
- override fun setCamera(on: Boolean) {}
107
- override fun switchCamera() {}
108
- }
109
67
  }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@poolse/react-native-calls",
3
- "version": "0.1.0",
3
+ "version": "0.1.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",