@voipsdk/react-native-voipcloud 0.2.1 → 1.0.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/README.md +181 -117
- package/android/build.gradle +7 -7
- package/android/src/main/java/com/voipcloud/sdk/rn/VoipCloudModule.java +148 -204
- package/android/src/main/java/com/voipcloud/sdk/rn/internal/BridgeGuard.java +74 -0
- package/android/src/main/java/com/voipcloud/sdk/rn/internal/Events.java +7 -2
- package/android/src/main/libs/voipcloud-release.aar +0 -0
- package/ios/VoipCloud.mm +8 -8
- package/lib/commonjs/VoipCloud.js +29 -47
- package/lib/commonjs/VoipCloud.js.map +1 -1
- package/lib/commonjs/events.js +13 -7
- package/lib/commonjs/events.js.map +1 -1
- package/lib/commonjs/index.js +2 -3
- package/lib/commonjs/index.js.map +1 -1
- package/lib/commonjs/nativeModule.js +18 -0
- package/lib/commonjs/nativeModule.js.map +1 -0
- package/lib/module/VoipCloud.js +30 -47
- package/lib/module/VoipCloud.js.map +1 -1
- package/lib/module/events.js +9 -8
- package/lib/module/events.js.map +1 -1
- package/lib/module/index.js +1 -2
- package/lib/module/index.js.map +1 -1
- package/lib/module/nativeModule.js +13 -0
- package/lib/module/nativeModule.js.map +1 -0
- package/lib/typescript/VoipCloud.d.ts +5 -30
- package/lib/typescript/events.d.ts +6 -2
- package/lib/typescript/index.d.ts +2 -3
- package/lib/typescript/nativeModule.d.ts +7 -0
- package/lib/typescript/types.d.ts +1 -17
- package/package.json +5 -4
- package/react-native-voipcloud.podspec +7 -5
- package/src/VoipCloud.ts +37 -48
- package/src/events.ts +9 -8
- package/src/index.ts +1 -3
- package/src/nativeModule.ts +17 -0
- package/src/types.ts +1 -18
- package/lib/commonjs/License.js +0 -51
- package/lib/commonjs/License.js.map +0 -1
- package/lib/module/License.js +0 -45
- package/lib/module/License.js.map +0 -1
- package/lib/typescript/License.d.ts +0 -31
- package/src/License.ts +0 -52
|
@@ -1,6 +1,5 @@
|
|
|
1
1
|
package com.voipcloud.sdk.rn;
|
|
2
2
|
|
|
3
|
-
import android.provider.Settings;
|
|
4
3
|
import androidx.annotation.NonNull;
|
|
5
4
|
|
|
6
5
|
import com.facebook.react.bridge.Arguments;
|
|
@@ -17,294 +16,221 @@ import com.voipcloud.sdk.CallOpParam;
|
|
|
17
16
|
import com.voipcloud.sdk.CallSendDtmfParam;
|
|
18
17
|
import com.voipcloud.sdk.Endpoint;
|
|
19
18
|
import com.voipcloud.sdk.EpConfig;
|
|
20
|
-
import com.voipcloud.sdk.License;
|
|
21
|
-
import com.voipcloud.sdk.LicenseRefresher;
|
|
22
19
|
import com.voipcloud.sdk.TransportConfig;
|
|
23
20
|
import com.voipcloud.sdk.pjsip_status_code;
|
|
24
21
|
import com.voipcloud.sdk.pjsip_transport_type_e;
|
|
25
22
|
import com.voipcloud.sdk.pjsua_destroy_flag;
|
|
26
23
|
|
|
24
|
+
import com.voipcloud.sdk.rn.internal.BridgeGuard;
|
|
27
25
|
import com.voipcloud.sdk.rn.internal.Events;
|
|
28
26
|
import com.voipcloud.sdk.rn.internal.ManagedAccount;
|
|
29
27
|
import com.voipcloud.sdk.rn.internal.ManagedCall;
|
|
30
28
|
import com.voipcloud.sdk.rn.internal.ManagedCallRegistry;
|
|
31
29
|
|
|
32
30
|
import java.util.concurrent.ConcurrentHashMap;
|
|
33
|
-
import java.util.concurrent.atomic.AtomicInteger;
|
|
34
31
|
|
|
35
32
|
/**
|
|
36
|
-
* React Native bridge for the VoipCloud SDK
|
|
37
|
-
*
|
|
33
|
+
* React Native bridge for the VoipCloud public SDK (PJSIP, no license gate).
|
|
34
|
+
* All failures are returned as rejected Promises with VC_* codes — never crash
|
|
35
|
+
* the host app for API misuse or recoverable PJSIP errors.
|
|
38
36
|
*/
|
|
39
37
|
public class VoipCloudModule extends ReactContextBaseJavaModule {
|
|
40
38
|
|
|
41
|
-
static {
|
|
42
|
-
// Load the prebuilt PJSIP + license-layer library. Names must
|
|
43
|
-
// match jniLibs path inside the AAR: jni/<abi>/libvoipcloud.so.
|
|
44
|
-
// libc++_shared is co-located and pulled in implicitly by the
|
|
45
|
-
// loader as a NEEDED dependency.
|
|
46
|
-
System.loadLibrary("voipcloud");
|
|
47
|
-
}
|
|
48
|
-
|
|
49
39
|
private final ReactApplicationContext ctx;
|
|
50
40
|
private final ManagedCallRegistry calls = new ManagedCallRegistry();
|
|
51
41
|
private final ConcurrentHashMap<Integer, ManagedAccount> accounts = new ConcurrentHashMap<>();
|
|
52
|
-
private final AtomicInteger nextAccKey = new AtomicInteger(1);
|
|
53
42
|
|
|
54
43
|
private Endpoint endpoint;
|
|
55
44
|
private boolean inited = false;
|
|
56
|
-
/** Last token accepted by License.set; reused by the refresher. */
|
|
57
|
-
private volatile String licenseToken = null;
|
|
58
|
-
/** Refresh URL currently driving LicenseRefresher. Used to detect
|
|
59
|
-
* when a token rotation also rotates the refresh endpoint. */
|
|
60
|
-
private volatile String activeRefreshUrl = null;
|
|
61
45
|
|
|
62
46
|
public VoipCloudModule(ReactApplicationContext ctx) {
|
|
63
47
|
super(ctx);
|
|
64
48
|
this.ctx = ctx;
|
|
65
|
-
|
|
66
|
-
// Tell native the consumer app's package up front so subsequent
|
|
67
|
-
// License.set() can enforce the bundle_id binding embedded in
|
|
68
|
-
// the token. Also anchor the clock-skew detector.
|
|
69
|
-
License.setBundle(ctx.getPackageName());
|
|
70
|
-
License.setStateDir(ctx.getFilesDir().getAbsolutePath());
|
|
71
49
|
}
|
|
72
50
|
|
|
51
|
+
/** Must match NATIVE_MODULE_NAME in src/nativeModule.ts (@voipsdk/react-native-voipcloud). */
|
|
73
52
|
@NonNull
|
|
74
53
|
@Override
|
|
75
|
-
public String getName() { return "
|
|
76
|
-
|
|
77
|
-
// ============== License ============================================
|
|
54
|
+
public String getName() { return "VoipsdkReactNativeVoipcloud"; }
|
|
78
55
|
|
|
79
|
-
|
|
80
|
-
|
|
56
|
+
private void destroyEndpointQuietly() {
|
|
57
|
+
if (endpoint == null) return;
|
|
81
58
|
try {
|
|
82
|
-
|
|
83
|
-
licenseToken = token;
|
|
84
|
-
bootstrapRefresher(License.refreshUrl());
|
|
85
|
-
promise.resolve(null);
|
|
86
|
-
} catch (Throwable t) {
|
|
87
|
-
// Stop any refresher tied to the previous (now-rejected) token.
|
|
88
|
-
stopRefresherInternal();
|
|
89
|
-
promise.reject(mapLicenseError(t), t.getMessage(), t);
|
|
90
|
-
}
|
|
91
|
-
}
|
|
92
|
-
|
|
93
|
-
/**
|
|
94
|
-
* Start, restart, or no-op the LicenseRefresher to match the URL
|
|
95
|
-
* embedded in the most recently accepted token. The consumer app
|
|
96
|
-
* never specifies a URL — it travels inside the signed token.
|
|
97
|
-
*/
|
|
98
|
-
private void bootstrapRefresher(String refreshUrl) {
|
|
99
|
-
if (refreshUrl == null || refreshUrl.isEmpty()) {
|
|
100
|
-
// Token opted out of refresh. Stop any leftover loop.
|
|
101
|
-
stopRefresherInternal();
|
|
102
|
-
return;
|
|
103
|
-
}
|
|
104
|
-
// If the URL is unchanged, the existing refresher is still
|
|
105
|
-
// correct — just leave it running so its next 24 h fire reuses
|
|
106
|
-
// the now-updated token via its internal cache.
|
|
107
|
-
if (refreshUrl.equals(activeRefreshUrl)) return;
|
|
108
|
-
|
|
109
|
-
// URL changed (or first start) — restart with the new endpoint.
|
|
110
|
-
try { LicenseRefresher.stop(); } catch (Throwable ignored) {}
|
|
111
|
-
LicenseRefresher.start(ctx, refreshUrl, licenseToken, autoDeviceId());
|
|
112
|
-
activeRefreshUrl = refreshUrl;
|
|
113
|
-
}
|
|
114
|
-
|
|
115
|
-
private void stopRefresherInternal() {
|
|
116
|
-
try { LicenseRefresher.stop(); } catch (Throwable ignored) {}
|
|
117
|
-
activeRefreshUrl = null;
|
|
118
|
-
}
|
|
119
|
-
|
|
120
|
-
/**
|
|
121
|
-
* Stable per-install identifier without asking the consumer. On
|
|
122
|
-
* Android 8+ the ANDROID_ID is scoped to package+signing key, so
|
|
123
|
-
* it survives reinstalls of the same APK but rotates if the app is
|
|
124
|
-
* sideloaded by another publisher — exactly the semantics a
|
|
125
|
-
* license server wants.
|
|
126
|
-
*/
|
|
127
|
-
private String autoDeviceId() {
|
|
128
|
-
try {
|
|
129
|
-
String id = Settings.Secure.getString(
|
|
130
|
-
ctx.getContentResolver(), Settings.Secure.ANDROID_ID);
|
|
131
|
-
if (id != null && !id.isEmpty()) return id;
|
|
59
|
+
endpoint.libDestroy(pjsua_destroy_flag.PJSUA_DESTROY_NO_NETWORK);
|
|
132
60
|
} catch (Throwable ignored) {}
|
|
133
|
-
// Fall back to a manifest-derived label.
|
|
134
|
-
return ctx.getPackageName() + ":" + android.os.Build.FINGERPRINT;
|
|
135
|
-
}
|
|
136
|
-
|
|
137
|
-
/** Translate a PJSUA2 Error into the granular VC_* code surfaced to JS. */
|
|
138
|
-
private static String mapLicenseError(Throwable t) {
|
|
139
|
-
String msg = t.getMessage();
|
|
140
|
-
if (msg == null) return "VC_LICENSE";
|
|
141
|
-
if (msg.contains("license not set")) return "VC_NO_LICENSE";
|
|
142
|
-
if (msg.contains("malformed")) return "VC_BAD_FORMAT";
|
|
143
|
-
if (msg.contains("signature invalid")) return "VC_BAD_SIGNATURE";
|
|
144
|
-
if (msg.contains("license expired")) return "VC_EXPIRED";
|
|
145
|
-
if (msg.contains("not active")) return "VC_NOT_ACTIVE";
|
|
146
|
-
if (msg.contains("revoked")) return "VC_REVOKED";
|
|
147
|
-
if (msg.contains("bundle id mismatch")) return "VC_BUNDLE_MISMATCH";
|
|
148
|
-
if (msg.contains("concurrent calls")) return "VC_CCU_EXCEEDED";
|
|
149
|
-
if (msg.contains("payload too large")) return "VC_PAYLOAD_TOO_BIG";
|
|
150
|
-
if (msg.contains("clock rolled back")) return "VC_CLOCK_SKEW";
|
|
151
|
-
return "VC_LICENSE";
|
|
152
|
-
}
|
|
153
|
-
|
|
154
|
-
@ReactMethod
|
|
155
|
-
public void getLicenseStatus(Promise promise) {
|
|
156
61
|
try {
|
|
157
|
-
|
|
158
|
-
|
|
159
|
-
|
|
160
|
-
|
|
161
|
-
m.putInt ("ccu", License.maxCcu());
|
|
162
|
-
m.putInt ("currentCalls",License.currentCalls());
|
|
163
|
-
m.putDouble ("expiry", License.expiryUnix());
|
|
164
|
-
m.putDouble ("serverTime", License.serverTime());
|
|
165
|
-
promise.resolve(m);
|
|
166
|
-
} catch (Throwable t) {
|
|
167
|
-
promise.reject("VC_STATUS", t.getMessage(), t);
|
|
168
|
-
}
|
|
62
|
+
endpoint.delete();
|
|
63
|
+
} catch (Throwable ignored) {}
|
|
64
|
+
endpoint = null;
|
|
65
|
+
inited = false;
|
|
169
66
|
}
|
|
170
67
|
|
|
171
|
-
/**
|
|
172
|
-
* The refresher bootstraps itself from token.ref during
|
|
173
|
-
* setLicense(); the consumer never starts it manually. We expose
|
|
174
|
-
* stop() only so apps can pause refresh during user logout / SDK
|
|
175
|
-
* shutdown.
|
|
176
|
-
*/
|
|
177
68
|
@ReactMethod
|
|
178
|
-
public void
|
|
179
|
-
|
|
180
|
-
promise.resolve(null);
|
|
181
|
-
}
|
|
69
|
+
public void init(ReadableMap config, Promise promise) {
|
|
70
|
+
if (!BridgeGuard.ensureNative(promise)) return;
|
|
71
|
+
if (inited) { promise.resolve(null); return; }
|
|
182
72
|
|
|
183
|
-
|
|
73
|
+
if (config == null) config = Arguments.createMap();
|
|
184
74
|
|
|
185
|
-
|
|
186
|
-
public void init(ReadableMap config, Promise promise) {
|
|
75
|
+
Endpoint ep = null;
|
|
187
76
|
try {
|
|
188
|
-
|
|
189
|
-
|
|
190
|
-
endpoint.libCreate();
|
|
77
|
+
ep = new Endpoint();
|
|
78
|
+
ep.libCreate();
|
|
191
79
|
|
|
192
80
|
EpConfig epCfg = new EpConfig();
|
|
193
|
-
if (config.hasKey("userAgent"))
|
|
81
|
+
if (config.hasKey("userAgent") && !config.isNull("userAgent"))
|
|
194
82
|
epCfg.getUaConfig().setUserAgent(config.getString("userAgent"));
|
|
195
|
-
if (config.hasKey("stunServer")) {
|
|
83
|
+
if (config.hasKey("stunServer") && !config.isNull("stunServer")) {
|
|
196
84
|
com.voipcloud.sdk.StringVector v = new com.voipcloud.sdk.StringVector();
|
|
197
85
|
v.add(config.getString("stunServer"));
|
|
198
86
|
epCfg.getUaConfig().setStunServer(v);
|
|
199
87
|
}
|
|
200
|
-
if (config.hasKey("logLevel"))
|
|
88
|
+
if (config.hasKey("logLevel") && !config.isNull("logLevel"))
|
|
201
89
|
epCfg.getLogConfig().setLevel(config.getInt("logLevel"));
|
|
202
90
|
|
|
203
|
-
|
|
91
|
+
ep.libInit(epCfg);
|
|
204
92
|
|
|
205
|
-
// Default transports — TLS handled per-account via registrar URI.
|
|
206
93
|
TransportConfig udp = new TransportConfig();
|
|
207
|
-
|
|
94
|
+
ep.transportCreate(pjsip_transport_type_e.PJSIP_TRANSPORT_UDP, udp);
|
|
208
95
|
TransportConfig tcp = new TransportConfig();
|
|
209
|
-
|
|
96
|
+
ep.transportCreate(pjsip_transport_type_e.PJSIP_TRANSPORT_TCP, tcp);
|
|
210
97
|
|
|
211
|
-
|
|
98
|
+
ep.libStart();
|
|
99
|
+
endpoint = ep;
|
|
212
100
|
inited = true;
|
|
213
101
|
promise.resolve(null);
|
|
214
102
|
} catch (Throwable t) {
|
|
215
|
-
|
|
103
|
+
if (ep != null) {
|
|
104
|
+
try { ep.libDestroy(pjsua_destroy_flag.PJSUA_DESTROY_NO_NETWORK); } catch (Throwable ignored) {}
|
|
105
|
+
try { ep.delete(); } catch (Throwable ignored) {}
|
|
106
|
+
}
|
|
107
|
+
endpoint = null;
|
|
108
|
+
inited = false;
|
|
109
|
+
BridgeGuard.reject(promise, "VC_INIT", "Failed to start VoipCloud endpoint", t);
|
|
216
110
|
}
|
|
217
111
|
}
|
|
218
112
|
|
|
219
113
|
@ReactMethod
|
|
220
114
|
public void shutdown(Promise promise) {
|
|
115
|
+
if (!BridgeGuard.ensureNative(promise)) return;
|
|
221
116
|
try {
|
|
222
117
|
calls.clear();
|
|
223
|
-
for (ManagedAccount a : accounts.values())
|
|
224
|
-
|
|
225
|
-
|
|
226
|
-
endpoint.libDestroy(pjsua_destroy_flag.PJSUA_DESTROY_NO_NETWORK);
|
|
227
|
-
endpoint.delete();
|
|
228
|
-
endpoint = null;
|
|
118
|
+
for (ManagedAccount a : accounts.values()) {
|
|
119
|
+
try { a.setRegistration(false); } catch (Throwable ignored) {}
|
|
120
|
+
try { a.delete(); } catch (Throwable ignored) {}
|
|
229
121
|
}
|
|
230
|
-
|
|
122
|
+
accounts.clear();
|
|
123
|
+
destroyEndpointQuietly();
|
|
231
124
|
promise.resolve(null);
|
|
232
125
|
} catch (Throwable t) {
|
|
233
|
-
|
|
126
|
+
BridgeGuard.reject(promise, "VC_SHUTDOWN", "Failed to shut down VoipCloud", t);
|
|
234
127
|
}
|
|
235
128
|
}
|
|
236
129
|
|
|
237
|
-
// ============== Account ============================================
|
|
238
|
-
|
|
239
130
|
@ReactMethod
|
|
240
131
|
public void register(ReadableMap cfg, Promise promise) {
|
|
132
|
+
if (!BridgeGuard.ensureNative(promise)) return;
|
|
133
|
+
if (!BridgeGuard.ensureInited(inited, promise)) return;
|
|
134
|
+
|
|
135
|
+
String uri = BridgeGuard.requireString(cfg, "uri", promise);
|
|
136
|
+
if (uri == null) return;
|
|
137
|
+
String registrar = BridgeGuard.requireString(cfg, "registrar", promise);
|
|
138
|
+
if (registrar == null) return;
|
|
139
|
+
String username = BridgeGuard.requireString(cfg, "username", promise);
|
|
140
|
+
if (username == null) return;
|
|
141
|
+
String password = BridgeGuard.requireString(cfg, "password", promise);
|
|
142
|
+
if (password == null) return;
|
|
143
|
+
|
|
241
144
|
try {
|
|
242
145
|
AccountConfig acfg = new AccountConfig();
|
|
243
|
-
acfg.setIdUri(
|
|
244
|
-
acfg.getRegConfig().setRegistrarUri(
|
|
146
|
+
acfg.setIdUri(uri);
|
|
147
|
+
acfg.getRegConfig().setRegistrarUri(registrar);
|
|
245
148
|
|
|
246
|
-
AuthCredInfo cred = new AuthCredInfo(
|
|
247
|
-
"digest", "*",
|
|
248
|
-
cfg.getString("username"), 0,
|
|
249
|
-
cfg.getString("password"));
|
|
149
|
+
AuthCredInfo cred = new AuthCredInfo("digest", "*", username, 0, password);
|
|
250
150
|
acfg.getSipConfig().getAuthCreds().add(cred);
|
|
251
151
|
|
|
252
152
|
ManagedAccount acc = new ManagedAccount(ctx, calls);
|
|
253
153
|
acc.create(acfg);
|
|
254
154
|
int id = acc.accountId();
|
|
155
|
+
if (id < 0) {
|
|
156
|
+
acc.delete();
|
|
157
|
+
promise.reject("VC_REGISTER", "Account created but returned invalid id");
|
|
158
|
+
return;
|
|
159
|
+
}
|
|
255
160
|
accounts.put(id, acc);
|
|
256
161
|
promise.resolve(id);
|
|
257
162
|
} catch (Throwable t) {
|
|
258
|
-
|
|
163
|
+
BridgeGuard.reject(promise, "VC_REGISTER", "Failed to register SIP account", t);
|
|
259
164
|
}
|
|
260
165
|
}
|
|
261
166
|
|
|
262
167
|
@ReactMethod
|
|
263
168
|
public void unregister(int accountId, Promise promise) {
|
|
169
|
+
if (!BridgeGuard.ensureNative(promise)) return;
|
|
264
170
|
try {
|
|
265
171
|
ManagedAccount acc = accounts.remove(accountId);
|
|
266
172
|
if (acc != null) {
|
|
267
|
-
acc.setRegistration(false);
|
|
268
|
-
acc.delete();
|
|
173
|
+
try { acc.setRegistration(false); } catch (Throwable ignored) {}
|
|
174
|
+
try { acc.delete(); } catch (Throwable ignored) {}
|
|
269
175
|
}
|
|
270
176
|
promise.resolve(null);
|
|
271
177
|
} catch (Throwable t) {
|
|
272
|
-
|
|
178
|
+
BridgeGuard.reject(promise, "VC_UNREGISTER", "Failed to unregister account", t);
|
|
273
179
|
}
|
|
274
180
|
}
|
|
275
181
|
|
|
276
|
-
// ============== Call ===============================================
|
|
277
|
-
|
|
278
182
|
@ReactMethod
|
|
279
183
|
public void makeCall(int accountId, String uri, Promise promise) {
|
|
184
|
+
if (!BridgeGuard.ensureNative(promise)) return;
|
|
185
|
+
if (!BridgeGuard.ensureInited(inited, promise)) return;
|
|
186
|
+
|
|
187
|
+
if (uri == null || uri.trim().isEmpty()) {
|
|
188
|
+
promise.reject("VC_INVALID_ARGUMENT", "destinationUri must not be empty");
|
|
189
|
+
return;
|
|
190
|
+
}
|
|
191
|
+
|
|
280
192
|
try {
|
|
281
193
|
ManagedAccount acc = accounts.get(accountId);
|
|
282
|
-
if (acc == null) {
|
|
194
|
+
if (acc == null) {
|
|
195
|
+
promise.reject("VC_NO_ACCOUNT", "Unknown accountId " + accountId + ". Call register() first.");
|
|
196
|
+
return;
|
|
197
|
+
}
|
|
283
198
|
ManagedCall call = new ManagedCall(ctx, acc);
|
|
284
|
-
call.makeCall(uri, new CallOpParam(true));
|
|
199
|
+
call.makeCall(uri.trim(), new CallOpParam(true));
|
|
200
|
+
int callId = call.callId();
|
|
201
|
+
if (callId < 0) {
|
|
202
|
+
promise.reject("VC_MAKE_CALL", "Call started but returned invalid callId");
|
|
203
|
+
return;
|
|
204
|
+
}
|
|
285
205
|
calls.put(call);
|
|
286
|
-
promise.resolve(
|
|
206
|
+
promise.resolve(callId);
|
|
287
207
|
} catch (Throwable t) {
|
|
288
|
-
|
|
208
|
+
BridgeGuard.reject(promise, "VC_MAKE_CALL", "Failed to place call", t);
|
|
289
209
|
}
|
|
290
210
|
}
|
|
291
211
|
|
|
292
212
|
@ReactMethod
|
|
293
213
|
public void answer(int callId, Promise promise) {
|
|
214
|
+
if (!BridgeGuard.ensureNative(promise)) return;
|
|
215
|
+
if (!BridgeGuard.ensureInited(inited, promise)) return;
|
|
294
216
|
try {
|
|
295
217
|
ManagedCall call = calls.get(callId);
|
|
296
|
-
if (call == null) {
|
|
218
|
+
if (call == null) {
|
|
219
|
+
promise.reject("VC_NO_CALL", "Unknown callId " + callId);
|
|
220
|
+
return;
|
|
221
|
+
}
|
|
297
222
|
CallOpParam op = new CallOpParam();
|
|
298
223
|
op.setStatusCode(pjsip_status_code.PJSIP_SC_OK);
|
|
299
224
|
call.answer(op);
|
|
300
225
|
promise.resolve(null);
|
|
301
226
|
} catch (Throwable t) {
|
|
302
|
-
|
|
227
|
+
BridgeGuard.reject(promise, "VC_ANSWER", "Failed to answer call", t);
|
|
303
228
|
}
|
|
304
229
|
}
|
|
305
230
|
|
|
306
231
|
@ReactMethod
|
|
307
232
|
public void hangup(int callId, Promise promise) {
|
|
233
|
+
if (!BridgeGuard.ensureNative(promise)) return;
|
|
308
234
|
try {
|
|
309
235
|
ManagedCall call = calls.get(callId);
|
|
310
236
|
if (call == null) { promise.resolve(null); return; }
|
|
@@ -314,48 +240,55 @@ public class VoipCloudModule extends ReactContextBaseJavaModule {
|
|
|
314
240
|
calls.remove(callId);
|
|
315
241
|
promise.resolve(null);
|
|
316
242
|
} catch (Throwable t) {
|
|
317
|
-
|
|
243
|
+
BridgeGuard.reject(promise, "VC_HANGUP", "Failed to hang up call", t);
|
|
318
244
|
}
|
|
319
245
|
}
|
|
320
246
|
|
|
321
247
|
@ReactMethod
|
|
322
248
|
public void hold(int callId, Promise promise) {
|
|
249
|
+
if (!BridgeGuard.ensureNative(promise)) return;
|
|
250
|
+
if (!BridgeGuard.ensureInited(inited, promise)) return;
|
|
323
251
|
try {
|
|
324
|
-
ManagedCall call =
|
|
325
|
-
if (call == null)
|
|
252
|
+
ManagedCall call = requireCall(callId, promise);
|
|
253
|
+
if (call == null) return;
|
|
326
254
|
call.setHold(new CallOpParam(true));
|
|
327
255
|
WritableMap ev = Arguments.createMap();
|
|
328
256
|
ev.putInt("callId", callId);
|
|
329
257
|
Events.emit(getReactApplicationContext(), Events.CALL_HOLD, ev);
|
|
330
258
|
promise.resolve(null);
|
|
331
259
|
} catch (Throwable t) {
|
|
332
|
-
|
|
260
|
+
BridgeGuard.reject(promise, "VC_HOLD", "Failed to hold call", t);
|
|
333
261
|
}
|
|
334
262
|
}
|
|
335
263
|
|
|
336
264
|
@ReactMethod
|
|
337
265
|
public void unhold(int callId, Promise promise) {
|
|
266
|
+
if (!BridgeGuard.ensureNative(promise)) return;
|
|
267
|
+
if (!BridgeGuard.ensureInited(inited, promise)) return;
|
|
338
268
|
try {
|
|
339
|
-
ManagedCall call =
|
|
340
|
-
if (call == null)
|
|
341
|
-
// PJSUA2 re-INVITE without an explicit hold flag re-offers
|
|
342
|
-
// sendrecv SDP; that releases the existing hold.
|
|
269
|
+
ManagedCall call = requireCall(callId, promise);
|
|
270
|
+
if (call == null) return;
|
|
343
271
|
call.reinvite(new CallOpParam(true));
|
|
344
272
|
WritableMap ev = Arguments.createMap();
|
|
345
273
|
ev.putInt("callId", callId);
|
|
346
274
|
Events.emit(getReactApplicationContext(), Events.CALL_UNHOLD, ev);
|
|
347
275
|
promise.resolve(null);
|
|
348
276
|
} catch (Throwable t) {
|
|
349
|
-
|
|
277
|
+
BridgeGuard.reject(promise, "VC_UNHOLD", "Failed to resume call", t);
|
|
350
278
|
}
|
|
351
279
|
}
|
|
352
280
|
|
|
353
281
|
@ReactMethod
|
|
354
282
|
public void mute(int callId, boolean on, Promise promise) {
|
|
283
|
+
if (!BridgeGuard.ensureNative(promise)) return;
|
|
284
|
+
if (!BridgeGuard.ensureInited(inited, promise)) return;
|
|
355
285
|
try {
|
|
356
|
-
ManagedCall call =
|
|
357
|
-
if (call == null)
|
|
358
|
-
if (endpoint == null) {
|
|
286
|
+
ManagedCall call = requireCall(callId, promise);
|
|
287
|
+
if (call == null) return;
|
|
288
|
+
if (endpoint == null) {
|
|
289
|
+
promise.reject("VC_NOT_INITIALIZED", "VoipCloud.init() must succeed before mute()");
|
|
290
|
+
return;
|
|
291
|
+
}
|
|
359
292
|
call.setMuted(endpoint, on);
|
|
360
293
|
WritableMap ev = Arguments.createMap();
|
|
361
294
|
ev.putInt("callId", callId);
|
|
@@ -363,15 +296,21 @@ public class VoipCloudModule extends ReactContextBaseJavaModule {
|
|
|
363
296
|
Events.emit(getReactApplicationContext(), Events.CALL_MUTE, ev);
|
|
364
297
|
promise.resolve(null);
|
|
365
298
|
} catch (Throwable t) {
|
|
366
|
-
|
|
299
|
+
BridgeGuard.reject(promise, "VC_MUTE", "Failed to change mute state", t);
|
|
367
300
|
}
|
|
368
301
|
}
|
|
369
302
|
|
|
370
303
|
@ReactMethod
|
|
371
304
|
public void sendDtmf(int callId, String digits, Promise promise) {
|
|
305
|
+
if (!BridgeGuard.ensureNative(promise)) return;
|
|
306
|
+
if (!BridgeGuard.ensureInited(inited, promise)) return;
|
|
307
|
+
if (digits == null || digits.isEmpty()) {
|
|
308
|
+
promise.reject("VC_INVALID_ARGUMENT", "digits must not be empty");
|
|
309
|
+
return;
|
|
310
|
+
}
|
|
372
311
|
try {
|
|
373
|
-
ManagedCall call =
|
|
374
|
-
if (call == null)
|
|
312
|
+
ManagedCall call = requireCall(callId, promise);
|
|
313
|
+
if (call == null) return;
|
|
375
314
|
CallSendDtmfParam p = new CallSendDtmfParam();
|
|
376
315
|
p.setDigits(digits);
|
|
377
316
|
call.sendDtmf(p);
|
|
@@ -381,48 +320,53 @@ public class VoipCloudModule extends ReactContextBaseJavaModule {
|
|
|
381
320
|
Events.emit(getReactApplicationContext(), Events.CALL_DTMF_SENT, ev);
|
|
382
321
|
promise.resolve(null);
|
|
383
322
|
} catch (Throwable t) {
|
|
384
|
-
|
|
323
|
+
BridgeGuard.reject(promise, "VC_DTMF", "Failed to send DTMF", t);
|
|
385
324
|
}
|
|
386
325
|
}
|
|
387
326
|
|
|
388
|
-
/**
|
|
389
|
-
* Blind-transfer a call (REFER). The Promise resolves once the
|
|
390
|
-
* REFER is *sent* — progress of the remote-side INVITE comes
|
|
391
|
-
* through `callTransferStatus` events (statusCode + finalNotify).
|
|
392
|
-
*/
|
|
393
327
|
@ReactMethod
|
|
394
328
|
public void transferCall(int callId, String destinationUri, Promise promise) {
|
|
329
|
+
if (!BridgeGuard.ensureNative(promise)) return;
|
|
330
|
+
if (!BridgeGuard.ensureInited(inited, promise)) return;
|
|
331
|
+
if (destinationUri == null || destinationUri.trim().isEmpty()) {
|
|
332
|
+
promise.reject("VC_INVALID_ARGUMENT", "destinationUri must not be empty");
|
|
333
|
+
return;
|
|
334
|
+
}
|
|
395
335
|
try {
|
|
396
|
-
ManagedCall call =
|
|
397
|
-
if (call == null)
|
|
398
|
-
call.xfer(destinationUri, new CallOpParam(true));
|
|
336
|
+
ManagedCall call = requireCall(callId, promise);
|
|
337
|
+
if (call == null) return;
|
|
338
|
+
call.xfer(destinationUri.trim(), new CallOpParam(true));
|
|
399
339
|
WritableMap ev = Arguments.createMap();
|
|
400
340
|
ev.putInt("callId", callId);
|
|
401
|
-
ev.putString("destinationUri", destinationUri);
|
|
341
|
+
ev.putString("destinationUri", destinationUri.trim());
|
|
402
342
|
Events.emit(getReactApplicationContext(), Events.CALL_TRANSFER, ev);
|
|
403
343
|
promise.resolve(null);
|
|
404
344
|
} catch (Throwable t) {
|
|
405
|
-
|
|
345
|
+
BridgeGuard.reject(promise, "VC_TRANSFER", "Failed to transfer call", t);
|
|
406
346
|
}
|
|
407
347
|
}
|
|
408
348
|
|
|
409
|
-
/**
|
|
410
|
-
* End every active call in one shot. Useful for SDK shutdown or
|
|
411
|
-
* a global "end all" UI action. Each call still emits its own
|
|
412
|
-
* DISCONNECTED callState.
|
|
413
|
-
*/
|
|
414
349
|
@ReactMethod
|
|
415
350
|
public void hangupAllCalls(Promise promise) {
|
|
351
|
+
if (!BridgeGuard.ensureNative(promise)) return;
|
|
416
352
|
try {
|
|
417
|
-
if (endpoint != null) endpoint.hangupAllCalls();
|
|
353
|
+
if (endpoint != null && inited) endpoint.hangupAllCalls();
|
|
354
|
+
calls.clear();
|
|
418
355
|
promise.resolve(null);
|
|
419
356
|
} catch (Throwable t) {
|
|
420
|
-
|
|
357
|
+
BridgeGuard.reject(promise, "VC_HANGUP_ALL", "Failed to hang up all calls", t);
|
|
358
|
+
}
|
|
359
|
+
}
|
|
360
|
+
|
|
361
|
+
private ManagedCall requireCall(int callId, Promise promise) {
|
|
362
|
+
ManagedCall call = calls.get(callId);
|
|
363
|
+
if (call == null) {
|
|
364
|
+
promise.reject("VC_NO_CALL", "Unknown callId " + callId);
|
|
365
|
+
return null;
|
|
421
366
|
}
|
|
367
|
+
return call;
|
|
422
368
|
}
|
|
423
369
|
|
|
424
|
-
// RN event-emitter contract — required on RN 0.65+ when used with
|
|
425
|
-
// NativeEventEmitter on the JS side.
|
|
426
370
|
@ReactMethod public void addListener(String eventName) { /* no-op */ }
|
|
427
371
|
@ReactMethod public void removeListeners(int count) { /* no-op */ }
|
|
428
372
|
}
|
|
@@ -0,0 +1,74 @@
|
|
|
1
|
+
package com.voipcloud.sdk.rn.internal;
|
|
2
|
+
|
|
3
|
+
import com.facebook.react.bridge.Promise;
|
|
4
|
+
import com.facebook.react.bridge.ReadableMap;
|
|
5
|
+
|
|
6
|
+
/**
|
|
7
|
+
* Shared guards and input validation for the RN bridge — reject with clear
|
|
8
|
+
* VC_* codes instead of letting native/JNI exceptions crash the app.
|
|
9
|
+
*/
|
|
10
|
+
public final class BridgeGuard {
|
|
11
|
+
|
|
12
|
+
private static volatile boolean nativeLoaded = false;
|
|
13
|
+
private static volatile String nativeLoadError =
|
|
14
|
+
"libvoipcloud.so failed to load (missing ABI, stale build, or AAR not packaged)";
|
|
15
|
+
|
|
16
|
+
/** Loads libvoipcloud.so before any SWIG type (Endpoint, voipcloudJNI). */
|
|
17
|
+
static {
|
|
18
|
+
try {
|
|
19
|
+
System.loadLibrary("voipcloud");
|
|
20
|
+
nativeLoaded = true;
|
|
21
|
+
} catch (UnsatisfiedLinkError e) {
|
|
22
|
+
nativeLoadError = "Failed to load libvoipcloud.so: " + e.getMessage()
|
|
23
|
+
+ ". Ensure voipcloud-release.aar is included and the app was rebuilt.";
|
|
24
|
+
} catch (Throwable t) {
|
|
25
|
+
nativeLoadError = "Failed to load libvoipcloud.so: " + t.getMessage();
|
|
26
|
+
}
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
private BridgeGuard() {}
|
|
30
|
+
|
|
31
|
+
public static boolean isNativeLoaded() { return nativeLoaded; }
|
|
32
|
+
|
|
33
|
+
public static String nativeLoadError() { return nativeLoadError; }
|
|
34
|
+
|
|
35
|
+
/** Reject the promise if native .so did not load. Returns false when rejected. */
|
|
36
|
+
public static boolean ensureNative(Promise promise) {
|
|
37
|
+
if (nativeLoaded) return true;
|
|
38
|
+
promise.reject("VC_NATIVE_LOAD", nativeLoadError);
|
|
39
|
+
return false;
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
/** Reject if {@link VoipCloudModule} endpoint is not running. */
|
|
43
|
+
public static boolean ensureInited(boolean inited, Promise promise) {
|
|
44
|
+
if (inited) return true;
|
|
45
|
+
promise.reject("VC_NOT_INITIALIZED",
|
|
46
|
+
"VoipCloud.init() must succeed before calling this method.");
|
|
47
|
+
return false;
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
/**
|
|
51
|
+
* Read a required non-empty string field from a JS config object.
|
|
52
|
+
* Rejects with VC_INVALID_ARGUMENT and returns null on failure.
|
|
53
|
+
*/
|
|
54
|
+
public static String requireString(ReadableMap map, String key, Promise promise) {
|
|
55
|
+
if (map == null || !map.hasKey(key) || map.isNull(key)) {
|
|
56
|
+
promise.reject("VC_INVALID_ARGUMENT", "Missing required field: " + key);
|
|
57
|
+
return null;
|
|
58
|
+
}
|
|
59
|
+
String value = map.getString(key);
|
|
60
|
+
if (value == null || value.trim().isEmpty()) {
|
|
61
|
+
promise.reject("VC_INVALID_ARGUMENT", "Field must not be empty: " + key);
|
|
62
|
+
return null;
|
|
63
|
+
}
|
|
64
|
+
return value.trim();
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
/** Reject with a stable code; message includes PJSIP detail when available. */
|
|
68
|
+
public static void reject(Promise promise, String code, String context, Throwable t) {
|
|
69
|
+
String detail = (t != null && t.getMessage() != null && !t.getMessage().isEmpty())
|
|
70
|
+
? t.getMessage()
|
|
71
|
+
: (t != null ? t.getClass().getSimpleName() : "unknown error");
|
|
72
|
+
promise.reject(code, context + ": " + detail, t);
|
|
73
|
+
}
|
|
74
|
+
}
|
|
@@ -32,7 +32,12 @@ public final class Events {
|
|
|
32
32
|
|
|
33
33
|
public static void emit(ReactContext ctx, String name, WritableMap payload) {
|
|
34
34
|
if (ctx == null || !ctx.hasActiveReactInstance()) return;
|
|
35
|
-
|
|
36
|
-
|
|
35
|
+
// PJSIP fires callbacks on its worker thread — hop to the RN UI queue
|
|
36
|
+
// so DeviceEventEmitter does not crash the app on some RN versions.
|
|
37
|
+
ctx.runOnUiQueueThread(() -> {
|
|
38
|
+
if (!ctx.hasActiveReactInstance()) return;
|
|
39
|
+
ctx.getJSModule(DeviceEventManagerModule.RCTDeviceEventEmitter.class)
|
|
40
|
+
.emit(name, payload);
|
|
41
|
+
});
|
|
37
42
|
}
|
|
38
43
|
}
|
|
Binary file
|