Skip to content

Add Quick Settings Tile service for VPN control#136

Open
Dan-Kingsley wants to merge 7 commits intonetbirdio:mainfrom
Dan-Kingsley:main
Open

Add Quick Settings Tile service for VPN control#136
Dan-Kingsley wants to merge 7 commits intonetbirdio:mainfrom
Dan-Kingsley:main

Conversation

@Dan-Kingsley
Copy link
Copy Markdown

@Dan-Kingsley Dan-Kingsley commented Feb 5, 2026

I have been using NetBird for some time now, but I find it annoying to need to into the app to turn the VPN connection on. I have created a Icon and button to exist in the Quick Settings notification panel. so that this can be easily accessed without needing to go into the NetBird app

Summary by CodeRabbit

  • New Features

    • Added a Quick Settings tile for NetBird to toggle the VPN and reflect its current state in real time.
    • Tile supports queued toggle actions and will launch the app when VPN permission is required.
  • Localization

    • Added tile label "NetBird" for display in Quick Settings.
  • Bug Fixes / Reliability

    • Improved service startup so the VPN service reliably enters foreground when started.

@coderabbitai
Copy link
Copy Markdown

coderabbitai bot commented Feb 5, 2026

📝 Walkthrough

Walkthrough

Adds a Quick Settings TileService (NetbirdTileService) and manifest entry; TileService binds to VPNService via a local binder, handles clicks and VPN permission flow, updates tile state, and VPNService exposes isRunning() and starts foreground notification for INTENT_ACTION_START.

Changes

Cohort / File(s) Summary
Manifest & Resources
app/src/main/AndroidManifest.xml, app/src/main/res/values/strings.xml
Adds exported NetbirdTileService manifest entry with android.service.quicksettings.action.QS_TILE, icon and label; adds quick_settings_tile_label string.
TileService Implementation
app/src/main/java/io/netbird/client/NetbirdTileService.java
New TileService subclass implementing onStartListening/onStopListening/onClick; binds/unbinds to VPNService, registers a ServiceStateListener, queues pending clicks during binding, handles VpnService.prepare() permission flow, toggles/starts/stops VPN via binder, and updates tile UI state.
VPN Binder API & Foreground behavior
tool/src/main/java/io/netbird/client/tool/VPNService.java
Adds public boolean isRunning() to VPNService.MyLocalBinder; onStartCommand now calls fgNotification.startForeground() when intent action equals INTENT_ACTION_START.

Sequence Diagram

sequenceDiagram
    actor User
    participant QS as Quick Settings
    participant Tile as NetbirdTileService
    participant VPN as VPNService (Binder)
    participant Engine as EngineRunner
    participant UI as MainActivity

    User->>QS: Tap Netbird tile
    QS->>Tile: onClick()
    alt Tile not bound
        Tile->>VPN: bindService()
        VPN-->>Tile: onServiceConnected (binder)
        Tile->>VPN: register ServiceStateListener
    end
    Tile->>VPN: binder.isRunning()
    VPN->>Engine: engineRunner.isRunning()
    Engine-->>VPN: running? (true/false)
    alt running == true
        Tile->>VPN: binder.stopEngine()
        VPN->>Engine: stop()
    else running == false
        Tile->>Tile: VpnService.prepare() -> intent?
        alt permission required
            Tile->>UI: startActivityAndCollapse / PendingIntent to request permission
        else permission granted
            Tile->>VPN: startForegroundService(INTENT_ACTION_START) + bind
            VPN->>Engine: start()
        end
    end
    Tile->>Tile: updateTile() (STATE_ACTIVE / STATE_INACTIVE)
    Tile->>QS: reflect tile state
Loading

Estimated code review effort

🎯 3 (Moderate) | ⏱️ ~25 minutes

Poem

🐰 I hopped into settings, bright and spry,

A tiny tile to flip on by,
I bind and listen, queue a click,
I wake the engine, quick and slick,
VPN hums — hop, hop — we're sly.

🚥 Pre-merge checks | ✅ 2 | ❌ 1

❌ Failed checks (1 warning)

Check name Status Explanation Resolution
Docstring Coverage ⚠️ Warning Docstring coverage is 0.00% which is insufficient. The required threshold is 80.00%. Write docstrings for the functions missing them to satisfy the coverage threshold.
✅ Passed checks (2 passed)
Check name Status Explanation
Description Check ✅ Passed Check skipped - CodeRabbit’s high-level summary is enabled.
Title check ✅ Passed The title accurately describes the main change: adding a Quick Settings Tile service for VPN control, which is the primary feature introduced across all modified files.

