@tencent-rtc/mcp 1.4.1 → 1.4.3

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (34) hide show
  1. package/package.json +1 -1
  2. package/resource/doc/callkit/core-sdk/android/answer-call.md +852 -0
  3. package/resource/doc/callkit/core-sdk/android/make-call.md +954 -0
  4. package/resource/doc/callkit/core-sdk/android/offline-push.md +361 -0
  5. package/resource/doc/callkit/core-sdk/flutter/answer-call.md +1160 -0
  6. package/resource/doc/callkit/core-sdk/flutter/make-call.md +1205 -0
  7. package/resource/doc/callkit/core-sdk/flutter/offline-push.md +398 -0
  8. package/resource/doc/callkit/core-sdk/ios/answer-call.md +1090 -0
  9. package/resource/doc/callkit/core-sdk/ios/make-call.md +1248 -0
  10. package/resource/doc/callkit/core-sdk/ios/offline-push.md +485 -0
  11. package/resource/doc/callkit/core-sdk/uni-app/answer-call.md +950 -0
  12. package/resource/doc/callkit/core-sdk/uni-app/make-call.md +1052 -0
  13. package/resource/doc/callkit/preset/android/integration.md +1 -1
  14. package/resource/doc/callkit/preset/flutter/integration.md +1 -1
  15. package/resource/doc/callkit/preset/ios/integration.md +1 -1
  16. package/resource/doc/callkit/preset/react/api.md +1 -1
  17. package/resource/doc/callkit/preset/vue/api.md +1 -1
  18. package/server.js +1 -1
  19. package/tools/call/get_native_callkit_core_integration.js +67 -0
  20. package/tools/call/get_native_callkit_core_integration.js.map +1 -0
  21. package/tools/common/present_framework_choice.js +1 -1
  22. package/tools/common/present_framework_choice.js.map +1 -1
  23. package/tools/common/record_skill_session.js +17 -6
  24. package/tools/common/record_skill_session.js.map +1 -1
  25. package/tools/definitions/get_native_call_uikit_integration_def.js +9 -3
  26. package/tools/definitions/get_native_call_uikit_integration_def.js.map +1 -1
  27. package/tools/definitions/get_native_callkit_core_integration_def.js +63 -0
  28. package/tools/definitions/get_native_callkit_core_integration_def.js.map +1 -0
  29. package/tools/index.js +2 -0
  30. package/tools/index.js.map +1 -1
  31. package/utils/report-cls-client.js +1 -1
  32. package/utils/report-es-client.js +1 -1
  33. package/utils/report-runtime-error-cls-client.js +91 -0
  34. package/utils/report-runtime-error-cls-client.js.map +1 -0
