added cache folder sweep check before file actions, preventing errors

This commit is contained in:
2026-08-07 21:36:09 +02:00
parent 62fa337188
commit b9cb1df473
4 changed files with 98 additions and 7 deletions
+7 -2
View File
@@ -7,6 +7,7 @@ import '../../utils/downloads/download_job.dart';
import '../../utils/downloads/download_manager.dart'; import '../../utils/downloads/download_manager.dart';
import '../../utils/haptics.dart'; import '../../utils/haptics.dart';
import 'downloads_sheet.dart'; import 'downloads_sheet.dart';
import 'stale_download_guard.dart';
/// Decides whether a just-finished download should open straight in the viewer. /// Decides whether a just-finished download should open straight in the viewer.
/// ///
@@ -198,9 +199,13 @@ class _DownloadTrayHostState extends State<DownloadTrayHost>
void _openJob(DownloadJob job) { void _openJob(DownloadJob job) {
final path = job.localPath; final path = job.localPath;
_manager.markOpened(job);
final ctx = AppRoutes.overlayContext; final ctx = AppRoutes.overlayContext;
if (path == null || ctx == null) return; if (path == null || ctx == null) {
_manager.markOpened(job);
return;
}
if (!ensureDownloadStillExists(ctx, job)) return;
_manager.markOpened(job);
Haptics.success(); Haptics.success();
AppRoutes.openFileViewer(ctx, path, remoteFile: job.remoteFile); AppRoutes.openFileViewer(ctx, path, remoteFile: job.remoteFile);
} }
+5 -1
View File
@@ -10,6 +10,7 @@ import '../../view/pages/files/data/file_type_icon.dart';
import '../centered_leading.dart'; import '../centered_leading.dart';
import '../details_bottom_sheet.dart'; import '../details_bottom_sheet.dart';
import '../info_dialog.dart'; import '../info_dialog.dart';
import 'stale_download_guard.dart';
/// Overview of all active and finished-but-unopened downloads. Lets the user /// Overview of all active and finished-but-unopened downloads. Lets the user
/// open/switch between finished files, cancel running ones and retry failures. /// open/switch between finished files, cancel running ones and retry failures.
@@ -86,8 +87,11 @@ class _DownloadsListState extends State<_DownloadsList> {
void _open(DownloadJob job) { void _open(DownloadJob job) {
final path = job.localPath; final path = job.localPath;
if (path == null) return; if (path == null) return;
Haptics.success(); // Pop the sheet first so a stale-file dialog lands on the underlying
// screen instead of an emptied sheet.
Navigator.of(widget.sheetContext).pop(); Navigator.of(widget.sheetContext).pop();
if (!ensureDownloadStillExists(widget.rootContext, job)) return;
Haptics.success();
DownloadManager.instance.markOpened(job); DownloadManager.instance.markOpened(job);
AppRoutes.openFileViewer( AppRoutes.openFileViewer(
widget.rootContext, widget.rootContext,
@@ -0,0 +1,40 @@
import 'dart:async';
import 'dart:io';
import 'package:flutter/material.dart';
import '../../utils/downloads/download_job.dart';
import '../../utils/downloads/download_manager.dart';
import '../confirm_dialog.dart';
import '../info_dialog.dart';
/// Verifies a finished download's cache file is still on disk before opening.
///
/// Downloads live in the app's temp dir, which Android may clear at any time
/// (storage pressure, "Cache leeren") — while completion notifications and
/// tray entries stay tappable indefinitely. Returns true when the file exists;
/// otherwise drops the stale job (incl. its notification) and offers a
/// re-download when the remote path is known (it isn't for notification taps
/// whose task metadata got lost).
bool ensureDownloadStillExists(BuildContext context, DownloadJob job) {
final path = job.localPath;
if (path == null || File(path).existsSync()) return true;
DownloadManager.instance.markOpened(job);
if (job.remotePath.isNotEmpty) {
ConfirmDialog(
title: 'Datei nicht mehr verfügbar',
content:
'Die heruntergeladene Datei wurde vom System aus dem Zwischenspeicher entfernt.\nErneut herunterladen?',
confirmButton: 'Herunterladen',
onConfirm: () => unawaited(DownloadManager.instance.retry(job)),
).asDialog(context);
} else {
InfoDialog.show(
context,
'Die heruntergeladene Datei wurde vom System aus dem Zwischenspeicher entfernt. Bitte lade sie erneut herunter.',
title: 'Datei nicht mehr verfügbar',
);
}
return false;
}
+46 -4
View File
@@ -14,8 +14,11 @@ import 'package:share_plus/share_plus.dart';
import '../routing/app_routes.dart'; import '../routing/app_routes.dart';
import '../share_intent/remote_file_ref.dart'; import '../share_intent/remote_file_ref.dart';
import '../state/app/modules/settings/bloc/settings_cubit.dart'; import '../state/app/modules/settings/bloc/settings_cubit.dart';
import '../utils/downloads/download_manager.dart';
import 'app_progress_indicator.dart'; import 'app_progress_indicator.dart';
import 'async_action_button.dart';
import 'centered_leading.dart'; import 'centered_leading.dart';
import 'confirm_dialog.dart';
import 'file_viewer/code_line.dart'; import 'file_viewer/code_line.dart';
import 'file_viewer/deferred_pdf_viewer.dart'; import 'file_viewer/deferred_pdf_viewer.dart';
import 'file_viewer/file_kind.dart'; import 'file_viewer/file_kind.dart';
@@ -82,9 +85,43 @@ class _FileViewerState extends State<FileViewer> {
super.dispose(); super.dispose();
} }
/// Android may clear the cache dir behind an open viewer at any time —
/// verify the file is still there before handing its path to an action.
bool _ensureLocalFile() {
if (File(widget.path).existsSync()) return true;
final remote = widget.remoteFile;
if (remote == null) {
InfoDialog.show(
context,
'Die Datei wurde vom System aus dem Zwischenspeicher entfernt. Bitte lade sie erneut herunter.',
title: 'Datei nicht mehr verfügbar',
);
return false;
}
ConfirmDialog(
title: 'Datei nicht mehr verfügbar',
content:
'Die Datei wurde vom System aus dem Zwischenspeicher entfernt.\nErneut herunterladen?',
confirmButton: 'Herunterladen',
onConfirm: () {
// Pop the viewer before starting so the fresh download auto-opens.
Navigator.of(context).pop();
unawaited(
DownloadManager.instance.start(
remotePath: remote.path,
name: remote.name,
remoteFile: remote,
),
);
},
).asDialog(context);
return false;
}
Future<void> _handleAction(FileViewingActions value) async { Future<void> _handleAction(FileViewingActions value) async {
switch (value) { switch (value) {
case FileViewingActions.openExternal: case FileViewingActions.openExternal:
if (!_ensureLocalFile()) return;
AppRoutes.openFileViewer( AppRoutes.openFileViewer(
context, context,
widget.path, widget.path,
@@ -99,16 +136,21 @@ class _FileViewerState extends State<FileViewer> {
AppRoutes.openInternalSaveToFolder(context, widget.remoteFile!); AppRoutes.openInternalSaveToFolder(context, widget.remoteFile!);
break; break;
case FileViewingActions.share: case FileViewingActions.share:
if (!_ensureLocalFile()) return;
unawaited( unawaited(
SharePlus.instance.share( runWithErrorDialog(
ShareParams( context,
files: [XFile(widget.path)], () => SharePlus.instance.share(
sharePositionOrigin: SharePositionOrigin.get(context), ShareParams(
files: [XFile(widget.path)],
sharePositionOrigin: SharePositionOrigin.get(context),
),
), ),
), ),
); );
break; break;
case FileViewingActions.save: case FileViewingActions.save:
if (!_ensureLocalFile()) return;
try { try {
final source = File(widget.path); final source = File(widget.path);
final size = await source.length(); final size = await source.length();