Skip to content

Conversation

@pappz
Copy link
Collaborator

@pappz pappz commented Nov 28, 2025

Summary by CodeRabbit

  • New Features

    • Multi-profile management UI: add, switch, logout, remove profiles with dialogs and feedback
    • Profiles entry in navigation drawer and new profile icon and layouts
    • Route controls exposed so UI can select/deselect routes and observe route changes
  • Refactor

    • Profile manager integrated app-wide so active profile, config and state paths are used consistently
  • Chores

    • Removed legacy VPN service repository and file-path helpers (internal plumbing updated)

✏️ Tip: You can customize this high-level summary in your review settings.

VPNServiceRepository created its own service binding which caused conflicts
when unbinding - it would disconnect other components relying on the service.

Now NetworksFragmentViewModel uses ServiceAccessor from MainActivity, which
maintains a single shared service binding across all components.
@coderabbitai
Copy link

coderabbitai bot commented Nov 28, 2025

Walkthrough

Adds profile management (Profile, ProfileManagerWrapper, UI, resources), replaces static Preferences path usage with ProfileManagerWrapper, removes VPNServiceRepository in favor of ServiceAccessor, updates EngineRunner and VPNService to use ProfileManagerWrapper, and exposes route APIs through MainActivity.

Changes

Cohort / File(s) Summary
Small cleanup
app/src/main/java/io/netbird/client/CustomTabURLOpener.java
Replaced anonymous ActivityResultCallback with a lambda; no behavioral change.
Service binding API
app/src/main/java/io/netbird/client/ServiceAccessor.java, app/src/main/java/io/netbird/client/repository/VPNServiceBindListener.java, app/src/main/java/io/netbird/client/repository/VPNServiceRepository.java
Removed VPNServiceRepository and VPNServiceBindListener; added route management methods and listener hooks to ServiceAccessor (select/deselect routes, add/remove RouteChangeListener).
Main application surface
app/src/main/java/io/netbird/client/MyApplication.java, app/src/main/java/io/netbird/client/MainActivity.java
Removed getVPNServiceRepository() from MyApplication; MainActivity adds delegating route APIs and an updateProfileMenuItem helper invoked at lifecycle/navigation points.
Profile model & wrapper
tool/src/main/java/io/netbird/client/tool/Profile.java, tool/src/main/java/io/netbird/client/tool/ProfileManagerWrapper.java, tool/src/main/java/io/netbird/client/tool/Preferences.java
Added Profile value class and ProfileManagerWrapper (list/switch/add/logout/remove, active config/state path getters); removed Preferences.configFile/stateFile helpers.
Engine & VPN integration
tool/src/main/java/io/netbird/client/tool/EngineRunner.java, tool/src/main/java/io/netbird/client/tool/VPNService.java
EngineRunner constructor changed to accept ProfileManagerWrapper; AndroidPlatformFiles creation delayed until run-time using active profile paths. VPNService now uses ProfileManagerWrapper, adds ACTION_STOP_ENGINE and a receiver to stop engine for profile operations.
Fragments using config path
app/src/main/java/io/netbird/client/ui/advanced/AdvancedFragment.java, app/src/main/java/io/netbird/client/ui/server/ChangeServerFragment.java
Replaced direct Preferences.configFile(...) calls with ProfileManagerWrapper.getActiveConfigPath() and added error handling for failures.
Networks UI & ViewModel
app/src/main/java/io/netbird/client/ui/home/NetworksFragment.java, app/src/main/java/io/netbird/client/ui/home/NetworksFragmentViewModel.java
Migrated from VPNServiceRepository to injecting ServiceAccessor; ViewModel implements RouteChangeListener, StateListener, adds getFactory(ServiceAccessor), and delegates route selection to ServiceAccessor.
Profiles UI components
app/src/main/java/io/netbird/client/ui/profile/ProfilesFragment.java, app/src/main/java/io/netbird/client/ui/profile/ProfilesAdapter.java
Added ProfilesFragment and ProfilesAdapter with ProfileActionListener; implements add/switch/logout/remove flows, dialogs, validation, and UI updates.
Resources: layouts, menu, navigation, drawable, strings
app/src/main/res/layout/fragment_profiles.xml, app/src/main/res/layout/list_item_profile.xml, app/src/main/res/menu/activity_main_drawer.xml, app/src/main/res/navigation/mobile_navigation.xml, app/src/main/res/drawable/ic_menu_profile.xml, app/src/main/res/values/strings.xml
Added profile screen layouts, list item layout, drawer menu item nav_profiles, navigation entry, drawable icon, and profile-related strings.
Submodule
netbird
Submodule pointer updated; no functional changes.

