mtp-rs 0.15.0

Pure-Rust MTP (Media Transfer Protocol) library for modern Android devices
Documentation
//! Cooperative cancellation for long-running MTP operations.
//!
//! [`CancelToken`] is a cheap-to-clone handle that callers pass into operations
//! like [`Storage::list_objects`](crate::mtp::Storage::list_objects) so they can
//! be aborted from another task. The operation checks the token between USB
//! roundtrips; when set, it returns [`Error::Cancelled`](crate::Error::Cancelled)
//! within one roundtrip's worth of latency instead of running to completion.
//!
//! # When to use this
//!
//! MTP list and recursive delete on large folders consist of many small
//! per-object USB transactions (`GetObjectInfo` per handle). A 1k-photo
//! `/DCIM/Camera` listing on Android can take 15+ seconds over USB 2.0. Without
//! cooperative cancel, a user-initiated cancel only stops issuing _new_
//! transactions after the current bulk call's _entire_ for-loop completes. By
//! then the device may already be wedged from a follow-up op.
//!
//! [`CancelToken`] gives every per-handle iteration a fast way to bail.
//!
//! # Mid-data-phase cancel
//!
//! For streaming downloads, see
//! [`FileDownload::cancel`](crate::mtp::FileDownload::cancel); that path uses
//! the USB Still Image Class (SIC) class-cancel mechanism to abort a long
//! bulk-IN transfer mid-stream. Per-handle list/delete operations don't need
//! that: each `GetObjectInfo` / `DeleteObject` USB roundtrip completes in
//! milliseconds, so checking the token between roundtrips is sufficient and
//! safe (no half-finished transfer to drain).
//!
//! # Example
//!
//! ```rust,no_run
//! use mtp_rs::{CancelToken, mtp::MtpDevice};
//!
//! # async fn example() -> Result<(), mtp_rs::Error> {
//! let device = MtpDevice::open_first().await?;
//! let storages = device.storages().await?;
//! let storage = &storages[0];
//!
//! let cancel = CancelToken::new();
//!
//! // Fire the cancel from another task after 500 ms.
//! let cancel_for_task = cancel.clone();
//! tokio::spawn(async move {
//!     tokio::time::sleep(std::time::Duration::from_millis(500)).await;
//!     cancel_for_task.cancel();
//! });
//!
//! match storage.list_objects_with_cancel(None, Some(&cancel)).await {
//!     Ok(objects) => println!("Listed {} objects", objects.len()),
//!     Err(mtp_rs::Error::Cancelled) => println!("Cancelled mid-listing"),
//!     Err(e) => return Err(e),
//! }
//! # Ok(())
//! # }
//! ```

use std::sync::atomic::{AtomicBool, Ordering};
use std::sync::Arc;

/// Cooperative cancellation handle for long-running MTP operations.
///
/// Cheap to clone (`Arc`-backed). `Send + Sync`. Once cancelled, the token
/// stays cancelled (no reset). Construct a fresh `CancelToken` per
/// logical operation.
///
/// See the [module-level docs](crate::cancel) for usage and semantics.
#[derive(Debug, Clone, Default)]
pub struct CancelToken {
    cancelled: Arc<AtomicBool>,
}

impl CancelToken {
    /// Creates a fresh, not-yet-cancelled token.
    #[must_use]
    pub fn new() -> Self {
        Self::default()
    }

    /// Wraps an externally-owned `Arc<AtomicBool>`.
    ///
    /// Use this when the caller already has its own cancellation flag (for
    /// example, threaded through several layers from a host application) and
    /// wants the same atomic to drive an mtp-rs operation, with no second
    /// polling task. The token shares ownership of the inner atomic, so
    /// flipping the original flag flips the token (and vice versa).
    #[must_use]
    pub fn from_arc(cancelled: Arc<AtomicBool>) -> Self {
        Self { cancelled }
    }

    /// Marks the token as cancelled. Subsequent [`is_cancelled`] calls return
    /// `true`. Cooperative consumers (operations holding `Option<&CancelToken>`)
    /// bail at their next check point with `Err(Error::Cancelled)`.
    ///
    /// Idempotent: calling on an already-cancelled token is a no-op.
    ///
    /// [`is_cancelled`]: Self::is_cancelled
    pub fn cancel(&self) {
        self.cancelled.store(true, Ordering::Release);
    }

    /// Returns `true` once [`cancel`] has been called on this token (or any
    /// clone of it).
    ///
    /// [`cancel`]: Self::cancel
    #[must_use]
    pub fn is_cancelled(&self) -> bool {
        self.cancelled.load(Ordering::Acquire)
    }
}

/// Helper: checks an optional cancel token and returns `Err(Error::Cancelled)`
/// when set. Use at iteration boundaries inside long operations.
#[inline]
pub(crate) fn bail_if_cancelled(token: Option<&CancelToken>) -> Result<(), crate::Error> {
    if let Some(t) = token {
        if t.is_cancelled() {
            return Err(crate::Error::Cancelled);
        }
    }
    Ok(())
}

#[cfg(test)]
mod tests {
    use super::*;

    #[test]
    fn new_token_is_not_cancelled() {
        let token = CancelToken::new();
        assert!(!token.is_cancelled());
    }

    #[test]
    fn cancel_flips_the_flag() {
        let token = CancelToken::new();
        token.cancel();
        assert!(token.is_cancelled());
    }

    #[test]
    fn cancel_is_visible_through_clones() {
        let token = CancelToken::new();
        let clone = token.clone();
        token.cancel();
        assert!(clone.is_cancelled());
    }

    #[test]
    fn cancel_is_idempotent() {
        let token = CancelToken::new();
        token.cancel();
        token.cancel();
        assert!(token.is_cancelled());
    }

    #[test]
    fn bail_if_cancelled_returns_ok_for_none() {
        assert!(bail_if_cancelled(None).is_ok());
    }

    #[test]
    fn bail_if_cancelled_returns_ok_for_unset_token() {
        let token = CancelToken::new();
        assert!(bail_if_cancelled(Some(&token)).is_ok());
    }

    #[test]
    fn bail_if_cancelled_returns_cancelled_for_set_token() {
        let token = CancelToken::new();
        token.cancel();
        let err = bail_if_cancelled(Some(&token)).unwrap_err();
        assert!(matches!(err, crate::Error::Cancelled));
    }
}