import 'dart:async'; import 'dart:io'; import 'package:flutter/material.dart'; import 'package:flutter/services.dart'; import 'package:image_picker/image_picker.dart'; import 'package:path_provider/path_provider.dart'; import 'package:path/path.dart' as path; import 'package:image/image.dart' as img; import 'package:geolocator/geolocator.dart'; import 'package:permission_handler/permission_handler.dart'; import 'package:flutter_bluetooth_serial/flutter_bluetooth_serial.dart'; import 'package:usb_serial/usb_serial.dart'; import 'location_service.dart'; import 'marine_api_service.dart'; import '../models/in_situ_sampling_data.dart'; import '../bluetooth/bluetooth_manager.dart'; import '../serial/serial_manager.dart'; /// A dedicated service to handle all business logic for the In-Situ Sampling feature. /// This includes location, image processing, device communication, and data submission. class InSituSamplingService { final LocationService _locationService = LocationService(); final MarineApiService _marineApiService = MarineApiService(); final BluetoothManager _bluetoothManager = BluetoothManager(); final SerialManager _serialManager = SerialManager(); // This channel name MUST match the one defined in MainActivity.kt static const platform = MethodChannel('com.example.environment_monitoring_app/usb'); // --- Location Services --- Future getCurrentLocation() => _locationService.getCurrentLocation(); double calculateDistance(double lat1, double lon1, double lat2, double lon2) => _locationService.calculateDistance(lat1, lon1, lat2, lon2); // --- Image Processing --- Future pickAndProcessImage(ImageSource source, { required InSituSamplingData data, required String imageInfo, bool isRequired = false, }) async { final picker = ImagePicker(); final XFile? photo = await picker.pickImage(source: source, imageQuality: 85, maxWidth: 1024); if (photo == null) return null; final bytes = await photo.readAsBytes(); img.Image? originalImage = img.decodeImage(bytes); if (originalImage == null) return null; if (isRequired && originalImage.height > originalImage.width) { debugPrint("Image rejected: Must be in landscape orientation."); return null; } final String watermarkTimestamp = "${data.samplingDate} ${data.samplingTime}"; final font = img.arial24; final textWidth = watermarkTimestamp.length * 12; img.fillRect(originalImage, x1: 5, y1: 5, x2: textWidth + 15, y2: 35, color: img.ColorRgb8(255, 255, 255)); img.drawString(originalImage, watermarkTimestamp, font: font, x: 10, y: 10, color: img.ColorRgb8(0, 0, 0)); final tempDir = await getTemporaryDirectory(); final stationCode = data.selectedStation?['man_station_code'] ?? 'NA'; final fileTimestamp = "${data.samplingDate}-${data.samplingTime}".replaceAll(':', '-'); final newFileName = "${stationCode}_${fileTimestamp}_${imageInfo.replaceAll(' ', '')}.jpg"; final filePath = path.join(tempDir.path, newFileName); return File(filePath)..writeAsBytesSync(img.encodeJpg(originalImage)); } // --- Device Connection (Delegated to Managers) --- ValueNotifier get bluetoothConnectionState => _bluetoothManager.connectionState; ValueNotifier get serialConnectionState => _serialManager.connectionState; // REPAIRED: This getter now dynamically returns the correct Sonde ID notifier // based on the active connection, which is essential for the UI. ValueNotifier get sondeId { if (_bluetoothManager.connectionState.value != BluetoothConnectionState.disconnected) { return _bluetoothManager.sondeId; } return _serialManager.sondeId; } Stream> get bluetoothDataStream => _bluetoothManager.dataStream; Stream> get serialDataStream => _serialManager.dataStream; // REPAIRED: Added .value to both getters for consistency and to prevent errors. String? get connectedBluetoothDeviceName => _bluetoothManager.connectedDeviceName.value; String? get connectedSerialDeviceName => _serialManager.connectedDeviceName.value; // --- Permissions --- Future requestDevicePermissions() async { Map statuses = await [ Permission.bluetoothScan, Permission.bluetoothConnect, Permission.locationWhenInUse, ].request(); // Return true only if the essential permissions are granted. if (statuses[Permission.bluetoothScan] == PermissionStatus.granted && statuses[Permission.bluetoothConnect] == PermissionStatus.granted) { return true; } else { return false; } } // --- Bluetooth Methods --- Future> getPairedBluetoothDevices() => _bluetoothManager.getPairedDevices(); Future connectToBluetoothDevice(BluetoothDevice device) => _bluetoothManager.connect(device); void disconnectFromBluetooth() => _bluetoothManager.disconnect(); void startBluetoothAutoReading({Duration? interval}) => _bluetoothManager.startAutoReading(interval: interval ?? const Duration(seconds: 5)); void stopBluetoothAutoReading() => _bluetoothManager.stopAutoReading(); // --- USB Serial Methods --- Future> getAvailableSerialDevices() => _serialManager.getAvailableDevices(); Future requestUsbPermission(UsbDevice device) async { try { return await platform.invokeMethod('requestUsbPermission', {'vid': device.vid, 'pid': device.pid}) ?? false; } on PlatformException catch (e) { debugPrint("Failed to request USB permission: '${e.message}'."); return false; } } Future connectToSerialDevice(UsbDevice device) async { final bool permissionGranted = await requestUsbPermission(device); if (permissionGranted) { await _serialManager.connect(device); } else { throw Exception("USB permission was not granted."); } } void disconnectFromSerial() => _serialManager.disconnect(); void startSerialAutoReading({Duration? interval}) => _serialManager.startAutoReading(interval: interval ?? const Duration(seconds: 5)); void stopSerialAutoReading() => _serialManager.stopAutoReading(); void dispose() { _bluetoothManager.dispose(); _serialManager.dispose(); } // --- Data Submission --- // MODIFIED: Method now requires the appSettings list to pass to the MarineApiService. Future> submitData(InSituSamplingData data, List>? appSettings) { return _marineApiService.submitInSituSample( formData: data.toApiFormData(), imageFiles: data.toApiImageFiles(), inSituData: data, appSettings: appSettings, // Added this required parameter ); } }