android_usbser/
usb_conn.rs

1use jni::objects::JObject;
2use jni_min_helper::*;
3
4use crate::Error;
5use futures_lite::StreamExt;
6use std::{io::ErrorKind, pin::Pin, task, time::Duration};
7
8use crate::usb::{jerr, list_devices, DeviceInfo};
9
10const USB_SERVICE: &str = "usb";
11const ACTION_USB_DEVICE_ATTACHED: &str = "android.hardware.usb.action.USB_DEVICE_ATTACHED";
12const ACTION_USB_DEVICE_DETACHED: &str = "android.hardware.usb.action.USB_DEVICE_DETACHED";
13const EXTRA_DEVICE: &str = "device";
14const ACTION_USB_PERMISSION: &str = "rust.android_usbser.USB_PERMISSION"; // custom
15const EXTRA_PERMISSION_GRANTED: &str = "permission";
16
17/// Gets a global reference of `android.hardware.usb.UsbManager`.
18#[inline(always)]
19pub(crate) fn usb_manager() -> Result<&'static jni::objects::JObject<'static>, Error> {
20    use std::sync::OnceLock;
21    static USB_MAN: OnceLock<jni::objects::GlobalRef> = OnceLock::new();
22    if let Some(ref_man) = USB_MAN.get() {
23        Ok(ref_man.as_obj())
24    } else {
25        let usb_man = get_usb_manager()?;
26        let _ = USB_MAN.set(usb_man.clone());
27        Ok(USB_MAN.get().unwrap().as_obj())
28    }
29}
30
31fn get_usb_manager() -> Result<jni::objects::GlobalRef, Error> {
32    jni_with_env(|env| {
33        let context = android_context();
34        let usb_service_id = USB_SERVICE.new_jobject(env)?;
35        let usb_man = env
36            .call_method(
37                context,
38                "getSystemService",
39                "(Ljava/lang/String;)Ljava/lang/Object;",
40                &[(&usb_service_id).into()],
41            )
42            .get_object(env)?;
43
44        let result = if !usb_man.is_null() {
45            Ok(env.new_global_ref(&usb_man)?)
46        } else {
47            Err(Error::new(
48                ErrorKind::Unsupported,
49                "USB system service not found",
50            ))
51        };
52        Ok(result)
53    })
54    .map_err(jerr)?
55}
56
57/// Checks if the Android context is an activity opened by an intent of
58/// `android.hardware.usb.action.USB_DEVICE_ATTACHED`. If so, it takes the `DeviceInfo`
59/// for the caller to open the device.
60pub fn check_attached_intent() -> Result<DeviceInfo, Error> {
61    // Note: `getIntent()` and `setIntent()` are functions of `Activity` (not `Context`)
62    let dev_info = jni_with_env(|env| {
63        let activity = android_context();
64
65        // the Intent instance is taken from Activity by getIntent()
66        let intent_startup = env
67            .call_method(activity, "getIntent", "()Landroid/content/Intent;", &[])
68            .get_object(env)?;
69        // checks if the action of current intent is ACTION_USB_DEVICE_ATTACHED
70        let action_startup = BroadcastReceiver::get_intent_action(&intent_startup, env)?;
71        if action_startup.trim() != ACTION_USB_DEVICE_ATTACHED {
72            return Ok(Err(Error::from(ErrorKind::NotFound)));
73        }
74        Ok(get_extra_device(&intent_startup, env))
75    })
76    .map_err(jerr)??;
77    if dev_info.check_connection() && dev_info.has_permission()? {
78        Ok(dev_info)
79    } else {
80        Err(Error::from(ErrorKind::NotConnected))
81    }
82}
83
84fn get_extra_device(intent: &JObject<'_>, env: &mut jni::JNIEnv<'_>) -> Result<DeviceInfo, Error> {
85    let extra_device = EXTRA_DEVICE.new_jobject(env).map_err(jerr)?;
86    let java_dev = env
87        .call_method(
88            intent,
89            "getParcelableExtra",
90            // TODO: this is deprecated in API 33 and above without the class parameter.
91            "(Ljava/lang/String;)Landroid/os/Parcelable;",
92            &[(&extra_device).into()],
93        )
94        .get_object(env)
95        .map_err(jerr)?;
96
97    if !java_dev.is_null() {
98        DeviceInfo::build(env, &java_dev).map_err(jerr)
99    } else {
100        Err(Error::new(
101            ErrorKind::NotFound,
102            "Unexpected: the Intent has no EXTRA_DEVICE",
103        ))
104    }
105}
106
107/// Gets a watcher of device connection / disconnection events.
108pub fn watch_devices() -> Result<HotplugWatch, Error> {
109    BroadcastWaiter::build([ACTION_USB_DEVICE_ATTACHED, ACTION_USB_DEVICE_DETACHED])
110        .map(|waiter| HotplugWatch { waiter })
111        .map_err(jerr)
112}
113
114/// Stream of device connection / disconnection events.
115#[derive(Debug)]
116pub struct HotplugWatch {
117    waiter: BroadcastWaiter,
118}
119
120/// Event returned from the `HotplugWatch` stream.
121#[derive(Clone, Debug)]
122pub enum HotplugEvent {
123    Connected(DeviceInfo),
124    Disconnected(DeviceInfo),
125}
126
127#[derive(Debug)]
128struct HotplugWatchFuture<'a> {
129    watch: &'a mut HotplugWatch,
130}
131
132impl HotplugWatch {
133    /// Returns the amount of received events available for checking.
134    pub fn count_available(&self) -> usize {
135        self.waiter.count_received()
136    }
137
138    /// Takes the next received event if available. This shouldn't conflict
139    /// with the asynchonous feature (which requires a mutable reference).
140    pub fn take_next(&mut self) -> Option<HotplugEvent> {
141        (self.count_available() > 0).then_some(())?;
142        self.wait_blocking(Duration::from_millis(1))
143    }
144
145    /// Waits for receiving an event; returns directly if an event is available.
146    /// Note: Waiting in the `android_main()` thread will prevent it from receiving.
147    pub fn wait_blocking(&mut self, timeout: Duration) -> Option<HotplugEvent> {
148        let fut = HotplugWatchFuture { watch: self };
149        block_for_timeout(fut, timeout)
150    }
151}
152
153impl futures_core::Stream for HotplugWatch {
154    type Item = HotplugEvent;
155
156    fn poll_next(
157        mut self: Pin<&mut Self>,
158        cx: &mut task::Context<'_>,
159    ) -> task::Poll<Option<Self::Item>> {
160        // `BroadcastWaiter` implementation makes `Ready(None)` impossible here
161        if let task::Poll::Ready(Some(intent)) = self.waiter.poll_next(cx) {
162            let result = jni_with_env(|env| {
163                let action = BroadcastWaiter::get_intent_action(&intent, env)?;
164                Ok(match action.trim() {
165                    ACTION_USB_DEVICE_ATTACHED => get_extra_device(intent.as_obj(), env)
166                        .ok()
167                        .map(HotplugEvent::Connected),
168                    ACTION_USB_DEVICE_DETACHED => get_extra_device(intent.as_obj(), env)
169                        .ok()
170                        .map(HotplugEvent::Disconnected),
171                    _ => None,
172                })
173            });
174            if let Ok(Some(event)) = result {
175                task::Poll::Ready(Some(event))
176            } else {
177                task::Poll::Pending
178            }
179        } else {
180            task::Poll::Pending
181        }
182    }
183
184    fn size_hint(&self) -> (usize, Option<usize>) {
185        self.waiter.size_hint()
186    }
187}
188
189impl<'a> std::future::Future for HotplugWatchFuture<'a> {
190    type Output = HotplugEvent;
191    fn poll(mut self: Pin<&mut Self>, cx: &mut task::Context<'_>) -> task::Poll<Self::Output> {
192        if let task::Poll::Ready(Some(event)) = self.watch.poll_next(cx) {
193            task::Poll::Ready(event)
194        } else {
195            task::Poll::Pending
196        }
197    }
198}
199
200impl DeviceInfo {
201    /// Returns true if the caller has permission to access the device.
202    pub fn has_permission(&self) -> Result<bool, Error> {
203        let usb_man = usb_manager()?;
204        jni_with_env(|env| {
205            env.call_method(
206                usb_man,
207                "hasPermission",
208                "(Landroid/hardware/usb/UsbDevice;)Z",
209                &[self.internal.as_obj().into()],
210            )
211            .get_boolean()
212        })
213        .map_err(jerr)
214    }
215
216    /// Checks if the device is still in the list of connected devices.
217    /// Note: The implementation can be optimized.
218    #[inline(always)]
219    pub fn check_connection(&self) -> bool {
220        let vec_dev = list_devices().unwrap_or_default(); // heavy
221        vec_dev.into_iter().any(|ref d| d == self)
222    }
223
224    /// Performs a permission request for the device.
225    ///
226    /// Returns `Ok(None)` if the permission is already granted. Otherwise it returns a
227    /// `PermissionRequest` handler.
228    ///
229    /// The activity might be paused by `requestPermission()` here, but resumed on receving result.
230    /// The state of `PermissionRequest` can be checked on `android_activity::MainEvent::Resume`,
231    /// Otherwise block in a background thread (it wouldn't be paused/resumed automatically).
232    pub fn request_permission(&self) -> Result<Option<PermissionRequest>, Error> {
233        if !self.check_connection() {
234            return Err(Error::from(ErrorKind::NotConnected));
235        }
236        if self.has_permission()? {
237            return Ok(None);
238        }
239
240        let usb_man = usb_manager()?;
241        jni_with_env(|env| {
242            let context = android_context();
243
244            let str_perm = ACTION_USB_PERMISSION.new_jobject(env)?;
245            let intent = env
246                .new_object(
247                    "android/content/Intent",
248                    "(Ljava/lang/String;)V",
249                    &[(&str_perm).into()],
250                )
251                .auto_local(env)?;
252
253            let flags = if android_api_level() < 31 {
254                0 // should it be FLAG_IMMUTABLE since API 23?
255            } else {
256                0x02000000 // FLAG_MUTABLE (since API 31, Android 12)
257            };
258            let pending = env
259                .call_static_method(
260                    "android/app/PendingIntent",
261                    "getBroadcast",
262                    "(Landroid/content/Context;ILandroid/content/Intent;I)Landroid/app/PendingIntent;",
263                    &[context.into(), 0_i32.into(), (&intent).into(), flags.into()],
264                )
265                .get_object(env)?;
266
267            env.call_method(
268                usb_man,
269                "requestPermission",
270                "(Landroid/hardware/usb/UsbDevice;Landroid/app/PendingIntent;)V",
271                &[(&self.internal.as_obj()).into(), (&pending).into()],
272            )
273            .clear_ex()?;
274
275            Ok(())
276        })
277        .map_err(jerr)?;
278
279        if self.has_permission()? {
280            return Ok(None); // almost impossible
281        }
282        BroadcastWaiter::build([ACTION_USB_PERMISSION])
283            .map(|waiter| {
284                Some(PermissionRequest {
285                    dev_info: self.clone(),
286                    waiter,
287                })
288            })
289            .map_err(jerr)
290    }
291
292    /// Opens the device. Returns error `PermissionDenied` if the permission is not granted.
293    pub fn open_device(&self) -> Result<nusb::Device, Error> {
294        if !self.check_connection() {
295            return Err(Error::new(
296                ErrorKind::NotConnected,
297                "the device has been disconnected",
298            ));
299        }
300        if !self.has_permission()? {
301            return Err(Error::from(ErrorKind::PermissionDenied));
302        }
303
304        jni_with_env(|env| {
305            let usb_man = match usb_manager() {
306                Ok(man) => man,
307                Err(e) => return Ok(Err(e)),
308            };
309            // Another thread executing `open_device` will block here, until the guard
310            // for the current thread is dropped after `LinuxDevice::create_inner`.
311            let _guard = env.lock_obj(usb_man).unwrap();
312
313            let conn = env
314                .call_method(
315                    usb_man,
316                    "openDevice",
317                    "(Landroid/hardware/usb/UsbDevice;)Landroid/hardware/usb/UsbDeviceConnection;",
318                    &[(&self.internal).into()],
319                )
320                .get_object(env)?;
321            if conn.is_null() {
322                return Ok(Err(Error::new(
323                    ErrorKind::NotFound,
324                    "`UsbManager.openDevice()` failed`",
325                )));
326            }
327            let raw_fd = env
328                .call_method(&conn, "getFileDescriptor", "()I", &[])
329                .get_int()?;
330
331            // Safety: `close()` is not called automatically when the JNI `AutoLocal` of `conn`
332            // and the corresponding Java object is destroyed. (check `UsbDeviceConnection` source)
333            use std::os::fd::*;
334            log::debug!("Wrapping fd {raw_fd} as usbfs device");
335            let owned_fd = unsafe { OwnedFd::from_raw_fd(raw_fd as RawFd) };
336            Ok(nusb::Device::from_fd(owned_fd))
337        })
338        .map_err(jerr)?
339    }
340}
341
342/// Represents an ongoing permission request.
343#[derive(Debug)]
344pub struct PermissionRequest {
345    dev_info: DeviceInfo,
346    waiter: BroadcastWaiter,
347}
348
349impl PermissionRequest {
350    /// Returns a reference of the associated `DeviceInfo` which can be cloned.
351    pub fn device_info(&self) -> &DeviceInfo {
352        &self.dev_info
353    }
354
355    /// Checks if the request has completed.
356    pub fn responsed(&self) -> bool {
357        self.waiter.count_received() > 0
358    }
359
360    /// Takes the `EXTRA_PERMISSION_GRANTED` extra from the received result.
361    /// This can be called *after* `responsed()` returned true.
362    pub fn take_response(self) -> Option<bool> {
363        self.responsed().then_some(())?;
364        block_for_timeout(self, Duration::from_millis(10))
365    }
366
367    /// Blocking permission request. Returns directly if the permission is already granted.
368    /// Note: Blocking the `android_main()` thread will prevent it from receiving the result.
369    pub fn wait_blocking(self, timeout: Duration) -> Result<bool, Error> {
370        block_for_timeout(self, timeout).ok_or(Error::from(ErrorKind::TimedOut))
371    }
372}
373
374impl std::future::Future for PermissionRequest {
375    type Output = bool;
376
377    fn poll(mut self: Pin<&mut Self>, cx: &mut task::Context<'_>) -> task::Poll<Self::Output> {
378        // `BroadcastWaiter` implementation makes `Ready(None)` impossible here
379        if let task::Poll::Ready(Some(intent)) = self.waiter.poll_next(cx) {
380            let result = jni_with_env(|env| {
381                let Ok(dev_info) = get_extra_device(intent.as_obj(), env) else {
382                    return Ok(None);
383                };
384                if dev_info == self.dev_info {
385                    let extra_name = EXTRA_PERMISSION_GRANTED.new_jobject(env)?;
386                    let granted = env
387                        .call_method(
388                            &intent,
389                            "getBooleanExtra",
390                            "(Ljava/lang/String;Z)Z",
391                            &[(&extra_name).into(), false.into()],
392                        )
393                        .get_boolean()
394                        .unwrap_or(false);
395                    let _ = self.waiter.receiver().unregister();
396                    Ok(Some(granted))
397                } else {
398                    Ok(None)
399                }
400            });
401            if let Ok(Some(granted)) = result {
402                task::Poll::Ready(granted)
403            } else if let Ok(None) = result {
404                task::Poll::Pending
405            } else {
406                task::Poll::Ready(false)
407            }
408        } else {
409            task::Poll::Pending
410        }
411    }
412}