update_service.dart 3.6 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117
  1. import 'dart:convert';
  2. import 'dart:io';
  3. import '../models/update_info.dart';
  4. class UpdateException implements Exception {
  5. UpdateException(this.message);
  6. final String message;
  7. @override
  8. String toString() => message;
  9. }
  10. /// Checks the public Gogs releases API. This is a notification only; installing
  11. /// an update remains a user action from the release page.
  12. class UpdateService {
  13. static const repository = 'kkazakov/car-interface-android';
  14. static const _baseUrl = 'https://git.krasi.net';
  15. static const releasePageUrl = '$_baseUrl/$repository/releases';
  16. Future<UpdateInfo> check(String currentVersion) async {
  17. final uri = Uri.parse('$_baseUrl/api/v1/repos/$repository/releases');
  18. final client = HttpClient()..connectionTimeout = const Duration(seconds: 8);
  19. try {
  20. final request = await client.getUrl(uri);
  21. request.headers.set(
  22. HttpHeaders.userAgentHeader,
  23. 'CarDash/$currentVersion',
  24. );
  25. final response = await request.close().timeout(
  26. const Duration(seconds: 10),
  27. );
  28. if (response.statusCode != HttpStatus.ok) {
  29. throw UpdateException(
  30. 'Release server returned ${response.statusCode}.',
  31. );
  32. }
  33. final body = await response.transform(utf8.decoder).join();
  34. final decoded = jsonDecode(body);
  35. if (decoded is! List) {
  36. throw UpdateException('Unexpected release response.');
  37. }
  38. final releases = <_Release>[];
  39. for (final raw in decoded) {
  40. if (raw is! Map) continue;
  41. if (raw['draft'] == true || raw['prerelease'] == true) continue;
  42. final tag = (raw['tag_name'] as String?)?.trim() ?? '';
  43. final version = _normaliseVersion(tag);
  44. if (version == null) continue;
  45. releases.add(
  46. _Release(
  47. version: version,
  48. title: (raw['name'] as String?)?.trim() ?? tag,
  49. ),
  50. );
  51. }
  52. if (releases.isEmpty) {
  53. throw UpdateException('No published releases found.');
  54. }
  55. releases.sort((a, b) => compareVersions(b.version, a.version));
  56. final latest = releases.first;
  57. final available = compareVersions(latest.version, currentVersion) > 0;
  58. return UpdateInfo(
  59. currentVersion: currentVersion,
  60. latestVersion: latest.version,
  61. available: available,
  62. releasePageUrl: releasePageUrl,
  63. downloadUrl:
  64. '$_baseUrl/$repository/raw/main/releases/'
  65. 'cardash-android-${latest.version}.apk',
  66. );
  67. } on UpdateException {
  68. rethrow;
  69. } catch (error) {
  70. throw UpdateException('Could not check for updates: $error');
  71. } finally {
  72. client.close(force: true);
  73. }
  74. }
  75. static String? _normaliseVersion(String tag) {
  76. final value = tag.replaceFirst(RegExp(r'^[vV]'), '').trim();
  77. return RegExp(r'^\d+\.\d+\.\d+$').hasMatch(value) ? value : null;
  78. }
  79. }
  80. /// Compares dot-separated numeric versions. Returns <0 when [a] is older.
  81. int compareVersions(String a, String b) {
  82. List<int> parts(String value) {
  83. final clean = value.replaceFirst(RegExp(r'^[vV]'), '');
  84. final numbers = clean
  85. .split('.')
  86. .map((part) => int.tryParse(part) ?? 0)
  87. .toList();
  88. while (numbers.length < 3) {
  89. numbers.add(0);
  90. }
  91. return numbers;
  92. }
  93. final left = parts(a);
  94. final right = parts(b);
  95. for (var index = 0; index < 3; index++) {
  96. final compared = left[index].compareTo(right[index]);
  97. if (compared != 0) return compared;
  98. }
  99. return 0;
  100. }
  101. class _Release {
  102. const _Release({required this.version, required this.title});
  103. final String version;
  104. final String title;
  105. }