@voltkit-rtc/react-native 0.0.0-stage → 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.
Files changed (225) hide show
  1. package/CHANGELOG.md +121 -0
  2. package/LICENSE +21 -0
  3. package/README.md +1608 -2
  4. package/android/build.gradle +70 -0
  5. package/android/consumer-rules.pro +21 -0
  6. package/android/src/main/AndroidManifest.xml +25 -0
  7. package/android/src/main/java/com/voltkit/rtc/reactnative/AppVisibility.kt +67 -0
  8. package/android/src/main/java/com/voltkit/rtc/reactnative/AudioDevices.kt +67 -0
  9. package/android/src/main/java/com/voltkit/rtc/reactnative/AudioRouter.kt +487 -0
  10. package/android/src/main/java/com/voltkit/rtc/reactnative/CallServiceTypes.kt +120 -0
  11. package/android/src/main/java/com/voltkit/rtc/reactnative/NativeTimers.kt +106 -0
  12. package/android/src/main/java/com/voltkit/rtc/reactnative/NetworkWatch.kt +120 -0
  13. package/android/src/main/java/com/voltkit/rtc/reactnative/OutputChoice.kt +65 -0
  14. package/android/src/main/java/com/voltkit/rtc/reactnative/RtcConfig.kt +83 -0
  15. package/android/src/main/java/com/voltkit/rtc/reactnative/ScoLink.kt +61 -0
  16. package/android/src/main/java/com/voltkit/rtc/reactnative/VoltkitCallService.kt +287 -0
  17. package/android/src/main/java/com/voltkit/rtc/reactnative/VoltkitRtcInitProvider.kt +215 -0
  18. package/android/src/main/java/com/voltkit/rtc/reactnative/VoltkitRtcModule.kt +313 -0
  19. package/android/src/main/java/com/voltkit/rtc/reactnative/VoltkitRtcPackage.kt +30 -0
  20. package/android/src/main/res/drawable/voltkit_rtc_default_call_icon.xml +15 -0
  21. package/android/src/main/res/raw/voltkit_rtc_keep.xml +7 -0
  22. package/app.plugin.js +17 -0
  23. package/lib/commonjs/audio.js +215 -0
  24. package/lib/commonjs/audio.js.map +1 -0
  25. package/lib/commonjs/components.js +207 -0
  26. package/lib/commonjs/components.js.map +1 -0
  27. package/lib/commonjs/create-client.js +47 -0
  28. package/lib/commonjs/create-client.js.map +1 -0
  29. package/lib/commonjs/errors.js +99 -0
  30. package/lib/commonjs/errors.js.map +1 -0
  31. package/lib/commonjs/globals.js +61 -0
  32. package/lib/commonjs/globals.js.map +1 -0
  33. package/lib/commonjs/index.js +234 -0
  34. package/lib/commonjs/index.js.map +1 -0
  35. package/lib/commonjs/local-media.js +392 -0
  36. package/lib/commonjs/local-media.js.map +1 -0
  37. package/lib/commonjs/native-session.js +365 -0
  38. package/lib/commonjs/native-session.js.map +1 -0
  39. package/lib/commonjs/native.js +99 -0
  40. package/lib/commonjs/native.js.map +1 -0
  41. package/lib/commonjs/package.json +1 -0
  42. package/lib/commonjs/permissions.js +166 -0
  43. package/lib/commonjs/permissions.js.map +1 -0
  44. package/lib/commonjs/platform/android.js +126 -0
  45. package/lib/commonjs/platform/android.js.map +1 -0
  46. package/lib/commonjs/platform/common.js +138 -0
  47. package/lib/commonjs/platform/common.js.map +1 -0
  48. package/lib/commonjs/platform/devices.js +151 -0
  49. package/lib/commonjs/platform/devices.js.map +1 -0
  50. package/lib/commonjs/platform/index.js +35 -0
  51. package/lib/commonjs/platform/index.js.map +1 -0
  52. package/lib/commonjs/platform/ios.js +54 -0
  53. package/lib/commonjs/platform/ios.js.map +1 -0
  54. package/lib/commonjs/platform/streams.js +273 -0
  55. package/lib/commonjs/platform/streams.js.map +1 -0
  56. package/lib/commonjs/platform/timers.js +71 -0
  57. package/lib/commonjs/platform/timers.js.map +1 -0
  58. package/lib/commonjs/platform/visibility.js +60 -0
  59. package/lib/commonjs/platform/visibility.js.map +1 -0
  60. package/lib/commonjs/preflight.js +77 -0
  61. package/lib/commonjs/preflight.js.map +1 -0
  62. package/lib/commonjs/provider.js +75 -0
  63. package/lib/commonjs/provider.js.map +1 -0
  64. package/lib/commonjs/specs/NativeVoltkitRtc.js +29 -0
  65. package/lib/commonjs/specs/NativeVoltkitRtc.js.map +1 -0
  66. package/lib/module/audio.js +207 -0
  67. package/lib/module/audio.js.map +1 -0
  68. package/lib/module/components.js +200 -0
  69. package/lib/module/components.js.map +1 -0
  70. package/lib/module/create-client.js +42 -0
  71. package/lib/module/create-client.js.map +1 -0
  72. package/lib/module/errors.js +87 -0
  73. package/lib/module/errors.js.map +1 -0
  74. package/lib/module/globals.js +53 -0
  75. package/lib/module/globals.js.map +1 -0
  76. package/lib/module/index.js +33 -0
  77. package/lib/module/index.js.map +1 -0
  78. package/lib/module/local-media.js +387 -0
  79. package/lib/module/local-media.js.map +1 -0
  80. package/lib/module/native-session.js +360 -0
  81. package/lib/module/native-session.js.map +1 -0
  82. package/lib/module/native.js +87 -0
  83. package/lib/module/native.js.map +1 -0
  84. package/lib/module/package.json +1 -0
  85. package/lib/module/permissions.js +160 -0
  86. package/lib/module/permissions.js.map +1 -0
  87. package/lib/module/platform/android.js +122 -0
  88. package/lib/module/platform/android.js.map +1 -0
  89. package/lib/module/platform/common.js +128 -0
  90. package/lib/module/platform/common.js.map +1 -0
  91. package/lib/module/platform/devices.js +142 -0
  92. package/lib/module/platform/devices.js.map +1 -0
  93. package/lib/module/platform/index.js +24 -0
  94. package/lib/module/platform/index.js.map +1 -0
  95. package/lib/module/platform/ios.js +49 -0
  96. package/lib/module/platform/ios.js.map +1 -0
  97. package/lib/module/platform/streams.js +268 -0
  98. package/lib/module/platform/streams.js.map +1 -0
  99. package/lib/module/platform/timers.js +67 -0
  100. package/lib/module/platform/timers.js.map +1 -0
  101. package/lib/module/platform/visibility.js +54 -0
  102. package/lib/module/platform/visibility.js.map +1 -0
  103. package/lib/module/preflight.js +72 -0
  104. package/lib/module/preflight.js.map +1 -0
  105. package/lib/module/provider.js +70 -0
  106. package/lib/module/provider.js.map +1 -0
  107. package/lib/module/specs/NativeVoltkitRtc.js +37 -0
  108. package/lib/module/specs/NativeVoltkitRtc.js.map +1 -0
  109. package/lib/typescript/commonjs/audio.d.ts +58 -0
  110. package/lib/typescript/commonjs/audio.d.ts.map +1 -0
  111. package/lib/typescript/commonjs/components.d.ts +58 -0
  112. package/lib/typescript/commonjs/components.d.ts.map +1 -0
  113. package/lib/typescript/commonjs/create-client.d.ts +35 -0
  114. package/lib/typescript/commonjs/create-client.d.ts.map +1 -0
  115. package/lib/typescript/commonjs/errors.d.ts +34 -0
  116. package/lib/typescript/commonjs/errors.d.ts.map +1 -0
  117. package/lib/typescript/commonjs/globals.d.ts +16 -0
  118. package/lib/typescript/commonjs/globals.d.ts.map +1 -0
  119. package/lib/typescript/commonjs/index.d.ts +24 -0
  120. package/lib/typescript/commonjs/index.d.ts.map +1 -0
  121. package/lib/typescript/commonjs/local-media.d.ts +86 -0
  122. package/lib/typescript/commonjs/local-media.d.ts.map +1 -0
  123. package/lib/typescript/commonjs/native-session.d.ts +38 -0
  124. package/lib/typescript/commonjs/native-session.d.ts.map +1 -0
  125. package/lib/typescript/commonjs/native.d.ts +18 -0
  126. package/lib/typescript/commonjs/native.d.ts.map +1 -0
  127. package/lib/typescript/commonjs/package.json +1 -0
  128. package/lib/typescript/commonjs/permissions.d.ts +30 -0
  129. package/lib/typescript/commonjs/permissions.d.ts.map +1 -0
  130. package/lib/typescript/commonjs/platform/android.d.ts +4 -0
  131. package/lib/typescript/commonjs/platform/android.d.ts.map +1 -0
  132. package/lib/typescript/commonjs/platform/common.d.ts +101 -0
  133. package/lib/typescript/commonjs/platform/common.d.ts.map +1 -0
  134. package/lib/typescript/commonjs/platform/devices.d.ts +42 -0
  135. package/lib/typescript/commonjs/platform/devices.d.ts.map +1 -0
  136. package/lib/typescript/commonjs/platform/index.d.ts +6 -0
  137. package/lib/typescript/commonjs/platform/index.d.ts.map +1 -0
  138. package/lib/typescript/commonjs/platform/ios.d.ts +4 -0
  139. package/lib/typescript/commonjs/platform/ios.d.ts.map +1 -0
  140. package/lib/typescript/commonjs/platform/streams.d.ts +82 -0
  141. package/lib/typescript/commonjs/platform/streams.d.ts.map +1 -0
  142. package/lib/typescript/commonjs/platform/timers.d.ts +19 -0
  143. package/lib/typescript/commonjs/platform/timers.d.ts.map +1 -0
  144. package/lib/typescript/commonjs/platform/visibility.d.ts +11 -0
  145. package/lib/typescript/commonjs/platform/visibility.d.ts.map +1 -0
  146. package/lib/typescript/commonjs/preflight.d.ts +19 -0
  147. package/lib/typescript/commonjs/preflight.d.ts.map +1 -0
  148. package/lib/typescript/commonjs/provider.d.ts +35 -0
  149. package/lib/typescript/commonjs/provider.d.ts.map +1 -0
  150. package/lib/typescript/commonjs/specs/NativeVoltkitRtc.d.ts +157 -0
  151. package/lib/typescript/commonjs/specs/NativeVoltkitRtc.d.ts.map +1 -0
  152. package/lib/typescript/module/audio.d.ts +58 -0
  153. package/lib/typescript/module/audio.d.ts.map +1 -0
  154. package/lib/typescript/module/components.d.ts +58 -0
  155. package/lib/typescript/module/components.d.ts.map +1 -0
  156. package/lib/typescript/module/create-client.d.ts +35 -0
  157. package/lib/typescript/module/create-client.d.ts.map +1 -0
  158. package/lib/typescript/module/errors.d.ts +34 -0
  159. package/lib/typescript/module/errors.d.ts.map +1 -0
  160. package/lib/typescript/module/globals.d.ts +16 -0
  161. package/lib/typescript/module/globals.d.ts.map +1 -0
  162. package/lib/typescript/module/index.d.ts +24 -0
  163. package/lib/typescript/module/index.d.ts.map +1 -0
  164. package/lib/typescript/module/local-media.d.ts +86 -0
  165. package/lib/typescript/module/local-media.d.ts.map +1 -0
  166. package/lib/typescript/module/native-session.d.ts +38 -0
  167. package/lib/typescript/module/native-session.d.ts.map +1 -0
  168. package/lib/typescript/module/native.d.ts +18 -0
  169. package/lib/typescript/module/native.d.ts.map +1 -0
  170. package/lib/typescript/module/package.json +1 -0
  171. package/lib/typescript/module/permissions.d.ts +30 -0
  172. package/lib/typescript/module/permissions.d.ts.map +1 -0
  173. package/lib/typescript/module/platform/android.d.ts +4 -0
  174. package/lib/typescript/module/platform/android.d.ts.map +1 -0
  175. package/lib/typescript/module/platform/common.d.ts +101 -0
  176. package/lib/typescript/module/platform/common.d.ts.map +1 -0
  177. package/lib/typescript/module/platform/devices.d.ts +42 -0
  178. package/lib/typescript/module/platform/devices.d.ts.map +1 -0
  179. package/lib/typescript/module/platform/index.d.ts +6 -0
  180. package/lib/typescript/module/platform/index.d.ts.map +1 -0
  181. package/lib/typescript/module/platform/ios.d.ts +4 -0
  182. package/lib/typescript/module/platform/ios.d.ts.map +1 -0
  183. package/lib/typescript/module/platform/streams.d.ts +82 -0
  184. package/lib/typescript/module/platform/streams.d.ts.map +1 -0
  185. package/lib/typescript/module/platform/timers.d.ts +19 -0
  186. package/lib/typescript/module/platform/timers.d.ts.map +1 -0
  187. package/lib/typescript/module/platform/visibility.d.ts +11 -0
  188. package/lib/typescript/module/platform/visibility.d.ts.map +1 -0
  189. package/lib/typescript/module/preflight.d.ts +19 -0
  190. package/lib/typescript/module/preflight.d.ts.map +1 -0
  191. package/lib/typescript/module/provider.d.ts +35 -0
  192. package/lib/typescript/module/provider.d.ts.map +1 -0
  193. package/lib/typescript/module/specs/NativeVoltkitRtc.d.ts +157 -0
  194. package/lib/typescript/module/specs/NativeVoltkitRtc.d.ts.map +1 -0
  195. package/package.json +140 -4
  196. package/plugin/build/android.d.ts +36 -0
  197. package/plugin/build/android.js +265 -0
  198. package/plugin/build/index.d.ts +9 -0
  199. package/plugin/build/index.js +54 -0
  200. package/plugin/build/ios.d.ts +4 -0
  201. package/plugin/build/ios.js +43 -0
  202. package/plugin/build/options.d.ts +93 -0
  203. package/plugin/build/options.js +140 -0
  204. package/react-native.config.js +11 -0
  205. package/src/audio.ts +221 -0
  206. package/src/components.tsx +233 -0
  207. package/src/create-client.ts +53 -0
  208. package/src/errors.ts +81 -0
  209. package/src/globals.ts +54 -0
  210. package/src/index.ts +95 -0
  211. package/src/local-media.ts +420 -0
  212. package/src/native-session.tsx +384 -0
  213. package/src/native.ts +84 -0
  214. package/src/permissions.ts +173 -0
  215. package/src/platform/android.ts +128 -0
  216. package/src/platform/common.ts +157 -0
  217. package/src/platform/devices.ts +148 -0
  218. package/src/platform/index.ts +22 -0
  219. package/src/platform/ios.ts +48 -0
  220. package/src/platform/streams.ts +285 -0
  221. package/src/platform/timers.ts +72 -0
  222. package/src/platform/visibility.ts +59 -0
  223. package/src/preflight.ts +60 -0
  224. package/src/provider.tsx +88 -0
  225. package/src/specs/NativeVoltkitRtc.ts +180 -0
