Online tracking, deviceidentityservice
This commit is contained in:
@@ -0,0 +1,195 @@
|
||||
// Eszközazonosítás és eszközinformációk.
|
||||
//
|
||||
// Tárolás:
|
||||
// FlutterSecureStorage → appInstanceId (UUID), deviceLabel
|
||||
// MethodChannel → ANDROID_ID, rendszer eszköznév
|
||||
// device_info_plus → gyártó, modell, OS verzió
|
||||
// package_info_plus → app verzió
|
||||
|
||||
import 'dart:async';
|
||||
import 'dart:io';
|
||||
|
||||
import 'package:device_info_plus/device_info_plus.dart';
|
||||
import 'package:flutter/services.dart';
|
||||
import 'package:flutter_secure_storage/flutter_secure_storage.dart';
|
||||
import 'package:get/get.dart';
|
||||
import 'package:package_info_plus/package_info_plus.dart';
|
||||
import 'package:uuid/uuid.dart';
|
||||
|
||||
import '../models/device_info_model.dart';
|
||||
|
||||
class DeviceIdentityService extends GetxService {
|
||||
static DeviceIdentityService get to => Get.find();
|
||||
|
||||
// ── Konstansok ────────────────────────────────────────────────────
|
||||
|
||||
static const _channel = MethodChannel('hu.app_dev.terepi_seged/deviceInfo');
|
||||
|
||||
static const _storage = FlutterSecureStorage(
|
||||
aOptions: AndroidOptions(
|
||||
encryptedSharedPreferences: true, // Android Keystore alapú titkosítás
|
||||
),
|
||||
iOptions: IOSOptions(
|
||||
accessibility: KeychainAccessibility.first_unlock_this_device,
|
||||
),
|
||||
);
|
||||
|
||||
static const _keyInstanceId = 'device_app_instance_id';
|
||||
static const _keyLabel = 'device_label';
|
||||
|
||||
// ── Publikus mezők ────────────────────────────────────────────────
|
||||
|
||||
/// Statikus eszközinformációk — egyszer töltődik be, nem változik.
|
||||
late final DeviceInfoModel info;
|
||||
|
||||
/// Felhasználó által megadott eszköznév — reaktív, szerkeszthető.
|
||||
/// Alapértelmezett: rendszer eszköznév (pl. "Pista telefonja").
|
||||
final deviceLabel = ''.obs;
|
||||
|
||||
bool _isReady = false;
|
||||
bool get isReady => _isReady;
|
||||
|
||||
// ── Lifecycle ─────────────────────────────────────────────────────
|
||||
|
||||
@override
|
||||
Future<void> onReady() async {
|
||||
super.onReady();
|
||||
await _load();
|
||||
// Háttérben regisztrálás — nem blokkolja az UI-t
|
||||
unawaited(_registerDevice());
|
||||
_isReady = true;
|
||||
}
|
||||
|
||||
// ── Betöltés ──────────────────────────────────────────────────────
|
||||
|
||||
Future<void> _load() async {
|
||||
// Párhuzamos lekérdezések az indulás gyorsításához
|
||||
final results = await Future.wait([
|
||||
_getOrCreateInstanceId(),
|
||||
_getSystemDeviceName(),
|
||||
_getStoredLabel(),
|
||||
DeviceInfoPlugin().androidInfo,
|
||||
PackageInfo.fromPlatform(),
|
||||
]);
|
||||
|
||||
final instanceId = results[0] as String;
|
||||
final systemName = results[1] as String;
|
||||
final storedLabel = results[2] as String?;
|
||||
final android = results[3] as AndroidDeviceInfo;
|
||||
final pkg = results[4] as PackageInfo;
|
||||
|
||||
// ANDROID_ID — MethodChannel-en keresztül (Settings.Secure.ANDROID_ID)
|
||||
final androidId = await _getAndroidId() ?? android.fingerprint;
|
||||
print('AndroidId: $androidId');
|
||||
|
||||
info = DeviceInfoModel(
|
||||
deviceId: androidId,
|
||||
appInstanceId: instanceId,
|
||||
manufacturer: android.manufacturer,
|
||||
model: android.model,
|
||||
brand: android.brand,
|
||||
systemDeviceName: systemName,
|
||||
osVersion: android.version.release,
|
||||
sdkInt: android.version.sdkInt,
|
||||
securityPatch: android.version.securityPatch ?? '',
|
||||
appVersion: pkg.version,
|
||||
buildNumber: pkg.buildNumber,
|
||||
);
|
||||
|
||||
// Label: tárolt érték > rendszer neve
|
||||
deviceLabel.value = storedLabel ?? systemName;
|
||||
print('Device label: ${deviceLabel.value}');
|
||||
}
|
||||
|
||||
// ── SecureStorage műveletek ───────────────────────────────────────
|
||||
|
||||
Future<String> _getOrCreateInstanceId() async {
|
||||
final existing = await _storage.read(key: _keyInstanceId);
|
||||
if (existing != null) return existing;
|
||||
|
||||
final newId = const Uuid().v4();
|
||||
await _storage.write(key: _keyInstanceId, value: newId);
|
||||
return newId;
|
||||
}
|
||||
|
||||
Future<String?> _getStoredLabel() => _storage.read(key: _keyLabel);
|
||||
|
||||
// ── MethodChannel hívások ─────────────────────────────────────────
|
||||
|
||||
/// Settings.Secure.ANDROID_ID — egyedi, app-specifikus (Android 8+)
|
||||
Future<String?> _getAndroidId() async {
|
||||
try {
|
||||
return await _channel.invokeMethod<String>('getAndroidId');
|
||||
} catch (_) {
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
/// Settings.Global.DEVICE_NAME — felhasználó által adott eszköznév
|
||||
Future<String> _getSystemDeviceName() async {
|
||||
try {
|
||||
final name = await _channel.invokeMethod<String>('getAndroidDeviceName');
|
||||
if (name != null && name.isNotEmpty) return name;
|
||||
} catch (_) {}
|
||||
// Fallback: gyártó + modell
|
||||
if (Platform.isAndroid) {
|
||||
final a = await DeviceInfoPlugin().androidInfo;
|
||||
return '${a.manufacturer} ${a.model}';
|
||||
}
|
||||
return 'Eszköz';
|
||||
}
|
||||
|
||||
// ── Eszköznév beállítása ──────────────────────────────────────────
|
||||
|
||||
/// Felhasználó által megadott eszköznév mentése.
|
||||
/// Üres string esetén visszaáll a rendszer névhez.
|
||||
Future<void> setLabel(String label) async {
|
||||
final trimmed = label.trim();
|
||||
|
||||
if (trimmed.isEmpty) {
|
||||
// Visszaállás rendszer névre
|
||||
await _storage.delete(key: _keyLabel);
|
||||
deviceLabel.value = info.systemDeviceName;
|
||||
} else {
|
||||
await _storage.write(key: _keyLabel, value: trimmed);
|
||||
deviceLabel.value = trimmed;
|
||||
}
|
||||
|
||||
unawaited(_registerDevice());
|
||||
}
|
||||
|
||||
// ── Supabase regisztráció ─────────────────────────────────────────
|
||||
|
||||
Future<void> _registerDevice() async {
|
||||
// await Supabase.instance.client
|
||||
// .from('devices')
|
||||
// .upsert(
|
||||
// info.toRegistrationMap(label: deviceLabel.value),
|
||||
// onConflict: 'device_id',
|
||||
// );
|
||||
}
|
||||
|
||||
// ── Gyors elérők (kényelemért) ────────────────────────────────────
|
||||
|
||||
String get deviceId => _isReady ? info.deviceId : '';
|
||||
String get appInstanceId =>
|
||||
_isReady ? info.appInstanceId : ''; // ← _isReady guard hiányzott
|
||||
String get deviceLabelSync => deviceLabel.value; // ← ÚJ
|
||||
String get model => _isReady ? '${info.manufacturer} ${info.model}' : '';
|
||||
String get osInfo =>
|
||||
_isReady ? 'Android ${info.osVersion} (SDK ${info.sdkInt})' : '';
|
||||
String get appInfo =>
|
||||
_isReady ? '${info.appVersion}+${info.buildNumber}' : '';
|
||||
// ── Debug ─────────────────────────────────────────────────────────
|
||||
|
||||
@override
|
||||
String toString() => [
|
||||
'DeviceIdentityService',
|
||||
' deviceId: ${info.deviceId}',
|
||||
' instanceId: ${info.appInstanceId}',
|
||||
' label: ${deviceLabel.value}',
|
||||
' model: $model',
|
||||
' os: $osInfo',
|
||||
' app: $appInfo',
|
||||
].join('\n');
|
||||
}
|
||||
@@ -0,0 +1,257 @@
|
||||
// Kétszintű Supabase szinkronizáció:
|
||||
// 1. Élő pozíció — minden 3 mp-ben UPSERT → device_positions
|
||||
// 2. Track pontok — batch INSERT (10 pont vagy 8 mp) → terepi_track_points
|
||||
|
||||
import 'dart:async';
|
||||
|
||||
import 'package:connectivity_plus/connectivity_plus.dart';
|
||||
import 'package:get/get.dart';
|
||||
import 'package:latlong2/latlong.dart';
|
||||
import 'package:supabase_flutter/supabase_flutter.dart';
|
||||
|
||||
import 'app_database.dart';
|
||||
import 'device_identity_service.dart';
|
||||
import '../models/track.dart';
|
||||
|
||||
class TrackSyncService extends GetxService {
|
||||
static TrackSyncService get to => Get.find();
|
||||
|
||||
final _supabase = Supabase.instance.client;
|
||||
|
||||
// ── Konfiguráció ──────────────────────────────────────────────────
|
||||
static const _batchSize = 10;
|
||||
static const _batchIntervalSec = 8;
|
||||
static const _positionIntervalSec = 3;
|
||||
|
||||
// ── Belső állapot ─────────────────────────────────────────────────
|
||||
final _buffer = <TrackPoint>[];
|
||||
Timer? _batchTimer;
|
||||
Timer? _posTimer;
|
||||
|
||||
Track? _track;
|
||||
LatLng? _lastPos; // utoljára kapott pozíció (broadcasthoz)
|
||||
bool _online = false;
|
||||
|
||||
// ── Publikus állapot ──────────────────────────────────────────────
|
||||
final isSyncing = false.obs;
|
||||
final pendingCount = 0.obs;
|
||||
|
||||
// ── Lifecycle ─────────────────────────────────────────────────────
|
||||
|
||||
@override
|
||||
Future<void> onInit() async {
|
||||
super.onInit();
|
||||
_online = await _checkOnline();
|
||||
_listenConnectivity();
|
||||
}
|
||||
|
||||
@override
|
||||
void onClose() {
|
||||
_stopTimers();
|
||||
super.onClose();
|
||||
}
|
||||
|
||||
// ── Session vezérlés (TrackingController hívja) ───────────────────
|
||||
|
||||
void startSession(Track track) {
|
||||
_track = track;
|
||||
_buffer.clear();
|
||||
pendingCount.value = 0;
|
||||
|
||||
if (track.isLocalOnly) return;
|
||||
|
||||
_batchTimer = Timer.periodic(
|
||||
const Duration(seconds: _batchIntervalSec),
|
||||
(_) => _flush(),
|
||||
);
|
||||
_posTimer = Timer.periodic(
|
||||
const Duration(seconds: _positionIntervalSec),
|
||||
(_) => _broadcastPosition(),
|
||||
);
|
||||
}
|
||||
|
||||
Future<void> stopSession(Track track) async {
|
||||
_stopTimers();
|
||||
if (track.isLocalOnly) return;
|
||||
|
||||
await _flush(); // utolsó batch
|
||||
|
||||
// Track fejléc lezárása Supabase-ben
|
||||
if (track.supabaseId != null) {
|
||||
await _supabase.from('terepi_seged_tracks').update({
|
||||
'end_time': track.endTime?.toIso8601String(),
|
||||
'status': 'finished',
|
||||
'distance_m': track.distanceMeters,
|
||||
'point_count': track.pointCount,
|
||||
}).eq('id', track.supabaseId!);
|
||||
}
|
||||
|
||||
await _setInactive();
|
||||
_track = null;
|
||||
_lastPos = null;
|
||||
}
|
||||
|
||||
// ── Pont pufferelés ───────────────────────────────────────────────
|
||||
|
||||
/// TrackingController._onPosition() hívja minden pontnál
|
||||
void onNewPoint(TrackPoint point) {
|
||||
_lastPos = LatLng(point.latitude, point.longitude);
|
||||
|
||||
if (_track == null || _track!.isLocalOnly) return;
|
||||
|
||||
_buffer.add(point);
|
||||
pendingCount.value = _buffer.length;
|
||||
|
||||
if (_buffer.length >= _batchSize) _flush();
|
||||
}
|
||||
|
||||
// ── Supabase track létrehozása ────────────────────────────────────
|
||||
|
||||
/// startRecording()-ban hívandó, visszaadja a Supabase UUID-t
|
||||
Future<String?> createRemoteTrack(Track track) async {
|
||||
if (!_online) return null;
|
||||
try {
|
||||
final res = await _supabase
|
||||
.from('terepi_seged_tracks')
|
||||
.insert({
|
||||
'device_id': DeviceIdentityService.to.deviceId,
|
||||
'name': track.name,
|
||||
'source': track.source,
|
||||
'start_time': track.startTime.toIso8601String(),
|
||||
'status': 'recording',
|
||||
})
|
||||
.select('id')
|
||||
.single();
|
||||
return res['id'] as String?;
|
||||
} catch (e) {
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
// ── Batch feltöltés ───────────────────────────────────────────────
|
||||
|
||||
Future<void> _flush() async {
|
||||
if (_buffer.isEmpty || !_online) return;
|
||||
|
||||
final supabaseId = _track?.supabaseId;
|
||||
if (supabaseId == null) return;
|
||||
|
||||
final batch = List<TrackPoint>.from(_buffer);
|
||||
_buffer.clear();
|
||||
pendingCount.value = 0;
|
||||
|
||||
try {
|
||||
isSyncing.value = true;
|
||||
await _supabase.from('terepi_seged_track_points').insert(
|
||||
batch
|
||||
.map((p) => {
|
||||
'track_id': supabaseId,
|
||||
'latitude': p.latitude,
|
||||
'longitude': p.longitude,
|
||||
'altitude': p.altitude,
|
||||
'accuracy': p.accuracy,
|
||||
'speed': p.speed,
|
||||
'heading': p.heading,
|
||||
'timestamp': p.timestamp.toIso8601String(),
|
||||
})
|
||||
.toList(),
|
||||
);
|
||||
} catch (_) {
|
||||
// Hiba → visszateszi a bufferbe
|
||||
_buffer.insertAll(0, batch);
|
||||
pendingCount.value = _buffer.length;
|
||||
} finally {
|
||||
isSyncing.value = false;
|
||||
}
|
||||
}
|
||||
|
||||
// ── Élő pozíció broadcast ─────────────────────────────────────────
|
||||
|
||||
Future<void> _broadcastPosition() async {
|
||||
final pos = _lastPos;
|
||||
if (pos == null || !_online) return;
|
||||
|
||||
final device = DeviceIdentityService.to;
|
||||
try {
|
||||
await _supabase.from('terepi_seged_device_positions').upsert({
|
||||
'device_id': device.deviceId,
|
||||
'user_name': device.deviceLabel.value,
|
||||
'latitude': pos.latitude,
|
||||
'longitude': pos.longitude,
|
||||
'track_id': _track?.supabaseId,
|
||||
'is_active': true,
|
||||
'updated_at': DateTime.now().toUtc().toIso8601String(),
|
||||
}, onConflict: 'device_id');
|
||||
} catch (_) {}
|
||||
}
|
||||
|
||||
Future<void> _setInactive() async {
|
||||
final deviceId = DeviceIdentityService.to.deviceId;
|
||||
try {
|
||||
await _supabase.from('terepi_seged_device_positions').update(
|
||||
{'is_active': false, 'track_id': null}).eq('device_id', deviceId);
|
||||
} catch (_) {}
|
||||
}
|
||||
|
||||
// ── Offline → Online szinkron ─────────────────────────────────────
|
||||
|
||||
Future<void> syncTrack(Track track) async {
|
||||
if (!_online) return;
|
||||
|
||||
String? supabaseId = track.supabaseId;
|
||||
if (supabaseId == null) {
|
||||
supabaseId = await createRemoteTrack(track);
|
||||
if (supabaseId == null) return;
|
||||
final updated = track.copyWith(supabaseId: supabaseId);
|
||||
await AppDatabase.instance.updateTrack(updated);
|
||||
}
|
||||
|
||||
final points = await AppDatabase.instance.getPoints(track.id!);
|
||||
if (points.isEmpty) return;
|
||||
|
||||
const chunk = 100;
|
||||
for (int i = 0; i < points.length; i += chunk) {
|
||||
final slice = points.sublist(i, (i + chunk).clamp(0, points.length));
|
||||
await _supabase.from('terepi_seged_track_points').insert(
|
||||
slice
|
||||
.map((p) => {
|
||||
'track_id': supabaseId,
|
||||
'latitude': p.latitude,
|
||||
'longitude': p.longitude,
|
||||
'altitude': p.altitude,
|
||||
'accuracy': p.accuracy,
|
||||
'speed': p.speed,
|
||||
'heading': p.heading,
|
||||
'timestamp': p.timestamp.toIso8601String(),
|
||||
})
|
||||
.toList(),
|
||||
);
|
||||
}
|
||||
|
||||
await AppDatabase.instance.updateTrack(
|
||||
track.copyWith(supabaseId: supabaseId),
|
||||
);
|
||||
}
|
||||
|
||||
// ── Kapcsolat figyelés ────────────────────────────────────────────
|
||||
|
||||
void _listenConnectivity() {
|
||||
Connectivity().onConnectivityChanged.listen((results) async {
|
||||
final wasOffline = !_online;
|
||||
_online = results.any((r) => r != ConnectivityResult.none);
|
||||
if (wasOffline && _online) await _flush();
|
||||
});
|
||||
}
|
||||
|
||||
Future<bool> _checkOnline() async {
|
||||
final r = await Connectivity().checkConnectivity();
|
||||
return r.any((r) => r != ConnectivityResult.none);
|
||||
}
|
||||
|
||||
void _stopTimers() {
|
||||
_batchTimer?.cancel();
|
||||
_posTimer?.cancel();
|
||||
_batchTimer = null;
|
||||
_posTimer = null;
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user