mirror of
https://github.com/dz0ny/meshcore-sar.git
synced 2026-08-11 16:30:28 +00:00
feat: Implement update checker and dialog for available app updates with localization support
This commit is contained in:
54
lib/services/build_info_service.dart
Normal file
54
lib/services/build_info_service.dart
Normal file
@@ -0,0 +1,54 @@
|
||||
import 'dart:io' show Platform;
|
||||
import 'package:flutter/services.dart';
|
||||
import 'package:flutter/foundation.dart';
|
||||
|
||||
/// Service for accessing build information from native platform code
|
||||
/// Currently supports Android only - returns "unknown" for other platforms
|
||||
class BuildInfoService {
|
||||
static final BuildInfoService _instance = BuildInfoService._internal();
|
||||
factory BuildInfoService() => _instance;
|
||||
BuildInfoService._internal();
|
||||
|
||||
static const MethodChannel _channel = MethodChannel('com.meshcore.sar/build_info');
|
||||
|
||||
String? _cachedCommitHash;
|
||||
|
||||
/// Get the commit hash that was embedded during build time
|
||||
/// Returns "unknown" if:
|
||||
/// - Not running on Android
|
||||
/// - Platform channel call fails
|
||||
/// - Build was not configured with COMMIT_HASH
|
||||
Future<String> getCommitHash() async {
|
||||
// Return cached value if available
|
||||
if (_cachedCommitHash != null) {
|
||||
return _cachedCommitHash!;
|
||||
}
|
||||
|
||||
// Only Android has the platform channel implementation
|
||||
if (!Platform.isAndroid) {
|
||||
debugPrint('[BuildInfoService] Not on Android platform, returning "unknown"');
|
||||
_cachedCommitHash = 'unknown';
|
||||
return _cachedCommitHash!;
|
||||
}
|
||||
|
||||
try {
|
||||
final String commitHash = await _channel.invokeMethod('getCommitHash');
|
||||
_cachedCommitHash = commitHash;
|
||||
debugPrint('[BuildInfoService] Commit hash: $commitHash');
|
||||
return commitHash;
|
||||
} on PlatformException catch (e) {
|
||||
debugPrint('[BuildInfoService] Failed to get commit hash: ${e.message}');
|
||||
_cachedCommitHash = 'unknown';
|
||||
return _cachedCommitHash!;
|
||||
} catch (e) {
|
||||
debugPrint('[BuildInfoService] Unexpected error getting commit hash: $e');
|
||||
_cachedCommitHash = 'unknown';
|
||||
return _cachedCommitHash!;
|
||||
}
|
||||
}
|
||||
|
||||
/// Clear cached commit hash (useful for testing)
|
||||
void clearCache() {
|
||||
_cachedCommitHash = null;
|
||||
}
|
||||
}
|
||||
135
lib/services/update_checker_service.dart
Normal file
135
lib/services/update_checker_service.dart
Normal file
@@ -0,0 +1,135 @@
|
||||
import 'dart:convert';
|
||||
import 'package:flutter/foundation.dart';
|
||||
import 'package:http/http.dart' as http;
|
||||
import '../models/update_info.dart';
|
||||
import 'build_info_service.dart';
|
||||
|
||||
/// Service for checking if a new app version is available
|
||||
/// Compares current build's commit hash with latest manifest from server
|
||||
class UpdateCheckerService {
|
||||
static final UpdateCheckerService _instance = UpdateCheckerService._internal();
|
||||
factory UpdateCheckerService() => _instance;
|
||||
UpdateCheckerService._internal();
|
||||
|
||||
final BuildInfoService _buildInfoService = BuildInfoService();
|
||||
|
||||
// Manifest URL for the latest unstable build
|
||||
static const String _manifestUrl = 'https://meshcore-sar.dz0ny.dev/unstable/latest/manifest.json';
|
||||
|
||||
/// Check if an update is available
|
||||
/// Returns UpdateInfo with availability status and download URL if available
|
||||
Future<UpdateInfo> checkForUpdate() async {
|
||||
try {
|
||||
// Get current build's commit hash
|
||||
final currentCommitHash = await _buildInfoService.getCommitHash();
|
||||
|
||||
// Skip check for dev builds (local development)
|
||||
if (currentCommitHash == 'dev' || currentCommitHash == 'unknown') {
|
||||
debugPrint('[UpdateChecker] Skipping update check for dev/unknown build');
|
||||
return UpdateInfo.noUpdate(currentCommitHash);
|
||||
}
|
||||
|
||||
debugPrint('[UpdateChecker] Current commit hash: $currentCommitHash');
|
||||
debugPrint('[UpdateChecker] Fetching latest manifest from: $_manifestUrl');
|
||||
|
||||
// Fetch manifest from server
|
||||
final response = await http.get(
|
||||
Uri.parse(_manifestUrl),
|
||||
headers: {'Accept': 'application/json'},
|
||||
).timeout(
|
||||
const Duration(seconds: 10),
|
||||
onTimeout: () {
|
||||
debugPrint('[UpdateChecker] Manifest fetch timed out');
|
||||
throw Exception('Manifest fetch timed out');
|
||||
},
|
||||
);
|
||||
|
||||
if (response.statusCode != 200) {
|
||||
debugPrint('[UpdateChecker] Failed to fetch manifest: ${response.statusCode}');
|
||||
return UpdateInfo.noUpdate(currentCommitHash);
|
||||
}
|
||||
|
||||
// Parse manifest JSON
|
||||
final Map<String, dynamic> manifest = json.decode(response.body);
|
||||
final latestCommitHash = manifest['commit'] as String?;
|
||||
final commitShort = manifest['commit_short'] as String?;
|
||||
final buildId = manifest['build_id'] as String?;
|
||||
final timestamp = manifest['timestamp'] as String?;
|
||||
final artifacts = manifest['artifacts'] as List<dynamic>?;
|
||||
|
||||
if (latestCommitHash == null || commitShort == null) {
|
||||
debugPrint('[UpdateChecker] Invalid manifest: missing commit information');
|
||||
return UpdateInfo.noUpdate(currentCommitHash);
|
||||
}
|
||||
|
||||
debugPrint('[UpdateChecker] Latest commit hash: $latestCommitHash');
|
||||
debugPrint('[UpdateChecker] Latest commit short: $commitShort');
|
||||
|
||||
// Compare commit hashes
|
||||
// Current hash might be full SHA or short (7 chars)
|
||||
// Latest from manifest is full SHA
|
||||
final isUpdateAvailable = !_compareCommitHashes(currentCommitHash, latestCommitHash);
|
||||
|
||||
if (!isUpdateAvailable) {
|
||||
debugPrint('[UpdateChecker] No update available (same commit)');
|
||||
return UpdateInfo.noUpdate(currentCommitHash);
|
||||
}
|
||||
|
||||
// Find Android APK in artifacts
|
||||
final String? apkUrl = _findAndroidApkUrl(artifacts);
|
||||
|
||||
if (apkUrl == null) {
|
||||
debugPrint('[UpdateChecker] Update available but no APK found in artifacts');
|
||||
return UpdateInfo.noUpdate(currentCommitHash);
|
||||
}
|
||||
|
||||
debugPrint('[UpdateChecker] Update available! APK URL: $apkUrl');
|
||||
|
||||
return UpdateInfo.available(
|
||||
currentCommitHash: currentCommitHash,
|
||||
latestCommitHash: commitShort,
|
||||
downloadUrl: apkUrl,
|
||||
buildId: buildId,
|
||||
timestamp: timestamp,
|
||||
);
|
||||
} catch (e) {
|
||||
debugPrint('[UpdateChecker] Error checking for update: $e');
|
||||
// Return no update on error to avoid disrupting app startup
|
||||
final currentCommitHash = await _buildInfoService.getCommitHash();
|
||||
return UpdateInfo.noUpdate(currentCommitHash);
|
||||
}
|
||||
}
|
||||
|
||||
/// Compare two commit hashes (handles both full SHA and short format)
|
||||
bool _compareCommitHashes(String current, String latest) {
|
||||
// Normalize to lowercase for comparison
|
||||
final currentLower = current.toLowerCase();
|
||||
final latestLower = latest.toLowerCase();
|
||||
|
||||
// Direct match
|
||||
if (currentLower == latestLower) return true;
|
||||
|
||||
// Check if current is short form of latest
|
||||
if (latestLower.startsWith(currentLower)) return true;
|
||||
|
||||
// Check if latest is short form of current
|
||||
if (currentLower.startsWith(latestLower)) return true;
|
||||
|
||||
return false;
|
||||
}
|
||||
|
||||
/// Find Android APK URL in artifacts list
|
||||
String? _findAndroidApkUrl(List<dynamic>? artifacts) {
|
||||
if (artifacts == null || artifacts.isEmpty) return null;
|
||||
|
||||
// Look for .apk file in artifacts
|
||||
for (final artifact in artifacts) {
|
||||
if (artifact is String && artifact.toLowerCase().endsWith('.apk')) {
|
||||
// Construct full URL
|
||||
return 'https://meshcore-sar.dz0ny.dev/unstable/latest/$artifact';
|
||||
}
|
||||
}
|
||||
|
||||
return null;
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user