import 'dart:convert'; import 'dart:io'; import '../models/update_info.dart'; class UpdateException implements Exception { UpdateException(this.message); final String message; @override String toString() => message; } /// Checks the public Gogs releases API. This is a notification only; installing /// an update remains a user action from the release page. class UpdateService { static const repository = 'kkazakov/car-interface-android'; static const _baseUrl = 'https://git.krasi.net'; static const releasePageUrl = '$_baseUrl/$repository/releases'; Future check(String currentVersion) async { final uri = Uri.parse('$_baseUrl/api/v1/repos/$repository/releases'); final client = HttpClient()..connectionTimeout = const Duration(seconds: 8); try { final request = await client.getUrl(uri); request.headers.set( HttpHeaders.userAgentHeader, 'CarDash/$currentVersion', ); final response = await request.close().timeout( const Duration(seconds: 10), ); if (response.statusCode != HttpStatus.ok) { throw UpdateException( 'Release server returned ${response.statusCode}.', ); } final body = await response.transform(utf8.decoder).join(); final decoded = jsonDecode(body); if (decoded is! List) { throw UpdateException('Unexpected release response.'); } final releases = <_Release>[]; for (final raw in decoded) { if (raw is! Map) continue; if (raw['draft'] == true || raw['prerelease'] == true) continue; final tag = (raw['tag_name'] as String?)?.trim() ?? ''; final version = _normaliseVersion(tag); if (version == null) continue; releases.add( _Release( version: version, title: (raw['name'] as String?)?.trim() ?? tag, ), ); } if (releases.isEmpty) { throw UpdateException('No published releases found.'); } releases.sort((a, b) => compareVersions(b.version, a.version)); final latest = releases.first; final available = compareVersions(latest.version, currentVersion) > 0; return UpdateInfo( currentVersion: currentVersion, latestVersion: latest.version, available: available, releasePageUrl: releasePageUrl, downloadUrl: '$_baseUrl/$repository/raw/main/releases/' 'cardash-android-${latest.version}.apk', ); } on UpdateException { rethrow; } catch (error) { throw UpdateException('Could not check for updates: $error'); } finally { client.close(force: true); } } static String? _normaliseVersion(String tag) { final value = tag.replaceFirst(RegExp(r'^[vV]'), '').trim(); return RegExp(r'^\d+\.\d+\.\d+$').hasMatch(value) ? value : null; } } /// Compares dot-separated numeric versions. Returns <0 when [a] is older. int compareVersions(String a, String b) { List parts(String value) { final clean = value.replaceFirst(RegExp(r'^[vV]'), ''); final numbers = clean .split('.') .map((part) => int.tryParse(part) ?? 0) .toList(); while (numbers.length < 3) { numbers.add(0); } return numbers; } final left = parts(a); final right = parts(b); for (var index = 0; index < 3; index++) { final compared = left[index].compareTo(right[index]); if (compared != 0) return compared; } return 0; } class _Release { const _Release({required this.version, required this.title}); final String version; final String title; }