✏️ Tip: You can configure your own custom pre-merge checks in the settings.

✨ Finishing Touches
🧪 Generate unit tests (beta)
  • Create PR with unit tests

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.

@Dan-Kingsley
Copy link
Copy Markdown
Author

Screenshot_20260205_152905_One UI Home.jpg

Copy link
Copy Markdown

@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: 1

🤖 Fix all issues with AI agents
In `@app/src/main/java/io/netbird/client/NetbirdTileService.java`:
- Around line 104-118: bindToVpnService currently starts the bind but you never
track a pending bind, so if onStopListening/unbindFromVpnService runs before
onServiceConnected the ServiceConnection remains; add a "bindingInProgress"
boolean set true in bindToVpnService, clear it in
serviceConnection.onServiceConnected and onServiceDisconnected, and modify
unbindFromVpnService to unbind when either isBound or bindingInProgress is true
(removing the serviceStateListener only if mBinder != null), then call
unbindService(serviceConnection) inside a try/catch to safely handle cases where
the system rejected the unbind, and reset isBound, bindingInProgress, and
mBinder appropriately.

Copy link
Copy Markdown

@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: 1

🤖 Fix all issues with AI agents
In `@app/src/main/java/io/netbird/client/NetbirdTileService.java`:
- Around line 72-77: Ensure pendingClick cannot persist across failed/aborted
binds: in NetbirdTileService.onStopListening() clear/reset the pendingClick flag
immediately (and any related state) so a subsequent successful bind won't act on
an old click; additionally, in the bind flow (the methods handling bindService
success/failure—e.g., your ServiceConnection callbacks and the code paths around
bindService / onServiceConnected / onServiceDisconnected used in the bind logic
between lines ~107–124) handle bind failures by clearing pendingClick and
aborting any pending toggle actions. Make sure both unbindFromVpnService() and
any bind-failure/error branches consistently reset pendingClick to a safe
default.

Copy link
Copy Markdown

@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: 2

🤖 Fix all issues with AI agents
In `@app/src/main/java/io/netbird/client/NetbirdTileService.java`:
- Around line 94-105: The toggle handler should avoid calling
startAndRunVpnService() when a bind is already in progress; update
handleToggle() to check the isBinding flag (or equivalent) before initiating the
service: if mBinder is null and isBinding is true, just set pendingClick = true
and return without calling startAndRunVpnService(), otherwise (when not binding)
set pendingClick and call startAndRunVpnService(); reference the handleToggle
method, mBinder, isBinding, pendingClick and startAndRunVpnService to locate and
implement this guard.
- Around line 84-88: NetbirdTileService currently calls
startActivityAndCollapse(Intent) which is deprecated and throws on Android 14+;
replace that call by building a PendingIntent for MainActivity (use the same
Intent with FLAG_ACTIVITY_NEW_TASK | FLAG_ACTIVITY_CLEAR_TOP), create the
PendingIntent with FLAG_UPDATE_CURRENT | FLAG_IMMUTABLE, and pass that
PendingIntent to startActivityAndCollapse(PendingIntent) instead of the raw
Intent so it works on SDK 35+ and avoids UnsupportedOperationException.
🧹 Nitpick comments (1)
app/src/main/java/io/netbird/client/NetbirdTileService.java (1)

39-45: Consider removing the service state listener in onServiceDisconnected.

When the service disconnects unexpectedly (e.g., process killed), mBinder is set to null without removing the serviceStateListener. While the listener reference will become stale, explicitly removing it before nulling the binder ensures cleaner lifecycle management and mirrors the pattern in unbindFromVpnService().

♻️ Suggested improvement
         `@Override`
         public void onServiceDisconnected(ComponentName name) {
+            if (mBinder != null) {
+                mBinder.removeServiceStateListener(serviceStateListener);
+            }
             mBinder = null;
             isBound = false;
             isBinding = false;
             updateTile();
         }

@Dan-Kingsley
Copy link
Copy Markdown
Author

@coderabbitai review

@coderabbitai
Copy link
Copy Markdown

coderabbitai bot commented Feb 5, 2026

✅ Actions performed