Sequence Diagram(s)

sequenceDiagram
    participant UI as ProfilesFragment / MainActivity
    participant PM as ProfileManagerWrapper
    participant App as VPNService
    participant Engine as EngineRunner

    UI->>PM: switchProfile(name)
    PM->>App: broadcast ACTION_STOP_ENGINE
    App->>App: receive ACTION_STOP_ENGINE → stop running engine
    App-->>PM: (implicit) engine stopped
    PM->>PM: change active profile (gomobile ProfileManager)
    PM-->>UI: return success
    UI->>Engine: start/run (on next engine start)
    Engine->>PM: getActiveConfigPath() / getActiveStateFilePath()
    Engine->>Engine: create AndroidPlatformFiles with paths and start client
Loading

Estimated code review effort

🎯 4 (Complex) | ⏱️ ~45–75 minutes

  • Review focus:
    • ServiceAccessor method exception propagation and all callers (MainActivity, ViewModel).
    • ProfileManagerWrapper.stopEngine() broadcasting and VPNService receiver registration/unregistration for races.
    • EngineRunner runtime AndroidPlatformFiles creation when profile paths are missing/invalid.
    • All call-sites updated for removed VPNServiceRepository/getVPNServiceRepository().
    • UI synchronization points (updateProfileMenuItem, navigation lifecycle).

Possibly related PRs

Suggested reviewers

  • doromaraujo

Poem

🐇 I hop through code with whiskers bright,
New profiles bloom in soft spotlight.
Routes now flow where rabbits lead,
Configs remapped for every seed.
A joyful sprint — the network's right! 🥕

Pre-merge checks and finishing touches

❌ Failed checks (1 warning, 1 inconclusive)
Check name Status Explanation Resolution
Docstring Coverage ⚠️ Warning Docstring coverage is 11.76% which is insufficient. The required threshold is 80.00%. You can run @coderabbitai generate docstrings to improve docstring coverage.
Title check ❓ Inconclusive The title 'Add profile switch' is vague and does not clearly convey the scope of changes, which include comprehensive profile management (add, switch, logout, remove), API refactoring, and service architecture changes. Consider a more descriptive title that captures the main objectives, such as 'Add profile management UI and refactor service architecture' or 'Implement profile management with routes API'.
✅ Passed checks (1 passed)
Check name Status Explanation
Description Check ✅ Passed Check skipped - CodeRabbit’s high-level summary is enabled.
✨ Finishing touches
  • 📝 Generate docstrings
🧪 Generate unit tests (beta)
  • Create PR with unit tests
  • Post copyable unit tests in a comment
  • Commit unit tests in branch profile-switch

Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out.

❤️ Share

Comment @coderabbitai help to get the list of available commands and usage tips.

@pappz pappz marked this pull request as ready for review December 1, 2025 14:45
@pappz pappz changed the base branch from fix-unbind to main December 1, 2025 17:08
Copy link

@coderabbitai coderabbitai bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 5

🧹 Nitpick comments (14)
app/src/main/java/io/netbird/client/ui/server/ChangeServerFragment.java (1)

111-118: Config path via ProfileManagerWrapper is correct; consider failure handling

Using ProfileManagerWrapper.getActiveConfigPath() to populate CONFIG_FILE_PATH_KEY keeps this aligned with the new profile model. The hard RuntimeException on failure makes this fragment non-recoverable if the active profile/config path can’t be resolved; if that’s not expected to be truly fatal, consider logging and showing a user-facing error instead of crashing.

Also applies to: 120-125

app/src/main/res/menu/activity_main_drawer.xml (1)

6-11: Drawer item wiring is fine; verify checked-state behavior across groups

The new group_profile and nav_profiles item are correctly defined and reference the new icon/title. Depending on how you hook the drawer to NavController, you may end up with one checked item in each group (profiles + another destination); if that’s not desired, consider having all navigable items in a single checkable group.

app/src/main/java/io/netbird/client/ui/advanced/AdvancedFragment.java (1)

71-79: Profile-based config path is correct; reassess throwing on failure

Initializing goPreferences from ProfileManagerWrapper.getActiveConfigPath() correctly ties advanced settings to the active profile. However, converting any failure here into a RuntimeException will crash the screen; if active profile resolution can fail in real-world cases (e.g., corrupted profile state), you might instead want to show a Toast/dialog and disable advanced options rather than crashing.

app/src/main/java/io/netbird/client/ui/profile/ProfilesFragment.java (3)

79-100: Unused dialog view inflation.