@@ -0,0 +1,287 @@
1
+ package com.voltkit.rtc.reactnative
2
+
3
+ import android.app.NotificationChannel
4
+ import android.app.NotificationManager
5
+ import android.app.PendingIntent
6
+ import android.app.Service
7
+ import android.content.ComponentName
8
+ import android.content.Context
9
+ import android.content.Intent
10
+ import android.content.pm.PackageManager
11
+ import android.os.Build
12
+ import android.os.Handler
13
+ import android.os.IBinder
14
+ import android.os.Looper
15
+ import android.util.Log
16
+ import androidx.core.app.NotificationCompat
17
+ import androidx.core.app.ServiceCompat
18
+ import java.util.concurrent.CopyOnWriteArraySet
19
+
20
+ /**
21
+ * The call service (plan §5.4): a plain foreground service that keeps a call's process, network and microphone alive
22
+ * while the app is in the background or the screen is off. Without one Android destroys the app's sockets about 6 s
23
+ * after it leaves the foreground or the screen goes off (Phase 0 finding 3), and Android 17 blocks background audio
24
+ * for apps with neither a visible activity nor a foreground service. JavaScript keeps working on native timers; this
25
+ * is not a Headless JS task.
26
+ *
27
+ * It is not in the library's manifest, so an app that never runs background calls takes on no Play declaration. The
28
+ * app declares it (the Expo plugin's callService option, or the manifest block in the docs):
29
+ *
30
+ * <service android:name="com.voltkit.rtc.reactnative.VoltkitCallService" android:exported="false"
31
+ * android:foregroundServiceType="microphone|mediaPlayback" />
32
+ *
33
+ * It is started with startService while the app is visible and promoted to the foreground at once: a refusal can then
34
+ * be reported, where a startForegroundService that never reaches startForeground crashes the app. Its types come from
35
+ * [CallServiceTypes], and are only ever widened while the app is visible. The notification opens the app and has a
36
+ * Leave action; swiping the app away ends the call (JavaScript closes the client with code 1000 on `taskRemoved`).
37
+ */
38
+ class VoltkitCallService : Service() {
39
+ private val main = Handler(Looper.getMainLooper())
40
+ private var foreground = false
41
+ private var startedAt = 0L
42
+
43
+ /** Into the foreground with [Request]'s notification and these types; the JVM tests stand in for it (no notifications there). */
44
+ internal var promote: (Request, Int) -> Unit = { next, types ->
45
+ ServiceCompat.startForeground(this, NOTIFICATION_ID, notification(next), types)
46
+ }
47
+
48
+ override fun onBind(intent: Intent?): IBinder? = null
49
+
50
+ override fun onStartCommand(intent: Intent?, flags: Int, startId: Int): Int {
51
+ when (intent?.action) {
52
+ ACTION_LEAVE -> {
53
+ // JavaScript leaves the call and stops the service; with no JavaScript listening, end it here.
54
+ if (listeners.isEmpty()) stopNow() else tell(EVENT_LEAVE, runningTypes, "")
55
+ }
56
+ else -> {
57
+ val next = takePending()
58
+ when {
59
+ next != null -> runWith(next.request, next.done)
60
+ // Started by something else (a stale intent): stay as it is, or go if it never got going.
61
+ !foreground -> stopSelf(startId)
62
+ }
63
+ }
64
+ }
65
+ return START_NOT_STICKY
66
+ }
67
+
68
+ /** Show [next] and run with its types; [done] hears the types it runs with, or why it could not. On the main thread. */
69
+ internal fun runWith(next: Request, done: (Result<Int>) -> Unit) {
70
+ val first = !foreground
71
+ if (first) startedAt = System.currentTimeMillis()
72
+ var types = next.types
73
+ var error: Throwable? = null
74
+ while (true) {
75
+ try {
76
+ promote(next, types)
77
+ error = null
78
+ break
79
+ } catch (t: Throwable) {
80
+ error = t
81
+ // The share's consent can be gone (Android 14): keep the call, without the screen-share type.
82
+ if (types and CallServiceTypes.MEDIA_PROJECTION != 0) {
83
+ Log.w(TAG, "the call service could not take the mediaProjection type (${t.message}); going on without it")
84
+ types = types and CallServiceTypes.MEDIA_PROJECTION.inv()
85
+ if (types != 0 || Build.VERSION.SDK_INT < 34) continue
86
+ }
87
+ break
88
+ }
89
+ }
90
+ val failure = error
91
+ if (failure != null) {
92
+ val message = "Android refused the call service: ${failure.javaClass.simpleName}: ${failure.message}"
93
+ Log.e(TAG, message)
94
+ if (first) {
95
+ // Never reached the foreground: started with startService, so stopping is safe.
96
+ stopSelf()
97
+ }
98
+ tell(EVENT_FAILED, if (first) 0 else runningTypes, message)
99
+ done(Result.failure(IllegalStateException(message, failure)))
100
+ return
101
+ }
102
+ foreground = true
103
+ running = this
104
+ runningTypes = types
105
+ tell(if (first) EVENT_STARTED else EVENT_UPDATED, types, "")
106
+ done(Result.success(types))
107
+ }
108
+
109
+ /**
110
+ * Out of the foreground and gone. It stops being the call's service now, not at onDestroy, which Android calls
111
+ * later: until then the status would still say running, and a start would ride this dying instance (onDestroy then
112
+ * taking the new call's service down). A start from here on goes through startService and gets a fresh instance.
113
+ */
114
+ internal fun stopNow() {
115
+ if (foreground) runCatching { ServiceCompat.stopForeground(this, ServiceCompat.STOP_FOREGROUND_REMOVE) }
116
+ foreground = false
117
+ if (running === this) {
118
+ running = null
119
+ runningTypes = 0
120
+ tell(EVENT_STOPPED, 0, "")
121
+ }
122
+ stopSelf()
123
+ }
124
+
125
+ override fun onTaskRemoved(rootIntent: Intent?) {
126
+ // The user swiped the app away: JavaScript ends the call (close code 1000) and stops the service. If it does
127
+ // not (its runtime is gone), the service goes on its own after a few seconds.
128
+ tell(EVENT_TASK_REMOVED, runningTypes, "")
129
+ main.postDelayed({ if (running === this) stopNow() }, TASK_REMOVED_GRACE_MILLIS)
130
+ super.onTaskRemoved(rootIntent)
131
+ }
132
+
133
+ override fun onDestroy() {
134
+ main.removeCallbacksAndMessages(null)
135
+ // stopNow already let go of it, unless it was stopped some other way (a stopService elsewhere in the app).
136
+ val was = running === this
137
+ if (was) {
138
+ running = null
139
+ runningTypes = 0
140
+ }
141
+ foreground = false
142
+ if (was) tell(EVENT_STOPPED, 0, "")
143
+ super.onDestroy()
144
+ }
145
+
146
+ private fun notification(next: Request): android.app.Notification {
147
+ val manager = getSystemService(Context.NOTIFICATION_SERVICE) as NotificationManager
148
+ if (Build.VERSION.SDK_INT >= 26 && manager.getNotificationChannel(CHANNEL_ID) == null) {
149
+ manager.createNotificationChannel(
150
+ NotificationChannel(CHANNEL_ID, CHANNEL_NAME, NotificationManager.IMPORTANCE_LOW).apply {
151
+ setShowBadge(false)
152
+ enableVibration(false)
153
+ setSound(null, null)
154
+ },
155
+ )
156
+ }
157
+ val immutable = PendingIntent.FLAG_IMMUTABLE
158
+ val open = packageManager.getLaunchIntentForPackage(packageName)?.let { launch ->
159
+ launch.addFlags(Intent.FLAG_ACTIVITY_NEW_TASK or Intent.FLAG_ACTIVITY_RESET_TASK_IF_NEEDED)
160
+ PendingIntent.getActivity(this, 0, launch, PendingIntent.FLAG_UPDATE_CURRENT or immutable)
161
+ }
162
+ val leave = PendingIntent.getService(
163
+ this,
164
+ 1,
165
+ Intent(this, VoltkitCallService::class.java).setAction(ACTION_LEAVE),
166
+ PendingIntent.FLAG_UPDATE_CURRENT or immutable,
167
+ )
168
+ return NotificationCompat.Builder(this, CHANNEL_ID)
169
+ .setContentTitle(next.title)
170
+ .setContentText(next.text)
171
+ .setSmallIcon(smallIcon())
172
+ .setOngoing(true)
173
+ .setOnlyAlertOnce(true)
174
+ .setCategory(NotificationCompat.CATEGORY_CALL)
175
+ .setPriority(NotificationCompat.PRIORITY_LOW)
176
+ .setVisibility(NotificationCompat.VISIBILITY_PUBLIC)
177
+ .setWhen(startedAt)
178
+ .setUsesChronometer(true)
179
+ .setForegroundServiceBehavior(NotificationCompat.FOREGROUND_SERVICE_IMMEDIATE)
180
+ .apply { if (open != null) setContentIntent(open) }
181
+ .addAction(0, next.leaveLabel, leave)
182
+ .build()
183
+ }
184
+
185
+ /**
186
+ * The app's own icon for calls if it ships one, else its general notification icon, else the SDK's white handset.
187
+ * Never the launcher icon: Android draws a small icon from its alpha channel only, so a launcher icon shows as a
188
+ * solid square, and an adaptive one can crash Android 8.0 ("Bad notification posted ... Couldn't create icon").
189
+ */
190
+ private fun smallIcon(): Int {
191
+ for (name in ICON_NAMES) {
192
+ @Suppress("DiscouragedApi")
193
+ val id = resources.getIdentifier(name, "drawable", packageName)
194
+ if (id != 0) return id
195
+ }
196
+ return R.drawable.voltkit_rtc_default_call_icon
197
+ }
198
+
199
+ /** What the notification shows and the types the service runs with. */
200
+ internal data class Request(val title: String, val text: String, val leaveLabel: String, val types: Int)
201
+
202
+ private class Pending(val request: Request, val done: (Result<Int>) -> Unit)
203
+
204
+ /** Told on the main thread: started, updated, stopped, leave, taskRemoved, failed. */
205
+ internal fun interface Listener {
206
+ fun onCallServiceEvent(type: String, types: Int, message: String)
207
+ }
208
+
209
+ internal companion object {
210
+ private const val TAG = "VoltKitRtc"
211
+ private const val NOTIFICATION_ID = 0x564b_5254 // "VKRT"
212
+ private const val CHANNEL_ID = "voltkit_rtc_call"
213
+ private const val CHANNEL_NAME = "Calls"
214
+ private const val ACTION_START = "com.voltkit.rtc.reactnative.action.START_CALL"
215
+ private const val ACTION_LEAVE = "com.voltkit.rtc.reactnative.action.LEAVE_CALL"
216
+ private const val TASK_REMOVED_GRACE_MILLIS = 5_000L
217
+ // The app's drawables, by name: res/raw/voltkit_rtc_keep.xml keeps them through resource shrinking.
218
+ private val ICON_NAMES = listOf("voltkit_call_notification_icon", "notification_icon", "ic_notification")
219
+
220
+ const val EVENT_STARTED = "started"
221
+ const val EVENT_UPDATED = "updated"
222
+ const val EVENT_STOPPED = "stopped"
223
+ const val EVENT_LEAVE = "leave"
224
+ const val EVENT_TASK_REMOVED = "taskRemoved"
225
+ const val EVENT_FAILED = "failed"
226
+
227
+ val listeners = CopyOnWriteArraySet<Listener>()
228
+
229
+ /** The service while it runs in the foreground (main thread). */
230
+ @Volatile
231
+ var running: VoltkitCallService? = null
232
+ private set
233
+
234
+ /** The types it runs with now; 0 when stopped. */
235
+ @Volatile
236
+ var runningTypes: Int = 0
237
+ private set
238
+
239
+ private var pending: Pending? = null
240
+
241
+ private fun takePending(): Pending? = synchronized(this) { pending.also { pending = null } }
242
+
243
+ private fun tell(type: String, types: Int, message: String) {
244
+ listeners.forEach { runCatching { it.onCallServiceEvent(type, types, message) } }
245
+ }
246
+
247
+ /** The types the app's manifest declares for the service, or null when it does not declare the service. */
248
+ fun declaredTypes(context: Context): Int? = try {
249
+ @Suppress("DEPRECATION")
250
+ val info = context.packageManager.getServiceInfo(ComponentName(context, VoltkitCallService::class.java), 0)
251
+ if (Build.VERSION.SDK_INT >= 29) info.foregroundServiceType else 0
252
+ } catch (_: PackageManager.NameNotFoundException) {
253
+ null
254
+ }
255
+
256
+ /** Start it (main thread): [done] hears the types it runs with, or why it could not start. */
257
+ fun start(context: Context, request: Request, done: (Result<Int>) -> Unit) {
258
+ running?.let { return it.runWith(request, done) }
259
+ val previous = synchronized(this) { pending.also { pending = Pending(request, done) } }
260
+ previous?.done?.invoke(Result.failure(IllegalStateException("replaced by a newer start")))
261
+ val started = try {
262
+ context.startService(Intent(context, VoltkitCallService::class.java).setAction(ACTION_START))
263
+ } catch (t: Throwable) {
264
+ // IllegalStateException: the app is in the background (Android 8+).
265
+ takePending()
266
+ return done(Result.failure(IllegalStateException("Android refused to start the call service: ${t.message}", t)))
267
+ }
268
+ if (started == null) {
269
+ takePending()
270
+ done(Result.failure(IllegalStateException("the app's manifest does not declare VoltkitCallService")))
271
+ }
272
+ }
273
+
274
+ /** New text or types for the running service (main thread); nothing when it is not running. */
275
+ fun update(request: Request, done: (Result<Int>) -> Unit) {
276
+ val service = running ?: return done(Result.success(0))
277
+ service.runWith(request, done)
278
+ }
279
+
280
+ /** Stop it, and any start still on its way (main thread). */
281
+ fun stop(context: Context) {
282
+ takePending()?.done?.invoke(Result.failure(IllegalStateException("stopped before it started")))
283
+ running?.stopNow()
284
+ runCatching { context.stopService(Intent(context, VoltkitCallService::class.java)) }
285
+ }
286
+ }
287
+ }
@@ -0,0 +1,215 @@
1
+ package com.voltkit.rtc.reactnative
2
+
3
+ import android.content.ContentProvider
4
+ import android.content.ContentValues
5
+ import android.content.Context
6
+ import android.content.pm.PackageManager
7
+ import android.database.Cursor
8
+ import android.media.AudioAttributes
9
+ import android.media.AudioDeviceInfo
10
+ import android.net.Uri
11
+ import android.util.Log
12
+ import com.oney.WebRTCModule.WebRTCModuleOptions
13
+ import org.webrtc.audio.JavaAudioDeviceModule
14
+ import java.lang.reflect.Field
15
+ import java.lang.reflect.Method
16
+ import java.util.concurrent.CopyOnWriteArraySet
17
+
18
+ /**
19
+ * Runs once when the app's process starts, before Application.onCreate and long before react-native-webrtc's
20
+ * WebRTCModule exists (plan §5.3): it reads the meta-data ([RtcConfig]), gives react-native-webrtc the audio device
21
+ * module for the audio mode (WebRTCModuleOptions.audioDeviceModule), and turns on react-native-webrtc's screen-share
22
+ * service when the app declares screen sharing. So apps edit neither MainApplication nor MainActivity. Phase 0 S7
23
+ * proved a module made here is the one react-native-webrtc plays and records with.
24
+ *
25
+ * It must not touch ProcessLifecycleOwner while being created: providers run before androidx.startup's
26
+ * InitializationProvider, which starts the process lifecycle (Phase 0 S8). [AppVisibility.install] posts its
27
+ * registration to the main looper, which runs after every provider and Application.onCreate.
28
+ *
29
+ * An app that sets WebRTCModuleOptions.audioDeviceModule itself in MainApplication.onCreate replaces this module
30
+ * (MainApplication runs after providers): the audio mode and echo cancellation are then the app's, and the constant
31
+ * audioDeviceModule is false. selectAudioInput keeps working when the app's module is a JavaAudioDeviceModule; with any
32
+ * other module, or none, it answers false.
33
+ */
34
+ class VoltkitRtcInitProvider : ContentProvider() {
35
+ override fun onCreate(): Boolean {
36
+ val context = context?.applicationContext ?: return true
37
+ try {
38
+ install(context)
39
+ } catch (t: Throwable) {
40
+ // Never stop the app from starting: react-native-webrtc then makes its own audio device module.
41
+ Log.e(TAG, "VoltKit RTC could not set up react-native-webrtc's audio: ${t.message}", t)
42
+ }
43
+ AppVisibility.install()
44
+ return true
45
+ }
46
+
47
+ private fun install(context: Context) {
48
+ val config = configOf(context)
49
+ config.problems.forEach { Log.w(TAG, "manifest meta-data: $it") }
50
+ val options = WebRTCModuleOptions.getInstance()
51
+ if (config.screenShare) options.enableMediaProjectionService = true
52
+ if (options.audioDeviceModule != null) {
53
+ Log.i(TAG, "react-native-webrtc already has an audio device module; keeping it")
54
+ return
55
+ }
56
+ val adm = createAudioDeviceModule(context, config)
57
+ options.audioDeviceModule = adm
58
+ audioDeviceModule = adm
59
+ }
60
+
61
+ override fun query(uri: Uri, projection: Array<out String>?, selection: String?, selectionArgs: Array<out String>?, sortOrder: String?): Cursor? = null
62
+
63
+ override fun getType(uri: Uri): String? = null
64
+
65
+ override fun insert(uri: Uri, values: ContentValues?): Uri? = null
66
+
67
+ override fun delete(uri: Uri, selection: String?, selectionArgs: Array<out String>?): Int = 0
68
+
69
+ override fun update(uri: Uri, values: ContentValues?, selection: String?, selectionArgs: Array<out String>?): Int = 0
70
+
71
+ internal companion object {
72
+ private const val TAG = "VoltKitRtc"
73
+
74
+ @Volatile
75
+ private var config: RtcConfig? = null
76
+
77
+ /**
78
+ * The module this provider gave react-native-webrtc; null when it did not run or found one already set. The app
79
+ * can still replace it in MainApplication: WebRTC uses it only while it is WebRTCModuleOptions.audioDeviceModule.
80
+ */
81
+ @Volatile
82
+ var audioDeviceModule: JavaAudioDeviceModule? = null
83
+ private set
84
+
85
+ /** Microphone failures the audio device module reports, for every client to report (mic-interrupted). */
86
+ val microphoneErrors = CopyOnWriteArraySet<(String) -> Unit>()
87
+
88
+ /** The process's settings from the manifest, read once (by the provider, or by the module when it did not run). */
89
+ fun configOf(context: Context): RtcConfig = config ?: synchronized(this) {
90
+ config ?: readConfig(context).also { config = it }
91
+ }
92
+
93
+ private fun readConfig(context: Context): RtcConfig {
94
+ val meta = try {
95
+ @Suppress("DEPRECATION")
96
+ context.packageManager.getApplicationInfo(context.packageName, PackageManager.GET_META_DATA).metaData
97
+ } catch (t: Throwable) {
98
+ Log.w(TAG, "could not read the app's meta-data: ${t.message}")
99
+ null
100
+ }
101
+ @Suppress("DEPRECATION")
102
+ return RtcConfig.from { key -> meta?.get(key) }
103
+ }
104
+
105
+ /**
106
+ * react-native-webrtc's audio device module for [config]: call audio (USAGE_VOICE_COMMUNICATION) or media
107
+ * playback (USAGE_MEDIA), the handset's echo canceller and noise suppressor where it has them, as the Android
108
+ * SDK's VoltKitRtc.create. The native module behind it is made only when WebRTC starts.
109
+ */
110
+ private fun createAudioDeviceModule(context: Context, config: RtcConfig): JavaAudioDeviceModule {
111
+ val playback = if (config.audioMode == AudioMode.communication) {
112
+ AudioAttributes.Builder()
113
+ .setUsage(AudioAttributes.USAGE_VOICE_COMMUNICATION)
114
+ .setContentType(AudioAttributes.CONTENT_TYPE_SPEECH)
115
+ .build()
116
+ } else {
117
+ AudioAttributes.Builder()
118
+ .setUsage(AudioAttributes.USAGE_MEDIA)
119
+ .setContentType(AudioAttributes.CONTENT_TYPE_MUSIC)
120
+ .build()
121
+ }
122
+ fun micError(message: String) {
123
+ Log.e(TAG, message)
124
+ microphoneErrors.forEach { runCatching { it(message) } }
125
+ }
126
+ return JavaAudioDeviceModule.builder(context)
127
+ .setAudioAttributes(playback)
128
+ .setUseHardwareAcousticEchoCanceler(
129
+ config.hardwareEchoCancellation && JavaAudioDeviceModule.isBuiltInAcousticEchoCancelerSupported(),
130
+ )
131
+ .setUseHardwareNoiseSuppressor(
132
+ config.hardwareEchoCancellation && JavaAudioDeviceModule.isBuiltInNoiseSuppressorSupported(),
133
+ )
134
+ .setEnableVolumeLogger(false)
135
+ .setAudioRecordErrorCallback(object : JavaAudioDeviceModule.AudioRecordErrorCallback {
136
+ override fun onWebRtcAudioRecordInitError(errorMessage: String) =
137
+ micError("microphone init failed: $errorMessage")
138
+
139
+ override fun onWebRtcAudioRecordStartError(
140
+ errorCode: JavaAudioDeviceModule.AudioRecordStartErrorCode,
141
+ errorMessage: String,
142
+ ) = micError("microphone start failed ($errorCode): $errorMessage")
143
+
144
+ override fun onWebRtcAudioRecordError(errorMessage: String) = micError("microphone error: $errorMessage")
145
+ })
146
+ .setAudioTrackErrorCallback(object : JavaAudioDeviceModule.AudioTrackErrorCallback {
147
+ override fun onWebRtcAudioTrackInitError(errorMessage: String) {
148
+ Log.e(TAG, "audio playback init failed: $errorMessage")
149
+ }
150
+
151
+ override fun onWebRtcAudioTrackStartError(
152
+ errorCode: JavaAudioDeviceModule.AudioTrackStartErrorCode,
153
+ errorMessage: String,
154
+ ) {
155
+ Log.e(TAG, "audio playback start failed ($errorCode): $errorMessage")
156
+ }
157
+
158
+ override fun onWebRtcAudioTrackError(errorMessage: String) {
159
+ Log.e(TAG, "audio playback error: $errorMessage")
160
+ }
161
+ })
162
+ .createAudioDeviceModule()
163
+ }
164
+ }
165
+ }
166
+
167
+ /**
168
+ * The microphone react-native-webrtc's audio device module records from (`selectAudioInput`).
169
+ *
170
+ * Going back to the default (null) cannot go through JavaAudioDeviceModule.setPreferredInputDevice in jitsi's WebRTC
171
+ * 155: it logs `preferredInputDevice.getId()` first, so null throws a NullPointerException (found on the API 37
172
+ * emulator, 2026-10-25). Its WebRtcAudioRecord.setPreferredDevice takes null, and is reached through the module's
173
+ * `audioInput` field (react-native-webrtc's rules keep org.webrtc's names; consumer-rules.pro keeps these two too).
174
+ */
175
+ internal object PreferredInput {
176
+ private const val TAG = "VoltKitRtc"
177
+
178
+ /** Record from [info]; false when the module refused it. */
179
+ fun choose(adm: JavaAudioDeviceModule, info: AudioDeviceInfo): Boolean = try {
180
+ adm.setPreferredInputDevice(info)
181
+ true
182
+ } catch (t: Throwable) {
183
+ Log.w(TAG, "could not choose the microphone ${info.id}: ${t.message}")
184
+ false
185
+ }
186
+
187
+ /** Back to the system's choice of microphone (a headset's when one is connected). */
188
+ fun clear(adm: JavaAudioDeviceModule): Boolean {
189
+ try {
190
+ adm.setPreferredInputDevice(null)
191
+ return true
192
+ } catch (_: NullPointerException) {
193
+ // jitsi's WebRTC 155: see above.
194
+ } catch (t: Throwable) {
195
+ Log.w(TAG, "could not go back to the default microphone: ${t.message}")
196
+ return false
197
+ }
198
+ return try {
199
+ val recorder = recorderField().get(adm) ?: return false
200
+ setterOf(recorder.javaClass).invoke(recorder, null as AudioDeviceInfo?)
201
+ true
202
+ } catch (t: Throwable) {
203
+ Log.w(TAG, "could not go back to the default microphone: ${t.message}")
204
+ false
205
+ }
206
+ }
207
+
208
+ /** JavaAudioDeviceModule's recorder. */
209
+ fun recorderField(): Field =
210
+ JavaAudioDeviceModule::class.java.getDeclaredField("audioInput").apply { isAccessible = true }
211
+
212
+ /** The recorder's setPreferredDevice(AudioDeviceInfo), which takes null. */
213
+ fun setterOf(recorder: Class<*>): Method =
214
+ recorder.getDeclaredMethod("setPreferredDevice", AudioDeviceInfo::class.java).apply { isAccessible = true }
215
+ }