Compare commits

...

3 Commits

Author SHA1 Message Date
84055eda57 update 2025-12-25 18:08:05 +08:00
2619288234 实现直接从安卓原生获取点击推送事件和参数,传入flutter处理推送。
实现了远程开锁请求功能的ui和开锁
测试了vivo和小米手机支持上述功能,三星、荣耀不支持,其他未测试。
修改了打印调试信息,后续需要恢复原状
2025-12-15 18:05:27 +08:00
7b9a3a0daf feat(锁详情): 添加推送触发的远程开锁功能
实现推送消息触发的远程开锁请求流程,包括:
1. 新增RemoteUnlockRequestEvent和PushExtraEvent事件类型
2. 在锁详情页添加倒计时弹窗和动画效果
3. 实现Native层推送数据缓存和转发机制
4. 添加RemoteUnlockCoordinator处理应用未启动时的推送导航
2025-11-29 14:45:42 +08:00
15 changed files with 1060 additions and 86 deletions

View File

@ -1,6 +1,8 @@
package com.skychip.lock package com.skychip.lock
import android.content.Intent import android.content.Intent
import android.content.Context
import android.content.Context.MODE_PRIVATE
import android.net.Uri import android.net.Uri
import android.os.Bundle import android.os.Bundle
import android.util.Log import android.util.Log
@ -11,6 +13,56 @@ import io.flutter.plugins.GeneratedPluginRegistrant
import android.bluetooth.BluetoothAdapter; import android.bluetooth.BluetoothAdapter;
import androidx.core.content.FileProvider import androidx.core.content.FileProvider
import java.io.File import java.io.File
import org.json.JSONObject
import org.json.JSONArray
private fun flagsToString(flags: Int): String {
val flagsList = mutableListOf<String>()
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() { class MainActivity : FlutterActivity() {
override fun onCreate(savedInstanceState: Bundle?) { override fun onCreate(savedInstanceState: Bundle?) {
@ -55,6 +107,23 @@ class MainActivity : FlutterActivity() {
result.notImplemented() // 没有实现的方法 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 = "") { 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<String, Any>) {
val sp = context.getSharedPreferences(SP_NAME, MODE_PRIVATE)
sp.edit().putString(KEY_PENDING_PUSH, JSONObject(data).toString()).apply()
}
/** 取出并清空 */
fun popPush(context: Context): Map<String, Any>? {
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<String, String> {
val results = HashMap<String, String>()
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<String, String> {
val out = HashMap<String, String>(8)
val stack = ArrayDeque<Any>()
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<String, Any>? {
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<String, String>()
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<String, Any>()
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<String, String>()
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
}
} }

View File

@ -5,7 +5,7 @@ import 'package:get/get.dart';
import 'package:star_lock/mine/about/debug/debug_console.dart'; import 'package:star_lock/mine/about/debug/debug_console.dart';
class AppLog { class AppLog {
static bool _printLog = false; static bool _printLog = true;
static bool _onlyError = false; static bool _onlyError = false;
static void showLog({required bool printLog, bool? onlyError}) { static void showLog({required bool printLog, bool? onlyError}) {
@ -14,6 +14,7 @@ class AppLog {
} }
static void log(String msg, {StackTrace? stackTrace, bool? error}) { static void log(String msg, {StackTrace? stackTrace, bool? error}) {
debugPrint(msg);
msg = '${DateTime.now().toIso8601String()} : $msg'; msg = '${DateTime.now().toIso8601String()} : $msg';
DebugConsole.info(msg, stackTrace: stackTrace, isErr: error ?? false); DebugConsole.info(msg, stackTrace: stackTrace, isErr: error ?? false);
if (!kDebugMode) { if (!kDebugMode) {

View File

@ -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/network/start_chart_api.dart';
import 'package:star_lock/talk/starChart/handle/impl/debug_Info_model.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/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/baseGetXController.dart';
import 'package:star_lock/tools/bugly/bugly_tool.dart'; import 'package:star_lock/tools/bugly/bugly_tool.dart';
import 'package:star_lock/tools/callkit_handler.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/eventBusEventManage.dart';
import 'package:star_lock/tools/jverify_one_click_login.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/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/notification_service.dart';
import 'package:star_lock/tools/push/xs_jPhush.dart'; import 'package:star_lock/tools/push/xs_jPhush.dart';
import 'package:star_lock/tools/storage.dart'; import 'package:star_lock/tools/storage.dart';
import 'package:star_lock/translations/current_locale_tool.dart'; import 'package:star_lock/translations/current_locale_tool.dart';
import 'package:star_lock/translations/trans_lib.dart'; import 'package:star_lock/translations/trans_lib.dart';
import 'package:star_lock/tools/remote_unlock_coordinator.dart';
import 'apm/apm_helper.dart'; import 'apm/apm_helper.dart';
import 'app.dart'; import 'app.dart';
@ -108,5 +111,15 @@ Future<void> privacySDKInitialization() async {
final XSJPushProvider jpushProvider = XSJPushProvider(); final XSJPushProvider jpushProvider = XSJPushProvider();
await jpushProvider.initJPushService(); await jpushProvider.initJPushService();
NotificationService().init(); // NotificationService().init(); //
RemoteUnlockCoordinator.init();
NativeInteractionTool().setupPushReceiver();
final Map<String, dynamic>? push =
await NativeInteractionTool().getPendingPush();
if(push != null) {
print('哈哈: $push');
if(push.isNotEmpty) {
// do something
MessageManagement.shuntingBus(push);
}
}
} }

View File

@ -113,6 +113,59 @@ class LockDetailLogic extends BaseGetXController {
}); });
} }
//
void initRemoteUnlockRequestListener() {
//
eventBus.on<RemoteUnlockRequestEvent>().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<void> _replyOpenLock(Reply reply) async { Future<void> _replyOpenLock(Reply reply) async {
final int status = reply.data[6]; final int status = reply.data[6];

View File

@ -1,4 +1,5 @@
import 'dart:async'; import 'dart:async';
import 'dart:math' show pi;
import 'package:flutter/cupertino.dart'; import 'package:flutter/cupertino.dart';
import 'package:flutter/material.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/flavors.dart';
import 'package:star_lock/main/lockDetail/lockDetail/device_network_info.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/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/main/lockMian/lockMain/lockMain_logic.dart';
import 'package:star_lock/mine/gateway/addGateway/gatewayConfigurationWifi/getGatewayConfiguration_entity.dart'; import 'package:star_lock/mine/gateway/addGateway/gatewayConfigurationWifi/getGatewayConfiguration_entity.dart';
import 'package:star_lock/network/api_repository.dart'; import 'package:star_lock/network/api_repository.dart';
@ -30,6 +30,7 @@ import '../../../common/XSConstantMacro/XSConstantMacro.dart';
import '../../../tools/appRouteObserver.dart'; import '../../../tools/appRouteObserver.dart';
import '../../../tools/dateTool.dart'; import '../../../tools/dateTool.dart';
import '../../../tools/eventBusEventManage.dart'; import '../../../tools/eventBusEventManage.dart';
import '../../../tools/remote_unlock_coordinator.dart';
import '../../lockMian/entity/lockListInfo_entity.dart'; import '../../lockMian/entity/lockListInfo_entity.dart';
import 'lockDetail_logic.dart'; import 'lockDetail_logic.dart';
@ -49,6 +50,7 @@ class _LockDetailPageState extends State<LockDetailPage> with TickerProviderStat
@override @override
void initState() { void initState() {
print("LockDetailPage initState");
state.animationController = AnimationController(duration: const Duration(seconds: 1), vsync: this); state.animationController = AnimationController(duration: const Duration(seconds: 1), vsync: this);
state.animationController?.repeat(); state.animationController?.repeat();
//StatusListener //StatusListener
@ -70,6 +72,7 @@ class _LockDetailPageState extends State<LockDetailPage> with TickerProviderStat
_initRefreshLockDetailInfoDataEventAction(); _initRefreshLockDetailInfoDataEventAction();
logic.initReplySubscription(); logic.initReplySubscription();
logic.initLockSetOpenOrCloseCheckInRefreshLockDetailWithAttendanceAction(); logic.initLockSetOpenOrCloseCheckInRefreshLockDetailWithAttendanceAction();
logic.initRemoteUnlockRequestListener(); //
logic.loadData(lockListInfoItemEntity: widget.lockListInfoItemEntity, isOnlyOneData: widget.isOnlyOneData); logic.loadData(lockListInfoItemEntity: widget.lockListInfoItemEntity, isOnlyOneData: widget.isOnlyOneData);
} }
@ -512,14 +515,16 @@ class _LockDetailPageState extends State<LockDetailPage> with TickerProviderStat
], ],
), ),
), ),
Visibility( Obx(() => Visibility(
visible: state.iSClosedUnlockSuccessfulPopup.value, visible: state.iSClosedUnlockSuccessfulPopup.value,
child: Container( child: Container(
width: 1.sw, width: 1.sw,
height: 1.sh - ScreenUtil().statusBarHeight * 2, height: 1.sh - ScreenUtil().statusBarHeight * 2,
color: Colors.black.withOpacity(0.3), color: Colors.black.withOpacity(0.3),
child: _unlockSuccessWidget()), child: _unlockSuccessWidget()),
) ))
,
SizedBox.shrink()
]), ]),
], ],
); );
@ -608,6 +613,7 @@ class _LockDetailPageState extends State<LockDetailPage> with TickerProviderStat
Icons.info, // 使 warning Icons.info, // 使 warning
color: AppColors.mainColor, // color: AppColors.mainColor, //
size: 25.w, // 30 size: 25.w, // 30
), ),
), ),
SizedBox(width: 20.w), SizedBox(width: 20.w),
@ -640,7 +646,6 @@ class _LockDetailPageState extends State<LockDetailPage> with TickerProviderStat
void callback() { void callback() {
setState(startUnLock); setState(startUnLock);
} }
logic.functionBlocker.block(callback); logic.functionBlocker.block(callback);
} }
}, },
@ -655,7 +660,6 @@ class _LockDetailPageState extends State<LockDetailPage> with TickerProviderStat
: 'images/main/icon_main_openLockBtn_center.png'), : 'images/main/icon_main_openLockBtn_center.png'),
width: 330.w, width: 330.w,
height: 330.w, height: 330.w,
// color: AppColors.primaryTopColor,
), ),
), ),
if (state.openDoorBtnisUneable.value == false) if (state.openDoorBtnisUneable.value == false)
@ -686,12 +690,13 @@ class _LockDetailPageState extends State<LockDetailPage> with TickerProviderStat
)), )),
], ],
), ),
)), ),
),
Positioned( Positioned(
right: 90.w, right: 90.w,
bottom: 1, bottom: 1,
child: Obx(() => Visibility( child: Obx(() => Visibility(
visible: state.keyInfos.value.lockSetting!.remoteUnlock == 1, visible: state.keyInfos.value.lockSetting!.remoteUnlock == 1 && !state.showPushRemoteUnlockRequest.value,
child: GestureDetector( child: GestureDetector(
onTap: () { onTap: () {
ShowCupertinoAlertView().isToRemoteUnLockAlert(remoteUnlockAction: () { ShowCupertinoAlertView().isToRemoteUnLockAlert(remoteUnlockAction: () {
@ -718,7 +723,9 @@ class _LockDetailPageState extends State<LockDetailPage> with TickerProviderStat
SizedBox( SizedBox(
height: 30.h, height: 30.h,
), ),
Container( Obx(() => Visibility(
visible: !state.showPushRemoteUnlockRequest.value,
child: Container(
padding: EdgeInsets.symmetric(horizontal: 20.w), padding: EdgeInsets.symmetric(horizontal: 20.w),
child: Center( child: Center(
child: Text( child: Text(
@ -729,6 +736,7 @@ class _LockDetailPageState extends State<LockDetailPage> with TickerProviderStat
), ),
), ),
), ),
)),
SizedBox( SizedBox(
height: 30.h, height: 30.h,
), ),
@ -1325,6 +1333,29 @@ class _LockDetailPageState extends State<LockDetailPage> with TickerProviderStat
return formattedTime; 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<void> isNeedRealNameAuthThenOpenLock() async { Future<void> isNeedRealNameAuthThenOpenLock() async {
final bool isNetWork = await logic.isConnected() ?? false; final bool isNetWork = await logic.isConnected() ?? false;
@ -1439,6 +1470,11 @@ class _LockDetailPageState extends State<LockDetailPage> with TickerProviderStat
logic.cancelBlueConnetctToastTimer(); logic.cancelBlueConnetctToastTimer();
BlueManage().disconnect(); BlueManage().disconnect();
state.openLockBtnState.value = 0; 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<LockDetailPage> with TickerProviderStat
BlueManage().disconnect(); 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;
}

View File

@ -80,4 +80,11 @@ class LockDetailState {
List<int> uploadRemoteControlDataList = <int>[];// List<int> uploadRemoteControlDataList = <int>[];//
List<int> uploadLockSetDataList = <int>[];// List<int> uploadLockSetDataList = <int>[];//
//
RxBool showPushRemoteUnlockRequest = false.obs; //
RxInt pushRemoteUnlockCountdownSeconds = 60.obs; //
Timer? pushRemoteUnlockRequestTimer; //
int currentPushOperateDate = 0; //
int handledPushOperateDate = 0; //
} }

View File

@ -24,7 +24,6 @@ class _StarLockApplicationState extends State<StarLockApplication> {
@override @override
void initState() { void initState() {
super.initState(); super.initState();
} }
@override @override

View File

@ -1,5 +1,6 @@
import 'package:flutter/services.dart'; import 'package:flutter/services.dart';
import 'package:star_lock/flavors.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 'package:star_lock/tools/push/xs_jPhush.dart';
import '../app_settings/app_settings.dart'; import '../app_settings/app_settings.dart';
@ -8,12 +9,14 @@ import '../app_settings/app_settings.dart';
class NativeInteractionConfig { class NativeInteractionConfig {
static String methodSendChannel = 'starLockFlutterSend'; static String methodSendChannel = 'starLockFlutterSend';
static String receiveEventChannel = 'starLockFlutterReceive'; static String receiveEventChannel = 'starLockFlutterReceive';
static String methodPushChannel = 'starLockFlutterPushCache';
} }
///flutter向原生发送消息 ///flutter向原生发送消息
typedef BlockBlueStatus = void Function(String status); typedef BlockBlueStatus = void Function(String status);
class NativeInteractionTool { class NativeInteractionTool {
MethodChannel? _pushCacheChannel;
MethodChannel sendChannel = MethodChannel sendChannel =
MethodChannel(NativeInteractionConfig.methodSendChannel); MethodChannel(NativeInteractionConfig.methodSendChannel);
MethodChannel receiveChannel = MethodChannel receiveChannel =
@ -46,7 +49,6 @@ class NativeInteractionTool {
// / // /
final String message = call.arguments; final String message = call.arguments;
blockBlueStatus(message); blockBlueStatus(message);
// AppLog.log('收到原生发送的信息getBlueStatus: $message');
break; break;
default: default:
throw MissingPluginException(); throw MissingPluginException();
@ -54,6 +56,41 @@ class NativeInteractionTool {
}); });
} }
/// /
void setupPushReceiver() {
receiveChannel.setMethodCallHandler((MethodCall call) async {
switch (call.method) {
case 'receivePush':
final Map<dynamic, dynamic> data = call.arguments;
try {
final Map<String, dynamic> push = Map<String, dynamic>.from(data);
print('收到原生 receivePush$push');
MessageManagement.shuntingBus(push);
} catch (e) {
print('NativeInteractionTool.setupPushReceiver 解析失败:$e');
}
break;
case 'getBlueStatus':
break;
default:
throw MissingPluginException();
}
});
}
///
Future<Map<String, dynamic>?> getPendingPush() async {
_pushCacheChannel ??= MethodChannel(NativeInteractionConfig.methodPushChannel);
print('进入getPendingPush');
try {
final Map<dynamic, dynamic>? data = await _pushCacheChannel!.invokeMethod('getPendingPush');
return data != null ? Map<String, dynamic>.from(data) : null;
} catch (e) {
print("获取缓存推送消息失败: '${e.toString()}'.");
return null;
}
}
Future<String?> getBundleIdentifier() async { Future<String?> getBundleIdentifier() async {
try { try {
final String? bundleIdentifier = final String? bundleIdentifier =

View File

@ -86,6 +86,22 @@ class LockSetChangeSetRefreshLockDetailWithType {
dynamic setResult; 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<String, dynamic> data;
}
/// ///
class GetTVDataRefreshUI { class GetTVDataRefreshUI {
GetTVDataRefreshUI(this.tvList); GetTVDataRefreshUI(this.tvList);

View File

@ -13,10 +13,11 @@ class MessageManagement {
if (message.isEmpty) { if (message.isEmpty) {
return; return;
} }
print('MessageManagement.shunting呀: $message');
Map<String, dynamic> extra = <String, dynamic>{}; Map<String, dynamic> extra = <String, dynamic>{};
if (GetPlatform.isAndroid) { if (GetPlatform.isAndroid) {
extra = _androidAnalysis(message); extra = _androidAnalysis(message);
// AppLog.log('MessageManagement.shunting GetPlatform.isAndroid: $extra'); AppLog.log('MessageManagement.shunting GetPlatform.isAndroid: $extra');
} else if (GetPlatform.isIOS) { } else if (GetPlatform.isIOS) {
extra = _iosAnalysis(message); extra = _iosAnalysis(message);
// AppLog.log('MessageManagement.shunting GetPlatform.isIos: $extra'); // AppLog.log('MessageManagement.shunting GetPlatform.isIos: $extra');
@ -27,7 +28,7 @@ class MessageManagement {
return; return;
} }
AppLog.log(message.toString()); AppLog.log(message.toString());
_shuntingBus(extra); shuntingBus(extra);
} }
//android解析 //android解析
@ -74,8 +75,10 @@ class MessageManagement {
} }
// //
static void _shuntingBus(Map<String, dynamic> data) { static void shuntingBus(Map<String, dynamic> data) {
final int eventNo = data['eventNo'] ?? -1; // final int eventNo = data['eventNo'] ?? -1;
final int eventNo = int.tryParse(data['eventNo']?.toString() ?? '') ?? -1;
eventBus.fire(PushExtraEvent(data));
switch (eventNo) { switch (eventNo) {
case MessageConstant.keyStateChange: case MessageConstant.keyStateChange:
final int keyId = data['keyId']; final int keyId = data['keyId'];
@ -108,7 +111,16 @@ class MessageManagement {
eventBus.fire(RefreshLockInfoDataEvent(keyId: keyId, lockId: lockId)); eventBus.fire(RefreshLockInfoDataEvent(keyId: keyId, lockId: lockId));
break; break;
case MessageConstant.talkPushBigImage: 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; break;
default: default:

View File

@ -4,6 +4,7 @@ import 'dart:ffi';
import 'dart:io'; import 'dart:io';
import 'package:flutter/foundation.dart'; import 'package:flutter/foundation.dart';
import 'package:flutter/services.dart';
import 'package:get/get.dart'; import 'package:get/get.dart';
import 'package:jpush_flutter/jpush_flutter.dart'; import 'package:jpush_flutter/jpush_flutter.dart';
import 'package:star_lock/flavors.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/baseGetXController.dart';
import 'package:star_lock/tools/callkit_handler.dart'; import 'package:star_lock/tools/callkit_handler.dart';
import 'package:star_lock/tools/debounce_throttle_tool.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/message_management.dart';
import 'package:star_lock/tools/push/notification_service.dart'; import 'package:star_lock/tools/push/notification_service.dart';
import 'package:star_lock/tools/storage.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 '../../app_settings/app_settings.dart';
import '../NativeInteractionTool.dart';
class XSJPushProvider { class XSJPushProvider {
static const Map<int, String> channelTypeMapping = <int, String>{ static const Map<int, String> channelTypeMapping = <int, String>{
@ -62,18 +67,18 @@ class XSJPushProvider {
// final String? bundleIdentifier = // final String? bundleIdentifier =
// await NativeInteractionTool().getBundleIdentifier(); // await NativeInteractionTool().getBundleIdentifier();
// print('bundleIdentifier: $bundleIdentifier'); // print('bundleIdentifier: $bundleIdentifier');
addJPushEventHandler();
jpush.setup( jpush.setup(
appKey: F.jPushKey, appKey: F.jPushKey,
channel: 'flutter_channel', channel: 'flutter_channel',
production: F.isProductionEnv, production: F.isProductionEnv,
debug: !F.isProductionEnv, debug: true,//!F.isProductionEnv,
); );
jpush.setAuth(enable: true); jpush.setAuth(enable: true);
jpush.applyPushAuthority( jpush.applyPushAuthority(
const NotificationSettingsIOS(sound: true, alert: true, badge: false), const NotificationSettingsIOS(sound: true, alert: true, badge: false),
); );
addJPushEventHandler();
AppLog.log('JPush initialized.'); AppLog.log('JPush initialized.');
debugPrint("initJPushService end"); debugPrint("initJPushService end");
} }
@ -117,7 +122,47 @@ class XSJPushProvider {
}, },
onOpenNotification: (Map<String, dynamic> message) async { onOpenNotification: (Map<String, dynamic> message) async {
AppLog.log('onOpenNotification: $message'); AppLog.log('onOpenNotification: $message');
debugPrint("addJPushEventHandler onOpenNotification:$message"); print("addJPushEventHandler onOpenNotification:$message");
try {
await MessageManagement.shunting(message);
Map<String, dynamic> extra = <String, dynamic>{};
if (GetPlatform.isAndroid) {
final Map<Object?, dynamic> 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<Object?, Object?> 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<String, dynamic> message) async { onReceiveMessage: (Map<String, dynamic> message) async {
AppLog.log('onReceiveMessage: $message'); AppLog.log('onReceiveMessage: $message');

View File

@ -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<RemoteUnlockRequestEvent> _pending = <RemoteUnlockRequestEvent>[];
String? _previousRoute;
dynamic _previousArgs;
bool get hasPrevious => _previousRoute != null;
static void init() {
_instance._init();
}
void _init() {
if (_inited) return;
_inited = true;
_sub = eventBus.on<PushExtraEvent>().listen((PushExtraEvent evt) async {
final Map<String, dynamic> 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<RemoteUnlockRequestEvent>.from(_pending)) {
await _navigateToLockDetailAndRefire(e);
print('导航到指定界面成功');
}
_pending.clear();
}
});
}
Future<void> _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<LockListInfoItemEntity?> _findLockItem(int lockId) async {
final LockListInfoGroupEntity? stored = await Storage.getLockMainListData();
LockListInfoItemEntity? item;
if (stored != null) {
for (final GroupList g in stored.groupList ?? <GroupList>[]) {
for (final LockListInfoItemEntity l in g.lockList ?? <LockListInfoItemEntity>[]) {
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 ?? <GroupList>[]) {
for (final LockListInfoItemEntity l in g.lockList ?? <LockListInfoItemEntity>[]) {
if ((l.lockId ?? 0) == lockId) {
return l;
}
}
}
return null;
}
}

View File

@ -0,0 +1,206 @@
import 'dart:async';
import 'dart:math';
import 'dart:ui' as ui;
import 'package:flutter/material.dart';
import 'package:flutter_screenutil/flutter_screenutil.dart';
import 'package:get/get.dart';
import 'package:star_lock/app_settings/app_colors.dart';
import 'package:star_lock/network/api_repository.dart';
class RemoteUnlockOverlay {
static Future<void> show({required int lockId, required String lockAlias, required int operateDate, int timeoutSeconds = 60}) async {
await Get.to(() => RemoteUnlockOverlayPage(lockId: lockId, lockAlias: lockAlias, operateDate: operateDate, timeoutSeconds: timeoutSeconds), opaque: false);
}
}
class RemoteUnlockOverlayPage extends StatefulWidget {
const RemoteUnlockOverlayPage({required this.lockId, required this.lockAlias, required this.operateDate, this.timeoutSeconds = 60, Key? key}) : super(key: key);
final int lockId;
final String lockAlias;
final int operateDate;
final int timeoutSeconds;
@override
State<RemoteUnlockOverlayPage> createState() => _RemoteUnlockOverlayPageState();
}
class _RemoteUnlockOverlayPageState extends State<RemoteUnlockOverlayPage> {
late int seconds;
Timer? timer;
@override
void initState() {
super.initState();
seconds = widget.timeoutSeconds;
timer = Timer.periodic(const Duration(seconds: 1), (Timer t) {
if (!mounted) {
return;
}
if (seconds <= 0) {
t.cancel();
timer?.cancel();
setState(() {
seconds = 0;
});
Get.back();
return;
}
setState(() {
seconds = seconds - 1;
});
});
}
@override
void dispose() {
timer?.cancel();
super.dispose();
}
@override
Widget build(BuildContext context) {
final double cardWidth = 0.9.sw;
final double ringSize = 260.r;
return PopScope(
canPop: false,
child: Stack(
children: [
Positioned.fill(
child: BackdropFilter(
filter: ui.ImageFilter.blur(sigmaX: 3, sigmaY: 3),
child: Container(color: Colors.black.withOpacity(0.18)),
),
),
Center(
child: Container(
width: cardWidth,
padding: EdgeInsets.symmetric(vertical: 24.h, horizontal: 20.w),
decoration: BoxDecoration(
color: Colors.white,
borderRadius: BorderRadius.circular(16.r),
boxShadow: [BoxShadow(color: Colors.black.withOpacity(0.08), blurRadius: 20.r, offset: const Offset(0, 6))],
),
child: Column(
mainAxisSize: MainAxisSize.min,
children: [
Row(
children: [
Icon(Icons.lock, color: AppColors.mainColor, size: 24.r),
SizedBox(width: 8.w),
Expanded(child: Text(widget.lockAlias, style: TextStyle(fontSize: 22.sp, color: AppColors.blackColor))),
],
),
SizedBox(height: 12.h),
Text('远程开锁请求'.tr, style: TextStyle(fontSize: 28.sp, color: AppColors.blackColor, fontWeight: FontWeight.w700)),
SizedBox(height: 20.h),
SizedBox(
width: ringSize,
height: ringSize,
child: Stack(
alignment: Alignment.center,
children: [
_DottedCountdownRing(seconds: seconds, size: ringSize),
Text('${seconds} s', style: TextStyle(fontSize: 48.sp, color: AppColors.mainColor, fontWeight: FontWeight.w700)),
],
),
),
SizedBox(height: 16.h),
Text('请确认是否允许该门锁进行远程开锁'.tr, style: TextStyle(fontSize: 18.sp, color: AppColors.btnDisableColor)),
SizedBox(height: 8.h),
Text('请求将在倒计时结束后自动取消'.tr, style: TextStyle(fontSize: 16.sp, color: AppColors.btnDisableColor)),
SizedBox(height: 20.h),
Row(
mainAxisAlignment: MainAxisAlignment.center,
children: [
GestureDetector(
onTap: _reject,
child: Container(
width: 0.35.sw,
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: 20.w),
GestureDetector(
onTap: _accept,
child: Container(
width: 0.35.sw,
height: 48.h,
alignment: Alignment.center,
decoration: BoxDecoration(color: AppColors.mainColor, borderRadius: BorderRadius.circular(8.r)),
child: Text('同意'.tr, style: TextStyle(color: Colors.white, fontSize: 22.sp)),
),
),
],
),
SizedBox(height: 8.h),
Container(
padding: EdgeInsets.symmetric(vertical: 10.h, horizontal: 12.w),
decoration: BoxDecoration(color: const Color(0xFFF7F9FC), borderRadius: BorderRadius.circular(8.r)),
child: Row(children: [Icon(Icons.info_outline, color: AppColors.mainColor, size: 18.r), SizedBox(width: 6.w), Expanded(child: Text('远程开锁需确保现场安全与人员确认'.tr, style: TextStyle(color: AppColors.btnDisableColor, fontSize: 14.sp)))]),
),
],
),
),
),
],
),
);
}
Future<void> _accept() async {
timer?.cancel();
await ApiRepository.to.remoteOpenLock(lockId: widget.lockId.toString(), timeOut: 60);
Get.back();
}
void _reject() {
timer?.cancel();
Get.back();
}
}
class _DottedCountdownRing extends StatelessWidget {
const _DottedCountdownRing({required this.seconds, required this.size});
final int seconds;
final double size;
@override
Widget build(BuildContext context) {
final double progress = seconds.clamp(0, 60) / 60.0;
return CustomPaint(size: Size(size, size), painter: _DottedCirclePainter(progress: progress));
}
}
class _DottedCirclePainter extends CustomPainter {
_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;
}

View File

@ -1,3 +1,4 @@
import 'dart:async';
import 'dart:typed_data'; import 'dart:typed_data';
import 'dart:ui'; 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: <Widget>[
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( void showBuyTipWithContentAlert(
{required String titleStr, required Function sureClick}) { {required String titleStr, required Function sureClick}) {

View File

@ -218,7 +218,7 @@ dependencies:
jpush_flutter: jpush_flutter:
git: git:
url: git@code.star-lock.cn:StarlockTeam/jpush_flutter.git url: https://code.skychip.top/sky/jpush_flutter.git
ref: 656df9ee91b1ec8b96aa1208a6b0df27a4516067 ref: 656df9ee91b1ec8b96aa1208a6b0df27a4516067
#视频播放器 #视频播放器