Skip to content

Align the Android client with the iOS and desktop clients - #179

Open
pappz wants to merge 59 commits into
mainfrom
ux/ios-style-redesign
Open

Align the Android client with the iOS and desktop clients#179
pappz wants to merge 59 commits into
mainfrom
ux/ios-style-redesign

Conversation

@pappz

@pappz pappz commented May 10, 2026

Copy link
Copy Markdown
Collaborator

Reworks the Android UI around the iOS client's patterns, and brings over features the desktop client already had.

Navigation

  • Replaces the side drawer and the modal bottom-sheet pager with a bottom navigation bar: Home, Peers, Resources, Settings.
  • Sub-screens (Advanced, Profiles, Management Server, Troubleshoot, About) hang off the Settings tab as sectioned lists, mirroring iOS.
  • Tapping a tab unwinds any sub-screen it left open.
  • Uses a navigation rail on large screens and TV; the toolbar shows only on sub-screens.

Home

  • Connect toggle no longer flickers while connecting or disconnecting.
  • Exit node selector, always on screen and disabled when there is nothing to pick.
  • IP rows collapse into one expandable box with copy actions.
  • The IPv6 address now shows up: it was parsed but never displayed, so the row stayed empty on dual-stack peers. It is middle-ellipsized to fit, and the row is hidden when the peer has no IPv6.
  • Profile chip opens a picker sheet: one-tap switch plus a link to manage profiles.

New screens and features

  • Peer detail screen, reachable from the peer list.
  • Profiles can be renamed, and each profile carries its own management server and setup key.
  • Localized into the nine desktop languages, with an in-app picker.
  • Theme mode picker; profile cards pick up the dark-mode contrast fix from Improve dark mode for profiles screen #137.
  • Advanced gains the IPv6 and force-relay toggles the desktop client has.

SSO session expiry

  • The persistent notification counts down to the session deadline and offers "Extend session"; extending re-authenticates without dropping the tunnel.
  • The home screen shows how long the session has left, and the connect status reads "Login required" instead of "Disconnected" once the server stops accepting the peer.

Fixes

  • Peer and resource lists no longer go stale after the app is rotated or restored.
  • The connection status no longer flashes "Disconnected" when re-entering the app.
  • When a self-hosted server can't be reached, the button now says "Add anyway" instead of silently accepting a second tap.

Replace the side drawer + collapsible bottom-sheet pattern with a
bottom navigation bar (4 tabs: Home, Peers, Resources, Settings) to
mirror the iOS client's TabView. Sub-screens (Advanced, Profiles,
Change Server, Troubleshoot, About) are reached from the Settings
tab and use an iOS-style sectioned list layout.

- New SettingsFragment (sectioned list mirroring iOSSettingsView)
- Promote PeersFragment / NetworksFragment to top-level destinations;
  drop the modal BottomDialogFragment + PagerAdapter
- Profile chip on Home opens a ProfilePickerSheet (one-tap switch +
  Manage profiles link), echoing the iOS ProfileBadge
- Restyle AdvancedFragment + TroubleshootFragment as sectioned lists;
  Theme Mode now opens a bottom-sheet picker
- Refresh menu icons to thinner outlined Material Symbols
- NavigationRailView via layout-w960dp for large screens / TV
- Toolbar hidden on top-level destinations; visible on sub-screens
- Profile cards adopt PR #137 dark-mode contrast fix
- Treat the empty-profile-state JSON read as a normal first-launch
  case in ProfileManagerWrapper instead of logging at error level
@coderabbitai

coderabbitai Bot commented May 10, 2026

Copy link
Copy Markdown
Contributor

Review Change Stack

Note

Reviews paused

It looks like this branch is under active development. To avoid overwhelming you with review comments due to an influx of new commits, CodeRabbit has automatically paused this review. You can configure this behavior by changing the reviews.auto_review.auto_pause_after_reviewed_commits setting.

Use the following commands to manage reviews:

  • @coderabbitai resume to resume automatic reviews.
  • @coderabbitai review to trigger a single review.

Use the checkboxes below for quick actions:

  • ▶️ Resume reviews
  • ✅ Review completed - (🔄 Check again to review again)
📝 Walkthrough

Walkthrough

The app switches from drawer navigation to bottom navigation, adds profile and theme bottom sheets, rebuilds home and settings screens around row-based layouts, and removes legacy dialog and Lottie-based UI pieces.

Changes

Navigation, profile, and settings shell

Layer / File(s) Summary
Navigation shell and activity chrome
app/src/main/java/io/netbird/client/MainActivity.java, app/src/main/res/layout/activity_main.xml, app/src/main/res/layout-w960dp/activity_main.xml, app/src/main/res/menu/bottom_nav.xml, app/src/main/res/navigation/mobile_navigation.xml, app/src/main/res/menu/activity_main_drawer.xml, app/src/main/res/layout/nav_header_main.xml, app/src/main/res/layout/nav_custom_bottom_item.xml
Main activity wiring moves to bottom navigation, the activity layouts and nav graph are updated, and the old drawer/header/custom item resources and drawer-specific activity handlers are removed.
Profile picker and home screen
app/src/main/java/io/netbird/client/ui/home/ProfilePickerSheet.java, app/src/main/java/io/netbird/client/ui/home/ProfilePickerAdapter.java, app/src/main/java/io/netbird/client/ui/home/HomeFragment.java, app/src/main/java/io/netbird/client/ui/profile/ProfilesFragment.java, app/src/main/res/layout/fragment_home.xml, app/src/main/res/layout/sheet_profile_picker.xml, app/src/main/res/layout/list_item_profile_picker.xml, app/src/main/res/layout/list_item_profile.xml, app/src/main/res/drawable/profile_chip_bg.xml, app/src/main/res/drawable/ic_profile_avatar.xml, app/src/main/res/values/strings.xml, app/src/main/res/values/colors.xml, app/src/main/res/values-night/colors.xml
Profile switching moves into a bottom sheet and profile chip; HomeFragment now drives connection/status/IP rows and profile updates; profile list/item visuals and supporting strings/colors are updated.
Theme picker and advanced settings
app/src/main/java/io/netbird/client/ui/advanced/ThemePickerSheet.java, app/src/main/java/io/netbird/client/ui/advanced/AdvancedFragment.java, app/src/main/res/layout/fragment_advanced.xml, app/src/main/res/layout/sheet_theme_picker.xml, app/src/main/res/color/connect_track_tint.xml, app/src/main/res/color/connect_thumb_tint.xml, app/src/main/res/drawable/connect_switch_track.xml, app/src/main/res/drawable/connect_switch_thumb.xml, app/src/main/res/drawable/info_row_bg.xml, app/src/main/res/drawable/info_row_divider.xml, app/src/main/res/drawable/settings_row_bg.xml, app/src/main/res/drawable/sheet_row_bg.xml, app/src/main/res/drawable/ic_add.xml, app/src/main/res/drawable/ic_arrow_drop_down.xml, app/src/main/res/drawable/ic_check.xml, app/src/main/res/drawable/ic_chevron_right.xml, app/src/main/res/drawable/ic_content_copy.xml, app/src/main/res/drawable/ic_open_in_new.xml, app/src/main/res/drawable/ic_menu_*, app/src/main/res/values/themes.xml, app/src/main/res/values-night/themes.xml, app/src/main/res/values/colors.xml, app/src/main/res/values-night/colors.xml, app/src/main/res/values/strings.xml
Advanced settings now use a theme picker sheet plus rebuilt row-based switches and toggles, with new theme, color, drawable, and string resources supporting the updated controls.
Settings screens and reusable rows
app/src/main/java/io/netbird/client/ui/settings/SettingsFragment.java, app/src/main/res/layout/fragment_settings.xml, app/src/main/res/layout/fragment_troubleshoot.xml, app/src/main/res/layout/fragment_networks.xml, app/src/main/res/layout/fragment_peers.xml, app/src/main/res/layout/fragment_profiles.xml, app/src/main/res/layout/fragment_server.xml, app/src/main/res/layout/fragment_about.xml, app/src/main/res/layout/list_item_setting.xml, app/src/main/res/layout/list_item_setting_toggle.xml, app/src/main/res/layout/list_item_setting_section.xml, app/src/main/res/layout/list_item_setting_divider.xml, app/src/main/res/res/drawable/info_row_divider.xml, app/src/main/res/values/dimens.xml, app/src/main/res/values-w960dp/dimens.xml, app/src/main/res/values/strings.xml
Settings, troubleshoot, peers, networks, profiles, server, and about layouts are restyled around row-based controls, section headers, and bottom inset spacing, with reusable settings-row layouts added.
Legacy UI and dependency cleanup
app/src/main/java/io/netbird/client/ui/home/BottomDialogFragment.java, app/src/main/java/io/netbird/client/ui/home/PagerAdapter.java, app/src/main/res/layout/fragment_bottom_dialog.xml, app/build.gradle.kts, gradle/libs.versions.toml, netbird
The old bottom dialog and pager implementation are removed, Lottie is dropped from Gradle and the version catalog, and the submodule pointer is advanced.

