From 671529c21629672532dbc803dfbc6515b2b0bab8 Mon Sep 17 00:00:00 2001 From: John Trujillo Date: Tue, 24 Feb 2026 17:03:20 -0500 Subject: [PATCH 1/2] fix(storage): check available space on startup to prevent crashes Shows a blocking dialog in SplashActivity if the device storage is full. --- .../androidide/activities/SplashActivity.kt | 7 +++++ .../com/itsaky/androidide/ui/StorageDialog.kt | 27 +++++++++++++++++++ .../itsaky/androidide/utils/StorageUtils.kt | 23 ++++++++++++++++ .../viewmodel/InstallationViewModel.kt | 15 ++--------- resources/src/main/res/values/strings.xml | 4 +++ 5 files changed, 63 insertions(+), 13 deletions(-) create mode 100644 app/src/main/java/com/itsaky/androidide/ui/StorageDialog.kt create mode 100644 app/src/main/java/com/itsaky/androidide/utils/StorageUtils.kt diff --git a/app/src/main/java/com/itsaky/androidide/activities/SplashActivity.kt b/app/src/main/java/com/itsaky/androidide/activities/SplashActivity.kt index c729e68512..d5d8dc48ad 100644 --- a/app/src/main/java/com/itsaky/androidide/activities/SplashActivity.kt +++ b/app/src/main/java/com/itsaky/androidide/activities/SplashActivity.kt @@ -23,7 +23,9 @@ import android.os.Build import android.os.Bundle import com.itsaky.androidide.app.configuration.CpuArch import com.itsaky.androidide.app.configuration.IDEBuildConfigProvider +import com.itsaky.androidide.ui.showLowStorageDialog import com.itsaky.androidide.utils.FeatureFlags +import com.itsaky.androidide.utils.hasEnoughStorageAvailable import kotlin.system.exitProcess /** @@ -33,6 +35,11 @@ class SplashActivity : Activity() { override fun onCreate(savedInstanceState: Bundle?) { super.onCreate(savedInstanceState) + if (!hasEnoughStorageAvailable()) { + showLowStorageDialog() + return + } + val isX86 = Build.SUPPORTED_ABIS.firstOrNull() in listOf(CpuArch.X86_64.abi, CpuArch.X86.abi) if (isX86 && (!IDEBuildConfigProvider.getInstance().supportsCpuAbi() || !FeatureFlags.isEmulatorUseEnabled)) { diff --git a/app/src/main/java/com/itsaky/androidide/ui/StorageDialog.kt b/app/src/main/java/com/itsaky/androidide/ui/StorageDialog.kt new file mode 100644 index 0000000000..60a705a347 --- /dev/null +++ b/app/src/main/java/com/itsaky/androidide/ui/StorageDialog.kt @@ -0,0 +1,27 @@ +package com.itsaky.androidide.ui + +import android.app.Activity +import android.app.AlertDialog +import android.content.Intent +import android.provider.Settings +import com.itsaky.androidide.resources.R +import com.itsaky.androidide.utils.getMinimumStorageNeeded + + +fun Activity.showLowStorageDialog() { + val appName = getString(R.string.app_name) + val minSpace = getMinimumStorageNeeded() + + AlertDialog.Builder(this) + .setTitle(R.string.err_insufficient_storage_title) + .setMessage(getString(R.string.err_insufficient_storage_msg, appName, minSpace)) + .setCancelable(false) + .setPositiveButton(getString(R.string.action_close_app, appName)) { _, _ -> + finishAndRemoveTask() + } + .setNegativeButton(R.string.action_free_up_space) { _, _ -> + startActivity(Intent(Settings.ACTION_INTERNAL_STORAGE_SETTINGS)) + finishAffinity() + } + .show() +} diff --git a/app/src/main/java/com/itsaky/androidide/utils/StorageUtils.kt b/app/src/main/java/com/itsaky/androidide/utils/StorageUtils.kt new file mode 100644 index 0000000000..6ab2714690 --- /dev/null +++ b/app/src/main/java/com/itsaky/androidide/utils/StorageUtils.kt @@ -0,0 +1,23 @@ +package com.itsaky.androidide.utils + +import android.os.Environment +import android.os.StatFs + +fun hasEnoughStorageAvailable(): Boolean { + return try { + val dataDir = Environment.getDataDirectory() + val stat = StatFs(dataDir.path) + val availableBytes = stat.availableBlocksLong * stat.blockSizeLong + availableBytes > getMinimumStorageNeeded().gigabytesToBytes() + } catch (_: Exception) { true } +} + +fun getMinimumStorageNeeded(): Long { + val minimumStorageStableGB = 4L + val minimumStorageExperimentalGB = 6L + + if (FeatureFlags.isExperimentsEnabled) { + return minimumStorageExperimentalGB; + } + return minimumStorageStableGB; +} diff --git a/app/src/main/java/com/itsaky/androidide/viewmodel/InstallationViewModel.kt b/app/src/main/java/com/itsaky/androidide/viewmodel/InstallationViewModel.kt index 11b082894e..95c1f7e714 100644 --- a/app/src/main/java/com/itsaky/androidide/viewmodel/InstallationViewModel.kt +++ b/app/src/main/java/com/itsaky/androidide/viewmodel/InstallationViewModel.kt @@ -11,8 +11,8 @@ import com.itsaky.androidide.events.InstallationEvent import com.itsaky.androidide.models.StorageInfo import com.itsaky.androidide.resources.R import com.itsaky.androidide.utils.Environment -import com.itsaky.androidide.utils.FeatureFlags import com.itsaky.androidide.utils.bytesToGigabytes +import com.itsaky.androidide.utils.getMinimumStorageNeeded import com.itsaky.androidide.utils.gigabytesToBytes import com.itsaky.androidide.utils.withStopWatch import com.itsaky.androidide.viewmodel.InstallationState.InstallationComplete @@ -34,17 +34,6 @@ import kotlinx.coroutines.withContext import org.slf4j.LoggerFactory class InstallationViewModel : ViewModel() { - companion object { - private const val MINIMUM_STORAGE_STABLE_GB = 4L - private const val MINIMUM_STORAGE_EXPERIMENTAL_GB = 6L - } - - private val minimumStorageNeeded: Long - get() = if (FeatureFlags.isExperimentsEnabled) { - MINIMUM_STORAGE_EXPERIMENTAL_GB - } else { - MINIMUM_STORAGE_STABLE_GB - } private val log = LoggerFactory.getLogger(InstallationViewModel::class.java) @@ -148,7 +137,7 @@ class InstallationViewModel : ViewModel() { val stat = StatFs(internalStoragePath) val availableStorageInBytes = stat.availableBlocksLong * stat.blockSizeLong - val requiredStorageInBytes = minimumStorageNeeded.gigabytesToBytes() + val requiredStorageInBytes = getMinimumStorageNeeded().gigabytesToBytes() val isLowStorage = availableStorageInBytes < requiredStorageInBytes diff --git a/resources/src/main/res/values/strings.xml b/resources/src/main/res/values/strings.xml index ccc30cd1dd..f2aefb5ebb 100644 --- a/resources/src/main/res/values/strings.xml +++ b/resources/src/main/res/values/strings.xml @@ -27,6 +27,10 @@ App Logs IDE Logs General information about this feature. + Insufficient Storage + %1$s requires at least %2$dGB of free space to run and extract the necessary tools.\n\nPlease free up space on your device and try again. + Close %1$s + Free Up Space 🤖 System: %1$s backend selected. From a58b488466e8806b41e7f2ae197a9f2e701a6aae Mon Sep 17 00:00:00 2001 From: John Trujillo Date: Wed, 25 Feb 2026 08:34:55 -0500 Subject: [PATCH 2/2] feat: Add protection against possible `ActivityNotFoundException` and fix the exception handling in `hasEnoughStorageAvailable` method --- .../main/java/com/itsaky/androidide/ui/StorageDialog.kt | 8 +++++++- .../main/java/com/itsaky/androidide/utils/StorageUtils.kt | 2 +- 2 files changed, 8 insertions(+), 2 deletions(-) diff --git a/app/src/main/java/com/itsaky/androidide/ui/StorageDialog.kt b/app/src/main/java/com/itsaky/androidide/ui/StorageDialog.kt index 60a705a347..b97d6cabf6 100644 --- a/app/src/main/java/com/itsaky/androidide/ui/StorageDialog.kt +++ b/app/src/main/java/com/itsaky/androidide/ui/StorageDialog.kt @@ -2,6 +2,7 @@ package com.itsaky.androidide.ui import android.app.Activity import android.app.AlertDialog +import android.content.ActivityNotFoundException import android.content.Intent import android.provider.Settings import com.itsaky.androidide.resources.R @@ -20,7 +21,12 @@ fun Activity.showLowStorageDialog() { finishAndRemoveTask() } .setNegativeButton(R.string.action_free_up_space) { _, _ -> - startActivity(Intent(Settings.ACTION_INTERNAL_STORAGE_SETTINGS)) + val intent = Intent(Settings.ACTION_INTERNAL_STORAGE_SETTINGS) + try { + startActivity(intent) + } catch (_: ActivityNotFoundException) { + startActivity(Intent(Settings.ACTION_SETTINGS)) + } finishAffinity() } .show() diff --git a/app/src/main/java/com/itsaky/androidide/utils/StorageUtils.kt b/app/src/main/java/com/itsaky/androidide/utils/StorageUtils.kt index 6ab2714690..41ef2d67f1 100644 --- a/app/src/main/java/com/itsaky/androidide/utils/StorageUtils.kt +++ b/app/src/main/java/com/itsaky/androidide/utils/StorageUtils.kt @@ -9,7 +9,7 @@ fun hasEnoughStorageAvailable(): Boolean { val stat = StatFs(dataDir.path) val availableBytes = stat.availableBlocksLong * stat.blockSizeLong availableBytes > getMinimumStorageNeeded().gigabytesToBytes() - } catch (_: Exception) { true } + } catch (_: Exception) { false } } fun getMinimumStorageNeeded(): Long {