diff --git a/CHANGELOG.md b/CHANGELOG.md
index cf15a8722..1c3d7c250 100644
--- a/CHANGELOG.md
+++ b/CHANGELOG.md
@@ -9,6 +9,10 @@ All notable changes to this project will be documented in this file.
- target Android 14 (API 34)
- upgraded Flutter to stable v3.13.8
+### Fixed
+
+- temporary files remaining in the cache directory forever
+
## [v1.9.7] - 2023-10-17
### Added
diff --git a/android/app/src/main/kotlin/deckers/thibault/aves/channel/calls/EmbeddedDataHandler.kt b/android/app/src/main/kotlin/deckers/thibault/aves/channel/calls/EmbeddedDataHandler.kt
index 028d4fec3..e956a25aa 100644
--- a/android/app/src/main/kotlin/deckers/thibault/aves/channel/calls/EmbeddedDataHandler.kt
+++ b/android/app/src/main/kotlin/deckers/thibault/aves/channel/calls/EmbeddedDataHandler.kt
@@ -33,7 +33,6 @@ import kotlinx.coroutines.CoroutineScope
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.SupervisorJob
import kotlinx.coroutines.launch
-import java.io.File
import java.io.InputStream
class EmbeddedDataHandler(private val context: Context) : MethodCallHandler {
@@ -279,8 +278,7 @@ class EmbeddedDataHandler(private val context: Context) : MethodCallHandler {
embeddedByteLength: Long,
) {
val extension = extensionFor(mimeType)
- val targetFile = File.createTempFile("aves", extension, context.cacheDir).apply {
- deleteOnExit()
+ val targetFile = StorageUtils.createTempFile(context, extension).apply {
transferFrom(embeddedByteStream, embeddedByteLength)
}
diff --git a/android/app/src/main/kotlin/deckers/thibault/aves/channel/calls/StorageHandler.kt b/android/app/src/main/kotlin/deckers/thibault/aves/channel/calls/StorageHandler.kt
index ed5d4a2ad..22f54d6d9 100644
--- a/android/app/src/main/kotlin/deckers/thibault/aves/channel/calls/StorageHandler.kt
+++ b/android/app/src/main/kotlin/deckers/thibault/aves/channel/calls/StorageHandler.kt
@@ -36,6 +36,7 @@ class StorageHandler(private val context: Context) : MethodCallHandler {
"getRestrictedDirectories" -> ioScope.launch { safe(call, result, ::getRestrictedDirectories) }
"revokeDirectoryAccess" -> safe(call, result, ::revokeDirectoryAccess)
"deleteEmptyDirectories" -> ioScope.launch { safe(call, result, ::deleteEmptyDirectories) }
+ "deleteTempDirectory" -> ioScope.launch { safe(call, result, ::deleteTempDirectory) }
"canRequestMediaFileBulkAccess" -> safe(call, result, ::canRequestMediaFileBulkAccess)
"canInsertMedia" -> safe(call, result, ::canInsertMedia)
else -> result.notImplemented()
@@ -200,6 +201,10 @@ class StorageHandler(private val context: Context) : MethodCallHandler {
result.success(deleted)
}
+ private fun deleteTempDirectory(@Suppress("unused_parameter") call: MethodCall, result: MethodChannel.Result) {
+ result.success(StorageUtils.deleteTempDirectory(context))
+ }
+
private fun canRequestMediaFileBulkAccess(@Suppress("unused_parameter") call: MethodCall, result: MethodChannel.Result) {
result.success(Build.VERSION.SDK_INT >= Build.VERSION_CODES.R)
}
diff --git a/android/app/src/main/kotlin/deckers/thibault/aves/channel/calls/fetchers/RegionFetcher.kt b/android/app/src/main/kotlin/deckers/thibault/aves/channel/calls/fetchers/RegionFetcher.kt
index 60276c136..65a3af050 100644
--- a/android/app/src/main/kotlin/deckers/thibault/aves/channel/calls/fetchers/RegionFetcher.kt
+++ b/android/app/src/main/kotlin/deckers/thibault/aves/channel/calls/fetchers/RegionFetcher.kt
@@ -16,7 +16,6 @@ import deckers.thibault.aves.utils.BitmapUtils.getBytes
import deckers.thibault.aves.utils.MimeTypes
import deckers.thibault.aves.utils.StorageUtils
import io.flutter.plugin.common.MethodChannel
-import java.io.File
import kotlin.math.roundToInt
class RegionFetcher internal constructor(
@@ -113,8 +112,7 @@ class RegionFetcher internal constructor(
.submit()
try {
val bitmap = target.get()
- val tempFile = File.createTempFile("aves", null, context.cacheDir).apply {
- deleteOnExit()
+ val tempFile = StorageUtils.createTempFile(context).apply {
outputStream().use { output ->
bitmap.compress(Bitmap.CompressFormat.JPEG, 100, output)
}
diff --git a/android/app/src/main/kotlin/deckers/thibault/aves/metadata/Metadata.kt b/android/app/src/main/kotlin/deckers/thibault/aves/metadata/Metadata.kt
index c8522a6cf..b82f1cf12 100644
--- a/android/app/src/main/kotlin/deckers/thibault/aves/metadata/Metadata.kt
+++ b/android/app/src/main/kotlin/deckers/thibault/aves/metadata/Metadata.kt
@@ -160,8 +160,7 @@ object Metadata {
}
fun createPreviewFile(context: Context, uri: Uri): File {
- return File.createTempFile("aves", null, context.cacheDir).apply {
- deleteOnExit()
+ return StorageUtils.createTempFile(context).apply {
transferFrom(StorageUtils.openInputStream(context, uri), PREVIEW_SIZE)
}
}
diff --git a/android/app/src/main/kotlin/deckers/thibault/aves/model/provider/ImageProvider.kt b/android/app/src/main/kotlin/deckers/thibault/aves/model/provider/ImageProvider.kt
index 20fa622af..8abdc5648 100644
--- a/android/app/src/main/kotlin/deckers/thibault/aves/model/provider/ImageProvider.kt
+++ b/android/app/src/main/kotlin/deckers/thibault/aves/model/provider/ImageProvider.kt
@@ -381,8 +381,7 @@ abstract class ImageProvider {
targetUri: Uri,
targetPath: String,
) {
- val editableFile = File.createTempFile("aves", null).apply {
- deleteOnExit()
+ val editableFile = StorageUtils.createTempFile(context).apply {
// copy original file to a temporary file for editing
val inputStream = StorageUtils.openInputStream(context, targetUri)
transferFrom(inputStream, File(targetPath).length())
@@ -514,8 +513,7 @@ abstract class ImageProvider {
output.write(bytes)
}
} else {
- val editableFile = withContext(Dispatchers.IO) { File.createTempFile("aves", null) }.apply {
- deleteOnExit()
+ val editableFile = withContext(Dispatchers.IO) { StorageUtils.createTempFile(contextWrapper) }.apply {
transferFrom(ByteArrayInputStream(bytes), bytes.size.toLong())
}
@@ -649,8 +647,7 @@ abstract class ImageProvider {
val originalFileSize = File(path).length()
val videoSize = MultiPage.getMotionPhotoOffset(context, uri, mimeType, originalFileSize)?.let { it.toInt() + trailerDiff }
var videoBytes: ByteArray? = null
- val editableFile = File.createTempFile("aves", null).apply {
- deleteOnExit()
+ val editableFile = StorageUtils.createTempFile(context).apply {
try {
if (videoSize != null) {
// handle motion photo and embedded video separately
@@ -733,8 +730,7 @@ abstract class ImageProvider {
val originalFileSize = File(path).length()
val videoSize = MultiPage.getMotionPhotoOffset(context, uri, mimeType, originalFileSize)?.let { it.toInt() + trailerDiff }
var videoBytes: ByteArray? = null
- val editableFile = File.createTempFile("aves", null).apply {
- deleteOnExit()
+ val editableFile = StorageUtils.createTempFile(context).apply {
try {
if (videoSize != null) {
// handle motion photo and embedded video separately
@@ -898,8 +894,7 @@ abstract class ImageProvider {
val originalFileSize = File(path).length()
val videoSize = MultiPage.getMotionPhotoOffset(context, uri, mimeType, originalFileSize)?.let { it.toInt() + trailerDiff }
- val editableFile = File.createTempFile("aves", null).apply {
- deleteOnExit()
+ val editableFile = StorageUtils.createTempFile(context).apply {
try {
editXmpWithPixy(
context = context,
@@ -1275,8 +1270,7 @@ abstract class ImageProvider {
return
}
- val editableFile = File.createTempFile("aves", null).apply {
- deleteOnExit()
+ val editableFile = StorageUtils.createTempFile(context).apply {
try {
val inputStream = StorageUtils.openInputStream(context, uri)
// partial copy
@@ -1316,8 +1310,7 @@ abstract class ImageProvider {
val originalFileSize = File(path).length()
val videoSize = MultiPage.getMotionPhotoOffset(context, uri, mimeType, originalFileSize)?.toInt()
- val editableFile = File.createTempFile("aves", null).apply {
- deleteOnExit()
+ val editableFile = StorageUtils.createTempFile(context).apply {
try {
outputStream().use { output ->
// reopen input to read from start
diff --git a/android/app/src/main/kotlin/deckers/thibault/aves/utils/StorageUtils.kt b/android/app/src/main/kotlin/deckers/thibault/aves/utils/StorageUtils.kt
index 306796ab3..a24cceda8 100644
--- a/android/app/src/main/kotlin/deckers/thibault/aves/utils/StorageUtils.kt
+++ b/android/app/src/main/kotlin/deckers/thibault/aves/utils/StorageUtils.kt
@@ -26,6 +26,7 @@ import deckers.thibault.aves.utils.PermissionManager.getGrantedDirForPath
import deckers.thibault.aves.utils.UriUtils.tryParseId
import java.io.File
import java.io.FileInputStream
+import java.io.IOException
import java.io.InputStream
import java.io.OutputStream
import java.util.*
@@ -593,8 +594,7 @@ object StorageUtils {
uriPath?.contains("/file/") == true -> {
// e.g. `content://media/external/file/...`
// create an ad-hoc temporary file for decoding only
- File.createTempFile("aves", null).apply {
- deleteOnExit()
+ createTempFile(context).apply {
try {
transferFrom(openInputStream(context, uri), sizeBytes)
return Uri.fromFile(this)
@@ -714,6 +714,25 @@ object StorageUtils {
}
}
+ private fun getTempDirectory(context: Context): File = File(context.cacheDir, "temp")
+
+ fun createTempFile(context: Context, extension: String? = null): File {
+ val directory = getTempDirectory(context)
+ if (!directory.exists() && !directory.mkdirs()) {
+ throw IOException("failed to create directories at path=$directory")
+ }
+ val tempFile = File.createTempFile("aves", extension, directory)
+ // `deleteOnExit` is unreliable, but it does not hurt
+ tempFile.deleteOnExit()
+ return tempFile
+ }
+
+ fun deleteTempDirectory(context: Context): Boolean {
+ val directory = getTempDirectory(context)
+ if (!directory.exists()) return false
+ return directory.deleteRecursively()
+ }
+
// convenience methods
fun getFolderSize(f: File): Long {
diff --git a/lib/l10n/app_en.arb b/lib/l10n/app_en.arb
index 65d5ab093..26c6de81f 100644
--- a/lib/l10n/app_en.arb
+++ b/lib/l10n/app_en.arb
@@ -543,6 +543,7 @@
"aboutDataUsageMisc": "Misc",
"aboutDataUsageInternal": "Internal",
"aboutDataUsageExternal": "External",
+ "aboutDataUsageClearCache": "Clear Cache",
"aboutCreditsSectionTitle": "Credits",
"aboutCreditsWorldAtlas1": "This app uses a TopoJSON file from",
diff --git a/lib/services/storage_service.dart b/lib/services/storage_service.dart
index 268076c27..835e0027a 100644
--- a/lib/services/storage_service.dart
+++ b/lib/services/storage_service.dart
@@ -27,6 +27,8 @@ abstract class StorageService {
// returns number of deleted directories
Future deleteEmptyRegularDirectories(Set dirPaths);
+ Future deleteTempDirectory();
+
// returns whether user granted access to a directory of his choosing
Future requestDirectoryAccess(String path);
@@ -158,6 +160,17 @@ class PlatformStorageService implements StorageService {
return 0;
}
+ @override
+ Future deleteTempDirectory() async {
+ try {
+ final result = await _platform.invokeMethod('deleteTempDirectory');
+ if (result != null) return result as bool;
+ } on PlatformException catch (e, stack) {
+ await reportService.recordError(e, stack);
+ }
+ return false;
+ }
+
@override
Future canRequestMediaFileBulkAccess() async {
try {
diff --git a/lib/widgets/about/data_usage.dart b/lib/widgets/about/data_usage.dart
index a522355cc..b811a28f5 100644
--- a/lib/widgets/about/data_usage.dart
+++ b/lib/widgets/about/data_usage.dart
@@ -7,6 +7,7 @@ import 'package:aves/utils/file_utils.dart';
import 'package:aves/widgets/common/action_mixins/feedback.dart';
import 'package:aves/widgets/common/extensions/build_context.dart';
import 'package:aves/widgets/common/identity/aves_donut.dart';
+import 'package:aves/widgets/common/identity/buttons/outlined_button.dart';
import 'package:flutter/material.dart';
import 'package:provider/provider.dart';
@@ -24,7 +25,7 @@ class _AboutDataUsageState extends State with FeedbackMixin {
@override
void initState() {
super.initState();
- _loader = storageService.getDataUsage();
+ _reload();
}
@override
@@ -81,6 +82,18 @@ class _AboutDataUsageState extends State with FeedbackMixin {
byTypes: cacheMap,
animationDuration: animationDuration,
),
+ Center(
+ child: AvesOutlinedButton(
+ label: context.l10n.aboutDataUsageClearCache,
+ onPressed: () async {
+ await storageService.deleteTempDirectory();
+ await mediaFetchService.clearSizedThumbnailDiskCache();
+ imageCache.clear();
+ _reload();
+ setState(() {});
+ },
+ ),
+ ),
],
);
},
@@ -92,6 +105,10 @@ class _AboutDataUsageState extends State with FeedbackMixin {
],
);
}
+
+ void _reload() {
+ _loader = storageService.getDataUsage();
+ }
}
class DataUsageDonut extends StatelessWidget {
diff --git a/lib/widgets/aves_app.dart b/lib/widgets/aves_app.dart
index e21c30380..dbf118a55 100644
--- a/lib/widgets/aves_app.dart
+++ b/lib/widgets/aves_app.dart
@@ -458,6 +458,7 @@ class _AvesAppState extends State with WidgetsBindingObserver {
_monitorSettings();
videoControllerFactory.init();
+ unawaited(storageService.deleteTempDirectory());
unawaited(_setupErrorReporting());
debugPrint('App setup in ${stopwatch.elapsed.inMilliseconds}ms');
diff --git a/untranslated.json b/untranslated.json
index cb38526b1..28f9a5ee4 100644
--- a/untranslated.json
+++ b/untranslated.json
@@ -307,6 +307,7 @@
"aboutDataUsageMisc",
"aboutDataUsageInternal",
"aboutDataUsageExternal",
+ "aboutDataUsageClearCache",
"aboutCreditsSectionTitle",
"aboutCreditsWorldAtlas1",
"aboutCreditsWorldAtlas2",
@@ -732,6 +733,7 @@
"aboutDataUsageMisc",
"aboutDataUsageInternal",
"aboutDataUsageExternal",
+ "aboutDataUsageClearCache",
"aboutCreditsSectionTitle",
"aboutCreditsWorldAtlas1",
"aboutCreditsWorldAtlas2",
@@ -1331,6 +1333,7 @@
"aboutDataUsageMisc",
"aboutDataUsageInternal",
"aboutDataUsageExternal",
+ "aboutDataUsageClearCache",
"aboutCreditsSectionTitle",
"aboutCreditsWorldAtlas1",
"aboutCreditsWorldAtlas2",
@@ -1864,6 +1867,7 @@
"aboutDataUsageMisc",
"aboutDataUsageInternal",
"aboutDataUsageExternal",
+ "aboutDataUsageClearCache",
"aboutCreditsSectionTitle",
"aboutCreditsWorldAtlas1",
"aboutCreditsWorldAtlas2",
@@ -2214,13 +2218,15 @@
],
"cs": [
- "overlayHistogramLuminance"
+ "overlayHistogramLuminance",
+ "aboutDataUsageClearCache"
],
"de": [
"overlayHistogramNone",
"overlayHistogramRGB",
"overlayHistogramLuminance",
+ "aboutDataUsageClearCache",
"settingsViewerShowHistogram"
],
@@ -2235,9 +2241,18 @@
"aboutDataUsageMisc",
"aboutDataUsageInternal",
"aboutDataUsageExternal",
+ "aboutDataUsageClearCache",
"settingsViewerShowHistogram"
],
+ "es": [
+ "aboutDataUsageClearCache"
+ ],
+
+ "eu": [
+ "aboutDataUsageClearCache"
+ ],
+
"fa": [
"saveCopyButtonLabel",
"applyTooltip",
@@ -2421,6 +2436,7 @@
"aboutDataUsageMisc",
"aboutDataUsageInternal",
"aboutDataUsageExternal",
+ "aboutDataUsageClearCache",
"aboutCreditsSectionTitle",
"aboutCreditsWorldAtlas1",
"aboutCreditsWorldAtlas2",
@@ -2942,6 +2958,7 @@
"aboutDataUsageMisc",
"aboutDataUsageInternal",
"aboutDataUsageExternal",
+ "aboutDataUsageClearCache",
"aboutCreditsSectionTitle",
"aboutCreditsWorldAtlas1",
"aboutCreditsWorldAtlas2",
@@ -3291,6 +3308,10 @@
"filePickerUseThisFolder"
],
+ "fr": [
+ "aboutDataUsageClearCache"
+ ],
+
"gl": [
"columnCount",
"saveCopyButtonLabel",
@@ -3489,6 +3510,7 @@
"aboutDataUsageMisc",
"aboutDataUsageInternal",
"aboutDataUsageExternal",
+ "aboutDataUsageClearCache",
"aboutCreditsSectionTitle",
"aboutCreditsWorldAtlas1",
"aboutCreditsWorldAtlas2",
@@ -4164,6 +4186,7 @@
"aboutDataUsageMisc",
"aboutDataUsageInternal",
"aboutDataUsageExternal",
+ "aboutDataUsageClearCache",
"aboutCreditsSectionTitle",
"aboutCreditsWorldAtlas1",
"aboutCreditsWorldAtlas2",
@@ -4819,6 +4842,7 @@
"aboutDataUsageMisc",
"aboutDataUsageInternal",
"aboutDataUsageExternal",
+ "aboutDataUsageClearCache",
"aboutCreditsSectionTitle",
"aboutCreditsWorldAtlas1",
"aboutCreditsWorldAtlas2",
@@ -5168,6 +5192,18 @@
"filePickerUseThisFolder"
],
+ "hu": [
+ "aboutDataUsageClearCache"
+ ],
+
+ "id": [
+ "aboutDataUsageClearCache"
+ ],
+
+ "it": [
+ "aboutDataUsageClearCache"
+ ],
+
"ja": [
"columnCount",
"saveCopyButtonLabel",
@@ -5205,6 +5241,7 @@
"aboutDataUsageMisc",
"aboutDataUsageInternal",
"aboutDataUsageExternal",
+ "aboutDataUsageClearCache",
"stateEmpty",
"placeEmpty",
"searchStatesSectionTitle",
@@ -5529,6 +5566,7 @@
"aboutDataUsageMisc",
"aboutDataUsageInternal",
"aboutDataUsageExternal",
+ "aboutDataUsageClearCache",
"aboutCreditsSectionTitle",
"aboutCreditsWorldAtlas1",
"aboutCreditsWorldAtlas2",
@@ -5878,6 +5916,10 @@
"filePickerUseThisFolder"
],
+ "ko": [
+ "aboutDataUsageClearCache"
+ ],
+
"lt": [
"columnCount",
"saveCopyButtonLabel",
@@ -5937,6 +5979,7 @@
"aboutDataUsageMisc",
"aboutDataUsageInternal",
"aboutDataUsageExternal",
+ "aboutDataUsageClearCache",
"drawerPlacePage",
"statePageTitle",
"stateEmpty",
@@ -6295,6 +6338,7 @@
"aboutDataUsageMisc",
"aboutDataUsageInternal",
"aboutDataUsageExternal",
+ "aboutDataUsageClearCache",
"aboutCreditsSectionTitle",
"aboutCreditsWorldAtlas1",
"aboutCreditsWorldAtlas2",
@@ -6652,6 +6696,7 @@
"widgetOpenPageCollection",
"widgetOpenPageViewer",
"menuActionConfigureView",
+ "aboutDataUsageClearCache",
"newFilterBanner",
"settingsDefault",
"settingsNavigationDrawerTile",
@@ -6767,6 +6812,7 @@
"patternDialogEnter",
"aboutDataUsageInternal",
"aboutDataUsageExternal",
+ "aboutDataUsageClearCache",
"settingsCollectionBurstPatternsTile",
"settingsCollectionBurstPatternsNone",
"settingsViewerShowHistogram",
@@ -6825,6 +6871,7 @@
"aboutDataUsageMisc",
"aboutDataUsageInternal",
"aboutDataUsageExternal",
+ "aboutDataUsageClearCache",
"drawerPlacePage",
"statePageTitle",
"stateEmpty",
@@ -6875,6 +6922,7 @@
"aboutDataUsageMisc",
"aboutDataUsageInternal",
"aboutDataUsageExternal",
+ "aboutDataUsageClearCache",
"aboutCreditsSectionTitle",
"aboutLicensesBanner",
"aboutLicensesAndroidLibrariesSectionTitle",
@@ -7182,6 +7230,7 @@
"aboutDataUsageMisc",
"aboutDataUsageInternal",
"aboutDataUsageExternal",
+ "aboutDataUsageClearCache",
"aboutCreditsWorldAtlas1",
"aboutCreditsWorldAtlas2",
"aboutLicensesSectionTitle",
@@ -7502,6 +7551,14 @@
"filePickerUseThisFolder"
],
+ "pl": [
+ "aboutDataUsageClearCache"
+ ],
+
+ "pt": [
+ "aboutDataUsageClearCache"
+ ],
+
"ro": [
"saveCopyButtonLabel",
"applyTooltip",
@@ -7527,6 +7584,7 @@
"aboutDataUsageMisc",
"aboutDataUsageInternal",
"aboutDataUsageExternal",
+ "aboutDataUsageClearCache",
"settingsAskEverytime",
"settingsViewerShowHistogram",
"settingsVideoPlaybackTile",
@@ -7536,6 +7594,14 @@
"tagEditorDiscardDialogMessage"
],
+ "ru": [
+ "aboutDataUsageClearCache"
+ ],
+
+ "sk": [
+ "aboutDataUsageClearCache"
+ ],
+
"sl": [
"itemCount",
"columnCount",
@@ -7862,6 +7928,7 @@
"aboutDataUsageMisc",
"aboutDataUsageInternal",
"aboutDataUsageExternal",
+ "aboutDataUsageClearCache",
"aboutCreditsSectionTitle",
"aboutCreditsWorldAtlas1",
"aboutCreditsWorldAtlas2",
@@ -8276,6 +8343,7 @@
"aboutDataUsageMisc",
"aboutDataUsageInternal",
"aboutDataUsageExternal",
+ "aboutDataUsageClearCache",
"collectionActionShowTitleSearch",
"collectionActionHideTitleSearch",
"collectionActionAddShortcut",
@@ -8664,6 +8732,7 @@
"aboutDataUsageMisc",
"aboutDataUsageInternal",
"aboutDataUsageExternal",
+ "aboutDataUsageClearCache",
"drawerPlacePage",
"statePageTitle",
"stateEmpty",
@@ -8687,6 +8756,14 @@
"tagPlaceholderState"
],
+ "uk": [
+ "aboutDataUsageClearCache"
+ ],
+
+ "vi": [
+ "aboutDataUsageClearCache"
+ ],
+
"zh": [
"saveCopyButtonLabel",
"chipActionGoToPlacePage",
@@ -8728,6 +8805,7 @@
"aboutDataUsageMisc",
"aboutDataUsageInternal",
"aboutDataUsageExternal",
+ "aboutDataUsageClearCache",
"drawerPlacePage",
"statePageTitle",
"stateEmpty",
@@ -8764,6 +8842,7 @@
"aboutDataUsageMisc",
"aboutDataUsageInternal",
"aboutDataUsageExternal",
+ "aboutDataUsageClearCache",
"settingsViewerShowHistogram"
]
}