@voipsdk/react-native-voipcloud 0.2.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/README.md +172 -0
- package/android/build.gradle +61 -0
- package/android/src/main/AndroidManifest.xml +2 -0
- package/android/src/main/java/com/voipcloud/sdk/rn/VoipCloudModule.java +428 -0
- package/android/src/main/java/com/voipcloud/sdk/rn/VoipCloudPackage.java +21 -0
- package/android/src/main/java/com/voipcloud/sdk/rn/internal/Events.java +38 -0
- package/android/src/main/java/com/voipcloud/sdk/rn/internal/ManagedAccount.java +63 -0
- package/android/src/main/java/com/voipcloud/sdk/rn/internal/ManagedCall.java +179 -0
- package/android/src/main/java/com/voipcloud/sdk/rn/internal/ManagedCallRegistry.java +26 -0
- package/android/src/main/libs/voipcloud-release.aar +0 -0
- package/ios/VoipCloud.mm +40 -0
- package/lib/commonjs/License.js +51 -0
- package/lib/commonjs/License.js.map +1 -0
- package/lib/commonjs/VoipCloud.js +89 -0
- package/lib/commonjs/VoipCloud.js.map +1 -0
- package/lib/commonjs/events.js +29 -0
- package/lib/commonjs/events.js.map +1 -0
- package/lib/commonjs/index.js +27 -0
- package/lib/commonjs/index.js.map +1 -0
- package/lib/commonjs/types.js +2 -0
- package/lib/commonjs/types.js.map +1 -0
- package/lib/module/License.js +45 -0
- package/lib/module/License.js.map +1 -0
- package/lib/module/VoipCloud.js +83 -0
- package/lib/module/VoipCloud.js.map +1 -0
- package/lib/module/events.js +23 -0
- package/lib/module/events.js.map +1 -0
- package/lib/module/index.js +4 -0
- package/lib/module/index.js.map +1 -0
- package/lib/module/types.js +2 -0
- package/lib/module/types.js.map +1 -0
- package/lib/typescript/License.d.ts +31 -0
- package/lib/typescript/VoipCloud.d.ts +52 -0
- package/lib/typescript/events.d.ts +6 -0
- package/lib/typescript/index.d.ts +4 -0
- package/lib/typescript/types.d.ts +136 -0
- package/package.json +76 -0
- package/react-native-voipcloud.podspec +16 -0
- package/src/License.ts +52 -0
- package/src/VoipCloud.ts +98 -0
- package/src/events.ts +23 -0
- package/src/index.ts +23 -0
- package/src/types.ts +144 -0
package/README.md
ADDED
|
@@ -0,0 +1,172 @@
|
|
|
1
|
+
# react-native-voipcloud
|
|
2
|
+
|
|
3
|
+
> Platform: Android (RN 0.70+, Android 12+).
|
|
4
|
+
|
|
5
|
+
---
|
|
6
|
+
|
|
7
|
+
## Install
|
|
8
|
+
|
|
9
|
+
```bash
|
|
10
|
+
npm install react-native-voipcloud
|
|
11
|
+
# or:
|
|
12
|
+
yarn add react-native-voipcloud
|
|
13
|
+
```
|
|
14
|
+
|
|
15
|
+
Android autolinks the `VoipCloudPackage` if you are on React Native 0.60+.
|
|
16
|
+
|
|
17
|
+
For older RN versions, add manually to `MainApplication.java`:
|
|
18
|
+
```java
|
|
19
|
+
packages.add(new com.voipcloud.sdk.rn.VoipCloudPackage());
|
|
20
|
+
```
|
|
21
|
+
|
|
22
|
+
Required `AndroidManifest.xml` permissions (declare in **your** app):
|
|
23
|
+
```xml
|
|
24
|
+
<uses-permission android:name="android.permission.INTERNET" />
|
|
25
|
+
<uses-permission android:name="android.permission.RECORD_AUDIO" />
|
|
26
|
+
<uses-permission android:name="android.permission.MODIFY_AUDIO_SETTINGS" />
|
|
27
|
+
<uses-permission android:name="android.permission.WAKE_LOCK" />
|
|
28
|
+
<uses-permission android:name="android.permission.FOREGROUND_SERVICE" />
|
|
29
|
+
<uses-permission android:name="android.permission.FOREGROUND_SERVICE_PHONE_CALL" />
|
|
30
|
+
```
|
|
31
|
+
|
|
32
|
+
Runtime permissions to request:
|
|
33
|
+
`RECORD_AUDIO`, `POST_NOTIFICATIONS` (Android 13+), `BLUETOOTH_CONNECT`.
|
|
34
|
+
|
|
35
|
+
---
|
|
36
|
+
|
|
37
|
+
## Quick start
|
|
38
|
+
|
|
39
|
+
```ts
|
|
40
|
+
import { VoipCloud } from 'react-native-voipcloud';
|
|
41
|
+
|
|
42
|
+
// 1. Apply the license your operator issued.
|
|
43
|
+
try {
|
|
44
|
+
await VoipCloud.License.set(myToken);
|
|
45
|
+
} catch (e) {
|
|
46
|
+
// VC_LICENSE — token format / signature / expiry / bundle issues.
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
// 2. Start the engine.
|
|
50
|
+
await VoipCloud.init({ userAgent: 'acme-voip/1.0' });
|
|
51
|
+
|
|
52
|
+
// 3. Register a SIP account.
|
|
53
|
+
const accId = await VoipCloud.register({
|
|
54
|
+
uri: 'sip:alice@example.com',
|
|
55
|
+
registrar: 'sip:example.com:5061;transport=tls',
|
|
56
|
+
username: 'alice',
|
|
57
|
+
password: '••••••',
|
|
58
|
+
transport: 'TLS',
|
|
59
|
+
});
|
|
60
|
+
|
|
61
|
+
// 4. Make a call.
|
|
62
|
+
const callId = await VoipCloud.makeCall(accId, 'sip:bob@example.com');
|
|
63
|
+
|
|
64
|
+
// 5. In-call controls.
|
|
65
|
+
await VoipCloud.hold(callId); // put on hold
|
|
66
|
+
await VoipCloud.unhold(callId); // resume
|
|
67
|
+
await VoipCloud.mute(callId, true); // mute mic
|
|
68
|
+
await VoipCloud.sendDtmf(callId, '1234#'); // send DTMF
|
|
69
|
+
await VoipCloud.transferCall(callId, 'sip:charlie@example.com'); // blind transfer
|
|
70
|
+
await VoipCloud.hangup(callId); // end this call
|
|
71
|
+
await VoipCloud.hangupAllCalls(); // end every active call
|
|
72
|
+
|
|
73
|
+
// 6. Subscribe to events.
|
|
74
|
+
const sub = VoipCloud.on('callState', ({ callId, state }) => {
|
|
75
|
+
console.log('call', callId, '→', state);
|
|
76
|
+
});
|
|
77
|
+
const subMedia = VoipCloud.on('callMediaState', ({ callId, hasAudio, isHold, isMuted }) => {
|
|
78
|
+
console.log('media', callId, { hasAudio, isHold, isMuted });
|
|
79
|
+
});
|
|
80
|
+
// sub.remove() / subMedia.remove() when done
|
|
81
|
+
|
|
82
|
+
// 7. License refresh is automatic.
|
|
83
|
+
// If your token's payload includes a `ref` field, the SDK starts a
|
|
84
|
+
// 24 h refresher in the background after `License.set` succeeds.
|
|
85
|
+
// No URL configuration needed on the app side.
|
|
86
|
+
//
|
|
87
|
+
// Optional: stop the refresher when the user logs out.
|
|
88
|
+
await VoipCloud.License.stopRefresh();
|
|
89
|
+
|
|
90
|
+
// 8. Tear down the SDK on app exit.
|
|
91
|
+
await VoipCloud.shutdown();
|
|
92
|
+
```
|
|
93
|
+
|
|
94
|
+
---
|
|
95
|
+
|
|
96
|
+
## Error codes (Promise rejection.code)
|
|
97
|
+
|
|
98
|
+
Mọi method trả Promise; khi reject, `code` mang một trong các mã sau.
|
|
99
|
+
Cụ thể lỗi license được tách thành các mã granular để app dễ hiển thị
|
|
100
|
+
thông báo phù hợp.
|
|
101
|
+
|
|
102
|
+
License (từ `License.set` / `License.status`):
|
|
103
|
+
|
|
104
|
+
| code | Ý nghĩa |
|
|
105
|
+
|-----------------------|------------------------------------------------------|
|
|
106
|
+
| `VC_NO_LICENSE` | Chưa cài token, hoặc token đã bị reset |
|
|
107
|
+
| `VC_BAD_FORMAT` | Token sai định dạng (không phải base64url JWT) |
|
|
108
|
+
| `VC_BAD_SIGNATURE` | Chữ ký Ed25519 không hợp lệ |
|
|
109
|
+
| `VC_EXPIRED` | Token hết hạn |
|
|
110
|
+
| `VC_NOT_ACTIVE` | Token bị tạm khoá (status ≠ active) |
|
|
111
|
+
| `VC_REVOKED` | Token đã bị thu hồi |
|
|
112
|
+
| `VC_BUNDLE_MISMATCH` | Bundle id app không khớp `bundle_id` trong token |
|
|
113
|
+
| `VC_PAYLOAD_TOO_BIG` | Payload token quá lớn |
|
|
114
|
+
| `VC_CLOCK_SKEW` | Đồng hồ thiết bị bị chỉnh lùi so với mốc server |
|
|
115
|
+
| `VC_LICENSE` | Lỗi license chung không khớp các mã trên |
|
|
116
|
+
| `VC_STATUS` | Đọc trạng thái thất bại |
|
|
117
|
+
|
|
118
|
+
Endpoint / call:
|
|
119
|
+
|
|
120
|
+
| code | Ý nghĩa |
|
|
121
|
+
|-----------------------|------------------------------------------------------|
|
|
122
|
+
| `VC_INIT` | Endpoint không khởi động được |
|
|
123
|
+
| `VC_SHUTDOWN` | Lỗi khi `shutdown()` dọn endpoint |
|
|
124
|
+
| `VC_REGISTER` | `register()` thất bại |
|
|
125
|
+
| `VC_UNREGISTER` | `unregister()` thất bại |
|
|
126
|
+
| `VC_NO_ACCOUNT` | accountId không tồn tại |
|
|
127
|
+
| `VC_MAKE_CALL` | `makeCall()` thất bại (bao gồm `VC_CCU_EXCEEDED`) |
|
|
128
|
+
| `VC_CCU_EXCEEDED` | Vượt số cuộc gọi đồng thời license cho phép |
|
|
129
|
+
| `VC_NO_CALL` | callId không tồn tại |
|
|
130
|
+
| `VC_ANSWER` | `answer()` thất bại |
|
|
131
|
+
| `VC_HANGUP` | `hangup()` thất bại |
|
|
132
|
+
| `VC_HOLD` / `VC_UNHOLD` | `hold()` / `unhold()` thất bại |
|
|
133
|
+
| `VC_MUTE` | `mute()` thất bại (vd: chưa init endpoint) |
|
|
134
|
+
| `VC_DTMF` | `sendDtmf()` thất bại |
|
|
135
|
+
| `VC_TRANSFER` | `transferCall()` thất bại |
|
|
136
|
+
| `VC_HANGUP_ALL` | `hangupAllCalls()` thất bại |
|
|
137
|
+
|
|
138
|
+
`message` đính kèm là `pj_strerror` từ PJSIP, hữu ích khi log.
|
|
139
|
+
|
|
140
|
+
---
|
|
141
|
+
|
|
142
|
+
## Events
|
|
143
|
+
|
|
144
|
+
Account / call lifecycle (do PJSIP phát):
|
|
145
|
+
|
|
146
|
+
```ts
|
|
147
|
+
VoipCloud.on('registrationState', e => /* { accountId, code, reason } */);
|
|
148
|
+
VoipCloud.on('incomingCall', e => /* { accountId, callId, from, to } */);
|
|
149
|
+
VoipCloud.on('callState', e => /* { callId, state, code, reason } */);
|
|
150
|
+
VoipCloud.on('callMediaState', e => /* { callId, hasAudio, isHold, isMuted } */);
|
|
151
|
+
```
|
|
152
|
+
|
|
153
|
+
Per-action xác nhận (bridge phát sau khi action thành công — tiện cho
|
|
154
|
+
UI bind 1-1 với từng nút bấm):
|
|
155
|
+
|
|
156
|
+
```ts
|
|
157
|
+
VoipCloud.on('callHold', e => /* { callId } */);
|
|
158
|
+
VoipCloud.on('callUnhold', e => /* { callId } */);
|
|
159
|
+
VoipCloud.on('callMute', e => /* { callId, isMuted } */);
|
|
160
|
+
VoipCloud.on('callDtmfSent', e => /* { callId, digits } */);
|
|
161
|
+
VoipCloud.on('callTransfer', e => /* { callId, destinationUri } */);
|
|
162
|
+
```
|
|
163
|
+
|
|
164
|
+
Remote-driven (PJSIP forward từ callback gốc):
|
|
165
|
+
|
|
166
|
+
```ts
|
|
167
|
+
// REFER multi-stage: nhận một event mỗi NOTIFY; finalNotify=true là kết quả cuối.
|
|
168
|
+
VoipCloud.on('callTransferStatus', e => /* { callId, statusCode, reason, finalNotify } */);
|
|
169
|
+
|
|
170
|
+
// DTMF do đầu remote gửi qua RTP (RFC 2833).
|
|
171
|
+
VoipCloud.on('incomingDtmf', e => /* { callId, digit, duration } */);
|
|
172
|
+
```
|
|
@@ -0,0 +1,61 @@
|
|
|
1
|
+
// react-native-voipcloud — Android library module.
|
|
2
|
+
//
|
|
3
|
+
// Consumer apps that include this package via Gradle pull in:
|
|
4
|
+
// * com.voipcloud.sdk.* from voipcloud-release.aar
|
|
5
|
+
// * com.voipcloud.sdk.rn.* React Native bridge classes (this module)
|
|
6
|
+
//
|
|
7
|
+
// The AAR ships its own ProGuard rules (proguard.txt) which R8 auto-
|
|
8
|
+
// applies to the consumer release build; no extra setup needed.
|
|
9
|
+
|
|
10
|
+
buildscript {
|
|
11
|
+
repositories {
|
|
12
|
+
google()
|
|
13
|
+
mavenCentral()
|
|
14
|
+
}
|
|
15
|
+
dependencies {
|
|
16
|
+
// Pinned to the AGP version known to compile with React Native
|
|
17
|
+
// 0.74 + AS Iguana. Bump in lockstep with peerDependencies.
|
|
18
|
+
classpath 'com.android.tools.build:gradle:8.2.1'
|
|
19
|
+
}
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
apply plugin: 'com.android.library'
|
|
23
|
+
|
|
24
|
+
def safeExtGet(prop, fallback) {
|
|
25
|
+
rootProject.ext.has(prop) ? rootProject.ext.get(prop) : fallback
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
android {
|
|
29
|
+
namespace 'com.voipcloud.sdk.rn'
|
|
30
|
+
compileSdkVersion safeExtGet('compileSdkVersion', 34)
|
|
31
|
+
|
|
32
|
+
defaultConfig {
|
|
33
|
+
minSdkVersion safeExtGet('minSdkVersion', 31)
|
|
34
|
+
targetSdkVersion safeExtGet('targetSdkVersion', 34)
|
|
35
|
+
|
|
36
|
+
ndk {
|
|
37
|
+
// The AAR ships native libs for these three ABIs only.
|
|
38
|
+
abiFilters 'arm64-v8a', 'armeabi-v7a', 'x86_64'
|
|
39
|
+
}
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
compileOptions {
|
|
43
|
+
sourceCompatibility JavaVersion.VERSION_17
|
|
44
|
+
targetCompatibility JavaVersion.VERSION_17
|
|
45
|
+
}
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
repositories {
|
|
49
|
+
google()
|
|
50
|
+
mavenCentral()
|
|
51
|
+
flatDir { dirs 'src/main/libs' }
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
dependencies {
|
|
55
|
+
// Prebuilt PJSIP + license layer (com.voipcloud.sdk.*).
|
|
56
|
+
implementation(name: 'voipcloud-release', ext: 'aar')
|
|
57
|
+
|
|
58
|
+
// Pin to a permissive range — the consumer app controls the actual
|
|
59
|
+
// React Native version via its own classpath.
|
|
60
|
+
implementation 'com.facebook.react:react-native:+'
|
|
61
|
+
}
|
|
@@ -0,0 +1,428 @@
|
|
|
1
|
+
package com.voipcloud.sdk.rn;
|
|
2
|
+
|
|
3
|
+
import android.provider.Settings;
|
|
4
|
+
import androidx.annotation.NonNull;
|
|
5
|
+
|
|
6
|
+
import com.facebook.react.bridge.Arguments;
|
|
7
|
+
import com.facebook.react.bridge.Promise;
|
|
8
|
+
import com.facebook.react.bridge.ReactApplicationContext;
|
|
9
|
+
import com.facebook.react.bridge.ReactContextBaseJavaModule;
|
|
10
|
+
import com.facebook.react.bridge.ReactMethod;
|
|
11
|
+
import com.facebook.react.bridge.ReadableMap;
|
|
12
|
+
import com.facebook.react.bridge.WritableMap;
|
|
13
|
+
|
|
14
|
+
import com.voipcloud.sdk.AccountConfig;
|
|
15
|
+
import com.voipcloud.sdk.AuthCredInfo;
|
|
16
|
+
import com.voipcloud.sdk.CallOpParam;
|
|
17
|
+
import com.voipcloud.sdk.CallSendDtmfParam;
|
|
18
|
+
import com.voipcloud.sdk.Endpoint;
|
|
19
|
+
import com.voipcloud.sdk.EpConfig;
|
|
20
|
+
import com.voipcloud.sdk.License;
|
|
21
|
+
import com.voipcloud.sdk.LicenseRefresher;
|
|
22
|
+
import com.voipcloud.sdk.TransportConfig;
|
|
23
|
+
import com.voipcloud.sdk.pjsip_status_code;
|
|
24
|
+
import com.voipcloud.sdk.pjsip_transport_type_e;
|
|
25
|
+
import com.voipcloud.sdk.pjsua_destroy_flag;
|
|
26
|
+
|
|
27
|
+
import com.voipcloud.sdk.rn.internal.Events;
|
|
28
|
+
import com.voipcloud.sdk.rn.internal.ManagedAccount;
|
|
29
|
+
import com.voipcloud.sdk.rn.internal.ManagedCall;
|
|
30
|
+
import com.voipcloud.sdk.rn.internal.ManagedCallRegistry;
|
|
31
|
+
|
|
32
|
+
import java.util.concurrent.ConcurrentHashMap;
|
|
33
|
+
import java.util.concurrent.atomic.AtomicInteger;
|
|
34
|
+
|
|
35
|
+
/**
|
|
36
|
+
* React Native bridge for the VoipCloud SDK. All methods are async
|
|
37
|
+
* Promises; license-gate failures rejecting with code "VC_<ENUM>".
|
|
38
|
+
*/
|
|
39
|
+
public class VoipCloudModule extends ReactContextBaseJavaModule {
|
|
40
|
+
|
|
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
|
+
private final ReactApplicationContext ctx;
|
|
50
|
+
private final ManagedCallRegistry calls = new ManagedCallRegistry();
|
|
51
|
+
private final ConcurrentHashMap<Integer, ManagedAccount> accounts = new ConcurrentHashMap<>();
|
|
52
|
+
private final AtomicInteger nextAccKey = new AtomicInteger(1);
|
|
53
|
+
|
|
54
|
+
private Endpoint endpoint;
|
|
55
|
+
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
|
+
|
|
62
|
+
public VoipCloudModule(ReactApplicationContext ctx) {
|
|
63
|
+
super(ctx);
|
|
64
|
+
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
|
+
}
|
|
72
|
+
|
|
73
|
+
@NonNull
|
|
74
|
+
@Override
|
|
75
|
+
public String getName() { return "VoipCloud"; }
|
|
76
|
+
|
|
77
|
+
// ============== License ============================================
|
|
78
|
+
|
|
79
|
+
@ReactMethod
|
|
80
|
+
public void setLicense(String token, Promise promise) {
|
|
81
|
+
try {
|
|
82
|
+
License.set(token);
|
|
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;
|
|
132
|
+
} 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
|
+
try {
|
|
157
|
+
WritableMap m = Arguments.createMap();
|
|
158
|
+
m.putBoolean("valid", License.isValid());
|
|
159
|
+
m.putString ("status", License.status());
|
|
160
|
+
m.putString ("customerId", License.customerId());
|
|
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
|
+
}
|
|
169
|
+
}
|
|
170
|
+
|
|
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
|
+
@ReactMethod
|
|
178
|
+
public void stopLicenseRefresh(Promise promise) {
|
|
179
|
+
stopRefresherInternal();
|
|
180
|
+
promise.resolve(null);
|
|
181
|
+
}
|
|
182
|
+
|
|
183
|
+
// ============== Endpoint lifecycle =================================
|
|
184
|
+
|
|
185
|
+
@ReactMethod
|
|
186
|
+
public void init(ReadableMap config, Promise promise) {
|
|
187
|
+
try {
|
|
188
|
+
if (inited) { promise.resolve(null); return; }
|
|
189
|
+
endpoint = new Endpoint();
|
|
190
|
+
endpoint.libCreate();
|
|
191
|
+
|
|
192
|
+
EpConfig epCfg = new EpConfig();
|
|
193
|
+
if (config.hasKey("userAgent"))
|
|
194
|
+
epCfg.getUaConfig().setUserAgent(config.getString("userAgent"));
|
|
195
|
+
if (config.hasKey("stunServer")) {
|
|
196
|
+
com.voipcloud.sdk.StringVector v = new com.voipcloud.sdk.StringVector();
|
|
197
|
+
v.add(config.getString("stunServer"));
|
|
198
|
+
epCfg.getUaConfig().setStunServer(v);
|
|
199
|
+
}
|
|
200
|
+
if (config.hasKey("logLevel"))
|
|
201
|
+
epCfg.getLogConfig().setLevel(config.getInt("logLevel"));
|
|
202
|
+
|
|
203
|
+
endpoint.libInit(epCfg);
|
|
204
|
+
|
|
205
|
+
// Default transports — TLS handled per-account via registrar URI.
|
|
206
|
+
TransportConfig udp = new TransportConfig();
|
|
207
|
+
endpoint.transportCreate(pjsip_transport_type_e.PJSIP_TRANSPORT_UDP, udp);
|
|
208
|
+
TransportConfig tcp = new TransportConfig();
|
|
209
|
+
endpoint.transportCreate(pjsip_transport_type_e.PJSIP_TRANSPORT_TCP, tcp);
|
|
210
|
+
|
|
211
|
+
endpoint.libStart();
|
|
212
|
+
inited = true;
|
|
213
|
+
promise.resolve(null);
|
|
214
|
+
} catch (Throwable t) {
|
|
215
|
+
promise.reject("VC_INIT", t.getMessage(), t);
|
|
216
|
+
}
|
|
217
|
+
}
|
|
218
|
+
|
|
219
|
+
@ReactMethod
|
|
220
|
+
public void shutdown(Promise promise) {
|
|
221
|
+
try {
|
|
222
|
+
calls.clear();
|
|
223
|
+
for (ManagedAccount a : accounts.values()) a.delete();
|
|
224
|
+
accounts.clear();
|
|
225
|
+
if (endpoint != null) {
|
|
226
|
+
endpoint.libDestroy(pjsua_destroy_flag.PJSUA_DESTROY_NO_NETWORK);
|
|
227
|
+
endpoint.delete();
|
|
228
|
+
endpoint = null;
|
|
229
|
+
}
|
|
230
|
+
inited = false;
|
|
231
|
+
promise.resolve(null);
|
|
232
|
+
} catch (Throwable t) {
|
|
233
|
+
promise.reject("VC_SHUTDOWN", t.getMessage(), t);
|
|
234
|
+
}
|
|
235
|
+
}
|
|
236
|
+
|
|
237
|
+
// ============== Account ============================================
|
|
238
|
+
|
|
239
|
+
@ReactMethod
|
|
240
|
+
public void register(ReadableMap cfg, Promise promise) {
|
|
241
|
+
try {
|
|
242
|
+
AccountConfig acfg = new AccountConfig();
|
|
243
|
+
acfg.setIdUri(cfg.getString("uri"));
|
|
244
|
+
acfg.getRegConfig().setRegistrarUri(cfg.getString("registrar"));
|
|
245
|
+
|
|
246
|
+
AuthCredInfo cred = new AuthCredInfo(
|
|
247
|
+
"digest", "*",
|
|
248
|
+
cfg.getString("username"), 0,
|
|
249
|
+
cfg.getString("password"));
|
|
250
|
+
acfg.getSipConfig().getAuthCreds().add(cred);
|
|
251
|
+
|
|
252
|
+
ManagedAccount acc = new ManagedAccount(ctx, calls);
|
|
253
|
+
acc.create(acfg);
|
|
254
|
+
int id = acc.accountId();
|
|
255
|
+
accounts.put(id, acc);
|
|
256
|
+
promise.resolve(id);
|
|
257
|
+
} catch (Throwable t) {
|
|
258
|
+
promise.reject("VC_REGISTER", t.getMessage(), t);
|
|
259
|
+
}
|
|
260
|
+
}
|
|
261
|
+
|
|
262
|
+
@ReactMethod
|
|
263
|
+
public void unregister(int accountId, Promise promise) {
|
|
264
|
+
try {
|
|
265
|
+
ManagedAccount acc = accounts.remove(accountId);
|
|
266
|
+
if (acc != null) {
|
|
267
|
+
acc.setRegistration(false);
|
|
268
|
+
acc.delete();
|
|
269
|
+
}
|
|
270
|
+
promise.resolve(null);
|
|
271
|
+
} catch (Throwable t) {
|
|
272
|
+
promise.reject("VC_UNREGISTER", t.getMessage(), t);
|
|
273
|
+
}
|
|
274
|
+
}
|
|
275
|
+
|
|
276
|
+
// ============== Call ===============================================
|
|
277
|
+
|
|
278
|
+
@ReactMethod
|
|
279
|
+
public void makeCall(int accountId, String uri, Promise promise) {
|
|
280
|
+
try {
|
|
281
|
+
ManagedAccount acc = accounts.get(accountId);
|
|
282
|
+
if (acc == null) { promise.reject("VC_NO_ACCOUNT", "unknown accountId"); return; }
|
|
283
|
+
ManagedCall call = new ManagedCall(ctx, acc);
|
|
284
|
+
call.makeCall(uri, new CallOpParam(true));
|
|
285
|
+
calls.put(call);
|
|
286
|
+
promise.resolve(call.callId());
|
|
287
|
+
} catch (Throwable t) {
|
|
288
|
+
promise.reject("VC_MAKE_CALL", t.getMessage(), t);
|
|
289
|
+
}
|
|
290
|
+
}
|
|
291
|
+
|
|
292
|
+
@ReactMethod
|
|
293
|
+
public void answer(int callId, Promise promise) {
|
|
294
|
+
try {
|
|
295
|
+
ManagedCall call = calls.get(callId);
|
|
296
|
+
if (call == null) { promise.reject("VC_NO_CALL", "unknown callId"); return; }
|
|
297
|
+
CallOpParam op = new CallOpParam();
|
|
298
|
+
op.setStatusCode(pjsip_status_code.PJSIP_SC_OK);
|
|
299
|
+
call.answer(op);
|
|
300
|
+
promise.resolve(null);
|
|
301
|
+
} catch (Throwable t) {
|
|
302
|
+
promise.reject("VC_ANSWER", t.getMessage(), t);
|
|
303
|
+
}
|
|
304
|
+
}
|
|
305
|
+
|
|
306
|
+
@ReactMethod
|
|
307
|
+
public void hangup(int callId, Promise promise) {
|
|
308
|
+
try {
|
|
309
|
+
ManagedCall call = calls.get(callId);
|
|
310
|
+
if (call == null) { promise.resolve(null); return; }
|
|
311
|
+
CallOpParam op = new CallOpParam();
|
|
312
|
+
op.setStatusCode(pjsip_status_code.PJSIP_SC_DECLINE);
|
|
313
|
+
call.hangup(op);
|
|
314
|
+
calls.remove(callId);
|
|
315
|
+
promise.resolve(null);
|
|
316
|
+
} catch (Throwable t) {
|
|
317
|
+
promise.reject("VC_HANGUP", t.getMessage(), t);
|
|
318
|
+
}
|
|
319
|
+
}
|
|
320
|
+
|
|
321
|
+
@ReactMethod
|
|
322
|
+
public void hold(int callId, Promise promise) {
|
|
323
|
+
try {
|
|
324
|
+
ManagedCall call = calls.get(callId);
|
|
325
|
+
if (call == null) { promise.reject("VC_NO_CALL", "unknown callId"); return; }
|
|
326
|
+
call.setHold(new CallOpParam(true));
|
|
327
|
+
WritableMap ev = Arguments.createMap();
|
|
328
|
+
ev.putInt("callId", callId);
|
|
329
|
+
Events.emit(getReactApplicationContext(), Events.CALL_HOLD, ev);
|
|
330
|
+
promise.resolve(null);
|
|
331
|
+
} catch (Throwable t) {
|
|
332
|
+
promise.reject("VC_HOLD", t.getMessage(), t);
|
|
333
|
+
}
|
|
334
|
+
}
|
|
335
|
+
|
|
336
|
+
@ReactMethod
|
|
337
|
+
public void unhold(int callId, Promise promise) {
|
|
338
|
+
try {
|
|
339
|
+
ManagedCall call = calls.get(callId);
|
|
340
|
+
if (call == null) { promise.reject("VC_NO_CALL", "unknown callId"); return; }
|
|
341
|
+
// PJSUA2 re-INVITE without an explicit hold flag re-offers
|
|
342
|
+
// sendrecv SDP; that releases the existing hold.
|
|
343
|
+
call.reinvite(new CallOpParam(true));
|
|
344
|
+
WritableMap ev = Arguments.createMap();
|
|
345
|
+
ev.putInt("callId", callId);
|
|
346
|
+
Events.emit(getReactApplicationContext(), Events.CALL_UNHOLD, ev);
|
|
347
|
+
promise.resolve(null);
|
|
348
|
+
} catch (Throwable t) {
|
|
349
|
+
promise.reject("VC_UNHOLD", t.getMessage(), t);
|
|
350
|
+
}
|
|
351
|
+
}
|
|
352
|
+
|
|
353
|
+
@ReactMethod
|
|
354
|
+
public void mute(int callId, boolean on, Promise promise) {
|
|
355
|
+
try {
|
|
356
|
+
ManagedCall call = calls.get(callId);
|
|
357
|
+
if (call == null) { promise.reject("VC_NO_CALL", "unknown callId"); return; }
|
|
358
|
+
if (endpoint == null) { promise.reject("VC_INIT", "endpoint not started"); return; }
|
|
359
|
+
call.setMuted(endpoint, on);
|
|
360
|
+
WritableMap ev = Arguments.createMap();
|
|
361
|
+
ev.putInt("callId", callId);
|
|
362
|
+
ev.putBoolean("isMuted", on);
|
|
363
|
+
Events.emit(getReactApplicationContext(), Events.CALL_MUTE, ev);
|
|
364
|
+
promise.resolve(null);
|
|
365
|
+
} catch (Throwable t) {
|
|
366
|
+
promise.reject("VC_MUTE", t.getMessage(), t);
|
|
367
|
+
}
|
|
368
|
+
}
|
|
369
|
+
|
|
370
|
+
@ReactMethod
|
|
371
|
+
public void sendDtmf(int callId, String digits, Promise promise) {
|
|
372
|
+
try {
|
|
373
|
+
ManagedCall call = calls.get(callId);
|
|
374
|
+
if (call == null) { promise.reject("VC_NO_CALL", "unknown callId"); return; }
|
|
375
|
+
CallSendDtmfParam p = new CallSendDtmfParam();
|
|
376
|
+
p.setDigits(digits);
|
|
377
|
+
call.sendDtmf(p);
|
|
378
|
+
WritableMap ev = Arguments.createMap();
|
|
379
|
+
ev.putInt("callId", callId);
|
|
380
|
+
ev.putString("digits", digits);
|
|
381
|
+
Events.emit(getReactApplicationContext(), Events.CALL_DTMF_SENT, ev);
|
|
382
|
+
promise.resolve(null);
|
|
383
|
+
} catch (Throwable t) {
|
|
384
|
+
promise.reject("VC_DTMF", t.getMessage(), t);
|
|
385
|
+
}
|
|
386
|
+
}
|
|
387
|
+
|
|
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
|
+
@ReactMethod
|
|
394
|
+
public void transferCall(int callId, String destinationUri, Promise promise) {
|
|
395
|
+
try {
|
|
396
|
+
ManagedCall call = calls.get(callId);
|
|
397
|
+
if (call == null) { promise.reject("VC_NO_CALL", "unknown callId"); return; }
|
|
398
|
+
call.xfer(destinationUri, new CallOpParam(true));
|
|
399
|
+
WritableMap ev = Arguments.createMap();
|
|
400
|
+
ev.putInt("callId", callId);
|
|
401
|
+
ev.putString("destinationUri", destinationUri);
|
|
402
|
+
Events.emit(getReactApplicationContext(), Events.CALL_TRANSFER, ev);
|
|
403
|
+
promise.resolve(null);
|
|
404
|
+
} catch (Throwable t) {
|
|
405
|
+
promise.reject("VC_TRANSFER", t.getMessage(), t);
|
|
406
|
+
}
|
|
407
|
+
}
|
|
408
|
+
|
|
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
|
+
@ReactMethod
|
|
415
|
+
public void hangupAllCalls(Promise promise) {
|
|
416
|
+
try {
|
|
417
|
+
if (endpoint != null) endpoint.hangupAllCalls();
|
|
418
|
+
promise.resolve(null);
|
|
419
|
+
} catch (Throwable t) {
|
|
420
|
+
promise.reject("VC_HANGUP_ALL", t.getMessage(), t);
|
|
421
|
+
}
|
|
422
|
+
}
|
|
423
|
+
|
|
424
|
+
// RN event-emitter contract — required on RN 0.65+ when used with
|
|
425
|
+
// NativeEventEmitter on the JS side.
|
|
426
|
+
@ReactMethod public void addListener(String eventName) { /* no-op */ }
|
|
427
|
+
@ReactMethod public void removeListeners(int count) { /* no-op */ }
|
|
428
|
+
}
|
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
package com.voipcloud.sdk.rn;
|
|
2
|
+
|
|
3
|
+
import com.facebook.react.ReactPackage;
|
|
4
|
+
import com.facebook.react.bridge.NativeModule;
|
|
5
|
+
import com.facebook.react.bridge.ReactApplicationContext;
|
|
6
|
+
import com.facebook.react.uimanager.ViewManager;
|
|
7
|
+
|
|
8
|
+
import java.util.Collections;
|
|
9
|
+
import java.util.List;
|
|
10
|
+
|
|
11
|
+
public class VoipCloudPackage implements ReactPackage {
|
|
12
|
+
@Override
|
|
13
|
+
public List<NativeModule> createNativeModules(ReactApplicationContext ctx) {
|
|
14
|
+
return Collections.singletonList(new VoipCloudModule(ctx));
|
|
15
|
+
}
|
|
16
|
+
|
|
17
|
+
@Override
|
|
18
|
+
public List<ViewManager> createViewManagers(ReactApplicationContext ctx) {
|
|
19
|
+
return Collections.emptyList();
|
|
20
|
+
}
|
|
21
|
+
}
|