Skip to content
Open
Show file tree
Hide file tree
Changes from 1 commit
Commits
Show all changes
121 commits
Select commit Hold shift + click to select a range
a6614db
ux: align Android UI with iOS client patterns
pappz May 10, 2026
5e7198c
Hide toolbar on first-install onboarding screen
pappz May 10, 2026
4504927
Persist sanitized profile name when adding from picker
pappz May 10, 2026
3031950
Guard Documentation row against missing browser
pappz May 10, 2026
628b8ae
Use theme attr for fill color on app-local vector icons
pappz May 10, 2026
cca9cd0
Announce theme picker selection to screen readers
pappz May 10, 2026
9055b6f
Override bottom_nav_inset to 0dp on w960dp+
pappz May 10, 2026
e07f430
Constrain profile picker name to a single line
pappz May 10, 2026
b28e21e
Reuse SettingsSectionHeader style in section template layout
pappz May 10, 2026
ab56499
Document why getActiveProfile uses string-match for empty-state
pappz May 10, 2026
268d7dd
Merge profile-id migration (#189) into iOS-style redesign
pappz Jun 19, 2026
1f48811
Redesign home screen in iOS style with toggle connect button
pappz Jun 19, 2026
4134a18
Migrate to profile ids (#189)
theodorsm Jul 2, 2026
eaa84e5
Merge branch 'main' into ux/ios-style-redesign
pappz Jul 3, 2026
5f57866
Polish home connect toggle and bottom nav styling
pappz Jul 3, 2026
70ad497
Make profile chip background white
pappz Jul 3, 2026
f62c109
Add connect thumb shadow and merge IP info rows into one box
pappz Jul 3, 2026
ded33c3
Remove connect thumb drop shadow
pappz Jul 3, 2026
9cbe806
Fix dark theme for bottom nav and profile chip
pappz Jul 3, 2026
03c6cfa
Shrink IP info rows and middle-ellipsize IPv6
pappz Jul 3, 2026
7c2d881
Style bottom sheets with 1B1B1B dark background and B3B3B3 label
pappz Jul 3, 2026
20b3cc3
Make add and check icons full-opacity orange
pappz Jul 3, 2026
5f7a4f8
Square off bottom nav item touch feedback
pappz Jul 27, 2026
ddcd1f7
Stop the connection status flashing Disconnected on re-entry
pappz Jul 27, 2026
59bc235
Scale the profile picker sheet to many profiles
pappz Jul 27, 2026
cc7246a
Bump the about-screen copyright year to 2026
pappz Jul 27, 2026
b8ee6fe
Advance the netbird submodule to 3358138cc
pappz Jul 27, 2026
7f4fa25
Let profiles be renamed from the manage screen
pappz Jul 27, 2026
26e7231
Keep the last profile card clear of the FAB
pappz Jul 27, 2026
75e3c1f
Add a peer detail screen
pappz Jul 27, 2026
9a49fd4
Fix peer detail instantly closing and stale tab state
pappz Jul 27, 2026
16db8b9
Drop the rounded corners from bottom nav touch feedback
pappz Jul 27, 2026
04c710e
Drop the connection indicator bar from resource rows
pappz Jul 27, 2026
d157a6f
List peer routes as rows instead of a copyable count
pappz Jul 27, 2026
ff93621
Sort peer routes and give the Resources header a row-style icon
pappz Jul 27, 2026
6a1b16c
Use the layers icon for the Resources tab
pappz Jul 27, 2026
9bfe7e0
Localize the app into the 9 desktop languages with an in-app picker
pappz Jul 27, 2026
c5b55aa
Fix stale peer/resource lists after activity recreation
pappz Jul 27, 2026
dde220a
Lay out the bottom nav below the content instead of overlaying it
pappz Jul 27, 2026
8a262ef
Add an exit node selector to the Home screen
pappz Jul 27, 2026
6901184
Latch the connect toggle on user actions to stop state flicker
pappz Jul 27, 2026
719046d
Center the Home connection group between the chip and the exit node row
pappz Jul 27, 2026
4c45fab
Lock phones to portrait
pappz Jul 27, 2026
ee220b5
Keep the exit node card on screen, disabled when there is nothing to …
pappz Jul 27, 2026
6f2acb1
[client] Unify management-server selection across the Android UI
pappz Jul 27, 2026
8fd1eb4
[client] Unwind any sub-screen when a bottom-nav tab is tapped
pappz Jul 27, 2026
123a445
[client] Use primary text colour for peer and resource names
pappz Jul 27, 2026
f61a6dd
[client] Share the home connect toggle's switch styling across the app
pappz Jul 27, 2026
3918757
[client] Keep the connect toggle in sync with the engine state
pappz Jul 27, 2026
23dc007
Use integration branch for submodule
pappz Jul 28, 2026
f95f43c
Merge branch 'main' into ux/ios-style-redesign
pappz Jul 28, 2026
81c0262
[client] Label the unreachable-server confirm step explicitly
pappz Jul 28, 2026
3b66975
[client] Surface the SSO session expiry in the Android UI
pappz Jul 28, 2026
2a95aa9
[client] Drop the session-expiring dialog
pappz Jul 28, 2026
4c5431c
[client] Match the desktop's rounding in the session expiry row
pappz Jul 28, 2026
79f17ce
[client] Translate the exit node and session strings
pappz Jul 28, 2026
670ec9e
[client] Localize the tool module's notification strings
pappz Jul 28, 2026
c2e0ff4
[client] Drop the onboarding title and sharpen its sub-text
pappz Jul 29, 2026
260dd1a
[client] Clip long IPv6 addresses in the peers list
pappz Jul 29, 2026
aa0a82a
Pull fresh TUN settings on rebuild instead of consuming the payload
pappz Jul 30, 2026
942f909
Bump netbird submodule: drop the initial GetNetworkMap fetch
pappz Jul 30, 2026
881e5d0
[client] Fade the debug bundle button's fill when it is disabled
pappz Jul 30, 2026
8d13450
[client] Stop the connect toggle restoring a stale checked state
pappz Jul 30, 2026
e7ad39e
[client] Show which account each profile is signed in with
pappz Jul 30, 2026
a31f6b1
Bump netbird submodule: login-hint review fixes
pappz Jul 30, 2026
d44c9c2
[client] Use the shared StateListenerAdapter in the stress test
pappz Aug 3, 2026
bf67e0c
Drop the w960dp dimen overrides left dead by the nav-below-content ch…
pappz Aug 3, 2026
2105a9e
Bump netbird submodule to main
pappz Aug 5, 2026
16abedd
ci: install the JDK from temurin instead of adopt
pappz Aug 5, 2026
35cbf37
Merge origin/main into ux/ios-style-redesign
pappz Aug 6, 2026
3b8f205
Match exit-node routes exactly instead of by substring
pappz Aug 6, 2026
5f0690f
Localize peer status labels and relative timestamps
pappz Aug 6, 2026
7e72975
Merge remote-tracking branch 'origin/main' into ux/ios-style-redesign
pappz Aug 7, 2026
c8102d8
Allow force-cancelling a stuck connect from the main toggle
pappz Aug 7, 2026
5c89309
Target Android 16 (API 36)
pappz Aug 7, 2026
6870f56
Keep showing onboarding until a server is chosen
pappz Aug 7, 2026
606fca3
Recognize comma-joined dual-stack default routes as exit nodes
pappz Aug 10, 2026
18a1b6e
E2e tests (#229)
pappz Aug 10, 2026
348e614
Restore the status bar background so its icons stay visible
pappz Aug 10, 2026
67f639f
Paint the navigation bar strip to match the bottom tabs
pappz Aug 10, 2026
abea4ef
Soften the focus highlight on dialog text fields
pappz Aug 11, 2026
d44e6fd
Expand the address rows without re-centering the home screen
pappz Aug 11, 2026
98f4a34
Cap the address rows against the exit node row
pappz Aug 11, 2026
77f4cc5
Show when the connect toggle cannot be tapped
pappz Aug 11, 2026
a924e29
Merge remote-tracking branch 'origin/main' into ux/ios-style-redesign
pappz Aug 12, 2026
93d0497
Update netbird submodule to main HEAD
pappz Aug 12, 2026
15e4609
Keep the home screen content from shifting on connect
pappz Aug 15, 2026
382da9e
Replace README screenshots with the redesigned UI
pappz Aug 15, 2026
cf4d28d
Add manual workflow to build a version-code-consistent APK for ad-hoc…
pappz Aug 16, 2026
4d10386
Pin gobind alongside gomobile instead of running gomobile init (#240)
pappz Aug 17, 2026
5498ddb
Anchor the snapshot version name to the nearest release tag
pappz Aug 17, 2026
5388621
Bump netbird submodule to main branch
pappz Aug 17, 2026
fa99c6c
Document the tag-anchored snapshot version name
pappz Aug 17, 2026
474a4fc
Keep a long hostname on one line on the home screen
pappz Aug 18, 2026
b28d024
Float the addresses when they have nowhere to open
pappz Aug 18, 2026
6cc1bee
Show remaining time in the session expiry notification
pappz Aug 18, 2026
3bfff5b
Bump submodule
pappz Aug 18, 2026
6db6293
Android ssh client (#235)
pappz Aug 18, 2026
ec17323
Merge remote-tracking branch 'origin/main' into ux/ios-style-redesign
pappz Aug 18, 2026
1b7f067
Bump netbird version
pappz Aug 19, 2026
16d6d1e
Bump the netbird submodule to the gobind toolchain pin
pappz Aug 22, 2026
aefe9af
Implement onStateChanged on the connection listeners
pappz Aug 22, 2026
212bca3
Fix stale route state and listener races around the resources UI
pappz Aug 22, 2026
d63422f
Refresh the exit node row on the transitional engine states
pappz Aug 22, 2026
01b8148
Update submodule to common mobile package
pappz Aug 24, 2026
5a83554
Bump netbird version
pappz Aug 25, 2026
10a35f9
Handle network changes without restarting the engine (#237)
pappz Aug 25, 2026
b286f98
Move the SSH action from the peer list rows to the peer detail view
pappz Sep 1, 2026
22c3317
Align dark theme colors with the text palette and add a toolbar divider
pappz Sep 1, 2026
dea25e5
Use the muted text color for dialog cancel buttons and restyle the de…
pappz Sep 1, 2026
9ff8ed9
Unify content backgrounds with tonal toolbar separation in dark mode
pappz Sep 1, 2026
53046db
Defer hiding the nav chrome until the fullscreen destination is in place
pappz Sep 1, 2026
8e9cc54
Replace the profile cards with list rows and an actions menu
pappz Sep 1, 2026
4315697
Bind the VPN service with AUTO_CREATE so a config relaunch cannot kil…
pappz Sep 1, 2026
e8c7432
Redraw the pin and key icons as outlined strokes matching the desktop…
pappz Sep 1, 2026
23e86dc
Put the peer identity row first and separate the detail rows
pappz Sep 1, 2026
454d0b6
Scroll the terminal scrollback with a touch drag
pappz Sep 2, 2026
459a652
Mask every password character with no last-typed reveal
pappz Sep 2, 2026
77b94a2
Pick the arrow key sequence from the live cursor keys mode
pappz Sep 2, 2026
d6f9bdc
Lift the armed-modifier letter out of the keyboard composition
pappz Sep 2, 2026
62323d6
Dim the armed modifier key instead of the released one
pappz Sep 2, 2026
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
235 changes: 63 additions & 172 deletions app/src/main/java/io/netbird/client/MainActivity.java
Original file line number Diff line number Diff line change
Expand Up @@ -12,29 +12,28 @@
import android.os.IBinder;
import android.text.Html;
import android.util.Log;
import android.view.KeyEvent;
import android.util.TypedValue;
import android.view.Menu;
import android.view.MenuItem;
import android.view.View;
import android.view.ViewGroup;
import android.widget.TextView;
import android.widget.Toast;

import java.util.ArrayList;
import java.util.HashSet;
import java.util.List;
import java.util.Set;

import com.google.android.material.navigation.NavigationView;
import com.google.android.material.navigation.NavigationBarView;

import androidx.activity.result.ActivityResultLauncher;
import androidx.activity.result.contract.ActivityResultContracts;
import androidx.annotation.NonNull;
import androidx.appcompat.app.AlertDialog;
import androidx.core.content.ContextCompat;
import androidx.core.view.GravityCompat;
import androidx.navigation.NavController;
import androidx.navigation.Navigation;
import androidx.navigation.ui.AppBarConfiguration;
import androidx.navigation.ui.NavigationUI;
import androidx.drawerlayout.widget.DrawerLayout;
import androidx.appcompat.app.AppCompatActivity;

import io.netbird.client.databinding.ActivityMainBinding;
Expand All @@ -48,7 +47,7 @@
import io.netbird.gomobile.android.URLOpener;


public class MainActivity extends AppCompatActivity implements NavigationView.OnNavigationItemSelectedListener, ServiceAccessor, StateListenerRegistry {
public class MainActivity extends AppCompatActivity implements ServiceAccessor, StateListenerRegistry {

private StateListAnimator stateAnim;

Expand Down Expand Up @@ -103,7 +102,7 @@ protected void onCreate(Bundle savedInstanceState) {

binding = ActivityMainBinding.inflate(getLayoutInflater());
setContentView(binding.getRoot());
setSupportActionBar(binding.appBarMain.toolbar);
setSupportActionBar(binding.toolbar);

isRunningOnTV = PlatformUtils.isAndroidTV(this);
useDeviceCodeFlow = PlatformUtils.requiresDeviceCodeFlow(this);
Expand All @@ -114,69 +113,36 @@ protected void onCreate(Bundle savedInstanceState) {
Log.i(LOGTAG, "Running on ChromeOS - using device code flow for authentication");
}

setVersionText();
NavigationBarView bottomNav = (NavigationBarView) binding.bottomNav;

DrawerLayout drawer = binding.drawerLayout;
NavigationView navigationView = binding.navView;

// Set the listener for menu item selections
navigationView.setNavigationItemSelectedListener(this);

// Update profile menu item with active profile name
updateProfileMenuItem(navigationView);

// On TV, request focus when drawer opens so D-pad navigation works
if (isRunningOnTV) {
drawer.addDrawerListener(new DrawerLayout.SimpleDrawerListener() {
@Override
public void onDrawerOpened(View drawerView) {
// Request focus on the drawer when it opens
navigationView.postDelayed(() -> {
navigationView.setFocusable(true);
navigationView.setFocusableInTouchMode(false);

if (!navigationView.requestFocus()) {
Log.d(LOGTAG, "NavigationView couldn't get focus, trying menu items");
}

// Try to find and focus the first visible menu item
View menuView = navigationView.getChildAt(0);
if (menuView != null) {
View firstFocusable = menuView.findFocus();
if (firstFocusable == null) {
menuView.requestFocus();
}
}
}, 100); // Delay to let drawer animation finish
}

@Override
public void onDrawerClosed(View drawerView) {
// Return focus to main content when drawer closed
View mainContent = findViewById(R.id.nav_host_fragment_content_main);
if (mainContent != null) {
mainContent.requestFocus();
}
}
});
}

// Passing each menu ID as a set of Ids because each
// menu should be considered as top level destinations.
mAppBarConfiguration = new AppBarConfiguration.Builder(
R.id.nav_home)
.setOpenableLayout(drawer)
.build();
// All four bottom-nav destinations are top-level — no Up arrow on those.
final Set<Integer> topLevelDestinations = new HashSet<>();
topLevelDestinations.add(R.id.nav_home);
topLevelDestinations.add(R.id.nav_peers);
topLevelDestinations.add(R.id.nav_networks);
topLevelDestinations.add(R.id.nav_settings);
mAppBarConfiguration = new AppBarConfiguration.Builder(topLevelDestinations).build();
navController = Navigation.findNavController(this, R.id.nav_host_fragment_content_main);
NavigationUI.setupActionBarWithNavController(this, navController, mAppBarConfiguration);
NavigationUI.setupWithNavController(bottomNav, navController);

navController.addOnDestinationChangedListener((controller, destination, arguments) -> {
if (destination.getId() == R.id.nav_home) {
int destId = destination.getId();

// Hide bottom nav on first-launch onboarding so the screen is clean.
if (destId == R.id.firstInstallFragment) {
bottomNav.setVisibility(View.GONE);
} else {
bottomNav.setVisibility(View.VISIBLE);
}

// Top-level destinations (Home, Peers, Networks, Settings) don't need a toolbar —
// bottom nav already identifies the screen. Sub-screens keep the toolbar with title + Up arrow.
boolean isTopLevel = topLevelDestinations.contains(destId);
setToolbarVisible(!isTopLevel);
Comment thread
coderabbitai[bot] marked this conversation as resolved.
Outdated

if (destId == R.id.nav_home) {
removeToolbarShadow();
// Update profile menu item when returning to home (e.g., after profile switch)
if (binding != null && binding.navView != null) {
updateProfileMenuItem(binding.navView);
}
} else {
resetToolbar();
}
Expand Down Expand Up @@ -271,15 +237,6 @@ public void onStart() {
startService();
}

@Override
protected void onResume() {
super.onResume();
// Update profile menu item when returning to MainActivity
if (binding != null && binding.navView != null) {
updateProfileMenuItem(binding.navView);
}
}

@Override
protected void onPause() {
super.onPause();
Expand Down Expand Up @@ -321,23 +278,6 @@ public boolean onCreateOptionsMenu(Menu menu) {
return true;
}

@Override
public boolean onNavigationItemSelected(@NonNull MenuItem item) {
int id = item.getItemId();
if (id == R.id.nav_docs) {
item.setCheckable(false);
binding.drawerLayout.closeDrawers();
openDocs();
return true;
}

// Use NavigationUI which handles launchSingleTop and saveState/restoreState
// This prevents fragment recreation and preserves state when alternating between destinations
boolean isHandled = NavigationUI.onNavDestinationSelected(item, navController);
binding.drawerLayout.closeDrawers();
return isHandled;
}

@Override
public boolean onSupportNavigateUp() {
NavController navController = Navigation.findNavController(this, R.id.nav_host_fragment_content_main);
Expand Down Expand Up @@ -480,12 +420,6 @@ public void unregisterServiceStateListener(StateListener listener) {
serviceStateListeners.remove(listener);
}

private void openDocs() {
String url = "https://docs.netbird.io"; // Replace with the desired URL
Intent intent = new Intent(Intent.ACTION_VIEW, Uri.parse(url));
startActivity(intent);
}

private void startService() {
Log.i(LOGTAG, "start VPN service");
Intent intent = new Intent(this, VPNService.class);
Expand All @@ -504,21 +438,43 @@ private void showFirstInstallFragment() {
}
}

private void setToolbarVisible(boolean visible) {
ViewGroup.LayoutParams lp = binding.toolbar.getLayoutParams();
int targetHeight;
if (visible) {
TypedValue tv = new TypedValue();
if (getTheme().resolveAttribute(android.R.attr.actionBarSize, tv, true)) {
targetHeight = TypedValue.complexToDimensionPixelSize(tv.data, getResources().getDisplayMetrics());
} else {
targetHeight = (int) (56 * getResources().getDisplayMetrics().density);
}
} else {
targetHeight = 0;
}
if (lp.height == targetHeight) {
return;
}
lp.height = targetHeight;
binding.toolbar.setLayoutParams(lp);
// Ensure AppBarLayout re-measures itself so the content below shifts up correctly.
binding.appbar.requestLayout();
}

private void removeToolbarShadow() {
stateAnim = binding.appBarMain.appbar.getStateListAnimator();
binding.appBarMain.appbar.setStateListAnimator(null);
binding.appBarMain.appbar.setElevation(0f);
binding.appBarMain.toolbar.setElevation(0);
binding.appBarMain.toolbar.setBackground(new ColorDrawable(ContextCompat.getColor(this, R.color.nb_bg_home)));
stateAnim = binding.appbar.getStateListAnimator();
binding.appbar.setStateListAnimator(null);
binding.appbar.setElevation(0f);
binding.toolbar.setElevation(0);
binding.toolbar.setBackground(new ColorDrawable(ContextCompat.getColor(this, R.color.nb_bg_home)));
}

private void resetToolbar() {
if(stateAnim!=null) {
binding.appBarMain.appbar.setStateListAnimator(stateAnim);
binding.appbar.setStateListAnimator(stateAnim);
}
binding.appBarMain.appbar.setElevation(10f);
binding.appBarMain.toolbar.setElevation(0);
binding.appBarMain.toolbar.setBackground(new ColorDrawable(ContextCompat.getColor(this, R.color.nb_bg)));
binding.appbar.setElevation(10f);
binding.toolbar.setElevation(0);
binding.toolbar.setBackground(new ColorDrawable(ContextCompat.getColor(this, R.color.nb_bg)));
}

private void showAlwaysOnDialog(Runnable onDismissAction) {
Expand All @@ -542,19 +498,6 @@ private void showAlwaysOnDialog(Runnable onDismissAction) {
alertDialog.show();
}

private void setVersionText() {
try {
try {
String packageName = getPackageName();
String versionName = getPackageManager().getPackageInfo(packageName, 0).versionName;
binding.navVersion.setText(versionName);
} catch (Exception e) {
binding.navVersion.setText("");
}
} catch (Exception e) {
}
}

ConnectionListener connectionListener = new ConnectionListener() {
@Override
public synchronized void onAddressChanged(String fqdn, String ip) {
Expand Down Expand Up @@ -634,56 +577,4 @@ public void onError(String msg) {
});
}
};

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);
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);
}
}

@Override
public boolean onKeyDown(int keyCode, KeyEvent event) {
if (!isRunningOnTV) {
return super.onKeyDown(keyCode, event);
}
else {
Log.d(LOGTAG, "Key pressed: " + keyCode + " (" + KeyEvent.keyCodeToString(keyCode) + "), repeat: " + event.getRepeatCount());

if (keyCode == KeyEvent.KEYCODE_DPAD_LEFT) {
boolean isOnHomeScreen = navController != null &&
navController.getCurrentDestination() != null &&
navController.getCurrentDestination().getId() == R.id.nav_home;

if (event.getRepeatCount() == 0 && isOnHomeScreen && !binding.drawerLayout.isDrawerOpen(GravityCompat.START)) {
Toast.makeText(this, R.string.tv_menu_hint, Toast.LENGTH_SHORT).show();
}

// drawer is not selectable on Android 16+, so we open via a long press of the left d-pad button instead
if (event.getRepeatCount() > 0 && !binding.drawerLayout.isDrawerOpen(GravityCompat.START)) {
Log.d(LOGTAG, "Long press LEFT detected - opening drawer");
binding.drawerLayout.openDrawer(GravityCompat.START);
binding.navView.requestFocus();
return true;
}
}

if (keyCode == KeyEvent.KEYCODE_BACK && binding.drawerLayout.isDrawerOpen(GravityCompat.START)) {
Log.d(LOGTAG, "Closing drawer with BACK");
binding.drawerLayout.closeDrawer(GravityCompat.START);
return true;
}
}

return super.onKeyDown(keyCode, event);
}
}
}
Loading
Loading