main_production_audio_video_call
Sikander Saleem 3 weeks ago
parent f2afab96fc
commit b26538b3d6

@ -1,4 +1,5 @@
<manifest xmlns:android="http://schemas.android.com/apk/res/android"> <manifest xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:tools="http://schemas.android.com/tools">
<uses-permission android:name="android.permission.INTERNET" /> <uses-permission android:name="android.permission.INTERNET" />
<uses-permission android:name="android.permission.ACCESS_NOTIFICATION_POLICY" /> <uses-permission android:name="android.permission.ACCESS_NOTIFICATION_POLICY" />
@ -43,6 +44,7 @@
<data android:scheme="tel" /> <data android:scheme="tel" />
</intent> </intent>
</queries> </queries>
<application <application
android:name="${applicationName}" android:name="${applicationName}"
android:icon="@mipmap/ic_launcher" android:icon="@mipmap/ic_launcher"
@ -51,6 +53,17 @@
android:showWhenLocked="true" android:showWhenLocked="true"
android:turnScreenOn="true" android:turnScreenOn="true"
android:usesCleartextTraffic="true"> android:usesCleartextTraffic="true">
<service
android:name="com.hiennv.flutter_callkit_incoming.CallkitNotificationService"
android:exported="false"
android:foregroundServiceType="phoneCall"
tools:replace="android:exported"/>
<service
android:name="com.hiennv.flutter_callkit_incoming.CallkitSoundPlayerService"
android:exported="false"
android:foregroundServiceType="phoneCall"
tools:replace="android:exported"/>
<!-- 16KB page size support - Required for modern Android devices --> <!-- 16KB page size support - Required for modern Android devices -->
<property <property

