forked from eatvio/eatvio_app
feat(video_calls): continous ringing on a defferent device fix
This commit is contained in:
@@ -16,7 +16,7 @@ class EatvioFcmMessagingService : FirebaseMessagingService() {
|
||||
Log.i(TAG, "onMessageReceived: ${message.data}")
|
||||
|
||||
if (VideoCallPushBridge.isEndedVideoCall(message.data)) {
|
||||
VideoCallPushBridge.dismissAllIncomingCalls(this)
|
||||
VideoCallPushBridge.dismissEndedVideoCall(this, message.data)
|
||||
return
|
||||
}
|
||||
|
||||
|
||||
@@ -21,7 +21,7 @@ class HmsPushService : HmsMessageService() {
|
||||
if (message == null) return
|
||||
|
||||
if (VideoCallPushBridge.isEndedVideoCall(message.dataOfMap)) {
|
||||
VideoCallPushBridge.dismissAllIncomingCalls(this)
|
||||
VideoCallPushBridge.dismissEndedVideoCall(this, message.dataOfMap)
|
||||
return
|
||||
}
|
||||
|
||||
|
||||
@@ -59,7 +59,16 @@ class MainActivity: FlutterActivity() {
|
||||
when (call.method) {
|
||||
"dismissIncomingCalls" -> {
|
||||
try {
|
||||
VideoCallPushBridge.dismissAllIncomingCalls(applicationContext)
|
||||
val args = call.arguments as? Map<*, *>
|
||||
val callId = args?.get("call_id")?.toString()
|
||||
?: args?.get("callId")?.toString()
|
||||
val conversationUuid = args?.get("conversation_uuid")?.toString()
|
||||
?: args?.get("conversationUuid")?.toString()
|
||||
VideoCallPushBridge.dismissIncomingCalls(
|
||||
applicationContext,
|
||||
callId = callId,
|
||||
conversationUuid = conversationUuid,
|
||||
)
|
||||
result.success(null)
|
||||
} catch (e: Exception) {
|
||||
result.error("DISMISS_INCOMING_CALLS_FAILED", e.message, null)
|
||||
|
||||
@@ -18,6 +18,7 @@ import com.hiennv.flutter_callkit_incoming.FlutterCallkitIncomingPlugin
|
||||
import com.hiennv.flutter_callkit_incoming.addCall
|
||||
import com.hiennv.flutter_callkit_incoming.getDataActiveCalls
|
||||
import com.hiennv.flutter_callkit_incoming.removeAllCalls
|
||||
import com.hiennv.flutter_callkit_incoming.removeCall
|
||||
import java.util.HashMap
|
||||
import java.util.concurrent.atomic.AtomicInteger
|
||||
|
||||
@@ -25,11 +26,13 @@ object VideoCallPushBridge {
|
||||
private const val TAG = "VideoCallPushBridge"
|
||||
private const val ANDROID_ACCENT_COLOR = "#2787F5"
|
||||
private const val MISSED_CALL_CLEANUP_DELAY_MS = 250L
|
||||
private const val TOMBSTONE_TTL_MS = 2 * 60 * 1000L
|
||||
private const val WAKE_LOCK_TAG = "eatvio:incoming_call_ring"
|
||||
|
||||
private val mainHandler = Handler(Looper.getMainLooper())
|
||||
private val playbackLock = Any()
|
||||
private val playbackGeneration = AtomicInteger(0)
|
||||
private val tombstonedCallIds = HashMap<String, Long>()
|
||||
|
||||
private var activeSoundManager: CallkitSoundPlayerManager? = null
|
||||
private var activeNotificationManager: CallkitNotificationManager? = null
|
||||
@@ -118,6 +121,12 @@ object VideoCallPushBridge {
|
||||
storedAppContext = appContext
|
||||
ensureIncomingCallCallbackRegistered()
|
||||
|
||||
val serverCallId = callId(data)
|
||||
if (isTombstoned(serverCallId)) {
|
||||
Log.i(TAG, "showIncomingCall: skip tombstoned call_id=$serverCallId")
|
||||
return
|
||||
}
|
||||
|
||||
val callData = buildCallDataBundle(data)
|
||||
if (callData == null) {
|
||||
Log.w(TAG, "showIncomingCall: missing conversation uuid, data=$data")
|
||||
@@ -126,7 +135,7 @@ object VideoCallPushBridge {
|
||||
|
||||
val callId = callData.getString(CallkitConstants.EXTRA_CALLKIT_ID)
|
||||
val seenGeneration = playbackGeneration.get()
|
||||
Log.i(TAG, "showIncomingCall: conversation=$callId")
|
||||
Log.i(TAG, "showIncomingCall: id=$callId conversation=${conversationUuid(data)}")
|
||||
|
||||
// Ringtone.play() and stop() must run on the same thread. FCM delivers
|
||||
// pushes on a background thread, while Answer runs on the main thread.
|
||||
@@ -180,17 +189,43 @@ object VideoCallPushBridge {
|
||||
}
|
||||
}
|
||||
|
||||
fun dismissAllIncomingCalls(context: Context) {
|
||||
fun dismissIncomingCalls(
|
||||
context: Context,
|
||||
callId: String? = null,
|
||||
conversationUuid: String? = null,
|
||||
) {
|
||||
val normalizedCallId = callId?.trim()?.takeIf { it.isNotEmpty() }
|
||||
val normalizedConversation = conversationUuid?.trim()?.takeIf { it.isNotEmpty() }
|
||||
if (normalizedCallId != null) {
|
||||
markTombstone(normalizedCallId)
|
||||
}
|
||||
|
||||
val appContext = context.applicationContext
|
||||
val calls = getDataActiveCalls(appContext)
|
||||
Log.i(TAG, "dismissAllIncomingCalls: active calls=${calls.size}")
|
||||
Log.i(
|
||||
TAG,
|
||||
"dismissIncomingCalls: active=${calls.size} call_id=${normalizedCallId ?: "-"} " +
|
||||
"conversation=${normalizedConversation ?: "-"}",
|
||||
)
|
||||
playbackGeneration.incrementAndGet()
|
||||
|
||||
val dismiss = Runnable {
|
||||
synchronized(playbackLock) {
|
||||
val notificationManager = resolveNotificationManager(appContext)
|
||||
val bundles = calls.map { it.toBundle() }
|
||||
stopPlaybackLocked(appContext)
|
||||
val allBundles = calls.map { it.toBundle() }
|
||||
val targeted = hasDismissTarget(normalizedCallId, normalizedConversation)
|
||||
val bundles = if (targeted) {
|
||||
allBundles.filter { bundleMatches(it, normalizedCallId, normalizedConversation) }
|
||||
} else {
|
||||
allBundles
|
||||
}
|
||||
|
||||
val activeMatches = activeCallBundle?.let { bundle ->
|
||||
!targeted || bundleMatches(bundle, normalizedCallId, normalizedConversation)
|
||||
} == true
|
||||
if (activeMatches || !targeted) {
|
||||
stopPlaybackLocked(appContext)
|
||||
}
|
||||
|
||||
for (bundle in bundles) {
|
||||
try {
|
||||
@@ -198,25 +233,39 @@ object VideoCallPushBridge {
|
||||
} catch (error: Exception) {
|
||||
Log.w(
|
||||
TAG,
|
||||
"dismissAllIncomingCalls: failed for call ${bundle.getString(CallkitConstants.EXTRA_CALLKIT_ID)}",
|
||||
"dismissIncomingCalls: failed for call ${bundle.getString(CallkitConstants.EXTRA_CALLKIT_ID)}",
|
||||
error,
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
try {
|
||||
CallkitNotificationService.stopService(appContext)
|
||||
} catch (error: Exception) {
|
||||
Log.w(TAG, "dismissAllIncomingCalls: failed to stop callkit service", error)
|
||||
if (!targeted || bundles.isNotEmpty()) {
|
||||
try {
|
||||
CallkitNotificationService.stopService(appContext)
|
||||
} catch (error: Exception) {
|
||||
Log.w(TAG, "dismissIncomingCalls: failed to stop callkit service", error)
|
||||
}
|
||||
}
|
||||
|
||||
try {
|
||||
removeAllCalls(appContext)
|
||||
} catch (error: Exception) {
|
||||
Log.w(TAG, "dismissAllIncomingCalls: failed to remove persisted calls", error)
|
||||
if (!targeted) {
|
||||
try {
|
||||
removeAllCalls(appContext)
|
||||
} catch (error: Exception) {
|
||||
Log.w(TAG, "dismissIncomingCalls: failed to remove persisted calls", error)
|
||||
}
|
||||
} else {
|
||||
for (bundle in bundles) {
|
||||
try {
|
||||
removeCall(appContext, Data.fromBundle(bundle))
|
||||
} catch (error: Exception) {
|
||||
Log.w(TAG, "dismissIncomingCalls: failed to remove targeted call", error)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
stopIncomingRingtone()
|
||||
if (activeMatches || !targeted) {
|
||||
stopIncomingRingtone()
|
||||
}
|
||||
scheduleMissedCallCleanup(notificationManager, bundles)
|
||||
}
|
||||
}
|
||||
@@ -228,6 +277,18 @@ object VideoCallPushBridge {
|
||||
}
|
||||
}
|
||||
|
||||
fun dismissAllIncomingCalls(context: Context) {
|
||||
dismissIncomingCalls(context)
|
||||
}
|
||||
|
||||
fun dismissEndedVideoCall(context: Context, data: Map<String, String>) {
|
||||
dismissIncomingCalls(
|
||||
context = context,
|
||||
callId = callId(data),
|
||||
conversationUuid = conversationUuid(data),
|
||||
)
|
||||
}
|
||||
|
||||
private fun ensureIncomingCallCallbackRegistered() {
|
||||
if (callbackRegistered) return
|
||||
FlutterCallkitIncomingPlugin.registerEventCallback(incomingCallEventCallback)
|
||||
@@ -403,6 +464,8 @@ object VideoCallPushBridge {
|
||||
|
||||
private fun buildCallDataBundle(data: Map<String, String>): Bundle? {
|
||||
val conversationUuid = conversationUuid(data) ?: return null
|
||||
val serverCallId = callId(data)
|
||||
val callKitId = serverCallId ?: conversationUuid
|
||||
val callerName = data["counterpartName"]
|
||||
?: data["counterpart_name"]
|
||||
?: data["caller_name"]
|
||||
@@ -428,6 +491,9 @@ object VideoCallPushBridge {
|
||||
|
||||
val extra = HashMap<String, Any?>()
|
||||
extra["conversationUuid"] = conversationUuid
|
||||
if (!serverCallId.isNullOrBlank()) {
|
||||
extra["call_id"] = serverCallId
|
||||
}
|
||||
extra["counterpartName"] = callerName
|
||||
if (avatarUrl.isNotBlank()) {
|
||||
extra["counterpartAvatarUrl"] = avatarUrl
|
||||
@@ -438,9 +504,13 @@ object VideoCallPushBridge {
|
||||
if (counterpartIsConsultant) {
|
||||
extra["counterpartIsConsultant"] = true
|
||||
}
|
||||
val messageId = data["message_id"] ?: data["messageId"]
|
||||
if (!messageId.isNullOrBlank()) {
|
||||
extra["message_id"] = messageId
|
||||
}
|
||||
|
||||
return Bundle().apply {
|
||||
putString(CallkitConstants.EXTRA_CALLKIT_ID, conversationUuid)
|
||||
putString(CallkitConstants.EXTRA_CALLKIT_ID, callKitId)
|
||||
putString(CallkitConstants.EXTRA_CALLKIT_NAME_CALLER, callerName)
|
||||
putString(CallkitConstants.EXTRA_CALLKIT_APP_NAME, "Итвио")
|
||||
putString(CallkitConstants.EXTRA_CALLKIT_HANDLE, conversationUuid)
|
||||
@@ -465,6 +535,74 @@ object VideoCallPushBridge {
|
||||
}
|
||||
}
|
||||
|
||||
private fun callId(data: Map<String, String>): String? {
|
||||
return data["call_id"]?.trim()?.takeIf { it.isNotEmpty() }
|
||||
?: data["callId"]?.trim()?.takeIf { it.isNotEmpty() }
|
||||
}
|
||||
|
||||
private fun hasDismissTarget(callId: String?, conversationUuid: String?): Boolean {
|
||||
return !callId.isNullOrBlank() || !conversationUuid.isNullOrBlank()
|
||||
}
|
||||
|
||||
private fun bundleMatches(
|
||||
bundle: Bundle,
|
||||
callId: String?,
|
||||
conversationUuid: String?,
|
||||
): Boolean {
|
||||
val normalizedCallId = callId?.trim()?.lowercase()
|
||||
if (!normalizedCallId.isNullOrEmpty()) {
|
||||
val id = bundle.getString(CallkitConstants.EXTRA_CALLKIT_ID)?.trim()?.lowercase()
|
||||
if (id != null && id == normalizedCallId) return true
|
||||
val extraCallId = extraString(bundle, "call_id")
|
||||
?: extraString(bundle, "callId")
|
||||
if (extraCallId != null && extraCallId.lowercase() == normalizedCallId) {
|
||||
return true
|
||||
}
|
||||
return false
|
||||
}
|
||||
|
||||
val normalizedConversation = conversationUuid?.trim()?.lowercase() ?: return false
|
||||
val handle = bundle.getString(CallkitConstants.EXTRA_CALLKIT_HANDLE)?.trim()?.lowercase()
|
||||
if (handle != null && handle == normalizedConversation) return true
|
||||
val extraConversation = extraString(bundle, "conversationUuid")
|
||||
?: extraString(bundle, "conversation_uuid")
|
||||
return extraConversation?.lowercase() == normalizedConversation
|
||||
}
|
||||
|
||||
@Suppress("UNCHECKED_CAST")
|
||||
private fun extraString(bundle: Bundle, key: String): String? {
|
||||
val extra = bundle.getSerializable(CallkitConstants.EXTRA_CALLKIT_EXTRA) as? HashMap<String, Any?>
|
||||
?: return null
|
||||
val value = extra[key] ?: return null
|
||||
val text = value.toString().trim()
|
||||
return text.takeIf { it.isNotEmpty() }
|
||||
}
|
||||
|
||||
private fun markTombstone(callId: String) {
|
||||
val normalized = callId.trim().lowercase()
|
||||
if (normalized.isEmpty()) return
|
||||
pruneTombstones()
|
||||
tombstonedCallIds[normalized] = System.currentTimeMillis() + TOMBSTONE_TTL_MS
|
||||
}
|
||||
|
||||
private fun isTombstoned(callId: String?): Boolean {
|
||||
val normalized = callId?.trim()?.lowercase() ?: return false
|
||||
if (normalized.isEmpty()) return false
|
||||
pruneTombstones()
|
||||
return tombstonedCallIds.containsKey(normalized)
|
||||
}
|
||||
|
||||
private fun pruneTombstones() {
|
||||
val now = System.currentTimeMillis()
|
||||
val iterator = tombstonedCallIds.entries.iterator()
|
||||
while (iterator.hasNext()) {
|
||||
val entry = iterator.next()
|
||||
if (entry.value <= now) {
|
||||
iterator.remove()
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private fun conversationUuid(data: Map<String, String>): String? {
|
||||
val fromData = data["conversationUuid"]
|
||||
?: data["conversation_uuid"]
|
||||
|
||||
Reference in New Issue
Block a user