@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.
- package/CHANGELOG.md +121 -0
- package/LICENSE +21 -0
- package/README.md +1608 -2
- package/android/build.gradle +70 -0
- package/android/consumer-rules.pro +21 -0
- package/android/src/main/AndroidManifest.xml +25 -0
- package/android/src/main/java/com/voltkit/rtc/reactnative/AppVisibility.kt +67 -0
- package/android/src/main/java/com/voltkit/rtc/reactnative/AudioDevices.kt +67 -0
- package/android/src/main/java/com/voltkit/rtc/reactnative/AudioRouter.kt +487 -0
- package/android/src/main/java/com/voltkit/rtc/reactnative/CallServiceTypes.kt +120 -0
- package/android/src/main/java/com/voltkit/rtc/reactnative/NativeTimers.kt +106 -0
- package/android/src/main/java/com/voltkit/rtc/reactnative/NetworkWatch.kt +120 -0
- package/android/src/main/java/com/voltkit/rtc/reactnative/OutputChoice.kt +65 -0
- package/android/src/main/java/com/voltkit/rtc/reactnative/RtcConfig.kt +83 -0
- package/android/src/main/java/com/voltkit/rtc/reactnative/ScoLink.kt +61 -0
- package/android/src/main/java/com/voltkit/rtc/reactnative/VoltkitCallService.kt +287 -0
- package/android/src/main/java/com/voltkit/rtc/reactnative/VoltkitRtcInitProvider.kt +215 -0
- package/android/src/main/java/com/voltkit/rtc/reactnative/VoltkitRtcModule.kt +313 -0
- package/android/src/main/java/com/voltkit/rtc/reactnative/VoltkitRtcPackage.kt +30 -0
- package/android/src/main/res/drawable/voltkit_rtc_default_call_icon.xml +15 -0
- package/android/src/main/res/raw/voltkit_rtc_keep.xml +7 -0
- package/app.plugin.js +17 -0
- package/lib/commonjs/audio.js +215 -0
- package/lib/commonjs/audio.js.map +1 -0
- package/lib/commonjs/components.js +207 -0
- package/lib/commonjs/components.js.map +1 -0
- package/lib/commonjs/create-client.js +47 -0
- package/lib/commonjs/create-client.js.map +1 -0
- package/lib/commonjs/errors.js +99 -0
- package/lib/commonjs/errors.js.map +1 -0
- package/lib/commonjs/globals.js +61 -0
- package/lib/commonjs/globals.js.map +1 -0
- package/lib/commonjs/index.js +234 -0
- package/lib/commonjs/index.js.map +1 -0
- package/lib/commonjs/local-media.js +392 -0
- package/lib/commonjs/local-media.js.map +1 -0
- package/lib/commonjs/native-session.js +365 -0
- package/lib/commonjs/native-session.js.map +1 -0
- package/lib/commonjs/native.js +99 -0
- package/lib/commonjs/native.js.map +1 -0
- package/lib/commonjs/package.json +1 -0
- package/lib/commonjs/permissions.js +166 -0
- package/lib/commonjs/permissions.js.map +1 -0
- package/lib/commonjs/platform/android.js +126 -0
- package/lib/commonjs/platform/android.js.map +1 -0
- package/lib/commonjs/platform/common.js +138 -0
- package/lib/commonjs/platform/common.js.map +1 -0
- package/lib/commonjs/platform/devices.js +151 -0
- package/lib/commonjs/platform/devices.js.map +1 -0
- package/lib/commonjs/platform/index.js +35 -0
- package/lib/commonjs/platform/index.js.map +1 -0
- package/lib/commonjs/platform/ios.js +54 -0
- package/lib/commonjs/platform/ios.js.map +1 -0
- package/lib/commonjs/platform/streams.js +273 -0
- package/lib/commonjs/platform/streams.js.map +1 -0
- package/lib/commonjs/platform/timers.js +71 -0
- package/lib/commonjs/platform/timers.js.map +1 -0
- package/lib/commonjs/platform/visibility.js +60 -0
- package/lib/commonjs/platform/visibility.js.map +1 -0
- package/lib/commonjs/preflight.js +77 -0
- package/lib/commonjs/preflight.js.map +1 -0
- package/lib/commonjs/provider.js +75 -0
- package/lib/commonjs/provider.js.map +1 -0
- package/lib/commonjs/specs/NativeVoltkitRtc.js +29 -0
- package/lib/commonjs/specs/NativeVoltkitRtc.js.map +1 -0
- package/lib/module/audio.js +207 -0
- package/lib/module/audio.js.map +1 -0
- package/lib/module/components.js +200 -0
- package/lib/module/components.js.map +1 -0
- package/lib/module/create-client.js +42 -0
- package/lib/module/create-client.js.map +1 -0
- package/lib/module/errors.js +87 -0
- package/lib/module/errors.js.map +1 -0
- package/lib/module/globals.js +53 -0
- package/lib/module/globals.js.map +1 -0
- package/lib/module/index.js +33 -0
- package/lib/module/index.js.map +1 -0
- package/lib/module/local-media.js +387 -0
- package/lib/module/local-media.js.map +1 -0
- package/lib/module/native-session.js +360 -0
- package/lib/module/native-session.js.map +1 -0
- package/lib/module/native.js +87 -0
- package/lib/module/native.js.map +1 -0
- package/lib/module/package.json +1 -0
- package/lib/module/permissions.js +160 -0
- package/lib/module/permissions.js.map +1 -0
- package/lib/module/platform/android.js +122 -0
- package/lib/module/platform/android.js.map +1 -0
- package/lib/module/platform/common.js +128 -0
- package/lib/module/platform/common.js.map +1 -0
- package/lib/module/platform/devices.js +142 -0
- package/lib/module/platform/devices.js.map +1 -0
- package/lib/module/platform/index.js +24 -0
- package/lib/module/platform/index.js.map +1 -0
- package/lib/module/platform/ios.js +49 -0
- package/lib/module/platform/ios.js.map +1 -0
- package/lib/module/platform/streams.js +268 -0
- package/lib/module/platform/streams.js.map +1 -0
- package/lib/module/platform/timers.js +67 -0
- package/lib/module/platform/timers.js.map +1 -0
- package/lib/module/platform/visibility.js +54 -0
- package/lib/module/platform/visibility.js.map +1 -0
- package/lib/module/preflight.js +72 -0
- package/lib/module/preflight.js.map +1 -0
- package/lib/module/provider.js +70 -0
- package/lib/module/provider.js.map +1 -0
- package/lib/module/specs/NativeVoltkitRtc.js +37 -0
- package/lib/module/specs/NativeVoltkitRtc.js.map +1 -0
- package/lib/typescript/commonjs/audio.d.ts +58 -0
- package/lib/typescript/commonjs/audio.d.ts.map +1 -0
- package/lib/typescript/commonjs/components.d.ts +58 -0
- package/lib/typescript/commonjs/components.d.ts.map +1 -0
- package/lib/typescript/commonjs/create-client.d.ts +35 -0
- package/lib/typescript/commonjs/create-client.d.ts.map +1 -0
- package/lib/typescript/commonjs/errors.d.ts +34 -0
- package/lib/typescript/commonjs/errors.d.ts.map +1 -0
- package/lib/typescript/commonjs/globals.d.ts +16 -0
- package/lib/typescript/commonjs/globals.d.ts.map +1 -0
- package/lib/typescript/commonjs/index.d.ts +24 -0
- package/lib/typescript/commonjs/index.d.ts.map +1 -0
- package/lib/typescript/commonjs/local-media.d.ts +86 -0
- package/lib/typescript/commonjs/local-media.d.ts.map +1 -0
- package/lib/typescript/commonjs/native-session.d.ts +38 -0
- package/lib/typescript/commonjs/native-session.d.ts.map +1 -0
- package/lib/typescript/commonjs/native.d.ts +18 -0
- package/lib/typescript/commonjs/native.d.ts.map +1 -0
- package/lib/typescript/commonjs/package.json +1 -0
- package/lib/typescript/commonjs/permissions.d.ts +30 -0
- package/lib/typescript/commonjs/permissions.d.ts.map +1 -0
- package/lib/typescript/commonjs/platform/android.d.ts +4 -0
- package/lib/typescript/commonjs/platform/android.d.ts.map +1 -0
- package/lib/typescript/commonjs/platform/common.d.ts +101 -0
- package/lib/typescript/commonjs/platform/common.d.ts.map +1 -0
- package/lib/typescript/commonjs/platform/devices.d.ts +42 -0
- package/lib/typescript/commonjs/platform/devices.d.ts.map +1 -0
- package/lib/typescript/commonjs/platform/index.d.ts +6 -0
- package/lib/typescript/commonjs/platform/index.d.ts.map +1 -0
- package/lib/typescript/commonjs/platform/ios.d.ts +4 -0
- package/lib/typescript/commonjs/platform/ios.d.ts.map +1 -0
- package/lib/typescript/commonjs/platform/streams.d.ts +82 -0
- package/lib/typescript/commonjs/platform/streams.d.ts.map +1 -0
- package/lib/typescript/commonjs/platform/timers.d.ts +19 -0
- package/lib/typescript/commonjs/platform/timers.d.ts.map +1 -0
- package/lib/typescript/commonjs/platform/visibility.d.ts +11 -0
- package/lib/typescript/commonjs/platform/visibility.d.ts.map +1 -0
- package/lib/typescript/commonjs/preflight.d.ts +19 -0
- package/lib/typescript/commonjs/preflight.d.ts.map +1 -0
- package/lib/typescript/commonjs/provider.d.ts +35 -0
- package/lib/typescript/commonjs/provider.d.ts.map +1 -0
- package/lib/typescript/commonjs/specs/NativeVoltkitRtc.d.ts +157 -0
- package/lib/typescript/commonjs/specs/NativeVoltkitRtc.d.ts.map +1 -0
- package/lib/typescript/module/audio.d.ts +58 -0
- package/lib/typescript/module/audio.d.ts.map +1 -0
- package/lib/typescript/module/components.d.ts +58 -0
- package/lib/typescript/module/components.d.ts.map +1 -0
- package/lib/typescript/module/create-client.d.ts +35 -0
- package/lib/typescript/module/create-client.d.ts.map +1 -0
- package/lib/typescript/module/errors.d.ts +34 -0
- package/lib/typescript/module/errors.d.ts.map +1 -0
- package/lib/typescript/module/globals.d.ts +16 -0
- package/lib/typescript/module/globals.d.ts.map +1 -0
- package/lib/typescript/module/index.d.ts +24 -0
- package/lib/typescript/module/index.d.ts.map +1 -0
- package/lib/typescript/module/local-media.d.ts +86 -0
- package/lib/typescript/module/local-media.d.ts.map +1 -0
- package/lib/typescript/module/native-session.d.ts +38 -0
- package/lib/typescript/module/native-session.d.ts.map +1 -0
- package/lib/typescript/module/native.d.ts +18 -0
- package/lib/typescript/module/native.d.ts.map +1 -0
- package/lib/typescript/module/package.json +1 -0
- package/lib/typescript/module/permissions.d.ts +30 -0
- package/lib/typescript/module/permissions.d.ts.map +1 -0
- package/lib/typescript/module/platform/android.d.ts +4 -0
- package/lib/typescript/module/platform/android.d.ts.map +1 -0
- package/lib/typescript/module/platform/common.d.ts +101 -0
- package/lib/typescript/module/platform/common.d.ts.map +1 -0
- package/lib/typescript/module/platform/devices.d.ts +42 -0
- package/lib/typescript/module/platform/devices.d.ts.map +1 -0
- package/lib/typescript/module/platform/index.d.ts +6 -0
- package/lib/typescript/module/platform/index.d.ts.map +1 -0
- package/lib/typescript/module/platform/ios.d.ts +4 -0
- package/lib/typescript/module/platform/ios.d.ts.map +1 -0
- package/lib/typescript/module/platform/streams.d.ts +82 -0
- package/lib/typescript/module/platform/streams.d.ts.map +1 -0
- package/lib/typescript/module/platform/timers.d.ts +19 -0
- package/lib/typescript/module/platform/timers.d.ts.map +1 -0
- package/lib/typescript/module/platform/visibility.d.ts +11 -0
- package/lib/typescript/module/platform/visibility.d.ts.map +1 -0
- package/lib/typescript/module/preflight.d.ts +19 -0
- package/lib/typescript/module/preflight.d.ts.map +1 -0
- package/lib/typescript/module/provider.d.ts +35 -0
- package/lib/typescript/module/provider.d.ts.map +1 -0
- package/lib/typescript/module/specs/NativeVoltkitRtc.d.ts +157 -0
- package/lib/typescript/module/specs/NativeVoltkitRtc.d.ts.map +1 -0
- package/package.json +140 -4
- package/plugin/build/android.d.ts +36 -0
- package/plugin/build/android.js +265 -0
- package/plugin/build/index.d.ts +9 -0
- package/plugin/build/index.js +54 -0
- package/plugin/build/ios.d.ts +4 -0
- package/plugin/build/ios.js +43 -0
- package/plugin/build/options.d.ts +93 -0
- package/plugin/build/options.js +140 -0
- package/react-native.config.js +11 -0
- package/src/audio.ts +221 -0
- package/src/components.tsx +233 -0
- package/src/create-client.ts +53 -0
- package/src/errors.ts +81 -0
- package/src/globals.ts +54 -0
- package/src/index.ts +95 -0
- package/src/local-media.ts +420 -0
- package/src/native-session.tsx +384 -0
- package/src/native.ts +84 -0
- package/src/permissions.ts +173 -0
- package/src/platform/android.ts +128 -0
- package/src/platform/common.ts +157 -0
- package/src/platform/devices.ts +148 -0
- package/src/platform/index.ts +22 -0
- package/src/platform/ios.ts +48 -0
- package/src/platform/streams.ts +285 -0
- package/src/platform/timers.ts +72 -0
- package/src/platform/visibility.ts +59 -0
- package/src/preflight.ts +60 -0
- package/src/provider.tsx +88 -0
- 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
|
+
}
|