Android SDK, emulators and Gradle
The android category (aliases: gradle, emulators) covers the Android toolchain of a React Native or Expo developer: emulators (AVDs) and their snapshots, SDK packages, the Gradle user home, Android Studio caches and settings, and installed JDKs.
Per-project folders such as android/build, android/app/build, android/.gradle and .cxx are project artifacts, covered in React Native projects.
Quick start
Section titled “Quick start”./gradlew --stop # from any Android project: stop Gradle daemons firstlu-cleaner scan -c android # what takes space; nothing is touchedlu-cleaner clean -c android # picker: nothing selected, press a for the recommended itemslu-cleaner devices # AVDs and system images next to iOS simulators (press a for smart select)A typical scan (sizes are illustrative):
Android 21.2 GB · 9 items · ★ 7.60 GB recommended NAME SIZE AGE RISK RECO AVD Pixel 8 · API 35 · arm64-v8a 9.80 GB 1d caution System image · API 33 · Google APIs · x86_64 3.90 GB — moderate ★ NDK 27.1.12297006 2.60 GB — moderate NDK 26.1.10909125 2.40 GB — moderate ★ Quick Boot snapshots · Pixel 8 1.90 GB 1d moderate Android Studio caches · 2024.1 1.10 GB 7mo safe ★ Gradle 8.13 caches 980 MB now moderate ! Gradle daemon running — stop it first (./gradlew --stop or pkill -f GradleDaemon) Gradle 8.3 distribution (bin) 196 MB 9mo moderate ★ JDK 11.0.24 (Zulu) · aarch64 190 MB — moderate
Here NDK 27.1.12297006 is kept because it is the newest installed NDK (or because a project uses it), the x86_64 system image cannot boot on Apple Silicon, and the Gradle 8.13 caches are left out of smart select while a daemon runs.
How lu-cleaner knows what is in use
Section titled “How lu-cleaner knows what is in use”An SDK package or a Gradle version is only worth deleting if no project needs it. To find out, lu-cleaner reads your projects: it walks your project roots and worktree roots (see Configuration) down to max_depth levels (8 by default), skipping heavy folders such as node_modules, build and Pods, and reads these files:
| File | What it tells lu-cleaner |
|---|---|
gradle/wrapper/gradle-wrapper.properties |
the Gradle distribution of the wrapper (distributionUrl) |
build.gradle, build.gradle.kts, gradle.properties |
ndkVersion, buildToolsVersion, compileSdk, the CMake version, org.gradle.java.home |
app.json, app.config.js, app.config.ts |
versions set through Expo's expo-build-properties |
local.properties |
sdk.dir and ndk.dir |
node_modules/react-native/gradle/libs.versions.toml |
the NDK, build-tools and compileSdk React Native builds with |
When a React Native project has no node_modules, the react-native version in its package.json gives the default NDK of that release. Gradle versions installed with Homebrew or SDKMAN also count as in use.
This walk always uses your configured (or auto-detected) roots. Roots passed on the command line (--root, lu-cleaner artifacts <root>) only narrow the artifacts scan: with a single project as root, every version your other projects use would look unused.
Where the SDK lives
Section titled “Where the SDK lives”lu-cleaner looks for Android SDKs in this order and deduplicates them by real path:
$ANDROID_HOME, then$ANDROID_SDK_ROOT- Android Studio settings (
options/android.sdk.path.xmlof eachAndroidStudio*settings folder) sdk.dirin your projects'local.properties, only if that path exists (a committed file may name another machine's SDK)- the default
~/Library/Android/sdk - Homebrew's
android-commandlinetools(/opt/homebrew/share/…and/usr/local/share/…)
Environment variables are read from the environment first, then from your shell startup files (.zshenv, .zprofile, .zshrc, .bash_profile, .bashrc, .profile), because GUI apps and AI agents do not inherit your shell's exports. Only simple assignments are understood: literal paths, ~, $HOME and variables already defined; anything computed with $(...) or backticks is ignored.
The same resolution is used for the AVD folder ($ANDROID_AVD_HOME, $ANDROID_EMULATOR_HOME/avd, $ANDROID_USER_HOME/avd, $ANDROID_PREFS_ROOT/.android/avd, then ~/.android/avd), for $GRADLE_USER_HOME (default ~/.gradle) and for $JAVA_HOME.
SDK and AVDs on an external drive
Section titled “SDK and AVDs on an external drive”A common setup moves the SDK and the emulators to an external SSD and leaves symlinks behind, for example ~/Library/Android/sdk pointing to /Volumes/External/Android/sdk. lu-cleaner resolves every symlink and checks which volume the real files are on. Anything that is not on your home volume is reported, never deleted, and the symlinks themselves are never removed:
| Situation | What you see |
|---|---|
| Files on another volume | Android SDK on External, on external volume — no internal gain |
| Drive not mounted | on /Volumes/External, which is not mounted — nothing to clean here (link kept) |
| Symlink target missing | dangling symlink to … — nothing to clean here (link kept) |
/Volumes/External is a plain folder on the internal disk |
… is a plain folder on the internal disk, not a mounted volume — check it by hand |
| SDK on the internal disk but outside your home folder (Homebrew) | measured, outside your home folder — remove it by hand if unused |
An AVD folder in the same situation shows up as Android emulators (3 AVDs) on External, with the same warnings, and its emulators are listed for information only.
Why: deleting from an external drive frees nothing on the internal disk you are trying to clean, and when the drive is unmounted lu-cleaner cannot see what is there. The last two cases are outside the areas the safety guard allows. Inside an internal SDK, a single package that is a symlink is reported the same way.
Emulators (AVDs)
Section titled “Emulators (AVDs)”An AVD is a <name>.avd folder plus a <name>.ini file next to it. lu-cleaner shows one item per AVD, for example AVD Pixel 8 · API 35 · arm64-v8a, and removes both.
| AVD | Risk | Smart select |
|---|---|---|
| Regular AVD | caution | never: it holds installed apps, their data and snapshots |
| x86 or x86_64 AVD on Apple Silicon | moderate | yes: it cannot boot on this Mac |
Broken entry (an .ini whose folder is gone) |
safe | yes: only the .ini is removed |
| Running emulator | as above | warning emulator is running — close it first |
Other warnings tell you when an AVD has no .ini (the emulator cannot see it) or when its system image is not installed (it cannot start). Recreating an AVD in Android Studio's Device Manager is quick, but its apps and data are gone.
Quick Boot snapshots
Section titled “Quick Boot snapshots”The snapshots/ folder of an AVD holds default_boot, the RAM and device state saved on exit for Quick Boot. Deleting it only costs speed: the next launch cold-boots (30 to 90 seconds), and apps and data stay. The item, Quick Boot snapshots · Pixel 8, is moderate and recommended once stale.
If you saved named snapshots yourself, the item becomes Emulator snapshots · Pixel 8, risk caution, with a warning listing them. lu-cleaner never removes an AVD's userdata images on their own: snapshots depend on them.
SDK packages
Section titled “SDK packages”NDKs, build-tools, platforms, CMake and sources follow the same keep rules:
- Used by a project: kept. The item shows
used_by(in the details panel and in--jsonmetadata) and is never recommended. - Newest installed version: always kept. Set
keep_latestin the configuration to keep more of the newest versions (1 by default). - Anything else: recommended, as long as the project walk completed.
System images follow the emulators instead: an image used by an AVD cannot be selected, and an unused one is recommended.
| Package | Kind (-k) |
Kept when | Coming back |
|---|---|---|---|
System images, system-images/android-<API>/<tag>/<abi> |
android-system-image |
an AVD uses it (then it cannot be selected at all) | SDK Manager, 1.5 to 7 GB each |
NDK, ndk/<version> |
android-ndk |
a project uses it, or newest | AGP downloads it again (~1 GB) |
Build-tools, build-tools/<version> |
android-build-tools |
a project uses it, or newest | AGP downloads it automatically |
Platforms, platforms/android-<N> |
android-platform |
a project's compileSdk, or newest |
AGP downloads it automatically |
CMake, cmake/<version> |
android-cmake |
a project uses it, newest, or 3.22.1 (the AGP 8 default React Native builds with) |
AGP downloads the version it needs |
Sources, sources/android-<N> |
android-sources |
a project's compileSdk, or newest |
only used to browse code in the IDE |
System images are moderate like the other packages. If an AVD folder cannot be read (unmounted drive), images carry an AVD folder is not reachable — cannot tell whether an emulator uses this image and are not recommended. A folder named like 35.0.0-2 is the leftover of a failed install: safe and recommended.
Legacy and leftover SDK content:
| Item | Kind (-k) |
Risk | Smart select |
|---|---|---|---|
SDK Manager temp and partial downloads (.temp, .downloadIntermediates, temp) |
android-sdk-temp |
safe | yes; skipped while Android Studio runs |
Legacy SDK Tools, tools/ |
android-sdk-legacy-tools |
moderate | yes, when cmdline-tools/latest is installed |
Superseded cmdline-tools/<version> copies |
android-cmdline-tools-old |
moderate | yes; latest is the one in use |
Legacy ndk-bundle |
android-ndk-bundle |
moderate | yes, unless a project pins ndk.dir or ndkPath |
Intel HAXM (extras/intel) on Apple Silicon |
android-haxm |
moderate | yes |
x86 images on Apple Silicon
Section titled “x86 images on Apple Silicon”x86 and x86_64 system images and AVDs cannot boot on an Apple Silicon Mac; they are typical leftovers of a migration from an Intel Mac. lu-cleaner adds cannot boot on Apple Silicon to their note and recommends them. This is a reason to delete them rather than a danger, so it is not shown as a warning.
Gradle
Section titled “Gradle”Gradle keeps everything in its user home, $GRADLE_USER_HOME or ~/.gradle. If that folder lives on another volume, it is reported and never cleaned.
| Item | Kind (-k) |
Risk | Smart select and notes |
|---|---|---|---|
Wrapper distribution, wrapper/dists/gradle-<v>-bin (or -all) |
android-gradle-dist |
moderate | recommended when no project wrapper uses it; ./gradlew re-downloads it (130 to 200 MB) |
Interrupted wrapper download (no .ok marker) |
android-gradle-dist |
safe | recommended, unless a .part file changed in the last hour |
Per-version caches, caches/<version> |
android-gradle-version-cache |
moderate | recommended when no wrapper, Homebrew or SDKMAN Gradle uses that version |
Old-format transforms, jars or build cache (for example transforms-3 when transforms-4 exists) |
android-gradle-transforms, android-gradle-jars, android-gradle-build-cache |
safe | recommended |
| Current local build cache | android-gradle-build-cache |
safe | only speeds up rebuilds; refills itself |
| Current transforms cache | android-gradle-transforms |
moderate | dexing, jetifier and AAR extraction are recomputed (a few extra minutes) |
Dependency cache, caches/modules-2 |
android-gradle-modules |
moderate | 1 to 3 GB re-downloaded by the next build; offline builds fail until then |
| Other shared caches (Kotlin DSL, scripts, keyrings…) | android-gradle-caches-other |
moderate | rebuilt or re-downloaded |
Daemon logs, daemon/<version> |
android-gradle-daemon-logs |
safe | while a daemon of that version runs, only logs older than 24 hours are proposed |
Temp files and partial downloads, .tmp |
android-gradle-tmp |
safe | files older than a day, which neither Gradle nor other cleaners remove; recommended |
Toolchain JDKs, jdks/* |
android-gradle-jdk |
moderate | re-downloaded (200 to 300 MB) when a build requests them; never recommended |
| Native helpers, Kotlin build reports, worker state | android-gradle-misc |
safe | re-created automatically |
caches/journal-1 is never proposed: Gradle's own cache cleanup relies on it.
The last-used date of a Gradle version comes from its daemon logs, the most reliable usage signal. A distribution or cache that a project uses is not recommended as unused, but like every moderate item it is recommended once it has not been used for longer than stale_after; the next build of that project then downloads or rebuilds it. See Risk levels and smart select.
When a Gradle daemon is running
Section titled “When a Gradle daemon is running”Gradle daemons are JVM processes named java, so lu-cleaner cannot block cleaning by process name the way it does for Xcode or Android Studio. Instead, when pgrep finds a GradleDaemon, the affected items get this warning and are left out of smart select:
Gradle daemon running — stop it first (./gradlew --stop or pkill -f GradleDaemon)
Version-specific items (distributions, per-version caches) are only flagged when a daemon of that version runs, or when a daemon's version cannot be read. Shared caches (transforms, jars, build cache, modules-2, other caches, toolchain JDKs, misc) are flagged whenever any daemon runs.
Why it matters: a running daemon keeps deleted files open, so their space is not freed until it exits, and a build in progress can fail halfway. clean --yes holds these items back unless you pass --risk caution, but the warning does not stop a clean you select yourself in the picker, so stop the daemons first. ./gradlew --stop stops the daemons of that project's Gradle version; pkill -f GradleDaemon stops all of them.
Android Studio
Section titled “Android Studio”| Item | Kind (-k) |
Risk | Notes |
|---|---|---|---|
Caches of the installed version, ~/Library/Caches/Google/AndroidStudio<version> |
android-studio-cache |
moderate | rebuilt on next launch (re-indexing takes minutes); skipped while Android Studio runs |
| Caches of a version no longer installed | android-studio-cache |
safe | nothing reads them; recommended |
Logs, ~/Library/Logs/Google/AndroidStudio* |
android-studio-logs |
safe | idea.log and crash reports |
Settings of an old version, ~/Library/Application Support/Google/AndroidStudio* |
android-studio-config |
moderate | only read to migrate settings on upgrade; recommended when the current version has its own settings, never while an installed version has not been started yet (it imports them at first launch) |
SDK repository cache, legacy build cache and emulator crash dumps in ~/.android |
android-user-cache |
safe | the contents of cache, build-cache and breakpad only |
The settings folder of the version you use is never proposed. lu-cleaner finds the installed versions from the product-info.json of every Android Studio*.app in /Applications, ~/Applications and Spotlight results. If none is found (custom location, JetBrains Toolbox…), the most recently used settings folder is treated as the current one, to stay on the safe side. With keep_latest above 1, that many recently used versions count as current.
A caches folder can hold LocalHistory, the IDE's history of your file edits. It is never proposed: the item then targets the other entries of the folder.
In ~/.android, adbkey, debug.keystore and the emulator settings are never touched: they are on the guard's protected list.
Android builds need a JDK, and most machines collect several. lu-cleaner lists them all and never recommends one:
| Location | Installed by | Handling |
|---|---|---|
/Library/Java/JavaVirtualMachines |
system installers, Homebrew casks | report only: removing it needs admin rights; the note suggests brew uninstall --cask <cask> or sudo rm -rf … |
~/Library/Java/JavaVirtualMachines |
IDE downloads | moderate |
~/.sdkman/candidates/java |
SDKMAN | moderate |
~/.local/share/mise/installs/java |
mise | moderate |
~/.asdf/installs/java |
asdf | moderate |
A JDK referenced by $JAVA_HOME (environment or shell profile) or by org.gradle.java.home in ~/.gradle/gradle.properties is marked in use and cannot be selected. The details also say when a JDK is registered in Android Studio's JDK table, when it is older than JDK 17 (React Native 0.73+ needs 17), and warn about x86_64 JDKs on Apple Silicon (x86_64 JDK: runs only under Rosetta).
Other Android-related data
Section titled “Other Android-related data”These fixed locations come from the catalog:
| Item | Location | Risk |
|---|---|---|
| Kotlin daemon state | ~/.kotlin, ~/Library/Application Support/kotlin/daemon |
moderate |
| Kotlin/Native dependencies and caches | ~/.konan/dependencies, ~/.konan/cache |
moderate |
| Kotlin/Native compilers (newest kept) | ~/.konan/kotlin-native-prebuilt-* |
moderate |
| Maven local repository | ~/.m2/repository |
caution: artifacts you published locally cannot be re-downloaded |
| Skiko native libs (newest kept) | ~/.skiko/* |
safe |
| Emulator temp files | $TMPDIR/android-* |
safe; skipped while an emulator runs |
| Genymotion virtual devices | ~/.Genymobile/Genymotion/deployed/* |
report only: delete them from Genymotion |
| APKs stored for Maestro | ~/.maestro/apps/android/*.apk |
report only |
Recommended workflow
Section titled “Recommended workflow”-
Stop Gradle daemons, close every emulator and quit Android Studio:
Terminal window pkill -f GradleDaemon -
Check that your project roots cover every Android project, so "unused" can be trusted:
Terminal window lu-cleaner config show -
Review everything, including report-only items such as an SDK on an external drive:
Terminal window lu-cleaner scan -c android --all -
Open the picker, press a to select the recommended items (or pick with space), review them category by category, then press d:
Terminal window lu-cleaner clean -c android
Scripting
Section titled “Scripting”# The plan smart select would run; nothing is deletedlu-cleaner clean --yes --smart -c android --dry-run
# Unused system images (x86 images on Apple Silicon included)lu-cleaner clean --yes --smart -k android-system-image
# Gradle distributions and per-version caches no project needslu-cleaner clean --yes --smart -k android-gradle-dist,android-gradle-version-cache --dry-run
# Gradle temp files and daemon logslu-cleaner clean --yes -k android-gradle-tmp,android-gradle-daemon-logsEvery kind above can be passed to -k, and the provider id android selects all the kinds of the Android scanner at once. The catalog entries of the previous section have their own ids (android-maven-local, android-konan-dependencies…), listed in the catalog. Each item's metadata (used_by, kept, api, abi, version, real_path…) is available in lu-cleaner scan --json, see JSON output.