기존 프로젝트 최초 업로드
This commit is contained in:
@@ -0,0 +1,172 @@
|
||||
// 🛡️ 화면이 꺼져도 동작하는 포그라운드 백그라운드 서비스.
|
||||
// 조도 센서로 주머니 속 밝기를 측정해 무단 반출을 감지하고, 진동+알림+서버 신고를 처리한다.
|
||||
import 'dart:async';
|
||||
import 'dart:convert';
|
||||
import 'dart:ui';
|
||||
import 'package:flutter_background_service/flutter_background_service.dart';
|
||||
import 'package:flutter_local_notifications/flutter_local_notifications.dart';
|
||||
import 'package:http/http.dart' as http;
|
||||
import 'package:light/light.dart';
|
||||
import 'package:vibration/vibration.dart';
|
||||
import 'config.dart' show baseUrl;
|
||||
|
||||
// 🔦 조도 센서 판정 관련 튜닝 값 (nfc_poccket_checkin_screen.dart와 동일한 기준)
|
||||
const int _calibrationSampleCount = 3;
|
||||
const Duration _placePhoneDelay = Duration(seconds: 4);
|
||||
const int _luxJumpThreshold = 40;
|
||||
|
||||
const String pocketWatchNotificationChannelId = 'pocket_watch_channel';
|
||||
const int pocketWatchNotificationId = 9001;
|
||||
|
||||
final FlutterLocalNotificationsPlugin _localNotifications =
|
||||
FlutterLocalNotificationsPlugin();
|
||||
|
||||
/// 앱 시작(main()) 시 한 번만 호출. 알림 채널 생성 + 백그라운드 서비스 설정.
|
||||
Future<void> initializePocketWatchService() async {
|
||||
const AndroidNotificationChannel channel = AndroidNotificationChannel(
|
||||
pocketWatchNotificationChannelId,
|
||||
'주머니 감시 서비스',
|
||||
description: '휴대폰 무단 반출 감시 중 상태를 표시합니다.',
|
||||
importance: Importance.low, // 조용한 알림 (소리/진동 없음)
|
||||
);
|
||||
|
||||
await _localNotifications
|
||||
.resolvePlatformSpecificImplementation<
|
||||
AndroidFlutterLocalNotificationsPlugin
|
||||
>()
|
||||
?.createNotificationChannel(channel);
|
||||
|
||||
final service = FlutterBackgroundService();
|
||||
await service.configure(
|
||||
androidConfiguration: AndroidConfiguration(
|
||||
onStart: onPocketWatchServiceStart,
|
||||
autoStart: false, // NFC 태깅으로 출석 성공했을 때만 수동으로 시작
|
||||
autoStartOnBoot: false,
|
||||
isForegroundMode: true,
|
||||
notificationChannelId: pocketWatchNotificationChannelId,
|
||||
initialNotificationTitle: '🛡️ 주머니 감시 대기 중',
|
||||
initialNotificationContent: '출석 태깅을 기다리는 중입니다.',
|
||||
foregroundServiceNotificationId: pocketWatchNotificationId,
|
||||
foregroundServiceTypes: [AndroidForegroundType.specialUse],
|
||||
),
|
||||
iosConfiguration: IosConfiguration(autoStart: false),
|
||||
);
|
||||
}
|
||||
|
||||
@pragma('vm:entry-point')
|
||||
void onPocketWatchServiceStart(ServiceInstance service) {
|
||||
DartPluginRegistrant.ensureInitialized();
|
||||
|
||||
StreamSubscription<int>? lightSub;
|
||||
final List<int> calibrationSamples = [];
|
||||
int? baselineLux;
|
||||
String? studentId;
|
||||
String? studentName;
|
||||
String? pocketNumber;
|
||||
bool violated = false;
|
||||
|
||||
void updateNotification(String title, String content) {
|
||||
if (service is AndroidServiceInstance) {
|
||||
_localNotifications.show(
|
||||
id: pocketWatchNotificationId,
|
||||
title: title,
|
||||
body: content,
|
||||
notificationDetails: const NotificationDetails(
|
||||
android: AndroidNotificationDetails(
|
||||
pocketWatchNotificationChannelId,
|
||||
'주머니 감시 서비스',
|
||||
icon: 'ic_bg_service_small',
|
||||
ongoing: true,
|
||||
importance: Importance.low,
|
||||
priority: Priority.low,
|
||||
),
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
Future<void> reportViolation() async {
|
||||
// 진동 3연타로 강하게 경고 (백그라운드 서비스에서는 HapticFeedback이 아닌 vibration 패키지를 써야 동작함)
|
||||
Vibration.vibrate(pattern: [0, 400, 150, 400, 150, 400]);
|
||||
updateNotification("🚨 무단 반출 감지!", "[$pocketNumber] 주머니에서 폰이 감지되지 않습니다.");
|
||||
service.invoke('violation_detected', {
|
||||
"pocketNumber": pocketNumber,
|
||||
"timestamp": DateTime.now().toIso8601String(),
|
||||
});
|
||||
|
||||
try {
|
||||
await http.post(
|
||||
Uri.parse("$baseUrl/attendance/violation"),
|
||||
headers: {"Content-Type": "application/json"},
|
||||
body: jsonEncode({
|
||||
"studentId": studentId,
|
||||
"studentName": studentName,
|
||||
"pocketNumber": pocketNumber,
|
||||
"type": "PHONE_REMOVED",
|
||||
"timestamp": DateTime.now().toIso8601String(),
|
||||
}),
|
||||
);
|
||||
} catch (_) {
|
||||
// 백그라운드 서비스에서는 UI로 에러를 띄울 수 없으니 조용히 무시.
|
||||
// (기록 자체는 이미 로컬 알림+haptic으로 학생에게 전달됨)
|
||||
}
|
||||
}
|
||||
|
||||
void onLightReading(int luxValue) {
|
||||
if (violated) return;
|
||||
|
||||
if (baselineLux == null) {
|
||||
calibrationSamples.add(luxValue);
|
||||
if (calibrationSamples.length >= _calibrationSampleCount) {
|
||||
baselineLux =
|
||||
calibrationSamples.reduce((a, b) => a + b) ~/
|
||||
calibrationSamples.length;
|
||||
updateNotification("🛡️ 감시 중", "[$pocketNumber] 폰이 주머니 안에 있는지 감시 중");
|
||||
}
|
||||
return;
|
||||
}
|
||||
|
||||
if (luxValue > baselineLux! + _luxJumpThreshold) {
|
||||
violated = true;
|
||||
lightSub?.cancel();
|
||||
reportViolation();
|
||||
}
|
||||
}
|
||||
|
||||
Future<void> startWatching() async {
|
||||
lightSub?.cancel();
|
||||
calibrationSamples.clear();
|
||||
baselineLux = null;
|
||||
violated = false;
|
||||
|
||||
updateNotification("📥 주머니에 넣는 중...", "잠시 후 감시가 시작됩니다.");
|
||||
|
||||
await Future.delayed(_placePhoneDelay);
|
||||
|
||||
updateNotification("🔎 밝기 측정 중...", "주머니 속 밝기를 기준값으로 설정하는 중입니다.");
|
||||
|
||||
try {
|
||||
await Light().requestAuthorization();
|
||||
lightSub = Light().lightSensorStream.listen(
|
||||
onLightReading,
|
||||
onError: (Object error) {
|
||||
updateNotification("❌ 조도 센서 오류", "$error");
|
||||
},
|
||||
);
|
||||
} catch (e) {
|
||||
updateNotification("❌ 조도 센서 시작 실패", "$e");
|
||||
}
|
||||
}
|
||||
|
||||
service.on('start_watch').listen((event) {
|
||||
studentId = event?['studentId'];
|
||||
studentName = event?['studentName'];
|
||||
pocketNumber = event?['pocketNumber'];
|
||||
startWatching();
|
||||
});
|
||||
|
||||
service.on('stop_watch').listen((event) {
|
||||
lightSub?.cancel();
|
||||
service.stopSelf();
|
||||
});
|
||||
}
|
||||
Reference in New Issue
Block a user