mirror of
https://github.com/zapstore/zapstore.git
synced 2026-10-05 20:48:24 +00:00
298 lines
12 KiB
Dart
298 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,
|
|
|
|
/// NIP-C1 proof did not verify against the downloaded APK signing certificate
|
|
c1ProofMismatch,
|
|
|
|
/// 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,
|
|
};
|
|
}
|