Skip to main content

droidrun_adb/
models.rs

1/// Data models for ADB operations.
2use std::fmt;
3use std::time::{Duration, UNIX_EPOCH};
4
5/// Device state as reported by ADB.
6#[derive(Debug, Clone, PartialEq, Eq)]
7pub enum DeviceState {
8    Device,
9    Offline,
10    Unauthorized,
11    Authorizing,
12    Connecting,
13    Recovery,
14    Bootloader,
15    Unknown(String),
16}
17
18impl DeviceState {
19    pub fn is_online(&self) -> bool {
20        matches!(self, DeviceState::Device)
21    }
22}
23
24impl fmt::Display for DeviceState {
25    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
26        match self {
27            Self::Device => write!(f, "device"),
28            Self::Offline => write!(f, "offline"),
29            Self::Unauthorized => write!(f, "unauthorized"),
30            Self::Authorizing => write!(f, "authorizing"),
31            Self::Connecting => write!(f, "connecting"),
32            Self::Recovery => write!(f, "recovery"),
33            Self::Bootloader => write!(f, "bootloader"),
34            Self::Unknown(s) => write!(f, "{s}"),
35        }
36    }
37}
38
39impl From<&str> for DeviceState {
40    fn from(s: &str) -> Self {
41        match s.trim() {
42            "device" => Self::Device,
43            "offline" => Self::Offline,
44            "unauthorized" => Self::Unauthorized,
45            "authorizing" => Self::Authorizing,
46            "connecting" => Self::Connecting,
47            "recovery" => Self::Recovery,
48            "bootloader" => Self::Bootloader,
49            other => Self::Unknown(other.to_string()),
50        }
51    }
52}
53
54/// Basic device info from `host:devices`.
55#[derive(Debug, Clone)]
56pub struct DeviceInfo {
57    pub serial: String,
58    pub state: DeviceState,
59}
60
61/// A port forward entry.
62#[derive(Debug, Clone)]
63pub struct ForwardEntry {
64    pub serial: String,
65    pub local: String,
66    pub remote: String,
67}
68
69impl ForwardEntry {
70    /// Extract port number from a "tcp:XXXXX" string.
71    pub fn local_port(&self) -> Option<u16> {
72        self.local
73            .strip_prefix("tcp:")
74            .and_then(|s| s.parse().ok())
75    }
76
77    pub fn remote_port(&self) -> Option<u16> {
78        self.remote
79            .strip_prefix("tcp:")
80            .and_then(|s| s.parse().ok())
81    }
82}
83
84// ── New types ───────────────────────────────────────────────────
85
86/// Shell command result with exit code.
87#[derive(Debug, Clone)]
88pub struct ShellOutput {
89    pub stdout: String,
90    pub exit_code: i32,
91}
92
93/// A reverse port forward entry (device-to-host).
94#[derive(Debug, Clone)]
95pub struct ReverseEntry {
96    pub remote: String,
97    pub local: String,
98}
99
100impl ReverseEntry {
101    /// Extract remote port number from a "tcp:XXXXX" string.
102    pub fn remote_port(&self) -> Option<u16> {
103        self.remote
104            .strip_prefix("tcp:")
105            .and_then(|s| s.parse().ok())
106    }
107
108    /// Extract local port number from a "tcp:XXXXX" string.
109    pub fn local_port(&self) -> Option<u16> {
110        self.local
111            .strip_prefix("tcp:")
112            .and_then(|s| s.parse().ok())
113    }
114}
115
116/// File metadata from the sync STAT command.
117#[derive(Debug, Clone)]
118pub struct FileStat {
119    pub mode: u32,
120    pub size: u32,
121    pub mtime: u32,
122}
123
124impl FileStat {
125    /// Whether this is a directory.
126    pub fn is_dir(&self) -> bool {
127        (self.mode & 0o40000) != 0
128    }
129
130    /// Whether this is a regular file.
131    pub fn is_file(&self) -> bool {
132        (self.mode & 0o100000) != 0
133    }
134
135    /// Whether the file exists (mode != 0 means it was found).
136    pub fn exists(&self) -> bool {
137        self.mode != 0
138    }
139
140    /// Convert mtime to a `SystemTime`.
141    pub fn modified_time(&self) -> std::time::SystemTime {
142        UNIX_EPOCH + Duration::from_secs(self.mtime as u64)
143    }
144}
145
146/// Directory entry from the sync LIST command.
147#[derive(Debug, Clone)]
148pub struct SyncDirEntry {
149    pub name: String,
150    pub mode: u32,
151    pub size: u32,
152    pub mtime: u32,
153}
154
155impl SyncDirEntry {
156    pub fn is_dir(&self) -> bool {
157        (self.mode & 0o40000) != 0
158    }
159
160    pub fn is_file(&self) -> bool {
161        (self.mode & 0o100000) != 0
162    }
163}
164
165/// Reboot mode.
166#[derive(Debug, Clone, Copy, PartialEq, Eq)]
167pub enum RebootMode {
168    /// Normal reboot.
169    Normal,
170    /// Reboot into bootloader (fastboot).
171    Bootloader,
172    /// Reboot into recovery.
173    Recovery,
174    /// Reboot into sideload mode.
175    Sideload,
176}
177
178impl RebootMode {
179    pub fn as_str(&self) -> &str {
180        match self {
181            Self::Normal => "",
182            Self::Bootloader => "bootloader",
183            Self::Recovery => "recovery",
184            Self::Sideload => "sideload",
185        }
186    }
187}
188
189/// Current foreground app information.
190#[derive(Debug, Clone)]
191pub struct CurrentApp {
192    pub package: String,
193    pub activity: String,
194}
195
196impl fmt::Display for CurrentApp {
197    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
198        write!(f, "{}/{}", self.package, self.activity)
199    }
200}
201
202/// Detailed app information from `dumpsys package`.
203#[derive(Debug, Clone)]
204pub struct AppDetail {
205    pub package: String,
206    pub version_name: Option<String>,
207    pub version_code: Option<i64>,
208    pub install_path: Option<String>,
209    pub first_install_time: Option<String>,
210    pub last_update_time: Option<String>,
211}
212
213/// Device tracking event (for `track_devices` stream).
214#[derive(Debug, Clone)]
215pub struct DeviceEvent {
216    pub serial: String,
217    pub state: DeviceState,
218}
219
220/// Screen dimensions in pixels.
221#[derive(Debug, Clone, Copy, PartialEq, Eq)]
222pub struct ScreenSize {
223    pub width: u32,
224    pub height: u32,
225}
226
227impl fmt::Display for ScreenSize {
228    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
229        write!(f, "{}x{}", self.width, self.height)
230    }
231}
232
233#[cfg(test)]
234mod tests {
235    use super::*;
236
237    #[test]
238    fn test_device_state_from_str() {
239        assert_eq!(DeviceState::from("device"), DeviceState::Device);
240        assert_eq!(DeviceState::from("offline"), DeviceState::Offline);
241        assert_eq!(DeviceState::from("unauthorized"), DeviceState::Unauthorized);
242        assert!(DeviceState::from("device").is_online());
243        assert!(!DeviceState::from("offline").is_online());
244    }
245
246    #[test]
247    fn test_forward_entry_ports() {
248        let entry = ForwardEntry {
249            serial: "emulator-5554".into(),
250            local: "tcp:27183".into(),
251            remote: "tcp:8080".into(),
252        };
253        assert_eq!(entry.local_port(), Some(27183));
254        assert_eq!(entry.remote_port(), Some(8080));
255    }
256
257    #[test]
258    fn test_forward_entry_non_tcp() {
259        let entry = ForwardEntry {
260            serial: "device".into(),
261            local: "localabstract:foo".into(),
262            remote: "tcp:5000".into(),
263        };
264        assert_eq!(entry.local_port(), None);
265        assert_eq!(entry.remote_port(), Some(5000));
266    }
267
268    // ── New type tests ──────────────────────────────────────────
269
270    #[test]
271    fn test_shell_output() {
272        let output = ShellOutput {
273            stdout: "hello world\n".into(),
274            exit_code: 0,
275        };
276        assert_eq!(output.exit_code, 0);
277        assert!(output.stdout.contains("hello"));
278    }
279
280    #[test]
281    fn test_reverse_entry_ports() {
282        let entry = ReverseEntry {
283            remote: "tcp:8080".into(),
284            local: "tcp:9090".into(),
285        };
286        assert_eq!(entry.remote_port(), Some(8080));
287        assert_eq!(entry.local_port(), Some(9090));
288    }
289
290    #[test]
291    fn test_reverse_entry_non_tcp() {
292        let entry = ReverseEntry {
293            remote: "localabstract:foo".into(),
294            local: "tcp:5000".into(),
295        };
296        assert_eq!(entry.remote_port(), None);
297        assert_eq!(entry.local_port(), Some(5000));
298    }
299
300    #[test]
301    fn test_file_stat_dir() {
302        let stat = FileStat {
303            mode: 0o40755,
304            size: 4096,
305            mtime: 1700000000,
306        };
307        assert!(stat.is_dir());
308        assert!(!stat.is_file());
309        assert!(stat.exists());
310    }
311
312    #[test]
313    fn test_file_stat_file() {
314        let stat = FileStat {
315            mode: 0o100644,
316            size: 1234,
317            mtime: 1700000000,
318        };
319        assert!(!stat.is_dir());
320        assert!(stat.is_file());
321        assert!(stat.exists());
322    }
323
324    #[test]
325    fn test_file_stat_not_found() {
326        let stat = FileStat {
327            mode: 0,
328            size: 0,
329            mtime: 0,
330        };
331        assert!(!stat.is_dir());
332        assert!(!stat.is_file());
333        assert!(!stat.exists());
334    }
335
336    #[test]
337    fn test_sync_dir_entry() {
338        let entry = SyncDirEntry {
339            name: "test.txt".into(),
340            mode: 0o100644,
341            size: 100,
342            mtime: 1700000000,
343        };
344        assert!(entry.is_file());
345        assert!(!entry.is_dir());
346    }
347
348    #[test]
349    fn test_reboot_mode_str() {
350        assert_eq!(RebootMode::Normal.as_str(), "");
351        assert_eq!(RebootMode::Bootloader.as_str(), "bootloader");
352        assert_eq!(RebootMode::Recovery.as_str(), "recovery");
353        assert_eq!(RebootMode::Sideload.as_str(), "sideload");
354    }
355
356    #[test]
357    fn test_current_app_display() {
358        let app = CurrentApp {
359            package: "com.example.app".into(),
360            activity: ".MainActivity".into(),
361        };
362        assert_eq!(app.to_string(), "com.example.app/.MainActivity");
363    }
364
365    #[test]
366    fn test_screen_size_display() {
367        let size = ScreenSize {
368            width: 1080,
369            height: 1920,
370        };
371        assert_eq!(size.to_string(), "1080x1920");
372    }
373
374    #[test]
375    fn test_device_event() {
376        let event = DeviceEvent {
377            serial: "emulator-5554".into(),
378            state: DeviceState::Device,
379        };
380        assert!(event.state.is_online());
381    }
382}