diff --git a/lib/services/background_service.dart b/lib/services/background_service.dart new file mode 100644 index 0000000..16a7ba8 --- /dev/null +++ b/lib/services/background_service.dart @@ -0,0 +1,141 @@ +import 'package:flutter/services.dart'; +import 'package:flutter_secure_storage/flutter_secure_storage.dart'; +import 'package:workmanager/workmanager.dart'; +import 'package:flutter_local_notifications/flutter_local_notifications.dart'; +import 'dart:convert'; +import 'package:http/http.dart' as http; + +const _taskCheckUpdates = 'checkUpdates'; + +InitializationSettings _buildInitSettings() { + const androidSettings = AndroidInitializationSettings('@mipmap/ic_launcher'); + return const InitializationSettings(android: androidSettings); +} + +@pragma('vm:entry-point') +void callbackDispatcher() { + Workmanager().executeTask((task, inputData) async { + if (task == _taskCheckUpdates) { + await _checkForUpdates(); + } + return true; + }); +} + +Future _checkForUpdates() async { + const storage = FlutterSecureStorage(); + final serverUrl = await storage.read(key: 'server_url'); + final token = await storage.read(key: 'auth_token'); + + if (serverUrl == null || token == null) return; + + try { + final response = await http.get( + Uri.parse('$serverUrl/api/v1/apps'), + headers: { + 'Authorization': 'Bearer $token', + 'Accept': 'application/json', + }, + ); + + if (response.statusCode != 200) return; + + final data = jsonDecode(response.body); + final teams = (data['teams'] as List).cast>(); + + final plugin = FlutterLocalNotificationsPlugin(); + await plugin.initialize(settings: _buildInitSettings()); + + int notifId = 0; + for (final team in teams) { + final apps = (team['apps'] as List).cast>(); + for (final app in apps) { + final tracks = ['release', 'debug', 'profile']; + for (final trackName in tracks) { + final track = app[trackName] as Map?; + if (track == null) continue; + + final current = track['current'] as Map?; + if (current == null || current['hasFile'] != true) continue; + + final installedVersion = await _getInstalledVersion(app['packageName']); + if (installedVersion == null) continue; + + final apiVersion = current['version'] as String; + if (_isNewer(apiVersion, installedVersion)) { + await plugin.show( + id: notifId++, + title: 'Update available: ${app['title']}', + body: 'Version $apiVersion is available on $trackName track', + notificationDetails: NotificationDetails( + android: AndroidNotificationDetails( + 'update_available', + 'App Updates', + channelDescription: 'Notifications for available app updates', + importance: Importance.high, + priority: Priority.high, + ), + ), + payload: app['packageName'], + ); + } + } + } + } + } catch (_) {} +} + +Future _getInstalledVersion(String packageName) async { + const channel = MethodChannel('xyz.nuark.update_forge_companion/device'); + try { + return await channel.invokeMethod( + 'getInstalledVersion', + {'packageName': packageName}, + ); + } on PlatformException { + return null; + } +} + +bool _isNewer(String apiVersion, String installedVersion) { + final apiParts = apiVersion.split('.').map((s) => int.tryParse(s.split('-').first) ?? 0).toList(); + final installedParts = installedVersion.split('.').map((s) => int.tryParse(s.split('-').first) ?? 0).toList(); + + final maxLen = apiParts.length > installedParts.length + ? apiParts.length + : installedParts.length; + + for (var i = 0; i < maxLen; i++) { + final a = i < apiParts.length ? apiParts[i] : 0; + final b = i < installedParts.length ? installedParts[i] : 0; + if (a > b) return true; + if (a < b) return false; + } + return false; +} + +class BackgroundService { + final FlutterLocalNotificationsPlugin _notifications = + FlutterLocalNotificationsPlugin(); + + Future initialize() async { + await _notifications.initialize(settings: _buildInitSettings()); + + await Workmanager().initialize(callbackDispatcher); + } + + Future schedulePeriodicCheck() async { + await Workmanager().registerPeriodicTask( + 'update-check', + _taskCheckUpdates, + frequency: const Duration(hours: 1), + constraints: Constraints( + networkType: NetworkType.connected, + ), + ); + } + + Future cancelAll() async { + await Workmanager().cancelAll(); + } +} diff --git a/lib/services/device_service.dart b/lib/services/device_service.dart new file mode 100644 index 0000000..7aded4e --- /dev/null +++ b/lib/services/device_service.dart @@ -0,0 +1,65 @@ +import 'package:flutter/services.dart'; +import 'package:path_provider/path_provider.dart'; +import 'dart:io'; + +class DeviceService { + static const _channel = MethodChannel('xyz.nuark.update_forge_companion/device'); + + Future getInstalledVersion(String packageName) async { + try { + final result = await _channel.invokeMethod( + 'getInstalledVersion', + {'packageName': packageName}, + ); + return result; + } on PlatformException { + return null; + } + } + + Future installApk(String filePath) async { + try { + final result = await _channel.invokeMethod( + 'installApk', + {'filePath': filePath}, + ); + return result ?? false; + } on PlatformException { + return false; + } + } + + Future openApp(String packageName) async { + try { + final result = await _channel.invokeMethod( + 'openApp', + {'packageName': packageName}, + ); + return result ?? false; + } on PlatformException { + return false; + } + } + + Future downloadFile({ + required Stream> stream, + required int? contentLength, + required String fileName, + void Function(int received, int? total)? onProgress, + }) async { + final dir = await getTemporaryDirectory(); + final file = File('${dir.path}/$fileName'); + final sink = file.openWrite(); + + int received = 0; + await for (final chunk in stream) { + sink.add(chunk); + received += chunk.length; + onProgress?.call(received, contentLength); + } + await sink.flush(); + await sink.close(); + + return file.path; + } +}