@kapsula-chat/capacitor-push-calls 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/Package.swift +31 -0
- package/README.md +252 -0
- package/android/build.gradle +46 -0
- package/android/src/main/AndroidManifest.xml +31 -0
- package/android/src/main/java/com/capacitor/voipcalls/CallManager.kt +213 -0
- package/android/src/main/java/com/capacitor/voipcalls/CapacitorVoipCallsPlugin.kt +584 -0
- package/android/src/main/java/com/capacitor/voipcalls/PushRouterMessagingService.kt +26 -0
- package/android/src/main/java/com/capacitor/voipcalls/VoipConnection.kt +112 -0
- package/android/src/main/java/com/capacitor/voipcalls/VoipConnectionService.kt +101 -0
- package/dist/esm/definitions.d.ts +279 -0
- package/dist/esm/definitions.d.ts.map +1 -0
- package/dist/esm/definitions.js +1 -0
- package/dist/esm/index.d.ts +5 -0
- package/dist/esm/index.d.ts.map +1 -0
- package/dist/esm/index.js +6 -0
- package/dist/esm/web.d.ts +67 -0
- package/dist/esm/web.d.ts.map +1 -0
- package/dist/esm/web.js +81 -0
- package/dist/plugin.cjs.js +96 -0
- package/dist/plugin.cjs.js.map +1 -0
- package/dist/plugin.js +99 -0
- package/dist/plugin.js.map +1 -0
- package/ios/Plugin/CallManager.swift +226 -0
- package/ios/Plugin/CapacitorVoipCallsPlugin.swift +517 -0
- package/ios/Plugin/Plugin.m +31 -0
- package/package.json +95 -0
package/Package.swift
ADDED
|
@@ -0,0 +1,31 @@
|
|
|
1
|
+
// swift-tools-version: 5.9
|
|
2
|
+
import PackageDescription
|
|
3
|
+
|
|
4
|
+
let package = Package(
|
|
5
|
+
name: "CapacitorPushCalls",
|
|
6
|
+
platforms: [.iOS(.v13)],
|
|
7
|
+
products: [
|
|
8
|
+
.library(
|
|
9
|
+
name: "CapacitorPushCalls",
|
|
10
|
+
targets: ["CapacitorPushCallsPlugin"])
|
|
11
|
+
],
|
|
12
|
+
dependencies: [
|
|
13
|
+
.package(url: "https://github.com/ionic-team/capacitor-swift-pm.git", branch: "main")
|
|
14
|
+
],
|
|
15
|
+
targets: [
|
|
16
|
+
.target(
|
|
17
|
+
name: "CapacitorPushCallsPlugin",
|
|
18
|
+
dependencies: [
|
|
19
|
+
.product(name: "Capacitor", package: "capacitor-swift-pm"),
|
|
20
|
+
.product(name: "Cordova", package: "capacitor-swift-pm")
|
|
21
|
+
],
|
|
22
|
+
path: "ios/Plugin",
|
|
23
|
+
exclude: ["Plugin.m"],
|
|
24
|
+
sources: [
|
|
25
|
+
"CallManager.swift",
|
|
26
|
+
"CapacitorVoipCallsPlugin.swift"
|
|
27
|
+
],
|
|
28
|
+
publicHeadersPath: "."
|
|
29
|
+
)
|
|
30
|
+
]
|
|
31
|
+
)
|
package/README.md
ADDED
|
@@ -0,0 +1,252 @@
|
|
|
1
|
+
# Capacitor Push Calls Plugin
|
|
2
|
+
|
|
3
|
+
`@kapsula-chat/capacitor-push-calls` is a unified Capacitor plugin for:
|
|
4
|
+
- regular push notifications (API compatible in shape with `@capacitor/push-notifications`)
|
|
5
|
+
- VoIP/telephony call handling
|
|
6
|
+
|
|
7
|
+
Platform behavior:
|
|
8
|
+
- iOS: regular push via APNS registration + VoIP via PushKit + CallKit UI
|
|
9
|
+
- Android: single FCM router service inside this plugin (`type=message|call`) + ConnectionService UI for calls
|
|
10
|
+
|
|
11
|
+
## Install
|
|
12
|
+
|
|
13
|
+
```bash
|
|
14
|
+
npm install @kapsula-chat/capacitor-push-calls
|
|
15
|
+
npx cap sync
|
|
16
|
+
```
|
|
17
|
+
|
|
18
|
+
## Add Plugin To App (iOS + Android)
|
|
19
|
+
|
|
20
|
+
1. Install and sync:
|
|
21
|
+
```bash
|
|
22
|
+
npm install @kapsula-chat/capacitor-push-calls
|
|
23
|
+
npx cap sync
|
|
24
|
+
```
|
|
25
|
+
2. Open native projects at least once so Capacitor updates plugin wiring:
|
|
26
|
+
```bash
|
|
27
|
+
npx cap open ios
|
|
28
|
+
npx cap open android
|
|
29
|
+
```
|
|
30
|
+
3. iOS:
|
|
31
|
+
- Ensure VoIP background mode is enabled in `Info.plist` (see iOS section below).
|
|
32
|
+
- If you use FCM for regular pushes on iOS, keep your APNs + Firebase setup configured in the app.
|
|
33
|
+
4. Android:
|
|
34
|
+
- Ensure your app is configured for Firebase Messaging (`google-services.json`, Firebase project).
|
|
35
|
+
- Register the phone account once on app startup (recommended):
|
|
36
|
+
|
|
37
|
+
```kotlin
|
|
38
|
+
import com.capacitor.voipcalls.VoipConnectionService
|
|
39
|
+
|
|
40
|
+
override fun onCreate(savedInstanceState: Bundle?) {
|
|
41
|
+
super.onCreate(savedInstanceState)
|
|
42
|
+
VoipConnectionService.registerPhoneAccount(this)
|
|
43
|
+
}
|
|
44
|
+
```
|
|
45
|
+
|
|
46
|
+
## Import
|
|
47
|
+
|
|
48
|
+
```ts
|
|
49
|
+
import { CapacitorPushCalls } from '@kapsula-chat/capacitor-push-calls';
|
|
50
|
+
```
|
|
51
|
+
|
|
52
|
+
## Push API (regular notifications)
|
|
53
|
+
|
|
54
|
+
### Register / permissions
|
|
55
|
+
|
|
56
|
+
```ts
|
|
57
|
+
const permissions = await CapacitorPushCalls.checkPermissions();
|
|
58
|
+
if (permissions.receive !== 'granted') {
|
|
59
|
+
await CapacitorPushCalls.requestPermissions();
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
await CapacitorPushCalls.register();
|
|
63
|
+
|
|
64
|
+
CapacitorPushCalls.addListener('registration', token => {
|
|
65
|
+
console.log('push token:', token.value);
|
|
66
|
+
// Send token to backend and bind with authenticated user/device
|
|
67
|
+
});
|
|
68
|
+
|
|
69
|
+
CapacitorPushCalls.addListener('registrationError', error => {
|
|
70
|
+
console.error('registration error:', error.error);
|
|
71
|
+
});
|
|
72
|
+
```
|
|
73
|
+
|
|
74
|
+
### Receive and tap events
|
|
75
|
+
|
|
76
|
+
```ts
|
|
77
|
+
CapacitorPushCalls.addListener('pushNotificationReceived', notification => {
|
|
78
|
+
console.log('push received:', notification);
|
|
79
|
+
});
|
|
80
|
+
|
|
81
|
+
CapacitorPushCalls.addListener('pushNotificationActionPerformed', action => {
|
|
82
|
+
console.log('push action:', action.actionId, action.notification);
|
|
83
|
+
});
|
|
84
|
+
```
|
|
85
|
+
|
|
86
|
+
### Delivered notifications and channels
|
|
87
|
+
|
|
88
|
+
```ts
|
|
89
|
+
const delivered = await CapacitorPushCalls.getDeliveredNotifications();
|
|
90
|
+
await CapacitorPushCalls.removeAllDeliveredNotifications();
|
|
91
|
+
|
|
92
|
+
await CapacitorPushCalls.createChannel({
|
|
93
|
+
id: 'messages',
|
|
94
|
+
name: 'Messages',
|
|
95
|
+
importance: 4,
|
|
96
|
+
});
|
|
97
|
+
|
|
98
|
+
const channels = await CapacitorPushCalls.listChannels();
|
|
99
|
+
await CapacitorPushCalls.deleteChannel({ id: 'messages' });
|
|
100
|
+
```
|
|
101
|
+
|
|
102
|
+
Note: channel APIs are Android-only (iOS returns no-op / empty list).
|
|
103
|
+
|
|
104
|
+
### Badge count
|
|
105
|
+
|
|
106
|
+
```ts
|
|
107
|
+
const current = await CapacitorPushCalls.getBadgeCount();
|
|
108
|
+
await CapacitorPushCalls.setBadgeCount({ count: current.count + 1 });
|
|
109
|
+
await CapacitorPushCalls.clearBadgeCount();
|
|
110
|
+
```
|
|
111
|
+
|
|
112
|
+
## Android Device Registration
|
|
113
|
+
|
|
114
|
+
Recommended bootstrap flow on Android:
|
|
115
|
+
|
|
116
|
+
```ts
|
|
117
|
+
const permissions = await CapacitorPushCalls.checkPermissions();
|
|
118
|
+
if (permissions.receive !== 'granted') {
|
|
119
|
+
await CapacitorPushCalls.requestPermissions();
|
|
120
|
+
}
|
|
121
|
+
|
|
122
|
+
await CapacitorPushCalls.register();
|
|
123
|
+
|
|
124
|
+
CapacitorPushCalls.addListener('registration', async ({ value }) => {
|
|
125
|
+
// Persist FCM token on backend for this device/session
|
|
126
|
+
await api.registerPushDevice({ token: value, platform: 'android' });
|
|
127
|
+
});
|
|
128
|
+
```
|
|
129
|
+
|
|
130
|
+
Use the same `registration` flow on iOS for regular push token registration.
|
|
131
|
+
|
|
132
|
+
## VoIP / calls API
|
|
133
|
+
|
|
134
|
+
### iOS VoIP registration
|
|
135
|
+
|
|
136
|
+
```ts
|
|
137
|
+
await CapacitorPushCalls.registerVoipNotifications();
|
|
138
|
+
|
|
139
|
+
CapacitorPushCalls.addListener('voipPushToken', ({ token }) => {
|
|
140
|
+
console.log('voip token:', token);
|
|
141
|
+
});
|
|
142
|
+
```
|
|
143
|
+
|
|
144
|
+
### Start / control calls
|
|
145
|
+
|
|
146
|
+
```ts
|
|
147
|
+
const { callId } = await CapacitorPushCalls.startCall({
|
|
148
|
+
handle: '+1234567890',
|
|
149
|
+
displayName: 'John Doe',
|
|
150
|
+
handleType: 'phone',
|
|
151
|
+
video: false,
|
|
152
|
+
});
|
|
153
|
+
|
|
154
|
+
await CapacitorPushCalls.setMuted({ muted: true });
|
|
155
|
+
await CapacitorPushCalls.setAudioRoute({ route: 'speaker' });
|
|
156
|
+
await CapacitorPushCalls.setCallOnHold({ callId, onHold: true });
|
|
157
|
+
await CapacitorPushCalls.endCall({ callId });
|
|
158
|
+
```
|
|
159
|
+
|
|
160
|
+
### Call events
|
|
161
|
+
|
|
162
|
+
```ts
|
|
163
|
+
CapacitorPushCalls.addListener('incomingCall', call => {});
|
|
164
|
+
CapacitorPushCalls.addListener('callStarted', call => {});
|
|
165
|
+
CapacitorPushCalls.addListener('callAnswered', call => {});
|
|
166
|
+
CapacitorPushCalls.addListener('callEnded', call => {});
|
|
167
|
+
CapacitorPushCalls.addListener('callRejected', call => {});
|
|
168
|
+
CapacitorPushCalls.addListener('callHeld', call => {});
|
|
169
|
+
```
|
|
170
|
+
|
|
171
|
+
## Android FCM routing contract
|
|
172
|
+
|
|
173
|
+
This plugin owns one `FirebaseMessagingService` and routes by `data.type`.
|
|
174
|
+
|
|
175
|
+
- `type=call` -> reported as incoming call to system UI
|
|
176
|
+
- anything else (or missing `type`) -> emitted as `pushNotificationReceived`
|
|
177
|
+
|
|
178
|
+
Example FCM message payloads:
|
|
179
|
+
|
|
180
|
+
### Message push
|
|
181
|
+
|
|
182
|
+
```json
|
|
183
|
+
{
|
|
184
|
+
"data": {
|
|
185
|
+
"type": "message",
|
|
186
|
+
"messageId": "msg-123",
|
|
187
|
+
"chatId": "room-7"
|
|
188
|
+
},
|
|
189
|
+
"notification": {
|
|
190
|
+
"title": "New message",
|
|
191
|
+
"body": "Hello"
|
|
192
|
+
}
|
|
193
|
+
}
|
|
194
|
+
```
|
|
195
|
+
|
|
196
|
+
### Call push
|
|
197
|
+
|
|
198
|
+
```json
|
|
199
|
+
{
|
|
200
|
+
"data": {
|
|
201
|
+
"type": "call",
|
|
202
|
+
"callId": "call-123",
|
|
203
|
+
"handle": "+1234567890",
|
|
204
|
+
"displayName": "John Doe",
|
|
205
|
+
"handleType": "phone",
|
|
206
|
+
"video": "false"
|
|
207
|
+
}
|
|
208
|
+
}
|
|
209
|
+
```
|
|
210
|
+
|
|
211
|
+
## Foreground Service (Android)
|
|
212
|
+
|
|
213
|
+
The plugin currently routes FCM and triggers ConnectionService/Call UI, but it does not run a long-lived custom foreground service for your app logic.
|
|
214
|
+
|
|
215
|
+
Use your own foreground service if you need:
|
|
216
|
+
- long-running signaling/reconnect loops in background,
|
|
217
|
+
- persistent background media/session processing,
|
|
218
|
+
- guaranteed ongoing processing beyond call UI handoff.
|
|
219
|
+
|
|
220
|
+
Typical split:
|
|
221
|
+
- plugin handles push ingress + system call UI bridge,
|
|
222
|
+
- app service handles long-lived networking/media responsibilities.
|
|
223
|
+
|
|
224
|
+
## iOS setup notes
|
|
225
|
+
|
|
226
|
+
Add VoIP background mode in `Info.plist`:
|
|
227
|
+
|
|
228
|
+
```xml
|
|
229
|
+
<key>UIBackgroundModes</key>
|
|
230
|
+
<array>
|
|
231
|
+
<string>voip</string>
|
|
232
|
+
</array>
|
|
233
|
+
```
|
|
234
|
+
|
|
235
|
+
Server side:
|
|
236
|
+
- regular push -> APNS/FCM token from `registration`
|
|
237
|
+
- VoIP call push -> PushKit token from `voipPushToken`
|
|
238
|
+
|
|
239
|
+
## Test app helper
|
|
240
|
+
|
|
241
|
+
Use included helper scripts and UI:
|
|
242
|
+
|
|
243
|
+
```bash
|
|
244
|
+
npm run test:setup
|
|
245
|
+
npm run test:update
|
|
246
|
+
```
|
|
247
|
+
|
|
248
|
+
The generated test app uses `scripts/test-app-ui.html`.
|
|
249
|
+
|
|
250
|
+
## API Types
|
|
251
|
+
|
|
252
|
+
See `/src/definitions.ts` for the complete TypeScript contract.
|
|
@@ -0,0 +1,46 @@
|
|
|
1
|
+
apply plugin: 'com.android.library'
|
|
2
|
+
apply plugin: 'kotlin-android'
|
|
3
|
+
|
|
4
|
+
android {
|
|
5
|
+
namespace "com.capacitor.voipcalls"
|
|
6
|
+
compileSdkVersion project.hasProperty('compileSdkVersion') ? rootProject.ext.compileSdkVersion : 33
|
|
7
|
+
defaultConfig {
|
|
8
|
+
minSdkVersion project.hasProperty('minSdkVersion') ? rootProject.ext.minSdkVersion : 23
|
|
9
|
+
targetSdkVersion project.hasProperty('targetSdkVersion') ? rootProject.ext.targetSdkVersion : 33
|
|
10
|
+
versionCode 1
|
|
11
|
+
versionName "1.0"
|
|
12
|
+
testInstrumentationRunner "androidx.test.runner.AndroidJUnitRunner"
|
|
13
|
+
}
|
|
14
|
+
buildTypes {
|
|
15
|
+
release {
|
|
16
|
+
minifyEnabled false
|
|
17
|
+
proguardFiles getDefaultProguardFile('proguard-android.txt'), 'proguard-rules.pro'
|
|
18
|
+
}
|
|
19
|
+
}
|
|
20
|
+
lintOptions {
|
|
21
|
+
abortOnError false
|
|
22
|
+
}
|
|
23
|
+
compileOptions {
|
|
24
|
+
sourceCompatibility JavaVersion.VERSION_17
|
|
25
|
+
targetCompatibility JavaVersion.VERSION_17
|
|
26
|
+
}
|
|
27
|
+
kotlinOptions {
|
|
28
|
+
jvmTarget = '17'
|
|
29
|
+
}
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
repositories {
|
|
33
|
+
google()
|
|
34
|
+
mavenCentral()
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
dependencies {
|
|
38
|
+
implementation fileTree(dir: 'libs', include: ['*.jar'])
|
|
39
|
+
implementation project(':capacitor-android')
|
|
40
|
+
implementation "androidx.appcompat:appcompat:$androidxAppCompatVersion"
|
|
41
|
+
implementation "org.jetbrains.kotlin:kotlin-stdlib:$kotlinVersion"
|
|
42
|
+
compileOnly "com.google.firebase:firebase-messaging:24.1.2"
|
|
43
|
+
testImplementation "junit:junit:$junitVersion"
|
|
44
|
+
androidTestImplementation "androidx.test.ext:junit:$androidxJunitVersion"
|
|
45
|
+
androidTestImplementation "androidx.test.espresso:espresso-core:$androidxEspressoCoreVersion"
|
|
46
|
+
}
|
|
@@ -0,0 +1,31 @@
|
|
|
1
|
+
<manifest xmlns:android="http://schemas.android.com/apk/res/android"
|
|
2
|
+
package="com.capacitor.voipcalls">
|
|
3
|
+
|
|
4
|
+
<uses-permission android:name="android.permission.RECORD_AUDIO" />
|
|
5
|
+
<uses-permission android:name="android.permission.CAMERA" />
|
|
6
|
+
<uses-permission android:name="android.permission.POST_NOTIFICATIONS" />
|
|
7
|
+
<uses-permission android:name="android.permission.MODIFY_AUDIO_SETTINGS" />
|
|
8
|
+
<uses-permission android:name="android.permission.BLUETOOTH" />
|
|
9
|
+
<uses-permission android:name="android.permission.BLUETOOTH_CONNECT" />
|
|
10
|
+
<uses-permission android:name="android.permission.READ_PHONE_STATE" />
|
|
11
|
+
<uses-permission android:name="android.permission.MANAGE_OWN_CALLS" />
|
|
12
|
+
|
|
13
|
+
<application>
|
|
14
|
+
<service
|
|
15
|
+
android:name=".VoipConnectionService"
|
|
16
|
+
android:permission="android.permission.BIND_TELECOM_CONNECTION_SERVICE"
|
|
17
|
+
android:exported="true">
|
|
18
|
+
<intent-filter>
|
|
19
|
+
<action android:name="android.telecom.ConnectionService" />
|
|
20
|
+
</intent-filter>
|
|
21
|
+
</service>
|
|
22
|
+
|
|
23
|
+
<service
|
|
24
|
+
android:name=".PushRouterMessagingService"
|
|
25
|
+
android:exported="false">
|
|
26
|
+
<intent-filter>
|
|
27
|
+
<action android:name="com.google.firebase.MESSAGING_EVENT" />
|
|
28
|
+
</intent-filter>
|
|
29
|
+
</service>
|
|
30
|
+
</application>
|
|
31
|
+
</manifest>
|
|
@@ -0,0 +1,213 @@
|
|
|
1
|
+
package com.capacitor.voipcalls
|
|
2
|
+
|
|
3
|
+
import android.content.Context
|
|
4
|
+
import android.media.AudioManager
|
|
5
|
+
import android.os.Build
|
|
6
|
+
import android.telecom.*
|
|
7
|
+
import androidx.annotation.RequiresApi
|
|
8
|
+
import com.getcapacitor.JSObject
|
|
9
|
+
|
|
10
|
+
@RequiresApi(Build.VERSION_CODES.M)
|
|
11
|
+
class CallManager(
|
|
12
|
+
private val context: Context,
|
|
13
|
+
private val eventEmitter: ((String, JSObject) -> Unit)? = null
|
|
14
|
+
) {
|
|
15
|
+
private val telecomManager: TelecomManager? =
|
|
16
|
+
context.getSystemService(Context.TELECOM_SERVICE) as? TelecomManager
|
|
17
|
+
|
|
18
|
+
private val activeCalls = mutableMapOf<String, VoipConnection>()
|
|
19
|
+
|
|
20
|
+
data class CallData(
|
|
21
|
+
val callId: String,
|
|
22
|
+
val handle: String,
|
|
23
|
+
val displayName: String,
|
|
24
|
+
val handleType: String,
|
|
25
|
+
val video: Boolean,
|
|
26
|
+
val metadata: JSObject? = null
|
|
27
|
+
)
|
|
28
|
+
|
|
29
|
+
constructor(plugin: CapacitorVoipCallsPlugin) : this(plugin.context, plugin::emitEvent)
|
|
30
|
+
|
|
31
|
+
fun startCall(callId: String, handle: String, displayName: String, handleType: String, video: Boolean) {
|
|
32
|
+
val phoneAccount = VoipConnectionService.getPhoneAccount(context)
|
|
33
|
+
|
|
34
|
+
val extras = android.os.Bundle().apply {
|
|
35
|
+
putString("callId", callId)
|
|
36
|
+
putString("handle", handle)
|
|
37
|
+
putString("displayName", displayName)
|
|
38
|
+
putString("handleType", handleType)
|
|
39
|
+
putBoolean("video", video)
|
|
40
|
+
putBoolean("isOutgoing", true)
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
val uri = android.net.Uri.fromParts("tel", handle, null)
|
|
44
|
+
val telecomExtras = android.os.Bundle().apply {
|
|
45
|
+
putParcelable(TelecomManager.EXTRA_PHONE_ACCOUNT_HANDLE, phoneAccount.accountHandle)
|
|
46
|
+
putInt(TelecomManager.EXTRA_START_CALL_WITH_VIDEO_STATE,
|
|
47
|
+
if (video) VideoProfile.STATE_BIDIRECTIONAL else VideoProfile.STATE_AUDIO_ONLY)
|
|
48
|
+
putBundle(TelecomManager.EXTRA_OUTGOING_CALL_EXTRAS, extras)
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
try {
|
|
52
|
+
telecomManager?.placeCall(uri, telecomExtras)
|
|
53
|
+
} catch (e: SecurityException) {
|
|
54
|
+
notifyError("Failed to place call: ${e.message}")
|
|
55
|
+
}
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
fun reportIncomingCall(
|
|
59
|
+
callId: String,
|
|
60
|
+
handle: String,
|
|
61
|
+
displayName: String,
|
|
62
|
+
handleType: String,
|
|
63
|
+
video: Boolean,
|
|
64
|
+
metadata: JSObject?
|
|
65
|
+
) {
|
|
66
|
+
val phoneAccount = VoipConnectionService.getPhoneAccount(context)
|
|
67
|
+
|
|
68
|
+
val extras = android.os.Bundle().apply {
|
|
69
|
+
putString("callId", callId)
|
|
70
|
+
putString("handle", handle)
|
|
71
|
+
putString("displayName", displayName)
|
|
72
|
+
putString("handleType", handleType)
|
|
73
|
+
putBoolean("video", video)
|
|
74
|
+
if (metadata != null) {
|
|
75
|
+
putString("metadata", metadata.toString())
|
|
76
|
+
}
|
|
77
|
+
}
|
|
78
|
+
|
|
79
|
+
val uri = android.net.Uri.fromParts("tel", handle, null)
|
|
80
|
+
|
|
81
|
+
try {
|
|
82
|
+
telecomManager?.addNewIncomingCall(phoneAccount.accountHandle, extras)
|
|
83
|
+
|
|
84
|
+
// Notify JavaScript layer
|
|
85
|
+
val data = JSObject().apply {
|
|
86
|
+
put("callId", callId)
|
|
87
|
+
put("handle", handle)
|
|
88
|
+
put("displayName", displayName)
|
|
89
|
+
put("handleType", handleType)
|
|
90
|
+
put("video", video)
|
|
91
|
+
if (metadata != null) {
|
|
92
|
+
put("metadata", metadata)
|
|
93
|
+
}
|
|
94
|
+
}
|
|
95
|
+
eventEmitter?.invoke("incomingCall", data)
|
|
96
|
+
} catch (e: SecurityException) {
|
|
97
|
+
notifyError("Failed to report incoming call: ${e.message}")
|
|
98
|
+
}
|
|
99
|
+
}
|
|
100
|
+
|
|
101
|
+
fun endCall(callId: String) {
|
|
102
|
+
activeCalls[callId]?.onDisconnect()
|
|
103
|
+
activeCalls.remove(callId)
|
|
104
|
+
}
|
|
105
|
+
|
|
106
|
+
fun answerCall(callId: String) {
|
|
107
|
+
activeCalls[callId]?.onAnswer()
|
|
108
|
+
}
|
|
109
|
+
|
|
110
|
+
fun rejectCall(callId: String) {
|
|
111
|
+
activeCalls[callId]?.onReject()
|
|
112
|
+
activeCalls.remove(callId)
|
|
113
|
+
}
|
|
114
|
+
|
|
115
|
+
fun setCallOnHold(callId: String, onHold: Boolean) {
|
|
116
|
+
if (onHold) {
|
|
117
|
+
activeCalls[callId]?.onHold()
|
|
118
|
+
} else {
|
|
119
|
+
activeCalls[callId]?.onUnhold()
|
|
120
|
+
}
|
|
121
|
+
}
|
|
122
|
+
|
|
123
|
+
fun setAudioRoute(route: String) {
|
|
124
|
+
val audioManager = context.getSystemService(Context.AUDIO_SERVICE) as AudioManager
|
|
125
|
+
|
|
126
|
+
when (route) {
|
|
127
|
+
"speaker" -> {
|
|
128
|
+
audioManager.isSpeakerphoneOn = true
|
|
129
|
+
}
|
|
130
|
+
"earpiece" -> {
|
|
131
|
+
audioManager.isSpeakerphoneOn = false
|
|
132
|
+
}
|
|
133
|
+
"bluetooth" -> {
|
|
134
|
+
// Bluetooth routing is handled automatically when connected
|
|
135
|
+
audioManager.isBluetoothScoOn = true
|
|
136
|
+
audioManager.startBluetoothSco()
|
|
137
|
+
}
|
|
138
|
+
}
|
|
139
|
+
}
|
|
140
|
+
|
|
141
|
+
fun setMuted(muted: Boolean) {
|
|
142
|
+
val audioManager = context.getSystemService(Context.AUDIO_SERVICE) as AudioManager
|
|
143
|
+
audioManager.isMicrophoneMute = muted
|
|
144
|
+
}
|
|
145
|
+
|
|
146
|
+
fun updateCallStatus(callId: String, status: String) {
|
|
147
|
+
val connection = activeCalls[callId] ?: return
|
|
148
|
+
|
|
149
|
+
when (status) {
|
|
150
|
+
"connecting" -> connection.setDialing()
|
|
151
|
+
"connected" -> connection.setActive()
|
|
152
|
+
"disconnected" -> {
|
|
153
|
+
connection.setDisconnected(DisconnectCause(DisconnectCause.LOCAL))
|
|
154
|
+
connection.destroy()
|
|
155
|
+
activeCalls.remove(callId)
|
|
156
|
+
}
|
|
157
|
+
"failed" -> {
|
|
158
|
+
connection.setDisconnected(DisconnectCause(DisconnectCause.ERROR))
|
|
159
|
+
connection.destroy()
|
|
160
|
+
activeCalls.remove(callId)
|
|
161
|
+
}
|
|
162
|
+
}
|
|
163
|
+
}
|
|
164
|
+
|
|
165
|
+
fun registerConnection(callId: String, connection: VoipConnection) {
|
|
166
|
+
activeCalls[callId] = connection
|
|
167
|
+
}
|
|
168
|
+
|
|
169
|
+
fun notifyCallStarted(callId: String) {
|
|
170
|
+
val data = JSObject().apply {
|
|
171
|
+
put("callId", callId)
|
|
172
|
+
}
|
|
173
|
+
eventEmitter?.invoke("callStarted", data)
|
|
174
|
+
}
|
|
175
|
+
|
|
176
|
+
fun notifyCallAnswered(callId: String) {
|
|
177
|
+
val data = JSObject().apply {
|
|
178
|
+
put("callId", callId)
|
|
179
|
+
}
|
|
180
|
+
eventEmitter?.invoke("callAnswered", data)
|
|
181
|
+
}
|
|
182
|
+
|
|
183
|
+
fun notifyCallEnded(callId: String, reason: String? = null) {
|
|
184
|
+
val data = JSObject().apply {
|
|
185
|
+
put("callId", callId)
|
|
186
|
+
if (reason != null) {
|
|
187
|
+
put("reason", reason)
|
|
188
|
+
}
|
|
189
|
+
}
|
|
190
|
+
eventEmitter?.invoke("callEnded", data)
|
|
191
|
+
activeCalls.remove(callId)
|
|
192
|
+
}
|
|
193
|
+
|
|
194
|
+
fun notifyCallRejected(callId: String) {
|
|
195
|
+
val data = JSObject().apply {
|
|
196
|
+
put("callId", callId)
|
|
197
|
+
}
|
|
198
|
+
eventEmitter?.invoke("callRejected", data)
|
|
199
|
+
activeCalls.remove(callId)
|
|
200
|
+
}
|
|
201
|
+
|
|
202
|
+
fun notifyCallHeld(callId: String, onHold: Boolean) {
|
|
203
|
+
val data = JSObject().apply {
|
|
204
|
+
put("callId", callId)
|
|
205
|
+
put("onHold", onHold)
|
|
206
|
+
}
|
|
207
|
+
eventEmitter?.invoke("callHeld", data)
|
|
208
|
+
}
|
|
209
|
+
|
|
210
|
+
private fun notifyError(message: String) {
|
|
211
|
+
android.util.Log.e("CallManager", message)
|
|
212
|
+
}
|
|
213
|
+
}
|