dialogView is inflated on line 80 but never used. The dialog only uses the EditText created on line 81. Either use the inflated layout or remove the unused inflation.

 private void showAddDialog() {
-    View dialogView = LayoutInflater.from(requireContext()).inflate(R.layout.dialog_simple_alert_message, null);
     EditText input = new EditText(requireContext());
     input.setHint(R.string.profiles_dialog_add_hint);

164-167: onBackPressed() is deprecated.

Activity.onBackPressed() is deprecated in API 33+. Consider using the OnBackPressedDispatcher or NavController.popBackStack() for navigation.

-            // Navigate back to home
-            requireActivity().onBackPressed();
+            // Navigate back to home
+            requireActivity().getOnBackPressedDispatcher().onBackPressed();

Or if using Navigation Component:

NavHostFragment.findNavController(this).popBackStack();

146-150: Hardcoded error message strings.

Error messages on lines 148, 170, 188, and 217 use hardcoded English text concatenated with the exception message. Consider using string resources with placeholders for consistency and i18n support.

-            Toast.makeText(requireContext(),
-                    "Failed to add profile: " + e.getMessage(),
-                    Toast.LENGTH_SHORT).show();
+            Toast.makeText(requireContext(),
+                    getString(R.string.profiles_error_add_failed, e.getMessage()),
+                    Toast.LENGTH_SHORT).show();
app/src/main/java/io/netbird/client/ui/profile/ProfilesAdapter.java (2)

7-7: Unused import.

ImageView is imported but not used in this file.

-import android.widget.ImageView;

82-89: Hardcoded "default" profile name.

The string "default" is hardcoded here and in ProfilesFragment.java (line 195). Consider extracting this to a constant for maintainability.

// In a shared location (e.g., Profile.java or a constants class)
public static final String DEFAULT_PROFILE_NAME = "default";
app/src/main/java/io/netbird/client/MainActivity.java (2)

380-398: Exception declaration mismatch with actual behavior.

Both selectRoute and deselectRoute declare throws Exception but silently return when mBinder is null instead of throwing. This inconsistency can mislead callers who expect an exception on failure.

Consider either:

  1. Throwing an exception when mBinder is null to match the signature
  2. Removing throws Exception if the null case should be silently ignored
 @Override
 public void selectRoute(String route) throws Exception {
     if (mBinder == null) {
         Log.w(LOGTAG, "VPN binder is null");
-        return;
+        throw new IllegalStateException("VPN service not bound");
     }

     mBinder.selectRoute(route);
 }

 @Override
 public void deselectRoute(String route) throws Exception {
     if (mBinder == null) {
         Log.w(LOGTAG, "VPN binder is null");
-        return;
+        throw new IllegalStateException("VPN service not bound");
     }

     mBinder.deselectRoute(route);
 }

614-628: Instantiating ProfileManagerWrapper on every call is inefficient.

updateProfileMenuItem is called from onCreate, onResume, and destination changes. Each invocation creates a new ProfileManagerWrapper instance. Consider caching the wrapper as a field.

+    private io.netbird.client.tool.ProfileManagerWrapper profileManager;
+
     private void updateProfileMenuItem(NavigationView navigationView) {
         try {
-            // Get active profile from ProfileManager instead of reading file
-            io.netbird.client.tool.ProfileManagerWrapper profileManager =
-                new io.netbird.client.tool.ProfileManagerWrapper(this);
+            if (profileManager == null) {
+                profileManager = new io.netbird.client.tool.ProfileManagerWrapper(this);
+            }
             String activeProfile = profileManager.getActiveProfile();
             Menu menu = navigationView.getMenu();
             MenuItem profileItem = menu.findItem(R.id.nav_profiles);
             if (profileItem != null && activeProfile != null) {
                 profileItem.setTitle(activeProfile);
             }
         } catch (Exception e) {
             Log.e(LOGTAG, "Failed to update profile menu item", e);
         }
     }
app/src/main/java/io/netbird/client/ui/home/NetworksFragmentViewModel.java (4)

28-39: Suppress unchecked cast warning in factory.

The unchecked cast (T) on line 34 will generate a compiler warning. Consider adding @SuppressWarnings("unchecked") to the method.

     public static ViewModelProvider.Factory getFactory(ServiceAccessor serviceAccessor) {
         return new ViewModelProvider.Factory() {
             @NonNull
             @Override
+            @SuppressWarnings("unchecked")
             public <T extends ViewModel> T create(@NonNull Class<T> modelClass) {
                 if (modelClass.isAssignableFrom(NetworksFragmentViewModel.class)) {
                     return (T) new NetworksFragmentViewModel(serviceAccessor);
                 }
                 throw new IllegalArgumentException("Unknown ViewModel class");
             }
         };
     }

51-63: Wrapping in RuntimeException loses error context.

Re-throwing a generic RuntimeException obscures the original exception type and provides no additional context. Consider logging or adding a message, or simply declaring the checked exception if the caller can handle it.

     private List<String> createPeerRoutesList(PeerRoutes peerRoutes) {
         List<String> routes = new ArrayList<>();

         try {
             for (int i = 0; i < peerRoutes.size(); i++) {
                 routes.add(peerRoutes.get(i));
             }
         } catch (Exception e) {
-            throw new RuntimeException(e);
+            throw new RuntimeException("Failed to create peer routes list", e);
         }

         return routes;
     }

86-88: Same issue: add context to RuntimeException.

         } catch (Exception e) {
-            throw new RuntimeException(e);
+            throw new RuntimeException("Failed to create network domains list", e);
         }

