OTA Update attempt
This commit is contained in:
@@ -0,0 +1,136 @@
|
||||
import 'dart:async';
|
||||
import 'dart:io';
|
||||
|
||||
import 'package:ota_update/ota_update.dart';
|
||||
import 'package:package_info_plus/package_info_plus.dart';
|
||||
import 'package:permission_handler/permission_handler.dart';
|
||||
import 'package:supabase_flutter/supabase_flutter.dart';
|
||||
|
||||
import '../models/app_version.dart';
|
||||
|
||||
/// Handles the business logic for fetching version information from the
|
||||
/// ``app_versions`` table and downloading/installing a replacement APK.
|
||||
///
|
||||
/// This class is intentionally *not* coupled to any UI framework so that it can
|
||||
/// be unit‑tested easily. Widgets should only call into the public methods and
|
||||
/// display whatever progress / error states they need.
|
||||
class AppUpdateService {
|
||||
AppUpdateService._();
|
||||
|
||||
/// Public singleton that other parts of the app can depend on. The private
|
||||
/// constructor prevents instantiation from tests; they should mock the
|
||||
/// individual methods instead.
|
||||
static final instance = AppUpdateService._();
|
||||
|
||||
final SupabaseClient _client = Supabase.instance.client;
|
||||
|
||||
/// Fetches the most recent record from ``app_versions``. The table is
|
||||
/// expected to contain a single row per release; we use the highest
|
||||
/// ``version_code`` so that historical entries may be kept if desired.
|
||||
Future<AppVersion?> _fetchLatestVersion() async {
|
||||
try {
|
||||
final Map<String, dynamic>? data = await _client
|
||||
.from('app_versions')
|
||||
.select()
|
||||
.order('version_code', ascending: false)
|
||||
.limit(1)
|
||||
.maybeSingle();
|
||||
if (data == null) return null;
|
||||
return AppVersion.fromMap(data);
|
||||
} catch (e) {
|
||||
// rethrow so callers can handle/log
|
||||
rethrow;
|
||||
}
|
||||
}
|
||||
|
||||
/// Compare the running build number with the latest data from the server and
|
||||
/// return an [AppUpdateInfo] object describing the relationship. Errors
|
||||
/// (network down, no supabase instance) are simply re‑thrown; the caller can
|
||||
/// decide whether to ignore them or surface them to the user.
|
||||
Future<AppUpdateInfo> checkForUpdate() async {
|
||||
final pkg = await PackageInfo.fromPlatform();
|
||||
final currentBuild = int.tryParse(pkg.buildNumber) ?? 0;
|
||||
|
||||
final serverVersion = await _fetchLatestVersion();
|
||||
|
||||
if (serverVersion == null) {
|
||||
// table empty – nothing to do
|
||||
return AppUpdateInfo(
|
||||
currentBuildNumber: currentBuild,
|
||||
latestVersion: null,
|
||||
isUpdateAvailable: false,
|
||||
isForceUpdate: false,
|
||||
);
|
||||
}
|
||||
|
||||
final available = currentBuild < serverVersion.versionCode;
|
||||
final force = currentBuild < serverVersion.minVersionRequired;
|
||||
|
||||
return AppUpdateInfo(
|
||||
currentBuildNumber: currentBuild,
|
||||
latestVersion: serverVersion,
|
||||
isUpdateAvailable: available,
|
||||
isForceUpdate: force,
|
||||
);
|
||||
}
|
||||
|
||||
/// Downloads and installs the APK at [downloadUrl] using `ota_update`.
|
||||
/// Progress is surfaced via [onProgress] (0.0–1.0). The returned future
|
||||
/// completes once the download has finished and installation has started.
|
||||
///
|
||||
/// Throws on permission denial or when the OTA stream reports an error.
|
||||
Future<void> downloadAndInstallApk(
|
||||
String downloadUrl, {
|
||||
required void Function(double) onProgress,
|
||||
}) async {
|
||||
if (!Platform.isAndroid) {
|
||||
throw UnsupportedError(
|
||||
'In‑app updates currently only supported on Android',
|
||||
);
|
||||
}
|
||||
|
||||
final status = await Permission.requestInstallPackages.request();
|
||||
if (!status.isGranted) {
|
||||
throw Exception('installation permission denied');
|
||||
}
|
||||
|
||||
final completer = Completer<void>();
|
||||
StreamSubscription<OtaEvent>? sub;
|
||||
try {
|
||||
sub = OtaUpdate()
|
||||
.execute(
|
||||
downloadUrl,
|
||||
destinationFilename:
|
||||
'app_${DateTime.now().millisecondsSinceEpoch}.apk',
|
||||
)
|
||||
.listen(
|
||||
(event) {
|
||||
switch (event.status) {
|
||||
case OtaStatus.DOWNLOADING:
|
||||
final pct = double.tryParse(event.value ?? '0') ?? 0;
|
||||
onProgress(pct / 100.0);
|
||||
break;
|
||||
case OtaStatus.INSTALLING:
|
||||
if (!completer.isCompleted) completer.complete();
|
||||
break;
|
||||
default:
|
||||
// treat all other statuses as failure; they provide a string
|
||||
// description in `event.value`.
|
||||
if (!completer.isCompleted) {
|
||||
completer.completeError(
|
||||
Exception('OTA update failed: ${event.status}'),
|
||||
);
|
||||
}
|
||||
break;
|
||||
}
|
||||
},
|
||||
onError: (e) {
|
||||
if (!completer.isCompleted) completer.completeError(e);
|
||||
},
|
||||
);
|
||||
await completer.future;
|
||||
} finally {
|
||||
if (sub != null) await sub.cancel();
|
||||
}
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user