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