1use std::fmt;
3use std::time::{Duration, UNIX_EPOCH};
4
5#[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#[derive(Debug, Clone)]
56pub struct DeviceInfo {
57 pub serial: String,
58 pub state: DeviceState,
59}
60
61#[derive(Debug, Clone)]
63pub struct ForwardEntry {
64 pub serial: String,
65 pub local: String,
66 pub remote: String,
67}
68
69impl ForwardEntry {
70 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#[derive(Debug, Clone)]
88pub struct ShellOutput {
89 pub stdout: String,
90 pub exit_code: i32,
91}
92
93#[derive(Debug, Clone)]
95pub struct ReverseEntry {
96 pub remote: String,
97 pub local: String,
98}
99
100impl ReverseEntry {
101 pub fn remote_port(&self) -> Option<u16> {
103 self.remote
104 .strip_prefix("tcp:")
105 .and_then(|s| s.parse().ok())
106 }
107
108 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#[derive(Debug, Clone)]
118pub struct FileStat {
119 pub mode: u32,
120 pub size: u32,
121 pub mtime: u32,
122}
123
124impl FileStat {
125 pub fn is_dir(&self) -> bool {
127 (self.mode & 0o40000) != 0
128 }
129
130 pub fn is_file(&self) -> bool {
132 (self.mode & 0o100000) != 0
133 }
134
135 pub fn exists(&self) -> bool {
137 self.mode != 0
138 }
139
140 pub fn modified_time(&self) -> std::time::SystemTime {
142 UNIX_EPOCH + Duration::from_secs(self.mtime as u64)
143 }
144}
145
146#[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#[derive(Debug, Clone, Copy, PartialEq, Eq)]
167pub enum RebootMode {
168 Normal,
170 Bootloader,
172 Recovery,
174 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#[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#[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#[derive(Debug, Clone)]
215pub struct DeviceEvent {
216 pub serial: String,
217 pub state: DeviceState,
218}
219
220#[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 #[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}