Skip to main content

android_emulator/
lib.rs

1//! Android Emulator gRPC Control Library
2//!
3//! This library provides Rust bindings for controlling Android Emulators via gRPC,
4//! along with utilities for starting and managing emulator instances.
5
6use std::io;
7use std::net::{Ipv4Addr, SocketAddrV4};
8use std::path::{Path, PathBuf};
9use std::sync::Arc;
10use std::time::Duration;
11use thiserror::Error;
12use tokio::io::{AsyncBufReadExt, BufReader};
13use tokio::process::{Child, Command};
14use tokio::task::JoinHandle;
15
16pub mod auth;
17pub mod proto;
18
19#[cfg(windows)]
20mod windows;
21
22#[cfg(unix)]
23mod unix;
24
25pub use proto::emulator_controller_client::EmulatorControllerClient;
26use tonic::transport::Channel;
27
28use crate::auth::AuthProvider;
29
30#[doc = include_str!("../README.md")]
31#[cfg(doctest)]
32pub struct ReadmeDoctests;
33
34const EMULATOR_BIN: &str = const {
35    if cfg!(windows) {
36        "emulator.exe"
37    } else {
38        "emulator"
39    }
40};
41
42#[derive(Error, Debug)]
43pub enum EmulatorError {
44    #[error(
45        "Android SDK not found. Checked:\n  - ANDROID_HOME environment variable\n  - ANDROID_SDK_ROOT environment variable\n  - Platform default locations (e.g., ~/Android/sdk)\nPlease install the Android SDK or set ANDROID_HOME"
46    )]
47    AndroidHomeNotFound,
48
49    #[error("No emulator AVDs found")]
50    NoAvdsFound,
51
52    #[error("Failed to spawn or connect to ADB server: {0}")]
53    AdbError(String),
54
55    #[error("Android SDK emulator tool not found at path: {0}")]
56    EmulatorToolNotFound(String),
57
58    #[error("Invalid gRPC endpoint URI: {0}")]
59    InvalidUri(String),
60
61    #[error("Failed to enumerate running emulators: {0}")]
62    EnumerationFailed(String),
63
64    #[error("Failed to start emulator: {0}")]
65    EmulatorStartFailed(String),
66
67    #[error("Failed to kill emulator: {0}")]
68    EmulatorKillFailed(String),
69
70    #[error("Emulator connection timed out")]
71    ConnectionTimeout,
72
73    #[error("Authentication error: {0}")]
74    AuthError(#[from] crate::auth::AuthError),
75
76    #[error("gRPC connection error: {0}")]
77    GrpcError(#[from] tonic::transport::Error),
78
79    #[error("gRPC status error: {0}")]
80    GrpcStatus(#[from] tonic::Status),
81
82    #[error("IO error: {0}")]
83    IoError(#[from] std::io::Error),
84}
85
86pub type Result<T> = std::result::Result<T, EmulatorError>;
87
88/// Find a free gRPC port by querying running emulators via ADB
89///
90/// Returns the first available port starting from 8554, or None if unable to enumerate devices.
91async fn find_free_grpc_port() -> Option<u16> {
92    use adb_client::emulator::ADBEmulatorDevice;
93    use std::collections::HashSet;
94
95    let mut server = adb_server().await.ok()?;
96
97    tokio::task::spawn_blocking(move || {
98        let devices = match server.devices() {
99            Ok(d) => d,
100            Err(_) => return None,
101        };
102
103        // Collect all ports currently in use
104        let mut used_ports = HashSet::new();
105        for device in devices {
106            if device.identifier.starts_with("emulator-")
107                && let Ok(mut emulator_device) = ADBEmulatorDevice::new(device.identifier, None)
108                && let Ok(discovery_path) = emulator_device.avd_discovery_path()
109                && let Ok(ini_content) = std::fs::read_to_string(&discovery_path)
110            {
111                let metadata = parse_ini(&ini_content);
112                if let Some(port_str) = metadata.get("grpc.port")
113                    && let Ok(port) = port_str.parse::<u16>()
114                {
115                    used_ports.insert(port);
116                }
117            }
118        }
119
120        // Find first available port starting from 8554
121        (8554..8600).find(|&port| !used_ports.contains(&port))
122    })
123    .await
124    .ok()?
125}
126
127/// Parse and log an emulator output line based on its prefix
128fn log_emulator_line(line: &str) {
129    let trimmed = line.trim_start();
130
131    if let Some(rest) = trimmed.strip_prefix("ERROR ") {
132        tracing::error!("{}", rest.trim_start());
133    } else if let Some(rest) = trimmed.strip_prefix("WARNING ") {
134        tracing::warn!("{}", rest.trim_start());
135    } else if let Some(rest) = trimmed.strip_prefix("WARN ") {
136        tracing::warn!("{}", rest.trim_start());
137    } else if let Some(rest) = trimmed.strip_prefix("INFO ") {
138        tracing::info!("{}", rest.trim_start());
139    } else if let Some(rest) = trimmed.strip_prefix("DEBUG ") {
140        tracing::debug!("{}", rest.trim_start());
141    } else if let Some(rest) = trimmed.strip_prefix("TRACE ") {
142        tracing::trace!("{}", rest.trim_start());
143    } else {
144        // No recognized prefix, log as debug
145        tracing::debug!("{}", line);
146    }
147}
148
149/// gRPC authentication configuration for the emulator
150#[derive(Debug, Clone)]
151pub enum GrpcAuthConfig {
152    /// No authentication required
153    None,
154    /// Basic authentication using console auth token as bearer token
155    Basic,
156    /// JWKS + JWT authentication
157    Jwt {
158        /// Issuer identifier. If None, will be derived from the port as "emulator-{port}"
159        issuer: Option<String>,
160    },
161}
162
163impl Default for GrpcAuthConfig {
164    fn default() -> Self {
165        GrpcAuthConfig::Jwt { issuer: None }
166    }
167}
168
169/// Configuration for starting an Android emulator
170#[derive(Debug)]
171pub struct EmulatorConfig {
172    /// Name of the AVD to start. If None, will use the first available AVD.
173    avd_name: String,
174    /// gRPC port. If None, a free port will be automatically selected.
175    grpc_port: Option<u16>,
176    /// gRPC authentication configuration
177    grpc_auth: GrpcAuthConfig,
178    /// Whether to show the emulator window
179    no_window: bool,
180    /// Whether to load snapshots
181    no_snapshot_load: bool,
182    /// Whether to save snapshots
183    no_snapshot_save: bool,
184    /// Whether to show the boot animation
185    no_boot_anim: bool,
186    /// Whether to disable hardware acceleration (e.g., for running in CI without KVM)
187    no_acceleration: bool,
188    /// Whether to pass -dalvik-vm-checkjni flag to the emulator
189    dalvik_vm_check_jni: bool,
190    /// Allow running multiple instances of the same AVD (without support for snapshots)
191    read_only: bool,
192    /// Optionally quit the emulator after it has booted and been idle for the specified duration (for testing purposes)
193    quit_after_boot: Option<Duration>,
194    /// Additional command-line arguments
195    extra_args: Vec<String>,
196    /// Custom allowlist configuration for JWT authentication
197    /// If None, a default allowlist will be generated
198    grpc_allowlist: Option<auth::GrpcAllowlist>,
199    /// Stdout redirect configuration
200    stdout: Option<std::process::Stdio>,
201    /// Stderr redirect configuration
202    stderr: Option<std::process::Stdio>,
203}
204
205impl EmulatorConfig {
206    /// Create a new config with a specific AVD name
207    pub fn new(avd_name: impl Into<String>) -> Self {
208        Self {
209            avd_name: avd_name.into(),
210            grpc_port: None,
211            grpc_auth: GrpcAuthConfig::default(),
212            no_window: true,
213            no_snapshot_load: false,
214            no_snapshot_save: false,
215            no_boot_anim: false,
216            no_acceleration: false,
217            dalvik_vm_check_jni: false,
218            read_only: false,
219            quit_after_boot: None,
220            extra_args: Vec::new(),
221            grpc_allowlist: None,
222            stdout: None,
223            stderr: None,
224        }
225    }
226
227    /// Get the AVD name that was specified
228    pub fn avd_id(&self) -> &str {
229        &self.avd_name
230    }
231
232    /// Poll ADB until the emulator appears and we can read its metadata
233    async fn poll_for_emulator(
234        grpc_port: u16,
235    ) -> Result<(String, std::collections::HashMap<String, String>, PathBuf)> {
236        use adb_client::emulator::ADBEmulatorDevice;
237
238        let mut server = adb_server().await?;
239        tokio::task::spawn_blocking(move || {
240            loop {
241                std::thread::sleep(Duration::from_millis(500));
242
243                let devices = match server.devices() {
244                    Ok(d) => d,
245                    Err(_) => continue,
246                };
247
248                for device in devices {
249                    if !device.identifier.starts_with("emulator-") {
250                        continue;
251                    }
252
253                    let mut emulator_device =
254                        match ADBEmulatorDevice::new(device.identifier.clone(), None) {
255                            Ok(d) => d,
256                            Err(_) => continue,
257                        };
258
259                    let discovery_path = match emulator_device.avd_discovery_path() {
260                        Ok(p) => p,
261                        Err(_) => continue,
262                    };
263
264                    let ini_content = match std::fs::read_to_string(&discovery_path) {
265                        Ok(c) => c,
266                        Err(_) => continue,
267                    };
268
269                    let metadata = parse_ini(&ini_content);
270
271                    // Check if this is our emulator by matching the gRPC port
272                    if let Some(port_str) = metadata.get("grpc.port")
273                        && let Ok(found_port) = port_str.parse::<u16>()
274                        && found_port == grpc_port
275                    {
276                        return Ok((device.identifier, metadata, discovery_path));
277                    }
278                }
279            }
280        })
281        .await
282        .map_err(|e| EmulatorError::EmulatorStartFailed(format!("Task join error: {}", e)))?
283    }
284
285    /// Configure gRPC authentication
286    ///
287    /// # Examples
288    ///
289    /// No authentication:
290    /// ```no_run
291    /// use android_emulator::{EmulatorConfig, GrpcAuthConfig};
292    ///
293    /// let config = EmulatorConfig::new("test")
294    ///     .with_grpc_auth(GrpcAuthConfig::None);
295    /// ```
296    ///
297    /// Basic authentication (console token as bearer):
298    /// ```no_run
299    /// use android_emulator::{EmulatorConfig, GrpcAuthConfig};
300    ///
301    /// let config = EmulatorConfig::new("test")
302    ///     .with_grpc_auth(GrpcAuthConfig::Basic)
303    ///     .with_grpc_port(8554);
304    /// ```
305    ///
306    /// JWT mode with custom issuer:
307    /// ```no_run
308    /// use android_emulator::{EmulatorConfig, GrpcAuthConfig};
309    ///
310    /// let config = EmulatorConfig::new("test")
311    ///     .with_grpc_auth(GrpcAuthConfig::Jwt {
312    ///         issuer: Some("mytool".to_string()),
313    ///     })
314    ///     .with_grpc_port(8555);
315    /// ```
316    ///
317    /// JWT mode with auto-derived issuer and auto-selected port:
318    /// ```no_run
319    /// use android_emulator::{EmulatorConfig, GrpcAuthConfig};
320    ///
321    /// let config = EmulatorConfig::new("test")
322    ///     .with_grpc_auth(GrpcAuthConfig::Jwt {
323    ///         issuer: None,  // Will be "emulator-{port}"
324    ///     });
325    /// ```
326    pub fn with_grpc_auth(mut self, auth: GrpcAuthConfig) -> Self {
327        self.grpc_auth = auth;
328        self
329    }
330
331    /// Set the gRPC port
332    ///
333    /// If not specified, a free port will be automatically selected.
334    ///
335    /// # Example
336    ///
337    /// ```no_run
338    /// use android_emulator::EmulatorConfig;
339    ///
340    /// let config = EmulatorConfig::new("test")
341    ///     .with_grpc_port(8554);
342    /// ```
343    pub fn with_grpc_port(mut self, port: u16) -> Self {
344        self.grpc_port = Some(port);
345        self
346    }
347
348    /// Configure whether to show the emulator window
349    ///
350    /// Default is false (headless). Set to true to show the window.
351    pub fn with_window(mut self, show: bool) -> Self {
352        self.no_window = !show;
353        self
354    }
355
356    /// Configure whether to load snapshots
357    ///
358    /// Default is true (load snapshots). Set to false to disable snapshot
359    /// loading on startup.
360    pub fn with_snapshot_load(mut self, load: bool) -> Self {
361        self.no_snapshot_load = !load;
362        self
363    }
364
365    /// Configure whether to save snapshots on exit
366    ///
367    /// Default is true (save snapshots). Set to false to disable snapshot
368    /// saving on exit.
369    pub fn with_snapshot_save(mut self, save: bool) -> Self {
370        self.no_snapshot_save = !save;
371        self
372    }
373
374    /// Configure whether to show the boot animation
375    ///
376    /// Default is true (show boot animation). Set to false to disable the boot
377    /// animation for faster startup.
378    pub fn with_boot_animation(mut self, show: bool) -> Self {
379        self.no_boot_anim = !show;
380        self
381    }
382
383    /// Configure whether to disable hardware acceleration (e.g., for running in
384    /// CI without KVM)
385    ///
386    /// Default is false (use hardware acceleration if available). Set to true
387    /// to disable hardware acceleration
388    pub fn with_acceleration(mut self, enable: bool) -> Self {
389        self.no_acceleration = !enable;
390        self
391    }
392
393    /// Configure whether to pass -dalvik-vm-checkjni flag to the emulator
394    ///
395    /// This can be useful for testing JNI-related functionality and catching
396    /// errors early. Default is false (don't check JNI). Set to true to enable
397    /// JNI checking.
398    pub fn with_dalvik_vm_check_jni(mut self, enable: bool) -> Self {
399        self.dalvik_vm_check_jni = enable;
400        self
401    }
402
403    /// Configure whether to allow running multiple instances of the same AVD
404    /// (without support for snapshots)
405    ///
406    /// Default is false (don't allow multiple instances). Set to true to allow
407    /// multiple instances of the same AVD, but note that snapshots will not
408    /// work in this mode.
409    pub fn with_read_only(mut self, read_only: bool) -> Self {
410        self.read_only = read_only;
411        self
412    }
413
414    /// Configure the emulator to automatically quit after it has booted and
415    /// been idle for the specified duration (for testing purposes)
416    ///
417    /// This can be useful for testing emulator startup and shutdown in CI
418    /// environments.
419    ///
420    /// Default is None (don't quit automatically). Set to Some(duration) to
421    /// enable this behavior.
422    pub fn with_quit_after_boot(mut self, duration: Option<Duration>) -> Self {
423        self.quit_after_boot = duration;
424        self
425    }
426
427    pub fn with_extra_args(mut self, args: Vec<String>) -> Self {
428        self.extra_args = args;
429        self
430    }
431
432    /// Set a custom gRPC allowlist configuration for JWT authentication
433    ///
434    /// This allows fine-grained control over which gRPC methods are accessible
435    /// and under what conditions when using JWT authentication. The allowlist
436    /// defines three categories of methods:
437    ///
438    /// - **Unprotected**: Methods that can be invoked without any
439    ///   authentication token
440    /// - **Allowed**: Methods that can be called with a valid JWT token, even
441    ///   without an `aud` claim
442    /// - **Protected**: Methods that require the specific method to be present
443    ///   in the JWT token's `aud` claim
444    ///
445    /// If you don't specify a custom allowlist, a default one will be generated
446    /// using
447    /// [`GrpcAllowlist::default_for_issuer`](auth::GrpcAllowlist::default_for_issuer).
448    ///
449    /// # Arguments
450    ///
451    /// * `allowlist` - A [`GrpcAllowlist`](auth::GrpcAllowlist) configuration
452    ///
453    /// # Example
454    ///
455    /// ```no_run
456    /// use android_emulator::{EmulatorConfig, GrpcAuthConfig, auth::{GrpcAllowlist, AllowlistEntry}};
457    ///
458    /// let allowlist = GrpcAllowlist {
459    ///     unprotected: vec![],  // No methods accessible without auth
460    ///     allowlist: vec![
461    ///         AllowlistEntry {
462    ///             iss: "mytool".to_string(),
463    ///             allowed: vec![
464    ///                 "/android.emulation.control.EmulatorController/.*".to_string(),
465    ///             ],
466    ///             protected: vec![
467    ///                 "/android.emulation.control.SnapshotService/.*".to_string(),
468    ///             ],
469    ///         },
470    ///     ],
471    /// };
472    ///
473    /// let config = EmulatorConfig::new("test")
474    ///     .with_grpc_auth(GrpcAuthConfig::Jwt {
475    ///         issuer: Some("mytool".to_string()),
476    ///     })
477    ///     .with_grpc_allowlist(allowlist);
478    /// ```
479    ///
480    /// # See Also
481    ///
482    /// - [`with_grpc_auth`](Self::with_grpc_auth) - For configuring
483    ///   authentication mode
484    /// - [`GrpcAllowlist::default_for_issuer`](auth::GrpcAllowlist::default_for_issuer)
485    ///   - For the default allowlist
486    pub fn with_grpc_allowlist(mut self, allowlist: auth::GrpcAllowlist) -> Self {
487        self.grpc_allowlist = Some(allowlist);
488        self
489    }
490
491    /// Configure stdout for the emulator process
492    pub fn stdout<T: Into<std::process::Stdio>>(mut self, cfg: T) -> Self {
493        self.stdout = Some(cfg.into());
494        self
495    }
496
497    /// Configure stderr for the emulator process
498    pub fn stderr<T: Into<std::process::Stdio>>(mut self, cfg: T) -> Self {
499        self.stderr = Some(cfg.into());
500        self
501    }
502
503    /// Start an Android emulator with the given configuration
504    pub async fn spawn(self) -> Result<Emulator> {
505        let android_home = get_android_home().await?;
506        let emulator_path = android_home.join("emulator").join(EMULATOR_BIN);
507
508        if !tokio::fs::try_exists(&emulator_path).await.unwrap_or(false) {
509            return Err(EmulatorError::EmulatorToolNotFound(
510                emulator_path.display().to_string(),
511            ));
512        }
513
514        let mut cmd = Command::new(&emulator_path);
515        cmd.arg("-avd").arg(&self.avd_name);
516
517        if self.no_window {
518            cmd.arg("-no-window");
519        }
520
521        if self.no_snapshot_load {
522            cmd.arg("-no-snapshot-load");
523        }
524
525        if self.no_acceleration {
526            cmd.arg("-accel").arg("off");
527        }
528
529        if self.no_boot_anim {
530            cmd.arg("-no-boot-anim");
531        }
532
533        if self.dalvik_vm_check_jni {
534            cmd.arg("-dalvik-vm-checkjni");
535        }
536
537        if self.read_only {
538            cmd.arg("-read-only");
539        }
540
541        if let Some(quit_after) = self.quit_after_boot {
542            cmd.arg("-quit-after-boot")
543                .arg(quit_after.as_secs().to_string());
544        }
545
546        // Track whether we're using default piped stdout/stderr for IO forwarding
547        let use_default_stdout = self.stdout.is_none();
548        let use_default_stderr = self.stderr.is_none();
549
550        // Configure stdout (default to piped and run task to forward output)
551        if let Some(stdout) = self.stdout {
552            cmd.stdout(stdout);
553        } else {
554            cmd.stdout(std::process::Stdio::piped());
555        }
556
557        // Configure stderr (default to piped and run task to forward output)
558        if let Some(stderr) = self.stderr {
559            cmd.stderr(stderr);
560        } else {
561            cmd.stderr(std::process::Stdio::piped());
562        }
563
564        cmd.stdin(std::process::Stdio::null());
565
566        // Configure gRPC based on authentication mode
567        // All modes start with -grpc <port>
568        let grpc_port = match self.grpc_port {
569            Some(port) => port,
570            None => find_free_grpc_port().await.unwrap_or(8554),
571        };
572        cmd.arg("-grpc").arg(grpc_port.to_string());
573
574        let issuer = match self.grpc_auth {
575            GrpcAuthConfig::None => {
576                // No additional flags needed
577                None
578            }
579            GrpcAuthConfig::Basic => {
580                // Add -grpc-use-token for basic authentication
581                cmd.arg("-grpc-use-token");
582                None
583            }
584            GrpcAuthConfig::Jwt { issuer } => {
585                // Derive issuer if not provided
586                let issuer = issuer.unwrap_or_else(|| format!("emulator-{}", grpc_port));
587
588                // Create allowlist file
589                let allowlist = self
590                    .grpc_allowlist
591                    .unwrap_or_else(|| auth::GrpcAllowlist::default_for_issuer(&issuer));
592
593                // Write allowlist to temp file
594                let allowlist_json = serde_json::to_string_pretty(&allowlist).map_err(|e| {
595                    EmulatorError::EmulatorStartFailed(format!(
596                        "Failed to serialize allowlist: {}",
597                        e
598                    ))
599                })?;
600
601                let temp_dir = std::env::temp_dir();
602                let allowlist_path =
603                    temp_dir.join(format!("emulator-allowlist-{}.json", std::process::id()));
604                tokio::fs::write(&allowlist_path, allowlist_json).await?;
605
606                cmd.arg("-grpc-allowlist").arg(&allowlist_path);
607
608                // Add -grpc-use-jwt for JWT mode
609                cmd.arg("-grpc-use-jwt");
610
611                Some(issuer)
612            }
613        };
614
615        for arg in &self.extra_args {
616            cmd.arg(arg);
617        }
618
619        // On Windows, use a dedicated Job Object per emulator to ensure that when we
620        // kill the emulator, all child processes (like qemu) are also terminated.
621        // On Unix, use a process group for the same purpose.
622        #[cfg(windows)]
623        let (job, mut process) = crate::windows::EmulatorJob::spawn(cmd)
624            .map_err(|e| EmulatorError::EmulatorStartFailed(e.to_string()))?;
625
626        #[cfg(unix)]
627        let (process_group, mut process) = crate::unix::EmulatorProcessGroup::spawn(cmd)
628            .map_err(|e| EmulatorError::EmulatorStartFailed(e.to_string()))?;
629
630        #[cfg(not(any(windows, unix)))]
631        let mut process = cmd
632            .spawn()
633            .map_err(|e| EmulatorError::EmulatorStartFailed(e.to_string()))?;
634
635        // Create shutdown channel for IO forwarding tasks
636        let (shutdown_tx, shutdown_rx) = tokio::sync::watch::channel(false);
637
638        // Create IO forwarding task for stdout if piped (default behavior)
639        let stdout_task = if use_default_stdout {
640            let child_out = process.stdout.take().expect("stdout should be piped");
641            let mut shutdown_rx = shutdown_rx.clone();
642
643            let handle = tokio::spawn(async move {
644                tracing::info!("Stdout forwarding task started");
645                let reader = BufReader::new(child_out);
646                let mut lines = reader.lines();
647
648                loop {
649                    tokio::select! {
650                        res = shutdown_rx.wait_for(|v| *v) => {
651                            match res {
652                                Ok(_) => tracing::info!("Stdout forwarding task received shutdown signal"),
653                                Err(_) => tracing::info!("Stdout forwarding task: shutdown sender dropped"),
654                            }
655                            break;
656                        }
657                        result = lines.next_line() => {
658                            match result {
659                                Ok(Some(line)) => log_emulator_line(&line),
660                                Ok(None) => {
661                                    tracing::debug!("Stdout EOF reached");
662                                    break;
663                                }
664                                Err(e) => {
665                                    tracing::error!("Error reading stdout: {}", e);
666                                    return Err(e);
667                                }
668                            }
669                        }
670                    }
671                }
672
673                tracing::info!("Stdout forwarding task exiting");
674                Ok(())
675            });
676            Some(handle)
677        } else {
678            None
679        };
680
681        // Create IO forwarding task for stderr if piped (default behavior)
682        let stderr_task = if use_default_stderr {
683            let child_stderr = process.stderr.take().expect("stderr should be piped");
684            let mut shutdown_rx = shutdown_rx.clone();
685
686            let handle = tokio::spawn(async move {
687                tracing::info!("Stderr forwarding task started");
688                let reader = BufReader::new(child_stderr);
689                let mut lines = reader.lines();
690
691                loop {
692                    tokio::select! {
693                        res = shutdown_rx.wait_for(|v| *v) => {
694                            match res {
695                                Ok(_) => tracing::info!("Stderr forwarding task received shutdown signal"),
696                                Err(_) => tracing::info!("Stderr forwarding task: shutdown sender dropped"),
697                            }
698                            break;
699                        }
700                        result = lines.next_line() => {
701                            match result {
702                                Ok(Some(line)) => log_emulator_line(&line),
703                                Ok(None) => {
704                                    tracing::debug!("Stderr EOF reached");
705                                    break;
706                                }
707                                Err(e) => {
708                                    tracing::error!("Error reading stderr: {}", e);
709                                    return Err(e);
710                                }
711                            }
712                        }
713                    }
714                }
715
716                tracing::info!("Stderr forwarding task exiting");
717                Ok(())
718            });
719            Some(handle)
720        } else {
721            None
722        };
723
724        // Poll ADB until the emulator appears and we can read its metadata
725        let (serial, metadata, discovery_path) = Self::poll_for_emulator(grpc_port).await?;
726
727        let owned_process = OwnedProcess {
728            process,
729            #[cfg(windows)]
730            job: Some(job),
731            #[cfg(unix)]
732            process_group: Some(process_group),
733            stdout_task,
734            stderr_task,
735            shutdown_tx: Some(shutdown_tx),
736        };
737
738        Ok(Emulator {
739            owned_process: Some(tokio::sync::Mutex::new(Some(owned_process))),
740            grpc_port,
741            serial,
742            metadata,
743            discovery_path,
744            issuer,
745        })
746    }
747}
748
749/// High-level client for controlling an Android Emulator via gRPC
750///
751/// This is a wrapper around the generated `EmulatorControllerClient` that provides
752/// a more convenient API.
753pub struct EmulatorClient {
754    provider: auth::AuthProvider,
755    interceptor: EmulatorControllerClient<
756        tonic::service::interceptor::InterceptedService<Channel, auth::AuthProvider>,
757    >,
758    endpoint: String,
759}
760
761impl EmulatorClient {
762    /// Try to find an emulator running the specified AVD and connect to it
763    pub async fn connect_avd(avd: &str) -> Result<Self> {
764        let emulators = list_emulators().await?;
765        let matching = emulators
766            .into_iter()
767            .find(|e| e.avd_id().map(|id| id == avd).unwrap_or(false));
768        if let Some(emulator) = matching {
769            emulator.connect(Some(Duration::from_secs(30)), true).await
770        } else {
771            Err(EmulatorError::EmulatorStartFailed(
772                "No running emulator found".to_string(),
773            ))
774        }
775    }
776
777    /// Connect to an emulator at the specified endpoint without authentication
778    pub async fn connect(endpoint: impl Into<String>) -> Result<Self> {
779        let endpoint = endpoint.into();
780        let channel = Channel::from_shared(endpoint.clone())
781            .map_err(|e| EmulatorError::InvalidUri(e.to_string()))?
782            .connect()
783            .await?;
784
785        let provider = std::sync::Arc::new(auth::NoOpTokenProvider);
786        let provider = auth::AuthProvider::new_with_token_provider(provider);
787
788        Ok(Self {
789            interceptor: EmulatorControllerClient::with_interceptor(channel, provider.clone()),
790            provider,
791            endpoint,
792        })
793    }
794
795    /// Connect to an emulator with JWT authentication
796    pub async fn connect_with_auth(
797        endpoint: impl Into<String>,
798        provider: auth::AuthProvider,
799    ) -> Result<Self> {
800        let endpoint = endpoint.into();
801        let channel = Channel::from_shared(endpoint.clone())
802            .map_err(|e| EmulatorError::InvalidUri(e.to_string()))?
803            .connect()
804            .await?;
805
806        Ok(Self {
807            interceptor: EmulatorControllerClient::with_interceptor(channel, provider.clone()),
808            provider,
809            endpoint,
810        })
811    }
812
813    /// Get the authentication scheme used by this client
814    pub fn auth_scheme(&self) -> &auth::AuthScheme {
815        self.provider.auth_scheme()
816    }
817
818    /// Export a bearer token for the given audiences and TTL
819    ///
820    /// This can be used to export a token with specific, limited audience
821    /// claims in order to grant something limited access to the emulator.
822    ///
823    /// Each audience in `auds` will be included as an `aud` claim in the token
824    /// and should correspond to the gRPC method patterns defined in the
825    /// allowlist.
826    ///
827    /// This is only applicable when [`Self::auth_scheme()`] returns
828    /// [`auth::AuthScheme::Jwt`].
829    pub fn export_token(&self, auds: &[&str], ttl: Duration) -> Result<auth::BearerToken> {
830        let token = self.provider.export_token(auds, ttl)?;
831        Ok(token)
832    }
833
834    /// Get the endpoint this client is connected to
835    pub fn endpoint(&self) -> &str {
836        &self.endpoint
837    }
838
839    /// Get a mutable reference to the generated gRPC client protobuf binding
840    pub fn protocol_mut(
841        &mut self,
842    ) -> &mut EmulatorControllerClient<
843        tonic::service::interceptor::InterceptedService<Channel, auth::AuthProvider>,
844    > {
845        &mut self.interceptor
846    }
847
848    /// Get a reference to the generated gRPC client protobuf binding
849    pub fn protocol(
850        &self,
851    ) -> &EmulatorControllerClient<
852        tonic::service::interceptor::InterceptedService<Channel, auth::AuthProvider>,
853    > {
854        &self.interceptor
855    }
856
857    /// Wait until the emulator has fully booted
858    ///
859    /// This method polls the emulator's boot status until it reports as booted,
860    /// or until the specified timeout is reached.
861    ///
862    /// # Arguments
863    ///
864    /// * `timeout` - Maximum duration to wait for the emulator to boot
865    /// * `poll_interval` - Duration to wait between boot status checks (defaults to 2 seconds if None)
866    ///
867    /// # Returns
868    ///
869    /// Returns `Ok(Duration)` with the time elapsed until boot completed, or an error if the
870    /// timeout is reached or a gRPC error occurs.
871    ///
872    /// # Errors
873    ///
874    /// Returns [`EmulatorError::ConnectionTimeout`] if the emulator doesn't boot within the timeout period.
875    /// Returns [`EmulatorError::GrpcStatus`] if there's a gRPC communication error.
876    ///
877    /// # Example
878    ///
879    /// ```no_run
880    /// use android_emulator::EmulatorClient;
881    /// use std::time::Duration;
882    ///
883    /// # async fn example() -> Result<(), Box<dyn std::error::Error>> {
884    /// let mut client = EmulatorClient::connect("http://localhost:8554").await?;
885    ///
886    /// // Wait up to 5 minutes for boot, checking every 2 seconds
887    /// let elapsed = client.wait_until_booted(Duration::from_secs(300), None).await?;
888    /// println!("Emulator booted in {:.1} seconds", elapsed.as_secs_f64());
889    /// # Ok(())
890    /// # }
891    /// ```
892    pub async fn wait_until_booted(
893        &mut self,
894        timeout: Duration,
895        poll_interval: Option<Duration>,
896    ) -> Result<Duration> {
897        let poll_interval = poll_interval.unwrap_or(Duration::from_secs(2));
898        let start = std::time::Instant::now();
899        let mut attempt = 0;
900
901        loop {
902            attempt += 1;
903            let status = self.protocol_mut().get_status(()).await?.into_inner();
904
905            if status.booted {
906                let elapsed = start.elapsed();
907                tracing::info!(
908                    "Emulator fully booted after {:.1} seconds ({} attempts)",
909                    elapsed.as_secs_f64(),
910                    attempt
911                );
912                return Ok(elapsed);
913            }
914
915            tracing::debug!(
916                "Boot status: {} (attempt {}, elapsed: {:.1}s)",
917                status.booted,
918                attempt,
919                start.elapsed().as_secs_f64()
920            );
921
922            // Check if we've exceeded the timeout
923            if start.elapsed() >= timeout {
924                return Err(EmulatorError::ConnectionTimeout);
925            }
926
927            // Calculate remaining time and sleep for the minimum of poll_interval or remaining time
928            let remaining = timeout.saturating_sub(start.elapsed());
929            let sleep_duration = poll_interval.min(remaining);
930
931            if sleep_duration.is_zero() {
932                return Err(EmulatorError::ConnectionTimeout);
933            }
934
935            tokio::time::sleep(sleep_duration).await;
936        }
937    }
938
939    /// Request a graceful shutdown of the emulator via the gRPC protocol
940    ///
941    /// This method requests a graceful shutdown by setting the VM state to
942    /// `SHUTDOWN` and polls the VM state until it reaches a terminal state or
943    /// the timeout is reached.
944    ///
945    /// This is a protocol-level operation that does not explicitly kill the
946    /// emulator process, but a successful shutdown will typically result in the
947    /// emulator process exiting on its own.
948    ///
949    /// If you spawned the emulator then explicitly calling
950    /// `Emulator::kill()` or dropping the `Emulator` after a shutdown
951    /// request will kill the process if it hasn't already exited cleanly.
952    ///
953    /// This is the preferred way to stop an emulator as it allows the guest OS
954    /// to shut down cleanly, save snapshots, and perform proper cleanup.
955    ///
956    /// # Arguments
957    ///
958    /// * `timeout` - Maximum duration to wait for the VM to shut down (defaults
959    ///   to 30 seconds if None)
960    ///
961    /// # Returns
962    ///
963    /// Returns `Ok(())` if the shutdown request was successful and the VM shut
964    /// down, or an error if the operation fails.
965    ///
966    /// # Errors
967    ///
968    /// Returns an error if:
969    /// - Setting the VM state fails
970    /// - The shutdown timeout is exceeded
971    ///
972    /// # Example
973    ///
974    /// ```no_run
975    /// use android_emulator::{EmulatorConfig, EmulatorClient};
976    /// use std::time::Duration;
977    ///
978    /// # async fn example() -> Result<(), Box<dyn std::error::Error>> {
979    /// let config = EmulatorConfig::new("test");
980    /// let instance = config.spawn().await?;
981    /// let mut client = instance.connect(Some(Duration::from_secs(30)), true).await?;
982    ///
983    /// // ... use the emulator ...
984    ///
985    /// // Gracefully shutdown via protocol
986    /// client.shutdown(None).await?;
987    ///
988    /// // Clean up the process if we spawned it
989    /// instance.kill().await?;
990    /// # Ok(())
991    /// # }
992    /// ```
993    pub async fn shutdown(&mut self, timeout: Option<Duration>) -> Result<()> {
994        use crate::proto::{VmRunState, vm_run_state::RunState};
995
996        let timeout = timeout.unwrap_or(Duration::from_secs(30));
997        let poll_interval = Duration::from_millis(500);
998
999        tracing::info!("Requesting graceful emulator shutdown...");
1000
1001        // Request shutdown
1002        let shutdown_state = VmRunState {
1003            state: RunState::Shutdown as i32,
1004        };
1005
1006        self.protocol_mut()
1007            .set_vm_state(shutdown_state)
1008            .await
1009            .map_err(|e| {
1010                EmulatorError::EmulatorKillFailed(format!(
1011                    "Failed to set VM state to SHUTDOWN: {}",
1012                    e
1013                ))
1014            })?;
1015
1016        tracing::info!("Shutdown request sent, waiting for VM to shut down...");
1017
1018        // Poll VM state until shutdown completes or timeout
1019        let start = std::time::Instant::now();
1020        let mut last_state = None;
1021
1022        loop {
1023            match self.protocol_mut().get_vm_state(()).await {
1024                Ok(response) => {
1025                    let vm_state = response.into_inner();
1026                    let state = RunState::try_from(vm_state.state).unwrap_or(RunState::Unknown);
1027
1028                    if last_state != Some(state) {
1029                        tracing::debug!("VM state: {:?}", state);
1030                        last_state = Some(state);
1031                    }
1032
1033                    // Check if we've reached a terminal state (or if the connection failed,
1034                    // which likely means the VM has shut down)
1035                    match state {
1036                        RunState::Unknown => {
1037                            // Unknown state might indicate the emulator is shutting down
1038                            tracing::info!("VM entered unknown state, proceeding with termination");
1039                            break;
1040                        }
1041                        _ => {
1042                            // Continue polling
1043                        }
1044                    }
1045                }
1046                Err(e) => {
1047                    // Connection error likely means the emulator has shut down
1048                    tracing::info!(
1049                        "Lost connection to emulator ({}), assuming shutdown complete",
1050                        e
1051                    );
1052                    break;
1053                }
1054            }
1055
1056            // Check timeout
1057            if start.elapsed() >= timeout {
1058                tracing::warn!(
1059                    "Shutdown timeout reached after {:.1} seconds, forcing termination",
1060                    timeout.as_secs_f64()
1061                );
1062                break;
1063            }
1064
1065            // Calculate remaining time and sleep
1066            let remaining = timeout.saturating_sub(start.elapsed());
1067            let sleep_duration = poll_interval.min(remaining);
1068
1069            if sleep_duration.is_zero() {
1070                break;
1071            }
1072
1073            tokio::time::sleep(sleep_duration).await;
1074        }
1075
1076        tracing::info!("VM shutdown complete");
1077        Ok(())
1078    }
1079}
1080
1081/// Process state for an emulator instance that was spawned by this crate
1082///
1083/// This encapsulates all the state needed to manage a process we own,
1084/// including the process handle itself, any IO forwarding tasks, and
1085/// the shutdown channel to coordinate their termination.
1086#[derive(Debug)]
1087struct OwnedProcess {
1088    /// The spawned child process
1089    process: Child,
1090    /// Windows Job Object for killing the entire process tree (emulator.exe + qemu)
1091    #[cfg(windows)]
1092    job: Option<crate::windows::EmulatorJob>,
1093    /// Unix process group for killing the entire process tree (emulator + qemu)
1094    #[cfg(unix)]
1095    process_group: Option<crate::unix::EmulatorProcessGroup>,
1096    /// IO forwarding task for stdout (None if stdout was redirected)
1097    stdout_task: Option<JoinHandle<io::Result<()>>>,
1098    /// IO forwarding task for stderr (None if stderr was redirected)
1099    stderr_task: Option<JoinHandle<io::Result<()>>>,
1100    /// Shutdown channel sender for IO forwarding tasks (None if no IO tasks)
1101    shutdown_tx: Option<tokio::sync::watch::Sender<bool>>,
1102}
1103
1104/// Kill an owned process and clean up its resources
1105///
1106/// This is used by both `Emulator::kill()` and `Emulator::drop()` to ensure
1107/// consistent cleanup behavior.
1108async fn kill_owned_process(mut owned_process: OwnedProcess) -> Result<()> {
1109    let pid = owned_process.process.id();
1110
1111    // Kill the process (this will cause EOF on stdout/stderr pipes)
1112    if let Some(pid) = pid {
1113        tracing::info!("Terminating emulator process with PID {}", pid);
1114    }
1115
1116    // On Windows, use the job object to kill the entire process tree
1117    // This ensures that child processes like qemu are also terminated
1118    #[cfg(windows)]
1119    {
1120        if let Some(job) = &owned_process.job {
1121            if let Err(err) = job.kill() {
1122                tracing::error!("Failed to kill emulator job: {}", err);
1123                return Err(EmulatorError::EmulatorKillFailed(err.to_string()));
1124            }
1125        } else {
1126            // Fallback if no job (shouldn't happen for owned processes)
1127            if let Err(err) = owned_process.process.start_kill() {
1128                tracing::error!("Failed to kill emulator process: {}", err);
1129                return Err(EmulatorError::EmulatorKillFailed(err.to_string()));
1130            }
1131        }
1132    }
1133
1134    // On Unix, use the process group to kill the entire process tree
1135    // This ensures that child processes like qemu are also terminated
1136    #[cfg(unix)]
1137    {
1138        if let Some(process_group) = &owned_process.process_group {
1139            if let Err(err) = process_group.kill() {
1140                tracing::error!("Failed to kill emulator process group: {}", err);
1141                return Err(EmulatorError::EmulatorKillFailed(err.to_string()));
1142            }
1143        } else {
1144            // Fallback if no process group (shouldn't happen for owned processes)
1145            if let Err(err) = owned_process.process.start_kill() {
1146                tracing::error!("Failed to kill emulator process: {}", err);
1147                return Err(EmulatorError::EmulatorKillFailed(err.to_string()));
1148            }
1149        }
1150    }
1151
1152    // On other platforms, just kill the child directly
1153    #[cfg(not(any(windows, unix)))]
1154    if let Err(err) = owned_process.process.start_kill() {
1155        tracing::error!("Failed to kill emulator process: {}", err);
1156        return Err(EmulatorError::EmulatorKillFailed(err.to_string()));
1157    }
1158
1159    // We first signal to kill the emulator and wait for it to exit
1160    // before shutting down the IO tasks to ensure the emulator doesn't
1161    // get blocked trying to write to a synchronous pipe with no reader.
1162
1163    // Wait for the process to exit (we already called start_kill or job.kill above)
1164    let wait_res = match owned_process.process.wait().await {
1165        Ok(status) => {
1166            if let Some(pid) = pid {
1167                tracing::info!(
1168                    "Emulator process with PID {} has exited with status: {:?}",
1169                    pid,
1170                    status
1171                );
1172            }
1173            Ok(())
1174        }
1175        Err(err) => {
1176            tracing::error!("Failed to wait for emulator process to exit: {}", err);
1177            Err(EmulatorError::EmulatorKillFailed(err.to_string()))
1178        }
1179    };
1180
1181    // Send shutdown signal to IO forwarding tasks
1182    if let Some(tx) = &owned_process.shutdown_tx {
1183        tracing::info!("Sending shutdown signal to IO forwarding tasks");
1184        let _ = tx.send(true);
1185    }
1186
1187    // Join the IO tasks if they exist
1188    if let Some(stdout_task) = owned_process.stdout_task.take() {
1189        tracing::info!("Joining stdout forwarding task...");
1190        match stdout_task.await {
1191            Ok(Ok(())) => {
1192                tracing::info!("Stdout forwarding task completed successfully")
1193            }
1194            Ok(Err(e)) => {
1195                tracing::warn!("Stdout forwarding task completed with error: {}", e)
1196            }
1197            Err(e) => {
1198                if e.is_cancelled() {
1199                    tracing::debug!("Stdout forwarding task was cancelled");
1200                } else {
1201                    tracing::error!("Failed to join stdout forwarding task: {}", e);
1202                }
1203            }
1204        }
1205    }
1206
1207    if let Some(stderr_task) = owned_process.stderr_task.take() {
1208        tracing::info!("Joining stderr forwarding task...");
1209        match stderr_task.await {
1210            Ok(Ok(())) => {
1211                tracing::info!("Stderr forwarding task completed successfully")
1212            }
1213            Ok(Err(e)) => {
1214                tracing::warn!("Stderr forwarding task completed with error: {}", e)
1215            }
1216            Err(e) => {
1217                if e.is_cancelled() {
1218                    tracing::debug!("Stderr forwarding task was cancelled");
1219                } else {
1220                    tracing::error!("Failed to join stderr forwarding task: {}", e);
1221                }
1222            }
1223        }
1224    }
1225
1226    wait_res
1227}
1228
1229/// Handle to a running emulator instance
1230///
1231/// This can represent an emulator we spawned with an [`EmulatorConfig`], or an
1232/// already-running emulator we discovered with [`list_emulators`].
1233#[derive(Debug)]
1234pub struct Emulator {
1235    /// Process state if this emulator was spawned by this crate
1236    /// - `None` for discovered emulators (not owned)
1237    /// - `Some(Mutex(Some(_)))` for owned emulators with live process
1238    /// - `Some(Mutex(None))` for owned emulators where process has been killed
1239    owned_process: Option<tokio::sync::Mutex<Option<OwnedProcess>>>,
1240    serial: String,
1241    grpc_port: u16,
1242    /// Path to the discovery .ini file
1243    discovery_path: PathBuf,
1244    /// Runtime metadata from the emulator's discovery .ini file
1245    metadata: std::collections::HashMap<String, String>,
1246    /// Issuer identifier for JWT authentication (if spawned with custom issuer)
1247    issuer: Option<String>,
1248}
1249
1250impl Emulator {
1251    /// Get the serial number of this emulator (if known)
1252    pub fn serial(&self) -> &str {
1253        &self.serial
1254    }
1255
1256    /// Check if this instance represents an emulator we spawned
1257    pub fn is_owned(&self) -> bool {
1258        self.owned_process.is_some()
1259    }
1260
1261    pub fn discovery_path(&self) -> &Path {
1262        self.discovery_path.as_path()
1263    }
1264
1265    /// Get a reference to all emulator metadata from the runtime .ini file
1266    pub fn metadata(&self) -> &std::collections::HashMap<String, String> {
1267        &self.metadata
1268    }
1269
1270    /// Get a specific metadata property value
1271    pub fn get_metadata(&self, key: &str) -> Option<&str> {
1272        self.metadata.get(key).map(|s| s.as_str())
1273    }
1274
1275    /// Check if this emulator requires JWT authentication
1276    pub fn requires_jwt_auth(&self) -> bool {
1277        self.get_metadata("grpc.jwk_active").is_some()
1278    }
1279
1280    /// Get the AVD name
1281    pub fn avd_name(&self) -> Option<&str> {
1282        self.get_metadata("avd.name")
1283    }
1284
1285    /// Get the AVD ID
1286    pub fn avd_id(&self) -> Option<&str> {
1287        self.get_metadata("avd.id")
1288    }
1289
1290    /// Get the AVD directory path
1291    pub fn avd_dir(&self) -> Option<&str> {
1292        self.get_metadata("avd.dir")
1293    }
1294
1295    /// Get the emulator version
1296    pub fn emulator_version(&self) -> Option<&str> {
1297        self.get_metadata("emulator.version")
1298    }
1299
1300    /// Get the emulator build ID
1301    pub fn emulator_build(&self) -> Option<&str> {
1302        self.get_metadata("emulator.build")
1303    }
1304
1305    /// Get the serial port number
1306    pub fn port_serial(&self) -> Option<u16> {
1307        self.get_metadata("port.serial")?.parse().ok()
1308    }
1309
1310    /// Get the ADB port number
1311    pub fn port_adb(&self) -> Option<u16> {
1312        self.get_metadata("port.adb")?.parse().ok()
1313    }
1314
1315    /// Get the command line used to launch the emulator
1316    pub fn cmdline(&self) -> Option<&str> {
1317        self.get_metadata("cmdline")
1318    }
1319
1320    /// Get the gRPC endpoint URL for this emulator
1321    pub fn grpc_endpoint(&self) -> String {
1322        format!("http://localhost:{}", self.grpc_port)
1323    }
1324
1325    /// Get the gRPC port
1326    pub fn grpc_port(&self) -> u16 {
1327        self.grpc_port
1328    }
1329
1330    /// Connect to the emulator's gRPC controller
1331    ///
1332    /// This method will automatically retry the connection if it fails, waiting up to the
1333    /// specified timeout duration. At least one connection attempt is always made before
1334    /// checking the timeout.
1335    ///
1336    /// # Arguments
1337    ///
1338    /// * `timeout` - Maximum time to wait for the connection. If `None`, will wait indefinitely.
1339    /// * `allow_basic_auth` - If `true`, will use basic auth (grpc.token / console token) if available.
1340    ///
1341    /// # Errors
1342    ///
1343    /// Returns an error if JWT authentication setup fails.
1344    pub async fn connect(
1345        &self,
1346        timeout: Option<Duration>,
1347        allow_basic_auth: bool,
1348    ) -> Result<EmulatorClient> {
1349        // Check if this emulator requires JWT authentication
1350        let basic_auth_token = self.get_metadata("grpc.token");
1351
1352        if self.requires_jwt_auth() {
1353            tracing::info!(
1354                "Emulator requires JWT authentication, setting up ES256 token provider..."
1355            );
1356
1357            // Connect with JWT authentication
1358            match self.connect_with_jwt_auth(timeout).await {
1359                Ok(client) => {
1360                    tracing::info!("Connected to emulator with JWT authentication.");
1361                    return Ok(client);
1362                }
1363                Err(err) => {
1364                    tracing::error!("Failed to connect with JWT authentication: {}", err);
1365                    if basic_auth_token.is_some() && allow_basic_auth {
1366                        tracing::warn!("Falling back to basic authentication...");
1367                    } else {
1368                        return Err(err);
1369                    }
1370                }
1371            }
1372        } else {
1373            tracing::info!("Emulator does not require JWT authentication.");
1374        }
1375
1376        // Check if this emulator accepts basic auth (grpc.token)
1377        // This has a lower priority than JWT auth because its mostly only intended for Android Studio use
1378        if allow_basic_auth && let Some(token) = basic_auth_token {
1379            tracing::info!("Emulator accepts basic auth, setting up BasicAuthTokenProvider...");
1380            return self.connect_with_basic_auth(token, timeout).await;
1381        }
1382
1383        // Connect without authentication (use no-op provider)
1384        self.connect_with_noop_auth(timeout).await
1385    }
1386
1387    /// Connect with no-op authentication (internal helper for unauth connections)
1388    async fn connect_with_noop_auth(&self, timeout: Option<Duration>) -> Result<EmulatorClient> {
1389        let start = std::time::Instant::now();
1390
1391        let provider = Arc::new(auth::NoOpTokenProvider);
1392        let provider = AuthProvider::new_with_token_provider(provider);
1393        loop {
1394            match EmulatorClient::connect_with_auth(self.grpc_endpoint(), provider.clone()).await {
1395                Ok(mut client) => {
1396                    // Try a simple call to verify the connection
1397                    if client.protocol_mut().get_status(()).await.is_ok() {
1398                        return Ok(client);
1399                    }
1400                }
1401                Err(err) => {
1402                    tracing::error!("No-auth connection attempt failed: {}", err);
1403                }
1404            }
1405
1406            // Check timeout after the connection attempt
1407            if let Some(timeout_duration) = timeout
1408                && start.elapsed() > timeout_duration
1409            {
1410                return Err(EmulatorError::ConnectionTimeout);
1411            }
1412
1413            tokio::time::sleep(Duration::from_secs(1)).await;
1414        }
1415    }
1416
1417    /// Connect with basic auth token (internal helper for token-based connections)
1418    async fn connect_with_basic_auth(
1419        &self,
1420        token: &str,
1421        timeout: Option<Duration>,
1422    ) -> Result<EmulatorClient> {
1423        let start = std::time::Instant::now();
1424
1425        let provider = Arc::new(auth::BearerTokenProvider::new(token.to_string()));
1426        let provider = AuthProvider::new_with_token_provider(provider);
1427
1428        loop {
1429            match EmulatorClient::connect_with_auth(self.grpc_endpoint(), provider.clone()).await {
1430                Ok(mut client) => {
1431                    // Try a simple call to verify the connection
1432                    if client.protocol_mut().get_status(()).await.is_ok() {
1433                        return Ok(client);
1434                    }
1435                }
1436                Err(err) => {
1437                    tracing::error!("Basic auth connection attempt failed: {}", err);
1438                }
1439            }
1440
1441            // Check timeout after the connection attempt
1442            if let Some(timeout_duration) = timeout
1443                && start.elapsed() > timeout_duration
1444            {
1445                return Err(EmulatorError::ConnectionTimeout);
1446            }
1447
1448            tokio::time::sleep(Duration::from_secs(1)).await;
1449        }
1450    }
1451
1452    /// Connect with ES256 authentication (internal helper for JWT connections)
1453    ///
1454    /// This will verify the JWT connection works, then return an unauthenticated client
1455    /// (since after JWT activation, the emulator will accept unauthenticated connections too)
1456    async fn connect_with_jwt_auth(&self, timeout: Option<Duration>) -> Result<EmulatorClient> {
1457        // Get the JWKS directory from metadata
1458        let jwks_path = self.get_metadata("grpc.jwks").ok_or_else(|| {
1459            EmulatorError::EmulatorStartFailed(
1460                "Emulator requires JWT auth but grpc.jwks path not found in metadata".to_string(),
1461            )
1462        })?;
1463
1464        let jwks_dir = PathBuf::from(jwks_path);
1465
1466        let issuer = self
1467            .issuer
1468            .as_deref()
1469            .unwrap_or("android-studio")
1470            .to_string();
1471
1472        // Generate and register key
1473        let jwt_provider = tokio::task::spawn_blocking(
1474            move || -> std::result::Result<_, crate::auth::AuthError> {
1475                tracing::info!(
1476                    "Generating and registering JWT token provider with issuer '{}'",
1477                    issuer
1478                );
1479                let provider = auth::JwtTokenProvider::new_and_register(&jwks_dir, issuer)?;
1480
1481                tracing::info!("JWT token provider registered, waiting for activation...");
1482                // Wait for activation (30 second timeout)
1483                provider.wait_for_activation(&jwks_dir, Duration::from_secs(10))?;
1484
1485                let provider = AuthProvider::new_with_token_provider(provider);
1486
1487                Ok(provider)
1488            },
1489        )
1490        .await
1491        .map_err(|err| {
1492            EmulatorError::EmulatorStartFailed(format!(
1493                "Failure running task to register JWT token provider: {err}"
1494            ))
1495        })??;
1496
1497        let start = std::time::Instant::now();
1498
1499        loop {
1500            tracing::info!("Attempting JWT connection...");
1501            match EmulatorClient::connect_with_auth(self.grpc_endpoint(), jwt_provider.clone())
1502                .await
1503            {
1504                Ok(mut client) => {
1505                    tracing::info!("JWT authentication successful.");
1506                    // Try a simple call to verify the JWT connection works
1507                    match client.protocol_mut().get_status(()).await {
1508                        Ok(_) => {
1509                            tracing::info!(
1510                                "Successfully connected to emulator with JWT authentication."
1511                            );
1512                            return Ok(client);
1513                        }
1514                        Err(err) => {
1515                            tracing::error!(
1516                                "Failed to get status with JWT authentication: {}",
1517                                err
1518                            );
1519                        }
1520                    }
1521                }
1522                Err(err) => {
1523                    tracing::error!("JWT connection attempt failed: {}", err);
1524                }
1525            }
1526
1527            // Check timeout after the connection attempt
1528            if let Some(timeout_duration) = timeout
1529                && start.elapsed() > timeout_duration
1530            {
1531                return Err(EmulatorError::ConnectionTimeout);
1532            }
1533            tracing::info!("Sleeping before retrying JWT connection...");
1534            tokio::time::sleep(Duration::from_secs(1)).await;
1535        }
1536    }
1537
1538    /// Kill the emulator process and wait for it to fully exit
1539    ///
1540    /// If this instance owns the process (spawned via `spawn()`), this will kill the process
1541    /// and wait for it to exit.
1542    ///
1543    /// If this instance was discovered via `find()`, this returns an error as we don't own
1544    /// the process.
1545    ///
1546    /// To kill a discovered emulator, use ADB commands directly.
1547    pub async fn kill(&self) -> Result<()> {
1548        // Check if this emulator is owned
1549        let Some(mutex) = &self.owned_process else {
1550            tracing::warn!("kill() called on an emulator that is not owned by this instance");
1551            return Ok(());
1552        };
1553
1554        // Take ownership of the process state
1555        let owned = mutex.lock().await.take();
1556
1557        if let Some(owned_process) = owned {
1558            kill_owned_process(owned_process).await?;
1559            tracing::info!("Emulator killed successfully");
1560            Ok(())
1561        } else {
1562            tracing::warn!("kill() called but process was already killed");
1563            Ok(())
1564        }
1565    }
1566}
1567
1568impl Drop for Emulator {
1569    fn drop(&mut self) {
1570        // We have exclusive ownership (&mut self), so we can use get_mut() directly
1571        // without needing to lock, which avoids panics in async contexts
1572
1573        if let Some(mutex) = &mut self.owned_process
1574            && let Some(owned_process) = mutex.get_mut().take()
1575        {
1576            // Spawn a background task to kill the process asynchronously
1577            // This ensures proper cleanup even when dropping outside an async context
1578            tokio::task::spawn(async move {
1579                if let Err(e) = kill_owned_process(owned_process).await {
1580                    tracing::error!("Failed to kill emulator in Drop: {}", e);
1581                }
1582            });
1583        }
1584    }
1585}
1586
1587/// Get the Android SDK home directory
1588///
1589/// This function tries to find the Android SDK in the following order:
1590/// 1. ANDROID_HOME environment variable
1591/// 2. ANDROID_SDK_ROOT environment variable
1592/// 3. Platform-specific default locations:
1593///    - Linux: $HOME/Android/sdk or $HOME/Android/Sdk
1594///    - macOS: $HOME/Library/Android/sdk
1595///    - Windows: %LOCALAPPDATA%/Android/Sdk
1596pub async fn get_android_home() -> Result<PathBuf> {
1597    // Try environment variables first
1598    if let Ok(path) = std::env::var("ANDROID_HOME") {
1599        return Ok(PathBuf::from(path));
1600    }
1601
1602    if let Ok(path) = std::env::var("ANDROID_SDK_ROOT") {
1603        return Ok(PathBuf::from(path));
1604    }
1605
1606    // Try platform-specific default locations
1607    #[cfg(target_os = "linux")]
1608    {
1609        if let Some(home) = dirs::home_dir() {
1610            // Try $HOME/Android/sdk first (lowercase)
1611            let sdk_path = home.join("Android").join("sdk");
1612            if tokio::fs::try_exists(&sdk_path).await.unwrap_or(false) {
1613                return Ok(sdk_path);
1614            }
1615
1616            // Try $HOME/Android/Sdk (capitalized)
1617            let sdk_path = home.join("Android").join("Sdk");
1618            if tokio::fs::try_exists(&sdk_path).await.unwrap_or(false) {
1619                return Ok(sdk_path);
1620            }
1621        }
1622    }
1623
1624    #[cfg(target_os = "macos")]
1625    {
1626        if let Some(home) = dirs::home_dir() {
1627            let sdk_path = home.join("Library").join("Android").join("sdk");
1628            if tokio::fs::try_exists(&sdk_path).await.unwrap_or(false) {
1629                return Ok(sdk_path);
1630            }
1631        }
1632    }
1633
1634    #[cfg(target_os = "windows")]
1635    {
1636        if let Some(local_data) = dirs::data_local_dir() {
1637            let sdk_path = local_data.join("Android").join("Sdk");
1638            if tokio::fs::try_exists(&sdk_path).await.unwrap_or(false) {
1639                return Ok(sdk_path);
1640            }
1641        }
1642    }
1643
1644    Err(EmulatorError::AndroidHomeNotFound)
1645}
1646
1647/// Find available Android Virtual Devices (AVDs)
1648pub async fn list_avds() -> Result<Vec<String>> {
1649    let android_home = get_android_home().await?;
1650
1651    tokio::task::spawn_blocking(move || {
1652        let emulator_path = android_home.join("emulator").join(EMULATOR_BIN);
1653
1654        if !emulator_path.exists() {
1655            return Err(EmulatorError::EmulatorToolNotFound(
1656                emulator_path.display().to_string(),
1657            ));
1658        }
1659
1660        let output = std::process::Command::new(&emulator_path)
1661            .arg("-list-avds")
1662            .output()?;
1663
1664        let avds: Vec<String> = String::from_utf8_lossy(&output.stdout)
1665            .lines()
1666            .map(|s| s.trim().to_string())
1667            .filter(|s| !s.is_empty())
1668            .collect();
1669
1670        if avds.is_empty() {
1671            Err(EmulatorError::NoAvdsFound)
1672        } else {
1673            Ok(avds)
1674        }
1675    })
1676    .await
1677    .map_err(|e| EmulatorError::EmulatorStartFailed(format!("Task join error: {}", e)))?
1678}
1679
1680/// Parse a simple INI-style file with key=value pairs
1681fn parse_ini(content: &str) -> std::collections::HashMap<String, String> {
1682    content
1683        .lines()
1684        .filter_map(|line| {
1685            let line = line.trim();
1686            if line.is_empty() || line.starts_with('#') {
1687                return None;
1688            }
1689            line.split_once('=')
1690                .map(|(k, v)| (k.trim().to_string(), v.trim().to_string()))
1691        })
1692        .collect()
1693}
1694
1695async fn adb_server() -> Result<adb_client::server::ADBServer> {
1696    use adb_client::server::ADBServer;
1697
1698    let android_home = get_android_home().await?;
1699    let adb_path = android_home.join("platform-tools").join("adb");
1700    let adb_path: String = adb_path
1701        .to_str()
1702        .ok_or_else(|| EmulatorError::AdbError("Invalid Android home path".to_string()))?
1703        .to_string();
1704    let addr = SocketAddrV4::new(Ipv4Addr::LOCALHOST, 5037);
1705
1706    tokio::task::spawn_blocking(move || Ok(ADBServer::new_from_path(addr, Some(adb_path))))
1707        .await
1708        .map_err(|e| EmulatorError::AdbError(format!("Task join error: {}", e)))?
1709}
1710
1711/// Enumerates all running emulators that are discoverable via ADB and
1712/// returns their metadata
1713///
1714/// This is useful for finding emulators that were launched outside of this
1715/// library, such as those launched by Android Studio. It reads the emulator
1716/// metadata from the discovery path .ini files, which include the gRPC port
1717/// and authentication settings.
1718///
1719/// This does not connect to any emulators; use `connect()` on one of the
1720/// returned instance to establish a gRPC connection.
1721pub async fn list_emulators() -> Result<Vec<Emulator>> {
1722    use adb_client::emulator::ADBEmulatorDevice;
1723
1724    let mut server = adb_server().await?;
1725
1726    tokio::task::spawn_blocking(move || {
1727        let mut emulators = vec![];
1728
1729        let devices = server.devices().map_err(|e| {
1730            EmulatorError::IoError(std::io::Error::other(format!(
1731                "Failed to list ADB devices: {}",
1732                e
1733            )))
1734        })?;
1735
1736        // Find emulator devices that don't require JWT authentication
1737        for device in devices {
1738            if device.identifier.starts_with("emulator-") {
1739                // Convert to ADBEmulatorDevice to access emulator-specific methods
1740                let mut emulator_device = ADBEmulatorDevice::new(device.identifier.clone(), None)
1741                    .map_err(|e| {
1742                    EmulatorError::IoError(std::io::Error::other(format!(
1743                        "Failed to create ADBEmulatorDevice: {}",
1744                        e
1745                    )))
1746                })?;
1747
1748                // Get the discovery path for the runtime metadata
1749                if let Ok(discovery_path) = emulator_device.avd_discovery_path()
1750                    && let Ok(ini_content) = std::fs::read_to_string(&discovery_path)
1751                {
1752                    let metadata = parse_ini(&ini_content);
1753
1754                    if let Some(port_str) = metadata.get("grpc.port")
1755                        && let Ok(grpc_port) = port_str.parse::<u16>()
1756                    {
1757                        emulators.push(Emulator {
1758                            owned_process: None,
1759                            grpc_port,
1760                            serial: device.identifier.clone(),
1761                            metadata,
1762                            discovery_path: discovery_path.clone(),
1763                            issuer: None,
1764                        });
1765                    }
1766                }
1767            }
1768        }
1769
1770        Ok(emulators)
1771    })
1772    .await
1773    .map_err(|e| EmulatorError::EnumerationFailed(format!("Task join error: {}", e)))?
1774}
1775
1776/// Try to connect to an emulator, or start a new one if none is running
1777///
1778/// This function attempts to connect to the first emulator it can find
1779/// that's running the same AVD associated with the provided configuration,
1780/// but if no running emulator is found, it will start a new one with the provided
1781/// configuration.
1782///
1783/// Returns both the connected client and an optional `Emulator` instance
1784/// representing any newly spawned emulator (which will be `None` if we
1785/// connected to an existing emulator).
1786pub async fn connect_or_start_emulator(
1787    config: EmulatorConfig,
1788) -> Result<(EmulatorClient, Option<Emulator>)> {
1789    // Try to connect to existing emulator first
1790    if let Ok(client) = EmulatorClient::connect_avd(config.avd_id()).await {
1791        tracing::info!("Connected to existing emulator");
1792        return Ok((client, None));
1793    }
1794
1795    tracing::info!("No existing emulator found, starting new one...");
1796    // Start a new emulator
1797    let instance = config.spawn().await?;
1798    tracing::info!("Emulator started at: {}", instance.grpc_endpoint());
1799
1800    // Wait for it to be ready and connect
1801    tracing::info!("Waiting for emulator to be ready...");
1802    let client = instance
1803        .connect(Some(Duration::from_secs(120)), true)
1804        .await?;
1805    tracing::info!("Connected to new emulator");
1806
1807    Ok((client, Some(instance)))
1808}