diff --git a/lib/services/package_manager/android_package_manager.dart b/lib/services/package_manager/android_package_manager.dart index abe899e..cd95766 100644 --- a/lib/services/package_manager/android_package_manager.dart +++ b/lib/services/package_manager/android_package_manager.dart @@ -321,8 +321,18 @@ final class AndroidPackageManager extends PackageManager { case InstallStatus.failed: final failureType = _errorCodeToFailureType(errorCode, message); + + // Hash mismatch from original source — retry from CDN before failing + if (failureType == FailureType.hashMismatch) { + if (cdnRetryTracker[appId] != true) { + _deleteFile(filePath); + clearInstallSlot(appId); + unawaited(retryDownloadFromCdn(appId, target)); + break; + } + } + final userMessage = _getUserFriendlyMessage(failureType); - // Preserve technical details: combine original message and description final technicalDetails = [ if (message != null) message, if (description != null) description, diff --git a/lib/services/package_manager/package_manager.dart b/lib/services/package_manager/package_manager.dart index 57f8b45..a853719 100644 --- a/lib/services/package_manager/package_manager.dart +++ b/lib/services/package_manager/package_manager.dart @@ -131,6 +131,12 @@ abstract class PackageManager extends StateNotifier { @protected String? activeInstall; + /// Tracks whether the last download for an appId came from CDN. + /// Set in [_handleDownloadComplete] from task metadata, read by subclasses + /// to decide whether to retry from CDN on hash mismatch. + @protected + final Map cdnRetryTracker = {}; + /// Lock to prevent concurrent queue processing bool _processingQueue = false; @@ -304,6 +310,7 @@ abstract class PackageManager extends StateNotifier { } void clearOperation(String appId) { + cdnRetryTracker.remove(appId); state = state.copyWith( operations: Map.from(state.operations)..remove(appId), ); @@ -702,6 +709,14 @@ abstract class PackageManager extends StateNotifier { } } + /// Re-download from CDN after a hash mismatch on the original source. + @protected + Future retryDownloadFromCdn(String appId, FileMetadata target) { + activeDownloads.add(appId); + final cdnUrl = 'https://cdn.zapstore.dev/${target.hash}'; + return _startDownloadTask(appId, target, cdnUrl, isCdnRetry: true); + } + // ═══════════════════════════════════════════════════════════════════════════ // DOWNLOAD INTERNALS // ═══════════════════════════════════════════════════════════════════════════ @@ -854,10 +869,7 @@ abstract class PackageManager extends StateNotifier { case TaskStatus.notFound: // 404 error - retry with CDN fallback if not already tried if (!isCdnRetry) { - final cdnUrl = 'https://cdn.zapstore.dev/${target.hash}'; - unawaited( - _startDownloadTask(appId, target, cdnUrl, isCdnRetry: true), - ); + unawaited(retryDownloadFromCdn(appId, target)); return; } // CDN also returned 404 - fail the operation @@ -938,6 +950,10 @@ abstract class PackageManager extends StateNotifier { // Remove from active downloads activeDownloads.remove(appId); + // Track CDN origin so subclasses can retry from CDN on hash mismatch + final (_, _, isCdnRetry) = _parseTaskMetadata(task.metaData); + cdnRetryTracker[appId] = isCdnRetry; + try { final filePath = await task.filePath(); // Proceed to install (this will add to install queue when ready) @@ -979,7 +995,7 @@ abstract class PackageManager extends StateNotifier { } final downloadUrl = op.target.urls.firstOrNull; - if (downloadUrl == null) { + if (downloadUrl == null || downloadUrl.isEmpty) { setOperation( appId, OperationFailed( @@ -997,6 +1013,7 @@ abstract class PackageManager extends StateNotifier { op.target, downloadUrl, displayName: op.displayName, + isCdnRetry: false, ); }