import 'dart:async'; import 'dart:io'; import 'package:flutter/services.dart'; import 'package:camera/camera.dart'; import 'package:flutter/foundation.dart'; import 'package:flutter/material.dart'; import 'package:permission_handler/permission_handler.dart' as ph; import 'package:qhd_prevention/customWidget/custom_alert_dialog.dart'; import 'package:qhd_prevention/customWidget/custom_button.dart'; import 'package:qhd_prevention/customWidget/toast_util.dart'; import 'package:qhd_prevention/http/ApiService.dart'; import 'package:qhd_prevention/pages/my_appbar.dart'; import 'package:qhd_prevention/tools/tools.dart'; // 在类最上面(State 内)加入一个 channel 常量 const MethodChannel _platformChan = MethodChannel('qhd_prevention/permissions'); /// 人脸识别模式 enum FaceMode { setUpdata, study, scan } class FaceRecognitionPage extends StatefulWidget { final String studentId; final Map data; final FaceMode mode; const FaceRecognitionPage({ Key? key, required this.studentId, required this.data, this.mode = FaceMode.study, }) : super(key: key); @override _FaceRecognitionPageState createState() => _FaceRecognitionPageState(); } class _FaceRecognitionPageState extends State with WidgetsBindingObserver { CameraController? _cameraController; Timer? _timer; int _attempts = 0; static const int _maxAttempts = 8; static const Duration _interval = Duration(seconds: 2); String _errMsg = ''; bool get _isManualMode => widget.mode == FaceMode.setUpdata; bool _isInitializing = false; bool _isTaking = false; @override void initState() { super.initState(); WidgetsBinding.instance.addObserver(this); // 延迟到首帧渲染后再请求权限并初始化相机,避免 iOS 在还未准备好时错过系统弹窗 WidgetsBinding.instance.addPostFrameCallback((_) { _initCameraWithPermission(); }); } @override void dispose() { WidgetsBinding.instance.removeObserver(this); _timer?.cancel(); try { _cameraController?.dispose(); } catch (_) {} super.dispose(); } // 生命周期:后台/前台切换时处理相机释放/恢复 @override void didChangeAppLifecycleState(AppLifecycleState state) { if (_cameraController == null || !_cameraController!.value.isInitialized) { return; } if (state == AppLifecycleState.inactive) { // 可选:释放相机,节省资源 try { _cameraController?.dispose(); Navigator.pop(context); } catch (_) {} _cameraController = null; } else if (state == AppLifecycleState.resumed) { // 恢复时重新初始化相机(并会再次请求权限) if (!_isInitializing) { _initCameraWithPermission(); } } } /// 请求 camera 权限(iOS:优先走原生 AVCaptureDevice.requestAccess;Android:使用 permission_handler) Future _requestCameraPermission() async { try { // iOS:使用原生 API 强制唤起系统授权弹窗 if (Platform.isIOS) { try { final dynamic res = await _platformChan.invokeMethod('requestCameraAccess'); // 原生返回 true/false if (res == true) { debugPrint('[FaceRecognition] iOS native camera access granted'); return true; } else { debugPrint('[FaceRecognition] iOS native camera access denied'); // 如果被拒绝(非永久还是永久都返回 false),再根据 permission_handler 的状态做后续提示/引导 final status = await ph.Permission.camera.status; if (status.isPermanentlyDenied) { await _showPermissionDialog(permanent: true); } else { await _showPermissionDialog(permanent: false); } return false; } } on PlatformException catch (e) { debugPrint('[FaceRecognition] platform channel error: $e — fallback to permission_handler'); // 如果 platform channel 异常,回退到 permission_handler } } // 非 iOS 或 platform 调用失败时走 permission_handler(保证 Android 正常) final result = await ph.Permission.camera.request(); debugPrint('[FaceRecognition] permission_handler camera result: $result'); if (result.isGranted) return true; if (result.isPermanentlyDenied) { await _showPermissionDialog(permanent: true); return false; } if (result.isDenied) { await _showPermissionDialog(permanent: false); return false; } if (result.isRestricted) { if (mounted) ToastUtil.showNormal(context, '相机权限受限,无法使用本功能'); return false; } if (result.isLimited) { if (mounted) ToastUtil.showNormal(context, '相机权限受限(limited)'); return false; } return false; } catch (e) { debugPrint('[FaceRecognition] permission request error: $e'); if (mounted) ToastUtil.showNormal(context, '请求相机权限时出错'); return false; } } /// 显示权限被拒/永久拒绝对话框(区分 permanent) Future _showPermissionDialog({required bool permanent}) async { if (!mounted) return; await CustomAlertDialog.showConfirm( context, title: '需要相机权限', content: permanent ? '检测到相机权限已被永久拒绝,请到系统设置中打开相机权限以继续使用人脸识别功能。' : '相机权限被拒绝,是否重试或前往设置打开权限?', cancelText: '取消', onConfirm: () async { try { final retry = await ph.Permission.camera.request(); debugPrint('[FaceRecognition] retry request result: $retry'); if (retry.isGranted) { if (mounted) await _initCamera(); } else if (retry.isPermanentlyDenied) { try { await ph.openAppSettings(); } catch (e) { debugPrint('[FaceRecognition] openAppSettings error: $e'); if (mounted) ToastUtil.showNormal(context, '无法打开设置,请手动前往系统设置授权'); } } else { if (mounted) ToastUtil.showNormal(context, '相机权限未授予'); } } catch (e) { debugPrint('[FaceRecognition] retry request error: $e'); } }, ); } /// 初始化:先请求权限,再打开相机 Future _initCameraWithPermission() async { if (!mounted) return; final ok = await _requestCameraPermission(); if (!ok) return; await _initCamera(); } Future _initCamera() async { if (_isInitializing) return; _isInitializing = true; try { final cams = await availableCameras(); CameraDescription? front; try { front = cams.firstWhere( (c) => c.lensDirection == CameraLensDirection.front, ); } catch (_) { if (cams.isNotEmpty) front = cams.first; } if (front == null) { if (!mounted) return; ToastUtil.showError(context, '未检测到可用摄像头'); _isInitializing = false; return; } _cameraController = CameraController( front, ResolutionPreset.medium, enableAudio: false, imageFormatGroup: ImageFormatGroup.yuv420, ); await _cameraController!.initialize(); // 尽量关闭闪光 try { await _cameraController!.setFlashMode(FlashMode.off); } catch (_) {} if (!mounted) return; setState(() {}); // 启动自动拍照定时器(若为自动模式) _timer?.cancel(); _attempts = 0; if (!_isManualMode) { _timer = Timer.periodic(_interval, (_) => _captureAndUpload()); } } catch (e, st) { debugPrint('[FaceRecognition] init camera error: $e\n$st'); if (mounted) { ToastUtil.showError(context, '初始化摄像头失败'); } } finally { _isInitializing = false; } } /// 自动模式:定时拍照并上传 Future _captureAndUpload() async { if (_isManualMode) return; if (_cameraController == null || !_cameraController!.value.isInitialized) return; if (_attempts >= _maxAttempts) return _onTimeout(); if (_isTaking) return; _isTaking = true; _attempts++; try { try { await _cameraController!.setFlashMode(FlashMode.off); } catch (_) {} final XFile pic = await _cameraController!.takePicture(); var res = {}; switch(widget.mode) { case FaceMode.study: res = await ApiService.getStudyUserFace(pic.path, widget.data); break; case FaceMode.setUpdata: res = await ApiService.getUpdataUserFace(pic.path, widget.data); break; case FaceMode.scan: res = await ApiService.getScanUserFace(pic.path, widget.data); break; } if (res['result'] == 'success') { _onSuccess(); } else { if (!mounted) return; setState(() { _errMsg = (res['msg'] ?? '').toString(); }); } } catch (e, st) { debugPrint('[FaceRecognition] capture error: $e\n$st'); // 忽略单次异常,等待下一次尝试 } finally { _isTaking = false; } } /// 手动拍照并上传 Future _captureAndReload() async { if (_cameraController == null || !_cameraController!.value.isInitialized) return; if (_isTaking) return; _isTaking = true; _showLoading(); try { // 再次确认 camera 权限(以防用户运行时撤销) final status = await ph.Permission.camera.status; if (!status.isGranted) { final ok = await _requestCameraPermission(); if (!ok) { _hideLoading(); _isTaking = false; return; } } try { await _cameraController!.setFlashMode(FlashMode.off); } catch (_) {} final XFile pic = await _cameraController!.takePicture(); final res = await ApiService.reloadMyFace(pic.path, widget.studentId); _hideLoading(); if (res['result'] == 'success') { _onSuccess(); } else { ToastUtil.showError(context, '验证失败,请重试'); } } catch (e, st) { debugPrint('[FaceRecognition] manual capture error: $e\n$st'); _hideLoading(); ToastUtil.showError(context, '拍照失败,请重试'); } finally { _isTaking = false; } } void _onSuccess() { _timer?.cancel(); if (widget.mode == FaceMode.setUpdata) { ToastUtil.showSuccess(context, '已更新人脸信息'); Future.delayed(const Duration(milliseconds: 800), () { if (mounted) Navigator.of(context).pop(true); }); return; } Future.delayed(const Duration(milliseconds: 800), () { if (mounted) Navigator.of(context).pop(true); }); } void _onTimeout() { _timer?.cancel(); ToastUtil.showError(context, '人脸超时,请重新识别!'); Future.delayed(const Duration(seconds: 3), () { if (mounted) Navigator.of(context).pop(false); }); } void _showLoading() { if (!mounted) return; showDialog( context: context, barrierDismissible: false, builder: (_) => const Center(child: CircularProgressIndicator()), ); } void _hideLoading() { if (!mounted) return; if (Navigator.canPop(context)) Navigator.pop(context); } void _showToast(String msg) { ToastUtil.showNormal(context, msg); } @override Widget build(BuildContext context) { if (_cameraController == null || !_cameraController!.value.isInitialized) { return const Scaffold( backgroundColor: Colors.white, body: Center(child: CircularProgressIndicator()), ); } final previewSize = _cameraController!.value.previewSize!; final previewAspect = previewSize.height / previewSize.width; final radius = (screenWidth(context) - 100) / 2; return Scaffold( backgroundColor: Colors.white, appBar: const MyAppbar(title: '人脸识别'), body: Stack( children: [ Positioned.fill(child: Container(color: Colors.white)), Transform.translate( offset: const Offset(0, -100), child: Stack( children: [ Center( child: ClipOval( child: AspectRatio( aspectRatio: previewAspect, child: CameraPreview(_cameraController!), ), ), ), Positioned.fill( child: CustomPaint( painter: _WhiteMaskPainter(radius: radius), ), ), ], ), ), Align( alignment: Alignment.center, child: Padding( padding: const EdgeInsets.only(top: 250), child: Column( mainAxisSize: MainAxisSize.min, children: [ const Text( '请将人脸置于圆圈内', style: TextStyle(fontSize: 16, color: Colors.black87), textAlign: TextAlign.center, ), if (_errMsg.isNotEmpty) Padding( padding: const EdgeInsets.only(top: 6.0), child: Text( _errMsg, style: const TextStyle(fontSize: 14, color: Colors.red), textAlign: TextAlign.center, ), ), const SizedBox(height: 20), if (_isManualMode) CustomButton( text: '拍照/上传', backgroundColor: Colors.blue, onPressed: _captureAndReload, ), ], ), ), ), ], ), ); } } class _WhiteMaskPainter extends CustomPainter { final double radius; _WhiteMaskPainter({required this.radius}); @override void paint(Canvas canvas, Size size) { canvas.saveLayer(Offset.zero & size, Paint()); canvas.drawRect(Offset.zero & size, Paint()..color = Colors.white); canvas.drawCircle( size.center(Offset.zero), radius, Paint()..blendMode = BlendMode.clear, ); canvas.restore(); } @override bool shouldRepaint(covariant CustomPainter old) => false; }