diff --git a/.changes/android-plugin-manager-relaunch.md b/.changes/android-plugin-manager-relaunch.md new file mode 100644 index 000000000000..eeb27f3a6fa0 --- /dev/null +++ b/.changes/android-plugin-manager-relaunch.md @@ -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. diff --git a/.changes/path-normalize-empty.md b/.changes/path-normalize-empty.md new file mode 100644 index 000000000000..bb8261bd18a1 --- /dev/null +++ b/.changes/path-normalize-empty.md @@ -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. diff --git a/.changes/windows-multiwebview-cached-focused-states.md b/.changes/windows-multiwebview-cached-focused-states.md new file mode 100644 index 000000000000..22e8afc482ea --- /dev/null +++ b/.changes/windows-multiwebview-cached-focused-states.md @@ -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 diff --git a/crates/tauri-runtime-wry/src/lib.rs b/crates/tauri-runtime-wry/src/lib.rs index d14f6570ded6..0470b036d943 100644 --- a/crates/tauri-runtime-wry/src/lib.rs +++ b/crates/tauri-runtime-wry/src/lib.rs @@ -3317,9 +3317,12 @@ fn handle_user_message( 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); @@ -3348,7 +3351,19 @@ fn handle_user_message( 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(), diff --git a/crates/tauri/mobile/android/src/main/java/app/tauri/plugin/PluginManager.kt b/crates/tauri/mobile/android/src/main/java/app/tauri/plugin/PluginManager.kt index 32a1cc519d23..7e5d4264034e 100644 --- a/crates/tauri/mobile/android/src/main/java/app/tauri/plugin/PluginManager.kt +++ b/crates/tauri/mobile/android/src/main/java/app/tauri/plugin/PluginManager.kt @@ -34,12 +34,18 @@ object PluginManager { fun onResult(result: ActivityResult) } - private val activities: HashSet = HashSet() + /** The result launchers belonging to one activity. */ + private class ResultLaunchers( + val startActivityForResult: ActivityResultLauncher, + val startIntentSenderForResult: ActivityResultLauncher, + val requestPermissions: ActivityResultLauncher> + ) + + // 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 = LinkedHashMap() var activity: AppCompatActivity? = null private val plugins: HashMap = HashMap() - private var startActivityForResultLauncher: ActivityResultLauncher? = null - private var startIntentSenderForResultLauncher: ActivityResultLauncher? = null - private var requestPermissionsLauncher: ActivityResultLauncher>? = null private var requestPermissionsCallback: RequestPermissionsCallback? = null private var startActivityForResultCallback: ActivityResultCallback? = null private var startIntentSenderForResultCallback: ActivityResultCallback? = null @@ -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) { @@ -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() } } @@ -149,12 +146,12 @@ 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( @@ -162,7 +159,7 @@ object PluginManager { callback: RequestPermissionsCallback ) { requestPermissionsCallback = callback - requestPermissionsLauncher!!.launch(permissionStrings) + currentLaunchers.requestPermissions.launch(permissionStrings) } @JniMethod diff --git a/crates/tauri/src/manager/mod.rs b/crates/tauri/src/manager/mod.rs index a60ac048f911..a8ee394eb36f 100644 --- a/crates/tauri/src/manager/mod.rs +++ b/crates/tauri/src/manager/mod.rs @@ -650,9 +650,9 @@ impl AppManager { 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) { diff --git a/crates/tauri/src/path/plugin.rs b/crates/tauri/src/path/plugin.rs index e65e5ddfdc2c..b095f338200e 100644 --- a/crates/tauri/src/path/plugin.rs +++ b/crates/tauri/src/path/plugin.rs @@ -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. @@ -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()), ".."); + } }