149-189: Consider using a default adapter or base class for StateListener.

All these empty implementations add boilerplate. If StateListener is under your control, adding default empty implementations to the interface would simplify this ViewModel. Alternatively, create a StateListenerAdapter base class.

📜 Review details

Configuration used: CodeRabbit UI

Review profile: CHILL

Plan: Pro

📥 Commits

Reviewing files that changed from the base of the PR and between f9376e3 and a2cfff6.

📒 Files selected for processing (24)
  • app/src/main/java/io/netbird/client/CustomTabURLOpener.java (1 hunks)
  • app/src/main/java/io/netbird/client/MainActivity.java (7 hunks)
  • app/src/main/java/io/netbird/client/MyApplication.java (0 hunks)
  • app/src/main/java/io/netbird/client/ServiceAccessor.java (2 hunks)
  • app/src/main/java/io/netbird/client/repository/VPNServiceBindListener.java (0 hunks)
  • app/src/main/java/io/netbird/client/repository/VPNServiceRepository.java (0 hunks)
  • app/src/main/java/io/netbird/client/ui/advanced/AdvancedFragment.java (4 hunks)
  • app/src/main/java/io/netbird/client/ui/home/NetworksFragment.java (4 hunks)
  • app/src/main/java/io/netbird/client/ui/home/NetworksFragmentViewModel.java (2 hunks)
  • app/src/main/java/io/netbird/client/ui/profile/ProfilesAdapter.java (1 hunks)
  • app/src/main/java/io/netbird/client/ui/profile/ProfilesFragment.java (1 hunks)
  • app/src/main/java/io/netbird/client/ui/server/ChangeServerFragment.java (2 hunks)
  • app/src/main/res/drawable/ic_menu_profile.xml (1 hunks)
  • app/src/main/res/layout/fragment_profiles.xml (1 hunks)
  • app/src/main/res/layout/list_item_profile.xml (1 hunks)
  • app/src/main/res/menu/activity_main_drawer.xml (1 hunks)
  • app/src/main/res/navigation/mobile_navigation.xml (1 hunks)
  • app/src/main/res/values/strings.xml (2 hunks)
  • netbird (1 hunks)
  • tool/src/main/java/io/netbird/client/tool/EngineRunner.java (2 hunks)
  • tool/src/main/java/io/netbird/client/tool/Preferences.java (0 hunks)
  • tool/src/main/java/io/netbird/client/tool/Profile.java (1 hunks)
  • tool/src/main/java/io/netbird/client/tool/ProfileManagerWrapper.java (1 hunks)
  • tool/src/main/java/io/netbird/client/tool/VPNService.java (4 hunks)
💤 Files with no reviewable changes (4)
  • app/src/main/java/io/netbird/client/repository/VPNServiceBindListener.java
  • app/src/main/java/io/netbird/client/MyApplication.java
  • tool/src/main/java/io/netbird/client/tool/Preferences.java
  • app/src/main/java/io/netbird/client/repository/VPNServiceRepository.java
🧰 Additional context used
🧬 Code graph analysis (5)
app/src/main/java/io/netbird/client/ui/home/NetworksFragment.java (1)
app/src/main/java/io/netbird/client/ui/home/NetworksFragmentViewModel.java (1)
  • NetworksFragmentViewModel (18-190)
app/src/main/java/io/netbird/client/ui/profile/ProfilesAdapter.java (1)
tool/src/main/java/io/netbird/client/tool/Profile.java (1)
  • Profile (3-37)
tool/src/main/java/io/netbird/client/tool/ProfileManagerWrapper.java (1)
tool/src/main/java/io/netbird/client/tool/VPNService.java (1)
  • VPNService (25-321)
app/src/main/java/io/netbird/client/ui/profile/ProfilesFragment.java (2)
tool/src/main/java/io/netbird/client/tool/Profile.java (1)
  • Profile (3-37)
