1use {
25 std::{
26 ffi::OsStr,
27 fmt,
28 io::Read,
29 process::{Child, Command, ExitStatus, Stdio},
30 thread,
31 time::Duration,
32 },
33 thiserror::Error,
34 tracing::trace,
35 wait_timeout::ChildExt,
36};
37
38#[derive(Debug, Error)]
42pub enum AdbError {
43 #[error("ADB not found: {0}")]
45 NotFound(String),
46
47 #[error("ADB command failed: {0}")]
49 CommandFailed(String),
50
51 #[error("ADB command timed out")]
53 Timeout,
54
55 #[error("ADB device not found: {0}")]
57 DeviceNotFound(String),
58}
59
60pub type AdbResult<T> = std::result::Result<T, AdbError>;
62
63#[derive(Debug, Clone, PartialEq, Eq)]
67pub enum DeviceState {
68 Connected,
70 Disconnected,
72 Unknown,
74}
75
76pub struct AdbShell;
84
85impl AdbShell {
86 pub fn verify_adb_available() -> AdbResult<()> {
91 Command::new("adb")
92 .arg("version")
93 .stdout(Stdio::null())
94 .stderr(Stdio::null())
95 .status()
96 .map_err(|e| AdbError::NotFound(e.to_string()))?
97 .success()
98 .then_some(())
99 .ok_or_else(|| AdbError::NotFound("adb returned non-zero exit status".to_string()))
100 }
101
102 pub fn get_device_serial() -> AdbResult<String> {
108 run_adb_command(["get-serialno"], None, |status, output, stderr| {
109 if !status.success() {
110 return Err(AdbError::DeviceNotFound(if stderr.is_empty() {
111 format!("adb exited with status: {}", status)
112 } else {
113 format!(
114 "adb exited with status: {}; stderr: {}",
115 status,
116 stderr.trim()
117 )
118 }));
119 }
120 let serial = output.trim().to_string();
123 if serial.is_empty() || serial == "unknown" {
124 return Err(AdbError::DeviceNotFound(
125 "no device connected (adb returned \"unknown\")".to_string(),
126 ));
127 }
128 Ok(serial)
129 })
130 }
131
132 pub fn get_device_state(serial: &str) -> AdbResult<DeviceState> {
134 check_serial(serial)?;
135 run_adb_command(
136 ["-s", serial, "get-state"],
137 None,
138 |status, output, _stderr| {
139 if !status.success() {
140 return Ok(DeviceState::Disconnected);
141 }
142 match output.trim() {
143 "device" => Ok(DeviceState::Connected),
144 _ => Ok(DeviceState::Unknown),
145 }
146 },
147 )
148 }
149
150 pub fn get_physical_screen_size(serial: &str) -> AdbResult<(u32, u32)> {
152 check_serial(serial)?;
153 run_adb_command(
154 ["-s", serial, "shell", "wm", "size"],
155 None,
156 |status, output, stderr| {
157 status
158 .success()
159 .then(|| {
160 output
161 .lines()
162 .find(|line| line.contains("Physical size:"))
163 .and_then(|line| line.split(':').nth(1))
164 .and_then(|size_part| {
165 let size_str = size_part.trim();
166 let mut parts = size_str.split('x');
167 let width = parts.next().and_then(|w| w.trim().parse::<u32>().ok());
168 let height =
169 parts.next().and_then(|h| h.trim().parse::<u32>().ok());
170 match (width, height) {
171 (Some(w), Some(h)) => Some((w, h)),
172 _ => None,
173 }
174 })
175 })
176 .flatten()
177 .ok_or_else(|| {
178 AdbError::CommandFailed(if stderr.is_empty() {
179 format!("Failed to parse screen size from output: {}", output.trim())
180 } else {
181 format!("Failed to parse screen size; stderr: {}", stderr.trim())
182 })
183 })
184 },
185 )
186 }
187
188 pub fn get_screen_orientation(serial: &str) -> AdbResult<u32> {
190 check_serial(serial)?;
191 run_adb_command(
192 ["-s", serial, "shell", "dumpsys", "window", "displays"],
193 Some(Duration::from_secs(3)),
194 |status, output, stderr| {
195 status
196 .success()
197 .then(|| {
198 output
199 .lines()
200 .find(|line| line.contains("mCurrentRotation"))
201 .and_then(|line| line.split('=').nth(1))
202 .and_then(|s| match s.trim() {
203 "ROTATION_0" | "0" => Some(0),
204 "ROTATION_90" | "1" => Some(1),
205 "ROTATION_180" | "2" => Some(2),
206 "ROTATION_270" | "3" => Some(3),
207 _ => None,
208 })
209 })
210 .flatten()
211 .ok_or_else(|| {
212 AdbError::CommandFailed(if stderr.is_empty() {
213 "Failed to parse screen orientation in dumpsys output".to_string()
214 } else {
215 format!(
216 "Failed to parse screen orientation; stderr: {}",
217 stderr.trim()
218 )
219 })
220 })
221 },
222 )
223 }
224
225 pub fn get_ime_state(serial: &str) -> AdbResult<bool> {
227 check_serial(serial)?;
228 run_adb_command(
229 ["-s", serial, "shell", "dumpsys", "window", "InputMethod"],
230 Some(Duration::from_secs(3)),
231 |status, output, stderr| {
232 if !status.success() {
233 return Err(AdbError::CommandFailed(if stderr.is_empty() {
234 format!(
235 "Failed to query IME state on [{}], adb exited with status: {}",
236 serial, status
237 )
238 } else {
239 format!(
240 "Failed to query IME state on [{}], adb exited with status: {}; \
241 stderr: {}",
242 serial,
243 status,
244 stderr.trim()
245 )
246 }));
247 }
248 Ok(output.lines().any(|line| line.contains("isVisible=true")))
249 },
250 )
251 }
252
253 pub fn get_android_version(serial: &str) -> AdbResult<u32> {
255 let output = Self::get_prop(serial, "ro.build.version.sdk")?;
256 output
257 .trim()
258 .parse()
259 .map_err(|e| AdbError::CommandFailed(format!("Failed to parse Android version: {}", e)))
260 }
261
262 pub fn get_platform(serial: &str) -> AdbResult<String> {
264 let platform = Self::get_prop(serial, "ro.board.platform")?;
265 if !platform.is_empty() && platform != "unknown" {
266 return Ok(platform);
267 }
268 Self::get_prop(serial, "ro.hardware")
269 }
270
271 pub fn get_prop(serial: &str, prop_name: &str) -> AdbResult<String> {
273 check_serial(serial)?;
274 run_adb_command(
275 ["-s", serial, "shell", "getprop", prop_name],
276 None,
277 |status, output, stderr| {
278 status
279 .success()
280 .then(|| output.trim().to_string())
281 .ok_or_else(|| {
282 AdbError::CommandFailed(if stderr.is_empty() {
283 format!(
284 "Failed to get property [{}], adb exited with status: {}",
285 prop_name, status
286 )
287 } else {
288 format!(
289 "Failed to get property [{}], adb exited with status: {}; stderr: {}",
290 prop_name, status, stderr.trim()
291 )
292 })
293 })
294 },
295 )
296 }
297
298 pub fn push_file(serial: &str, file: &str, path: &str) -> AdbResult<()> {
300 check_serial(serial)?;
301 let out = Command::new("adb")
302 .args(["-s", serial, "push", file, path])
303 .stdout(Stdio::null())
304 .stderr(Stdio::piped())
305 .output()
306 .map_err(|e| AdbError::NotFound(format!("Failed to spawn adb: {}", e)))?;
307 if !out.status.success() {
308 let stderr = String::from_utf8_lossy(&out.stderr);
309 return Err(AdbError::CommandFailed(if stderr.trim().is_empty() {
310 format!(
311 "Failed to push file [{}] to [{}], exit: {}",
312 file, path, out.status
313 )
314 } else {
315 format!(
316 "Failed to push file [{}] to [{}], exit: {}; stderr: {}",
317 file,
318 path,
319 out.status,
320 stderr.trim()
321 )
322 }));
323 }
324 Ok(())
325 }
326
327 pub fn execute_jar<I, S>(
329 serial: &str,
330 jar: &str,
331 running_dir: &str,
332 class_name: &str,
333 version: &str,
334 args: I,
335 ) -> AdbResult<Child>
336 where
337 I: IntoIterator<Item = S>,
338 S: AsRef<OsStr>,
339 {
340 check_serial(serial)?;
341 let child = Command::new("adb")
342 .args(["-s", serial])
343 .args([
344 "shell".to_string(),
345 format!("CLASSPATH={}", jar),
346 "app_process".to_string(),
347 running_dir.to_string(),
348 class_name.to_string(),
349 version.to_string(),
350 ])
351 .args(args)
352 .stdout(Stdio::null())
353 .stderr(Stdio::null())
354 .spawn()
355 .map_err(|e| {
356 AdbError::CommandFailed(format!(
357 "Failed to execute JAR [{}] on device [{}]: {}",
358 jar, serial, e
359 ))
360 })?;
361 Ok(child)
362 }
363
364 pub fn setup_reverse_tunnel(serial: &str, socket_name: &str, local_port: u16) -> AdbResult<()> {
366 check_serial(serial)?;
367 let status = Command::new("adb")
368 .args([
369 "-s",
370 serial,
371 "reverse",
372 &format!("localabstract:{}", socket_name),
373 &format!("tcp:{}", local_port),
374 ])
375 .status()
376 .map_err(|e| {
377 AdbError::CommandFailed(format!(
378 "Failed to setup reverse tunnel for [{}]: {}",
379 serial, e
380 ))
381 })?;
382 if !status.success() {
383 return Err(AdbError::CommandFailed(format!(
384 "Failed to setup reverse tunnel for [{}], exit: {}",
385 serial, status
386 )));
387 }
388 Ok(())
389 }
390
391 pub fn remove_reverse_tunnel(serial: &str, socket_name: &str) -> AdbResult<()> {
393 check_serial(serial)?;
394 let output = Command::new("adb")
395 .args([
396 "-s",
397 serial,
398 "reverse",
399 "--remove",
400 &format!("localabstract:{}", socket_name),
401 ])
402 .output();
403 match output {
404 Ok(out) if !out.status.success() => {
405 let stderr = String::from_utf8_lossy(&out.stderr);
406 if stderr.contains("not found") || stderr.contains("No such reverse") {
407 return Ok(());
408 }
409 Err(AdbError::CommandFailed(if stderr.is_empty() {
410 format!(
411 "Failed to remove reverse tunnel for [{}], exit: {}",
412 serial, out.status
413 )
414 } else {
415 format!(
416 "Failed to remove reverse tunnel for [{}], exit: {}, stderr: {}",
417 serial,
418 out.status,
419 stderr.trim()
420 )
421 }))
422 }
423 Ok(_) => Ok(()),
424 Err(e) => Err(AdbError::CommandFailed(format!(
425 "Failed to remove reverse tunnel for [{}]: {}",
426 serial, e
427 ))),
428 }
429 }
430}
431
432fn run_adb_command<I, S, F, R>(args: I, timeout: Option<Duration>, parse: F) -> AdbResult<R>
444where
445 I: IntoIterator<Item = S> + fmt::Debug,
446 S: AsRef<OsStr>,
447 F: FnOnce(ExitStatus, &str, &str) -> AdbResult<R>,
448{
449 trace!("Running adb command with args: {:?}", args);
450
451 if timeout.is_none() {
452 let out = Command::new("adb")
454 .args(args)
455 .output()
456 .map_err(|e| AdbError::NotFound(format!("Failed to spawn adb: {e}")))?;
457 let stdout = String::from_utf8_lossy(&out.stdout).into_owned();
458 let stderr = String::from_utf8_lossy(&out.stderr).into_owned();
459 return parse(out.status, &stdout, &stderr);
460 }
461
462 let mut child = Command::new("adb")
464 .args(args)
465 .stdout(Stdio::piped())
466 .stderr(Stdio::piped())
467 .spawn()
468 .map_err(|e| AdbError::NotFound(format!("Failed to spawn adb: {e}")))?;
469 let stdout_pipe = child.stdout.take();
470 let stderr_pipe = child.stderr.take();
471
472 let stdout_thread = thread::spawn(move || {
474 let mut buf = Vec::new();
475 if let Some(mut pipe) = stdout_pipe {
476 let _ = pipe.read_to_end(&mut buf);
477 }
478 buf
479 });
480 let stderr_thread = thread::spawn(move || {
482 let mut buf = Vec::new();
483 if let Some(mut pipe) = stderr_pipe {
484 let _ = pipe.read_to_end(&mut buf);
485 }
486 buf
487 });
488
489 let to = timeout.unwrap();
490 let status = match child
491 .wait_timeout(to)
492 .map_err(|e| AdbError::CommandFailed(format!("Failed to wait with timeout for adb: {e}")))?
493 {
494 Some(s) => s,
495 None => {
496 let _ = child.kill();
497 let _ = child.wait();
498 return Err(AdbError::Timeout);
499 }
500 };
501
502 let stdout_bytes = stdout_thread.join().unwrap_or_default();
503 let stderr_bytes = stderr_thread.join().unwrap_or_default();
504 let stdout = String::from_utf8_lossy(&stdout_bytes).into_owned();
505 let stderr = String::from_utf8_lossy(&stderr_bytes).into_owned();
506 parse(status, &stdout, &stderr)
507}
508
509fn check_serial(serial: &str) -> AdbResult<()> {
510 if serial.is_empty() {
511 return Err(AdbError::CommandFailed(
512 "Device serial cannot be empty".to_string(),
513 ));
514 }
515 Ok(())
516}