1use eyre::eyre;
2use smol::channel::{Receiver, Sender};
3use smol::io::AsyncWriteExt;
4use smol::spawn;
5use sysinfo::{ProcessRefreshKind, ProcessesToUpdate, System, UpdateKind};
6use tracing::{debug, error};
7
8use std::ffi::{OsStr, OsString};
9use std::path::Path;
10use std::process::{ExitStatus, Output, Stdio};
11use std::sync::OnceLock;
12use std::time::Duration;
13
14use crate::{
15 android::adb::Adb,
16 android::platform::AndroidAbi,
17 android::toolchain::AndroidSdk,
18 device::{
19 ApplicationExit, Artifact, Device, DeviceEvent, FailToRun, LogLevel, RunOptions, Running,
20 },
21 toolchain::Host,
22 utils::{CommandError, parse_whitespace_separated_u32s},
23};
24
25#[derive(Debug, Clone)]
27struct PanicInfo {
28 payload: String,
29 location: Option<String>,
30}
31
32#[derive(Debug, Clone)]
33enum AndroidRuntimeEvent {
34 Panic(PanicInfo),
35 NativeCrash(String),
36 ActivityFinished,
37}
38
39const ADB_DEVICE_COMMAND_TIMEOUT: Duration = Duration::from_secs(10);
40const ANDROID_ACTIVITY_FINISHED_MARKER: &str = "WATERUI_ACTIVITY_FINISHED";
41
42#[derive(Debug, thiserror::Error)]
44enum AdbCommandError {
45 #[error(transparent)]
47 Spawn(#[from] CommandError),
48 #[error("{operation} timed out after {seconds} seconds")]
50 Timeout {
51 operation: String,
53 seconds: u64,
55 },
56 #[error("{operation} failed with status {status}{details}")]
58 Failed {
59 operation: String,
61 status: ExitStatus,
63 details: String,
65 },
66}
67
68async fn run_bounded_adb_output<A, S>(
69 host: &Host,
70 adb: &Adb,
71 args: A,
72 operation: &str,
73) -> Result<Output, AdbCommandError>
74where
75 A: IntoIterator<Item = S>,
76 S: AsRef<OsStr>,
77{
78 let args = args
79 .into_iter()
80 .map(|argument| argument.as_ref().to_os_string())
81 .collect::<Vec<_>>();
82 let operation = operation.to_owned();
83 let command = Box::pin(async move {
84 host.output(adb.path(), &args)
85 .await
86 .map_err(AdbCommandError::from)
87 });
88 let timeout = Box::pin(async move {
89 smol::Timer::after(ADB_DEVICE_COMMAND_TIMEOUT).await;
90 Err(AdbCommandError::Timeout {
91 operation,
92 seconds: ADB_DEVICE_COMMAND_TIMEOUT.as_secs(),
93 })
94 });
95
96 match futures_util::future::select(command, timeout).await {
97 futures_util::future::Either::Left((result, _))
98 | futures_util::future::Either::Right((result, _)) => result,
99 }
100}
101
102async fn run_bounded_adb_command<A, S>(
103 host: &Host,
104 adb: &Adb,
105 args: A,
106 operation: &str,
107) -> Result<String, AdbCommandError>
108where
109 A: IntoIterator<Item = S>,
110 S: AsRef<OsStr>,
111{
112 let output = run_bounded_adb_output(host, adb, args, operation).await?;
113 if output.status.success() {
114 return Ok(String::from_utf8_lossy(&output.stdout).to_string());
115 }
116
117 let stderr = String::from_utf8_lossy(&output.stderr);
118 let stdout = String::from_utf8_lossy(&output.stdout);
119 let details = if !stderr.is_empty() {
120 format!("\nstderr:\n{stderr}")
121 } else if !stdout.is_empty() {
122 format!("\nstdout:\n{stdout}")
123 } else {
124 String::new()
125 };
126 Err(AdbCommandError::Failed {
127 operation: operation.to_owned(),
128 status: output.status,
129 details,
130 })
131}
132
133#[derive(Debug)]
135pub struct AndroidDevice {
136 identifier: String,
137 abi: AndroidAbi,
139}
140
141impl AndroidDevice {
142 #[must_use]
144 pub const fn new(identifier: String, abi: AndroidAbi) -> Self {
145 Self { identifier, abi }
146 }
147
148 #[must_use]
150 pub fn identifier(&self) -> &str {
151 &self.identifier
152 }
153
154 #[must_use]
156 pub const fn abi(&self) -> AndroidAbi {
157 self.abi
158 }
159}
160
161impl Device for AndroidDevice {
162 fn name(&self) -> &str {
163 &self.identifier
164 }
165
166 async fn launch(&self, host: &Host) -> eyre::Result<()> {
167 let adb = Adb::locate(host).await?;
168 host.run(adb.path(), ["-s", &self.identifier, "wait-for-device"])
169 .await?;
170 Ok(())
171 }
172
173 async fn run(
174 &self,
175 host: &Host,
176 artifact: Artifact,
177 options: RunOptions,
178 ) -> Result<Running, FailToRun> {
179 run_on_android(host, &self.identifier, artifact, options).await
180 }
181
182 async fn scan(host: &Host) -> eyre::Result<Vec<Self>> {
183 let adb = Adb::locate(host).await?;
184 Self::scan_with_adb(host, &adb).await
185 }
186}
187
188impl AndroidDevice {
189 pub async fn scan_with_adb(host: &Host, adb: &Adb) -> eyre::Result<Vec<Self>> {
195 let output =
196 run_bounded_adb_command(host, adb, ["devices", "-l"], "listing Android devices")
197 .await
198 .map_err(|e| eyre!("Failed to list devices: {e}"))?;
199
200 let mut devices = Vec::new();
201
202 for line in output.lines().skip(1) {
203 let parts: Vec<&str> = line.split_whitespace().collect();
204 if parts.len() >= 2 && parts[1] == "device" {
205 let identifier = parts[0].to_string();
206
207 let abi = run_bounded_adb_command(
209 host,
210 adb,
211 ["-s", &identifier, "shell", "getprop", "ro.product.cpu.abi"],
212 "querying Android device ABI",
213 )
214 .await
215 .map_err(|e| eyre!("Failed to get device ABI: {e}"))?;
216 let abi = abi
217 .trim()
218 .parse::<AndroidAbi>()
219 .map_err(|e| eyre!("Unsupported device ABI: {e}"))?;
220
221 devices.push(Self::new(identifier, abi));
222 }
223 }
224
225 Ok(devices)
226 }
227}
228
229pub trait AndroidAbiProvider {
231 fn android_abi(&self) -> AndroidAbi;
233}
234
235impl AndroidAbiProvider for AndroidDevice {
236 fn android_abi(&self) -> AndroidAbi {
237 self.abi()
238 }
239}
240
241impl AndroidAbiProvider for AndroidEmulator {
242 fn android_abi(&self) -> AndroidAbi {
243 self.expected_abi()
244 }
245}
246
247async fn run_on_android(
257 host: &Host,
258 device_id: &str,
259 artifact: Artifact,
260 options: RunOptions,
261) -> Result<Running, FailToRun> {
262 let adb = Adb::locate(host)
263 .await
264 .map_err(|error| FailToRun::Run(error.into()))?;
265 let env_vars = options
266 .env_vars()
267 .map(|(key, value)| (key.to_string(), value.to_string()))
268 .collect::<Vec<_>>();
269
270 if let Some(port) =
274 crate::web::dev_url_port(env_vars.iter().map(|(k, v)| (k.as_str(), v.as_str())))
275 .map_err(|error| FailToRun::Launch(eyre!("Invalid dev-server handoff: {error}")))?
276 {
277 reverse_dev_server_port(host, &adb, device_id, port).await?;
278 }
279
280 install_android_artifact(host, &adb, device_id, artifact.path()).await?;
281 launch_android_app(
282 host,
283 &adb,
284 build_android_start_args(device_id, &artifact, &env_vars),
285 )
286 .await?;
287
288 let pid = wait_for_app_pid(host, &adb, device_id, artifact.bundle_id()).await?;
290
291 let host_for_kill = host.clone();
292 let adb_for_kill = adb.clone();
293 let device_id_for_kill = device_id.to_string();
294 let bundle_id_for_kill = artifact.bundle_id().to_string();
295
296 let (running, sender) = Running::new(move || {
297 spawn_android_force_stop(
298 &host_for_kill,
299 adb_for_kill,
300 device_id_for_kill,
301 bundle_id_for_kill,
302 );
303 });
304
305 spawn_android_runtime_tasks(AndroidRuntimeTaskContext {
306 host,
307 adb: &adb,
308 device_id,
309 bundle_id: artifact.bundle_id(),
310 pid,
311 log_level: options.log_level(),
312 sender,
313 });
314
315 Ok(running)
316}
317
318async fn install_android_artifact(
319 host: &Host,
320 adb: &Adb,
321 device_id: &str,
322 artifact_path: &Path,
323) -> Result<(), FailToRun> {
324 let install_output = host
325 .command(adb.path())
326 .args(["-s", device_id, "install", "-r"])
327 .arg(artifact_path)
328 .stdout(Stdio::piped())
329 .stderr(Stdio::piped())
330 .output()
331 .await
332 .map_err(|error| FailToRun::Install(eyre!("Failed to install APK: {error}")))?;
333
334 if install_output.status.success() {
335 return Ok(());
336 }
337
338 Err(FailToRun::Install(eyre!(
339 "Failed to install APK:\n{}\n{}",
340 String::from_utf8_lossy(&install_output.stdout).trim(),
341 String::from_utf8_lossy(&install_output.stderr).trim(),
342 )))
343}
344
345fn build_android_start_args(
349 device_id: &str,
350 artifact: &Artifact,
351 env_vars: &[(String, String)],
352) -> Vec<String> {
353 let mut start_args = vec![
354 "-s".to_string(),
355 device_id.to_string(),
356 "shell".to_string(),
357 "am".to_string(),
358 "start".to_string(),
359 "-S".to_string(),
360 "-n".to_string(),
361 format!("{}/.MainActivity", artifact.bundle_id()),
362 ];
363
364 for (key, value) in env_vars {
365 start_args.push("--es".to_string());
366 start_args.push(format!("waterui.env.{key}"));
367 start_args.push(value.clone());
368 }
369
370 start_args
371}
372
373async fn reverse_dev_server_port(
376 host: &Host,
377 adb: &Adb,
378 device_id: &str,
379 port: u16,
380) -> Result<(), FailToRun> {
381 let output = host
382 .command(adb.path())
383 .args(crate::web::adb_reverse_args(device_id, port))
384 .stdout(Stdio::piped())
385 .stderr(Stdio::piped())
386 .output()
387 .await
388 .map_err(|error| FailToRun::Launch(eyre!("Failed to run `adb reverse`: {error}")))?;
389
390 if output.status.success() {
391 return Ok(());
392 }
393
394 Err(FailToRun::Launch(eyre!(
395 "`adb reverse tcp:{port} tcp:{port}` failed:\n{}\n{}",
396 String::from_utf8_lossy(&output.stdout).trim(),
397 String::from_utf8_lossy(&output.stderr).trim(),
398 )))
399}
400
401async fn launch_android_app(
402 host: &Host,
403 adb: &Adb,
404 start_args: Vec<String>,
405) -> Result<(), FailToRun> {
406 let output = host
407 .command(adb.path())
408 .args(&start_args)
409 .stdout(Stdio::piped())
410 .stderr(Stdio::piped())
411 .output()
412 .await
413 .map_err(|error| FailToRun::Launch(eyre!("Failed to launch app: {error}")))?;
414
415 if output.status.success() {
416 return Ok(());
417 }
418
419 Err(FailToRun::Launch(eyre!(
420 "Failed to launch app:\n{}\n{}",
421 String::from_utf8_lossy(&output.stdout).trim(),
422 String::from_utf8_lossy(&output.stderr).trim(),
423 )))
424}
425
426fn spawn_android_force_stop(host: &Host, adb: Adb, device_id: String, bundle_id: String) {
427 let host = host.clone();
428 let spawn_result = std::thread::Builder::new()
429 .name("waterui-android-force-stop".to_string())
430 .spawn(move || {
431 let result = host
432 .std_command(adb.path())
433 .args(["-s", &device_id, "shell", "am", "force-stop", &bundle_id])
434 .output();
435
436 match result {
437 Ok(output) => {
438 tracing::debug!(
439 "Force-stop command executed: status={}, stdout={}, stderr={}",
440 output.status,
441 String::from_utf8_lossy(&output.stdout),
442 String::from_utf8_lossy(&output.stderr)
443 );
444 }
445 Err(error) => {
446 error!("Failed to stop app {}: {}", bundle_id, error);
447 }
448 }
449 });
450
451 if let Err(error) = spawn_result {
452 error!("Failed to spawn force-stop worker thread: {error}");
453 }
454}
455
456struct AndroidRuntimeTaskContext<'a> {
457 host: &'a Host,
458 adb: &'a Adb,
459 device_id: &'a str,
460 bundle_id: &'a str,
461 pid: u32,
462 log_level: Option<LogLevel>,
463 sender: Sender<DeviceEvent>,
464}
465
466fn spawn_android_runtime_tasks(context: AndroidRuntimeTaskContext<'_>) {
467 let AndroidRuntimeTaskContext {
468 host,
469 adb,
470 device_id,
471 bundle_id,
472 pid,
473 log_level,
474 sender,
475 } = context;
476 let sender_for_monitor = sender.clone();
477 let sender_for_runtime_event = sender.clone();
478 let sender_for_logs = sender;
479 let adb_for_monitor = adb.clone();
480 let host_for_monitor = host.clone();
481 let device_id_for_monitor = device_id.to_string();
482 let bundle_id_for_monitor = bundle_id.to_string();
483
484 smol::spawn(async move {
485 monitor_android_process(
486 host_for_monitor,
487 adb_for_monitor,
488 &device_id_for_monitor,
489 &bundle_id_for_monitor,
490 pid,
491 sender_for_monitor,
492 )
493 .await;
494 })
495 .detach();
496
497 let runtime_event_rx =
498 start_android_log_stream(host, adb, device_id, pid, log_level, sender_for_logs);
499 spawn(async move {
500 if let Ok(event) = runtime_event_rx.recv().await {
501 match event {
502 AndroidRuntimeEvent::Panic(info) => {
503 let _ = sender_for_runtime_event
504 .send(DeviceEvent::Crashed(format_android_panic(&info)))
505 .await;
506 }
507 AndroidRuntimeEvent::NativeCrash(log) => {
508 let _ = sender_for_runtime_event
509 .send(DeviceEvent::Crashed(format!(
510 "Android process crashed.\n\n=== Crash Log ===\n{log}"
511 )))
512 .await;
513 }
514 AndroidRuntimeEvent::ActivityFinished => {
515 let _ = sender_for_runtime_event
516 .send(DeviceEvent::Exited(ApplicationExit::user_closed()))
517 .await;
518 }
519 }
520 }
521 })
522 .detach();
523}
524
525fn format_android_panic(info: &PanicInfo) -> String {
526 let mut msg = format!("Panic: {}", info.payload);
527 if let Some(location) = &info.location {
528 msg.push('\n');
529 msg.push_str(" at ");
530 msg.push_str(location);
531 }
532 msg
533}
534
535async fn wait_for_app_pid(
537 host: &Host,
538 adb: &Adb,
539 device_id: &str,
540 bundle_id: &str,
541) -> Result<u32, FailToRun> {
542 for _ in 0..10 {
543 smol::Timer::after(std::time::Duration::from_millis(200)).await;
544 if let Ok(output) = run_bounded_adb_command(
545 host,
546 adb,
547 ["-s", device_id, "shell", "pidof", bundle_id],
548 "querying the launched Android process",
549 )
550 .await
551 && let Some(pid) = parse_whitespace_separated_u32s(&output).into_iter().next()
552 {
553 return Ok(pid);
554 }
555 }
556
557 let crash_info = match run_bounded_adb_command(
559 host,
560 adb,
561 [
562 "-s",
563 device_id,
564 "logcat",
565 "-d",
566 "-t",
567 "100",
568 "-s",
569 "AndroidRuntime:E",
570 "DEBUG:*",
571 "WaterUI:*",
572 ],
573 "collecting Android startup crash logs",
574 )
575 .await
576 {
577 Ok(output) => output,
578 Err(err) => format!("(failed to collect logcat crash info: {err})"),
579 };
580
581 let mut error_msg = format!("App {bundle_id} crashed on startup (process not found).\n\n");
582
583 if !crash_info.trim().is_empty() {
584 error_msg.push_str("=== Crash Log ===\n");
585 error_msg.push_str(&crash_info);
586 }
587
588 Err(FailToRun::Launch(eyre!("{}", error_msg)))
589}
590
591pub async fn emulator_avd_name_with_adb(
596 host: &Host,
597 adb: &Adb,
598 emulator_id: &str,
599) -> eyre::Result<String> {
600 if !emulator_id.starts_with("emulator-") {
601 eyre::bail!("Not an Android emulator identifier: {emulator_id}");
602 }
603
604 let output = run_bounded_adb_command(
605 host,
606 adb,
607 ["-s", emulator_id, "emu", "avd", "name"],
608 "querying the Android emulator name",
609 )
610 .await?;
611 let name = output.lines().next().unwrap_or_default().trim();
612 if name.is_empty() {
613 eyre::bail!("Failed to query AVD name for {emulator_id}: empty response");
614 }
615 Ok(name.to_string())
616}
617
618pub async fn emulator_avd_name(host: &Host, emulator_id: &str) -> eyre::Result<String> {
623 let adb = Adb::locate(host).await?;
624 emulator_avd_name_with_adb(host, &adb, emulator_id).await
625}
626
627async fn try_find_running_emulator_for_avd(
628 host: &Host,
629 adb: &Adb,
630 avd_name: &str,
631) -> eyre::Result<Option<AndroidDevice>> {
632 let devices = AndroidDevice::scan_with_adb(host, adb).await?;
633
634 for device in devices {
635 let id = device.identifier();
636 if !id.starts_with("emulator-") {
637 continue;
638 }
639
640 match emulator_avd_name_with_adb(host, adb, id).await {
641 Ok(name) if name == avd_name => return Ok(Some(device)),
642 Ok(_) => {}
643 Err(e) => {
644 debug!("Failed to query AVD name for {id}: {e}");
646 }
647 }
648 }
649
650 Ok(None)
651}
652
653async fn adb_emulator_states(host: &Host, adb: &Adb) -> eyre::Result<String> {
654 let output = run_bounded_adb_command(
655 host,
656 adb,
657 ["devices", "-l"],
658 "querying Android emulator state",
659 )
660 .await?;
661 let states: Vec<String> = output
662 .lines()
663 .skip(1)
664 .map(str::trim)
665 .filter(|line| line.starts_with("emulator-"))
666 .map(ToOwned::to_owned)
667 .collect();
668
669 if states.is_empty() {
670 return Ok(String::new());
671 }
672
673 Ok(states.join("; "))
674}
675
676async fn adb_emulator_boot_completed(host: &Host, adb: &Adb, emulator_id: &str) -> bool {
677 run_bounded_adb_command(
678 host,
679 adb,
680 ["-s", emulator_id, "shell", "getprop", "sys.boot_completed"],
681 "querying Android emulator boot completion",
682 )
683 .await
684 .is_ok_and(|value| value.trim() == "1")
685}
686
687fn adb_reports_device_ready(output: &str, device_id: &str) -> bool {
688 output.lines().skip(1).any(|line| {
689 let mut fields = line.split_whitespace();
690 fields.next() == Some(device_id) && fields.next() == Some("device")
691 })
692}
693
694async fn adb_device_is_ready(host: &Host, adb: &Adb, device_id: &str) -> eyre::Result<bool> {
695 let output = run_bounded_adb_command(
696 host,
697 adb,
698 ["devices", "-l"],
699 "querying Android device readiness",
700 )
701 .await?;
702 Ok(adb_reports_device_ready(&output, device_id))
703}
704
705fn command_targets_avd(command: &[OsString], avd_name: &OsStr) -> bool {
706 command
707 .windows(2)
708 .any(|arguments| arguments[0] == "-avd" && arguments[1] == avd_name)
709}
710
711async fn avd_process_is_running(avd_name: &str) -> bool {
712 let avd_name = OsString::from(avd_name);
713 smol::unblock(move || {
714 let mut processes = System::new();
715 processes.refresh_processes_specifics(
716 ProcessesToUpdate::All,
717 true,
718 ProcessRefreshKind::nothing().with_cmd(UpdateKind::Always),
719 );
720 processes
721 .processes()
722 .values()
723 .any(|process| command_targets_avd(process.cmd(), &avd_name))
724 })
725 .await
726}
727
728async fn adb_package_manager_ready(host: &Host, adb: &Adb, emulator_id: &str) -> bool {
729 run_bounded_adb_command(
730 host,
731 adb,
732 ["-s", emulator_id, "shell", "pm", "path", "android"],
733 "querying Android package manager readiness",
734 )
735 .await
736 .is_ok_and(|output| {
737 output
738 .lines()
739 .any(|line| line.trim().starts_with("package:"))
740 })
741}
742
743async fn monitor_android_process(
745 host: Host,
746 adb: Adb,
747 device_id: &str,
748 bundle_id: &str,
749 pid: u32,
750 sender: smol::channel::Sender<DeviceEvent>,
751) {
752 loop {
754 smol::Timer::after(std::time::Duration::from_secs(1)).await;
755
756 let pids = match query_android_process_pids(&host, &adb, device_id, bundle_id).await {
760 Ok(pids) => pids,
761 Err(err) => {
762 if adb_device_is_ready(&host, &adb, device_id)
763 .await
764 .is_ok_and(|ready| !ready)
765 {
766 debug!(
767 "Android device {device_id} disconnected while monitoring {bundle_id}: {err}"
768 );
769 let _ = sender
770 .send(DeviceEvent::Exited(ApplicationExit::user_closed()))
771 .await;
772 break;
773 }
774 debug!(
775 "Failed to query process state via pidof for {bundle_id} on {device_id}: {err}"
776 );
777 continue;
778 }
779 };
780
781 let still_running = pids.contains(&pid);
783
784 if !still_running {
785 let pid_arg = format!("--pid={pid}");
787 let pid_log_args = vec![
788 "-s".to_string(),
789 device_id.to_string(),
790 "logcat".to_string(),
791 "-v".to_string(),
792 "threadtime".to_string(),
793 "-d".to_string(),
794 "-t".to_string(),
795 "200".to_string(),
796 pid_arg,
797 "*:V".to_string(),
798 ];
799 let pid_log = run_bounded_adb_output(
800 &host,
801 &adb,
802 pid_log_args
803 .iter()
804 .map(|s| std::ffi::OsStr::new(s.as_str())),
805 "collecting Android process exit logs",
806 )
807 .await
808 .map_or_else(
809 |err| {
810 debug!("Failed to fetch PID-filtered logcat: {err}");
811 String::new()
812 },
813 |output| {
814 if output.status.success() {
815 String::from_utf8_lossy(&output.stdout).to_string()
816 } else {
817 debug!("PID-filtered logcat exited with status {}", output.status);
818 String::new()
819 }
820 },
821 );
822
823 if android_log_looks_like_crash(&pid_log, bundle_id, pid) {
824 let crash_log = pid_log;
825
826 let error_msg = if crash_log.trim().is_empty() {
827 format!("Process {bundle_id} crashed.")
828 } else {
829 format!("Process {bundle_id} crashed.\n\n=== Crash Log ===\n{crash_log}")
830 };
831
832 let _ = sender.send(DeviceEvent::Crashed(error_msg)).await;
833 } else {
834 let _ = sender
835 .send(DeviceEvent::Exited(ApplicationExit::user_closed()))
836 .await;
837 }
838 break;
839 }
840 }
841}
842
843async fn query_android_process_pids(
844 host: &Host,
845 adb: &Adb,
846 device_id: &str,
847 bundle_id: &str,
848) -> eyre::Result<Vec<u32>> {
849 let output = run_bounded_adb_output(
850 host,
851 adb,
852 ["-s", device_id, "shell", "pidof", bundle_id].map(OsStr::new),
853 "querying Android process state",
854 )
855 .await?;
856 let stdout = String::from_utf8_lossy(&output.stdout);
857 if output.status.success() || stdout.trim().is_empty() {
858 return Ok(parse_whitespace_separated_u32s(&stdout));
859 }
860
861 let stderr = String::from_utf8_lossy(&output.stderr);
862 eyre::bail!(
863 "pidof failed with status {}: {}",
864 output.status,
865 stderr.trim()
866 );
867}
868
869fn log_mentions_pid(log: &str, pid: u32) -> bool {
870 let pid_str = pid.to_string();
871 let pid_lower = format!("pid: {pid}");
872 let pid_upper = format!("PID: {pid}");
873
874 log.lines().any(|line| {
875 line.split_whitespace().any(|part| part == pid_str)
876 || line.contains(&pid_lower)
877 || line.contains(&pid_upper)
878 })
879}
880
881fn android_log_looks_like_crash(log: &str, bundle_id: &str, pid: u32) -> bool {
882 if log.trim().is_empty() {
883 return false;
884 }
885
886 let relevant = log.contains(bundle_id) || log_mentions_pid(log, pid);
887 if !relevant {
888 return false;
889 }
890
891 if android_log_line_looks_like_crash(log) {
892 return true;
893 }
894
895 if !log.contains(bundle_id) {
897 return false;
898 }
899
900 log.contains("AndroidRuntime")
902 && (log.contains("E AndroidRuntime") || log.contains("Exception"))
903}
904
905fn android_log_line_looks_like_crash(line: &str) -> bool {
906 line.contains("FATAL EXCEPTION")
907 || line.contains("Fatal signal")
908 || line.contains("SIGSEGV")
909 || line.contains("SIGABRT")
910 || line.contains("SIGBUS")
911 || line.contains("SIGILL")
912 || line.contains("SIGFPE")
913 || line.contains("Abort message:")
914 || line.contains("backtrace:")
915}
916
917fn start_android_log_stream(
922 host: &Host,
923 adb: &Adb,
924 device_id: &str,
925 pid: u32,
926 log_level: Option<LogLevel>,
927 sender: Sender<DeviceEvent>,
928) -> Receiver<AndroidRuntimeEvent> {
929 use futures_util::StreamExt;
930 use futures_util::io::{AsyncBufReadExt, BufReader};
931
932 let (runtime_event_tx, runtime_event_rx) = smol::channel::bounded::<AndroidRuntimeEvent>(1);
934
935 let priority = match log_level {
938 Some(LogLevel::Debug) => 'D',
939 Some(LogLevel::Verbose) => 'V',
940 Some(LogLevel::Error | LogLevel::Warn | LogLevel::Info) | None => 'I',
941 };
942
943 let pid_arg = format!("--pid={pid}");
945 let mut cmd = host.command(adb.path());
946 cmd.args(["-s", device_id, "logcat", "-v", "threadtime"])
947 .arg(pid_arg)
948 .arg(format!("*:{priority}"))
949 .stdout(std::process::Stdio::piped())
950 .stderr(std::process::Stdio::null());
951
952 let mut child = match cmd.spawn() {
953 Ok(c) => c,
954 Err(e) => {
955 tracing::warn!("Failed to spawn logcat: {e}");
956 return runtime_event_rx;
957 }
958 };
959
960 let Some(stdout) = child.stdout.take() else {
961 return runtime_event_rx;
962 };
963
964 let reader = BufReader::new(stdout);
965 let mut lines = reader.lines();
966
967 spawn(async move {
968 while let Some(result) = lines.next().await {
971 let Ok(line) = result else { break };
972
973 if let Some(event) = android_runtime_event_from_log_line(&line) {
975 let _ = runtime_event_tx.try_send(event);
976 }
977
978 if let Some(requested_level) = log_level {
980 let (parsed_level, message) = parse_logcat_line(&line);
981
982 if log_level_allows(requested_level, parsed_level)
983 && sender
984 .try_send(DeviceEvent::Log {
985 level: parsed_level,
986 message,
987 })
988 .is_err()
989 {
990 break;
991 }
992 }
993 }
994
995 let _ = child.kill();
997 })
998 .detach();
999
1000 runtime_event_rx
1001}
1002
1003fn android_runtime_event_from_log_line(line: &str) -> Option<AndroidRuntimeEvent> {
1004 if line.contains("panic.payload=")
1005 && let Some(info) = extract_panic_info_from_log(line)
1006 {
1007 return Some(AndroidRuntimeEvent::Panic(info));
1008 }
1009 if android_log_line_looks_like_crash(line) {
1010 return Some(AndroidRuntimeEvent::NativeCrash(line.to_string()));
1011 }
1012 if line.contains(ANDROID_ACTIVITY_FINISHED_MARKER) {
1013 return Some(AndroidRuntimeEvent::ActivityFinished);
1014 }
1015 None
1016}
1017
1018fn log_level_allows(requested: LogLevel, actual: tracing::Level) -> bool {
1019 match requested {
1020 LogLevel::Error => actual == tracing::Level::ERROR,
1021 LogLevel::Warn => matches!(actual, tracing::Level::ERROR | tracing::Level::WARN),
1022 LogLevel::Info => matches!(
1023 actual,
1024 tracing::Level::ERROR | tracing::Level::WARN | tracing::Level::INFO
1025 ),
1026 LogLevel::Debug => actual != tracing::Level::TRACE,
1027 LogLevel::Verbose => true,
1028 }
1029}
1030
1031fn extract_panic_info_from_log(line: &str) -> Option<PanicInfo> {
1033 let mut payload = None;
1034 let mut location = None;
1035
1036 if let Some(start) = line.find("panic.payload=\"") {
1038 let start = start + 15;
1039 if let Some(end) = line[start..].find('"') {
1040 payload = Some(line[start..start + end].to_string());
1041 }
1042 }
1043
1044 if let Some(start) = line.find("panic.location=\"") {
1046 let start = start + 16;
1047 if let Some(end) = line[start..].find('"') {
1048 location = Some(line[start..start + end].to_string());
1049 }
1050 }
1051
1052 payload.map(|p| PanicInfo {
1053 payload: p,
1054 location,
1055 })
1056}
1057
1058struct LogcatParsed {
1060 level: tracing::Level,
1061 tag: String,
1062 message: String,
1063}
1064
1065fn parse_logcat_line(line: &str) -> (tracing::Level, String) {
1068 if let Some(parsed) = try_parse_logcat(line) {
1070 let formatted = format!("[{}] {}", parsed.tag, parsed.message);
1071 return (parsed.level, formatted);
1072 }
1073
1074 (tracing::Level::INFO, line.to_string())
1076}
1077
1078fn try_parse_logcat(line: &str) -> Option<LogcatParsed> {
1080 let parts: Vec<&str> = line.splitn(7, char::is_whitespace).collect();
1085
1086 if parts.len() < 6 {
1088 return None;
1089 }
1090
1091 let mut level_idx = None;
1093 for (i, part) in parts.iter().enumerate() {
1094 if part.len() == 1 {
1095 let c = part.chars().next()?;
1096 if matches!(c, 'V' | 'D' | 'I' | 'W' | 'E' | 'F') {
1097 level_idx = Some(i);
1098 break;
1099 }
1100 }
1101 }
1102
1103 let level_idx = level_idx?;
1104 if level_idx + 1 >= parts.len() {
1105 return None;
1106 }
1107
1108 let level = match parts[level_idx] {
1109 "E" | "F" => tracing::Level::ERROR,
1110 "W" => tracing::Level::WARN,
1111 "D" => tracing::Level::DEBUG,
1112 "V" => tracing::Level::TRACE,
1113 _ => tracing::Level::INFO,
1114 };
1115
1116 let level_char = parts[level_idx].chars().next()?;
1120 let search_start = 18.min(line.len());
1121 let level_pos = line[search_start..]
1122 .find(level_char)
1123 .map(|p| p + search_start)?;
1124
1125 let after_level = line.get(level_pos + 1..)?.trim_start();
1126
1127 after_level.find(": ").map_or_else(
1129 || {
1130 Some(LogcatParsed {
1131 level,
1132 tag: "unknown".to_string(),
1133 message: after_level.to_string(),
1134 })
1135 },
1136 |colon_pos| {
1137 let tag = after_level[..colon_pos].trim();
1138 let message = after_level[colon_pos + 2..].to_string();
1139 Some(LogcatParsed {
1140 level,
1141 tag: tag.to_string(),
1142 message,
1143 })
1144 },
1145 )
1146}
1147
1148#[derive(Debug)]
1153pub struct AndroidEmulator {
1154 avd_name: String,
1156 expected_abi: AndroidAbi,
1157 device: OnceLock<AndroidDevice>,
1158}
1159
1160impl AndroidEmulator {
1161 pub async fn open(host: &Host, avd_name: String) -> eyre::Result<Self> {
1166 let expected_abi = read_avd_abi(host, &avd_name).await?;
1167 Ok(Self {
1168 avd_name,
1169 expected_abi,
1170 device: OnceLock::new(),
1171 })
1172 }
1173
1174 #[must_use]
1176 pub fn avd_name(&self) -> &str {
1177 &self.avd_name
1178 }
1179
1180 #[must_use]
1181 pub const fn expected_abi(&self) -> AndroidAbi {
1183 self.expected_abi
1184 }
1185}
1186
1187impl Device for AndroidEmulator {
1188 fn name(&self) -> &str {
1189 &self.avd_name
1190 }
1191
1192 async fn launch(&self, host: &Host) -> eyre::Result<()> {
1193 let emulator_path = AndroidSdk::emulator_path(host)
1194 .ok_or_else(|| eyre::eyre!("Android emulator not found"))?;
1195 let adb = Adb::locate(host).await?;
1196
1197 let mut emulator_process = if avd_process_is_running(&self.avd_name).await {
1198 debug!(
1199 "AVD '{}' already has a running emulator process; waiting for it to become ready",
1200 self.avd_name
1201 );
1202 None
1203 } else {
1204 let mut emulator_cmd = host.std_command(&emulator_path);
1207 emulator_cmd
1208 .arg("-avd")
1209 .arg(&self.avd_name)
1210 .arg("-no-snapshot-load")
1211 .stdout(std::process::Stdio::null())
1212 .stderr(std::process::Stdio::null());
1213
1214 #[cfg(unix)]
1215 {
1216 use std::os::unix::process::CommandExt as _;
1217 emulator_cmd.process_group(0);
1220 }
1221
1222 Some(smol::unblock(move || emulator_cmd.spawn()).await?)
1223 };
1224
1225 let start = std::time::Instant::now();
1226 let timeout = std::time::Duration::from_mins(5);
1227 let mut last_emulator_states = String::new();
1228
1229 loop {
1230 if let Some(process) = emulator_process.as_mut()
1231 && let Some(status) = process.try_wait()?
1232 {
1233 if avd_process_is_running(&self.avd_name).await {
1234 debug!(
1235 "Launched emulator process exited with {status}, but another process owns AVD '{}'; waiting for that instance",
1236 self.avd_name
1237 );
1238 emulator_process = None;
1239 } else {
1240 eyre::bail!(
1241 "Emulator process exited before becoming ready (status: {status}). Check AVD configuration and run `emulator -avd {}` manually for details.",
1242 self.avd_name
1243 );
1244 }
1245 } else if emulator_process.is_none() && !avd_process_is_running(&self.avd_name).await {
1246 eyre::bail!(
1247 "Existing emulator process for AVD '{}' exited before becoming ready.",
1248 self.avd_name
1249 );
1250 }
1251
1252 if start.elapsed() > timeout {
1253 let states = if last_emulator_states.is_empty() {
1254 "no emulator device reported by adb".to_string()
1255 } else {
1256 last_emulator_states.clone()
1257 };
1258
1259 eyre::bail!(
1260 "Emulator launch timed out after 300 seconds (ADB state: {}).",
1261 states
1262 );
1263 }
1264
1265 last_emulator_states = match adb_emulator_states(host, &adb).await {
1266 Ok(states) => states,
1267 Err(err) => format!("failed to query emulator state via adb: {err}"),
1268 };
1269
1270 if let Some(device) =
1271 try_find_running_emulator_for_avd(host, &adb, &self.avd_name).await?
1272 {
1273 if device.abi() != self.expected_abi {
1274 eyre::bail!(
1275 "AVD '{}' expected ABI {}, but running emulator reports {}",
1276 self.avd_name,
1277 self.expected_abi.as_str(),
1278 device.abi().as_str()
1279 );
1280 }
1281
1282 let emulator_id = device.identifier().to_string();
1283 let boot_completed = adb_emulator_boot_completed(host, &adb, &emulator_id).await;
1284 let package_ready = adb_package_manager_ready(host, &adb, &emulator_id).await;
1285
1286 if !boot_completed || !package_ready {
1287 debug!(
1288 "Emulator {} detected but not fully ready yet (boot_completed={}, package_ready={})",
1289 emulator_id, boot_completed, package_ready
1290 );
1291 smol::Timer::after(std::time::Duration::from_secs(2)).await;
1292 continue;
1293 }
1294
1295 self.device
1296 .set(device)
1297 .map_err(|_| eyre::eyre!("Emulator device already initialized"))?;
1298 return Ok(());
1299 }
1300
1301 smol::Timer::after(std::time::Duration::from_secs(2)).await;
1302 }
1303 }
1304
1305 async fn run(
1306 &self,
1307 host: &Host,
1308 artifact: Artifact,
1309 options: RunOptions,
1310 ) -> Result<Running, FailToRun> {
1311 let device = self.device.get().ok_or_else(|| {
1312 FailToRun::Run(eyre!(
1313 "Android emulator '{}' is not launched. Launch it before running.",
1314 self.avd_name
1315 ))
1316 })?;
1317 run_on_android(host, device.identifier(), artifact, options).await
1318 }
1319
1320 async fn scan(host: &Host) -> eyre::Result<Vec<Self>> {
1321 let emulator_path = AndroidSdk::emulator_path(host)
1323 .ok_or_else(|| eyre::eyre!("Android emulator not found"))?;
1324
1325 let output = host
1326 .output(&emulator_path, ["-list-avds"])
1327 .await
1328 .map_err(|e| eyre!("Failed to list AVDs: {e}"))?;
1329
1330 if !output.status.success() {
1331 let stderr = String::from_utf8_lossy(&output.stderr);
1332 eyre::bail!("Failed to list AVDs: {}", stderr.trim());
1333 }
1334
1335 let stdout = String::from_utf8_lossy(&output.stdout);
1336 let mut avds = Vec::new();
1337 for name in stdout.lines().map(str::trim).filter(|l| !l.is_empty()) {
1338 avds.push(Self::open(host, name.to_string()).await?);
1339 }
1340
1341 Ok(avds)
1342 }
1343}
1344
1345async fn read_avd_abi(host: &Host, avd_name: &str) -> eyre::Result<AndroidAbi> {
1346 let home = host
1347 .home_dir()
1348 .ok_or_else(|| eyre!("Failed to resolve home directory"))?;
1349 let config_path = home
1350 .join(".android/avd")
1351 .join(format!("{avd_name}.avd"))
1352 .join("config.ini");
1353
1354 let content = smol::fs::read_to_string(&config_path)
1355 .await
1356 .map_err(|e| eyre!("Failed to read AVD config {}: {e}", config_path.display()))?;
1357
1358 let abi_value = content
1359 .lines()
1360 .filter_map(|line| line.split_once('=').map(|(k, v)| (k.trim(), v.trim())))
1361 .find_map(|(k, v)| (k == "abi.type").then_some(v))
1362 .ok_or_else(|| eyre!("AVD config {} missing key abi.type", config_path.display()))?;
1363
1364 abi_value
1365 .parse::<AndroidAbi>()
1366 .map_err(|e| eyre!("Unsupported AVD ABI '{abi_value}': {e}"))
1367}
1368
1369pub async fn screenshot(host: &Host, device_id: &str, output: &Path) -> eyre::Result<()> {
1379 let adb = Adb::locate(host).await?;
1380
1381 let output_result = run_bounded_adb_output(
1382 host,
1383 &adb,
1384 ["-s", device_id, "exec-out", "screencap", "-p"],
1385 "capturing the Android device screen",
1386 )
1387 .await?;
1388
1389 if !output_result.status.success() {
1390 let stderr = String::from_utf8_lossy(&output_result.stderr);
1391 eyre::bail!("Failed to capture screenshot: {}", stderr.trim());
1392 }
1393
1394 let mut file = smol::fs::File::create(output).await?;
1396 file.write_all(&output_result.stdout).await?;
1397 file.flush().await?;
1398
1399 Ok(())
1400}
1401
1402pub async fn tap(host: &Host, device_id: &str, x: u32, y: u32) -> eyre::Result<()> {
1410 let adb = Adb::locate(host).await?;
1411
1412 run_bounded_adb_command(
1413 host,
1414 &adb,
1415 [
1416 "-s",
1417 device_id,
1418 "shell",
1419 "input",
1420 "tap",
1421 &x.to_string(),
1422 &y.to_string(),
1423 ],
1424 "performing an Android tap",
1425 )
1426 .await?;
1427
1428 Ok(())
1429}
1430
1431pub async fn swipe(
1446 host: &Host,
1447 device_id: &str,
1448 from: (u32, u32),
1449 to: (u32, u32),
1450 duration_ms: Option<u32>,
1451) -> eyre::Result<()> {
1452 let adb = Adb::locate(host).await?;
1453
1454 let mut args = vec!["-s", device_id, "shell", "input", "swipe"];
1455
1456 let x1 = from.0.to_string();
1457 let y1 = from.1.to_string();
1458 let x2 = to.0.to_string();
1459 let y2 = to.1.to_string();
1460 let duration = duration_ms.map(|d| d.to_string());
1461
1462 args.push(&x1);
1463 args.push(&y1);
1464 args.push(&x2);
1465 args.push(&y2);
1466
1467 if let Some(ref d) = duration {
1468 args.push(d);
1469 }
1470
1471 run_bounded_adb_command(host, &adb, args, "performing an Android swipe").await?;
1472
1473 Ok(())
1474}
1475
1476pub async fn text(host: &Host, device_id: &str, input: &str) -> eyre::Result<()> {
1485 let adb = Adb::locate(host).await?;
1486
1487 let escaped = input
1489 .replace('\\', "\\\\")
1490 .replace(' ', "%s")
1491 .replace('"', "\\\"")
1492 .replace('\'', "\\'")
1493 .replace('&', "\\&")
1494 .replace('<', "\\<")
1495 .replace('>', "\\>")
1496 .replace('|', "\\|")
1497 .replace(';', "\\;")
1498 .replace('(', "\\(")
1499 .replace(')', "\\)");
1500
1501 run_bounded_adb_command(
1502 host,
1503 &adb,
1504 ["-s", device_id, "shell", "input", "text", &escaped],
1505 "entering text on an Android device",
1506 )
1507 .await?;
1508
1509 Ok(())
1510}
1511
1512pub async fn screenshot_bytes(host: &Host, device_id: &str) -> eyre::Result<Vec<u8>> {
1520 let adb = Adb::locate(host).await?;
1521
1522 let output = run_bounded_adb_output(
1523 host,
1524 &adb,
1525 ["-s", device_id, "exec-out", "screencap", "-p"],
1526 "capturing the Android device screen",
1527 )
1528 .await?;
1529
1530 if !output.status.success() {
1531 let stderr = String::from_utf8_lossy(&output.stderr);
1532 eyre::bail!("Failed to capture screenshot: {}", stderr.trim());
1533 }
1534
1535 Ok(output.stdout)
1536}
1537
1538pub async fn describe(host: &Host, device_id: &str) -> eyre::Result<String> {
1546 let adb = Adb::locate(host).await?;
1547
1548 let dump_path = "/sdcard/window_dump.xml";
1550 run_bounded_adb_command(
1551 host,
1552 &adb,
1553 ["-s", device_id, "shell", "uiautomator", "dump", dump_path],
1554 "dumping the Android accessibility hierarchy",
1555 )
1556 .await?;
1557
1558 let output = run_bounded_adb_output(
1560 host,
1561 &adb,
1562 ["-s", device_id, "shell", "cat", dump_path],
1563 "reading the Android accessibility hierarchy",
1564 )
1565 .await?;
1566
1567 if !output.status.success() {
1568 let stderr = String::from_utf8_lossy(&output.stderr);
1569 eyre::bail!("Failed to read UI dump: {}", stderr.trim());
1570 }
1571
1572 let xml = String::from_utf8_lossy(&output.stdout).to_string();
1573
1574 let json = xml_to_ui_json(&xml)?;
1576
1577 let _ = host
1579 .run(adb.path(), ["-s", device_id, "shell", "rm", dump_path])
1580 .await;
1581
1582 Ok(json)
1583}
1584
1585fn xml_to_ui_json(xml: &str) -> eyre::Result<String> {
1587 let mut elements = Vec::new();
1588
1589 for line in xml.lines() {
1591 if !line.contains("<node") {
1592 continue;
1593 }
1594
1595 let mut element = serde_json::Map::new();
1596
1597 if let Some(bounds_start) = line.find("bounds=\"[") {
1599 let bounds_str = &line[bounds_start + 8..];
1600 if let Some(bounds_end) = bounds_str.find('"') {
1601 let bounds = &bounds_str[..bounds_end];
1602 let parts: Vec<&str> = bounds
1604 .trim_matches(|c| c == '[' || c == ']')
1605 .split("][")
1606 .collect();
1607 if parts.len() == 2 {
1608 let lt: Vec<i32> = parts[0].split(',').filter_map(|s| s.parse().ok()).collect();
1609 let rb: Vec<i32> = parts[1].split(',').filter_map(|s| s.parse().ok()).collect();
1610 if lt.len() == 2 && rb.len() == 2 {
1611 let mut frame = serde_json::Map::new();
1612 frame.insert("x".to_string(), serde_json::Value::Number(lt[0].into()));
1613 frame.insert("y".to_string(), serde_json::Value::Number(lt[1].into()));
1614 frame.insert(
1615 "width".to_string(),
1616 serde_json::Value::Number((rb[0] - lt[0]).into()),
1617 );
1618 frame.insert(
1619 "height".to_string(),
1620 serde_json::Value::Number((rb[1] - lt[1]).into()),
1621 );
1622 element.insert("frame".to_string(), serde_json::Value::Object(frame));
1623 }
1624 }
1625 }
1626 }
1627
1628 for attr in ["text", "content-desc", "class", "resource-id"] {
1630 let search = format!("{attr}=\"");
1631 if let Some(start) = line.find(&search) {
1632 let value_start = start + search.len();
1633 let rest = &line[value_start..];
1634 if let Some(end) = rest.find('"') {
1635 let value = &rest[..end];
1636 if !value.is_empty() {
1637 let key = match attr {
1638 "content-desc" => "AXLabel",
1639 "class" => "type",
1640 "resource-id" => "AXUniqueId",
1641 "text" => "AXValue",
1642 _ => attr,
1643 };
1644 element.insert(
1645 key.to_string(),
1646 serde_json::Value::String(value.to_string()),
1647 );
1648 }
1649 }
1650 }
1651 }
1652
1653 if line.contains("clickable=\"true\"") {
1655 element.insert("clickable".to_string(), serde_json::Value::Bool(true));
1656 }
1657 if line.contains("enabled=\"true\"") {
1658 element.insert("enabled".to_string(), serde_json::Value::Bool(true));
1659 }
1660
1661 if !element.is_empty() {
1662 elements.push(serde_json::Value::Object(element));
1663 }
1664 }
1665
1666 serde_json::to_string(&elements).map_err(|e| eyre!("Failed to serialize UI elements: {e}"))
1667}
1668
1669#[cfg(test)]
1670mod tests {
1671 use std::ffi::OsString;
1672
1673 use super::{
1674 AndroidRuntimeEvent, adb_reports_device_ready, android_log_looks_like_crash,
1675 android_runtime_event_from_log_line, command_targets_avd, log_level_allows,
1676 log_mentions_pid,
1677 };
1678 use crate::device::LogLevel;
1679
1680 #[test]
1681 fn detects_pid_mentions_in_threadtime_lines() {
1682 let log = "12-10 23:04:40.190 28184 28184 F libc : Fatal signal 11 (SIGSEGV)\n";
1683 assert!(log_mentions_pid(log, 28184));
1684 assert!(!log_mentions_pid(log, 12345));
1685 }
1686
1687 #[test]
1688 fn avoids_false_positive_from_unrelated_fatal_signal_in_global_dump() {
1689 let unrelated = "12-10 23:04:40.190 999 999 F libc : Fatal signal 11 (SIGSEGV)\n";
1690 assert!(!android_log_looks_like_crash(
1691 unrelated,
1692 "com.example.app",
1693 28184
1694 ));
1695 }
1696
1697 #[test]
1698 fn detects_native_crash_when_pid_is_mentioned() {
1699 let log = "I DEBUG : Fatal signal 11 (SIGSEGV), code 1, fault addr 0x0 in tid 1 (main) pid: 28184\n";
1700 assert!(android_log_looks_like_crash(log, "com.example.app", 28184));
1701 }
1702
1703 #[test]
1704 fn detects_java_crash_for_app() {
1705 let log = "E AndroidRuntime: FATAL EXCEPTION: main\nE AndroidRuntime: Process: com.example.app, PID: 28184\n";
1706 assert!(android_log_looks_like_crash(log, "com.example.app", 28184));
1707 }
1708
1709 #[test]
1710 fn detects_activity_completion_marker() {
1711 let event = android_runtime_event_from_log_line(
1712 "07-26 20:00:00.000 28184 28184 I WaterUI.MainActivity: WATERUI_ACTIVITY_FINISHED",
1713 );
1714 assert!(matches!(event, Some(AndroidRuntimeEvent::ActivityFinished)));
1715 }
1716
1717 #[test]
1718 fn filters_internal_lifecycle_logs_from_stricter_user_log_levels() {
1719 assert!(!log_level_allows(LogLevel::Error, tracing::Level::INFO));
1720 assert!(!log_level_allows(LogLevel::Warn, tracing::Level::INFO));
1721 assert!(log_level_allows(LogLevel::Info, tracing::Level::INFO));
1722 assert!(log_level_allows(LogLevel::Debug, tracing::Level::DEBUG));
1723 assert!(log_level_allows(LogLevel::Verbose, tracing::Level::TRACE));
1724 }
1725
1726 #[test]
1727 fn matches_emulator_process_to_exact_avd_argument() {
1728 let command = [
1729 OsString::from("qemu-system-aarch64"),
1730 OsString::from("-netdelay"),
1731 OsString::from("none"),
1732 OsString::from("-avd"),
1733 OsString::from("Pixel_9"),
1734 ];
1735
1736 assert!(command_targets_avd(&command, "Pixel_9".as_ref()));
1737 assert!(!command_targets_avd(&command, "Pixel_9_Pro".as_ref()));
1738 }
1739
1740 #[test]
1741 fn does_not_match_unrelated_avd_text() {
1742 let command = [
1743 OsString::from("emulator-helper"),
1744 OsString::from("--log"),
1745 OsString::from("starting Pixel_9"),
1746 ];
1747
1748 assert!(!command_targets_avd(&command, "Pixel_9".as_ref()));
1749 }
1750
1751 #[test]
1752 fn parses_ready_android_device_state() {
1753 let output = "List of devices attached\nemulator-5554 device product:sdk_phone64_arm64 transport_id:1\n";
1754
1755 assert!(adb_reports_device_ready(output, "emulator-5554"));
1756 assert!(!adb_reports_device_ready(output, "emulator-5556"));
1757 }
1758
1759 #[test]
1760 fn rejects_offline_android_device_state() {
1761 let output = "List of devices attached\nemulator-5554 offline transport_id:1\n";
1762
1763 assert!(!adb_reports_device_ready(output, "emulator-5554"));
1764 }
1765}