@@ -0,0 +1,954 @@
1
+ This guide walks you through implementing the "Make a Call" feature with the **AtomicXCore SDK**. You'll use three core building blocks—**DeviceStore**, **CallStore**, and the main UI component **CallCoreView**—to build a complete calling experience.
2
+
3
+ ## Core Features
4
+
5
+ **AtomicXCore** offers three essential modules for building multi-party Audio/Video Call scenarios:
6
+
7
+ | **Module** | **Feature Description** |
8
+ | --- | --- |
9
+ | [CallCoreView](https://liteav.sdk.cloudcachetci.com/doc/product/tuikit/atomic-x/android/en/-atomic-x%20-core%20-a-p-i/io.trtc.tuikit.atomicxcore.api.view/-call-core-view/index.html) | The main call UI component. It observes `CallStore` data and handles video rendering automatically, and supports customization of layout, avatars, and icons. |
10
+ | [CallStore](https://liteav.sdk.cloudcachetci.com/doc/product/tuikit/atomic-x/android/en/-atomic-x%20-core%20-a-p-i/io.trtc.tuikit.atomicxcore.api.call/-call-store/index.html) | Manages the call lifecycle (place a call, answer, reject, hang up) and exposes real-time participant audio/video status, call duration, call logs, and more. |
11
+ | [DeviceStore](https://liteav.sdk.cloudcachetci.com/doc/product/tuikit/atomic-x/android/en/-atomic-x%20-core%20-a-p-i/io.trtc.tuikit.atomicxcore.api.device/-device-store/index.html) | Controls audio/video devices: microphone (toggle/volume), camera (toggle/switch/quality), screen sharing, and real-time device status monitoring. |
12
+
13
+ ## Preparation
14
+
15
+ ### Step 1: Activate the Service
16
+
17
+ Follow the instructions in Activate the service to obtain either a trial or paid edition of the SDK.
18
+
19
+ ### Step 2: Integrate the SDK
20
+
21
+ 1. Add the following dependencies to your **build.gradle** file, then run **Gradle Sync**:
22
+
23
+ ```groovy
24
+ dependencies {
25
+ api "io.trtc.uikit:atomicx-core:latest.release"
26
+ api "com.tencent.imsdk:imsdk-plus:8.7.7201"
27
+ // Other dependencies...
28
+ }
29
+ ```
30
+
31
+ 2. The SDK relies on Java reflection for some features. Add a keep rule in `proguard-rules.pro` under the `app` module (append at the end), then click **Sync Now** in the IDE.
32
+
33
+ ```
34
+ -keep class com.tencent.** { *; }
35
+ ```
36
+
37
+ 3. In `AndroidManifest.xml` under `app`, add `tools:replace="android:allowBackup"` on the `<application>` node so your app's settings take precedence over libraries.
38
+
39
+ ```xml
40
+ <application
41
+ android:name=".BaseApplication"
42
+ android:allowBackup="false"
43
+ android:icon="@drawable/app_ic_launcher"
44
+ android:label="@string/app_name"
45
+ android:largeHeap="true"
46
+ android:theme="@style/AppTheme"
47
+ tools:replace="android:allowBackup">
48
+ ```
49
+
50
+ ### Step 3: Initialize and Log In
51
+
52
+ Complete these steps in order:
53
+
54
+ 1. **Create `CallStore`:** `CallStore.shared` initializes the call data layer.
55
+ 2. **Log in the user:** call `LoginStore.shared.login(...)` to complete IM login.
56
+ 3. **Initialize the call engine:** after login succeeds, call `TUICallEngine.createInstance(context).init(sdkAppId, userId, userSig, null)` to initialize the TRTC call signaling engine (**required**; without it, calls will not work reliably).
57
+
58
+ Sample code:
59
+
60
+ ```kotlin
61
+ class MainActivity : ComponentActivity() {
62
+ override fun onCreate(savedInstanceState: Bundle?) {
63
+ super.onCreate(savedInstanceState)
64
+ // Initialize CallStore
65
+ CallStore.shared
66
+
67
+ val sdkAppId = 1400000001 // Replace with your SDKAppID
68
+ val userId = "test_001" // Replace with your UserID
69
+ val userSig = "xxxxxxxxxxx" // Use a temporary userSig during development; when MCP is configured with sdkAppId and sdkSecretKey, the tool can issue it for you
70
+
71
+ LoginStore.shared.login(
72
+ this,
73
+ sdkAppId,
74
+ userId,
75
+ userSig,
76
+ object : CompletionHandler {
77
+ override fun onSuccess() {
78
+ // Initialize TUICallEngine
79
+ TUICallEngine.createInstance(this@MainActivity).init(sdkAppId, userId, userSig, null)
80
+ // Login successful
81
+ Log.d("Login", "login success")
82
+ }
83
+
84
+ override fun onFailure(code: Int, desc: String) {
85
+ // Login failed
86
+ Log.e("Login", "login failed, code: $code, error: $desc")
87
+ }
88
+ }
89
+ )
90
+ }
91
+ }
92
+ ```
93
+
94
+ **Login parameters:**
95
+
96
+ | **Parameter** | **Type** | **Description** |
97
+ | --- | --- | --- |
98
+ | `userId` | `String` | Unique user ID, up to **32 bytes**, with **no special characters**; use letters and digits. Avoid simple IDs like `1` or `123` to reduce multi-device conflicts. |
99
+ | `sdkAppId` | `int` | Obtain from the Tencent RTC console; typically a 10-digit integer starting with `140` or `160`. |
100
+ | `userSig` | `String` | Authentication token for TRTC. **Development**: locally generate a temporary `userSig` using [GenerateTestUserSig.genTestUserSig](https://github.com/Tencent-RTC/TUIKit_Android/blob/main/application/debug/src/main/java/com/tencent/qcloud/tuikit/debug/GenerateTestUserSig.java). When **Tencent RTC MCP Server** is configured with both `sdkAppId` and `sdkSecretKey`, the tool can issue a `userSig` for you automatically. **Production**: always generate `userSig` server-side with the SDKSecretKey; never embed or distribute the SDKSecretKey in the client. |
101
+
102
+ ## Implementation
103
+
104
+ > **Note:**
105
+ >
106
+ > You must log in before making a call. The following steps show how to make a call.
107
+
108
+ ### Step 1: Create Call Interface
109
+
110
+ Set up a dedicated call screen that appears when a call is initiated:
111
+
112
+ 1. **Create the call screen:** Implement a new Activity to host the call UI and handle navigation when a call is received.
113
+
114
+ 2. **Attach `CallCoreView`:** This core view component observes **CallStore** data and renders the call UI. It supports layout switching, avatar, and icon customization.
115
+
116
+ ```kotlin
117
+ class CallActivity : AppCompatActivity() {
118
+ private var callCoreView: CallCoreView? = null
119
+
120
+ // 1. Create call page container
121
+ override fun onCreate(savedInstanceState: Bundle?) {
122
+ super.onCreate(savedInstanceState)
123
+ // 2. Attach CallCoreView to the call screen
124
+ callCoreView = CallCoreView(this)
125
+ setContentView(callCoreView)
126
+ }
127
+ }
128
+ ```
129
+
130
+ **CallCoreView Component Features:**
131
+
132
+ | **Feature** | **Description** | **Reference** |
133
+ | --- | --- | --- |
134
+ | **Set Layout Mode** | Switch between different layout modes. If not specified, layout adapts automatically based on participant count. | Switch Layout Mode |
135
+ | **Set Avatar** | Customize avatars for specific users via resource path. | Customize Default Avatar |
136
+ | **Set Volume Indicator Icon** | Personalize volume indicator icons for different volume levels. | Customize Volume Indicator Icon |
137
+ | **Set Network Indicator Icon** | Show network quality status with real-time indicator icons. | Customize Network Indicator Icon |
138
+ | **Set Waiting Animation for Users** | Display GIF animations for users in the waiting state during multi-party calls. | Customize Loading Animation |
139
+
140
+ ### Step 2: Add Call Controls
141
+
142
+ Use the APIs from `DeviceStore` and `CallStore` to add and customize call control buttons:
143
+
144
+ **DeviceStore:** Microphone (toggle and volume), camera (toggle, switch, quality), and screen sharing. Wire these APIs to your buttons and observe device state to update the UI in real time.
145
+
146
+ **CallStore:** Call actions (answer, hang up, reject). Wire these to button clicks and show/hide or enable/disable buttons based on call status.
147
+
148
+ **Icons:** You can download button icons from GitHub. They are designed for TUICallKit and free for use.
149
+
150
+ | **Resource** | **Description** | **Download** |
151
+ | --- | --- | --- |
152
+ | `callview_ic_dialing.png` | Answer/accept call icon. | [GitHub](https://github.com/Tencent-RTC/TUIKit_Android/blob/main/atomic_x/src/main/res-callview/drawable-xxhdpi/callview_ic_dialing.png) |
153
+ | `callview_ic_hangup.png` | Hang up / reject call icon. | [GitHub](https://github.com/Tencent-RTC/TUIKit_Android/blob/main/atomic_x/src/main/res-callview/drawable-xxhdpi/callview_ic_hangup.png) |
154
+ | `callview_ic_camera_disable.png` | Camera off icon. | [GitHub](https://github.com/Tencent-RTC/TUIKit_Android/blob/main/atomic_x/src/main/res-callview/drawable-xxhdpi/callview_ic_camera_disable.png) |
155
+ | `callview_ic_camera_enable.png` | Camera on icon. | [GitHub](https://github.com/Tencent-RTC/TUIKit_Android/blob/main/atomic_x/src/main/res-callview/drawable-xxhdpi/callview_ic_camera_enable.png) |
156
+ | `callview_ic_mic_unmute.png` | Microphone unmuted icon. | [GitHub](https://github.com/Tencent-RTC/TUIKit_Android/blob/main/atomic_x/src/main/res-callview/drawable-xxhdpi/callview_ic_mic_unmute.png) |
157
+ | `callview_ic_mic_mute.png` | Microphone muted icon. | [GitHub](https://github.com/Tencent-RTC/TUIKit_Android/blob/main/atomic_x/src/main/res-callview/drawable-xxhdpi/callview_ic_mic_mute.png) |
158
+ | `callview_ic_handsfree_disable.png` | Handsfree off icon. | [GitHub](https://github.com/Tencent-RTC/TUIKit_Android/blob/main/atomic_x/src/main/res-callview/drawable-xxhdpi/callview_ic_handsfree_disable.png) |
159
+ | `callview_ic_handsfree_enable.png` | Handsfree on icon. | [GitHub](https://github.com/Tencent-RTC/TUIKit_Android/blob/main/atomic_x/src/main/res-callview/drawable-xxhdpi/callview_ic_handsfree_enable.png) |
160
+
161
+ **Example: Adding hang up, microphone, and camera buttons**
162
+
163
+ 1.1 **Create Bottom Bar Container:** Add a container at the bottom of the call screen to hold the hang up, microphone, and camera buttons.
164
+
165
+ ```kotlin
166
+ import io.trtc.tuikit.atomicxcore.api.call.CallStore
167
+ import io.trtc.tuikit.atomicxcore.api.view.CallCoreView
168
+ import io.trtc.tuikit.atomicxcore.api.device.DeviceStore
169
+
170
+ class CallActivity : AppCompatActivity() {
171
+ private var buttonContainer: Button? = null
172
+
173
+ override fun onCreate(savedInstanceState: Bundle?) {
174
+ super.onCreate(savedInstanceState)
175
+ // Other initialization code
176
+ // Create bottom bar container
177
+ createButtonContainer()
178
+ setContentView(buttonContainer)
179
+ }
180
+
181
+ private fun createButtonContainer() {
182
+ buttonContainer = LinearLayout(this).apply {
183
+ orientation = LinearLayout.HORIZONTAL
184
+ gravity = Gravity.CENTER
185
+ layoutParams = FrameLayout.LayoutParams(
186
+ FrameLayout.LayoutParams.MATCH_PARENT,
187
+ FrameLayout.LayoutParams.WRAP_CONTENT
188
+ ).apply {
189
+ gravity = Gravity.BOTTOM
190
+ bottomMargin = dpToPx(80)
191
+ }
192
+ }
193
+ }
194
+ }
195
+ ```
196
+
197
+ 1.2 **Add Hang Up Button:** Insert a hang up button into the bottom toolbar. On click, call the `hangup` API and close the call screen.
198
+
199
+ ```kotlin
200
+ import io.trtc.tuikit.atomicxcore.api.call.CallStore
201
+ import io.trtc.tuikit.atomicxcore.api.view.CallCoreView
202
+ import io.trtc.tuikit.atomicxcore.api.device.DeviceStore
203
+
204
+ class CallActivity : AppCompatActivity() {
205
+ private var buttonContainer: Button? = null
206
+ private var buttonHangup: Button? = null
207
+
208
+ override fun onCreate(savedInstanceState: Bundle?) {
209
+ super.onCreate(savedInstanceState)
210
+ // Other initialization code
211
+ // 1. Add hang up button to bottom toolbar
212
+ addHangupButton()
213
+ setContentView(buttonContainer)
214
+ }
215
+
216
+ private fun addHangupButton() {
217
+ buttonHangup = Button(this).apply {
218
+ text = "Hang Up"
219
+ layoutParams = LinearLayout.LayoutParams(0, LinearLayout.LayoutParams.WRAP_CONTENT, 1f).apply {
220
+ marginEnd = dpToPx(8)
221
+ }
222
+ setOnClickListener {
223
+ // 2. Call hangup API and close the screen
224
+ CallStore.shared.hangup(null)
225
+ finish()
226
+ }
227
+ }
228
+ buttonContainer?.addView(buttonHangup)
229
+ }
230
+ }
231
+ ```
232
+
233
+ 1.3 **Add Microphone Toggle Button:** Add a microphone toggle button to the toolbar. On click, call `openLocalMicrophone` or `closeLocalMicrophone` as needed.
234
+
235
+ ```kotlin
236
+ import io.trtc.tuikit.atomicxcore.api.call.CallStore
237
+ import io.trtc.tuikit.atomicxcore.api.view.CallCoreView
238
+ import io.trtc.tuikit.atomicxcore.api.device.DeviceStore
239
+
240
+ class CallActivity : AppCompatActivity() {
241
+ private var buttonContainer: Button? = null
242
+ private var buttonMicrophone: Button? = null
243
+
244
+ override fun onCreate(savedInstanceState: Bundle?) {
245
+ super.onCreate(savedInstanceState)
246
+ // Other initialization code
247
+ // 1. Add microphone toggle to bottom toolbar
248
+ addMicrophoneButton()
249
+ setContentView(buttonContainer)
250
+ }
251
+
252
+ private fun addMicrophoneButton() {
253
+ buttonMicrophone = Button(this).apply {
254
+ layoutParams = LinearLayout.LayoutParams(0, LinearLayout.LayoutParams.WRAP_CONTENT, 1f).apply {
255
+ setMargins(16, 0, 16, 0)
256
+ }
257
+ setOnClickListener {
258
+ // 2. Toggle microphone on click
259
+ val isMicrophoneOpen = DeviceStore.shared().deviceState.microphoneStatus.value == DeviceStatus.ON
260
+ if (isMicrophoneOpen) {
261
+ DeviceStore.shared().closeLocalMicrophone()
262
+ } else {
263
+ DeviceStore.shared().openLocalMicrophone(null)
264
+ }
265
+ }
266
+ }
267
+ val isMicrophoneOpen = DeviceStore.shared().deviceState.microphoneStatus.value == DeviceStatus.ON
268
+ buttonMicrophone?.text = if (isMicrophoneOpen) "Mute Microphone" else "Unmute Microphone"
269
+ buttonContainer?.addView(buttonMicrophone)
270
+ }
271
+ }
272
+ ```
273
+
274
+ 1.4 **Add Camera Toggle Button:** Add a camera toggle button to the toolbar. On click, call `openLocalCamera` or `closeLocalCamera` as needed.
275
+
276
+ ```kotlin
277
+ import io.trtc.tuikit.atomicxcore.api.call.CallStore
278
+ import io.trtc.tuikit.atomicxcore.api.view.CallCoreView
279
+ import io.trtc.tuikit.atomicxcore.api.device.DeviceStore
280
+
281
+ class CallActivity : AppCompatActivity() {
282
+ private var buttonContainer: Button? = null
283
+ private var buttonCamera: Button? = null
284
+
285
+ override fun onCreate(savedInstanceState: Bundle?) {
286
+ super.onCreate(savedInstanceState)
287
+ // Other initialization code
288
+ // 1. Add camera toggle button to bottom toolbar
289
+ addCameraButton()
290
+ setContentView(buttonContainer)
291
+ }
292
+
293
+ private fun addCameraButton() {
294
+ buttonCamera = Button(this).apply {
295
+ layoutParams = LinearLayout.LayoutParams(0, LinearLayout.LayoutParams.WRAP_CONTENT, 1f).apply {
296
+ setMargins(16, 0, 16, 0)
297
+ }
298
+ setOnClickListener {
299
+ // 2. Toggle camera on click
300
+ val isCameraOpen = DeviceStore.shared().deviceState.cameraStatus.value == DeviceStatus.ON
301
+ if (isCameraOpen) {
302
+ DeviceStore.shared().closeLocalCamera()
303
+ } else {
304
+ val isFrontCamera = DeviceStore.shared().deviceState.isFrontCamera.value
305
+ DeviceStore.shared().openLocalCamera(isFrontCamera, null)
306
+ }
307
+ }
308
+ }
309
+ val isCameraOpen = DeviceStore.shared().deviceState.cameraStatus.value == DeviceStatus.ON
310
+ buttonCamera?.text = if (isCameraOpen) "Turn Off Camera" else "Turn On Camera"
311
+ buttonContainer?.addView(buttonCamera)
312
+ }
313
+ }
314
+ ```
315
+
316
+ 1.5 **Update Button Text in Real Time:** Listen for microphone and camera status changes and update button text accordingly.
317
+
318
+ ```kotlin
319
+ import io.trtc.tuikit.atomicxcore.api.call.*
320
+ import io.trtc.tuikit.atomicxcore.api.device.DeviceStore
321
+ import kotlinx.coroutines.*
322
+
323
+ class CallActivity : AppCompatActivity() {
324
+ private var buttonCamera: Button? = null
325
+ private var buttonMicrophone: Button? = null
326
+
327
+ override fun onCreate(savedInstanceState: Bundle?) {
328
+ super.onCreate(savedInstanceState)
329
+ // Other initialization code
330
+ // 1. Observe microphone and camera status
331
+ observeDeviceState()
332
+ }
333
+
334
+ private fun observeDeviceState() {
335
+ deviceStateJob = CoroutineScope(Dispatchers.Main).launch {
336
+ supervisorScope {
337
+ launch {
338
+ DeviceStore.shared().deviceState.cameraStatus.collect { status ->
339
+ // 2. Update camera button text
340
+ buttonCamera?.text = if (status == DeviceStatus.ON) "Turn Off Camera" else "Turn On Camera"
341
+ }
342
+ }
343
+ launch {
344
+ DeviceStore.shared().deviceState.microphoneStatus.collect { status ->
345
+ // 2. Update microphone button text
346
+ buttonMicrophone?.text = if (status == DeviceStatus.ON) "Mute Microphone" else "Unmute Microphone"
347
+ }
348
+ }
349
+ }
350
+ }
351
+ }
352
+ }
353
+ ```
354
+
355
+ ### Step 3: Request Microphone and Camera Permissions
356
+
357
+ Before making a call, check for audio and video permissions. If permissions are missing, prompt the user to grant them dynamically.
358
+
359
+ 1. **Declare permissions:** Add camera and microphone permissions to your `AndroidManifest.xml`.
360
+
361
+ ```xml
362
+ <manifest xmlns:android="http://schemas.android.com/apk/res/android">
363
+ <!-- Microphone permission -->
364
+ <uses-permission android:name="android.permission.RECORD_AUDIO" />
365
+ <!-- Camera permission -->
366
+ <uses-permission android:name="android.permission.CAMERA" />
367
+
368
+ <application>
369
+ <!-- ... -->
370
+ <activity
371
+ android:name=".CallActivity"
372
+ android:exported="false"
373
+ android:screenOrientation="portrait" />
374
+ </application>
375
+ </manifest>
376
+ ```
377
+
378
+ 2. **Request permissions at runtime:** Dynamically request audio and video permissions as needed when making a call.
379
+
380
+ ```kotlin
381
+ // Request permissions dynamically
382
+ private fun requestAllCallPermissions() {
383
+ val allPermissions = arrayOf(
384
+ Manifest.permission.CAMERA,
385
+ Manifest.permission.RECORD_AUDIO
386
+ )
387
+ if (!checkPermissions(allPermissions)) {
388
+ ActivityCompat.requestPermissions(this, allPermissions, PERMISSION_REQUEST_CODE)
389
+ }
390
+ }
391
+
392
+ // Handle permission request result
393
+ override fun onRequestPermissionsResult(
394
+ requestCode: Int,
395
+ permissions: Array<out String>,
396
+ grantResults: IntArray
397
+ ) {
398
+ super.onRequestPermissionsResult(requestCode, permissions, grantResults)
399
+
400
+ if (requestCode == PERMISSION_REQUEST_CODE) {
401
+ // Check if all permissions are granted
402
+ var allGranted = true
403
+ for (result in grantResults) {
404
+ if (result != PackageManager.PERMISSION_GRANTED) {
405
+ allGranted = false
406
+ break
407
+ }
408
+ }
409
+ if (allGranted) {
410
+ // Permissions granted
411
+ } else {
412
+ // Some permissions denied
413
+ }
414
+ }
415
+ }
416
+ ```
417
+
418
+ ### Step 4: Make a Call
419
+
420
+ After the `calls` API succeeds, navigate to the call screen. For a better experience, enable the microphone (and camera for video calls) automatically based on the media type.
421
+
422
+ 1. **Initiate the call:** Use the `calls` API to start a call.
423
+
424
+ 2. **Enable media devices:** Once the call is initiated, turn on the microphone; for video calls, also enable the camera.
425
+
426
+ 3. **Open the call screen:** On successful call initiation, launch the call Activity.
427
+
428
+ ```kotlin
429
+ import io.trtc.tuikit.atomicxcore.api.CompletionHandler
430
+ import io.trtc.tuikit.atomicxcore.api.call.CallMediaType
431
+ import io.trtc.tuikit.atomicxcore.api.call.CallStore
432
+
433
+ class MainActivity : ComponentActivity() {
434
+ // 1. Make a call
435
+ private fun startCall(userIdList: List<String>, mediaType: CallMediaType) {
436
+ CallStore.shared.calls(userIdList, mediaType, null, object : CompletionHandler {
437
+ override fun onFailure(code: Int, desc: String) {
438
+ }
439
+
440
+ override fun onSuccess() {
441
+ // 2. Enable media devices
442
+ openDeviceForMediaType(mediaType)
443
+ // 3. Launch call screen
444
+ val intent = Intent(this@MainActivity, CallActivity::class.java)
445
+ startActivity(intent)
446
+ }
447
+ })
448
+ }
449
+
450
+ private fun openDeviceForMediaType(mediaType: CallMediaType?) {
451
+ if (mediaType == null) {
452
+ return
453
+ }
454
+ DeviceStore.shared().openLocalMicrophone(null)
455
+ if (mediaType == CallMediaType.Video) {
456
+ val isFrontCamera = DeviceStore.shared().deviceState.isFrontCamera.value
457
+ DeviceStore.shared().openLocalCamera(isFrontCamera, null)
458
+ }
459
+ }
460
+ }
461
+ ```
462
+
463
+ **calls API Parameter Reference:**
464
+
465
+ | **Parameter** | **Type** | **Required** | **Description** |
466
+ | --- | --- | --- | --- |
467
+ | `userIdList` | `List` | Yes | List of target user `userId`s. |
468
+ | `mediaType` | [CallMediaType](https://liteav.sdk.cloudcachetci.com/doc/product/tuikit/atomic-x/android/en/-atomic-x%20-core%20-a-p-i/io.trtc.tuikit.atomicxcore.api.call/-call-media-type/index.html) | Yes | Call media type: audio or video.<br>- `CallMediaType.Video`: Video call.<br>- `CallMediaType.Audio`: Audio call. |
469
+ | `params` | [CallParams](https://liteav.sdk.cloudcachetci.com/doc/product/tuikit/atomic-x/android/en/-atomic-x%20-core%20-a-p-i/io.trtc.tuikit.atomicxcore.api.call/-call-params/index.html) | No | Optional call parameters, such as room ID, call invitation timeout, etc.<br>- `roomId` (`String`): Room ID, optional. If not specified, the server assigns one.<br>- `timeout` (`Int`): Call timeout (seconds).<br>- `userData` (`String`): Custom user data.<br>- `chatGroupId` (`String`): Chat group ID for group calls.<br>- `isEphemeralCall` (`Boolean`): If `true`, the call is encrypted and not logged. |
470
+
471
+ ### Step 5: End the Call
472
+
473
+ Whether the local user hangs up via `hangup` or the remote party ends the call, the `onCallEnded` callback is fired. Subscribe to it and finish the call Activity when the call ends.
474
+
475
+ 1. **Listen for call end events:** Subscribe to the `onCallEnded` event.
476
+
477
+ 2. **Close the call screen:** When `onCallEnded` is triggered, finish the Activity.
478
+
479
+ ```kotlin
480
+ import io.trtc.tuikit.atomicxcore.api.call.CallEndReason
481
+ import io.trtc.tuikit.atomicxcore.api.call.CallListener
482
+ import io.trtc.tuikit.atomicxcore.api.call.CallMediaType
483
+ import io.trtc.tuikit.atomicxcore.api.call.CallStore
484
+
485
+ class CallActivity : AppCompatActivity() {
486
+ private var callListener: CallListener? = null
487
+
488
+ override fun onCreate(savedInstanceState: Bundle?) {
489
+ super.onCreate(savedInstanceState)
490
+ // ... Other initialization code
491
+ // 1. Listen for call end event
492
+ addListener()
493
+ }
494
+
495
+ private fun addListener() {
496
+ callListener = object : CallListener() {
497
+ override fun onCallEnded(callId: String, mediaType: CallMediaType, reason: CallEndReason, userId: String) {
498
+ // 2. Close call screen
499
+ finish()
500
+ }
501
+ }
502
+ callListener?.let { CallStore.shared.addListener(it) }
503
+ }
504
+ }
505
+ ```
506
+
507
+ **onCallEnded Event Parameters:**
508
+
509
+ | **Parameter** | **Type** | **Description** |
510
+ | --- | --- | --- |
511
+ | `callId` | `String` | Unique identifier for the call. |
512
+ | `mediaType` | [CallMediaType](https://liteav.sdk.cloudcachetci.com/doc/product/tuikit/atomic-x/android/en/-atomic-x%20-core%20-a-p-i/io.trtc.tuikit.atomicxcore.api.call/-call-media-type/index.html) | Call media type: audio or video.<br>- `CallMediaType.Video`: Video call.<br>- `CallMediaType.Audio`: Audio call. |
513
+ | `reason` | [CallEndReason](https://liteav.sdk.cloudcachetci.com/doc/product/tuikit/atomic-x/android/en/-atomic-x%20-core%20-a-p-i/io.trtc.tuikit.atomicxcore.api.call/-call-end-reason/index.html) | Reason for call end.<br>- `Unknown`: Unknown reason.<br>- `Hangup`: User ended the call.<br>- `Reject`: Callee rejected the call.<br>- `NoResponse`: No answer within timeout.<br>- `Offline`: Callee is offline.<br>- `LineBusy`: Callee is busy.<br>- `Canceled`: Caller canceled before callee answered.<br>- `OtherDeviceAccepted`: Call answered on another device.<br>- `OtherDeviceReject`: Call rejected on another device.<br>- `EndByServer`: Call ended by server. |
514
+ | `userId` | `String` | ID of the user who triggered the end event. |
515
+
516
+ ### Result
517
+
518
+ Once you complete these five steps, your app will have a complete call-initiation flow working.
519
+
520
+ ## Customization
521
+
522
+ **CallCoreView** supports rich UI customization, including avatars and volume indicators. You can download ready-to-use icons from GitHub to speed up integration; they are designed for TUICallKit and free for commercial use.
523
+
524
+ ### Custom Volume Indicator Icons
525
+
526
+ Use the `setVolumeLevelIcons` method to assign custom icons for each volume level.
527
+
528
+ **setVolumeLevelIcons Example:**
529
+
530
+ ```kotlin
531
+ private fun setIconResourcePath() {
532
+ val volumeLevelIcons = mapOf(VolumeLevel.Mute to "path/to/icon/resource")
533
+ val callCoreView = CallCoreView(context)
534
+ callCoreView.setVolumeLevelIcons(volumeLevelIcons)
535
+ }
536
+ ```
537
+
538
+ **setVolumeLevelIcons API Parameters:**
539
+
540
+ | **Parameter** | **Type** | **Required** | **Description** |
541
+ | --- | --- | --- | --- |
542
+ | `icons` | `Map` | Yes | Maps each volume level to an icon resource.<br>**Key (`VolumeLevel`):**<br>- `VolumeLevel.Mute`: Microphone muted.<br>- `VolumeLevel.Low`: Volume (0-25].<br>- `VolumeLevel.Medium`: Volume (25-50].<br>- `VolumeLevel.High`: Volume (50-75].<br>- `VolumeLevel.Peak`: Volume (75-100].<br>**Value (`String`):** Path to the icon resource for each level. |
543
+
544
+ **Volume Indicator Icons:**
545
+
546
+ | **Resource** | **Description** | **Download** |
547
+ | --- | --- | --- |
548
+ | `callview_ic_audio_input.png` | Volume indicator. Use for `VolumeLevel.Low` or `VolumeLevel.Medium` when the user's volume is above the corresponding threshold. | [GitHub](https://github.com/Tencent-RTC/TUIKit_Android/blob/main/atomic_x/src/main/res-callview/drawable-xxhdpi/callview_ic_audio_input.png) |
549
+ | `callview_ic_self_mute.png` | Mute indicator. Use for `VolumeLevel.Mute` when the user is muted. | [GitHub](https://github.com/Tencent-RTC/TUIKit_Android/blob/main/atomic_x/src/main/res-callview/drawable-xxhdpi/callview_ic_self_mute.png) |
550
+
551
+ ### Custom Network Indicator Icons
552
+
553
+ Use `setNetworkQualityIcons` to set custom icons for different network statuses.
554
+
555
+ **setNetworkQualityIcons Example:**
556
+
557
+ ```kotlin
558
+ private fun setNetworkQualityIcons() {
559
+ val volumeLevelIcons = mapOf(NetworkQuality.BAD to "path/to/icon")
560
+ val callCoreView = CallCoreView(context)
561
+ callCoreView.setNetworkQualityIcons(volumeLevelIcons)
562
+ }
563
+ ```
564
+
565
+ **setNetworkQualityIcons API Parameters:**
566
+
567
+ | **Parameter** | **Type** | **Required** | **Description** |
568
+ | --- | --- | --- | --- |
569
+ | `icons` | `Map` | Yes | Maps each network quality level to an icon resource.<br>**Key (`NetworkQuality`):**<br>- `NetworkQuality.UNKNOWN`: Unknown.<br>- `NetworkQuality.EXCELLENT`: Excellent.<br>- `NetworkQuality.GOOD`: Good.<br>- `NetworkQuality.POOR`: Poor.<br>- `NetworkQuality.BAD`: Bad.<br>- `NetworkQuality.VERY_BAD`: Very bad.<br>- `NetworkQuality.DOWN`: Disconnected.<br>**Value (`String`):** Path to the icon resource for each status. |
570
+
571
+ **Poor Network Indicator Icon:**
572
+
573
+ | **Resource** | **Description** | **Download** |
574
+ | --- | --- | --- |
575
+ | `callview_ic_network_bad.png` | Poor network indicator. Use for `NetworkQuality.BAD`, `VERY_BAD`, or `DOWN` when the network is poor or disconnected. | [GitHub](https://github.com/Tencent-RTC/TUIKit_Android/blob/main/atomic_x/src/main/res-callview/drawable-xxhdpi/callview_ic_network_bad.png) |
576
+
577
+ ### Custom Default Avatar
578
+
579
+ Use `setParticipantAvatars` to assign avatars to users. We recommend observing the data streams of `allParticipants`: display custom avatars when available, and fall back to the default avatar if not set or if loading fails.
580
+
581
+ **setParticipantAvatars Example:**
582
+
583
+ ```kotlin
584
+ private fun setParticipantAvatars() {
585
+ val avatars: MutableMap<String, String> = mutableMapOf()
586
+ val userId = "" // User ID
587
+ val avatarPath = "" // Path to user's default avatar resource
588
+ avatars[userId] = avatarPath
589
+ val callCoreView = CallCoreView(context)
590
+ callCoreView.setParticipantAvatars(avatars)
591
+ }
592
+ ```
593
+
594
+ **setParticipantAvatars API Parameters:**
595
+
596
+ | **Parameter** | **Type** | **Required** | **Description** |
597
+ | --- | --- | --- | --- |
598
+ | `icons` | `Map` | Yes | Maps user IDs to avatar resources.<br>- **Key:** User's `userID`.<br>- **Value:** Absolute path to the user's avatar resource. |
599
+
600
+ **Default Avatar Resource:**
601
+
602
+ | **Resource** | **Description** | **Download** |
603
+ | --- | --- | --- |
604
+ | `callview_ic_avatar.png` | Default avatar. Use when a user's avatar is not set or fails to load. | [GitHub](https://github.com/Tencent-RTC/TUIKit_Android/blob/main/atomic_x/src/main/res-callview/drawable-xxhdpi/callview_ic_avatar.png) |
605
+
606
+ ### Custom Loading Animation
607
+
608
+ Use `setWaitingAnimation` to set a GIF animation for users in the waiting state, improving the user experience.
609
+
610
+ **setWaitingAnimation Example:**
611
+
612
+ ```kotlin
613
+ private fun setWaitingAnimation() {
614
+ val waitingAnimationPath = "" // Path to waiting animation GIF resource
615
+ val callCoreView = CallCoreView(context)
616
+ callCoreView.setWaitingAnimation(waitingAnimationPath)
617
+ }
618
+ ```
619
+
620
+ **setWaitingAnimation API Parameters:**
621
+
622
+ | **Parameter** | **Type** | **Required** | **Description** |
623
+ | --- | --- | --- | --- |
624
+ | `path` | `String` | Yes | Absolute path to the GIF animation resource. |
625
+
626
+ **Waiting Animation:**
627
+
628
+ | **Resource** | **Description** | **Download** |
629
+ | --- | --- | --- |
630
+ | `callview_ic_loading.gif` | Waiting-for-answer animation. Use in group calls when a participant's status is waiting. | [GitHub](https://github.com/Tencent-RTC/TUIKit_Android/blob/main/atomic_x/src/main/res-callview/drawable-xxhdpi/callview_ic_loading.gif) |
631
+
632
+ ### Add Call Duration Display
633
+
634
+ You can display call duration in real time using the `activeCall.duration` field.
635
+
636
+ 1. **Subscribe to the data layer:** Observe `CallStore.observerState.activeCall` for updates to the current call.
637
+
638
+ 2. **Bind duration data:** Bind `activeCall.duration` to a UI control. The UI updates automatically—no need to manage a timer manually.
639
+
640
+ ```kotlin
641
+ import android.content.Context
642
+ import androidx.appcompat.widget.AppCompatTextView
643
+ import androidx.core.content.ContextCompat
644
+ import com.tencent.qcloud.tuicore.util.DateTimeUtil
645
+ import io.trtc.tuikit.atomicxcore.api.call.CallStore
646
+ import io.trtc.tuikit.atomicxcore.api.call.CallParticipantStatus
647
+ import kotlinx.coroutines.CoroutineScope
648
+ import kotlinx.coroutines.Dispatchers
649
+ import kotlinx.coroutines.Job
650
+ import kotlinx.coroutines.launch
651
+
652
+ class TimerView(context: Context) : AppCompatTextView(context) {
653
+ private var subscribeStateJob: Job? = null
654
+
655
+ override fun onAttachedToWindow() {
656
+ super.onAttachedToWindow()
657
+ // 1. Subscribe to activeCall
658
+ registerActiveCallObserver()
659
+ }
660
+
661
+ private fun registerActiveCallObserver() {
662
+ subscribeStateJob = CoroutineScope(Dispatchers.Main).launch {
663
+ CallStore.shared.observerState.activeCall.collect { activeCall ->
664
+ // 2. Bind call duration data, update timer
665
+ updateDurationView(activeCall)
666
+ }
667
+ }
668
+ }
669
+
670
+ private fun updateDurationView(activeCall: CallInfo) {
671
+ val currentDuration = activeCall.duration
672
+ text = DateTimeUtil.formatSecondsTo00(currentDuration.toInt())
673
+ }
674
+
675
+ override fun onDetachedFromWindow() {
676
+ super.onDetachedFromWindow()
677
+ subscribeStateJob?.cancel()
678
+ }
679
+ }
680
+ ```
681
+
682
+ > **Note:**
683
+ >
684
+ > For more reactive call status data, see CallState.
685
+
686
+ ## More Features
687
+
688
+ ### Set Avatar and Nickname
689
+
690
+ Before a call starts, use `setSelfInfo` to set your nickname and avatar.
691
+
692
+ **setSelfInfo Example:**
693
+
694
+ ```kotlin
695
+ val user = UserProfile()
696
+ user.userID = "" // Your userId
697
+ user.avatarURL = "" // Avatar URL
698
+ user.nickname = "" // Nickname to set
699
+
700
+ LoginStore.shared.setSelfInfo(user, object : CompletionHandler {
701
+ override fun onSuccess() {
702
+ // Success callback
703
+ }
704
+
705
+ override fun onFailure(code: Int, desc: String) {
706
+ // Failure callback
707
+ }
708
+ })
709
+ ```
710
+
711
+ **setSelfInfo API Parameters:**
712
+
713
+ | **Parameter** | **Type** | **Required** | **Description** |
714
+ | --- | --- | --- | --- |
715
+ | `userProfile` | [UserProfile](https://liteav.sdk.cloudcachetci.com/doc/product/tuikit/atomic-x/android/en/-atomic-x%20-core%20-a-p-i/io.trtc.tuikit.atomicxcore.api.login/-user-profile/index.html) | Yes | User profile struct.<br>- `userID` (`String`): User ID.<br>- `avatarURL` (`String`): Avatar URL.<br>- `nickname` (`String`): Nickname.<br>See [UserProfile](https://liteav.sdk.cloudcachetci.com/doc/product/tuikit/atomic-x/android/en/-atomic-x%20-core%20-a-p-i/io.trtc.tuikit.atomicxcore.api.login/-user-profile/index.html) for more fields. |
716
+ | `completion` | `CompletionHandler` | No | Callback for the result of the operation. |
717
+
718
+ ### Switch Layout Mode
719
+
720
+ Use `setLayoutTemplate` to switch between layout modes. If not set, **CallCoreView** adapts automatically: 1v1 calls use `Float` mode by default, while multi-party calls use `Grid` mode.
721
+
722
+ | **Mode** | **Layout** | **Interaction** |
723
+ | --- | --- | --- |
724
+ | **Float Mode** | Full screen self-view while waiting; after connecting, full screen remote view with self-view as a floating window. | Floating window supports drag and click-to-swap with the main view. |
725
+ | **Grid Mode** | Grid layout for all participants, suitable for 2+ users. Supports click-to-enlarge. | Click any participant's view to enlarge. |
726
+ | **PIP Mode** | 1v1 shows fixed remote view; multi-party uses active speaker strategy with full screen for the current speaker. | Waiting state shows self-view; after connecting, call duration is displayed. |
727
+
728
+ **setLayoutTemplate Example:**
729
+
730
+ ```kotlin
731
+ private fun setLayoutTemplate() {
732
+ val callCoreView = CallCoreView(context)
733
+ val template = CallLayoutTemplate.Grid
734
+ // Set layout mode
735
+ callCoreView.setLayoutTemplate(template)
736
+ setContentView(callCoreView)
737
+ }
738
+ ```
739
+
740
+ **setLayoutTemplate API Parameters:**
741
+
742
+ | **Parameter** | **Type** | **Required** | **Description** |
743
+ | --- | --- | --- | --- |
744
+ | `template` | [CallLayoutTemplate](https://liteav.sdk.cloudcachetci.com/doc/product/tuikit/atomic-x/android/en/-atomic-x%20-core%20-a-p-i/io.trtc.tuikit.atomicxcore.api.view/-call-layout-template/index.html) | Yes | `CallCoreView` layout mode.<br>- `CallLayoutTemplate.Float`: Full screen self-view while waiting; after connecting, full screen remote view with self-view as a floating window.<br>- `CallLayoutTemplate.Grid`: Grid layout for all members, suitable for 2+ participants, supports click-to-enlarge.<br>- `CallLayoutTemplate.Pip`: 1v1 fixed remote view; multi-party uses active speaker strategy. |
745
+
746
+ ### Set Default Call Timeout
747
+
748
+ When making a call with `calls`, set the `timeout` field in `CallParams` to specify the waiting timeout.
749
+
750
+ ```kotlin
751
+ val callParams = CallParams()
752
+ callParams.timeout = 30 // Set call timeout
753
+ CallStore.shared.calls(userIdList, CallMediaType.Video, callParams, null)
754
+ ```
755
+
756
+ **calls API Parameter Reference:**
757
+
758
+ | **Parameter** | **Type** | **Required** | **Description** |
759
+ | --- | --- | --- | --- |
760
+ | `userIdList` | `List` | Yes | List of target `userId`s. |
761
+ | `mediaType` | [CallMediaType](https://liteav.sdk.cloudcachetci.com/doc/product/tuikit/atomic-x/android/en/-atomic-x%20-core%20-a-p-i/io.trtc.tuikit.atomicxcore.api.call/-call-media-type/index.html) | Yes | Call media type: audio or video.<br>- `CallMediaType.Video`: Video call.<br>- `CallMediaType.Audio`: Audio call. |
762
+ | `params` | [CallParams](https://liteav.sdk.cloudcachetci.com/doc/product/tuikit/atomic-x/android/en/-atomic-x%20-core%20-a-p-i/io.trtc.tuikit.atomicxcore.api.call/-call-params/index.html) | No | Optional call parameters, such as room ID, call invitation timeout, etc.<br>- `roomId` (`String`): Room ID, optional.<br>- `timeout` (`Int`): Call timeout (seconds).<br>- `userData` (`String`): Custom user data.<br>- `chatGroupId` (`String`): Chat group ID for group calls.<br>- `isEphemeralCall` (`Boolean`): If `true`, the call is encrypted and not logged. |
763
+
764
+ ### Implement Picture-in-Picture (PiP)
765
+
766
+ Picture-in-Picture requires Android 8.0 (API 26) or higher. For best results, switch **CallCoreView** to `Pip` layout when entering PiP mode.
767
+
768
+ **Start PiP:** Call the system method `enterPictureInPictureMode`.
769
+
770
+ ```kotlin
771
+ private fun enterPictureInPictureModeWithBuild() {
772
+ if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O && hasPipModePermission()) {
773
+ val pictureInPictureParams: PictureInPictureParams.Builder = PictureInPictureParams.Builder()
774
+ val floatViewWidth = resources.getDimensionPixelSize(R.dimen.callkit_video_small_view_width)
775
+ val floatViewHeight = resources.getDimensionPixelSize(R.dimen.callkit_video_small_view_height)
776
+ val aspectRatio = Rational(floatViewWidth, floatViewHeight)
777
+ pictureInPictureParams.setAspectRatio(aspectRatio).build()
778
+ this.enterPictureInPictureMode(pictureInPictureParams.build())
779
+ }
780
+ }
781
+ ```
782
+
783
+ **On PiP Entered:** In `onPictureInPictureModeChanged`, set **CallCoreView** layout to **Pip** when entering PiP mode.
784
+
785
+ ```kotlin
786
+ val callCoreView = CallCoreView(context)
787
+
788
+ override fun onPictureInPictureModeChanged(isInPictureInPictureMode: Boolean) {
789
+ super.onPictureInPictureModeChanged(isInPictureInPictureMode)
790
+ if (isInPictureInPictureMode) {
791
+ callCoreView.setLayoutTemplate(CallLayoutTemplate.Pip)
792
+ }
793
+ }
794
+ ```
795
+
796
+ **Return from PiP:** When returning from PiP (full screen), `onResume` is triggered. Reset the layout based on participant count: use `Float` for 1v1, `Grid` for multi-party.
797
+
798
+ ```kotlin
799
+ val callCoreView = CallCoreView(context)
800
+
801
+ override fun onResume() {
802
+ super.onResume()
803
+ val allParticipants = CallStore.shared.observerState.allParticipants.value
804
+ if (allParticipants.size > 2) {
805
+ callCoreView?.setLayoutTemplate(CallLayoutTemplate.Grid)
806
+ } else {
807
+ callCoreView?.setLayoutTemplate(CallLayoutTemplate.Float)
808
+ }
809
+ }
810
+ ```
811
+
812
+ ### Keep Screen Awake During Call
813
+
814
+ To keep the screen on during a call, use the `WindowManager.LayoutParams.FLAG_KEEP_SCREEN_ON` flag. This is the recommended approach.
815
+
816
+ ```kotlin
817
+ class CallActivity : AppCompatActivity() {
818
+ override fun onCreate(savedInstanceState: Bundle?) {
819
+ super.onCreate(savedInstanceState)
820
+
821
+ window.addFlags(
822
+ WindowManager.LayoutParams.FLAG_SHOW_WHEN_LOCKED or // Show when locked
823
+ WindowManager.LayoutParams.FLAG_DISMISS_KEYGUARD or // Dismiss keyguard
824
+ WindowManager.LayoutParams.FLAG_KEEP_SCREEN_ON or // Keep screen on
825
+ WindowManager.LayoutParams.FLAG_TURN_SCREEN_ON // Turn screen on
826
+ )
827
+
828
+ setContentView(R.layout.activity_call)
829
+ }
830
+ }
831
+ ```
832
+
833
+ ### Play Waiting Ringtone
834
+
835
+ You can observe call state and play a ringtone while waiting for an answer. Stop the ringtone when the call is answered or ends.
836
+
837
+ ```kotlin
838
+ MainScope().launch {
839
+ CallStore.shared.observerState.selfInfo.collect { selfInfo ->
840
+ if (selfInfo.status == CallParticipantStatus.Accept || selfInfo.status == CallParticipantStatus.None) {
841
+ // Stop ringtone
842
+ return@collect
843
+ }
844
+ if (selfInfo.status == CallParticipantStatus.Waiting) {
845
+ // Play ringtone
846
+ }
847
+ }
848
+ }
849
+ ```
850
+
851
+ ### Enable Background Audio/Video Capture
852
+
853
+ 1. **Configure permissions and service (`AndroidManifest.xml`):** Starting with Android 9.0 (API 28), you must declare foreground service permissions. Android 14 (API 34) requires explicit service types (microphone and camera).
854
+
855
+ ```xml
856
+ <manifest xmlns:android="http://schemas.android.com/apk/res/android">
857
+
858
+ <uses-permission android:name="android.permission.FOREGROUND_SERVICE" />
859
+ <uses-permission android:name="android.permission.FOREGROUND_SERVICE_CAMERA" />
860
+ <uses-permission android:name="android.permission.FOREGROUND_SERVICE_MICROPHONE" />
861
+
862
+ <application>
863
+ <service
864
+ android:name=".CallForegroundService"
865
+ android:enabled="true"
866
+ android:exported="false"
867
+ android:foregroundServiceType="camera|microphone" />
868
+ </application>
869
+ </manifest>
870
+ ```
871
+
872
+ 2. **Create a Foreground Service class (`CallForegroundService`):**
873
+
874
+ ```kotlin
875
+ import android.app.Notification
876
+ import android.app.NotificationChannel
877
+ import android.app.NotificationManager
878
+ import android.app.Service
879
+ import android.content.Context
880
+ import android.content.Intent
881
+ import android.os.Build
882
+ import android.os.IBinder
883
+ import androidx.core.app.NotificationCompat
884
+
885
+ class CallForegroundService : Service() {
886
+ companion object {
887
+ private const val NOTIFICATION_ID = 1001
888
+ private const val CHANNEL_ID = "call_foreground_channel"
889
+
890
+ fun start(context: Context) {
891
+ val intent = Intent(context, CallForegroundService::class.java)
892
+ if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O) {
893
+ context.startForegroundService(intent)
894
+ } else {
895
+ context.startService(intent)
896
+ }
897
+ }
898
+
899
+ fun stop(context: Context) {
900
+ val intent = Intent(context, CallForegroundService::class.java)
901
+ context.stopService(intent)
902
+ }
903
+ }
904
+
905
+ override fun onCreate() {
906
+ super.onCreate()
907
+ createNotificationChannel()
908
+ // Start foreground notification to ensure background capture permission
909
+ startForeground(NOTIFICATION_ID, createNotification())
910
+ }
911
+
912
+ override fun onBind(intent: Intent?): IBinder? = null
913
+
914
+ private fun createNotification(): Notification {
915
+ return NotificationCompat.Builder(this, CHANNEL_ID)
916
+ .setContentTitle("In Call")
917
+ .setContentText("App is running in the background to keep the call active")
918
+ .setSmallIcon(android.R.drawable.ic_menu_call) // Replace with your app icon
919
+ .setPriority(NotificationCompat.PRIORITY_HIGH)
920
+ .build()
921
+ }
922
+
923
+ private fun createNotificationChannel() {
924
+ if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O) {
925
+ val channel = NotificationChannel(
926
+ CHANNEL_ID,
927
+ "Call Keep-Alive Service",
928
+ NotificationManager.IMPORTANCE_HIGH
929
+ )
930
+ val manager = getSystemService(NotificationManager::class.java)
931
+ manager.createNotificationChannel(channel)
932
+ }
933
+ }
934
+ }
935
+ ```
936
+
937
+ ## FAQs
938
+
939
+ ### Why does the UI show incorrectly when I open other activities after entering Picture-in-Picture?
940
+
941
+ **Reason:** Android's PiP mode is based on the task stack. By default, all Activities share the same task stack. If you launch a new Activity during PiP, it may appear in the PiP window, causing UI issues.
942
+
943
+ **Solution:** Declare the call Activity as a separate task stack in `AndroidManifest.xml`.
944
+
945
+ ```xml
946
+ <activity
947
+ android:name=".view.CallActivity"
948
+ android:launchMode="singleTask"
949
+ android:taskAffinity="${applicationId}.call" />
950
+ ```
951
+
952
+ ### If the callee comes online before the call invitation times out, do they get the incoming call?
953
+
954
+ For single calls, if the callee comes online within the timeout, they will receive an incoming call invitation. For group calls, up to 20 unhandled group messages are pulled when coming online within the timeout; if a call invitation exists, the incoming call event is triggered.