From d0d774e56c73faae9a4839cdd0757a3299e76aa8 Mon Sep 17 00:00:00 2001 From: Pramoth Suwanpech Date: Thu, 15 Oct 2026 19:57:23 +0700 Subject: [PATCH] fix(browser): stop close() from re-opening the last URL on Android When a deep link into the app's singleTask activity clears the controller off the task, it is destroyed only later. A close() in that window creates a new BrowserControllerActivity, whose onCreate ran the open() listener again and launched the last URL a second time. If the old controller's onDestroy cleared the listener first, the new one launched nothing and stayed on top of the app. - finish at once in onCreate when started with the close extra - make the open() listener one-shot - onDestroy forgets only its own instance, not the listener - set the listener before startActivity(), and make both statics volatile Fixes #2610 --- .../browser/BrowserControllerActivity.java | 17 +++++++++--- .../plugins/browser/BrowserPlugin.java | 26 ++++++++++++++----- 2 files changed, 33 insertions(+), 10 deletions(-) diff --git a/browser/android/src/main/java/com/capacitorjs/plugins/browser/BrowserControllerActivity.java b/browser/android/src/main/java/com/capacitorjs/plugins/browser/BrowserControllerActivity.java index 578fde6e1..9a7f9f2f0 100644 --- a/browser/android/src/main/java/com/capacitorjs/plugins/browser/BrowserControllerActivity.java +++ b/browser/android/src/main/java/com/capacitorjs/plugins/browser/BrowserControllerActivity.java @@ -19,6 +19,14 @@ public class BrowserControllerActivity extends ComponentActivity { protected void onCreate(@Nullable Bundle savedInstanceState) { super.onCreate(savedInstanceState); + // close() starts a new instance when the previous one was already cleared off the task + // (e.g. by a deep link into a singleTask activity) but not destroyed yet. There is nothing + // to close then, and the last open() must not run again. + if (getIntent().hasExtra("close")) { + finish(); + return; + } + customTabLauncher = registerForActivityResult(new ActivityResultContracts.StartActivityForResult(), (result) -> { if (implementation != null) { implementation.notifyBrowserFinished(); @@ -26,8 +34,11 @@ protected void onCreate(@Nullable Bundle savedInstanceState) { finish(); }); - if (BrowserPlugin.browserControllerListener != null) { - BrowserPlugin.browserControllerListener.onControllerReady(this); + // Each open() is served once, by the controller it started. + BrowserControllerListener listener = BrowserPlugin.browserControllerListener; + BrowserPlugin.browserControllerListener = null; + if (listener != null) { + listener.onControllerReady(this); } } @@ -64,6 +75,6 @@ public void open(Browser implementation, Uri url, Integer toolbarColor) { protected void onDestroy() { super.onDestroy(); implementation = null; - BrowserPlugin.setBrowserControllerListener(null); + BrowserPlugin.onControllerDestroyed(this); } } diff --git a/browser/android/src/main/java/com/capacitorjs/plugins/browser/BrowserPlugin.java b/browser/android/src/main/java/com/capacitorjs/plugins/browser/BrowserPlugin.java index 639bfa0c3..a8ab90354 100644 --- a/browser/android/src/main/java/com/capacitorjs/plugins/browser/BrowserPlugin.java +++ b/browser/android/src/main/java/com/capacitorjs/plugins/browser/BrowserPlugin.java @@ -15,8 +15,9 @@ public class BrowserPlugin extends Plugin { private Browser implementation; - public static BrowserControllerListener browserControllerListener; - private static BrowserControllerActivity browserControllerActivityInstance; + // Written on the plugin thread, read on the main thread. + public static volatile BrowserControllerListener browserControllerListener; + private static volatile BrowserControllerActivity browserControllerActivityInstance; public static void setBrowserControllerListener(BrowserControllerListener listener) { browserControllerListener = listener; @@ -25,6 +26,16 @@ public static void setBrowserControllerListener(BrowserControllerListener listen } } + /** + * Called by each controller as it is destroyed. It forgets that controller only: the listener + * may already belong to a newer open() whose controller has not been created yet. + */ + static void onControllerDestroyed(BrowserControllerActivity activity) { + if (browserControllerActivityInstance == activity) { + browserControllerActivityInstance = null; + } + } + public void load() { implementation = new Browser(getContext()); implementation.setBrowserEventListener(this::onBrowserEvent); @@ -61,11 +72,7 @@ public void open(PluginCall call) { // open the browser and finish - Intent intent = new Intent(getContext(), BrowserControllerActivity.class); - intent.addFlags(Intent.FLAG_ACTIVITY_CLEAR_TOP); - intent.addFlags(Intent.FLAG_ACTIVITY_NEW_TASK); - getContext().startActivity(intent); - + // Set the listener before starting the activity, so its onCreate can't run first and miss it. Integer finalToolbarColor = toolbarColor; setBrowserControllerListener((activity) -> { try { @@ -77,6 +84,11 @@ public void open(PluginCall call) { call.reject("Unable to display URL"); } }); + + Intent intent = new Intent(getContext(), BrowserControllerActivity.class); + intent.addFlags(Intent.FLAG_ACTIVITY_CLEAR_TOP); + intent.addFlags(Intent.FLAG_ACTIVITY_NEW_TASK); + getContext().startActivity(intent); } @PluginMethod