Skip to main content

null_e/cleaners/
android.rs

1//! Android Studio cleanup module
2//!
3//! Handles cleanup of Android development files:
4//! - AVD (Android Virtual Devices / Emulators)
5//! - SDK components
6//! - Gradle caches
7//! - Android Studio caches
8
9use super::{calculate_dir_size, get_mtime, CleanableItem, SafetyLevel};
10use crate::error::Result;
11use std::path::PathBuf;
12
13/// Android cleaner
14pub struct AndroidCleaner {
15    home: PathBuf,
16}
17
18impl AndroidCleaner {
19    /// Create a new Android cleaner
20    pub fn new() -> Option<Self> {
21        let home = dirs::home_dir()?;
22        Some(Self { home })
23    }
24
25    /// Detect all Android cleanable items
26    pub fn detect(&self) -> Result<Vec<CleanableItem>> {
27        let mut items = Vec::new();
28
29        // AVD Emulators
30        items.extend(self.detect_avd()?);
31
32        // SDK System Images
33        items.extend(self.detect_system_images()?);
34
35        // Gradle caches (Android-specific)
36        items.extend(self.detect_gradle_caches()?);
37
38        // Android build caches
39        items.extend(self.detect_android_caches()?);
40
41        // Android Studio caches
42        items.extend(self.detect_studio_caches()?);
43
44        Ok(items)
45    }
46
47    /// Detect AVD (Android Virtual Devices)
48    fn detect_avd(&self) -> Result<Vec<CleanableItem>> {
49        let avd_path = self.home.join(".android/avd");
50
51        if !avd_path.exists() {
52            return Ok(vec![]);
53        }
54
55        let mut items = Vec::new();
56
57        if let Ok(entries) = std::fs::read_dir(&avd_path) {
58            for entry in entries.filter_map(|e| e.ok()) {
59                let path = entry.path();
60
61                // AVD directories end with .avd
62                if path.is_dir() && path.extension().map(|e| e == "avd").unwrap_or(false) {
63                    let name = path.file_stem()
64                        .map(|n| n.to_string_lossy().to_string())
65                        .unwrap_or_else(|| "Unknown".to_string());
66
67                    let (size, file_count) = calculate_dir_size(&path)?;
68                    if size == 0 {
69                        continue;
70                    }
71
72                    items.push(CleanableItem {
73                        name: format!("Emulator: {}", name),
74                        category: "Android".to_string(),
75                        subcategory: "AVD".to_string(),
76                        icon: "🤖",
77                        path,
78                        size,
79                        file_count: Some(file_count),
80                        last_modified: get_mtime(&entry.path()),
81                        description: "Android Virtual Device with user data.",
82                        safe_to_delete: SafetyLevel::Caution,
83                        clean_command: Some(format!("avdmanager delete avd -n {}", name)),
84                    });
85                }
86            }
87        }
88
89        Ok(items)
90    }
91
92    /// Detect SDK System Images
93    fn detect_system_images(&self) -> Result<Vec<CleanableItem>> {
94        // Try common SDK locations
95        let sdk_paths = [
96            self.home.join("Library/Android/sdk"),  // macOS default
97            self.home.join("Android/Sdk"),          // Linux default
98            self.home.join(".android/sdk"),         // Alternative
99        ];
100
101        let mut items = Vec::new();
102
103        for sdk_path in sdk_paths {
104            let sys_images = sdk_path.join("system-images");
105            if !sys_images.exists() {
106                continue;
107            }
108
109            // system-images/<android-version>/<variant>/<arch>
110            if let Ok(versions) = std::fs::read_dir(&sys_images) {
111                for version in versions.filter_map(|e| e.ok()) {
112                    let version_path = version.path();
113                    if !version_path.is_dir() {
114                        continue;
115                    }
116
117                    let version_name = version_path.file_name()
118                        .map(|n| n.to_string_lossy().to_string())
119                        .unwrap_or_default();
120
121                    if let Ok(variants) = std::fs::read_dir(&version_path) {
122                        for variant in variants.filter_map(|e| e.ok()) {
123                            let variant_path = variant.path();
124                            if !variant_path.is_dir() {
125                                continue;
126                            }
127
128                            let variant_name = variant_path.file_name()
129                                .map(|n| n.to_string_lossy().to_string())
130                                .unwrap_or_default();
131
132                            let (size, file_count) = calculate_dir_size(&variant_path)?;
133                            if size == 0 {
134                                continue;
135                            }
136
137                            items.push(CleanableItem {
138                                name: format!("System Image: {} {}", version_name, variant_name),
139                                category: "Android".to_string(),
140                                subcategory: "SDK".to_string(),
141                                icon: "💿",
142                                path: variant_path,
143                                size,
144                                file_count: Some(file_count),
145                                last_modified: get_mtime(&variant.path()),
146                                description: "Android system image for emulator.",
147                                safe_to_delete: SafetyLevel::SafeWithCost,
148                                clean_command: Some("sdkmanager --uninstall".to_string()),
149                            });
150                        }
151                    }
152                }
153            }
154        }
155
156        Ok(items)
157    }
158
159    /// Detect Gradle caches
160    fn detect_gradle_caches(&self) -> Result<Vec<CleanableItem>> {
161        let gradle_paths = [
162            ("Gradle Caches", ".gradle/caches"),
163            ("Gradle Wrapper", ".gradle/wrapper"),
164            ("Gradle Daemon Logs", ".gradle/daemon"),
165            ("Gradle Native", ".gradle/native"),
166        ];
167
168        let mut items = Vec::new();
169
170        for (name, rel_path) in gradle_paths {
171            let path = self.home.join(rel_path);
172            if !path.exists() {
173                continue;
174            }
175
176            let (size, file_count) = calculate_dir_size(&path)?;
177            if size < 10_000_000 {
178                // Skip if less than 10MB
179                continue;
180            }
181
182            items.push(CleanableItem {
183                name: name.to_string(),
184                category: "Android".to_string(),
185                subcategory: "Gradle".to_string(),
186                icon: "🐘",
187                path,
188                size,
189                file_count: Some(file_count),
190                last_modified: None,
191                description: "Gradle build cache. Will be rebuilt on next build.",
192                safe_to_delete: SafetyLevel::SafeWithCost,
193                clean_command: None,
194            });
195        }
196
197        Ok(items)
198    }
199
200    /// Detect Android build caches
201    fn detect_android_caches(&self) -> Result<Vec<CleanableItem>> {
202        let cache_paths = [
203            ("Android Cache", ".android/cache"),
204            ("Android Build Cache", ".android/build-cache"),
205            ("ADB Keys", ".android/.adb_keys_backup"),
206        ];
207
208        let mut items = Vec::new();
209
210        for (name, rel_path) in cache_paths {
211            let path = self.home.join(rel_path);
212            if !path.exists() {
213                continue;
214            }
215
216            let (size, file_count) = if path.is_dir() {
217                calculate_dir_size(&path)?
218            } else {
219                let meta = std::fs::metadata(&path)?;
220                (meta.len(), 1)
221            };
222
223            if size == 0 {
224                continue;
225            }
226
227            items.push(CleanableItem {
228                name: name.to_string(),
229                category: "Android".to_string(),
230                subcategory: "Cache".to_string(),
231                icon: "🗂️",
232                path,
233                size,
234                file_count: Some(file_count),
235                last_modified: None,
236                description: "Android build cache files.",
237                safe_to_delete: SafetyLevel::Safe,
238                clean_command: None,
239            });
240        }
241
242        Ok(items)
243    }
244
245    /// Detect Android Studio caches
246    fn detect_studio_caches(&self) -> Result<Vec<CleanableItem>> {
247        let mut items = Vec::new();
248
249        // Android Studio versions (macOS)
250        #[cfg(target_os = "macos")]
251        {
252            let cache_base = self.home.join("Library/Caches");
253            if cache_base.exists() {
254                if let Ok(entries) = std::fs::read_dir(&cache_base) {
255                    for entry in entries.filter_map(|e| e.ok()) {
256                        let name = entry.file_name().to_string_lossy().to_string();
257                        if name.starts_with("Google.AndroidStudio") {
258                            let path = entry.path();
259                            let (size, file_count) = calculate_dir_size(&path)?;
260                            if size == 0 {
261                                continue;
262                            }
263
264                            items.push(CleanableItem {
265                                name: format!("Android Studio Cache: {}", name.replace("Google.", "")),
266                                category: "Android".to_string(),
267                                subcategory: "IDE Cache".to_string(),
268                                icon: "💻",
269                                path,
270                                size,
271                                file_count: Some(file_count),
272                                last_modified: get_mtime(&entry.path()),
273                                description: "Android Studio IDE cache.",
274                                safe_to_delete: SafetyLevel::Safe,
275                                clean_command: None,
276                            });
277                        }
278                    }
279                }
280            }
281
282            // Application Support
283            let support_base = self.home.join("Library/Application Support");
284            if support_base.exists() {
285                if let Ok(entries) = std::fs::read_dir(&support_base) {
286                    for entry in entries.filter_map(|e| e.ok()) {
287                        let name = entry.file_name().to_string_lossy().to_string();
288                        if name.starts_with("Google") && name.contains("AndroidStudio") {
289                            let path = entry.path();
290                            let (size, file_count) = calculate_dir_size(&path)?;
291                            if size < 100_000_000 {
292                                // Skip if less than 100MB
293                                continue;
294                            }
295
296                            items.push(CleanableItem {
297                                name: format!("Android Studio Data: {}", name.replace("Google/", "")),
298                                category: "Android".to_string(),
299                                subcategory: "IDE Data".to_string(),
300                                icon: "💻",
301                                path,
302                                size,
303                                file_count: Some(file_count),
304                                last_modified: get_mtime(&entry.path()),
305                                description: "Android Studio settings and plugins.",
306                                safe_to_delete: SafetyLevel::Caution,
307                                clean_command: None,
308                            });
309                        }
310                    }
311                }
312            }
313        }
314
315        // Linux locations
316        #[cfg(target_os = "linux")]
317        {
318            let config_base = self.home.join(".config");
319            if config_base.exists() {
320                if let Ok(entries) = std::fs::read_dir(&config_base) {
321                    for entry in entries.filter_map(|e| e.ok()) {
322                        let name = entry.file_name().to_string_lossy().to_string();
323                        if name.starts_with("Google") && name.contains("AndroidStudio") {
324                            let path = entry.path();
325                            let (size, file_count) = calculate_dir_size(&path)?;
326                            if size == 0 {
327                                continue;
328                            }
329
330                            items.push(CleanableItem {
331                                name: format!("Android Studio: {}", name),
332                                category: "Android".to_string(),
333                                subcategory: "IDE".to_string(),
334                                icon: "💻",
335                                path,
336                                size,
337                                file_count: Some(file_count),
338                                last_modified: get_mtime(&entry.path()),
339                                description: "Android Studio configuration.",
340                                safe_to_delete: SafetyLevel::Caution,
341                                clean_command: None,
342                            });
343                        }
344                    }
345                }
346            }
347        }
348
349        Ok(items)
350    }
351}
352
353impl Default for AndroidCleaner {
354    fn default() -> Self {
355        Self::new().expect("AndroidCleaner requires home directory")
356    }
357}
358
359#[cfg(test)]
360mod tests {
361    use super::*;
362
363    #[test]
364    fn test_android_cleaner_creation() {
365        let cleaner = AndroidCleaner::new();
366        assert!(cleaner.is_some());
367    }
368
369    #[test]
370    fn test_android_detection() {
371        if let Some(cleaner) = AndroidCleaner::new() {
372            let items = cleaner.detect().unwrap();
373            println!("Found {} Android items", items.len());
374            for item in &items {
375                println!("  {} {} ({} bytes)", item.icon, item.name, item.size);
376            }
377        }
378    }
379}