Estimated code review effort: 4 (Complex) | ~75 minutes

Possibly related PRs

Suggested reviewers: doromaraujo

Poem

I’m a rabbit with a hop and a grin,
Through bottom-nav lanes I now dart in. 🐇
Chips, sheets, and rows make the UI sing,
Old drawers and dialogs have gone for a spring.
I nibble the code, then I bounce away,
With tidy new screens for the bright app day.

🚥 Pre-merge checks | ✅ 4 | ❌ 1

❌ Failed checks (1 warning)

Check name Status Explanation Resolution
Docstring Coverage ⚠️ Warning Docstring coverage is 1.79% which is insufficient. The required threshold is 80.00%. Write docstrings for the functions missing them to satisfy the coverage threshold.
✅ Passed checks (4 passed)
Check name Status Explanation
Linked Issues check ✅ Passed Check skipped because no linked issues were found for this pull request.
Out of Scope Changes check ✅ Passed Check skipped because no linked issues were found for this pull request.
Description Check ✅ Passed Check skipped - CodeRabbit’s high-level summary is enabled.
Title check ✅ Passed The title accurately summarizes the broad redesign and navigation/UI alignment across Android, iOS, and desktop patterns.
✨ Finishing Touches
🧪 Generate unit tests (beta)
  • Create PR with unit tests
  • Commit unit tests in branch ux/ios-style-redesign

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.

@coderabbitai coderabbitai Bot left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

Actionable comments posted: 7

🧹 Nitpick comments (4)
app/src/main/res/layout/list_item_profile_picker.xml (1)

21-29: ⚡ Quick win

Constrain profile name to a single line for stable row layout.

At Line 21-29, long names can wrap and push row height unexpectedly. Add maxLines="1" and ellipsize="end" to keep picker rows consistent.

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@app/src/main/res/layout/list_item_profile_picker.xml` around lines 21 - 29,
The TextView with id profile_picker_name can wrap long names and change row
height; update the element (profile_picker_name) to constrain it to a single
line by adding maxLines="1" and ellipsize="end" so overflowing text is truncated
with an ellipsis and picker rows remain a stable height.
app/src/main/res/drawable/ic_nav_settings.xml (1)

7-8: ⚡ Quick win

Avoid hardcoded white fill for navigation icons.

Line 7 uses #FFFFFFFF, which makes this asset less theme-adaptive. Prefer a theme color (or rely on menu/icon tint) so the icon works across light/dark and future palette changes.

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@app/src/main/res/drawable/ic_nav_settings.xml` around lines 7 - 8, The vector
drawable currently hardcodes android:fillColor="#FFFFFFFF" which prevents
theming; update the path element in ic_nav_settings.xml to use a theme attribute
instead (e.g. replace android:fillColor="#FFFFFFFF" with
android:fillColor="?attr/colorControlNormal" or another appropriate theme attr
like ?attr/colorOnSurface), or remove the fillColor so the menu/icon tint can
apply; ensure the path element with android:pathData remains unchanged.
tool/src/main/java/io/netbird/client/tool/ProfileManagerWrapper.java (1)

56-64: ⚡ Quick win

Consider improving error detection robustness for first-launch profile state handling.

The code currently detects empty profile state files by matching the error message "unexpected end of JSON input". While this message originates from Go's standard encoding/json package (making it inherently stable), relying on message text remains fragile as a detection pattern. For improved maintainability, consider checking for the underlying exception type or implementing a more explicit state-check approach (e.g., attempting to detect empty/missing state files before calling getActiveProfile(), or requesting gomobile expose a dedicated error code or exception type for this scenario).

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@tool/src/main/java/io/netbird/client/tool/ProfileManagerWrapper.java` around
lines 56 - 64, The code in ProfileManagerWrapper is brittle because it detects
the first-launch empty profile by matching the error message text from
getActiveProfile; instead, detect the empty/missing state more robustly before
parsing (e.g., check the profile state file exists and its length/content is
empty) or catch a specific exception type if gomobile exposes one; update
getActiveProfile call-site to first inspect the profile state file (or wrap the
parsing call and inspect the underlying cause) and only treat the empty-file
case as a benign fallback (log via TAG) while letting other exceptions be logged
as errors.
app/src/main/res/layout/list_item_setting_section.xml (1)

4-12: ⚡ Quick win

Use the shared SettingsSectionHeader style here to avoid style drift.

This layout duplicates the same header attributes now defined in @style/SettingsSectionHeader. Reusing the style keeps section headers consistent across screens.

Suggested diff
 <TextView xmlns:android="http://schemas.android.com/apk/res/android"
     xmlns:tools="http://schemas.android.com/tools"
-    android:layout_width="match_parent"
-    android:layout_height="wrap_content"
-    android:paddingStart="20dp"
-    android:paddingEnd="20dp"
-    android:paddingTop="24dp"
-    android:paddingBottom="8dp"
-    android:textAllCaps="true"
-    android:textColor="@color/nb_txt_light"
-    android:textSize="13sp"
+    style="@style/SettingsSectionHeader"
     tools:text="Connection" />
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@app/src/main/res/layout/list_item_setting_section.xml` around lines 4 - 12,
The TextView in this layout duplicates header attributes that are already
captured by the shared style; replace the inline attributes by applying
style="@style/SettingsSectionHeader" on the TextView (remove duplicated
android:textAllCaps, android:textColor, android:textSize and any padding/text
attributes that the style covers) so the view uses the single source of truth
(SettingsSectionHeader) and avoid style drift; ensure any attributes not in the
style that are specific to this layout remain, and verify the TextView ID/text
content is unchanged.
🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

Inline comments:
In `@app/src/main/java/io/netbird/client/MainActivity.java`:
- Around line 129-142: The first-install onboarding only hides bottomNav but
still falls through to compute isTopLevel and shows the toolbar; update the
navController.addOnDestinationChangedListener handler so that when
destination.getId() == R.id.firstInstallFragment you both set
bottomNav.setVisibility(View.GONE) and call setToolbarVisible(false) (and then
skip the top-level logic, e.g., return or an else) so the onboarding screen
bypasses both bottom navigation and the toolbar; reference the listener,
firstInstallFragment, bottomNav, setToolbarVisible, and topLevelDestinations
when making the change.

