From cff35c7e8a32709ac462aa4ffa048a13c50e5a18 Mon Sep 17 00:00:00 2001
From: Alysson Ribeiro <15274059+sonalys@users.noreply.github.com>
Date: Wed, 24 Jun 2026 08:03:06 +0200
Subject: [PATCH 1/2] feat: Add split tunneling
---
app/build.gradle.kts | 4 +
app/src/main/AndroidManifest.xml | 1 +
.../client/ui/advanced/AdvancedFragment.java | 7 +
.../client/ui/splittunneling/AppInfo.java | 17 +++
.../splittunneling/SplitTunnelingAdapter.java | 87 ++++++++++++
.../SplitTunnelingFragment.java | 131 ++++++++++++++++++
.../main/res/drawable/ic_chevron_right.xml | 9 ++
app/src/main/res/layout/fragment_advanced.xml | 59 +++++++-
.../res/layout/fragment_split_tunneling.xml | 88 ++++++++++++
app/src/main/res/layout/list_item_app.xml | 47 +++++++
.../main/res/navigation/mobile_navigation.xml | 6 +
app/src/main/res/values/strings.xml | 7 +
gradle/libs.versions.toml | 8 ++
.../java/io/netbird/client/tool/IFace.java | 41 +++++-
.../io/netbird/client/tool/Preferences.java | 34 +++++
15 files changed, 541 insertions(+), 5 deletions(-)
create mode 100644 app/src/main/java/io/netbird/client/ui/splittunneling/AppInfo.java
create mode 100644 app/src/main/java/io/netbird/client/ui/splittunneling/SplitTunnelingAdapter.java
create mode 100644 app/src/main/java/io/netbird/client/ui/splittunneling/SplitTunnelingFragment.java
create mode 100644 app/src/main/res/drawable/ic_chevron_right.xml
create mode 100644 app/src/main/res/layout/fragment_split_tunneling.xml
create mode 100644 app/src/main/res/layout/list_item_app.xml
diff --git a/app/build.gradle.kts b/app/build.gradle.kts
index febc2632..906c86ad 100644
--- a/app/build.gradle.kts
+++ b/app/build.gradle.kts
@@ -71,6 +71,8 @@ android {
dependencies {
implementation(project(":tool"))
implementation(project(":gomobile"))
+ implementation(libs.androidx.navigation.fragment)
+ implementation(libs.androidx.navigation.ui)
implementation(libs.appcompat)
implementation(libs.material)
implementation(libs.constraintlayout)
@@ -78,6 +80,8 @@ dependencies {
implementation(libs.lifecycle.viewmodel.ktx)
implementation(libs.navigation.fragment)
implementation(libs.navigation.ui)
+ implementation("androidx.fragment:fragment:1.8.5")
+ implementation("androidx.recyclerview:recyclerview:1.3.2")
testImplementation(libs.junit)
androidTestImplementation(libs.ext.junit)
androidTestImplementation(libs.espresso.core)
diff --git a/app/src/main/AndroidManifest.xml b/app/src/main/AndroidManifest.xml
index 897d5fdf..2a4e55a3 100644
--- a/app/src/main/AndroidManifest.xml
+++ b/app/src/main/AndroidManifest.xml
@@ -10,6 +10,7 @@
+
{
+ NavController navController = Navigation.findNavController(v);
+ navController.navigate(R.id.nav_split_tunneling);
+ });
+
return root;
}
diff --git a/app/src/main/java/io/netbird/client/ui/splittunneling/AppInfo.java b/app/src/main/java/io/netbird/client/ui/splittunneling/AppInfo.java
new file mode 100644
index 00000000..97e5e75e
--- /dev/null
+++ b/app/src/main/java/io/netbird/client/ui/splittunneling/AppInfo.java
@@ -0,0 +1,17 @@
+package io.netbird.client.ui.splittunneling;
+
+import android.graphics.drawable.Drawable;
+
+public class AppInfo {
+ public final String name;
+ public final String packageName;
+ public final Drawable icon;
+ public boolean isSelected;
+
+ public AppInfo(String name, String packageName, Drawable icon, boolean isSelected) {
+ this.name = name;
+ this.packageName = packageName;
+ this.icon = icon;
+ this.isSelected = isSelected;
+ }
+}
diff --git a/app/src/main/java/io/netbird/client/ui/splittunneling/SplitTunnelingAdapter.java b/app/src/main/java/io/netbird/client/ui/splittunneling/SplitTunnelingAdapter.java
new file mode 100644
index 00000000..0c029caf
--- /dev/null
+++ b/app/src/main/java/io/netbird/client/ui/splittunneling/SplitTunnelingAdapter.java
@@ -0,0 +1,87 @@
+package io.netbird.client.ui.splittunneling;
+
+import android.view.LayoutInflater;
+import android.view.View;
+import android.view.ViewGroup;
+import android.widget.CheckBox;
+import android.widget.ImageView;
+import android.widget.TextView;
+
+import androidx.annotation.NonNull;
+import androidx.recyclerview.widget.RecyclerView;
+
+import java.util.ArrayList;
+import java.util.List;
+import java.util.stream.Collectors;
+
+import io.netbird.client.R;
+
+public class SplitTunnelingAdapter extends RecyclerView.Adapter {
+
+ private List allApps = new ArrayList<>();
+ private List filteredApps = new ArrayList<>();
+ private final OnAppClickListener listener;
+
+ public interface OnAppClickListener {
+ void onAppClick(AppInfo app);
+ }
+
+ public SplitTunnelingAdapter(OnAppClickListener listener) {
+ this.listener = listener;
+ }
+
+ public void setApps(List apps) {
+ this.allApps = apps;
+ this.filteredApps = new ArrayList<>(apps);
+ notifyDataSetChanged();
+ }
+
+ public void filter(String query) {
+ if (query.isEmpty()) {
+ filteredApps = new ArrayList<>(allApps);
+ } else {
+ String lowerQuery = query.toLowerCase();
+ filteredApps = allApps.stream()
+ .filter(app -> app.name.toLowerCase().contains(lowerQuery) || app.packageName.toLowerCase().contains(lowerQuery))
+ .collect(Collectors.toList());
+ }
+ notifyDataSetChanged();
+ }
+
+ @NonNull
+ @Override
+ public ViewHolder onCreateViewHolder(@NonNull ViewGroup parent, int viewType) {
+ View view = LayoutInflater.from(parent.getContext()).inflate(R.layout.list_item_app, parent, false);
+ return new ViewHolder(view);
+ }
+
+ @Override
+ public void onBindViewHolder(@NonNull ViewHolder holder, int position) {
+ AppInfo app = filteredApps.get(position);
+ holder.textAppName.setText(app.name);
+ holder.textPackageName.setText(app.packageName);
+ holder.imgAppIcon.setImageDrawable(app.icon);
+ holder.checkApp.setChecked(app.isSelected);
+ holder.itemView.setOnClickListener(v -> listener.onAppClick(app));
+ }
+
+ @Override
+ public int getItemCount() {
+ return filteredApps.size();
+ }
+
+ public static class ViewHolder extends RecyclerView.ViewHolder {
+ ImageView imgAppIcon;
+ TextView textAppName;
+ TextView textPackageName;
+ CheckBox checkApp;
+
+ public ViewHolder(@NonNull View itemView) {
+ super(itemView);
+ imgAppIcon = itemView.findViewById(R.id.img_app_icon);
+ textAppName = itemView.findViewById(R.id.text_app_name);
+ textPackageName = itemView.findViewById(R.id.text_package_name);
+ checkApp = itemView.findViewById(R.id.check_app);
+ }
+ }
+}
diff --git a/app/src/main/java/io/netbird/client/ui/splittunneling/SplitTunnelingFragment.java b/app/src/main/java/io/netbird/client/ui/splittunneling/SplitTunnelingFragment.java
new file mode 100644
index 00000000..32d80665
--- /dev/null
+++ b/app/src/main/java/io/netbird/client/ui/splittunneling/SplitTunnelingFragment.java
@@ -0,0 +1,131 @@
+package io.netbird.client.ui.splittunneling;
+
+import android.content.pm.ApplicationInfo;
+import android.content.pm.PackageManager;
+import android.graphics.drawable.Drawable;
+import android.os.Bundle;
+import android.text.Editable;
+import android.text.TextWatcher;
+import android.view.LayoutInflater;
+import android.view.View;
+import android.view.ViewGroup;
+import android.widget.RadioGroup;
+
+import androidx.annotation.NonNull;
+import androidx.annotation.Nullable;
+import androidx.appcompat.app.AlertDialog;
+import androidx.fragment.app.Fragment;
+import androidx.recyclerview.widget.LinearLayoutManager;
+
+import java.util.ArrayList;
+import java.util.HashSet;
+import java.util.List;
+import java.util.Set;
+import java.util.stream.Collectors;
+
+import io.netbird.client.R;
+import io.netbird.client.databinding.FragmentSplitTunnelingBinding;
+import io.netbird.client.tool.Preferences;
+import io.netbird.client.tool.Preferences.SplitTunnelingMode;
+
+public class SplitTunnelingFragment extends Fragment {
+
+ private FragmentSplitTunnelingBinding binding;
+ private SplitTunnelingAdapter adapter;
+ private Preferences preferences;
+ private List allApps = new ArrayList<>();
+
+ @Nullable
+ @Override
+ public View onCreateView(@NonNull LayoutInflater inflater, @Nullable ViewGroup container, @Nullable Bundle savedInstanceState) {
+ binding = FragmentSplitTunnelingBinding.inflate(inflater, container, false);
+ preferences = new Preferences(requireContext());
+ setupUI();
+ loadApps();
+ return binding.getRoot();
+ }
+
+ private void setupUI() {
+ adapter = new SplitTunnelingAdapter(app -> {
+ app.isSelected = !app.isSelected;
+ saveApps();
+ adapter.notifyDataSetChanged();
+ });
+
+ binding.recyclerApps.setLayoutManager(new LinearLayoutManager(requireContext()));
+ binding.recyclerApps.setAdapter(adapter);
+
+ Preferences.SplitTunnelingMode mode = preferences.getSplitTunnelingMode();
+ switch (mode) {
+ case NONE:
+ binding.radioGroupMode.check(R.id.radio_mode_none);
+ break;
+ case EXCLUDE:
+ binding.radioGroupMode.check(R.id.radio_mode_exclude);
+ break;
+ case INCLUDE:
+ binding.radioGroupMode.check(R.id.radio_mode_include);
+ break;
+ }
+
+ binding.radioGroupMode.setOnCheckedChangeListener((group, checkedId) -> {
+ Preferences.SplitTunnelingMode newMode;
+ if (checkedId == R.id.radio_mode_none) {
+ newMode = Preferences.SplitTunnelingMode.NONE;
+ } else if (checkedId == R.id.radio_mode_exclude) {
+ newMode = Preferences.SplitTunnelingMode.EXCLUDE;
+ } else if (checkedId == R.id.radio_mode_include) {
+ newMode = Preferences.SplitTunnelingMode.INCLUDE;
+ } else {
+ newMode = Preferences.SplitTunnelingMode.NONE;
+ }
+ preferences.setSplitTunnelingMode(newMode);
+ });
+
+ binding.editSearch.addTextChangedListener(new TextWatcher() {
+ @Override
+ public void beforeTextChanged(CharSequence s, int start, int count, int after) {}
+
+ @Override
+ public void onTextChanged(CharSequence s, int start, int before, int count) {
+ adapter.filter(s.toString());
+ }
+
+ @Override
+ public void afterTextChanged(Editable s) {}
+ });
+ }
+
+ private void loadApps() {
+ PackageManager pm = requireContext().getPackageManager();
+ List apps = pm.getInstalledApplications(PackageManager.GET_META_DATA);
+ Set selectedApps = preferences.getSplitTunnelingApps();
+ String myPackageName = requireContext().getPackageName();
+
+ allApps = apps.stream()
+ .filter(app -> !app.packageName.equals(myPackageName))
+ .map(app -> {
+ String name = pm.getApplicationLabel(app).toString();
+ Drawable icon = pm.getApplicationIcon(app);
+ return new AppInfo(name, app.packageName, icon, selectedApps.contains(app.packageName));
+ })
+ .sorted((a, b) -> a.name.compareToIgnoreCase(b.name))
+ .collect(Collectors.toList());
+
+ adapter.setApps(allApps);
+ }
+
+ private void saveApps() {
+ Set selectedApps = allApps.stream()
+ .filter(app -> app.isSelected)
+ .map(app -> app.packageName)
+ .collect(Collectors.toSet());
+ preferences.setSplitTunnelingApps(selectedApps);
+ }
+
+ @Override
+ public void onDestroyView() {
+ super.onDestroyView();
+ binding = null;
+ }
+}
diff --git a/app/src/main/res/drawable/ic_chevron_right.xml b/app/src/main/res/drawable/ic_chevron_right.xml
new file mode 100644
index 00000000..e28524ed
--- /dev/null
+++ b/app/src/main/res/drawable/ic_chevron_right.xml
@@ -0,0 +1,9 @@
+
+
+
diff --git a/app/src/main/res/layout/fragment_advanced.xml b/app/src/main/res/layout/fragment_advanced.xml
index ce48fdf5..362fd6b6 100644
--- a/app/src/main/res/layout/fragment_advanced.xml
+++ b/app/src/main/res/layout/fragment_advanced.xml
@@ -523,6 +523,63 @@
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintTop_toBottomOf="@id/layout_disable_ipv6" />
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+ app:layout_constraintTop_toBottomOf="@id/layout_split_tunneling">
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
diff --git a/app/src/main/res/layout/list_item_app.xml b/app/src/main/res/layout/list_item_app.xml
new file mode 100644
index 00000000..0e6c749b
--- /dev/null
+++ b/app/src/main/res/layout/list_item_app.xml
@@ -0,0 +1,47 @@
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
diff --git a/app/src/main/res/navigation/mobile_navigation.xml b/app/src/main/res/navigation/mobile_navigation.xml
index 3f749393..b4e6e5c1 100644
--- a/app/src/main/res/navigation/mobile_navigation.xml
+++ b/app/src/main/res/navigation/mobile_navigation.xml
@@ -36,6 +36,12 @@
android:label="@string/menu_profiles"
tools:layout="@layout/fragment_profiles" />
+
+
Choose the app appearance mode.
Force relay connection
Forces usage of relay when connecting to peers
+ Split Tunneling
+ Select which apps should use or bypass the VPN
+ Split Tunneling
+ None (All apps use VPN)
+ Exclude (Selected apps bypass VPN)
+ Include (Selected apps use VPN)
+ Search apps
exclamation mark
To apply the setting, you will need to reconnect.
Using setup keys for user devices is not recommended. SSO with MFA provides stronger security, proper user-device association, and periodic re-authentication.
diff --git a/gradle/libs.versions.toml b/gradle/libs.versions.toml
index 018bc26c..7dcb8cbe 100644
--- a/gradle/libs.versions.toml
+++ b/gradle/libs.versions.toml
@@ -10,6 +10,8 @@ appcompat = "1.7.0"
lottie = "6.4.0"
material = "1.12.0"
constraintlayout = "2.2.1"
+androidx-recyclerview = "1.3.2"
+androidx-fragment = "1.8.5"
lifecycleLivedataKtx = "2.9.0"
lifecycleViewmodelKtx = "2.9.0"
navigationFragment = "2.9.0"
@@ -18,6 +20,8 @@ firebase-bom = "34.2.0"
google-services = "4.4.2"
firebase-crashlytics-plugin = "3.0.6"
zxing = "4.3.0"
+navigationFragmentVersion = "2.9.8"
+navigationUiVersion = "2.9.8"
[libraries]
browser = { module = "androidx.browser:browser", version.ref = "browser" }
@@ -30,6 +34,8 @@ appcompat = { group = "androidx.appcompat", name = "appcompat", version.ref = "a
lottie = { module = "com.airbnb.android:lottie", version.ref = "lottie" }
material = { group = "com.google.android.material", name = "material", version.ref = "material" }
constraintlayout = { group = "androidx.constraintlayout", name = "constraintlayout", version.ref = "constraintlayout" }
+androidx-recyclerview = { group = "androidx.recyclerview", name = "recyclerview", version.ref = "androidx-recyclerview" }
+androidx-fragment = { group = "androidx.fragment", name = "fragment", version.ref = "androidx-fragment" }
lifecycle-livedata-ktx = { group = "androidx.lifecycle", name = "lifecycle-livedata-ktx", version.ref = "lifecycleLivedataKtx" }
lifecycle-viewmodel-ktx = { group = "androidx.lifecycle", name = "lifecycle-viewmodel-ktx", version.ref = "lifecycleViewmodelKtx" }
navigation-fragment = { group = "androidx.navigation", name = "navigation-fragment", version.ref = "navigationFragment" }
@@ -38,6 +44,8 @@ firebase-bom = { group = "com.google.firebase", name = "firebase-bom", version.r
firebase-crashlytics = { group = "com.google.firebase", name = "firebase-crashlytics" }
firebase-analytics = { group = "com.google.firebase", name = "firebase-analytics" }
zxing = { group = "com.journeyapps", name = "zxing-android-embedded", version.ref = "zxing" }
+androidx-navigation-fragment = { group = "androidx.navigation", name = "navigation-fragment", version.ref = "navigationFragmentVersion" }
+androidx-navigation-ui = { group = "androidx.navigation", name = "navigation-ui", version.ref = "navigationUiVersion" }
[plugins]
android-application = { id = "com.android.application", version.ref = "agp" }
diff --git a/tool/src/main/java/io/netbird/client/tool/IFace.java b/tool/src/main/java/io/netbird/client/tool/IFace.java
index 137fe8ce..fa683c63 100644
--- a/tool/src/main/java/io/netbird/client/tool/IFace.java
+++ b/tool/src/main/java/io/netbird/client/tool/IFace.java
@@ -12,6 +12,7 @@
import android.util.Log;
import java.util.LinkedList;
+import java.util.Set;
import java.util.concurrent.CountDownLatch;
import io.netbird.gomobile.android.TunAdapter;
@@ -86,10 +87,7 @@ private int createTun(String ip, int prefixLength, InetNetwork addrV6, int mtu,
Log.d(LOGTAG, "add route: "+r.addr+"/"+r.prefixLength);
}
- disallowApp(builder, "com.google.android.projection.gearhead");
- disallowApp(builder, "com.google.android.apps.chromecast.app");
- disallowApp(builder, "com.google.android.apps.messaging");
- disallowApp(builder, "com.google.stadia.android");
+ applySplitTunneling(builder);
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.Q) {
builder.setMetered(false);
@@ -145,6 +143,41 @@ private void disallowApp(VpnService.Builder builder, String packageName) {
}
}
+ private void applySplitTunneling(VpnService.Builder builder) {
+ Preferences prefs = new Preferences(vpnService);
+ Preferences.SplitTunnelingMode mode = prefs.getSplitTunnelingMode();
+ Set apps = prefs.getSplitTunnelingApps();
+
+ Log.d(LOGTAG, "Applying split tunneling mode: " + mode);
+
+ if (mode == Preferences.SplitTunnelingMode.INCLUDE) {
+ if (apps.isEmpty()) {
+ Log.w(LOGTAG, "Include mode selected but no apps specified, VPN will be effectively disabled for all apps");
+ }
+ for (String app : apps) {
+ try {
+ builder.addAllowedApplication(app);
+ Log.d(LOGTAG, "Allowed app: " + app);
+ } catch (PackageManager.NameNotFoundException e) {
+ Log.w(LOGTAG, "App not found for allowed list: " + app);
+ }
+ }
+ } else {
+ // Default and EXCLUDE mode
+ disallowApp(builder, "com.google.android.projection.gearhead");
+ disallowApp(builder, "com.google.android.apps.chromecast.app");
+ disallowApp(builder, "com.google.android.apps.messaging");
+ disallowApp(builder, "com.google.stadia.android");
+
+ if (mode == Preferences.SplitTunnelingMode.EXCLUDE) {
+ for (String app : apps) {
+ disallowApp(builder, app);
+ Log.d(LOGTAG, "Disallowed app: " + app);
+ }
+ }
+ }
+ }
+
@SuppressLint("DefaultLocale")
@Override
public void updateAddr(String s) throws Exception {
diff --git a/tool/src/main/java/io/netbird/client/tool/Preferences.java b/tool/src/main/java/io/netbird/client/tool/Preferences.java
index ddf57ca5..e49d9655 100644
--- a/tool/src/main/java/io/netbird/client/tool/Preferences.java
+++ b/tool/src/main/java/io/netbird/client/tool/Preferences.java
@@ -3,12 +3,19 @@
import android.content.Context;
import android.content.SharedPreferences;
+import java.util.HashSet;
+import java.util.Set;
+
public class Preferences {
private final String keyTraceLog = "tracelog";
private final String keyForceRelayConnection = "isConnectionForceRelayed";
+ private final String keySplitTunnelingMode = "splitTunnelingMode";
+
+ private final String keySplitTunnelingApps = "splitTunnelingApps";
+
private final SharedPreferences sharedPref;
public Preferences(Context context) {
@@ -41,4 +48,31 @@ public void disableForcedRelayConnection() {
public static String defaultServer() {
return "https://api.netbird.io";
}
+
+ public enum SplitTunnelingMode {
+ NONE,
+ EXCLUDE,
+ INCLUDE
+ }
+
+ public SplitTunnelingMode getSplitTunnelingMode() {
+ String mode = sharedPref.getString(keySplitTunnelingMode, SplitTunnelingMode.NONE.name());
+ try {
+ return SplitTunnelingMode.valueOf(mode);
+ } catch (IllegalArgumentException e) {
+ return SplitTunnelingMode.NONE;
+ }
+ }
+
+ public void setSplitTunnelingMode(SplitTunnelingMode mode) {
+ sharedPref.edit().putString(keySplitTunnelingMode, mode.name()).apply();
+ }
+
+ public Set getSplitTunnelingApps() {
+ return sharedPref.getStringSet(keySplitTunnelingApps, new HashSet<>());
+ }
+
+ public void setSplitTunnelingApps(Set apps) {
+ sharedPref.edit().putStringSet(keySplitTunnelingApps, apps).apply();
+ }
}
From da271e94b8824825e268be5f49f70c2b9ba06a8b Mon Sep 17 00:00:00 2001
From: Alysson Ribeiro <15274059+sonalys@users.noreply.github.com>
Date: Wed, 24 Jun 2026 08:49:01 +0200
Subject: [PATCH 2/2] fix: apply code rabbit feedback
---
app/build.gradle.kts | 6 +-
app/src/main/AndroidManifest.xml | 15 +++-
.../SplitTunnelingFragment.java | 72 +++++++++++++------
.../main/res/drawable/ic_chevron_right.xml | 3 +-
.../res/layout/fragment_split_tunneling.xml | 10 +++
.../java/io/netbird/client/tool/IFace.java | 2 +-
6 files changed, 79 insertions(+), 29 deletions(-)
diff --git a/app/build.gradle.kts b/app/build.gradle.kts
index 906c86ad..14273628 100644
--- a/app/build.gradle.kts
+++ b/app/build.gradle.kts
@@ -78,10 +78,8 @@ dependencies {
implementation(libs.constraintlayout)
implementation(libs.lifecycle.livedata.ktx)
implementation(libs.lifecycle.viewmodel.ktx)
- implementation(libs.navigation.fragment)
- implementation(libs.navigation.ui)
- implementation("androidx.fragment:fragment:1.8.5")
- implementation("androidx.recyclerview:recyclerview:1.3.2")
+ implementation(libs.androidx.fragment)
+ implementation(libs.androidx.recyclerview)
testImplementation(libs.junit)
androidTestImplementation(libs.ext.junit)
androidTestImplementation(libs.espresso.core)
diff --git a/app/src/main/AndroidManifest.xml b/app/src/main/AndroidManifest.xml
index 2a4e55a3..5e8076da 100644
--- a/app/src/main/AndroidManifest.xml
+++ b/app/src/main/AndroidManifest.xml
@@ -10,7 +10,6 @@
-
+
+
+
+
+
+
+
+
+
+
+
+
+
+
allApps = new ArrayList<>();
+ private ExecutorService backgroundExecutor;
@Nullable
@Override
public View onCreateView(@NonNull LayoutInflater inflater, @Nullable ViewGroup container, @Nullable Bundle savedInstanceState) {
binding = FragmentSplitTunnelingBinding.inflate(inflater, container, false);
preferences = new Preferences(requireContext());
+ backgroundExecutor = Executors.newSingleThreadExecutor();
setupUI();
loadApps();
return binding.getRoot();
@@ -97,35 +101,59 @@ public void afterTextChanged(Editable s) {}
}
private void loadApps() {
- PackageManager pm = requireContext().getPackageManager();
- List apps = pm.getInstalledApplications(PackageManager.GET_META_DATA);
- Set selectedApps = preferences.getSplitTunnelingApps();
- String myPackageName = requireContext().getPackageName();
-
- allApps = apps.stream()
- .filter(app -> !app.packageName.equals(myPackageName))
- .map(app -> {
- String name = pm.getApplicationLabel(app).toString();
- Drawable icon = pm.getApplicationIcon(app);
- return new AppInfo(name, app.packageName, icon, selectedApps.contains(app.packageName));
- })
- .sorted((a, b) -> a.name.compareToIgnoreCase(b.name))
- .collect(Collectors.toList());
-
- adapter.setApps(allApps);
+ if (backgroundExecutor == null) return;
+ Context context = getContext();
+ if (context == null) return;
+
+ PackageManager pm = context.getPackageManager();
+ String myPackageName = context.getPackageName();
+ binding.progressBar.setVisibility(View.VISIBLE);
+
+ backgroundExecutor.execute(() -> {
+ List apps = pm.getInstalledApplications(PackageManager.GET_META_DATA);
+ Set selectedApps = preferences.getSplitTunnelingApps();
+
+ List loadedApps = apps.stream()
+ .filter(app -> !app.packageName.equals(myPackageName))
+ .map(app -> {
+ String name = pm.getApplicationLabel(app).toString();
+ Drawable icon = pm.getApplicationIcon(app);
+ return new AppInfo(name, app.packageName, icon, selectedApps.contains(app.packageName));
+ })
+ .sorted((a, b) -> a.name.compareToIgnoreCase(b.name))
+ .collect(Collectors.toList());
+
+ if (binding != null) {
+ binding.getRoot().post(() -> {
+ if (binding != null) {
+ allApps = loadedApps;
+ adapter.setApps(allApps);
+ binding.progressBar.setVisibility(View.GONE);
+ }
+ });
+ }
+ });
}
private void saveApps() {
- Set selectedApps = allApps.stream()
- .filter(app -> app.isSelected)
- .map(app -> app.packageName)
- .collect(Collectors.toSet());
- preferences.setSplitTunnelingApps(selectedApps);
+ if (backgroundExecutor == null) return;
+ List appsSnapshot = new ArrayList<>(allApps);
+ backgroundExecutor.execute(() -> {
+ Set selectedApps = appsSnapshot.stream()
+ .filter(app -> app.isSelected)
+ .map(app -> app.packageName)
+ .collect(Collectors.toSet());
+ preferences.setSplitTunnelingApps(selectedApps);
+ });
}
@Override
public void onDestroyView() {
super.onDestroyView();
+ if (backgroundExecutor != null) {
+ backgroundExecutor.shutdown();
+ backgroundExecutor = null;
+ }
binding = null;
}
}
diff --git a/app/src/main/res/drawable/ic_chevron_right.xml b/app/src/main/res/drawable/ic_chevron_right.xml
index e28524ed..c55f00c0 100644
--- a/app/src/main/res/drawable/ic_chevron_right.xml
+++ b/app/src/main/res/drawable/ic_chevron_right.xml
@@ -2,7 +2,8 @@
android:width="24dp"
android:height="24dp"
android:viewportWidth="24"
- android:viewportHeight="24">
+ android:viewportHeight="24"
+ android:tint="?attr/colorControlNormal">
diff --git a/app/src/main/res/layout/fragment_split_tunneling.xml b/app/src/main/res/layout/fragment_split_tunneling.xml
index 836bb453..5f5f2593 100644
--- a/app/src/main/res/layout/fragment_split_tunneling.xml
+++ b/app/src/main/res/layout/fragment_split_tunneling.xml
@@ -83,6 +83,16 @@
app:layout_constraintEnd_toEndOf="parent"
tools:listitem="@layout/list_item_app" />
+
+
diff --git a/tool/src/main/java/io/netbird/client/tool/IFace.java b/tool/src/main/java/io/netbird/client/tool/IFace.java
index fa683c63..1401d1b3 100644
--- a/tool/src/main/java/io/netbird/client/tool/IFace.java
+++ b/tool/src/main/java/io/netbird/client/tool/IFace.java
@@ -152,7 +152,7 @@ private void applySplitTunneling(VpnService.Builder builder) {
if (mode == Preferences.SplitTunnelingMode.INCLUDE) {
if (apps.isEmpty()) {
- Log.w(LOGTAG, "Include mode selected but no apps specified, VPN will be effectively disabled for all apps");
+ Log.w(LOGTAG, "Include mode selected but no apps specified, VPN will be effectively enabled for all apps");
}
for (String app : apps) {
try {