1use super::{calculate_dir_size, get_mtime, CleanableItem, SafetyLevel};
10use crate::error::Result;
11use std::path::PathBuf;
12
13pub struct AndroidCleaner {
15 home: PathBuf,
16}
17
18impl AndroidCleaner {
19 pub fn new() -> Option<Self> {
21 let home = dirs::home_dir()?;
22 Some(Self { home })
23 }
24
25 pub fn detect(&self) -> Result<Vec<CleanableItem>> {
27 let mut items = Vec::new();
28
29 items.extend(self.detect_avd()?);
31
32 items.extend(self.detect_system_images()?);
34
35 items.extend(self.detect_gradle_caches()?);
37
38 items.extend(self.detect_android_caches()?);
40
41 items.extend(self.detect_studio_caches()?);
43
44 Ok(items)
45 }
46
47 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 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 fn detect_system_images(&self) -> Result<Vec<CleanableItem>> {
94 let sdk_paths = [
96 self.home.join("Library/Android/sdk"), self.home.join("Android/Sdk"), self.home.join(".android/sdk"), ];
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 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 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 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 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 fn detect_studio_caches(&self) -> Result<Vec<CleanableItem>> {
247 let mut items = Vec::new();
248
249 #[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 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 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 #[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}