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" ] }