Skip to content
Merged
11 changes: 11 additions & 0 deletions app/src/main/AndroidManifest.xml
Original file line number Diff line number Diff line change
Expand Up @@ -43,6 +43,17 @@
<category android:name="android.intent.category.LEANBACK_LAUNCHER" />
</intent-filter>
</activity>

<service
android:name=".NetbirdTileService"
android:exported="true"
android:icon="@drawable/ic_netbird_btn"
android:label="@string/quick_settings_tile_label"
android:permission="android.permission.BIND_QUICK_SETTINGS_TILE">
<intent-filter>
<action android:name="android.service.quicksettings.action.QS_TILE" />
</intent-filter>
</service>
</application>

</manifest>
153 changes: 153 additions & 0 deletions app/src/main/java/io/netbird/client/NetbirdTileService.java
Original file line number Diff line number Diff line change
@@ -0,0 +1,153 @@
package io.netbird.client;

import android.content.ComponentName;
import android.content.Context;
import android.content.Intent;
import android.content.ServiceConnection;
import android.net.VpnService;
import android.os.IBinder;
import android.service.quicksettings.Tile;
import android.service.quicksettings.TileService;
import android.util.Log;

import io.netbird.client.tool.ServiceStateListener;
import io.netbird.client.tool.VPNService;

public class NetbirdTileService extends TileService {

private static final String TAG = "NetbirdTileService";
private VPNService.MyLocalBinder mBinder;
private boolean isBound = false;
private boolean isBinding = false;
private boolean pendingClick = false;

private final ServiceConnection serviceConnection = new ServiceConnection() {
@Override
public void onServiceConnected(ComponentName name, IBinder binder) {
mBinder = (VPNService.MyLocalBinder) binder;
isBound = true;
isBinding = false;
mBinder.addServiceStateListener(serviceStateListener);
updateTile();

if (pendingClick) {
pendingClick = false;
handleToggle();
}
}

@Override
public void onServiceDisconnected(ComponentName name) {
mBinder = null;
isBound = false;
isBinding = false;
updateTile();
}
};

private final ServiceStateListener serviceStateListener = new ServiceStateListener() {
@Override
public void onStarted() {
updateTile();
}

@Override
public void onStopped() {
updateTile();
}

@Override
public void onError(String msg) {
updateTile();
}
};
Comment thread
coderabbitai[bot] marked this conversation as resolved.

@Override
public void onStartListening() {
super.onStartListening();
Log.d(TAG, "onStartListening");
bindToVpnService();
}

@Override
public void onStopListening() {
super.onStopListening();
Log.d(TAG, "onStopListening");
unbindFromVpnService();
}
Comment thread
coderabbitai[bot] marked this conversation as resolved.

@Override
public void onClick() {
super.onClick();
Log.d(TAG, "onClick");

if (VpnService.prepare(this) != null) {
Intent intent = new Intent(this, MainActivity.class);
intent.addFlags(Intent.FLAG_ACTIVITY_NEW_TASK);
startActivityAndCollapse(intent);
return;
Comment thread
coderabbitai[bot] marked this conversation as resolved.
}

handleToggle();
}

private void handleToggle() {
if (mBinder != null) {
if (mBinder.isRunning()) {
mBinder.stopEngine();
} else {
mBinder.runEngine(null, false);
}
} else {
pendingClick = true;
startAndRunVpnService();
}
Comment thread
coderabbitai[bot] marked this conversation as resolved.
}
Comment thread
coderabbitai[bot] marked this conversation as resolved.

private void bindToVpnService() {
Intent intent = new Intent(this, VPNService.class);
isBinding = bindService(intent, serviceConnection, Context.BIND_AUTO_CREATE);
if (!isBinding) {
pendingClick = false;
Log.w(TAG, "bindService failed");
}
}

private void unbindFromVpnService() {
if (isBound || isBinding) {
if (mBinder != null) {
mBinder.removeServiceStateListener(serviceStateListener);
}
try {
unbindService(serviceConnection);
} catch (IllegalArgumentException e) {
Log.w(TAG, "Service not bound", e);
}
isBound = false;
isBinding = false;
mBinder = null;
}
pendingClick = false;
}
Comment thread
coderabbitai[bot] marked this conversation as resolved.

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

bindToVpnService();
}

private boolean isVpnRunning() {
return mBinder != null && isBound && mBinder.isRunning();
}

private void updateTile() {
Tile tile = getQsTile();
if (tile == null) return;

boolean running = isVpnRunning();

tile.setState(running ? Tile.STATE_ACTIVE : Tile.STATE_INACTIVE);
tile.updateTile();
}
}
1 change: 1 addition & 0 deletions app/src/main/res/values/strings.xml
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,7 @@
<string name="nav_header_subtitle">android.studio@android.com</string>
<string name="nav_header_desc">Navigation header</string>
<string name="action_settings">Settings</string>
<string name="quick_settings_tile_label">NetBird</string>

<string name="menu_advanced">Advanced</string>
<string name="menu_about">About</string>
Expand Down
4 changes: 4 additions & 0 deletions tool/src/main/java/io/netbird/client/tool/VPNService.java
Original file line number Diff line number Diff line change
Expand Up @@ -196,6 +196,10 @@ public void stopEngine() {
engineRunner.stop();
}

public boolean isRunning() {
return engineRunner.isRunning();
}

public PeerInfoArray peersInfo() {
return engineRunner.peersInfo();
}
Expand Down