@stream-io/react-native-callingx 0.1.0-beta.7 → 0.1.1-beta.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/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 +145 -0
- package/android/src/main/java/io/getstream/rn/callingx/CallService.kt +301 -83
- package/android/src/main/java/io/getstream/rn/callingx/CallingxModuleImpl.kt +148 -390
- 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 +188 -48
- 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 +20 -24
- package/dist/module/CallingxModule.js.map +1 -1
- package/dist/module/spec/NativeCallingx.js.map +1 -1
- package/dist/module/utils/constants.js +24 -14
- package/dist/module/utils/constants.js.map +1 -1
- package/dist/typescript/src/CallingxModule.d.ts +4 -2
- package/dist/typescript/src/CallingxModule.d.ts.map +1 -1
- package/dist/typescript/src/spec/NativeCallingx.d.ts +7 -4
- package/dist/typescript/src/spec/NativeCallingx.d.ts.map +1 -1
- package/dist/typescript/src/types.d.ts +33 -5
- package/dist/typescript/src/types.d.ts.map +1 -1
- package/dist/typescript/src/utils/constants.d.ts +2 -3
- 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 +20 -21
- package/src/spec/NativeCallingx.ts +10 -6
- package/src/types.ts +36 -4
- package/src/utils/constants.ts +23 -12
- /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
|
@@ -0,0 +1,48 @@
|
|
|
1
|
+
package io.getstream.rn.callingx
|
|
2
|
+
|
|
3
|
+
import android.annotation.SuppressLint
|
|
4
|
+
import com.google.firebase.messaging.RemoteMessage
|
|
5
|
+
import io.invertase.firebase.messaging.ReactNativeFirebaseMessagingService
|
|
6
|
+
|
|
7
|
+
/**
|
|
8
|
+
* Extends React Native Firebase's messaging service to start [CallService] when a
|
|
9
|
+
* data message contains "stream" (e.g. incoming call push), then delegates to the
|
|
10
|
+
* parent so setBackgroundMessageHandler() still runs in JS.
|
|
11
|
+
*
|
|
12
|
+
* Only compiled when the app has @react-native-firebase/app and @react-native-firebase/messaging
|
|
13
|
+
* as dependencies. The app must remove the default [io.invertase.firebase.messaging.ReactNativeFirebaseMessagingService] from
|
|
14
|
+
* the merged manifest so this service is the single FCM handler
|
|
15
|
+
*/
|
|
16
|
+
@SuppressLint("MissingFirebaseInstanceTokenRefresh")
|
|
17
|
+
class StreamMessagingService : ReactNativeFirebaseMessagingService() {
|
|
18
|
+
|
|
19
|
+
companion object {
|
|
20
|
+
const val TAG = "[Callingx] StreamMessagingService"
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
override fun onMessageReceived(remoteMessage: RemoteMessage) {
|
|
24
|
+
val data = remoteMessage.data
|
|
25
|
+
debugLog(TAG, "onMessageReceived data = $data")
|
|
26
|
+
|
|
27
|
+
val isSupportedStreamVideoCallRing =
|
|
28
|
+
data["sender"] == "stream.video" && data["type"] == "call.ring"
|
|
29
|
+
|
|
30
|
+
if (isSupportedStreamVideoCallRing) {
|
|
31
|
+
val callCid = data["call_cid"]
|
|
32
|
+
if (callCid.isNullOrEmpty()) {
|
|
33
|
+
debugLog(
|
|
34
|
+
TAG,
|
|
35
|
+
"missing call_cid for call.ring, skipping CallService start",
|
|
36
|
+
)
|
|
37
|
+
} else {
|
|
38
|
+
CallService.startIncomingCallFromPush(applicationContext, data)
|
|
39
|
+
}
|
|
40
|
+
} else {
|
|
41
|
+
debugLog(TAG, "sender or type is not supported, skipping CallService start")
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
// Let React Native Firebase continue its normal processing so
|
|
45
|
+
// setBackgroundMessageHandler() still runs in JS.
|
|
46
|
+
super.onMessageReceived(remoteMessage)
|
|
47
|
+
}
|
|
48
|
+
}
|
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,229 @@ 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
|
-
val displaySubtitle: String?,
|
|
61
89
|
val displayName: CharSequence,
|
|
62
90
|
val address: Uri
|
|
63
91
|
)
|
|
64
92
|
|
|
65
|
-
|
|
93
|
+
/**
|
|
94
|
+
* Creates a snapshot of the call state used to detect notification changes.
|
|
95
|
+
* @return NotificationSnapshot
|
|
96
|
+
*/
|
|
97
|
+
private fun Call.Registered.toSnapshot(callId: String) = NotificationSnapshot(
|
|
66
98
|
id = id,
|
|
67
99
|
isActive = isActive,
|
|
68
100
|
isIncoming = isIncoming(),
|
|
101
|
+
optimisticState = notificationsState[callId]?.optimisticState ?: OptimisticState.NONE,
|
|
69
102
|
displayTitle = displayOptions?.getString(CallService.EXTRA_DISPLAY_TITLE),
|
|
70
|
-
displaySubtitle = displayOptions?.getString(CallService.EXTRA_DISPLAY_SUBTITLE),
|
|
71
103
|
displayName = callAttributes.displayName,
|
|
72
104
|
address = callAttributes.address
|
|
73
105
|
)
|
|
74
106
|
|
|
75
|
-
fun
|
|
107
|
+
fun getOrCreateNotificationId(callId: String): Int = synchronized(lock) {
|
|
108
|
+
if (!notificationsState.containsKey(callId)) {
|
|
109
|
+
notificationsState[callId] = CallNotificationState()
|
|
110
|
+
}
|
|
111
|
+
if (foregroundCallId == null) {
|
|
112
|
+
foregroundCallId = callId
|
|
113
|
+
}
|
|
114
|
+
return@synchronized getNotificationId(callId)
|
|
115
|
+
}
|
|
116
|
+
|
|
117
|
+
/**
|
|
118
|
+
* Sets the optimistic state of the call notification.
|
|
119
|
+
* Optimistic state is used to update the notification text while the app is connecting or declining the call.
|
|
120
|
+
* @param state The optimistic state to set.
|
|
121
|
+
*/
|
|
122
|
+
fun setOptimisticState(callId: String, state: OptimisticState) = synchronized(lock) {
|
|
123
|
+
// Be resilient to races where we receive optimistic actions before a notification state entry exists.
|
|
124
|
+
val current =
|
|
125
|
+
notificationsState[callId]
|
|
126
|
+
?: CallNotificationState().also {
|
|
127
|
+
if (foregroundCallId == null) {
|
|
128
|
+
foregroundCallId = callId
|
|
129
|
+
}
|
|
130
|
+
}
|
|
131
|
+
notificationsState[callId] = if (state != OptimisticState.NONE) {
|
|
132
|
+
current.copy(optimisticState = state, lastSnapshot = null)
|
|
133
|
+
} else {
|
|
134
|
+
current.copy(optimisticState = state)
|
|
135
|
+
}
|
|
136
|
+
}
|
|
137
|
+
|
|
138
|
+
/**
|
|
139
|
+
* Creates a notification for the call.
|
|
140
|
+
* Notification is created based on the call state and optimistic state.
|
|
141
|
+
* @param call The call to create a notification for.
|
|
142
|
+
* @return The notification.
|
|
143
|
+
*/
|
|
144
|
+
fun createNotification(callId: String, call: Call.Registered): Notification = synchronized(lock) {
|
|
76
145
|
debugLog(TAG,"[notifications] createNotification: Creating notification for call ID: ${call.id}")
|
|
77
146
|
|
|
147
|
+
val state = notificationsState[callId]
|
|
148
|
+
val optimisticState = state?.optimisticState ?: OptimisticState.NONE
|
|
149
|
+
|
|
78
150
|
val contentIntent =
|
|
79
151
|
NotificationIntentFactory.getLaunchActivityIntent(
|
|
80
152
|
context,
|
|
81
153
|
CallingxModuleImpl.CALL_ANSWERED_ACTION,
|
|
82
154
|
call.id
|
|
83
155
|
)
|
|
84
|
-
val callStyle = createCallStyle(call)
|
|
85
|
-
val channelId = getChannelId(call)
|
|
156
|
+
val callStyle = createCallStyle(call, optimisticState)
|
|
157
|
+
val channelId = getChannelId(call, optimisticState)
|
|
86
158
|
debugLog(TAG, "[notifications] createNotification: Channel ID: $channelId")
|
|
87
159
|
|
|
88
160
|
val builder =
|
|
89
161
|
NotificationCompat.Builder(context, channelId)
|
|
90
162
|
.setContentIntent(contentIntent)
|
|
91
163
|
.setFullScreenIntent(contentIntent, true)
|
|
92
|
-
.setStyle(callStyle)
|
|
93
164
|
.setSmallIcon(R.drawable.ic_round_call_24)
|
|
94
165
|
.setCategory(NotificationCompat.CATEGORY_CALL)
|
|
95
166
|
.setPriority(NotificationCompat.PRIORITY_MAX)
|
|
96
|
-
.setOngoing(
|
|
167
|
+
.setOngoing(optimisticState != OptimisticState.REJECTING)
|
|
97
168
|
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
|
|
169
|
+
builder.setStyle(callStyle)
|
|
170
|
+
|
|
171
|
+
// When call becomes active we need to set the when to current time and show the chronometer
|
|
172
|
+
if (call.isActive && optimisticState == OptimisticState.NONE && state != null) {
|
|
173
|
+
// We need to set the activation time once when call becomes active
|
|
174
|
+
if (!state.hasBecameActive) {
|
|
175
|
+
debugLog(TAG, "[notifications] createNotification: Setting when to current time for $callId")
|
|
176
|
+
val now = System.currentTimeMillis()
|
|
177
|
+
notificationsState[callId] = state.copy(activeWhen = now, hasBecameActive = true)
|
|
178
|
+
}
|
|
179
|
+
builder.setWhen(notificationsState[callId]?.activeWhen ?: System.currentTimeMillis())
|
|
101
180
|
builder.setUsesChronometer(true)
|
|
102
181
|
builder.setShowWhen(true)
|
|
103
|
-
hasBecameActive = true
|
|
104
182
|
}
|
|
105
183
|
|
|
106
|
-
call
|
|
107
|
-
|
|
108
|
-
|
|
184
|
+
// If the call is not active and the optimistic state is not none, we need to set the notification text
|
|
185
|
+
// based on exact action that is being taken (accepting or rejecting)
|
|
186
|
+
if (optimisticState != OptimisticState.NONE && !call.isActive) {
|
|
187
|
+
val text = when (optimisticState) {
|
|
188
|
+
OptimisticState.ACCEPTING -> SettingsStore.getOptimisticAcceptingText(context)
|
|
189
|
+
OptimisticState.REJECTING -> SettingsStore.getOptimisticRejectingText(context)
|
|
190
|
+
else -> null
|
|
109
191
|
}
|
|
192
|
+
if (text != null) builder.setContentText(text)
|
|
110
193
|
}
|
|
111
194
|
|
|
112
195
|
return builder.build()
|
|
113
196
|
}
|
|
114
197
|
|
|
115
|
-
|
|
116
|
-
|
|
117
|
-
|
|
118
|
-
|
|
119
|
-
|
|
120
|
-
|
|
121
|
-
|
|
122
|
-
|
|
123
|
-
|
|
124
|
-
|
|
125
|
-
|
|
126
|
-
|
|
127
|
-
|
|
128
|
-
|
|
198
|
+
/**
|
|
199
|
+
* Updates the call notification.
|
|
200
|
+
* If the call is None or Unregistered, we need to dismiss the notification.
|
|
201
|
+
* If the call is active and the optimistic state is not none, we need to reset the optimistic state.
|
|
202
|
+
* If the call is active and the optimistic state is none, we need to create a new notification.
|
|
203
|
+
* @param call The call to update the notification for.
|
|
204
|
+
*/
|
|
205
|
+
fun updateCallNotification(callId: String, call: Call.Registered) = synchronized(lock) {
|
|
206
|
+
val state = notificationsState[callId]
|
|
207
|
+
val optimisticState = state?.optimisticState ?: OptimisticState.NONE
|
|
208
|
+
if (call.isActive && optimisticState != OptimisticState.NONE && state != null) {
|
|
209
|
+
notificationsState[callId] = state.copy(optimisticState = OptimisticState.NONE)
|
|
210
|
+
debugLog(TAG, "[notifications] updateCallNotification[$callId]: Resetting optimistic state")
|
|
211
|
+
}
|
|
129
212
|
|
|
130
|
-
|
|
131
|
-
|
|
132
|
-
|
|
133
|
-
|
|
213
|
+
// If the new snapshot is the same as the last posted snapshot, we need to skip the update
|
|
214
|
+
val newSnapshot = call.toSnapshot(callId)
|
|
215
|
+
if (newSnapshot == notificationsState[callId]?.lastSnapshot) {
|
|
216
|
+
debugLog(TAG, "[notifications] updateCallNotification[$callId]: Skipping - no state change")
|
|
217
|
+
return@synchronized
|
|
218
|
+
}
|
|
219
|
+
|
|
220
|
+
val notificationId = getOrCreateNotificationId(callId)
|
|
221
|
+
notificationsState[callId] =
|
|
222
|
+
notificationsState[callId]?.copy(lastSnapshot = newSnapshot)
|
|
223
|
+
?: CallNotificationState(lastSnapshot = newSnapshot)
|
|
224
|
+
val notification = createNotification(callId, call)
|
|
225
|
+
notificationManager.notify(notificationId, notification)
|
|
226
|
+
debugLog(TAG, "[notifications] updateCallNotification[$callId]: Notification posted (id=$notificationId)")
|
|
227
|
+
}
|
|
228
|
+
|
|
229
|
+
fun postNotification(callId: String, notification: Notification) = synchronized(lock) {
|
|
230
|
+
val notificationId = getOrCreateNotificationId(callId)
|
|
231
|
+
notificationManager.notify(notificationId, notification)
|
|
232
|
+
}
|
|
233
|
+
|
|
234
|
+
/**
|
|
235
|
+
* Returns a new foreground notification ID if the caller needs to call startForeground()
|
|
236
|
+
* to re-promote the service, or null if no action is needed.
|
|
237
|
+
*/
|
|
238
|
+
fun cancelNotification(callId: String): Int? = synchronized(lock) {
|
|
239
|
+
debugLog(TAG, "[notifications] cancelNotification[$callId]")
|
|
240
|
+
val state = notificationsState.remove(callId)
|
|
241
|
+
val notificationId = getNotificationId(callId)
|
|
242
|
+
notificationManager.cancel(notificationId)
|
|
243
|
+
if (state != null) {
|
|
244
|
+
debugLog(TAG, "[notifications] cancelNotification[$callId]: Cancelled (id=$notificationId)")
|
|
245
|
+
}
|
|
246
|
+
|
|
247
|
+
if (foregroundCallId == callId) {
|
|
248
|
+
foregroundCallId = notificationsState.keys.firstOrNull()
|
|
249
|
+
// Return the new foreground notification ID so the service can re-promote
|
|
250
|
+
if (foregroundCallId != null) {
|
|
251
|
+
return@synchronized getNotificationId(foregroundCallId!!)
|
|
134
252
|
}
|
|
135
253
|
}
|
|
254
|
+
return@synchronized null
|
|
136
255
|
}
|
|
137
256
|
|
|
138
|
-
fun
|
|
139
|
-
|
|
140
|
-
|
|
141
|
-
|
|
257
|
+
fun getForegroundCallId(): String? = synchronized(lock) { foregroundCallId }
|
|
258
|
+
|
|
259
|
+
fun cancelAllNotifications() = synchronized(lock) {
|
|
260
|
+
for (callId in notificationsState.keys) {
|
|
261
|
+
notificationManager.cancel(getNotificationId(callId))
|
|
262
|
+
}
|
|
263
|
+
notificationsState.clear()
|
|
264
|
+
foregroundCallId = null
|
|
142
265
|
}
|
|
143
266
|
|
|
144
267
|
fun startRingtone() {
|
|
@@ -174,18 +297,24 @@ class CallNotificationManager(
|
|
|
174
297
|
ringtone = null
|
|
175
298
|
}
|
|
176
299
|
|
|
177
|
-
|
|
178
|
-
|
|
300
|
+
fun resetOptimisticState(callId: String) = synchronized(lock) {
|
|
301
|
+
debugLog(TAG, "[notifications] resetOptimisticState[$callId]: Resetting optimistic state")
|
|
302
|
+
val current = notificationsState[callId] ?: return@synchronized
|
|
303
|
+
notificationsState[callId] = current.copy(optimisticState = OptimisticState.NONE, lastSnapshot = null)
|
|
304
|
+
}
|
|
305
|
+
|
|
306
|
+
private fun getChannelId(call: Call.Registered, optimisticState: OptimisticState): String {
|
|
307
|
+
return if (call.isIncoming() && !call.isActive && optimisticState == OptimisticState.NONE) {
|
|
179
308
|
notificationsConfig.incomingChannel.id
|
|
180
309
|
} else {
|
|
181
310
|
notificationsConfig.ongoingChannel.id
|
|
182
311
|
}
|
|
183
312
|
}
|
|
184
313
|
|
|
185
|
-
private fun createCallStyle(call: Call.Registered): NotificationCompat.CallStyle {
|
|
314
|
+
private fun createCallStyle(call: Call.Registered, optimisticState: OptimisticState): NotificationCompat.CallStyle? {
|
|
186
315
|
val caller = createPerson(call)
|
|
187
316
|
|
|
188
|
-
if (call.isIncoming() && !call.isActive) {
|
|
317
|
+
if (call.isIncoming() && !call.isActive && optimisticState == OptimisticState.NONE) {
|
|
189
318
|
return NotificationCompat.CallStyle.forIncomingCall(
|
|
190
319
|
caller,
|
|
191
320
|
NotificationIntentFactory.getPendingBroadcastIntent(
|
|
@@ -211,6 +340,17 @@ class CallNotificationManager(
|
|
|
211
340
|
)
|
|
212
341
|
}
|
|
213
342
|
|
|
343
|
+
if (optimisticState == OptimisticState.REJECTING) {
|
|
344
|
+
return NotificationCompat.CallStyle.forOngoingCall(
|
|
345
|
+
caller,
|
|
346
|
+
NotificationIntentFactory.getPendingBroadcastIntent(
|
|
347
|
+
context,
|
|
348
|
+
"io.getstream.CALL_END_NOOP",
|
|
349
|
+
call.id
|
|
350
|
+
) ,
|
|
351
|
+
).setDeclineButtonColorHint(DISABLED_COLOR.toColorInt())
|
|
352
|
+
}
|
|
353
|
+
|
|
214
354
|
return NotificationCompat.CallStyle.forOngoingCall(
|
|
215
355
|
caller,
|
|
216
356
|
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,
|