From 7b9a3a0daf9fd393b5eaa35537ae1735a4189042 Mon Sep 17 00:00:00 2001 From: Xie Jing Date: Sat, 29 Nov 2025 14:45:42 +0800 Subject: [PATCH 1/2] =?UTF-8?q?feat(=E9=94=81=E8=AF=A6=E6=83=85):=20?= =?UTF-8?q?=E6=B7=BB=E5=8A=A0=E6=8E=A8=E9=80=81=E8=A7=A6=E5=8F=91=E7=9A=84?= =?UTF-8?q?=E8=BF=9C=E7=A8=8B=E5=BC=80=E9=94=81=E5=8A=9F=E8=83=BD?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 实现推送消息触发的远程开锁请求流程,包括: 1. 新增RemoteUnlockRequestEvent和PushExtraEvent事件类型 2. 在锁详情页添加倒计时弹窗和动画效果 3. 实现Native层推送数据缓存和转发机制 4. 添加RemoteUnlockCoordinator处理应用未启动时的推送导航 --- .../kotlin/com/skychip/lock/MainActivity.kt | 136 ++++++++++++++ lib/app_settings/app_settings.dart | 3 +- lib/main.dart | 15 +- .../lockDetail/lockDetail_logic.dart | 44 +++++ .../lockDetail/lockDetail_page.dart | 170 ++++++++++++++++-- .../lockDetail/lockDetail_state.dart | 7 +- .../starLockApplication.dart | 1 - lib/tools/NativeInteractionTool.dart | 39 +++- lib/tools/eventBusEventManage.dart | 15 ++ lib/tools/push/message_management.dart | 18 +- lib/tools/push/xs_jPhush.dart | 51 +++++- lib/tools/remote_unlock_coordinator.dart | 118 ++++++++++++ lib/tools/showCupertinoAlertView.dart | 61 +++++++ pubspec.yaml | 2 +- 14 files changed, 657 insertions(+), 23 deletions(-) create mode 100644 lib/tools/remote_unlock_coordinator.dart diff --git a/android/app/src/main/kotlin/com/skychip/lock/MainActivity.kt b/android/app/src/main/kotlin/com/skychip/lock/MainActivity.kt index 76fc8e66..2ec29f6a 100755 --- a/android/app/src/main/kotlin/com/skychip/lock/MainActivity.kt +++ b/android/app/src/main/kotlin/com/skychip/lock/MainActivity.kt @@ -1,6 +1,8 @@ package com.skychip.lock import android.content.Intent +import android.content.Context +import android.content.Context.MODE_PRIVATE import android.net.Uri import android.os.Bundle import android.util.Log @@ -11,6 +13,55 @@ import io.flutter.plugins.GeneratedPluginRegistrant import android.bluetooth.BluetoothAdapter; import androidx.core.content.FileProvider import java.io.File +import org.json.JSONObject + +private fun flagsToString(flags: Int): String { + val flagsList = mutableListOf() + + if (flags and Intent.FLAG_ACTIVITY_NEW_TASK != 0) flagsList.add("FLAG_ACTIVITY_NEW_TASK") + if (flags and Intent.FLAG_ACTIVITY_CLEAR_TOP != 0) flagsList.add("FLAG_ACTIVITY_CLEAR_TOP") + if (flags and Intent.FLAG_ACTIVITY_SINGLE_TOP != 0) flagsList.add("FLAG_ACTIVITY_SINGLE_TOP") + if (flags and Intent.FLAG_ACTIVITY_CLEAR_TASK != 0) flagsList.add("FLAG_ACTIVITY_CLEAR_TASK") + if (flags and Intent.FLAG_ACTIVITY_NO_HISTORY != 0) flagsList.add("FLAG_ACTIVITY_NO_HISTORY") + if (flags and Intent.FLAG_ACTIVITY_MULTIPLE_TASK != 0) flagsList.add("FLAG_ACTIVITY_MULTIPLE_TASK") + if (flags and Intent.FLAG_ACTIVITY_EXCLUDE_FROM_RECENTS != 0) flagsList.add("FLAG_ACTIVITY_EXCLUDE_FROM_RECENTS") + if (flags and Intent.FLAG_ACTIVITY_BROUGHT_TO_FRONT != 0) flagsList.add("FLAG_ACTIVITY_BROUGHT_TO_FRONT") + if (flags and Intent.FLAG_ACTIVITY_RESET_TASK_IF_NEEDED != 0) flagsList.add("FLAG_ACTIVITY_RESET_TASK_IF_NEEDED") + if (flags and Intent.FLAG_ACTIVITY_LAUNCHED_FROM_HISTORY != 0) flagsList.add("FLAG_ACTIVITY_LAUNCHED_FROM_HISTORY") + if (flags and Intent.FLAG_ACTIVITY_FORWARD_RESULT != 0) flagsList.add("FLAG_ACTIVITY_FORWARD_RESULT") + if (flags and Intent.FLAG_ACTIVITY_PREVIOUS_IS_TOP != 0) flagsList.add("FLAG_ACTIVITY_PREVIOUS_IS_TOP") + if (flags and Intent.FLAG_GRANT_READ_URI_PERMISSION != 0) flagsList.add("FLAG_GRANT_READ_URI_PERMISSION") + if (flags and Intent.FLAG_GRANT_WRITE_URI_PERMISSION != 0) flagsList.add("FLAG_GRANT_WRITE_URI_PERMISSION") + + return if (flagsList.isEmpty()) "No flags set" else flagsList.joinToString(", ") +} + +fun Intent.debugPrint(tag: String = "INTENT_DEBUG") { + Log.d(tag, "===== Intent Debug Information =====") + + listOf( + "Action" to action, + "Data" to data, + "Type" to type, + "Component" to component, + "Package" to `package`, + "Flags" to "${Integer.toHexString(flags)} ${flagsToString(flags)}", + "Source Bounds" to sourceBounds + ).forEach { (key, value) -> + Log.d(tag, "$key: $value") + } + + categories?.forEach { category -> + Log.d(tag, "Category: $category") + } + + extras?.keySet()?.forEach { key -> + val value = extras?.get(key) + Log.d(tag, "Extra - $key: $value (${value?.javaClass?.simpleName})") + } + + Log.d(tag, "===== End Intent Debug =====") +} class MainActivity : FlutterActivity() { override fun onCreate(savedInstanceState: Bundle?) { @@ -55,6 +106,23 @@ class MainActivity : FlutterActivity() { result.notImplemented() // 没有实现的方法 } } + + MethodChannel( + flutterEngine?.dartExecutor!!.binaryMessenger, + "starLockFlutterPushCache" + ).setMethodCallHandler { call, result -> + if(call.method == "getPendingPush") { + val map = PushCache.popPush(this) + Log.i("PUSH_INTENT", "获取缓存的推送数据:$map") + result.success(map) + } else { + result.notImplemented() // 没有实现的方法 + } + Log.i("PUSH_INTENT", "怎么回事:$call") + } + + Log.i("PUSH_INTENT", "应用启动点击推送:$intent") + PushIntentHandler.handlePushIntent(this, intent) // 统一处理所有厂商推送点击数据 } fun shareText(text: String?, subject: String = "", imageUrl: String = "") { @@ -114,4 +182,72 @@ class MainActivity : FlutterActivity() { } } } + + // 处理应用已在运行时点击推送的情况 + override fun onNewIntent(intent: Intent) { + super.onNewIntent(intent) + setIntent(intent) + Log.i("PUSH_INTENT", "应用在运行点击推送:$intent") + val map = PushIntentHandler.handlePushIntent(this, intent) + try { + val flutterEngine: FlutterEngine? = this.flutterEngine + MethodChannel( + flutterEngine?.dartExecutor!!.binaryMessenger, + "starLockFlutterReceive" + ).invokeMethod("receivePush", map) + Log.i("PUSH_INTENT", "已主动发送 receivePush 到 Flutter:$map") + } catch (e: Exception) { + Log.e("PUSH_INTENT", "发送 receivePush 到 Flutter 失败: ${e.message}") + } + } +} +/// 推送数据缓存 +object PushCache { + + private const val SP_NAME = "push_cache_sp" + private const val KEY_PENDING_PUSH = "pending_push_json" + + /** 保存推送数据 */ + fun savePush(context: Context, data: Map) { + val sp = context.getSharedPreferences(SP_NAME, MODE_PRIVATE) + sp.edit().putString(KEY_PENDING_PUSH, JSONObject(data).toString()).apply() + } + + /** 取出并清空 */ + fun popPush(context: Context): Map? { + val sp = context.getSharedPreferences(SP_NAME, MODE_PRIVATE) + val text = sp.getString(KEY_PENDING_PUSH, null) ?: return null + + sp.edit().remove(KEY_PENDING_PUSH).apply() + + val json = JSONObject(text) + return json.keys().asSequence().associateWith { json.get(it) } + } +} + +/// 推送数据处理 +object PushIntentHandler { + + fun handlePushIntent(context: Context, intent: Intent?): Map? { + if (intent == null) return null + + intent.debugPrint() + Log.i("PUSH_INTENT", "原始推送数据:${intent.extras}") + val map = mutableMapOf() + val extras = intent.extras ?: return null + + val targetKeys = arrayOf("lockType", "eventNo", "lockId", + "imageUrl", "operateDate") + for (key in targetKeys) { + val value = extras.getString(key) + if (value != null) { + map[key] = value + Log.i("PushIntentHandler", "key=$key, value=$value") + } + } + + // 保存到 SharedPreferences + PushCache.savePush(context, map) + return map + } } diff --git a/lib/app_settings/app_settings.dart b/lib/app_settings/app_settings.dart index a79ebe66..e09cf7b2 100755 --- a/lib/app_settings/app_settings.dart +++ b/lib/app_settings/app_settings.dart @@ -5,7 +5,7 @@ import 'package:get/get.dart'; import 'package:star_lock/mine/about/debug/debug_console.dart'; class AppLog { - static bool _printLog = false; + static bool _printLog = true; static bool _onlyError = false; static void showLog({required bool printLog, bool? onlyError}) { @@ -14,6 +14,7 @@ class AppLog { } static void log(String msg, {StackTrace? stackTrace, bool? error}) { + debugPrint(msg); msg = '${DateTime.now().toIso8601String()} : $msg'; DebugConsole.info(msg, stackTrace: stackTrace, isErr: error ?? false); if (!kDebugMode) { diff --git a/lib/main.dart b/lib/main.dart index 6a3963db..7bb2a367 100755 --- a/lib/main.dart +++ b/lib/main.dart @@ -17,6 +17,7 @@ import 'package:star_lock/network/api_repository.dart'; import 'package:star_lock/network/start_chart_api.dart'; import 'package:star_lock/talk/starChart/handle/impl/debug_Info_model.dart'; import 'package:star_lock/talk/starChart/status/appLifecycle_observer.dart'; +import 'package:star_lock/tools/NativeInteractionTool.dart'; import 'package:star_lock/tools/baseGetXController.dart'; import 'package:star_lock/tools/bugly/bugly_tool.dart'; import 'package:star_lock/tools/callkit_handler.dart'; @@ -24,11 +25,13 @@ import 'package:star_lock/tools/device_info_service.dart'; import 'package:star_lock/tools/eventBusEventManage.dart'; import 'package:star_lock/tools/jverify_one_click_login.dart'; import 'package:star_lock/tools/platform_info_services.dart'; +import 'package:star_lock/tools/push/message_management.dart'; import 'package:star_lock/tools/push/notification_service.dart'; import 'package:star_lock/tools/push/xs_jPhush.dart'; import 'package:star_lock/tools/storage.dart'; import 'package:star_lock/translations/current_locale_tool.dart'; import 'package:star_lock/translations/trans_lib.dart'; +import 'package:star_lock/tools/remote_unlock_coordinator.dart'; import 'apm/apm_helper.dart'; import 'app.dart'; @@ -108,5 +111,15 @@ Future privacySDKInitialization() async { final XSJPushProvider jpushProvider = XSJPushProvider(); await jpushProvider.initJPushService(); NotificationService().init(); // 初始化通知服务 - + RemoteUnlockCoordinator.init(); + NativeInteractionTool().setupPushReceiver(); + final Map? push = + await NativeInteractionTool().getPendingPush(); + if(push != null) { + print('哈哈: $push'); + if(push.isNotEmpty) { + // do something + MessageManagement.shuntingBus(push); + } + } } diff --git a/lib/main/lockDetail/lockDetail/lockDetail_logic.dart b/lib/main/lockDetail/lockDetail/lockDetail_logic.dart index 432c2052..732e4e47 100755 --- a/lib/main/lockDetail/lockDetail/lockDetail_logic.dart +++ b/lib/main/lockDetail/lockDetail/lockDetail_logic.dart @@ -113,6 +113,50 @@ class LockDetailLogic extends BaseGetXController { }); } + // 初始化远程解锁请求监听器 + void initRemoteUnlockRequestListener() { + // 监听推送消息触发的远程解锁请求 + eventBus.on().listen((RemoteUnlockRequestEvent event) { + // 只处理与当前锁相关的远程解锁请求 + if (event.lockId == state.keyInfos.value.lockId) { + print('触发!!'); + showPushRemoteUnlockRequest(timeoutSeconds: event.timeoutSeconds ?? 60); + } + }); + } + + // 显示推送消息触发的远程解锁请求界面 + void showPushRemoteUnlockRequest({required int timeoutSeconds}) { + state.showPushRemoteUnlockRequest.value = true; + state.pushRemoteUnlockCountdownSeconds.value = timeoutSeconds; + + // 启动倒计时 + state.pushRemoteUnlockRequestTimer?.cancel(); + state.pushRemoteUnlockRequestTimer = Timer.periodic(const Duration(seconds: 1), (Timer timer) { + if (state.showPushRemoteUnlockRequest.value && state.pushRemoteUnlockCountdownSeconds.value > 0) { + state.pushRemoteUnlockCountdownSeconds.value--; + } else if (state.showPushRemoteUnlockRequest.value && state.pushRemoteUnlockCountdownSeconds.value <= 0) { + // 倒计时结束,自动拒绝远程解锁请求 + rejectPushRemoteUnlockRequest(); + } + }); + } + + // 接受推送消息触发的远程解锁请求 + void acceptPushRemoteUnlockRequest() { + state.pushRemoteUnlockRequestTimer?.cancel(); + state.showPushRemoteUnlockRequest.value = false; + state.pushRemoteUnlockCountdownSeconds.value = 60; + remoteOpenLock(); + } + + // 拒绝推送消息触发的远程解锁请求 + void rejectPushRemoteUnlockRequest() { + state.pushRemoteUnlockRequestTimer?.cancel(); + state.showPushRemoteUnlockRequest.value = false; + state.pushRemoteUnlockCountdownSeconds.value = 60; + } + // 开门数据解析 Future _replyOpenLock(Reply reply) async { final int status = reply.data[6]; diff --git a/lib/main/lockDetail/lockDetail/lockDetail_page.dart b/lib/main/lockDetail/lockDetail/lockDetail_page.dart index e7d2f9cc..b897ac78 100755 --- a/lib/main/lockDetail/lockDetail/lockDetail_page.dart +++ b/lib/main/lockDetail/lockDetail/lockDetail_page.dart @@ -1,4 +1,5 @@ import 'dart:async'; +import 'dart:math' show pi; import 'package:flutter/cupertino.dart'; import 'package:flutter/material.dart'; @@ -10,7 +11,6 @@ import 'package:star_lock/app_settings/app_colors.dart'; import 'package:star_lock/flavors.dart'; import 'package:star_lock/main/lockDetail/lockDetail/device_network_info.dart'; import 'package:star_lock/main/lockDetail/lockDetail/lockDetail_state.dart'; -import 'package:star_lock/main/lockDetail/lockSet/lockSet/lockSetInfo_entity.dart'; import 'package:star_lock/main/lockMian/lockMain/lockMain_logic.dart'; import 'package:star_lock/mine/gateway/addGateway/gatewayConfigurationWifi/getGatewayConfiguration_entity.dart'; import 'package:star_lock/network/api_repository.dart'; @@ -49,6 +49,7 @@ class _LockDetailPageState extends State with TickerProviderStat @override void initState() { + print("LockDetailPage initState"); state.animationController = AnimationController(duration: const Duration(seconds: 1), vsync: this); state.animationController?.repeat(); //动画开始、结束、向前移动或向后移动时会调用StatusListener @@ -70,6 +71,7 @@ class _LockDetailPageState extends State with TickerProviderStat _initRefreshLockDetailInfoDataEventAction(); logic.initReplySubscription(); logic.initLockSetOpenOrCloseCheckInRefreshLockDetailWithAttendanceAction(); + logic.initRemoteUnlockRequestListener(); // 初始化远程解锁请求监听器 logic.loadData(lockListInfoItemEntity: widget.lockListInfoItemEntity, isOnlyOneData: widget.isOnlyOneData); } @@ -94,7 +96,13 @@ class _LockDetailPageState extends State with TickerProviderStat @override Widget build(BuildContext context) { - return F.sw(skyCall: skWidget, xhjCall: xhjWidget); + return Obx(() { + final bool overlayVisible = state.showPushRemoteUnlockRequest.value; + return PopScope( + canPop: !overlayVisible, + child: F.sw(skyCall: skWidget, xhjCall: xhjWidget), + ); + }); } //鑫泓佳布局 @@ -512,14 +520,25 @@ class _LockDetailPageState extends State with TickerProviderStat ], ), ), - Visibility( - visible: state.iSClosedUnlockSuccessfulPopup.value, - child: Container( - width: 1.sw, - height: 1.sh - ScreenUtil().statusBarHeight * 2, - color: Colors.black.withOpacity(0.3), - child: _unlockSuccessWidget()), - ) + Obx(() => Visibility( + visible: state.iSClosedUnlockSuccessfulPopup.value, + child: Container( + width: 1.sw, + height: 1.sh - ScreenUtil().statusBarHeight * 2, + color: Colors.black.withOpacity(0.3), + child: _unlockSuccessWidget()), + )) + , + // 添加推送远程解锁请求界面 + Obx(() => Visibility( + visible: state.showPushRemoteUnlockRequest.value, + child: Container( + width: 1.sw, + height: 1.sh - ScreenUtil().statusBarHeight * 2, + color: Colors.black.withOpacity(0.3), + child: _pushRemoteUnlockRequestWidget(), + ), + )) ]), ], ); @@ -608,6 +627,7 @@ class _LockDetailPageState extends State with TickerProviderStat Icons.info, // 使用内置的 warning 图标,它是一个叹号 color: AppColors.mainColor, // 设置图标颜色为红色 size: 25.w, // 设置图标大小为 30 + ), ), SizedBox(width: 20.w), @@ -1325,6 +1345,93 @@ class _LockDetailPageState extends State with TickerProviderStat return formattedTime; } + // 推送消息触发的远程解锁请求界面 + Widget _pushRemoteUnlockRequestWidget() { + return Center( + child: Container( + width: 0.9.sw, + margin: EdgeInsets.symmetric(horizontal: 0.05.sw), + padding: EdgeInsets.symmetric(vertical: 30.h, horizontal: 20.w), + decoration: BoxDecoration( + color: Colors.white, + borderRadius: BorderRadius.circular(16.r), + ), + child: Column( + mainAxisSize: MainAxisSize.min, + children: [ + Text('远程开锁请求'.tr, style: TextStyle(fontSize: 28.sp, color: AppColors.blackColor)), + SizedBox(height: 20.h), + // 显示倒计时的虚线圆环动画 + SizedBox( + height: 200.r, + child: Stack( + alignment: Alignment.center, + children: [ + _pushRemoteUnlockCountdownAnimation(), + Obx(() => Text( + '${state.pushRemoteUnlockCountdownSeconds.value} s', + style: TextStyle(fontSize: 40.sp, color: AppColors.mainColor, fontWeight: FontWeight.w600), + )), + ], + ), + ), + SizedBox(height: 20.h), + // 同意和拒绝按钮 + Row( + mainAxisAlignment: MainAxisAlignment.center, + children: [ + GestureDetector( + onTap: () { + logic.rejectPushRemoteUnlockRequest(); + }, + child: Container( + width: 140.w, + height: 48.h, + alignment: Alignment.center, + decoration: BoxDecoration( + color: Colors.redAccent, + borderRadius: BorderRadius.circular(8.r), + ), + child: Text('拒绝'.tr, style: TextStyle(color: Colors.white, fontSize: 22.sp)), + ), + ), + SizedBox(width: 30.w), + GestureDetector( + onTap: () { + logic.acceptPushRemoteUnlockRequest(); + }, + child: Container( + width: 140.w, + height: 48.h, + alignment: Alignment.center, + decoration: BoxDecoration( + color: Colors.green,//AppColors.mainColor, + borderRadius: BorderRadius.circular(8.r), + ), + child: Text('同意'.tr, style: TextStyle(color: Colors.white, fontSize: 22.sp)), + ), + ), + ], + ) + ], + ), + ), + ); + } + + // 推送远程解锁倒计时动画 + Widget _pushRemoteUnlockCountdownAnimation() { + return Obx(() { + // 计算虚线圆环的可见部分(根据倒计时进度) + final double progress = state.pushRemoteUnlockCountdownSeconds.value / 60.0; + + return CustomPaint( + size: Size(168.r, 168.r), + painter: DottedCirclePainter(progress: progress), + ); + }); + } + //如果需要实名认证,需认证完成,方可开锁 Future isNeedRealNameAuthThenOpenLock() async { final bool isNetWork = await logic.isConnected() ?? false; @@ -1457,3 +1564,46 @@ class _LockDetailPageState extends State with TickerProviderStat BlueManage().disconnect(); } } + +// 推送远程解锁倒计时动画绘制器 +class DottedCirclePainter extends CustomPainter { // 0.0 to 1.0 + + DottedCirclePainter({required this.progress}); + final double progress; + + @override + void paint(Canvas canvas, Size size) { + final Paint paint = Paint() + ..color = AppColors.mainColor + ..strokeWidth = 4.w + ..style = PaintingStyle.stroke; + + final double radius = size.width / 2 - 2.w; + final Offset center = Offset(size.width / 2, size.height / 2); + + // 绘制虚线圆环(根据进度显示部分) + final Path path = Path(); + final double angle = -pi / 2 + 2 * pi * progress.clamp(0.0, 1.0); + + for (int i = 0; i <= 60; i++) { + final double startAngle = (2 * pi / 60) * i - pi / 2; + final double endAngle = startAngle + (2 * pi / 60) * 0.7; + + if (startAngle <= angle) { + final Path segmentPath = Path(); + segmentPath.arcTo( + Rect.fromCircle(center: center, radius: radius), + startAngle, + endAngle - startAngle, + true, + ); + path.addPath(segmentPath, Offset.zero); + } + } + + canvas.drawPath(path, paint); + } + + @override + bool shouldRepaint(covariant CustomPainter oldDelegate) => true; +} diff --git a/lib/main/lockDetail/lockDetail/lockDetail_state.dart b/lib/main/lockDetail/lockDetail/lockDetail_state.dart index eb4b6d30..60066f6a 100755 --- a/lib/main/lockDetail/lockDetail/lockDetail_state.dart +++ b/lib/main/lockDetail/lockDetail/lockDetail_state.dart @@ -80,4 +80,9 @@ class LockDetailState { List uploadRemoteControlDataList = [];// 上传遥控的数据 List uploadLockSetDataList = [];// 上传锁设置数据 -} + + // 远程开锁请求(推送消息触发) + RxBool showPushRemoteUnlockRequest = false.obs; // 是否显示推送远程开锁确认界面 + RxInt pushRemoteUnlockCountdownSeconds = 60.obs; // 推送远程开锁倒计时秒数 + Timer? pushRemoteUnlockRequestTimer; // 推送远程开锁倒计时定时器 +} \ No newline at end of file diff --git a/lib/starLockApplication/starLockApplication.dart b/lib/starLockApplication/starLockApplication.dart index 12d1fb10..fb635026 100755 --- a/lib/starLockApplication/starLockApplication.dart +++ b/lib/starLockApplication/starLockApplication.dart @@ -24,7 +24,6 @@ class _StarLockApplicationState extends State { @override void initState() { super.initState(); - } @override diff --git a/lib/tools/NativeInteractionTool.dart b/lib/tools/NativeInteractionTool.dart index 5326d2bf..8875f4a3 100755 --- a/lib/tools/NativeInteractionTool.dart +++ b/lib/tools/NativeInteractionTool.dart @@ -1,5 +1,6 @@ import 'package:flutter/services.dart'; import 'package:star_lock/flavors.dart'; +import 'package:star_lock/tools/push/message_management.dart'; import 'package:star_lock/tools/push/xs_jPhush.dart'; import '../app_settings/app_settings.dart'; @@ -8,12 +9,14 @@ import '../app_settings/app_settings.dart'; class NativeInteractionConfig { static String methodSendChannel = 'starLockFlutterSend'; static String receiveEventChannel = 'starLockFlutterReceive'; + static String methodPushChannel = 'starLockFlutterPushCache'; } ///原生交互flutter向原生发送消息 typedef BlockBlueStatus = void Function(String status); class NativeInteractionTool { + MethodChannel? _pushCacheChannel; MethodChannel sendChannel = MethodChannel(NativeInteractionConfig.methodSendChannel); MethodChannel receiveChannel = @@ -46,7 +49,6 @@ class NativeInteractionTool { // 获取设备蓝牙开启/关闭状态 final String message = call.arguments; blockBlueStatus(message); - // AppLog.log('收到原生发送的信息getBlueStatus: $message'); break; default: throw MissingPluginException(); @@ -54,6 +56,41 @@ class NativeInteractionTool { }); } + /// 初始化推送接收(前台/后台切入场景) + void setupPushReceiver() { + receiveChannel.setMethodCallHandler((MethodCall call) async { + switch (call.method) { + case 'receivePush': + final Map data = call.arguments; + try { + final Map push = Map.from(data); + print('收到原生 receivePush:$push'); + MessageManagement.shuntingBus(push); + } catch (e) { + print('NativeInteractionTool.setupPushReceiver 解析失败:$e'); + } + break; + case 'getBlueStatus': + break; + default: + throw MissingPluginException(); + } + }); + } + + /// 获取待处理的推送消息 + Future?> getPendingPush() async { + _pushCacheChannel ??= MethodChannel(NativeInteractionConfig.methodPushChannel); + print('进入getPendingPush'); + try { + final Map? data = await _pushCacheChannel!.invokeMethod('getPendingPush'); + return data != null ? Map.from(data) : null; + } catch (e) { + print("获取缓存推送消息失败: '${e.toString()}'."); + return null; + } + } + Future getBundleIdentifier() async { try { final String? bundleIdentifier = diff --git a/lib/tools/eventBusEventManage.dart b/lib/tools/eventBusEventManage.dart index 37511292..63fcd59a 100755 --- a/lib/tools/eventBusEventManage.dart +++ b/lib/tools/eventBusEventManage.dart @@ -86,6 +86,21 @@ class LockSetChangeSetRefreshLockDetailWithType { dynamic setResult; } +/// 远程开锁请求事件(门铃触发) +class RemoteUnlockRequestEvent { + RemoteUnlockRequestEvent({required this.lockId, this.timeoutSeconds = 60}); + + int lockId; + int timeoutSeconds; +} + +/// 推送原始数据事件 +class PushExtraEvent { + PushExtraEvent(this.data); + + Map data; +} + /// 获取到视频流数据然后刷新界面 class GetTVDataRefreshUI { GetTVDataRefreshUI(this.tvList); diff --git a/lib/tools/push/message_management.dart b/lib/tools/push/message_management.dart index 93f7c28d..e4d2cb5a 100644 --- a/lib/tools/push/message_management.dart +++ b/lib/tools/push/message_management.dart @@ -13,6 +13,7 @@ class MessageManagement { if (message.isEmpty) { return; } + print('MessageManagement.shunting呀: $message'); Map extra = {}; if (GetPlatform.isAndroid) { extra = _androidAnalysis(message); @@ -27,7 +28,7 @@ class MessageManagement { return; } AppLog.log(message.toString()); - _shuntingBus(extra); + shuntingBus(extra); } //android解析 @@ -74,8 +75,10 @@ class MessageManagement { } //识别参数分发消息 - static void _shuntingBus(Map data) { - final int eventNo = data['eventNo'] ?? -1; + static void shuntingBus(Map data) { + // final int eventNo = data['eventNo'] ?? -1; + final int eventNo = int.tryParse(data['eventNo']?.toString() ?? '') ?? -1; + eventBus.fire(PushExtraEvent(data)); switch (eventNo) { case MessageConstant.keyStateChange: final int keyId = data['keyId']; @@ -108,7 +111,14 @@ class MessageManagement { eventBus.fire(RefreshLockInfoDataEvent(keyId: keyId, lockId: lockId)); break; case MessageConstant.talkPushBigImage: - // XSJPushProvider().showCustomNotification(data); + print('MessageManagement._shuntingBus 收到远程开锁请求:$data'); + try { + final int lockId = int.tryParse(data['lockId']?.toString() ?? '') ?? -1; + // NotificationService().showTextNotification('远程开锁请求'.tr, '收到远程开锁请求'.tr); + eventBus.fire(RemoteUnlockRequestEvent(lockId: lockId, timeoutSeconds: 60)); + } catch (e) { + print('MessageManagement._shuntingBus 远程开锁请求异常:$e'); + } break; default: diff --git a/lib/tools/push/xs_jPhush.dart b/lib/tools/push/xs_jPhush.dart index 2763d2d0..0a474e54 100755 --- a/lib/tools/push/xs_jPhush.dart +++ b/lib/tools/push/xs_jPhush.dart @@ -4,6 +4,7 @@ import 'dart:ffi'; import 'dart:io'; import 'package:flutter/foundation.dart'; +import 'package:flutter/services.dart'; import 'package:get/get.dart'; import 'package:jpush_flutter/jpush_flutter.dart'; import 'package:star_lock/flavors.dart'; @@ -13,11 +14,15 @@ import 'package:star_lock/network/api_repository.dart'; import 'package:star_lock/tools/baseGetXController.dart'; import 'package:star_lock/tools/callkit_handler.dart'; import 'package:star_lock/tools/debounce_throttle_tool.dart'; +import 'package:star_lock/tools/push/message_constant.dart'; import 'package:star_lock/tools/push/message_management.dart'; import 'package:star_lock/tools/push/notification_service.dart'; import 'package:star_lock/tools/storage.dart'; +import 'package:star_lock/tools/showCupertinoAlertView.dart'; +import 'package:flutter_easyloading/flutter_easyloading.dart'; import '../../app_settings/app_settings.dart'; +import '../NativeInteractionTool.dart'; class XSJPushProvider { static const Map channelTypeMapping = { @@ -62,18 +67,18 @@ class XSJPushProvider { // final String? bundleIdentifier = // await NativeInteractionTool().getBundleIdentifier(); // print('bundleIdentifier: $bundleIdentifier'); + addJPushEventHandler(); jpush.setup( appKey: F.jPushKey, channel: 'flutter_channel', production: F.isProductionEnv, - debug: !F.isProductionEnv, + debug: true,//!F.isProductionEnv, ); jpush.setAuth(enable: true); jpush.applyPushAuthority( const NotificationSettingsIOS(sound: true, alert: true, badge: false), ); - addJPushEventHandler(); AppLog.log('JPush initialized.'); debugPrint("initJPushService end"); } @@ -117,7 +122,47 @@ class XSJPushProvider { }, onOpenNotification: (Map message) async { AppLog.log('onOpenNotification: $message'); - debugPrint("addJPushEventHandler onOpenNotification:$message"); + print("addJPushEventHandler onOpenNotification:$message"); + try { + await MessageManagement.shunting(message); + Map extra = {}; + if (GetPlatform.isAndroid) { + final Map extras = message['extras']; + final extraData = extras['cn.jpush.android.EXTRA']; + if (extraData is String) { + extra = json.decode(extraData); + } else if (extraData is Map) { + extra = {}; + extraData.forEach((key, value) { + extra[key.toString()] = value; + }); + } + } else if (GetPlatform.isIOS) { + final Map extras = message['extras']; + extras.forEach((Object? key, Object? value) { + extra[key!.toString()] = value; + }); + } + + final int eventNo = extra['eventNo'] ?? -1; + if (eventNo == MessageConstant.talkPushBigImage) { + final int lockId = extra['lockId'] ?? 0; + ShowCupertinoAlertView().isToRemoteUnLockCountdownAlert( + timeoutSeconds: 60, + onAccept: () async { + final entity = await ApiRepository.to.remoteOpenLock( + lockId: lockId.toString(), + timeOut: 60, + ); + if (entity.errorCode!.codeIsSuccessful) { + EasyLoading.showToast('已开锁'.tr); + } + }, + ); + } + } catch (e) { + AppLog.log('onOpenNotification handle error: $e'); + } }, onReceiveMessage: (Map message) async { AppLog.log('onReceiveMessage: $message'); diff --git a/lib/tools/remote_unlock_coordinator.dart b/lib/tools/remote_unlock_coordinator.dart new file mode 100644 index 00000000..ceda0496 --- /dev/null +++ b/lib/tools/remote_unlock_coordinator.dart @@ -0,0 +1,118 @@ +import 'dart:async'; +import 'package:get/get.dart'; +import 'package:star_lock/appRouters.dart'; +import 'package:star_lock/main/lockDetail/lockDetail/lockDetail_logic.dart'; +import 'package:star_lock/main/lockMian/entity/lockListInfo_entity.dart'; +import 'package:star_lock/network/api_repository.dart'; +import 'package:star_lock/tools/eventBusEventManage.dart'; +import 'package:star_lock/tools/push/message_constant.dart'; +import 'package:star_lock/tools/storage.dart'; + +class RemoteUnlockCoordinator { + factory RemoteUnlockCoordinator() => _instance; + RemoteUnlockCoordinator._(); + static final RemoteUnlockCoordinator _instance = RemoteUnlockCoordinator._(); + + StreamSubscription? _sub; + bool _inited = false; + + final List _pending = []; + + static void init() { + _instance._init(); + } + + void _init() { + if (_inited) return; + _inited = true; + _sub = eventBus.on().listen((PushExtraEvent evt) async { + final Map data = evt.data; + final int eventNo = int.tryParse(data['eventNo']?.toString() ?? '') ?? -1; + if (eventNo != MessageConstant.talkPushBigImage) { + return; + } + final int lockId = int.tryParse(data['lockId']?.toString() ?? '') ?? -1; + final RemoteUnlockRequestEvent event = RemoteUnlockRequestEvent(lockId: lockId, timeoutSeconds: 60); + if (Get.isRegistered()) { + final int currentLockId = Get.find().state.keyInfos.value.lockId ?? 0; + if (currentLockId == event.lockId) { + return; + } + } + if (Get.context == null) { + _pending.add(event); + _waitAppReadyAndFlush(); + return; + } + await _navigateToLockDetailAndRefire(event); + }); + } + + void _waitAppReadyAndFlush() { + Timer.periodic(const Duration(milliseconds: 100), (Timer t) async { + print('等待app启动中...${Get.context}'); + if (Get.context != null) { + t.cancel(); + for (final RemoteUnlockRequestEvent e in List.from(_pending)) { + await _navigateToLockDetailAndRefire(e); + print('导航到指定界面成功'); + } + _pending.clear(); + } + }); + } + + Future _navigateToLockDetailAndRefire(RemoteUnlockRequestEvent event) async { + final LockListInfoItemEntity? item = await _findLockItem(event.lockId); + print('导航item: $item'); + if (item == null) return; + + Get.toNamed(Routers.lockDetailMainPage, arguments: { + 'keyInfo': item, + 'isOnlyOneData': false, + }); + print('导航到了指定界面:${event.lockId}, ${event.timeoutSeconds}'); + + int tries = 0; + while (tries < 40) { + tries++; + if (Get.isRegistered()) { + final int currentLockId = Get.find().state.keyInfos.value.lockId ?? 0; + if (currentLockId == event.lockId) { + break; + } + } + await Future.delayed(const Duration(milliseconds: 50)); + } + + eventBus.fire(RemoteUnlockRequestEvent(lockId: event.lockId, timeoutSeconds: event.timeoutSeconds)); + } + + Future _findLockItem(int lockId) async { + final LockListInfoGroupEntity? stored = await Storage.getLockMainListData(); + LockListInfoItemEntity? item; + if (stored != null) { + for (final GroupList g in stored.groupList ?? []) { + for (final LockListInfoItemEntity l in g.lockList ?? []) { + if ((l.lockId ?? 0) == lockId) { + item = l; + break; + } + } + if (item != null) break; + } + } + if (item != null) return item; + + final LockListInfoEntity res = await ApiRepository.to.getStarLockListInfo(pageNo: 1, pageSize: 50, isUnShowLoading: true); + for (final GroupList g in res.data?.groupList ?? []) { + for (final LockListInfoItemEntity l in g.lockList ?? []) { + if ((l.lockId ?? 0) == lockId) { + return l; + } + } + } + return null; + } +} + diff --git a/lib/tools/showCupertinoAlertView.dart b/lib/tools/showCupertinoAlertView.dart index cf585bf1..097652bf 100755 --- a/lib/tools/showCupertinoAlertView.dart +++ b/lib/tools/showCupertinoAlertView.dart @@ -1,3 +1,4 @@ +import 'dart:async'; import 'dart:typed_data'; import 'dart:ui'; @@ -323,6 +324,66 @@ class ShowCupertinoAlertView { ); } + void isToRemoteUnLockCountdownAlert({required int timeoutSeconds, required Function onAccept}) { + int seconds = timeoutSeconds; + Timer? timer; + showCupertinoDialog( + context: Get.context!, + builder: (BuildContext context) { + return StatefulBuilder(builder: (context, setState) { + timer ??= Timer.periodic(const Duration(seconds: 1), (Timer t) { + if (seconds <= 1) { + t.cancel(); + timer = null; + Get.back(); + } else { + seconds = seconds - 1; + setState(() {}); + } + }); + return CupertinoAlertDialog( + title: Container(), + content: Column( + children: [ + Text('远程开锁请求'.tr), + SizedBox(height: 10.h), + Text('${seconds} s', style: TextStyle(fontSize: 26.sp, color: AppColors.mainColor)), + ], + ), + actions: [ + CupertinoDialogAction( + onPressed: () { + timer?.cancel(); + timer = null; + Get.back(); + }, + child: Text( + '拒绝'.tr, + style: TextStyle(color: AppColors.mainColor), + ), + ), + CupertinoDialogAction( + onPressed: () async { + timer?.cancel(); + timer = null; + await onAccept(); + Get.back(); + }, + child: Text( + '同意'.tr, + style: TextStyle(color: AppColors.mainColor), + ), + ), + ], + ); + }); + }, + ).whenComplete(() { + timer?.cancel(); + timer = null; + }); + } + // 购买按钮 void showBuyTipWithContentAlert( {required String titleStr, required Function sureClick}) { diff --git a/pubspec.yaml b/pubspec.yaml index bf10df09..ac874a48 100755 --- a/pubspec.yaml +++ b/pubspec.yaml @@ -218,7 +218,7 @@ dependencies: jpush_flutter: git: - url: git@code.star-lock.cn:StarlockTeam/jpush_flutter.git + url: https://code.skychip.top/sky/jpush_flutter.git ref: 656df9ee91b1ec8b96aa1208a6b0df27a4516067 #视频播放器 From 2619288234dbac2f6f193e70de605ce62e081b32 Mon Sep 17 00:00:00 2001 From: Xie Jing Date: Mon, 15 Dec 2025 18:05:27 +0800 Subject: [PATCH 2/2] =?UTF-8?q?=E5=AE=9E=E7=8E=B0=E7=9B=B4=E6=8E=A5?= =?UTF-8?q?=E4=BB=8E=E5=AE=89=E5=8D=93=E5=8E=9F=E7=94=9F=E8=8E=B7=E5=8F=96?= =?UTF-8?q?=E7=82=B9=E5=87=BB=E6=8E=A8=E9=80=81=E4=BA=8B=E4=BB=B6=E5=92=8C?= =?UTF-8?q?=E5=8F=82=E6=95=B0=EF=BC=8C=E4=BC=A0=E5=85=A5flutter=E5=A4=84?= =?UTF-8?q?=E7=90=86=E6=8E=A8=E9=80=81=E3=80=82=20=E5=AE=9E=E7=8E=B0?= =?UTF-8?q?=E4=BA=86=E8=BF=9C=E7=A8=8B=E5=BC=80=E9=94=81=E8=AF=B7=E6=B1=82?= =?UTF-8?q?=E5=8A=9F=E8=83=BD=E7=9A=84ui=E5=92=8C=E5=BC=80=E9=94=81=20?= =?UTF-8?q?=E6=B5=8B=E8=AF=95=E4=BA=86vivo=E5=92=8C=E5=B0=8F=E7=B1=B3?= =?UTF-8?q?=E6=89=8B=E6=9C=BA=E6=94=AF=E6=8C=81=E4=B8=8A=E8=BF=B0=E5=8A=9F?= =?UTF-8?q?=E8=83=BD=EF=BC=8C=E4=B8=89=E6=98=9F=E3=80=81=E8=8D=A3=E8=80=80?= =?UTF-8?q?=E4=B8=8D=E6=94=AF=E6=8C=81=EF=BC=8C=E5=85=B6=E4=BB=96=E6=9C=AA?= =?UTF-8?q?=E6=B5=8B=E8=AF=95=E3=80=82=20=E4=BF=AE=E6=94=B9=E4=BA=86?= =?UTF-8?q?=E6=89=93=E5=8D=B0=E8=B0=83=E8=AF=95=E4=BF=A1=E6=81=AF=EF=BC=8C?= =?UTF-8?q?=E5=90=8E=E7=BB=AD=E9=9C=80=E8=A6=81=E6=81=A2=E5=A4=8D=E5=8E=9F?= =?UTF-8?q?=E7=8A=B6?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../kotlin/com/skychip/lock/MainActivity.kt | 187 +++++++++++++- .../lockDetail/lockDetail_logic.dart | 17 +- .../lockDetail/lockDetail_page.dart | 237 ++++++------------ .../lockDetail/lockDetail_state.dart | 4 +- lib/tools/eventBusEventManage.dart | 3 +- lib/tools/push/message_management.dart | 16 +- lib/tools/remote_unlock_coordinator.dart | 80 ++++-- 7 files changed, 339 insertions(+), 205 deletions(-) diff --git a/android/app/src/main/kotlin/com/skychip/lock/MainActivity.kt b/android/app/src/main/kotlin/com/skychip/lock/MainActivity.kt index 2ec29f6a..d49415a7 100755 --- a/android/app/src/main/kotlin/com/skychip/lock/MainActivity.kt +++ b/android/app/src/main/kotlin/com/skychip/lock/MainActivity.kt @@ -14,6 +14,7 @@ import android.bluetooth.BluetoothAdapter; import androidx.core.content.FileProvider import java.io.File import org.json.JSONObject +import org.json.JSONArray private fun flagsToString(flags: Int): String { val flagsList = mutableListOf() @@ -228,26 +229,190 @@ object PushCache { /// 推送数据处理 object PushIntentHandler { + private const val PARSE_TAG = "PUSH_PARSE" + + private val CANONICAL_KEY_MAP = mapOf( + "eventno" to "EventNo", + "imageurl" to "ImageUrl", + "lockid" to "LockId", + "operatedate" to "OperateDate", + "locktype" to "LockType" + ) + + private val LEGACY_KEY_MAP = mapOf( + "eventno" to "eventNo", + "imageurl" to "imageUrl", + "lockid" to "lockId", + "operatedate" to "operateDate", + "locktype" to "lockType" + ) + + private val EMBEDDED_JSON_PATTERN = Regex("\\$\\{(.*?)\\}\\$") + + private fun looksLikeJsonObject(s: String): Boolean = s.isNotEmpty() && s.trim().let { it.startsWith("{") && it.endsWith("}") } + private fun looksLikeJsonArray(s: String): Boolean = s.isNotEmpty() && s.trim().let { it.startsWith("[") && it.endsWith("]") } + + private fun extractEmbeddedJson(text: String): Map { + val results = HashMap() + val matches = EMBEDDED_JSON_PATTERN.findAll(text) + for (match in matches) { + val content = match.groupValues[1] + // 构造合法的 JSON:将提取内容包裹在 {} 中,并处理转义字符 + // 注意:小米推送的格式看起来是 "key":value,且包含转义的引号 \" + // 我们尝试将其还原为标准 JSON 字符串 + val jsonString = "{$content}" + .replace("\\\"", "\"") + .replace("\\/", "/") + + try { + val jsonObject = JSONObject(jsonString) + val keys = jsonObject.keys() + while (keys.hasNext()) { + val key = keys.next() + val value = jsonObject.opt(key) + if (value != null) { + results[key.lowercase()] = value.toString() + } + } + } catch (e: Exception) { + Log.w(PARSE_TAG, "尝试解析嵌入 JSON 失败: $jsonString", e) + } + } + return results + } + + private fun flattenExtras(extras: android.os.Bundle): Map { + val out = HashMap(8) + val stack = ArrayDeque() + stack.add(extras) + + while (stack.isNotEmpty()) { + when (val item = stack.removeLast()) { + is android.os.Bundle -> { + for (key in item.keySet()) { + val v = item.get(key) + if (v == null) continue + when (v) { + is android.os.Bundle -> stack.add(v) + is Map<*, *> -> stack.add(v) + is JSONObject -> stack.add(v) + is JSONArray -> stack.add(v) + is String -> { + val s = v + val ls = key.lowercase() + if (looksLikeJsonObject(s)) { + try { stack.add(JSONObject(s)) } catch (_: Exception) {} + } else if (looksLikeJsonArray(s)) { + try { stack.add(JSONArray(s)) } catch (_: Exception) {} + } + out[ls] = s + } + else -> { + out[key.lowercase()] = v.toString() + } + } + } + } + is Map<*, *> -> { + for ((mk, mv) in item.entries) { + val k = mk?.toString() ?: continue + if (mv == null) continue + when (mv) { + is android.os.Bundle -> stack.add(mv) + is Map<*, *> -> stack.add(mv) + is JSONObject -> stack.add(mv) + is JSONArray -> stack.add(mv) + is String -> { + val s = mv + val ls = k.lowercase() + if (looksLikeJsonObject(s)) { + try { stack.add(JSONObject(s)) } catch (_: Exception) {} + } else if (looksLikeJsonArray(s)) { + try { stack.add(JSONArray(s)) } catch (_: Exception) {} + } + out[ls] = s + } + else -> { + out[k.lowercase()] = mv.toString() + } + } + } + } + is JSONObject -> { + val obj = item + val keys = obj.keys() + while (keys.hasNext()) { + val k = keys.next().toString() + val v = obj.opt(k) + if (v == null) continue + when (v) { + is JSONObject -> stack.add(v) + is JSONArray -> stack.add(v) + is String -> out[k.lowercase()] = v + else -> out[k.lowercase()] = v.toString() + } + } + } + is JSONArray -> { + val arr = item + for (i in 0 until arr.length()) { + val e = arr.opt(i) + when (e) { + is JSONObject -> stack.add(e) + is JSONArray -> stack.add(e) + } + } + } + } + } + return out + } + fun handlePushIntent(context: Context, intent: Intent?): Map? { if (intent == null) return null intent.debugPrint() Log.i("PUSH_INTENT", "原始推送数据:${intent.extras}") - val map = mutableMapOf() val extras = intent.extras ?: return null - val targetKeys = arrayOf("lockType", "eventNo", "lockId", - "imageUrl", "operateDate") - for (key in targetKeys) { - val value = extras.getString(key) - if (value != null) { - map[key] = value - Log.i("PushIntentHandler", "key=$key, value=$value") + Log.i(PARSE_TAG, "开始解析 extras") + val flat = flattenExtras(extras).toMutableMap() + + // 二次扫描:尝试从所有字符串值中提取嵌入的 JSON(如小米推送格式) + val embeddedFields = HashMap() + for (value in flat.values) { + val extracted = extractEmbeddedJson(value) + if (extracted.isNotEmpty()) { + embeddedFields.putAll(extracted) + } + } + if (embeddedFields.isNotEmpty()) { + Log.i(PARSE_TAG, "提取到嵌入字段:$embeddedFields") + flat.putAll(embeddedFields) + } + + Log.i(PARSE_TAG, "扁平化后:${JSONObject(flat as Map<*, *>)}") + + val legacyMap = mutableMapOf() + for ((lowerKey, legacyKey) in LEGACY_KEY_MAP) { + val v = flat[lowerKey] + if (v != null) { + legacyMap[legacyKey] = v + Log.i("PushIntentHandler", "key=$legacyKey, value=$v") } } - // 保存到 SharedPreferences - PushCache.savePush(context, map) - return map + val canonicalMap = mutableMapOf() + for ((lowerKey, canonicalKey) in CANONICAL_KEY_MAP) { + val v = flat[lowerKey] + if (v != null) { + canonicalMap[canonicalKey] = v + } + } + val finalJson = JSONObject(canonicalMap as Map<*, *>) + Log.i(PARSE_TAG, "最终生成 JSON:$finalJson") + + PushCache.savePush(context, legacyMap) + return legacyMap } } diff --git a/lib/main/lockDetail/lockDetail/lockDetail_logic.dart b/lib/main/lockDetail/lockDetail/lockDetail_logic.dart index 732e4e47..7561cb08 100755 --- a/lib/main/lockDetail/lockDetail/lockDetail_logic.dart +++ b/lib/main/lockDetail/lockDetail/lockDetail_logic.dart @@ -117,10 +117,15 @@ class LockDetailLogic extends BaseGetXController { void initRemoteUnlockRequestListener() { // 监听推送消息触发的远程解锁请求 eventBus.on().listen((RemoteUnlockRequestEvent event) { - // 只处理与当前锁相关的远程解锁请求 if (event.lockId == state.keyInfos.value.lockId) { - print('触发!!'); - showPushRemoteUnlockRequest(timeoutSeconds: event.timeoutSeconds ?? 60); + // 去重:如果该operateDate已处理过,则忽略 + if (event.operateDate != 0 && event.operateDate == state.handledPushOperateDate) { + return; + } + state.currentPushOperateDate = event.operateDate; + SchedulerBinding.instance.addPostFrameCallback((_) { + showPushRemoteUnlockRequest(timeoutSeconds: event.timeoutSeconds ?? 60); + }); } }); } @@ -147,6 +152,8 @@ class LockDetailLogic extends BaseGetXController { state.pushRemoteUnlockRequestTimer?.cancel(); state.showPushRemoteUnlockRequest.value = false; state.pushRemoteUnlockCountdownSeconds.value = 60; + state.handledPushOperateDate = state.currentPushOperateDate; + state.currentPushOperateDate = 0; remoteOpenLock(); } @@ -155,6 +162,8 @@ class LockDetailLogic extends BaseGetXController { state.pushRemoteUnlockRequestTimer?.cancel(); state.showPushRemoteUnlockRequest.value = false; state.pushRemoteUnlockCountdownSeconds.value = 60; + state.handledPushOperateDate = state.currentPushOperateDate; + state.currentPushOperateDate = 0; } // 开门数据解析 @@ -167,7 +176,7 @@ class LockDetailLogic extends BaseGetXController { final String getMobile = (await Storage.getMobile())!; ApmHelper.instance.trackEvent('open_lock', { 'lock_name': state.keyInfos.value.lockName!, - 'account': getMobile.isNotEmpty ? getMobile : (await Storage.getEmail())!, + 'account': getMobile.isNotEmpty ? getMobile : (await Storage.getEmail())!, 'date': DateTool().getNowDateWithType(1), 'open_lock_result': '${reply.data}', }); diff --git a/lib/main/lockDetail/lockDetail/lockDetail_page.dart b/lib/main/lockDetail/lockDetail/lockDetail_page.dart index b897ac78..49ed39fa 100755 --- a/lib/main/lockDetail/lockDetail/lockDetail_page.dart +++ b/lib/main/lockDetail/lockDetail/lockDetail_page.dart @@ -30,6 +30,7 @@ import '../../../common/XSConstantMacro/XSConstantMacro.dart'; import '../../../tools/appRouteObserver.dart'; import '../../../tools/dateTool.dart'; import '../../../tools/eventBusEventManage.dart'; +import '../../../tools/remote_unlock_coordinator.dart'; import '../../lockMian/entity/lockListInfo_entity.dart'; import 'lockDetail_logic.dart'; @@ -96,13 +97,7 @@ class _LockDetailPageState extends State with TickerProviderStat @override Widget build(BuildContext context) { - return Obx(() { - final bool overlayVisible = state.showPushRemoteUnlockRequest.value; - return PopScope( - canPop: !overlayVisible, - child: F.sw(skyCall: skWidget, xhjCall: xhjWidget), - ); - }); + return F.sw(skyCall: skWidget, xhjCall: xhjWidget); } //鑫泓佳布局 @@ -529,16 +524,7 @@ class _LockDetailPageState extends State with TickerProviderStat child: _unlockSuccessWidget()), )) , - // 添加推送远程解锁请求界面 - Obx(() => Visibility( - visible: state.showPushRemoteUnlockRequest.value, - child: Container( - width: 1.sw, - height: 1.sh - ScreenUtil().statusBarHeight * 2, - color: Colors.black.withOpacity(0.3), - child: _pushRemoteUnlockRequestWidget(), - ), - )) + SizedBox.shrink() ]), ], ); @@ -649,69 +635,68 @@ class _LockDetailPageState extends State with TickerProviderStat child: Stack( children: [ Center( - child: GestureDetector( - onTap: () { - if (state.openDoorBtnisUneable.value == true) { - logic.functionBlocker.block(isNeedRealNameAuthThenOpenLock); - } - }, - onLongPressStart: (LongPressStartDetails details) { - if (state.openDoorBtnisUneable.value == true) { - void callback() { - setState(startUnLock); + child: GestureDetector( + onTap: () { + if (state.openDoorBtnisUneable.value == true) { + logic.functionBlocker.block(isNeedRealNameAuthThenOpenLock); } - - logic.functionBlocker.block(callback); - } - }, - child: Stack( - children: [ - FlavorsImg( - child: Image.asset( - state.openDoorBtnisUneable.value == false - ? 'images/main/icon_main_openLockBtn_grey.png' - : (state.isOpenPassageMode.value == 1 - ? 'images/main/icon_main_normallyOpenMode_center.png' - : 'images/main/icon_main_openLockBtn_center.png'), - width: 330.w, - height: 330.w, - // color: AppColors.primaryTopColor, - ), - ), - if (state.openDoorBtnisUneable.value == false) - Positioned( - child: FlavorsImg( - child: Image.asset( - 'images/main/icon_main_openLockBtn_grey.png', - width: 330.w, - height: 330.w, - ), + }, + onLongPressStart: (LongPressStartDetails details) { + if (state.openDoorBtnisUneable.value == true) { + void callback() { + setState(startUnLock); + } + logic.functionBlocker.block(callback); + } + }, + child: Stack( + children: [ + FlavorsImg( + child: Image.asset( + state.openDoorBtnisUneable.value == false + ? 'images/main/icon_main_openLockBtn_grey.png' + : (state.isOpenPassageMode.value == 1 + ? 'images/main/icon_main_normallyOpenMode_center.png' + : 'images/main/icon_main_openLockBtn_center.png'), + width: 330.w, + height: 330.w, ), - ) - else - state.openLockBtnState.value == 1 - ? buildRotationTransition( + ), + if (state.openDoorBtnisUneable.value == false) + Positioned( + child: FlavorsImg( + child: Image.asset( + 'images/main/icon_main_openLockBtn_grey.png', width: 330.w, height: 330.w, - ) - : Positioned( - child: FlavorsImg( - child: Image.asset( - state.isOpenPassageMode.value == 1 - ? 'images/main/icon_main_normallyOpenMode_circle.png' - : 'images/main/icon_main_openLockBtn_circle.png', + ), + ), + ) + else + state.openLockBtnState.value == 1 + ? buildRotationTransition( width: 330.w, height: 330.w, - ), - )), - ], + ) + : Positioned( + child: FlavorsImg( + child: Image.asset( + state.isOpenPassageMode.value == 1 + ? 'images/main/icon_main_normallyOpenMode_circle.png' + : 'images/main/icon_main_openLockBtn_circle.png', + width: 330.w, + height: 330.w, + ), + )), + ], + ), ), - )), + ), Positioned( right: 90.w, bottom: 1, child: Obx(() => Visibility( - visible: state.keyInfos.value.lockSetting!.remoteUnlock == 1, + visible: state.keyInfos.value.lockSetting!.remoteUnlock == 1 && !state.showPushRemoteUnlockRequest.value, child: GestureDetector( onTap: () { ShowCupertinoAlertView().isToRemoteUnLockAlert(remoteUnlockAction: () { @@ -738,17 +723,20 @@ class _LockDetailPageState extends State with TickerProviderStat SizedBox( height: 30.h, ), - Container( - padding: EdgeInsets.symmetric(horizontal: 20.w), - child: Center( - child: Text( - logic.getKeyStatusTextAndShow(), - maxLines: 2, - textAlign: TextAlign.center, - style: TextStyle(fontSize: 22.sp, color: AppColors.btnDisableColor, fontWeight: FontWeight.w500), - ), - ), - ), + Obx(() => Visibility( + visible: !state.showPushRemoteUnlockRequest.value, + child: Container( + padding: EdgeInsets.symmetric(horizontal: 20.w), + child: Center( + child: Text( + logic.getKeyStatusTextAndShow(), + maxLines: 2, + textAlign: TextAlign.center, + style: TextStyle(fontSize: 22.sp, color: AppColors.btnDisableColor, fontWeight: FontWeight.w500), + ), + ), + ), + )), SizedBox( height: 30.h, ), @@ -1345,80 +1333,6 @@ class _LockDetailPageState extends State with TickerProviderStat return formattedTime; } - // 推送消息触发的远程解锁请求界面 - Widget _pushRemoteUnlockRequestWidget() { - return Center( - child: Container( - width: 0.9.sw, - margin: EdgeInsets.symmetric(horizontal: 0.05.sw), - padding: EdgeInsets.symmetric(vertical: 30.h, horizontal: 20.w), - decoration: BoxDecoration( - color: Colors.white, - borderRadius: BorderRadius.circular(16.r), - ), - child: Column( - mainAxisSize: MainAxisSize.min, - children: [ - Text('远程开锁请求'.tr, style: TextStyle(fontSize: 28.sp, color: AppColors.blackColor)), - SizedBox(height: 20.h), - // 显示倒计时的虚线圆环动画 - SizedBox( - height: 200.r, - child: Stack( - alignment: Alignment.center, - children: [ - _pushRemoteUnlockCountdownAnimation(), - Obx(() => Text( - '${state.pushRemoteUnlockCountdownSeconds.value} s', - style: TextStyle(fontSize: 40.sp, color: AppColors.mainColor, fontWeight: FontWeight.w600), - )), - ], - ), - ), - SizedBox(height: 20.h), - // 同意和拒绝按钮 - Row( - mainAxisAlignment: MainAxisAlignment.center, - children: [ - GestureDetector( - onTap: () { - logic.rejectPushRemoteUnlockRequest(); - }, - child: Container( - width: 140.w, - height: 48.h, - alignment: Alignment.center, - decoration: BoxDecoration( - color: Colors.redAccent, - borderRadius: BorderRadius.circular(8.r), - ), - child: Text('拒绝'.tr, style: TextStyle(color: Colors.white, fontSize: 22.sp)), - ), - ), - SizedBox(width: 30.w), - GestureDetector( - onTap: () { - logic.acceptPushRemoteUnlockRequest(); - }, - child: Container( - width: 140.w, - height: 48.h, - alignment: Alignment.center, - decoration: BoxDecoration( - color: Colors.green,//AppColors.mainColor, - borderRadius: BorderRadius.circular(8.r), - ), - child: Text('同意'.tr, style: TextStyle(color: Colors.white, fontSize: 22.sp)), - ), - ), - ], - ) - ], - ), - ), - ); - } - // 推送远程解锁倒计时动画 Widget _pushRemoteUnlockCountdownAnimation() { return Obx(() { @@ -1432,6 +1346,16 @@ class _LockDetailPageState extends State with TickerProviderStat }); } + Widget _pushRemoteUnlockCountdownAnimationLarge() { + return Obx(() { + final double progress = state.pushRemoteUnlockCountdownSeconds.value / 60.0; + return CustomPaint( + size: Size(330.w, 330.w), + painter: DottedCirclePainter(progress: progress), + ); + }); + } + //如果需要实名认证,需认证完成,方可开锁 Future isNeedRealNameAuthThenOpenLock() async { final bool isNetWork = await logic.isConnected() ?? false; @@ -1546,6 +1470,11 @@ class _LockDetailPageState extends State with TickerProviderStat logic.cancelBlueConnetctToastTimer(); BlueManage().disconnect(); state.openLockBtnState.value = 0; + state.pushRemoteUnlockRequestTimer?.cancel(); + state.showPushRemoteUnlockRequest.value = false; + state.pushRemoteUnlockCountdownSeconds.value = 60; + state.handledPushOperateDate = state.currentPushOperateDate; + state.currentPushOperateDate = 0; } /// 从下级返回 当前界面即将出现 diff --git a/lib/main/lockDetail/lockDetail/lockDetail_state.dart b/lib/main/lockDetail/lockDetail/lockDetail_state.dart index 60066f6a..da67a7b3 100755 --- a/lib/main/lockDetail/lockDetail/lockDetail_state.dart +++ b/lib/main/lockDetail/lockDetail/lockDetail_state.dart @@ -85,4 +85,6 @@ class LockDetailState { RxBool showPushRemoteUnlockRequest = false.obs; // 是否显示推送远程开锁确认界面 RxInt pushRemoteUnlockCountdownSeconds = 60.obs; // 推送远程开锁倒计时秒数 Timer? pushRemoteUnlockRequestTimer; // 推送远程开锁倒计时定时器 -} \ No newline at end of file + int currentPushOperateDate = 0; // 当前推送请求操作时间戳 + int handledPushOperateDate = 0; // 已处理的推送请求操作时间戳,避免重复显示 +} diff --git a/lib/tools/eventBusEventManage.dart b/lib/tools/eventBusEventManage.dart index 63fcd59a..8a1395ec 100755 --- a/lib/tools/eventBusEventManage.dart +++ b/lib/tools/eventBusEventManage.dart @@ -88,10 +88,11 @@ class LockSetChangeSetRefreshLockDetailWithType { /// 远程开锁请求事件(门铃触发) class RemoteUnlockRequestEvent { - RemoteUnlockRequestEvent({required this.lockId, this.timeoutSeconds = 60}); + RemoteUnlockRequestEvent({required this.lockId, this.timeoutSeconds = 60, this.operateDate = 0}); int lockId; int timeoutSeconds; + int operateDate; } /// 推送原始数据事件 diff --git a/lib/tools/push/message_management.dart b/lib/tools/push/message_management.dart index e4d2cb5a..ed435a59 100644 --- a/lib/tools/push/message_management.dart +++ b/lib/tools/push/message_management.dart @@ -17,7 +17,7 @@ class MessageManagement { Map extra = {}; if (GetPlatform.isAndroid) { extra = _androidAnalysis(message); - // AppLog.log('MessageManagement.shunting GetPlatform.isAndroid: $extra'); + AppLog.log('MessageManagement.shunting GetPlatform.isAndroid: $extra'); } else if (GetPlatform.isIOS) { extra = _iosAnalysis(message); // AppLog.log('MessageManagement.shunting GetPlatform.isIos: $extra'); @@ -112,12 +112,14 @@ class MessageManagement { break; case MessageConstant.talkPushBigImage: print('MessageManagement._shuntingBus 收到远程开锁请求:$data'); - try { - final int lockId = int.tryParse(data['lockId']?.toString() ?? '') ?? -1; - // NotificationService().showTextNotification('远程开锁请求'.tr, '收到远程开锁请求'.tr); - eventBus.fire(RemoteUnlockRequestEvent(lockId: lockId, timeoutSeconds: 60)); - } catch (e) { - print('MessageManagement._shuntingBus 远程开锁请求异常:$e'); + final int lockType = int.tryParse(data['lockType']?.toString() ?? '') ?? -1; + if (lockType == 1) { // 半自动锁 + try { + final int lockId = int.tryParse(data['lockId']?.toString() ?? '') ?? -1; + eventBus.fire(RemoteUnlockRequestEvent(lockId: lockId, timeoutSeconds: 60)); + } catch (e) { + print('MessageManagement._shuntingBus 远程开锁请求异常:$e'); + } } break; diff --git a/lib/tools/remote_unlock_coordinator.dart b/lib/tools/remote_unlock_coordinator.dart index ceda0496..8cf433e5 100644 --- a/lib/tools/remote_unlock_coordinator.dart +++ b/lib/tools/remote_unlock_coordinator.dart @@ -1,5 +1,7 @@ import 'dart:async'; import 'package:get/get.dart'; +import 'package:flutter/scheduler.dart'; +import 'package:get/get.dart'; import 'package:star_lock/appRouters.dart'; import 'package:star_lock/main/lockDetail/lockDetail/lockDetail_logic.dart'; import 'package:star_lock/main/lockMian/entity/lockListInfo_entity.dart'; @@ -7,6 +9,7 @@ import 'package:star_lock/network/api_repository.dart'; import 'package:star_lock/tools/eventBusEventManage.dart'; import 'package:star_lock/tools/push/message_constant.dart'; import 'package:star_lock/tools/storage.dart'; +import 'package:star_lock/tools/remote_unlock_overlay.dart'; class RemoteUnlockCoordinator { factory RemoteUnlockCoordinator() => _instance; @@ -17,6 +20,9 @@ class RemoteUnlockCoordinator { bool _inited = false; final List _pending = []; + String? _previousRoute; + dynamic _previousArgs; + bool get hasPrevious => _previousRoute != null; static void init() { _instance._init(); @@ -31,14 +37,22 @@ class RemoteUnlockCoordinator { if (eventNo != MessageConstant.talkPushBigImage) { return; } - final int lockId = int.tryParse(data['lockId']?.toString() ?? '') ?? -1; - final RemoteUnlockRequestEvent event = RemoteUnlockRequestEvent(lockId: lockId, timeoutSeconds: 60); - if (Get.isRegistered()) { - final int currentLockId = Get.find().state.keyInfos.value.lockId ?? 0; - if (currentLockId == event.lockId) { - return; - } - } + final int lockId = int.tryParse( + data['lockId']?.toString() ?? '', + ) ?? -1; + final int operateDate = int.tryParse( + data['operateDate']?.toString() ?? '', + ) ?? 0; + final int currentDate = DateTime.now().millisecondsSinceEpoch; + final int timeoutSeconds = (60 - (currentDate - operateDate) ~/ 1000) < 0 + ? 0 + : (60 - (currentDate - operateDate) ~/ 1000); + final RemoteUnlockRequestEvent event = RemoteUnlockRequestEvent( + lockId: lockId, + timeoutSeconds: timeoutSeconds, + operateDate: operateDate, + ); + // 无论当前是否在目标锁详情页,都显示覆盖层以保证体验一致 if (Get.context == null) { _pending.add(event); _waitAppReadyAndFlush(); @@ -65,27 +79,39 @@ class RemoteUnlockCoordinator { Future _navigateToLockDetailAndRefire(RemoteUnlockRequestEvent event) async { final LockListInfoItemEntity? item = await _findLockItem(event.lockId); print('导航item: $item'); - if (item == null) return; - - Get.toNamed(Routers.lockDetailMainPage, arguments: { - 'keyInfo': item, - 'isOnlyOneData': false, - }); - print('导航到了指定界面:${event.lockId}, ${event.timeoutSeconds}'); - - int tries = 0; - while (tries < 40) { - tries++; - if (Get.isRegistered()) { - final int currentLockId = Get.find().state.keyInfos.value.lockId ?? 0; - if (currentLockId == event.lockId) { - break; - } - } - await Future.delayed(const Duration(milliseconds: 50)); + if (item == null) { + return; } - eventBus.fire(RemoteUnlockRequestEvent(lockId: event.lockId, timeoutSeconds: event.timeoutSeconds)); + final String? lastHandledStr = await Storage.getString('handledRemoteUnlockOperateDate'); + final int lastHandled = int.tryParse(lastHandledStr ?? '0') ?? 0; + if (event.timeoutSeconds <= 0) { + print('远程请求已超时,忽略展示'); + return; + } + if (event.operateDate != 0 && event.operateDate == lastHandled) { + print('重复的远程请求,已处理,忽略展示'); + return; + } + + _previousRoute = Get.currentRoute; + _previousArgs = Get.arguments; + await RemoteUnlockOverlay.show( + lockId: event.lockId, + lockAlias: item.lockAlias ?? item.lockName ?? '', + operateDate: event.operateDate, + timeoutSeconds: event.timeoutSeconds, + ); + + await Storage.setString('handledRemoteUnlockOperateDate', event.operateDate.toString()); + } + + void restorePreviousRouteIfAny() { + if (_previousRoute != null) { + Get.toNamed(_previousRoute!, arguments: _previousArgs); + _previousRoute = null; + _previousArgs = null; + } } Future _findLockItem(int lockId) async {