tool/src/main/java/io/netbird/client/tool/ProfileManagerWrapper.java (1)
  • ProfileManagerWrapper (16-145)
app/src/main/java/io/netbird/client/ui/server/ChangeServerFragment.java (2)
tool/src/main/java/io/netbird/client/tool/ProfileManagerWrapper.java (1)
  • ProfileManagerWrapper (16-145)
app/src/main/java/io/netbird/client/ui/server/ChangeServerFragmentViewModel.java (1)
  • ChangeServerFragmentViewModel (19-182)
⏰ Context from checks skipped due to timeout of 90000ms. You can increase the timeout in your CodeRabbit configuration to a maximum of 15 minutes (900000ms). (2)
  • GitHub Check: build-debug
  • GitHub Check: Analyze (java-kotlin)
🔇 Additional comments (22)
netbird (1)

1-1: Ensure the gomobile API dependency is verified through the build process.

The submodule pointer update introduces new gomobile APIs used by ProfileManagerWrapper (Android.newProfileManager(), switchProfile(), addProfile(), etc.). Verify that the new commit 49cd86440f222682f21c17029b1e8a5cc3ad6d05 provides these ProfileManager APIs by confirming the Android build succeeds without API compatibility errors. The GitHub Actions workflows will catch any missing or incompatible APIs during compilation.

app/src/main/java/io/netbird/client/CustomTabURLOpener.java (1)

30-34: Lambda callback refactor is behaviorally identical

The lambda correctly preserves the previous behavior (resetting isOpened and invoking resultCallback.onClosed()), so this is a safe and cleaner refactor.

app/src/main/res/navigation/mobile_navigation.xml (1)

27-31: Profiles destination wiring looks consistent

nav_profiles correctly targets io.netbird.client.ui.profile.ProfilesFragment with the expected label and layout; this should integrate cleanly with the drawer item and profiles UI.

app/src/main/res/drawable/ic_menu_profile.xml (1)

1-9: Vector icon definition is sound

The profile vector drawable is well-formed (24dp viewport, single path, solid fill) and appropriate for use as the new drawer/menu icon.

app/src/main/java/io/netbird/client/ui/home/NetworksFragment.java (1)

39-55: ServiceAccessor injection is correct; ensure hosting activity implements it

Using ServiceAccessor via onAttach and passing it into NetworksFragmentViewModel.getFactory(serviceAccessor) is consistent and keeps the ViewModel decoupled from the Fragment. Just make sure every activity that can host NetworksFragment now implements ServiceAccessor, otherwise this will throw at runtime.

Also applies to: 69-70

app/src/main/res/layout/fragment_profiles.xml (1)

1-40: Profiles layout is structurally solid

RecyclerView and FAB are correctly constrained; padding and a11y (FAB contentDescription) look good. This should give a clean profiles list screen out of the box.

app/src/main/res/layout/list_item_profile.xml (1)

1-116: LGTM!

The layout is well-structured using MaterialCardView and ConstraintLayout with proper constraint chains for button distribution. The active badge visibility toggle and button styling are appropriately configured.

app/src/main/java/io/netbird/client/ServiceAccessor.java (1)

3-19: LGTM!

The interface extension is clean. Route management methods appropriately declare checked exceptions, and the listener pattern for route changes is consistent with the existing service architecture.

app/src/main/java/io/netbird/client/ui/profile/ProfilesAdapter.java (1)

69-106: Click listeners re-check conditions that are already enforced by button state.

The click listeners on lines 92 and 102 re-check !profile.isActive() and !profile.getName().equals("default") even though the buttons are already disabled. While this is defensive, it's redundant.

The defensive checks are acceptable as they provide an extra safety layer if button state is somehow inconsistent.

tool/src/main/java/io/netbird/client/tool/EngineRunner.java (2)

76-96: LGTM - Runtime profile path resolution enables dynamic switching.

The refactoring to retrieve config/state paths at runtime from ProfileManagerWrapper is well-structured and enables profile switching without recreating the entire client. Error handling with RuntimeException is appropriate for this critical path.


87-90: Uncaught RuntimeException on background thread.

If profileManager.getActiveConfigPath() or getActiveStateFilePath() fails, the RuntimeException is thrown inside a Runnable on a new thread (line 115). This will crash the thread but may not be surfaced to the user or properly logged beyond the thread's default uncaught exception handler.

Verify that the thread's uncaught exception is handled appropriately. Consider catching the exception and calling notifyError() instead:

             } catch (Exception e) {
                 Log.e(LOGTAG, "Failed to get profile paths from ProfileManager", e);
-                throw new RuntimeException("Failed to get profile paths: " + e.getMessage(), e);
+                notifyError(new Exception("Failed to get profile paths: " + e.getMessage(), e));
+                return;
             }
