Skip to content
Merged
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
5 changes: 5 additions & 0 deletions .changes/android-plugin-manager-relaunch.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
---
tauri: patch:bug
---

On Android, fixed a crash when an activity is destroyed while another one is already running — installing an APK over the running app is the common way to hit it. The plugin manager tried to move its activity result launchers to the surviving activity, and `registerForActivityResult` rejects that with `IllegalStateException: LifecycleOwner ... is attempting to register while current state is RESUMED`. Each activity now registers its own launchers when it is created.
5 changes: 5 additions & 0 deletions .changes/path-normalize-empty.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
---
tauri: patch:bug
---

Fix `path.normalize("")` returning an empty string instead of `"."`, matching Node.js and the existing `path.join("")` behavior.
6 changes: 6 additions & 0 deletions .changes/windows-multiwebview-cached-focused-states.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,6 @@
---
tauri: patch:bug
tauri-runtime-wry: patch:bug
---

On Windows, fixed `Window::is_focused` always returns `false` in multi-webview mode
19 changes: 17 additions & 2 deletions crates/tauri-runtime-wry/src/lib.rs
Original file line number Diff line number Diff line change
Expand Up @@ -3317,9 +3317,12 @@ fn handle_user_message<T: UserEvent>(
w.webviews.clone(),
w.has_children.load(Ordering::Relaxed),
w.window_event_listeners.clone(),
w.focused_webview.clone(),
)
});
if let Some((Some(window), webviews, has_children, window_event_listeners)) = w {
if let Some((Some(window), webviews, has_children, window_event_listeners, focused_webview)) =
w
{
match window_message {
WindowMessage::AddEventListener(id, listener) => {
window_event_listeners.lock().unwrap().insert(id, listener);
Expand Down Expand Up @@ -3348,7 +3351,19 @@ fn handle_user_message<T: UserEvent>(
WindowMessage::IsFullscreen(tx) => tx.send(window.fullscreen().is_some()).unwrap(),
WindowMessage::IsMinimized(tx) => tx.send(window.is_minimized()).unwrap(),
WindowMessage::IsMaximized(tx) => tx.send(window.is_maximized()).unwrap(),
WindowMessage::IsFocused(tx) => tx.send(window.is_focused()).unwrap(),
WindowMessage::IsFocused(tx) => {
let focused = if has_children {
// on multiwebview mode, get the focused state from cache,
// as the window might not have direct focus
matches!(
*focused_webview.lock().unwrap(),
FocusState::WindowFocused | FocusState::WebviewFocused { .. }
)
} else {
window.is_focused()
};
tx.send(focused).unwrap()
}
WindowMessage::IsDecorated(tx) => tx.send(window.is_decorated()).unwrap(),
WindowMessage::IsResizable(tx) => tx.send(window.is_resizable()).unwrap(),
WindowMessage::IsMaximizable(tx) => tx.send(window.is_maximizable()).unwrap(),
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -34,12 +34,18 @@ object PluginManager {
fun onResult(result: ActivityResult)
}

private val activities: HashSet<AppCompatActivity> = HashSet()
/** The result launchers belonging to one activity. */
private class ResultLaunchers(
val startActivityForResult: ActivityResultLauncher<Intent>,
val startIntentSenderForResult: ActivityResultLauncher<IntentSenderRequest>,
val requestPermissions: ActivityResultLauncher<Array<String>>
)

// Insertion ordered, so the activity taken over when the current one goes away is the oldest
// surviving one rather than an arbitrary member of a hash set.
private val launchers: LinkedHashMap<AppCompatActivity, ResultLaunchers> = LinkedHashMap()
var activity: AppCompatActivity? = null
private val plugins: HashMap<String, PluginHandle> = HashMap()
private var startActivityForResultLauncher: ActivityResultLauncher<Intent>? = null
private var startIntentSenderForResultLauncher: ActivityResultLauncher<IntentSenderRequest>? = null
private var requestPermissionsLauncher: ActivityResultLauncher<Array<String>>? = null
private var requestPermissionsCallback: RequestPermissionsCallback? = null
private var startActivityForResultCallback: ActivityResultCallback? = null
private var startIntentSenderForResultCallback: ActivityResultCallback? = null
Expand All @@ -57,39 +63,36 @@ object PluginManager {
}

fun onCreate(activity: AppCompatActivity) {
// Record the activity, and if that's the only activity we got, register result launchers
activities.add(activity)
// Every activity gets its own launchers, and gets them here: registerForActivityResult must
// be called before its owner reaches STARTED, so an activity that is already running can
// never be given launchers later.
launchers[activity] = registerResultLaunchers(activity)
if (this.activity == null) {
this.activity = activity
registerResultLaunchers(activity)
}
}

private fun registerResultLaunchers(activity: AppCompatActivity) {
startActivityForResultLauncher =
private fun registerResultLaunchers(activity: AppCompatActivity): ResultLaunchers =
ResultLaunchers(
activity.registerForActivityResult(ActivityResultContracts.StartActivityForResult()
) { result ->
if (startActivityForResultCallback != null) {
startActivityForResultCallback!!.onResult(result)
}
}
startActivityForResultCallback?.onResult(result)
},

startIntentSenderForResultLauncher =
activity.registerForActivityResult(ActivityResultContracts.StartIntentSenderForResult()
) { result ->
if (startIntentSenderForResultCallback != null) {
startIntentSenderForResultCallback!!.onResult(result)
}
}
startIntentSenderForResultCallback?.onResult(result)
},

requestPermissionsLauncher =
activity.registerForActivityResult(ActivityResultContracts.RequestMultiplePermissions()
) { result ->
if (requestPermissionsCallback != null) {
requestPermissionsCallback!!.onResult(result)
}
requestPermissionsCallback?.onResult(result)
}
}
)

private val currentLaunchers: ResultLaunchers
get() = launchers[activity]
?: throw IllegalStateException("the plugin manager has no activity to launch from")

fun onNewIntent(intent: Intent) {
for (plugin in plugins.values) {
Expand Down Expand Up @@ -126,18 +129,12 @@ object PluginManager {
plugin.instance.triggerOnDestroy(activity)
}

activities.remove(activity)
val nextActivity = activities.firstOrNull()
if (nextActivity != null) {
if (this.activity == activity) {
this.activity = nextActivity
registerResultLaunchers(nextActivity)
}
} else {
this.activity = null
this.startActivityForResultLauncher = null
this.startIntentSenderForResultLauncher = null
this.requestPermissionsLauncher = null
launchers.remove(activity)
if (this.activity == activity) {
// Whatever is left already holds its own launchers, registered when it was created. Moving
// this activity's launchers over instead would mean registering against an activity that is
// already running, which registerForActivityResult rejects with an IllegalStateException.
this.activity = launchers.keys.firstOrNull()
}
}

Expand All @@ -149,20 +146,20 @@ object PluginManager {

fun startActivityForResult(intent: Intent, callback: ActivityResultCallback) {
startActivityForResultCallback = callback
startActivityForResultLauncher!!.launch(intent)
currentLaunchers.startActivityForResult.launch(intent)
}

fun startIntentSenderForResult(intent: IntentSenderRequest, callback: ActivityResultCallback) {
startIntentSenderForResultCallback = callback
startIntentSenderForResultLauncher!!.launch(intent)
currentLaunchers.startIntentSenderForResult.launch(intent)
}

fun requestPermissions(
permissionStrings: Array<String>,
callback: RequestPermissionsCallback
) {
requestPermissionsCallback = callback
requestPermissionsLauncher!!.launch(permissionStrings)
currentLaunchers.requestPermissions.launch(permissionStrings)
}

@JniMethod
Expand Down
6 changes: 3 additions & 3 deletions crates/tauri/src/manager/mod.rs
Original file line number Diff line number Diff line change
Expand Up @@ -650,9 +650,9 @@ impl<R: Runtime> AppManager<R> {
self
.window
.windows_lock()
.iter()
.find(|w| w.1.is_focused().unwrap_or(false))
.map(|w| w.1.clone())
.values()
.find(|w| w.is_focused().unwrap_or(false))
.cloned()
}

pub(crate) fn on_window_close(&self, label: &str) {
Expand Down
9 changes: 8 additions & 1 deletion crates/tauri/src/path/plugin.rs
Original file line number Diff line number Diff line change
Expand Up @@ -120,7 +120,7 @@ pub fn normalize(path: String) -> String {
// and `"."` for `normalize("")` or `normalize(".")`
if p.is_empty() && path == ".." {
"..".into()
} else if p.is_empty() && path == "." {
} else if p.is_empty() && (path.is_empty() || path == ".") {
".".into()
} else {
// Add a trailing separator if the path passed to this functions had a trailing separator. That's how Node.js behaves.
Expand Down Expand Up @@ -310,4 +310,11 @@ mod tests {
check(vec!["a", "b/c", "d"], "a/b/c/d", r"a\b\c\d");
check(vec!["a/", "b"], "a/b", r"a\b");
}

#[test]
fn normalize() {
assert_eq!(super::normalize("".into()), ".");
assert_eq!(super::normalize(".".into()), ".");
assert_eq!(super::normalize("..".into()), "..");
}
}
Loading