In `@app/src/main/java/io/netbird/client/ui/home/ProfilePickerSheet.java`:
- Around line 133-143: The validation uses sanitizeProfileName(profileName) but
the code still calls profileManager.addProfile(profileName), so invalid
characters get written; change the write to use the sanitized value instead. In
ProfilePickerSheet.replace the call to profileManager.addProfile(profileName)
should be profileManager.addProfile(sanitized) (and likewise use sanitized in
the success Toast/getString call) so the persisted profile and user feedback
match the validated name.

In `@app/src/main/java/io/netbird/client/ui/settings/SettingsFragment.java`:
- Around line 56-58: The click handler for binding.rowDocumentation creates an
ACTION_VIEW Intent with DOCS_URL and directly calls startActivity, which can
crash if no browser-capable handler exists; update the lambda that handles
binding.rowDocumentation.setOnClickListener to first query the PackageManager
(e.g., via requireContext().getPackageManager()) and use
intent.resolveActivity(packageManager) or packageManager.queryIntentActivities
to ensure there's at least one handler before calling startActivity, and if none
is found fail gracefully (e.g., show a toast or disable the action).

In `@app/src/main/res/drawable/ic_add.xml`:
- Line 7: The vector path in ic_add.xml hardcodes android:fillColor="#FFFFFFFF";
remove that literal and make the icon theme-tintable by replacing the hardcoded
value with a neutral theme attribute (e.g. ?attr/colorControlNormal or
?attr/colorOnBackground) or a named color resource, so the host view/menu can
apply tinting; update the android:fillColor attribute accordingly (and ensure
ImageView/MenuItem usage applies tint if needed).

In `@app/src/main/res/layout/activity_main.xml`:
- Around line 27-45: The root ConstraintLayout in fragment_home.xml is missing
the bottom navigation inset padding; open fragment_home.xml, locate the root
ConstraintLayout element and add the attribute
android:paddingBottom="@dimen/bottom_nav_inset" so it matches other top-level
fragments (e.g., fragment_peers, fragment_networks) and prevents content from
being hidden behind the BottomNavigationView; ensure you reference the existing
dimen resource bottom_nav_inset (no other changes needed).

In `@app/src/main/res/layout/sheet_theme_picker.xml`:
- Around line 21-116: The theme rows (theme_row_system, theme_row_light,
theme_row_dark) don't expose selection state to TalkBack; update each row to set
an accessible role and dynamic state by adding a contentDescription or
stateDescription that reflects whether its associated check ImageView
(theme_check_system, theme_check_light, theme_check_dark) is visible/selected,
and update that description whenever selection changes; additionally, after
changing selection call announceForAccessibility with a short message (e.g.
"Light theme selected") or attach a custom AccessibilityDelegate on the row
views to send TYPE_VIEW_SELECTED/STATE_CHANGED events so screen readers announce
the new selection.

In `@app/src/main/res/values/dimens.xml`:
- Around line 11-12: The bottom_nav_inset resource is set universally to 80dp
but large-screen rail layouts shouldn't have that bottom inset; add a
configuration-specific override by creating a w960dp-qualified values dimen
resource (e.g., values with qualifier w960dp) that defines bottom_nav_inset as
0dp (or another rail-appropriate value) while keeping the existing default 80dp
in the base dimens.xml so large screens won't get unnecessary bottom padding.

---

Nitpick comments:
In `@app/src/main/res/drawable/ic_nav_settings.xml`:
- Around line 7-8: The vector drawable currently hardcodes
android:fillColor="#FFFFFFFF" which prevents theming; update the path element in
ic_nav_settings.xml to use a theme attribute instead (e.g. replace
android:fillColor="#FFFFFFFF" with android:fillColor="?attr/colorControlNormal"
or another appropriate theme attr like ?attr/colorOnSurface), or remove the
fillColor so the menu/icon tint can apply; ensure the path element with
android:pathData remains unchanged.

In `@app/src/main/res/layout/list_item_profile_picker.xml`:
- Around line 21-29: The TextView with id profile_picker_name can wrap long
names and change row height; update the element (profile_picker_name) to
constrain it to a single line by adding maxLines="1" and ellipsize="end" so
overflowing text is truncated with an ellipsis and picker rows remain a stable
height.

In `@app/src/main/res/layout/list_item_setting_section.xml`:
- Around line 4-12: The TextView in this layout duplicates header attributes
that are already captured by the shared style; replace the inline attributes by
applying style="@style/SettingsSectionHeader" on the TextView (remove duplicated
android:textAllCaps, android:textColor, android:textSize and any padding/text
attributes that the style covers) so the view uses the single source of truth
(SettingsSectionHeader) and avoid style drift; ensure any attributes not in the
style that are specific to this layout remain, and verify the TextView ID/text
content is unchanged.

In `@tool/src/main/java/io/netbird/client/tool/ProfileManagerWrapper.java`:
- Around line 56-64: The code in ProfileManagerWrapper is brittle because it
detects the first-launch empty profile by matching the error message text from
getActiveProfile; instead, detect the empty/missing state more robustly before
parsing (e.g., check the profile state file exists and its length/content is
empty) or catch a specific exception type if gomobile exposes one; update
getActiveProfile call-site to first inspect the profile state file (or wrap the
parsing call and inspect the underlying cause) and only treat the empty-file
case as a benign fallback (log via TAG) while letting other exceptions be logged
as errors.
🪄 Autofix (Beta)

Fix all unresolved CodeRabbit comments on this PR:

  • Push a commit to this branch (recommended)
  • Create a new PR with the fixes

ℹ️ Review info
⚙️ Run configuration

Configuration used: Organization UI

Review profile: CHILL

Plan: Pro

Run ID: 411e484a-8e49-4f4f-b6b3-3b442e3b8742

📥 Commits

Reviewing files that changed from the base of the PR and between 590b73d and a6614db.