Review triggered.

Note: CodeRabbit is an incremental review system and does not re-review already reviewed commits. This command is applicable only when automatic reviews are paused.

@pappz
Copy link
Copy Markdown
Collaborator

pappz commented Mar 21, 2026

Hello @Dan-Kingsley! Thank you for the PR. It looks nice! I’ll review it soon and merge it.

private void startAndRunVpnService() {
Intent intent = new Intent(this, VPNService.class);
intent.setAction(VPNService.INTENT_ACTION_START);
startService(intent);
Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

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

startService() can throw IllegalStateException. Please use startForegroundService() and handle the INTENT_ACTION_START action in VPNService.onStartCommand() to call startForeground() immediately.

 switch (action) {
    case INTENT_ALWAYS_ON_START:
        fgNotification.startForeground();
        engineRunner.runWithoutAuth();
        break;
    case INTENT_ACTION_START:
        fgNotification.startForeground();
        break;
}

this, 0, intent,
PendingIntent.FLAG_UPDATE_CURRENT | PendingIntent.FLAG_IMMUTABLE
);
startActivityAndCollapse(pendingIntent);
Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

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

startActivityAndCollapse(PendingIntent) requires API 34+, but minSdk is 26. Needs a version check to fall back to the deprecated startActivityAndCollapse(Intent) on older
devices.

Copy link
Copy Markdown

@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: 1

🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.

Inline comments:
In `@app/src/main/java/io/netbird/client/NetbirdTileService.java`:
- Around line 50-65: The callbacks in the anonymous ServiceStateListener
(onStarted, onStopped, onError) are executed from
EngineRunner.notifyServiceStateListeners' background thread but call
updateTile() which touches getQsTile() and tile.updateTile() and must run on the
main thread; change each callback to post a Runnable to the main thread (e.g.
via a Handler created with Looper.getMainLooper()) that invokes updateTile() so
all UI/tile work runs on the main thread instead of directly calling
updateTile() from the background thread.

ℹ️ Review info
⚙️ Run configuration

Configuration used: defaults

Review profile: CHILL

Plan: Pro

Run ID: 5b4e35dd-1080-409d-addb-c233c0a83a67

📥 Commits

Reviewing files that changed from the base of the PR and between e067bf2 and dcb60cb.

📒 Files selected for processing (2)
  • app/src/main/java/io/netbird/client/NetbirdTileService.java
  • tool/src/main/java/io/netbird/client/tool/VPNService.java

@Dan-Kingsley
Copy link
Copy Markdown
Author

@coderabbitai review

@coderabbitai
Copy link
Copy Markdown

coderabbitai bot commented Mar 25, 2026

✅ Actions performed

Review triggered.

Note: CodeRabbit is an incremental review system and does not re-review already reviewed commits. This command is applicable only when automatic reviews are paused.

@Dan-Kingsley Dan-Kingsley requested a review from pappz March 25, 2026 23:31
Copy link
Copy Markdown

@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: 1

🧹 Nitpick comments (1)
app/src/main/java/io/netbird/client/NetbirdTileService.java (1)

122-124: Avoid auto-creating VPNService just to paint the tile.

