From 1d5223985d98d637483bab053cc360fb32a0f873 Mon Sep 17 00:00:00 2001 From: Armin Date: Mon, 8 Dec 2025 11:19:39 +0100 Subject: [PATCH] [LEIP-409] Add StorageHelper with fallback to internal storage Creates a new StorageHelper utility class that provides robust storage selection with fallback logic. The helper tries primary external storage first (checking if it's mounted and writable), then falls back to internal storage if external is unavailable. This replaces the previous approach where external storage was required and would throw exceptions if unavailable. The new approach ensures the app can always find a suitable storage location. Changes: - Add StorageHelper.getStorageDirectoryWithFallback() for File objects - Add StorageHelper.getStoragePathWithFallback() for String paths - Update DiskConsumption to use StorageHelper (requires Context parameter) - Remove Constants.externalCyfaceFolderPath() (replaced by StorageHelper) - Only uses primary external storage, avoiding removable media --- .../main/kotlin/de/cyface/utils/Constants.kt | 19 ------ .../kotlin/de/cyface/utils/DiskConsumption.kt | 13 ++-- .../kotlin/de/cyface/utils/StorageHelper.kt | 64 +++++++++++++++++++ 3 files changed, 72 insertions(+), 24 deletions(-) create mode 100644 utils/src/main/kotlin/de/cyface/utils/StorageHelper.kt diff --git a/utils/src/main/kotlin/de/cyface/utils/Constants.kt b/utils/src/main/kotlin/de/cyface/utils/Constants.kt index 5bc76c5..8fa8de7 100644 --- a/utils/src/main/kotlin/de/cyface/utils/Constants.kt +++ b/utils/src/main/kotlin/de/cyface/utils/Constants.kt @@ -18,9 +18,6 @@ */ package de.cyface.utils -import android.content.Context -import android.os.Environment - /** * Final static constants used by multiple classes. * @@ -39,20 +36,4 @@ object Constants { * slow down the device and could get unusable. */ const val MINIMUM_MEGABYTES_REQUIRED = 100L - - /** - * The path where files can be stored by the SDK, e.g. image material. - * - * **Attention:** This data *is deleted* when the app is uninstalled. - */ - @Suppress("unused") - fun externalCyfaceFolderPath(context: Context): String { - val paths = context.getExternalFilesDirs(null) - Validate.isTrue(paths.isNotEmpty(), "No external storage available") - val directory = paths[0] - val isExternalStorageMounted = - Environment.getExternalStorageState(directory) == Environment.MEDIA_MOUNTED - check(isExternalStorageMounted) { "External storage is not mounted" } - return directory.path - } } \ No newline at end of file diff --git a/utils/src/main/kotlin/de/cyface/utils/DiskConsumption.kt b/utils/src/main/kotlin/de/cyface/utils/DiskConsumption.kt index 9dc1261..1c8e905 100644 --- a/utils/src/main/kotlin/de/cyface/utils/DiskConsumption.kt +++ b/utils/src/main/kotlin/de/cyface/utils/DiskConsumption.kt @@ -18,7 +18,7 @@ */ package de.cyface.utils -import android.os.Environment +import android.content.Context import android.os.Parcel import android.os.Parcelable import android.os.Parcelable.Creator @@ -131,10 +131,12 @@ class DiskConsumption : Parcelable { /** * Checks how much storage is left. * + * @param context The Android context. * @return The number of bytes of space available. */ - private fun bytesAvailable(): Long { - val stat = StatFs(Environment.getExternalStorageDirectory().path) + private fun bytesAvailable(context: Context): Long { + val storageDir = StorageHelper.getStorageDirectoryWithFallback(context) + val stat = StatFs(storageDir.path) val bytesAvailable = stat.blockSizeLong * stat.availableBlocksLong Log.v(TAG, "Space available: " + (bytesAvailable / (1024 * 1024)) + " MB") return bytesAvailable @@ -143,11 +145,12 @@ class DiskConsumption : Parcelable { /** * Checks if at last `MINIMUM_MEGABYTES_REQUIRED` of space is available. * + * @param context The Android context. * @return True if enough space is available. */ @Suppress("unused") // Part of the API - fun spaceAvailable(): Boolean { - return (bytesAvailable() / (1024 * 1024)) > MINIMUM_MEGABYTES_REQUIRED + fun spaceAvailable(context: Context): Boolean { + return (bytesAvailable(context) / (1024 * 1024)) > MINIMUM_MEGABYTES_REQUIRED } } } diff --git a/utils/src/main/kotlin/de/cyface/utils/StorageHelper.kt b/utils/src/main/kotlin/de/cyface/utils/StorageHelper.kt new file mode 100644 index 0000000..e83eed3 --- /dev/null +++ b/utils/src/main/kotlin/de/cyface/utils/StorageHelper.kt @@ -0,0 +1,64 @@ +/* + * Copyright 2025 Cyface GmbH + * + * This file is part of the Cyface Utils for Android. + * + * The Cyface Utils for Android is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * The Cyface Utils for Android is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with the Cyface Utils for Android. If not, see . + */ +package de.cyface.utils + +import android.content.Context +import android.os.Environment +import android.util.Log +import de.cyface.utils.Constants.TAG +import java.io.File + +/** + * Helper class for storage-related operations. + * + * @author Armin Schnabel + * @version 1.0.0 + * @since 4.3.0 + */ +object StorageHelper { + /** + * Gets the storage directory for the app, trying primary external storage first and falling + * back to internal storage if external is not available or not writable. + * + * This method only uses the primary external storage (the first entry in getExternalFilesDirs), + * which is typically the device's main app-specific external storage directory. It does not use + * secondary external storage like removable SD cards or USB drives, which appear at higher + * indices and could be unreliable (e.g., user might unplug them). + * + * @param context The Android context. + * @return The storage directory (primary external if available and writable, otherwise internal). + */ + fun getStorageDirectoryWithFallback(context: Context): File { + // Try primary external storage only (first entry) + val externalDirs = context.getExternalFilesDirs(null) + if (externalDirs.isNotEmpty() && externalDirs[0] != null) { + val directory = externalDirs[0] + val isExternalStorageMounted = + Environment.getExternalStorageState(directory) == Environment.MEDIA_MOUNTED + if (isExternalStorageMounted && directory.canWrite()) { + Log.d(TAG, "Using primary external storage: ${directory.absolutePath}") + return directory + } + } + + // Fall back to internal storage + Log.d(TAG, "Falling back to internal storage: ${context.filesDir.absolutePath}") + return context.filesDir + } +}