|
|
|
@ -14,7 +14,10 @@ import 'package:flutter_callkit_incoming/entities/notification_params.dart';
|
|
|
|
import 'package:flutter_callkit_incoming/flutter_callkit_incoming.dart';
|
|
|
|
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:permission_handler/permission_handler.dart' as ph;
|
|
|
|
import 'package:test_sa/controllers/notification/notification_manger.dart';
|
|
|
|
import 'package:test_sa/controllers/notification/notification_manger.dart';
|
|
|
|
|
|
|
|
import 'package:test_sa/helper/utils.dart';
|
|
|
|
|
|
|
|
import 'package:test_sa/views/widgets/dialogs/dialog.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';
|
|
|
|
@ -50,7 +53,7 @@ Future<void> firebaseMessagingBackgroundHandler(RemoteMessage message) async {
|
|
|
|
Future<void> onBackgroundMessage(CallEvent event) async {
|
|
|
|
Future<void> onBackgroundMessage(CallEvent event) async {
|
|
|
|
switch (event) {
|
|
|
|
switch (event) {
|
|
|
|
case CallEventActionCallDecline(:final callKitParams):
|
|
|
|
case CallEventActionCallDecline(:final callKitParams):
|
|
|
|
await _notifyCallerDeclined(callKitParams.extra ?? {});
|
|
|
|
await notifyCallerDeclined(callKitParams.extra ?? {});
|
|
|
|
default:
|
|
|
|
default:
|
|
|
|
// Accept is handled when the app opens (splash_page.dart -> checkVoipCalling via activeCalls).
|
|
|
|
// Accept is handled when the app opens (splash_page.dart -> checkVoipCalling via activeCalls).
|
|
|
|
break;
|
|
|
|
break;
|
|
|
|
@ -58,14 +61,14 @@ Future<void> onBackgroundMessage(CallEvent event) async {
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
/// Tells the caller the call was declined (same signalling as `_handleCallDeclined` in land_page.dart).
|
|
|
|
/// Tells the caller the call was declined (same signalling as `_handleCallDeclined` in land_page.dart).
|
|
|
|
Future<void> _notifyCallerDeclined(Map<String, dynamic> extra) async {
|
|
|
|
Future<void> notifyCallerDeclined(Map<String, dynamic> extra, {String? reason}) async {
|
|
|
|
final callee = extra["calleeEmployeeNumber"]?.toString() ?? '';
|
|
|
|
final callee = extra["calleeEmployeeNumber"]?.toString() ?? '';
|
|
|
|
final caller = extra["callerEmployeeNumber"]?.toString() ?? '';
|
|
|
|
final caller = extra["callerEmployeeNumber"]?.toString() ?? '';
|
|
|
|
if (callee.isEmpty || caller.isEmpty) return;
|
|
|
|
if (callee.isEmpty || caller.isEmpty) return;
|
|
|
|
final provider = CallProvider();
|
|
|
|
final provider = CallProvider();
|
|
|
|
try {
|
|
|
|
try {
|
|
|
|
if (await provider.buildHubConnection(callee)) {
|
|
|
|
if (await provider.buildHubConnection(callee)) {
|
|
|
|
await provider.declineCall(callee, caller, moduleId: int.tryParse(extra["moduleId"]?.toString() ?? '') ?? 0);
|
|
|
|
await provider.declineCall(callee, caller, moduleId: int.tryParse(extra["moduleId"]?.toString() ?? '') ?? 0, reason: reason);
|
|
|
|
}
|
|
|
|
}
|
|
|
|
} catch (_) {
|
|
|
|
} catch (_) {
|
|
|
|
// Best effort - the caller side also times out.
|
|
|
|
// Best effort - the caller side also times out.
|
|
|
|
@ -413,6 +416,13 @@ Future<void> handleDataMessage(Map<String, dynamic> messageData, bool isFromBack
|
|
|
|
WidgetsFlutterBinding.ensureInitialized();
|
|
|
|
WidgetsFlutterBinding.ensureInitialized();
|
|
|
|
FirebaseNotificationManger.cancelMissedCallTimer();
|
|
|
|
FirebaseNotificationManger.cancelMissedCallTimer();
|
|
|
|
FirebaseNotificationManger._activeMissedCallId = callId;
|
|
|
|
FirebaseNotificationManger._activeMissedCallId = callId;
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
// Android 14+: on Accept, the plugin starts its "ongoing call" notification as a foreground
|
|
|
|
|
|
|
|
// service of type microphone (+camera for video). Without those runtime permissions Android
|
|
|
|
|
|
|
|
// throws a SecurityException and the app crashes before any Dart runs. When permissions are
|
|
|
|
|
|
|
|
// missing we skip that notification, so Accept just opens the app and the accept handler
|
|
|
|
|
|
|
|
// (land_page / splash_page) asks for permission instead.
|
|
|
|
|
|
|
|
final canShowOngoingNotification = !Platform.isAndroid || await hasCallPermissions(isVideoCall);
|
|
|
|
try {
|
|
|
|
try {
|
|
|
|
final callKitParams = CallKitParams(
|
|
|
|
final callKitParams = CallKitParams(
|
|
|
|
id: callKitId,
|
|
|
|
id: callKitId,
|
|
|
|
@ -430,8 +440,8 @@ Future<void> handleDataMessage(Map<String, dynamic> messageData, bool isFromBack
|
|
|
|
subtitle: 'Missed call',
|
|
|
|
subtitle: 'Missed call',
|
|
|
|
callbackText: 'Call back',
|
|
|
|
callbackText: 'Call back',
|
|
|
|
),
|
|
|
|
),
|
|
|
|
callingNotification: const NotificationParams(
|
|
|
|
callingNotification: NotificationParams(
|
|
|
|
showNotification: true,
|
|
|
|
showNotification: canShowOngoingNotification,
|
|
|
|
isShowCallback: true,
|
|
|
|
isShowCallback: true,
|
|
|
|
subtitle: 'Calling...',
|
|
|
|
subtitle: 'Calling...',
|
|
|
|
callbackText: 'Hang Up',
|
|
|
|
callbackText: 'Hang Up',
|
|
|
|
@ -476,6 +486,8 @@ Future<void> handleDataMessage(Map<String, dynamic> messageData, bool isFromBack
|
|
|
|
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.",
|
|
|
|
@ -529,6 +541,61 @@ final RegExp _uuidPattern = RegExp(r'^[0-9a-fA-F]{8}-[0-9a-fA-F]{4}-[0-9a-fA-F]{
|
|
|
|
|
|
|
|
|
|
|
|
bool _isUuid(String value) => _uuidPattern.hasMatch(value);
|
|
|
|
bool _isUuid(String value) => _uuidPattern.hasMatch(value);
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
/// Permissions needed to answer a call: microphone, plus camera for video.
|
|
|
|
|
|
|
|
///
|
|
|
|
|
|
|
|
/// Android 14+ (targetSdk 34+): when Accept is tapped, flutter_callkit_incoming natively starts a
|
|
|
|
|
|
|
|
/// foreground service of type `microphone` (and `camera` for video). Starting it without these
|
|
|
|
|
|
|
|
/// runtime permissions throws a SecurityException that kills the app before any Dart code runs,
|
|
|
|
|
|
|
|
/// so the Accept button must never be shown while they are missing.
|
|
|
|
|
|
|
|
Future<bool> hasCallPermissions(bool isVideoCall) async {
|
|
|
|
|
|
|
|
try {
|
|
|
|
|
|
|
|
if (!await ph.Permission.microphone.isGranted) return false;
|
|
|
|
|
|
|
|
if (isVideoCall && !await ph.Permission.camera.isGranted) return false;
|
|
|
|
|
|
|
|
return true;
|
|
|
|
|
|
|
|
} catch (_) {
|
|
|
|
|
|
|
|
return false;
|
|
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
/// Requests call permissions (needs a foreground Activity). Returns true when all are granted.
|
|
|
|
|
|
|
|
Future<bool> requestCallPermissions({bool includeCamera = true}) async {
|
|
|
|
|
|
|
|
try {
|
|
|
|
|
|
|
|
final result = await [ph.Permission.microphone, if (includeCamera) ph.Permission.camera].request();
|
|
|
|
|
|
|
|
return result.values.every((s) => s.isGranted);
|
|
|
|
|
|
|
|
} catch (_) {
|
|
|
|
|
|
|
|
return false;
|
|
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
/// Used when the user taps Accept: returns true if the call can proceed.
|
|
|
|
|
|
|
|
/// Asks for permission if needed; if it was permanently denied (Android stops showing the
|
|
|
|
|
|
|
|
/// system prompt), offers to open App Settings instead of failing silently.
|
|
|
|
|
|
|
|
Future<bool> ensureCallPermissionsOnAccept(BuildContext context, {required bool isVideoCall}) async {
|
|
|
|
|
|
|
|
if (await hasCallPermissions(isVideoCall)) return true;
|
|
|
|
|
|
|
|
if (await requestCallPermissions(includeCamera: isVideoCall)) return true;
|
|
|
|
|
|
|
|
if (!context.mounted) return false;
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
final what = isVideoCall ? "Camera and microphone" : "Microphone";
|
|
|
|
|
|
|
|
final permanentlyDenied = await ph.Permission.microphone.isPermanentlyDenied || (isVideoCall && await ph.Permission.camera.isPermanentlyDenied);
|
|
|
|
|
|
|
|
if (!context.mounted) return false;
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
if (permanentlyDenied) {
|
|
|
|
|
|
|
|
final openSettings = await showDialog<bool>(
|
|
|
|
|
|
|
|
context: context,
|
|
|
|
|
|
|
|
builder: (_) => AAlertDialog(
|
|
|
|
|
|
|
|
title: "Permission Required",
|
|
|
|
|
|
|
|
content: "$what access is turned off for ATOMS. Enable it in Settings to answer calls.",
|
|
|
|
|
|
|
|
button1Text: "Open Settings",
|
|
|
|
|
|
|
|
button2Text: "Cancel",
|
|
|
|
|
|
|
|
),
|
|
|
|
|
|
|
|
);
|
|
|
|
|
|
|
|
if (openSettings == true) await ph.openAppSettings();
|
|
|
|
|
|
|
|
} else {
|
|
|
|
|
|
|
|
Utils.showToast("$what permission is required to answer calls.");
|
|
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
return false;
|
|
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
/// RFC 4122 version-4 UUID (avoids adding a dependency for one call site).
|
|
|
|
/// RFC 4122 version-4 UUID (avoids adding a dependency for one call site).
|
|
|
|
String _uuidV4() {
|
|
|
|
String _uuidV4() {
|
|
|
|
final rnd = Random.secure();
|
|
|
|
final rnd = Random.secure();
|
|
|
|
|