import 'dart:async'; import 'package:appflowy/generated/locale_keys.g.dart'; import 'package:appflowy/shared/version_checker/version_checker.dart'; import 'package:appflowy/startup/tasks/app_widget.dart'; import 'package:appflowy/startup/tasks/device_info_task.dart'; import 'package:appflowy/workspace/presentation/widgets/dialogs.dart'; import 'package:appflowy_backend/log.dart'; import 'package:auto_updater/auto_updater.dart'; import 'package:collection/collection.dart'; import 'package:easy_localization/easy_localization.dart'; import 'package:flutter/material.dart'; import 'package:universal_platform/universal_platform.dart'; import '../startup.dart'; class AutoUpdateTask extends LaunchTask { AutoUpdateTask(); static const _feedUrl = 'https://github.com/AppFlowy-IO/AppFlowy/releases/latest/download/appcast-{os}-{arch}.xml'; final _listener = _AppFlowyAutoUpdaterListener(); @override Future initialize(LaunchContext context) async { // the auto updater is not supported on mobile if (UniversalPlatform.isMobile) { return; } // don't use await here, because the auto updater is not a blocking operation unawaited(_setupAutoUpdater()); ApplicationInfo.isCriticalUpdateNotifier.addListener( _showCriticalUpdateDialog, ); } @override Future dispose() async { autoUpdater.removeListener(_listener); ApplicationInfo.isCriticalUpdateNotifier.removeListener( _showCriticalUpdateDialog, ); } // On macOS and windows, we use auto_updater to check for updates. // On linux, we use the version checker to check for updates because the auto_updater is not supported. Future _setupAutoUpdater() async { Log.info( '[AutoUpdate] current version: ${ApplicationInfo.applicationVersion}, current cpu architecture: ${ApplicationInfo.architecture}', ); // Since the appcast.xml is not supported the arch, we separate the feed url by os and arch. final feedUrl = _feedUrl .replaceAll('{os}', ApplicationInfo.os) .replaceAll('{arch}', ApplicationInfo.architecture); // the auto updater is only supported on macOS and windows, so we don't need to check the platform if (UniversalPlatform.isMacOS || UniversalPlatform.isWindows) { autoUpdater.addListener(_listener); } Log.info('[AutoUpdate] feed url: $feedUrl'); versionChecker.setFeedUrl(feedUrl); final item = await versionChecker.checkForUpdateInformation(); if (item != null) { ApplicationInfo.latestAppcastItem = item; ApplicationInfo.latestVersionNotifier.value = item.displayVersionString ?? ''; } } void _showCriticalUpdateDialog() { showCustomConfirmDialog( context: AppGlobals.rootNavKey.currentContext!, title: LocaleKeys.autoUpdate_criticalUpdateTitle.tr(), description: LocaleKeys.autoUpdate_criticalUpdateDescription.tr( namedArgs: { 'currentVersion': ApplicationInfo.applicationVersion, 'newVersion': ApplicationInfo.latestVersion, }, ), builder: (context) => const SizedBox.shrink(), // if the update is critical, dont allow the user to dismiss the dialog barrierDismissible: false, showCloseButton: false, enableKeyboardListener: false, closeOnConfirm: false, confirmLabel: LocaleKeys.autoUpdate_criticalUpdateButton.tr(), onConfirm: () async { await versionChecker.checkForUpdate(); }, ); } } class _AppFlowyAutoUpdaterListener extends UpdaterListener { @override void onUpdaterBeforeQuitForUpdate(AppcastItem? item) {} @override void onUpdaterCheckingForUpdate(Appcast? appcast) { // Due to the reason documented in the following link, the update will not be found if the user has skipped the update. // We have to check the skipped version manually. // https://sparkle-project.org/documentation/api-reference/Classes/SPUUpdater.html#/c:objc(cs)SPUUpdater(im)checkForUpdateInformation final items = appcast?.items; if (items != null) { final String? currentPlatform; if (UniversalPlatform.isMacOS) { currentPlatform = 'macos'; } else if (UniversalPlatform.isWindows) { currentPlatform = 'windows'; } else { currentPlatform = null; } final matchingItem = items.firstWhereOrNull( (item) => item.os == currentPlatform, ); if (matchingItem != null) { _updateVersionNotifier(matchingItem); Log.info( '[AutoUpdate] latest version: ${matchingItem.displayVersionString}', ); } } } @override void onUpdaterError(UpdaterError? error) { Log.error('[AutoUpdate] On update error: $error'); } @override void onUpdaterUpdateNotAvailable(UpdaterError? error) { Log.info('[AutoUpdate] Update not available $error'); } @override void onUpdaterUpdateAvailable(AppcastItem? item) { _updateVersionNotifier(item); Log.info('[AutoUpdate] Update available: ${item?.displayVersionString}'); } @override void onUpdaterUpdateDownloaded(AppcastItem? item) { Log.info('[AutoUpdate] Update downloaded: ${item?.displayVersionString}'); } @override void onUpdaterUpdateCancelled(AppcastItem? item) { _updateVersionNotifier(item); Log.info('[AutoUpdate] Update cancelled: ${item?.displayVersionString}'); } @override void onUpdaterUpdateInstalled(AppcastItem? item) { _updateVersionNotifier(item); Log.info('[AutoUpdate] Update installed: ${item?.displayVersionString}'); } @override void onUpdaterUpdateSkipped(AppcastItem? item) { _updateVersionNotifier(item); Log.info('[AutoUpdate] Update skipped: ${item?.displayVersionString}'); } void _updateVersionNotifier(AppcastItem? item) { if (item != null) { ApplicationInfo.latestAppcastItem = item; ApplicationInfo.latestVersionNotifier.value = item.displayVersionString ?? ''; } } } class AppFlowyAutoUpdateVersion { AppFlowyAutoUpdateVersion({ required this.latestVersion, required this.currentVersion, required this.isForceUpdate, }); factory AppFlowyAutoUpdateVersion.initial() => AppFlowyAutoUpdateVersion( latestVersion: '0.0.0', currentVersion: '0.0.0', isForceUpdate: false, ); final String latestVersion; final String currentVersion; final bool isForceUpdate; bool get isUpdateAvailable => latestVersion != currentVersion; }