Skip to main content

diskforge_core/rules/
android.rs

1use std::path::{Path, PathBuf};
2
3use crate::sizing;
4use crate::types::{Category, CleanableItem, Risk};
5
6/// Scan for Android development artifacts (SDK components, NDK versions, AVDs)
7pub fn scan_android(home: &str) -> Vec<CleanableItem> {
8    let mut items = Vec::new();
9    if let Some(sdk) = find_android_sdk(home) {
10        scan_ndk_versions(&sdk, &mut items);
11        scan_platforms(&sdk, &mut items);
12        scan_build_tools(&sdk, &mut items);
13        scan_system_images(&sdk, &mut items);
14    }
15    scan_avds(home, &mut items);
16    items
17}
18
19/// Locate the Android SDK root, checking env vars and common macOS paths
20fn find_android_sdk(home: &str) -> Option<PathBuf> {
21    for var in &["ANDROID_HOME", "ANDROID_SDK_ROOT"] {
22        if let Ok(val) = std::env::var(var) {
23            let p = PathBuf::from(val);
24            if p.exists() {
25                return Some(p);
26            }
27        }
28    }
29    [
30        format!("{home}/Library/Android/sdk"),
31        format!("{home}/android-sdk"),
32    ]
33    .into_iter()
34    .map(PathBuf::from)
35    .find(|path| path.exists())
36}
37
38/// List each NDK version — newest is Medium risk, older ones are Low
39fn scan_ndk_versions(sdk: &Path, items: &mut Vec<CleanableItem>) {
40    let ndk_dir = sdk.join("ndk");
41    if !ndk_dir.exists() {
42        return;
43    }
44    let Ok(entries) = std::fs::read_dir(&ndk_dir) else {
45        return;
46    };
47    let mut versions: Vec<(PathBuf, u64)> = entries
48        .flatten()
49        .filter(|e| e.file_type().map(|t| t.is_dir()).unwrap_or(false))
50        .map(|e| {
51            let p = e.path();
52            let size = sizing::dir_size(&p);
53            (p, size)
54        })
55        .filter(|(_, size)| *size > 1024 * 1024)
56        .collect();
57
58    // Descending sort: newest version first
59    versions.sort_by(|a, b| b.0.file_name().cmp(&a.0.file_name()));
60
61    for (i, (path, size)) in versions.into_iter().enumerate() {
62        let version = path
63            .file_name()
64            .map(|n| n.to_string_lossy().to_string())
65            .unwrap_or_default();
66        let risk = if i == 0 { Risk::Medium } else { Risk::Low };
67        let last_modified = sizing::dir_last_modified(&path);
68        items.push(CleanableItem {
69            category: Category::Android,
70            path,
71            size,
72            risk,
73            regenerates: false,
74            regeneration_hint: Some("Android Studio → SDK Manager → NDK to reinstall".into()),
75            last_modified,
76            description: format!("Android NDK {version}"),
77            cleanup_command: None,
78        });
79    }
80}
81
82/// List each Android platform (android-30, android-31, …)
83fn scan_platforms(sdk: &Path, items: &mut Vec<CleanableItem>) {
84    let dir = sdk.join("platforms");
85    if !dir.exists() {
86        return;
87    }
88    let Ok(entries) = std::fs::read_dir(&dir) else {
89        return;
90    };
91    for entry in entries.flatten() {
92        let path = entry.path();
93        if !path.is_dir() {
94            continue;
95        }
96        let size = sizing::dir_size(&path);
97        if size < 1024 * 1024 {
98            continue;
99        }
100        let name = path
101            .file_name()
102            .map(|n| n.to_string_lossy().to_string())
103            .unwrap_or_default();
104        let last_modified = sizing::dir_last_modified(&path);
105        items.push(CleanableItem {
106            category: Category::Android,
107            path,
108            size,
109            risk: Risk::Low,
110            regenerates: false,
111            regeneration_hint: Some(
112                "Android Studio → SDK Manager → SDK Platforms to reinstall".into(),
113            ),
114            last_modified,
115            description: format!("Android Platform: {name}"),
116            cleanup_command: None,
117        });
118    }
119}
120
121/// List each build-tools version — newest is Medium risk, older ones are Low
122fn scan_build_tools(sdk: &Path, items: &mut Vec<CleanableItem>) {
123    let dir = sdk.join("build-tools");
124    if !dir.exists() {
125        return;
126    }
127    let Ok(entries) = std::fs::read_dir(&dir) else {
128        return;
129    };
130    let mut versions: Vec<(PathBuf, u64)> = entries
131        .flatten()
132        .filter(|e| e.file_type().map(|t| t.is_dir()).unwrap_or(false))
133        .map(|e| {
134            let p = e.path();
135            let size = sizing::dir_size(&p);
136            (p, size)
137        })
138        .filter(|(_, size)| *size > 1024 * 1024)
139        .collect();
140
141    versions.sort_by(|a, b| b.0.file_name().cmp(&a.0.file_name()));
142
143    for (i, (path, size)) in versions.into_iter().enumerate() {
144        let version = path
145            .file_name()
146            .map(|n| n.to_string_lossy().to_string())
147            .unwrap_or_default();
148        let risk = if i == 0 { Risk::Medium } else { Risk::Low };
149        let last_modified = sizing::dir_last_modified(&path);
150        items.push(CleanableItem {
151            category: Category::Android,
152            path,
153            size,
154            risk,
155            regenerates: false,
156            regeneration_hint: Some("Android Studio → SDK Manager → SDK Tools to reinstall".into()),
157            last_modified,
158            description: format!("Android Build Tools {version}"),
159            cleanup_command: None,
160        });
161    }
162}
163
164/// List system images (AVD base images) — android-XX/variant/arch
165fn scan_system_images(sdk: &Path, items: &mut Vec<CleanableItem>) {
166    let dir = sdk.join("system-images");
167    if !dir.exists() {
168        return;
169    }
170    // Structure: system-images/android-XX/google_apis/x86_64/
171    let Ok(api_entries) = std::fs::read_dir(&dir) else {
172        return;
173    };
174    for api_entry in api_entries.flatten() {
175        let api_path = api_entry.path();
176        if !api_path.is_dir() {
177            continue;
178        }
179        let api_name = api_path
180            .file_name()
181            .map(|n| n.to_string_lossy().to_string())
182            .unwrap_or_default();
183        let Ok(variant_entries) = std::fs::read_dir(&api_path) else {
184            continue;
185        };
186        for variant_entry in variant_entries.flatten() {
187            let variant_path = variant_entry.path();
188            if !variant_path.is_dir() {
189                continue;
190            }
191            let variant_name = variant_path
192                .file_name()
193                .map(|n| n.to_string_lossy().to_string())
194                .unwrap_or_default();
195            let Ok(arch_entries) = std::fs::read_dir(&variant_path) else {
196                continue;
197            };
198            for arch_entry in arch_entries.flatten() {
199                let arch_path = arch_entry.path();
200                if !arch_path.is_dir() {
201                    continue;
202                }
203                let size = sizing::dir_size(&arch_path);
204                if size < 100 * 1024 * 1024 {
205                    // skip < 100 MB
206                    continue;
207                }
208                let arch_name = arch_path
209                    .file_name()
210                    .map(|n| n.to_string_lossy().to_string())
211                    .unwrap_or_default();
212                let last_modified = sizing::dir_last_modified(&arch_path);
213                items.push(CleanableItem {
214                    category: Category::Android,
215                    path: arch_path,
216                    size,
217                    risk: Risk::Low,
218                    regenerates: false,
219                    regeneration_hint: Some(
220                        "Android Studio → SDK Manager → SDK Images to reinstall".into(),
221                    ),
222                    last_modified,
223                    description: format!(
224                        "Android System Image: {api_name}/{variant_name}/{arch_name}"
225                    ),
226                    cleanup_command: None,
227                });
228            }
229        }
230    }
231}
232
233/// List Android Virtual Devices (~/.android/avd/*.avd)
234fn scan_avds(home: &str, items: &mut Vec<CleanableItem>) {
235    let avd_dir = PathBuf::from(format!("{home}/.android/avd"));
236    if !avd_dir.exists() {
237        return;
238    }
239    let Ok(entries) = std::fs::read_dir(&avd_dir) else {
240        return;
241    };
242    for entry in entries.flatten() {
243        let path = entry.path();
244        if path.extension().map(|e| e != "avd").unwrap_or(true) {
245            continue;
246        }
247        let size = sizing::dir_size(&path);
248        if size < 1024 * 1024 {
249            continue;
250        }
251        let name = path
252            .file_stem()
253            .map(|n| format!("Android AVD: {}", n.to_string_lossy()))
254            .unwrap_or_else(|| "Android AVD".into());
255        let last_modified = sizing::dir_last_modified(&path);
256        items.push(CleanableItem {
257            category: Category::Android,
258            path,
259            size,
260            risk: Risk::Medium,
261            regenerates: false,
262            regeneration_hint: Some("Android Studio → Device Manager to recreate".into()),
263            last_modified,
264            description: name,
265            cleanup_command: None,
266        });
267    }
268}