Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
40 commits
Select commit Hold shift + click to select a range
57f9d56
feat(android): prepare for multi-window support
lucasfernog Oct 22, 2025
a71c707
add android multiwindow apis
lucasfernog Oct 23, 2025
f12df40
activity_name getter, JS API, auto inherit from manager on builder::new
lucasfernog Oct 24, 2025
c389ba4
refactor(core): improve iOS log messages from stdout/stderr
lucasfernog Oct 28, 2025
9e0bbff
patch
lucasfernog Oct 28, 2025
067ef61
Merge branch 'dev' into feat/mobile-multi-window
lucasfernog Oct 28, 2025
6854f8b
Merge branch 'refactor/ios-stdout-stderr-log' into feat/mobile-multi-…
lucasfernog Oct 28, 2025
b5d874d
use context (created_by_activity_name) from caller on TS API
lucasfernog Oct 29, 2025
bcc25c0
add scene identifier getter and setter
lucasfernog Oct 29, 2025
e4ccc32
expose apis
lucasfernog Oct 29, 2025
9a360af
fix desktop build
lucasfernog Oct 29, 2025
c5dbb02
Merge remote-tracking branch 'origin/dev' into feat/mobile-multi-window
lucasfernog Oct 30, 2025
6b94116
fix window from config
lucasfernog Oct 30, 2025
a602008
update lock
lucasfernog Oct 30, 2025
eb51b13
size is now respected on iOS, so default to None
lucasfernog Oct 30, 2025
f5f9b3d
add SceneRequested event
lucasfernog Nov 6, 2025
220cc2a
Merge remote-tracking branch 'origin/dev' into feat/mobile-multi-window
lucasfernog Nov 12, 2025
6eb2dee
update features
lucasfernog Nov 12, 2025
bfc74a7
fix default size
lucasfernog Nov 12, 2025
e97e2e5
Revert "fix default size"
lucasfernog Nov 12, 2025
11fc78b
ignore size from config
lucasfernog Nov 12, 2025
e5f65a6
lint
lucasfernog Nov 12, 2025
1702b27
Merge remote-tracking branch 'origin/dev' into feat/mobile-multi-window
lucasfernog Nov 17, 2025
2172252
use git
lucasfernog Nov 17, 2025
7f953fc
change file
lucasfernog Nov 17, 2025
8934778
feat(ios): add file association support
lucasfernog Nov 17, 2025
b522d94
feat(mobile): implement file association
lucasfernog Nov 17, 2025
8c28838
Merge branch 'feat/mobile-multi-window' into feat/mobile-file-associa…
lucasfernog Nov 17, 2025
7f02cf5
add supports_multiple_windows API
lucasfernog Dec 2, 2025
9d20d69
add android_intent_action_filters
lucasfernog Dec 2, 2025
98e2a44
fix custom type
lucasfernog Dec 3, 2025
12aaef6
add androidx.lifecycle:lifecycle-process
lucasfernog Dec 3, 2025
a39fb82
Merge branch 'feat/mobile-multi-window' into feat/mobile-file-associa…
lucasfernog Dec 3, 2025
00dcedc
update wry
lucasfernog Dec 3, 2025
58ac70f
Merge branch 'feat/mobile-multi-window' into feat/mobile-file-associa…
lucasfernog Dec 3, 2025
00d5db6
Merge remote-tracking branch 'origin/dev' into feat/mobile-multi-window
lucasfernog Dec 28, 2025
afb2268
Merge branch 'feat/mobile-multi-window' into feat/mobile-file-associa…
lucasfernog Dec 28, 2025
6a07a6a
Merge remote-tracking branch 'origin/dev' into feat/mobile-file-assoc…
FabianLars Apr 13, 2026
461f53f
remove git patches
FabianLars Apr 13, 2026
22e50d9
merge mistakes
FabianLars Apr 13, 2026
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
9 changes: 9 additions & 0 deletions .changes/file-association.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,9 @@
---
"tauri": minor:feat
"tauri-build": minor:feat
"tauri-plugin": minor:feat
"tauri-cli": minor:feat
"tauri-bundler": minor:feat
---

Implement file association for Android and iOS.
7 changes: 7 additions & 0 deletions .changes/mobile-file-associations.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,7 @@
---
"tauri": minor:feat
"tauri-runtime": minor:feat
"tauri-runtime-wry": minor:feat
---

Trigger `RunEvent::Opened` on Android.
1 change: 1 addition & 0 deletions Cargo.lock

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

