feat: auto-updater

This commit is contained in:
2026-07-29 00:26:38 +02:00
parent e53bdf1c38
commit 4f9781e3ff
11 changed files with 461 additions and 195 deletions
+98 -92
View File
@@ -58,6 +58,8 @@ class AppUpdateUtil {
final response = await http.get(url);
debugPrint("Response from update server: ${response.body}");
if (response.statusCode != 200) {
throw Exception("Update server unavailable");
}
@@ -71,96 +73,100 @@ class AppUpdateUtil {
return UpdateInfo.fromJson(data);
}
/// Downloads the APK
Future<File> downloadApk(
UpdateInfo update, {
Function(double progress)? onProgress,
}) async {
final url = update.download.startsWith("http")
? update.download
: "$serverUrl${update.download}";
final request = http.Request("GET", Uri.parse(url));
final response = await request.send();
if (response.statusCode != 200) {
throw Exception("APK download failed");
}
final total = response.contentLength ?? 0;
int received = 0;
final directory = await getTemporaryDirectory();
final file = File("${directory.path}/update.apk");
final sink = file.openWrite();
await for (final chunk in response.stream) {
sink.add(chunk);
received += chunk.length;
if (total > 0 && onProgress != null) {
onProgress(received / total);
}
}
await sink.close();
return file;
}
/// Verifies APK checksum
Future<bool> verifySha256(File file, String expectedHash) async {
final bytes = await file.readAsBytes();
final digest = sha256.convert(bytes);
return digest.toString().toLowerCase() == expectedHash.toLowerCase();
}
/// Opens Android APK installer
Future<void> installApk(File file) async {
// final result =
// await OpenFilex.open(
// file.path,
// );
//
//
// if (result.type != ResultType.done) {
// throw Exception(
// "Could not open APK installer",
// );
// }
}
/// Full update flow
///
/// Returns:
/// - null if no update exists
/// - UpdateInfo if update is available
///
Future<UpdateInfo?> update({Function(double progress)? onProgress}) async {
final info = await checkForUpdate();
if (info == null) {
return null;
}
final apk = await downloadApk(info, onProgress: onProgress);
final valid = await verifySha256(apk, info.sha256);
if (!valid) {
throw Exception("APK checksum mismatch");
}
await installApk(apk);
return info;
}
// /// Downloads the APK
// Future<File> downloadApk(
// UpdateInfo update, {
//
// Function(double progress)? onProgress,
// }) async {
// final url = update.download.startsWith("http")
// ? update.download
// : "$serverUrl${update.download}";
//
// final request = http.Request("GET", Uri.parse(url));
//
// final response = await request.send();
//
// if (response.statusCode != 200) {
// throw Exception("APK download failed");
// }
//
// final total = response.contentLength ?? 0;
//
// int received = 0;
//
// final directory = await getTemporaryDirectory();
//
// final file = File("${directory.path}/update.apk");
//
// final sink = file.openWrite();
//
// await for (final chunk in response.stream) {
// sink.add(chunk);
//
// received += chunk.length;
//
// if (total > 0 && onProgress != null) {
// onProgress(received / total);
// }
// }
//
// await sink.close();
//
// return file;
// }
//
// /// Verifies APK checksum
// Future<bool> verifySha256(File file, String expectedHash) async {
// final bytes = await file.readAsBytes();
//
// final digest = sha256.convert(bytes);
//
// return digest.toString().toLowerCase() == expectedHash.toLowerCase();
// }
//
// /// Opens Android APK installer
// Future<void> installApk(File file) async {
// final result =
// await OpenFilex.open(
// file.path,
// );
//
// if(result.type == ResultType.done) {
// debugPrint("APK installer opened successfully");
// }
//
//
// if (result.type != ResultType.done) {
// throw Exception(
// "Could not open APK installer",
// );
// }
// }
//
// /// Full update flow
// ///
// /// Returns:
// /// - null if no update exists
// /// - UpdateInfo if update is available
// ///
// Future<UpdateInfo?> update({Function(double progress)? onProgress}) async {
// final info = await checkForUpdate();
//
// if (info == null) {
// return null;
// }
//
// final apk = await downloadApk(info, onProgress: onProgress);
//
// final valid = await verifySha256(apk, info.sha256);
//
// if (!valid) {
// throw Exception("APK checksum mismatch");
// }
//
// await installApk(apk);
//
// return info;
// }
}
+143
View File
@@ -0,0 +1,143 @@
import 'package:flutter/material.dart';
import 'package:kooltab2/utils/app_update_util.dart';
import 'package:ota_update/ota_update.dart';
class UpdateChecker {
static bool _hasChecked = false;
static Future<void> check(BuildContext context) async {
if (_hasChecked) {
debugPrint("Update already checked this boot");
return;
}
_hasChecked = true;
final updater = AppUpdateUtil(
serverUrl: "https://updater.brammie15.dev",
);
try {
final update = await updater.checkForUpdate();
if (update == null) {
debugPrint("No update available");
return;
}
debugPrint("Update available: ${update.version}");
if (!context.mounted) return;
_showUpdateDialog(context, update);
} catch (e) {
debugPrint("Update check failed: $e");
}
}
static void _showUpdateDialog(
BuildContext context,
UpdateInfo update,
) {
showDialog(
context: context,
barrierDismissible: !update.mandatory,
builder: (_) => AlertDialog(
title: const Text("Update available"),
content: Column(
mainAxisSize: MainAxisSize.min,
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Text("Version ${update.version} is available."),
const SizedBox(height: 12),
Text(update.notes),
],
),
actions: [
if (!update.mandatory)
TextButton(
onPressed: () => Navigator.pop(context),
child: const Text("Later"),
),
FilledButton(
onPressed: () async {
Navigator.pop(context);
await _downloadAndInstall(update);
},
child: const Text("Update"),
),
],
),
);
}
}
Future<void> _downloadAndInstall(UpdateInfo update) async {
try {
final url = update.download.startsWith("http")
? update.download
: "https://updater.brammie15.dev${update.download}";
OtaUpdate()
.execute(
url,
destinationFilename: "update.apk",
sha256checksum: update.sha256,
)
.listen(
(OtaEvent event) {
debugPrint(
"OTA status: ${event.status}",
);
debugPrint(
"OTA value: ${event.value}",
);
switch(event.status) {
case OtaStatus.DOWNLOADING:
final progress =
double.tryParse(event.value ?? "0") ?? 0;
debugPrint(
"Downloading ${progress.toStringAsFixed(0)}%",
);
break;
case OtaStatus.INSTALLING:
debugPrint(
"Installing update",
);
break;
case OtaStatus.INSTALLATION_ERROR:
debugPrint(
"Installation error: ${event.value}",
);
break;
case OtaStatus.DOWNLOAD_ERROR:
debugPrint(
"Download error: ${event.value}",
);
break;
default:
break;
}
},
);
} catch (e) {
debugPrint(
"OTA update failed: $e",
);
}
}