Compare commits

...

2 Commits

Author SHA1 Message Date
Lucas Fernandes Nogueira
1f032989f2 Merge branch 'dev' into feat/android-embedded-activity-generator 2026-04-16 15:57:35 -03:00
Lucas Nogueira
8a8d24e625 feat(core): add androidEmbedding config
automatically configures the project to support Activity embedding and generate split rules from config
2026-04-16 15:51:17 -03:00
9 changed files with 1085 additions and 8 deletions

View File

@@ -0,0 +1,5 @@
---
"tauri-utils": minor:feat
---
Added `tauri.conf.json > bundle > android > activityEmbedding` config to enable Activity embedding and define split rules for Android multi-window support.

View File

@@ -0,0 +1,5 @@
---
"tauri-build": minor:feat
---
Enable Activity embedding for Android when `tauri.conf.json > bundle > android > activityEmbedding > enabled` is true.

View File

@@ -0,0 +1,5 @@
---
"tauri-build": minor:feat
---
Generate Android split rules based on `tauri.conf.json > bundle > android > activityEmbedding > splitRules`.

View File

@@ -496,12 +496,22 @@ pub fn try_build(attributes: Attributes) -> Result<()> {
println!("cargo:rustc-env=TAURI_ANDROID_PACKAGE_NAME_PREFIX={android_package_prefix}");
if let Some(project_dir) = env::var_os("TAURI_ANDROID_PROJECT_PATH").map(PathBuf::from) {
mobile::generate_gradle_files(project_dir)?;
let activity_embedding = config
.bundle
.android
.activity_embedding
.as_ref()
.filter(|c| c.enabled && !c.split_rules.is_empty());
mobile::generate_gradle_files(project_dir.clone(), activity_embedding)?;
// Update Android manifest with file associations
if let Some(associations) = config.bundle.file_associations.as_ref() {
mobile::update_android_manifest_file_associations(associations)?;
}
if let Some(embedding) = activity_embedding {
mobile::setup_activity_embedding(&project_dir, embedding, &config.identifier)?;
}
}
cfg_alias("dev", is_dev());

View File

@@ -2,10 +2,19 @@
// SPDX-License-Identifier: Apache-2.0
// SPDX-License-Identifier: MIT
use std::{collections::HashSet, path::PathBuf};
use std::{
collections::HashSet,
path::{Path, PathBuf},
};
use anyhow::{Context, Result};
use tauri_utils::{config::AndroidIntentAction, write_if_changed};
use tauri_utils::{
config::{
ActivityEmbeddingConfig, AndroidIntentAction, EmbeddingAspectRatio, SplitFinishBehavior,
SplitLayoutDirection, SplitPairRule, SplitType,
},
write_if_changed,
};
/// Updates the Android manifest to add file association intent filters
pub fn update_android_manifest_file_associations(
@@ -144,7 +153,10 @@ fn extension_to_mime_type(ext: &str) -> Option<String> {
)
}
pub fn generate_gradle_files(project_dir: PathBuf) -> Result<()> {
pub fn generate_gradle_files(
project_dir: PathBuf,
activity_embedding: Option<&ActivityEmbeddingConfig>,
) -> 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");
@@ -156,6 +168,11 @@ dependencies {
implementation(\"androidx.lifecycle:lifecycle-process:2.10.0\")"
.to_string();
if activity_embedding.is_some() {
app_build_gradle.push_str("\n implementation(\"androidx.window:window:1.5.0\")");
app_build_gradle.push_str("\n implementation(\"androidx.startup:startup-runtime:1.2.0\")");
}
for (env, value) in std::env::vars_os() {
let env = env.to_string_lossy();
if env.starts_with("DEP_") && env.ends_with("_ANDROID_LIBRARY_PATH") {
@@ -187,7 +204,6 @@ dependencies {
app_build_gradle.push_str("\n}");
// Overwrite only if changed to not trigger rebuilds
write_if_changed(&gradle_settings_path, gradle_settings)
.context("failed to write tauri.settings.gradle")?;
@@ -199,3 +215,256 @@ dependencies {
Ok(())
}
/// Configures Android Activity Embedding: updates the manifest, generates Gradle
/// dependencies, and writes the `TauriSplitInitializer.kt` file.
pub fn setup_activity_embedding(
project_dir: &Path,
config: &ActivityEmbeddingConfig,
identifier: &str,
) -> Result<()> {
if config.split_rules.is_empty() {
return Ok(());
}
let package_name = identifier_to_android_package(identifier);
update_android_manifest_activity_embedding(config, &package_name)?;
generate_split_initializer(project_dir, config, &package_name)?;
Ok(())
}
fn identifier_to_android_package(identifier: &str) -> String {
identifier
.split('.')
.map(|s| s.replace('-', "_"))
.collect::<Vec<_>>()
.join(".")
}
fn update_android_manifest_activity_embedding(
config: &ActivityEmbeddingConfig,
package_name: &str,
) -> Result<()> {
let mut xml = String::new();
xml.push_str(
"<property\n \
android:name=\"android.window.PROPERTY_ACTIVITY_EMBEDDING_SPLITS_ENABLED\"\n \
android:value=\"true\" />\n",
);
xml.push_str(&format!(
"<provider\n \
android:name=\"androidx.startup.InitializationProvider\"\n \
android:authorities=\"${{applicationId}}.androidx-startup\"\n \
android:exported=\"false\">\n \
<meta-data\n \
android:name=\"{package_name}.TauriSplitInitializer\"\n \
android:value=\"androidx.startup\" />\n\
</provider>\n"
));
let mut declared = HashSet::new();
for rule in &config.split_rules {
let name = if rule.secondary.contains('.') {
rule.secondary.clone()
} else {
format!(".{}", rule.secondary)
};
if !declared.insert(name.clone()) {
continue;
}
xml.push_str(&format!(
"<activity\n \
android:name=\"{name}\"\n \
android:exported=\"false\"\n \
android:configChanges=\"orientation|screenSize|screenLayout|smallestScreenSize\" />\n"
));
}
tauri_utils::build::update_android_manifest("tauri-activity-embedding", "application", xml)
}
fn generate_split_initializer(
project_dir: &Path,
config: &ActivityEmbeddingConfig,
package_name: &str,
) -> Result<()> {
let package_dir = package_name.replace('.', "/");
let source_dir = project_dir
.join("app/src/main/java")
.join(&package_dir)
.join("generated");
std::fs::create_dir_all(&source_dir)
.context("failed to create Android source directory for split initializer")?;
let mut kt = String::new();
kt.push_str(&format!(
"// THIS IS AN AUTOGENERATED FILE. DO NOT EDIT THIS FILE DIRECTLY.\npackage {package_name}\n\n"
));
kt.push_str(
"import android.content.ComponentName\n\
import android.content.Context\n\
import androidx.startup.Initializer\n\
import androidx.window.embedding.EmbeddingAspectRatio\n\
import androidx.window.embedding.RuleController\n\
import androidx.window.embedding.SplitAttributes\n\
import androidx.window.embedding.SplitPairFilter\n\
import androidx.window.embedding.SplitPairRule\n\
import androidx.window.embedding.SplitRule\n\n",
);
kt.push_str(
"class TauriSplitInitializer : Initializer<RuleController> {\n \
override fun create(context: Context): RuleController {\n \
val ruleController = RuleController.getInstance(context)\n\n",
);
for (i, rule) in config.split_rules.iter().enumerate() {
append_split_rule(&mut kt, i, rule, package_name);
}
kt.push_str(
" return ruleController\n \
}\n\n \
override fun dependencies(): List<Class<out Initializer<*>>> = emptyList()\n\
}\n",
);
let initializer_path = source_dir.join("TauriSplitInitializer.kt");
write_if_changed(&initializer_path, kt).context("failed to write TauriSplitInitializer.kt")?;
Ok(())
}
fn append_split_rule(kt: &mut String, i: usize, rule: &SplitPairRule, package_name: &str) {
let primary = qualify_activity(&rule.primary, package_name);
let secondary = qualify_activity(&rule.secondary, package_name);
let secondary_intent_action = rule
.secondary_intent_action
.as_deref()
.map(|s| format!("{s:?}"))
.unwrap_or_else(|| "null".to_string());
kt.push_str(&format!(
" val filter{i} = SplitPairFilter(\n \
ComponentName(context, {primary}::class.java),\n \
ComponentName(context, {secondary}::class.java),\n \
{secondary_intent_action}\n \
)\n"
));
kt.push_str(&format!(
" val attrsBuilder{i} = SplitAttributes.Builder()\n"
));
if let Some(split_type) = &rule.split_type {
kt.push_str(&format!(
" attrsBuilder{i}.setSplitType({})\n",
kotlin_split_type(split_type)
));
}
if let Some(direction) = rule.layout_direction {
kt.push_str(&format!(
" attrsBuilder{i}.setLayoutDirection({})\n",
kotlin_layout_direction(direction)
));
}
kt.push_str(&format!(" val attrs{i} = attrsBuilder{i}.build()\n"));
kt.push_str(&format!(
" val ruleBuilder{i} = SplitPairRule.Builder(setOf(filter{i}))\n \
ruleBuilder{i}.setDefaultSplitAttributes(attrs{i})\n"
));
if let Some(dp) = rule.min_width_dp {
kt.push_str(&format!(" ruleBuilder{i}.setMinWidthDp({dp})\n"));
}
if let Some(dp) = rule.min_height_dp {
kt.push_str(&format!(" ruleBuilder{i}.setMinHeightDp({dp})\n"));
}
if let Some(dp) = rule.min_smallest_width_dp {
kt.push_str(&format!(
" ruleBuilder{i}.setMinSmallestWidthDp({dp})\n"
));
}
if let Some(ar) = &rule.max_aspect_ratio_in_portrait {
kt.push_str(&format!(
" ruleBuilder{i}.setMaxAspectRatioInPortrait({})\n",
kotlin_aspect_ratio(ar)
));
}
if let Some(ar) = &rule.max_aspect_ratio_in_landscape {
kt.push_str(&format!(
" ruleBuilder{i}.setMaxAspectRatioInLandscape({})\n",
kotlin_aspect_ratio(ar)
));
}
if let Some(b) = rule.finish_primary_with_secondary {
kt.push_str(&format!(
" ruleBuilder{i}.setFinishPrimaryWithSecondary({})\n",
kotlin_finish_behavior(b)
));
}
if let Some(b) = rule.finish_secondary_with_primary {
kt.push_str(&format!(
" ruleBuilder{i}.setFinishSecondaryWithPrimary({})\n",
kotlin_finish_behavior(b)
));
}
if let Some(clear_top) = rule.clear_top {
kt.push_str(&format!(
" ruleBuilder{i}.setClearTop({clear_top})\n"
));
}
if let Some(tag) = &rule.tag {
kt.push_str(&format!(" ruleBuilder{i}.setTag({tag:?})\n"));
}
kt.push_str(&format!(
" val rule{i} = ruleBuilder{i}.build()\n \
ruleController.addRule(rule{i})\n\n"
));
}
fn qualify_activity(name: &str, package_name: &str) -> String {
if name.contains('.') {
name.to_string()
} else {
format!("{package_name}.{name}")
}
}
fn kotlin_split_type(split_type: &SplitType) -> String {
match split_type {
SplitType::Ratio(r) => format!("SplitAttributes.SplitType.ratio({r}f)"),
SplitType::Expand => "SplitAttributes.SplitType.SPLIT_TYPE_EXPAND".to_string(),
SplitType::Hinge => "SplitAttributes.SplitType.SPLIT_TYPE_HINGE".to_string(),
}
}
fn kotlin_layout_direction(direction: SplitLayoutDirection) -> &'static str {
match direction {
SplitLayoutDirection::Locale => "SplitAttributes.LayoutDirection.LOCALE",
SplitLayoutDirection::LeftToRight => "SplitAttributes.LayoutDirection.LEFT_TO_RIGHT",
SplitLayoutDirection::RightToLeft => "SplitAttributes.LayoutDirection.RIGHT_TO_LEFT",
SplitLayoutDirection::TopToBottom => "SplitAttributes.LayoutDirection.TOP_TO_BOTTOM",
SplitLayoutDirection::BottomToTop => "SplitAttributes.LayoutDirection.BOTTOM_TO_TOP",
}
}
fn kotlin_finish_behavior(behavior: SplitFinishBehavior) -> &'static str {
match behavior {
SplitFinishBehavior::Never => "SplitRule.FinishBehavior.NEVER",
SplitFinishBehavior::Always => "SplitRule.FinishBehavior.ALWAYS",
SplitFinishBehavior::Adjacent => "SplitRule.FinishBehavior.ADJACENT",
}
}
fn kotlin_aspect_ratio(ar: &EmbeddingAspectRatio) -> String {
match ar {
EmbeddingAspectRatio::AlwaysAllow => "EmbeddingAspectRatio.ALWAYS_ALLOW".to_string(),
EmbeddingAspectRatio::AlwaysDisallow => "EmbeddingAspectRatio.ALWAYS_DISALLOW".to_string(),
EmbeddingAspectRatio::Ratio(r) => format!("EmbeddingAspectRatio.ratio({r}f)"),
}
}

View File

@@ -3914,10 +3914,306 @@
"description": "Whether to automatically increment the `versionCode` on each build.\n\n - If `true`, the generator will try to read the last `versionCode` from\n `tauri.properties` and increment it by 1 for every build.\n - If `false` or not set, it falls back to `version_code` or semver-derived logic.\n\n Note that to use this feature, you should remove `/tauri.properties` from `src-tauri/gen/android/app/.gitignore` so the current versionCode is committed to the repository.",
"default": false,
"type": "boolean"
},
"activityEmbedding": {
"description": "Activity embedding for large screens (tablets, foldables).\n\n When set and enabled, Tauri generates the Gradle dependencies, Android manifest entries,\n and a `TauriSplitInitializer` Kotlin class for split-screen activity layouts.",
"anyOf": [
{
"$ref": "#/definitions/ActivityEmbeddingConfig"
},
{
"type": "null"
}
]
}
},
"additionalProperties": false
},
"ActivityEmbeddingConfig": {
"description": "Configuration for Android Activity Embedding, which splits activities\n side by side on large screens.\n\n See <https://developer.android.com/guide/topics/large-screens/activity-embedding>",
"type": "object",
"properties": {
"enabled": {
"description": "When `false`, Tauri does not generate embedding Gradle entries, manifest updates, or `TauriSplitInitializer`.",
"default": true,
"type": "boolean"
},
"splitRules": {
"description": "Split pair rules defining how activities are laid out side by side.",
"default": [],
"type": "array",
"items": {
"$ref": "#/definitions/SplitPairRule"
}
}
},
"additionalProperties": false
},
"SplitPairRule": {
"description": "A split pair rule for Android Activity Embedding.\n\n Defines how a primary and secondary activity are displayed side by side\n on screens that satisfy the configured minimum dimensions.\n\n Mirrors the fields of [`androidx.window.embedding.SplitPairRule.Builder`] and\n the [`SplitPairFilter`] it accepts. Only [`primary`](Self::primary) and\n [`secondary`](Self::secondary) are required; all other fields fall back to\n the Android SDK defaults when omitted.\n\n [`androidx.window.embedding.SplitPairRule.Builder`]: https://developer.android.com/reference/androidx/window/embedding/SplitPairRule.Builder\n [`SplitPairFilter`]: https://developer.android.com/reference/androidx/window/embedding/SplitPairFilter",
"type": "object",
"required": [
"primary",
"secondary"
],
"properties": {
"primary": {
"description": "The primary activity class name, relative to the application package\n (e.g. `\"MainActivity\"` or a fully-qualified name like\n `\"com.example.MainActivity\"`).",
"type": "string"
},
"secondary": {
"description": "The secondary activity class name, relative to the application package\n (e.g. `\"DetailActivity\"` or a fully-qualified name).",
"type": "string"
},
"secondaryIntentAction": {
"description": "Optional intent action used to match the secondary activity when it is\n started via an implicit intent.",
"type": [
"string",
"null"
]
},
"splitType": {
"description": "How the parent window is split. When omitted, the SDK uses an equal\n 50/50 ratio.",
"anyOf": [
{
"$ref": "#/definitions/SplitType"
},
{
"type": "null"
}
]
},
"layoutDirection": {
"description": "The layout direction of the primary/secondary containers. Defaults to\n the system locale direction.",
"anyOf": [
{
"$ref": "#/definitions/SplitLayoutDirection"
},
{
"type": "null"
}
]
},
"minWidthDp": {
"description": "Minimum parent window width in dp for the split to apply. Defaults to the\n SDK value (`600`).",
"type": [
"integer",
"null"
],
"format": "uint32",
"minimum": 0.0
},
"minHeightDp": {
"description": "Minimum parent window height in dp for the split to apply. Defaults to the\n SDK value (`600`).",
"type": [
"integer",
"null"
],
"format": "uint32",
"minimum": 0.0
},
"minSmallestWidthDp": {
"description": "Minimum smallest width of the parent window in dp for the split to apply.\n Defaults to the SDK value (`600`).",
"type": [
"integer",
"null"
],
"format": "uint32",
"minimum": 0.0
},
"maxAspectRatioInPortrait": {
"description": "Maximum height/width aspect ratio (portrait) for which the split applies.",
"anyOf": [
{
"$ref": "#/definitions/EmbeddingAspectRatio"
},
{
"type": "null"
}
]
},
"maxAspectRatioInLandscape": {
"description": "Maximum height/width aspect ratio (landscape) for which the split applies.",
"anyOf": [
{
"$ref": "#/definitions/EmbeddingAspectRatio"
},
{
"type": "null"
}
]
},
"finishPrimaryWithSecondary": {
"description": "Behavior of the primary container when all activities in the secondary\n container finish. Defaults to [`SplitFinishBehavior::Never`].",
"anyOf": [
{
"$ref": "#/definitions/SplitFinishBehavior"
},
{
"type": "null"
}
]
},
"finishSecondaryWithPrimary": {
"description": "Behavior of the secondary container when all activities in the primary\n container finish. Defaults to [`SplitFinishBehavior::Always`].",
"anyOf": [
{
"$ref": "#/definitions/SplitFinishBehavior"
},
{
"type": "null"
}
]
},
"clearTop": {
"description": "Whether the existing secondary container and all activities in it should\n be destroyed when a new split is created using this rule.",
"type": [
"boolean",
"null"
]
},
"tag": {
"description": "Optional tag used to identify this rule at runtime.",
"type": [
"string",
"null"
]
}
},
"additionalProperties": false
},
"SplitType": {
"description": "How the parent window is split between primary and secondary containers.\n\n Mirrors [`androidx.window.embedding.SplitAttributes.SplitType`].\n\n [`androidx.window.embedding.SplitAttributes.SplitType`]: https://developer.android.com/reference/androidx/window/embedding/SplitAttributes.SplitType",
"oneOf": [
{
"description": "Splits the parent into two containers with the given weight\n for the primary container (0.0 exclusive to 1.0 exclusive).",
"type": "object",
"required": [
"ratio"
],
"properties": {
"ratio": {
"type": "number",
"format": "double"
}
},
"additionalProperties": false
},
{
"description": "The secondary container expands to cover the entire parent window.",
"type": "string",
"enum": [
"expand"
]
},
{
"description": "The split aligns with the device hinge/fold. Cannot be used as a\n default split type without hinge-aware devices.",
"type": "string",
"enum": [
"hinge"
]
}
]
},
"SplitLayoutDirection": {
"description": "A layout direction for an activity embedding split.\n\n Mirrors [`androidx.window.embedding.SplitAttributes.LayoutDirection`].\n\n [`androidx.window.embedding.SplitAttributes.LayoutDirection`]: https://developer.android.com/reference/androidx/window/embedding/SplitAttributes.LayoutDirection",
"oneOf": [
{
"description": "Use the system locale's layout direction.",
"type": "string",
"enum": [
"locale"
]
},
{
"description": "Primary on the left, secondary on the right.",
"type": "string",
"enum": [
"leftToRight"
]
},
{
"description": "Primary on the right, secondary on the left.",
"type": "string",
"enum": [
"rightToLeft"
]
},
{
"description": "Primary on top, secondary on the bottom.",
"type": "string",
"enum": [
"topToBottom"
]
},
{
"description": "Primary on the bottom, secondary on top.",
"type": "string",
"enum": [
"bottomToTop"
]
}
]
},
"EmbeddingAspectRatio": {
"description": "Maximum aspect ratio (height / width) of the parent window for which\n activity embedding should apply.\n\n Mirrors [`androidx.window.embedding.EmbeddingAspectRatio`].\n\n [`androidx.window.embedding.EmbeddingAspectRatio`]: https://developer.android.com/reference/androidx/window/embedding/EmbeddingAspectRatio",
"oneOf": [
{
"description": "Embedding always applies regardless of aspect ratio.",
"type": "string",
"enum": [
"alwaysAllow"
]
},
{
"description": "Embedding never applies in this orientation.",
"type": "string",
"enum": [
"alwaysDisallow"
]
},
{
"description": "Embedding applies when the parent window aspect ratio is less than or\n equal to this value. Must be greater than `1.0`.",
"type": "object",
"required": [
"ratio"
],
"properties": {
"ratio": {
"type": "number",
"format": "double"
}
},
"additionalProperties": false
}
]
},
"SplitFinishBehavior": {
"description": "Describes how an activity container should finish when the linked\n container finishes.\n\n Mirrors [`androidx.window.embedding.SplitRule.FinishBehavior`].\n\n [`androidx.window.embedding.SplitRule.FinishBehavior`]: https://developer.android.com/reference/androidx/window/embedding/SplitRule.FinishBehavior",
"oneOf": [
{
"description": "Never finish the container when the linked container finishes.",
"type": "string",
"enum": [
"never"
]
},
{
"description": "Always finish the container when the linked container finishes.",
"type": "string",
"enum": [
"always"
]
},
{
"description": "Finish the container only when the linked container finishes\n while they are displayed side-by-side.",
"type": "string",
"enum": [
"adjacent"
]
}
]
},
"PluginConfig": {
"description": "The plugin configs holds a HashMap mapping a plugin name to its configuration object.\n\n See more: <https://v2.tauri.app/reference/config/#pluginconfig>",
"type": "object",

View File

@@ -3914,10 +3914,306 @@
"description": "Whether to automatically increment the `versionCode` on each build.\n\n - If `true`, the generator will try to read the last `versionCode` from\n `tauri.properties` and increment it by 1 for every build.\n - If `false` or not set, it falls back to `version_code` or semver-derived logic.\n\n Note that to use this feature, you should remove `/tauri.properties` from `src-tauri/gen/android/app/.gitignore` so the current versionCode is committed to the repository.",
"default": false,
"type": "boolean"
},
"activityEmbedding": {
"description": "Activity embedding for large screens (tablets, foldables).\n\n When set and enabled, Tauri generates the Gradle dependencies, Android manifest entries,\n and a `TauriSplitInitializer` Kotlin class for split-screen activity layouts.",
"anyOf": [
{
"$ref": "#/definitions/ActivityEmbeddingConfig"
},
{
"type": "null"
}
]
}
},
"additionalProperties": false
},
"ActivityEmbeddingConfig": {
"description": "Configuration for Android Activity Embedding, which splits activities\n side by side on large screens.\n\n See <https://developer.android.com/guide/topics/large-screens/activity-embedding>",
"type": "object",
"properties": {
"enabled": {
"description": "When `false`, Tauri does not generate embedding Gradle entries, manifest updates, or `TauriSplitInitializer`.",
"default": true,
"type": "boolean"
},
"splitRules": {
"description": "Split pair rules defining how activities are laid out side by side.",
"default": [],
"type": "array",
"items": {
"$ref": "#/definitions/SplitPairRule"
}
}
},
"additionalProperties": false
},
"SplitPairRule": {
"description": "A split pair rule for Android Activity Embedding.\n\n Defines how a primary and secondary activity are displayed side by side\n on screens that satisfy the configured minimum dimensions.\n\n Mirrors the fields of [`androidx.window.embedding.SplitPairRule.Builder`] and\n the [`SplitPairFilter`] it accepts. Only [`primary`](Self::primary) and\n [`secondary`](Self::secondary) are required; all other fields fall back to\n the Android SDK defaults when omitted.\n\n [`androidx.window.embedding.SplitPairRule.Builder`]: https://developer.android.com/reference/androidx/window/embedding/SplitPairRule.Builder\n [`SplitPairFilter`]: https://developer.android.com/reference/androidx/window/embedding/SplitPairFilter",
"type": "object",
"required": [
"primary",
"secondary"
],
"properties": {
"primary": {
"description": "The primary activity class name, relative to the application package\n (e.g. `\"MainActivity\"` or a fully-qualified name like\n `\"com.example.MainActivity\"`).",
"type": "string"
},
"secondary": {
"description": "The secondary activity class name, relative to the application package\n (e.g. `\"DetailActivity\"` or a fully-qualified name).",
"type": "string"
},
"secondaryIntentAction": {
"description": "Optional intent action used to match the secondary activity when it is\n started via an implicit intent.",
"type": [
"string",
"null"
]
},
"splitType": {
"description": "How the parent window is split. When omitted, the SDK uses an equal\n 50/50 ratio.",
"anyOf": [
{
"$ref": "#/definitions/SplitType"
},
{
"type": "null"
}
]
},
"layoutDirection": {
"description": "The layout direction of the primary/secondary containers. Defaults to\n the system locale direction.",
"anyOf": [
{
"$ref": "#/definitions/SplitLayoutDirection"
},
{
"type": "null"
}
]
},
"minWidthDp": {
"description": "Minimum parent window width in dp for the split to apply. Defaults to the\n SDK value (`600`).",
"type": [
"integer",
"null"
],
"format": "uint32",
"minimum": 0.0
},
"minHeightDp": {
"description": "Minimum parent window height in dp for the split to apply. Defaults to the\n SDK value (`600`).",
"type": [
"integer",
"null"
],
"format": "uint32",
"minimum": 0.0
},
"minSmallestWidthDp": {
"description": "Minimum smallest width of the parent window in dp for the split to apply.\n Defaults to the SDK value (`600`).",
"type": [
"integer",
"null"
],
"format": "uint32",
"minimum": 0.0
},
"maxAspectRatioInPortrait": {
"description": "Maximum height/width aspect ratio (portrait) for which the split applies.",
"anyOf": [
{
"$ref": "#/definitions/EmbeddingAspectRatio"
},
{
"type": "null"
}
]
},
"maxAspectRatioInLandscape": {
"description": "Maximum height/width aspect ratio (landscape) for which the split applies.",
"anyOf": [
{
"$ref": "#/definitions/EmbeddingAspectRatio"
},
{
"type": "null"
}
]
},
"finishPrimaryWithSecondary": {
"description": "Behavior of the primary container when all activities in the secondary\n container finish. Defaults to [`SplitFinishBehavior::Never`].",
"anyOf": [
{
"$ref": "#/definitions/SplitFinishBehavior"
},
{
"type": "null"
}
]
},
"finishSecondaryWithPrimary": {
"description": "Behavior of the secondary container when all activities in the primary\n container finish. Defaults to [`SplitFinishBehavior::Always`].",
"anyOf": [
{
"$ref": "#/definitions/SplitFinishBehavior"
},
{
"type": "null"
}
]
},
"clearTop": {
"description": "Whether the existing secondary container and all activities in it should\n be destroyed when a new split is created using this rule.",
"type": [
"boolean",
"null"
]
},
"tag": {
"description": "Optional tag used to identify this rule at runtime.",
"type": [
"string",
"null"
]
}
},
"additionalProperties": false
},
"SplitType": {
"description": "How the parent window is split between primary and secondary containers.\n\n Mirrors [`androidx.window.embedding.SplitAttributes.SplitType`].\n\n [`androidx.window.embedding.SplitAttributes.SplitType`]: https://developer.android.com/reference/androidx/window/embedding/SplitAttributes.SplitType",
"oneOf": [
{
"description": "Splits the parent into two containers with the given weight\n for the primary container (0.0 exclusive to 1.0 exclusive).",
"type": "object",
"required": [
"ratio"
],
"properties": {
"ratio": {
"type": "number",
"format": "double"
}
},
"additionalProperties": false
},
{
"description": "The secondary container expands to cover the entire parent window.",
"type": "string",
"enum": [
"expand"
]
},
{
"description": "The split aligns with the device hinge/fold. Cannot be used as a\n default split type without hinge-aware devices.",
"type": "string",
"enum": [
"hinge"
]
}
]
},
"SplitLayoutDirection": {
"description": "A layout direction for an activity embedding split.\n\n Mirrors [`androidx.window.embedding.SplitAttributes.LayoutDirection`].\n\n [`androidx.window.embedding.SplitAttributes.LayoutDirection`]: https://developer.android.com/reference/androidx/window/embedding/SplitAttributes.LayoutDirection",
"oneOf": [
{
"description": "Use the system locale's layout direction.",
"type": "string",
"enum": [
"locale"
]
},
{
"description": "Primary on the left, secondary on the right.",
"type": "string",
"enum": [
"leftToRight"
]
},
{
"description": "Primary on the right, secondary on the left.",
"type": "string",
"enum": [
"rightToLeft"
]
},
{
"description": "Primary on top, secondary on the bottom.",
"type": "string",
"enum": [
"topToBottom"
]
},
{
"description": "Primary on the bottom, secondary on top.",
"type": "string",
"enum": [
"bottomToTop"
]
}
]
},
"EmbeddingAspectRatio": {
"description": "Maximum aspect ratio (height / width) of the parent window for which\n activity embedding should apply.\n\n Mirrors [`androidx.window.embedding.EmbeddingAspectRatio`].\n\n [`androidx.window.embedding.EmbeddingAspectRatio`]: https://developer.android.com/reference/androidx/window/embedding/EmbeddingAspectRatio",
"oneOf": [
{
"description": "Embedding always applies regardless of aspect ratio.",
"type": "string",
"enum": [
"alwaysAllow"
]
},
{
"description": "Embedding never applies in this orientation.",
"type": "string",
"enum": [
"alwaysDisallow"
]
},
{
"description": "Embedding applies when the parent window aspect ratio is less than or\n equal to this value. Must be greater than `1.0`.",
"type": "object",
"required": [
"ratio"
],
"properties": {
"ratio": {
"type": "number",
"format": "double"
}
},
"additionalProperties": false
}
]
},
"SplitFinishBehavior": {
"description": "Describes how an activity container should finish when the linked\n container finishes.\n\n Mirrors [`androidx.window.embedding.SplitRule.FinishBehavior`].\n\n [`androidx.window.embedding.SplitRule.FinishBehavior`]: https://developer.android.com/reference/androidx/window/embedding/SplitRule.FinishBehavior",
"oneOf": [
{
"description": "Never finish the container when the linked container finishes.",
"type": "string",
"enum": [
"never"
]
},
{
"description": "Always finish the container when the linked container finishes.",
"type": "string",
"enum": [
"always"
]
},
{
"description": "Finish the container only when the linked container finishes\n while they are displayed side-by-side.",
"type": "string",
"enum": [
"adjacent"
]
}
]
},
"PluginConfig": {
"description": "The plugin configs holds a HashMap mapping a plugin name to its configuration object.\n\n See more: <https://v2.tauri.app/reference/config/#pluginconfig>",
"type": "object",

View File

@@ -1549,7 +1549,7 @@ pub enum V1Compatible {
///
/// See more: <https://v2.tauri.app/reference/config/#bundleconfig>
#[skip_serializing_none]
#[derive(Debug, Default, PartialEq, Eq, Clone, Deserialize, Serialize)]
#[derive(Debug, Default, PartialEq, Clone, Deserialize, Serialize)]
#[cfg_attr(feature = "schema", derive(JsonSchema))]
#[serde(rename_all = "camelCase", deny_unknown_fields)]
pub struct BundleConfig {
@@ -3176,9 +3176,179 @@ impl Default for IosConfig {
}
}
/// Configuration for Android Activity Embedding, which splits activities
/// side by side on large screens.
///
/// See <https://developer.android.com/guide/topics/large-screens/activity-embedding>
#[derive(Debug, PartialEq, Clone, Deserialize, Serialize)]
#[cfg_attr(feature = "schema", derive(JsonSchema))]
#[serde(rename_all = "camelCase", deny_unknown_fields)]
pub struct ActivityEmbeddingConfig {
/// When `false`, Tauri does not generate embedding Gradle entries, manifest updates, or `TauriSplitInitializer`.
#[serde(default = "default_true")]
pub enabled: bool,
/// Split pair rules defining how activities are laid out side by side.
#[serde(alias = "split-rules", default)]
pub split_rules: Vec<SplitPairRule>,
}
/// A split pair rule for Android Activity Embedding.
///
/// Defines how a primary and secondary activity are displayed side by side
/// on screens that satisfy the configured minimum dimensions.
///
/// Mirrors the fields of [`androidx.window.embedding.SplitPairRule.Builder`] and
/// the [`SplitPairFilter`] it accepts. Only [`primary`](Self::primary) and
/// [`secondary`](Self::secondary) are required; all other fields fall back to
/// the Android SDK defaults when omitted.
///
/// [`androidx.window.embedding.SplitPairRule.Builder`]: https://developer.android.com/reference/androidx/window/embedding/SplitPairRule.Builder
/// [`SplitPairFilter`]: https://developer.android.com/reference/androidx/window/embedding/SplitPairFilter
#[skip_serializing_none]
#[derive(Debug, PartialEq, Clone, Deserialize, Serialize)]
#[cfg_attr(feature = "schema", derive(JsonSchema))]
#[serde(rename_all = "camelCase", deny_unknown_fields)]
pub struct SplitPairRule {
/// The primary activity class name, relative to the application package
/// (e.g. `"MainActivity"` or a fully-qualified name like
/// `"com.example.MainActivity"`).
pub primary: String,
/// The secondary activity class name, relative to the application package
/// (e.g. `"DetailActivity"` or a fully-qualified name).
pub secondary: String,
/// Optional intent action used to match the secondary activity when it is
/// started via an implicit intent.
#[serde(alias = "secondary-intent-action")]
pub secondary_intent_action: Option<String>,
/// How the parent window is split. When omitted, the SDK uses an equal
/// 50/50 ratio.
#[serde(alias = "split-type")]
pub split_type: Option<SplitType>,
/// The layout direction of the primary/secondary containers. Defaults to
/// the system locale direction.
#[serde(alias = "layout-direction")]
pub layout_direction: Option<SplitLayoutDirection>,
/// Minimum parent window width in dp for the split to apply. Defaults to the
/// SDK value (`600`).
#[serde(alias = "min-width-dp")]
pub min_width_dp: Option<u32>,
/// Minimum parent window height in dp for the split to apply. Defaults to the
/// SDK value (`600`).
#[serde(alias = "min-height-dp")]
pub min_height_dp: Option<u32>,
/// Minimum smallest width of the parent window in dp for the split to apply.
/// Defaults to the SDK value (`600`).
#[serde(alias = "min-smallest-width-dp")]
pub min_smallest_width_dp: Option<u32>,
/// Maximum height/width aspect ratio (portrait) for which the split applies.
#[serde(alias = "max-aspect-ratio-in-portrait")]
pub max_aspect_ratio_in_portrait: Option<EmbeddingAspectRatio>,
/// Maximum height/width aspect ratio (landscape) for which the split applies.
#[serde(alias = "max-aspect-ratio-in-landscape")]
pub max_aspect_ratio_in_landscape: Option<EmbeddingAspectRatio>,
/// Behavior of the primary container when all activities in the secondary
/// container finish. Defaults to [`SplitFinishBehavior::Never`].
#[serde(alias = "finish-primary-with-secondary")]
pub finish_primary_with_secondary: Option<SplitFinishBehavior>,
/// Behavior of the secondary container when all activities in the primary
/// container finish. Defaults to [`SplitFinishBehavior::Always`].
#[serde(alias = "finish-secondary-with-primary")]
pub finish_secondary_with_primary: Option<SplitFinishBehavior>,
/// Whether the existing secondary container and all activities in it should
/// be destroyed when a new split is created using this rule.
#[serde(alias = "clear-top")]
pub clear_top: Option<bool>,
/// Optional tag used to identify this rule at runtime.
pub tag: Option<String>,
}
/// How the parent window is split between primary and secondary containers.
///
/// Mirrors [`androidx.window.embedding.SplitAttributes.SplitType`].
///
/// [`androidx.window.embedding.SplitAttributes.SplitType`]: https://developer.android.com/reference/androidx/window/embedding/SplitAttributes.SplitType
#[derive(Debug, PartialEq, Clone, Deserialize, Serialize)]
#[cfg_attr(feature = "schema", derive(JsonSchema))]
#[serde(rename_all = "camelCase", deny_unknown_fields)]
pub enum SplitType {
/// Splits the parent into two containers with the given weight
/// for the primary container (0.0 exclusive to 1.0 exclusive).
Ratio(f64),
/// The secondary container expands to cover the entire parent window.
Expand,
/// The split aligns with the device hinge/fold. Cannot be used as a
/// default split type without hinge-aware devices.
Hinge,
}
/// A layout direction for an activity embedding split.
///
/// Mirrors [`androidx.window.embedding.SplitAttributes.LayoutDirection`].
///
/// [`androidx.window.embedding.SplitAttributes.LayoutDirection`]: https://developer.android.com/reference/androidx/window/embedding/SplitAttributes.LayoutDirection
#[derive(Debug, PartialEq, Eq, Clone, Copy, Deserialize, Serialize)]
#[cfg_attr(feature = "schema", derive(JsonSchema))]
#[serde(rename_all = "camelCase")]
pub enum SplitLayoutDirection {
/// Use the system locale's layout direction.
Locale,
/// Primary on the left, secondary on the right.
LeftToRight,
/// Primary on the right, secondary on the left.
RightToLeft,
/// Primary on top, secondary on the bottom.
TopToBottom,
/// Primary on the bottom, secondary on top.
BottomToTop,
}
/// Describes how an activity container should finish when the linked
/// container finishes.
///
/// Mirrors [`androidx.window.embedding.SplitRule.FinishBehavior`].
///
/// [`androidx.window.embedding.SplitRule.FinishBehavior`]: https://developer.android.com/reference/androidx/window/embedding/SplitRule.FinishBehavior
#[derive(Debug, PartialEq, Eq, Clone, Copy, Deserialize, Serialize)]
#[cfg_attr(feature = "schema", derive(JsonSchema))]
#[serde(rename_all = "camelCase")]
pub enum SplitFinishBehavior {
/// Never finish the container when the linked container finishes.
Never,
/// Always finish the container when the linked container finishes.
Always,
/// Finish the container only when the linked container finishes
/// while they are displayed side-by-side.
Adjacent,
}
/// Maximum aspect ratio (height / width) of the parent window for which
/// activity embedding should apply.
///
/// Mirrors [`androidx.window.embedding.EmbeddingAspectRatio`].
///
/// [`androidx.window.embedding.EmbeddingAspectRatio`]: https://developer.android.com/reference/androidx/window/embedding/EmbeddingAspectRatio
#[derive(Debug, PartialEq, Clone, Deserialize, Serialize)]
#[cfg_attr(feature = "schema", derive(JsonSchema))]
#[serde(rename_all = "camelCase", deny_unknown_fields)]
pub enum EmbeddingAspectRatio {
/// Embedding always applies regardless of aspect ratio.
AlwaysAllow,
/// Embedding never applies in this orientation.
AlwaysDisallow,
/// Embedding applies when the parent window aspect ratio is less than or
/// equal to this value. Must be greater than `1.0`.
Ratio(f64),
}
/// General configuration for the Android target.
#[skip_serializing_none]
#[derive(Debug, PartialEq, Eq, Clone, Deserialize, Serialize)]
#[derive(Debug, PartialEq, Clone, Deserialize, Serialize)]
#[cfg_attr(feature = "schema", derive(JsonSchema))]
#[serde(rename_all = "camelCase", deny_unknown_fields)]
pub struct AndroidConfig {
@@ -3205,6 +3375,13 @@ pub struct AndroidConfig {
/// Note that to use this feature, you should remove `/tauri.properties` from `src-tauri/gen/android/app/.gitignore` so the current versionCode is committed to the repository.
#[serde(alias = "auto-increment-version-code", default)]
pub auto_increment_version_code: bool,
/// Activity embedding for large screens (tablets, foldables).
///
/// When set and enabled, Tauri generates the Gradle dependencies, Android manifest entries,
/// and a `TauriSplitInitializer` Kotlin class for split-screen activity layouts.
#[serde(alias = "activity-embedding")]
pub activity_embedding: Option<ActivityEmbeddingConfig>,
}
impl Default for AndroidConfig {
@@ -3213,6 +3390,7 @@ impl Default for AndroidConfig {
min_sdk_version: default_min_sdk_version(),
version_code: None,
auto_increment_version_code: false,
activity_embedding: None,
}
}
}

View File

@@ -127,6 +127,19 @@
"nsis": {
"compression": "none"
}
},
"android": {
"activityEmbedding": {
"enabled": true,
"splitRules": [
{
"primary": "MainActivity",
"secondary": "DetailActivity",
"splitType": { "ratio": 0.33 },
"minWidthDp": 840
}
]
}
}
}
}