diff --git a/open_wearable/lib/models/fota_post_update_verification.dart b/open_wearable/lib/models/fota_post_update_verification.dart index 81524661..6e04526b 100644 --- a/open_wearable/lib/models/fota_post_update_verification.dart +++ b/open_wearable/lib/models/fota_post_update_verification.dart @@ -60,8 +60,19 @@ class FotaPostUpdateVerificationCoordinator { static const Duration _maxPendingAge = Duration(minutes: 20); final Map _pendingById = {}; + final StreamController> _pendingIdsController = + StreamController>.broadcast(); int _nextVerificationId = 0; + /// Emits the current set of active verification ids whenever it changes. + Stream> get pendingVerificationIds => + _pendingIdsController.stream; + + /// Returns whether the given verification is still awaiting reconnect + /// confirmation. + bool isVerificationPending(String verificationId) => + _pendingById.containsKey(verificationId); + Future armFromUpdateRequest({ required FirmwareUpdateRequest request, Wearable? selectedWearable, @@ -111,6 +122,7 @@ class FotaPostUpdateVerificationCoordinator { expectedFirmwareVersion: expectedFirmwareVersion, armedAt: DateTime.now(), ); + _publishPendingIds(); return ArmedFotaPostUpdateVerification( verificationId: verificationId, @@ -156,6 +168,7 @@ class FotaPostUpdateVerificationCoordinator { ); _pendingById.remove(pending.verificationId); + _publishPendingIds(); final displayName = pending.displayWearableName ?? _displayName(wearable.name) ?? @@ -380,9 +393,19 @@ class FotaPostUpdateVerificationCoordinator { } final now = DateTime.now(); + final removed = []; _pendingById.removeWhere( - (_, pending) => now.difference(pending.armedAt) > _maxPendingAge, + (_, pending) { + final isExpired = now.difference(pending.armedAt) > _maxPendingAge; + if (isExpired) { + removed.add(pending.verificationId); + } + return isExpired; + }, ); + if (removed.isNotEmpty) { + _publishPendingIds(); + } } void _removeConflictingPending({ @@ -394,9 +417,11 @@ class FotaPostUpdateVerificationCoordinator { return; } + final removed = []; _pendingById.removeWhere((_, pending) { if (expectedDeviceId != null && pending.expectedDeviceId == expectedDeviceId) { + removed.add(pending.verificationId); return true; } @@ -406,11 +431,21 @@ class FotaPostUpdateVerificationCoordinator { expectedName != null && sameName && sameSide) { + removed.add(pending.verificationId); return true; } return false; }); + if (removed.isNotEmpty) { + _publishPendingIds(); + } + } + + /// Publishes a defensive copy so listeners can react to verification + /// completion without mutating coordinator state. + void _publishPendingIds() { + _pendingIdsController.add(Set.unmodifiable(_pendingById.keys)); } String? _extractExpectedFirmwareVersion(SelectedFirmware? firmware) { diff --git a/open_wearable/lib/widgets/fota/fota_verification_banner.dart b/open_wearable/lib/widgets/fota/fota_verification_banner.dart index b9cc43a5..d78f6d10 100644 --- a/open_wearable/lib/widgets/fota/fota_verification_banner.dart +++ b/open_wearable/lib/widgets/fota/fota_verification_banner.dart @@ -235,11 +235,23 @@ void dismissFotaVerificationBannerById( _pruneMissingFotaVerificationBannerKeys(controller); final key = _activeFotaVerificationBannerKeys.remove(verificationId); - if (key == null) { + if (key != null) { + controller.hideBannerByKey(key); + _fotaVerificationDeadlinesById.remove(verificationId); return; } - controller.hideBannerByKey(key); + final fallbackPrefix = 'fota_verification_banner_${verificationId}_'; + final fallbackKey = controller.activeBanners + .map((banner) => banner.key) + .whereType>() + .firstWhere( + (candidate) => candidate.value.startsWith(fallbackPrefix), + orElse: () => const ValueKey(''), + ); + if (fallbackKey.value.isNotEmpty) { + controller.hideBannerByKey(fallbackKey); + } _fotaVerificationDeadlinesById.remove(verificationId); } diff --git a/open_wearable/lib/widgets/fota/stepper_view/update_view.dart b/open_wearable/lib/widgets/fota/stepper_view/update_view.dart index a94c2f44..97a22830 100644 --- a/open_wearable/lib/widgets/fota/stepper_view/update_view.dart +++ b/open_wearable/lib/widgets/fota/stepper_view/update_view.dart @@ -36,8 +36,10 @@ class _UpdateStepViewState extends State { bool _lastReportedRunning = false; bool _startRequested = false; bool _verificationBannerShown = false; + bool _isVerificationPending = false; bool _loopWarningHandled = false; String? _lastResetValidateStage; + StreamSubscription>? _verificationPendingSubscription; int _resetValidateLoopTransitions = 0; @override @@ -63,6 +65,7 @@ class _UpdateStepViewState extends State { @override void dispose() { + _verificationPendingSubscription?.cancel(); if (_lastReportedRunning) { widget.onUpdateRunningChanged?.call(false); } @@ -114,6 +117,7 @@ class _UpdateStepViewState extends State { if (!mounted || armedVerification == null) { return; } + _bindVerificationLifecycle(armedVerification.verificationId); showFotaVerificationBanner( this.context, verificationId: armedVerification.verificationId, @@ -142,6 +146,25 @@ class _UpdateStepViewState extends State { return true; } + /// Subscribes the page-level success panel to the coordinator entry created + /// for this update so it disappears immediately after reconnect validation. + void _bindVerificationLifecycle(String verificationId) { + _verificationPendingSubscription?.cancel(); + _isVerificationPending = FotaPostUpdateVerificationCoordinator.instance + .isVerificationPending(verificationId); + _verificationPendingSubscription = FotaPostUpdateVerificationCoordinator + .instance.pendingVerificationIds + .listen((pendingIds) { + final isPending = pendingIds.contains(verificationId); + if (!mounted || _isVerificationPending == isPending) { + return; + } + setState(() { + _isVerificationPending = isPending; + }); + }); + } + /// Shows a one-time warning when the update appears to restart image uploads /// more often than the selected firmware package should require. Future _maybeShowLoopWarning({ @@ -417,7 +440,7 @@ class _UpdateStepViewState extends State { _abortButton(context), const SizedBox(height: 10), ], - if (showSuccessMessage) ...[ + if (showSuccessMessage && _isVerificationPending) ...[ _successPanel(context), const SizedBox(height: 10), ], diff --git a/open_wearable/pubspec.lock b/open_wearable/pubspec.lock index 2e48b157..7031a8b8 100644 --- a/open_wearable/pubspec.lock +++ b/open_wearable/pubspec.lock @@ -101,10 +101,10 @@ packages: dependency: transitive description: name: characters - sha256: f71061c654a3380576a52b451dd5532377954cf9dbd272a78fc8479606670803 + sha256: faf38497bda5ead2a8c7615f4f7939df04333478bf32e4173fcb06d428b5716b url: "https://pub.dev" source: hosted - version: "1.4.0" + version: "1.4.1" clock: dependency: transitive description: @@ -500,18 +500,18 @@ packages: dependency: transitive description: name: matcher - sha256: dc58c723c3c24bf8d3e2d3ad3f2f9d7bd9cf43ec6feaa64181775e60190153f2 + sha256: dc0b7dc7651697ea4ff3e69ef44b0407ea32c487a39fff6a4004fa585e901861 url: "https://pub.dev" source: hosted - version: "0.12.17" + version: "0.12.19" material_color_utilities: dependency: transitive description: name: material_color_utilities - sha256: f7142bb1154231d7ea5f96bc7bde4bda2a0945d2806bb11670e30b850d56bdec + sha256: "9c337007e82b1889149c82ed242ed1cb24a66044e30979c44912381e9be4c48b" url: "https://pub.dev" source: hosted - version: "0.11.1" + version: "0.13.0" mcumgr_flutter: dependency: "direct main" description: @@ -945,10 +945,10 @@ packages: dependency: transitive description: name: test_api - sha256: ab2726c1a94d3176a45960b6234466ec367179b87dd74f1611adb1f3b5fb9d55 + sha256: "8161c84903fd860b26bfdefb7963b3f0b68fee7adea0f59ef805ecca346f0c7a" url: "https://pub.dev" source: hosted - version: "0.7.7" + version: "0.7.10" tuple: dependency: transitive description: