|
|
|
|
@ -2,6 +2,7 @@ import 'dart:async';
|
|
|
|
|
import 'dart:convert';
|
|
|
|
|
import 'dart:developer';
|
|
|
|
|
import 'dart:io';
|
|
|
|
|
import 'dart:math';
|
|
|
|
|
|
|
|
|
|
import 'package:firebase_messaging/firebase_messaging.dart';
|
|
|
|
|
import 'package:flutter/material.dart';
|
|
|
|
|
@ -14,7 +15,6 @@ import 'package:flutter_callkit_incoming/flutter_callkit_incoming.dart';
|
|
|
|
|
import 'package:google_api_availability/google_api_availability.dart';
|
|
|
|
|
import 'package:huawei_push/huawei_push.dart' as h_push;
|
|
|
|
|
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/new_models/gas_refill_model.dart';
|
|
|
|
|
import 'package:test_sa/modules/cm_module/cm_detail_page.dart';
|
|
|
|
|
@ -28,53 +28,71 @@ import 'package:test_sa/modules/tm_module/gas_refill/gas_refill_details.dart';
|
|
|
|
|
import 'package:test_sa/new_views/common_widgets/default_app_bar.dart';
|
|
|
|
|
import 'package:test_sa/views/widgets/loaders/no_data_found.dart';
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
@pragma('vm:entry-point')
|
|
|
|
|
Future<void> firebaseMessagingBackgroundHandler(RemoteMessage message) async {
|
|
|
|
|
if (message.notification == null && message.data != null) {
|
|
|
|
|
// Killed app: main() does not run for an FCM background message, so the callkit background
|
|
|
|
|
// engine (registered in main) is not started and Accept/Decline taps would be dropped.
|
|
|
|
|
// Registering here starts it, so decline reaches `onBackgroundMessage` below.
|
|
|
|
|
if (Platform.isAndroid && message.data["type"] == "incoming_call") {
|
|
|
|
|
try {
|
|
|
|
|
await FlutterCallkitIncoming.onBackgroundMessage(onBackgroundMessage);
|
|
|
|
|
} catch (_) {}
|
|
|
|
|
}
|
|
|
|
|
handleDataMessage(message.data, true);
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
/// Android: receives callkit events while no UI listener is active (app killed / not on LandPage).
|
|
|
|
|
/// When the app is in the foreground, events go to `callingEvents` in land_page.dart instead.
|
|
|
|
|
@pragma('vm:entry-point')
|
|
|
|
|
Future<void> onBackgroundMessage(CallEvent event1) async {
|
|
|
|
|
// This executes when a background/killed action happens
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
FlutterCallkitIncoming.onEvent.listen((CallEvent? event) {
|
|
|
|
|
switch (event!.eventName) {
|
|
|
|
|
case CallEventConstants.actionCallAccept:
|
|
|
|
|
// Handle logic when user hits Accept while app transitions to foreground
|
|
|
|
|
// var extraData = event?.body['extra'];
|
|
|
|
|
// Navigate to Call Screen
|
|
|
|
|
print("onBackgroundMessage:actionCallAccept:${event}");
|
|
|
|
|
break;
|
|
|
|
|
case CallEventConstants.actionCallDecline:
|
|
|
|
|
// Notify backend that user declined
|
|
|
|
|
print("onBackgroundMessage:actionCallDecline:${event}");
|
|
|
|
|
break;
|
|
|
|
|
default:
|
|
|
|
|
break;
|
|
|
|
|
}
|
|
|
|
|
});
|
|
|
|
|
|
|
|
|
|
switch (event1.eventName) {
|
|
|
|
|
case CallEventConstants.actionCallAccept:
|
|
|
|
|
"actionCallAccept".showToast;
|
|
|
|
|
break;
|
|
|
|
|
case CallEventConstants.actionCallDecline:
|
|
|
|
|
"actionCallDecline".showToast;
|
|
|
|
|
break;
|
|
|
|
|
Future<void> onBackgroundMessage(CallEvent event) async {
|
|
|
|
|
switch (event) {
|
|
|
|
|
case CallEventActionCallDecline(:final callKitParams):
|
|
|
|
|
await _notifyCallerDeclined(callKitParams.extra ?? {});
|
|
|
|
|
default:
|
|
|
|
|
// Accept is handled when the app opens (splash_page.dart -> checkVoipCalling via activeCalls).
|
|
|
|
|
break;
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
/// Tells the caller the call was declined (same signalling as `_handleCallDeclined` in land_page.dart).
|
|
|
|
|
Future<void> _notifyCallerDeclined(Map<String, dynamic> extra) async {
|
|
|
|
|
final callee = extra["calleeEmployeeNumber"]?.toString() ?? '';
|
|
|
|
|
final caller = extra["callerEmployeeNumber"]?.toString() ?? '';
|
|
|
|
|
if (callee.isEmpty || caller.isEmpty) return;
|
|
|
|
|
final provider = CallProvider();
|
|
|
|
|
try {
|
|
|
|
|
if (await provider.buildHubConnection(callee)) {
|
|
|
|
|
await provider.declineCall(callee, caller, moduleId: int.tryParse(extra["moduleId"]?.toString() ?? '') ?? 0);
|
|
|
|
|
}
|
|
|
|
|
} catch (_) {
|
|
|
|
|
// Best effort - the caller side also times out.
|
|
|
|
|
} finally {
|
|
|
|
|
try {
|
|
|
|
|
await provider.endSession();
|
|
|
|
|
} catch (_) {}
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
class FirebaseNotificationManger {
|
|
|
|
|
static FirebaseMessaging messaging = FirebaseMessaging.instance;
|
|
|
|
|
static String? token;
|
|
|
|
|
static String? voipToken;
|
|
|
|
|
static Timer? _missedCallTimer;
|
|
|
|
|
static String? _activeMissedCallId;
|
|
|
|
|
|
|
|
|
|
/// iOS only. The VoIP token is registered natively in AppDelegate (PushKit) and stored
|
|
|
|
|
/// by flutter_callkit_incoming. It is sent to the backend as `voIPToken` on chat login.
|
|
|
|
|
static Future<void> refreshVoipToken() async {
|
|
|
|
|
if (!Platform.isIOS) return;
|
|
|
|
|
try {
|
|
|
|
|
final value = await FlutterCallkitIncoming.getDevicePushTokenVoIP();
|
|
|
|
|
if (value != null && value.isNotEmpty) voipToken = value;
|
|
|
|
|
} catch (_) {}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
static void cancelMissedCallTimer([String? callId]) {
|
|
|
|
|
if (_activeMissedCallId == null) return;
|
|
|
|
|
if (callId != null && _activeMissedCallId != callId) return;
|
|
|
|
|
@ -83,6 +101,15 @@ class FirebaseNotificationManger {
|
|
|
|
|
_activeMissedCallId = null;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
static final FirebaseNotificationManger _instance = FirebaseNotificationManger._internal();
|
|
|
|
|
|
|
|
|
|
FirebaseNotificationManger._internal();
|
|
|
|
|
|
|
|
|
|
factory FirebaseNotificationManger() => _instance;
|
|
|
|
|
|
|
|
|
|
static FirebaseNotificationManger getInstance() => _instance;
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
static Future<void> getToken() async {
|
|
|
|
|
NotificationSettings settings = await messaging.requestPermission(alert: true, announcement: false, badge: true, carPlay: false, criticalAlert: false, provisional: false, sound: true);
|
|
|
|
|
|
|
|
|
|
@ -112,10 +139,11 @@ class FirebaseNotificationManger {
|
|
|
|
|
print("Notification Exception: " + ex.toString());
|
|
|
|
|
}
|
|
|
|
|
} else {
|
|
|
|
|
// VoIP token does not depend on notification permission.
|
|
|
|
|
await refreshVoipToken();
|
|
|
|
|
if (settings.authorizationStatus == AuthorizationStatus.authorized) {
|
|
|
|
|
try {
|
|
|
|
|
token = await messaging.getToken();
|
|
|
|
|
voipToken = await FlutterCallkitIncoming.getDevicePushTokenVoIP();
|
|
|
|
|
} catch (ex) {}
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
@ -254,8 +282,14 @@ class FirebaseNotificationManger {
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
static initialized(BuildContext context) async {
|
|
|
|
|
//TOD0 add platform check here also
|
|
|
|
|
initialized(BuildContext context) async {
|
|
|
|
|
// iOS incoming calls: VoIP pushes are handled natively in AppDelegate (PushKit -> CallKit
|
|
|
|
|
// via flutter_callkit_incoming). Accept/decline arrive through FlutterCallkitIncoming.onEvent
|
|
|
|
|
// (land_page.dart) or, from a cold start, FlutterCallkitIncoming.activeCalls() (splash_page.dart).
|
|
|
|
|
if (Platform.isIOS) {
|
|
|
|
|
await refreshVoipToken();
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
if (!(await isGoogleServicesAvailable()) && Platform.isAndroid) {
|
|
|
|
|
var initialNotification = await h_push.Push.getInitialNotification();
|
|
|
|
|
if (initialNotification != null) {
|
|
|
|
|
@ -366,18 +400,30 @@ Future<void> handleDataMessage(Map<String, dynamic> messageData, bool isFromBack
|
|
|
|
|
|
|
|
|
|
print("handleDataMessage:moduleId:$moduleId");
|
|
|
|
|
|
|
|
|
|
// Android: any string id works. iOS: CallKit silently ignores non-UUID ids, so use a
|
|
|
|
|
// generated UUID for CallKit and keep the backend id in `extra.callId` (used everywhere else).
|
|
|
|
|
String callKitId = callId;
|
|
|
|
|
if (Platform.isIOS) {
|
|
|
|
|
// Already reported via a VoIP push (AppDelegate)? Don't ring twice.
|
|
|
|
|
final active = await FlutterCallkitIncoming.activeCalls();
|
|
|
|
|
if (active.any((c) => c.extra?['callId']?.toString() == callId)) return;
|
|
|
|
|
if (!_isUuid(callId)) callKitId = _uuidV4();
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
WidgetsFlutterBinding.ensureInitialized();
|
|
|
|
|
FirebaseNotificationManger.cancelMissedCallTimer();
|
|
|
|
|
FirebaseNotificationManger._activeMissedCallId = callId;
|
|
|
|
|
try {
|
|
|
|
|
final callKitParams = CallKitParams(
|
|
|
|
|
id: callId,
|
|
|
|
|
id: callKitId,
|
|
|
|
|
nameCaller: callerName,
|
|
|
|
|
appName: 'ATOMS',
|
|
|
|
|
avatar: '',
|
|
|
|
|
handle: callerEmployeeNumber,
|
|
|
|
|
type: isVideoCall ? 1 : 0,
|
|
|
|
|
duration: 30000,
|
|
|
|
|
// Slightly longer than the 30s missed-call timer below, so that timer (which also sends
|
|
|
|
|
// NO_ANSWER to the caller) always runs first; the plugin timeout is only a fallback.
|
|
|
|
|
duration: 32000,
|
|
|
|
|
missedCallNotification: const NotificationParams(
|
|
|
|
|
showNotification: true,
|
|
|
|
|
isShowCallback: false,
|
|
|
|
|
@ -444,7 +490,15 @@ Future<void> handleDataMessage(Map<String, dynamic> messageData, bool isFromBack
|
|
|
|
|
FirebaseNotificationManger._missedCallTimer = Timer(const Duration(seconds: 30), () async {
|
|
|
|
|
if (FirebaseNotificationManger._activeMissedCallId != callId) return;
|
|
|
|
|
|
|
|
|
|
await FlutterCallkitIncoming.endCall(callId);
|
|
|
|
|
// This timer may live in a background isolate that cannot be cancelled from the
|
|
|
|
|
// decline/accept handlers, so re-check: only a call that is still ringing is "missed".
|
|
|
|
|
final stillRinging = (await FlutterCallkitIncoming.activeCalls()).any((c) => c.id == callKitId && !c.isAccepted);
|
|
|
|
|
if (!stillRinging) {
|
|
|
|
|
FirebaseNotificationManger.cancelMissedCallTimer(callId);
|
|
|
|
|
return;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
await FlutterCallkitIncoming.endCall(callKitId);
|
|
|
|
|
await FlutterCallkitIncoming.endAllCalls();
|
|
|
|
|
await FlutterCallkitIncoming.showMissCallNotification(callKitParams);
|
|
|
|
|
try {
|
|
|
|
|
@ -470,3 +524,18 @@ Future<void> handleDataMessage(Map<String, dynamic> messageData, bool isFromBack
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
final RegExp _uuidPattern = RegExp(r'^[0-9a-fA-F]{8}-[0-9a-fA-F]{4}-[0-9a-fA-F]{4}-[0-9a-fA-F]{4}-[0-9a-fA-F]{12}$');
|
|
|
|
|
|
|
|
|
|
bool _isUuid(String value) => _uuidPattern.hasMatch(value);
|
|
|
|
|
|
|
|
|
|
/// RFC 4122 version-4 UUID (avoids adding a dependency for one call site).
|
|
|
|
|
String _uuidV4() {
|
|
|
|
|
final rnd = Random.secure();
|
|
|
|
|
final b = List<int>.generate(16, (_) => rnd.nextInt(256));
|
|
|
|
|
b[6] = (b[6] & 0x0f) | 0x40;
|
|
|
|
|
b[8] = (b[8] & 0x3f) | 0x80;
|
|
|
|
|
final h = b.map((x) => x.toRadixString(16).padLeft(2, '0')).join();
|
|
|
|
|
return '${h.substring(0, 8)}-${h.substring(8, 12)}-${h.substring(12, 16)}-${h.substring(16, 20)}-${h.substring(20)}';
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|