tool/src/main/java/io/netbird/client/tool/ProfileManagerWrapper.java (2)

95-100: TOCTOU between getActiveProfile() and conditional stopEngine().

There's a time-of-check-to-time-of-use gap: the active profile could change between the getActiveProfile() call on line 96 and the stopEngine() call on line 99 if another thread/process modifies the profile state.

This is likely a minor concern in practice since profile operations are user-initiated and serialized through the UI. Verify this assumption holds.


31-47: LGTM - Robust profile listing with null-safety.

The listProfiles() method properly handles null checks for both the array and individual profile elements, with exception logging and graceful empty list fallback.

tool/src/main/java/io/netbird/client/tool/VPNService.java (3)

57-70: LGTM - Clean integration of ProfileManagerWrapper.

The ProfileManagerWrapper is properly instantiated and passed to EngineRunner, enabling dynamic profile-based configuration. The initialization order is logical, with the profile manager created before the engine runner.


81-99: LGTM - Broadcast receiver properly registered.

The stopEngineReceiver is correctly registered with RECEIVER_NOT_EXPORTED flag, ensuring only internal broadcasts are received. The null check on engineRunner before calling stop() is good defensive practice.


136-143: LGTM - Proper cleanup with exception handling.

The receiver unregistration in onDestroy() properly catches IllegalArgumentException in case the receiver was never registered or already unregistered.

app/src/main/java/io/netbird/client/MainActivity.java (2)

400-418: LGTM!

The route change listener management methods correctly delegate to the binder with appropriate null checks and logging.


119-121: Profile menu updates in multiple lifecycle hooks look correct.

Updating the profile menu item in onCreate, onResume, and when navigating back to home ensures the UI stays in sync after profile switches. The null checks on binding and binding.navView are appropriate.

Also applies to: 171-174, 263-266

app/src/main/java/io/netbird/client/ui/home/NetworksFragmentViewModel.java (2)

23-26: LGTM!

The constructor properly initializes the service accessor and registers the route change listener, and onCleared() correctly unregisters to prevent leaks.

Also applies to: 41-45


128-134: Correct use of postValue for background thread updates.

Using postValue instead of setValue is correct here since route changes may be triggered from background threads.

app/src/main/res/values/strings.xml (2)

117-143: LGTM! Comprehensive and well-structured profile strings.

The string resources cover all necessary UI scenarios including dialogs, validation errors, and success messages. Format specifiers (%s) are used correctly for dynamic profile names, and apostrophes are properly escaped.


14-14: LGTM!

Menu string added appropriately alongside other menu items.

Comment on lines 328 to 337
private void setPreSharedKey(String key, Context context) {
String configFilePath = Preferences.configFile(context);
ProfileManagerWrapper profileManager = new ProfileManagerWrapper(context);
String configFilePath;
try {
configFilePath = profileManager.getActiveConfigPath();
} catch (Exception e) {
Toast.makeText(context, "Failed to get config path: " + e.getMessage(), Toast.LENGTH_LONG).show();
return;
}
io.netbird.gomobile.android.Preferences preferences = new io.netbird.gomobile.android.Preferences(configFilePath);
Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟡 Minor

Config path reuse is good; fix pre‑shared key error Toast message

Using ProfileManagerWrapper in both setPreSharedKey and hasPreSharedKey keeps these operations aligned with the active profile and adds reasonable error handling (Toast/log + early return), which is good.

There is, however, a bug in the error Toast in setPreSharedKey: it concatenates the string resource ID directly into the message, resulting in something like "2131230890: <msg>" instead of the localized text. Consider this adjustment:

-        } catch (Exception e) {
-            Log.e(LOGTAG, "Failed to save pre-shared key", e);
-            Toast.makeText(context, R.string.advanced_presharedkey_save_error + ": " + e.getMessage(), Toast.LENGTH_LONG).show();
-        }
+        } catch (Exception e) {
+            Log.e(LOGTAG, "Failed to save pre-shared key", e);
+            String message = context.getString(R.string.advanced_presharedkey_save_error) + ": " + e.getMessage();
+            Toast.makeText(context, message, Toast.LENGTH_LONG).show();
+        }

Also applies to: 348-356

🤖 Prompt for AI Agents
In app/src/main/java/io/netbird/client/ui/advanced/AdvancedFragment.java around
lines 328-337 (and likewise apply to 348-356), the Toast constructs concatenate
a string resource ID instead of using the localized string; change the Toast to
call context.getString(...) (preferably a string resource that accepts the
exception/message as an argument) or build the message via
context.getString(R.string.your_string, e.getMessage()) so the localized text is
used, then pass that resulting String into Toast.makeText(...). Ensure both
occurrences are updated and remove direct concatenation of the resource ID.

