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..d49415a7 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,56 @@ import io.flutter.plugins.GeneratedPluginRegistrant 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() + + 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 +107,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 +183,236 @@ 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 { + + 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 extras = intent.extras ?: return null + + 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") + } + } + + 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/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 291d88cd..f1d94d22 100755 --- a/lib/main/lockDetail/lockDetail/lockDetail_logic.dart +++ b/lib/main/lockDetail/lockDetail/lockDetail_logic.dart @@ -129,6 +129,59 @@ class LockDetailLogic extends BaseGetXController { }); } + // 初始化远程解锁请求监听器 + void initRemoteUnlockRequestListener() { + // 监听推送消息触发的远程解锁请求 + eventBus.on().listen((RemoteUnlockRequestEvent event) { + if (event.lockId == state.keyInfos.value.lockId) { + // 去重:如果该operateDate已处理过,则忽略 + if (event.operateDate != 0 && event.operateDate == state.handledPushOperateDate) { + return; + } + state.currentPushOperateDate = event.operateDate; + SchedulerBinding.instance.addPostFrameCallback((_) { + 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; + state.handledPushOperateDate = state.currentPushOperateDate; + state.currentPushOperateDate = 0; + remoteOpenLock(); + } + + // 拒绝推送消息触发的远程解锁请求 + void rejectPushRemoteUnlockRequest() { + state.pushRemoteUnlockRequestTimer?.cancel(); + state.showPushRemoteUnlockRequest.value = false; + state.pushRemoteUnlockCountdownSeconds.value = 60; + state.handledPushOperateDate = state.currentPushOperateDate; + state.currentPushOperateDate = 0; + } + // 开门数据解析 Future _replyOpenLock(Reply reply) async { final int status = reply.data[6]; @@ -139,7 +192,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 7f0e95c3..4ea43070 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'; @@ -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'; @@ -49,6 +50,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 +72,7 @@ class _LockDetailPageState extends State with TickerProviderStat _initRefreshLockDetailInfoDataEventAction(); logic.initReplySubscription(); logic.initLockSetOpenOrCloseCheckInRefreshLockDetailWithAttendanceAction(); + logic.initRemoteUnlockRequestListener(); // 初始化远程解锁请求监听器 logic.loadData(lockListInfoItemEntity: widget.lockListInfoItemEntity, isOnlyOneData: widget.isOnlyOneData); } @@ -512,14 +515,16 @@ 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()), + )) + , + SizedBox.shrink() ]), ], ); @@ -608,6 +613,7 @@ class _LockDetailPageState extends State with TickerProviderStat Icons.info, // 使用内置的 warning 图标,它是一个叹号 color: AppColors.mainColor, // 设置图标颜色为红色 size: 25.w, // 设置图标大小为 30 + ), ), SizedBox(width: 20.w), @@ -629,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: () { @@ -718,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, ), @@ -1325,6 +1333,29 @@ class _LockDetailPageState extends State with TickerProviderStat return formattedTime; } + // 推送远程解锁倒计时动画 + Widget _pushRemoteUnlockCountdownAnimation() { + return Obx(() { + // 计算虚线圆环的可见部分(根据倒计时进度) + final double progress = state.pushRemoteUnlockCountdownSeconds.value / 60.0; + + return CustomPaint( + size: Size(168.r, 168.r), + painter: DottedCirclePainter(progress: progress), + ); + }); + } + + 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; @@ -1439,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; } /// 从下级返回 当前界面即将出现 @@ -1457,3 +1493,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 e388dd96..8aa1c1d3 100755 --- a/lib/main/lockDetail/lockDetail/lockDetail_state.dart +++ b/lib/main/lockDetail/lockDetail/lockDetail_state.dart @@ -83,4 +83,11 @@ class LockDetailState { List uploadRemoteControlDataList = [];// 上传遥控的数据 List uploadLockSetDataList = [];// 上传锁设置数据 + + // 远程开锁请求(推送消息触发) + RxBool showPushRemoteUnlockRequest = false.obs; // 是否显示推送远程开锁确认界面 + RxInt pushRemoteUnlockCountdownSeconds = 60.obs; // 推送远程开锁倒计时秒数 + Timer? pushRemoteUnlockRequestTimer; // 推送远程开锁倒计时定时器 + int currentPushOperateDate = 0; // 当前推送请求操作时间戳 + int handledPushOperateDate = 0; // 已处理的推送请求操作时间戳,避免重复显示 } 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..8a1395ec 100755 --- a/lib/tools/eventBusEventManage.dart +++ b/lib/tools/eventBusEventManage.dart @@ -86,6 +86,22 @@ class LockSetChangeSetRefreshLockDetailWithType { dynamic setResult; } +/// 远程开锁请求事件(门铃触发) +class RemoteUnlockRequestEvent { + RemoteUnlockRequestEvent({required this.lockId, this.timeoutSeconds = 60, this.operateDate = 0}); + + int lockId; + int timeoutSeconds; + int operateDate; +} + +/// 推送原始数据事件 +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..ed435a59 100644 --- a/lib/tools/push/message_management.dart +++ b/lib/tools/push/message_management.dart @@ -13,10 +13,11 @@ class MessageManagement { if (message.isEmpty) { return; } + print('MessageManagement.shunting呀: $message'); 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'); @@ -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,16 @@ class MessageManagement { eventBus.fire(RefreshLockInfoDataEvent(keyId: keyId, lockId: lockId)); break; case MessageConstant.talkPushBigImage: - // XSJPushProvider().showCustomNotification(data); + print('MessageManagement._shuntingBus 收到远程开锁请求:$data'); + 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; 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..8cf433e5 --- /dev/null +++ b/lib/tools/remote_unlock_coordinator.dart @@ -0,0 +1,144 @@ +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'; +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; + RemoteUnlockCoordinator._(); + static final RemoteUnlockCoordinator _instance = RemoteUnlockCoordinator._(); + + StreamSubscription? _sub; + bool _inited = false; + + final List _pending = []; + String? _previousRoute; + dynamic _previousArgs; + bool get hasPrevious => _previousRoute != null; + + 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 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(); + 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; + } + + 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 { + 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 #视频播放器