📒 Files selected for processing (62)
  • app/src/main/java/io/netbird/client/MainActivity.java
  • app/src/main/java/io/netbird/client/ui/advanced/AdvancedFragment.java
  • app/src/main/java/io/netbird/client/ui/advanced/ThemePickerSheet.java
  • app/src/main/java/io/netbird/client/ui/home/BottomDialogFragment.java
  • app/src/main/java/io/netbird/client/ui/home/HomeFragment.java
  • app/src/main/java/io/netbird/client/ui/home/PagerAdapter.java
  • app/src/main/java/io/netbird/client/ui/home/ProfilePickerAdapter.java
  • app/src/main/java/io/netbird/client/ui/home/ProfilePickerSheet.java
  • app/src/main/java/io/netbird/client/ui/settings/SettingsFragment.java
  • app/src/main/res/drawable/ic_add.xml
  • app/src/main/res/drawable/ic_arrow_drop_down.xml
  • app/src/main/res/drawable/ic_check.xml
  • app/src/main/res/drawable/ic_chevron_right.xml
  • app/src/main/res/drawable/ic_menu_about.xml
  • app/src/main/res/drawable/ic_menu_advanced.xml
  • app/src/main/res/drawable/ic_menu_change_server.xml
  • app/src/main/res/drawable/ic_menu_docs.xml
  • app/src/main/res/drawable/ic_menu_profile.xml
  • app/src/main/res/drawable/ic_menu_troubleshoot.xml
  • app/src/main/res/drawable/ic_nav_home.xml
  • app/src/main/res/drawable/ic_nav_networks.xml
  • app/src/main/res/drawable/ic_nav_peers.xml
  • app/src/main/res/drawable/ic_nav_settings.xml
  • app/src/main/res/drawable/ic_open_in_new.xml
  • app/src/main/res/drawable/ic_profile_avatar.xml
  • app/src/main/res/drawable/profile_chip_bg.xml
  • app/src/main/res/drawable/settings_row_bg.xml
  • app/src/main/res/drawable/sheet_row_bg.xml
  • app/src/main/res/layout-w960dp/activity_main.xml
  • app/src/main/res/layout/activity_main.xml
  • app/src/main/res/layout/app_bar_main.xml
  • app/src/main/res/layout/content_main.xml
  • app/src/main/res/layout/fragment_about.xml
  • app/src/main/res/layout/fragment_advanced.xml
  • app/src/main/res/layout/fragment_bottom_dialog.xml
  • app/src/main/res/layout/fragment_home.xml
  • app/src/main/res/layout/fragment_networks.xml
  • app/src/main/res/layout/fragment_peers.xml
  • app/src/main/res/layout/fragment_profiles.xml
  • app/src/main/res/layout/fragment_server.xml
  • app/src/main/res/layout/fragment_settings.xml
  • app/src/main/res/layout/fragment_troubleshoot.xml
  • app/src/main/res/layout/list_item_profile.xml
  • app/src/main/res/layout/list_item_profile_picker.xml
  • app/src/main/res/layout/list_item_setting.xml
  • app/src/main/res/layout/list_item_setting_divider.xml
  • app/src/main/res/layout/list_item_setting_section.xml
  • app/src/main/res/layout/list_item_setting_toggle.xml
  • app/src/main/res/layout/nav_custom_bottom_item.xml
  • app/src/main/res/layout/nav_header_main.xml
  • app/src/main/res/layout/sheet_profile_picker.xml
  • app/src/main/res/layout/sheet_theme_picker.xml
  • app/src/main/res/menu/activity_main_drawer.xml
  • app/src/main/res/menu/bottom_nav.xml
  • app/src/main/res/navigation/mobile_navigation.xml
  • app/src/main/res/values-night/colors.xml
  • app/src/main/res/values-night/themes.xml
  • app/src/main/res/values/colors.xml
  • app/src/main/res/values/dimens.xml
  • app/src/main/res/values/strings.xml
  • app/src/main/res/values/themes.xml
  • tool/src/main/java/io/netbird/client/tool/ProfileManagerWrapper.java
💤 Files with no reviewable changes (8)
  • app/src/main/res/layout/nav_custom_bottom_item.xml
  • app/src/main/res/layout/fragment_bottom_dialog.xml
  • app/src/main/res/layout/nav_header_main.xml
  • app/src/main/java/io/netbird/client/ui/home/PagerAdapter.java
  • app/src/main/res/layout/app_bar_main.xml
  • app/src/main/res/menu/activity_main_drawer.xml
  • app/src/main/res/layout/content_main.xml
  • app/src/main/java/io/netbird/client/ui/home/BottomDialogFragment.java

Comment thread app/src/main/java/io/netbird/client/MainActivity.java
Comment thread app/src/main/java/io/netbird/client/ui/home/ProfilePickerSheet.java Outdated
Comment thread app/src/main/java/io/netbird/client/ui/settings/SettingsFragment.java Outdated
Comment thread app/src/main/res/drawable/ic_add.xml Outdated
Comment thread app/src/main/res/layout/activity_main.xml Outdated
Comment thread app/src/main/res/layout/sheet_theme_picker.xml
Comment thread app/src/main/res/values/dimens.xml Outdated
pappz added 9 commits May 10, 2026 16:27
The destination listener was hiding the bottom nav on the first-launch
fragment but still falling through to the top-level toolbar logic, so
the toolbar stayed visible. Treat firstInstallFragment as a full-screen
take-over and bypass the rest of the listener.
ProfilePickerSheet validated the input via sanitizeProfileName() but
still wrote the raw user string, so disallowed characters (anything
outside [A-Za-z0-9_-]) made it into the engine. Pass the sanitized
value to addProfile() and the success/duplicate toasts so what the
user sees matches what was stored.
On a TV or stripped-down build there may not be an Activity that
handles ACTION_VIEW for an https URL. Catch ActivityNotFoundException
and surface a toast instead of crashing the app.
Hardcoded #FFFFFFFF prevents these icons from adapting to theme
overlays (e.g. dark mode, focus inversions on TV). Switch to
?attr/colorControlNormal so each icon picks up the correct tint
from its host theme.

Affects ic_nav_home, ic_nav_peers, ic_nav_networks, ic_nav_settings,
ic_add, ic_check, ic_chevron_right, ic_arrow_drop_down,
ic_open_in_new — all custom icons introduced in this branch.
Theme picker rows now expose:
- contentDescription with the theme label
- stateDescription "Selected" on the active row (Android R+)
- isSelected=true on the active row for accessibility services
After picking, announce "<theme> theme selected" on the sheet root
so TalkBack confirms the change before the sheet dismisses.
On large screens the bottom navigation moves to a side rail
(layout-w960dp/activity_main.xml). The 80dp bottom padding fragments
add to clear the bottom bar is therefore wasted vertical space — set
the dimen to 0dp at the same qualifier so layouts pick the right
value automatically without per-layout overrides.
A long profile name was wrapping and pushing the picker row taller
than the others, breaking the row rhythm. maxLines=1 + ellipsize=end
truncates the overflow with "…" so picker rows stay uniform.
list_item_setting_section.xml duplicated the same padding / text size
/ caps attributes already defined in @style/SettingsSectionHeader,
which the section headers inside fragment_settings.xml etc. already
apply via the style. Replace the inline attributes with a single
style= reference so the section template is the same source of truth.
CodeRabbit suggested catching a typed exception or pre-checking the
state file length. The gomobile binding flattens the Go error chain
into go.Universe$proxyerror without surfacing the underlying type,
and the state-file path is a Go-side constant not exposed to Java,
so neither approach is available without a gomobile API change.
Expand the comment to make that trade-off explicit so the next
reader doesn't try the same refactor.
@pappz
pappz requested a review from lixmal May 10, 2026 18:33
pappz and others added 3 commits June 19, 2026 05:35
Squash-merge the four commits from PR #189 (profile-id branch) onto the
redesign branch, adapting the profile-id migration to the new bottom-nav
UI instead of the old drawer/NavigationView layout:

- getActiveProfile() now returns a Profile (with ID) instead of a String;
  update SettingsFragment and HomeFragment callers to use getName().
- Drop the PR's drawer-specific MainActivity changes (updateProfileMenuItem,
  drawer onKeyDown) — the redesign replaced the drawer with bottom nav.
- Graft the new disable-IPv6 switch listener into AdvancedFragment and add
  the IPv6 settings row to fragment_advanced.xml in the redesign row style.
- Bump netbird submodule to 62afff6 (adds Profile.ID to the gomobile binding).
Replace the Lottie connect button and background mask with a custom
pill-shaped SwitchMaterial toggle (white thumb, orange track when
connected), and restructure the home layout to match the iOS client:
centered profile chip, NetBird logo, status text, hostname with a
tappable IP/IPv6 detail section.

- Remove Lottie dependency, ButtonAnimation and unused JSON assets
- Add Inter and JetBrains Mono fonts; logo from the iOS client
- Hostname shown emphasized, IPv4 in the muted summary (with chevron)
- Info rows (IPv4 + IPv6) expand on tapping the summary; IPv6 row only
  shown when an IPv6 address is available; copy-to-clipboard buttons
- Append ellipsis to Connecting/Disconnecting status
- Make bottom nav unselected items white in night mode
- Drop the 'profile created'/'switched to profile' success toasts
* Add profile id migration

* Check if ID is set on Profile

* Bump netbird

* Update profile-id-name branch

* Fix active profile errors, bump netbird