BIND_AUTO_CREATE here instantiates VPNService every time Quick Settings starts listening, and VPNService.onCreate() does engine setup plus callback/receiver registration. A lightweight persisted state, or a non-auto-create bind that treats “service missing” as inactive, would keep the tile much cheaper.

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@app/src/main/java/io/netbird/client/NetbirdTileService.java` around lines 122
- 124, The tile currently forces creation of VPNService via bindService(intent,
serviceConnection, Context.BIND_AUTO_CREATE) in bindToVpnService(), causing
heavy onCreate work; change to bind without auto-create (use 0 instead of
Context.BIND_AUTO_CREATE) so bindService only succeeds if VPNService is already
running, set isBinding based on the boolean result, and treat a failed bind or
null service in serviceConnection as “inactive” (update the tile state and avoid
starting or creating the service). Keep serviceConnection and isBinding handling
intact, and if you need persisted lightweight state use a simple
SharedPreferences flag or existing persisted state instead of auto-creating
VPNService.
🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.

Inline comments:
In `@app/src/main/java/io/netbird/client/NetbirdTileService.java`:
- Around line 107-119: handleToggle currently calls mBinder.runEngine() on a
service that may be only bound (which fails on Android 12+); ensure the service
is explicitly started before invoking runEngine: when mBinder != null and
!mBinder.isRunning(), call the existing
startForegroundService/startAndRunVpnService (or a
startService/startForegroundService equivalent) to start the VPN service first,
then call mBinder.runEngine(null, false); similarly ensure any code path that
directly calls mBinder.runEngine uses startForegroundService prior to runEngine
so the service is both started and bound (refer to
NetbirdTileService.handleToggle, mBinder.runEngine and startAndRunVpnService).

---

Nitpick comments:
In `@app/src/main/java/io/netbird/client/NetbirdTileService.java`:
- Around line 122-124: The tile currently forces creation of VPNService via
bindService(intent, serviceConnection, Context.BIND_AUTO_CREATE) in
bindToVpnService(), causing heavy onCreate work; change to bind without
auto-create (use 0 instead of Context.BIND_AUTO_CREATE) so bindService only
succeeds if VPNService is already running, set isBinding based on the boolean
result, and treat a failed bind or null service in serviceConnection as
“inactive” (update the tile state and avoid starting or creating the service).
Keep serviceConnection and isBinding handling intact, and if you need persisted
lightweight state use a simple SharedPreferences flag or existing persisted
state instead of auto-creating VPNService.

ℹ️ Review info
⚙️ Run configuration

Configuration used: defaults

Review profile: CHILL

Plan: Pro

Run ID: 64a04621-2058-42e2-9a71-a73c87d60634

📥 Commits

Reviewing files that changed from the base of the PR and between e067bf2 and c965ed7.

📒 Files selected for processing (2)
  • app/src/main/java/io/netbird/client/NetbirdTileService.java
  • tool/src/main/java/io/netbird/client/tool/VPNService.java

@Dan-Kingsley
Copy link
Copy Markdown
Author

@coderabbitai resume

@coderabbitai
Copy link
Copy Markdown

coderabbitai bot commented Mar 26, 2026

✅ Actions performed

Reviews resumed.

Copy link
Copy Markdown

@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.

🧹 Nitpick comments (1)
app/src/main/java/io/netbird/client/NetbirdTileService.java (1)

166-174: Optional: avoid redundant tile updates when state is unchanged.

This can reduce unnecessary tile.updateTile() calls during repeated callbacks with the same running state.

♻️ Proposed micro-refactor
 private void updateTile() {
     Tile tile = getQsTile();
     if (tile == null) return;

-    boolean running = isVpnRunning();
-
-    tile.setState(running ? Tile.STATE_ACTIVE : Tile.STATE_INACTIVE);
-    tile.updateTile();
+    int newState = isVpnRunning() ? Tile.STATE_ACTIVE : Tile.STATE_INACTIVE;
+    if (tile.getState() != newState) {
+        tile.setState(newState);
+        tile.updateTile();
+    }
 }
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@app/src/main/java/io/netbird/client/NetbirdTileService.java` around lines 166
- 174, The updateTile() method currently always calls tile.updateTile(); change
it to only call tile.updateTile() when the state actually changes: retrieve the
current Tile via getQsTile(), compute the desired state from isVpnRunning(),
compare against the tile.getState() (or stored state) and only call
tile.setState(...) followed by tile.updateTile() when they differ; reference
functions: updateTile(), getQsTile(), isVpnRunning(), tile.getState(),
tile.setState(), tile.updateTile().
🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.

Nitpick comments:
In `@app/src/main/java/io/netbird/client/NetbirdTileService.java`:
- Around line 166-174: The updateTile() method currently always calls
tile.updateTile(); change it to only call tile.updateTile() when the state
actually changes: retrieve the current Tile via getQsTile(), compute the desired
state from isVpnRunning(), compare against the tile.getState() (or stored state)
and only call tile.setState(...) followed by tile.updateTile() when they differ;
reference functions: updateTile(), getQsTile(), isVpnRunning(), tile.getState(),
tile.setState(), tile.updateTile().

ℹ️ Review info
⚙️ Run configuration

Configuration used: defaults

Review profile: CHILL

Plan: Pro

Run ID: 786b25d8-5fcc-41e5-bada-b9e80938c0f7

📥 Commits

Reviewing files that changed from the base of the PR and between c965ed7 and 74470aa.

📒 Files selected for processing (1)
  • app/src/main/java/io/netbird/client/NetbirdTileService.java

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