Comment on lines +65 to +74
public void switchProfile(String profileName) throws Exception {
if (profileName == null || profileName.trim().isEmpty()) {
throw new IllegalArgumentException("Profile name cannot be empty");
}

// Stop VPN service before switching profile
stopEngine();

profileManager.switchProfile(profileName);
}
Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟠 Major

🧩 Analysis chain

🏁 Script executed:

cd tool/src/main/java/io/netbird/client/tool && cat -n ProfileManagerWrapper.java | head -80

Repository: netbirdio/android-client

Length of output: 3125


🏁 Script executed:

# Search for stopEngine implementation and related broadcast code
rg -n "stopEngine|ACTION_STOP_ENGINE" --type=java -A 5 -B 2

Repository: netbirdio/android-client

Length of output: 17821


🏁 Script executed:

# Search for VPNService to understand broadcast handling
rg -n "class VPNService|sendBroadcast" --type=java -A 3

Repository: netbirdio/android-client

Length of output: 2277


🏁 Script executed:

# Check if profileManager.switchProfile accesses any engine state or config
rg -n "switchProfile|profileManager.switchProfile" --type=java -B 3 -A 3

Repository: netbirdio/android-client

Length of output: 3502


🏁 Script executed:

# Look for any synchronization mechanisms (locks, CountdownLatch, etc.) around stopEngine
rg -n "synchronized|Lock|CountdownLatch|Semaphore|wait\(\)|notify\(\)" tool/src/main/java/io/netbird/client/tool/ProfileManagerWrapper.java

Repository: netbirdio/android-client

Length of output: 50


🏁 Script executed:

# Search for EngineRunner class and stop method more thoroughly
fd -n "EngineRunner" --type f --extension java

Repository: netbirdio/android-client

Length of output: 298


🏁 Script executed:

# Check if there's documentation or comments about thread safety
rg -n "EngineRunner|engineRunner\.stop" --type=java -B 2 -A 8 | head -100

Repository: netbirdio/android-client

Length of output: 9379


🏁 Script executed:

# Get the full EngineRunner.java file to see stop() implementation
wc -l tool/src/main/java/io/netbird/client/tool/EngineRunner.java

Repository: netbirdio/android-client

Length of output: 131


🏁 Script executed:

# Read EngineRunner to find stop() method
cat tool/src/main/java/io/netbird/client/tool/EngineRunner.java | head -150

Repository: netbirdio/android-client

Length of output: 6018


🏁 Script executed:

# Read the rest of EngineRunner to find stop() method
tail -n +150 tool/src/main/java/io/netbird/client/tool/EngineRunner.java

Repository: netbirdio/android-client

Length of output: 2653


Race condition confirmed: profile switch executes before engine actually stops.

stopEngine() sends an asynchronous broadcast (line 138), but switchProfile() is called synchronously on line 73 without waiting for the engine to stop. The BroadcastReceiver.onReceive() handler in VPNService (which calls engineRunner.stop()) executes later on the main thread, potentially after the profile has already been switched. Although EngineRunner.stop() itself is synchronized, this doesn't prevent the race since the broadcast delivery is inherently asynchronous.

To fix this, consider using a direct synchronous call to stop the engine (e.g., via service binding) instead of broadcasting, or add a synchronization mechanism (e.g., CountDownLatch or callback) to confirm engine stop before proceeding with the profile switch.

🤖 Prompt for AI Agents
tool/src/main/java/io/netbird/client/tool/ProfileManagerWrapper.java around
lines 65-74: the current switchProfile calls stopEngine() which only sends an
asynchronous broadcast, causing a race where
profileManager.switchProfile(profileName) executes before the engine actually
stops; change stopEngine to use a synchronous stop path (preferably bind to the
VPN service and call a direct stop method on the service/EngineRunner) or
implement a synchronization handshake (e.g., send the stop intent but block with
a CountDownLatch or wait on a callback that the VPNService/EngineRunner signals
when fully stopped) and include a reasonable timeout and proper exception
handling so switchProfile only calls profileManager.switchProfile(profileName)
after the engine stop confirmation.

Comment on lines +134 to +144
private void stopEngine() {
try {
Intent stopIntent = new Intent(VPNService.ACTION_STOP_ENGINE);
stopIntent.setPackage(context.getPackageName());
context.sendBroadcast(stopIntent);
Log.d(TAG, "Sent stop engine broadcast for profile operation");
} catch (Exception e) {
Log.w(TAG, "Failed to send stop engine broadcast: " + e.getMessage());
// Don't throw exception - profile operations should continue even if stop fails
}
}
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