* Bump netbird to v0.74.0

@coderabbitai coderabbitai Bot left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

Actionable comments posted: 2

Caution

Some comments are outside the diff and can’t be posted inline due to platform limitations.

⚠️ Outside diff range comments (2)
app/src/main/java/io/netbird/client/ui/profile/ProfilesFragment.java (1)

121-135: 🎯 Functional Correctness | 🟡 Minor | ⚡ Quick win

Reject names that sanitize to empty
ServiceManager.AddProfile() already strips invalid characters, so this flow still needs the post-sanitize empty-name check from ProfilePickerSheet (or the backend should enforce it). Inputs like !!! become "" and get written as .json, creating a blank profile entry.

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@app/src/main/java/io/netbird/client/ui/profile/ProfilesFragment.java` around
lines 121 - 135, The add-profile flow in showAddDialog currently only rejects
null or raw empty input, but names like !!! can sanitize to an empty string and
still be passed to addProfile, creating a blank .json entry. Update the
validation in the showAddDialog callback, and mirror the ProfilePickerSheet
post-sanitize check, so the sanitized profile name is verified to be non-empty
before calling addProfile (or enforce the same rule in
ServiceManager.AddProfile).
app/src/main/java/io/netbird/client/ui/home/HomeFragment.java (1)

73-88: 🎯 Functional Correctness | 🟡 Minor | ⚡ Quick win

Connect click path doesn't disable the toggle, unlike disconnect.

The disconnect branch disables buttonConnect immediately before calling switchConnection(false), but the connect branch does not disable it before switchConnection(true). A rapid double-tap on "connect" can trigger switchConnection(true) twice before onConnecting() arrives and disables the button.

🐛 Proposed fix
             } else {
                 // We're currently disconnected, so connect
+                buttonConnect.setEnabled(false);
                 setStatusText(R.string.main_status_connecting);
                 serviceAccessor.switchConnection(true);
             }
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@app/src/main/java/io/netbird/client/ui/home/HomeFragment.java` around lines
73 - 88, The connect click path in HomeFragment’s buttonConnect listener is
missing the immediate disable that the disconnect path already uses, which
allows repeated taps to call serviceAccessor.switchConnection(true) multiple
times before onConnecting() disables the control. Update the else branch in the
buttonConnect onClickListener to disable buttonConnect before setting the
connecting status and invoking switchConnection(true), matching the disconnect
flow and preventing double-triggered connection attempts.
🧹 Nitpick comments (1)
tool/src/main/java/io/netbird/client/tool/EngineRunner.java (1)

86-89: 📐 Maintainability & Code Quality | 🔵 Trivial | ⚡ Quick win

Confirm Profile overrides toString() for this log line.

activeProfile is now a Profile object (previously a String), and it's logged directly via "Initializing engine with profile: " + activeProfile. If Profile doesn't override toString(), this will log an unhelpful Profile@hashcode instead of the profile name/id, degrading this debug log's usefulness.