5 changes: 5 additions & 0 deletions crates/tauri-build/src/lib.rs
Original file line number Diff line number Diff line change
Expand Up @@ -497,6 +497,11 @@ pub fn try_build(attributes: Attributes) -> Result<()> {

if let Some(project_dir) = env::var_os("TAURI_ANDROID_PROJECT_PATH").map(PathBuf::from) {
mobile::generate_gradle_files(project_dir)?;

// Update Android manifest with file associations
if let Some(associations) = config.bundle.file_associations.as_ref() {
mobile::update_android_manifest_file_associations(associations)?;
}
}

cfg_alias("dev", is_dev());
Expand Down
141 changes: 139 additions & 2 deletions crates/tauri-build/src/mobile.rs
Original file line number Diff line number Diff line change
Expand Up @@ -2,10 +2,147 @@
// SPDX-License-Identifier: Apache-2.0
// SPDX-License-Identifier: MIT

use std::path::PathBuf;
use std::{collections::HashSet, path::PathBuf};

use anyhow::{Context, Result};
use tauri_utils::write_if_changed;
use tauri_utils::{config::AndroidIntentAction, write_if_changed};

/// Updates the Android manifest to add file association intent filters
pub fn update_android_manifest_file_associations(
associations: &[tauri_utils::config::FileAssociation],
) -> Result<()> {
if associations.is_empty() {
return Ok(());
}

let intent_filters = generate_file_association_intent_filters(associations);
tauri_utils::build::update_android_manifest("tauri-file-associations", "activity", intent_filters)
}

fn generate_file_association_intent_filters(
associations: &[tauri_utils::config::FileAssociation],
) -> String {
let mut filters = String::new();

for association in associations {
// Get mime types - use explicit mime_type, or infer from extensions
let mut mime_types = HashSet::new();

if let Some(mime_type) = &association.mime_type {
mime_types.insert((
mime_type.clone(),
association.android_intent_action_filters.clone(),
));
} else {
// Infer mime types from extensions
for ext in &association.ext {
if let Some(mime) = extension_to_mime_type(&ext.0) {
mime_types.insert((mime, association.android_intent_action_filters.clone()));
}
}
}

// If we have mime types, create intent filters
if !mime_types.is_empty() {
for (mime_type, actions) in &mime_types {
filters.push_str("<intent-filter>\n");

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Tauri config has rank but this is not being used on Android.

{
  "ext": ["png"],
  "mimeType": "image/png",
  "rank": "Owner"  // ← This exists in config
}

Android can specify a priority on intent filters which is a signed integer between -1000 to 1000.

<intent-filter android:priority="1000">
    <action android:name="android.intent.action.VIEW" />
    <data android:mimeType="image/png" />
</intent-filter>

Copy link
Copy Markdown
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

the priority is only related to other intent filters in the same app
rank is a macOS config that describes whether the app is the owner of a file type or just a secondary viewer.

if let Some(actions) = actions {
for action in actions {
let action = match action {
AndroidIntentAction::Send => "SEND",
AndroidIntentAction::SendMultiple => "SEND_MULTIPLE",
AndroidIntentAction::View => "VIEW",
_ => unimplemented!(),
};
filters.push_str(&format!(
" <action android:name=\"android.intent.action.{action}\" />\n"
));
}
} else {
filters.push_str(" <action android:name=\"android.intent.action.SEND\" />\n");
filters.push_str(" <action android:name=\"android.intent.action.SEND_MULTIPLE\" />\n");
filters.push_str(" <action android:name=\"android.intent.action.VIEW\" />\n");
}
filters.push_str(" <category android:name=\"android.intent.category.DEFAULT\" />\n");
filters.push_str(" <category android:name=\"android.intent.category.BROWSABLE\" />\n");

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

We are missing ACTION_VIEW e.g. user taps in file manager. Ideally, if would be good if we could define which actions we which to listen to.

Copy link
Copy Markdown
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

From Android Docs:

BROWSABLE - The target activity allows itself to be started by a web browser to display data referenced by a link, such as an image or an e-mail message.

This is related to deep-linking not file association. Does this need to be handled differently? How will this interact with the deep-link plugin?

Copy link
Copy Markdown
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

the deep-link plugin will need a small change to actually detect that the intent contains a URL that was registered as a deep link - currently it always emits the event

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Thanks @lucasfernog. Is this task being tracked somewhere? Ensuring that both features continue to work together is an important requirement.

filters.push_str(&format!(
" <data android:mimeType=\"{}\" />\n",
mime_type
));

// Add file scheme and path patterns for extensions
if !association.ext.is_empty() {
// Create path patterns for each extension
// Android's pathPattern needs \\. (double backslash-dot) in XML to match a literal dot
let path_patterns: Vec<String> = association
.ext
.iter()
.map(|ext| format!(".*\\\\.{}", ext.0))

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Android's pathPattern has known limitations e.g. Doesn't match paths with multiple dots (e.g., file.backup.png).

Workarounds:

  1. Use multiple patterns
<data android:pathPattern=".*\\.png" />
<data android:pathPattern=".*\\..*\\.png" />
<data android:pathPattern=".*\\..*\\..*\\.png" />
  1. Use MIME type only
<intent-filter>
    <action android:name="android.intent.action.SEND" />
    <data android:mimeType="image/png" />
    <!-- No pathPattern - rely on MIME type -->
</intent-filter>
  1. Scheme + MIME Type
<intent-filter>
    <action android:name="android.intent.action.VIEW" />
    <data android:scheme="file" />
    <data android:scheme="content" />
    <data android:mimeType="image/png" />
</intent-filter

If filters are not generated properly, the only way forward is then to use a wildcard (*) filter which associates the app with all file/MIME types - this is not an ideal UX experience.

Copy link
Copy Markdown
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

this can be accomplished by just setting a mimeType in the file association config

.collect();

for pattern in &path_patterns {
filters.push_str(&format!(
" <data android:pathPattern=\"{}\" />\n",
pattern
));
}
}

filters.push_str("</intent-filter>\n");
}
} else if !association.ext.is_empty() {
// If no mime type but we have extensions, use a generic approach
filters.push_str("<intent-filter>\n");
filters.push_str(" <action android:name=\"android.intent.action.VIEW\" />\n");
filters.push_str(" <category android:name=\"android.intent.category.DEFAULT\" />\n");
filters.push_str(" <category android:name=\"android.intent.category.BROWSABLE\" />\n");

for ext in &association.ext {
// Android's pathPattern needs \\. (double backslash-dot) in XML to match a literal dot
filters.push_str(&format!(
" <data android:pathPattern=\".*\\\\.{}\" />\n",
ext.0
));
}

filters.push_str("</intent-filter>\n");
}
}

filters
}

fn extension_to_mime_type(ext: &str) -> Option<String> {

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

While this covers ~25 file types it is important to include support for custom file extensions/MIME types.

We need to be able to define these in tauri.conf.json for desktop/mobile so they are available to the builder in order to generate correct platform-specific metadata e.g. info.plist, AndroidManifest.xml.

Copy link
Copy Markdown
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

these are just some default values because a mime type is required for the iOS file association to work; you can still define those manually in tauri.conf.json to support other types

Some(
match ext.to_lowercase().as_str() {
"png" => "image/png",
"jpg" | "jpeg" => "image/jpeg",
"gif" => "image/gif",
"bmp" => "image/bmp",
"webp" => "image/webp",
"svg" => "image/svg+xml",
"ico" => "image/x-icon",
"tiff" | "tif" => "image/tiff",
"heic" | "heif" => "image/heic",
"mp4" => "video/mp4",
"mov" => "video/quicktime",
"avi" => "video/x-msvideo",
"mkv" => "video/x-matroska",
"mp3" => "audio/mpeg",
"wav" => "audio/wav",
"aac" => "audio/aac",
"m4a" => "audio/mp4",
"pdf" => "application/pdf",
"txt" => "text/plain",
"html" | "htm" => "text/html",
"json" => "application/json",
"xml" => "application/xml",
"rtf" => "application/rtf",
_ => return None,
}
.to_string(),
)
}

pub fn generate_gradle_files(project_dir: PathBuf) -> Result<()> {
let gradle_settings_path = project_dir.join("tauri.settings.gradle");
Expand Down
103 changes: 8 additions & 95 deletions crates/tauri-bundler/src/bundle/macos/app.rs
Original file line number Diff line number Diff line change
Expand Up @@ -262,102 +262,15 @@ fn create_info_plist(
}

if let Some(associations) = settings.file_associations() {
let exported_associations = associations
.iter()
.filter_map(|association| {
association.exported_type.as_ref().map(|exported_type| {
let mut dict = plist::Dictionary::new();

dict.insert(
"UTTypeIdentifier".into(),
exported_type.identifier.clone().into(),
);
if let Some(description) = &association.description {
dict.insert("UTTypeDescription".into(), description.clone().into());
}
if let Some(conforms_to) = &exported_type.conforms_to {
dict.insert(
"UTTypeConformsTo".into(),
plist::Value::Array(conforms_to.iter().map(|s| s.clone().into()).collect()),
);
}

let mut specification = plist::Dictionary::new();
specification.insert(
"public.filename-extension".into(),
plist::Value::Array(
association
.ext
.iter()
.map(|s| s.to_string().into())
.collect(),
),
);
if let Some(mime_type) = &association.mime_type {
specification.insert("public.mime-type".into(), mime_type.clone().into());
}

dict.insert("UTTypeTagSpecification".into(), specification.into());

plist::Value::Dictionary(dict)
})
})
.collect::<Vec<_>>();

if !exported_associations.is_empty() {
plist.insert(
"UTExportedTypeDeclarations".into(),
plist::Value::Array(exported_associations),
);
if let Some(file_associations_plist) =
tauri_utils::config::file_associations_plist(associations)
{
if let Some(plist_dict) = file_associations_plist.as_dictionary() {
for (key, value) in plist_dict {
plist.insert(key.clone(), value.clone());
}
}
}

plist.insert(
"CFBundleDocumentTypes".into(),
plist::Value::Array(
associations
.iter()
.map(|association| {
let mut dict = plist::Dictionary::new();

if !association.ext.is_empty() {
dict.insert(
"CFBundleTypeExtensions".into(),
plist::Value::Array(
association
.ext
.iter()
.map(|ext| ext.to_string().into())
.collect(),
),
);
}

if let Some(content_types) = &association.content_types {
dict.insert(
"LSItemContentTypes".into(),
plist::Value::Array(content_types.iter().map(|s| s.to_string().into()).collect()),
);
}

dict.insert(
"CFBundleTypeName".into(),
association
.name
.as_ref()
.unwrap_or(&association.ext[0].0)
.to_string()
.into(),
);
dict.insert(
"CFBundleTypeRole".into(),
association.role.to_string().into(),
);
dict.insert("LSHandlerRank".into(), association.rank.to_string().into());
plist::Value::Dictionary(dict)
})
.collect(),
),
);
}

if let Some(path) = bundle_icon_file {
Expand Down
36 changes: 36 additions & 0 deletions crates/tauri-cli/config.schema.json
Original file line number Diff line number Diff line change
Expand Up @@ -2517,6 +2517,16 @@
"type": "null"
}
]
},
"androidIntentActionFilters": {
"description": "Intent action filters for this file association.\n\n By default all filters are used.",
"type": [
"array",
"null"
],
"items": {
"$ref": "#/definitions/AndroidIntentAction"
}
}
},
"additionalProperties": false
Expand Down Expand Up @@ -2622,6 +2632,32 @@
},
"additionalProperties": false
},
"AndroidIntentAction": {
"description": "Android intent action.",
"oneOf": [
{
"description": "ACTION_SEND.\n\n <https://developer.android.com/reference/android/content/Intent#ACTION_SEND>",
"type": "string",
"enum": [
"send"
]
},
{
"description": "ACTION_SEND_MULTIPLE.\n\n <https://developer.android.com/reference/android/content/Intent#ACTION_SEND_MULTIPLE>",
"type": "string",
"enum": [
"sendMultiple"
]
},
{
"description": "ACTION_VIEW.\n\n <https://developer.android.com/reference/android/content/Intent#ACTION_SEND>",
"type": "string",
"enum": [
"view"
]
}
]
},
"WindowsConfig": {
"description": "Windows bundler configuration.\n\n See more: <https://v2.tauri.app/reference/config/#windowsconfig>",
"type": "object",
Expand Down
11 changes: 9 additions & 2 deletions crates/tauri-cli/src/mobile/ios/build.rs
Original file line number Diff line number Diff line change
Expand Up @@ -243,8 +243,15 @@ pub fn run(options: Options, noise_level: NoiseLevel, dirs: &Dirs) -> Result<Bui
if dirs.tauri.join("Info.ios.plist").exists() {
src_plists.push(dirs.tauri.join("Info.ios.plist").into());
}
if let Some(info_plist) = &tauri_config.bundle.ios.info_plist {
src_plists.push(info_plist.clone().into());
{
if let Some(info_plist) = &tauri_config.bundle.ios.info_plist {
src_plists.push(info_plist.clone().into());
}
if let Some(associations) = tauri_config.bundle.file_associations.as_ref() {
if let Some(file_associations) = tauri_utils::config::file_associations_plist(associations) {
src_plists.push(file_associations.into());
}
}
}
let merged_info_plist = merge_plist(src_plists)?;
merged_info_plist
Expand Down
11 changes: 9 additions & 2 deletions crates/tauri-cli/src/mobile/ios/dev.rs
Original file line number Diff line number Diff line change
Expand Up @@ -230,8 +230,15 @@ fn run_command(options: Options, noise_level: NoiseLevel, dirs: Dirs) -> Result<
if dirs.tauri.join("Info.ios.plist").exists() {
src_plists.push(dirs.tauri.join("Info.ios.plist").into());
}
if let Some(info_plist) = &tauri_config.bundle.ios.info_plist {
src_plists.push(info_plist.clone().into());
{
if let Some(info_plist) = &tauri_config.bundle.ios.info_plist {
src_plists.push(info_plist.clone().into());
}
if let Some(associations) = tauri_config.bundle.file_associations.as_ref() {
if let Some(file_associations) = tauri_utils::config::file_associations_plist(associations) {
src_plists.push(file_associations.into());
}
}
}
let merged_info_plist = merge_plist(src_plists)?;
merged_info_plist
Expand Down
Loading
Loading