feat: add device and background services
This commit is contained in:
parent
32fd7da5be
commit
22d364893c
2 changed files with 206 additions and 0 deletions
141
lib/services/background_service.dart
Normal file
141
lib/services/background_service.dart
Normal 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();
|
||||
}
|
||||
}
|
||||
65
lib/services/device_service.dart
Normal file
65
lib/services/device_service.dart
Normal 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;
|
||||
}
|
||||
}
|
||||
Loading…
Add table
Add a link
Reference in a new issue