♻️ Proposed fix
-                Profile activeProfile = profileManager.getActiveProfile();
-                Log.d(LOGTAG, "Initializing engine with profile: " + activeProfile);
+                Profile activeProfile = profileManager.getActiveProfile();
+                Log.d(LOGTAG, "Initializing engine with profile: " + activeProfile.getName() + " (" + activeProfile.getId() + ")");
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@tool/src/main/java/io/netbird/client/tool/EngineRunner.java` around lines 86
- 89, The EngineRunner log line now concatenates the activeProfile Profile
object directly, so verify Profile overrides toString() to return a meaningful
name or id. If it does not, update Profile’s toString() or change the log in
EngineRunner to explicitly log the desired profile field via getActiveProfile()
so the "Initializing engine with profile" message stays readable and useful.
🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

Inline comments:
In `@app/src/main/java/io/netbird/client/ui/home/HomeFragment.java`:
- Around line 159-176: The status/button update paths in HomeFragment are
re-reading fragment view fields across the post() thread hop, which can become
null after the initial check. In setStatusText(), setToggle(), and the
onAddressChanged handling, capture textConnStatus, buttonConnect, and binding
into local variables first, null-check those locals, and use only the locals
inside the posted Runnable so onDestroyView() cannot null them between checks
and use.

In `@tool/src/main/java/io/netbird/client/tool/ProfileManagerWrapper.java`:
- Around line 77-86: Profile switching is still using the profile name in the
caller while ProfileManagerWrapper.switchProfile now expects an ID. Update the
Home profile chip flow that invokes switchProfile so it passes profile.getID()
instead of the display name, matching the wrapper’s ID-based routing and keeping
the behavior consistent with ProfilePickerSheet.

---

Outside diff comments:
In `@app/src/main/java/io/netbird/client/ui/home/HomeFragment.java`:
- Around line 73-88: The connect click path in HomeFragment’s buttonConnect
listener is missing the immediate disable that the disconnect path already uses,
which allows repeated taps to call serviceAccessor.switchConnection(true)
multiple times before onConnecting() disables the control. Update the else
branch in the buttonConnect onClickListener to disable buttonConnect before
setting the connecting status and invoking switchConnection(true), matching the
disconnect flow and preventing double-triggered connection attempts.

In `@app/src/main/java/io/netbird/client/ui/profile/ProfilesFragment.java`:
- Around line 121-135: The add-profile flow in showAddDialog currently only
rejects null or raw empty input, but names like !!! can sanitize to an empty
string and still be passed to addProfile, creating a blank .json entry. Update
the validation in the showAddDialog callback, and mirror the ProfilePickerSheet
post-sanitize check, so the sanitized profile name is verified to be non-empty
before calling addProfile (or enforce the same rule in
ServiceManager.AddProfile).

---

Nitpick comments:
In `@tool/src/main/java/io/netbird/client/tool/EngineRunner.java`:
- Around line 86-89: The EngineRunner log line now concatenates the
activeProfile Profile object directly, so verify Profile overrides toString() to
return a meaningful name or id. If it does not, update Profile’s toString() or
change the log in EngineRunner to explicitly log the desired profile field via
getActiveProfile() so the "Initializing engine with profile" message stays
readable and useful.
🪄 Autofix (Beta)

Fix all unresolved CodeRabbit comments on this PR:

  • Push a commit to this branch (recommended)
  • Create a new PR with the fixes

ℹ️ Review info
⚙️ Run configuration

Configuration used: Organization UI

Review profile: CHILL

Plan: Pro

Run ID: 3acf034a-011b-4f62-a07e-db1353f256a2

📥 Commits

Reviewing files that changed from the base of the PR and between a6614db and a34b4a8.

⛔ Files ignored due to path filters (6)
  • app/src/main/res/drawable-night-xxhdpi/bg_bottom.png is excluded by !**/*.png
  • app/src/main/res/drawable-night/nb_logo_full.png is excluded by !**/*.png
  • app/src/main/res/drawable-xxhdpi/bg_bottom.png is excluded by !**/*.png
  • app/src/main/res/drawable/nb_logo_full.png is excluded by !**/*.png
  • app/src/main/res/font/inter_variable.ttf is excluded by !**/*.ttf
  • app/src/main/res/font/jetbrains_mono_variable.ttf is excluded by !**/*.ttf
📒 Files selected for processing (52)
  • Readme.md
  • app/build.gradle.kts
  • app/src/main/assets/button_full.json
  • app/src/main/assets/button_full_dark.json
  • app/src/main/assets/loading.json
  • app/src/main/java/io/netbird/client/MainActivity.java
  • app/src/main/java/io/netbird/client/ui/advanced/AdvancedFragment.java
  • app/src/main/java/io/netbird/client/ui/advanced/ThemePickerSheet.java
  • app/src/main/java/io/netbird/client/ui/home/ButtonAnimation.java
  • app/src/main/java/io/netbird/client/ui/home/HomeFragment.java
  • app/src/main/java/io/netbird/client/ui/home/Peer.java
  • app/src/main/java/io/netbird/client/ui/home/PeersAdapter.java
  • app/src/main/java/io/netbird/client/ui/home/PeersFragmentViewModel.java
  • app/src/main/java/io/netbird/client/ui/home/ProfilePickerSheet.java
  • app/src/main/java/io/netbird/client/ui/home/Resource.java
  • app/src/main/java/io/netbird/client/ui/profile/ProfilesFragment.java
  • app/src/main/java/io/netbird/client/ui/settings/SettingsFragment.java
  • app/src/main/res/color/connect_thumb_tint.xml
  • app/src/main/res/color/connect_track_tint.xml
  • app/src/main/res/color/tab_icon_color.xml
  • app/src/main/res/color/tab_text_color.xml
  • app/src/main/res/drawable/connect_switch_thumb.xml
  • app/src/main/res/drawable/connect_switch_track.xml
  • app/src/main/res/drawable/ic_add.xml
  • app/src/main/res/drawable/ic_arrow_drop_down.xml
  • app/src/main/res/drawable/ic_check.xml
  • app/src/main/res/drawable/ic_chevron_right.xml
  • app/src/main/res/drawable/ic_content_copy.xml
  • app/src/main/res/drawable/ic_nav_home.xml
  • app/src/main/res/drawable/ic_nav_networks.xml
  • app/src/main/res/drawable/ic_nav_peers.xml
  • app/src/main/res/drawable/ic_nav_settings.xml
  • app/src/main/res/drawable/ic_open_in_new.xml
  • app/src/main/res/drawable/info_row_bg.xml
  • app/src/main/res/layout/fragment_advanced.xml
  • app/src/main/res/layout/fragment_home.xml
  • app/src/main/res/layout/list_item_profile_picker.xml
  • app/src/main/res/layout/list_item_setting_section.xml
  • app/src/main/res/menu/peer_clipboard_menu.xml
  • app/src/main/res/values-night/colors.xml
  • app/src/main/res/values-w960dp/dimens.xml
  • app/src/main/res/values/colors.xml
  • app/src/main/res/values/strings.xml
  • gradle/libs.versions.toml
  • netbird
  • tool/src/main/java/io/netbird/client/tool/EngineRunner.java
  • tool/src/main/java/io/netbird/client/tool/IFace.java
  • tool/src/main/java/io/netbird/client/tool/NetworkChangeNotifier.java
  • 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/TUNParameters.java
  • tool/src/main/java/io/netbird/client/tool/VPNService.java
💤 Files with no reviewable changes (6)
  • app/src/main/assets/loading.json
  • app/src/main/java/io/netbird/client/ui/home/ButtonAnimation.java
  • app/src/main/assets/button_full.json
  • app/src/main/assets/button_full_dark.json
  • app/build.gradle.kts
  • gradle/libs.versions.toml
✅ Files skipped from review due to trivial changes (14)
  • app/src/main/res/color/connect_thumb_tint.xml
  • app/src/main/res/drawable/ic_chevron_right.xml
  • app/src/main/res/values-w960dp/dimens.xml
  • app/src/main/res/drawable/ic_check.xml
  • app/src/main/res/drawable/ic_nav_settings.xml
  • app/src/main/res/color/connect_track_tint.xml
  • app/src/main/res/drawable/ic_nav_home.xml
  • app/src/main/res/layout/list_item_setting_section.xml
  • app/src/main/res/color/tab_text_color.xml
  • app/src/main/res/drawable/ic_add.xml
  • app/src/main/res/layout/list_item_profile_picker.xml
  • Readme.md
  • app/src/main/res/drawable/ic_nav_peers.xml
  • app/src/main/res/values-night/colors.xml
🚧 Files skipped from review as they are similar to previous changes (9)
  • app/src/main/res/drawable/ic_nav_networks.xml
  • app/src/main/res/drawable/ic_arrow_drop_down.xml
  • app/src/main/java/io/netbird/client/ui/home/ProfilePickerSheet.java
  • app/src/main/java/io/netbird/client/ui/settings/SettingsFragment.java
  • app/src/main/res/layout/fragment_advanced.xml
  • app/src/main/java/io/netbird/client/ui/advanced/ThemePickerSheet.java
  • app/src/main/java/io/netbird/client/ui/advanced/AdvancedFragment.java
  • app/src/main/res/values/strings.xml
  • app/src/main/java/io/netbird/client/MainActivity.java

Comment thread app/src/main/java/io/netbird/client/ui/home/HomeFragment.java Outdated
pappz added 6 commits July 3, 2026 19:26
Resolve conflicts in favor of the iOS-style bottom-nav redesign:
- MainActivity: keep bottom-nav AppBarConfiguration; drop drawer setup,
  updateProfileMenuItem and drawer-based onKeyDown (drawer no longer exists)
- AdvancedFragment: keep compact row-click listeners; drop redundant main
  block and a duplicate layoutDisableIpv6 listener
- fragment_advanced.xml: keep settings_row_bg IPv6/firewall rows; drop main's
  ConstraintLayout variant that duplicated @+id/layout_force_relay_connection
- ProfilesFragment: keep no success toast on profile switch
- ProfileManagerWrapper: take main's getActiveProfile (getIsActive + throw);
  fresh-install empty state is now handled inside the engine
- netbird submodule: advance to current main pointer (3aa6c02)

Note: gomobile netbird.aar must be regenerated from the new submodule so the
Profile.getIsActive() binding is available.
- Rework the connect toggle thumb as a centred layer so the white circle
  size is independent of the track height
- Give the disconnected track a light grey fill with a thin darker border
  and disable Material auto-tint so the custom per-state colours render
- Make the bottom nav background white and drive item icon/text colour off
  state_checked; use opaque icon fills so itemIconTint applies at full strength
- Lighten the home background
- Give the connect toggle thumb a soft drop shadow (baked into the drawable;
  drop thumbTint so the shadow is not tinted white)
- Merge the two info rows (IP / secondary value) into a single bordered box
  with a middle divider; showDividers hides the separator when the secondary
  row is gone
Revert to a plain white circle for the connect toggle thumb.
The recent white-surface tweaks hardcoded @color/white and #4A4A4A, which
have no night variant, breaking dark mode (white bottom nav, white profile
chip, invisible unselected icons).

Introduce theme-aware semantic colours instead:
- nb_bottom_nav_bg / nb_chip_bg: white in light, dark surface in night
- tab_icon_unselected: #4A4A4A in light, white in night

@coderabbitai coderabbitai Bot left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

🧹 Nitpick comments (1)
app/src/main/res/drawable/connect_switch_track.xml (1)

10-11: 📐 Maintainability & Code Quality | 🔵 Trivial | 💤 Low value

Consider extracting repeated dimensions.

88dp/44dp/100dp are duplicated across both selector items. Minor, purely stylistic.

Also applies to: 19-20

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@app/src/main/res/drawable/connect_switch_track.xml` around lines 10 - 11, The
switch track drawable repeats the same size and corner radius values in both
selector items; extract the shared 88dp, 44dp, and 100dp measurements into
reusable dimension resources and reference them from the shape definitions in
the connect_switch_track drawable so both items stay consistent and easier to
maintain.
🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

Nitpick comments:
In `@app/src/main/res/drawable/connect_switch_track.xml`:
- Around line 10-11: The switch track drawable repeats the same size and corner
radius values in both selector items; extract the shared 88dp, 44dp, and 100dp
measurements into reusable dimension resources and reference them from the shape
definitions in the connect_switch_track drawable so both items stay consistent
and easier to maintain.

