diff --git a/lib/services/package_manager/android_package_manager.dart b/lib/services/package_manager/android_package_manager.dart index fc5912b..f96ddd4 100644 --- a/lib/services/package_manager/android_package_manager.dart +++ b/lib/services/package_manager/android_package_manager.dart @@ -658,31 +658,10 @@ final class AndroidPackageManager extends PackageManager { // Derive general silent install capability from per-app data _supportsSilentInstall = anyCanInstallSilently; - // Preserve any packages we've directly updated (they have installTime set) - // This prevents race conditions where sync returns stale data - final preserved = {}; - for (final entry in state.installed.entries) { - if (entry.value.installTime != null && - !packages.containsKey(entry.key)) { - // We directly updated this but sync doesn't have it yet - keep ours - preserved[entry.key] = entry.value; - } else if (entry.value.installTime != null && - packages.containsKey(entry.key)) { - // Both have it - use sync data but preserve installTime marker - final syncPkg = packages[entry.key]!; - preserved[entry.key] = PackageInfo( - appId: syncPkg.appId, - name: syncPkg.name, - version: syncPkg.version, - versionCode: syncPkg.versionCode, - signatureHash: syncPkg.signatureHash, - installTime: entry.value.installTime, - canInstallSilently: syncPkg.canInstallSilently, - ); - } - } - - state = state.copyWith(installed: {...packages, ...preserved}); + // Trust the native source as the single source of truth. + // Android's SUCCESS broadcast only fires after the package is committed, + // so there's no race condition with queryable state. + state = state.copyWith(installed: packages); await InstalledPackagesSnapshot.save(state.installed); // Clear operations for apps where the installed version matches the target version diff --git a/lib/services/package_manager/package_manager.dart b/lib/services/package_manager/package_manager.dart index bbff87c..f01fc21 100644 --- a/lib/services/package_manager/package_manager.dart +++ b/lib/services/package_manager/package_manager.dart @@ -1438,6 +1438,7 @@ class BatchProgress { required this.installing, required this.queued, required this.failed, + required this.cancelled, required this.phase, }); @@ -1462,10 +1463,14 @@ class BatchProgress { /// Operations that failed final int failed; + /// Operations cancelled by user (InstallCancelled - can retry individually) + final int cancelled; + /// Current dominant phase final BatchPhase phase; /// Whether any operations are in progress (not terminal) + /// Terminal states: Completed, OperationFailed, InstallCancelled bool get hasInProgress => downloading > 0 || verifying > 0 || installing > 0 || queued > 0; @@ -1494,7 +1499,8 @@ final batchProgressProvider = Provider((ref) { verifying = 0, installing = 0, queued = 0, - failed = 0; + failed = 0, + cancelled = 0; for (final op in ops.values) { switch (op) { @@ -1511,7 +1517,7 @@ final batchProgressProvider = Provider((ref) { case OperationFailed(): failed++; case InstallCancelled(): - queued++; // User can retry + cancelled++; // Terminal for batch - user can retry individually case AwaitingPermission(): queued++; // Waiting for permission case Uninstalling(): @@ -1540,6 +1546,7 @@ final batchProgressProvider = Provider((ref) { installing: installing, queued: queued, failed: failed, + cancelled: cancelled, phase: phase, ); }); diff --git a/spec/features/FEAT-001-package-manager.md b/spec/features/FEAT-001-package-manager.md index 31b808a..56c2bf4 100644 --- a/spec/features/FEAT-001-package-manager.md +++ b/spec/features/FEAT-001-package-manager.md @@ -199,8 +199,10 @@ There is no third option. Timeouts, crashes, backgrounding, network loss—all p - Successful operations transition to `Completed` state (stay in map) - Total = operations.length (includes completed) - Completed = count of `Completed` operations - - In-progress = Total - Completed - Failed + - In-progress = Total - Completed - Failed - Cancelled - Phase = derived from operation types currently active +- Terminal states for batch progress: `Completed`, `OperationFailed`, `InstallCancelled` + - `InstallCancelled` counts as cancelled (user can retry individually, but batch doesn't wait) - When all operations reach terminal state, banner shows "X of X updated ✓" - After 3 seconds with no in-progress operations, completed operations auto-clear