diff --git a/android/app/src/main/java/com/czg/cashier_reserve/call/CallManager.java b/android/app/src/main/java/com/czg/cashier_reserve/call/CallManager.java index 5e46aed..8ecb104 100644 --- a/android/app/src/main/java/com/czg/cashier_reserve/call/CallManager.java +++ b/android/app/src/main/java/com/czg/cashier_reserve/call/CallManager.java @@ -17,6 +17,8 @@ import org.json.JSONObject; import java.text.SimpleDateFormat; import java.util.Date; +import cn.kaer.callmodule.ctrl.CallController; +import cn.kaer.callmodule.exceptions.CallException; import io.flutter.Log; public class CallManager { @@ -68,4 +70,22 @@ public class CallManager { broad.putExtra("callLog", callLogArray.toString()); mainActivityContext.sendBroadcast(broad); } + + public static void acceptCall() { + try { + CallController.get().answer(); + } catch (CallException e) { + e.printStackTrace(); + } + } + + public static void rejectCall() { + try { + CallController.get().term(); + } catch (CallException e) { + e.printStackTrace(); + } + } + + public static void endCall() {} } diff --git a/android/app/src/main/java/com/czg/cashier_reserve/channel/ChannelNames.java b/android/app/src/main/java/com/czg/cashier_reserve/channel/ChannelNames.java index e90f9d4..a3c6679 100644 --- a/android/app/src/main/java/com/czg/cashier_reserve/channel/ChannelNames.java +++ b/android/app/src/main/java/com/czg/cashier_reserve/channel/ChannelNames.java @@ -8,6 +8,9 @@ public class ChannelNames { public static final String CALL_LOG_CALLBACK = "callLogCallback"; public static final String CALL_STATUS_CHANGE = "callStatusChange"; + public static final String ACCEPT_CALL = "acceptCall"; + public static final String REJECT_CALL = "rejectCall"; + public static final String END_CALL = "endCall"; public static String getChannelName(String name) { return BASE_NAME + name; diff --git a/android/app/src/main/java/com/czg/cashier_reserve/channel/MessageChannel.java b/android/app/src/main/java/com/czg/cashier_reserve/channel/MessageChannel.java index 39b11f1..35403d4 100644 --- a/android/app/src/main/java/com/czg/cashier_reserve/channel/MessageChannel.java +++ b/android/app/src/main/java/com/czg/cashier_reserve/channel/MessageChannel.java @@ -8,6 +8,9 @@ import io.flutter.plugin.common.MethodChannel; public class MessageChannel { public static void addMessageChannel(BinaryMessenger messenger) { callLogChannel(messenger); + acceptCallChannel(messenger); + rejectCallChannel(messenger); + endCallChannel(messenger); } public static void callLogChannel(BinaryMessenger messenger) { @@ -26,4 +29,55 @@ public class MessageChannel { }; }); } + + public static void acceptCallChannel(BinaryMessenger messenger) { + if (messenger == null) { + return; + } + + MethodChannel methodChannel = new MethodChannel(messenger, ChannelNames.getChannelName(ChannelNames.ACCEPT_CALL)); + + methodChannel.setMethodCallHandler((call, result) -> { + System.out.println("call.method: " + call.method); + if (call.method.equals(ChannelNames.ACCEPT_CALL)) { + System.out.println("接听电话"); + result.success(null); + CallManager.acceptCall(); + } + }); + } + + public static void rejectCallChannel(BinaryMessenger messenger) { + if (messenger == null) { + return; + } + + MethodChannel methodChannel = new MethodChannel(messenger, ChannelNames.getChannelName(ChannelNames.REJECT_CALL)); + + methodChannel.setMethodCallHandler((call, result) -> { + System.out.println("call.method: " + call.method); + if (call.method.equals(ChannelNames.REJECT_CALL)) { + System.out.println("拒接电话"); + result.success(null); + CallManager.rejectCall(); + } + }); + } + + public static void endCallChannel(BinaryMessenger messenger) { + if (messenger == null) { + return; + } + + MethodChannel methodChannel = new MethodChannel(messenger, ChannelNames.getChannelName(ChannelNames.END_CALL)); + + methodChannel.setMethodCallHandler((call, result) -> { + System.out.println("call.method: " + call.method); + if (call.method.equals(ChannelNames.END_CALL)) { + System.out.println("结束通话"); + result.success(null); + CallManager.endCall(); + } + }); + } } diff --git a/images/call/accept.png b/images/call/accept.png new file mode 100644 index 0000000..0723f72 Binary files /dev/null and b/images/call/accept.png differ diff --git a/images/call/reject.png b/images/call/reject.png new file mode 100644 index 0000000..93a498f Binary files /dev/null and b/images/call/reject.png differ diff --git a/lib/call/call_view.dart b/lib/call/call_view.dart index abba097..4fcee6e 100644 --- a/lib/call/call_view.dart +++ b/lib/call/call_view.dart @@ -1,10 +1,14 @@ +import 'dart:async'; + import 'package:cashier_reserve/common/base/ui.dart'; import 'package:cashier_reserve/common/channel/model/call_status_change_model.dart'; +import 'package:cashier_reserve/common/manager/event_manager.dart'; class CallView extends StatefulWidget { final CallStatusChangeModel statusModel; + final Function(String action)? onAction; - const CallView({super.key, required this.statusModel}); + const CallView({super.key, required this.statusModel, this.onAction}); @override State createState() { @@ -13,6 +17,26 @@ class CallView extends StatefulWidget { } class _CallViewState extends State { + int _callDuration = 0; + Timer? _callDurationTimer; + + bool _isAccept = false; + + @override + void initState() { + super.initState(); + + EventManager.addListener(this, (event) { + _acceptCall(); + }); + } + + @override + void dispose() { + _cancelDurationTimer(); + EventManager.cancelListener(this); + super.dispose(); + } @override Widget build(BuildContext context) { @@ -27,11 +51,118 @@ class _CallViewState extends State { child: Container( width: MediaQuery.of(context).size.width, height: MediaQuery.of(context).size.height, - color: Colors.blue, - child: Container(), + color: const Color(0x55000000), + child: Center( + child: Container( + width: MediaQuery.of(context).size.width * 0.8, + height: MediaQuery.of(context).size.height * 0.8, + decoration: BoxDecoration( + color: Colors.blue.withOpacity(0.9), + borderRadius: BorderRadius.circular(10), + ), + child: _buildCallInfo(context), + ), + ), ), ), ), ); } + + String _getCallDuration() { + // 将秒数转换为时分秒 + int hour = _callDuration ~/ 3600; + int minute = _callDuration % 3600 ~/ 60; + int second = _callDuration % 60; + return "${hour.toString().padLeft(2, '0')}:${minute.toString().padLeft(2, '0')}:${second.toString().padLeft(2, '0')}"; + } + + _acceptCall() { + if (_isAccept) { + return; + } + _isAccept = true; + _startDurationTimer(); + } + + _startDurationTimer() { + _callDurationTimer = Timer.periodic(const Duration(seconds: 1), (timer) { + setState(() { + _callDuration++; + }); + }); + } + + _cancelDurationTimer() { + _callDurationTimer?.cancel(); + } + + Widget _buildCallInfo(BuildContext context) { + return Column( + mainAxisAlignment: MainAxisAlignment.center, + crossAxisAlignment: CrossAxisAlignment.center, + children: [ + _buildIncomingInfo(context), + const SizedBox(height: 70), + _buildActionBtn(context), + ], + ); + } + + Widget _buildIncomingInfo(BuildContext context) { + return Column( + mainAxisAlignment: MainAxisAlignment.center, + crossAxisAlignment: CrossAxisAlignment.center, + children: [ + Text( + "${widget.statusModel.number}", + style: const TextStyle( + fontSize: 35, color: Colors.white, fontWeight: FontWeight.bold), + ), + const SizedBox(height: 10), + Text( + "${widget.statusModel.region}", + style: const TextStyle(fontSize: 15, color: Colors.white), + ), + const SizedBox(height: 20), + Text(_getCallDuration(), + style: const TextStyle(fontSize: 20, color: Colors.white)), + ], + ); + } + + Widget _buildActionBtn(BuildContext context) { + return Row( + mainAxisAlignment: MainAxisAlignment.center, + children: [ + InkWell( + onTap: () { + // 拒绝 + if (widget.onAction != null) { + widget.onAction!("reject"); + } + }, + child: Container( + padding: const EdgeInsets.all(10), + child: Image.asset('images/call/reject.png', width: 60, height: 60), + ), + ), + if (!_isAccept) const SizedBox(width: 80), + if (!_isAccept) InkWell( + onTap: () { + // 接听 + if (widget.onAction != null) { + widget.onAction!("accept"); + } + + _acceptCall(); + }, + child: Container( + padding: const EdgeInsets.all(10), + child: Image.asset('images/call/accept.png', width: 60, height: 60), + ), + ), + ], + ); + } } diff --git a/lib/common/channel/channel_manager.dart b/lib/common/channel/channel_manager.dart index 5e5e7f3..973da48 100644 --- a/lib/common/channel/channel_manager.dart +++ b/lib/common/channel/channel_manager.dart @@ -13,4 +13,34 @@ class ChannelManager { yjPrint('$kGetCallLog 发生异常:$e'); } } + + static Future acceptCall() async { + MethodChannel channel = MethodChannel(getChannelName(kAcceptCall)); + try { + final result = await channel.invokeMethod(kAcceptCall); + yjPrint(result); + } on PlatformException catch (e) { + yjPrint('$kAcceptCall 发生异常:$e'); + } + } + + static Future rejectCall() async { + MethodChannel channel = MethodChannel(getChannelName(kRejectCall)); + try { + final result = await channel.invokeMethod(kRejectCall); + yjPrint(result); + } on PlatformException catch (e) { + yjPrint('$kRejectCall 发生异常:$e'); + } + } + + static Future endCall() async { + MethodChannel channel = MethodChannel(getChannelName(kEndCall)); + try { + final result = await channel.invokeMethod(kEndCall); + yjPrint(result); + } on PlatformException catch (e) { + yjPrint('$kEndCall 发生异常:$e'); + } + } } \ No newline at end of file diff --git a/lib/common/channel/names.dart b/lib/common/channel/names.dart index 9e2ee7a..9947a9f 100644 --- a/lib/common/channel/names.dart +++ b/lib/common/channel/names.dart @@ -5,6 +5,9 @@ const String kGetCallLog = 'getCallLog'; const String kCallLogCallback = 'callLogCallback'; const String kCallStatusChange = 'callStatusChange'; +const String kAcceptCall = 'acceptCall'; +const String kEndCall = 'endCall'; +const String kRejectCall = 'rejectCall'; String getChannelName(name) { return kChannelBaseName + name; diff --git a/lib/common/manager/event_manager.dart b/lib/common/manager/event_manager.dart index f38ae96..cb5c019 100644 --- a/lib/common/manager/event_manager.dart +++ b/lib/common/manager/event_manager.dart @@ -55,7 +55,15 @@ class GetCallLogEvent extends MyEvent { } class CallStatusChangeEvent extends MyEvent { + /// state = IncomingNumberReceived 来电接听 + /// state = OutGoing 呼出 + /// state = End 通话结束 + /// state = Incoming 来电 CallStatusChangeModel model; CallStatusChangeEvent({required this.model}); +} + +class CallReceivedEvent extends MyEvent { + CallReceivedEvent(); } \ No newline at end of file diff --git a/lib/home/home_view_model.dart b/lib/home/home_view_model.dart index c28befa..1372e94 100644 --- a/lib/home/home_view_model.dart +++ b/lib/home/home_view_model.dart @@ -1,6 +1,7 @@ import 'package:cashier_reserve/call/call_view.dart'; import 'package:cashier_reserve/common/base/ui.dart'; import 'package:cashier_reserve/common/base/ui_model.dart'; +import 'package:cashier_reserve/common/channel/channel_manager.dart'; import 'package:cashier_reserve/common/channel/model/call_status_change_model.dart'; import 'package:cashier_reserve/common/manager/app_manager.dart'; import 'package:cashier_reserve/common/manager/event_manager.dart'; @@ -51,11 +52,27 @@ class HomeViewModel extends BaseUIModel { }); EventManager.addListener(this, (event) { - yjPrint("HomeViewModel CallStatusChangeEvent state: ${event.model.state}"); - if (event.model.state == "Incoming") { - showCallInfoView(event.model); - } else { - hideCallInfoView(); + yjPrint( + "HomeViewModel CallStatusChangeEvent state: ${event.model.state}"); + + /// state = IncomingNumberReceived 来电接听 + /// state = OutGoing 呼出 + /// state = End 通话结束 + /// state = Incoming 来电 + /// state = Idle 空闲 + + switch (event.model.state) { + case "IncomingNumberReceived": + EventManager.postEvent(CallReceivedEvent()); + break; + case "OutGoing": + break; + case "Incoming": + showCallInfoView(event.model); + break; + default: + hideCallInfoView(); + break; } }); } @@ -85,7 +102,19 @@ class HomeViewModel extends BaseUIModel { if (isShowCallView) { return; } - YJPush.presentWidget(context!, CallView(statusModel: model)); + YJPush.presentWidget( + context!, + CallView( + statusModel: model, + onAction: (action) { + yjPrint("call view action: $action"); + if (action == "accept") { + ChannelManager.acceptCall(); + } else { + ChannelManager.rejectCall(); + hideCallInfoView(); + } + })); isShowCallView = true; } diff --git a/lib/home/reserve_view.dart b/lib/home/reserve_view.dart index 62511df..32278c7 100644 --- a/lib/home/reserve_view.dart +++ b/lib/home/reserve_view.dart @@ -78,7 +78,9 @@ class ReserveView extends BaseUI with TickerProviderStateMixin { width: 15, ), InkWell( - onTap: () { }, + onTap: () { + provider.testCallIncoming(); + }, child: SizedBox( width: 40, height: 40, @@ -95,9 +97,7 @@ class ReserveView extends BaseUI with TickerProviderStateMixin { width: 5, ), InkWell( - onTap: () { - provider.loadTableAreaList(); - }, + onTap: () { }, child: SizedBox( width: 40, height: 40, diff --git a/lib/home/reserve_view_model.dart b/lib/home/reserve_view_model.dart index 013821d..7a20a9a 100644 --- a/lib/home/reserve_view_model.dart +++ b/lib/home/reserve_view_model.dart @@ -1,6 +1,7 @@ import 'package:cashier_reserve/common/base/ui_model.dart'; import 'package:cashier_reserve/common/channel/model/call_log_model.dart'; import 'package:cashier_reserve/common/channel/channel_manager.dart'; +import 'package:cashier_reserve/common/channel/model/call_status_change_model.dart'; import 'package:cashier_reserve/common/manager/app_manager.dart'; import 'package:cashier_reserve/common/manager/event_manager.dart'; import 'package:cashier_reserve/common/print/print.dart'; @@ -138,6 +139,17 @@ class ReserveViewModel extends BaseUIModel { loadReserveSms(); } + void testCallIncoming() { + CallStatusChangeModel model = CallStatusChangeModel( + state: "Incoming", + number: "123456789", + name: "测试", + region: "陕西省 西安市", + ); + + EventManager.postEvent(CallStatusChangeEvent(model: model)); + } + void loadCallLog() { ChannelManager.getCallLog("getCallLog"); } diff --git a/pubspec.yaml b/pubspec.yaml index 8aa50d6..3c9fe0a 100644 --- a/pubspec.yaml +++ b/pubspec.yaml @@ -84,6 +84,8 @@ flutter: - images/reserve/ + - images/call/ + # An image asset can refer to one or more resolution-specific "variants", see # https://flutter.dev/to/resolution-aware-images