Files
zapstore/lib/services/package_manager/install_operation.dart
T

296 lines
12 KiB
Dart

import 'package:models/models.dart';
/// Stale download threshold - operations older than this will be cleaned up
const staleOperationThreshold = Duration(days: 7);
/// Delay between queueing operations to prevent UI flood
const batchQueueDelayMs = 50;
/// Dart-side watchdog timeout (fallback if native events stop arriving).
/// For downloads, this is measured from the last progress update (activity-based).
/// For other phases, this is measured from when the phase started.
const watchdogTimeout = Duration(minutes: 2);
/// How often the watchdog timer checks for stale operations
const watchdogCheckInterval = Duration(seconds: 30);
// ═══════════════════════════════════════════════════════════════════════════════
// INSTALL OPERATION STATE MACHINE
// ═══════════════════════════════════════════════════════════════════════════════
/// Represents an active install operation for an app.
/// When there's no operation, the app simply has no entry in the operations map.
sealed class InstallOperation {
/// The target file metadata being installed (works for both FileMetadata and SoftwareAsset)
final FileMetadata target;
const InstallOperation({required this.target});
}
// ═══════════════════════════════════════════════════════════════════════════════
// DOWNLOAD PHASE (Cancel allowed)
// ═══════════════════════════════════════════════════════════════════════════════
/// Waiting for download slot (max concurrent reached)
class DownloadQueued extends InstallOperation {
final String? displayName;
const DownloadQueued({required super.target, this.displayName});
}
/// Actively downloading
class Downloading extends InstallOperation {
final double progress;
final String taskId;
final DateTime startedAt;
/// Last time progress was received. The watchdog uses this (not [startedAt])
/// so that slow-but-active downloads are not killed prematurely.
final DateTime lastProgressAt;
Downloading({
required super.target,
required this.progress,
required this.taskId,
DateTime? startedAt,
DateTime? lastProgressAt,
}) : startedAt = startedAt ?? DateTime.now(),
lastProgressAt = lastProgressAt ?? startedAt ?? DateTime.now();
Downloading copyWith({double? progress}) {
return Downloading(
target: target,
progress: progress ?? this.progress,
taskId: taskId,
startedAt: startedAt,
// Reset activity timestamp when progress changes
lastProgressAt: progress != null ? DateTime.now() : lastProgressAt,
);
}
}
/// Download paused by user
class DownloadPaused extends InstallOperation {
final double progress;
final String taskId;
const DownloadPaused({
required super.target,
required this.progress,
required this.taskId,
});
}
// ═══════════════════════════════════════════════════════════════════════════════
// VERIFICATION PHASE (Kotlin is verifying hash)
// ═══════════════════════════════════════════════════════════════════════════════
/// Verifying downloaded file hash (happens in Kotlin, visible to UI)
class Verifying extends InstallOperation {
final String filePath;
final double progress;
final DateTime startedAt;
Verifying({
required super.target,
required this.filePath,
this.progress = 0.0,
DateTime? startedAt,
}) : startedAt = startedAt ?? DateTime.now();
Verifying copyWith({double? progress}) {
return Verifying(
target: target,
filePath: filePath,
progress: progress ?? this.progress,
startedAt: startedAt,
);
}
}
// ═══════════════════════════════════════════════════════════════════════════════
// PERMISSION PHASE (Explicit for UX feedback)
// ═══════════════════════════════════════════════════════════════════════════════
/// Waiting for user to grant install permission
class AwaitingPermission extends InstallOperation {
final String filePath;
const AwaitingPermission({required super.target, required this.filePath});
}
// ═══════════════════════════════════════════════════════════════════════════════
// INSTALL PHASE (No cancel - Android controls)
// ═══════════════════════════════════════════════════════════════════════════════
/// File verified, waiting for install to be triggered
class ReadyToInstall extends InstallOperation {
final String filePath;
const ReadyToInstall({required super.target, required this.filePath});
}
/// Native installation in progress
class Installing extends InstallOperation {
final String filePath;
final bool isSilent;
final DateTime startedAt;
Installing({
required super.target,
required this.filePath,
this.isSilent = false,
DateTime? startedAt,
}) : startedAt = startedAt ?? DateTime.now();
}
/// User cancelled the install dialog - file is still ready, no re-download needed.
/// User can tap "Install (retry)" to show the dialog again.
class InstallCancelled extends InstallOperation {
final String filePath;
const InstallCancelled({required super.target, required this.filePath});
}
/// Uninstalling app (for force update: uninstall → install)
class Uninstalling extends InstallOperation {
final String filePath;
const Uninstalling({required super.target, required this.filePath});
}
/// System is processing the install - cannot be cancelled.
/// This state is entered when the install session has been committed to Android
/// and is taking longer than expected. The system will eventually complete or fail.
class SystemProcessing extends InstallOperation {
final String filePath;
final DateTime startedAt;
SystemProcessing({
required super.target,
required this.filePath,
DateTime? startedAt,
}) : startedAt = startedAt ?? DateTime.now();
}
// ═══════════════════════════════════════════════════════════════════════════════
// TERMINAL STATES
// ═══════════════════════════════════════════════════════════════════════════════
/// Operation completed successfully.
/// Stays in operations map until batch completes (for progress tracking).
class Completed extends InstallOperation {
final DateTime completedAt;
Completed({required super.target, DateTime? completedAt})
: completedAt = completedAt ?? DateTime.now();
}
// ═══════════════════════════════════════════════════════════════════════════════
// FAILURE STATE (Dismiss available)
// ═══════════════════════════════════════════════════════════════════════════════
/// Operation failed - may be retryable depending on type
class OperationFailed extends InstallOperation {
final FailureType type;
final String message;
final String? description;
final String? filePath;
const OperationFailed({
required super.target,
required this.type,
required this.message,
this.description,
this.filePath,
});
/// Whether this requires force update (uninstall + install)
bool get needsForceUpdate => type == FailureType.certMismatch;
}
/// Types of failures that can occur during install operations
enum FailureType {
/// Network error, timeout, server error during download
downloadFailed,
/// Hash doesn't match expected - can retry with reckless mode
hashMismatch,
/// File is corrupted or not a valid APK
invalidFile,
/// Generic installation error
installFailed,
/// Certificate/signature mismatch - needs force update (uninstall + install)
certMismatch,
/// User doesn't have install permission
permissionDenied,
/// Not enough storage space
insufficientStorage,
/// Device is incompatible with the app (wrong architecture, API level, etc.)
incompatible,
}
// ═══════════════════════════════════════════════════════════════════════════════
// HELPER EXTENSIONS
// ═══════════════════════════════════════════════════════════════════════════════
extension InstallOperationX on InstallOperation {
/// Whether this operation is in a download state (cancel allowed)
bool get isDownloading =>
this is DownloadQueued || this is Downloading || this is DownloadPaused;
/// Whether this operation is actively processing (not waiting for user)
bool get isActive =>
this is Downloading ||
this is Verifying ||
this is Installing ||
this is SystemProcessing ||
this is Uninstalling;
/// Whether this operation needs watchdog monitoring (waiting for native events)
bool get needsWatchdog =>
this is Downloading ||
this is Verifying ||
this is Installing ||
this is SystemProcessing;
/// Whether this operation is in the verification phase
bool get isVerifying => this is Verifying;
/// Whether this operation is in a terminal state (completed or failed)
bool get isTerminal => this is Completed || this is OperationFailed;
/// Whether this operation is still in progress (not terminal)
bool get isInProgress => !isTerminal;
/// Get the relevant timestamp for watchdog monitoring.
/// For downloads: last progress update (activity-based, so slow downloads survive).
/// For other phases: when the phase started.
DateTime? get watchdogTimestamp => switch (this) {
Downloading(:final lastProgressAt) => lastProgressAt,
Verifying(:final startedAt) => startedAt,
Installing(:final startedAt) => startedAt,
SystemProcessing(:final startedAt) => startedAt,
_ => null,
};
/// Get file path if available
String? get filePath => switch (this) {
Verifying(:final filePath) => filePath,
AwaitingPermission(:final filePath) => filePath,
ReadyToInstall(:final filePath) => filePath,
Installing(:final filePath) => filePath,
SystemProcessing(:final filePath) => filePath,
InstallCancelled(:final filePath) => filePath,
Uninstalling(:final filePath) => filePath,
OperationFailed(:final filePath) => filePath,
_ => null,
};
}