Skip to content
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
24 changes: 22 additions & 2 deletions Cargo.lock

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

8 changes: 8 additions & 0 deletions Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,10 @@ members = [
"crates/*",
]

exclude = [
"examples/example-share-sheet",
]

# Note: we haven't yet implemented the top-level crate of re-exports.
#
# ## The top-level crate just re-exports the contents of each sub-crate
Expand Down Expand Up @@ -43,12 +47,15 @@ repository = "https://github.com/project-robius/robius"

[workspace.dependencies]
## General dependencies (target-independent) used in multiple crates in the workspace.
robius-common = { version = "0.2.0", path = "crates/common" }
cfg-if = "1.0.0"
log = "0.4"
retry = "2.0.0"
tokio = { version = "1.43.1", default-features = false }




## Android-specific dependencies used in multiple crates in the workspace.
jni = { version = "0.21.1", default-features = false }
robius-android-env = "0.2.0"
Expand All @@ -72,6 +79,7 @@ objc2-uniform-type-identifiers = { version = "0.3.1", default-features = false,
## Linux-specific dependencies used in multiple crates in the workspace.
zbus = "5.12.0"
zbus_polkit = "5.0.0"
libc = "0.2"

## Windows-specific dependencies used in multiple crates in the workspace.
windows-core = { version = "0.56.0", default-features = false }
Expand Down
2 changes: 1 addition & 1 deletion crates/file-picker/Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -26,7 +26,7 @@ android-build.workspace = true

[dependencies]
cfg-if.workspace = true
robius-common = { version = "0.2.0", path = "../common" }
robius-common.workspace = true


[target.'cfg(target_os = "android")'.dependencies]
Expand Down
90 changes: 90 additions & 0 deletions crates/share/Cargo.toml
Original file line number Diff line number Diff line change
@@ -0,0 +1,90 @@
[package]
name = "robius-share"
version.workspace = true
edition.workspace = true
authors = [
"Kevin Boos <kevinaboos@gmail.com>",
"Project Robius Maintainers",
]
description = "Rust abstractions for showing native system share sheets on multiple platforms"
documentation = "https://docs.rs/robius-share"
homepage.workspace = true
keywords = ["robius", "share", "sheet", "intent", "native"]
categories.workspace = true
license.workspace = true
repository.workspace = true
readme = "README.md"


## Note: ideally this would be only included when we're building for Android,
## but Cargo doesn't support target-specific build scripts yet.
## See: <https://github.com/rust-lang/cargo/issues/4932> and
## <https://github.com/rust-lang/cargo/issues/14378#issuecomment-2278333431>.
[build-dependencies]
android-build.workspace = true


[dependencies]
cfg-if.workspace = true
robius-common.workspace = true


[target.'cfg(target_os = "android")'.dependencies]
jni.workspace = true
robius-android-env.workspace = true


[target.'cfg(target_os = "linux")'.dependencies]
libc.workspace = true


[target.'cfg(any(target_os = "ios", target_os = "macos"))'.dependencies]
dispatch2 = { workspace = true, features = ["objc2"] }
objc2.workspace = true
objc2-foundation = { workspace = true, features = [
"NSArray",
"NSEnumerator",
"NSObject",
"NSString",
"NSURL",
] }


[target.'cfg(target_os = "ios")'.dependencies]
objc2-ui-kit = { workspace = true, features = [
"UIApplication",
"UIActivity",
"UIActivityViewController",
"UIPopoverPresentationController",
"UIPresentationController",
"UIResponder",
"UIView",
"UIViewController",
"UIWindow",
"block2",
"objc2-core-foundation",
] }


[target.'cfg(target_os = "macos")'.dependencies]
objc2-app-kit = { workspace = true, features = [
"NSApplication",
"NSResponder",
"NSSharingService",
"NSView",
"NSWindow",
"objc2-core-foundation",
] }


[target.'cfg(target_os = "windows")'.dependencies]
windows = { workspace = true, features = [
"ApplicationModel_DataTransfer",
"Foundation",
"Foundation_Collections",
"implement",
"Storage",
"Win32_Foundation",
"Win32_UI_Shell",
"Win32_UI_WindowsAndMessaging",
] }
143 changes: 143 additions & 0 deletions crates/share/README.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,143 @@
# robius-share

`robius-share` provides a Rust builder API for showing the native system share
sheet from an app.

```rust
use robius_share::ShareSheet;

ShareSheet::new()
.set_title("Share")
.set_subject("Robius")
.add_text("Cross-platform native APIs from Rust")
.add_url("https://robius.rs/")
.share()?;
# Ok::<(), robius_share::Error>(())
```

## Platform behavior

- Android uses `Intent.ACTION_SEND` / `ACTION_SEND_MULTIPLE` through
`Intent.createChooser`.
- iOS uses `UIActivityViewController`.
- macOS uses `NSSharingServicePicker`.
- Windows uses the native WinRT Share UI through desktop-window interop.
- Linux uses the XDG desktop portal app chooser when available, calling
`OpenURI` for URI payloads and `OpenFile` for local files, then falling back
to `xdg-open`. For text-only or mixed payloads that cannot be represented as
one URI or file, it writes a temporary text payload and opens that with the
platform app chooser/default handler.

Android `content://` attachments are shared directly. Filesystem path
attachments are copied to a shareable MediaStore item on Android 10 and newer.
On Android 8 and 9, API 26 through 28, text files can be shared as text
content; arbitrary binary path attachments require a host-app `ContentProvider`.

## Android file attachments

Android receivers should be given `content://` URIs, not private app filesystem
paths. A `ContentProvider` is the Android component that turns an app-private
file into a URI another app can read temporarily. A provider authority is the
unique name Android uses to route that URI back to the app that owns the
provider. For example, an app whose package id is `dev.example.notes` might use:

```text
dev.example.notes.robius.share
```

That authority must be unique per installed app. `robius-share` cannot use one
global authority like `robius.share`, because two different apps that depend on
this crate would then declare the same provider authority and conflict at
install time. The authority also has to appear in the final Android manifest;
it cannot be registered after the app starts.

This provider setup is only required by `robius-share` when all of these are
true:

- the app is running on Android 8 or 9, API 26 through 28;
- the payload is an app-private filesystem path;
- the file must be shared as a real attachment rather than as text content;
- the file is an arbitrary binary file, such as a PDF, image, video, archive, or
custom document.

It is not needed for `content://` URIs returned by `robius-file-picker`, because
those URIs already come from a provider. It is also not needed for
`add_file(path)` on Android 10 and newer, because `robius-share` copies the file
to a shareable MediaStore item before opening the Android Sharesheet. If your
app already has a `FileProvider` or custom `ContentProvider`, you can reuse that
instead of adding a separate Robius-specific provider.

The normal app integration is:

```xml
<provider
android:name="androidx.core.content.FileProvider"
android:authorities="${applicationId}.robius.share"
android:exported="false"
android:grantUriPermissions="true">
<meta-data
android:name="android.support.FILE_PROVIDER_PATHS"
android:resource="@xml/robius_share_paths" />
</provider>
```

with a matching `res/xml/robius_share_paths.xml`, for example:

```xml
<paths>
<cache-path name="robius-share-cache" path="robius-share/" />
<files-path name="robius-share-files" path="robius-share/" />
</paths>
```

For Makepad apps, the same idea can be expressed in
`resources/android/AndroidManifest.xml.template` using Makepad's package token:

```xml
android:authorities="{package_id}.robius.share"
```

Once your app has converted a file path into a provider-backed `content://` URI,
pass that URI to `robius-share`:

```rust
use robius_share::ShareSheet;

let uri = "content://dev.example.notes.robius.share/robius-share-cache/report.pdf";

ShareSheet::new()
.set_title("Share report")
.add_file_uri_with_mime_type(uri, "application/pdf")
.share()?;
# Ok::<(), robius_share::Error>(())
```

Use `add_file(path)` when you have a normal filesystem path and the current
platform can make that path shareable:

```rust
use std::{env, fs};
use robius_share::ShareSheet;

let path = env::temp_dir().join("robius-share-report.txt");
fs::write(&path, "Generated report\n")?;

ShareSheet::new()
.set_title("Share report")
.add_file_with_mime_type(&path, "text/plain")
.share()?;
# Ok::<(), Box<dyn std::error::Error>>(())
```

That `add_file(...)` example is a true file attachment on iOS, macOS, Windows,
Linux, and Android 10 or newer. On Android 8 and 9, the crate can share small
text files as text content, but arbitrary binary paths such as PDFs, images, or
videos need the host app to provide a `content://` URI and call `add_file_uri`.

Android's FileProvider setup is documented here:
<https://developer.android.com/training/secure-file-sharing/setup-sharing>.
Android's MediaStore behavior is documented here:
<https://developer.android.com/training/data-storage/shared/media>.

See [TESTING.md](TESTING.md) for the automated checks and native test cases
used to validate this crate across platforms.
Loading