@stream-io/react-native-callingx 0.1.0-beta.7 → 0.1.1-beta.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/android/build.gradle +7 -1
- package/android/src/main/AndroidManifest.xml +31 -1
- package/android/src/main/java/io/getstream/rn/callingx/CallEventBroadcastReceiver.kt +17 -0
- package/android/src/main/java/io/getstream/rn/callingx/CallRegistrationStore.kt +176 -0
- package/android/src/main/java/io/getstream/rn/callingx/CallService.kt +302 -80
- package/android/src/main/java/io/getstream/rn/callingx/CallingxModuleImpl.kt +176 -191
- package/android/src/main/java/io/getstream/rn/callingx/StreamMessagingService.kt +48 -0
- package/android/src/main/java/io/getstream/rn/callingx/model/Call.kt +1 -0
- package/android/src/main/java/io/getstream/rn/callingx/notifications/CallNotificationManager.kt +196 -46
- package/android/src/main/java/io/getstream/rn/callingx/notifications/NotificationIntentFactory.kt +14 -8
- package/android/src/main/java/io/getstream/rn/callingx/notifications/NotificationReceiverActivity.kt +12 -1
- package/android/src/main/java/io/getstream/rn/callingx/notifications/NotificationReceiverService.kt +7 -0
- package/android/src/main/java/io/getstream/rn/callingx/repo/CallRepository.kt +38 -19
- package/android/src/main/java/io/getstream/rn/callingx/repo/LegacyCallRepository.kt +64 -55
- package/android/src/main/java/io/getstream/rn/callingx/repo/TelecomCallRepository.kt +241 -195
- package/android/src/main/java/io/getstream/rn/callingx/utils/CallEventBus.kt +61 -0
- package/android/src/main/java/io/getstream/rn/callingx/utils/SettingsStore.kt +51 -0
- package/android/src/newarch/java/io/getstream/rn/callingx/CallingxModule.kt +12 -3
- package/android/src/oldarch/java/io/getstream/rn/callingx/CallingxModule.kt +13 -3
- package/dist/module/CallingxModule.js +13 -10
- package/dist/module/CallingxModule.js.map +1 -1
- package/dist/module/spec/NativeCallingx.js.map +1 -1
- package/dist/module/utils/constants.js +24 -13
- package/dist/module/utils/constants.js.map +1 -1
- package/dist/typescript/src/CallingxModule.d.ts +3 -0
- package/dist/typescript/src/CallingxModule.d.ts.map +1 -1
- package/dist/typescript/src/spec/NativeCallingx.d.ts +7 -1
- package/dist/typescript/src/spec/NativeCallingx.d.ts.map +1 -1
- package/dist/typescript/src/types.d.ts +31 -0
- package/dist/typescript/src/types.d.ts.map +1 -1
- package/dist/typescript/src/utils/constants.d.ts +1 -1
- package/dist/typescript/src/utils/constants.d.ts.map +1 -1
- package/ios/AudioSessionManager.swift +2 -2
- package/ios/Callingx.mm +41 -17
- package/ios/CallingxImpl.swift +213 -83
- package/ios/Settings.swift +2 -2
- package/ios/UUIDStorage.swift +10 -10
- package/ios/VoipNotificationsManager.swift +8 -8
- package/package.json +4 -2
- package/src/CallingxModule.ts +14 -10
- package/src/spec/NativeCallingx.ts +10 -3
- package/src/types.ts +34 -0
- package/src/utils/constants.ts +23 -9
- /package/android/src/main/java/io/getstream/rn/callingx/{ResourceUtils.kt → utils/ResourceUtils.kt} +0 -0
- /package/android/src/main/java/io/getstream/rn/callingx/{Utils.kt → utils/Utils.kt} +0 -0
package/android/src/main/java/io/getstream/rn/callingx/notifications/CallNotificationManager.kt
CHANGED
|
@@ -20,11 +20,15 @@ import io.getstream.rn.callingx.debugLog
|
|
|
20
20
|
import io.getstream.rn.callingx.getDisconnectCauseString
|
|
21
21
|
import io.getstream.rn.callingx.model.Call
|
|
22
22
|
import io.getstream.rn.callingx.repo.CallRepository
|
|
23
|
+
import io.getstream.rn.callingx.utils.SettingsStore
|
|
24
|
+
import androidx.core.graphics.toColorInt
|
|
23
25
|
|
|
24
26
|
/**
|
|
25
27
|
* Handles call status changes and updates the notification accordingly. For more guidance around
|
|
26
28
|
* notifications check https://developer.android.com/develop/ui/views/notifications
|
|
27
29
|
*
|
|
30
|
+
* Supports multiple simultaneous call notifications, each keyed by callId.
|
|
31
|
+
*
|
|
28
32
|
* @see updateCallNotification
|
|
29
33
|
*/
|
|
30
34
|
class CallNotificationManager(
|
|
@@ -35,110 +39,239 @@ class CallNotificationManager(
|
|
|
35
39
|
|
|
36
40
|
internal companion object {
|
|
37
41
|
private const val TAG = "[Callingx] CallNotificationManager"
|
|
38
|
-
|
|
39
|
-
const val NOTIFICATION_ID = 200
|
|
42
|
+
private const val DISABLED_COLOR = "#757575" // NOTE: hint color might be ignored by OS
|
|
40
43
|
}
|
|
41
44
|
|
|
45
|
+
enum class OptimisticState { NONE, ACCEPTING, REJECTING }
|
|
46
|
+
|
|
47
|
+
private val lock = Any()
|
|
48
|
+
|
|
42
49
|
private var notificationsConfig = NotificationsConfig.loadNotificationsConfig(context)
|
|
43
50
|
|
|
44
51
|
private var ringtone: Ringtone? = null
|
|
45
52
|
|
|
46
|
-
|
|
53
|
+
/**
|
|
54
|
+
* Per-call notification state. Consolidates all per-call tracking into a single struct.
|
|
55
|
+
*/
|
|
56
|
+
private data class CallNotificationState(
|
|
57
|
+
val optimisticState: OptimisticState = OptimisticState.NONE,
|
|
58
|
+
val lastSnapshot: NotificationSnapshot? = null,
|
|
59
|
+
val activeWhen: Long? = null,
|
|
60
|
+
val hasBecameActive: Boolean = false,
|
|
61
|
+
)
|
|
62
|
+
|
|
63
|
+
// Per-call state, all guarded by [lock]
|
|
64
|
+
private val notificationsState = mutableMapOf<String, CallNotificationState>()
|
|
47
65
|
|
|
48
|
-
|
|
66
|
+
/** The callId whose notification was used for startForeground(). */
|
|
67
|
+
private var foregroundCallId: String? = null
|
|
68
|
+
|
|
69
|
+
/**
|
|
70
|
+
* Deterministic per-call notification ID.
|
|
71
|
+
*
|
|
72
|
+
* `NotificationManager.notify()`/`cancel()` require a stable integer id for updates/cancels.
|
|
73
|
+
* We derive it from `callId` so we don't need to allocate/store ids.
|
|
74
|
+
*/
|
|
75
|
+
private fun getNotificationId(callId: String): Int {
|
|
76
|
+
// Keep the value non-negative (defensive for Android-side expectations).
|
|
77
|
+
return callId.hashCode() and 0x7fffffff
|
|
78
|
+
}
|
|
49
79
|
|
|
50
80
|
/**
|
|
51
81
|
* Snapshot of call state used to detect notification changes.
|
|
52
|
-
* Using a data class ensures immutable comparison and avoids issues
|
|
53
|
-
* with mutable Call.Registered objects.
|
|
54
82
|
*/
|
|
55
|
-
|
|
83
|
+
data class NotificationSnapshot(
|
|
56
84
|
val id: String,
|
|
57
85
|
val isActive: Boolean,
|
|
58
86
|
val isIncoming: Boolean,
|
|
87
|
+
val optimisticState: OptimisticState,
|
|
59
88
|
val displayTitle: String?,
|
|
60
89
|
val displaySubtitle: String?,
|
|
61
90
|
val displayName: CharSequence,
|
|
62
91
|
val address: Uri
|
|
63
92
|
)
|
|
64
93
|
|
|
65
|
-
|
|
94
|
+
/**
|
|
95
|
+
* Creates a snapshot of the call state used to detect notification changes.
|
|
96
|
+
* @return NotificationSnapshot
|
|
97
|
+
*/
|
|
98
|
+
private fun Call.Registered.toSnapshot(callId: String) = NotificationSnapshot(
|
|
66
99
|
id = id,
|
|
67
100
|
isActive = isActive,
|
|
68
101
|
isIncoming = isIncoming(),
|
|
102
|
+
optimisticState = notificationsState[callId]?.optimisticState ?: OptimisticState.NONE,
|
|
69
103
|
displayTitle = displayOptions?.getString(CallService.EXTRA_DISPLAY_TITLE),
|
|
70
104
|
displaySubtitle = displayOptions?.getString(CallService.EXTRA_DISPLAY_SUBTITLE),
|
|
71
105
|
displayName = callAttributes.displayName,
|
|
72
106
|
address = callAttributes.address
|
|
73
107
|
)
|
|
74
108
|
|
|
75
|
-
fun
|
|
109
|
+
fun getOrCreateNotificationId(callId: String): Int = synchronized(lock) {
|
|
110
|
+
if (!notificationsState.containsKey(callId)) {
|
|
111
|
+
notificationsState[callId] = CallNotificationState()
|
|
112
|
+
}
|
|
113
|
+
if (foregroundCallId == null) {
|
|
114
|
+
foregroundCallId = callId
|
|
115
|
+
}
|
|
116
|
+
return@synchronized getNotificationId(callId)
|
|
117
|
+
}
|
|
118
|
+
|
|
119
|
+
/**
|
|
120
|
+
* Sets the optimistic state of the call notification.
|
|
121
|
+
* Optimistic state is used to update the notification text while the app is connecting or declining the call.
|
|
122
|
+
* @param state The optimistic state to set.
|
|
123
|
+
*/
|
|
124
|
+
fun setOptimisticState(callId: String, state: OptimisticState) = synchronized(lock) {
|
|
125
|
+
// Be resilient to races where we receive optimistic actions before a notification state entry exists.
|
|
126
|
+
val current =
|
|
127
|
+
notificationsState[callId]
|
|
128
|
+
?: CallNotificationState().also {
|
|
129
|
+
if (foregroundCallId == null) {
|
|
130
|
+
foregroundCallId = callId
|
|
131
|
+
}
|
|
132
|
+
}
|
|
133
|
+
notificationsState[callId] = if (state != OptimisticState.NONE) {
|
|
134
|
+
current.copy(optimisticState = state, lastSnapshot = null)
|
|
135
|
+
} else {
|
|
136
|
+
current.copy(optimisticState = state)
|
|
137
|
+
}
|
|
138
|
+
}
|
|
139
|
+
|
|
140
|
+
/**
|
|
141
|
+
* Creates a notification for the call.
|
|
142
|
+
* Notification is created based on the call state and optimistic state.
|
|
143
|
+
* @param call The call to create a notification for.
|
|
144
|
+
* @return The notification.
|
|
145
|
+
*/
|
|
146
|
+
fun createNotification(callId: String, call: Call.Registered): Notification = synchronized(lock) {
|
|
76
147
|
debugLog(TAG,"[notifications] createNotification: Creating notification for call ID: ${call.id}")
|
|
77
148
|
|
|
149
|
+
val state = notificationsState[callId]
|
|
150
|
+
val optimisticState = state?.optimisticState ?: OptimisticState.NONE
|
|
151
|
+
|
|
78
152
|
val contentIntent =
|
|
79
153
|
NotificationIntentFactory.getLaunchActivityIntent(
|
|
80
154
|
context,
|
|
81
155
|
CallingxModuleImpl.CALL_ANSWERED_ACTION,
|
|
82
156
|
call.id
|
|
83
157
|
)
|
|
84
|
-
val callStyle = createCallStyle(call)
|
|
85
|
-
val channelId = getChannelId(call)
|
|
158
|
+
val callStyle = createCallStyle(call, optimisticState)
|
|
159
|
+
val channelId = getChannelId(call, optimisticState)
|
|
86
160
|
debugLog(TAG, "[notifications] createNotification: Channel ID: $channelId")
|
|
87
161
|
|
|
88
162
|
val builder =
|
|
89
163
|
NotificationCompat.Builder(context, channelId)
|
|
90
164
|
.setContentIntent(contentIntent)
|
|
91
165
|
.setFullScreenIntent(contentIntent, true)
|
|
92
|
-
.setStyle(callStyle)
|
|
93
166
|
.setSmallIcon(R.drawable.ic_round_call_24)
|
|
94
167
|
.setCategory(NotificationCompat.CATEGORY_CALL)
|
|
95
168
|
.setPriority(NotificationCompat.PRIORITY_MAX)
|
|
96
|
-
.setOngoing(
|
|
169
|
+
.setOngoing(optimisticState != OptimisticState.REJECTING)
|
|
170
|
+
|
|
171
|
+
builder.setStyle(callStyle)
|
|
97
172
|
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
|
|
173
|
+
// When call becomes active we need to set the when to current time and show the chronometer
|
|
174
|
+
if (call.isActive && optimisticState == OptimisticState.NONE && state != null) {
|
|
175
|
+
// We need to set the activation time once when call becomes active
|
|
176
|
+
if (!state.hasBecameActive) {
|
|
177
|
+
debugLog(TAG, "[notifications] createNotification: Setting when to current time for $callId")
|
|
178
|
+
val now = System.currentTimeMillis()
|
|
179
|
+
notificationsState[callId] = state.copy(activeWhen = now, hasBecameActive = true)
|
|
180
|
+
}
|
|
181
|
+
builder.setWhen(notificationsState[callId]?.activeWhen ?: System.currentTimeMillis())
|
|
101
182
|
builder.setUsesChronometer(true)
|
|
102
183
|
builder.setShowWhen(true)
|
|
103
|
-
hasBecameActive = true
|
|
104
184
|
}
|
|
105
185
|
|
|
106
|
-
call
|
|
107
|
-
|
|
108
|
-
|
|
186
|
+
// If the call is not active and the optimistic state is not none, we need to set the notification text
|
|
187
|
+
// based on exact action that is being taken (accepting or rejecting)
|
|
188
|
+
if (optimisticState != OptimisticState.NONE && !call.isActive) {
|
|
189
|
+
val text = when (optimisticState) {
|
|
190
|
+
OptimisticState.ACCEPTING -> SettingsStore.getOptimisticAcceptingText(context)
|
|
191
|
+
OptimisticState.REJECTING -> SettingsStore.getOptimisticRejectingText(context)
|
|
192
|
+
else -> null
|
|
193
|
+
}
|
|
194
|
+
if (text != null) builder.setContentText(text)
|
|
195
|
+
} else {
|
|
196
|
+
// If the call is active, we need to set the notification text
|
|
197
|
+
// based on the call display options (defined on js side)
|
|
198
|
+
call.displayOptions?.let {
|
|
199
|
+
if (it.containsKey(CallService.EXTRA_DISPLAY_SUBTITLE)) {
|
|
200
|
+
builder.setContentText(it.getString(CallService.EXTRA_DISPLAY_SUBTITLE))
|
|
201
|
+
}
|
|
109
202
|
}
|
|
110
203
|
}
|
|
111
204
|
|
|
112
205
|
return builder.build()
|
|
113
206
|
}
|
|
114
207
|
|
|
115
|
-
|
|
116
|
-
|
|
117
|
-
|
|
118
|
-
|
|
119
|
-
|
|
120
|
-
|
|
121
|
-
|
|
122
|
-
|
|
123
|
-
|
|
124
|
-
|
|
125
|
-
|
|
126
|
-
|
|
127
|
-
|
|
128
|
-
|
|
208
|
+
/**
|
|
209
|
+
* Updates the call notification.
|
|
210
|
+
* If the call is None or Unregistered, we need to dismiss the notification.
|
|
211
|
+
* If the call is active and the optimistic state is not none, we need to reset the optimistic state.
|
|
212
|
+
* If the call is active and the optimistic state is none, we need to create a new notification.
|
|
213
|
+
* @param call The call to update the notification for.
|
|
214
|
+
*/
|
|
215
|
+
fun updateCallNotification(callId: String, call: Call.Registered) = synchronized(lock) {
|
|
216
|
+
val state = notificationsState[callId]
|
|
217
|
+
val optimisticState = state?.optimisticState ?: OptimisticState.NONE
|
|
218
|
+
if (call.isActive && optimisticState != OptimisticState.NONE && state != null) {
|
|
219
|
+
notificationsState[callId] = state.copy(optimisticState = OptimisticState.NONE)
|
|
220
|
+
debugLog(TAG, "[notifications] updateCallNotification[$callId]: Resetting optimistic state")
|
|
221
|
+
}
|
|
222
|
+
|
|
223
|
+
// If the new snapshot is the same as the last posted snapshot, we need to skip the update
|
|
224
|
+
val newSnapshot = call.toSnapshot(callId)
|
|
225
|
+
if (newSnapshot == notificationsState[callId]?.lastSnapshot) {
|
|
226
|
+
debugLog(TAG, "[notifications] updateCallNotification[$callId]: Skipping - no state change")
|
|
227
|
+
return@synchronized
|
|
228
|
+
}
|
|
229
|
+
|
|
230
|
+
val notificationId = getOrCreateNotificationId(callId)
|
|
231
|
+
notificationsState[callId] =
|
|
232
|
+
notificationsState[callId]?.copy(lastSnapshot = newSnapshot)
|
|
233
|
+
?: CallNotificationState(lastSnapshot = newSnapshot)
|
|
234
|
+
val notification = createNotification(callId, call)
|
|
235
|
+
notificationManager.notify(notificationId, notification)
|
|
236
|
+
debugLog(TAG, "[notifications] updateCallNotification[$callId]: Notification posted (id=$notificationId)")
|
|
237
|
+
}
|
|
238
|
+
|
|
239
|
+
fun postNotification(callId: String, notification: Notification) = synchronized(lock) {
|
|
240
|
+
val notificationId = getOrCreateNotificationId(callId)
|
|
241
|
+
notificationManager.notify(notificationId, notification)
|
|
242
|
+
}
|
|
129
243
|
|
|
130
|
-
|
|
131
|
-
|
|
132
|
-
|
|
133
|
-
|
|
244
|
+
/**
|
|
245
|
+
* Returns a new foreground notification ID if the caller needs to call startForeground()
|
|
246
|
+
* to re-promote the service, or null if no action is needed.
|
|
247
|
+
*/
|
|
248
|
+
fun cancelNotification(callId: String): Int? = synchronized(lock) {
|
|
249
|
+
debugLog(TAG, "[notifications] cancelNotification[$callId]")
|
|
250
|
+
val state = notificationsState.remove(callId)
|
|
251
|
+
val notificationId = getNotificationId(callId)
|
|
252
|
+
notificationManager.cancel(notificationId)
|
|
253
|
+
if (state != null) {
|
|
254
|
+
debugLog(TAG, "[notifications] cancelNotification[$callId]: Cancelled (id=$notificationId)")
|
|
255
|
+
}
|
|
256
|
+
|
|
257
|
+
if (foregroundCallId == callId) {
|
|
258
|
+
foregroundCallId = notificationsState.keys.firstOrNull()
|
|
259
|
+
// Return the new foreground notification ID so the service can re-promote
|
|
260
|
+
if (foregroundCallId != null) {
|
|
261
|
+
return@synchronized getNotificationId(foregroundCallId!!)
|
|
134
262
|
}
|
|
135
263
|
}
|
|
264
|
+
return@synchronized null
|
|
136
265
|
}
|
|
137
266
|
|
|
138
|
-
fun
|
|
139
|
-
|
|
140
|
-
|
|
141
|
-
|
|
267
|
+
fun getForegroundCallId(): String? = synchronized(lock) { foregroundCallId }
|
|
268
|
+
|
|
269
|
+
fun cancelAllNotifications() = synchronized(lock) {
|
|
270
|
+
for (callId in notificationsState.keys) {
|
|
271
|
+
notificationManager.cancel(getNotificationId(callId))
|
|
272
|
+
}
|
|
273
|
+
notificationsState.clear()
|
|
274
|
+
foregroundCallId = null
|
|
142
275
|
}
|
|
143
276
|
|
|
144
277
|
fun startRingtone() {
|
|
@@ -174,18 +307,24 @@ class CallNotificationManager(
|
|
|
174
307
|
ringtone = null
|
|
175
308
|
}
|
|
176
309
|
|
|
177
|
-
|
|
178
|
-
|
|
310
|
+
fun resetOptimisticState(callId: String) = synchronized(lock) {
|
|
311
|
+
debugLog(TAG, "[notifications] resetOptimisticState[$callId]: Resetting optimistic state")
|
|
312
|
+
val current = notificationsState[callId] ?: return@synchronized
|
|
313
|
+
notificationsState[callId] = current.copy(optimisticState = OptimisticState.NONE, lastSnapshot = null)
|
|
314
|
+
}
|
|
315
|
+
|
|
316
|
+
private fun getChannelId(call: Call.Registered, optimisticState: OptimisticState): String {
|
|
317
|
+
return if (call.isIncoming() && !call.isActive && optimisticState == OptimisticState.NONE) {
|
|
179
318
|
notificationsConfig.incomingChannel.id
|
|
180
319
|
} else {
|
|
181
320
|
notificationsConfig.ongoingChannel.id
|
|
182
321
|
}
|
|
183
322
|
}
|
|
184
323
|
|
|
185
|
-
private fun createCallStyle(call: Call.Registered): NotificationCompat.CallStyle {
|
|
324
|
+
private fun createCallStyle(call: Call.Registered, optimisticState: OptimisticState): NotificationCompat.CallStyle? {
|
|
186
325
|
val caller = createPerson(call)
|
|
187
326
|
|
|
188
|
-
if (call.isIncoming() && !call.isActive) {
|
|
327
|
+
if (call.isIncoming() && !call.isActive && optimisticState == OptimisticState.NONE) {
|
|
189
328
|
return NotificationCompat.CallStyle.forIncomingCall(
|
|
190
329
|
caller,
|
|
191
330
|
NotificationIntentFactory.getPendingBroadcastIntent(
|
|
@@ -211,6 +350,17 @@ class CallNotificationManager(
|
|
|
211
350
|
)
|
|
212
351
|
}
|
|
213
352
|
|
|
353
|
+
if (optimisticState == OptimisticState.REJECTING) {
|
|
354
|
+
return NotificationCompat.CallStyle.forOngoingCall(
|
|
355
|
+
caller,
|
|
356
|
+
NotificationIntentFactory.getPendingBroadcastIntent(
|
|
357
|
+
context,
|
|
358
|
+
"io.getstream.CALL_END_NOOP",
|
|
359
|
+
call.id
|
|
360
|
+
) ,
|
|
361
|
+
).setDeclineButtonColorHint(DISABLED_COLOR.toColorInt())
|
|
362
|
+
}
|
|
363
|
+
|
|
214
364
|
return NotificationCompat.CallStyle.forOngoingCall(
|
|
215
365
|
caller,
|
|
216
366
|
NotificationIntentFactory.getPendingBroadcastIntent(
|
package/android/src/main/java/io/getstream/rn/callingx/notifications/NotificationIntentFactory.kt
CHANGED
|
@@ -5,12 +5,18 @@ import android.content.Context
|
|
|
5
5
|
import android.content.Intent
|
|
6
6
|
import android.os.Build
|
|
7
7
|
import io.getstream.rn.callingx.CallingxModuleImpl
|
|
8
|
+
import kotlin.math.absoluteValue
|
|
8
9
|
|
|
9
10
|
object NotificationIntentFactory {
|
|
10
|
-
//
|
|
11
|
+
// Base request codes for PendingIntents — combined with callId hash for uniqueness
|
|
11
12
|
private const val REQUEST_CODE_LAUNCH_ACTIVITY = 1001
|
|
12
|
-
private const val REQUEST_CODE_RECEIVER_ACTIVITY =
|
|
13
|
-
private const val REQUEST_CODE_SERVICE =
|
|
13
|
+
private const val REQUEST_CODE_RECEIVER_ACTIVITY = 2001
|
|
14
|
+
private const val REQUEST_CODE_SERVICE = 3001
|
|
15
|
+
|
|
16
|
+
/** Generates a unique request code per callId + base offset to avoid PendingIntent collisions. */
|
|
17
|
+
private fun requestCodeFor(callId: String, base: Int): Int {
|
|
18
|
+
return (base + callId.hashCode()).absoluteValue
|
|
19
|
+
}
|
|
14
20
|
|
|
15
21
|
fun getPendingNotificationIntent(
|
|
16
22
|
context: Context,
|
|
@@ -35,7 +41,7 @@ object NotificationIntentFactory {
|
|
|
35
41
|
|
|
36
42
|
return PendingIntent.getService(
|
|
37
43
|
context,
|
|
38
|
-
REQUEST_CODE_SERVICE,
|
|
44
|
+
requestCodeFor(callId, REQUEST_CODE_SERVICE),
|
|
39
45
|
intent,
|
|
40
46
|
PendingIntent.FLAG_UPDATE_CURRENT or PendingIntent.FLAG_MUTABLE
|
|
41
47
|
)
|
|
@@ -57,7 +63,7 @@ object NotificationIntentFactory {
|
|
|
57
63
|
|
|
58
64
|
return PendingIntent.getActivities(
|
|
59
65
|
context,
|
|
60
|
-
REQUEST_CODE_RECEIVER_ACTIVITY,
|
|
66
|
+
requestCodeFor(callId, REQUEST_CODE_RECEIVER_ACTIVITY),
|
|
61
67
|
arrayOf(launchActivityIntent, receiverIntent),
|
|
62
68
|
PendingIntent.FLAG_MUTABLE or PendingIntent.FLAG_UPDATE_CURRENT
|
|
63
69
|
)
|
|
@@ -75,7 +81,7 @@ object NotificationIntentFactory {
|
|
|
75
81
|
|
|
76
82
|
return PendingIntent.getActivity(
|
|
77
83
|
context,
|
|
78
|
-
REQUEST_CODE_LAUNCH_ACTIVITY,
|
|
84
|
+
requestCodeFor(callId, REQUEST_CODE_LAUNCH_ACTIVITY),
|
|
79
85
|
callIntent,
|
|
80
86
|
PendingIntent.FLAG_MUTABLE or PendingIntent.FLAG_UPDATE_CURRENT,
|
|
81
87
|
)
|
|
@@ -94,10 +100,10 @@ object NotificationIntentFactory {
|
|
|
94
100
|
addExtras(this)
|
|
95
101
|
}
|
|
96
102
|
|
|
97
|
-
// Use action
|
|
103
|
+
// Use action + callId hash for unique request code per action per call
|
|
98
104
|
return PendingIntent.getBroadcast(
|
|
99
105
|
context,
|
|
100
|
-
action.hashCode(),
|
|
106
|
+
(action.hashCode() + callId.hashCode()).absoluteValue,
|
|
101
107
|
intent,
|
|
102
108
|
PendingIntent.FLAG_IMMUTABLE or PendingIntent.FLAG_UPDATE_CURRENT
|
|
103
109
|
)
|
package/android/src/main/java/io/getstream/rn/callingx/notifications/NotificationReceiverActivity.kt
CHANGED
|
@@ -4,6 +4,7 @@ import android.app.Activity
|
|
|
4
4
|
import android.content.Intent
|
|
5
5
|
import android.os.Bundle
|
|
6
6
|
import io.getstream.rn.callingx.CallingxModuleImpl
|
|
7
|
+
import io.getstream.rn.callingx.debugLog
|
|
7
8
|
|
|
8
9
|
// For Android 12+
|
|
9
10
|
class NotificationReceiverActivity : Activity() {
|
|
@@ -26,10 +27,20 @@ class NotificationReceiverActivity : Activity() {
|
|
|
26
27
|
return
|
|
27
28
|
}
|
|
28
29
|
|
|
29
|
-
//we need it only for answered call event, as for cold start case we need to send broadcast event and to launch the app
|
|
30
30
|
if (intent.action == CallingxModuleImpl.CALL_ANSWERED_ACTION) {
|
|
31
|
+
debugLog("[Callingx] NotificationReceiverActivity", "[receiver] answered call action")
|
|
31
32
|
val callId = intent.getStringExtra(CallingxModuleImpl.EXTRA_CALL_ID)
|
|
32
33
|
val source = intent.getStringExtra(CallingxModuleImpl.EXTRA_SOURCE)
|
|
34
|
+
|
|
35
|
+
if (callId != null) {
|
|
36
|
+
Intent(CallingxModuleImpl.CALL_OPTIMISTIC_ACCEPT_ACTION)
|
|
37
|
+
.apply {
|
|
38
|
+
setPackage(packageName)
|
|
39
|
+
putExtra(CallingxModuleImpl.EXTRA_CALL_ID, callId)
|
|
40
|
+
}
|
|
41
|
+
.also { sendBroadcast(it) }
|
|
42
|
+
}
|
|
43
|
+
|
|
33
44
|
Intent(CallingxModuleImpl.CALL_ANSWERED_ACTION)
|
|
34
45
|
.apply {
|
|
35
46
|
setPackage(packageName)
|
package/android/src/main/java/io/getstream/rn/callingx/notifications/NotificationReceiverService.kt
CHANGED
|
@@ -34,6 +34,13 @@ class NotificationReceiverService : Service() {
|
|
|
34
34
|
val source = intent.getStringExtra(CallingxModuleImpl.EXTRA_SOURCE)
|
|
35
35
|
callId?.let {
|
|
36
36
|
try {
|
|
37
|
+
Intent(CallingxModuleImpl.CALL_OPTIMISTIC_ACCEPT_ACTION)
|
|
38
|
+
.apply {
|
|
39
|
+
setPackage(packageName)
|
|
40
|
+
putExtra(CallingxModuleImpl.EXTRA_CALL_ID, it)
|
|
41
|
+
}
|
|
42
|
+
.also { it -> sendBroadcast(it) }
|
|
43
|
+
|
|
37
44
|
NotificationIntentFactory.getPendingBroadcastIntent(
|
|
38
45
|
applicationContext,
|
|
39
46
|
CallingxModuleImpl.CALL_ANSWERED_ACTION,
|
|
@@ -29,7 +29,7 @@ abstract class CallRepository(protected val context: Context) {
|
|
|
29
29
|
}
|
|
30
30
|
|
|
31
31
|
interface Listener {
|
|
32
|
-
fun onCallStateChanged(call: Call)
|
|
32
|
+
fun onCallStateChanged(callId: String, call: Call)
|
|
33
33
|
fun onIsCallAnswered(callId: String, source: EventSource)
|
|
34
34
|
fun onIsCallDisconnected(callId: String?, cause: DisconnectCause, source: EventSource)
|
|
35
35
|
fun onIsCallInactive(callId: String)
|
|
@@ -39,8 +39,8 @@ abstract class CallRepository(protected val context: Context) {
|
|
|
39
39
|
fun onCallEndpointChanged(callId: String, endpoint: String)
|
|
40
40
|
}
|
|
41
41
|
|
|
42
|
-
protected val
|
|
43
|
-
val
|
|
42
|
+
protected val _calls: MutableStateFlow<Map<String, Call.Registered>> = MutableStateFlow(emptyMap())
|
|
43
|
+
val calls: StateFlow<Map<String, Call.Registered>> = _calls.asStateFlow()
|
|
44
44
|
|
|
45
45
|
protected var _listener: Listener? = null
|
|
46
46
|
protected val scope: CoroutineScope = CoroutineScope(SupervisorJob() + Dispatchers.Default)
|
|
@@ -65,9 +65,19 @@ abstract class CallRepository(protected val context: Context) {
|
|
|
65
65
|
isVideo: Boolean,
|
|
66
66
|
displayOptions: Bundle?,
|
|
67
67
|
) {
|
|
68
|
-
|
|
68
|
+
updateCallById(callId) { copy(displayOptions = displayOptions) }
|
|
69
69
|
}
|
|
70
70
|
|
|
71
|
+
fun getCall(callId: String): Call.Registered? = _calls.value[callId]
|
|
72
|
+
|
|
73
|
+
fun hasAnyCalls(): Boolean = _calls.value.isNotEmpty()
|
|
74
|
+
|
|
75
|
+
fun hasRingingCall(excludeCallId: String? = null): Boolean =
|
|
76
|
+
_calls.value.any { (id, c) -> id != excludeCallId && !c.isPending && c.isIncoming() && !c.isActive }
|
|
77
|
+
|
|
78
|
+
fun hasActiveCall(excludeCallId: String? = null): Boolean =
|
|
79
|
+
_calls.value.any { (id, c) -> id != excludeCallId && !c.isPending && c.isActive }
|
|
80
|
+
|
|
71
81
|
//this call instance is used to display call notification before the call is registered, this is needed to invoke startForeground method on the service
|
|
72
82
|
public fun getTempCall(callInfo: CallService.CallInfo, incoming: Boolean): Call.Registered {
|
|
73
83
|
val attributes = createCallAttributes(
|
|
@@ -79,6 +89,7 @@ abstract class CallRepository(protected val context: Context) {
|
|
|
79
89
|
|
|
80
90
|
return Call.Registered(
|
|
81
91
|
id = callInfo.callId,
|
|
92
|
+
isPending = true,
|
|
82
93
|
isActive = false,
|
|
83
94
|
isOnHold = false,
|
|
84
95
|
callAttributes = attributes,
|
|
@@ -92,34 +103,42 @@ abstract class CallRepository(protected val context: Context) {
|
|
|
92
103
|
}
|
|
93
104
|
|
|
94
105
|
/**
|
|
95
|
-
* Update the
|
|
96
|
-
*
|
|
106
|
+
* Update the state of a specific call applying the transform lambda only if the call is
|
|
107
|
+
* found in the map. Otherwise keep the current state.
|
|
97
108
|
*/
|
|
98
|
-
protected fun
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
"[repository] updateCurrentCall: Current call state: ${currentState::class.simpleName}"
|
|
103
|
-
)
|
|
104
|
-
|
|
105
|
-
_currentCall.update { call ->
|
|
106
|
-
if (call is Call.Registered) {
|
|
109
|
+
protected fun updateCallById(callId: String, transform: Call.Registered.() -> Call) {
|
|
110
|
+
_calls.update { currentMap ->
|
|
111
|
+
val call = currentMap[callId]
|
|
112
|
+
if (call != null) {
|
|
107
113
|
val updated = call.transform()
|
|
108
114
|
debugLog(
|
|
109
115
|
getTag(),
|
|
110
|
-
"[repository]
|
|
116
|
+
"[repository] updateCallById: Call $callId state updated to: ${updated::class.simpleName}"
|
|
111
117
|
)
|
|
112
|
-
updated
|
|
118
|
+
if (updated is Call.Registered) {
|
|
119
|
+
currentMap + (callId to updated)
|
|
120
|
+
} else {
|
|
121
|
+
// Call transitioned to non-Registered state (e.g. Unregistered) — remove from map
|
|
122
|
+
currentMap - callId
|
|
123
|
+
}
|
|
113
124
|
} else {
|
|
114
125
|
Log.w(
|
|
115
126
|
getTag(),
|
|
116
|
-
"[repository]
|
|
127
|
+
"[repository] updateCallById: Call $callId not found in map, skipping update"
|
|
117
128
|
)
|
|
118
|
-
|
|
129
|
+
currentMap
|
|
119
130
|
}
|
|
120
131
|
}
|
|
121
132
|
}
|
|
122
133
|
|
|
134
|
+
protected fun addCall(callId: String, call: Call.Registered) {
|
|
135
|
+
_calls.update { it + (callId to call) }
|
|
136
|
+
}
|
|
137
|
+
|
|
138
|
+
protected fun removeCall(callId: String) {
|
|
139
|
+
_calls.update { it - callId }
|
|
140
|
+
}
|
|
141
|
+
|
|
123
142
|
protected fun createCallAttributes(
|
|
124
143
|
displayName: String,
|
|
125
144
|
address: Uri,
|