Skip to main content

maa_framework/
controller.rs

1//! Device controller for input, screen capture, and app management.
2
3use crate::{MaaError, MaaResult, common, sys};
4use std::collections::HashMap;
5use std::ffi::CString;
6use std::os::raw::c_void;
7use std::ptr::NonNull;
8use std::sync::{Arc, Mutex};
9
10/// Device controller interface.
11///
12/// Handles interaction with the target device, including:
13/// - Input events (click, swipe, key press)
14/// - Screen capture
15/// - App management (start/stop)
16/// - Connection management
17///
18/// See also: [`AdbControllerBuilder`] for advanced ADB configuration.
19#[derive(Clone)]
20pub struct Controller {
21    inner: Arc<ControllerInner>,
22}
23
24struct ControllerInner {
25    handle: NonNull<sys::MaaController>,
26    owns_handle: bool,
27    callbacks: Mutex<HashMap<sys::MaaSinkId, usize>>,
28    event_sinks: Mutex<HashMap<sys::MaaSinkId, usize>>,
29}
30
31unsafe impl Send for ControllerInner {}
32unsafe impl Sync for ControllerInner {}
33
34// Controller is Send/Sync because it holds Arc<ControllerInner> which is Send/Sync
35unsafe impl Send for Controller {}
36unsafe impl Sync for Controller {}
37
38impl std::fmt::Debug for Controller {
39    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
40        f.debug_struct("Controller")
41            .field("handle", &self.inner.handle)
42            .finish()
43    }
44}
45
46impl Controller {
47    /// Create a new ADB controller for Android device control.
48    ///
49    /// # Arguments
50    /// * `adb_path` - Path to the ADB executable
51    /// * `address` - Device address (e.g., "127.0.0.1:5555" or "emulator-5554")
52    /// * `config` - JSON configuration string for advanced options
53    /// * `agent_path` - Path to MaaAgent binary; pass `""` to use current directory (may return `Err` if resolution fails).
54    #[cfg(feature = "adb")]
55    pub fn new_adb(
56        adb_path: &str,
57        address: &str,
58        config: &str,
59        agent_path: &str,
60    ) -> MaaResult<Self> {
61        Self::create_adb(
62            adb_path,
63            address,
64            sys::MaaAdbScreencapMethod_Default as sys::MaaAdbScreencapMethod,
65            sys::MaaAdbInputMethod_Default as sys::MaaAdbInputMethod,
66            config,
67            agent_path,
68        )
69    }
70
71    /// Resolves to the current directory if the string is empty; otherwise, uses it as-is. Returns Err if parsing fails.
72    #[cfg(feature = "adb")]
73    fn resolve_agent_path(agent_path: &str) -> MaaResult<String> {
74        if !agent_path.is_empty() {
75            return Ok(agent_path.to_string());
76        }
77        let cur = std::env::current_dir().map_err(|e| {
78            MaaError::InvalidArgument(format!("agent_path empty and current_dir failed: {}", e))
79        })?;
80        let s = cur.to_str().ok_or_else(|| {
81            MaaError::InvalidArgument(
82                "agent_path empty and current directory is not valid UTF-8".to_string(),
83            )
84        })?;
85        Ok(s.to_string())
86    }
87
88    #[cfg(feature = "adb")]
89    pub(crate) fn create_adb(
90        adb_path: &str,
91        address: &str,
92        screencap_method: sys::MaaAdbScreencapMethod,
93        input_method: sys::MaaAdbInputMethod,
94        config: &str,
95        agent_path: &str,
96    ) -> MaaResult<Self> {
97        let path = Self::resolve_agent_path(agent_path)?;
98        let c_adb = CString::new(adb_path)?;
99        let c_addr = CString::new(address)?;
100        let c_cfg = CString::new(config)?;
101        let c_agent = CString::new(path.as_str())?;
102
103        let handle = unsafe {
104            sys::MaaAdbControllerCreate(
105                c_adb.as_ptr(),
106                c_addr.as_ptr(),
107                screencap_method,
108                input_method,
109                c_cfg.as_ptr(),
110                c_agent.as_ptr(),
111            )
112        };
113
114        if let Some(ptr) = NonNull::new(handle) {
115            Ok(Self {
116                inner: Arc::new(ControllerInner {
117                    handle: ptr,
118                    owns_handle: true,
119                    callbacks: Mutex::new(HashMap::new()),
120                    event_sinks: Mutex::new(HashMap::new()),
121                }),
122            })
123        } else {
124            Err(MaaError::FrameworkError(-1))
125        }
126    }
127
128    /// Create a new Win32 controller for Windows window control.
129    #[cfg(feature = "win32")]
130    pub fn new_win32(
131        hwnd: *mut c_void,
132        screencap_method: sys::MaaWin32ScreencapMethod,
133        mouse_method: sys::MaaWin32InputMethod,
134        keyboard_method: sys::MaaWin32InputMethod,
135    ) -> MaaResult<Self> {
136        let handle = unsafe {
137            sys::MaaWin32ControllerCreate(hwnd, screencap_method, mouse_method, keyboard_method)
138        };
139
140        Self::from_handle(handle)
141    }
142
143    /// Create a new PlayCover controller for iOS app control on macOS.
144
145    pub fn new_playcover(address: &str, uuid: &str) -> MaaResult<Self> {
146        let c_addr = CString::new(address)?;
147        let c_uuid = CString::new(uuid)?;
148        let handle = unsafe { sys::MaaPlayCoverControllerCreate(c_addr.as_ptr(), c_uuid.as_ptr()) };
149
150        Self::from_handle(handle)
151    }
152
153    /// Create a new WlRoots controller for apps running in wlroots compositor on Linux.
154    ///
155    /// # Arguments
156    /// * `wlr_socket_path` - Wayland socket path
157    pub fn new_wlroots(wlr_socket_path: &str) -> MaaResult<Self> {
158        let c_path = CString::new(wlr_socket_path)?;
159        let handle = unsafe { sys::MaaWlRootsControllerCreate(c_path.as_ptr()) };
160
161        Self::from_handle(handle)
162    }
163
164    /// Create a custom controller with user-defined callbacks.
165    #[cfg(feature = "custom")]
166    pub fn new_custom<T: crate::custom_controller::CustomControllerCallback + 'static>(
167        callback: T,
168    ) -> MaaResult<Self> {
169        let boxed: Box<Box<dyn crate::custom_controller::CustomControllerCallback>> =
170            Box::new(Box::new(callback));
171        let cb_ptr = Box::into_raw(boxed) as *mut c_void;
172        let callbacks = crate::custom_controller::get_callbacks();
173        let handle =
174            unsafe { sys::MaaCustomControllerCreate(callbacks as *const _ as *mut _, cb_ptr) };
175
176        NonNull::new(handle)
177            .map(|ptr| Self {
178                inner: Arc::new(ControllerInner {
179                    handle: ptr,
180                    owns_handle: true,
181                    callbacks: Mutex::new(HashMap::new()),
182                    event_sinks: Mutex::new(HashMap::new()),
183                }),
184            })
185            .ok_or_else(|| {
186                unsafe {
187                    let _ = Box::from_raw(
188                        cb_ptr as *mut Box<dyn crate::custom_controller::CustomControllerCallback>,
189                    );
190                }
191                MaaError::FrameworkError(-1)
192            })
193    }
194
195    /// Helper to create controller from raw handle.
196    fn from_handle(handle: *mut sys::MaaController) -> MaaResult<Self> {
197        if let Some(ptr) = NonNull::new(handle) {
198            Ok(Self {
199                inner: Arc::new(ControllerInner {
200                    handle: ptr,
201                    owns_handle: true,
202                    callbacks: Mutex::new(HashMap::new()),
203                    event_sinks: Mutex::new(HashMap::new()),
204                }),
205            })
206        } else {
207            Err(MaaError::FrameworkError(-1))
208        }
209    }
210
211    /// Post a click action at the specified coordinates.
212    pub fn post_click(&self, x: i32, y: i32) -> MaaResult<common::MaaId> {
213        let id = unsafe { sys::MaaControllerPostClick(self.inner.handle.as_ptr(), x, y) };
214        Ok(id)
215    }
216
217    /// Post a screenshot capture request.
218    pub fn post_screencap(&self) -> MaaResult<common::MaaId> {
219        let id = unsafe { sys::MaaControllerPostScreencap(self.inner.handle.as_ptr()) };
220        Ok(id)
221    }
222
223    /// Post a click action with contact and pressure parameters.
224    ///
225    /// # Arguments
226    /// * `x`, `y` - Click coordinates
227    /// * `contact` - Contact/finger index (for multi-touch)
228    /// * `pressure` - Touch pressure (1 = normal)
229    pub fn post_click_v2(
230        &self,
231        x: i32,
232        y: i32,
233        contact: i32,
234        pressure: i32,
235    ) -> MaaResult<common::MaaId> {
236        let id = unsafe {
237            sys::MaaControllerPostClickV2(self.inner.handle.as_ptr(), x, y, contact, pressure)
238        };
239        Ok(id)
240    }
241
242    /// Post a swipe action from one point to another.
243    ///
244    /// # Arguments
245    /// * `x1`, `y1` - Start coordinates
246    /// * `x2`, `y2` - End coordinates
247    /// * `duration` - Swipe duration in milliseconds
248    pub fn post_swipe(
249        &self,
250        x1: i32,
251        y1: i32,
252        x2: i32,
253        y2: i32,
254        duration: i32,
255    ) -> MaaResult<common::MaaId> {
256        let id = unsafe {
257            sys::MaaControllerPostSwipe(self.inner.handle.as_ptr(), x1, y1, x2, y2, duration)
258        };
259        Ok(id)
260    }
261
262    /// Post a key click action.
263    ///
264    /// # Arguments
265    /// * `keycode` - Virtual key code (ADB keycode for Android, VK for Win32)
266    pub fn post_click_key(&self, keycode: i32) -> MaaResult<common::MaaId> {
267        let id = unsafe { sys::MaaControllerPostClickKey(self.inner.handle.as_ptr(), keycode) };
268        Ok(id)
269    }
270
271    /// Alias for [`post_click_key`](Self::post_click_key).
272    #[deprecated(note = "Use post_click_key instead")]
273    pub fn post_press(&self, keycode: i32) -> MaaResult<common::MaaId> {
274        self.post_click_key(keycode)
275    }
276
277    /// Post a text input action.
278    ///
279    /// # Arguments
280    /// * `text` - Text to input
281    pub fn post_input_text(&self, text: &str) -> MaaResult<common::MaaId> {
282        let c_text = CString::new(text)?;
283        let id =
284            unsafe { sys::MaaControllerPostInputText(self.inner.handle.as_ptr(), c_text.as_ptr()) };
285        Ok(id)
286    }
287
288    /// Post a shell command execution (ADB only).
289    ///
290    /// # Arguments
291    /// * `cmd` - Shell command to execute
292    /// * `timeout` - Timeout in milliseconds
293    pub fn post_shell(&self, cmd: &str, timeout: i64) -> MaaResult<common::MaaId> {
294        let c_cmd = CString::new(cmd)?;
295        let id = unsafe {
296            sys::MaaControllerPostShell(self.inner.handle.as_ptr(), c_cmd.as_ptr(), timeout)
297        };
298        Ok(id)
299    }
300
301    /// Post a touch down event.
302    ///
303    /// # Arguments
304    /// * `contact` - Contact/finger index
305    /// * `x`, `y` - Touch coordinates
306    /// * `pressure` - Touch pressure
307    pub fn post_touch_down(
308        &self,
309        contact: i32,
310        x: i32,
311        y: i32,
312        pressure: i32,
313    ) -> MaaResult<common::MaaId> {
314        let id = unsafe {
315            sys::MaaControllerPostTouchDown(self.inner.handle.as_ptr(), contact, x, y, pressure)
316        };
317        Ok(id)
318    }
319
320    /// Post a touch move event.
321    ///
322    /// # Arguments
323    /// * `contact` - Contact/finger index
324    /// * `x`, `y` - New touch coordinates
325    /// * `pressure` - Touch pressure
326    pub fn post_touch_move(
327        &self,
328        contact: i32,
329        x: i32,
330        y: i32,
331        pressure: i32,
332    ) -> MaaResult<common::MaaId> {
333        let id = unsafe {
334            sys::MaaControllerPostTouchMove(self.inner.handle.as_ptr(), contact, x, y, pressure)
335        };
336        Ok(id)
337    }
338
339    /// Post a touch up event.
340    ///
341    /// # Arguments
342    /// * `contact` - Contact/finger index to release
343    pub fn post_touch_up(&self, contact: i32) -> MaaResult<common::MaaId> {
344        let id = unsafe { sys::MaaControllerPostTouchUp(self.inner.handle.as_ptr(), contact) };
345        Ok(id)
346    }
347
348    /// Returns the underlying raw controller handle.
349    #[inline]
350    pub fn raw(&self) -> *mut sys::MaaController {
351        self.inner.handle.as_ptr()
352    }
353
354    // === Connection ===
355
356    /// Post a connection request to the device.
357    ///
358    /// Returns a job ID that can be used with [`wait`](Self::wait) to block until connected.
359    pub fn post_connection(&self) -> MaaResult<common::MaaId> {
360        let id = unsafe { sys::MaaControllerPostConnection(self.inner.handle.as_ptr()) };
361        Ok(id)
362    }
363
364    /// Returns `true` if the controller is connected to the device.
365    pub fn connected(&self) -> bool {
366        unsafe { sys::MaaControllerConnected(self.inner.handle.as_ptr()) != 0 }
367    }
368
369    /// Gets the unique identifier (UUID) of the connected device.
370    pub fn uuid(&self) -> MaaResult<String> {
371        let buffer = crate::buffer::MaaStringBuffer::new()?;
372        let ret = unsafe { sys::MaaControllerGetUuid(self.inner.handle.as_ptr(), buffer.as_ptr()) };
373        if ret != 0 {
374            Ok(buffer.to_string())
375        } else {
376            Err(MaaError::FrameworkError(0))
377        }
378    }
379
380    /// Gets the controller information as a JSON value.
381    ///
382    /// Returns controller-specific information including type, constructor parameters
383    /// and current state. The returned JSON always contains a "type" field.
384    pub fn info(&self) -> MaaResult<serde_json::Value> {
385        let buffer = crate::buffer::MaaStringBuffer::new()?;
386        let ret = unsafe { sys::MaaControllerGetInfo(self.inner.handle.as_ptr(), buffer.as_ptr()) };
387        if ret != 0 {
388            serde_json::from_str(&buffer.to_string()).map_err(|e| {
389                MaaError::InvalidArgument(format!("Failed to parse controller info: {}", e))
390            })
391        } else {
392            Err(MaaError::FrameworkError(0))
393        }
394    }
395
396    /// Gets the device screen resolution as (width, height).
397    pub fn resolution(&self) -> MaaResult<(i32, i32)> {
398        let mut width: i32 = 0;
399        let mut height: i32 = 0;
400        let ret = unsafe {
401            sys::MaaControllerGetResolution(self.inner.handle.as_ptr(), &mut width, &mut height)
402        };
403        if ret != 0 {
404            Ok((width, height))
405        } else {
406            Err(MaaError::FrameworkError(0))
407        }
408    }
409
410    // === Swipe V2 ===
411
412    /// Post a swipe action with contact and pressure parameters.
413    ///
414    /// # Arguments
415    /// * `x1`, `y1` - Start coordinates
416    /// * `x2`, `y2` - End coordinates
417    /// * `duration` - Swipe duration in milliseconds
418    /// * `contact` - Contact/finger index
419    /// * `pressure` - Touch pressure
420    pub fn post_swipe_v2(
421        &self,
422        x1: i32,
423        y1: i32,
424        x2: i32,
425        y2: i32,
426        duration: i32,
427        contact: i32,
428        pressure: i32,
429    ) -> MaaResult<common::MaaId> {
430        let id = unsafe {
431            sys::MaaControllerPostSwipeV2(
432                self.inner.handle.as_ptr(),
433                x1,
434                y1,
435                x2,
436                y2,
437                duration,
438                contact,
439                pressure,
440            )
441        };
442        Ok(id)
443    }
444
445    // === Key control ===
446
447    /// Post a key down event.
448    pub fn post_key_down(&self, keycode: i32) -> MaaResult<common::MaaId> {
449        let id = unsafe { sys::MaaControllerPostKeyDown(self.inner.handle.as_ptr(), keycode) };
450        Ok(id)
451    }
452
453    /// Post a key up event.
454    pub fn post_key_up(&self, keycode: i32) -> MaaResult<common::MaaId> {
455        let id = unsafe { sys::MaaControllerPostKeyUp(self.inner.handle.as_ptr(), keycode) };
456        Ok(id)
457    }
458
459    // === App control ===
460
461    /// Start an application.
462    ///
463    /// # Arguments
464    /// * `intent` - Package name or activity (ADB), app identifier (Win32)
465    pub fn post_start_app(&self, intent: &str) -> MaaResult<common::MaaId> {
466        let c_intent = CString::new(intent)?;
467        let id = unsafe {
468            sys::MaaControllerPostStartApp(self.inner.handle.as_ptr(), c_intent.as_ptr())
469        };
470        Ok(id)
471    }
472
473    /// Stop an application.
474    ///
475    /// # Arguments
476    /// * `intent` - Package name (ADB)
477    pub fn post_stop_app(&self, intent: &str) -> MaaResult<common::MaaId> {
478        let c_intent = CString::new(intent)?;
479        let id =
480            unsafe { sys::MaaControllerPostStopApp(self.inner.handle.as_ptr(), c_intent.as_ptr()) };
481        Ok(id)
482    }
483
484    // === Scroll ===
485
486    /// Post a scroll action (Win32 only).
487    ///
488    /// # Arguments
489    /// * `dx` - Horizontal scroll delta (positive = right)
490    /// * `dy` - Vertical scroll delta (positive = down)
491    pub fn post_scroll(&self, dx: i32, dy: i32) -> MaaResult<common::MaaId> {
492        let id = unsafe { sys::MaaControllerPostScroll(self.inner.handle.as_ptr(), dx, dy) };
493        Ok(id)
494    }
495
496    // === Inactive ===
497
498    /// Post an inactive request to the controller.
499    ///
500    /// For Win32 controllers, this restores window position (removes topmost) and unblocks user input.
501    /// For other controllers, this is a no-op that always succeeds.
502    pub fn post_inactive(&self) -> MaaResult<common::MaaId> {
503        let id = unsafe { sys::MaaControllerPostInactive(self.inner.handle.as_ptr()) };
504        Ok(id)
505    }
506
507    // === Image ===
508
509    /// Gets the most recently captured screenshot.
510    pub fn cached_image(&self) -> MaaResult<crate::buffer::MaaImageBuffer> {
511        let buffer = crate::buffer::MaaImageBuffer::new()?;
512        let ret =
513            unsafe { sys::MaaControllerCachedImage(self.inner.handle.as_ptr(), buffer.as_ptr()) };
514        if ret != 0 {
515            Ok(buffer)
516        } else {
517            Err(MaaError::FrameworkError(0))
518        }
519    }
520
521    // === Shell output ===
522
523    /// Gets the output from the most recent shell command (ADB only).
524    pub fn shell_output(&self) -> MaaResult<String> {
525        let buffer = crate::buffer::MaaStringBuffer::new()?;
526        let ret = unsafe {
527            sys::MaaControllerGetShellOutput(self.inner.handle.as_ptr(), buffer.as_ptr())
528        };
529        if ret != 0 {
530            Ok(buffer.to_string())
531        } else {
532            Err(MaaError::FrameworkError(0))
533        }
534    }
535
536    // === Status ===
537
538    /// Gets the status of a controller operation.
539    pub fn status(&self, ctrl_id: common::MaaId) -> common::MaaStatus {
540        let s = unsafe { sys::MaaControllerStatus(self.inner.handle.as_ptr(), ctrl_id) };
541        common::MaaStatus(s)
542    }
543
544    /// Blocks until a controller operation completes.
545    pub fn wait(&self, ctrl_id: common::MaaId) -> common::MaaStatus {
546        let s = unsafe { sys::MaaControllerWait(self.inner.handle.as_ptr(), ctrl_id) };
547        common::MaaStatus(s)
548    }
549
550    // === Screenshot options ===
551
552    /// Sets the target long side for screenshot scaling.
553    pub fn set_screenshot_target_long_side(&self, long_side: i32) -> MaaResult<()> {
554        let mut val = long_side;
555        let ret = unsafe {
556            sys::MaaControllerSetOption(
557                self.inner.handle.as_ptr(),
558                sys::MaaCtrlOptionEnum_MaaCtrlOption_ScreenshotTargetLongSide as i32,
559                &mut val as *mut _ as *mut c_void,
560                std::mem::size_of::<i32>() as u64,
561            )
562        };
563        common::check_bool(ret)
564    }
565
566    /// Sets the target short side for screenshot scaling.
567    pub fn set_screenshot_target_short_side(&self, short_side: i32) -> MaaResult<()> {
568        let mut val = short_side;
569        let ret = unsafe {
570            sys::MaaControllerSetOption(
571                self.inner.handle.as_ptr(),
572                sys::MaaCtrlOptionEnum_MaaCtrlOption_ScreenshotTargetShortSide as i32,
573                &mut val as *mut _ as *mut c_void,
574                std::mem::size_of::<i32>() as u64,
575            )
576        };
577        common::check_bool(ret)
578    }
579
580    /// Sets whether to use raw (unscaled) screenshot resolution.
581    pub fn set_screenshot_use_raw_size(&self, enable: bool) -> MaaResult<()> {
582        let mut val: u8 = if enable { 1 } else { 0 };
583        let ret = unsafe {
584            sys::MaaControllerSetOption(
585                self.inner.handle.as_ptr(),
586                sys::MaaCtrlOptionEnum_MaaCtrlOption_ScreenshotUseRawSize as i32,
587                &mut val as *mut _ as *mut c_void,
588                std::mem::size_of::<u8>() as u64,
589            )
590        };
591        common::check_bool(ret)
592    }
593
594    // === New controller types ===
595
596    #[cfg(feature = "dbg")]
597    pub fn new_dbg(
598        read_path: &str,
599        write_path: &str,
600        dbg_type: sys::MaaDbgControllerType,
601        config: &str,
602    ) -> MaaResult<Self> {
603        let c_read = CString::new(read_path)?;
604        let c_write = CString::new(write_path)?;
605        let c_cfg = CString::new(config)?;
606        let handle = unsafe {
607            sys::MaaDbgControllerCreate(c_read.as_ptr(), c_write.as_ptr(), dbg_type, c_cfg.as_ptr())
608        };
609        Self::from_handle(handle)
610    }
611
612    /// Create a virtual gamepad controller (Windows only).
613    #[cfg(feature = "win32")]
614    pub fn new_gamepad(
615        hwnd: *mut c_void,
616        gamepad_type: crate::common::GamepadType,
617        screencap_method: crate::common::Win32ScreencapMethod,
618    ) -> MaaResult<Self> {
619        let handle = unsafe {
620            sys::MaaGamepadControllerCreate(hwnd, gamepad_type as u64, screencap_method.bits())
621        };
622        Self::from_handle(handle)
623    }
624
625    // === EventSink ===
626
627    /// Returns sink_id for later removal. Callback lifetime managed by caller.
628    pub fn add_sink<F>(&self, callback: F) -> MaaResult<sys::MaaSinkId>
629    where
630        F: Fn(&str, &str) + Send + Sync + 'static,
631    {
632        let (cb_fn, cb_arg) = crate::callback::EventCallback::new(callback);
633        let sink_id =
634            unsafe { sys::MaaControllerAddSink(self.inner.handle.as_ptr(), cb_fn, cb_arg) };
635        if sink_id != 0 {
636            self.inner
637                .callbacks
638                .lock()
639                .unwrap()
640                .insert(sink_id, cb_arg as usize);
641            Ok(sink_id)
642        } else {
643            unsafe { crate::callback::EventCallback::drop_callback(cb_arg) };
644            Err(MaaError::FrameworkError(0))
645        }
646    }
647
648    /// Register a strongly-typed event sink.
649    ///
650    /// This method registers an implementation of the [`EventSink`](crate::event_sink::EventSink) trait
651    /// to receive structured notifications from this controller.
652    ///
653    /// # Arguments
654    /// * `sink` - The event sink implementation (must be boxed).
655    ///
656    /// # Returns
657    /// A `MaaSinkId` which can be used to manually remove the sink later via [`remove_sink`](Self::remove_sink).
658    /// The sink will be automatically unregistered and dropped when the `Controller` is dropped.
659    pub fn add_event_sink(
660        &self,
661        sink: Box<dyn crate::event_sink::EventSink>,
662    ) -> MaaResult<sys::MaaSinkId> {
663        let handle_id = self.inner.handle.as_ptr() as crate::common::MaaId;
664        let (cb, arg) = crate::callback::EventCallback::new_sink(handle_id, sink);
665        let id = unsafe { sys::MaaControllerAddSink(self.inner.handle.as_ptr(), cb, arg) };
666        if id > 0 {
667            self.inner
668                .event_sinks
669                .lock()
670                .unwrap()
671                .insert(id, arg as usize);
672            Ok(id)
673        } else {
674            unsafe { crate::callback::EventCallback::drop_sink(arg) };
675            Err(MaaError::FrameworkError(0))
676        }
677    }
678
679    pub fn remove_sink(&self, sink_id: sys::MaaSinkId) {
680        unsafe { sys::MaaControllerRemoveSink(self.inner.handle.as_ptr(), sink_id) };
681        if let Some(ptr) = self.inner.callbacks.lock().unwrap().remove(&sink_id) {
682            unsafe { crate::callback::EventCallback::drop_callback(ptr as *mut c_void) };
683        } else if let Some(ptr) = self.inner.event_sinks.lock().unwrap().remove(&sink_id) {
684            unsafe { crate::callback::EventCallback::drop_sink(ptr as *mut c_void) };
685        }
686    }
687
688    pub fn clear_sinks(&self) {
689        unsafe { sys::MaaControllerClearSinks(self.inner.handle.as_ptr()) };
690        let mut callbacks = self.inner.callbacks.lock().unwrap();
691        for (_, ptr) in callbacks.drain() {
692            unsafe { crate::callback::EventCallback::drop_callback(ptr as *mut c_void) };
693        }
694        let mut event_sinks = self.inner.event_sinks.lock().unwrap();
695        for (_, ptr) in event_sinks.drain() {
696            unsafe { crate::callback::EventCallback::drop_sink(ptr as *mut c_void) };
697        }
698    }
699}
700
701impl Drop for ControllerInner {
702    fn drop(&mut self) {
703        unsafe {
704            sys::MaaControllerClearSinks(self.handle.as_ptr());
705            let mut callbacks = self.callbacks.lock().unwrap();
706            for (_, ptr) in callbacks.drain() {
707                crate::callback::EventCallback::drop_callback(ptr as *mut c_void);
708            }
709            let mut event_sinks = self.event_sinks.lock().unwrap();
710            for (_, ptr) in event_sinks.drain() {
711                crate::callback::EventCallback::drop_sink(ptr as *mut c_void);
712            }
713            if self.owns_handle {
714                sys::MaaControllerDestroy(self.handle.as_ptr());
715            }
716        }
717    }
718}
719
720/// Builder for ADB controller configuration.
721///
722/// Provides a fluent API for configuring ADB controllers with sensible defaults.
723#[cfg(feature = "adb")]
724pub struct AdbControllerBuilder {
725    adb_path: String,
726    address: String,
727    screencap_methods: sys::MaaAdbScreencapMethod,
728    input_methods: sys::MaaAdbInputMethod,
729    config: String,
730    agent_path: String,
731}
732
733#[cfg(feature = "adb")]
734impl AdbControllerBuilder {
735    /// Create a new builder with required ADB path and device address.
736    pub fn new(adb_path: &str, address: &str) -> Self {
737        Self {
738            adb_path: adb_path.to_string(),
739            address: address.to_string(),
740            screencap_methods: sys::MaaAdbScreencapMethod_Default as sys::MaaAdbScreencapMethod,
741            input_methods: sys::MaaAdbInputMethod_Default as sys::MaaAdbInputMethod,
742            config: "{}".to_string(),
743            agent_path: String::new(),
744        }
745    }
746
747    /// Set the screencap methods to use.
748    pub fn screencap_methods(mut self, methods: sys::MaaAdbScreencapMethod) -> Self {
749        self.screencap_methods = methods;
750        self
751    }
752
753    /// Set the input methods to use.
754    pub fn input_methods(mut self, methods: sys::MaaAdbInputMethod) -> Self {
755        self.input_methods = methods;
756        self
757    }
758
759    /// Set additional configuration as JSON.
760    pub fn config(mut self, config: &str) -> Self {
761        self.config = config.to_string();
762        self
763    }
764
765    /// Set the path to MaaAgentBinary.
766    pub fn agent_path(mut self, path: &str) -> Self {
767        self.agent_path = path.to_string();
768        self
769    }
770
771    /// Build the controller with the configured options.
772    pub fn build(self) -> MaaResult<Controller> {
773        Controller::create_adb(
774            &self.adb_path,
775            &self.address,
776            self.screencap_methods,
777            self.input_methods,
778            &self.config,
779            &self.agent_path,
780        )
781    }
782}
783
784/// A borrowed reference to a Controller.
785///
786/// This is a non-owning view that can be used for read-only operations.
787/// It does NOT call destroy when dropped and should only be used while
788/// the underlying Controller is still alive.
789pub struct ControllerRef<'a> {
790    handle: *mut sys::MaaController,
791    _marker: std::marker::PhantomData<&'a ()>,
792}
793
794impl<'a> std::fmt::Debug for ControllerRef<'a> {
795    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
796        f.debug_struct("ControllerRef")
797            .field("handle", &self.handle)
798            .finish()
799    }
800}
801
802impl<'a> ControllerRef<'a> {
803    pub(crate) fn from_ptr(handle: *mut sys::MaaController) -> Option<Self> {
804        if handle.is_null() {
805            None
806        } else {
807            Some(Self {
808                handle,
809                _marker: std::marker::PhantomData,
810            })
811        }
812    }
813
814    /// Check if connected.
815    pub fn connected(&self) -> bool {
816        unsafe { sys::MaaControllerConnected(self.handle) != 0 }
817    }
818
819    /// Get device UUID.
820    pub fn uuid(&self) -> MaaResult<String> {
821        let buffer = crate::buffer::MaaStringBuffer::new()?;
822        let ret = unsafe { sys::MaaControllerGetUuid(self.handle, buffer.as_ptr()) };
823        if ret != 0 {
824            Ok(buffer.to_string())
825        } else {
826            Err(MaaError::FrameworkError(0))
827        }
828    }
829
830    /// Get controller information as a JSON value.
831    pub fn info(&self) -> MaaResult<serde_json::Value> {
832        let buffer = crate::buffer::MaaStringBuffer::new()?;
833        let ret = unsafe { sys::MaaControllerGetInfo(self.handle, buffer.as_ptr()) };
834        if ret != 0 {
835            serde_json::from_str(&buffer.to_string()).map_err(|e| {
836                MaaError::InvalidArgument(format!("Failed to parse controller info: {}", e))
837            })
838        } else {
839            Err(MaaError::FrameworkError(0))
840        }
841    }
842
843    /// Get device resolution.
844    pub fn resolution(&self) -> MaaResult<(i32, i32)> {
845        let mut width: i32 = 0;
846        let mut height: i32 = 0;
847        let ret = unsafe { sys::MaaControllerGetResolution(self.handle, &mut width, &mut height) };
848        if ret != 0 {
849            Ok((width, height))
850        } else {
851            Err(MaaError::FrameworkError(0))
852        }
853    }
854
855    /// Get operation status.
856    pub fn status(&self, ctrl_id: common::MaaId) -> common::MaaStatus {
857        let s = unsafe { sys::MaaControllerStatus(self.handle, ctrl_id) };
858        common::MaaStatus(s)
859    }
860
861    /// Wait for operation to complete.
862    pub fn wait(&self, ctrl_id: common::MaaId) -> common::MaaStatus {
863        let s = unsafe { sys::MaaControllerWait(self.handle, ctrl_id) };
864        common::MaaStatus(s)
865    }
866
867    /// Get cached screenshot.
868    pub fn cached_image(&self) -> MaaResult<crate::buffer::MaaImageBuffer> {
869        let buffer = crate::buffer::MaaImageBuffer::new()?;
870        let ret = unsafe { sys::MaaControllerCachedImage(self.handle, buffer.as_ptr()) };
871        if ret != 0 {
872            Ok(buffer)
873        } else {
874            Err(MaaError::FrameworkError(0))
875        }
876    }
877
878    /// Get raw handle.
879    pub fn raw(&self) -> *mut sys::MaaController {
880        self.handle
881    }
882}