1use std::path::Path;
3
4use regex::Regex;
5use tokio::io::{AsyncReadExt, AsyncWriteExt};
6use tracing::{debug, trace, warn};
7
8use crate::connection::AdbConnection;
9use crate::error::{AdbError, Result};
10use crate::models::{
11 AppDetail, CurrentApp, DeviceState, FileStat, ForwardEntry, RebootMode, ReverseEntry,
12 ScreenSize, ShellOutput, SyncDirEntry,
13};
14
15#[derive(Debug, Clone)]
21pub struct AdbDevice {
22 host: String,
23 port: u16,
24 pub serial: String,
25}
26
27impl AdbDevice {
28 pub fn new(serial: impl Into<String>, host: impl Into<String>, port: u16) -> Self {
30 Self {
31 serial: serial.into(),
32 host: host.into(),
33 port,
34 }
35 }
36
37 pub fn with_serial(serial: impl Into<String>) -> Self {
39 Self::new(serial, "127.0.0.1", 5037)
40 }
41
42 async fn connect_server(&self) -> Result<AdbConnection> {
48 AdbConnection::connect(&self.host, self.port).await
49 }
50
51 async fn connect_transport(&self) -> Result<AdbConnection> {
53 let mut conn = self.connect_server().await?;
54 conn.send_and_okay(&format!("host:transport:{}", self.serial))
55 .await?;
56 Ok(conn)
57 }
58
59 async fn connect_sync(&self) -> Result<AdbConnection> {
61 let mut conn = self.connect_transport().await?;
62 conn.send_and_okay("sync:").await?;
63 Ok(conn)
64 }
65
66 pub async fn get_state(&self) -> Result<DeviceState> {
72 let mut conn = self.connect_server().await?;
73 conn.send_command(&format!("host-serial:{}:get-state", self.serial))
74 .await?;
75 conn.read_status().await?;
76 let state_str = conn.read_length_prefixed_string().await?;
77 Ok(DeviceState::from(state_str.as_str()))
78 }
79
80 pub async fn get_serialno(&self) -> Result<String> {
82 let mut conn = self.connect_server().await?;
83 conn.send_and_okay(&format!("host-serial:{}:get-serialno", self.serial))
84 .await?;
85 let serial = conn.read_length_prefixed_string().await?;
86 Ok(serial.trim().to_string())
87 }
88
89 pub async fn get_features(&self) -> Result<Vec<String>> {
91 let mut conn = self.connect_server().await?;
92 conn.send_and_okay(&format!("host-serial:{}:features", self.serial))
93 .await?;
94 let features_str = conn.read_length_prefixed_string().await?;
95 Ok(features_str
96 .split(',')
97 .map(|s| s.trim().to_string())
98 .filter(|s| !s.is_empty())
99 .collect())
100 }
101
102 pub async fn shell(&self, cmd: &str) -> Result<String> {
108 debug!("adb shell: {cmd}");
109 let mut conn = self.connect_transport().await?;
110 conn.send_and_okay(&format!("shell:{cmd}")).await?;
111 let output = conn.read_until_close_string().await?;
112 trace!(
113 "shell output ({} bytes): {}",
114 output.len(),
115 &output[..output.len().min(200)]
116 );
117 Ok(output)
118 }
119
120 pub async fn shell_bytes(&self, cmd: &str) -> Result<Vec<u8>> {
122 debug!("adb shell (bytes): {cmd}");
123 let mut conn = self.connect_transport().await?;
124 conn.send_and_okay(&format!("shell:{cmd}")).await?;
125 conn.read_until_close_bytes().await
126 }
127
128 pub async fn shell2(&self, cmd: &str) -> Result<ShellOutput> {
134 let sentinel = "DROIDRUN_EXIT:";
135 let full_cmd = format!("({cmd}); echo {sentinel}$?");
136 let raw = self.shell(&full_cmd).await?;
137
138 let (stdout, exit_code) = if let Some(pos) = raw.rfind(sentinel) {
139 let code_str = raw[pos + sentinel.len()..].trim();
140 let code = code_str.parse::<i32>().unwrap_or(-1);
141 let stdout = raw[..pos].to_string();
142 (stdout, code)
143 } else {
144 (raw, -1)
145 };
146
147 Ok(ShellOutput { stdout, exit_code })
148 }
149
150 pub async fn getprop(&self, name: &str) -> Result<String> {
156 let output = self.shell(&format!("getprop {name}")).await?;
157 Ok(output.trim().to_string())
158 }
159
160 pub async fn prop_model(&self) -> Result<String> {
162 self.getprop("ro.product.model").await
163 }
164
165 pub async fn prop_name(&self) -> Result<String> {
167 self.getprop("ro.product.name").await
168 }
169
170 pub async fn prop_device(&self) -> Result<String> {
172 self.getprop("ro.product.device").await
173 }
174
175 pub async fn tap(&self, x: i32, y: i32) -> Result<()> {
181 self.shell(&format!("input tap {x} {y}")).await?;
182 Ok(())
183 }
184
185 pub async fn swipe(
187 &self,
188 x1: i32,
189 y1: i32,
190 x2: i32,
191 y2: i32,
192 duration_ms: u32,
193 ) -> Result<()> {
194 self.shell(&format!("input swipe {x1} {y1} {x2} {y2} {duration_ms}"))
195 .await?;
196 Ok(())
197 }
198
199 pub async fn keyevent(&self, keycode: i32) -> Result<()> {
201 self.shell(&format!("input keyevent {keycode}")).await?;
202 Ok(())
203 }
204
205 pub async fn drag(
207 &self,
208 sx: i32,
209 sy: i32,
210 ex: i32,
211 ey: i32,
212 duration_ms: u32,
213 ) -> Result<()> {
214 self.shell(&format!("input draganddrop {sx} {sy} {ex} {ey} {duration_ms}"))
215 .await?;
216 Ok(())
217 }
218
219 pub async fn input_text(&self, text: &str) -> Result<()> {
224 let escaped = text
225 .replace('\\', "\\\\")
226 .replace('"', "\\\"")
227 .replace(' ', "%s")
228 .replace('&', "\\&")
229 .replace('<', "\\<")
230 .replace('>', "\\>")
231 .replace('\'', "\\'");
232 self.shell(&format!("input text \"{escaped}\"")).await?;
233 Ok(())
234 }
235
236 pub async fn screencap(&self) -> Result<Vec<u8>> {
242 debug!("taking screenshot via screencap");
243 let data = self.shell_bytes("screencap -p").await?;
244 if data.is_empty() {
245 return Err(AdbError::ShellError(
246 "screencap returned empty data".into(),
247 ));
248 }
249 Ok(data)
250 }
251
252 pub async fn app_start(&self, package: &str, activity: Option<&str>) -> Result<String> {
258 let activity = match activity {
259 Some(a) => a.to_string(),
260 None => {
261 let output = self
262 .shell(&format!("cmd package resolve-activity --brief {package}"))
263 .await?;
264 let lines: Vec<&str> = output.lines().collect();
265 if lines.len() < 2 {
266 return Err(AdbError::ShellError(format!(
267 "cannot resolve main activity for {package}"
268 )));
269 }
270 let full = lines[1].trim();
271 match full.split_once('/') {
272 Some((_, act)) => act.to_string(),
273 None => full.to_string(),
274 }
275 }
276 };
277
278 debug!("starting {package}/{activity}");
279 let result = self
280 .shell(&format!("am start -n {package}/{activity}"))
281 .await?;
282 Ok(result)
283 }
284
285 pub async fn app_stop(&self, package: &str) -> Result<()> {
287 debug!("stopping {package}");
288 self.shell(&format!("am force-stop {package}")).await?;
289 Ok(())
290 }
291
292 pub async fn app_clear(&self, package: &str) -> Result<String> {
294 debug!("clearing {package}");
295 let output = self.shell(&format!("pm clear {package}")).await?;
296 Ok(output.trim().to_string())
297 }
298
299 pub async fn app_current(&self) -> Result<CurrentApp> {
301 let output = self
302 .shell("dumpsys activity activities | grep -E 'mResumedActivity|mCurrentFocus'")
303 .await?;
304
305 let re = Regex::new(r"(\S+)/(\S+)\s").unwrap();
307 for line in output.lines() {
308 if let Some(caps) = re.captures(line) {
309 let full = caps.get(1).unwrap().as_str();
310 let activity = caps.get(2).unwrap().as_str();
311 let activity = activity.trim_end_matches('}').trim_end();
313 return Ok(CurrentApp {
314 package: full.to_string(),
315 activity: activity.to_string(),
316 });
317 }
318 }
319
320 let re2 = Regex::new(r"([a-zA-Z0-9_.]+)/([a-zA-Z0-9_.]+)").unwrap();
322 for line in output.lines() {
323 if line.contains("mResumedActivity") || line.contains("mCurrentFocus") {
324 if let Some(caps) = re2.captures(line) {
325 return Ok(CurrentApp {
326 package: caps.get(1).unwrap().as_str().to_string(),
327 activity: caps.get(2).unwrap().as_str().to_string(),
328 });
329 }
330 }
331 }
332
333 Err(AdbError::Parse(
334 "cannot determine current foreground app".into(),
335 ))
336 }
337
338 pub async fn app_info(&self, package: &str) -> Result<AppDetail> {
340 let output = self
341 .shell(&format!("dumpsys package {package}"))
342 .await?;
343
344 let mut detail = AppDetail {
345 package: package.to_string(),
346 version_name: None,
347 version_code: None,
348 install_path: None,
349 first_install_time: None,
350 last_update_time: None,
351 };
352
353 for line in output.lines() {
354 let trimmed = line.trim();
355 if trimmed.starts_with("versionName=") {
356 detail.version_name = Some(trimmed.trim_start_matches("versionName=").to_string());
357 } else if trimmed.starts_with("versionCode=") {
358 let val = trimmed
360 .trim_start_matches("versionCode=")
361 .split_whitespace()
362 .next()
363 .unwrap_or("0");
364 detail.version_code = val.parse().ok();
365 } else if trimmed.starts_with("codePath=") {
366 detail.install_path = Some(trimmed.trim_start_matches("codePath=").to_string());
367 } else if trimmed.starts_with("firstInstallTime=") {
368 detail.first_install_time =
369 Some(trimmed.trim_start_matches("firstInstallTime=").to_string());
370 } else if trimmed.starts_with("lastUpdateTime=") {
371 detail.last_update_time =
372 Some(trimmed.trim_start_matches("lastUpdateTime=").to_string());
373 }
374 }
375
376 Ok(detail)
377 }
378
379 pub async fn install(&self, apk_path: &Path, flags: &[&str]) -> Result<String> {
383 if !apk_path.exists() {
384 return Err(AdbError::InstallFailed(format!(
385 "APK not found: {}",
386 apk_path.display()
387 )));
388 }
389
390 let remote_path = "/data/local/tmp/_droidrun_install.apk";
391
392 self.push_file(apk_path, remote_path).await?;
393
394 let flag_str = if flags.is_empty() {
395 String::new()
396 } else {
397 format!(" {}", flags.join(" "))
398 };
399 let result = self
400 .shell(&format!("pm install{flag_str} {remote_path}"))
401 .await?;
402
403 let _ = self.shell(&format!("rm -f {remote_path}")).await;
405
406 if result.contains("Success") {
407 debug!("install succeeded");
408 Ok(result.trim().to_string())
409 } else {
410 Err(AdbError::InstallFailed(result.trim().to_string()))
411 }
412 }
413
414 pub async fn uninstall(&self, package: &str) -> Result<String> {
416 debug!("uninstalling {package}");
417 let output = self.shell(&format!("pm uninstall {package}")).await?;
418 if output.contains("Success") {
419 Ok(output.trim().to_string())
420 } else {
421 Err(AdbError::UninstallFailed(output.trim().to_string()))
422 }
423 }
424
425 pub async fn list_packages(&self, flags: &[&str]) -> Result<Vec<String>> {
427 let flag_str = if flags.is_empty() {
428 String::new()
429 } else {
430 format!(" {}", flags.join(" "))
431 };
432 let output = self
433 .shell(&format!("pm list packages{flag_str}"))
434 .await?;
435 Ok(output
436 .lines()
437 .filter_map(|l| l.strip_prefix("package:"))
438 .map(|s| s.trim().to_string())
439 .collect())
440 }
441
442 pub async fn forward(&self, local_port: u16, remote_port: u16) -> Result<u16> {
450 let mut conn = self.connect_server().await?;
451
452 if local_port == 0 {
453 let cmd = format!(
454 "host-serial:{}:forward:tcp:0;tcp:{}",
455 self.serial, remote_port
456 );
457 conn.send_and_okay(&cmd).await?;
458 conn.read_status().await?;
460 let port_str = conn.read_length_prefixed_string().await?;
461 let port = port_str
462 .trim()
463 .parse::<u16>()
464 .map_err(|_| AdbError::Parse(format!("cannot parse port: {port_str}")))?;
465 debug!("forwarded tcp:{port} -> tcp:{remote_port}");
466 Ok(port)
467 } else {
468 let cmd = format!(
469 "host-serial:{}:forward:tcp:{};tcp:{}",
470 self.serial, local_port, remote_port
471 );
472 conn.send_and_okay(&cmd).await?;
473 debug!("forwarded tcp:{local_port} -> tcp:{remote_port}");
474 Ok(local_port)
475 }
476 }
477
478 pub async fn forward_list(&self) -> Result<Vec<ForwardEntry>> {
480 let mut conn = self.connect_server().await?;
481 conn.send_and_okay("host:list-forward").await?;
482 let data = conn.read_length_prefixed_string().await?;
483
484 let entries: Vec<ForwardEntry> = data
485 .lines()
486 .filter(|l| !l.is_empty())
487 .filter_map(|line| {
488 let parts: Vec<&str> = line.split_whitespace().collect();
489 if parts.len() >= 3 {
490 Some(ForwardEntry {
491 serial: parts[0].to_string(),
492 local: parts[1].to_string(),
493 remote: parts[2].to_string(),
494 })
495 } else {
496 warn!("cannot parse forward entry: {line}");
497 None
498 }
499 })
500 .filter(|e| e.serial == self.serial)
501 .collect();
502
503 Ok(entries)
504 }
505
506 pub async fn forward_remove(&self, local_port: u16) -> Result<()> {
508 let mut conn = self.connect_server().await?;
509 let cmd = format!(
510 "host-serial:{}:killforward:tcp:{}",
511 self.serial, local_port
512 );
513 conn.send_and_okay(&cmd).await?;
514 Ok(())
515 }
516
517 pub async fn forward_remove_all(&self) -> Result<()> {
519 let mut conn = self.connect_server().await?;
520 let cmd = format!("host-serial:{}:killforward-all", self.serial);
521 conn.send_and_okay(&cmd).await?;
522 Ok(())
523 }
524
525 pub async fn reverse(&self, remote_port: u16, local_port: u16) -> Result<()> {
531 let mut conn = self.connect_transport().await?;
532 conn.send_and_okay(&format!(
533 "reverse:forward:tcp:{remote_port};tcp:{local_port}"
534 ))
535 .await?;
536 debug!("reverse: device tcp:{remote_port} -> host tcp:{local_port}");
537 Ok(())
538 }
539
540 pub async fn reverse_list(&self) -> Result<Vec<ReverseEntry>> {
542 let mut conn = self.connect_transport().await?;
543 conn.send_and_okay("reverse:list-forward").await?;
544 let data = conn.read_length_prefixed_string().await?;
545
546 let entries: Vec<ReverseEntry> = data
547 .lines()
548 .filter(|l| !l.is_empty())
549 .filter_map(|line| {
550 let parts: Vec<&str> = line.split_whitespace().collect();
551 if parts.len() >= 3 {
553 Some(ReverseEntry {
554 remote: parts[1].to_string(),
555 local: parts[2].to_string(),
556 })
557 } else if parts.len() == 2 {
558 Some(ReverseEntry {
559 remote: parts[0].to_string(),
560 local: parts[1].to_string(),
561 })
562 } else {
563 warn!("cannot parse reverse entry: {line}");
564 None
565 }
566 })
567 .collect();
568
569 Ok(entries)
570 }
571
572 pub async fn reverse_remove(&self, remote_port: u16) -> Result<()> {
574 let mut conn = self.connect_transport().await?;
575 conn.send_and_okay(&format!("reverse:killforward:tcp:{remote_port}"))
576 .await?;
577 Ok(())
578 }
579
580 pub async fn reverse_remove_all(&self) -> Result<()> {
582 let mut conn = self.connect_transport().await?;
583 conn.send_and_okay("reverse:killforward-all").await?;
584 Ok(())
585 }
586
587 pub async fn root(&self) -> Result<String> {
593 let mut conn = self.connect_transport().await?;
594 conn.send_and_okay("root:").await?;
595 let response = conn.read_until_close_string().await?;
596 debug!("root: {}", response.trim());
597 Ok(response.trim().to_string())
598 }
599
600 pub async fn tcpip(&self, port: u16) -> Result<String> {
602 let mut conn = self.connect_transport().await?;
603 conn.send_and_okay(&format!("tcpip:{port}")).await?;
604 let response = conn.read_until_close_string().await?;
605 debug!("tcpip: {}", response.trim());
606 Ok(response.trim().to_string())
607 }
608
609 pub async fn reboot(&self, mode: RebootMode) -> Result<()> {
611 let mut conn = self.connect_transport().await?;
612 let cmd = match mode {
613 RebootMode::Normal => "reboot:".to_string(),
614 other => format!("reboot:{}", other.as_str()),
615 };
616 conn.send_and_okay(&cmd).await?;
617 debug!("reboot: {:?}", mode);
618 Ok(())
619 }
620
621 pub async fn push(&self, local_path: &Path, remote_path: &str) -> Result<()> {
627 self.push_file(local_path, remote_path).await
628 }
629
630 async fn push_file(&self, local: &Path, remote: &str) -> Result<()> {
632 debug!("pushing {} -> {remote}", local.display());
633
634 let data = tokio::fs::read(local).await?;
635 let size = data.len();
636
637 let mut conn = self.connect_sync().await?;
638 let stream = conn.stream_mut();
639
640 let path_with_mode = format!("{remote},33188"); let path_bytes = path_with_mode.as_bytes();
643 stream.write_all(b"SEND").await?;
644 stream
645 .write_all(&(path_bytes.len() as u32).to_le_bytes())
646 .await?;
647 stream.write_all(path_bytes).await?;
648
649 let chunk_size = 64 * 1024;
651 for chunk in data.chunks(chunk_size) {
652 stream.write_all(b"DATA").await?;
653 stream
654 .write_all(&(chunk.len() as u32).to_le_bytes())
655 .await?;
656 stream.write_all(chunk).await?;
657 }
658
659 let mtime = std::time::SystemTime::now()
661 .duration_since(std::time::UNIX_EPOCH)
662 .unwrap_or_default()
663 .as_secs() as u32;
664 stream.write_all(b"DONE").await?;
665 stream.write_all(&mtime.to_le_bytes()).await?;
666
667 let mut status = [0u8; 4];
669 stream.read_exact(&mut status).await?;
670 match &status {
671 b"OKAY" => {
672 debug!("pushed {size} bytes to {remote}");
673 stream.write_all(b"QUIT").await?;
674 stream.write_all(&0u32.to_le_bytes()).await?;
675 Ok(())
676 }
677 b"FAIL" => {
678 let mut len_buf = [0u8; 4];
679 stream.read_exact(&mut len_buf).await?;
680 let len = u32::from_le_bytes(len_buf) as usize;
681 let mut msg_buf = vec![0u8; len];
682 stream.read_exact(&mut msg_buf).await?;
683 let msg = String::from_utf8_lossy(&msg_buf);
684 Err(AdbError::ServerFailed(format!("push failed: {msg}")))
685 }
686 _ => Err(AdbError::Protocol(format!(
687 "unexpected sync response: {:?}",
688 status
689 ))),
690 }
691 }
692
693 pub async fn push_bytes(&self, data: &[u8], remote_path: &str) -> Result<()> {
695 debug!("pushing {} bytes -> {remote_path}", data.len());
696
697 let mut conn = self.connect_sync().await?;
698 let stream = conn.stream_mut();
699
700 let path_with_mode = format!("{remote_path},33188");
701 let path_bytes = path_with_mode.as_bytes();
702 stream.write_all(b"SEND").await?;
703 stream
704 .write_all(&(path_bytes.len() as u32).to_le_bytes())
705 .await?;
706 stream.write_all(path_bytes).await?;
707
708 let chunk_size = 64 * 1024;
709 for chunk in data.chunks(chunk_size) {
710 stream.write_all(b"DATA").await?;
711 stream
712 .write_all(&(chunk.len() as u32).to_le_bytes())
713 .await?;
714 stream.write_all(chunk).await?;
715 }
716
717 let mtime = std::time::SystemTime::now()
718 .duration_since(std::time::UNIX_EPOCH)
719 .unwrap_or_default()
720 .as_secs() as u32;
721 stream.write_all(b"DONE").await?;
722 stream.write_all(&mtime.to_le_bytes()).await?;
723
724 let mut status = [0u8; 4];
725 stream.read_exact(&mut status).await?;
726 match &status {
727 b"OKAY" => {
728 stream.write_all(b"QUIT").await?;
729 stream.write_all(&0u32.to_le_bytes()).await?;
730 Ok(())
731 }
732 b"FAIL" => {
733 let mut len_buf = [0u8; 4];
734 stream.read_exact(&mut len_buf).await?;
735 let len = u32::from_le_bytes(len_buf) as usize;
736 let mut msg_buf = vec![0u8; len];
737 stream.read_exact(&mut msg_buf).await?;
738 Err(AdbError::SyncError(
739 String::from_utf8_lossy(&msg_buf).to_string(),
740 ))
741 }
742 _ => Err(AdbError::Protocol(format!(
743 "unexpected sync response: {:?}",
744 status
745 ))),
746 }
747 }
748
749 pub async fn pull_bytes(&self, remote_path: &str) -> Result<Vec<u8>> {
751 debug!("pulling {remote_path}");
752
753 let mut conn = self.connect_sync().await?;
754 let stream = conn.stream_mut();
755
756 let path_bytes = remote_path.as_bytes();
757 stream.write_all(b"RECV").await?;
758 stream
759 .write_all(&(path_bytes.len() as u32).to_le_bytes())
760 .await?;
761 stream.write_all(path_bytes).await?;
762
763 let mut data = Vec::new();
764 loop {
765 let mut id = [0u8; 4];
766 stream.read_exact(&mut id).await?;
767
768 match &id {
769 b"DATA" => {
770 let mut len_buf = [0u8; 4];
771 stream.read_exact(&mut len_buf).await?;
772 let chunk_len = u32::from_le_bytes(len_buf) as usize;
773 if chunk_len > 0 {
774 let mut chunk = vec![0u8; chunk_len];
775 stream.read_exact(&mut chunk).await?;
776 data.extend_from_slice(&chunk);
777 }
778 }
779 b"DONE" => {
780 let mut _trailing = [0u8; 4];
781 stream.read_exact(&mut _trailing).await?;
782 break;
783 }
784 b"FAIL" => {
785 let mut len_buf = [0u8; 4];
786 stream.read_exact(&mut len_buf).await?;
787 let msg_len = u32::from_le_bytes(len_buf) as usize;
788 let mut msg_buf = vec![0u8; msg_len];
789 stream.read_exact(&mut msg_buf).await?;
790 return Err(AdbError::SyncError(
791 String::from_utf8_lossy(&msg_buf).to_string(),
792 ));
793 }
794 _ => {
795 return Err(AdbError::SyncError(format!(
796 "unexpected sync response: {:?}",
797 String::from_utf8_lossy(&id)
798 )));
799 }
800 }
801 }
802
803 stream.write_all(b"QUIT").await?;
805 stream.write_all(&0u32.to_le_bytes()).await?;
806
807 debug!("pulled {} bytes from {remote_path}", data.len());
808 Ok(data)
809 }
810
811 pub async fn pull(&self, remote_path: &str, local_path: &Path) -> Result<()> {
813 let data = self.pull_bytes(remote_path).await?;
814 tokio::fs::write(local_path, &data).await?;
815 debug!(
816 "saved {} bytes to {}",
817 data.len(),
818 local_path.display()
819 );
820 Ok(())
821 }
822
823 pub async fn stat(&self, path: &str) -> Result<FileStat> {
825 let mut conn = self.connect_sync().await?;
826 let stream = conn.stream_mut();
827
828 let path_bytes = path.as_bytes();
829 stream.write_all(b"STAT").await?;
830 stream
831 .write_all(&(path_bytes.len() as u32).to_le_bytes())
832 .await?;
833 stream.write_all(path_bytes).await?;
834
835 let mut header = [0u8; 4];
836 stream.read_exact(&mut header).await?;
837 if &header != b"STAT" {
838 return Err(AdbError::SyncError(format!(
839 "expected STAT, got {:?}",
840 String::from_utf8_lossy(&header)
841 )));
842 }
843
844 let mut buf = [0u8; 12];
845 stream.read_exact(&mut buf).await?;
846 let mode = u32::from_le_bytes(buf[0..4].try_into().unwrap());
847 let size = u32::from_le_bytes(buf[4..8].try_into().unwrap());
848 let mtime = u32::from_le_bytes(buf[8..12].try_into().unwrap());
849
850 stream.write_all(b"QUIT").await?;
852 stream.write_all(&0u32.to_le_bytes()).await?;
853
854 Ok(FileStat { mode, size, mtime })
855 }
856
857 pub async fn list_dir(&self, path: &str) -> Result<Vec<SyncDirEntry>> {
859 let mut conn = self.connect_sync().await?;
860 let stream = conn.stream_mut();
861
862 let path_bytes = path.as_bytes();
863 stream.write_all(b"LIST").await?;
864 stream
865 .write_all(&(path_bytes.len() as u32).to_le_bytes())
866 .await?;
867 stream.write_all(path_bytes).await?;
868
869 let mut entries = Vec::new();
870 loop {
871 let mut id = [0u8; 4];
872 stream.read_exact(&mut id).await?;
873
874 if &id == b"DONE" {
875 let mut _zero = [0u8; 4];
876 stream.read_exact(&mut _zero).await?;
877 break;
878 }
879
880 if &id != b"DENT" {
881 return Err(AdbError::SyncError(format!(
882 "expected DENT/DONE, got {:?}",
883 String::from_utf8_lossy(&id)
884 )));
885 }
886
887 let mut meta = [0u8; 16];
889 stream.read_exact(&mut meta).await?;
890 let mode = u32::from_le_bytes(meta[0..4].try_into().unwrap());
891 let size = u32::from_le_bytes(meta[4..8].try_into().unwrap());
892 let mtime = u32::from_le_bytes(meta[8..12].try_into().unwrap());
893 let namelen = u32::from_le_bytes(meta[12..16].try_into().unwrap()) as usize;
894
895 let mut name_buf = vec![0u8; namelen];
896 stream.read_exact(&mut name_buf).await?;
897 let name = String::from_utf8_lossy(&name_buf).to_string();
898
899 if name != "." && name != ".." {
900 entries.push(SyncDirEntry {
901 name,
902 mode,
903 size,
904 mtime,
905 });
906 }
907 }
908
909 stream.write_all(b"QUIT").await?;
911 stream.write_all(&0u32.to_le_bytes()).await?;
912
913 debug!("listed {} entries in {path}", entries.len());
914 Ok(entries)
915 }
916
917 pub async fn exists(&self, path: &str) -> Result<bool> {
923 let output = self
924 .shell(&format!("[ -e '{path}' ] && echo 1 || echo 0"))
925 .await?;
926 Ok(output.trim() == "1")
927 }
928
929 pub async fn remove(&self, path: &str) -> Result<()> {
931 self.shell(&format!("rm -f '{path}'")).await?;
932 Ok(())
933 }
934
935 pub async fn rmtree(&self, path: &str) -> Result<()> {
937 self.shell(&format!("rm -rf '{path}'")).await?;
938 Ok(())
939 }
940
941 pub async fn window_size(&self) -> Result<ScreenSize> {
947 let output = self.shell("wm size").await?;
948 let re = Regex::new(r"(\d+)x(\d+)").unwrap();
949 if let Some(caps) = re.captures(&output) {
950 let width = caps[1]
951 .parse()
952 .map_err(|_| AdbError::Parse("width".into()))?;
953 let height = caps[2]
954 .parse()
955 .map_err(|_| AdbError::Parse("height".into()))?;
956 Ok(ScreenSize { width, height })
957 } else {
958 Err(AdbError::Parse(format!(
959 "cannot parse wm size output: {output}"
960 )))
961 }
962 }
963
964 pub async fn rotation(&self) -> Result<u8> {
966 let output = self
967 .shell("dumpsys input | grep SurfaceOrientation")
968 .await?;
969 if let Some(digit) = output.chars().rev().find(|c| c.is_ascii_digit()) {
970 Ok(digit.to_digit(10).unwrap_or(0) as u8)
971 } else {
972 Ok(0)
973 }
974 }
975
976 pub async fn is_screen_on(&self) -> Result<bool> {
978 let output = self
979 .shell("dumpsys power | grep mWakefulness")
980 .await?;
981 Ok(output.contains("Awake"))
982 }
983
984 pub async fn switch_screen(&self, on: bool) -> Result<()> {
986 let currently_on = self.is_screen_on().await?;
987 if currently_on != on {
988 self.keyevent(26).await?; }
990 Ok(())
991 }
992
993 pub async fn wlan_ip(&self) -> Result<String> {
999 let output = self
1000 .shell("ip addr show wlan0 | grep 'inet '")
1001 .await?;
1002 let re = Regex::new(r"inet (\d+\.\d+\.\d+\.\d+)").unwrap();
1003 if let Some(caps) = re.captures(&output) {
1004 Ok(caps[1].to_string())
1005 } else {
1006 Err(AdbError::ShellError("no wlan0 IP found".into()))
1007 }
1008 }
1009
1010 pub async fn get_date(&self) -> Result<String> {
1016 let result = self.shell("date").await?;
1017 Ok(result.trim().to_string())
1018 }
1019
1020 pub async fn logcat(
1028 &self,
1029 filter: Option<&str>,
1030 ) -> Result<tokio::sync::mpsc::Receiver<String>> {
1031 let cmd = match filter {
1032 Some(f) => format!("logcat {f}"),
1033 None => "logcat".to_string(),
1034 };
1035
1036 let mut conn = self.connect_transport().await?;
1037 conn.send_and_okay(&format!("shell:{cmd}")).await?;
1038
1039 let (tx, rx) = tokio::sync::mpsc::channel(256);
1040 let stream = conn.into_stream();
1041
1042 tokio::spawn(async move {
1043 use tokio::io::{AsyncBufReadExt, BufReader};
1044 let reader = BufReader::new(stream);
1045 let mut lines = reader.lines();
1046 while let Ok(Some(line)) = lines.next_line().await {
1047 if tx.send(line).await.is_err() {
1048 break;
1049 }
1050 }
1051 });
1052
1053 debug!("logcat stream started");
1054 Ok(rx)
1055 }
1056}
1057
1058#[cfg(test)]
1059mod tests {
1060 use super::*;
1061
1062 #[test]
1063 fn test_device_creation() {
1064 let d = AdbDevice::with_serial("emulator-5554");
1065 assert_eq!(d.serial, "emulator-5554");
1066 assert_eq!(d.host, "127.0.0.1");
1067 assert_eq!(d.port, 5037);
1068 }
1069
1070 #[test]
1071 fn test_device_custom_host() {
1072 let d = AdbDevice::new("device123", "192.168.1.100", 5038);
1073 assert_eq!(d.serial, "device123");
1074 assert_eq!(d.host, "192.168.1.100");
1075 assert_eq!(d.port, 5038);
1076 }
1077
1078 #[test]
1079 fn test_parse_shell2_output() {
1080 let raw = "hello world\nDROIDRUN_EXIT:0\n";
1081 let sentinel = "DROIDRUN_EXIT:";
1082 let (stdout, exit_code) = if let Some(pos) = raw.rfind(sentinel) {
1083 let code_str = raw[pos + sentinel.len()..].trim();
1084 let code = code_str.parse::<i32>().unwrap_or(-1);
1085 let stdout = raw[..pos].to_string();
1086 (stdout, code)
1087 } else {
1088 (raw.to_string(), -1)
1089 };
1090 assert_eq!(stdout, "hello world\n");
1091 assert_eq!(exit_code, 0);
1092 }
1093
1094 #[test]
1095 fn test_parse_shell2_failure() {
1096 let raw = "error: not found\nDROIDRUN_EXIT:1\n";
1097 let sentinel = "DROIDRUN_EXIT:";
1098 let (stdout, exit_code) = if let Some(pos) = raw.rfind(sentinel) {
1099 let code_str = raw[pos + sentinel.len()..].trim();
1100 let code = code_str.parse::<i32>().unwrap_or(-1);
1101 let stdout = raw[..pos].to_string();
1102 (stdout, code)
1103 } else {
1104 (raw.to_string(), -1)
1105 };
1106 assert_eq!(stdout, "error: not found\n");
1107 assert_eq!(exit_code, 1);
1108 }
1109
1110 #[test]
1111 fn test_parse_wm_size() {
1112 let output = "Physical size: 1080x1920\n";
1113 let re = Regex::new(r"(\d+)x(\d+)").unwrap();
1114 let caps = re.captures(output).unwrap();
1115 let width: u32 = caps[1].parse().unwrap();
1116 let height: u32 = caps[2].parse().unwrap();
1117 assert_eq!(width, 1080);
1118 assert_eq!(height, 1920);
1119 }
1120
1121 #[test]
1122 fn test_parse_wm_size_override() {
1123 let output = "Physical size: 1440x2960\nOverride size: 1080x2220\n";
1124 let re = Regex::new(r"(\d+)x(\d+)").unwrap();
1125 let caps = re.captures(output).unwrap();
1126 let width: u32 = caps[1].parse().unwrap();
1127 let height: u32 = caps[2].parse().unwrap();
1128 assert_eq!(width, 1440);
1129 assert_eq!(height, 2960);
1130 }
1131
1132 #[test]
1133 fn test_parse_current_app() {
1134 let output =
1135 " mResumedActivity: ActivityRecord{abcdef u0 com.example.app/.MainActivity t1}\n";
1136 let re = Regex::new(r"([a-zA-Z0-9_.]+)/([a-zA-Z0-9_.]+)").unwrap();
1137 let caps = re.captures(output).unwrap();
1138 assert_eq!(&caps[1], "com.example.app");
1139 assert_eq!(&caps[2], ".MainActivity");
1140 }
1141
1142 #[test]
1143 fn test_parse_app_info() {
1144 let output = " versionName=1.2.3\n versionCode=42 minSdk=24\n codePath=/data/app/com.example\n firstInstallTime=2024-01-01\n lastUpdateTime=2024-06-15\n";
1145 let mut version_name = None;
1146 let mut version_code = None;
1147 let mut install_path = None;
1148
1149 for line in output.lines() {
1150 let trimmed = line.trim();
1151 if trimmed.starts_with("versionName=") {
1152 version_name = Some(trimmed.trim_start_matches("versionName=").to_string());
1153 } else if trimmed.starts_with("versionCode=") {
1154 let val = trimmed
1155 .trim_start_matches("versionCode=")
1156 .split_whitespace()
1157 .next()
1158 .unwrap_or("0");
1159 version_code = val.parse::<i64>().ok();
1160 } else if trimmed.starts_with("codePath=") {
1161 install_path = Some(trimmed.trim_start_matches("codePath=").to_string());
1162 }
1163 }
1164
1165 assert_eq!(version_name.as_deref(), Some("1.2.3"));
1166 assert_eq!(version_code, Some(42));
1167 assert_eq!(install_path.as_deref(), Some("/data/app/com.example"));
1168 }
1169
1170 #[test]
1171 fn test_parse_wlan_ip() {
1172 let output = " inet 192.168.1.42/24 brd 192.168.1.255 scope global wlan0\n";
1173 let re = Regex::new(r"inet (\d+\.\d+\.\d+\.\d+)").unwrap();
1174 let caps = re.captures(output).unwrap();
1175 assert_eq!(&caps[1], "192.168.1.42");
1176 }
1177
1178 #[test]
1179 fn test_parse_screen_on() {
1180 let output = " mWakefulness=Awake\n";
1181 assert!(output.contains("Awake"));
1182
1183 let output2 = " mWakefulness=Asleep\n";
1184 assert!(!output2.contains("Awake"));
1185 }
1186}