feat: add device and background services

This commit is contained in:
Andrew 2026-06-17 04:01:32 +07:00
parent 32fd7da5be
commit 22d364893c
2 changed files with 206 additions and 0 deletions

View file

@ -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<void> _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<Map<String, dynamic>>();
final plugin = FlutterLocalNotificationsPlugin();
await plugin.initialize(settings: _buildInitSettings());
int notifId = 0;
for (final team in teams) {
final apps = (team['apps'] as List).cast<Map<String, dynamic>>();
for (final app in apps) {
final tracks = ['release', 'debug', 'profile'];
for (final trackName in tracks) {
final track = app[trackName] as Map<String, dynamic>?;
if (track == null) continue;
final current = track['current'] as Map<String, dynamic>?;
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<String?> _getInstalledVersion(String packageName) async {
const channel = MethodChannel('xyz.nuark.update_forge_companion/device');
try {
return await channel.invokeMethod<String>(
'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<void> initialize() async {
await _notifications.initialize(settings: _buildInitSettings());
await Workmanager().initialize(callbackDispatcher);
}
Future<void> schedulePeriodicCheck() async {
await Workmanager().registerPeriodicTask(
'update-check',
_taskCheckUpdates,
frequency: const Duration(hours: 1),
constraints: Constraints(
networkType: NetworkType.connected,
),
);
}
Future<void> cancelAll() async {
await Workmanager().cancelAll();
}
}

View file

@ -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<String?> getInstalledVersion(String packageName) async {
try {
final result = await _channel.invokeMethod<String>(
'getInstalledVersion',
{'packageName': packageName},
);
return result;
} on PlatformException {
return null;
}
}
Future<bool> installApk(String filePath) async {
try {
final result = await _channel.invokeMethod<bool>(
'installApk',
{'filePath': filePath},
);
return result ?? false;
} on PlatformException {
return false;
}
}
Future<bool> openApp(String packageName) async {
try {
final result = await _channel.invokeMethod<bool>(
'openApp',
{'packageName': packageName},
);
return result ?? false;
} on PlatformException {
return false;
}
}
Future<String> downloadFile({
required Stream<List<int>> 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;
}
}