ℹ️ Review info
⚙️ Run configuration

Configuration used: Organization UI

Review profile: CHILL

Plan: Pro

Run ID: 09e5f192-5e9b-4894-aa5d-ddc9fa50b5ac

📥 Commits

Reviewing files that changed from the base of the PR and between 5c7e33c and 35f5a77.

📒 Files selected for processing (15)
  • app/src/main/res/color/connect_track_tint.xml
  • app/src/main/res/color/tab_icon_color.xml
  • app/src/main/res/color/tab_text_color.xml
  • app/src/main/res/drawable/connect_switch_thumb.xml
  • app/src/main/res/drawable/connect_switch_track.xml
  • app/src/main/res/drawable/ic_nav_home.xml
  • app/src/main/res/drawable/ic_nav_networks.xml
  • app/src/main/res/drawable/ic_nav_peers.xml
  • app/src/main/res/drawable/ic_nav_settings.xml
  • app/src/main/res/drawable/info_row_divider.xml
  • app/src/main/res/drawable/profile_chip_bg.xml
  • app/src/main/res/layout/activity_main.xml
  • app/src/main/res/layout/fragment_home.xml
  • app/src/main/res/values-night/colors.xml
  • app/src/main/res/values/colors.xml
✅ Files skipped from review due to trivial changes (7)
  • app/src/main/res/drawable/connect_switch_thumb.xml
  • app/src/main/res/color/tab_text_color.xml
  • app/src/main/res/drawable/ic_nav_networks.xml
  • app/src/main/res/drawable/profile_chip_bg.xml
  • app/src/main/res/drawable/info_row_divider.xml
  • app/src/main/res/color/connect_track_tint.xml
  • app/src/main/res/values-night/colors.xml
🚧 Files skipped from review as they are similar to previous changes (5)
  • app/src/main/res/color/tab_icon_color.xml
  • app/src/main/res/drawable/ic_nav_settings.xml
  • app/src/main/res/layout/activity_main.xml
  • app/src/main/res/layout/fragment_home.xml
  • app/src/main/res/values/colors.xml

pappz added 6 commits July 3, 2026 22:53
- Reduce row height (52->40dp), text (15->12sp) and copy buttons (40->34dp)
- Middle-ellipsize the IPv6 row so a long address is truncated in the centre
  while the copy button still copies the full value
Use opaque fillColor so app:tint renders the profile picker + and
check icons at full nb_orange intensity, matching the manage icon.
Replace the default circular ripple mask on bottom nav / navigation rail
items with a rounded-rect (8dp) mask.
Navigating back to home re-inflates the fragment, so the hardcoded
android:text default painted "Disconnected" before the real state
arrived — and the state update was deferred by two nested post() calls
even though registration replays it on the main thread.

Move the layout default to tools:text, and apply view updates inline
when already on the main thread (engine callbacks, which arrive off the
main thread, still post). Snap the toggle thumb after setChecked so it
doesn't animate into place on a fresh view.
The sheet listed every profile in a wrap_content RecyclerView, so past a
handful of profiles the list grew past the screen and pushed the "Add
profile" and "Manage profiles" rows out of reach.

Cap the list height, show only the five most recently used profiles, and
surface a "Show all profiles (N)" row into the manage screen when there
are more. Expand the sheet on open so a long list no longer parks at peek
height.

Recency is tracked in SharedPreferences since the gomobile Profile has no
last-used field. The store prunes entries for profiles that no longer
exist on every read, so deleting a profile needs no bookkeeping from the
caller and the store cannot outgrow the profile count.

Also fix the sheet passing a profile name where switchProfile expects an
ID, which made switching from the sheet fail whenever the two differed.
pappz added 17 commits July 27, 2026 19:52
Routes come out of a Go map, so their order changed between fetches --
visible churn, and it defeated Peer's value equality, forcing a rebuild
of the detail rows on every poll tick. Sort them once in the view model.

The Resources section header now carries the layers icon and uses the
same size, colour and capitalisation as the other row labels.
Add full translations (de, hu, ru, es, fr, it, pt, ja, zh-CN) matching
the desktop client's locale list, reusing its wording from
client/ui/i18n/locales where the same strings exist.

Add a Display Language row to Settings that opens a bottom sheet
listing languages by their native names, mirroring the desktop's
_index.json. Selection is applied via AppCompat per-app locales
(autoStoreLocales for API < 33, localeConfig for 13+) and defaults
to the OS language until the user picks one.

Extract the remaining hardcoded user-facing messages (VPN permission,
debug bundle toasts, generic errors, QR dialog Close) into string
resources so they translate too; English output is unchanged.
The Peers and Networks ViewModels captured the MainActivity (as
ServiceAccessor) at construction. ViewModels survive configuration
changes such as the in-app language switch, so after a recreate they
kept querying the destroyed activity, whose binder is null, and posted
empty lists forever - the zero-peers page stuck even while connected.

Follow the HomeFragment pattern instead: the fragment hands the current
accessor to the ViewModel on every view creation and detaches it on
view destruction, so refreshes always go through the live activity and
retained ViewModels cannot leak a dead one.

Also stop disguising "service not bound yet" as an empty peer/network
list: the accessor now returns null while unbound and the ViewModels
keep their last snapshot instead of flashing the zero view. Route
change listeners registered before the binding completes are queued in
the activity and attached in onServiceConnected, so the Networks screen
no longer silently loses its route subscription in the bind race.
The nav host used to fill the whole screen with the bottom navigation
drawn on top, so every fragment had to reserve an 80dp bottom inset or
its content ended up hidden behind the bar. Stack the content area and
the nav bar vertically instead — matching the w960dp navigation-rail
variant — and drop the per-fragment insets; only the FAB clearance on
the profiles list remains.
Exit nodes (0.0.0.0/0 or ::/0 routes) no longer show up as toggleable
rows in the Resources list; they get a dedicated card pinned to the
bottom of the Home screen instead, mirroring the desktop main view. The
card names the active exit node (or "None selected") and opens a
single-choice bottom sheet — a "None" row plus the available exit
nodes, check on the active one. Selecting a node routes all traffic
through it; the engine deselects the previous one, so at most one is
active.

The card refreshes on route changes and on peer-list changes: the
latter is what makes it appear on a fresh connect, since onConnected
fires before the network map lands and the route notifier is silent
for its initial baseline set.
Engine teardown emits a transient Connecting before its own
Disconnecting latch is set, which flipped the toggle back on for a
moment while disconnecting (and similar noise showed up while
connecting). Mirror the desktop MainConnectionStatusSwitch: while a tap
is in flight, engine reports that contradict its target don't repaint
the toggle. The latch releases when the target state arrives, when a
connect attempt demonstrably fails, or after a 7s timeout that falls
back to the last state the engine actually reported.
The logo/switch/status/address block hung from the top of the screen.
Chain it (packed) between the profile chip and the exit node row so it
sits vertically centered in the available space and structurally can't
overlap the exit node card.
Landscape has no layout of its own and the portrait screens fall apart
when rotated. Lock the activity to portrait like the iOS app does, but
only off TV: Android TV is landscape by nature and uses the w960dp
navigation-rail layout.
…pick

Instead of hiding the card while disconnected or without shared exit
nodes, keep it in place dimmed and non-clickable with a "No exit nodes
available" subtitle — matching the desktop main view, and keeping the
Home layout stable across states.
Port the desktop app's profile dialog to Android and make it the single
place where a profile's management server is chosen. The desktop offers
one Cloud/Self-hosted control in its profile modal, settings page and
welcome window; Android had three unrelated surfaces instead.

