436 lines
15 KiB
Dart
436 lines
15 KiB
Dart
// lib/services/background_service.dart
|
|
|
|
import 'dart:async';
|
|
import 'dart:io';
|
|
import 'dart:ui';
|
|
import 'package:flutter/foundation.dart' show kIsWeb, kDebugMode;
|
|
import 'package:flutter_background_service/flutter_background_service.dart';
|
|
import 'package:flutter_background_service_android/flutter_background_service_android.dart';
|
|
import 'package:firebase_core/firebase_core.dart';
|
|
import 'package:firebase_database/firebase_database.dart';
|
|
import 'package:cloud_firestore/cloud_firestore.dart';
|
|
import 'package:flutter_local_notifications/flutter_local_notifications.dart';
|
|
import 'package:shared_preferences/shared_preferences.dart';
|
|
import 'package:workmanager/workmanager.dart';
|
|
|
|
/// Inisialisasi plugin notifikasi
|
|
final FlutterLocalNotificationsPlugin _notifPlugin =
|
|
FlutterLocalNotificationsPlugin();
|
|
|
|
/// ID channel notifikasi
|
|
const String _channelId = 'rain_notification_channel';
|
|
|
|
/// Nama channel notifikasi
|
|
const String _channelName = 'Rain Notifications';
|
|
|
|
/// Deskripsi channel notifikasi
|
|
const String _channelDesc = 'Notifications for rain sensor updates';
|
|
|
|
/// Channel Android
|
|
const _channelIdAndroid = 'monitoring_hujan_channel';
|
|
const _channelNameAndroid = 'Monitoring Hujan';
|
|
const _channelDescAndroid = 'Notifikasi status hujan';
|
|
|
|
Future<void> saveToFirestoreIfNotDuplicate({
|
|
required double suhu,
|
|
required double kelembaban,
|
|
required double cahaya,
|
|
required double rainValue,
|
|
required String statusHujan,
|
|
required String statusCahaya,
|
|
}) async {
|
|
final now = DateTime.now();
|
|
final thirtySecondsAgo = now.subtract(const Duration(seconds: 30));
|
|
final query = await FirebaseFirestore.instance
|
|
.collection('sensor_history')
|
|
.where('timestamp', isGreaterThan: Timestamp.fromDate(thirtySecondsAgo))
|
|
.get();
|
|
if (query.docs.isEmpty) {
|
|
await FirebaseFirestore.instance.collection('sensor_history').add({
|
|
'temperature': suhu,
|
|
'humidity': kelembaban,
|
|
'light': cahaya,
|
|
'rain': rainValue,
|
|
'rain_status': statusHujan,
|
|
'weather_status': statusCahaya,
|
|
'timestamp': FieldValue.serverTimestamp(),
|
|
});
|
|
print('✅ Data tersimpan ke Firestore (background)');
|
|
} else {
|
|
print('⏩ Data sudah ada, tidak disimpan ulang (anti duplikat, background)');
|
|
}
|
|
}
|
|
|
|
@pragma('vm:entry-point')
|
|
Future<void> initializeService() async {
|
|
if (kIsWeb) return;
|
|
|
|
// 1) Setup channel & plugin notifikasi
|
|
const androidSetup = AndroidInitializationSettings('@mipmap/ic_launcher');
|
|
final initSettings = InitializationSettings(android: androidSetup);
|
|
await _notifPlugin.initialize(initSettings);
|
|
|
|
// Hapus channel lama jika ada
|
|
await _notifPlugin
|
|
.resolvePlatformSpecificImplementation<
|
|
AndroidFlutterLocalNotificationsPlugin>()
|
|
?.deleteNotificationChannel(_channelIdAndroid);
|
|
|
|
// Buat channel baru dengan deskripsi yang diperbarui
|
|
await _notifPlugin
|
|
.resolvePlatformSpecificImplementation<
|
|
AndroidFlutterLocalNotificationsPlugin>()
|
|
?.createNotificationChannel(
|
|
const AndroidNotificationChannel(
|
|
_channelIdAndroid,
|
|
_channelNameAndroid,
|
|
description: _channelDescAndroid,
|
|
importance: Importance.max,
|
|
enableVibration: true,
|
|
enableLights: true,
|
|
showBadge: true,
|
|
),
|
|
);
|
|
|
|
// 2) Konfigurasikan service
|
|
final service = FlutterBackgroundService();
|
|
await service.configure(
|
|
androidConfiguration: AndroidConfiguration(
|
|
onStart: onStart,
|
|
autoStart: true,
|
|
isForegroundMode: true,
|
|
notificationChannelId: _channelIdAndroid,
|
|
initialNotificationTitle: 'Monitoring Hujan',
|
|
initialNotificationContent: 'Memulai monitoring...',
|
|
foregroundServiceNotificationId: 888,
|
|
),
|
|
iosConfiguration: IosConfiguration(
|
|
autoStart: true,
|
|
onForeground: onStart,
|
|
onBackground: onIosBackground,
|
|
),
|
|
);
|
|
|
|
// Mulai service
|
|
await service.startService();
|
|
}
|
|
|
|
@pragma('vm:entry-point')
|
|
Future<bool> onIosBackground(ServiceInstance service) async {
|
|
return true;
|
|
}
|
|
|
|
@pragma('vm:entry-point')
|
|
void onStart(ServiceInstance service) async {
|
|
if (kIsWeb) return;
|
|
|
|
print('🚀 Background service dimulai...');
|
|
|
|
// Inisialisasi WorkManager
|
|
await Workmanager().initialize(callbackDispatcher);
|
|
|
|
// Inisialisasi SharedPreferences
|
|
final prefs = await SharedPreferences.getInstance();
|
|
int saveInterval = prefs.getInt('saveInterval') ?? 1; // menit
|
|
int remainingSeconds =
|
|
prefs.getInt('remainingSeconds') ?? (saveInterval * 60);
|
|
DateTime? lastSaveTime = prefs.getString('lastSaveTime') != null
|
|
? DateTime.parse(prefs.getString('lastSaveTime')!)
|
|
: null;
|
|
|
|
// --- Tambahkan variabel untuk menyimpan rain sebelumnya ---
|
|
double? previousRainValue;
|
|
|
|
// Inisialisasi Firebase jika belum ada
|
|
try {
|
|
if (Firebase.apps.isEmpty) {
|
|
await Firebase.initializeApp(
|
|
options: const FirebaseOptions(
|
|
apiKey: 'AIzaSyBoYp4GpkwF-aMPHVW1gs7PmOj5ucF4xZs',
|
|
appId: '1:289397695071:android:f6e034034faf366003b1ea',
|
|
messagingSenderId: '289397695071',
|
|
projectId: 'monitoring-hujan',
|
|
databaseURL: 'https://monitoring-hujan-default-rtdb.firebaseio.com',
|
|
),
|
|
);
|
|
print('✅ Firebase berhasil diinisialisasi di background service');
|
|
|
|
// Ambil interval dari Firestore
|
|
try {
|
|
final settingsDoc = await FirebaseFirestore.instance
|
|
.collection('settings')
|
|
.doc('save_interval')
|
|
.get();
|
|
|
|
if (settingsDoc.exists) {
|
|
saveInterval = settingsDoc.data()?['interval'] ?? 1;
|
|
await prefs.setInt('saveInterval', saveInterval);
|
|
print(
|
|
'✅ Interval berhasil diambil dari Firestore: $saveInterval menit');
|
|
} else {
|
|
// Buat dokumen settings jika belum ada
|
|
await FirebaseFirestore.instance
|
|
.collection('settings')
|
|
.doc('save_interval')
|
|
.set({
|
|
'interval': saveInterval,
|
|
'last_updated': FieldValue.serverTimestamp(),
|
|
});
|
|
print(
|
|
'✅ Dokumen settings dibuat dengan interval default: $saveInterval menit');
|
|
}
|
|
} catch (e) {
|
|
print('⚠️ Gagal mengambil interval dari Firestore: $e');
|
|
}
|
|
}
|
|
} catch (e) {
|
|
print('❌ Error inisialisasi Firebase: $e');
|
|
if (service is AndroidServiceInstance) {
|
|
await service.setForegroundNotificationInfo(
|
|
title: 'Monitoring Hujan',
|
|
content: 'Error inisialisasi aplikasi',
|
|
);
|
|
}
|
|
return;
|
|
}
|
|
|
|
// Hitung sisa waktu jika ada lastSaveTime
|
|
if (lastSaveTime != null) {
|
|
final now = DateTime.now();
|
|
final difference = now.difference(lastSaveTime).inSeconds;
|
|
remainingSeconds = (saveInterval * 60) - difference;
|
|
if (remainingSeconds < 0) remainingSeconds = 0;
|
|
}
|
|
|
|
// Pastikan ini tetap demi foreground service
|
|
if (service is AndroidServiceInstance) {
|
|
await service.setAsForegroundService();
|
|
print('✅ Service diatur sebagai foreground service');
|
|
}
|
|
|
|
// Update notifikasi awal
|
|
if (service is AndroidServiceInstance) {
|
|
final minutes = (remainingSeconds ~/ 60).toString().padLeft(2, '0');
|
|
final seconds = (remainingSeconds % 60).toString().padLeft(2, '0');
|
|
await service.setForegroundNotificationInfo(
|
|
title: 'Monitoring Hujan',
|
|
content: 'Mengambil data sensor... Next save: $minutes:$seconds',
|
|
);
|
|
print('📱 Notifikasi awal diperbarui');
|
|
}
|
|
|
|
// Timer untuk menyimpan data ke history
|
|
Timer? saveTimer;
|
|
|
|
// Fungsi untuk mencoba koneksi ulang
|
|
Future<void> retryConnection() async {
|
|
try {
|
|
final dbRef = FirebaseDatabase.instance.ref();
|
|
await dbRef.get();
|
|
print('✅ Koneksi Firebase berhasil');
|
|
return;
|
|
} catch (e) {
|
|
print('❌ Gagal koneksi: $e');
|
|
if (service is AndroidServiceInstance) {
|
|
await service.setForegroundNotificationInfo(
|
|
title: 'Monitoring Hujan',
|
|
content: 'Mencoba menghubungkan kembali...',
|
|
);
|
|
}
|
|
// Tunggu 5 detik sebelum mencoba lagi
|
|
await Future.delayed(const Duration(seconds: 5));
|
|
return retryConnection();
|
|
}
|
|
}
|
|
|
|
// Fungsi untuk setup listener
|
|
void setupListener(DatabaseReference dbRef) {
|
|
dbRef.onValue.listen(
|
|
(event) async {
|
|
try {
|
|
if (event.snapshot.value != null) {
|
|
final data = event.snapshot.value as Map<dynamic, dynamic>;
|
|
final suhu = double.parse(data['temperature']?.toString() ?? '0');
|
|
final kelembaban =
|
|
double.parse(data['humidity']?.toString() ?? '0');
|
|
final cahaya = double.parse(data['ldr']?.toString() ?? '0');
|
|
final rainValue = double.parse(data['rain']?.toString() ?? '0');
|
|
final statusHujan =
|
|
data['statusHujan']?.toString() ?? _getStatusHujan(rainValue);
|
|
final statusCahaya =
|
|
data['statusCahaya']?.toString() ?? _getStatusCuaca(cahaya);
|
|
|
|
print('📊 Data diperbarui:');
|
|
print(' - Suhu: $suhu°C');
|
|
print(' - Kelembaban: $kelembaban%');
|
|
print(' - Cahaya: $cahaya');
|
|
print(' - Rain value: $rainValue');
|
|
print(' - Status Hujan: $statusHujan');
|
|
print(' - Status Cahaya: $statusCahaya');
|
|
|
|
// Simpan ke Firestore setiap interval
|
|
if (remainingSeconds <= 0) {
|
|
await saveToFirestoreIfNotDuplicate(
|
|
suhu: suhu,
|
|
kelembaban: kelembaban,
|
|
cahaya: cahaya,
|
|
rainValue: rainValue,
|
|
statusHujan: statusHujan,
|
|
statusCahaya: statusCahaya,
|
|
);
|
|
}
|
|
|
|
// --- Update notifikasi hanya jika rain berubah ---
|
|
if (previousRainValue == null || previousRainValue != rainValue) {
|
|
previousRainValue = rainValue;
|
|
if (service is AndroidServiceInstance) {
|
|
final minutes =
|
|
(remainingSeconds ~/ 60).toString().padLeft(2, '0');
|
|
final seconds =
|
|
(remainingSeconds % 60).toString().padLeft(2, '0');
|
|
await service.setForegroundNotificationInfo(
|
|
title: 'Status Hujan: ${_getStatusHujan(rainValue)}',
|
|
content: 'Curah Hujan: ${rainValue.toStringAsFixed(0)}',
|
|
);
|
|
print('🔔 Notifikasi diperbarui karena rain berubah');
|
|
}
|
|
}
|
|
// --- END ---
|
|
}
|
|
} catch (e) {
|
|
print('❌ Error memproses data: $e');
|
|
if (service is AndroidServiceInstance) {
|
|
await service.setForegroundNotificationInfo(
|
|
title: 'Monitoring Hujan',
|
|
content: 'Error memproses data sensor',
|
|
);
|
|
}
|
|
}
|
|
},
|
|
onError: (error) {
|
|
print('❌ Error pada listener: $error');
|
|
if (service is AndroidServiceInstance) {
|
|
service.setForegroundNotificationInfo(
|
|
title: 'Monitoring Hujan',
|
|
content: 'Mencoba menghubungkan kembali...',
|
|
);
|
|
}
|
|
// Coba setup ulang listener setelah error
|
|
Future.delayed(const Duration(seconds: 5), () {
|
|
setupListener(FirebaseDatabase.instance.ref());
|
|
});
|
|
},
|
|
cancelOnError: false,
|
|
);
|
|
}
|
|
|
|
// Mulai proses koneksi
|
|
try {
|
|
print('🔄 Mencoba menghubungkan ke Firebase...');
|
|
await retryConnection();
|
|
|
|
// Setup listener setelah koneksi berhasil
|
|
final dbRef = FirebaseDatabase.instance.ref();
|
|
setupListener(dbRef);
|
|
|
|
// Setup timer untuk countdown
|
|
saveTimer = Timer.periodic(const Duration(seconds: 1), (timer) async {
|
|
if (remainingSeconds > 0) {
|
|
remainingSeconds--;
|
|
await prefs.setInt('remainingSeconds', remainingSeconds);
|
|
}
|
|
});
|
|
} catch (e) {
|
|
print('❌ Error fatal: $e');
|
|
if (service is AndroidServiceInstance) {
|
|
await service.setForegroundNotificationInfo(
|
|
title: 'Monitoring Hujan',
|
|
content: 'Error koneksi ke server',
|
|
);
|
|
}
|
|
// Coba koneksi ulang setelah error fatal
|
|
Future.delayed(const Duration(seconds: 5), () {
|
|
onStart(service);
|
|
});
|
|
}
|
|
|
|
// Cleanup saat service dihentikan
|
|
service.on('stopService').listen((event) {
|
|
saveTimer?.cancel();
|
|
print('🛑 Service dihentikan');
|
|
});
|
|
}
|
|
|
|
/// Helper menentukan status hujan
|
|
String _getStatusHujan(double rainValue) {
|
|
// Status hujan berdasarkan nilai sensor
|
|
return (rainValue < 500) ? "Hujan" : "Tidak Hujan";
|
|
}
|
|
|
|
/// Helper menentukan status cuaca
|
|
String _getStatusCuaca(double cahaya) {
|
|
// Status cahaya berdasarkan nilai LDR
|
|
return (cahaya < 700) ? "Terang" : "Gelap";
|
|
}
|
|
|
|
Future<void> _updateNotification(
|
|
String title, String content, String status) async {
|
|
// Implementasi untuk mengupdate notifikasi
|
|
}
|
|
|
|
@pragma('vm:entry-point')
|
|
void callbackDispatcher() {
|
|
Workmanager().executeTask((task, inputData) async {
|
|
print('🔄 Background task dijalankan: $task');
|
|
|
|
if (task == 'saveData') {
|
|
try {
|
|
// Inisialisasi Firebase jika belum ada
|
|
if (Firebase.apps.isEmpty) {
|
|
await Firebase.initializeApp(
|
|
options: const FirebaseOptions(
|
|
apiKey: 'AIzaSyBoYp4GpkwF-aMPHVW1gs7PmOj5ucF4xZs',
|
|
appId: '1:289397695071:android:f6e034034faf366003b1ea',
|
|
messagingSenderId: '289397695071',
|
|
projectId: 'monitoring-hujan',
|
|
databaseURL:
|
|
'https://monitoring-hujan-default-rtdb.firebaseio.com',
|
|
),
|
|
);
|
|
}
|
|
|
|
// Ambil data dari Firebase
|
|
final dbRef = FirebaseDatabase.instance.ref();
|
|
final snapshot = await dbRef.get();
|
|
|
|
if (snapshot.value != null) {
|
|
final data = snapshot.value as Map<dynamic, dynamic>;
|
|
final suhu = double.parse(data['temperature']?.toString() ?? '0');
|
|
final kelembaban = double.parse(data['humidity']?.toString() ?? '0');
|
|
final cahaya = double.parse(data['ldr']?.toString() ?? '0');
|
|
final rainValue = double.parse(data['rain']?.toString() ?? '0');
|
|
final statusHujan =
|
|
data['statusHujan']?.toString() ?? _getStatusHujan(rainValue);
|
|
final statusCahaya =
|
|
data['statusCahaya']?.toString() ?? _getStatusCuaca(cahaya);
|
|
|
|
// Simpan ke Firestore dengan anti-duplikat
|
|
await saveToFirestoreIfNotDuplicate(
|
|
suhu: suhu,
|
|
kelembaban: kelembaban,
|
|
cahaya: cahaya,
|
|
rainValue: rainValue,
|
|
statusHujan: statusHujan,
|
|
statusCahaya: statusCahaya,
|
|
);
|
|
print('✅ Data berhasil disimpan dari background task');
|
|
}
|
|
} catch (e) {
|
|
print('❌ Error dalam background task: $e');
|
|
}
|
|
}
|
|
|
|
return Future.value(true);
|
|
});
|
|
}
|