We could use a similar approach that was used with EngineRestarter, wrapping EngineRunner to a class to both stop the engine without a broadcast receiver and to track for sure that it was stopped using the ServiceStateListener interface and pass it as a parameter to ProfileManagerWrapper.

If using this approach, the ProfileManagerWrapper instance could be created in VPNService (either when bound or created), and provided to the ProfilesFragment via the ServiceAccessor interface.

Copy link
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I chose this approach because a synchronous call during profile switching is not expected. It’s fine if it finishes once in the background, but I prefer your solution. If we want to use this method differently, then it would be better to expose it through a Service Accessor.

Copy link

@coderabbitai coderabbitai bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 0

♻️ Duplicate comments (1)
app/src/main/java/io/netbird/client/ui/profile/ProfilesFragment.java (1)

79-114: Tighten add‑dialog implementation and align sanitization semantics/i18n

The validation flow is sound (no empty names, no “all invalid characters” names, dialog doesn’t auto‑dismiss), but a few details can be improved:

  • dialogView (Line 80) is inflated and then never used. Either bind input from that layout or drop the inflation to avoid unnecessary work and confusion.
  • The error message for invalid characters is hard‑coded (Lines 105‑107). This should be moved to a string resource (you already appear to have profiles_error_invalid_name) so it’s localizable and consistent with the rest of the UI.
  • sanitizeProfileName() returns the sanitized value, but you only use it to check for emptiness and still pass the original profileName into addProfile() (Line 111). That means:
    • The success/error toasts use the unsanitized string.
    • The stored/listed name will be whatever the Go client sanitizes to, which can differ from what the user just saw in the toast.

Consider either (a) passing sanitizedName to addProfile() and using it in success messages, or (b) renaming sanitizeProfileName() / updating its Javadoc to make it clear it is used only for validation, not for the actual persisted name, so future maintainers don’t assume the UI and stored names always match.

Also applies to: 116-130

🧹 Nitpick comments (2)
app/src/main/java/io/netbird/client/ui/profile/ProfilesFragment.java (1)

162-182: Harden user‑visible error handling and avoid leaking raw exception messages

Across addProfile, switchProfile, logoutProfile, and removeProfile, failures are shown with toasts like "Failed to X: " + e.getMessage():

  • e.getMessage() can be null, resulting in slightly odd “...: null” messages.
  • Raw exception messages can be technical, inconsistent, or expose internal details that aren’t useful to end users.
  • These strings are not localized, unlike the success paths and specific “already exists” / cannot‑remove messages.

Consider:

  • Using localized, human‑friendly fallback messages for the generic failure cases (e.g., R.string.profiles_error_add_generic, R.string.profiles_error_switch_generic, etc.).
  • Optionally logging the full exception (as you already do with Log.e) but keeping the toast generic, without e.getMessage().

Functionally things work, but this would tighten UX and i18n and avoid accidentally surfacing internal error details.

Also applies to: 197-201, 216-219, 245-248

tool/src/main/java/io/netbird/client/tool/Profile.java (1)

5-41: Immutable value object with null‑safety looks good

This class is in good shape:

  • Immutable fields with a constructor guard against null names.
  • equals/hashCode implemented via Objects and consistent (both key off name).
  • toString cleanly reflects the active state.

If you want to align fully with your use of Objects, you could optionally replace the manual null check in the constructor with Objects.requireNonNull(name, "Profile name cannot be null");, but that’s purely stylistic.

📜 Review details

Configuration used: CodeRabbit UI

Review profile: CHILL

Plan: Pro

📥 Commits

Reviewing files that changed from the base of the PR and between 57ffbcc and c6aaa19.

📒 Files selected for processing (3)
  • app/src/main/java/io/netbird/client/ui/profile/ProfilesFragment.java (1 hunks)
  • netbird (1 hunks)
  • tool/src/main/java/io/netbird/client/tool/Profile.java (1 hunks)
✅ Files skipped from review due to trivial changes (1)
  • netbird
🧰 Additional context used
🧬 Code graph analysis (1)
app/src/main/java/io/netbird/client/ui/profile/ProfilesFragment.java (2)
tool/src/main/java/io/netbird/client/tool/Profile.java (1)
  • Profile (5-42)
tool/src/main/java/io/netbird/client/tool/ProfileManagerWrapper.java (1)
  • ProfileManagerWrapper (16-145)
⏰ Context from checks skipped due to timeout of 90000ms. You can increase the timeout in your CodeRabbit configuration to a maximum of 15 minutes (900000ms). (1)
  • GitHub Check: Analyze (java-kotlin)

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

3 participants