|
| 1 | +// ============================================================================== |
| 2 | +// AETHER 2.0 ADB Mobile Device Deployer & Wireless Hot-Reload Bridge |
| 3 | +// 1-Click APK Installation & Activity Launch via ADB with Zero External Dependencies |
| 4 | +// ============================================================================== |
| 5 | + |
| 6 | +use std::path::{Path, PathBuf}; |
| 7 | +use std::process::Command; |
| 8 | + |
| 9 | +#[derive(Debug, Clone)] |
| 10 | +pub struct AdbDevice { |
| 11 | + pub id: String, |
| 12 | + pub model: String, |
| 13 | + pub product: String, |
| 14 | + pub status: String, |
| 15 | +} |
| 16 | + |
| 17 | +#[derive(Debug, Clone)] |
| 18 | +pub struct DeployReport { |
| 19 | + pub device: AdbDevice, |
| 20 | + pub apk_path: PathBuf, |
| 21 | + pub installed: bool, |
| 22 | + pub launched: bool, |
| 23 | + pub reverse_port_forwarded: bool, |
| 24 | + pub message: String, |
| 25 | +} |
| 26 | + |
| 27 | +pub struct AdbDeployer; |
| 28 | + |
| 29 | +impl AdbDeployer { |
| 30 | + /// Detect all currently connected Android devices via ADB |
| 31 | + pub fn list_devices() -> Result<Vec<AdbDevice>, String> { |
| 32 | + let output = Command::new("adb") |
| 33 | + .arg("devices") |
| 34 | + .arg("-l") |
| 35 | + .output() |
| 36 | + .map_err(|e| format!("Failed to execute 'adb devices': {}. Please ensure Android SDK Platform-Tools (adb) is in your PATH.", e))?; |
| 37 | + |
| 38 | + if !output.status.success() { |
| 39 | + let stderr = String::from_utf8_lossy(&output.stderr); |
| 40 | + return Err(format!("ADB returned error: {}", stderr)); |
| 41 | + } |
| 42 | + |
| 43 | + let stdout = String::from_utf8_lossy(&output.stdout); |
| 44 | + let mut devices = Vec::new(); |
| 45 | + |
| 46 | + for line in stdout.lines() { |
| 47 | + let line = line.trim(); |
| 48 | + if line.is_empty() || line.starts_with("List of devices") || line.starts_with('*') { |
| 49 | + continue; |
| 50 | + } |
| 51 | + |
| 52 | + let parts: Vec<&str> = line.split_whitespace().collect(); |
| 53 | + if parts.len() >= 2 { |
| 54 | + let id = parts[0].to_string(); |
| 55 | + let status = parts[1].to_string(); |
| 56 | + |
| 57 | + let mut model = "Android Device".to_string(); |
| 58 | + let mut product = String::new(); |
| 59 | + |
| 60 | + for part in &parts[2..] { |
| 61 | + if let Some(m) = part.strip_prefix("model:") { |
| 62 | + model = m.replace('_', " "); |
| 63 | + } else if let Some(p) = part.strip_prefix("product:") { |
| 64 | + product = p.to_string(); |
| 65 | + } |
| 66 | + } |
| 67 | + |
| 68 | + devices.push(AdbDevice { |
| 69 | + id, |
| 70 | + model, |
| 71 | + product, |
| 72 | + status, |
| 73 | + }); |
| 74 | + } |
| 75 | + } |
| 76 | + |
| 77 | + Ok(devices) |
| 78 | + } |
| 79 | + |
| 80 | + /// Select an active device or automatically choose the default if only one device is connected |
| 81 | + pub fn select_device(preferred_id: Option<&str>) -> Result<AdbDevice, String> { |
| 82 | + let devices = Self::list_devices()?; |
| 83 | + |
| 84 | + if devices.is_empty() { |
| 85 | + return Err("No Android devices found. Please connect your phone via USB with USB Debugging enabled, or start an emulator.".to_string()); |
| 86 | + } |
| 87 | + |
| 88 | + if let Some(id) = preferred_id { |
| 89 | + devices |
| 90 | + .into_iter() |
| 91 | + .find(|d| d.id.eq_ignore_ascii_case(id) || d.model.to_lowercase().contains(&id.to_lowercase())) |
| 92 | + .ok_or_else(|| format!("Specified device '{}' not found in connected devices.", id)) |
| 93 | + } else if devices.len() == 1 { |
| 94 | + Ok(devices[0].clone()) |
| 95 | + } else { |
| 96 | + let list_str = devices |
| 97 | + .iter() |
| 98 | + .map(|d| format!(" • {} ({}) - {}", d.id, d.model, d.status)) |
| 99 | + .collect::<Vec<_>>() |
| 100 | + .join("\n"); |
| 101 | + Err(format!( |
| 102 | + "Multiple devices detected. Please specify using `--device <id>`:\n{}", |
| 103 | + list_str |
| 104 | + )) |
| 105 | + } |
| 106 | + } |
| 107 | + |
| 108 | + /// Deploy and install an APK onto the target Android device |
| 109 | + pub fn deploy( |
| 110 | + device_id: Option<&str>, |
| 111 | + apk_path: &Path, |
| 112 | + package_name: &str, |
| 113 | + activity: Option<&str>, |
| 114 | + launch: bool, |
| 115 | + setup_live_reload_port: Option<u16>, |
| 116 | + ) -> Result<DeployReport, String> { |
| 117 | + if !apk_path.exists() { |
| 118 | + return Err(format!("Target APK file '{}' does not exist.", apk_path.display())); |
| 119 | + } |
| 120 | + |
| 121 | + let device = Self::select_device(device_id)?; |
| 122 | + |
| 123 | + println!("================================================================================"); |
| 124 | + println!("📱 AETHER 1-CLICK MOBILE DEPLOYER"); |
| 125 | + println!("Target Device: {} ({}) [{}]", device.model, device.product, device.id); |
| 126 | + println!("Target APK: {} ({} KB)", apk_path.display(), apk_path.metadata().map(|m| m.len() / 1024).unwrap_or(0)); |
| 127 | + println!("================================================================================"); |
| 128 | + |
| 129 | + // 1. Setup reverse port forwarding for Live Hot-Reload if requested |
| 130 | + let mut reverse_ok = false; |
| 131 | + if let Some(port) = setup_live_reload_port { |
| 132 | + let port_str = format!("tcp:{}", port); |
| 133 | + print!(" [1/3] Reverse port-forwarding {} -> mobile localhost... ", port); |
| 134 | + let rev_res = Command::new("adb") |
| 135 | + .args(["-s", &device.id, "reverse", &port_str, &port_str]) |
| 136 | + .output(); |
| 137 | + if rev_res.map(|o| o.status.success()).unwrap_or(false) { |
| 138 | + println!("OK ✅"); |
| 139 | + reverse_ok = true; |
| 140 | + } else { |
| 141 | + println!("SKIPPED ⚠️"); |
| 142 | + } |
| 143 | + } |
| 144 | + |
| 145 | + // 2. Install APK with replace (-r) and downgrade permissions (-d) |
| 146 | + print!(" [2/3] Installing APK onto {}... ", device.model); |
| 147 | + let install_res = Command::new("adb") |
| 148 | + .args(["-s", &device.id, "install", "-r", "-d", &apk_path.to_string_lossy()]) |
| 149 | + .output() |
| 150 | + .map_err(|e| format!("Failed to invoke 'adb install': {}", e))?; |
| 151 | + |
| 152 | + let install_out = String::from_utf8_lossy(&install_res.stdout); |
| 153 | + let install_err = String::from_utf8_lossy(&install_res.stderr); |
| 154 | + |
| 155 | + if !install_res.status.success() || (!install_out.contains("Success") && !install_err.contains("Success")) { |
| 156 | + println!("FAILED ❌"); |
| 157 | + return Err(format!("ADB installation failed:\n{}{}", install_out, install_err)); |
| 158 | + } |
| 159 | + println!("SUCCESS ✅"); |
| 160 | + |
| 161 | + // 3. Launch App Activity |
| 162 | + let mut launched = false; |
| 163 | + if launch { |
| 164 | + let target_activity = activity.unwrap_or(".MainActivity"); |
| 165 | + let component = format!("{}/{}", package_name, target_activity); |
| 166 | + print!(" [3/3] Launching {}... ", component); |
| 167 | + let launch_res = Command::new("adb") |
| 168 | + .args(["-s", &device.id, "shell", "am", "start", "-n", &component]) |
| 169 | + .output(); |
| 170 | + |
| 171 | + if launch_res.map(|o| o.status.success()).unwrap_or(false) { |
| 172 | + println!("LAUNCHED 🚀"); |
| 173 | + launched = true; |
| 174 | + } else { |
| 175 | + println!("FAILED ⚠️"); |
| 176 | + } |
| 177 | + } |
| 178 | + |
| 179 | + println!("================================================================================"); |
| 180 | + println!("✨ App is now LIVE on your {}!", device.model); |
| 181 | + println!("================================================================================\n"); |
| 182 | + |
| 183 | + Ok(DeployReport { |
| 184 | + device, |
| 185 | + apk_path: apk_path.to_path_buf(), |
| 186 | + installed: true, |
| 187 | + launched, |
| 188 | + reverse_port_forwarded: reverse_ok, |
| 189 | + message: "Mobile deployment completed successfully.".into(), |
| 190 | + }) |
| 191 | + } |
| 192 | +} |
0 commit comments