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

251 lines
10 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;
// ═══════════════════════════════════════════════════════════════════════════════
// 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;
const Downloading({
required super.target,
required this.progress,
required this.taskId,
});
Downloading copyWith({double? progress}) {
return Downloading(
target: target,
progress: progress ?? this.progress,
taskId: taskId,
);
}
}
/// 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;
const Verifying({
required super.target,
required this.filePath,
this.progress = 0.0,
});
Verifying copyWith({double? progress}) {
return Verifying(
target: target,
filePath: filePath,
progress: progress ?? this.progress,
);
}
}
// ═══════════════════════════════════════════════════════════════════════════════
// 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;
const SystemProcessing({required super.target, required this.filePath});
}
// ═══════════════════════════════════════════════════════════════════════════════
// 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 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 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,
};
}