@ -6,6 +6,7 @@ import 'dart:io';
import 'package:firebase_messaging/firebase_messaging.dart'; import 'package:firebase_messaging/firebase_messaging.dart';
import 'package:flutter/material.dart'; import 'package:flutter/material.dart';
import 'package:flutter_callkit_incoming/entities/android_params.dart'; import 'package:flutter_callkit_incoming/entities/android_params.dart';
import 'package:flutter_callkit_incoming/entities/call_event.dart';
import 'package:flutter_callkit_incoming/entities/call_kit_params.dart'; import 'package:flutter_callkit_incoming/entities/call_kit_params.dart';
import 'package:flutter_callkit_incoming/entities/ios_params.dart'; import 'package:flutter_callkit_incoming/entities/ios_params.dart';
import 'package:flutter_callkit_incoming/entities/notification_params.dart'; import 'package:flutter_callkit_incoming/entities/notification_params.dart';
@ -13,6 +14,7 @@ import 'package:flutter_callkit_incoming/flutter_callkit_incoming.dart';
import 'package:google_api_availability/google_api_availability.dart'; import 'package:google_api_availability/google_api_availability.dart';
import 'package:huawei_push/huawei_push.dart' as h_push; import 'package:huawei_push/huawei_push.dart' as h_push;
import 'package:test_sa/controllers/notification/notification_manger.dart'; import 'package:test_sa/controllers/notification/notification_manger.dart';
import 'package:test_sa/extensions/string_extensions.dart';
import 'package:test_sa/models/device/device_transfer.dart'; import 'package:test_sa/models/device/device_transfer.dart';
import 'package:test_sa/models/new_models/gas_refill_model.dart'; import 'package:test_sa/models/new_models/gas_refill_model.dart';
import 'package:test_sa/modules/cm_module/cm_detail_page.dart'; import 'package:test_sa/modules/cm_module/cm_detail_page.dart';
@ -28,7 +30,23 @@ import 'package:test_sa/views/widgets/loaders/no_data_found.dart';
@pragma('vm:entry-point') @pragma('vm:entry-point')
Future<void> firebaseMessagingBackgroundHandler(RemoteMessage message) async { Future<void> firebaseMessagingBackgroundHandler(RemoteMessage message) async {
if (message.notification == null && message.data != null) { if (message.notification == null && message.data != null) {
handleDataMessage(message.data); handleDataMessage(message.data, true);
}
}
@pragma('vm:entry-point')
Future<void> onBackgroundMessage(CallEvent event) async {
// This executes when a background/killed action happens
switch (event.eventName) {
case CallEventConstants.actionCallAccept:
"actionCallAccept".showToast;
break;
case CallEventConstants.actionCallDecline:
"actionCallDecline".showToast;
break;
default:
break;
} }
} }
@ -268,7 +286,7 @@ class FirebaseNotificationManger {
} }
} }
if (message.notification == null && message.data != null) { if (message.notification == null && message.data != null) {
handleDataMessage(message.data); handleDataMessage(message.data, false);
} }
return; return;
}); });
@ -279,7 +297,7 @@ class FirebaseNotificationManger {
} }
} }
Future<void> handleDataMessage(Map<String, dynamic> messageData) async { Future<void> handleDataMessage(Map<String, dynamic> messageData, bool isFromBackground) async {
print("handleDataMessage:${messageData}"); print("handleDataMessage:${messageData}");
// if (messageData["data"] != null) { // if (messageData["data"] != null) {
// messageData = messageData["data"]; // messageData = messageData["data"];
@ -310,8 +328,7 @@ Future<void> handleDataMessage(Map<String, dynamic> messageData) async {
final referenceId = messageData['referenceId'] as String?; final referenceId = messageData['referenceId'] as String?;
final conversationId = messageData['conversationId'] as String?; final conversationId = messageData['conversationId'] as String?;
log('📱 [FOREGROUND] Showing CallKit UI...', name: 'FirebaseNotificationManger'); WidgetsFlutterBinding.ensureInitialized();
Timer? _callTimeoutTimer; Timer? _callTimeoutTimer;
try { try {
final callKitParams = CallKitParams( final callKitParams = CallKitParams(
@ -324,10 +341,16 @@ Future<void> handleDataMessage(Map<String, dynamic> messageData) async {
duration: 30000, duration: 30000,
missedCallNotification: const NotificationParams( missedCallNotification: const NotificationParams(
showNotification: true, showNotification: true,
isShowCallback: true, isShowCallback: false,
subtitle: 'Missed call', subtitle: 'Missed call',
callbackText: 'Call back', callbackText: 'Call back',
), ),
callingNotification: const NotificationParams(
showNotification: true,
isShowCallback: true,
subtitle: 'Calling...',
callbackText: 'Hang Up',
),
extra: { extra: {
'callId': callId, 'callId': callId,
'callerId': callerId, 'callerId': callerId,
@ -341,7 +364,7 @@ Future<void> handleDataMessage(Map<String, dynamic> messageData) async {
'timestamp': DateTime.now().toIso8601String(), 'timestamp': DateTime.now().toIso8601String(),
}, },
android: const AndroidParams( android: const AndroidParams(
isCustomNotification: true, // isCustomNotification: true,
isShowLogo: false, isShowLogo: false,
ringtonePath: 'system_ringtone_default', ringtonePath: 'system_ringtone_default',
backgroundColor: '#0955fa', backgroundColor: '#0955fa',
@ -368,11 +391,11 @@ Future<void> handleDataMessage(Map<String, dynamic> messageData) async {
ringtonePath: 'system_ringtone_default', ringtonePath: 'system_ringtone_default',
), ),
); );
// await FlutterCallkitIncoming.requestNotificationPermission({ await FlutterCallkitIncoming.requestNotificationPermission({
// "title": "Notification permission", // // "title": "Notification permission",
// "rationaleMessagePermission": "Notification permission is required, to show notification.", // // "rationaleMessagePermission": "Notification permission is required, to show notification.",
// "postNotificationMessageRequired": "Notification permission is required, Please allow notification permission from setting." // // "postNotificationMessageRequired": "Notification permission is required, Please allow notification permission from setting."
// }); });
await FlutterCallkitIncoming.canUseFullScreenIntent(); await FlutterCallkitIncoming.canUseFullScreenIntent();
await FlutterCallkitIncoming.requestFullIntentPermission(); await FlutterCallkitIncoming.requestFullIntentPermission();
@ -382,6 +405,8 @@ Future<void> handleDataMessage(Map<String, dynamic> messageData) async {
_callTimeoutTimer = Timer(const Duration(seconds: 30), () async { _callTimeoutTimer = Timer(const Duration(seconds: 30), () async {
await FlutterCallkitIncoming.endCall(callId); await FlutterCallkitIncoming.endCall(callId);
await FlutterCallkitIncoming.endAllCalls(); await FlutterCallkitIncoming.endAllCalls();
await FlutterCallkitIncoming.showMissCallNotification(callKitParams);
_callTimeoutTimer?.cancel(); _callTimeoutTimer?.cancel();
_callTimeoutTimer = null; _callTimeoutTimer = null;
}); });

@ -4,6 +4,7 @@ import 'package:firebase_core/firebase_core.dart';
import 'package:flutter/foundation.dart'; import 'package:flutter/foundation.dart';
import 'package:flutter/material.dart'; import 'package:flutter/material.dart';
import 'package:flutter/services.dart'; import 'package:flutter/services.dart';
import 'package:flutter_callkit_incoming/flutter_callkit_incoming.dart';
import 'package:localization/localization.dart'; import 'package:localization/localization.dart';
import 'package:flutter_timezone/flutter_timezone.dart'; import 'package:flutter_timezone/flutter_timezone.dart';
import 'package:provider/provider.dart'; import 'package:provider/provider.dart';
@ -119,6 +120,7 @@ import 'package:test_sa/views/widgets/equipment/my_assets_page.dart';
import 'package:timezone/data/latest_all.dart' as tz; import 'package:timezone/data/latest_all.dart' as tz;
import 'package:timezone/timezone.dart' as tz; import 'package:timezone/timezone.dart' as tz;
import 'controllers/notification/firebase_notification_manger.dart';
import 'controllers/notification/notification_manger.dart'; import 'controllers/notification/notification_manger.dart';
import 'controllers/providers/api/gas_refill_comments.dart'; import 'controllers/providers/api/gas_refill_comments.dart';
import 'controllers/providers/api/user_provider.dart'; import 'controllers/providers/api/user_provider.dart';
@ -156,6 +158,8 @@ import 'modules/cleaner_module/provider/cleaner_provider.dart';
void main() async { void main() async {
WidgetsFlutterBinding.ensureInitialized(); WidgetsFlutterBinding.ensureInitialized();
FlutterCallkitIncoming.onBackgroundMessage(onBackgroundMessage);
FlutterCallkitIncoming.endAllCalls();
SystemChrome.setEnabledSystemUIMode(SystemUiMode.edgeToEdge); SystemChrome.setEnabledSystemUIMode(SystemUiMode.edgeToEdge);
// HttpOverrides.global = MyHttpOverrides(); // for later use. // HttpOverrides.global = MyHttpOverrides(); // for later use.
_configureLocalTimeZone(); _configureLocalTimeZone();

@ -1,5 +1,6 @@
import 'package:flutter/material.dart'; import 'package:flutter/material.dart';
import 'package:provider/provider.dart'; import 'package:provider/provider.dart';
import 'package:test_sa/extensions/context_extension.dart';
import 'package:test_sa/extensions/int_extensions.dart'; import 'package:test_sa/extensions/int_extensions.dart';
import 'package:test_sa/extensions/text_extensions.dart'; import 'package:test_sa/extensions/text_extensions.dart';
import 'package:test_sa/extensions/widget_extensions.dart'; import 'package:test_sa/extensions/widget_extensions.dart';
@ -25,6 +26,7 @@ class AudioCallPage extends StatefulWidget {
class _AudioCallPageState extends State<AudioCallPage> { class _AudioCallPageState extends State<AudioCallPage> {
late CallProvider callPro; late CallProvider callPro;
late ChatProvider chatProvider; late ChatProvider chatProvider;
// Participants? participants; // Participants? participants;
String currentStatus = "Connecting..."; String currentStatus = "Connecting...";
@ -39,6 +41,14 @@ class _AudioCallPageState extends State<AudioCallPage> {
} }
void makeCall() async { void makeCall() async {
bool isConnected = await callPro.buildHubConnection(widget.sender.employeeNumber!);
if (!isConnected) {
context.showConfirmDialog("Error while connecting to call service. Please try again later.", title: "Connection Error", onTap: () {
Navigator.pop(context);
Navigator.pop(context);
});
return;
}
callPro.initAudioCallListeners(); callPro.initAudioCallListeners();
callPro.startCall(widget.sender.employeeNumber ?? "", widget.recipient?.employeeNumber ?? "", false, chatProvider.moduleID, chatProvider.referenceID); callPro.startCall(widget.sender.employeeNumber ?? "", widget.recipient?.employeeNumber ?? "", false, chatProvider.moduleID, chatProvider.referenceID);

@ -8,8 +8,13 @@ import 'package:signalr_netcore/http_connection_options.dart';
import 'package:signalr_netcore/hub_connection.dart'; import 'package:signalr_netcore/hub_connection.dart';
import 'package:signalr_netcore/hub_connection_builder.dart'; import 'package:signalr_netcore/hub_connection_builder.dart';
import 'package:test_sa/controllers/api_routes/urls.dart'; import 'package:test_sa/controllers/api_routes/urls.dart';
import 'package:test_sa/modules/cx_module/chat/api_client.dart';
import 'package:http/http.dart' as http;
import '../chat_provider.dart'; import '../chat_provider.dart';
import '../model/chat_login_response_model.dart';
HubConnection? callHubConnection;
class CallProvider with ChangeNotifier, DiagnosticableTreeMixin { class CallProvider with ChangeNotifier, DiagnosticableTreeMixin {
///////////////////// Web RTC Video Calling ////////////////////// ///////////////////// Web RTC Video Calling //////////////////////
@ -27,112 +32,93 @@ class CallProvider with ChangeNotifier, DiagnosticableTreeMixin {
bool isPeerMuted = false; bool isPeerMuted = false;
bool isPeerCameraOn = true; bool isPeerCameraOn = true;
bool isLoading = false;
Future<void> _disposeConnection() async { Future<void> _disposeConnection() async {
try { try {
if (chatHubConnection != null) { if (callHubConnection != null) {
await chatHubConnection!.stop(); await callHubConnection!.stop();
if (kDebugMode) {
print('🔌 SignalR connection closed successfully');
}
} }
} catch (e) { } catch (e) {
if (kDebugMode) {
print('⚠️ Error closing SignalR connection: $e');
}
} finally { } finally {
chatHubConnection = null; callHubConnection = null;
} }
// isLoading = false;
// notifyListeners();
} }
@override @override
void dispose() { void dispose() {
_disposeConnection().then((_) { _disposeConnection().then((_) {}).catchError((error) {});
if (kDebugMode) {
print('✅ ChatProvider disposed');
}
}).catchError((error) {
if (kDebugMode) {
print('⚠️ Error during ChatProvider disposal: $error');
}
});
super.dispose(); super.dispose();
} }
Future<HubConnection> getHubConnection() async { Future<HubConnection> getHubConnection(String token) async {
if (kDebugMode) {
print('🔧 Creating new SignalR hub connection...');
}
HubConnection hub; HubConnection hub;
HttpConnectionOptions httpOp = HttpConnectionOptions(skipNegotiation: false, logMessageContent: true); HttpConnectionOptions httpOp = HttpConnectionOptions(skipNegotiation: false, logMessageContent: true);
hub = HubConnectionBuilder().withUrl("${URLs.chatHubUrlChat}?accessTokenFactory=${URLs.chatApiKey}", options: httpOp).withAutomaticReconnect(retryDelays: <int>[2000, 5000, 10000, 20000]).build(); hub = HubConnectionBuilder().withUrl("${URLs.chatHubUrlChat}?accessTokenFactory=$token}", options: httpOp).withAutomaticReconnect(retryDelays: <int>[2000, 5000, 10000, 20000]).build();
if (kDebugMode) {
print('✅ SignalR hub connection created');
}
return hub; return hub;
} }
Future<bool> buildHubConnection() async { Future<bool> buildHubConnection(String employeeNumber) async {
try { try {
// Dispose existing connection if any
await _disposeConnection(); await _disposeConnection();
// isLoading = true;
chatHubConnection = await getHubConnection(); // notifyListeners();
await chatHubConnection!.start(); http.Response response = await ApiClient().postJsonForResponse(URLs.chatSdkToken, {"apiKey": URLs.chatApiKey, "employeeNumber": employeeNumber, "voIPToken": null});
if (kDebugMode) { if (response.statusCode == 200) {
print("🔌 SignalR Hub Connection: Started"); ChatLoginResponse chatResponse = ChatLoginResponse.fromJson(jsonDecode(response.body));
print("chatResponse:${chatResponse.token}");
callHubConnection = await getHubConnection(chatResponse.token!);
await callHubConnection!.start();
// isLoading = false;
// notifyListeners();
return true;
} }
return true; // isLoading = false;
// notifyListeners();
//group On message return false;
// chatHubConnection.on("OnDeliveredGroupChatHistoryAsync", onGroupMsgReceived);
} catch (e) { } catch (e) {
if (kDebugMode) { print("buildHubConnectionE:$e");
print('⚠️ Error building SignalR connection: $e');
}
// Clean up on error
await _disposeConnection(); await _disposeConnection();
return false; return false;
rethrow; // Rethrow so caller knows about the error
} }
} }
void initAudioCallListeners() { void initAudioCallListeners() {
chatHubConnection!.on("OnCallAcceptedAsync", onCallAcceptedAsync); callHubConnection!.on("OnCallAcceptedAsync", onCallAcceptedAsync);
chatHubConnection!.on("OnIceCandidateAsync", onIceCandidateAsync); callHubConnection!.on("OnIceCandidateAsync", onIceCandidateAsync);
chatHubConnection!.on("OnOfferAsync", onOfferAsync); callHubConnection!.on("OnOfferAsync", onOfferAsync);
chatHubConnection!.on("OnAnswerOffer", onAnswerOffer); callHubConnection!.on("OnAnswerOffer", onAnswerOffer);
chatHubConnection!.on("OnHangUpAsync", onHangUpAsync); callHubConnection!.on("OnHangUpAsync", onHangUpAsync);
chatHubConnection!.on("OnCallDeclinedAsync", onCallDeclinedAsync); callHubConnection!.on("OnCallDeclinedAsync", onCallDeclinedAsync);
chatHubConnection!.on('OnAudioToggle', onAudioToggle); callHubConnection!.on('OnAudioToggle', onAudioToggle);
} }
void initCallListeners() { void initCallListeners() {
chatHubConnection!.on("OnCallAcceptedAsync", onCallAcceptedAsync); callHubConnection!.on("OnCallAcceptedAsync", onCallAcceptedAsync);
chatHubConnection!.on("OnIceCandidateAsync", onIceCandidateAsync); callHubConnection!.on("OnIceCandidateAsync", onIceCandidateAsync);
chatHubConnection!.on("OnOfferAsync", onOfferAsync); callHubConnection!.on("OnOfferAsync", onOfferAsync);
chatHubConnection!.on("OnAnswerOffer", onAnswerOffer); callHubConnection!.on("OnAnswerOffer", onAnswerOffer);
chatHubConnection!.on("OnHangUpAsync", onHangUpAsync); callHubConnection!.on("OnHangUpAsync", onHangUpAsync);
chatHubConnection!.on("OnCallDeclinedAsync", onCallDeclinedAsync); callHubConnection!.on("OnCallDeclinedAsync", onCallDeclinedAsync);
chatHubConnection!.on('OnAudioToggle', onAudioToggle); callHubConnection!.on('OnAudioToggle', onAudioToggle);
chatHubConnection!.on('OnCameraToggle', onCameraToggle); callHubConnection!.on('OnCameraToggle', onCameraToggle);
// chatHubConnection!.on('OnIncomingCallAsync', onHandleIncomingCall); // callHubConnection!.on('OnIncomingCallAsync', onHandleIncomingCall);
// chatHubConnection!.on('OnCallAcceptedAsync', _handleCallAccepted); // callHubConnection!.on('OnCallAcceptedAsync', _handleCallAccepted);
// chatHubConnection!.on('OnCallDeclinedAsync', _handleCallDeclined); // callHubConnection!.on('OnCallDeclinedAsync', _handleCallDeclined);
// chatHubConnection!.on('OnHangUpAsync', onHangUpAsync); // callHubConnection!.on('OnHangUpAsync', onHangUpAsync);
// chatHubConnection!.on('OnOfferAsync', _handleOffer); // callHubConnection!.on('OnOfferAsync', _handleOffer);
// chatHubConnection!.on('OnAnswerOfferAsync', onAnswerOffer); // callHubConnection!.on('OnAnswerOfferAsync', onAnswerOffer);
// chatHubConnection!.on('OnIceCandidateAsync', _handleIceCandidate); // callHubConnection!.on('OnIceCandidateAsync', _handleIceCandidate);
// chatHubConnection!.on('OnAudioToggle', _handleAudioToggle); // callHubConnection!.on('OnAudioToggle', _handleAudioToggle);
// chatHubConnection!.on('OnCameraToggle', _handleCameraToggle); // callHubConnection!.on('OnCameraToggle', _handleCameraToggle);
// chatHubConnection!.on('OnUserOnlineAsync', _handleUserOnline); // callHubConnection!.on('OnUserOnlineAsync', _handleUserOnline);
// chatHubConnection!.on('OnUserOfflineAsync', _handleUserOffline); // callHubConnection!.on('OnUserOfflineAsync', _handleUserOffline);
// chatHubConnection!.on('OnCallHistoryUpdated', _handleCallHistoryUpdated); // callHubConnection!.on('OnCallHistoryUpdated', _handleCallHistoryUpdated);
// chatHubConnection!.on('OnError', _handleError); // callHubConnection!.on('OnError', _handleError);
} }
//Video Constraints //Video Constraints
@ -337,16 +323,16 @@ class CallProvider with ChangeNotifier, DiagnosticableTreeMixin {
} else if (invokeMethod == "AnswerOfferAsync") { } else if (invokeMethod == "AnswerOfferAsync") {
args = [targetUserID, data]; args = [targetUserID, data];
} }
await chatHubConnection!.invoke(invokeMethod, args: args); await callHubConnection?.invoke(invokeMethod, args: args);
} }
void stopListeners() async { void stopListeners() async {
chatHubConnection!.off('OnCallDeclinedAsync'); callHubConnection?.off('OnCallDeclinedAsync');
chatHubConnection!.off('OnCallAcceptedAsync'); callHubConnection?.off('OnCallAcceptedAsync');
chatHubConnection!.off('OnIceCandidateAsync'); callHubConnection?.off('OnIceCandidateAsync');
chatHubConnection!.off('OnAnswerOffer'); callHubConnection?.off('OnAnswerOffer');
chatHubConnection!.off('OnOfferAsync'); callHubConnection?.off('OnOfferAsync');
chatHubConnection!.off('OnHangUpAsync'); callHubConnection?.off('OnHangUpAsync');
chatHubConnection!.off('OnAudioToggle'); callHubConnection?.off('OnAudioToggle');
} }
} }

@ -227,9 +227,10 @@ class _LandPageState extends State<LandPage> {
break; break;
case CallEventConstants.actionCallAccept: case CallEventConstants.actionCallAccept:
var activeCalls = await FlutterCallkitIncoming.activeCalls(); var activeCalls = await FlutterCallkitIncoming.activeCalls();
if (activeCalls is List && activeCalls.isNotEmpty) { if (activeCalls is List && activeCalls.isNotEmpty) {
final lastCall = activeCalls.last; final lastCall = activeCalls.last;
await FlutterCallkitIncoming.endCall(lastCall.extra?["callId"]);
FlutterCallkitIncoming.endAllCalls();
Navigator.of(context).push(MaterialPageRoute( Navigator.of(context).push(MaterialPageRoute(
builder: (_) => builder: (_) =>
AudioCallPage(sender: Participants(employeeNumber: lastCall.extra?["calleeEmployeeNumber"]), recipient: Participants(employeeNumber: lastCall.extra?["callerEmployeeNumber"])))); AudioCallPage(sender: Participants(employeeNumber: lastCall.extra?["calleeEmployeeNumber"]), recipient: Participants(employeeNumber: lastCall.extra?["callerEmployeeNumber"]))));

Loading…
Cancel
Save