Skip to main content

droidrun_adb/
device.rs

1/// ADB device — async operations against a single Android device.
2use 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/// Represents a connection to a specific Android device via the ADB server.
16///
17/// Each operation opens a fresh TCP connection to the ADB server, selects
18/// the transport for this device, then performs the command. This matches
19/// how the ADB protocol works — there is no persistent session.
20#[derive(Debug, Clone)]
21pub struct AdbDevice {
22    host: String,
23    port: u16,
24    pub serial: String,
25}
26
27impl AdbDevice {
28    /// Create a new device handle.
29    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    /// Create a device handle using default ADB server address.
38    pub fn with_serial(serial: impl Into<String>) -> Self {
39        Self::new(serial, "127.0.0.1", 5037)
40    }
41
42    // ══════════════════════════════════════════════════════════════
43    //  Connection helpers
44    // ══════════════════════════════════════════════════════════════
45
46    /// Open a connection to the ADB server.
47    async fn connect_server(&self) -> Result<AdbConnection> {
48        AdbConnection::connect(&self.host, self.port).await
49    }
50
51    /// Open a connection and select this device's transport.
52    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    /// Open a sync protocol session (transport + sync:).
60    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    // ══════════════════════════════════════════════════════════════
67    //  Device state & info
68    // ══════════════════════════════════════════════════════════════
69
70    /// Get the device state (device, offline, unauthorized, etc.).
71    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    /// Get the real serial number of the device.
81    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    /// Get the device feature list (e.g., shell_v2, cmd, stat_v2).
90    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    // ══════════════════════════════════════════════════════════════
103    //  Shell
104    // ══════════════════════════════════════════════════════════════
105
106    /// Run a shell command and return stdout as a String.
107    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    /// Run a shell command and return stdout as raw bytes.
121    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    /// Run a shell command and return both stdout and exit code.
129    ///
130    /// Wraps the command in a subshell `(cmd)` so that even `exit N` won't
131    /// prevent the sentinel from being printed, then appends
132    /// `; echo DROIDRUN_EXIT:$?` to capture the exit code.
133    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    // ══════════════════════════════════════════════════════════════
151    //  System properties
152    // ══════════════════════════════════════════════════════════════
153
154    /// Get a system property by name.
155    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    /// Get the device model (ro.product.model).
161    pub async fn prop_model(&self) -> Result<String> {
162        self.getprop("ro.product.model").await
163    }
164
165    /// Get the device name (ro.product.name).
166    pub async fn prop_name(&self) -> Result<String> {
167        self.getprop("ro.product.name").await
168    }
169
170    /// Get the device codename (ro.product.device).
171    pub async fn prop_device(&self) -> Result<String> {
172        self.getprop("ro.product.device").await
173    }
174
175    // ══════════════════════════════════════════════════════════════
176    //  Input actions
177    // ══════════════════════════════════════════════════════════════
178
179    /// Tap at screen coordinates.
180    pub async fn tap(&self, x: i32, y: i32) -> Result<()> {
181        self.shell(&format!("input tap {x} {y}")).await?;
182        Ok(())
183    }
184
185    /// Swipe from (x1, y1) to (x2, y2) over duration_ms milliseconds.
186    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    /// Send a key event.
200    pub async fn keyevent(&self, keycode: i32) -> Result<()> {
201        self.shell(&format!("input keyevent {keycode}")).await?;
202        Ok(())
203    }
204
205    /// Drag from (sx, sy) to (ex, ey) over duration_ms milliseconds.
206    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    /// Type text using ADB input method.
220    ///
221    /// Note: For reliable Unicode text input, use droidrun-core's Portal keyboard.
222    /// This method escapes special shell characters but cannot handle all Unicode.
223    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    // ══════════════════════════════════════════════════════════════
237    //  Screenshots
238    // ══════════════════════════════════════════════════════════════
239
240    /// Take a screenshot and return PNG bytes.
241    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    // ══════════════════════════════════════════════════════════════
253    //  App management
254    // ══════════════════════════════════════════════════════════════
255
256    /// Start an app with optional activity name.
257    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    /// Force stop an app.
286    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    /// Clear app data and cache.
293    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    /// Get the current foreground app.
300    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        // Try mResumedActivity first, then mCurrentFocus
306        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                // Remove trailing } or spaces
312                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        // Fallback: try to parse differently
321        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    /// Get detailed info about an installed app.
339    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                // Format: "versionCode=123 minSdk=..."
359                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    /// Install an APK on the device.
380    ///
381    /// Pushes the APK to `/data/local/tmp/`, runs `pm install`, then removes it.
382    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        // Cleanup
404        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    /// Uninstall an app.
415    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    /// List installed packages.
426    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    // ══════════════════════════════════════════════════════════════
443    //  Port forwarding (host-to-device)
444    // ══════════════════════════════════════════════════════════════
445
446    /// Set up port forwarding. Returns the local port.
447    ///
448    /// If `local_port` is 0, the ADB server assigns a free port.
449    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            // Read the second OKAY status (double-OKAY protocol)
459            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    /// List all port forwards for this device.
479    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    /// Remove a specific port forward.
507    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    /// Remove all port forwards for this device.
518    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    // ══════════════════════════════════════════════════════════════
526    //  Reverse port forwarding (device-to-host)
527    // ══════════════════════════════════════════════════════════════
528
529    /// Set up reverse port forwarding (device → host).
530    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    /// List all reverse port forwards.
541    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                // Format varies: sometimes "host remote local", sometimes just "remote local"
552                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    /// Remove a specific reverse forward.
573    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    /// Remove all reverse forwards.
581    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    // ══════════════════════════════════════════════════════════════
588    //  System commands (ADB protocol level)
589    // ══════════════════════════════════════════════════════════════
590
591    /// Restart adbd as root. Only works on userdebug/eng builds or emulators.
592    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    /// Switch adbd to TCP/IP mode on the given port.
601    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    /// Reboot the device.
610    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    // ══════════════════════════════════════════════════════════════
622    //  File operations (sync protocol)
623    // ══════════════════════════════════════════════════════════════
624
625    /// Push a local file to the device using the sync protocol.
626    pub async fn push(&self, local_path: &Path, remote_path: &str) -> Result<()> {
627        self.push_file(local_path, remote_path).await
628    }
629
630    /// Internal push implementation using sync protocol.
631    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        // SEND command: "SEND" + length-prefixed path with permissions
641        let path_with_mode = format!("{remote},33188"); // 0o100644
642        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        // Send data in chunks
650        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        // DONE command with mtime
660        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        // Read response
668        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    /// Push raw bytes to a file on the device.
694    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    /// Pull a file from the device and return its contents as bytes.
750    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        // QUIT
804        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    /// Pull a file from the device to a local path.
812    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    /// Get file metadata via sync STAT protocol.
824    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        // QUIT
851        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    /// List directory contents via sync LIST protocol.
858    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            // DENT: mode(4) + size(4) + mtime(4) + namelen(4) + name(namelen)
888            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        // QUIT
910        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    // ══════════════════════════════════════════════════════════════
918    //  File operations (shell-based)
919    // ══════════════════════════════════════════════════════════════
920
921    /// Check if a file or directory exists on the device.
922    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    /// Delete a file on the device.
930    pub async fn remove(&self, path: &str) -> Result<()> {
931        self.shell(&format!("rm -f '{path}'")).await?;
932        Ok(())
933    }
934
935    /// Delete a directory recursively on the device.
936    pub async fn rmtree(&self, path: &str) -> Result<()> {
937        self.shell(&format!("rm -rf '{path}'")).await?;
938        Ok(())
939    }
940
941    // ══════════════════════════════════════════════════════════════
942    //  Screen & display
943    // ══════════════════════════════════════════════════════════════
944
945    /// Get screen dimensions.
946    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    /// Get current screen rotation (0=natural, 1=left, 2=inverted, 3=right).
965    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    /// Check if the screen is currently on.
977    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    /// Turn screen on or off.
985    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?; // KEYCODE_POWER
989        }
990        Ok(())
991    }
992
993    // ══════════════════════════════════════════════════════════════
994    //  Network info
995    // ══════════════════════════════════════════════════════════════
996
997    /// Get the device's WLAN IP address.
998    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    // ══════════════════════════════════════════════════════════════
1011    //  Device info (misc)
1012    // ══════════════════════════════════════════════════════════════
1013
1014    /// Get the device date/time.
1015    pub async fn get_date(&self) -> Result<String> {
1016        let result = self.shell("date").await?;
1017        Ok(result.trim().to_string())
1018    }
1019
1020    // ══════════════════════════════════════════════════════════════
1021    //  Logcat
1022    // ══════════════════════════════════════════════════════════════
1023
1024    /// Stream logcat output. Returns an mpsc receiver.
1025    ///
1026    /// The stream runs in a background task until the receiver is dropped.
1027    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}