diff --git a/app/src/main/java/io/netbird/client/MainActivity.java b/app/src/main/java/io/netbird/client/MainActivity.java index f1be8a39..c1f4fa1d 100644 --- a/app/src/main/java/io/netbird/client/MainActivity.java +++ b/app/src/main/java/io/netbird/client/MainActivity.java @@ -55,6 +55,7 @@ import io.netbird.client.tool.ServiceStateListener; import io.netbird.client.tool.SessionEventListener; import io.netbird.client.tool.VPNService; +import io.netbird.client.ui.DialogFragments; import io.netbird.client.ui.PreferenceUI; import io.netbird.client.ui.ssh.SshSessionManager; import io.netbird.gomobile.android.Android; @@ -308,7 +309,7 @@ public boolean canConnect() { @Override public void open(String url, String userCode) { runOnUiThread(() -> { - qrCodeDialog = QrCodeDialog.newInstance(url, userCode, () -> { + showQrCodeDialog(QrCodeDialog.newInstance(url, userCode, () -> { if (isSSOFinishedWell) { return; } @@ -316,8 +317,7 @@ public void open(String url, String userCode) { return; } mBinder.stopEngine(); - }); - qrCodeDialog.show(getSupportFragmentManager(), "QrCodeDialog"); + })); if (!isRunningOnTV) { try { @@ -1087,6 +1087,14 @@ public void onError(Exception e) { }); } + private boolean showQrCodeDialog(QrCodeDialog dialog) { + if (!DialogFragments.showNow(this, getSupportFragmentManager(), dialog, "QrCodeDialog")) { + return false; + } + qrCodeDialog = dialog; + return true; + } + // The login urlOpener stops the engine when the SSO surface is dismissed // without success, which also kills its pending PKCE wait. An extend must // keep the tunnel up, so it cancels just the extend flow instead — @@ -1100,17 +1108,23 @@ private URLOpener buildExtendURLOpener() { @Override public void open(String url, String userCode) { runOnUiThread(() -> { - qrCodeDialog = QrCodeDialog.newInstance(url, userCode, - MainActivity.this::cancelExtendSession); - qrCodeDialog.show(getSupportFragmentManager(), "QrCodeDialog"); + boolean dialogShown = showQrCodeDialog(QrCodeDialog.newInstance(url, userCode, + MainActivity.this::cancelExtendSession)); + boolean browserOpened = false; if (!isRunningOnTV) { try { startActivity(new Intent(Intent.ACTION_VIEW, Uri.parse(url))); + browserOpened = true; } catch (Exception e) { Log.e(LOGTAG, "Failed to open browser for device code flow: " + e.getMessage()); } } + // Without a dialog there is nobody to dismiss, so the extend would + // otherwise sit on its loopback port until it times out. + if (!dialogShown && !browserOpened) { + cancelExtendSession(); + } }); } diff --git a/app/src/main/java/io/netbird/client/ui/DialogFragments.java b/app/src/main/java/io/netbird/client/ui/DialogFragments.java new file mode 100644 index 00000000..c4933028 --- /dev/null +++ b/app/src/main/java/io/netbird/client/ui/DialogFragments.java @@ -0,0 +1,33 @@ +package io.netbird.client.ui; + +import android.app.Activity; + +import androidx.fragment.app.DialogFragment; +import androidx.fragment.app.Fragment; +import androidx.fragment.app.FragmentManager; +import androidx.lifecycle.Lifecycle; +import androidx.lifecycle.LifecycleOwner; + +public final class DialogFragments { + + private DialogFragments() { + } + + // A click listener runs as a posted message and the engine's URLOpener runs + // from a background callback, so by the time either gets to show a dialog + // the host may already be paused, finishing or past onSaveInstanceState. + // None of those can host a window. Showing synchronously keeps a pending + // show() from being flushed by the pause itself; the resumed check covers + // the click landing behind the pause. + public static boolean showNow(LifecycleOwner host, FragmentManager fm, DialogFragment dialog, String tag) { + if (!host.getLifecycle().getCurrentState().isAtLeast(Lifecycle.State.RESUMED)) { + return false; + } + Activity activity = host instanceof Fragment ? ((Fragment) host).getActivity() : host instanceof Activity ? (Activity) host : null; + if (activity != null && activity.isFinishing()) { + return false; + } + dialog.showNow(fm, tag); + return true; + } +} diff --git a/app/src/main/java/io/netbird/client/ui/advanced/AdvancedFragment.java b/app/src/main/java/io/netbird/client/ui/advanced/AdvancedFragment.java index 1c71c5d8..dcea910c 100644 --- a/app/src/main/java/io/netbird/client/ui/advanced/AdvancedFragment.java +++ b/app/src/main/java/io/netbird/client/ui/advanced/AdvancedFragment.java @@ -21,6 +21,7 @@ import io.netbird.client.tool.MDMRestrictions; import io.netbird.client.tool.Preferences; import io.netbird.client.tool.ProfileManagerWrapper; +import io.netbird.client.ui.DialogFragments; import io.netbird.client.ui.MDMLock; @@ -147,7 +148,7 @@ public View onCreateView(@NonNull LayoutInflater inflater, binding.rowTheme.setOnClickListener(v -> { ThemePickerSheet sheet = new ThemePickerSheet(); - sheet.show(getChildFragmentManager(), "ThemePickerSheet"); + DialogFragments.showNow(this, getChildFragmentManager(), sheet, "ThemePickerSheet"); }); // Last: locking a row takes away the listeners installed above. diff --git a/app/src/main/java/io/netbird/client/ui/home/HomeFragment.java b/app/src/main/java/io/netbird/client/ui/home/HomeFragment.java index ad55eeb9..1b77a462 100644 --- a/app/src/main/java/io/netbird/client/ui/home/HomeFragment.java +++ b/app/src/main/java/io/netbird/client/ui/home/HomeFragment.java @@ -36,6 +36,7 @@ import io.netbird.client.tool.Profile; import io.netbird.client.tool.ProfileManagerWrapper; import io.netbird.client.tool.RouteChangeListener; +import io.netbird.client.ui.DialogFragments; import io.netbird.gomobile.android.NetworkArray; public class HomeFragment extends Fragment implements StateListener, RouteChangeListener, ProfilePickerSheet.OnProfileSwitchedListener { @@ -209,12 +210,12 @@ public View onCreateView(@NonNull LayoutInflater inflater, binding.profileChip.setOnClickListener(v -> { ProfilePickerSheet sheet = new ProfilePickerSheet(); - sheet.show(getChildFragmentManager(), "ProfilePickerSheet"); + DialogFragments.showNow(this, getChildFragmentManager(), sheet, "ProfilePickerSheet"); }); binding.exitNodeRow.setOnClickListener(v -> { ExitNodePickerSheet sheet = new ExitNodePickerSheet(); - sheet.show(getChildFragmentManager(), "ExitNodePickerSheet"); + DialogFragments.showNow(this, getChildFragmentManager(), sheet, "ExitNodePickerSheet"); }); binding.sessionExpiryRow.setOnClickListener(v -> { if (serviceAccessor != null) { diff --git a/app/src/main/java/io/netbird/client/ui/settings/SettingsFragment.java b/app/src/main/java/io/netbird/client/ui/settings/SettingsFragment.java index 8980e04d..52ee5e5e 100644 --- a/app/src/main/java/io/netbird/client/ui/settings/SettingsFragment.java +++ b/app/src/main/java/io/netbird/client/ui/settings/SettingsFragment.java @@ -22,6 +22,7 @@ import io.netbird.client.tool.MDMRestrictions; import io.netbird.client.tool.Profile; import io.netbird.client.tool.ProfileManagerWrapper; +import io.netbird.client.ui.DialogFragments; import io.netbird.client.ui.MDMLock; import io.netbird.client.ui.profile.ProfileEditorDialog; @@ -58,8 +59,8 @@ public void onViewCreated(@NonNull View view, @Nullable Bundle savedInstanceStat binding.rowSplitTunneling.setOnClickListener(v -> navController.navigate(R.id.nav_split_tunneling)); - binding.rowLanguage.setOnClickListener(v -> - new LanguagePickerSheet().show(getChildFragmentManager(), "language_picker")); + binding.rowLanguage.setOnClickListener(v -> DialogFragments.showNow( + this, getChildFragmentManager(), new LanguagePickerSheet(), "language_picker")); binding.rowTroubleshoot.setOnClickListener(v -> navController.navigate(R.id.nav_troubleshoot)); diff --git a/app/src/main/java/io/netbird/client/ui/splittunneling/SplitTunnelingFragment.java b/app/src/main/java/io/netbird/client/ui/splittunneling/SplitTunnelingFragment.java index 96a0a3bc..4b66585b 100644 --- a/app/src/main/java/io/netbird/client/ui/splittunneling/SplitTunnelingFragment.java +++ b/app/src/main/java/io/netbird/client/ui/splittunneling/SplitTunnelingFragment.java @@ -25,6 +25,7 @@ import io.netbird.client.tool.MDMBridge; import io.netbird.client.tool.SplitTunnelConfig; import io.netbird.client.tool.SplitTunnelStore; +import io.netbird.client.ui.DialogFragments; import io.netbird.client.ui.MDMLock; /** @@ -88,8 +89,8 @@ public void onViewCreated(@NonNull View view, @Nullable Bundle savedInstanceStat if (managed) { MDMLock.lock(binding.rowMode); } else { - binding.rowMode.setOnClickListener(v -> - SplitTunnelModeSheet.newInstance(mode).show(getChildFragmentManager(), "split_tunnel_mode")); + binding.rowMode.setOnClickListener(v -> DialogFragments.showNow( + this, getChildFragmentManager(), SplitTunnelModeSheet.newInstance(mode), "split_tunnel_mode")); } binding.searchView.addTextChangedListener(new TextWatcher() { diff --git a/app/src/main/java/io/netbird/client/ui/troubleshoot/TroubleshootFragment.java b/app/src/main/java/io/netbird/client/ui/troubleshoot/TroubleshootFragment.java index 3249824f..18f038d3 100644 --- a/app/src/main/java/io/netbird/client/ui/troubleshoot/TroubleshootFragment.java +++ b/app/src/main/java/io/netbird/client/ui/troubleshoot/TroubleshootFragment.java @@ -35,6 +35,7 @@ import io.netbird.client.tool.MDMBridge; import io.netbird.client.tool.Preferences; import io.netbird.client.tool.ProfileManagerWrapper; +import io.netbird.client.ui.DialogFragments; import io.netbird.client.ui.MDMLock; public class TroubleshootFragment extends Fragment implements AnonymizeLevelSheet.OnLevelChangedListener { @@ -87,9 +88,9 @@ public View onCreateView(@NonNull LayoutInflater inflater, ViewGroup container, }); updateAnonymizeValue(); - binding.anonymizeLayout.setOnClickListener(v -> - AnonymizeLevelSheet.newInstance(preferences.getAnonymizeLevel()) - .show(getChildFragmentManager(), "anonymize_level")); + binding.anonymizeLayout.setOnClickListener(v -> DialogFragments.showNow( + this, getChildFragmentManager(), + AnonymizeLevelSheet.newInstance(preferences.getAnonymizeLevel()), "anonymize_level")); initializeRemoteJobsSwitch(inflater.getContext());