Add a profile editor dialog that names the profile and picks the server
in one step, reused for editing so the pencil on a profile card opens
the same window seeded with the profile's current values. Rework the
first-run screen along the same lines, replacing a sentence whose
clickable word was the literal "change_server" resource id.

Delete the Change Server screen: with onboarding, the profile cards and
the settings row all routing to the editor, it had no remaining entry
point. Its setup-key field lives on in both surfaces, since mobile
devices are commonly provisioned that way even though the desktop UI
has no equivalent.

The segmented control, setup-key section and URL handling become shared
classes rather than being copied between the dialog and onboarding,
mirroring how the desktop shares ManagementServerSwitch and
useManagementUrl.

Also:
- Give dialogs a MaterialComponents parent theme so Material widgets
  resolve the NetBird palette instead of the framework's purple accent.
- Convert the wordmark to a vector drawable generated from the
  desktop's netbird-full.svg. The old PNG sat the bird 7px above the
  lettering's centre and padded the canvas unevenly.
- Reuse the desktop's existing translations for every new string.
The earlier fix for this matched peer detail by id, because that was the
only destination outside the tab set at the time. Settings has since
gained four sub-screens — Advanced, Troubleshoot, About and Profiles —
which are siblings of the tabs rather than children, so the check missed
them: tapping another tab left one on the back stack, stranding the bar
with nothing highlighted and re-showing the sub-screen instead of the
Settings root when that tab was tapped again.

Key the check on "current destination is not a tab" rather than a list
of sub-screens, reusing the top-level set already built for the app bar,
so destinations added later need no change here.
The peer FQDN on the Peers screen and the resource name on the Resources
screen used nb_txt_light, the secondary text token, which left them the
same weight as the IP and peer subtitles beneath them. Switch both to
nb_txt so the name reads as the primary line of each row: white on the
dark card in night mode, dark grey in light mode.
Every switch outside the home screen used stock Material drawables, so the
Resources rows, Advanced settings and the profile dialog all looked unrelated
to the connect toggle the redesign introduced.

Add Widget.NetBird.Switch, backed by nb_switch_track/nb_switch_thumb: the same
orange-when-on, bordered-grey-when-off track and floating white thumb as
connect_switch_*, sized for a list row (34x20dp track, 16dp thumb) rather than
the 88x44dp hero control. useMaterialThemeColors stays false so the theme tint
does not paint over the colours baked into the shapes.

Apply it to all 17 switches and drop Widget.MyApp.SwitchMaterial.Custom, which
only tinted the stock drawables and is now unused. The home toggle keeps its
own larger drawables.
@pappz
pappz force-pushed the ux/ios-style-redesign branch from 315174f to f61a6dd Compare July 27, 2026 23:01
pappz added 7 commits July 28, 2026 01:10
A tap has already flipped the switch by the time the click listener runs:
SwitchMaterial is a CompoundButton, and performClick() toggles isChecked
before dispatching the click. The handler only wrote the status label, so
the switch kept whatever position the tap gave it. Combined with the paint
latch ignoring a DISCONNECTED report unless a CONNECTING was seen first,
a connect that failed up front (e.g. VPN permission declined, which reports
engine-stopped without ever connecting) stranded the toggle showing
connected under a Disconnected label.

Paint the toggle and the label together through setToggle() in every tap
branch, and release the latch on any DISCONNECTED while a connect is
pending, whether or not a CONNECTING preceded it. The teardown-transient
suppression the latch was built for (Connecting reported mid-disconnect)
is unchanged.

Also jump the toggle drawables only when the checked state actually
changes: SwitchCompat's jump ends the in-flight position animator, and the
tap handler now repaints while the tap's own thumb slide is still playing.
Drop setStatusText() and sawConnectingSinceTap, which this leaves unused.
# Conflicts:
#	app/src/main/java/io/netbird/client/MainActivity.java
#	app/src/main/java/io/netbird/client/ui/profile/ProfilesFragment.java
#	netbird
#	tool/src/main/java/io/netbird/client/tool/ProfileManagerWrapper.java
The reachability probe on the profile editor and first-run screen is a
soft warning: a second press of the submit button skips the probe and
saves anyway. Nothing indicated that, so the accepted-on-second-try flow
read as flaky. Switch the button to "Add/Save/Continue anyway" while
the warning is showing, and reset it when the URL is edited or the
server mode changes.
An expired session used to show up as a raw Go error string in a toast,
with the home screen still reading "Disconnected" — nothing said that
reconnecting needs an interactive login, and nothing warned before the
session ran out.

Consume the binding's new session API from the service, which is where
the desktop keeps this logic too, so it also works with no UI bound
(always-on VPN, boot start):

- SessionMonitor forwards the engine's expiry warnings and edge-detects
  the NeedsLogin status label, replaying both to late-binding listeners.
- The persistent notification gains a live countdown to the deadline and
  an "Extend session" action; a high-importance channel carries the
  expiring and expired warnings.
- The home screen shows a session row below the exit node row, and the
  connect status reads "Login required" instead of "Disconnected"
  once the server has rejected the peer. Tapping either the row or the
  toggle runs the flow that fixes it.

Bumps the netbird submodule for the binding.
The warning already reaches the user twice: a heads-up notification from
the service, which is the only channel that works with no UI bound, and
the home screen's session row, which states the deadline continuously
with the same extend action. A modal was a third copy of that, and an
interruption an event known ten minutes ahead does not warrant.

Removes the dialog with the now-unused dismiss plumbing and strings.

Bumps the netbird submodule for the binding's concurrency fixes.
The row truncated instead of rounding up, so 119 seconds read as "in 1
minutes" — less time than the user actually had, which is the wrong
direction for a decision about extending. It also called a live session
"expired" during its final minute, stayed in minutes up to 119 of them,
and always used the plural.

Follow the tray's formatSessionRemaining: round every unit up, give the
sub-minute tail its own wording, switch units at 59 minutes and 23 hours,
and split singular from plural.
@pappz pappz changed the title Align Android UI with iOS client patterns Align the Android client with the iOS and desktop clients Jul 28, 2026
pappz added 4 commits July 28, 2026 18:03
Eleven strings and the three session-duration plurals were only in
English, so every localized build fell back for the exit node selector,
the session row and the login-required status.

Russian gets the one/few/many forms its plurals need; Hungarian,
Japanese and Chinese take a single form.
The service module had no localized resources, so the foreground and
session notifications stayed English in every locale — including the
expiry warnings, which are the only thing the user sees when no UI is
open.

Covers the nine desktop languages. service_name is left alone as a brand
name.
The "Set up NetBird" heading sat directly under the full NetBird logo
and only repeated it, so remove the TextView and its now-unused string.
The description takes over the heading's 20dp top margin to keep the
spacing below the logo unchanged.

Reword the sub-text to name the outcome of each choice rather than just
"get started", and translate it in every locale using that locale's own
segmented-control labels.
An IPv6 address was appended to the IPv4 one as a second line of the
same TextView, so a full-length address ran past the row and collided
with the status label. ellipsize only clips single-line text, so give
IPv6 its own view and hide it when the peer has no IPv6 address.

Clip it in the middle, matching how addresses are already truncated in
PeerDetailFragment.addRow() and the home screen's secondary value, so
both the prefix and the host part stay readable. The long-press menu
still copies the address in full.

Constrain the IPv4 view's end to the status label too: as wrap_content
it had no right-hand bound, so a long address could run under the
label. Widen the gap before the status label from 8dp to 16dp on all
three text rows, and drop the status line's end constraint on the IPv4
view, which would now be a circular width dependency.
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.

2 participants