From 9ce7b035ff1ec839448ea9f200b2f85cf3d90c3f Mon Sep 17 00:00:00 2001 From: misha Date: Tue, 30 Jun 2026 18:48:45 +0300 Subject: [PATCH] feat(video_calls): fixed call ended push handling --- .../kotlin/com/eatvioapp/app/MainActivity.kt | 16 +++ .../com/eatvioapp/app/VideoCallPushBridge.kt | 113 ++++++++++++++++-- .../reports/problems/problems-report.html | 2 +- .../video_call_ended_push_handler.dart | 18 +++ pubspec.yaml | 2 +- 5 files changed, 138 insertions(+), 13 deletions(-) diff --git a/android/app/src/main/kotlin/com/eatvioapp/app/MainActivity.kt b/android/app/src/main/kotlin/com/eatvioapp/app/MainActivity.kt index 65903d96..c6b2fb43 100644 --- a/android/app/src/main/kotlin/com/eatvioapp/app/MainActivity.kt +++ b/android/app/src/main/kotlin/com/eatvioapp/app/MainActivity.kt @@ -25,8 +25,10 @@ class MainActivity: FlutterActivity() { private val channelName = "com.eatvioapp/hms_push" private val videoCallPipChannelName = "com.eatvioapp/video_call_pip" + private val videoCallPushChannelName = "com.eatvioapp/video_call_push" private var methodChannel: MethodChannel? = null private var videoCallPipChannel: MethodChannel? = null + private var videoCallPushChannel: MethodChannel? = null private var videoCallPipEnabled: Boolean = false override fun onResume() { @@ -44,6 +46,20 @@ class MainActivity: FlutterActivity() { methodChannel = MethodChannel(flutterEngine.dartExecutor.binaryMessenger, channelName) videoCallPipChannel = MethodChannel(flutterEngine.dartExecutor.binaryMessenger, videoCallPipChannelName) + videoCallPushChannel = MethodChannel(flutterEngine.dartExecutor.binaryMessenger, videoCallPushChannelName) + videoCallPushChannel?.setMethodCallHandler { call, result -> + when (call.method) { + "dismissIncomingCalls" -> { + try { + VideoCallPushBridge.dismissAllIncomingCalls(applicationContext) + result.success(null) + } catch (e: Exception) { + result.error("DISMISS_INCOMING_CALLS_FAILED", e.message, null) + } + } + else -> result.notImplemented() + } + } videoCallPipChannel?.setMethodCallHandler { call, result -> when (call.method) { "setPipEnabled" -> { diff --git a/android/app/src/main/kotlin/com/eatvioapp/app/VideoCallPushBridge.kt b/android/app/src/main/kotlin/com/eatvioapp/app/VideoCallPushBridge.kt index a52976e9..fbad936e 100644 --- a/android/app/src/main/kotlin/com/eatvioapp/app/VideoCallPushBridge.kt +++ b/android/app/src/main/kotlin/com/eatvioapp/app/VideoCallPushBridge.kt @@ -2,12 +2,17 @@ package com.eatvioapp.app import android.content.Context import android.os.Bundle +import android.os.Handler +import android.os.Looper import android.util.Log +import androidx.core.app.NotificationCompat +import androidx.core.app.NotificationManagerCompat import com.hiennv.flutter_callkit_incoming.CallkitConstants -import com.hiennv.flutter_callkit_incoming.CallkitIncomingBroadcastReceiver import com.hiennv.flutter_callkit_incoming.CallkitNotificationManager +import com.hiennv.flutter_callkit_incoming.CallkitNotificationService import com.hiennv.flutter_callkit_incoming.CallkitSoundPlayerManager import com.hiennv.flutter_callkit_incoming.Data +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 @@ -16,6 +21,10 @@ import java.util.HashMap object VideoCallPushBridge { private const val TAG = "VideoCallPushBridge" private const val ANDROID_ACCENT_COLOR = "#2787F5" + private const val MISSED_CALL_CLEANUP_DELAY_MS = 250L + + private var activeSoundManager: CallkitSoundPlayerManager? = null + private var activeNotificationManager: CallkitNotificationManager? = null fun isIncomingVideoCall(data: Map): Boolean { val rawType = data["type"] @@ -90,6 +99,8 @@ object VideoCallPushBridge { val soundManager = CallkitSoundPlayerManager(appContext) val notificationManager = CallkitNotificationManager(appContext, soundManager) + activeSoundManager = soundManager + activeNotificationManager = notificationManager notificationManager.createNotificationChanel(callData) notificationManager.showIncomingNotification(callData) @@ -105,28 +116,108 @@ object VideoCallPushBridge { val calls = getDataActiveCalls(appContext) Log.i(TAG, "dismissAllIncomingCalls: active calls=${calls.size}") - val soundManager = CallkitSoundPlayerManager(appContext) - val notificationManager = CallkitNotificationManager(appContext, soundManager) + stopIncomingRingtone(appContext) - for (call in calls) { - val bundle = call.toBundle() + val notificationManager = resolveNotificationManager(appContext) + val bundles = calls.map { it.toBundle() } + + for (bundle in bundles) { try { - notificationManager.clearIncomingNotification(bundle, isAccepted = false) - appContext.sendBroadcast( - CallkitIncomingBroadcastReceiver.getIntentEnded(appContext, bundle), - ) + dismissIncomingCallNotification(appContext, notificationManager, bundle) } catch (error: Exception) { - Log.w(TAG, "dismissAllIncomingCalls: failed for call ${call.id}", error) + Log.w( + TAG, + "dismissAllIncomingCalls: 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) + } + try { removeAllCalls(appContext) } catch (error: Exception) { Log.w(TAG, "dismissAllIncomingCalls: failed to remove persisted calls", error) } - soundManager.stop() + stopIncomingRingtone(appContext) + scheduleMissedCallCleanup(notificationManager, bundles) + activeSoundManager = null + activeNotificationManager = null + } + + private fun resolveNotificationManager(context: Context): CallkitNotificationManager { + activeNotificationManager?.let { return it } + FlutterCallkitIncomingPlugin.getInstance() + ?.getCallkitNotificationManager() + ?.let { return it } + + val soundManager = CallkitSoundPlayerManager(context) + return CallkitNotificationManager(context, soundManager) + } + + private fun stopIncomingRingtone(context: Context) { + activeSoundManager?.stop() + FlutterCallkitIncomingPlugin.getInstance() + ?.getCallkitSoundPlayerManager() + ?.stop() + } + + /** + * Replaces the incoming notification before cancel so Android does not fire the + * timeout delete intent (which would show a missed-call notification). + */ + private fun dismissIncomingCallNotification( + context: Context, + notificationManager: CallkitNotificationManager, + bundle: Bundle, + ) { + val notificationId = incomingNotificationId(bundle) + + val silentNotification = NotificationCompat.Builder( + context, + CallkitNotificationManager.NOTIFICATION_CHANNEL_ID_INCOMING, + ) + .setSmallIcon(context.applicationInfo.icon) + .setContentTitle("") + .setSilent(true) + .setOngoing(false) + .setPriority(NotificationCompat.PRIORITY_MIN) + .build() + + val notificationManagerCompat = NotificationManagerCompat.from(context) + notificationManagerCompat.notify(notificationId, silentNotification) + notificationManagerCompat.cancel(notificationId) + + bundle.putBoolean(CallkitConstants.EXTRA_CALLKIT_MISSED_CALL_SHOW, false) + notificationManager.clearIncomingNotification(bundle, isAccepted = true) + notificationManager.clearMissCallNotification(bundle) + } + + private fun scheduleMissedCallCleanup( + notificationManager: CallkitNotificationManager, + bundles: List, + ) { + if (bundles.isEmpty()) return + + Handler(Looper.getMainLooper()).postDelayed({ + for (bundle in bundles) { + try { + notificationManager.clearMissCallNotification(bundle) + } catch (error: Exception) { + Log.w(TAG, "dismissAllIncomingCalls: delayed missed-call cleanup failed", error) + } + } + }, MISSED_CALL_CLEANUP_DELAY_MS) + } + + private fun incomingNotificationId(bundle: Bundle): Int { + return bundle.getString(CallkitConstants.EXTRA_CALLKIT_ID, "callkit_incoming").hashCode() } private fun buildCallDataBundle(data: Map): Bundle? { diff --git a/android/build/reports/problems/problems-report.html b/android/build/reports/problems/problems-report.html index 379f47c9..fa7c1a97 100644 --- a/android/build/reports/problems/problems-report.html +++ b/android/build/reports/problems/problems-report.html @@ -650,7 +650,7 @@ code + .copy-button { diff --git a/lib/features/video_calls/services/video_call_ended_push_handler.dart b/lib/features/video_calls/services/video_call_ended_push_handler.dart index 1b27f32f..5ff7ec6e 100644 --- a/lib/features/video_calls/services/video_call_ended_push_handler.dart +++ b/lib/features/video_calls/services/video_call_ended_push_handler.dart @@ -1,8 +1,26 @@ +import 'dart:io'; + +import 'package:flutter/services.dart'; import 'package:flutter_callkit_incoming/flutter_callkit_incoming.dart'; /// Dismisses incoming call UI after a remote `video_call_ended` push. abstract final class VideoCallEndedPushHandler { + static const MethodChannel _androidPushChannel = MethodChannel( + 'com.eatvioapp/video_call_push', + ); + static Future dismissIncomingCallUi() async { + if (Platform.isAndroid) { + try { + await _androidPushChannel.invokeMethod('dismissIncomingCalls'); + return; + } catch (_) {} + } + + await _dismissViaCallKit(); + } + + static Future _dismissViaCallKit() async { try { final activeCalls = await FlutterCallkitIncoming.activeCalls(); for (final call in activeCalls) { diff --git a/pubspec.yaml b/pubspec.yaml index ee16681e..94fc5131 100644 --- a/pubspec.yaml +++ b/pubspec.yaml @@ -1,6 +1,6 @@ name: eatvio_app description: A new Flutter project. -version: 1.3.32+121 +version: 1.3.33+122 publish_to: 'none' environment: