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

293 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 {
final Installable 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.
/// [triggeredAt] is set when [triggerInstall] is actually called, so the
/// watchdog can detect native sessions that never respond.
class ReadyToInstall extends InstallOperation {
final String filePath;
final DateTime? triggeredAt;
const ReadyToInstall({
required super.target,
required this.filePath,
this.triggeredAt,
});
}
/// 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;
/// Whether this was an update (app was already installed) or a new install.
final bool isUpdate;
Completed({required super.target, DateTime? completedAt, this.isUpdate = false})
: 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,
});
}
/// 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,
/// APK signing certificate does not match what the publisher declared in Nostr metadata
certMetadataMismatch,
/// 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 Dart-side watchdog monitoring.
/// Only downloads are monitored — install phases are managed by native
/// callbacks (Android PackageInstaller / SessionCallback).
bool get needsWatchdog => this is Downloading;
/// 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,
};
}