-
-
Notifications
You must be signed in to change notification settings - Fork 3.2k
feat(mobile): add file association support #14486
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
base: feat/mobile-multi-window
Are you sure you want to change the base?
Changes from all commits
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
| 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. |
| 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. |
Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.
| Original file line number | Diff line number | Diff line change |
|---|---|---|
|
|
@@ -2,11 +2,130 @@ | |
| // 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; | ||
|
|
||
| /// 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()); | ||
| } 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); | ||
| } | ||
| } | ||
| } | ||
|
|
||
| // If we have mime types, create intent filters | ||
| if !mime_types.is_empty() { | ||
| for mime_type in &mime_types { | ||
| filters.push_str("<intent-filter>\n"); | ||
| 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(" <category android:name=\"android.intent.category.DEFAULT\" />\n"); | ||
| filters.push_str(" <category android:name=\"android.intent.category.BROWSABLE\" />\n"); | ||
|
Contributor
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. We are missing
Contributor
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. From Android Docs:
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? |
||
| 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)) | ||
|
Contributor
There was a problem hiding this comment. Choose a reason for hiding this commentThe 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:
<data android:pathPattern=".*\\.png" />
<data android:pathPattern=".*\\..*\\.png" />
<data android:pathPattern=".*\\..*\\..*\\.png" />
<intent-filter>
<action android:name="android.intent.action.SEND" />
<data android:mimeType="image/png" />
<!-- No pathPattern - rely on MIME type -->
</intent-filter>
<intent-filter>
<action android:name="android.intent.action.VIEW" />
<data android:scheme="file" />
<data android:scheme="content" />
<data android:mimeType="image/png" />
</intent-filterIf 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. |
||
| .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> { | ||
|
Contributor
There was a problem hiding this comment. Choose a reason for hiding this commentThe 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. |
||
| 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"); | ||
| let app_build_gradle_path = project_dir.join("app").join("tauri.build.gradle.kts"); | ||
|
|
||
There was a problem hiding this comment.
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.